Hugdrigo's picture
Upload 3 files
9c1db87 verified
Raw History Blame Contribute Delete
47 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="#10252b" />
<meta name="description" content="CircularFlow is a local-first circular economy and material recovery simulator with fictional data." />
<title>CircularFlow | Circular Economy Intelligence Studio</title>
<style>
:root{color-scheme:light;--bg:#f4f7f6;--surface:#fff;--surface-alt:#f8faf9;--ink:#163039;--muted:#72868c;--line:#e1eae7;--green:#087f6b;--mint:#e4f7ef;--amber:#bc8033;--red:#ba5666;--shadow:0 12px 36px rgba(12,46,43,.048)}
body.dark{color-scheme:dark;--bg:#0e2227;--surface:#182f35;--surface-alt:#1e383e;--ink:#f0faf8;--muted:#a4bdc1;--line:#325056;--green:#76dec0;--mint:#234b45;--amber:#edbc73;--red:#ff9cad;--shadow:none}
*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;font-family:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:13px;line-height:1.5;background:var(--bg);color:var(--ink)}button,input,select{font:inherit}button{cursor:pointer}button:disabled{opacity:.45;cursor:not-allowed}button:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid #47c7a7;outline-offset:2px}.app{min-height:100vh;display:grid;grid-template-columns:248px minmax(0,1fr)}.sidebar{position:sticky;top:0;align-self:start;min-height:100vh;background:#102a33;color:#b8cfd2;padding:30px 17px;display:flex;flex-direction:column;z-index:3}.identity{display:flex;align-items:center;gap:12px;margin:0 5px 42px}.symbol{height:48px;width:48px;display:grid;place-items:center;border-radius:15px;background:linear-gradient(135deg,#bff2bc,#45d0b5);font-size:30px;color:#113b38;font-weight:900;box-shadow:0 10px 25px #64e3bc24}.identity b{display:block;color:#fff;font-size:21px;letter-spacing:-1px}.identity small{font-size:9px;letter-spacing:1.8px;color:#98b7bb;text-transform:uppercase;font-weight:800}.navlabel{font-size:9px;text-transform:uppercase;letter-spacing:2px;font-weight:900;padding:0 13px 12px;color:#829ea6}.nav{display:grid;gap:5px}.nav button{width:100%;display:flex;align-items:center;gap:13px;text-align:left;border:0;border-radius:10px;background:transparent;color:#bfd0d3;padding:12px 13px;font-weight:720;font-size:12px}.nav button span{font-size:17px;min-width:20px}.nav button:hover,.nav button.selected{background:#244d51;color:#fff}.nav button.selected{box-shadow:inset 3px 0 #77dfb4}.sidefoot{margin-top:auto;background:#20444a;border-radius:14px;padding:15px;color:#aacece;font-size:11px}.sidefoot b{display:block;color:#e6fff5;margin-bottom:5px}.sidefoot i{display:inline-block;width:8px;height:8px;border-radius:50%;background:#6fdcad;margin-right:7px}.main{min-width:0;max-width:1710px;width:100%;padding:24px clamp(16px,3vw,46px) 35px}.topbar,.heading,.row,.panelhead,.toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px}.topbar{margin-bottom:29px}.breadcrumb{font-size:11px;font-weight:730;color:var(--muted)}.breadcrumb strong{color:var(--ink)}.tools{display:flex;gap:9px;flex-wrap:wrap}.button{border-radius:10px;border:1px solid var(--line);background:var(--surface);color:var(--ink);padding:10px 14px;font-size:11px;font-weight:790;white-space:nowrap}.button:hover{filter:brightness(.96)}.button.primary{background:#168d72;border-color:#168d72;color:#fff}.button.soft{background:var(--mint);border-color:transparent;color:var(--green)}.button.danger{color:var(--red)}.heading{margin-bottom:20px;align-items:center}.heading h1{font-size:clamp(27px,3vw,39px);line-height:1.17;letter-spacing:-1.4px;margin:4px 0 4px}.heading p{color:var(--muted);font-size:12px;margin:0;max-width:620px}.eyebrow{text-transform:uppercase;letter-spacing:2.1px;font-weight:900;color:var(--green);font-size:10px}.heading-icon{font-size:35px;border-radius:22px;background:var(--mint);color:var(--green);height:70px;width:70px;display:grid;place-items:center}.hero{background:linear-gradient(108deg,#11363b,#0c6356 69%,#148b6c);padding:26px 34px;border-radius:19px;color:#fff;min-height:164px;display:flex;align-items:center;justify-content:space-between;position:relative;overflow:hidden;margin-bottom:17px}.hero:before{content:"";position:absolute;border-radius:50%;height:340px;width:340px;top:-200px;right:-25px;border:1px solid #bbfbe955;box-shadow:0 0 0 37px #f3fff60b,0 0 0 78px #f3fff608}.hero h2{font-size:clamp(23px,2.7vw,35px);line-height:1.18;letter-spacing:-1.1px;margin:8px 0}.hero p{font-size:12px;max-width:530px;color:#d9f2ed;margin:0 0 14px}.hero .micro{font-size:10px;font-weight:900;letter-spacing:2px;text-transform:uppercase;color:#a6edce}.herographic{font-size:83px;color:#b8edb3;opacity:.7;margin-right:40px;transform:rotate(-8deg)}.kpis{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin-bottom:17px}.kpi,.panel{min-width:0;background:var(--surface);border:1px solid var(--line);border-radius:15px;box-shadow:var(--shadow)}.kpi{padding:19px}.kpihead{display:flex;align-items:center;justify-content:space-between;gap:8px;color:var(--muted);font-size:10px;letter-spacing:.7px;font-weight:850;text-transform:uppercase}.kpihead span:last-child{background:var(--mint);border-radius:9px;display:grid;place-items:center;height:32px;width:32px;color:var(--green);font-size:17px;letter-spacing:0}.kpi strong{display:block;font-size:clamp(22px,2.45vw,32px);font-variant-numeric:tabular-nums;letter-spacing:-1.1px;margin-top:10px;line-height:1.2}.kpi small{font-size:10px;color:var(--muted)}.cols{display:grid;grid-template-columns:minmax(0,1.57fr) minmax(0,1fr);gap:16px}.cols.equal{grid-template-columns:repeat(2,minmax(0,1fr))}.panel{padding:20px;margin-bottom:16px}.panelhead{align-items:flex-start;margin-bottom:16px}.panelhead h3{margin:0;font-size:15px;letter-spacing:-.3px}.panelhead p{color:var(--muted);margin:4px 0 0;font-size:11px}.pill{display:inline-block;background:var(--mint);color:var(--green);font-size:10px;font-weight:850;padding:5px 10px;border-radius:99px;white-space:nowrap}.pill.amber{background:var(--surface-alt);color:var(--amber)}.pill.red{background:var(--surface-alt);color:var(--red)}.chart svg{display:block;width:100%;height:auto}.legend{display:flex;gap:15px;flex-wrap:wrap;font-size:10px;color:var(--muted);font-weight:730;margin-top:9px}.legend i{display:inline-block;height:9px;width:10px;border-radius:3px;margin-right:5px}.metric{display:flex;justify-content:space-between;align-items:center;gap:10px;border-bottom:1px solid var(--line);padding:11px 0;font-size:11px}.metric:last-child{border-bottom:0}.metric span{color:var(--muted)}.metric b{font-size:12px;text-align:right}.rank{display:grid;grid-template-columns:minmax(0,1fr) 95px 57px;gap:10px;align-items:center;border-bottom:1px solid var(--line);padding:10px 0;font-size:11px}.rank:last-child{border:0}.rank strong,.rank small{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.rank small{font-size:10px;color:var(--muted)}.progress{background:var(--surface-alt);height:8px;border-radius:20px;overflow:hidden}.progress i{display:block;height:100%;border-radius:20px;background:linear-gradient(90deg,#1a9b7b,#9bdec0)}.view{display:none}.view.active{display:block}.presets{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}.preset{min-width:0;text-align:left;border:1px solid var(--line);border-radius:12px;color:var(--ink);background:var(--surface-alt);padding:13px}.preset b{display:block;font-size:12px}.preset small{display:block;font-size:10px;color:var(--muted);margin-top:3px}.preset.active{border:2px solid #19a382;padding:12px;background:var(--mint)}.inputs{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.field{min-width:0}.field label{display:flex;align-items:center;justify-content:space-between;gap:6px;font-weight:820;font-size:11px;margin-bottom:8px}.field label output{color:var(--green)}.field input:not([type=range]),.field select,.input{width:100%;min-width:0;background:var(--surface);border:1px solid var(--line);border-radius:9px;color:var(--ink);padding:10px}.field input[type=range]{width:100%;accent-color:#139f80}.field small{display:block;color:var(--muted);font-size:10px;margin-top:4px}.tablewrap{overflow:auto;width:100%}table{border-collapse:collapse;width:100%;min-width:790px}th,td{padding:12px 10px;border-bottom:1px solid var(--line);text-align:left;white-space:nowrap;font-size:11px}th{font-size:9px;letter-spacing:.7px;text-transform:uppercase;color:var(--muted);background:var(--surface-alt)}tbody tr:last-child td{border:0}.right{text-align:right}.muted{color:var(--muted)}.toolbar{flex-wrap:wrap;margin-bottom:13px}.toolbar .input{max-width:240px}.steps{display:grid;gap:0}.step{display:flex;align-items:flex-start;gap:12px;padding:13px 0;border-bottom:1px solid var(--line)}.step:last-child{border:0}.step em{font-style:normal;font-size:14px;font-weight:900;color:var(--green);background:var(--mint);border-radius:9px;height:31px;width:31px;flex:none;display:grid;place-items:center}.step b{font-size:12px}.step p{font-size:11px;color:var(--muted);margin:3px 0}.callout{border:1px solid var(--line);background:var(--surface-alt);padding:15px;border-radius:12px;color:var(--muted);font-size:11px}.callout b{color:var(--ink)}.foot{font-size:10px;color:var(--muted);display:flex;justify-content:space-between;flex-wrap:wrap;gap:9px;margin-top:18px}.modalback{position:fixed;inset:0;z-index:15;background:#061d24b3;display:none;align-items:center;justify-content:center;padding:15px}.modalback.open{display:flex}.modal{background:var(--surface);border:1px solid var(--line);border-radius:17px;width:min(560px,100%);padding:23px;max-height:93vh;overflow:auto;box-shadow:0 28px 70px #0c212b40}.modal h2{font-size:20px;margin:0 0 15px}.formgrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.wide{grid-column:1/-1}.toast{position:fixed;bottom:19px;right:19px;z-index:30;border-radius:10px;background:#133c3d;color:white;padding:11px 16px;font-size:12px;box-shadow:0 10px 26px #08272b50;opacity:0;transform:translateY(8px);transition:.2s;pointer-events:none}.toast.show{opacity:1;transform:translateY(0)}.sr-only{position:absolute;left:-10000px}.status{display:flex;align-items:center;gap:6px;font-size:10px}.status:before{content:"";height:7px;width:7px;border-radius:50%;background:#8bd8b3;display:inline-block}
@media(max-width:1120px){.app{grid-template-columns:215px minmax(0,1fr)}.sidebar{padding:23px 12px}.main{padding:22px}.kpis{grid-template-columns:repeat(2,minmax(0,1fr))}.cols,.cols.equal{grid-template-columns:1fr}.inputs{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:760px){.app{display:block}.sidebar{min-height:0;position:relative;padding:12px 14px 10px}.identity{margin:0 0 12px}.symbol{height:38px;width:38px;border-radius:11px;font-size:23px}.identity b{font-size:19px}.navlabel,.sidefoot{display:none}.nav{display:flex;overflow:auto;gap:5px;padding-bottom:3px}.nav button{flex:none;white-space:nowrap;width:auto;padding:8px 10px;font-size:10px}.nav button span{font-size:14px;min-width:auto}.main{padding:16px 14px 28px}.topbar{margin-bottom:20px;align-items:flex-start;flex-wrap:wrap}.tools{justify-content:flex-start}.heading-icon,.heroGraphic{display:none}.hero{padding:23px;min-height:0}.herographic{display:none}.presets{grid-template-columns:repeat(2,minmax(0,1fr))}.formgrid{grid-template-columns:1fr}}
@media(max-width:430px){.heading h1{font-size:27px}.kpis{gap:8px}.kpi{padding:13px}.kpi strong{font-size:23px}.kpihead{font-size:9px}.panel{padding:15px}.inputs{grid-template-columns:1fr}.hero h2{font-size:27px}.button{padding:8px 10px;font-size:10px}.topbar{gap:9px}.foot{font-size:9px}}
</style>
</head>
<body>
<div class="app">
<aside class="sidebar">
<div class="identity"><div class="symbol">↻</div><div><b>CircularFlow</b><small>Material intelligence</small></div></div>
<div class="navlabel">WORKSPACE</div>
<nav class="nav" aria-label="Main navigation">
<button class="selected" data-view="overview"><span>▦</span> Overview</button>
<button data-view="simulator"><span>◈</span> Scenario lab</button>
<button data-view="materials"><span>▤</span> Material registry</button>
<button data-view="insights"><span>▥</span> Impact analysis</button>
<button data-view="methodology"><span>◎</span> Methodology</button>
</nav>
<div class="sidefoot"><b><i></i> Offline simulation</b>No live measurements, external services, or private data are required.<br><br><strong>HUGDRIGO · PROJECT 104</strong></div>
</aside>
<main class="main">
<div class="topbar"><div class="breadcrumb">HUGDRIGO / SUSTAINABILITY / <strong id="crumb">Overview</strong></div><div class="tools"><button class="button" id="darkToggle">☾ Dark mode</button><button class="button" id="exportJSON">↓ Backup JSON</button><button class="button primary" id="exportCSV">↓ Export CSV</button><input type="file" id="importFile" accept="application/json,.json" class="sr-only"/></div></div>
<div class="view active" id="overview">
<div class="heading"><div><div class="eyebrow">RESOURCE PERFORMANCE / EXECUTIVE VIEW</div><h1>Material circularity overview</h1><p>Follow recovered tonnage, circularity performance, net processing costs and indicative carbon effects across your operations.</p></div><div class="heading-icon">♻</div></div>
<div class="hero"><div><div class="micro">Portfolio intelligence · demo dataset</div><h2>Turn discarded material<br>into measurable value.</h2><p>Understand where material is lost, which streams deliver value, and how recovery investments change operational outcomes.</p><span class="pill" style="background:#ffffff22;color:#d5ffe6">8 illustrative material streams</span></div><div class="herographic">♻</div></div>
<div class="kpis" id="overviewKpis"></div>
<div class="cols"><section class="panel"><div class="panelhead"><div><h3>Monthly material flow</h3><p>Generated, diverted and recovered, hypothetical tonnes</p></div><span class="pill">12-month projection</span></div><div class="chart" id="flowChart"></div><div class="legend"><span><i style="background:#b6c9cd"></i>Generated</span><span><i style="background:#65c9a9"></i>Diverted</span><span><i style="background:#087f6b"></i>Recovered</span></div></section><section class="panel"><div class="panelhead"><div><h3>Recovery by material</h3><p>Share of total material successfully recovered</p></div><span class="pill">Portfolio</span></div><div id="recoveryRanks"></div></section></div>
<div class="cols equal"><section class="panel"><div class="panelhead"><div><h3>Economic performance</h3><p>Monthly operating assumptions, USD equivalent</p></div></div><div id="economicMetrics"></div></section><section class="panel"><div class="panelhead"><div><h3>Improvement opportunities</h3><p>Highest modeled residual material streams</p></div></div><div id="opportunityMetrics"></div></section></div>
</div>
<div class="view" id="simulator">
<div class="heading"><div><div class="eyebrow">INTERACTIVE MODEL / SCENARIO STUDIO</div><h1>Explore circularity scenarios</h1><p>Change collection, quality and commodity-price assumptions. Compare every simulated outcome to the unchanged baseline.</p></div></div>
<section class="panel"><div class="panelhead"><div><h3>Preset operating conditions</h3><p>Choose a preset, then fine-tune the operating assumptions below.</p></div><span class="pill">Non-predictive scenarios</span></div><div class="presets" id="scenarioPresets"></div></section>
<section class="panel"><div class="panelhead"><div><h3>Scenario controls</h3><p>All changes apply to active records without modifying saved baseline assumptions.</p></div><button class="button" id="resetScenario">Reset controls</button></div><div class="inputs" id="scenarioControls"></div></section>
<div class="kpis" id="scenarioKpis"></div>
<div class="cols"><section class="panel"><div class="panelhead"><div><h3>Baseline vs simulated</h3><p>Monthly mass balance, tonnes</p></div><span class="pill">Comparison</span></div><div class="chart" id="scenarioChart"></div></section><section class="panel"><div class="panelhead"><div><h3>Economic &amp; carbon implications</h3><p>Illustrative changes relative to the baseline</p></div></div><div id="scenarioMetrics"></div></section></div>
<section class="panel"><div class="panelhead"><div><h3>Stream-level scenario impact</h3><p>Understand which changes contribute most to the portfolio result.</p></div></div><div class="tablewrap"><table><thead><tr><th>Stream</th><th>Recovered baseline</th><th>Recovered scenario</th><th>Recovery change</th><th>Net cost change</th></tr></thead><tbody id="scenarioRows"></tbody></table></div></section>
</div>
<div class="view" id="materials">
<div class="heading"><div><div class="eyebrow">SOURCE DATA / EDITABLE REGISTRY</div><h1>Material stream registry</h1><p>Maintain a local portfolio of streams. Add and update records without deleting archived work.</p></div><button class="button primary" id="addStream">+ Add stream</button></div>
<section class="panel"><div class="toolbar"><div class="row"><input class="input" id="searchRecords" placeholder="Search streams or facilities" aria-label="Search material streams"/><select class="input" id="recordFilter" aria-label="Filter material streams" style="width:160px"><option value="active">Active streams</option><option value="archived">Archived streams</option><option value="all">All streams</option></select></div><div class="row"><button class="button" id="openImport">↑ Restore JSON</button><button class="button" id="resetDemo">↺ Reset demo</button></div></div><div class="tablewrap"><table><thead><tr><th>Material &amp; facility</th><th>Monthly generated</th><th>Diverted</th><th>Recovered</th><th>Recovery rate</th><th>Net cost</th><th>Status</th><th>Actions</th></tr></thead><tbody id="registryRows"></tbody></table></div><p class="muted" style="font-size:10px" id="registryCount"></p></section>
</div>
<div class="view" id="insights">
<div class="heading"><div><div class="eyebrow">IMPACT INTELLIGENCE / TRANSPARENCY</div><h1>Impact &amp; opportunity analysis</h1><p>Separate physical recycling outcomes, material economics and illustrative emissions estimates to avoid misleading claims.</p></div></div>
<div class="kpis" id="impactKpis"></div>
<div class="cols equal"><section class="panel"><div class="panelhead"><div><h3>Material flow accounting</h3><p>Mass conservation for all active streams</p></div></div><div id="massMetrics"></div></section><section class="panel"><div class="panelhead"><div><h3>Carbon sensitivity</h3><p>Substitution credit less processing impact</p></div></div><div id="carbonMetrics"></div></section></div>
<section class="panel"><div class="panelhead"><div><h3>Stream impact table</h3><p>All values use the editable assumptions in the current local dataset.</p></div></div><div class="tablewrap"><table><thead><tr><th>Material stream</th><th>Generated (t)</th><th>Diverted (t)</th><th>Recovered (t)</th><th>Residual (t)</th><th>Net cost (USD)</th><th>Indicative tCO₂e</th></tr></thead><tbody id="impactRows"></tbody></table></div></section>
<div class="callout"><b>Interpretation.</b> Residual tonnes equal all generated material not successfully recovered. The emissions figure is a scenario-specific substitution proxy, not a greenhouse-gas inventory, life-cycle assessment or certified carbon saving.</div>
</div>
<div class="view" id="methodology">
<div class="heading"><div><div class="eyebrow">METHOD / ASSUMPTIONS / LIMITATIONS</div><h1>How the model works</h1><p>Reproducible first-principles accounting with explicit rules, editable parameters and no hidden predictive model.</p></div></div>
<div class="cols equal"><section class="panel"><div class="panelhead"><div><h3>Material &amp; financial formulas</h3><p>Monthly values for each active stream</p></div></div><div class="steps"><div class="step"><em>1</em><div><b>Collection and diversion</b><p>Diverted = generated × diversion fraction. A stream may be sent to processing without successfully yielding recovered material.</p></div></div><div class="step"><em>2</em><div><b>Effective material recovery</b><p>Recovered = diverted × (1 − contamination) × recovery yield. Residual = generated − recovered.</p></div></div><div class="step"><em>3</em><div><b>Net cost</b><p>Net cost = residual × disposal unit cost + diverted × processing unit cost − recovered × resale unit value.</p></div></div><div class="step"><em>4</em><div><b>Indicative emissions</b><p>Emissions proxy = recovered × avoided-impact factor − diverted × processing-impact factor, in tonnes CO₂e.</p></div></div></div></section><section class="panel"><div class="panelhead"><div><h3>Model boundaries</h3><p>What this demonstration includes and excludes</p></div></div><div class="steps"><div class="step"><em>✓</em><div><b>Transparent portfolio totals</b><p>Generated = recovered + residual in every scenario. All forecasts use an illustrative seasonal profile.</p></div></div><div class="step"><em>✓</em><div><b>Controlled scenarios</b><p>Percentage-point modifiers clamp diversion and contamination between 0 and 100 percent. Price and volume modifiers scale baselines.</p></div></div><div class="step"><em>!</em><div><b>Not a lifecycle assessment</b><p>No upstream scope boundaries, transport distances, actual processing energy, landfill methane or product-specific displacement factors are audited.</p></div></div><div class="step"><em>!</em><div><b>Illustrative financials</b><p>No taxes, financing, fixed investment, transport charges or market price uncertainty are included unless represented in user-set unit assumptions.</p></div></div></div></section></div>
<div class="callout"><b>Privacy &amp; security.</b> CircularFlow runs in your browser. No network access, accounts, external APIs, cookies or remote persistence. Data backups are explicit JSON exports. Browser storage is local to the page origin and device. Avoid uploading confidential datasets to public Space repositories.</div>
</div>
<footer class="foot"><span>© 2026 CircularFlow · Hugdrigo portfolio · Project #104</span><span>Illustrative data only · No live environmental claims</span></footer>
</main>
</div>
<div class="modalback" id="modalBack"><div class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle"><h2 id="modalTitle">Material stream</h2><form id="streamForm"><div class="formgrid" id="streamFields"></div><div class="row" style="justify-content:flex-end;margin-top:19px"><button class="button" type="button" id="cancelModal">Cancel</button><button class="button primary" type="submit">Save stream</button></div></form></div></div>
<div class="toast" role="status" id="toast"></div>
<script>
'use strict';
const STORAGE='hugdrigo.circularflow104.v1';
const MAX_RECORDS=200;
const DEMO=[
{id:'cf01',facility:'North Distribution Hub',material:'Cardboard packaging',type:'Paper',generated:142,diversion:72,contamination:7,yield:91,disposalCost:78,processingCost:42,resaleValue:72,avoidedFactor:1.1,processingFactor:.17,archived:false},
{id:'cf02',facility:'Atlas Assembly Plant',material:'Aluminium offcuts',type:'Metal',generated:45,diversion:86,contamination:4,yield:94,disposalCost:110,processingCost:130,resaleValue:950,avoidedFactor:6.8,processingFactor:.55,archived:false},
{id:'cf03',facility:'Central Retail Network',material:'Mixed flexible plastics',type:'Plastic',generated:88,diversion:42,contamination:19,yield:68,disposalCost:105,processingCost:120,resaleValue:145,avoidedFactor:1.7,processingFactor:.52,archived:false},
{id:'cf04',facility:'East Industrial Campus',material:'Steel machining scrap',type:'Metal',generated:95,diversion:83,contamination:5,yield:93,disposalCost:90,processingCost:82,resaleValue:215,avoidedFactor:1.8,processingFactor:.31,archived:false},
{id:'cf05',facility:'Food Processing Cluster',material:'Organic residuals',type:'Organic',generated:170,diversion:58,contamination:8,yield:81,disposalCost:94,processingCost:57,resaleValue:18,avoidedFactor:.47,processingFactor:.19,archived:false},
{id:'cf06',facility:'South Fulfilment Center',material:'Wood pallets',type:'Wood',generated:65,diversion:77,contamination:9,yield:87,disposalCost:76,processingCost:39,resaleValue:89,avoidedFactor:.53,processingFactor:.14,archived:false},
{id:'cf07',facility:'Metro Packaging Plant',material:'PET bottle scrap',type:'Plastic',generated:76,diversion:66,contamination:13,yield:83,disposalCost:108,processingCost:105,resaleValue:270,avoidedFactor:1.4,processingFactor:.38,archived:false},
{id:'cf08',facility:'West Manufacturing Site',material:'Container glass',type:'Glass',generated:110,diversion:71,contamination:10,yield:88,disposalCost:74,processingCost:63,resaleValue:48,avoidedFactor:.38,processingFactor:.16,archived:false}
];
const PRESETS={baseline:{label:'Current operations',desc:'Reference assumptions',volume:0,diversion:0,contamination:0,resale:0},collection:{label:'Collection upgrade',desc:'Higher diversion',volume:0,diversion:17,contamination:-3,resale:0},quality:{label:'Quality improvement',desc:'Cleaner recycling feed',volume:0,diversion:6,contamination:-9,resale:7},stress:{label:'Commodity downturn',desc:'Higher waste and lower prices',volume:14,diversion:-8,contamination:11,resale:-35}};
const CONTROL_INFO=[['volume','Material volume',-40,60,'%'],['diversion','Diversion change',-35,35,'pp'],['contamination','Contamination change',-30,30,'pp'],['resale','Resale price change',-70,80,'%']];
const TYPES=['Paper','Metal','Plastic','Organic','Wood','Glass','Other'];
const EDIT_FIELDS=[['facility','Facility / operation','text',null,null],['material','Material stream','text',null,null],['type','Material category','select',null,null],['generated','Generated (tonnes/month)','number',0,1e7],['diversion','Diversion (%)','number',0,100],['contamination','Contamination (%)','number',0,100],['yield','Recovery yield (%)','number',0,100],['disposalCost','Disposal cost (USD/t)','number',0,1e7],['processingCost','Processing cost (USD/t)','number',0,1e7],['resaleValue','Resale value (USD/t)','number',0,1e7],['avoidedFactor','Avoided impact (tCO₂e/t)','number',0,100],['processingFactor','Processing impact (tCO₂e/t)','number',0,100]];
const clone=x=>JSON.parse(JSON.stringify(x));
const num=(n,d=1)=>Number(n).toLocaleString('en-US',{maximumFractionDigits:d,minimumFractionDigits:d});
const money=n=>new Intl.NumberFormat('en-US',{style:'currency',currency:'USD',maximumFractionDigits:0}).format(n);
const clamp=(n,a,b)=>Math.min(b,Math.max(a,n));
const esc=v=>String(v).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const safeCSV=v=>{if(typeof v==='number'||typeof v==='boolean')return '"'+String(v)+'"';let s=String(v??'');if(/^[\s\t\r\n]*[=+@\-]/.test(s))s="'"+s;return '"'+s.replaceAll('"','""')+'"';};
const isPlain=o=>!!o&&typeof o==='object'&&!Array.isArray(o);
let records=clone(DEMO),scenario='baseline',settings={...PRESETS.baseline},editing=null,toastHandle=null;
function validateRecord(x){
if(!isPlain(x)||typeof x.id!=='string'||!/^[a-zA-Z0-9_-]{1,60}$/.test(x.id)||typeof x.facility!=='string'||typeof x.material!=='string'||x.facility.trim().length<2||x.facility.length>90||x.material.trim().length<2||x.material.length>90||!TYPES.includes(x.type)||typeof x.archived!=='boolean')throw Error('Invalid record metadata');
const fields={generated:1e7,diversion:100,contamination:100,yield:100,disposalCost:1e7,processingCost:1e7,resaleValue:1e7,avoidedFactor:100,processingFactor:100};
for(const [key,max] of Object.entries(fields))if(typeof x[key]!=='number'||!Number.isFinite(x[key])||x[key]<0||x[key]>max)throw Error('Invalid numeric parameter: '+key);
return {id:x.id,facility:x.facility.trim(),material:x.material.trim(),type:x.type,generated:x.generated,diversion:x.diversion,contamination:x.contamination,yield:x.yield,disposalCost:x.disposalCost,processingCost:x.processingCost,resaleValue:x.resaleValue,avoidedFactor:x.avoidedFactor,processingFactor:x.processingFactor,archived:x.archived};
}
function validSettings(x){if(!isPlain(x))throw Error('Invalid settings');const obj={};for(const [key,,min,max] of CONTROL_INFO){if(typeof x[key]!=='number'||!Number.isFinite(x[key])||x[key]<min||x[key]>max)throw Error('Invalid scenario settings');obj[key]=x[key];}return obj;}
function load(){try{const str=localStorage.getItem(STORAGE);if(!str)return;const p=JSON.parse(str);applyBackup(p,false)}catch(e){console.warn('Local data unavailable:',e.message)}}
function save(){try{localStorage.setItem(STORAGE,JSON.stringify({project:'CircularFlow',version:1,records,scenario,settings}));}catch(e){notify('Browser storage unavailable. Export JSON to keep your changes.')}}
function applyBackup(p,persist=true){if(!isPlain(p)||p.project!=='CircularFlow'||p.version!==1||!Array.isArray(p.records)||p.records.length>MAX_RECORDS||p.records.length<1||typeof p.scenario!=='string'||!Object.hasOwn(PRESETS,p.scenario))throw Error('Invalid backup format');const rs=p.records.map(validateRecord);if(new Set(rs.map(r=>r.id)).size!==rs.length)throw Error('Duplicate record IDs');const s=validSettings(p.settings);records=rs;scenario=p.scenario;settings=s;if(persist)save();render();}
const active=()=>records.filter(r=>!r.archived);
function calc(r,s={volume:0,diversion:0,contamination:0,resale:0}){
const generated=r.generated*(1+s.volume/100);
const diversion=clamp(r.diversion+s.diversion,0,100)/100;
const contamination=clamp(r.contamination+s.contamination,0,100)/100;
const diverted=generated*diversion;
const recovered=diverted*(1-contamination)*r.yield/100;
const residual=generated-recovered;
const disposalCost=residual*r.disposalCost;
const processingCost=diverted*r.processingCost;
const recoveredRevenue=recovered*r.resaleValue*(1+s.resale/100);
const netCost=disposalCost+processingCost-recoveredRevenue;
const emissions=recovered*r.avoidedFactor-diverted*r.processingFactor;
return {id:r.id,generated,diverted,recovered,residual,disposalCost,processingCost,recoveredRevenue,netCost,emissions};
}
function totals(rs,s){const init={generated:0,diverted:0,recovered:0,residual:0,disposalCost:0,processingCost:0,recoveredRevenue:0,netCost:0,emissions:0};return rs.reduce((a,r)=>{const p=calc(r,s);for(const k of Object.keys(init))a[k]+=p[k];return a;},init);}
function pct(n,d){return d>0?n/d*100:0}
function labelKpi(label,value,note,icon){return `<article class="kpi"><div class="kpihead"><span>${esc(label)}</span><span>${icon}</span></div><strong>${value}</strong><small>${esc(note)}</small></article>`;}
function chartBars(xs,keys,labels,colors){const w=650,h=265,padL=51,padR=16,padT=25,padB=44;const all=xs.flatMap(x=>keys.map(k=>x[k]));const max=Math.max(1,...all)*1.13;const plotW=w-padL-padR,plotH=h-padT-padB,barW=(plotW/xs.length)*.63/keys.length,gap=plotW/xs.length;let s=`<svg viewBox="0 0 ${w} ${h}" role="img" aria-label="${esc(labels.join(', '))} comparison"><title>Material flow comparison</title>`;for(let k=0;k<=4;k++){const y=padT+plotH*(k/4),v=max*(1-k/4);s+=`<line x1="${padL}" x2="${w-padR}" y1="${y}" y2="${y}" stroke="var(--line)" stroke-width="1"/><text x="${padL-8}" y="${y+4}" text-anchor="end" fill="var(--muted)" font-size="10">${Math.round(v)}</text>`;}
xs.forEach((x,i)=>{const x0=padL+i*gap+gap*.19;keys.forEach((k,j)=>{const v=Math.max(0,x[k]),bh=plotH*v/max; s+=`<rect x="${x0+j*barW}" y="${padT+plotH-bh}" width="${Math.max(2,barW-1)}" height="${bh}" rx="2" fill="${colors[j]}"><title>${esc(x.name)}: ${labels[j]} ${num(v,1)} tonnes</title></rect>`});s+=`<text x="${padL+i*gap+gap*.5}" y="${h-18}" text-anchor="middle" fill="var(--muted)" font-size="10">${esc(x.name)}</text>`});return s+'</svg>';}
function monthChart(t){const months=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];const factors=[.94,.98,1.04,1.01,1.06,1.10,1.07,1.03,.96,.98,1,.99];return chartBars(months.map((m,i)=>({name:m,generated:t.generated*factors[i],diverted:t.diverted*factors[i],recovered:t.recovered*factors[i]})),['generated','diverted','recovered'],['Generated','Diverted','Recovered'],['#c2d3d4','#5fc7a1','#107965']);}
function metric(label,value){return `<div class="metric"><span>${esc(label)}</span><b>${value}</b></div>`;}
function renderOverview(){const rs=active(),t=totals(rs);document.getElementById('overviewKpis').innerHTML=labelKpi('Material generated',num(t.generated)+' t','Monthly throughput','▤')+labelKpi('Recovered material',num(t.recovered)+' t',`${num(pct(t.recovered,t.generated))}% effective recovery`,'♻')+labelKpi('Net operating cost',money(t.netCost),'After resale revenue','$')+labelKpi('Carbon impact proxy',num(t.emissions)+' t','Indicative tCO₂e / month','◈');document.getElementById('flowChart').innerHTML=monthChart(t);
const byType={};for(const r of rs){const p=calc(r);byType[r.type]=(byType[r.type]||0)+p.recovered;}const rank=Object.entries(byType).sort((a,b)=>b[1]-a[1]);const top=rank[0]?.[1]||1;document.getElementById('recoveryRanks').innerHTML=rank.map(([name,v])=>`<div class="rank"><div><strong>${esc(name)}</strong><small>${num(v)} recovered t</small></div><div class="progress"><i style="width:${pct(v,top)}%"></i></div><b class="right">${num(pct(v,t.recovered),0)}%</b></div>`).join('')||'<p class="muted">No active streams.</p>';
document.getElementById('economicMetrics').innerHTML=metric('Disposal spend',money(t.disposalCost))+metric('Processing spend',money(t.processingCost))+metric('Recovered material resale',money(t.recoveredRevenue))+metric('Net cost per tonne generated',t.generated?money(t.netCost/t.generated):'$0');
document.getElementById('opportunityMetrics').innerHTML=rs.map(r=>({r,p:calc(r)})).sort((a,b)=>b.p.residual-a.p.residual).slice(0,4).map(({r,p})=>`<div class="rank"><div><strong>${esc(r.material)}</strong><small>${esc(r.facility)}</small></div><div class="progress"><i style="width:${pct(p.residual,t.residual)}%"></i></div><b class="right">${num(p.residual)} t</b></div>`).join('')||'<p class="muted">No active streams.</p>';}
function renderControls(){document.getElementById('scenarioPresets').innerHTML=Object.entries(PRESETS).map(([key,p])=>`<button class="preset ${scenario===key&&['volume','diversion','contamination','resale'].every(k=>settings[k]===PRESETS[key][k])?'active':''}" data-preset="${key}" aria-pressed="${scenario===key&&['volume','diversion','contamination','resale'].every(k=>settings[k]===PRESETS[key][k])}"><b>${esc(p.label)}</b><small>${esc(p.desc)}</small></button>`).join('');document.getElementById('scenarioControls').innerHTML=CONTROL_INFO.map(([k,name,min,max,unit])=>`<div class="field"><label for="control_${k}">${name}<output id="out_${k}">${settings[k]>0?'+':''}${settings[k]} ${unit}</output></label><input type="range" id="control_${k}" data-control="${k}" min="${min}" max="${max}" value="${settings[k]}"/><small>${k==='resale'?'Changes recovered-material revenue':k==='diversion'?'Percentage-point change in collection':k==='contamination'?'Percentage-point change in feed quality':'Volume change applied to every stream'}</small></div>`).join('');}
function renderScenarioResults(){const rs=active(),b=totals(rs),s=totals(rs,settings);const delta=(val)=>`${val>=0?'+':''}${num(val)}`;document.getElementById('scenarioKpis').innerHTML=labelKpi('Scenario recovery',num(s.recovered)+' t',`${delta(s.recovered-b.recovered)} t vs baseline`,'♻')+labelKpi('Recovery rate',num(pct(s.recovered,s.generated))+'%',`${delta(pct(s.recovered,s.generated)-pct(b.recovered,b.generated))} pp`,'◈')+labelKpi('Scenario net cost',money(s.netCost),`${money(s.netCost-b.netCost)} vs baseline`,'$')+labelKpi('Carbon impact proxy',num(s.emissions)+' t',`${delta(s.emissions-b.emissions)} tCO₂e vs baseline`,'◎');document.getElementById('scenarioChart').innerHTML=chartBars([{name:'Baseline',generated:b.generated,diverted:b.diverted,recovered:b.recovered},{name:'Scenario',generated:s.generated,diverted:s.diverted,recovered:s.recovered}],['generated','diverted','recovered'],['Generated','Diverted','Recovered'],['#bfd0d2','#67cbaa','#087f6b'])+`<div class="legend"><span><i style="background:#bfd0d2"></i>Generated</span><span><i style="background:#67cbaa"></i>Diverted</span><span><i style="background:#087f6b"></i>Recovered</span></div>`;document.getElementById('scenarioMetrics').innerHTML=metric('Generated material change',delta(s.generated-b.generated)+' t')+metric('Diverted material change',delta(s.diverted-b.diverted)+' t')+metric('Residual material change',delta(s.residual-b.residual)+' t')+metric('Recovered revenue change',money(s.recoveredRevenue-b.recoveredRevenue))+metric('Net monthly cost change',money(s.netCost-b.netCost))+metric('Indicative CO₂e change',delta(s.emissions-b.emissions)+' t');document.getElementById('scenarioRows').innerHTML=rs.map(r=>{const a=calc(r),z=calc(r,settings);return `<tr><td>${esc(r.material)}</td><td>${num(a.recovered)} t</td><td>${num(z.recovered)} t</td><td>${delta(z.recovered-a.recovered)} t</td><td>${money(z.netCost-a.netCost)}</td></tr>`;}).join('')||'<tr><td colspan="5">No active streams.</td></tr>';}
function renderRegistry(){const filter=document.getElementById('recordFilter').value,query=document.getElementById('searchRecords').value.trim().toLowerCase();let rs=records.filter(r=>(filter==='all'||(filter==='active'?!r.archived:r.archived))&&(!query||(r.material+' '+r.facility+' '+r.type).toLowerCase().includes(query)));document.getElementById('registryRows').innerHTML=rs.map(r=>{const p=calc(r);return `<tr><td><strong>${esc(r.material)}</strong><br><span class="muted">${esc(r.facility)} · ${esc(r.type)}</span></td><td>${num(p.generated)} t</td><td>${num(p.diverted)} t</td><td>${num(p.recovered)} t</td><td>${num(pct(p.recovered,p.generated))}%</td><td>${money(p.netCost)}</td><td><span class="pill ${r.archived?'amber':''}">${r.archived?'Archived':'Active'}</span></td><td><div class="row"><button class="button" data-edit="${r.id}">Edit</button><button class="button ${r.archived?'soft':'danger'}" data-archive="${r.id}">${r.archived?'Restore':'Archive'}</button></div></td></tr>`;}).join('')||'<tr><td colspan="8">No streams match this filter.</td></tr>';document.getElementById('registryCount').textContent=`${rs.length} matching streams · ${active().length} active / ${records.length} total · ${MAX_RECORDS} maximum`;
}
function renderInsights(){const rs=active(),t=totals(rs);document.getElementById('impactKpis').innerHTML=labelKpi('Diversion rate',num(pct(t.diverted,t.generated))+'%','Collected for processing','↗')+labelKpi('Actual recovery',num(pct(t.recovered,t.generated))+'%','Successfully recovered','♻')+labelKpi('Residual material',num(t.residual)+' t','Monthly non-recovered mass','▤')+labelKpi('Net emissions proxy',num(t.emissions)+' t','Indicative tonnes CO₂e','◎');document.getElementById('massMetrics').innerHTML=metric('Total generated',num(t.generated)+' t')+metric('Sent for processing',num(t.diverted)+' t')+metric('Recovered',num(t.recovered)+' t')+metric('Unrecovered residual',num(t.residual)+' t')+metric('Mass conservation difference',num(t.generated-t.recovered-t.residual,6)+' t');document.getElementById('carbonMetrics').innerHTML=metric('Total substitution proxy',num(rs.reduce((s,r)=>s+calc(r).recovered*r.avoidedFactor,0))+' tCO₂e')+metric('Total processing impact',num(rs.reduce((s,r)=>s+calc(r).diverted*r.processingFactor,0))+' tCO₂e')+metric('Net modeled effect',num(t.emissions)+' tCO₂e')+metric('Proxy per recovered tonne',t.recovered?num(t.emissions/t.recovered,2)+' tCO₂e/t':'0');document.getElementById('impactRows').innerHTML=rs.map(r=>{const p=calc(r);return `<tr><td>${esc(r.material)}<br><span class="muted">${esc(r.facility)}</span></td><td>${num(p.generated)}</td><td>${num(p.diverted)}</td><td>${num(p.recovered)}</td><td>${num(p.residual)}</td><td>${money(p.netCost)}</td><td>${num(p.emissions)}</td></tr>`;}).join('')||'<tr><td colspan="7">No active streams.</td></tr>';}
function render(){renderOverview();renderControls();renderScenarioResults();renderRegistry();renderInsights();}
function notify(message){const el=document.getElementById('toast');el.textContent=message;el.classList.add('show');clearTimeout(toastHandle);toastHandle=setTimeout(()=>el.classList.remove('show'),4000)}
function openModal(id=null){editing=id;const r=id?records.find(r=>r.id===id):{facility:'',material:'',type:'Paper',generated:100,diversion:65,contamination:8,yield:85,disposalCost:85,processingCost:60,resaleValue:90,avoidedFactor:1,processingFactor:.2};document.getElementById('modalTitle').textContent=id?'Edit material stream':'Add material stream';document.getElementById('streamFields').innerHTML=EDIT_FIELDS.map(([key,label,type,min,max])=>`<div class="field ${key==='facility'||key==='material'?'wide':''}"><label for="field_${key}">${esc(label)}</label>${type==='select'?`<select id="field_${key}" name="${key}">${TYPES.map(t=>`<option value="${t}" ${r[key]===t?'selected':''}>${t}</option>`).join('')}</select>`:`<input id="field_${key}" name="${key}" type="${type}" ${type==='number'?`min="${min}" max="${max}" step="any"`:'maxlength="90"'} required value="${esc(r[key])}"/>`}</div>`).join('');document.getElementById('modalBack').classList.add('open');document.getElementById('field_facility').focus();}
function closeModal(){document.getElementById('modalBack').classList.remove('open');editing=null;}
function download(name,mime,content){const blob=new Blob([content],{type:mime});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download=name;document.body.append(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1500);}
function exportCSV(){const cols=['id','facility','material','type','generated_t','diverted_t','recovered_t','residual_t','net_cost_usd','indicative_tco2e','archived'];const rows=records.map(r=>{const p=calc(r);return [r.id,r.facility,r.material,r.type,p.generated,p.diverted,p.recovered,p.residual,p.netCost,p.emissions,r.archived].map(safeCSV).join(',')});download('circularflow-materials.csv','text/csv;charset=utf-8','\ufeff'+cols.join(',')+'\r\n'+rows.join('\r\n'));notify('Material CSV exported');}
function exportJSON(){download('circularflow-backup.json','application/json',JSON.stringify({project:'CircularFlow',version:1,exportedAt:new Date().toISOString(),records,scenario,settings},null,2));notify('JSON backup exported');}
function onImport(e){const file=e.target.files?.[0];e.target.value='';if(!file)return;if(file.size>350000) return notify('Backup rejected: file exceeds 350 KB');const reader=new FileReader();reader.onload=()=>{try{const p=JSON.parse(String(reader.result));applyBackup(p);notify('Backup restored successfully')}catch(err){notify('Backup rejected: '+err.message)}};reader.onerror=()=>notify('Unable to read backup');reader.readAsText(file);}
function editSubmit(e){e.preventDefault();try{if(!editing&&records.length>=MAX_RECORDS)throw Error('200-record limit reached');const fd=new FormData(e.target);const out={id:editing||('cf'+Date.now().toString(36)),archived:editing?records.find(r=>r.id===editing).archived:false};for(const [key,,type] of EDIT_FIELDS){const v=fd.get(key);out[key]=type==='number'?Number(v):String(v);}const clean=validateRecord(out);const idx=records.findIndex(r=>r.id===editing);if(idx>=0)records[idx]=clean;else records.push(clean);save();render();const updated=Boolean(editing);closeModal();notify(updated?'Material stream updated':'Material stream saved');}catch(err){notify('Cannot save: '+err.message)}}
function switchView(next){document.querySelectorAll('.nav button').forEach(b=>b.classList.toggle('selected',b.dataset.view===next));document.querySelectorAll('.view').forEach(v=>v.classList.toggle('active',v.id===next));const btn=document.querySelector(`[data-view="${next}"]`);document.getElementById('crumb').textContent=btn?btn.textContent.trim():'Overview';window.scrollTo?.({top:0,behavior:'instant'});}
function init(){load();document.querySelectorAll('.nav button').forEach(b=>b.addEventListener('click',()=>switchView(b.dataset.view)));document.getElementById('scenarioPresets').addEventListener('click',e=>{const btn=e.target.closest('[data-preset]');if(!btn)return;scenario=btn.dataset.preset;settings={volume:PRESETS[scenario].volume,diversion:PRESETS[scenario].diversion,contamination:PRESETS[scenario].contamination,resale:PRESETS[scenario].resale};save();render();});document.getElementById('scenarioControls').addEventListener('input',e=>{const k=e.target.dataset.control;if(!k)return;settings[k]=Number(e.target.value);scenario='baseline';save();document.getElementById('out_'+k).textContent=(settings[k]>0?'+':'')+settings[k]+(k==='volume'||k==='resale'?' %':' pp');document.querySelectorAll('[data-preset]').forEach(b=>{b.classList.remove('active');b.setAttribute('aria-pressed','false')});renderScenarioResults();});document.getElementById('resetScenario').addEventListener('click',()=>{scenario='baseline';settings={volume:0,diversion:0,contamination:0,resale:0};save();render();notify('Scenario reset to baseline')});document.getElementById('searchRecords').addEventListener('input',renderRegistry);document.getElementById('recordFilter').addEventListener('change',renderRegistry);document.getElementById('registryRows').addEventListener('click',e=>{const ed=e.target.closest('[data-edit]'),arc=e.target.closest('[data-archive]');if(ed)openModal(ed.dataset.edit);if(arc){const r=records.find(x=>x.id===arc.dataset.archive);if(!r)return;r.archived=!r.archived;save();render();notify(r.archived?'Stream archived; it can be restored':'Stream restored');}});document.getElementById('addStream').addEventListener('click',()=>openModal());document.getElementById('streamForm').addEventListener('submit',editSubmit);document.getElementById('cancelModal').addEventListener('click',closeModal);document.getElementById('modalBack').addEventListener('click',e=>{if(e.target.id==='modalBack')closeModal()});document.addEventListener('keydown',e=>{if(e.key==='Escape')closeModal()});document.getElementById('exportJSON').addEventListener('click',exportJSON);document.getElementById('exportCSV').addEventListener('click',exportCSV);document.getElementById('importFile').addEventListener('change',onImport);document.getElementById('openImport').addEventListener('click',()=>document.getElementById('importFile').click());document.getElementById('resetDemo').addEventListener('click',()=>{if(!confirm('Replace current local data with the original fictional demo? Export a backup first if needed.'))return;records=clone(DEMO);scenario='baseline';settings={volume:0,diversion:0,contamination:0,resale:0};save();render();notify('Demo data restored')});document.getElementById('darkToggle').addEventListener('click',()=>{document.body.classList.toggle('dark');document.getElementById('darkToggle').textContent=document.body.classList.contains('dark')?'☀ Light mode':'☾ Dark mode';});render();}
init();
</script>
</body>
</html>