/* MCL Builder — all state lives in this browser. Nothing is transmitted anywhere. */ (function () { "use strict"; var STORE = "mcl-builder-v1"; var SCHEMA = window.MCL_SCHEMA; var GT = window.MCL_GENERAL_TERMS; var DEFS = window.MCL_DEFINITIONS; var NOTES = window.MCL_NOTES; var PURPOSES = window.MCL_PURPOSE_OPTIONS; var FIELDS = {}; SCHEMA.forEach(function (s) { s.fields.forEach(function (f) { FIELDS[f.k] = f; }); }); var PURPOSE_FIELDS = ["pTextOutput", "pAvOutput", "pTextDisplay", "pAvDisplay"]; var RIGHT_OF_PURPOSE = { pTextOutput:"rTextOutput", pAvOutput:"rAvOutput", pTextDisplay:"rTextDisplay", pAvDisplay:"rAvDisplay" }; var RIGHT_LABEL = { rTraining:"the Training Rights for internal use only", rTextOutput:"the Text Output Rights", rAvOutput:"the Audio-Visual Output Rights", rTextDisplay:"the Text Display Rights", rAvDisplay:"the Audio-Visual Display Rights" }; var state = {}; var activeSection = 0; var renderMode = "preview"; var highlight = true; /* ───────────── state ───────────── */ function defaults() { var d = {}; SCHEMA.forEach(function (s) { s.fields.forEach(function (f) { d[f.k] = f.defaultValue !== undefined ? f.defaultValue : ""; }); }); PURPOSE_FIELDS.forEach(function (k) { d[k] = ""; }); ["rTraining","rTextOutput","rAvOutput","rTextDisplay","rAvDisplay"].forEach(function (k) { d[k] = false; }); return d; } function load() { state = defaults(); try { var raw = localStorage.getItem(STORE); if (raw) { var saved = JSON.parse(raw); Object.keys(saved).forEach(function (k) { if (k in state) state[k] = saved[k]; }); } } catch (e) { /* corrupt or unavailable storage — fall back to defaults */ } } function save() { try { localStorage.setItem(STORE, JSON.stringify(state)); } catch (e) { /* private mode */ } } /* ───────────── helpers ───────────── */ function esc(s) { return String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } function nl2br(s) { return esc(s).replace(/\n/g, "
"); } function txt(k) { return (state[k] == null ? "" : String(state[k])).trim(); } function has(k) { return txt(k).length > 0; } /* Renders a value, or the template's own bracketed placeholder if empty. */ function v(k, fallbackPh) { var raw = txt(k); if (raw) return nl2br(raw); var f = FIELDS[k]; var label = (f && f.ph) || fallbackPh || k.toUpperCase(); if (renderMode === "print") return '' + esc("[" + label + "]") + ""; return '' + esc("[" + label + "]") + ""; } function grantedRights() { return ["rTraining","rTextOutput","rAvOutput","rTextDisplay","rAvDisplay"].filter(function (k) { return !!state[k]; }); } function purposesUsed() { var out = []; PURPOSE_FIELDS.forEach(function (p) { if (state[RIGHT_OF_PURPOSE[p]] && txt(p)) out.push(txt(p)); }); return out; } function anyNonCommercial() { return purposesUsed().indexOf("nc") !== -1; } function hasOutputRights() { return !!(state.rTextOutput || state.rAvOutput); } function hasDisplayRights() { return !!(state.rTextDisplay || state.rAvDisplay); } /* ───────────── completeness ───────────── */ function visible(f) { if (!f.showIf) return true; return f.showIf.in.indexOf(txt(f.showIf.k)) !== -1; } function isRequired(f) { if (f.requiredIf) return f.requiredIf.in.indexOf(txt(f.requiredIf.k)) !== -1; if (f.requiredIfNonCommercial) return anyNonCommercial(); return !!f.required; } /* Returns {req, done, missing:[{k,label}]} for a section. */ function sectionStatus(s) { var req = 0, done = 0, missing = []; if (s.id === "rights") { PURPOSE_FIELDS.forEach(function (p) { var r = RIGHT_OF_PURPOSE[p]; if (state[r]) { req++; if (txt(p)) done++; else missing.push({ k: p, label: "Purpose for " + RIGHT_LABEL[r].replace(/^the /, "") }); } }); return { req: req, done: done, missing: missing, optional: req === 0 }; } s.fields.forEach(function (f) { if (!visible(f) || !isRequired(f)) return; req++; if (has(f.k)) done++; else missing.push({ k: f.k, label: f.label }); }); return { req: req, done: done, missing: missing, optional: req === 0 }; } function overall() { var req = 0, done = 0; SCHEMA.forEach(function (s) { var st = sectionStatus(s); req += st.req; done += st.done; }); return { req: req, done: done }; } /* ───────────── form rendering ───────────── */ /* CABI's own drafting notes, shown verbatim and attributed, so they read as the template author's guidance rather than as this app's commentary. */ function draftingNote(text) { if (!text) return ""; var paras = text.split(/\n+/).filter(function (x) { return x.trim(); }); return '
CABI drafting note' + paras.map(function (x) { return "

" + esc(x) + "

"; }).join("") + "
"; } function fieldControl(f) { var id = "f_" + f.k; var val = state[f.k]; var badge = ""; if (f.templateDefault) badge = 'template default — confirm'; if (f.sourceDefect) badge = 'source template defect'; if (f.departure) badge = 'departs from published wording'; var reqMark = isRequired(f) ? '*' : 'optional'; var help = f.help ? '

' + f.help + "

" : ""; var body; if (f.type === "textarea") { body = '"; } else if (f.type === "radio") { body = '
' + f.options.map(function (o) { return '"; }).join("") + "
"; } else if (f.type === "static-on") { return '
✓' + '
' + f.label + "" + help + "
"; } else if (f.type === "check") { var on = !!val; var pk = f.purposeKey; var purposeBlock = ""; if (pk) { purposeBlock = '
' + 'Granted for:' + PURPOSES.map(function (p) { return '"; }).join("") + (on && !txt(pk) ? 'choose one' : "") + "
"; } return '
' + '" + purposeBlock + draftingNote(NOTES.fields[f.k]) + "
"; } else { body = ''; } return '
' + '" + body + help + draftingNote(NOTES.fields[f.k]) + "
"; } function renderForm() { var s = SCHEMA[activeSection]; var html = '

' + s.heading + "

" + s.intro + "

"; if (s.contactNotice) { html += '
Personal contact details. ' + "These are entered and stored only in your own browser. If you are working from a shared or public " + "machine, clear your answers when you are done.
"; } html += draftingNote(NOTES.sections[s.id]); html += s.fields.filter(visible).map(fieldControl).join(""); document.getElementById("formHost").innerHTML = html; document.getElementById("btnPrev").disabled = activeSection === 0; document.getElementById("btnNext").textContent = activeSection === SCHEMA.length - 1 ? "Review & export →" : "Next →"; } /* The nav is built once and then patched in place. Replacing its innerHTML on every keystroke would destroy the button between mousedown and mouseup whenever a user typed in a field and then clicked straight onto a nav item, swallowing that click. */ function buildNav() { document.getElementById("sectionNav").innerHTML = SCHEMA.map(function (s, i) { return '
  • '; }).join(""); } function renderNav() { var o = overall(); document.getElementById("progressFill").style.width = (o.req ? (o.done / o.req * 100) : 0) + "%"; document.getElementById("progressText").textContent = o.done + " of " + o.req + " required answers"; SCHEMA.forEach(function (s, i) { var btn = document.querySelector('.navbtn[data-sec="' + i + '"]'); if (!btn) return; var st = sectionStatus(s); var cls = st.optional ? "opt" : (st.done === st.req ? "done" : (st.done ? "part" : "todo")); btn.className = "navbtn " + cls + (i === activeSection ? " is-active" : ""); btn.querySelector(".navcount").textContent = st.optional ? "" : st.done + "/" + st.req; }); } /* ───────────── document rendering ───────────── */ function row(n, term, body) { return '' + n + '' + term + '' + body + ""; } function rightsList() { var items = ['
  • the Technical Rights for internal use only;
  • ']; if (state.rTraining) items.push("
  • the Training Rights for internal use only;
  • "); [["rTextOutput","pTextOutput"],["rAvOutput","pAvOutput"],["rTextDisplay","pTextDisplay"],["rAvDisplay","pAvDisplay"]] .forEach(function (pair) { if (!state[pair[0]]) return; var p = txt(pair[1]); var pl = p === "nc" ? "Non-Commercial Use" : p === "c" ? "Commercial Use" : null; var purpose = pl ? esc(pl) : (renderMode === "print" ? '[NON-COMMERCIAL USE / COMMERCIAL USE]' : '[NON-COMMERCIAL USE / COMMERCIAL USE]'); items.push("
  • " + RIGHT_LABEL[pair[0]] + " for " + purpose + ";
  • "); }); var last = items.pop().replace(/;<\/li>$/, "."); items.push(last); return ""; } function deliverySentence() { var m = txt("deliveryMethod"); if (m === "sftp") return "deliver the Licensed Materials by secure file transfer, via encrypted protocols (e.g. SFTP) to the Licensee."; if (m === "cloud") return "make the Licensed Materials available by providing the Licensee access to its private cloud-based repository."; if (m === "api") return "make the Licensed Materials available to the Licensee via streaming or querying the Licensor’s API."; return v("deliveryMethod", "SELECT A DELIVERY METHOD"); } function sigLine() { return ''; } function buildSpecialTerms() { var h = ""; h += '

    AI Licence Agreement

    ' + '

    Model Content Licence for AI

    ' + '

    Special Terms and General Terms

    '; h += '

    AI LICENCE AGREEMENT No. ' + v("agreementNo") + "

    "; h += "

    Introduction

      " + "
    1. The Licensor owns or has the right to grant rights in certain materials.
    2. " + "
    3. The Licensee is developing an artificial intelligence solution.
    4. " + "
    5. The Licensor has agreed to licence the Licensee to use certain of its materials on the terms set out in this AI Licence Agreement, which is made up of the Special Terms below and the General Terms attached (together, the Agreement).
    6. " + "
    "; h += "

    Special Terms

    "; h += ''; h += row(1, "Parties", "

    (1) " + v("licensorName") + ", registered in " + v("licensorCountry") + ", with company number " + v("licensorNumber") + " whose registered office is at " + v("licensorAddress") + " (Licensor); and

    (2) " + v("licenseeName") + ", incorporated and registered in " + v("licenseeCountry") + " with company number " + v("licenseeNumber") + " whose registered office is at " + v("licenseeAddress") + " (Licensee).

    "); h += row(2, "Term", "

    This Agreement will come into force from date of last signature of these Special Terms, and unless it is terminated earlier in accordance with its terms, will continue for a period of " + v("termYears") + " years.

    "); var mm = txt("materialsMode"); var matBody; if (mm === "updating") { matBody = "

    Subject to the terms of this Agreement, the Licensor will make available to the Licensee:

    "; } else if (mm === "fixed") { matBody = "

    Subject to the terms of this Agreement, the Licensor will make available to the Licensee the materials specified in Part A of Annex 1 to these Special Terms. Those materials do not include any update, revisions or new versions.

    "; } else { matBody = "

    Subject to the terms of this Agreement, the Licensor will make available to the Licensee " + v("materialsMode", "CHOOSE: FIXED SNAPSHOT OR SNAPSHOT PLUS UPDATES") + "

    "; } matBody += "

    The Licensee does not have the right to use any content included in the Licensed Materials, that is referenced or identified in Part B of Annex 1.

    "; h += row(3, "Licensed Materials", matBody); h += row(4, "Designated AI Solution", "

    The Licensee will use the Licensed Materials according to the Agreement, in connection with the following AI solution only:

    " + "

    No use of the Licensed Materials is permitted in connection with any AI system or solution, other than the Designated AI Solution without the Licensor’s prior written consent (not to be unreasonably withheld or delayed) and subject to further terms and conditions.

    "); h += row(5, "Rights to use Licensed Materials and Purpose", "

    The Licensor grants the Licensee the rights set out below in accordance with clause 5 of the General Terms:

    " + rightsList()); h += row(6, "Additional Limitations on Use", has("additionalLimitations") ? "

    The usage restrictions for the Licensed Materials described in clause 6 of the General Terms apply, together with the following additional limitations: " + nl2br(txt("additionalLimitations")) + "

    " : "

    The usage restrictions for the Licensed Materials described in clause 6 of the General Terms apply.

    "); h += row(7, "Delivery Method", "

    The Licensor will within " + v("deliveryDays") + " days of execution of these Special Terms " + deliverySentence() + "

    "); h += row(8, "Fees", "

    The Licensee will pay the Licensor the following fees: " + v("feeCurrency") + " " + v("feeAmount") + " according to the following payment schedule: " + v("paymentSchedule") + "

    "); h += row(9, "Representatives", "

    The Parties’ representatives for implementing the Agreement are:

    " + "

    or their designated representatives or replacements.

    "); h += row(10, "Documents that make up this Agreement", "

    The individual documents that make up the Agreement are as follows:

    1. These Special Terms
    2. General Terms
    " + "

    If there is a conflict between the provisions of the different documents which form part of the Agreement, these Special Terms will take precedence over the General Terms to the extent of that conflict.

    "); var amendments = []; if (txt("purposeWording") !== "original") { amendments.push("The definition of Purpose in clause 2.1 of the General Terms is amended to provide for a separate Purpose in respect of each of the Output Rights and Display Rights granted under row 5 above, in place of a single Purpose governing the Agreement as a whole."); } var typed = txt("amendments"); if (typed && typed.toUpperCase() !== "N/A") amendments.push(nl2br(typed)); h += row(11, "Amendments to the General Terms", amendments.length ? (amendments.length === 1 ? "

    " + amendments[0] + "

    " : "
      " + amendments.map(function (a) { return "
    1. " + a + "
    2. "; }).join("") + "
    ") : "

    N/A

    "); h += "
    #TermKey terms
    "; h += '

    These Special Terms and the General Terms overleaf, govern the licensing of the Licensed Materials to the Licensee for use in accordance with this Agreement. By signing below, the Parties indicate their acceptance of this Agreement.

    ' + '
    ' + '

    For and on behalf of the Licensor: ' + (has("licensorName") ? "" + esc(txt("licensorName")) + "" : "") + "

    " + '

    Signature ' + sigLine() + "

    " + '

    Name ' + sigLine() + "

    " + '

    Title ' + sigLine() + "

    " + '

    Date ' + sigLine() + "

    " + '

    For and on behalf of the Licensee: ' + (has("licenseeName") ? "" + esc(txt("licenseeName")) + "" : "") + "

    " + '

    Signature ' + sigLine() + "

    " + '

    Name ' + sigLine() + "

    " + '

    Title ' + sigLine() + "

    " + '

    Date ' + sigLine() + "

    " + "
    "; return h; } function buildAnnex() { var mode = txt("annexBMode"); var partB = ""; if (mode === "default" || mode === "both" || !mode) { partB += "
  • any audio-visual materials, including graphics, illustrations, images or video included in the Licensed Materials, unless a particular item has been expressly included in Part A of this Annex 1" + (mode === "both" ? "; and" : ".") + "
  • "; } if (mode === "custom" || mode === "both") { partB += "
  • " + v("annexBCustom") + "
  • "; } if (!mode) { partB = "
  • " + v("annexBMode", "CHOOSE THE PART B EXCLUSIONS") + "
  • " + partB; } return '

    Annex 1 to Special Terms

    ' + "

    Part A — Licensed Materials

    " + v("annexADescription") + "

    " + "

    Technical Specification for Licensed Materials

    " + v("annexATechSpec") + "

    " + "

    Part B

    To avoid doubt, Licensed Materials do not include the following items:

    "; } /* Tokens whose text is derived from a choice rather than typed into a field. */ var COMPUTED = { purposeTail: function () { if (txt("purposeWording") === "original") { return "which will either be Non-Commercial Use or Commercial Use, whichever is specified in the Special Terms."; } return "which will be either Non-Commercial Use or Commercial Use in respect of each of the Output Rights and Display Rights granted, as specified in row 5 of the Special Terms. Where different Purposes are specified for different Rights, each such Right is exercisable only for the Purpose specified for it."; } }; function fillTokens(s) { return s.replace(/\{\{(\w+)\}\}/g, function (_, k) { return COMPUTED[k] ? esc(COMPUTED[k]()) : v(k); }); } function defsTable() { return '' + DEFS.map(function (d) { return ""; }).join("") + "
    " + esc(d[0]) + "" + fillTokens(esc(d[1])).replace(/\n/g, "
    ") + "
    "; } function renderItems(items, depth) { return items.map(function (it) { var body = '
    ' + (it.n ? '' + esc(it.n) + "" : '') + '' + fillTokens(esc(it.text)) + "
    "; if (it.definitions) body += defsTable(); if (it.items) body += renderItems(it.items, depth + 1); return body; }).join(""); } function buildGeneralTerms() { return '

    General Terms for AI Content Licence Agreement

    ' + GT.map(function (c) { return '

    ' + esc(c.n) + "" + esc(c.title) + "

    " + renderItems(c.items, 1) + "
    "; }).join(""); } function buildDocument() { return buildSpecialTerms() + buildAnnex() + buildGeneralTerms(); } var previewTimer = null, pointerDown = false, previewDirty = false; function renderPreview() { renderMode = "preview"; document.getElementById("docPreview").innerHTML = buildDocument(); previewDirty = false; } /* Debounced, and held off entirely while a pointer is down so that a click starting on a placeholder chip is not orphaned by the preview being rebuilt underneath it. */ function renderPreviewSoon() { previewDirty = true; clearTimeout(previewTimer); previewTimer = setTimeout(function () { if (pointerDown) { renderPreviewSoon(); return; } renderPreview(); }, 140); } function renderFullDoc() { renderMode = "preview"; document.getElementById("docFull").innerHTML = buildDocument(); } /* ───────────── review tab ───────────── */ /* AI-Rights label (draft proposal — see Annex A). Derived mechanically from row 5: P is always granted (Technical Rights), and purpose attaches only to Output and Display Rights, per CABI's drafting note for row 5. */ var LABEL_RSL = { "AI-NONE": { permits: [], prohibits: ["all"] }, "AI-PREP": { permits: ["ai-index*"], prohibits: ["ai-train", "ai-input", "search"] }, "AI-TRAIN": { permits: ["ai-index*", "ai-train"], prohibits: ["ai-input", "search"] }, "AI-QUOTE": { permits: ["ai-index*", "search"], prohibits: ["ai-train", "ai-input"] }, "AI-GROUND": { permits: ["ai-index*", "ai-input", "search"], prohibits: ["ai-train"] }, "AI-FULL": { permits: ["ai-all"], prohibits: [] } }; var LABEL_PROSE = { "AI-NONE": "No AI use permitted.", "AI-PREP": "Prepare and analyse only \u2014 no training, no outputs, nothing surfaced to end users.", "AI-TRAIN": "Prepare and train \u2014 internal use only; no user-facing outputs or extracts.", "AI-QUOTE": "Prepare and show attributed verbatim extracts \u2014 no training, no generated outputs.", "AI-GROUND": "Prepare, generate grounded outputs and quote attributed extracts \u2014 no training on the corpus.", "AI-FULL": "Prepare, train, generate outputs and quote attributed extracts." }; function aiRightsLabel() { var T = !!state.rTraining, G = hasOutputRights(), Q = hasDisplayRights(); var profile = "AI-PREP"; // Technical Rights are always granted if (T && (G || Q)) profile = "AI-FULL"; else if (T) profile = "AI-TRAIN"; else if (G) profile = "AI-GROUND"; else if (Q) profile = "AI-QUOTE"; var suffix = "", note = ""; if (G || Q) { var ps = purposesUsed(); var uniq = ps.filter(function (v, i) { return ps.indexOf(v) === i; }); if (uniq.length === 1) suffix = uniq[0] === "nc" ? "-NC" : "-C"; else if (uniq.length > 1) { suffix = "-C"; note = "Row 5 mixes Commercial and Non-Commercial purposes; the label takes the broader of the two."; } else note = "No purpose chosen yet for the granted Output or Display Rights."; } return { label: profile + suffix, profile: profile, note: note, rsl: LABEL_RSL[profile] }; } function renderLabel() { var host = document.getElementById("labelBox"); if (!host) return; var L = aiRightsLabel(); function toks(a) { return a.length ? "" + a.join(" ") + "" : "none"; } host.innerHTML = '
    ' + esc(L.label) + "
    " + '

    ' + esc(LABEL_PROSE[L.profile]) + "

    " + (L.note ? '

    ' + esc(L.note) + "

    " : "") + '
    Machine-readable form' + "

    RSL 1.0 \u2014 permits " + toks(L.rsl.permits) + ", prohibits " + toks(L.rsl.prohibits) + ".

    " + '

    * ai-index stands in for a preparation token. GAIA proposes ' + "ai-prepare; RSL 1.0 has no equivalent.

    "; } function renderChecklist() { var out = "", total = 0; SCHEMA.forEach(function (s, i) { var st = sectionStatus(s); if (st.optional || st.done === st.req) return; total += st.missing.length; out += '
    "; }); document.getElementById("checklist").innerHTML = total === 0 ? '
    Every required answer is filled in. The document below is ready to export.
    ' : '
    ' + total + " item" + (total === 1 ? "" : "s") + " still blank. " + "You can export anyway — blanks print as bracketed prompts.
    " + out; } function renderGuidance() { var g = []; if (hasOutputRights()) { g.push(["Output Rights assign the results back to the Licensor", "Clause 14.2 provides that all Outputs which translate, substantially reproduce, summarise, transform or derive from the Licensed Materials vest in the Licensor as Derived Outputs, and that the Licensee assigns the copyright in them. AI developers frequently do not expect this. Decide whether it is intended."]); } if (hasDisplayRights()) { g.push(["Display Rights trigger attribution", "Clause 9 requires every Display to name the Work, the Author and the Author’s affiliation, and to identify the Licensor as publisher with a link to the source where feasible. The Licensor must approve the look and placement before any rights are exercised."]); } if ((hasOutputRights() || hasDisplayRights()) && purposesUsed().indexOf("c") !== -1) { g.push(["Public-facing use requires an end user licence agreement", "Clause 12.1.2 obliges the Licensee to put an EULA in front of end users covering personal-use-only, IP reservation, an ‘as is’ disclaimer and a no-reliance acknowledgement."]); } var ps = purposesUsed(); if (ps.indexOf("nc") !== -1 && ps.indexOf("c") !== -1) { if (txt("purposeWording") === "original") { g.push(["Mixed purposes are not supported by the wording you have selected", "You have chosen CABI’s published definition of Purpose, which reads as a single value governing the whole agreement, but row 5 grants different rights on different commercial terms. Either switch clause 2.1 to the per-right wording in the General Terms section, or align every granted right on one purpose."]); } else { g.push(["Mixed purposes are carried by the amended Purpose definition", "Row 5 grants rights on both Commercial and Non-Commercial terms. The per-right wording of clause 2.1 accommodates that split, and row 11 records the amendment. Flag the departure to your counterparty."]); } } if (anyNonCommercial() && !has("strategiesLink")) { g.push(["Non-Commercial Use is not yet anchored", "The definition of Non-Commercial Use turns on alignment with the Licensor’s published mission and strategies. Without the link in the General Terms section, that test has nothing to point at."]); } if ((state.rAvOutput || state.rAvDisplay) && txt("annexBMode") === "default") { g.push(["Audio-visual rights conflict with the Part B carve-out", "You have granted audio-visual Output or Display Rights, but Part B of Annex 1 currently excludes all graphics, illustrations, images and video from the Licensed Materials. Either name the included items expressly in Part A, or change the Part B option."]); } if (hasOutputRights() && !state.rTraining) { g.push(["Output Rights without Training Rights", "Output Rights do not include Training Rights (clause 2.1). As drafted the Licensee may generate outputs influenced by the materials but may not train or fine-tune on them — for example a retrieval-augmented set-up rather than a training set. Confirm that is the intent."]); } if (txt("deliveryMethod") === "api") { g.push(["API delivery activates clause 6.1.10", "Where delivery is by API, the Licensee may not systematically download the materials in a way that allows later offline retrieval, beyond temporary copies."]); } if (state.rTraining) { g.push(["Training survives termination; use does not", "Clause 23.1.1 does not require the Licensee to delete or withdraw a model already trained on the materials. What ends is further use of the materials and the generation of new Displays."]); } if (txt("purposeWording") !== "original") { g.push(["This document departs from CABI’s published wording in one place", "The definition of Purpose in clause 2.1 has been amended to allow a separate Purpose for each right granted; every other clause is verbatim. The amendment is disclosed in row 11 of the Special Terms. Revert it in the General Terms section if CABI confirms the single-Purpose reading."]); } g.push(["Standing obligations on the Licensee, whatever you grant", "Monthly analytics reports covering activity, ingestion counts, prompt logs and output volumes (clause 10.2); a 24-hour security breach notification (clause 8.1.2); removal of withdrawn content within 4 days (clause 7.7.1); audit access during the term and for 2 years after (clause 13.2); professional indemnity and cyber insurance (clause 21)."]); document.getElementById("guidance").innerHTML = g.map(function (x) { return '

    ' + esc(x[0]) + "

    " + esc(x[1]) + "

    "; }).join(""); } /* ───────────── export ───────────── */ function doPrint() { renderMode = "print"; document.getElementById("printRoot").innerHTML = buildDocument(); renderMode = "preview"; document.body.classList.add("printing"); window.print(); } function afterPrint() { document.body.classList.remove("printing"); } function exportPdf() { var o = overall(); var gap = o.req - o.done; if (gap === 0) { doPrint(); return; } openModal( "Export with " + gap + " blank" + (gap === 1 ? "" : "s") + "?", "

    " + gap + " required answer" + (gap === 1 ? " is" : "s are") + " still empty. They will print as bracketed prompts such as " + "[FULL LEGAL NAME OF LICENSOR], which is usually what you want for a draft circulated for comment — " + "but not for a document going to signature.

    ", [{ label: "Go back and complete", cls: "btn-ghost", fn: function () { closeModal(); showTab("build"); } }, { label: "Export anyway", cls: "btn-primary", fn: function () { closeModal(); doPrint(); } }] ); } /* ───────────── modal ───────────── */ function openModal(title, body, actions) { document.getElementById("modalTitle").textContent = title; document.getElementById("modalBody").innerHTML = body; var host = document.getElementById("modalActions"); host.innerHTML = ""; actions.forEach(function (a) { var b = document.createElement("button"); b.className = "btn " + a.cls; b.textContent = a.label; b.onclick = a.fn; host.appendChild(b); }); document.getElementById("modal").hidden = false; } function closeModal() { document.getElementById("modal").hidden = true; } /* ───────────── tabs & wiring ───────────── */ function showTab(name) { ["about","build","review"].forEach(function (t) { document.getElementById("tab-" + t).classList.toggle("is-active", t === name); }); Array.prototype.forEach.call(document.querySelectorAll(".tab"), function (b) { b.classList.toggle("is-active", b.dataset.tab === name); }); if (name === "review") { renderChecklist(); renderGuidance(); renderLabel(); renderFullDoc(); } scrollTop(window); } /* Scroll helpers: guarded so an environment without smooth-scroll support, or without these APIs at all, degrades instead of throwing mid-handler. */ function scrollTop(el) { try { el.scrollTo(0, 0); } catch (e) { if ("scrollTop" in el) el.scrollTop = 0; } } function scrollIntoView(el, opts) { try { if (el && el.scrollIntoView) el.scrollIntoView(opts); } catch (e) {} } function refresh() { save(); renderNav(); renderPreviewSoon(); } function gotoSection(i) { activeSection = Math.max(0, Math.min(SCHEMA.length - 1, i)); renderForm(); renderNav(); document.querySelector(".formpane").scrollTop = 0; scrollIntoView(document.querySelector('.navbtn[data-sec="' + activeSection + '"]'), { block: "nearest" }); } function findSectionOfField(k) { for (var i = 0; i < SCHEMA.length; i++) { if (SCHEMA[i].fields.some(function (f) { return f.k === k; })) return i; if (SCHEMA[i].id === "rights" && PURPOSE_FIELDS.indexOf(k) !== -1) return i; } return 0; } function focusField(k) { showTab("build"); gotoSection(findSectionOfField(k)); setTimeout(function () { var el = document.querySelector('[data-k="' + k + '"]'); var rowEl = document.querySelector('[data-field-row="' + k + '"]') || (el && el.closest(".field")); if (rowEl) { rowEl.classList.add("flash"); setTimeout(function () { rowEl.classList.remove("flash"); }, 1400); scrollIntoView(rowEl, { block: "center", behavior: "smooth" }); } if (el && el.focus) el.focus(); }, 40); } function init() { load(); buildNav(); renderForm(); renderNav(); renderPreview(); document.addEventListener("pointerdown", function () { pointerDown = true; }); document.addEventListener("pointerup", function () { pointerDown = false; if (previewDirty) renderPreviewSoon(); }); document.addEventListener("input", function (e) { var k = e.target.dataset && e.target.dataset.k; if (!k) return; state[k] = e.target.type === "checkbox" ? e.target.checked : e.target.value; refresh(); }); document.addEventListener("change", function (e) { var k = e.target.dataset && e.target.dataset.k; if (!k) return; state[k] = e.target.type === "checkbox" ? e.target.checked : e.target.value; if (e.target.type === "checkbox" || e.target.type === "radio") renderForm(); refresh(); }); document.addEventListener("click", function (e) { var t = e.target.closest("[data-tab],[data-sec],[data-goto],[data-field]"); if (!t) return; if (t.dataset.tab) { showTab(t.dataset.tab); return; } if (t.dataset.goto) { showTab(t.dataset.goto); return; } if (t.dataset.sec !== undefined) { showTab("build"); gotoSection(+t.dataset.sec); return; } if (t.dataset.field) { focusField(t.dataset.field); } }); document.addEventListener("keydown", function (e) { if (e.key === "Escape") { closeModal(); return; } if (e.key !== "Enter" && e.key !== " ") return; var t = e.target.closest && e.target.closest("[data-field]"); if (t) { e.preventDefault(); focusField(t.dataset.field); } }); document.getElementById("btnPrev").onclick = function () { gotoSection(activeSection - 1); }; document.getElementById("btnNext").onclick = function () { if (activeSection === SCHEMA.length - 1) showTab("review"); else gotoSection(activeSection + 1); }; document.getElementById("btnExport").onclick = exportPdf; document.getElementById("chkHighlight").onchange = function (e) { highlight = e.target.checked; renderPreview(); }; document.getElementById("btnReset").onclick = function () { openModal("Clear all answers?", "

    This removes everything you have entered from this browser. It cannot be undone.

    ", [ { label: "Keep my answers", cls: "btn-ghost", fn: closeModal }, { label: "Clear everything", cls: "btn-danger", fn: function () { try { localStorage.removeItem(STORE); } catch (err) {} state = defaults(); activeSection = 0; closeModal(); renderForm(); save(); renderNav(); renderPreview(); } } ]); }; document.getElementById("modal").addEventListener("click", function (e) { if (e.target.id === "modal") closeModal(); }); window.addEventListener("afterprint", afterPrint); } document.addEventListener("DOMContentLoaded", init); })();