Hugdrigo's picture
Upload 3 files
2886822 verified
Raw History Blame Contribute Delete
46.9 kB
<!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="#102b3c">
<meta name="description" content="ImpactLedger: a local-first, transparent program impact and cost-effectiveness simulation studio using fictional data.">
<title>ImpactLedger | Program Impact Intelligence Studio</title>
<style>
:root{color-scheme:light;--bg:#f3f7f9;--card:#ffffff;--card2:#f8fafb;--text:#203749;--muted:#728696;--line:#e1e8ed;--nav:#102b3c;--nav2:#1d4153;--blue:#3772d0;--blueSoft:#e9f1fc;--sea:#118b83;--seaSoft:#e2f5f0;--amber:#a66f26;--red:#c25458;--shadow:0 8px 30px rgba(25,61,82,.045)}
body.dark{color-scheme:dark;--bg:#101e2a;--card:#182c3b;--card2:#213748;--text:#e7f2f7;--muted:#9db4c2;--line:#344f5d;--nav:#091a27;--nav2:#203d4e;--blue:#9ec4ff;--blueSoft:#273f5b;--sea:#65dac7;--seaSoft:#244a4d;--amber:#f1bd7d;--red:#ffa1a4;--shadow:none}
*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--bg);color:var(--text);font:13px/1.55 system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}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 #62b0ff;outline-offset:2px}.shell{min-height:100vh;display:grid;grid-template-columns:248px minmax(0,1fr)}.side{position:sticky;top:0;min-height:100vh;height:100vh;background:var(--nav);color:#acc4ce;padding:26px 17px 18px;display:flex;flex-direction:column}.identity{display:flex;gap:12px;align-items:center;padding:2px 7px;margin-bottom:38px}.emblem{width:45px;height:45px;background:linear-gradient(140deg,#9ef2d6,#79c4f5);color:#0c3544;border-radius:14px;display:grid;place-items:center;font-size:28px;font-weight:900;box-shadow:0 8px 22px #72dbc831}.identity b{color:#fff;font-size:21px;letter-spacing:-1.2px;display:block;line-height:1.05}.identity small{font-size:9px;font-weight:800;letter-spacing:1.65px;color:#91b8c8}.nav-title{font-size:10px;color:#769cad;letter-spacing:2px;font-weight:850;margin:0 10px 11px}.nav{display:grid;gap:5px}.nav button{width:100%;display:flex;align-items:center;gap:12px;padding:12px 13px;border:0;background:none;border-radius:11px;color:#b0cad4;text-align:left;font-size:12px;font-weight:750}.nav .glyph{font-size:17px;width:24px;text-align:center}.nav button.active,.nav button:hover{color:#fff;background:#25495c}.nav button.active{box-shadow:inset 3px 0 #72dec7}.side-end{margin-top:auto;padding:14px;border-radius:13px;background:#204154;font-size:11px;line-height:1.7}.side-end b{display:block;color:#fff;margin-bottom:3px}.online{height:8px;width:8px;display:inline-block;background:#7ff2cb;border-radius:50%;margin-right:7px}
main{min-width:0;max-width:1610px;width:100%;padding:24px clamp(17px,3vw,45px) 26px}.top{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:26px}.crumb{font-size:11px;font-weight:750;color:var(--muted)}.crumb strong{color:var(--text)}.actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.btn{background:var(--card);border:1px solid var(--line);color:var(--text);padding:10px 13px;white-space:nowrap;border-radius:10px;display:inline-flex;align-items:center;gap:7px;font-size:11px;font-weight:800}.btn:hover{filter:brightness(.96)}.btn.primary{background:#3179c5;border-color:#3179c5;color:#fff}.btn.subtle{background:var(--blueSoft);color:var(--blue);border-color:transparent}.btn.danger{color:var(--red)}.head{display:flex;justify-content:space-between;align-items:center;gap:15px;margin-bottom:22px}.eyebrow{color:var(--sea);font-size:10px;letter-spacing:2px;font-weight:900}.head h1{font-size:clamp(27px,3.2vw,39px);letter-spacing:-1.35px;margin:5px 0;line-height:1.15}.head p{max-width:790px;color:var(--muted);font-size:12px;margin:5px 0 0}.head-icon{height:65px;width:65px;border-radius:18px;background:var(--seaSoft);color:var(--sea);font-size:36px;display:grid;place-items:center}.hero{position:relative;overflow:hidden;border-radius:19px;background:linear-gradient(115deg,#173449 0%,#1b5870 57%,#16877a 100%);padding:27px 31px;color:white;margin-bottom:16px;min-height:164px;display:flex;align-items:center;justify-content:space-between}.hero:after{content:'';position:absolute;right:5%;top:-100px;height:355px;width:355px;border-radius:50%;border:1px solid #b2eff646;box-shadow:0 0 0 36px #b2eff610,0 0 0 75px #b2eff608;pointer-events:none}.hero b{font-size:10px;letter-spacing:2px;color:#a6f4e2}.hero h2{font-size:clamp(22px,3vw,34px);line-height:1.13;letter-spacing:-.95px;margin:11px 0}.hero p{font-size:12px;color:#d5e9ef;max-width:640px;margin:0}.hero-mark{font-size:83px;opacity:.55;margin-right:65px;position:relative;z-index:1}.grid4{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin-bottom:16px}.stat,.panel{min-width:0;border-radius:15px;background:var(--card);border:1px solid var(--line);box-shadow:var(--shadow)}.stat{padding:18px}.stat .label{font-weight:850;letter-spacing:.8px;text-transform:uppercase;font-size:10px;color:var(--muted);display:flex;justify-content:space-between;gap:6px}.stat .label i{font-style:normal;background:var(--blueSoft);border-radius:8px;padding:5px;color:var(--blue);font-size:15px}.stat strong{display:block;font-size:clamp(21px,2.6vw,33px);margin:8px 0 1px;letter-spacing:-1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-variant-numeric:tabular-nums}.stat small{color:var(--muted);font-size:10px}.columns{display:grid;grid-template-columns:minmax(0,1.45fr) minmax(0,1fr);gap:16px}.columns.equal{grid-template-columns:repeat(2,minmax(0,1fr))}.panel{padding:20px;margin-bottom:16px}.panel h3{font-size:15px;letter-spacing:-.3px;margin:0}.panel p.desc{font-size:11px;color:var(--muted);margin:4px 0 16px}.panel-title{display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:15px}.panel-title p{font-size:11px;color:var(--muted);margin:3px 0 0}.tag{font-size:10px;font-weight:800;padding:5px 9px;background:var(--seaSoft);color:var(--sea);border-radius:30px;white-space:nowrap}.muted{color:var(--muted)}.table-wrap{max-width:100%;overflow-x:auto}table{border-collapse:collapse;width:100%;font-size:11px;white-space:nowrap}th{text-align:left;color:var(--muted);font-size:9px;letter-spacing:.7px;text-transform:uppercase;background:var(--card2);font-weight:850}th,td{padding:12px 11px;border-bottom:1px solid var(--line)}tr:last-child td{border:0}th.right,td.right{text-align:right}td strong{font-size:11px}.tiny{font-size:10px}.positive{color:var(--sea);font-weight:850}.negative{color:var(--red);font-weight:850}.bar-row{margin:0 0 16px}.bar-heading{display:flex;justify-content:space-between;font-size:11px;gap:12px;margin-bottom:7px}.bar-heading b{font-size:11px}.track{background:var(--card2);height:11px;border-radius:15px;overflow:hidden}.track span{height:100%;display:block;background:linear-gradient(90deg,#52baa7,#3482c2);border-radius:inherit;min-width:0}.progress{display:flex;gap:8px;align-items:center;font-size:10px;color:var(--muted)}.quality{display:inline-flex;padding:3px 8px;border-radius:20px;background:var(--blueSoft);color:var(--blue);font-weight:850}.note{border:1px solid var(--line);background:var(--card2);border-radius:12px;padding:15px;margin:3px 0 16px;color:var(--muted);font-size:11px}.note b{color:var(--text)}.hint{font-size:10px;color:var(--muted)}.view{display:none}.view.active{display:block}.filters{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:14px}.filters .actions{flex-wrap:wrap}.input{background:var(--card);border:1px solid var(--line);border-radius:9px;color:var(--text);padding:10px 11px;max-width:100%;min-width:0}.search{width:220px}.form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:15px}.field{min-width:0}.field label{font-size:10px;color:var(--muted);font-weight:800;display:block;margin-bottom:7px}.field input,.field select{width:100%;padding:11px;background:var(--card);border:1px solid var(--line);border-radius:9px;color:var(--text)}.field input[type=range]{padding:0;accent-color:var(--sea)}.fields3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}.scenario-buttons{display:flex;gap:7px;flex-wrap:wrap;margin-bottom:18px}.scenario-buttons button{padding:10px 12px;border:1px solid var(--line);border-radius:9px;background:var(--card);color:var(--text);font-weight:750;font-size:11px}.scenario-buttons button.selected{border-color:#62b7a8;color:var(--sea);background:var(--seaSoft)}.slider{margin:0 0 16px}.slider-top{display:flex;justify-content:space-between;gap:12px;font-size:11px;font-weight:700;margin-bottom:9px}.slider output{color:var(--sea);font-weight:900}.slider input{width:100%;accent-color:var(--sea)}.chart{width:100%;height:220px;overflow:hidden}.chart svg{width:100%;height:100%}.legend{display:flex;flex-wrap:wrap;gap:14px;font-size:10px;color:var(--muted);margin:8px 0}.legend i{display:inline-block;width:9px;height:9px;border-radius:50%;background:#1e9a93;margin-right:5px}.two-cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:11px}.mini{padding:13px;background:var(--card2);border:1px solid var(--line);border-radius:11px}.mini span{display:block;color:var(--muted);font-size:10px}.mini b{font-size:22px;letter-spacing:-.7px}.step{display:flex;gap:13px;margin-bottom:18px}.step .badge{background:var(--blueSoft);color:var(--blue);font-weight:900;border-radius:11px;width:31px;height:31px;flex-shrink:0;display:grid;place-items:center}.step b{font-size:12px}.step p{margin:3px 0 0;color:var(--muted);font-size:11px}.rowactions{display:flex;gap:5px}.rowactions button{font-size:10px;padding:6px 7px;background:var(--card2);color:var(--text);border:1px solid var(--line);border-radius:7px}.toast{min-height:17px;font-size:11px;color:var(--sea);font-weight:800;margin-bottom:8px}.footer{display:flex;flex-wrap:wrap;justify-content:space-between;gap:10px;color:var(--muted);font-size:10px;padding:12px 0}.empty{padding:25px;text-align:center;color:var(--muted)}.modaloverlay{position:fixed;inset:0;z-index:15;background:#061d2aca;display:none;align-items:center;justify-content:center;padding:20px;overflow:auto}.modaloverlay.show{display:flex}.modal{width:min(660px,100%);max-height:min(94vh,850px);overflow:auto;background:var(--card);color:var(--text);padding:24px;border-radius:18px;box-shadow:0 20px 70px #00172266}.modal h2{font-size:23px;margin:0 0 5px}.modal p{font-size:11px;color:var(--muted);margin:0 0 18px}.modal .form-grid{gap:13px}.modal .form-grid .field input,.modal .form-grid .field select{padding:9px}.modal-bottom{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}.hidden{display:none!important}
@media(max-width:1200px){.grid4{grid-template-columns:repeat(2,minmax(0,1fr))}.hero-mark{margin-right:14px}}@media(max-width:950px){.shell{grid-template-columns:minmax(0,1fr)}.side{height:auto;min-height:0;min-width:0;position:relative;display:block;padding:15px}.identity{margin:0 0 15px}.nav-title,.side-end{display:none}.nav{display:flex;overflow-x:auto;gap:5px}.nav button{width:auto;flex-shrink:0;padding:10px}.nav .glyph{width:auto}main{padding:18px}.columns,.columns.equal{grid-template-columns:1fr}}@media(max-width:560px){.chart{overflow-x:auto}.chart svg{width:680px;min-width:680px;height:100%}.top{flex-direction:column;align-items:stretch}.top .actions{width:100%}.grid4,.fields3{grid-template-columns:repeat(2,minmax(0,1fr))}.top{align-items:flex-start}.top .actions .btn{padding:8px}.crumb{max-width:175px}.head-icon,.hero-mark{display:none}.hero{padding:23px;min-height:145px}.panel{padding:15px}.stat{padding:14px}.stat strong{font-size:23px}.form-grid{grid-template-columns:1fr}.search{width:165px}.mini b{font-size:18px}.head h1{font-size:29px}}@media(max-width:375px){.grid4{grid-template-columns:1fr 1fr;gap:8px}.stat strong{font-size:19px}.stat .label{font-size:9px}.two-cards{grid-template-columns:1fr}.crumb{max-width:125px}}
</style>
</head>
<body>
<div class="shell">
<aside class="side"><div class="identity"><div class="emblem">◎</div><div><b>ImpactLedger</b><small>INTELLIGENCE STUDIO</small></div></div><div class="nav-title">WORKSPACE</div><nav class="nav" aria-label="Main navigation">
<button class="active" data-view="overview"><span class="glyph">▣</span> Impact overview</button><button data-view="studio"><span class="glyph">◉</span> Scenario studio</button><button data-view="programs"><span class="glyph">▤</span> Program registry</button><button data-view="uncertainty"><span class="glyph">◇</span> Uncertainty lab</button><button data-view="method"><span class="glyph">☷</span> Methodology</button></nav><div class="side-end"><b><span class="online"></span> Local-first workspace</b>Fictional impact programs<br>Transparent assumptions<br>No paid APIs or server</div></aside>
<main><div class="top"><div class="crumb">HUGDRIGO / PORTFOLIO / <strong>#106 IMPACTLEDGER</strong></div><div class="actions"><button class="btn" id="themeBtn" title="Toggle theme">◐ Theme</button><button class="btn" id="backupBtn">⇩ Backup JSON</button><button class="btn primary" id="createBtn">+ New program</button></div></div>
<section id="overview" class="view active"><header class="head"><div><div class="eyebrow">PROGRAM EFFECTIVENESS / IMPACT ACCOUNTING</div><h1>Impact overview</h1><p>Bring budgets, reach, measured outcomes and attribution assumptions into one transparent decision workspace.</p></div><div class="head-icon">◎</div></header><div class="hero"><div><b>PORTFOLIO INTELLIGENCE / PROJECT 106</b><h2>Measure what matters.<br>Explain what you assume.</h2><p>Compare program effectiveness without presenting modeled outcomes as verified causal impact.</p></div><div class="hero-mark">◈</div></div><div class="grid4" id="overviewKpis"></div><div class="columns"><section class="panel"><div class="panel-title"><div><h3>Incremental outcomes by program</h3><p>Illustrative incremental outcomes; negative results are retained</p></div><span class="tag">Attribution-adjusted</span></div><div id="impactChart" class="chart"></div><div class="legend"><span><i></i>Positive incremental outcomes</span><span><i style="background:#e99b73"></i>Negative outcomes</span></div></section><section class="panel"><div class="panel-title"><div><h3>Budget allocation</h3><p>Top active programs by assumed total budget</p></div></div><div id="budgetBars"></div></section></div><div class="columns"><section class="panel"><div class="panel-title"><div><h3>Program effectiveness</h3><p>Illustrative cost and incremental outcome comparisons</p></div><span class="tag">No causal proof</span></div><div class="table-wrap"><table><thead><tr><th>Program</th><th class="right">Budget</th><th class="right">Reach</th><th class="right">Incremental</th><th class="right">Cost / outcome</th></tr></thead><tbody id="overviewRows"></tbody></table></div></section><section class="panel"><div class="panel-title"><div><h3>Interpretation guide</h3><p>What the portfolio numbers can and cannot tell you</p></div></div><div class="step"><span class="badge">1</span><div><b>Measured change ≠ causal impact</b><p>Before-and-after differences may reflect confounding, selection and other factors.</p></div></div><div class="step"><span class="badge">2</span><div><b>Attribution is an assumption</b><p>A percentage chosen by the user is not established by this simulator.</p></div></div><div class="step"><span class="badge">3</span><div><b>Negative impacts stay visible</b><p>Harmful or deteriorating results are not concealed in aggregates.</p></div></div></section></div></section>
<section id="studio" class="view"><header class="head"><div><div class="eyebrow">INTERACTIVE WHAT-IF ANALYSIS</div><h1>Scenario studio</h1><p>Stress-test how program coverage, measured change, costs and assumed attribution affect impact efficiency.</p></div><div class="head-icon">◉</div></header><div class="scenario-buttons" id="presets"></div><div class="columns"><section class="panel"><div class="panel-title"><div><h3>Scenario controls</h3><p>Changes apply to all active programs; original data remains unchanged</p></div><span class="tag">Illustrative</span></div><div class="slider"><div class="slider-top"><span>Reach adjustment</span><output id="reachOut"></output></div><input type="range" id="reachSlider" min="-60" max="100" step="5"></div><div class="slider"><div class="slider-top"><span>Follow-up outcome rate adjustment</span><output id="liftOut"></output></div><input type="range" id="liftSlider" min="-20" max="20" step="1"></div><div class="slider"><div class="slider-top"><span>Budget adjustment</span><output id="costOut"></output></div><input type="range" id="costSlider" min="-40" max="100" step="5"></div><div class="slider"><div class="slider-top"><span>Attribution assumption adjustment</span><output id="attribOut"></output></div><input type="range" id="attribSlider" min="-40" max="40" step="5"></div><div class="note"><b>Assumption discipline:</b> Changing attribution does not establish causal effects. Rates are clamped to 0–100%; reach and budget cannot become negative.</div></section><section class="panel"><div class="panel-title"><div><h3>Scenario outcomes</h3><p>Relative to the unchanged reference portfolio</p></div></div><div class="two-cards" id="scenarioCards"></div><div class="panel-title" style="margin-top:20px"><div><h3>Net incremental outcomes</h3><p>Baseline vs selected scenario</p></div></div><div id="compareChart" class="chart" style="height:190px"></div></section></div><section class="panel"><div class="panel-title"><div><h3>Detailed comparison</h3><p>Negative incremental results are possible and shown as negative values</p></div></div><div class="table-wrap"><table><thead><tr><th>Program</th><th class="right">Scenario reach</th><th class="right">Budget</th><th class="right">Incremental outcomes</th><th class="right">Cost per positive outcome</th></tr></thead><tbody id="scenarioRows"></tbody></table></div></section></section>
<section id="programs" class="view"><header class="head"><div><div class="eyebrow">MANAGE / COMPARE / PRESERVE</div><h1>Program registry</h1><p>Edit impact programs and assumptions. Archiving keeps each record in your backup rather than deleting it.</p></div><div class="head-icon">▤</div></header><div id="registryKpis" class="grid4"></div><section class="panel"><div class="filters"><div class="actions"><input class="input search" id="search" type="search" placeholder="Search programs..." aria-label="Search programs"><select class="input" id="registryFilter" aria-label="Registry filter"><option value="active">Active</option><option value="archived">Archived</option><option value="all">All programs</option></select></div><div class="actions"><button class="btn" id="csvBtn">⇩ Export CSV</button><button class="btn" id="importBtn">⇧ Restore JSON</button><button class="btn danger" id="resetBtn">↺ Reset demo</button></div></div><div id="registryStatus" class="toast" role="status" aria-live="polite"></div><div class="table-wrap"><table><thead><tr><th>Program</th><th>Sector</th><th class="right">Budget ($)</th><th class="right">Reach</th><th class="right">Pre / Post</th><th class="right">Attribution</th><th class="right">Evidence</th><th>Actions</th></tr></thead><tbody id="registryRows"></tbody></table></div></section><div class="note"><b>Data ownership.</b> Your changes are stored in this browser if local storage is available. Export a JSON backup before resetting the demo or switching devices. No data leaves your browser.</div></section>
<section id="uncertainty" class="view"><header class="head"><div><div class="eyebrow">UNCERTAINTY / SENSITIVITY / PRIORITIES</div><h1>Uncertainty lab</h1><p>Reproducible illustrative Monte Carlo draws reveal how sensitive modeled outcomes are to assumptions.</p></div><div class="head-icon">◇</div></header><div id="riskKpis" class="grid4"></div><div class="columns"><section class="panel"><div class="panel-title"><div><h3>Uncertainty distribution</h3><p>400 deterministic draws · illustrative net incremental outcomes</p></div><span class="tag">Seeded</span></div><div id="riskChart" class="chart" style="height:260px"></div><div id="riskPercentiles" class="two-cards"></div></section><section class="panel"><div class="panel-title"><div><h3>Priorities &amp; limitations</h3><p>Signals to investigate before reporting real impact</p></div></div><div id="watchlist"></div></section></div><div class="columns equal"><section class="panel"><div class="panel-title"><div><h3>Program outcome sensitivity</h3><p>Assumed incremental outcomes under ±10pp attribution</p></div></div><div id="sensitivity"></div></section><section class="panel"><div class="panel-title"><div><h3>How draws are constructed</h3></div></div><div class="step"><span class="badge">1</span><div><b>Reach shocks</b><p>Each draw changes projected participants around the current scenario using bounded multiplicative shocks.</p></div></div><div class="step"><span class="badge">2</span><div><b>Measured change shocks</b><p>Follow-up outcome rates receive bounded synthetic variation, with valid rate limits.</p></div></div><div class="step"><span class="badge">3</span><div><b>Attribution shocks</b><p>Attribution assumptions fluctuate, but draws are not calibrated uncertainty intervals.</p></div></div></section></div><div class="note"><b>Important:</b> The 10th/50th/90th percentiles are distributions of invented assumptions, not inferential confidence or credible intervals and not proof of impact.</div></section>
<section id="method" class="view"><header class="head"><div><div class="eyebrow">REPRODUCIBLE / TRANSPARENT / NO EXTERNAL SERVICES</div><h1>Methodology</h1><p>Inspect the exact logic and limitations behind each score. A simulation does not substitute a real evaluation design.</p></div><div class="head-icon">☷</div></header><div class="columns equal"><section class="panel"><div class="panel-title"><div><h3>Impact equations</h3></div></div><div class="step"><span class="badge">1</span><div><b>Observed outcome rate difference</b><p>Change = follow-up outcome rate − baseline outcome rate, expressed in percentage points.</p></div></div><div class="step"><span class="badge">2</span><div><b>Attributed incremental outcomes</b><p>Incremental = reach × (follow-up − baseline)/100 × assumed attribution/100. Negative outcomes are preserved.</p></div></div><div class="step"><span class="badge">3</span><div><b>Cost effectiveness</b><p>Cost per positive incremental outcome = budget ÷ positive incremental outcomes. Undefined when net additional outcomes ≤ 0.</p></div></div><div class="step"><span class="badge">4</span><div><b>Portfolio accumulation</b><p>Aggregate budgets, reach and signed incremental outcomes. Programs are assumed non-overlapping for summary purposes.</p></div></div></section><section class="panel"><div class="panel-title"><div><h3>Scope boundaries</h3></div></div><div class="step"><span class="badge">!</span><div><b>Not causal inference</b><p>No randomized design, control group adjustment, confounder identification or counterfactual estimation.</p></div></div><div class="step"><span class="badge">!</span><div><b>Do not claim verified social return</b><p>Different outcome types should not be treated as inherently comparable; the demo assumes standardized outcome units.</p></div></div><div class="step"><span class="badge">!</span><div><b>No privacy certification</b><p>Browser local storage is not encrypted. Use fictional or non-sensitive data.</p></div></div><div class="step"><span class="badge">✓</span><div><b>Completely offline</b><p>HTML, CSS and JavaScript. No API keys, analytics, paid services, external scripts or network requests.</p></div></div></section></div><div class="panel"><div class="panel-title"><div><h3>Demo program catalog</h3><p>All assumptions are editable; no figures represent real-world research findings</p></div></div><div id="methodTable" class="table-wrap"></div></div><div class="note"><b>License:</b> MIT · 2026 Hugdrigo · Project #106. Educational program evaluation simulator only.</div></section>
<footer class="footer"><span>© 2026 ImpactLedger · Hugdrigo · Project #106</span><span>Fictional demonstration · Transparent formulas · Local-first</span></footer>
</main></div>
<div id="modalOverlay" class="modaloverlay" role="presentation"><div class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle"><h2 id="modalTitle">New program</h2><p>Use fictional demonstration data. Changes are stored locally when allowed.</p><form id="programForm"><div class="form-grid">
<div class="field"><label for="fName">Program name</label><input id="fName" maxlength="60" required placeholder="e.g. Mentorship pilot"></div><div class="field"><label for="fSector">Sector</label><select id="fSector"><option>Education</option><option>Health</option><option>Employment</option><option>Climate</option><option>Inclusion</option><option>Community</option></select></div>
<div class="field"><label for="fBudget">Total program budget (USD)</label><input id="fBudget" type="number" min="0" max="100000000" step="0.01" required></div><div class="field"><label for="fReach">Participants reached</label><input id="fReach" type="number" min="0" max="10000000" step="1" required></div>
<div class="field"><label for="fPre">Baseline outcome rate (%)</label><input id="fPre" type="number" min="0" max="100" step="0.1" required></div><div class="field"><label for="fPost">Follow-up outcome rate (%)</label><input id="fPost" type="number" min="0" max="100" step="0.1" required></div>
<div class="field"><label for="fAttrib">Assumed attribution (%)</label><input id="fAttrib" type="number" min="0" max="100" step="0.1" required></div><div class="field"><label for="fEvidence">Evidence strength (1–5, subjective)</label><select id="fEvidence"><option value="1">1 - exploratory</option><option value="2">2 - limited</option><option value="3">3 - moderate</option><option value="4">4 - stronger</option><option value="5">5 - highest demo tier</option></select></div>
</div><div class="modal-bottom"><button class="btn" id="cancelBtn" type="button">Cancel</button><button class="btn primary" type="submit">Save program</button></div></form></div></div>
<input type="file" id="fileInput" accept=".json,application/json" hidden>
<script>
(()=>{'use strict';
const $=id=>document.getElementById(id), storageKey='hugdrigo-impactledger106-v1', sectorOptions=['Education','Health','Employment','Climate','Inclusion','Community'];
const demo=[
{id:1,name:'Learning Bridge',sector:'Education',budget:168000,reach:2450,pre:38,post:63,attrib:62,evidence:3,archived:false},
{id:2,name:'Healthy Start',sector:'Health',budget:213000,reach:3200,pre:51,post:69,attrib:57,evidence:4,archived:false},
{id:3,name:'Career Launch',sector:'Employment',budget:184500,reach:1680,pre:32,post:57,attrib:71,evidence:3,archived:false},
{id:4,name:'Clean Energy Homes',sector:'Climate',budget:289000,reach:1120,pre:26,post:48,attrib:52,evidence:2,archived:false},
{id:5,name:'Digital Access',sector:'Inclusion',budget:94000,reach:2100,pre:45,post:61,attrib:43,evidence:3,archived:false},
{id:6,name:'Community Connect',sector:'Community',budget:128000,reach:1820,pre:58,post:54,attrib:55,evidence:2,archived:false},
{id:7,name:'Skills Forward',sector:'Employment',budget:152000,reach:1340,pre:39,post:72,attrib:65,evidence:4,archived:false},
{id:8,name:'Green Futures',sector:'Climate',budget:115000,reach:980,pre:22,post:44,attrib:47,evidence:2,archived:false}];
const presets={baseline:{label:'Baseline',reach:0,lift:0,cost:0,attrib:0},tight:{label:'Funding squeeze',reach:-15,lift:-3,cost:-20,attrib:-5},accelerate:{label:'Delivery boost',reach:35,lift:6,cost:20,attrib:5},stress:{label:'Measurement stress',reach:-10,lift:-8,cost:15,attrib:-20}};
const clone=x=>JSON.parse(JSON.stringify(x)), clamp=(x,min,max)=>Math.max(min,Math.min(max,x)), esc=x=>String(x).replace(/[&<>"']/g,ch=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[ch]));
const money=x=>Number(x).toLocaleString('en-US',{style:'currency',currency:'USD',maximumFractionDigits:0}),num=x=>Number(x).toLocaleString('en-US',{maximumFractionDigits:1}),compact=x=>Number(x).toLocaleString('en-US',{notation:'compact',maximumFractionDigits:1}),signed=x=>(x>0?'+':'')+num(x), pct=x=>num(x)+'%';
let state={programs:clone(demo),scenario:{reach:0,lift:0,cost:0,attrib:0},theme:'light'}, editing=null, active='overview';
function validProgram(p){return p&&typeof p==='object'&&Number.isSafeInteger(p.id)&&p.id>=1&&p.id<=1e9&&typeof p.name==='string'&&p.name.trim().length>0&&p.name.length<=60&&sectorOptions.includes(p.sector)&&[['budget',0,1e8],['reach',0,1e7],['pre',0,100],['post',0,100],['attrib',0,100],['evidence',1,5]].every(([key,a,b])=>typeof p[key]==='number'&&Number.isFinite(p[key])&&p[key]>=a&&p[key]<=b)&&(Number.isInteger(p.reach)&&Number.isInteger(p.evidence))&&typeof p.archived==='boolean'}
function validBackup(d){return d&&d.format==='ImpactLedger-106'&&d.version===1&&Array.isArray(d.programs)&&d.programs.length<=120&&d.programs.every(validProgram)&&new Set(d.programs.map(p=>p.id)).size===d.programs.length&&d.scenario&&[['reach',-60,100],['lift',-20,20],['cost',-40,100],['attrib',-40,40]].every(([k,lo,hi])=>Number.isFinite(d.scenario[k])&&d.scenario[k]>=lo&&d.scenario[k]<=hi)&&['light','dark'].includes(d.theme)}
try{const loaded=JSON.parse(localStorage.getItem(storageKey)||'null');if(validBackup(loaded))state={programs:loaded.programs,scenario:loaded.scenario,theme:loaded.theme}}catch(_){}
function save(){try{localStorage.setItem(storageKey,JSON.stringify({format:'ImpactLedger-106',version:1,...state}))}catch(_){}}
function activePrograms(){return state.programs.filter(p=>!p.archived)}
function projected(p,s={reach:0,lift:0,cost:0,attrib:0}){const reach=p.reach*(1+s.reach/100),budget=p.budget*(1+s.cost/100),post=clamp(p.post+s.lift,0,100),attrib=clamp(p.attrib+s.attrib,0,100),delta=post-p.pre,incremental=reach*delta/100*attrib/100;return {...p,reach,budget,post,attrib,delta,incremental,unitCost:incremental>0?budget/incremental:null}}
function portfolio(items){return items.reduce((s,p)=>(s.budget+=p.budget,s.reach+=p.reach,s.incremental+=p.incremental,s.count++,s.weightEvidence+=p.evidence*p.reach,s),{budget:0,reach:0,incremental:0,count:0,weightEvidence:0})}
function current(){return activePrograms().map(p=>projected(p,state.scenario))}
function baseline(){return activePrograms().map(p=>projected(p))}
function stat(label,val,caption,ico){return `<div class="stat"><div class="label">${esc(label)}<i>${ico}</i></div><strong title="${esc(val)}">${esc(val)}</strong><small>${esc(caption)}</small></div>`}
function outcome(val){return `<span class="${val<0?'negative':'positive'}">${esc(signed(val))}</span>`}
function unitCost(p){return p.unitCost===null?'N/A':money(p.unitCost)}
function svgBars(data){const w=760,h=216,ml=40,mr=14,top=22,bottom=45,plot=h-top-bottom; if(!data.length)return '<div class="empty">No active programs</div>';const max=Math.max(1,...data.map(x=>Math.abs(x.incremental)))*1.1;const zero=plot/2+top;const space=(w-ml-mr)/data.length,bw=Math.min(60,space*.58);let out=`<svg viewBox="0 0 ${w} ${h}" role="img" aria-label="Signed attributed incremental outcomes chart">`;
for(let g=-2;g<=2;g++){const y=zero-g*plot/4;out+=`<line x1="${ml}" y1="${y}" x2="${w-mr}" y2="${y}" stroke="${g===0?'#9bb2bd':'#cfdae0'}" stroke-dasharray="${g===0?'0':'3 5'}" opacity=".7"/><text x="${ml-7}" y="${y+3}" text-anchor="end" fill="#94a6b1" font-size="9">${g===0?'0':num(g*max/2)}</text>`}
data.forEach((x,i)=>{const height=Math.abs(x.incremental)/max*plot/2,cx=ml+space*(i+.5),y=x.incremental>=0?zero-height:zero;out+=`<rect x="${cx-bw/2}" y="${y}" width="${bw}" height="${Math.max(1,height)}" rx="5" fill="${x.incremental>=0?'#26a99e':'#e9a279'}"><title>${esc(x.name)}: ${signed(x.incremental)}</title></rect><text x="${cx}" y="${h-bottom+20}" fill="#8a9ca8" text-anchor="middle" font-size="9">${esc(x.name.split(' ')[0].slice(0,13))}</text>`});return out+'</svg>'}
function renderOverview(){const data=baseline(),tot=portfolio(data),avg=tot.reach?tot.weightEvidence/tot.reach:0;$('overviewKpis').innerHTML=stat('Total program budget',money(tot.budget),'Assumed one-cycle spending','$')+stat('Participants reached',compact(tot.reach),'May overlap across programs','♙')+stat('Net incremental',signed(tot.incremental),'Assumption-adjusted outcome units','↗')+stat('Cost / net outcome',tot.incremental>0?money(tot.budget/tot.incremental):'N/A',`Evidence tier ${avg?avg.toFixed(1):'0.0'} / 5`,'◎');$('impactChart').innerHTML=svgBars(data);let sorted=data.slice().sort((a,b)=>b.budget-a.budget),max=Math.max(1,...sorted.map(p=>p.budget));$('budgetBars').innerHTML=sorted.slice(0,6).map(p=>`<div class="bar-row"><div class="bar-heading"><b>${esc(p.name)}</b><span>${money(p.budget)}</span></div><div class="track"><span style="width:${100*p.budget/max}%"></span></div></div>`).join('')||'<div class="empty">No active programs</div>';$('overviewRows').innerHTML=sorted.map(p=>`<tr><td><strong>${esc(p.name)}</strong><div class="hint">${esc(p.sector)}</div></td><td class="right">${money(p.budget)}</td><td class="right">${num(p.reach)}</td><td class="right">${outcome(p.incremental)}</td><td class="right">${unitCost(p)}</td></tr>`).join('')||'<tr><td colspan="5" class="empty">No active programs</td></tr>'}
function setScenario(values){state.scenario={reach:values.reach,lift:values.lift,cost:values.cost,attrib:values.attrib};save();renderScenario();renderRisk()}
function presetName(){return Object.keys(presets).find(k=>['reach','lift','cost','attrib'].every(f=>presets[k][f]===state.scenario[f]))||'custom'}
function renderScenario(){const p=presetName();$('presets').innerHTML=Object.entries(presets).map(([key,s])=>`<button type="button" class="${p===key?'selected':''}" data-preset="${key}">${esc(s.label)}</button>`).join('')+`<button type="button" class="${p==='custom'?'selected':''}" disabled>Custom settings</button>`;
for(const [k,id,txt] of [['reach','reachSlider','%'],['lift','liftSlider','pp'],['cost','costSlider','%'],['attrib','attribSlider','pp']]){$(id).value=state.scenario[k];$(k==='lift'?'liftOut':k==='attrib'?'attribOut':k==='reach'?'reachOut':'costOut').textContent=(state.scenario[k]>0?'+':'')+state.scenario[k]+txt}
const base=portfolio(baseline()),scenario=current(),tot=portfolio(scenario);const diff=tot.incremental-base.incremental;$('scenarioCards').innerHTML=`<div class="mini"><span>Adjusted budget</span><b>${money(tot.budget)}</b></div><div class="mini"><span>Participants reached</span><b>${compact(tot.reach)}</b></div><div class="mini"><span>Incremental outcomes</span><b>${signed(tot.incremental)}</b></div><div class="mini"><span>Change from baseline</span><b class="${diff<0?'negative':'positive'}">${signed(diff)}</b></div>`;
let vals=[{name:'Reference',incremental:base.incremental},{name:'Scenario',incremental:tot.incremental}];$('compareChart').innerHTML=svgBars(vals);$('scenarioRows').innerHTML=scenario.map(p=>`<tr><td><strong>${esc(p.name)}</strong></td><td class="right">${num(p.reach)}</td><td class="right">${money(p.budget)}</td><td class="right">${outcome(p.incremental)}</td><td class="right">${unitCost(p)}</td></tr>`).join('')||'<tr><td colspan="5" class="empty">No active programs</td></tr>'}
function renderRegistry(){const all=state.programs,act=activePrograms(),tot=portfolio(act.map(p=>projected(p))),arch=all.length-act.length;$('registryKpis').innerHTML=stat('Active programs',String(act.length),'Currently included in portfolio','▤')+stat('Archived programs',String(arch),'Retained in backups','▣')+stat('Combined budget',money(tot.budget),'Active program assumptions','$')+stat('Evidence level',tot.reach?num(tot.weightEvidence/tot.reach)+'/5':'N/A','Reach-weighted subjective tier','◎');const query=$('search').value.toLowerCase().trim(),filter=$('registryFilter').value;let show=all.filter(x=>(filter==='all'||(filter==='active'?!x.archived:x.archived))&&(x.name.toLowerCase().includes(query)||x.sector.toLowerCase().includes(query)));$('registryRows').innerHTML=show.map(p=>`<tr><td><strong>${esc(p.name)}</strong>${p.archived?'<div class="hint">Archived</div>':''}</td><td>${esc(p.sector)}</td><td class="right">${money(p.budget)}</td><td class="right">${num(p.reach)}</td><td class="right">${num(p.pre)}% / ${num(p.post)}%</td><td class="right">${pct(p.attrib)}</td><td class="right"><span class="quality">${p.evidence}/5</span></td><td><div class="rowactions"><button type="button" data-edit="${p.id}">Edit</button><button type="button" data-archive="${p.id}">${p.archived?'Restore':'Archive'}</button></div></td></tr>`).join('')||'<tr><td colspan="8" class="empty">No matching programs</td></tr>'}
function rng(seed){let s=seed>>>0;return ()=>{s=(Math.imul(1664525,s)+1013904223)>>>0;return s/4294967296}}
function simulate(programs,scenario,trials=400){const random=rng(1062026),out=[];for(let k=0;k<trials;k++){let total=0;for(const p of programs){const stretch=.83+random()*.34,rateNoise=(random()+random()+random()-1.5)*7,attribNoise=(random()+random()-1)*16;const reach=p.reach*(1+scenario.reach/100)*stretch,post=clamp(p.post+scenario.lift+rateNoise,0,100),attrib=clamp(p.attrib+scenario.attrib+attribNoise,0,100);total+=reach*(post-p.pre)/100*attrib/100}out.push(total)}return out.sort((a,b)=>a-b)}
function renderRisk(){const data=current(),tot=portfolio(data), draws=simulate(activePrograms(),state.scenario);const percentile=q=>draws.length?draws[Math.floor((draws.length-1)*q)]:0;const p10=percentile(.1),p50=percentile(.5),p90=percentile(.9),neg=data.filter(p=>p.incremental<0),lowEvidence=data.filter(p=>p.evidence<=2);$('riskKpis').innerHTML=stat('Modeled P10',signed(p10),'Synthetic lower scenario percentile','◃')+stat('Modeled median',signed(p50),'Middle simulated outcome','◇')+stat('Modeled P90',signed(p90),'Synthetic upper scenario percentile','▹')+stat('Declining programs',String(neg.length),'Negative incremental outcomes','!');
const bins=18,min=draws.length?draws[0]:0,max=draws.length?draws[draws.length-1]:1,range=Math.max(max-min,1e-9),counts=Array(bins).fill(0);for(const v of draws)counts[clamp(Math.floor((v-min)/range*bins),0,bins-1)]++;const top=Math.max(1,...counts),w=760,h=250,l=40,r=12,t=18,b=39,area=w-l-r,step=area/bins;$('riskChart').innerHTML=`<svg viewBox="0 0 ${w} ${h}" role="img" aria-label="Synthetic impact outcomes histogram">`+counts.map((v,i)=>{const bh=(h-t-b-8)*v/top;return `<rect x="${l+i*step+2}" y="${h-b-bh}" width="${step-4}" height="${bh}" rx="3" fill="#3cae9f"><title>${v} simulations</title></rect>`}).join('')+`<line x1="${l}" y1="${h-b}" x2="${w-r}" y2="${h-b}" stroke="#9db4c2"/><text x="${l}" y="${h-9}" fill="#90a9b6" font-size="10">${num(min)}</text><text x="${w-r}" y="${h-9}" text-anchor="end" fill="#90a9b6" font-size="10">${num(max)}</text></svg>`;
$('riskPercentiles').innerHTML=`<div class="mini"><span>Middle 80% illustrative range</span><b>${num(p10)} – ${num(p90)}</b></div><div class="mini"><span>Reference net incremental</span><b>${signed(tot.incremental)}</b></div>`;
const issues=[...neg.map(x=>({name:x.name,msg:'Negative attributed outcome estimate; investigate program performance.'})),...lowEvidence.map(x=>({name:x.name,msg:'Low subjective evidence tier; prioritize rigorous evaluation.'}))]; if(tot.reach===0)issues.unshift({name:'No active reach',msg:'Add or restore program records to compare effects.'});$('watchlist').innerHTML=(issues.slice(0,7).map(i=>`<div class="step"><span class="badge">!</span><div><b>${esc(i.name)}</b><p>${esc(i.msg)}</p></div></div>`).join('')||'<div class="note">No basic negative-outcome or low-evidence flags with these inputs. This does not establish validity.</div>');$('sensitivity').innerHTML=data.map(p=>{const lo=projected(p,{...state.scenario,attrib:state.scenario.attrib-10}).incremental,hi=projected(p,{...state.scenario,attrib:state.scenario.attrib+10}).incremental;return `<div class="bar-row"><div class="bar-heading"><b>${esc(p.name)}</b><span>${signed(lo)} to ${signed(hi)}</span></div><div class="track"><span style="width:${clamp(Math.abs(p.incremental)/Math.max(1,...data.map(x=>Math.abs(x.incremental)))*100,0,100)}%"></span></div></div>`}).join('')||'<div class="empty">No active programs</div>'}
function renderMethod(){$('methodTable').innerHTML=`<table><thead><tr><th>Program</th><th>Sector</th><th class="right">Baseline rate</th><th class="right">Follow-up rate</th><th class="right">Attribution assumption</th><th class="right">Subjective evidence</th></tr></thead><tbody>`+state.programs.map(p=>`<tr><td>${esc(p.name)}${p.archived?' (archived)':''}</td><td>${esc(p.sector)}</td><td class="right">${pct(p.pre)}</td><td class="right">${pct(p.post)}</td><td class="right">${pct(p.attrib)}</td><td class="right">${p.evidence}/5</td></tr>`).join('')+'</tbody></table>'}
function render(){renderOverview();renderScenario();renderRegistry();renderRisk();renderMethod();document.body.classList.toggle('dark',state.theme==='dark')}
function showView(name){if(!['overview','studio','programs','uncertainty','method'].includes(name))return;active=name;document.querySelectorAll('.view').forEach(el=>el.classList.toggle('active',el.id===name));document.querySelectorAll('.nav button').forEach(b=>b.classList.toggle('active',b.dataset.view===name));window.scrollTo({top:0,behavior:'instant'})}
function setStatus(msg){$('registryStatus').textContent=msg}
function openModal(id=null){editing=id;const p=state.programs.find(p=>p.id===id)||{name:'',sector:'Education',budget:10000,reach:500,pre:35,post:50,attrib:50,evidence:3};$('modalTitle').textContent=id===null?'New program':'Edit program';for(const [key,field] of [['name','fName'],['sector','fSector'],['budget','fBudget'],['reach','fReach'],['pre','fPre'],['post','fPost'],['attrib','fAttrib'],['evidence','fEvidence']])$(field).value=p[key];$('modalOverlay').classList.add('show');$('fName').focus()}
function closeModal(){$('modalOverlay').classList.remove('show');editing=null}
function csvCell(v){let s=String(v??'');if(/^[\s\uFEFF]*[=+@\t\r]/.test(s)||/^[\s\uFEFF]*-(?!\d+(?:\.\d+)?$)/.test(s))s="'"+s;return '"'+s.replace(/"/g,'""')+'"'}
function download(filename,text,mime){const blob=new Blob([text],{type:mime});const url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=filename;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),2000)}
function backup(){download('ImpactLedger_106_backup.json',JSON.stringify({format:'ImpactLedger-106',version:1,...state},null,2),'application/json')}
function csv(){const heads=['id','name','sector','budget_usd','reach','baseline_rate_pct','followup_rate_pct','attribution_pct','evidence_tier','archived','incremental_outcomes','cost_per_positive_outcome_usd'];const rows=[heads.join(',')];for(const p of state.programs){const a=projected(p);rows.push([p.id,p.name,p.sector,p.budget,p.reach,p.pre,p.post,p.attrib,p.evidence,p.archived,a.incremental,a.unitCost??''].map(csvCell).join(','))}download('ImpactLedger_106_programs.csv','\uFEFF'+rows.join('\r\n'),'text/csv;charset=utf-8');setStatus('CSV exported with formula-injection protection.')}
function importBackup(file){if(!file)return;if(file.size>2*1024*1024){setStatus('Import rejected: file exceeds 2 MB.');return}const reader=new FileReader();reader.onload=()=>{try{const data=JSON.parse(String(reader.result));if(!validBackup(data))throw Error('Invalid program fields or backup format.');state={programs:clone(data.programs),scenario:{...data.scenario},theme:data.theme};save();render();setStatus('Backup restored successfully.')}catch(e){setStatus('Import rejected: '+e.message)}};reader.onerror=()=>setStatus('Unable to read the file.');reader.readAsText(file)}
for(const b of document.querySelectorAll('.nav button'))b.addEventListener('click',()=>showView(b.dataset.view));$('themeBtn').addEventListener('click',()=>{state.theme=state.theme==='dark'?'light':'dark';save();render()});$('backupBtn').addEventListener('click',backup);$('createBtn').addEventListener('click',()=>openModal());$('cancelBtn').addEventListener('click',closeModal);$('modalOverlay').addEventListener('click',e=>{if(e.target===$('modalOverlay'))closeModal()});document.addEventListener('keydown',e=>{if(e.key==='Escape')closeModal()});
$('programForm').addEventListener('submit',e=>{e.preventDefault();const row={id:editing===null?Math.max(0,...state.programs.map(x=>x.id))+1:editing,name:$('fName').value.trim(),sector:$('fSector').value,budget:Number($('fBudget').value),reach:Number($('fReach').value),pre:Number($('fPre').value),post:Number($('fPost').value),attrib:Number($('fAttrib').value),evidence:Number($('fEvidence').value),archived:state.programs.find(x=>x.id===editing)?.archived||false};if(!validProgram(row)){alert('Please correct the program name and numeric ranges.');return}if(editing===null){if(state.programs.length>=120){alert('Maximum 120 programs.');return}state.programs.push(row)}else{const i=state.programs.findIndex(x=>x.id===editing);if(i<0)return;state.programs[i]=row}save();render();closeModal();setStatus('Program saved successfully.')});
$('registryRows').addEventListener('click',e=>{const edit=e.target.closest('[data-edit]'),archive=e.target.closest('[data-archive]');if(edit){openModal(Number(edit.dataset.edit));return}if(archive){const p=state.programs.find(x=>x.id===Number(archive.dataset.archive));if(!p)return;p.archived=!p.archived;save();render();setStatus(`${p.name} ${p.archived?'archived':'restored'}.`)}});
$('presets').addEventListener('click',e=>{const b=e.target.closest('[data-preset]');if(b&&presets[b.dataset.preset])setScenario(presets[b.dataset.preset])});for(const [k,id] of [['reach','reachSlider'],['lift','liftSlider'],['cost','costSlider'],['attrib','attribSlider']])$(id).addEventListener('input',()=>{state.scenario[k]=Number($(id).value);save();renderScenario();renderRisk()});
$('search').addEventListener('input',renderRegistry);$('registryFilter').addEventListener('change',renderRegistry);$('csvBtn').addEventListener('click',csv);$('importBtn').addEventListener('click',()=>$('fileInput').click());$('fileInput').addEventListener('change',e=>{importBackup(e.target.files?.[0]);e.target.value=''});$('resetBtn').addEventListener('click',()=>{if(!confirm('Restore fictional demo data? This replaces the current local workspace. Back up first if needed.'))return;state={programs:clone(demo),scenario:{reach:0,lift:0,cost:0,attrib:0},theme:state.theme};save();render();setStatus('Demo reset complete.')});
render();window.ImpactLedgerTest={projected,portfolio,simulate,validProgram,validBackup,csvCell,activePrograms,refresh:render,getState:()=>clone(state)};
})();
</script>
</body>
</html>