File size: 45,119 Bytes
61d68b0
f841046
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#15293e"><meta name="description" content="ContractPilot: offline contract renewal and exposure planning simulator with synthetic data."><title>ContractPilot | Contract Exposure Intelligence Studio</title>
<style>
:root{color-scheme:light;--bg:#f3f6fa;--card:#fff;--card2:#f7f9fc;--text:#172b43;--muted:#73859b;--line:#e1e8ef;--nav:#14293d;--blue:#3369c4;--teal:#0f928d;--orange:#c27c3d;--red:#bf5962;--pale:#eaf1fc;--shade:#ecf7f5;--shadow:0 12px 44px rgba(27,56,84,.045)}
body.dark{color-scheme:dark;--bg:#101d2b;--card:#182b3b;--card2:#22384a;--text:#e7eff9;--muted:#a4b9cd;--line:#345066;--nav:#0a1825;--blue:#a3beff;--teal:#71d9cf;--orange:#ffca85;--red:#ff9ca5;--pale:#273c55;--shade:#244440;--shadow:none}
*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;font:13px/1.55 system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:var(--text);background:var(--bg)}button,input,select{font:inherit}button{cursor:pointer}button:disabled{opacity:.45;cursor:not-allowed}a{color:var(--blue)}button:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid var(--blue);outline-offset:3px}.shell{display:grid;grid-template-columns:254px minmax(0,1fr);min-height:100vh}.side{position:sticky;top:0;height:100vh;background:var(--nav);padding:26px 18px;display:flex;flex-direction:column;color:#adc4d3}.brand{display:flex;align-items:center;gap:12px;margin:3px 4px 37px}.mark{width:46px;height:46px;display:grid;place-items:center;background:linear-gradient(130deg,#9cedcf,#84b4f8);border-radius:14px;color:#123047;font-size:25px;font-weight:900;box-shadow:0 9px 18px #4fdac425}.brand b{display:block;font-size:21px;line-height:1.1;color:#fff;letter-spacing:-.8px}.brand small{letter-spacing:1.55px;font-size:9px;font-weight:800;color:#93b7ca}.nav-label{margin:0 12px 10px;color:#7296ab;font-size:10px;letter-spacing:1.7px;font-weight:850}.nav{display:grid;gap:5px}.nav button{border:0;background:transparent;color:#b3cad9;width:100%;display:flex;align-items:center;gap:13px;padding:12px 13px;border-radius:11px;text-align:left;font-weight:750;font-size:12px}.nav button:hover,.nav button.active{background:#2b495f;color:#fff}.nav button.active{box-shadow:inset 3px 0 #78e7d1}.nav .sym{font-size:17px;width:22px;text-align:center}.sidefoot{margin-top:auto;border:1px solid #355365;border-radius:14px;padding:15px;background:#1c384c;font-size:11px;color:#b6d0db}.sidefoot strong{display:block;color:#fff;margin-bottom:4px}.dot{display:inline-block;width:8px;height:8px;background:#83e7bd;border-radius:50%;margin-right:7px}
main{min-width:0;width:100%;max-width:1650px;padding:25px clamp(16px,3.1vw,46px) 34px}.top{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:28px}.crumb{font-size:11px;color:var(--muted);font-weight:700}.crumb strong{color:var(--text)}.topact{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.btn{display:inline-flex;align-items:center;justify-content:center;gap:7px;border-radius:9px;padding:9px 13px;background:var(--card);border:1px solid var(--line);color:var(--text);font-size:11px;font-weight:800;white-space:nowrap;min-height:35px}.btn:hover{filter:brightness(.96)}.btn.primary{background:#3977ce;color:white;border-color:#3977ce}.btn.ghost{background:var(--pale);border-color:transparent;color:var(--blue)}.btn.danger{color:var(--red)}.hero{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;margin-bottom:25px}.eyebrow{text-transform:uppercase;font-weight:900;letter-spacing:1.65px;color:var(--teal);font-size:10px}.hero h1{margin:3px 0 5px;font-size:clamp(26px,3vw,38px);line-height:1.16;letter-spacing:-1.2px}.hero p{margin:0;color:var(--muted);max-width:670px}.pill{border-radius:30px;background:var(--shade);color:var(--teal);font-weight:800;padding:7px 12px;font-size:10px;white-space:nowrap}.cards{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:15px;margin:0 0 18px}.card{background:var(--card);border:1px solid var(--line);border-radius:15px;box-shadow:var(--shadow);min-width:0}.kpi{padding:19px 18px}.kpihead{display:flex;justify-content:space-between;align-items:center;gap:6px;font-size:11px;font-weight:780;color:var(--muted)}.kpiicon{width:31px;height:31px;background:var(--pale);color:var(--blue);border-radius:9px;display:grid;place-items:center;font-size:15px}.kpiv{font-size:clamp(20px,2.1vw,29px);letter-spacing:-.9px;font-weight:850;margin:12px 0 4px;overflow-wrap:anywhere}.kpif{font-size:10px;color:var(--muted)}.kpi.danger .kpiicon{background:#fff0ee;color:#b65f59}.grid{display:grid;grid-template-columns:minmax(0,1.65fr) minmax(310px,1fr);gap:16px;margin-bottom:16px}.grid.equal{grid-template-columns:repeat(2,minmax(0,1fr))}.pane{padding:22px}.panehead{display:flex;justify-content:space-between;align-items:flex-start;gap:8px;margin-bottom:17px}.panehead h2{font-size:14px;margin:0;letter-spacing:-.25px}.panehead p{margin:3px 0 0;color:var(--muted);font-size:11px}.mini{font-size:10px;color:var(--muted)}.legend{display:flex;gap:13px;flex-wrap:wrap;font-size:10px;color:var(--muted)}.swatch{display:inline-block;width:8px;height:8px;background:var(--blue);border-radius:2px;margin-right:5px}.swatch.teal{background:var(--teal)}.swatch.orange{background:var(--orange)}.chartbox{width:100%;min-height:215px;overflow:hidden}.chartbox svg{display:block;width:100%;height:auto}.rowlist{display:grid;gap:12px}.lineitem{display:flex;gap:12px;align-items:center}.lineitem .name{min-width:100px;font-size:11px;font-weight:750;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.track{height:10px;background:var(--card2);border-radius:10px;flex:1;overflow:hidden}.bar{height:100%;border-radius:9px;background:linear-gradient(90deg,#62a5df,#4b7aca)}.bar.teal{background:linear-gradient(90deg,#60c8ae,#0f948d)}.bar.orange{background:#d9a366}.lineitem .num{min-width:69px;font-size:11px;text-align:right;font-weight:850}.insight{margin-top:16px;background:var(--card2);border:1px solid var(--line);padding:13px;border-radius:11px;color:var(--muted);font-size:11px}.insight strong{color:var(--text)}.badgetag{display:inline-block;border-radius:7px;background:var(--pale);color:var(--blue);padding:4px 8px;font-size:10px;font-weight:850}.badgetag.warn{background:#fbeddc;color:#a77930}.badgetag.risk{background:#fde9e8;color:#a84d5c}.badgetag.good{background:var(--shade);color:var(--teal)}.listrow{display:flex;gap:12px;justify-content:space-between;align-items:center;padding:13px 0;border-bottom:1px solid var(--line)}.listrow:last-child{border:0}.listrow b{font-size:11px}.listrow small{display:block;color:var(--muted);font-size:10px;margin-top:2px}.value{text-align:right;font-weight:840;font-size:12px}.value small{font-weight:500}.section-title{font-size:12px;text-transform:uppercase;letter-spacing:1.1px;color:var(--muted);font-weight:850;margin:22px 0 12px}.tools{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:0 0 15px}.field{display:grid;gap:6px;min-width:0}.field label{font-weight:800;font-size:10px;color:var(--muted);letter-spacing:.15px}.field input,.field select{border:1px solid var(--line);background:var(--card);color:var(--text);border-radius:9px;padding:10px 11px;min-width:0;width:100%}.tools .field{min-width:190px;flex:1}.tools .field.shrink{min-width:140px;flex:0 1 175px}.tablewrap{overflow:auto;width:100%;border-radius:13px;border:1px solid var(--line)}table{border-collapse:collapse;width:100%;font-size:11px}th{text-align:left;background:var(--card2);color:var(--muted);font-size:10px;white-space:nowrap;font-weight:850;letter-spacing:.3px}td,th{padding:13px 14px;border-bottom:1px solid var(--line)}td{vertical-align:middle}tbody tr:last-child td{border:0}td b{font-size:11px}td small{color:var(--muted);font-size:10px;display:block}td:last-child,th:last-child{text-align:right}.mono{font-variant-numeric:tabular-nums}.empty{padding:27px;text-align:center;color:var(--muted)}.smallmuted{color:var(--muted);font-size:11px}.hidden{display:none!important}.cols3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.control{padding:15px;border:1px solid var(--line);background:var(--card2);border-radius:12px}.control .clabel{display:flex;justify-content:space-between;font-size:11px;font-weight:800;gap:8px;margin-bottom:12px}.control output{color:var(--blue);font-weight:900}.control input[type=range]{width:100%;accent-color:#3984c6}.presetrow{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:17px}.presetrow .btn.sel{background:var(--nav);color:#fff;border-color:var(--nav)}.methodlist{display:grid;gap:13px}.method{border:1px solid var(--line);background:var(--card2);padding:15px 17px;border-radius:12px}.method b{display:block;font-size:12px;margin-bottom:4px}.method p{margin:0;color:var(--muted);font-size:11px}.method code{display:block;white-space:normal;margin:6px 0;color:var(--teal)}.footnote{color:var(--muted);font-size:10px;margin:16px 0 0}.page{display:none}.page.active{display:block}.alert{border-left:3px solid var(--orange);padding:12px 14px;background:var(--card2);border-radius:0 10px 10px 0;color:var(--muted);font-size:11px;margin:12px 0}.alert strong{color:var(--text)}.notice{font-size:10px;color:var(--muted)}.cal{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:7px;min-width:540px}.calmonth{background:var(--card2);padding:10px 6px;border-radius:9px;text-align:center;min-height:96px}.calmonth b{font-size:10px;color:var(--muted)}.calcount{display:block;font-size:24px;font-weight:900;margin:10px 0 1px}.calmonth small{font-size:9px;color:var(--muted)}.calendarbox{overflow:auto}.modalback{position:fixed;inset:0;background:#0b1a2dbb;display:none;align-items:center;justify-content:center;padding:14px;z-index:30}.modalback.open{display:flex}.modal{background:var(--card);color:var(--text);border-radius:18px;width:min(650px,100%);max-height:92vh;overflow:auto;padding:25px;box-shadow:0 25px 80px #0004}.modal h2{margin:0 0 4px;font-size:21px;letter-spacing:-.5px}.modal p{margin:0 0 18px;color:var(--muted);font-size:11px}.formgrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:13px}.modfoot{display:flex;justify-content:flex-end;gap:8px;margin-top:19px;flex-wrap:wrap}.mobilebar{display:none}.toast{position:fixed;right:24px;bottom:22px;background:#183247;color:#fff;padding:12px 18px;border-radius:11px;font-size:12px;box-shadow:0 8px 32px #0002;z-index:40;max-width:min(410px,90vw)}
@media(max-width:1120px){.shell{grid-template-columns:214px minmax(0,1fr)}.side{padding:25px 11px}.brand b{font-size:18px}.cards{grid-template-columns:repeat(2,minmax(0,1fr))}.grid{grid-template-columns:minmax(0,1fr)}.cols3{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:730px){.chartbox{min-height:145px}.shell{display:block}.side{height:auto;position:relative;min-height:0;padding:12px 15px}.brand{margin:0}.side .nav-label,.side .nav,.sidefoot{display:none}.mobilebar{display:flex;position:sticky;top:0;z-index:10;background:var(--nav);padding:7px 9px;gap:5px;overflow:auto;box-shadow:0 4px 15px #0919291e}.mobilebar button{border:0;background:transparent;color:#c6d7e2;border-radius:9px;padding:9px 12px;font-weight:750;font-size:11px;white-space:nowrap}.mobilebar button.active{background:#36536a;color:#fff}main{padding:18px 13px 26px}.top{margin-bottom:17px;align-items:flex-start}.topact{justify-content:flex-end}.topact .btn{padding:8px 9px}.hero{display:block}.hero .pill{display:inline-block;margin-top:10px}.pane{padding:16px}.cards{gap:9px}.kpi{padding:15px 13px}.kpiv{font-size:23px}.cols3,.grid.equal{grid-template-columns:minmax(0,1fr)}.formgrid{grid-template-columns:1fr}.lineitem .name{min-width:75px}.tools .field{min-width:135px}.topact .hide-small{display:none}}@media(max-width:365px){.top .crumb{font-size:10px}.cards{grid-template-columns:repeat(2,minmax(0,1fr))}.kpiv{font-size:20px}.brand b{font-size:17px}.kpiicon{width:25px;height:25px}.btn{font-size:10px}}
</style></head><body>
<div class="shell"><aside class="side"><div class="brand"><span class="mark">⌘</span><div><b>ContractPilot</b><small>HUGDRIGO / #107</small></div></div><div class="nav-label">WORKSPACE</div><nav id="desktopNav" class="nav" aria-label="Main navigation">
<button data-page="overview" class="active"><span class="sym">▦</span>Command overview</button><button data-page="scenarios"><span class="sym">◫</span>Scenario studio</button><button data-page="registry"><span class="sym">▤</span>Contract registry</button><button data-page="renewals"><span class="sym">◷</span>Renewal calendar</button><button data-page="method"><span class="sym">◇</span>Methodology</button></nav><div class="sidefoot"><strong><span class="dot"></span>Local simulation</strong>Sample contracts only<br>Offline calculations · No external API<br>Data stored in your browser</div></aside>
<div class="mobilebar" id="mobileNav"><button data-page="overview" class="active">Overview</button><button data-page="scenarios">Scenarios</button><button data-page="registry">Registry</button><button data-page="renewals">Renewals</button><button data-page="method">Methods</button></div>
<main><header class="top"><span class="crumb">HUGDRIGO <span style="opacity:.55">/ BUSINESS INTELLIGENCE /</span> <strong id="crumb">OVERVIEW</strong></span><div class="topact"><button id="themeBtn" class="btn" aria-label="Toggle dark mode">◐ Theme</button><button id="csvBtn" class="btn hide-small">↓ Export CSV</button><button id="jsonBtn" class="btn primary">↓ Backup</button></div></header>
<section id="overview" class="page active"><div class="hero"><div><div class="eyebrow">CONTRACT EXPOSURE INTELLIGENCE</div><h1>Control obligations.<br>See exposure clearly.</h1><p>Model annual commitments, renewal deadlines, and conditional loss exposure with a transparent, local-first simulator.</p></div><span class="pill">◆ SYNTHETIC DATASET</span></div><div id="kpis" class="cards"></div><div class="grid"><div class="card pane"><div class="panehead"><div><h2>Renewal exposure · next 12 months</h2><p>Upcoming annualized amounts by renewal month</p></div><span class="badgetag">USD</span></div><div class="legend"><span><i class="swatch"></i>Annual contract amount</span><span><i class="swatch teal"></i>Expected loss exposure</span></div><div id="renewalChart" class="chartbox"></div></div><div class="card pane"><div class="panehead"><div><h2>Exposure by category</h2><p>Annual commitment across active contracts</p></div><span class="badgetag">PORTFOLIO</span></div><div id="categoryBars" class="rowlist"></div><div id="categoryInsight" class="insight"></div></div></div><div class="grid equal"><div class="card pane"><div class="panehead"><div><h2>Priority notice deadlines</h2><p>Contractual opt-out or renegotiation notices</p></div><button data-go="renewals" class="btn">Calendar →</button></div><div id="deadlineList"></div></div><div class="card pane"><div class="panehead"><div><h2>Largest conditional losses</h2><p>Simple probability × impact exposure ranking</p></div><button data-go="registry" class="btn">Review →</button></div><div id="riskList"></div></div></div></section>
<section id="scenarios" class="page"><div class="hero"><div><div class="eyebrow">SCENARIO PLANNING LAB</div><h1>Stress-test the portfolio.</h1><p>Change assumptions, see the difference versus baseline, and explore bounded simulated loss outcomes.</p></div><span class="pill">4 SCENARIOS</span></div><div id="presets" class="presetrow"></div><div class="cols3" id="sliders"></div><div id="scenarioKpis" class="cards" style="margin-top:18px"></div><div class="grid"><div class="card pane"><div class="panehead"><div><h2>Estimated annual exposure · comparison</h2><p>Scenario assumptions, not contract forecasts</p></div><span class="badgetag">BASE VS SCENARIO</span></div><div id="scenarioChart" class="chartbox"></div></div><div class="card pane"><div class="panehead"><div><h2>Simulated incident-loss distribution</h2><p>400 seeded independent draws · illustrative P10 / P50 / P90</p></div><span class="badgetag">MONTE CARLO</span></div><div id="simResults"></div><div class="alert"><strong>Interpretation:</strong> A percentage here describes only the selected hypothetical model, not a calibrated real-world risk.</div></div></div></section>
<section id="registry" class="page"><div class="hero"><div><div class="eyebrow">CONTRACT REGISTER</div><h1>One registry. Clear actions.</h1><p>Edit fictional agreements, preserve archived records, and export your working assumptions as portable files.</p></div><button id="addBtn" class="btn primary">+ New contract</button></div><div class="card pane"><div class="tools"><div class="field"><label for="search">Search contracts or counterparties</label><input id="search" type="search" placeholder="Search portfolio…" maxlength="100"></div><div class="field shrink"><label for="catFilter">Category</label><select id="catFilter"><option value="all">All categories</option><option>Software</option><option>Logistics</option><option>Facilities</option><option>Professional services</option></select></div><div class="field shrink"><label for="statusFilter">Status</label><select id="statusFilter"><option value="active">Active</option><option value="archived">Archived</option><option value="all">All records</option></select></div></div><div class="tablewrap"><table><thead><tr><th>Contract / counterparty</th><th>Category</th><th>Annual USD</th><th>Renewal</th><th>Notice window</th><th>Loss exposure</th><th>Actions</th></tr></thead><tbody id="contractRows"></tbody></table></div><p class="footnote" id="registryFoot"></p></div><div class="alert">Imported and edited entries are stored locally on this device when browser storage is available. Do not enter confidential contracts or personally identifiable data.</div></section>
<section id="renewals" class="page"><div class="hero"><div><div class="eyebrow">RENEWAL CONTROL CENTER</div><h1>Prepare before deadlines.</h1><p>See the notice deadlines and renewal concentration of active contracts. The dashboard calculates dates; it does not issue reminders.</p></div><span class="pill">12-MONTH VIEW</span></div><div class="card pane" style="margin-bottom:16px"><div class="panehead"><div><h2>Contract renewal calendar</h2><p>Count and value of agreements renewing within each coming month</p></div></div><div id="monthCalendar" class="calendarbox"></div></div><div class="card pane"><div class="panehead"><div><h2>Notice deadline tracker</h2><p>Notice deadline = renewal date − contract notice days</p></div><span class="badgetag">DATE-BASED</span></div><div class="tablewrap"><table><thead><tr><th>Agreement</th><th>Notice deadline</th><th>Days remaining</th><th>Renewal</th><th>Annual USD</th><th>Priority</th></tr></thead><tbody id="noticeRows"></tbody></table></div><p class="footnote">Dates are computed using UTC calendar days to avoid local daylight-saving shifts. Always confirm real contractual notice terms independently.</p></div></section>
<section id="method" class="page"><div class="hero"><div><div class="eyebrow">ASSUMPTIONS & LIMITS</div><h1>Transparent by design.</h1><p>Every display and simulated result follows explicit formulas. Nothing uses external data or constitutes legal advice.</p></div><span class="pill">RULE-BASED DEMO</span></div><div class="grid equal"><div class="card pane"><div class="panehead"><h2>Calculation rules</h2></div><div class="methodlist"><div class="method"><b>Annual commitment</b><code>Σ annual_value over active contracts</code><p>Contract values are simplified annual USD equivalents, not accrued expenses or a cash flow forecast.</p></div><div class="method"><b>Expected incident-loss exposure</b><code>Σ (annual_incident_probability × conditional_loss_amount)</code><p>Assumes one independent binary loss event per agreement during the modeled year. No correlations.</p></div><div class="method"><b>Scenario adjustment</b><code>annual_value' = annual_value × (1 + price_delta / 100)<br>p' = clamp(p + probability_delta, 0, 100)<br>loss' = loss_amount × (1 + severity_delta / 100)</code><p>Scenarios modify all active records uniformly and do not forecast contractual terms.</p></div><div class="method"><b>Renewal notice</b><code>notice_deadline = renewal_date − notice_days</code><p>Only the first supplied renewal date is shown. Automatic renewal, extension and termination clauses are not modeled.</p></div></div></div><div class="card pane"><div class="panehead"><h2>Responsible interpretation</h2></div><div class="methodlist"><div class="method"><b>Synthetic starting data</b><p>Eight entirely invented contract records and counterparties. No real pricing, legal terms, renewal schedules or incidents.</p></div><div class="method"><b>Simulation</b><p>400 seeded draws sample a Bernoulli loss event for each active contract, with illustrative severity variation of ±10%. P10/P50/P90 summarize those draws, not a statistical confidence interval.</p></div><div class="method"><b>Privacy and security</b><p>No server or remote API; browser localStorage is not encrypted. Backups contain plain JSON. Avoid confidential contractual terms. Imported names are escaped before HTML display and CSV is protected against formula execution.</p></div><div class="method"><b>Not legal or financial advice</b><p>This is a self-contained educational decision-support demonstrator. Validate notice obligations, applicable law, pricing, risk probabilities, contract rights and interdependencies separately before taking action.</p></div></div><div class="tools" style="margin-top:15px"><button id="importBtn" class="btn">↑ Restore JSON</button><button id="resetBtn" class="btn danger">↺ Reset demo data</button></div></div></div></section>
<footer class="footnote">HUGDRIGO PORTFOLIO · PROJECT #107 · LOCAL-FIRST · SYNTHETIC EXAMPLES · © 2026 · FOR EDUCATIONAL SIMULATION ONLY</footer></main></div>
<div class="modalback" id="modalWrap" role="presentation"><div class="modal" role="dialog" aria-modal="true" aria-labelledby="modalHeading"><h2 id="modalHeading">New contract</h2><p>Fictional planning information only. Fields are validated before saving.</p><form id="contractForm"><div class="formgrid"><div class="field"><label for="fName">Contract name</label><input id="fName" required maxlength="72" autocomplete="off"></div><div class="field"><label for="fParty">Counterparty</label><input id="fParty" required maxlength="72" autocomplete="off"></div><div class="field"><label for="fCategory">Category</label><select id="fCategory"><option>Software</option><option>Logistics</option><option>Facilities</option><option>Professional services</option></select></div><div class="field"><label for="fValue">Annual contract value (USD)</label><input id="fValue" type="number" required min="0" max="100000000" step="0.01"></div><div class="field"><label for="fRenew">Next renewal date</label><input id="fRenew" type="date" required min="2020-01-01" max="2100-12-31"></div><div class="field"><label for="fNotice">Notice days before renewal</label><input id="fNotice" type="number" required min="0" max="365" step="1"></div><div class="field"><label for="fRisk">Annual incident probability (%)</label><input id="fRisk" type="number" required min="0" max="100" step="0.1"></div><div class="field"><label for="fLoss">Loss amount if incident occurs (USD)</label><input id="fLoss" type="number" required min="0" max="100000000" step="0.01"></div></div><div class="modfoot"><button id="cancelModal" type="button" class="btn">Cancel</button><button type="submit" class="btn primary">Save contract</button></div></form></div></div><input id="fileInput" type="file" accept=".json,application/json" class="hidden" aria-label="Select JSON backup">
<script>
'use strict';
(() => {
const CATEGORIES=['Software','Logistics','Facilities','Professional services'];
const DEFAULTS=[
{id:1,name:'Nimbus Cloud Suite',party:'Cumulus Systems (demo)',category:'Software',value:148000,renew:'2026-11-18',notice:30,risk:11,loss:92000,archived:false},
{id:2,name:'Regional Freight Network',party:'BlueRoute Freight (demo)',category:'Logistics',value:265000,renew:'2027-01-12',notice:60,risk:22,loss:154000,archived:false},
{id:3,name:'Facilities Service Plan',party:'Cedar Property Group (demo)',category:'Facilities',value:98000,renew:'2027-02-28',notice:45,risk:8,loss:54000,archived:false},
{id:4,name:'Business Systems Advisory',party:'Ridge Partners (demo)',category:'Professional services',value:74000,renew:'2026-12-15',notice:45,risk:13,loss:32000,archived:false},
{id:5,name:'Data Operations License',party:'Mesa Analytics (demo)',category:'Software',value:113000,renew:'2027-05-10',notice:90,risk:17,loss:135000,archived:false},
{id:6,name:'Cross-Dock Distribution',party:'NorthBridge Cargo (demo)',category:'Logistics',value:172000,renew:'2027-06-25',notice:60,risk:24,loss:89000,archived:false},
{id:7,name:'Workplace Energy Service',party:'Solstice Utilities (demo)',category:'Facilities',value:79000,renew:'2027-08-30',notice:120,risk:9,loss:62000,archived:false},
{id:8,name:'Compliance Operations',party:'Axial Consulting (demo)',category:'Professional services',value:59000,renew:'2027-09-14',notice:60,risk:14,loss:76000,archived:false}
];
const PRESETS=[{name:'Baseline',price:0,prob:0,severity:0},{name:'Price shock',price:16,prob:0,severity:0},{name:'Supplier disruption',price:5,prob:18,severity:25},{name:'Renegotiation',price:-10,prob:-6,severity:-10}];
const STORE='hugdrigo_contractpilot_107_v1', THEME='hugdrigo_contractpilot_theme';
const clone=o=>JSON.parse(JSON.stringify(o));
const byId=id=>document.getElementById(id);
const cap=(x,lo,hi)=>Math.min(hi,Math.max(lo,x));
const esc=v=>String(v).replace(/[&<>"']/g,x=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[x]));
const fmt=(n,currency=true)=>currency?'$'+Number(n).toLocaleString('en-US',{maximumFractionDigits:0}):Number(n).toLocaleString('en-US',{maximumFractionDigits:1});
const compact=n=>Math.abs(n)>=1000000?(n/1000000).toFixed(2)+'M':Math.abs(n)>=1000?(n/1000).toFixed(1)+'K':Math.round(n).toString();
const iso=/^\d{4}-\d{2}-\d{2}$/;
const dayMs=86400000;
const dateNumber=s=>{const t=Date.parse(s+'T00:00:00Z');return Number.isFinite(t)?t:NaN};
const utcDay=()=>{const d=new Date();return Date.UTC(d.getFullYear(),d.getMonth(),d.getDate())};
const shiftDate=(s,days)=>{const d=new Date(dateNumber(s)-days*dayMs);return d.toISOString().slice(0,10)};
const dateLabel=s=>new Date(dateNumber(s)).toLocaleDateString('en-US',{timeZone:'UTC',month:'short',day:'numeric',year:'numeric'});
const active=()=>state.items.filter(i=>!i.archived);
function validContract(r){if(!r||typeof r!=='object'||Array.isArray(r))return false;
if(!Number.isSafeInteger(r.id)||r.id<1||r.id>1000000000)return false;
if(typeof r.name!=='string'||!r.name.trim()||r.name.length>72||typeof r.party!=='string'||!r.party.trim()||r.party.length>72)return false;
if(!CATEGORIES.includes(r.category)||typeof r.renew!=='string'||!iso.test(r.renew)||!Number.isFinite(dateNumber(r.renew))||new Date(dateNumber(r.renew)).toISOString().slice(0,10)!==r.renew||r.renew<'2020-01-01'||r.renew>'2100-12-31')return false;
if(typeof r.archived!=='boolean')return false;
return ['value','notice','risk','loss'].every(k=>typeof r[k]==='number'&&Number.isFinite(r[k]))&&r.value>=0&&r.value<=100000000&&Number.isInteger(r.notice)&&r.notice>=0&&r.notice<=365&&r.risk>=0&&r.risk<=100&&r.loss>=0&&r.loss<=100000000;
}
function validBackup(obj){if(!obj||obj.kind!=='ContractPilotBackup'||obj.version!==1||!Array.isArray(obj.items)||obj.items.length>150||typeof obj.nextId!=='number'||!Number.isSafeInteger(obj.nextId)||obj.nextId<1||obj.nextId>1000000001||!obj.items.every(validContract))return false;
const ids=obj.items.map(r=>r.id);return new Set(ids).size===ids.length && obj.nextId>Math.max(0,...ids);
}
let state={items:clone(DEFAULTS),nextId:9};let page='overview';let scenario={price:0,prob:0,severity:0},preset='Baseline',editing=null,toastId=0;
function load(){try{const raw=localStorage.getItem(STORE);if(raw){const data=JSON.parse(raw);if(validBackup(data))state=data;}}catch(e){}try{if(localStorage.getItem(THEME)==='dark')document.body.classList.add('dark')}catch(e){}}
function save(){try{localStorage.setItem(STORE,JSON.stringify({kind:'ContractPilotBackup',version:1,...state}));}catch(e){notify('Local storage unavailable; use JSON backup.')}}
function notify(m){const old=document.querySelector('.toast');if(old)old.remove();const p=document.createElement('div');p.className='toast';p.setAttribute('role','status');p.textContent=m;document.body.appendChild(p);const id=++toastId;setTimeout(()=>{if(id===toastId)p.remove()},3500)}
function project(sc=scenario,items=active()) {let committed=0,expected=0,baseExpected=0;for(const x of items){if(x.archived)continue;const v=x.value*(1+sc.price/100);const p=cap(x.risk+sc.prob,0,100)/100;const l=x.loss*(1+sc.severity/100);committed+=v;expected+=p*l;baseExpected+=x.loss*x.risk/100}return{committed,expected,baseExpected,total:committed+expected}}
function kpi(title,value,detail,icon,cls=''){return `<div class="card kpi ${cls}"><div class="kpihead"><span>${esc(title)}</span><span class="kpiicon">${esc(icon)}</span></div><div class="kpiv mono">${esc(value)}</div><div class="kpif">${esc(detail)}</div></div>`}
function riskFor(x,sc={price:0,prob:0,severity:0}){return cap(x.risk+sc.prob,0,100)/100*x.loss*(1+sc.severity/100)}
function dates(x){const deadline=shiftDate(x.renew,x.notice);return {deadline,days:Math.round((dateNumber(deadline)-utcDay())/dayMs),renewDays:Math.round((dateNumber(x.renew)-utcDay())/dayMs)}}
function deadlineTag(d){return d<0?'Overdue':d<=30?'Due soon':'Scheduled'}
function deadlineClass(d){return d<0?'risk':d<=30?'warn':'good'}
function monthBuckets(items){const now=new Date(utcDay()), first=Date.UTC(now.getUTCFullYear(),now.getUTCMonth(),1);return Array.from({length:12},(_,i)=>{const t=new Date(Date.UTC(now.getUTCFullYear(),now.getUTCMonth()+i,1));const end=Date.UTC(t.getUTCFullYear(),t.getUTCMonth()+1,1);const list=items.filter(x=>dateNumber(x.renew)>=Math.max(first,t.getTime())&&dateNumber(x.renew)<end);return{label:t.toLocaleDateString('en-US',{month:'short',timeZone:'UTC'}),ym:t.toISOString().slice(0,7),count:list.length,amount:list.reduce((s,x)=>s+x.value,0),risk:list.reduce((s,x)=>s+riskFor(x),0)}})}
function svgBars(data,mode='renewals'){
const mx=Math.max(1,...data.map(d=>d.amount)),w=750,h=258,left=30,bottom=217,step=(w-50)/Math.max(1,data.length),barW=Math.min(40,step*.62);
let content=`<svg viewBox="0 0 ${w} ${h}" role="img" aria-label="${mode==='renewals'?'Monthly contract renewal amounts and associated expected loss':'Baseline and scenario cost comparison'}"><g stroke="var(--line)" stroke-width="1">`;
for(let j=0;j<4;j++){const y=31+j*56;content+=`<line x1="32" y1="${y}" x2="${w-13}" y2="${y}" stroke-dasharray="3,5"/>`;}content+='</g>';
for(let i=0;i<data.length;i++){
 const d=data[i],x=left+step*i+step/2-barW/2,ht=d.amount/mx*162,y=bottom-ht,split=mode==='renewals';
 const aw=split?barW*.61:barW;
 content+=`<rect x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${aw.toFixed(1)}" height="${Math.max(0,ht).toFixed(1)}" rx="4" fill="#4b88cb"/>`;
 if(split){const rh=(d.risk||0)/mx*162;content+=`<rect x="${(x+aw+2).toFixed(1)}" y="${(bottom-rh).toFixed(1)}" width="${(barW-aw-2).toFixed(1)}" height="${Math.max(0,rh).toFixed(1)}" rx="3" fill="#15a49c"/>`;}
 content+=`<text x="${(x+barW/2).toFixed(1)}" y="237" font-size="11" text-anchor="middle" fill="var(--muted)">${esc(d.label)}</text>`;
 if(ht>30)content+=`<text x="${(x+barW/2).toFixed(1)}" y="${Math.max(14,y-7).toFixed(1)}" font-size="10" text-anchor="middle" fill="var(--text)" font-weight="700">${compact(d.amount)}</text>`;
}
return content+'</svg>'}
function overview(){const items=active(),val=project({price:0,prob:0,severity:0},items),urgent=items.map(x=>({...x,...dates(x)})).filter(x=>x.renewDays>=0&&x.days<=30).length;
byId('kpis').innerHTML=kpi('Annual commitments',fmt(val.committed),'Sum of active annual equivalents','▤')+kpi('Expected loss exposure',fmt(val.expected),'Probability × impact model','⚑','danger')+kpi('Active contracts',String(items.length),`${state.items.filter(x=>x.archived).length} archived · preserved`,'▦')+kpi('Notice priorities',String(urgent),'Overdue or within 30 days','◷');
const months=monthBuckets(items);byId('renewalChart').innerHTML=svgBars(months);
const amounts=CATEGORIES.map(c=>({name:c,amount:items.filter(x=>x.category===c).reduce((s,x)=>s+x.value,0)})).sort((a,b)=>b.amount-a.amount);const mx=Math.max(1,...amounts.map(a=>a.amount));byId('categoryBars').innerHTML=amounts.map((a,i)=>`<div class="lineitem"><span class="name">${esc(a.name)}</span><div class="track"><div class="bar ${i===0?'teal':''}" style="width:${(100*a.amount/mx).toFixed(2)}%"></div></div><span class="num mono">${fmt(a.amount)}</span></div>`).join('');byId('categoryInsight').innerHTML=amounts[0]&&amounts[0].amount?`<strong>Highest commitment:</strong> ${esc(amounts[0].name)} represents ${(amounts[0].amount/Math.max(1,val.committed)*100).toFixed(1)}% of modeled annual spend.`:'No active contract values to compare.';
const priorities=items.map(x=>({...x,...dates(x)})).filter(x=>x.renewDays>=0).sort((a,b)=>a.days-b.days).slice(0,4);byId('deadlineList').innerHTML=priorities.length?priorities.map(x=>`<div class="listrow"><div><b>${esc(x.name)}</b><small>Notice ${dateLabel(x.deadline)}</small></div><div class="value"><span class="badgetag ${deadlineClass(x.days)}">${deadlineTag(x.days)}</span><small>${x.days<0?Math.abs(x.days)+' days late':x.days+' days left'}</small></div></div>`).join(''):'<div class="empty">No upcoming renewals.</div>';
const risks=[...items].sort((a,b)=>riskFor(b)-riskFor(a)).slice(0,4);byId('riskList').innerHTML=risks.length?risks.map(x=>`<div class="listrow"><div><b>${esc(x.name)}</b><small>${esc(x.category)} · ${x.risk}% assumed incident probability</small></div><div class="value mono">${fmt(riskFor(x))}<small>conditional expected loss</small></div></div>`).join(''):'<div class="empty">No active contracts.</div>';
}
function seed(n){return ()=>{n=(Math.imul(1664525,n)+1013904223)>>>0;return n/4294967296}}
function simulate(sc=scenario,items=active(),iterations=400){const rnd=seed(1072026);let arr=[];for(let i=0;i<iterations;i++){let loss=0;for(const x of items){const p=cap(x.risk+sc.prob,0,100)/100;if(rnd()<p)loss+=x.loss*(1+sc.severity/100)*(0.9+0.2*rnd());}arr.push(loss)}arr.sort((a,b)=>a-b);return {p10:arr[Math.floor((iterations-1)*.10)],p50:arr[Math.floor((iterations-1)*.5)],p90:arr[Math.floor((iterations-1)*.90)],min:arr[0],max:arr[arr.length-1]}}
function renderScenario(refreshControls=true){if(refreshControls){byId('presets').innerHTML=PRESETS.map(x=>`<button class="btn ${preset===x.name?'sel':''}" data-preset="${esc(x.name)}">${esc(x.name)}</button>`).join('');const controls=[['price','Contract price adjustment','%',-25,50],['prob','Incident probability change',' pp',-20,35],['severity','Conditional loss severity','%',-40,60]];byId('sliders').innerHTML=controls.map(([key,label,unit,min,max])=>`<div class="control"><div class="clabel"><label for="slide_${key}">${esc(label)}</label><output id="out_${key}">${scenario[key]>0?'+':''}${scenario[key]}${unit}</output></div><input id="slide_${key}" type="range" min="${min}" max="${max}" step="1" value="${scenario[key]}" data-slide="${key}" aria-label="${esc(label)}"></div>`).join('');}
const a=project({price:0,prob:0,severity:0}),b=project();byId('scenarioKpis').innerHTML=kpi('Modeled annual spend',fmt(b.committed),`Change: ${fmt(b.committed-a.committed)}`,'▤')+kpi('Modeled loss exposure',fmt(b.expected),`Change: ${fmt(b.expected-a.expected)}`,'⚑')+kpi('Total economic exposure',fmt(b.total),'Annual spend + expected loss','▦')+kpi('Spend change',`${a.committed?((b.committed/a.committed-1)*100).toFixed(1):'0.0'}%`,'Relative to baseline','↗');
byId('scenarioChart').innerHTML=svgBars([{label:'Baseline',amount:a.total},{label:'Scenario',amount:b.total}],'scenario')+'<p class="smallmuted">Comparison combines modeled annual spend and expected incident loss. These are different financial concepts and are shown together only for sensitivity analysis.</p>';
const sim=simulate();byId('simResults').innerHTML=`<div class="rowlist">${[['P10',sim.p10],['Median (P50)',sim.p50],['P90',sim.p90]].map(([name,v])=>`<div class="listrow"><div><b>${esc(name)}</b><small>Simulated incident-loss total</small></div><span class="value mono">${fmt(v)}</span></div>`).join('')}</div><p class="footnote">Fixed seed · 400 draws · 8 or fewer independent events in default data · No calibrated probabilities</p>`;
}
function contractRows(){const query=byId('search').value.trim().toLowerCase(),cat=byId('catFilter').value,status=byId('statusFilter').value;let items=state.items.filter(x=>(!query||(x.name+' '+x.party).toLowerCase().includes(query))&&(cat==='all'||x.category===cat)&&(status==='all'||(status==='active'?!x.archived:x.archived)));items.sort((a,b)=>a.renew.localeCompare(b.renew));byId('contractRows').innerHTML=items.map(x=>{const d=dates(x);return `<tr><td><b>${esc(x.name)}</b><small>${esc(x.party)} ${x.archived?'· archived':''}</small></td><td>${esc(x.category)}</td><td class="mono">${fmt(x.value)}</td><td class="mono">${dateLabel(x.renew)}</td><td><span class="badgetag ${deadlineClass(d.days)}">${x.notice} days</span></td><td class="mono">${fmt(riskFor(x))}</td><td><div style="display:flex;justify-content:flex-end;gap:6px"><button class="btn" data-edit="${x.id}">Edit</button><button class="btn ${x.archived?'ghost':'danger'}" data-archive="${x.id}">${x.archived?'Restore':'Archive'}</button></div></td></tr>`}).join('')||'<tr><td colspan="7" class="empty">No matching contracts.</td></tr>';byId('registryFoot').textContent=`Showing ${items.length} of ${state.items.length} records. Archived records stay in your backup.`}
function renewals(){const months=monthBuckets(active());byId('monthCalendar').innerHTML=`<div class="cal">${months.map(x=>`<div class="calmonth"><b>${esc(x.label)} ${esc(x.ym.slice(2,4))}</b><span class="calcount">${x.count}</span><small>${fmt(x.amount)}</small></div>`).join('')}</div>`;const sorted=active().map(x=>({...x,...dates(x)})).sort((a,b)=>a.deadline.localeCompare(b.deadline));byId('noticeRows').innerHTML=sorted.map(x=>`<tr><td><b>${esc(x.name)}</b><small>${esc(x.party)}</small></td><td class="mono">${dateLabel(x.deadline)}</td><td>${x.days<0?'−'+Math.abs(x.days):x.days} days</td><td class="mono">${dateLabel(x.renew)}</td><td class="mono">${fmt(x.value)}</td><td><span class="badgetag ${deadlineClass(x.days)}">${deadlineTag(x.days)}</span></td></tr>`).join('')||'<tr><td class="empty" colspan="6">No active contracts.</td></tr>'}
function update(){overview();renderScenario();contractRows();renewals()}
function setPage(p){if(!['overview','scenarios','registry','renewals','method'].includes(p))return;page=p;document.querySelectorAll('.page').forEach(el=>el.classList.toggle('active',el.id===p));document.querySelectorAll('[data-page]').forEach(el=>el.classList.toggle('active',el.dataset.page===p));byId('crumb').textContent=({overview:'OVERVIEW',scenarios:'SCENARIO STUDIO',registry:'CONTRACTS',renewals:'RENEWALS',method:'METHODOLOGY'})[p];if(p==='scenarios')renderScenario();if(p==='registry')contractRows();if(p==='renewals')renewals();window.scrollTo(0,0)}
function openModal(id=null){editing=id;const x=state.items.find(i=>i.id===id);byId('modalHeading').textContent=x?'Edit contract':'Add contract';const fields={fName:x?.name||'',fParty:x?.party||'',fCategory:x?.category||'Software',fValue:x?.value??75000,fRenew:x?.renew||'2027-04-15',fNotice:x?.notice??60,fRisk:x?.risk??12,fLoss:x?.loss??40000};Object.entries(fields).forEach(([k,v])=>byId(k).value=v);byId('modalWrap').classList.add('open');byId('fName').focus()}
function closeModal(){byId('modalWrap').classList.remove('open');editing=null}
function formData(){const n=k=>Number(byId(k).value);return{id:editing===null?state.nextId:editing,name:byId('fName').value.trim(),party:byId('fParty').value.trim(),category:byId('fCategory').value,value:n('fValue'),renew:byId('fRenew').value,notice:n('fNotice'),risk:n('fRisk'),loss:n('fLoss'),archived:editing===null?false:state.items.find(x=>x.id===editing)?.archived??false}}
function csvCell(v){let s=String(v??'');if(/^[\t\r\n\s]*[=+@-]/.test(s)&&!/^[-]\d+(\.\d+)?$/.test(s.trim()))s="'"+s;return '"'+s.replace(/"/g,'""')+'"'}
function download(name,txt,mime){const b=new Blob([txt],{type:mime});const url=URL.createObjectURL(b);const a=document.createElement('a');a.href=url;a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1000)}
function exportCsv(){const heads=['id','name','counterparty','category','annual_usd','renewal_date','notice_days','incident_probability_percent','incident_loss_usd','expected_loss_usd','archived'];const rows=state.items.map(x=>[x.id,x.name,x.party,x.category,x.value,x.renew,x.notice,x.risk,x.loss,riskFor(x),x.archived]);download('contractpilot-contracts.csv',[heads,...rows].map(a=>a.map(csvCell).join(',')).join('\r\n'),'text/csv;charset=utf-8');notify('CSV exported.')}function exportJson(){download('contractpilot-backup.json',JSON.stringify({kind:'ContractPilotBackup',version:1,...state},null,2),'application/json');notify('JSON backup exported.')}
function attach(){document.querySelectorAll('[data-page]').forEach(el=>el.addEventListener('click',()=>setPage(el.dataset.page)));document.querySelectorAll('[data-go]').forEach(el=>el.addEventListener('click',()=>setPage(el.dataset.go)));byId('themeBtn').addEventListener('click',()=>{document.body.classList.toggle('dark');try{localStorage.setItem(THEME,document.body.classList.contains('dark')?'dark':'light')}catch(e){};update()});byId('csvBtn').addEventListener('click',exportCsv);byId('jsonBtn').addEventListener('click',exportJson);
byId('addBtn').addEventListener('click',()=>{if(state.items.length>=150)return notify('Limit of 150 contracts reached.');openModal()});['search','catFilter','statusFilter'].forEach(k=>byId(k).addEventListener(k==='search'?'input':'change',contractRows));
byId('cancelModal').addEventListener('click',closeModal);byId('modalWrap').addEventListener('click',e=>{if(e.target.id==='modalWrap')closeModal()});document.addEventListener('keydown',e=>{if(e.key==='Escape')closeModal()});
byId('contractForm').addEventListener('submit',e=>{e.preventDefault();const x=formData();if(!validContract(x))return notify('Invalid contract input. Check all fields and limits.');if(editing===null){if(state.items.length>=150)return;state.items.push(x);state.nextId++}else{const ix=state.items.findIndex(r=>r.id===editing);if(ix<0)return;state.items[ix]=x}save();closeModal();update();notify('Contract saved locally.')});
byId('contractRows').addEventListener('click',e=>{const ed=e.target.closest('[data-edit]'),ar=e.target.closest('[data-archive]');if(ed){openModal(Number(ed.dataset.edit))}if(ar){const id=Number(ar.dataset.archive),x=state.items.find(r=>r.id===id);if(!x)return;x.archived=!x.archived;save();update();notify(x.archived?'Record archived, not deleted.':'Record restored.')}});
byId('presets').addEventListener('click',e=>{const b=e.target.closest('[data-preset]');if(!b)return;const a=PRESETS.find(x=>x.name===b.dataset.preset);if(!a)return;scenario={price:a.price,prob:a.prob,severity:a.severity};preset=a.name;renderScenario()});byId('sliders').addEventListener('input',e=>{if(!e.target.matches('[data-slide]'))return;const key=e.target.dataset.slide;if(!['price','prob','severity'].includes(key))return;scenario[key]=Number(e.target.value);preset='Custom';byId('out_'+key).textContent=(scenario[key]>0?'+':'')+scenario[key]+(key==='prob'?' pp':'%');document.querySelectorAll('[data-preset]').forEach(b=>b.classList.remove('sel'));renderScenario(false)});
byId('importBtn').addEventListener('click',()=>byId('fileInput').click());byId('fileInput').addEventListener('change',async e=>{const f=e.target.files?.[0];e.target.value='';if(!f)return;if(f.size>2*1024*1024){notify('Backup is too large (max 2 MB).');return}try{const x=JSON.parse(await f.text());if(!validBackup(x)){notify('Backup rejected: invalid schema or field values.');return}if(!confirm('Replace current local registry with the selected validated backup?'))return;state={items:clone(x.items),nextId:x.nextId};save();update();notify('Validated backup restored.')}catch(err){notify('Backup rejected: could not parse JSON.')}});
byId('resetBtn').addEventListener('click',()=>{if(!confirm('Replace ALL local records with eight fictional demo contracts? Export JSON first if needed.'))return;state={items:clone(DEFAULTS),nextId:9};save();update();notify('Demo restored.')});}
load();attach();update();
window.__contractPilotTest={cap,esc,validContract,validBackup,project,simulate,riskFor,dates,monthBuckets,seed,preview:()=>clone(state),setScenario:s=>{scenario=s;renderScenario()},version:1};
})();
</script></body></html>