File size: 26,348 Bytes
ecfece4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
/* Pactsprig app: agreement picker, guided Cover Page form, live preview, PDF / DOCX / print, saved companies,
 * presets and backup. Everything stays in this browser. © 2026 CyberMax. All rights reserved. */
(function () {
  "use strict";
  var C = window.PactCore, P = window.PactPDF, D = window.PactDOCX, X = window.CMXPro;
  var KEY = "pactsprig:v1", FREE_PDFS = 3;
  var AGREEMENTS = [
    { id: "mnda", name: "Mutual NDA", desc: "Protect what both sides share", free: true },
    { id: "psa", name: "Professional Services", desc: "Key Terms + Statement of Work", free: false },
    { id: "design-partner", name: "Design Partner", desc: "Early access for feedback", free: false },
  ];
  var tpls = {}, state, tpl, model, renderTimer;
  function $(id) { return document.getElementById(id); }
  function pro() { return X.isPro(); }
  function toast(m) { X.toast(m); }
  function upsell(msg, from) { X.upsell(msg || "That is part of Pactsprig Pro.", from); }
  function esc(s) { return String(s == null ? "" : s).replace(/[&<>"]/g, function (c) { return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]; }); }
  function agreementInfo(id) { return AGREEMENTS.filter(function (a) { return a.id === id; })[0] || AGREEMENTS[0]; }
  function partiesEmpty() { return state.parties.every(function (p) { return !((p.company || "") + (p.name || "")).trim(); }); }
  function allowed() { return agreementInfo(state.agreement).free || pro(); }

  /* ---------- storage ---------- */
  function load() {
    var s = null;
    try { s = JSON.parse(localStorage.getItem(KEY) || "null"); } catch (e) { s = null; }
    if (!s || typeof s !== "object") s = { fresh: true };
    s.agreement = agreementInfo(s.agreement).id;
    s.docs = s.docs || {};
    s.parties = Array.isArray(s.parties) && s.parties.length === 2 ? s.parties : [C.emptyParty(), C.emptyParty()];
    s.savedParties = Array.isArray(s.savedParties) ? s.savedParties : [];
    s.presets = Array.isArray(s.presets) ? s.presets : [];
    s.page = s.page === "A4" ? "A4" : "Letter";
    return s;
  }
  function save() {
    try { var c = Object.assign({}, state); delete c.fresh; localStorage.setItem(KEY, JSON.stringify(c)); } catch (e) { /* private mode */ }
  }

  function getTpl(id) {
    if (tpls[id]) return Promise.resolve(tpls[id]);
    return fetch("templates/" + id + ".json").then(function (r) { if (!r.ok) throw new Error("template " + id); return r.json(); }).then(function (t) { tpls[id] = t; return t; });
  }

  /* ---------- picker ---------- */
  function renderPicker() {
    $("picker").innerHTML = AGREEMENTS.map(function (a) {
      return '<button type="button" role="radio" data-agreement="' + a.id + '" aria-checked="' + (a.id === state.agreement) + '"><b>' + esc(a.name) + "</b><span>" + esc(a.desc) + "</span>" +
        (a.free ? '<i class="tag free">Free</i>' : '<i class="tag pro">Pro</i>') + "</button>";
    }).join("");
  }
  $("picker").addEventListener("click", function (e) {
    var b = e.target.closest("button[data-agreement]");
    if (!b) return;
    switchTo(b.getAttribute("data-agreement"));
  });
  $("picker").addEventListener("keydown", function (e) {
    if (["ArrowRight", "ArrowLeft", "ArrowDown", "ArrowUp"].indexOf(e.key) < 0) return;
    var i = AGREEMENTS.findIndex(function (a) { return a.id === state.agreement; }), d = e.key === "ArrowRight" || e.key === "ArrowDown" ? 1 : -1;
    var next = AGREEMENTS[(i + d + AGREEMENTS.length) % AGREEMENTS.length].id;
    e.preventDefault();
    switchTo(next).then(function () { var b = document.querySelector('[data-agreement="' + next + '"]'); if (b) b.focus(); });
  });

  function switchTo(id, useExample) {
    return getTpl(id).then(function (t) {
      state.agreement = id; tpl = t;
      var doc = state.docs[id], ex = C.example(t, C.isoToday());
      if (useExample === "blank") { state.docs[id] = { values: C.defaults(t, C.isoToday()) }; state.demo = false; }
      else if (useExample === true || (!doc && (state.demo || state.fresh || partiesEmpty()))) {
        // examples come with their own parties; keep the user's real parties if they typed some
        state.docs[id] = { values: ex.values };
        if (useExample === true || state.demo || state.fresh || partiesEmpty()) { state.parties = ex.parties; state.demo = true; }
      } else if (!doc) state.docs[id] = { values: C.defaults(t, C.isoToday()) };
      else {
        // add any keys a newer template introduced
        if (state.demo) state.parties = ex.parties;
        var dv = C.defaults(t, C.isoToday());
        Object.keys(dv).forEach(function (k) { if (!(k in doc.values)) doc.values[k] = dv[k]; });
      }
      if (!agreementInfo(id).free && !pro()) toast(t.name + " is part of Pro. You can fill it in and preview it; downloads need Pro.");
      renderPicker(); renderParties(); renderForm(); renderPreview(); save();
    }).catch(function (err) { if (window.console) console.warn(err); toast("Could not load that agreement. Check your connection and try again."); });
  }

  /* ---------- parties ---------- */
  function renderParties() {
    var roles = tpl.partyRoles || tpl.parties || ["Party 1", "Party 2"];
    [0, 1].forEach(function (i) {
      $("pl-" + i).textContent = roles[i];
      ["company", "name", "title", "address"].forEach(function (f) { var el = $("p" + i + "-" + f); if (el !== document.activeElement) el.value = state.parties[i][f] || ""; });
    });
    var opts = '<option value="">Saved companies…</option>' + state.savedParties.map(function (p, i) { return '<option value="' + i + '">' + esc(p.company || p.name) + "</option>"; }).join("");
    Array.prototype.forEach.call(document.querySelectorAll("select[data-load-party]"), function (s) { s.innerHTML = opts; s.hidden = !state.savedParties.length; });
  }
  document.querySelector(".parties").addEventListener("input", function (e) {
    var el = e.target, i = el.getAttribute("data-party"), f = el.getAttribute("data-field");
    if (i == null || !f) return;
    state.parties[+i][f] = el.value;
    state.demo = false;
    schedule();
  });
  document.querySelector(".parties").addEventListener("click", function (e) {
    var b = e.target.closest("[data-save-party]");
    if (!b) return;
    if (!pro()) return upsell("Saved companies are part of Pro: save a company once and fill it into any agreement.", "parties");
    var p = Object.assign({}, state.parties[+b.getAttribute("data-save-party")]);
    if (!(p.company || "").trim()) return toast("Type the company name first.");
    var at = state.savedParties.findIndex(function (q) { return (q.company || "").toLowerCase() === p.company.toLowerCase(); });
    if (at > -1) state.savedParties[at] = p; else state.savedParties.push(p);
    save(); renderParties(); renderPresets();
    toast("Saved " + p.company + ".");
  });
  document.querySelector(".parties").addEventListener("change", function (e) {
    var s = e.target.closest("select[data-load-party]");
    if (!s || s.value === "") return;
    if (!pro()) { s.value = ""; return upsell(null, "parties"); }
    var p = state.savedParties[+s.value];
    if (p) { state.parties[+s.getAttribute("data-load-party")] = Object.assign(C.emptyParty(), p); renderParties(); renderPreview(); save(); }
    s.value = "";
  });

  /* ---------- cover page form ---------- */
  function inputFor(key, spec, vals, label) {
    var v = vals[key] == null ? "" : vals[key], id = "v-" + key.replace(/[^A-Za-z0-9]+/g, "-");
    var aria = ' aria-label="' + esc(label) + '"';
    if (spec.k === "sel") return '<select id="' + id + '" data-key="' + esc(key) + '"' + aria + ">" + spec.o.map(function (o) { return "<option" + (o === v ? " selected" : "") + ">" + esc(o) + "</option>"; }).join("") + "</select>";
    if (spec.k === "date") return '<input type="date" id="' + id + '" data-key="' + esc(key) + '" value="' + esc(v) + '"' + aria + ">";
    if (spec.k === "area") return '<textarea id="' + id + '" class="wide" rows="4" data-key="' + esc(key) + '" placeholder="' + esc(spec.ph || "") + '"' + aria + ">" + esc(v) + "</textarea>";
    var num = spec.k === "num";
    return '<input id="' + id + '" class="inl' + (num ? " num" : "") + '" data-key="' + esc(key) + '" value="' + esc(v) + '"' + (num ? ' inputmode="decimal"' : "") + ' placeholder="' + esc(spec.ph || "") + '" style="--w:' + (spec.w || 12) + 'ch"' + aria + ">";
  }
  /** Option text with its inline inputs: "Expires {years} year(s)" -> text + <input>. */
  function inline(t, key, ins, vals, label) {
    return String(t).replace(/\{(@?\w+)\}|([^{]+)/g, function (m, name, txt) {
      if (txt) return '<span class="ot">' + esc(txt) + "</span>";
      if (name.charAt(0) === "@") return '<span class="ot glob">' + esc(name === "@p1" ? "[Party 1 company]" : name === "@p2" ? "[Party 2 company]" : "[Key Terms Effective Date]") + "</span>";
      return ins && ins[name] ? inputFor(key + "." + name, ins[name], vals, label + " " + name) : esc(m);
    });
  }
  function optionsHtml(pgId, row, vals) {
    var k = pgId + ":" + row.id, name = "r-" + k.replace(/[^A-Za-z0-9]+/g, "-");
    var html = row.lead ? '<p class="lead">' + esc(row.lead) + "</p>" : "";
    function subs(o, ok) {
      if (!o.sub) return "";
      return '<div class="subs">' + o.sub.map(function (s) {
        var sk = ok + "." + s.id;
        return '<div class="opt-row"><label class="optl"><input type="checkbox" data-key="' + esc(sk) + '"' + (vals[sk] ? " checked" : "") + "></label><div class=\"opt-body\">" + inline(s.t, sk, s.in, vals, row.label) + "</div></div>";
      }).join("") + "</div>";
    }
    (row.options || []).forEach(function (o) {
      var ok = k + "." + o.id, on = row.type === "choice" ? vals[k] === o.id : !!vals[ok];
      var ctl = row.type === "choice" ? '<input type="radio" name="' + name + '" data-key="' + esc(k) + '" value="' + esc(o.id) + '"' + (on ? " checked" : "") + ">" : '<input type="checkbox" data-key="' + esc(ok) + '"' + (on ? " checked" : "") + ">";
      html += '<div class="opt-row' + (on ? " on" : "") + '"><label class="optl">' + ctl + '</label><div class="opt-body">' + inline(o.t, ok, o.in, vals, row.label) + (on ? subs(o, ok) : "") + "</div></div>";
    });
    if (row.type === "choice" && row.optional) {
      html += '<div class="opt-row' + (!vals[k] ? " on" : "") + '"><label class="optl"><input type="radio" name="' + name + '" data-key="' + esc(k) + '" value=""' + (!vals[k] ? " checked" : "") + '></label><div class="opt-body"><span class="ot muted">None (leave this out)</span></div></div>';
    }
    return html;
  }
  function rowHtml(pg, row, vals) {
    if (row.type === "heading") return '<h3 class="fh">' + esc(row.label) + "</h3>";
    var k = pg.id + ":" + row.id, id = "v-" + k.replace(/[^A-Za-z0-9]+/g, "-");
    var free = row.type === "text" || row.type === "textarea";
    var head = row.omitLabel ? "" : '<div class="fl"><label' + (free ? ' for="' + id + '"' : "") + ">" + esc(row.label) + (row.optional ? ' <span class="opt">optional</span>' : "") + "</label>" + (row.sub ? '<span class="fsub">' + esc(row.sub) + "</span>" : "") + "</div>";
    var body;
    if (row.type === "text" || row.type === "textarea") {
      body = row.type === "text" ? '<input id="' + id + '" data-key="' + esc(k) + '" value="' + esc(vals[k]) + '" placeholder="' + esc(row.ph || "") + '">'
        : '<textarea id="' + id + '" rows="3" data-key="' + esc(k) + '" placeholder="' + esc(row.ph || "") + '">' + esc(vals[k]) + "</textarea>";
    } else if (row.type === "line") body = '<div class="line">' + inline(row.t, k, row.in, vals, row.label) + "</div>";
    else if (row.type === "fixed") body = '<div class="line fixed">' + esc(row.t) + "</div>";
    else body = '<div class="opts" role="group" aria-label="' + esc(row.label || row.lead || "") + '">' + optionsHtml(pg.id, row, vals) + "</div>";
    return '<div class="frow' + (row.omitLabel ? " cont" : "") + '" data-row="' + esc(k) + '">' + head + body + (row.help ? '<p class="help">' + esc(row.help) + "</p>" : "") + "</div>";
  }
  function renderForm() {
    var vals = state.docs[state.agreement].values;
    $("cover-form").innerHTML = tpl.cover.map(function (pg, i) {
      var title = pg.kicker ? (pg.kicker === "SOW" ? "Statement of Work (SOW)" : pg.kicker) : "Cover Page";
      return '<details class="card cover-card" open data-page="' + esc(pg.id) + '"><summary><h2>' + esc(title) + '</h2><span class="step">' + esc(tpl.short || tpl.name) + "</span></summary>" +
        (i === 0 ? '<p class="hint">' + esc(tpl.blurb || "") + "</p>" : "") + pg.rows.map(function (r) { return rowHtml(pg, r, vals); }).join("") + "</details>";
    }).join("");
  }
  function onField(e) {
    var el = e.target, key = el.getAttribute && el.getAttribute("data-key");
    if (!key) return;
    var vals = state.docs[state.agreement].values;
    if (el.type === "checkbox") vals[key] = el.checked;
    else vals[key] = el.value;
    if (el.type === "checkbox" || el.type === "radio") {
      // re-draw this row so nested options appear / disappear
      var rowEl = el.closest(".frow"), rk = rowEl && rowEl.getAttribute("data-row");
      var pgId = rk.split(":")[0], rid = rk.split(":")[1];
      var pg = tpl.cover.filter(function (p) { return p.id === pgId; })[0], row = pg.rows.filter(function (r) { return r.id === rid; })[0];
      var tmp = document.createElement("div");
      tmp.innerHTML = rowHtml(pg, row, vals);
      rowEl.replaceWith(tmp.firstChild);
      var again = document.querySelector('[data-row="' + rk + '"] [data-key="' + key + '"]' + (el.type === "radio" ? '[value="' + el.value + '"]' : ""));
      if (again) again.focus();
    }
    schedule();
  }
  $("cover-form").addEventListener("input", onField);
  $("cover-form").addEventListener("change", function (e) { if (e.target.type === "checkbox" || e.target.type === "radio" || e.target.tagName === "SELECT") onField(e); });

  /* ---------- preview ---------- */
  function schedule() { clearTimeout(renderTimer); renderTimer = setTimeout(function () { renderPreview(); save(); }, 120); }
  function itemHtml(it) {
    var t = esc(it.t).split(esc(C.BLANK)).join('<span class="miss">' + C.BLANK + "</span>");
    return '<div class="d-it' + (it.bullet ? " b" : "") + (it.lead ? " lead" : "") + '" style="--lvl:' + it.lvl + '">' + t + "</div>";
  }
  function runsHtml(b) {
    var h = "";
    if (b.l === 1 && b.h && b.r) h += "<b>" + esc(b.n + " " + b.h) + ".</b> ";
    else if (b.n) h += b.l <= 2 ? "<b>" + esc(b.n) + "</b> " : esc(b.n) + " ";
    if (b.h3) h += "<b>" + esc(b.h3) + "</b> ";
    (b.r || []).forEach(function (r) { h += typeof r === "string" ? esc(r) : r[0] === "b" ? "<b>" + esc(r[1]) + "</b>" : '<span class="v">' + esc(r[1]) + "</span>"; });
    return h;
  }
  function renderPreview() {
    var d = state.docs[state.agreement];
    model = C.build(tpl, { values: d.values, parties: state.parties });
    var h = "";
    model.pages.forEach(function (pg) {
      h += '<section class="d-cover">' + (pg.kicker ? '<div class="d-kicker">' + esc(pg.kicker) + "</div>" : "") + '<h2 class="d-title">' + esc(pg.title) + "</h2>";
      if (pg.intro) h += '<p class="d-intro">' + esc(pg.intro) + "</p>";
      if (pg.lead) h += '<p class="d-lead">' + esc(pg.lead) + "</p>";
      h += '<div class="d-rows">' + pg.rows.map(function (r) {
        if (r.heading) return '<div class="d-head">' + esc(r.heading) + "</div>";
        return '<div class="d-row"><div class="d-l"><b>' + esc(r.label) + "</b>" + (r.sub ? "<small>" + esc(r.sub) + "</small>" : "") + '</div><div class="d-r">' + r.items.map(itemHtml).join("") + "</div></div>";
      }).join("") + "</div>";
      if (pg.closing) h += '<p class="d-closing">' + esc(pg.closing) + "</p>";
      h += '<table class="d-sign"><thead><tr><th></th>' + pg.sign.heads.map(function (x) { return "<th>" + esc(x) + "</th>"; }).join("") + "</tr></thead><tbody>" +
        pg.sign.rows.map(function (r) {
          return '<tr><th scope="row">' + esc(r.label) + (r.note ? "<small>" + esc(r.note) + "</small>" : "") + "</th>" + r.values.map(function (v) { return r.line ? '<td class="sigline"><span></span></td>' : "<td>" + (esc(v) || '<span class="miss">' + C.BLANK + "</span>") + "</td>"; }).join("") + "</tr>";
        }).join("") + "</tbody></table>";
      h += '<p class="d-att">' + esc(model.attribution) + "</p></section>";
    });
    h += '<section class="d-terms"><h2 class="d-tt">' + esc(model.termsTitle) + '</h2><p class="d-tn">' + esc(model.termsNote) + "</p>";
    h += model.terms.map(function (b) {
      if (b.l === 1 && b.h && !b.r) return '<h3 class="d-sec">' + esc(b.n + " " + b.h) + "</h3>";
      return '<p class="d-p l' + b.l + '">' + runsHtml(b) + "</p>";
    }).join("");
    h += '<p class="d-att end"><a href="' + esc(model.termsUrl) + '" target="_blank" rel="noopener">' + esc(model.attribution.replace(/ CC BY 4\.0\.$/, "")) + '</a> <a href="' + esc(model.licenceUrl) + '" target="_blank" rel="noopener">CC BY 4.0</a>.</p>' +
      '<p class="d-src">Standard Terms: ' + esc(model.termsUrl) + " · Licence: " + esc(model.licenceUrl) + "</p></section>";
    h += '<p class="d-credit">' + esc(C.CREDIT) + "</p>";
    if (!allowed()) h += '<div class="d-lock"><p><b>' + esc(tpl.name) + "</b> is part of Pactsprig Pro. Keep filling it in; downloads and printing unlock with Pro ($29 once).</p><button type=\"button\" class=\"btn\" id=\"btn-unlock\">Unlock all agreements</button></div>";
    var docEl = $("doc");
    docEl.innerHTML = h;
    docEl.classList.toggle("locked", !allowed());
    docEl.classList.toggle("a4", state.page === "A4");
    var ul = $("btn-unlock"); if (ul) ul.onclick = function () { upsell(null, "agreement"); };
    $("doc-meta").textContent = tpl.short + " · Version " + tpl.version;
    var w = $("warn");
    w.textContent = model.missing.length ? "Still blank: " + model.missing.slice(0, 4).join(", ") + (model.missing.length > 4 ? " and " + (model.missing.length - 4) + " more" : "") + ". Blank lines print as ________ to fill by hand." : "";
    quotaLine();
  }

  /* ---------- quota + downloads ---------- */
  function quotaLine() {
    var el = $("quota");
    if (pro()) { el.textContent = "Pro: unlimited downloads, Word export, no Pactsprig footer line."; el.className = "quota"; return; }
    if (!agreementInfo(state.agreement).free) { el.textContent = "This agreement is part of Pro. The Mutual NDA is free."; el.className = "quota out"; return; }
    var q = X.quota("pdf", FREE_PDFS);
    el.textContent = q.left > 0 ? q.left + " of " + FREE_PDFS + " free PDFs left this month · printing is free" : "You have used this month's " + FREE_PDFS + " free PDFs. Printing still works, or go Pro for unlimited.";
    el.className = "quota" + (q.left > 0 ? "" : " out");
  }
  function lockedUpsell() { upsell(tpl.name + " is part of Pactsprig Pro, with Word export and unlimited downloads.", "agreement"); }
  function downloadPdf() {
    if (!allowed()) return lockedUpsell();
    var q = X.quota("pdf", FREE_PDFS);
    if (!pro() && q.left <= 0) { quotaLine(); return upsell("You have used this month's " + FREE_PDFS + " free PDFs. Go Pro for unlimited downloads with no footer line.", "pdf-limit"); }
    var bytes;
    try { bytes = P.build(model, { credit: !pro(), page: state.page }); } catch (e) { toast("Sorry, the PDF could not be made: " + e.message); return; }
    q.take();
    X.download(C.fileName(tpl, state.parties, "pdf"), new Blob([bytes], { type: "application/pdf" }));
    quotaLine();
    if (!pro()) {
      $("pro-cta").hidden = false;
      var left = X.quota("pdf", FREE_PDFS).left;
      toast("PDF downloaded. " + (left ? left + " free PDF" + (left === 1 ? "" : "s") + " left this month." : "That was your last free PDF this month."));
    } else toast("PDF downloaded.");
  }
  function downloadDocx() {
    if (!pro()) return upsell("Word (DOCX) export is part of Pro, so you can edit the Cover Page in Word or upload it to your e-signature tool.", "docx");
    var bytes = D.build(model, { page: state.page });
    X.download(C.fileName(tpl, state.parties, "docx"), new Blob([bytes], { type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }));
    toast("Word file downloaded.");
  }
  $("btn-pdf").addEventListener("click", downloadPdf);
  $("m-pdf").addEventListener("click", downloadPdf);
  $("btn-docx").addEventListener("click", downloadDocx);
  $("btn-print").addEventListener("click", function () {
    if (!allowed()) return lockedUpsell();
    window.print();
  });
  $("f-page").addEventListener("change", function (e) { state.page = e.target.value === "A4" ? "A4" : "Letter"; save(); renderPreview(); });

  /* ---------- presets, saved companies, backup ---------- */
  function renderPresets() {
    var box = $("presets");
    if (!pro()) {
      box.innerHTML = '<p class="hint">Pro remembers the companies you work with and your usual Cover Page answers (for example “Standard NDA: Delaware, 2 years”), so the next agreement takes seconds.</p><button type="button" class="btn sm" id="btn-presets-pro">Unlock with Pro</button>';
      $("btn-presets-pro").onclick = function () { upsell(null, "presets"); };
      return;
    }
    var mine = state.presets.filter(function (p) { return p.agreement === state.agreement; });
    box.innerHTML = '<h3 class="mini">Cover Page presets for ' + esc(tpl.short) + '</h3><div class="preset-add"><label class="sr" for="f-preset">Preset name</label><input id="f-preset" placeholder="e.g. Standard NDA, Delaware, 2 years"><button type="button" class="btn sm" id="btn-preset-save">Save preset</button></div>' +
      '<div class="chips">' + (mine.length ? mine.map(function (p) { var i = state.presets.indexOf(p); return '<span class="chip"><button type="button" class="chip-load" data-preset="' + i + '">' + esc(p.name) + '</button><button type="button" data-del-preset="' + i + '" aria-label="Delete preset ' + esc(p.name) + '">×</button></span>'; }).join("") : '<span class="hint">No presets yet.</span>') + "</div>" +
      '<h3 class="mini">Saved companies</h3><div class="chips">' + (state.savedParties.length ? state.savedParties.map(function (p, i) { return '<span class="chip"><span>' + esc(p.company) + '</span><button type="button" data-del-party="' + i + '" aria-label="Delete ' + esc(p.company) + '">×</button></span>'; }).join("") : '<span class="hint">Use “Save company” on a party to keep it here.</span>') + "</div>";
  }
  $("presets").addEventListener("click", function (e) {
    var t = e.target;
    if (t.id === "btn-preset-save") {
      var name = ($("f-preset").value || "").trim() || tpl.short + " preset " + (state.presets.length + 1);
      state.presets.push({ name: name, agreement: state.agreement, values: Object.assign({}, state.docs[state.agreement].values) });
      save(); renderPresets(); toast("Preset saved: " + name);
    } else if (t.hasAttribute("data-preset")) {
      var p = state.presets[+t.getAttribute("data-preset")];
      if (p) { state.docs[state.agreement].values = Object.assign(C.defaults(tpl, C.isoToday()), p.values); renderForm(); renderPreview(); save(); toast("Loaded preset: " + p.name); }
    } else if (t.hasAttribute("data-del-preset")) {
      state.presets.splice(+t.getAttribute("data-del-preset"), 1); save(); renderPresets();
    } else if (t.hasAttribute("data-del-party")) {
      state.savedParties.splice(+t.getAttribute("data-del-party"), 1); save(); renderPresets(); renderParties();
    }
  });
  $("btn-backup").addEventListener("click", function () {
    if (!pro()) return upsell("JSON backup is part of Pro.", "backup");
    var c = Object.assign({}, state); delete c.fresh;
    X.download("pactsprig-backup-" + C.isoToday() + ".json", JSON.stringify({ app: "pactsprig", version: 1, saved: new Date().toISOString(), data: c }, null, 1), "application/json");
  });
  $("f-restore").addEventListener("click", function (e) { if (!pro()) { e.preventDefault(); upsell("JSON backup and restore are part of Pro.", "backup"); } });
  $("f-restore").addEventListener("change", function (e) {
    var f = e.target.files && e.target.files[0];
    if (!f) return;
    f.text().then(function (txt) {
      var j = JSON.parse(txt);
      if (!j || j.app !== "pactsprig" || !j.data) throw new Error("not a Pactsprig backup");
      localStorage.setItem(KEY, JSON.stringify(j.data));
      state = load();
      return switchTo(state.agreement).then(function () { renderPresets(); toast("Backup restored."); });
    }).catch(function (err) { toast("Could not restore: " + err.message); });
    e.target.value = "";
  });

  /* ---------- hero buttons ---------- */
  $("btn-example").addEventListener("click", function () { switchTo(state.agreement, true).then(function () { toast("Example loaded. Change anything; the contract updates as you type."); }); });
  $("btn-clear").addEventListener("click", function () {
    state.parties = [C.emptyParty(), C.emptyParty()];
    switchTo(state.agreement, "blank").then(function () { toast("Blank Cover Page. Your saved companies and presets are still here."); });
  });
  $("btn-cta").addEventListener("click", function () { upsell(null, "mid-cta"); });

  /* ---------- Pro on/off ---------- */
  function onPro(on) {
    $("pro-pill").textContent = on ? "Pro ✓" : "Pro";
    if (on) $("pro-cta").hidden = true;
    if (tpl) { renderPreview(); renderPresets(); }
  }

  /* ---------- PWA ---------- */
  var deferred = null;
  window.addEventListener("beforeinstallprompt", function (e) { e.preventDefault(); deferred = e; $("btn-install").hidden = false; });
  $("btn-install").addEventListener("click", function () {
    if (!deferred) return;
    deferred.prompt();
    deferred.userChoice.finally(function () { deferred = null; $("btn-install").hidden = true; });
  });
  if ("serviceWorker" in navigator && (location.protocol === "https:" || /^(localhost|127\.0\.0\.1)$/.test(location.hostname))) {
    window.addEventListener("load", function () { navigator.serviceWorker.register("sw.js").catch(function () { /* offline cache is optional */ }); });
  }

  /* ---------- start ---------- */
  state = load();
  $("f-page").value = state.page;
  renderPicker();
  switchTo(state.agreement).then(function () {
    state.fresh = false;
    X.onChange(onPro);
    document.documentElement.setAttribute("data-ready", "1");
  });
})();