← back to Model Arena
record five live detailed challenge results
daffeef2304eab9d9366ac752c935a2e5a45066d · 2026-08-31 16:29:41 -0700 · Steve Abrams
Files touched
A data/artifacts/4c21edc43a09/claude-code.htmlA data/artifacts/4c21edc43a09/qwen25-7b.htmlA data/artifacts/4c21edc43a09/qwen3-14b.htmlA data/artifacts/6e2d6635424a/claude-code.htmlA data/artifacts/6e2d6635424a/qwen25-7b.htmlA data/artifacts/6e2d6635424a/qwen3-14b.htmlA data/artifacts/84f06b214f8c/claude-code.htmlA data/artifacts/84f06b214f8c/qwen25-7b.htmlA data/artifacts/84f06b214f8c/qwen3-14b.htmlA data/artifacts/89e23514bd71/qwen25-7b.htmlA data/artifacts/89e23514bd71/qwen3-14b.htmlA data/artifacts/96c1603e5f4a/claude-code.htmlA data/artifacts/96c1603e5f4a/qwen25-7b.htmlA data/artifacts/96c1603e5f4a/qwen3-14b.htmlA verification/five-detailed-live.pngA verification/five-detailed-proof.jsonA verification/five-detailed.e2e.js
Diff
commit daffeef2304eab9d9366ac752c935a2e5a45066d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 31 16:29:41 2026 -0700
record five live detailed challenge results
---
data/artifacts/4c21edc43a09/claude-code.html | 974 ++++++++++++++++++++++
data/artifacts/4c21edc43a09/qwen25-7b.html | 108 +++
data/artifacts/4c21edc43a09/qwen3-14b.html | 490 +++++++++++
data/artifacts/6e2d6635424a/claude-code.html | 765 +++++++++++++++++
data/artifacts/6e2d6635424a/qwen25-7b.html | 116 +++
data/artifacts/6e2d6635424a/qwen3-14b.html | 400 +++++++++
data/artifacts/84f06b214f8c/claude-code.html | 1134 ++++++++++++++++++++++++++
data/artifacts/84f06b214f8c/qwen25-7b.html | 223 +++++
data/artifacts/84f06b214f8c/qwen3-14b.html | 451 ++++++++++
data/artifacts/89e23514bd71/qwen25-7b.html | 159 ++++
data/artifacts/89e23514bd71/qwen3-14b.html | 510 ++++++++++++
data/artifacts/96c1603e5f4a/claude-code.html | 694 ++++++++++++++++
data/artifacts/96c1603e5f4a/qwen25-7b.html | 128 +++
data/artifacts/96c1603e5f4a/qwen3-14b.html | 713 ++++++++++++++++
verification/five-detailed-live.png | Bin 0 -> 235640 bytes
verification/five-detailed-proof.json | 30 +
verification/five-detailed.e2e.js | 35 +
17 files changed, 6930 insertions(+)
diff --git a/data/artifacts/4c21edc43a09/claude-code.html b/data/artifacts/4c21edc43a09/claude-code.html
new file mode 100644
index 0000000..9a78e80
--- /dev/null
+++ b/data/artifacts/4c21edc43a09/claude-code.html
@@ -0,0 +1,974 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Margin Intelligence — Monday Buying Review</title>
+<style>
+ :root{
+ --bg:#f4f2ec;
+ --panel:#fbfaf6;
+ --ink:#1c1a17;
+ --ink-2:#514c44;
+ --ink-3:#8a8377;
+ --line:#ded9cd;
+ --line-2:#eae6dc;
+ --accent:#3a5a44;
+ --accent-soft:#e4ede4;
+ --gold:#8a6d2f;
+ --pos:#2f6b47;
+ --neg:#a8402c;
+ --warn:#b06b1a;
+ --warn-soft:#f6e9d4;
+ --neg-soft:#f4dfd8;
+ --pos-soft:#dfeadf;
+ --focus:#1f5fbf;
+ --shadow:0 1px 2px rgba(28,26,23,.06),0 8px 24px rgba(28,26,23,.06);
+ --mono:"SF Mono",ui-monospace,"Roboto Mono",Menlo,Consolas,monospace;
+ --serif:Georgia,"Times New Roman",'Iowan Old Style',serif;
+ --sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;
+ }
+ *{box-sizing:border-box}
+ html,body{margin:0;padding:0}
+ body{
+ background:var(--bg);
+ color:var(--ink);
+ font-family:var(--sans);
+ font-size:14px;
+ line-height:1.45;
+ -webkit-font-smoothing:antialiased;
+ }
+ h1,h2,h3{margin:0;font-weight:600;letter-spacing:-.01em}
+ a{color:var(--accent)}
+ .num{font-family:var(--mono);font-variant-numeric:tabular-nums;font-feature-settings:"tnum"}
+ .pos{color:var(--pos)}
+ .neg{color:var(--neg)}
+ .warn{color:var(--warn)}
+ .muted{color:var(--ink-3)}
+
+ /* focus visibility */
+ :focus-visible{outline:2.5px solid var(--focus);outline-offset:2px;border-radius:3px}
+ button,select,input{font:inherit;color:inherit}
+
+ /* ---- Masthead ---- */
+ header.masthead{
+ border-bottom:1px solid var(--line);
+ background:var(--panel);
+ padding:14px 22px 12px;
+ position:sticky;top:0;z-index:40;
+ }
+ .brandrow{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap}
+ .brand{font-family:var(--serif);font-size:20px;letter-spacing:.02em}
+ .brand .mono-tag{
+ font-family:var(--mono);font-size:10px;letter-spacing:.14em;
+ text-transform:uppercase;color:var(--ink-3);border:1px solid var(--line);
+ padding:2px 6px;border-radius:3px;margin-left:2px;
+ }
+ .kicker{font-family:var(--mono);font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-3)}
+ .brandrow .spacer{flex:1}
+ .stale{
+ display:none;align-items:center;gap:8px;font-size:12px;color:var(--warn);
+ background:var(--warn-soft);border:1px solid #e6cfa0;padding:5px 10px;border-radius:4px;
+ }
+ .stale.show{display:inline-flex}
+ .stale button{
+ background:none;border:1px solid #d3b479;color:var(--warn);border-radius:4px;
+ padding:2px 8px;cursor:pointer;font-size:11px;
+ }
+ .dot{width:7px;height:7px;border-radius:50%;background:var(--warn);display:inline-block}
+
+ /* ---- Controls ---- */
+ .controls{
+ display:flex;gap:18px;flex-wrap:wrap;align-items:flex-end;margin-top:12px;
+ }
+ .ctl{display:flex;flex-direction:column;gap:4px}
+ .ctl > label{font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-3)}
+ .seg{display:inline-flex;border:1px solid var(--line);border-radius:6px;overflow:hidden;background:var(--bg)}
+ .seg button{
+ background:transparent;border:none;padding:6px 11px;cursor:pointer;font-size:12.5px;
+ color:var(--ink-2);border-right:1px solid var(--line-2);
+ }
+ .seg button:last-child{border-right:none}
+ .seg button[aria-pressed="true"]{background:var(--ink);color:#fbfaf6;font-weight:600}
+ select{
+ border:1px solid var(--line);border-radius:6px;padding:6px 26px 6px 10px;background:var(--bg);
+ font-size:12.5px;cursor:pointer;appearance:none;
+ background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6'><path d='M0 0l5 6 5-6z' fill='%238a8377'/></svg>");
+ background-repeat:no-repeat;background-position:right 9px center;min-width:130px;
+ }
+ .ctl-reset button{
+ background:none;border:none;color:var(--accent);cursor:pointer;font-size:12px;
+ text-decoration:underline;text-underline-offset:2px;padding:6px 0;
+ }
+
+ /* ---- Layout ---- */
+ main{display:grid;grid-template-columns:minmax(0,1fr) 372px;gap:0}
+ .left{padding:20px 22px 60px;min-width:0}
+ .right{
+ border-left:1px solid var(--line);background:var(--panel);
+ padding:20px;position:sticky;top:114px;align-self:start;height:calc(100vh - 114px);overflow:auto;
+ }
+
+ /* ---- KPI strip ---- */
+ .kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px}
+ .kpi{
+ background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:12px 13px;box-shadow:var(--shadow);
+ }
+ .kpi .lab{font-family:var(--mono);font-size:9.5px;letter-spacing:.11em;text-transform:uppercase;color:var(--ink-3)}
+ .kpi .val{font-family:var(--serif);font-size:23px;margin-top:3px;letter-spacing:-.01em}
+ .kpi .sub{font-size:11px;color:var(--ink-3);margin-top:2px}
+
+ .sec-title{
+ font-family:var(--mono);font-size:11px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);
+ margin:26px 0 10px;display:flex;align-items:center;gap:10px;
+ }
+ .sec-title::after{content:"";flex:1;height:1px;background:var(--line-2)}
+
+ /* ---- Waterfall ---- */
+ .waterfall-wrap{
+ background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:16px 16px 8px;box-shadow:var(--shadow);
+ }
+ .wf{display:flex;align-items:flex-end;gap:10px;height:180px;padding-top:6px}
+ .wf-col{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;min-width:0;height:100%}
+ .wf-bararea{width:100%;flex:1;display:flex;flex-direction:column;justify-content:flex-end;position:relative}
+ .wf-bar{width:78%;margin:0 auto;border-radius:2px 2px 0 0;position:relative;transition:height .5s cubic-bezier(.4,0,.2,1)}
+ .wf-bar.total{background:var(--ink)}
+ .wf-bar.sub{background:var(--neg);opacity:.82}
+ .wf-bar.final{background:var(--accent)}
+ .wf-lab{font-size:10px;color:var(--ink-2);text-align:center;margin-top:7px;line-height:1.25;min-height:26px}
+ .wf-val{font-family:var(--mono);font-size:10.5px;margin-top:5px;color:var(--ink)}
+ .wf-connector{border-top:1px dashed var(--line);position:absolute;left:0;right:0}
+
+ /* ---- Table ---- */
+ .table-scroll{overflow-x:auto;border:1px solid var(--line);border-radius:8px;background:var(--panel);box-shadow:var(--shadow)}
+ table{width:100%;border-collapse:collapse;min-width:720px}
+ thead th{
+ font-family:var(--mono);font-size:9.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3);
+ text-align:right;padding:10px 12px;border-bottom:1px solid var(--line);white-space:nowrap;
+ position:sticky;top:0;background:var(--panel);cursor:pointer;user-select:none;
+ }
+ thead th:first-child{text-align:left}
+ thead th .arrow{opacity:0;margin-left:3px}
+ thead th[aria-sort="ascending"] .arrow,thead th[aria-sort="descending"] .arrow{opacity:1}
+ thead th[aria-sort]{color:var(--ink)}
+ tbody td{padding:11px 12px;text-align:right;border-bottom:1px solid var(--line-2);white-space:nowrap}
+ tbody td:first-child{text-align:left}
+ tbody tr{cursor:pointer}
+ tbody tr:hover{background:var(--accent-soft)}
+ tbody tr[aria-selected="true"]{background:var(--accent-soft);box-shadow:inset 3px 0 0 var(--accent)}
+ tbody tr:last-child td{border-bottom:none}
+ .fam{display:flex;flex-direction:column;gap:1px}
+ .fam .name{font-weight:600;font-size:13.5px}
+ .fam .vend{font-size:11px;color:var(--ink-3)}
+ .flag{
+ display:inline-flex;align-items:center;gap:4px;font-size:10px;font-family:var(--mono);
+ letter-spacing:.04em;background:var(--neg-soft);color:var(--neg);border:1px solid #e3b8ab;
+ padding:1px 6px;border-radius:10px;margin-top:3px;text-transform:uppercase;
+ }
+ .bar-cell{position:relative}
+ .minibar{height:4px;border-radius:2px;margin-top:4px;background:var(--line);overflow:hidden}
+ .minibar i{display:block;height:100%;border-radius:2px}
+ .leak{font-weight:600}
+ .chip{font-family:var(--mono);font-size:11px;padding:2px 6px;border-radius:4px}
+ .chip.g{background:var(--pos-soft);color:var(--pos)}
+ .chip.b{background:var(--neg-soft);color:var(--neg)}
+ .chip.w{background:var(--warn-soft);color:var(--warn)}
+
+ /* ---- States ---- */
+ .state{
+ display:none;text-align:center;padding:54px 20px;border:1px dashed var(--line);border-radius:8px;
+ background:var(--panel);color:var(--ink-3);
+ }
+ .state.show{display:block}
+ .state h3{font-family:var(--serif);font-size:18px;color:var(--ink);font-weight:500;margin-bottom:6px}
+ .state p{max-width:340px;margin:0 auto 14px}
+ .state button{background:var(--ink);color:#fbfaf6;border:none;border-radius:6px;padding:8px 16px;cursor:pointer;font-size:13px}
+ .skel{background:linear-gradient(90deg,var(--line-2) 25%,#f2eee4 37%,var(--line-2) 63%);background-size:400% 100%;animation:sh 1.3s infinite;border-radius:4px}
+ @keyframes sh{0%{background-position:100% 0}100%{background-position:0 0}}
+ .skel-row{display:grid;grid-template-columns:2fr 1fr 1fr 1fr 1fr;gap:12px;padding:12px}
+ .skel-row div{height:16px}
+
+ /* ---- Side panel ---- */
+ .sp-empty{color:var(--ink-3);font-size:13px;line-height:1.6;padding-top:8px}
+ .sp-empty .hint{font-family:var(--serif);font-size:16px;color:var(--ink);margin-bottom:8px;font-weight:500}
+ .sp-head .eyebrow{font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--gold)}
+ .sp-head h2{font-family:var(--serif);font-size:22px;font-weight:500;margin-top:3px;letter-spacing:-.01em}
+ .sp-head .vend{font-size:12px;color:var(--ink-3);margin-top:2px}
+ .sp-metrics{display:grid;grid-template-columns:1fr 1fr;gap:1px;background:var(--line-2);border:1px solid var(--line-2);border-radius:7px;overflow:hidden;margin:16px 0}
+ .sp-metrics div{background:var(--panel);padding:9px 11px}
+ .sp-metrics .l{font-family:var(--mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;color:var(--ink-3)}
+ .sp-metrics .v{font-family:var(--mono);font-size:15px;margin-top:2px}
+ .spark{height:44px;width:100%;margin:6px 0 2px;display:block}
+ .sp-block{margin-top:18px}
+ .sp-block h3{font-family:var(--mono);font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-3);margin-bottom:7px}
+ .diag{font-size:13px;line-height:1.55;color:var(--ink-2)}
+ .diag b{color:var(--ink)}
+ .callout{border-left:3px solid var(--neg);background:var(--neg-soft);padding:9px 12px;border-radius:0 5px 5px 0;font-size:12.5px;margin-top:8px}
+ .callout.ok{border-color:var(--pos);background:var(--pos-soft)}
+ .callout.wn{border-color:var(--warn);background:var(--warn-soft)}
+ .rec{background:var(--accent-soft);border:1px solid #cfe0d3;border-radius:8px;padding:12px 13px;margin-top:8px}
+ .rec .act{font-family:var(--serif);font-size:15px;color:var(--ink);margin-bottom:8px;line-height:1.35}
+
+ /* assumptions */
+ .assump{display:flex;flex-direction:column;gap:12px;margin-top:6px}
+ .assump .row{display:grid;grid-template-columns:1fr auto;align-items:center;gap:8px}
+ .assump .row label{font-size:12px;color:var(--ink-2)}
+ .assump .row .valbox{font-family:var(--mono);font-size:12.5px;color:var(--ink);min-width:56px;text-align:right}
+ .assump input[type=range]{grid-column:1/3;width:100%;accent-color:var(--accent);height:22px}
+ .impact{
+ margin-top:14px;border-top:1px solid var(--line);padding-top:12px;
+ }
+ .impact .row{display:flex;justify-content:space-between;align-items:baseline;padding:4px 0;font-size:13px}
+ .impact .row .k{color:var(--ink-2)}
+ .impact .big{font-family:var(--serif);font-size:26px;letter-spacing:-.01em}
+ .impact .lead{font-size:11px;color:var(--ink-3);margin-top:2px;line-height:1.4}
+ .btn-row{display:flex;gap:8px;margin-top:14px}
+ .btn{border:none;border-radius:6px;padding:9px 12px;cursor:pointer;font-size:12.5px;font-weight:500}
+ .btn.primary{background:var(--accent);color:#fbfaf6;flex:1}
+ .btn.ghost{background:transparent;border:1px solid var(--line);color:var(--ink-2)}
+ .reset-assump{background:none;border:none;color:var(--accent);text-decoration:underline;text-underline-offset:2px;cursor:pointer;font-size:11.5px;padding:0}
+
+ .footnote{font-size:11px;color:var(--ink-3);margin-top:26px;line-height:1.5;border-top:1px solid var(--line-2);padding-top:12px}
+ kbd{font-family:var(--mono);font-size:10.5px;background:var(--bg);border:1px solid var(--line);border-bottom-width:2px;border-radius:4px;padding:1px 5px}
+ .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}
+
+ /* ---- Responsive ---- */
+ @media (max-width:1000px){
+ main{grid-template-columns:1fr}
+ .right{position:static;height:auto;border-left:none;border-top:1px solid var(--line)}
+ .kpis{grid-template-columns:repeat(2,1fr)}
+ }
+ @media (max-width:560px){
+ header.masthead{padding:12px 14px}
+ .left{padding:16px 14px 48px}
+ .right{padding:16px 14px}
+ .kpis{grid-template-columns:1fr 1fr;gap:9px}
+ .kpi .val{font-size:20px}
+ .controls{gap:12px}
+ .brand{font-size:17px}
+ .wf{height:150px;gap:5px}
+ .wf-lab{font-size:9px}
+ }
+</style>
+</head>
+<body>
+<a class="sr-only" href="#opp-table">Skip to opportunity table</a>
+
+<header class="masthead">
+ <div class="brandrow">
+ <div>
+ <div class="kicker">Alderman & Vale — Fine Interiors Distribution</div>
+ <div class="brand">Margin Intelligence <span class="mono-tag">Owner console</span></div>
+ </div>
+ <div class="spacer"></div>
+ <div class="stale" id="staleFlag" role="status">
+ <span class="dot"></span>
+ <span id="staleText">Data snapshot is 26 h old — refreshed Fri 6:02 PM.</span>
+ <button id="refreshBtn">Refresh</button>
+ </div>
+ </div>
+
+ <div class="controls" role="group" aria-label="Filters">
+ <div class="ctl">
+ <label id="lbl-range">Date range</label>
+ <div class="seg" role="group" aria-labelledby="lbl-range" id="rangeSeg">
+ <button data-range="7" aria-pressed="false">7 d</button>
+ <button data-range="30" aria-pressed="true">30 d</button>
+ <button data-range="90" aria-pressed="false">Quarter</button>
+ <button data-range="365" aria-pressed="false">Year</button>
+ </div>
+ </div>
+ <div class="ctl">
+ <label for="channelSel">Channel</label>
+ <select id="channelSel" aria-label="Filter by channel">
+ <option value="all">All channels</option>
+ <option value="Trade">Trade / designer</option>
+ <option value="Retail">Retail showroom</option>
+ <option value="Ecom">E-commerce</option>
+ </select>
+ </div>
+ <div class="ctl">
+ <label for="vendorSel">Vendor</label>
+ <select id="vendorSel" aria-label="Filter by vendor">
+ <option value="all">All vendors</option>
+ </select>
+ </div>
+ <div class="ctl ctl-reset">
+ <label> </label>
+ <button id="resetFilters">Reset filters</button>
+ </div>
+ </div>
+</header>
+
+<main>
+ <!-- ================= LEFT ================= -->
+ <section class="left" aria-label="Portfolio overview">
+
+ <div class="kpis" id="kpiStrip" aria-live="polite"></div>
+
+ <div class="sec-title">Contribution-margin waterfall</div>
+ <div class="waterfall-wrap">
+ <div class="wf" id="waterfall" aria-hidden="false"></div>
+ <p class="muted" style="font-size:11px;margin:10px 2px 4px">
+ From gross revenue to true contribution — each deduction is a lever you can pull. The
+ <b>freight & oversize</b> and <b>sample & return</b> steps are where recoverable margin most often hides.
+ </p>
+ </div>
+
+ <div class="sec-title">Opportunity table
+ <span class="muted" style="font-family:var(--sans);text-transform:none;letter-spacing:0;font-size:11px">
+ — sorted by recoverable margin. Select a row for diagnosis.</span>
+ </div>
+
+ <!-- loading -->
+ <div class="state" id="loadingState">
+ <div class="table-scroll" style="border:none;box-shadow:none;background:transparent">
+ <div class="skel-row"><div class="skel"></div><div class="skel"></div><div class="skel"></div><div class="skel"></div><div class="skel"></div></div>
+ <div class="skel-row"><div class="skel"></div><div class="skel"></div><div class="skel"></div><div class="skel"></div><div class="skel"></div></div>
+ <div class="skel-row"><div class="skel"></div><div class="skel"></div><div class="skel"></div><div class="skel"></div><div class="skel"></div></div>
+ <div class="skel-row"><div class="skel"></div><div class="skel"></div><div class="skel"></div><div class="skel"></div><div class="skel"></div></div>
+ </div>
+ <p class="muted" style="margin-top:14px">Loading margin snapshot…</p>
+ </div>
+
+ <!-- no results -->
+ <div class="state" id="emptyState">
+ <h3>No families match these filters</h3>
+ <p>Nothing in this channel/vendor combination for the selected range. Widen the range or clear a filter to see the full portfolio.</p>
+ <button id="emptyReset">Clear filters</button>
+ </div>
+
+ <!-- table -->
+ <div class="table-scroll" id="tableWrap">
+ <table id="opp-table" aria-label="Product family opportunities">
+ <thead>
+ <tr>
+ <th data-key="name" aria-sort="none" scope="col">Product family <span class="arrow">▾</span></th>
+ <th data-key="revenue" aria-sort="none" scope="col">Revenue <span class="arrow">▾</span></th>
+ <th data-key="units" aria-sort="none" scope="col">Units <span class="arrow">▾</span></th>
+ <th data-key="gm" aria-sort="none" scope="col">Gross margin <span class="arrow">▾</span></th>
+ <th data-key="contrib" aria-sort="none" scope="col">Contribution <span class="arrow">▾</span></th>
+ <th data-key="sampleConv" aria-sort="none" scope="col">Sample conv. <span class="arrow">▾</span></th>
+ <th data-key="returnRate" aria-sort="none" scope="col">Return rate <span class="arrow">▾</span></th>
+ <th data-key="cover" aria-sort="none" scope="col">Inv. cover <span class="arrow">▾</span></th>
+ <th data-key="leak" aria-sort="descending" scope="col">Recoverable <span class="arrow">▾</span></th>
+ </tr>
+ </thead>
+ <tbody id="oppBody"></tbody>
+ </table>
+ </div>
+
+ <p class="footnote">
+ Contribution margin = gross margin − freight/oversize handling − sample-program cost − return-processing cost − channel commission.
+ A family can post strong revenue and still return <span class="neg">negative contribution</span> once these below-the-line costs land — those are flagged.
+ Keyboard: <kbd>↑</kbd><kbd>↓</kbd> move rows, <kbd>Enter</kbd> opens diagnosis, sort headers with <kbd>Space</kbd>.
+ </p>
+ </section>
+
+ <!-- ================= RIGHT ================= -->
+ <aside class="right" id="sidePanel" aria-label="Family diagnosis and action modeler">
+ <div class="sp-empty" id="spEmpty">
+ <div class="hint">Select a product family</div>
+ Pick any row in the opportunity table to see its 12-period trend, a plain-language diagnosis of where
+ margin is leaking, and a recommended action you can model — adjust the assumptions and watch the
+ projected annual impact update live.
+ <p style="margin-top:14px;font-size:12px" class="muted">
+ Tip: the family at the top of the table carries the largest recoverable margin leak right now.
+ </p>
+ </div>
+ <div id="spContent" hidden></div>
+ </aside>
+</main>
+
+<script>
+"use strict";
+
+/* ---------------- Data model ---------------- */
+// Base figures are for a 30-day window; ranges scale them with realistic multipliers.
+const FAMILIES = [
+ {
+ id:"grasscloth", name:"Grasscloth & Natural Wallcovering", vendor:"Seneca Mills", channelMix:{Trade:.62,Retail:.28,Ecom:.10},
+ revenue:412000, units:1840, gmPct:.51, freightPct:.11, samplePct:.052, returnRate:.094, commissionPct:.06,
+ sampleConv:.19, cover:2.1, trend:[38,42,40,44,46,49,47,52,55,58,61,64],
+ note:"return", diagnosisKey:"grasscloth"
+ },
+ {
+ id:"performance", name:"Performance Upholstery Textiles", vendor:"Coteau Weaves", channelMix:{Trade:.40,Retail:.34,Ecom:.26},
+ revenue:685000, units:5120, gmPct:.44, freightPct:.05, samplePct:.021, returnRate:.031, commissionPct:.05,
+ sampleConv:.34, cover:3.4, trend:[70,68,72,74,73,76,78,77,80,82,84,86],
+ note:"star", diagnosisKey:"performance"
+ },
+ {
+ id:"stone", name:"Oversize Stone & Slab Surfaces", vendor:"Aventine Stone", channelMix:{Trade:.71,Retail:.24,Ecom:.05},
+ revenue:1240000, units:410, gmPct:.39, freightPct:.27, samplePct:.014, returnRate:.078, commissionPct:.07,
+ sampleConv:.44, cover:5.9, trend:[120,118,124,130,126,133,129,136,131,138,134,141],
+ note:"contradiction", diagnosisKey:"stone"
+ },
+ {
+ id:"drapery", name:"Custom Drapery Hardware", vendor:"Halden Forge", channelMix:{Trade:.55,Retail:.40,Ecom:.05},
+ revenue:298000, units:2260, gmPct:.58, freightPct:.09, samplePct:.018, returnRate:.052, commissionPct:.05,
+ sampleConv:.29, cover:1.4, trend:[34,33,31,32,30,31,29,30,28,29,27,28],
+ note:"decline", diagnosisKey:"drapery"
+ },
+ {
+ id:"rugs", name:"Hand-Knotted Area Rugs", vendor:"Aravalli Atelier", channelMix:{Trade:.48,Retail:.44,Ecom:.08},
+ revenue:520000, units:640, gmPct:.47, freightPct:.14, samplePct:.061, returnRate:.121, commissionPct:.06,
+ sampleConv:.12, cover:7.8, trend:[58,55,52,50,49,47,46,45,44,43,45,44],
+ note:"leak", diagnosisKey:"rugs"
+ },
+ {
+ id:"lighting", name:"Decorative Lighting & Fixtures", vendor:"Lumen Bros.", channelMix:{Trade:.33,Retail:.42,Ecom:.25},
+ revenue:376000, units:1490, gmPct:.49, freightPct:.10, samplePct:.009, returnRate:.067, commissionPct:.06,
+ sampleConv:.31, cover:2.9, trend:[40,41,39,43,42,45,44,46,45,47,48,49],
+ note:"steady", diagnosisKey:"lighting"
+ }
+];
+
+const RANGE_MULT = { "7":0.24, "30":1, "90":2.95, "365":11.6 };
+const RANGE_LABEL = { "7":"last 7 days", "30":"last 30 days", "90":"this quarter", "365":"trailing year" };
+
+/* ---------------- State ---------------- */
+let state = { range:"30", channel:"all", vendor:"all", sortKey:"leak", sortDir:"desc", selected:null };
+let assumptions = {}; // per-selected-family live model
+
+/* ---------------- Helpers ---------------- */
+const $ = s => document.querySelector(s);
+const money = n => {
+ const a = Math.abs(n);
+ if(a>=1e6) return (n<0?"−$":"$")+(a/1e6).toFixed(a>=1e7?1:2)+"M";
+ if(a>=1e3) return (n<0?"−$":"$")+Math.round(a/1e3)+"k";
+ return (n<0?"−$":"$")+Math.round(a);
+};
+const moneyFull = n => (n<0?"−$":"$")+Math.abs(Math.round(n)).toLocaleString("en-US");
+const pct = n => (n*100).toFixed(n*100<10?1:0)+"%";
+const clamp = (v,a,b)=>Math.max(a,Math.min(b,v));
+
+// Compute derived per-family figures for the active range + filters.
+function compute(f){
+ const m = RANGE_MULT[state.range];
+ // channel share applied to revenue/units when a specific channel is selected
+ let share = 1;
+ if(state.channel!=="all") share = f.channelMix[state.channel] || 0;
+ const revenue = f.revenue * m * share;
+ const units = Math.round(f.units * m * share);
+ const grossMargin = revenue * f.gmPct;
+ const freight = revenue * f.freightPct;
+ const sampleCost = revenue * f.samplePct;
+ const returnCost = revenue * f.returnRate * 0.42; // processing/restock portion of returned value
+ const commission = (state.channel==="Trade"||state.channel==="all"? revenue*f.commissionPct : revenue*f.commissionPct*0.6);
+ const contrib = grossMargin - freight - sampleCost - returnCost - commission;
+ const contribPct = revenue>0 ? contrib/revenue : 0;
+ // "Recoverable" = a defensible estimate of margin retrievable via the recommended action, annualized-in-window.
+ const leak = recoverable(f, {revenue,freight,sampleCost,returnCost});
+ return {revenue,units,grossMargin,freight,sampleCost,returnCost,commission,contrib,contribPct,leak,gmPct:f.gmPct,
+ sampleConv:f.sampleConv, returnRate:f.returnRate, cover:f.cover, raw:f};
+}
+
+// Recoverable margin heuristic per family archetype.
+function recoverable(f, c){
+ switch(f.diagnosisKey){
+ case "stone": return c.freight*0.55; // renegotiate freight / consolidate LTL
+ case "rugs": return c.returnCost*0.6 + c.sampleCost*0.5;// cut return + low-converting samples
+ case "grasscloth": return c.returnCost*0.5; // reduce damage returns
+ case "drapery": return c.revenue*0.03; // reprice up on inelastic demand
+ case "lighting":return c.sampleCost*0.3 + c.revenue*0.01;
+ default: return c.revenue*0.008; // healthy — small optimization
+ }
+}
+
+/* ---------------- Filtering + sort ---------------- */
+function activeFamilies(){
+ let list = FAMILIES.filter(f=>{
+ if(state.vendor!=="all" && f.vendor!==state.vendor) return false;
+ if(state.channel!=="all" && (f.channelMix[state.channel]||0)<0.001) return false;
+ return true;
+ }).map(f=>({f, c:compute(f)}));
+
+ const key = state.sortKey, dir = state.sortDir==="asc"?1:-1;
+ const getv = ({f,c})=>({
+ name:f.name.toLowerCase(), revenue:c.revenue, units:c.units, gm:c.grossMargin,
+ contrib:c.contrib, sampleConv:c.sampleConv, returnRate:c.returnRate, cover:c.cover, leak:c.leak
+ })[key];
+ list.sort((a,b)=>{
+ const va=getv(a), vb=getv(b);
+ if(typeof va==="string") return va<vb?-1*dir:va>vb?1*dir:0;
+ return (va-vb)*dir;
+ });
+ return list;
+}
+
+/* ---------------- Renderers ---------------- */
+function renderKPIs(list){
+ const tot = list.reduce((a,{c})=>{
+ a.rev+=c.revenue; a.gm+=c.grossMargin; a.contrib+=c.contrib; a.leak+=c.leak; a.units+=c.units; return a;
+ },{rev:0,gm:0,contrib:0,leak:0,units:0});
+ const cPct = tot.rev>0? tot.contrib/tot.rev : 0;
+ const top = [...list].sort((a,b)=>b.c.leak-a.c.leak)[0];
+ const strip = $("#kpiStrip");
+ strip.innerHTML = `
+ <div class="kpi"><div class="lab">Revenue · ${RANGE_LABEL[state.range]}</div>
+ <div class="val num">${money(tot.rev)}</div>
+ <div class="sub">${tot.units.toLocaleString()} units across ${list.length} ${list.length===1?"family":"families"}</div></div>
+ <div class="kpi"><div class="lab">Contribution margin</div>
+ <div class="val num ${tot.contrib<0?'neg':''}">${money(tot.contrib)}</div>
+ <div class="sub">${pct(cPct)} of revenue after below-line cost</div></div>
+ <div class="kpi"><div class="lab">Recoverable margin</div>
+ <div class="val num ${tot.leak>0?'pos':''}">${money(tot.leak)}</div>
+ <div class="sub">identified & actionable before Monday</div></div>
+ <div class="kpi"><div class="lab">Largest single leak</div>
+ <div class="val" style="font-family:var(--serif);font-size:16px;line-height:1.25">${top? top.f.name.split(" ").slice(0,3).join(" ") : "—"}</div>
+ <div class="sub">${top? money(top.c.leak)+" recoverable" : ""}</div></div>`;
+}
+
+function renderWaterfall(list){
+ const tot = list.reduce((a,{c})=>{
+ a.rev+=c.revenue; a.cogs+=(c.revenue-c.grossMargin); a.freight+=c.freight;
+ a.sample+=c.sampleCost; a.ret+=c.returnCost; a.comm+=c.commission; a.contrib+=c.contrib; return a;
+ },{rev:0,cogs:0,freight:0,sample:0,ret:0,comm:0,contrib:0});
+
+ const steps = [
+ {label:"Gross revenue", val:tot.rev, type:"total", running:tot.rev},
+ {label:"− Cost of goods", val:tot.cogs, type:"sub"},
+ {label:"− Freight & oversize", val:tot.freight, type:"sub"},
+ {label:"− Sample program", val:tot.sample, type:"sub"},
+ {label:"− Returns", val:tot.ret, type:"sub"},
+ {label:"− Commission", val:tot.comm, type:"sub"},
+ {label:"Contribution", val:tot.contrib, type:"final", running:tot.contrib},
+ ];
+ const max = tot.rev || 1;
+ let running = tot.rev;
+ const wf = $("#waterfall");
+ wf.innerHTML = "";
+ steps.forEach((s,i)=>{
+ let barTop, barH;
+ if(s.type==="total"){ barH=100; barTop=0; running=tot.rev; }
+ else if(s.type==="final"){ barH=(s.val/max)*100; barTop=100-barH; }
+ else { running-=s.val; barH=(s.val/max)*100; barTop=(running/max)*100; }
+ const col = document.createElement("div");
+ col.className="wf-col";
+ col.innerHTML = `
+ <div class="wf-bararea">
+ <div class="wf-bar ${s.type}" style="height:${Math.max(barH,1.5)}%;margin-top:auto;position:relative;top:${s.type==='sub'?'-'+(barTop)+'%':'0'}"></div>
+ </div>
+ <div class="wf-lab">${s.label}</div>
+ <div class="wf-val num ${s.type==='sub'?'neg':''}">${s.type==='sub'?'−':''}${money(s.val).replace('−','')}</div>`;
+ // simplified stacked positioning: use fl/flex end for total & final, absolute-ish for subs
+ wf.appendChild(col);
+ });
+ // Re-do sub bar vertical offset cleanly (float them at running height)
+ running = tot.rev;
+ const cols = wf.querySelectorAll(".wf-col");
+ steps.forEach((s,i)=>{
+ const bar = cols[i].querySelector(".wf-bar");
+ const area = cols[i].querySelector(".wf-bararea");
+ if(s.type==="sub"){
+ const before = running;
+ running -= s.val;
+ const h = (s.val/max)*100;
+ const bottom = (running/max)*100;
+ bar.style.position="absolute";
+ bar.style.left="11%"; bar.style.width="78%";
+ bar.style.bottom=bottom+"%";
+ bar.style.top="auto";
+ bar.style.height=Math.max(h,1.2)+"%";
+ } else {
+ bar.style.position="relative";
+ bar.style.top="0";
+ }
+ });
+}
+
+function renderTable(list){
+ const body = $("#oppBody");
+ body.innerHTML="";
+ const maxRev = Math.max(...list.map(x=>x.c.revenue),1);
+ const maxLeak = Math.max(...list.map(x=>x.c.leak),1);
+ list.forEach(({f,c},idx)=>{
+ const tr = document.createElement("tr");
+ tr.tabIndex=0;
+ tr.setAttribute("role","button");
+ tr.dataset.id=f.id;
+ tr.setAttribute("aria-selected", state.selected===f.id?"true":"false");
+ tr.setAttribute("aria-label", `${f.name}. Revenue ${money(c.revenue)}, contribution ${money(c.contrib)}, recoverable ${money(c.leak)}. Open diagnosis.`);
+ const contradiction = c.contrib<0 && c.revenue>0;
+ const negClass = c.contrib<0?"neg":"";
+ tr.innerHTML = `
+ <td>
+ <div class="fam">
+ <span class="name">${f.name}</span>
+ <span class="vend">${f.vendor}</span>
+ ${contradiction? `<span class="flag" title="High revenue but negative contribution">⚠ Rev-up · contrib-negative</span>`:""}
+ </div>
+ </td>
+ <td class="bar-cell">
+ <span class="num">${money(c.revenue)}</span>
+ <div class="minibar"><i style="width:${(c.revenue/maxRev)*100}%;background:var(--ink)"></i></div>
+ </td>
+ <td class="num">${c.units.toLocaleString()}</td>
+ <td class="num">${pct(c.gmPct)}</td>
+ <td class="num ${negClass}">${money(c.contrib)}<div class="minibar"><i style="width:${clamp(Math.abs(c.contribPct)*260,4,100)}%;background:${c.contrib<0?'var(--neg)':'var(--pos)'}"></i></div></td>
+ <td><span class="chip ${f.sampleConv<0.15?'b':f.sampleConv>0.3?'g':'w'}">${pct(f.sampleConv)}</span></td>
+ <td><span class="chip ${f.returnRate>0.09?'b':f.returnRate<0.05?'g':'w'}">${pct(f.returnRate)}</span></td>
+ <td class="num ${f.cover>6?'warn':''}">${f.cover.toFixed(1)}mo</td>
+ <td class="num leak ${c.leak>maxLeak*0.4?'pos':''}">${money(c.leak)}
+ <div class="minibar"><i style="width:${(c.leak/maxLeak)*100}%;background:var(--gold)"></i></div>
+ </td>`;
+ tr.addEventListener("click",()=>select(f.id));
+ tr.addEventListener("keydown",e=>{
+ if(e.key==="Enter"||e.key===" "){e.preventDefault();select(f.id);}
+ else if(e.key==="ArrowDown"){e.preventDefault();(tr.nextElementSibling||body.firstElementChild).focus();}
+ else if(e.key==="ArrowUp"){e.preventDefault();(tr.previousElementSibling||body.lastElementChild).focus();}
+ });
+ body.appendChild(tr);
+ });
+}
+
+/* ---------------- Diagnoses ---------------- */
+const DIAG = {
+ stone:{
+ verdict:"contradiction",
+ why:(c)=>`This is your headline revenue line at <b>${money(c.revenue)}</b>, yet contribution is <b class="neg">${money(c.contrib)}</b>. The culprit is freight: oversize slab shipments burn <b>${money(c.freight)}</b> — ${pct(c.raw.freightPct)} of revenue — mostly on half-empty less-than-truckload runs and residential lift-gate surcharges. Gross margin looks fine at ${pct(c.gmPct)}; the money leaves <i>below</i> the gross line.`,
+ action:"Consolidate slab freight to full-truckload lanes and pass lift-gate/oversize surcharges through to the order.",
+ calloutTone:"neg",
+ calloutText:"Do not pause or promote on gross margin alone — it hides a negative contribution.",
+ levers:[
+ {id:"ltlShift", label:"Shipments moved to full-truckload", min:0, max:80, val:45, unit:"%"},
+ {id:"surcharge", label:"Oversize surcharge passed through", min:0, max:100, val:60, unit:"%"}
+ ],
+ project:(c,l)=>{
+ const freightSaved = c.freight*(l.ltlShift/100)*0.62;
+ const surchargeRecovered = c.freight*0.28*(l.surcharge/100);
+ const win = freightSaved+surchargeRecovered;
+ return {win, base:c.contrib, note:"Freight consolidation typically recovers ~62% of the LTL premium; surcharge pass-through recovers the residential-delivery penalty."};
+ }
+ },
+ rugs:{
+ verdict:"leak",
+ why:(c)=>`Revenue is steady but this family carries the portfolio's worst two ratios: a <b class="neg">${pct(c.returnRate)}</b> return rate and only a <b class="neg">${pct(c.sampleConv)}</b> sample-to-order conversion. You are shipping ${money(c.sampleCost)} of samples and processing ${money(c.returnCost)} of returns to move rugs that ${pct(c.sampleConv)} of samplers actually buy. Inventory cover at ${c.cover.toFixed(1)} months compounds the drag.`,
+ action:"Gate the free-sample program behind a design-trade login and add a fiber/size guide to cut mismatched-expectation returns.",
+ calloutTone:"wn",
+ calloutText:"Low sample conversion + high returns = you're subsidizing browsers. Qualify demand before you ship.",
+ levers:[
+ {id:"sampleCut", label:"Reduction in unqualified samples", min:0, max:70, val:40, unit:"%"},
+ {id:"returnCut", label:"Return-rate reduction from sizing guide", min:0, max:50, val:25, unit:"%"}
+ ],
+ project:(c,l)=>{
+ const sampleSaved = c.sampleCost*(l.sampleCut/100);
+ const returnSaved = c.returnCost*(l.returnCut/100);
+ const win = sampleSaved+returnSaved;
+ return {win, base:c.contrib, note:"Assumes qualified samplers convert at the same rate, so revenue holds while sample & return cost falls."};
+ }
+ },
+ grasscloth:{
+ verdict:"leak",
+ why:(c)=>`Growing nicely and gross margin is a healthy ${pct(c.gmPct)}. The soft spot is a <b>${pct(c.returnRate)}</b> return rate — natural grasscloth arrives damaged or dye-lot-mismatched, costing ${money(c.returnCost)} to process and restock. Sample conversion is fine, so demand is real; this is a fulfillment-quality leak, not a demand problem.`,
+ action:"Add rigid-tube packaging and per-roll dye-lot photos at pick, targeting the damage/mismatch return reasons.",
+ calloutTone:"ok",
+ calloutText:"Strong, real demand. Protect it by fixing the packaging leak — this is your safest lever.",
+ levers:[
+ {id:"pkg", label:"Damage returns eliminated", min:0, max:70, val:45, unit:"%"},
+ {id:"dye", label:"Dye-lot mismatch returns cut", min:0, max:60, val:35, unit:"%"}
+ ],
+ project:(c,l)=>{
+ const win = c.returnCost*(0.55*(l.pkg/100)+0.45*(l.dye/100));
+ return {win, base:c.contrib, note:"Splits return cost between damage (~55%) and dye-lot mismatch (~45%) reasons."};
+ }
+ },
+ drapery:{
+ verdict:"decline",
+ why:(c)=>`Best gross margin in the book at <b>${pct(c.gmPct)}</b>, but the 12-period trend is drifting <b class="neg">down</b> and inventory cover is thin at ${c.cover.toFixed(1)} months. Demand is inelastic and specification-driven — hardware is chosen after the fabric decision, so a modest price increase rarely loses the order.`,
+ action:"Take a 4–6% list increase on the top drapery-hardware SKUs and refresh their showroom placement to arrest the volume slide.",
+ calloutTone:"wn",
+ calloutText:"Thin cover + inelastic demand: reprice up before you reorder, not after.",
+ levers:[
+ {id:"priceUp", label:"List price increase", min:0, max:10, val:5, unit:"%"},
+ {id:"attrition", label:"Assumed volume attrition", min:0, max:8, val:2, unit:"%"}
+ ],
+ project:(c,l)=>{
+ const win = c.revenue*(l.priceUp/100) - c.revenue*(l.attrition/100)*c.gmPct;
+ return {win, base:c.contrib, note:"Net of assumed volume attrition valued at gross margin. Inelastic demand keeps attrition low."};
+ }
+ },
+ lighting:{
+ verdict:"steady",
+ why:(c)=>`A balanced, three-channel line trending gently up. Sample cost is already low (${money(c.sampleCost)}) and returns are middling at ${pct(c.returnRate)}. There's no crisis here — the opportunity is incremental: tighten the e-commerce return window and trim slow display-model samples.`,
+ action:"Shorten the e-commerce return window to 14 days and retire the lowest-turn display samples.",
+ calloutTone:"ok",
+ calloutText:"Healthy line — optimize, don't intervene. Spend your Monday on the flagged families first.",
+ levers:[
+ {id:"retWindow", label:"E-com returns avoided (tighter window)", min:0, max:40, val:20, unit:"%"},
+ {id:"sampleTrim", label:"Slow display samples retired", min:0, max:60, val:30, unit:"%"}
+ ],
+ project:(c,l)=>{
+ const win = c.returnCost*0.25*(l.retWindow/100)+c.sampleCost*(l.sampleTrim/100);
+ return {win, base:c.contrib, note:"Small, low-risk gains. Use as a fill-in, not a headline move."};
+ }
+ },
+ performance:{
+ verdict:"star",
+ why:(c)=>`Your model line: <b>${pct(c.gmPct)}</b> gross margin, the best sample conversion in the book (<b class="pos">${pct(c.sampleConv)}</b>), low returns, and a steadily rising trend. Contribution is solidly positive at ${money(c.contrib)}. There is little to <i>recover</i> here — the play is to <b>promote</b> and protect availability.`,
+ action:"Feature this family in the Monday buy and secure forward inventory before lead times stretch.",
+ calloutTone:"ok",
+ calloutText:"This is what 'working' looks like — use it as the benchmark for the leaking families.",
+ levers:[
+ {id:"promo", label:"Incremental volume from promotion", min:0, max:20, val:8, unit:"%"},
+ {id:"holdMargin", label:"Margin held through promo", min:80, max:100, val:96, unit:"%"}
+ ],
+ project:(c,l)=>{
+ const win = c.revenue*(l.promo/100)*c.gmPct*(l.holdMargin/100)*0.5;
+ return {win, base:c.contrib, note:"Upside from promotion, not recovery. Modeled conservatively at half the incremental gross margin."};
+ }
+ }
+};
+
+/* ---------------- Side panel ---------------- */
+function select(id){
+ state.selected = id;
+ document.querySelectorAll("#oppBody tr").forEach(tr=>tr.setAttribute("aria-selected", tr.dataset.id===id?"true":"false"));
+ const f = FAMILIES.find(x=>x.id===id);
+ const c = compute(f);
+ const d = DIAG[f.diagnosisKey];
+ // init assumptions from levers
+ assumptions = {};
+ d.levers.forEach(l=>assumptions[l.id]=l.val);
+ $("#spEmpty").hidden = true;
+ $("#spContent").hidden = false;
+ drawPanel(f,c,d);
+}
+
+function drawPanel(f,c,d){
+ const contradiction = c.contrib<0 && c.revenue>0;
+ const el = $("#spContent");
+ el.innerHTML = `
+ <div class="sp-head">
+ <div class="eyebrow">${({contradiction:"Contradictory signal",leak:"Margin leak",decline:"Softening line",steady:"Stable line",star:"Benchmark line",star:"Benchmark line"})[d.verdict]||"Diagnosis"}</div>
+ <h2>${f.name}</h2>
+ <div class="vend">${f.vendor} · ${state.channel==="all"?"all channels":state.channel} · ${RANGE_LABEL[state.range]}</div>
+ </div>
+
+ <div class="sp-metrics">
+ <div><div class="l">Revenue</div><div class="v">${money(c.revenue)}</div></div>
+ <div><div class="l">Contribution</div><div class="v ${c.contrib<0?'neg':'pos'}">${money(c.contrib)}</div></div>
+ <div><div class="l">Gross margin</div><div class="v">${pct(c.gmPct)}</div></div>
+ <div><div class="l">Contribution %</div><div class="v ${c.contribPct<0?'neg':''}">${pct(c.contribPct)}</div></div>
+ <div><div class="l">Sample conv.</div><div class="v ${f.sampleConv<0.15?'neg':''}">${pct(f.sampleConv)}</div></div>
+ <div><div class="l">Return rate</div><div class="v ${f.returnRate>0.09?'neg':''}">${pct(f.returnRate)}</div></div>
+ </div>
+
+ <div class="sp-block">
+ <h3>Trend · 12 periods</h3>
+ ${sparkline(f.trend)}
+ <p class="muted" style="font-size:11px;margin-top:2px">Revenue index, oldest → newest. ${trendWord(f.trend)}.</p>
+ </div>
+
+ <div class="sp-block">
+ <h3>Diagnosis</h3>
+ <p class="diag">${d.why(c)}</p>
+ <div class="callout ${d.calloutTone==='neg'?'':d.calloutTone==='ok'?'ok':'wn'}">${contradiction?"⚠ ":""}${d.calloutText}</div>
+ </div>
+
+ <div class="sp-block">
+ <h3>Recommended action</h3>
+ <div class="rec">
+ <div class="act">${d.action}</div>
+ <div class="assump" id="assumpBox">
+ ${d.levers.map(l=>`
+ <div>
+ <div class="row">
+ <label for="lev-${l.id}">${l.label}</label>
+ <span class="valbox num" id="val-${l.id}">${assumptions[l.id]}${l.unit}</span>
+ </div>
+ <input type="range" id="lev-${l.id}" min="${l.min}" max="${l.max}" step="1" value="${assumptions[l.id]}"
+ aria-label="${l.label}" data-lever="${l.id}" data-unit="${l.unit}">
+ </div>`).join("")}
+ </div>
+
+ <div class="impact" id="impactBox"></div>
+
+ <div class="btn-row">
+ <button class="btn primary" id="commitBtn">Add to Monday buy list</button>
+ <button class="reset-assump" id="resetAssump">Reset assumptions</button>
+ </div>
+ </div>
+ </div>`;
+
+ // wire sliders
+ el.querySelectorAll("input[type=range]").forEach(inp=>{
+ inp.addEventListener("input",()=>{
+ const id=inp.dataset.lever;
+ assumptions[id]=+inp.value;
+ $("#val-"+id).textContent = inp.value + inp.dataset.unit;
+ updateImpact(f,c,d);
+ });
+ });
+ $("#resetAssump").addEventListener("click",()=>{
+ d.levers.forEach(l=>{
+ assumptions[l.id]=l.val;
+ const inp=$("#lev-"+l.id); inp.value=l.val;
+ $("#val-"+l.id).textContent=l.val+l.unit;
+ });
+ updateImpact(f,c,d);
+ });
+ $("#commitBtn").addEventListener("click",(e)=>{
+ const b=e.currentTarget;
+ b.textContent="✓ Added to Monday buy list";
+ b.style.background="var(--ink)";
+ setTimeout(()=>{b.textContent="Add to Monday buy list";b.style.background="";},1900);
+ });
+ updateImpact(f,c,d);
+}
+
+function updateImpact(f,c,d){
+ const p = d.project(c, assumptions);
+ const windowGain = p.win;
+ // annualize the in-window recovery
+ const annual = windowGain * (RANGE_MULT["365"]/RANGE_MULT[state.range]);
+ const newContrib = p.base + windowGain;
+ const box = $("#impactBox");
+ const isUpside = d.verdict==="star" || d.verdict==="steady";
+ box.innerHTML = `
+ <div class="row"><span class="k">Recovered this ${RANGE_LABEL[state.range].replace('last ','').replace('trailing ','')}</span>
+ <span class="num pos">${money(windowGain)}</span></div>
+ <div class="row"><span class="k">Contribution after change</span>
+ <span class="num ${newContrib<0?'neg':''}">${money(newContrib)} ${p.base<0&&newContrib>=0?'<span class="chip g" style="margin-left:4px">turns positive</span>':''}</span></div>
+ <div class="row" style="align-items:flex-end;margin-top:4px">
+ <span class="k">Projected annual ${isUpside?'upside':'recovery'}</span>
+ <span class="big num pos">${money(annual)}</span>
+ </div>
+ <div class="lead">${p.note}</div>`;
+}
+
+/* ---------------- Sparkline + trend ---------------- */
+function sparkline(data){
+ const w=320,h=44,pad=3;
+ const min=Math.min(...data),max=Math.max(...data),rng=(max-min)||1;
+ const pts=data.map((v,i)=>{
+ const x=pad+(i/(data.length-1))*(w-2*pad);
+ const y=h-pad-((v-min)/rng)*(h-2*pad);
+ return [x,y];
+ });
+ const line=pts.map((p,i)=>(i?"L":"M")+p[0].toFixed(1)+" "+p[1].toFixed(1)).join(" ");
+ const area=line+` L ${pts[pts.length-1][0].toFixed(1)} ${h} L ${pts[0][0].toFixed(1)} ${h} Z`;
+ const up = data[data.length-1]>=data[0];
+ const col = up?"var(--pos)":"var(--neg)";
+ const last=pts[pts.length-1];
+ return `<svg class="spark" viewBox="0 0 ${w} ${h}" preserveAspectRatio="none" role="img" aria-label="12-period revenue trend, ${up?'rising':'falling'}">
+ <path d="${area}" fill="${up?'var(--pos-soft)':'var(--neg-soft)'}" opacity=".7"></path>
+ <path d="${line}" fill="none" stroke="${col}" stroke-width="1.8" stroke-linejoin="round"></path>
+ <circle cx="${last[0].toFixed(1)}" cy="${last[1].toFixed(1)}" r="2.6" fill="${col}"></circle>
+ </svg>`;
+}
+function trendWord(d){
+ const chg=(d[d.length-1]-d[0])/d[0];
+ if(chg>0.12) return "Clear upward trajectory";
+ if(chg>0.02) return "Gently rising";
+ if(chg<-0.1) return "Declining — needs attention";
+ if(chg<-0.02) return "Softening";
+ return "Flat / holding";
+}
+
+/* ---------------- Main render + states ---------------- */
+function render(){
+ const list = activeFamilies();
+ const showEmpty = list.length===0;
+ $("#emptyState").classList.toggle("show", showEmpty);
+ $("#tableWrap").style.display = showEmpty?"none":"block";
+ if(showEmpty){
+ $("#kpiStrip").innerHTML="";
+ $("#waterfall").innerHTML="";
+ } else {
+ renderKPIs(list);
+ renderWaterfall(list);
+ renderTable(list);
+ // keep selection valid
+ if(state.selected && !list.find(x=>x.f.id===state.selected)){
+ state.selected=null; $("#spContent").hidden=true; $("#spEmpty").hidden=false;
+ } else if(state.selected){
+ select(state.selected); // refresh panel with new range/filter figures
+ }
+ }
+ updateSortHeaders();
+}
+
+function updateSortHeaders(){
+ document.querySelectorAll("thead th").forEach(th=>{
+ if(th.dataset.key===state.sortKey){
+ th.setAttribute("aria-sort", state.sortDir==="asc"?"ascending":"descending");
+ th.querySelector(".arrow").textContent = state.sortDir==="asc"?"▴":"▾";
+ } else {
+ th.setAttribute("aria-sort","none");
+ }
+ });
+}
+
+/* ---------------- Wiring ---------------- */
+function initVendorOptions(){
+ const sel=$("#vendorSel");
+ [...new Set(FAMILIES.map(f=>f.vendor))].sort().forEach(v=>{
+ const o=document.createElement("option");o.value=v;o.textContent=v;sel.appendChild(o);
+ });
+}
+
+function wire(){
+ // range segmented
+ $("#rangeSeg").addEventListener("click",e=>{
+ const b=e.target.closest("button"); if(!b)return;
+ state.range=b.dataset.range;
+ $("#rangeSeg").querySelectorAll("button").forEach(x=>x.setAttribute("aria-pressed", x===b?"true":"false"));
+ withLoading(render);
+ });
+ $("#channelSel").addEventListener("change",e=>{state.channel=e.target.value;withLoading(render);});
+ $("#vendorSel").addEventListener("change",e=>{state.vendor=e.target.value;withLoading(render);});
+ const doReset=()=>{
+ state.channel="all";state.vendor="all";state.range="30";
+ $("#channelSel").value="all";$("#vendorSel").value="all";
+ $("#rangeSeg").querySelectorAll("button").forEach(x=>x.setAttribute("aria-pressed", x.dataset.range==="30"?"true":"false"));
+ withLoading(render);
+ };
+ $("#resetFilters").addEventListener("click",doReset);
+ $("#emptyReset").addEventListener("click",doReset);
+
+ // sort headers
+ document.querySelectorAll("thead th").forEach(th=>{
+ th.tabIndex=0;
+ const act=()=>{
+ const k=th.dataset.key;
+ if(state.sortKey===k) state.sortDir = state.sortDir==="asc"?"desc":"asc";
+ else { state.sortKey=k; state.sortDir = (k==="name")?"asc":"desc"; }
+ render();
+ };
+ th.addEventListener("click",act);
+ th.addEventListener("keydown",e=>{if(e.key==="Enter"||e.key===" "){e.preventDefault();act();}});
+ });
+
+ // stale-data flag + refresh
+ $("#refreshBtn").addEventListener("click",()=>{
+ $("#staleFlag").classList.remove("show");
+ withLoading(()=>{ render(); });
+ $("#staleText").textContent="Data refreshed just now.";
+ });
+}
+
+// Loading-state wrapper — briefly show skeleton to make async states real.
+let loadTimer=null;
+function withLoading(fn){
+ const ls=$("#loadingState"), tw=$("#tableWrap"), es=$("#emptyState");
+ ls.classList.add("show"); tw.style.display="none"; es.classList.remove("show");
+ clearTimeout(loadTimer);
+ loadTimer=setTimeout(()=>{
+ ls.classList.remove("show");
+ fn();
+ }, 430);
+}
+
+/* ---------------- Boot ---------------- */
+initVendorOptions();
+wire();
+withLoading(()=>{
+ render();
+ // surface the stale-data banner shortly after first paint
+ setTimeout(()=>$("#staleFlag").classList.add("show"), 900);
+});
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/4c21edc43a09/qwen25-7b.html b/data/artifacts/4c21edc43a09/qwen25-7b.html
new file mode 100644
index 0000000..4cdd78e
--- /dev/null
+++ b/data/artifacts/4c21edc43a09/qwen25-7b.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Margin Intelligence Workspace</title>
+ <style>
+ * { box-sizing: border-box; }
+ body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f4f4f9; }
+ .container { width: 100%; max-width: 1440px; margin: auto; overflow-x: hidden; }
+ header, footer { padding: 20px; text-align: center; background-color: white; }
+ .main-content { padding: 20px; background-color: #fff; min-height: calc(100vh - 60px); }
+ .filters { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
+ .date-range, .channel-filters, .vendor-filters { width: 30%; }
+ table { width: 100%; border-collapse: collapse; font-size: 0.9em; }
+ th, td { padding: 8px 16px; text-align: left; border-bottom: 1px solid #ddd; }
+ th { background-color: #f2f2f2; }
+ .side-panel { width: 33%; float: right; margin-left: 10px; }
+ .side-panel h2, .side-panel ul { margin: 0; padding: 0; list-style-type: none; }
+ .side-panel li { margin-bottom: 8px; }
+ .waterfall-chart { width: 100%; height: 300px; background-color: #e9ecef; margin-top: 20px; overflow-y: scroll; }
+ </style>
+</head>
+<body>
+ <div class="container">
+ <header>
+ Margin Intelligence Workspace
+ </header>
+ <main class="main-content">
+ <div class="filters">
+ <label>Date Range:</label>
+ <input type="date" id="startDate" value="2023-10-01">
+ <input type="date" id="endDate" value="2023-10-31">
+ <div class="channel-filters">
+ <h4>Channels</h4>
+ <label><input type="checkbox"> Online</label>
+ <label><input type="checkbox"> Offline</label>
+ </div>
+ <div class="vendor-filters">
+ <h4>Vendors</h4>
+ <label><input type="checkbox"> Vendor A</label>
+ <label><input type="checkbox"> Vendor B</label>
+ </div>
+ </div>
+ <table id="opportunityTable">
+ <thead>
+ <tr>
+ <th>Product Family</th>
+ <th>Revenue ($)</th>
+ <th>Units</th>
+ <th>Gross Margin (%)</th>
+ <th>Sample Conversion Rate (%)</th>
+ <th>Return Rate (%)</th>
+ <th>Inventory Cover (#)</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr onclick="updateSidePanel(this)">
+ <td>Living Room Furniture</td>
+ <td>$120,000</td>
+ <td>500</td>
+ <td>45%</td>
+ <td>8.3%</td>
+ <td>3.2%</td>
+ <td>3x</td>
+ </tr>
+ <!-- More rows here -->
+ </tbody>
+ </table>
+ </main>
+ <aside class="side-panel">
+ <h2>Trend & Diagnosis</h2>
+ <ul>
+ <li>Revenue has decreased by 5% in the last month.</li>
+ <li>Gross margin is negatively impacted due to increased return rates.</li>
+ </ul>
+ <h3>Recommended Action</h3>
+ <p><strong>Action:</strong> Implement a quality check process for Living Room Furniture items.</p>
+ </aside>
+ </div>
+ <footer>
+ © 2023 Margin Intelligence Workspace
+ </footer>
+
+ <script>
+ function updateSidePanel(row) {
+ const trendDiagnosis = document.querySelector('.side-panel ul');
+ const recommendationAction = document.querySelector('.side-panel p');
+ if (row) {
+ trendDiagnosis.innerHTML = `
+ <li>Revenue has decreased by 10% in the last month.</li>
+ <li>Gross margin is negatively impacted due to increased return rates and lower sample conversion rate.</li>
+ `;
+ recommendationAction.textContent = 'Action: Implement a quality check process for Living Room Furniture items and improve marketing strategies.';
+ } else {
+ trendDiagnosis.innerHTML = '';
+ recommendationAction.textContent = 'No action recommended at this time.';
+ }
+ }
+
+ document.getElementById('opportunityTable').addEventListener('click', (event) => {
+ if (event.target.tagName === 'TR') {
+ updateSidePanel(event.target);
+ }
+ });
+ </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/4c21edc43a09/qwen3-14b.html b/data/artifacts/4c21edc43a09/qwen3-14b.html
new file mode 100644
index 0000000..f8c9640
--- /dev/null
+++ b/data/artifacts/4c21edc43a09/qwen3-14b.html
@@ -0,0 +1,490 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Margin Intelligence Workspace</title>
+ <style>
+ * {
+ box-sizing: border-box;
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ margin: 0;
+ padding: 0;
+ }
+
+ body {
+ background-color: #f5f5f5;
+ color: #2d2d2d;
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ overflow: hidden;
+ }
+
+ header {
+ background: #2d2d2d;
+ color: #fff;
+ padding: 12px 24px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 18px;
+ font-weight: 600;
+ }
+
+ .toolbar {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ }
+
+ .toolbar select,
+ .toolbar input {
+ padding: 6px;
+ font-size: 14px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ }
+
+ .toolbar label {
+ font-size: 12px;
+ margin-right: 4px;
+ }
+
+ main {
+ flex: 1;
+ display: flex;
+ overflow: hidden;
+ }
+
+ .table-container {
+ flex: 1;
+ overflow: auto;
+ padding: 12px;
+ }
+
+ table {
+ width: 100%;
+ border-collapse: collapse;
+ background: #fff;
+ border-radius: 6px;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+ }
+
+ th,
+ td {
+ padding: 10px 12px;
+ border-bottom: 1px solid #eee;
+ text-align: left;
+ vertical-align: middle;
+ }
+
+ th {
+ background: #f0f0f0;
+ font-weight: 600;
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ }
+
+ td:focus-within,
+ th:focus-within {
+ outline: 2px solid #007bff;
+ }
+
+ tr:hover {
+ background: #f9f9f9;
+ }
+
+ .waterfall {
+ background: #fff;
+ padding: 16px;
+ border-radius: 6px;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+ margin-top: 12px;
+ }
+
+ .waterfall h3 {
+ margin-bottom: 8px;
+ font-size: 16px;
+ font-weight: 600;
+ }
+
+ .waterfall .bar {
+ display: flex;
+ align-items: center;
+ margin-bottom: 8px;
+ }
+
+ .waterfall .bar label {
+ flex: 1;
+ font-size: 12px;
+ text-align: right;
+ opacity: 0.7;
+ }
+
+ .waterfall .bar .value {
+ flex: 2;
+ text-align: right;
+ font-size: 14px;
+ font-weight: 600;
+ }
+
+ .waterfall .bar .color {
+ flex: 1;
+ min-width: 100px;
+ height: 16px;
+ border-radius: 4px;
+ background: #ccc;
+ }
+
+ .waterfall .bar .positive {
+ background: #4caf50;
+ }
+
+ .waterfall .bar .negative {
+ background: #f44336;
+ }
+
+ .side-panel {
+ background: #fff;
+ padding: 16px;
+ border-radius: 6px;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+ width: 300px;
+ overflow-y: auto;
+ max-height: 100%;
+ position: sticky;
+ top: 0;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .side-panel h2 {
+ margin-bottom: 12px;
+ font-size: 16px;
+ font-weight: 600;
+ color: #2d2d2d;
+ }
+
+ .side-panel .section {
+ margin-bottom: 16px;
+ }
+
+ .side-panel .section label {
+ display: block;
+ margin-bottom: 4px;
+ font-size: 12px;
+ color: #666;
+ }
+
+ .side-panel .section input,
+ .side-panel .section select {
+ width: 100%;
+ padding: 6px;
+ font-size: 14px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ }
+
+ .side-panel .action {
+ margin-top: 16px;
+ }
+
+ .side-panel .action p {
+ font-size: 12px;
+ margin-bottom: 4px;
+ }
+
+ .side-panel .action .impact {
+ font-weight: 600;
+ }
+
+ .status {
+ padding: 12px;
+ text-align: center;
+ font-size: 16px;
+ font-weight: 600;
+ color: #777;
+ }
+
+ .loading {
+ color: #007bff;
+ }
+
+ .no-results {
+ color: #999;
+ }
+
+ .stale {
+ color: #e67e22;
+ }
+
+ @media (max-width: 768px) {
+ main {
+ flex-direction: column;
+ }
+
+ .side-panel {
+ width: 100%;
+ }
+ }
+
+ @media (max-width: 390px) {
+ header {
+ font-size: 14px;
+ }
+
+ .toolbar select,
+ .toolbar input {
+ font-size: 12px;
+ }
+
+ th,
+ td {
+ padding: 8px;
+ font-size: 12px;
+ }
+
+ .side-panel {
+ padding: 8px;
+ }
+
+ .side-panel .section label,
+ .side-panel .section input {
+ font-size: 10px;
+ }
+
+ .side-panel h2 {
+ font-size: 14px;
+ }
+ }
+ </style>
+</head>
+<body>
+ <header>
+ <div>Margin Intelligence Workspace</div>
+ <div class="toolbar">
+ <label for="date-range">Date:</label>
+ <select id="date-range">
+ <option>Last 7 Days</option>
+ <option>Last 30 Days</option>
+ <option>Last 90 Days</option>
+ </select>
+ <label for="channel-filter">Channel:</label>
+ <input type="text" id="channel-filter" placeholder="e.g., Online" />
+ <label for="vendor-filter">Vendor:</label>
+ <input type="text" id="vendor-filter" placeholder="e.g., Designer Brands" />
+ </div>
+ </header>
+
+ <main>
+ <div class="table-container">
+ <table id="opportunity-table">
+ <thead>
+ <tr>
+ <th>Product Family</th>
+ <th>Revenue ($)</th>
+ <th>Units</th>
+ <th>Gross Margin (%)</th>
+ <th>Sample Conversion (%)</th>
+ <th>Return Rate (%)</th>
+ <th>Inventory Cover</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td data-type="name">Modern Lighting</td>
+ <td data-type="revenue">120000</td>
+ <td data-type="units">1500</td>
+ <td data-type="gross-margin">35</td>
+ <td data-type="sample-conversion">50</td>
+ <td data-type="return-rate">8</td>
+ <td data-type="inventory-cover">1.2</td>
+ </tr>
+ <tr>
+ <td data-type="name">Vintage Furniture</td>
+ <td data-type="revenue">95000</td>
+ <td data-type="units">800</td>
+ <td data-type="gross-margin">22</td>
+ <td data-type="sample-conversion">30</td>
+ <td data-type="return-rate">15</td>
+ <td data-type="inventory-cover">0.8</td>
+ </tr>
+ <tr>
+ <td data-type="name">Contemporary Rugs</td>
+ <td data-type="revenue">180000</td>
+ <td data-type="units">1200</td>
+ <td data-type="gross-margin">40</td>
+ <td data-type="sample-conversion">45</td>
+ <td data-type="return-rate">6</td>
+ <td data-type="inventory-cover">2.0</td>
+ </tr>
+ <tr>
+ <td data-type="name">Industrial Fixtures</td>
+ <td data-type="revenue">210000</td>
+ <td data-type="units">1400</td>
+ <td data-type="gross-margin">28</td>
+ <td data-type="sample-conversion">20</td>
+ <td data-type="return-rate">12</td>
+ <td data-type="inventory-cover">1.5</td>
+ </tr>
+ <tr>
+ <td data-type="name">Handcrafted Tables</td>
+ <td data-type="revenue">60000</td>
+ <td data-type="units">500</td>
+ <td data-type="gross-margin">20</td>
+ <td data-type="sample-conversion">40</td>
+ <td data-type="return-rate">20</td>
+ <td data-type="inventory-cover">0.6</td>
+ </tr>
+ <tr>
+ <td data-type="name">Artisan Ceramics</td>
+ <td data-type="revenue">30000</td>
+ <td data-type="units">300</td>
+ <td data-type="gross-margin">-5</td>
+ <td data-type="sample-conversion">10</td>
+ <td data-type="return-rate">30</td>
+ <td data-type="inventory-cover">1.0</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="side-panel" id="side-panel">
+ <div class="status loading">Loading...</div>
+ </div>
+ </main>
+
+ <div class="waterfall">
+ <h3>Contribution Margin Waterfall</h3>
+ <div class="bar">
+ <label>Revenue</label>
+ <div class="value">$0</div>
+ <div class="color"></div>
+ </div>
+ <div class="bar">
+ <label>COGS</label>
+ <div class="value">$0</div>
+ <div class="color"></div>
+ </div>
+ <div class="bar">
+ <label>Gross Margin</label>
+ <div class="value">$0</div>
+ <div class="color"></div>
+ </div>
+ <div class="bar">
+ <label>Marketing Cost</label>
+ <div class="value">$0</div>
+ <div class="color"></div>
+ </div>
+ <div class="bar">
+ <label>Contribution Margin</label>
+ <div class="value">$0</div>
+ <div class="color"></div>
+ </div>
+ </div>
+
+ <script>
+ const table = document.getElementById('opportunity-table');
+ const rows = table.querySelectorAll('tbody tr');
+ const sidePanel = document.getElementById('side-panel');
+ const waterfallBars = document.querySelectorAll('.waterfall .bar .value');
+ const status = document.querySelector('.status');
+
+ function updateWaterfall(name, revenue, margin) {
+ const revenueBar = document.querySelector('.bar:nth-child(1) .value');
+ const cogsBar = document.querySelector('.bar:nth-child(2) .value');
+ const grossBar = document.querySelector('.bar:nth-child(3) .value');
+ const marketingBar = document.querySelector('.bar:nth-child(4) .value');
+ const contribBar = document.querySelector('.bar:nth-child(5) .value');
+
+ const rev = revenue || 0;
+ const cogs = rev * (100 - margin) / 100;
+ const gross = rev - cogs;
+ const marketing = rev * 0.10; // 10% assumed
+ const contrib = gross - marketing;
+
+ revenueBar.textContent = `$${rev.toFixed(0)}`;
+ cogsBar.textContent = `$${cogs.toFixed(0)}`;
+ grossBar.textContent = `$${gross.toFixed(0)}`;
+ marketingBar.textContent = `$${marketing.toFixed(0)}`;
+ contribBar.textContent = `$${contrib.toFixed(0)}`;
+
+ const contribColor = contrib >= 0 ? 'positive' : 'negative';
+ contribBar.parentElement.querySelector('.color').className = 'color ' + contribColor;
+ }
+
+ function updateSidePanel(product) {
+ sidePanel.innerHTML = `
+ <h2>${product}</h2>
+ <div class="section">
+ <label>Trend</label>
+ <input type="text" value="High revenue but negative contribution" readonly />
+ </div>
+ <div class="section">
+ <label>Diagnosis</label>
+ <input type="text" value="High return rate and low gross margin" readonly />
+ </div>
+ <div class="section">
+ <label>Recommended Action</label>
+ <input type="text" value="Pause vendor" readonly />
+ </div>
+ <div class="section">
+ <label>Assumptions</label>
+ <input type="number" placeholder="Return rate reduction (%)" />
+ <input type="number" placeholder="Gross margin improvement (%)" />
+ </div>
+ <div class="action">
+ <p>Projected impact on contribution margin: <span class="impact">$0</span></p>
+ </div>
+ `;
+ }
+
+ function applyFilters() {
+ const dateRange = document.getElementById('date-range').value;
+ const channelFilter = document.getElementById('channel-filter').value.toLowerCase();
+ const vendorFilter = document.getElementById('vendor-filter').value.toLowerCase();
+
+ rows.forEach(row => {
+ const name = row.querySelector('[data-type="name"]').textContent.toLowerCase();
+ const vendor = row.getAttribute('data-vendor')?.toLowerCase() || '';
+ const channel = row.getAttribute('data-channel')?.toLowerCase() || '';
+
+ const matchesDate = true; // Assume date range filter is applied in backend
+ const matchesChannel = channel.includes(channelFilter);
+ const matchesVendor = vendor.includes(vendorFilter);
+
+ if (matchesDate && matchesChannel && matchesVendor) {
+ row.style.display = '';
+ } else {
+ row.style.display = 'none';
+ }
+ });
+ }
+
+ function loadState() {
+ status.classList.remove('loading', 'no-results', 'stale');
+ status.textContent = 'Loading...';
+ setTimeout(() => {
+ status.classList.remove('loading');
+ status.classList.add('stale');
+ status.textContent = 'Data is stale. Refresh for new insights.';
+ }, 2000);
+ }
+
+ function init() {
+ applyFilters();
+ loadState();
+ rows.forEach(row => {
+ row.addEventListener('click', () => {
+ const name = row.querySelector('[data-type="name"]').textContent;
+ updateSidePanel(name);
+ status.classList.remove('loading', 'no-results', 'stale');
+ });
+ });
+ }
+
+ init();
+ </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/6e2d6635424a/claude-code.html b/data/artifacts/6e2d6635424a/claude-code.html
new file mode 100644
index 0000000..d0a557c
--- /dev/null
+++ b/data/artifacts/6e2d6635424a/claude-code.html
@@ -0,0 +1,765 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>RailOps — Evening Rush Recovery Console</title>
+<style>
+:root{
+ --bg:#0e1116; --panel:#151a22; --panel2:#1b212b; --edge:#2a3340; --edge2:#38455a;
+ --ink:#e6ecf3; --ink2:#9fb0c3; --ink3:#6b7c92; --accent:#4aa8ff;
+ /* color-blind-safe status palette (Okabe-Ito derived) + glyph pairing */
+ --s-crit:#e8608c; --s-high:#e69f00; --s-med:#f0e442; --s-low:#33bbc7; --s-ok:#4a90ff; --s-unk:#9a8cff;
+ --good:#00b894; --bad:#e8608c; --warn:#e69f00;
+ --mono:"SFMono-Regular",Consolas,"Roboto Mono",monospace;
+ --sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
+}
+*{box-sizing:border-box}
+html,body{margin:0;height:100%}
+body{background:var(--bg);color:var(--ink);font-family:var(--sans);font-size:12.5px;line-height:1.35;overflow:hidden}
+button,input,select{font-family:inherit;font-size:inherit;color:inherit}
+button{cursor:pointer;background:var(--panel2);border:1px solid var(--edge);border-radius:4px;padding:3px 7px}
+button:hover{border-color:var(--edge2);background:#222b38}
+button:focus-visible,[tabindex]:focus-visible,input:focus-visible,select:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
+.kbd{font-family:var(--mono);font-size:9.5px;background:#0a0d12;border:1px solid var(--edge);border-radius:3px;padding:0 4px;color:var(--ink3)}
+h1,h2,h3,h4{margin:0;font-weight:600}
+.mono{font-family:var(--mono)}
+.muted{color:var(--ink3)}
+.dim{color:var(--ink2)}
+hr{border:0;border-top:1px solid var(--edge);margin:6px 0}
+
+/* ---- topbar ---- */
+#top{height:40px;display:flex;align-items:center;gap:14px;padding:0 12px;background:linear-gradient(#1a212c,#141a22);border-bottom:1px solid var(--edge)}
+#top .brand{font-weight:700;letter-spacing:.5px}
+#top .brand b{color:var(--accent)}
+.pill{display:inline-flex;align-items:center;gap:5px;padding:2px 8px;border-radius:11px;border:1px solid var(--edge);background:var(--panel2);font-size:11px}
+#clock{font-family:var(--mono);color:var(--ink2)}
+#simbadge{margin-left:auto;display:none;align-items:center;gap:8px;padding:3px 10px;border-radius:5px;border:1px dashed var(--warn);background:rgba(230,159,0,.09);color:var(--warn);font-weight:600}
+#simbadge.on{display:inline-flex}
+
+/* ---- layout ---- */
+#grid{display:grid;grid-template-columns:300px minmax(360px,1.35fr) 380px;height:calc(100% - 40px)}
+.col{overflow:auto;border-right:1px solid var(--edge);padding:10px}
+.col:last-child{border-right:0}
+.sect{background:var(--panel);border:1px solid var(--edge);border-radius:7px;margin-bottom:10px}
+.sect>.hd{display:flex;align-items:center;gap:6px;padding:7px 9px;border-bottom:1px solid var(--edge);font-size:11px;letter-spacing:.4px;text-transform:uppercase;color:var(--ink2)}
+.sect>.hd .r{margin-left:auto;font-size:10px}
+.sect>.bd{padding:9px}
+
+/* ---- network map ---- */
+#map{width:100%;height:auto;display:block;background:#0b0e13;border-radius:5px}
+.node{cursor:pointer}
+.legend{display:flex;flex-wrap:wrap;gap:5px;margin-top:7px}
+.lg{display:inline-flex;align-items:center;gap:4px;font-size:10px;color:var(--ink2)}
+.lg i{width:9px;height:9px;border-radius:50%;display:inline-block}
+
+/* status token */
+.st{display:inline-flex;align-items:center;gap:4px;padding:1px 6px;border-radius:4px;font-size:10px;font-weight:600;white-space:nowrap;border:1px solid transparent}
+.st .g{font-family:var(--mono)}
+.st-crit{background:rgba(232,96,140,.16);color:var(--s-crit);border-color:rgba(232,96,140,.4)}
+.st-high{background:rgba(230,159,0,.16);color:var(--s-high);border-color:rgba(230,159,0,.4)}
+.st-med{background:rgba(240,228,66,.14);color:#d8c93a;border-color:rgba(240,228,66,.35)}
+.st-low{background:rgba(51,187,199,.15);color:var(--s-low);border-color:rgba(51,187,199,.4)}
+.st-ok{background:rgba(74,144,255,.16);color:var(--s-ok);border-color:rgba(74,144,255,.4)}
+
+/* filters */
+.frow{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:6px}
+.chip{padding:2px 8px;border-radius:11px;border:1px solid var(--edge);background:var(--panel2);font-size:10.5px;color:var(--ink2);user-select:none}
+.chip.on{background:#26364a;border-color:var(--accent);color:var(--ink)}
+#q{width:100%;background:#0b0e13;border:1px solid var(--edge);border-radius:5px;padding:5px 7px}
+
+/* incident list */
+.inc{position:relative;padding:7px 9px 7px 12px;border:1px solid var(--edge);border-radius:6px;margin-bottom:6px;background:var(--panel2);cursor:pointer}
+.inc:hover{border-color:var(--edge2)}
+.inc.sel{border-color:var(--accent);background:#1a2534;box-shadow:0 0 0 1px var(--accent) inset}
+.inc .bar{position:absolute;left:0;top:0;bottom:0;width:4px;border-radius:6px 0 0 6px}
+.inc .l1{display:flex;align-items:center;gap:6px}
+.inc .l1 .id{font-family:var(--mono);color:var(--ink3);font-size:10px}
+.inc .l1 .ttl{font-weight:600;font-size:12.5px}
+.inc .l2{display:flex;align-items:center;gap:6px;margin-top:4px;flex-wrap:wrap}
+.tag{font-size:9.5px;padding:1px 5px;border-radius:3px;background:#0d1219;border:1px solid var(--edge);color:var(--ink2)}
+.tag.warnflag{color:var(--warn);border-color:rgba(230,159,0,.4)}
+.tag.stale{color:var(--s-unk);border-color:rgba(154,140,255,.4)}
+.risk{margin-left:auto;font-family:var(--mono);font-weight:700}
+.load-mini{height:4px;border-radius:2px;background:#0a0d12;overflow:hidden;margin-top:5px}
+.load-mini i{display:block;height:100%}
+
+/* detail */
+.kv{display:grid;grid-template-columns:96px 1fr;gap:3px 8px;font-size:11.5px}
+.kv .k{color:var(--ink3)}
+.tl{border-left:2px solid var(--edge);margin:4px 0 4px 6px;padding-left:12px}
+.tl .ev{position:relative;padding:4px 0}
+.tl .ev::before{content:"";position:absolute;left:-17px;top:8px;width:8px;height:8px;border-radius:50%;background:var(--accent);border:2px solid var(--panel)}
+.tl .ev.conflict::before{background:var(--warn)}
+.tl .ev .t{font-family:var(--mono);font-size:10px;color:var(--ink3)}
+.tl .ev.conflict .msg{color:var(--warn)}
+.actbar{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
+
+/* plans */
+.plan{border:1px solid var(--edge);border-radius:7px;margin-bottom:8px;overflow:hidden}
+.plan.win{border-color:var(--good);box-shadow:0 0 0 1px rgba(0,184,148,.4)}
+.plan .ph{display:flex;align-items:center;gap:8px;padding:7px 9px;background:var(--panel2)}
+.plan .ph .nm{font-weight:700}
+.plan .ph .sc{margin-left:auto;font-family:var(--mono);font-weight:700;font-size:15px}
+.plan .win-b{font-size:9px;background:var(--good);color:#062;padding:1px 6px;border-radius:9px;font-weight:700}
+.plan .pb{padding:8px 9px;border-top:1px solid var(--edge);display:none}
+.plan.open .pb{display:block}
+.crit{display:grid;grid-template-columns:1fr auto;gap:2px 6px;align-items:center;margin:3px 0}
+.crit .lab{font-size:10.5px;color:var(--ink2)}
+.crit .track{grid-column:1/2;height:8px;background:#0a0d12;border-radius:4px;overflow:hidden}
+.crit .track i{display:block;height:100%;background:var(--accent)}
+.crit .val{grid-column:2;font-family:var(--mono);font-size:10px;color:var(--ink2);width:58px;text-align:right}
+
+/* weights */
+.wt{display:grid;grid-template-columns:78px 1fr 34px;gap:6px;align-items:center;margin:6px 0}
+.wt label{font-size:11px}
+.wt input[type=range]{width:100%}
+.wt .n{font-family:var(--mono);text-align:right}
+#wsum{font-family:var(--mono)}
+#why{font-size:11px;background:#0c1017;border:1px solid var(--edge);border-radius:6px;padding:8px;margin-top:6px}
+#why b{color:var(--ink)}
+#why .flip{color:var(--warn)}
+
+/* decision log */
+#log{max-height:230px;overflow:auto}
+.le{display:flex;gap:7px;padding:4px 0;border-bottom:1px dashed var(--edge)}
+.le .t{font-family:var(--mono);font-size:10px;color:var(--ink3);white-space:nowrap}
+.le .m{font-size:11px}
+.le .m b{color:var(--ink)}
+
+/* modal */
+#ov{position:fixed;inset:0;background:rgba(4,7,11,.72);display:none;align-items:center;justify-content:center;z-index:40}
+#ov.on{display:flex}
+.modal{width:min(440px,92vw);background:var(--panel);border:1px solid var(--edge2);border-radius:9px;box-shadow:0 20px 60px rgba(0,0,0,.6)}
+.modal .mh{padding:11px 13px;border-bottom:1px solid var(--edge);display:flex;align-items:center;gap:8px}
+.modal .mb{padding:13px}
+.modal .mf{padding:11px 13px;border-top:1px solid var(--edge);display:flex;gap:8px;justify-content:flex-end}
+.btn-p{background:var(--accent);border-color:var(--accent);color:#04203f;font-weight:700}
+.btn-d{background:rgba(232,96,140,.15);border-color:var(--s-crit);color:var(--s-crit);font-weight:700}
+.warnbox{background:rgba(230,159,0,.1);border:1px solid rgba(230,159,0,.4);border-radius:6px;padding:8px;font-size:11.5px;color:#f0d089;margin-top:8px}
+select.mini{background:#0b0e13;border:1px solid var(--edge);border-radius:4px;padding:3px 5px}
+
+/* toast */
+#toast{position:fixed;bottom:14px;left:50%;transform:translateX(-50%);background:#10202f;border:1px solid var(--accent);color:var(--ink);padding:8px 14px;border-radius:6px;display:none;z-index:60;font-size:12px;box-shadow:0 6px 20px rgba(0,0,0,.5)}
+#toast.on{display:block}
+
+/* responsive 390px command view */
+#tabbar{display:none}
+@media(max-width:900px){
+ #grid{grid-template-columns:1fr;grid-template-rows:1fr}
+ .col{display:none;border-right:0}
+ .col.active{display:block}
+ #tabbar{display:flex;height:38px;border-top:1px solid var(--edge);background:var(--panel)}
+ #tabbar button{flex:1;border:0;border-radius:0;border-right:1px solid var(--edge);background:transparent;font-size:11px;padding:0}
+ #tabbar button.on{background:#1c2635;color:var(--accent);font-weight:700;box-shadow:inset 0 -2px 0 var(--accent)}
+ #simbadge span.hint{display:none}
+}
+</style>
+</head>
+<body>
+<div id="top">
+ <div class="brand">RAIL<b>OPS</b></div>
+ <span class="pill" title="Evening rush window">🌆 Evening Rush · 17:42–19:30</span>
+ <span class="pill" id="clock">--:--:--</span>
+ <span class="pill muted">Ctrl: <span class="kbd">/</span> search · <span class="kbd">↑↓</span> select · <span class="kbd">S</span> severity · <span class="kbd">A</span> assign · <span class="kbd">M</span> sim · <span class="kbd">U</span> undo</span>
+ <div id="simbadge"><span>⚙ SIMULATION MODE</span> <span class="hint">— changes are sandboxed</span> <button id="simEnd">Exit & keep</button> <button id="simDiscard" class="btn-d" style="padding:2px 8px">Discard all</button></div>
+</div>
+
+<div id="grid">
+ <!-- ============ COL 1: MAP + FILTERS ============ -->
+ <div class="col active" id="colMap" data-tab="map">
+ <div class="sect">
+ <div class="hd">Network Schematic <span class="r muted">click a station → incident</span></div>
+ <div class="bd">
+ <svg id="map" viewBox="0 0 280 250" role="img" aria-label="City rail schematic map"></svg>
+ <div class="legend">
+ <span class="lg"><i style="background:var(--s-crit)"></i>▲ Critical</span>
+ <span class="lg"><i style="background:var(--s-high)"></i>◆ High</span>
+ <span class="lg"><i style="background:var(--s-med)"></i>● Medium</span>
+ <span class="lg"><i style="background:var(--s-low)"></i>■ Low</span>
+ <span class="lg"><i style="background:var(--s-ok)"></i>✔ Recovered</span>
+ <span class="lg"><i style="background:var(--s-unk)"></i>◇ Stale telem.</span>
+ </div>
+ </div>
+ </div>
+ <div class="sect">
+ <div class="hd">Filter Incidents</div>
+ <div class="bd">
+ <input id="q" placeholder="Search id / title / station…" aria-label="Search incidents">
+ <div style="height:7px"></div>
+ <div class="muted" style="font-size:10px;margin-bottom:3px">LINE</div>
+ <div class="frow" id="fLines"></div>
+ <div class="muted" style="font-size:10px;margin-bottom:3px">SEVERITY</div>
+ <div class="frow" id="fSev"></div>
+ <div class="frow"><span class="chip" data-flag="crew">⚠ No crew</span><span class="chip" data-flag="stale">◇ Stale telemetry</span><span class="chip" data-flag="conflict">⚡ Conflicting</span><span class="chip" data-flag="open">Exclude recovered</span></div>
+ </div>
+ </div>
+ <div class="sect">
+ <div class="hd">Crew Availability</div>
+ <div class="bd" id="crewBox" style="font-size:11px"></div>
+ </div>
+ </div>
+
+ <!-- ============ COL 2: INCIDENTS + DETAIL ============ -->
+ <div class="col" id="colInc" data-tab="inc">
+ <div class="sect">
+ <div class="hd">Active Incidents <span class="r muted" id="incCount"></span></div>
+ <div class="bd" id="incList"></div>
+ </div>
+ <div class="sect" id="detailSect">
+ <div class="hd">Incident Detail & Timeline <span class="r muted" id="detailId"></span></div>
+ <div class="bd" id="detail"><div class="muted">Select an incident to inspect its telemetry, timeline and response options.</div></div>
+ </div>
+ </div>
+
+ <!-- ============ COL 3: PLANS + WEIGHTS + LOG ============ -->
+ <div class="col" id="colPlan" data-tab="plan">
+ <div class="sect">
+ <div class="hd">Decision Criteria Weights <span class="r"><span id="wsum">100</span>%</span></div>
+ <div class="bd" id="weights"></div>
+ </div>
+ <div class="sect">
+ <div class="hd">Recovery Plan Comparison</div>
+ <div class="bd">
+ <div id="plans"></div>
+ <div id="why" aria-live="polite"></div>
+ </div>
+ </div>
+ <div class="sect">
+ <div class="hd">Decision Log <span class="r"><button id="expLog" style="font-size:10px;padding:1px 6px">Export</button></span></div>
+ <div class="bd"><div id="log"></div></div>
+ </div>
+ </div>
+</div>
+
+<div id="tabbar">
+ <button data-t="map" class="on">Map</button>
+ <button data-t="inc">Incidents</button>
+ <button data-t="plan">Plans</button>
+</div>
+
+<!-- confirmation / action modal -->
+<div id="ov"><div class="modal" role="dialog" aria-modal="true">
+ <div class="mh"><span id="mIcon">⚠</span><h3 id="mTitle">Confirm</h3></div>
+ <div class="mb" id="mBody"></div>
+ <div class="mf" id="mFoot"></div>
+</div></div>
+<div id="toast"></div>
+
+<script>
+"use strict";
+/* ============================ DATA (seed) ============================ */
+const LINES = {
+ RED: {name:"Red / Harbor", color:"#e8608c"},
+ BLU: {name:"Blue / Central",color:"#4a90ff"},
+ GRN: {name:"Green / Uni", color:"#00b894"},
+ YEL: {name:"Yellow / Airport",color:"#e6a700"}
+};
+const SEV = ["crit","high","med","low","ok"];
+const SEVMETA = {
+ crit:{lab:"Critical",g:"▲",cls:"st-crit",col:"var(--s-crit)",rank:4},
+ high:{lab:"High", g:"◆",cls:"st-high",col:"var(--s-high)",rank:3},
+ med: {lab:"Medium", g:"●",cls:"st-med", col:"#d8c93a",rank:2},
+ low: {lab:"Low", g:"■",cls:"st-low", col:"var(--s-low)",rank:1},
+ ok: {lab:"Recovered",g:"✔",cls:"st-ok", col:"var(--s-ok)",rank:0}
+};
+
+// crew teams
+let CREW = [
+ {id:"T-Alpha", type:"Track", base:"Union", status:"available"},
+ {id:"T-Bravo", type:"Track", base:"Harbor", status:"deployed", to:"I-2"},
+ {id:"P-Power", type:"Power", base:"Central", status:"available"},
+ {id:"S-Signal",type:"Signals",base:"Junction",status:"deployed", to:"I-1"},
+ {id:"M-Med", type:"Medical",base:"Central", status:"available"},
+ {id:"R-Recov", type:"Rescue", base:"Airport", status:"unavailable", note:"weather standby"}
+];
+
+// stations for schematic — x,y in 280x250 viewBox
+const STATIONS = {
+ Harbor:{x:40,y:40}, Pier:{x:40,y:95}, Market:{x:95,y:130}, Union:{x:150,y:130},
+ Central:{x:150,y:75},Junction:{x:205,y:75},Uni:{x:95,y:200}, Green:{x:150,y:200},
+ Tower:{x:205,y:130}, Airport:{x:250,y:180}, North:{x:150,y:25}
+};
+const ROUTES = {
+ RED:["Harbor","Pier","Market","Union","Tower"],
+ BLU:["North","Central","Union","Green"],
+ GRN:["Market","Uni","Green","Airport"],
+ YEL:["Central","Junction","Tower","Airport"]
+};
+
+// 8 incidents
+let INCIDENTS = [
+ {id:"I-1", line:"RED", station:"Market", title:"Signal failure northbound", sev:"crit",
+ load:94, cause:"Interlocking fault", eta:"—", crew:"S-Signal", stale:false, conflict:false,
+ tl:[["17:44","Signal drop detected @ Market interlocking"],["17:47","Northbound held; 3 trains stacked"],["17:52","Signals crew S-Signal dispatched"]]},
+ {id:"I-2", line:"RED", station:"Harbor", title:"Person on trackway", sev:"high",
+ load:78, cause:"Trespasser report", eta:"18:20", crew:"T-Bravo", stale:false, conflict:true,
+ tl:[["17:38","Passenger report: person on track"],["17:41","CCTV inconclusive — no visual",true],["17:49","Second report conflicts: cleared vs present",true]]},
+ {id:"I-3", line:"BLU", station:"Central", title:"Overcrowding surge", sev:"high",
+ load:97, cause:"Concert egress", eta:"18:35", crew:null, stale:false, conflict:false,
+ tl:[["17:30","Platform density rising"],["17:55","Load at 97% — hold recommended"]]},
+ {id:"I-4", line:"YEL", station:"Junction", title:"Power dip — reduced traction", sev:"med",
+ load:61, cause:"Substation sag", eta:"18:10", crew:null, stale:true, conflict:false,
+ tl:[["17:20","Voltage sag flagged"],["17:33","Telemetry link degraded — last update stale",false]]},
+ {id:"I-5", line:"GRN", station:"Uni", title:"Door fault — train withdrawn", sev:"med",
+ load:52, cause:"Rolling stock", eta:"18:05", crew:"T-Alpha?", stale:false, conflict:false,
+ tl:[["17:40","Door interlock fault"],["17:46","Unit withdrawn to depot"]]},
+ {id:"I-6", line:"YEL", station:"Airport", title:"Flooding at portal", sev:"crit",
+ load:44, cause:"Storm drain", eta:"19:05", crew:null, stale:false, conflict:false,
+ tl:[["17:15","Water ingress at Airport portal"],["17:50","Rising — R-Recov on weather standby",false]]},
+ {id:"I-7", line:"GRN", station:"Green", title:"Minor delay — congestion", sev:"low",
+ load:38, cause:"Knock-on RED", eta:"18:00", crew:null, stale:false, conflict:false,
+ tl:[["17:52","Marginal delays from Red-line hold"]]},
+ {id:"I-8", line:"BLU", station:"North", title:"Signal reset — service restored", sev:"ok",
+ load:22, cause:"Transient fault", eta:"done", crew:null, stale:false, conflict:false,
+ tl:[["17:10","Signal transient @ North"],["17:28","Auto-reset succeeded"],["17:31","Service restored — monitoring"]]}
+];
+
+// 3 recovery plans, scored 0-100 per criterion (higher = better outcome on that criterion)
+const PLANS = [
+ {id:"A", name:"Contain & Single-Track", tag:"Hold Red, single-track around Market",
+ scores:{safety:88, delay:55, cost:82, recovery:60},
+ note:"Isolate the Market signal fault, single-track Red past it, keep other lines nominal. Low crew demand.",
+ ev:{safety:"No passenger movement across faulted interlocking.", delay:"Red riders +14 min; network mostly unaffected.", cost:"Uses on-hand signals crew only.", recovery:"Slow — depends on I-1 repair ETA."}},
+ {id:"B", name:"Reroute via Blue Bridge", tag:"Divert Red→Blue at Union, run shuttle",
+ scores:{safety:70, delay:84, cost:58, recovery:82},
+ note:"Divert Red services onto Blue between Union and Tower; adds pressure to already-crowded Central (I-3).",
+ ev:{safety:"Adds load to overcrowded Central platform (I-3 at 97%).", delay:"Best throughput — Red riders +6 min avg.", cost:"Needs 2 extra crews + shuttle stock.", recovery:"Fast network normalization once shuttle runs."}},
+ {id:"C", name:"Suspend Red Core, Bus Bridge", tag:"Suspend Harbor–Union, bus bridge",
+ scores:{safety:95, delay:38, cost:45, recovery:70},
+ note:"Fully suspend the Red core to clear the person-on-track (I-2) and signal fault safely; bus bridge the gap.",
+ ev:{safety:"Highest — clears trackway before any movement.", delay:"Worst — bus bridge adds ~22 min.", cost:"Expensive — buses + drivers + overtime.", recovery:"Moderate — clean restart, no residual risk."}}
+];
+const CRITERIA = [
+ {k:"safety", lab:"Safety"},
+ {k:"delay", lab:"Passenger delay"},
+ {k:"cost", lab:"Cost efficiency"},
+ {k:"recovery", lab:"Network recovery"}
+];
+let WEIGHTS = {safety:40, delay:25, cost:15, recovery:20};
+
+/* ============================ STATE ============================ */
+let sel = null; // selected incident id
+let filter = {q:"", lines:new Set(), sevs:new Set(), flags:new Set()};
+let sim = {on:false, snapshot:null, changes:0};
+let LOG = [];
+let lastTop = null; // for flip detection
+
+/* ============================ UTIL ============================ */
+const $ = s=>document.querySelector(s);
+const el = (t,c,h)=>{const e=document.createElement(t);if(c)e.className=c;if(h!=null)e.innerHTML=h;return e;};
+function nowStr(){const d=new Date();return d.toTimeString().slice(0,8);}
+function inc(id){return INCIDENTS.find(i=>i.id===id);}
+function crewFor(id){return CREW.find(c=>c.status==="deployed"&&c.to===id);}
+function loadColor(v){return v>=90?"var(--s-crit)":v>=75?"var(--s-high)":v>=55?"#d8c93a":"var(--s-low)";}
+
+function logAdd(msg, kind){
+ LOG.unshift({t:nowStr(), msg, kind:kind||"", sim:sim.on});
+ renderLog();
+}
+function toast(m){const t=$("#toast");t.textContent=m;t.classList.add("on");clearTimeout(t._x);t._x=setTimeout(()=>t.classList.remove("on"),2200);}
+
+/* ============================ MAP ============================ */
+function renderMap(){
+ const svg=$("#map");
+ let s='';
+ // route lines
+ for(const [lk,path] of Object.entries(ROUTES)){
+ const pts=path.map(n=>`${STATIONS[n].x},${STATIONS[n].y}`).join(" ");
+ s+=`<polyline points="${pts}" fill="none" stroke="${LINES[lk].color}" stroke-width="3" stroke-opacity="0.55" stroke-linecap="round" stroke-linejoin="round"/>`;
+ }
+ // stations that have incidents get a status ring; map station->worst incident
+ const byStation={};
+ INCIDENTS.forEach(i=>{
+ const cur=byStation[i.station];
+ if(!cur || SEVMETA[i.sev].rank>SEVMETA[cur.sev].rank) byStation[i.station]=i;
+ });
+ for(const [nm,pt] of Object.entries(STATIONS)){
+ const i=byStation[nm];
+ const base=`<circle cx="${pt.x}" cy="${pt.y}" r="4.5" fill="#0b0e13" stroke="#556" stroke-width="1.5"/>`;
+ let node=base, glyph="", ring="", cls="node", extra="";
+ if(i){
+ const m=SEVMETA[i.sev];
+ const col = i.stale ? "var(--s-unk)" : m.col;
+ ring=`<circle cx="${pt.x}" cy="${pt.y}" r="8.5" fill="none" stroke="${col}" stroke-width="2" ${i.sev==='crit'?'class="pulse"':''} ${i.stale?'stroke-dasharray="2 2"':''}/>`;
+ glyph=`<text x="${pt.x}" y="${pt.y-11}" text-anchor="middle" font-size="8" fill="${col}">${m.g}</text>`;
+ node=`<circle cx="${pt.x}" cy="${pt.y}" r="4.5" fill="${col}"/>`;
+ extra=`data-inc="${i.id}"`;
+ }
+ s+=`<g class="${cls}" ${extra} tabindex="${i?0:-1}" role="${i?'button':'presentation'}" ${i?`aria-label="${nm} station, incident ${i.id} ${SEVMETA[i.sev].lab}"`:''}>${ring}${node}${glyph}<text x="${pt.x}" y="${pt.y+13}" text-anchor="middle" font-size="6.5" fill="#8a9aad">${nm}</text></g>`;
+ }
+ svg.innerHTML=s+`<style>.pulse{animation:pr 1.6s infinite}@keyframes pr{0%{stroke-opacity:1}50%{stroke-opacity:.25}100%{stroke-opacity:1}}</style>`;
+ svg.querySelectorAll("[data-inc]").forEach(g=>{
+ const id=g.getAttribute("data-inc");
+ g.addEventListener("click",()=>selectInc(id,true));
+ g.addEventListener("keydown",e=>{if(e.key==="Enter"||e.key===" "){e.preventDefault();selectInc(id,true);}});
+ });
+}
+
+/* ============================ FILTERS ============================ */
+function buildFilterChips(){
+ const fl=$("#fLines"); fl.innerHTML="";
+ Object.entries(LINES).forEach(([k,v])=>{
+ const c=el("span","chip",`<span style="color:${v.color}">●</span> ${k}`);
+ c.dataset.line=k; fl.appendChild(c);
+ });
+ const fs=$("#fSev"); fs.innerHTML="";
+ SEV.forEach(k=>{
+ const m=SEVMETA[k];
+ const c=el("span","chip",`${m.g} ${m.lab}`); c.dataset.sev=k; fs.appendChild(c);
+ });
+ document.querySelectorAll("#fLines .chip,#fSev .chip,#colMap [data-flag]").forEach(c=>{
+ c.addEventListener("click",()=>{
+ if(c.dataset.line){toggle(filter.lines,c.dataset.line);}
+ else if(c.dataset.sev){toggle(filter.sevs,c.dataset.sev);}
+ else if(c.dataset.flag){toggle(filter.flags,c.dataset.flag);}
+ c.classList.toggle("on");
+ renderList();
+ });
+ });
+ $("#q").addEventListener("input",e=>{filter.q=e.target.value.toLowerCase();renderList();});
+}
+function toggle(set,v){set.has(v)?set.delete(v):set.add(v);}
+
+function passes(i){
+ if(filter.q){const hay=(i.id+i.title+i.station+i.line).toLowerCase();if(!hay.includes(filter.q))return false;}
+ if(filter.lines.size&&!filter.lines.has(i.line))return false;
+ if(filter.sevs.size&&!filter.sevs.has(i.sev))return false;
+ if(filter.flags.has("crew")&&!( !i.crew || /\?$/.test(i.crew) ))return false;
+ if(filter.flags.has("stale")&&!i.stale)return false;
+ if(filter.flags.has("conflict")&&!i.conflict)return false;
+ if(filter.flags.has("open")&&i.sev==="ok")return false;
+ return true;
+}
+
+/* ============================ INCIDENT LIST ============================ */
+function riskScore(i){
+ // composite: severity rank * load — informs "highest-risk"
+ return SEVMETA[i.sev].rank*20 + i.load*0.6 + (i.conflict?8:0) + (i.stale?5:0) - (i.crew&&!/\?$/.test(i.crew)?6:0);
+}
+function renderList(){
+ const list=$("#incList"); list.innerHTML="";
+ const rows=INCIDENTS.filter(passes).sort((a,b)=>riskScore(b)-riskScore(a));
+ $("#incCount").textContent=`${rows.length}/${INCIDENTS.length}`;
+ if(!rows.length){list.appendChild(el("div","muted","No incidents match filters."));}
+ const maxRisk=Math.max(...INCIDENTS.map(riskScore));
+ rows.forEach(i=>{
+ const m=SEVMETA[i.sev];
+ const noCrew = !i.crew || /\?$/.test(i.crew);
+ const d=el("div","inc"+(sel===i.id?" sel":""));
+ d.tabIndex=0; d.dataset.id=i.id;
+ const flags=[];
+ if(noCrew) flags.push(`<span class="tag warnflag">⚠ no crew</span>`);
+ if(i.stale) flags.push(`<span class="tag stale">◇ stale</span>`);
+ if(i.conflict) flags.push(`<span class="tag warnflag">⚡ conflict</span>`);
+ d.innerHTML=`
+ <span class="bar" style="background:${i.stale?'var(--s-unk)':m.col}"></span>
+ <div class="l1">
+ <span class="id">${i.id}</span>
+ <span class="st ${m.cls}"><span class="g">${m.g}</span>${m.lab}</span>
+ <span class="ttl">${i.title}</span>
+ <span class="risk" style="color:${loadColor(i.load)}" title="Composite risk">R${Math.round(riskScore(i))}</span>
+ </div>
+ <div class="l2">
+ <span class="tag" style="border-color:${LINES[i.line].color}55;color:${LINES[i.line].color}">${i.line}</span>
+ <span class="tag">${i.station}</span>
+ <span class="tag">load ${i.load}%</span>
+ ${flags.join("")}
+ </div>
+ <div class="load-mini"><i style="width:${i.load}%;background:${loadColor(i.load)}"></i></div>`;
+ d.addEventListener("click",()=>selectInc(i.id));
+ d.addEventListener("keydown",e=>{if(e.key==="Enter"){selectInc(i.id);}});
+ list.appendChild(d);
+ });
+}
+
+/* ============================ DETAIL ============================ */
+function selectInc(id, fromMap){
+ sel=id; renderList(); renderDetail();
+ if(fromMap && window.innerWidth<=900) switchTab("inc");
+ const node=$(`#incList .inc[data-id="${id}"]`); if(node) node.scrollIntoView({block:"nearest"});
+}
+function renderDetail(){
+ const box=$("#detail");
+ if(!sel){box.innerHTML='<div class="muted">Select an incident to inspect its telemetry, timeline and response options.</div>';$("#detailId").textContent="";return;}
+ const i=inc(sel); const m=SEVMETA[i.sev];
+ $("#detailId").textContent=i.id;
+ const noCrew=!i.crew||/\?$/.test(i.crew);
+ const crewLine = i.sev==="ok" ? '<span class="st st-ok">✔ none needed</span>'
+ : noCrew ? '<span class="tag warnflag">⚠ NO CREW ASSIGNED — none available at base</span>'
+ : `<span class="st st-low">${i.crew} on scene</span>`;
+ let telem = i.stale
+ ? `<span class="tag stale">◇ TELEMETRY DELAYED</span> <span class="muted">last packet aged — figures may be stale, explore with caution</span>`
+ : `<span class="st st-ok" style="font-size:10px">live</span>`;
+ const tl=i.tl.map(ev=>`<div class="ev ${ev[2]?'conflict':''}"><div class="t">${ev[0]}${ev[2]?' · ⚡conflict':''}</div><div class="msg">${ev[1]}</div></div>`).join("");
+ box.innerHTML=`
+ <div class="kv">
+ <div class="k">Status</div><div><span class="st ${m.cls}"><span class="g">${m.g}</span>${m.lab}</span> · telemetry ${telem}</div>
+ <div class="k">Line / Stn</div><div><span style="color:${LINES[i.line].color}">●</span> ${LINES[i.line].name} — ${i.station}</div>
+ <div class="k">Cause</div><div>${i.cause}</div>
+ <div class="k">Pax load</div><div><b style="color:${loadColor(i.load)}">${i.load}%</b> platform density ${i.load>=90?'· ⚠ surge':''}</div>
+ <div class="k">Est. clear</div><div>${i.eta}</div>
+ <div class="k">Response</div><div>${crewLine}</div>
+ </div>
+ ${i.conflict?`<div class="warnbox">⚡ Conflicting reports on record — verify before committing a consequential action. Timeline shows the divergence.</div>`:''}
+ <hr>
+ <div class="muted" style="font-size:10px;margin-bottom:2px">EVENT TIMELINE</div>
+ <div class="tl">${tl}</div>
+ <div class="actbar">
+ <button id="actSev">⇅ Change severity <span class="kbd">S</span></button>
+ <button id="actAssign">👷 Assign team <span class="kbd">A</span></button>
+ <button id="actResolve" class="btn-p" style="color:#04203f">✔ Mark recovered</button>
+ </div>`;
+ $("#actSev").onclick=openSeverity;
+ $("#actAssign").onclick=openAssign;
+ $("#actResolve").onclick=()=>confirmAction({
+ title:"Mark incident recovered",icon:"✔",consequential:true,
+ body:`Mark <b>${i.id} — ${i.title}</b> as <b>recovered</b>? This updates the network map and closes crew tasking.`,
+ warn: i.conflict?"This incident has UNRESOLVED conflicting reports. Confirm the trackway is genuinely clear.":null,
+ okLabel:"Confirm recovered",
+ run:()=>{i.sev="ok";i.eta="done";i.tl.push([nowStr(),"Marked recovered by controller"]);logAdd(`<b>${i.id}</b> marked recovered`,"resolve");afterMutate();}
+ });
+}
+
+/* ============================ SEVERITY / ASSIGN ============================ */
+function openSeverity(){
+ const i=inc(sel);
+ const opts=SEV.map(k=>`<option value="${k}" ${k===i.sev?'selected':''}>${SEVMETA[k].g} ${SEVMETA[k].lab}</option>`).join("");
+ confirmAction({
+ title:`Change severity — ${i.id}`,icon:"⇅",consequential:true,
+ body:`Reclassify <b>${i.title}</b>.<br><br>Severity: <select class="mini" id="selSev">${opts}</select><div class="muted" style="margin-top:6px">Escalation re-ranks the incident list and the network map.</div>`,
+ okLabel:"Apply severity",
+ run:()=>{const nv=$("#selSev").value;const old=i.sev;if(nv===old)return;i.sev=nv;i.tl.push([nowStr(),`Severity ${SEVMETA[old].lab} → ${SEVMETA[nv].lab}`]);logAdd(`<b>${i.id}</b> severity ${SEVMETA[old].lab} → ${SEVMETA[nv].lab}`,"sev");afterMutate();}
+ });
+}
+function openAssign(){
+ const i=inc(sel);
+ const avail=CREW.filter(c=>c.status==="available");
+ const unav=CREW.filter(c=>c.status!=="available");
+ let body;
+ if(!avail.length){
+ body=`<div class="warnbox">⚠ NO CREW AVAILABLE. All teams are deployed or on standby. You may still queue a request; exploration is not blocked.</div>
+ <div class="muted" style="margin-top:8px">Deployed / standby:</div>
+ <div style="font-size:11px">${CREW.map(c=>`${c.id} <span class="muted">(${c.status}${c.to?` → ${c.to}`:''}${c.note?`, ${c.note}`:''})</span>`).join("<br>")}</div>`;
+ } else {
+ const o=avail.map(c=>`<option value="${c.id}">${c.id} — ${c.type} (base ${c.base})</option>`).join("");
+ body=`Assign a response team to <b>${i.id} — ${i.title}</b>:<br><br>
+ <select class="mini" id="selCrew">${o}</select>
+ <div class="muted" style="margin-top:6px">Dispatching removes the team from the availability pool.</div>`;
+ }
+ confirmAction({
+ title:`Assign response team — ${i.id}`,icon:"👷",consequential:true,
+ body, okLabel: avail.length?"Dispatch team":"Queue request (no crew)",
+ warn: i.conflict?"Conflicting reports open — a team may arrive to a changed situation.":null,
+ run:()=>{
+ if(!avail.length){i.tl.push([nowStr(),"Crew request QUEUED — none available"]);logAdd(`<b>${i.id}</b> crew request queued (none available)`,"assign");afterMutate();return;}
+ const cid=$("#selCrew").value;const c=CREW.find(x=>x.id===cid);
+ c.status="deployed";c.to=i.id;i.crew=cid;i.tl.push([nowStr(),`${cid} dispatched`]);
+ logAdd(`<b>${cid}</b> dispatched to <b>${i.id}</b>`,"assign");afterMutate();
+ }
+ });
+}
+
+/* ============================ CREW BOX ============================ */
+function renderCrew(){
+ const box=$("#crewBox");
+ const av=CREW.filter(c=>c.status==="available").length;
+ box.innerHTML=`<div style="margin-bottom:6px"><span class="st st-ok">${av} available</span> <span class="st st-high" style="margin-left:4px">${CREW.filter(c=>c.status==='deployed').length} deployed</span> <span class="st st-crit" style="margin-left:4px">${CREW.filter(c=>c.status==='unavailable').length} standby</span></div>`+
+ CREW.map(c=>{
+ const dot=c.status==="available"?"var(--s-ok)":c.status==="deployed"?"var(--s-high)":"var(--s-crit)";
+ const g=c.status==="available"?"✔":c.status==="deployed"?"◆":"■";
+ return `<div style="display:flex;gap:6px;align-items:center;padding:2px 0"><span style="color:${dot}">${g}</span><b>${c.id}</b><span class="muted">${c.type}</span><span style="margin-left:auto" class="muted">${c.status}${c.to?` → ${c.to}`:''}</span></div>`;
+ }).join("");
+}
+
+/* ============================ WEIGHTS + PLANS ============================ */
+function renderWeights(){
+ const w=$("#weights"); w.innerHTML="";
+ CRITERIA.forEach(c=>{
+ const row=el("div","wt");
+ row.innerHTML=`<label for="w_${c.k}">${c.lab}</label>
+ <input type="range" id="w_${c.k}" min="0" max="100" step="5" value="${WEIGHTS[c.k]}" aria-label="${c.lab} weight">
+ <span class="n" id="wn_${c.k}">${WEIGHTS[c.k]}</span>`;
+ w.appendChild(row);
+ row.querySelector("input").addEventListener("input",e=>{
+ WEIGHTS[c.k]=+e.target.value; $("#wn_"+c.k).textContent=e.target.value; updateSum();
+ });
+ row.querySelector("input").addEventListener("change",()=>{ scorePlans(true); });
+ });
+ updateSum();
+}
+function updateSum(){
+ const s=CRITERIA.reduce((a,c)=>a+WEIGHTS[c.k],0);
+ const el2=$("#wsum"); el2.textContent=s;
+ el2.style.color = s===100 ? "var(--good)" : "var(--warn)";
+}
+function planScore(p){
+ const s=CRITERIA.reduce((a,c)=>a+WEIGHTS[c.k],0)||1;
+ return CRITERIA.reduce((a,c)=>a + p.scores[c.k]*WEIGHTS[c.k], 0)/s;
+}
+function scorePlans(fromReweight){
+ const ranked=PLANS.map(p=>({p,score:planScore(p)})).sort((a,b)=>b.score-a.score);
+ const top=ranked[0], second=ranked[1];
+ renderPlans(ranked);
+ renderWhy(ranked, fromReweight);
+ if(fromReweight){
+ if(lastTop && lastTop!==top.p.id){
+ logAdd(`Reweight → recommendation <b>flipped ${lastTop} → Plan ${top.p.id}</b> (${top.p.name})`,"reweight");
+ } else {
+ logAdd(`Criteria reweighted — recommendation holds at <b>Plan ${top.p.id}</b>`,"reweight");
+ }
+ }
+ lastTop=top.p.id;
+}
+function renderPlans(ranked){
+ const box=$("#plans"); box.innerHTML="";
+ ranked.forEach(({p,score},idx)=>{
+ const win=idx===0;
+ const d=el("div","plan"+(win?" win":"")+(win?" open":""));
+ d.innerHTML=`<div class="ph"><span class="nm">Plan ${p.id}</span><span class="muted" style="font-size:11px">${p.name}</span>${win?'<span class="win-b">◀ RECOMMENDED</span>':''}<span class="sc" style="color:${win?'var(--good)':'var(--ink2)'}">${score.toFixed(1)}</span></div>
+ <div class="pb">
+ <div class="muted" style="font-size:11px;margin-bottom:5px">${p.note}</div>
+ ${CRITERIA.map(c=>`<div class="crit"><div class="lab">${c.lab}</div><div class="track"><i style="width:${p.scores[c.k]}%;background:${win?'var(--good)':'var(--accent)'}"></i></div><div class="val">${p.scores[c.k]} ·w${WEIGHTS[c.k]}</div></div><div class="muted" style="font-size:10px;grid-column:1/3;margin:-2px 0 4px 2px">↳ ${p.ev[c.k]}</div>`).join("")}
+ <button class="btn-p" style="color:#04203f;margin-top:4px" data-adopt="${p.id}">Adopt Plan ${p.id}</button>
+ </div>`;
+ d.querySelector(".ph").addEventListener("click",()=>d.classList.toggle("open"));
+ d.querySelector("[data-adopt]").addEventListener("click",e=>{e.stopPropagation();adoptPlan(p);});
+ box.appendChild(d);
+ });
+}
+function renderWhy(ranked, fromReweight){
+ const top=ranked[0], second=ranked[1];
+ // find the criterion contributing most to top's lead over second
+ let best=null, bestGap=-1e9;
+ CRITERIA.forEach(c=>{
+ const contrib=(top.p.scores[c.k]-second.p.scores[c.k])*WEIGHTS[c.k];
+ if(contrib>bestGap){bestGap=contrib;best=c;}
+ });
+ const margin=(top.score-second.score).toFixed(1);
+ const flipNote = (fromReweight && lastTop && lastTop!==top.p.id)
+ ? `<div class="flip">⚑ The reweight FLIPPED the recommendation from Plan ${lastTop} to Plan ${top.p.id}.</div>` : "";
+ $("#why").innerHTML=`${flipNote}
+ <b>Recommendation: Plan ${top.p.id} — ${top.p.name}</b> (score ${top.score.toFixed(1)}, +${margin} over Plan ${second.p.id}).<br>
+ <b>Deciding evidence:</b> <span style="color:var(--accent)">${best.lab}</span> — weighted at ${WEIGHTS[best.k]}%, Plan ${top.p.id} scores ${top.p.scores[best.k]} vs Plan ${second.p.id}'s ${second.p.scores[best.k]}.<br>
+ <span class="muted">↳ ${top.p.ev[best.k]}</span><br>
+ <span class="muted" style="font-size:10px">Change the weights above to see how the recommendation — and the deciding evidence — shifts.</span>`;
+}
+function adoptPlan(p){
+ confirmAction({
+ title:`Adopt Recovery Plan ${p.id}`,icon:"▶",consequential:true,
+ body:`Commit to <b>Plan ${p.id} — ${p.name}</b>?<br><span class="muted">${p.tag}</span><br><br>Weighted score ${planScore(p).toFixed(1)} under current criteria (Safety ${WEIGHTS.safety} · Delay ${WEIGHTS.delay} · Cost ${WEIGHTS.cost} · Recovery ${WEIGHTS.recovery}).`,
+ warn: sim.on?"You are in SIMULATION mode — adoption is sandboxed and reversible.":"This is a network-wide operational decision affecting all four lines.",
+ okLabel:`Commit Plan ${p.id}`,
+ run:()=>{logAdd(`<b>Plan ${p.id} adopted</b> — ${p.name} (score ${planScore(p).toFixed(1)})`,"adopt");toast(`Plan ${p.id} committed`);}
+ });
+}
+
+/* ============================ DECISION LOG ============================ */
+function renderLog(){
+ const box=$("#log");
+ if(!LOG.length){box.innerHTML='<div class="muted">No decisions logged yet.</div>';return;}
+ box.innerHTML=LOG.map(e=>`<div class="le"><span class="t">${e.t}${e.sim?' ⚙':''}</span><span class="m">${e.msg}</span></div>`).join("");
+}
+$("#expLog").onclick=()=>{
+ const txt=LOG.slice().reverse().map(e=>`[${e.t}]${e.sim?' (SIM)':''} ${e.msg.replace(/<[^>]+>/g,"")}`).join("\n");
+ confirmAction({title:"Decision trail export",icon:"⤓",consequential:false,okLabel:"Close",cancel:false,
+ body:`<div class="muted" style="font-size:10px;margin-bottom:4px">${LOG.length} entries — timestamped controller decision trail</div><pre style="white-space:pre-wrap;font-family:var(--mono);font-size:10px;max-height:300px;overflow:auto;background:#0a0d12;border:1px solid var(--edge);border-radius:5px;padding:8px">${txt||'(empty)'}</pre>`,
+ run:()=>{}});
+};
+
+/* ============================ SIMULATION MODE ============================ */
+function snapshot(){
+ return JSON.stringify({INCIDENTS,CREW,WEIGHTS,PLANScores:PLANS.map(p=>p.scores)});
+}
+function toggleSim(){
+ if(!sim.on){
+ sim.on=true; sim.snapshot=snapshot(); sim.changes=0;
+ $("#simbadge").classList.add("on");
+ logAdd("⚙ <b>Simulation mode ON</b> — subsequent changes are sandboxed","sim");
+ toast("Simulation mode ON — changes can be undone");
+ }
+}
+function endSim(keep){
+ if(!sim.on)return;
+ if(!keep){
+ const snap=JSON.parse(sim.snapshot);
+ INCIDENTS.length=0; snap.INCIDENTS.forEach(x=>INCIDENTS.push(x));
+ CREW.length=0; snap.CREW.forEach(x=>CREW.push(x));
+ Object.assign(WEIGHTS,snap.WEIGHTS);
+ snap.PLANScores.forEach((s,idx)=>PLANS[idx].scores=s);
+ logAdd("↩ <b>Simulation DISCARDED</b> — state rolled back","sim");
+ toast("Simulation discarded — rolled back");
+ } else {
+ logAdd("⚙ <b>Simulation committed</b> to live state","sim");
+ toast("Simulation changes kept");
+ }
+ sim.on=false; $("#simbadge").classList.remove("on");
+ renderAll();
+}
+$("#simEnd").onclick=()=>endSim(true);
+$("#simDiscard").onclick=()=>confirmAction({title:"Discard simulation",icon:"↩",consequential:true,
+ body:"Roll back <b>all</b> simulated changes to the snapshot taken when simulation started?",okLabel:"Discard & roll back",
+ run:()=>endSim(false)});
+
+/* ============================ CONFIRMATION MODAL ============================ */
+let modalRun=null;
+function confirmAction(o){
+ $("#mIcon").textContent=o.icon||"⚠";
+ $("#mTitle").textContent=o.title||"Confirm";
+ $("#mBody").innerHTML=(o.body||"")+(o.warn?`<div class="warnbox">⚠ ${o.warn}</div>`:"")+
+ (o.consequential?`<div class="muted" style="font-size:10px;margin-top:8px">Consequential action — requires confirmation. It will be timestamped in the decision log.</div>`:"");
+ const f=$("#mFoot"); f.innerHTML="";
+ if(o.cancel!==false){const cb=el("button",null,"Cancel");cb.onclick=closeModal;f.appendChild(cb);}
+ const ok=el("button",o.consequential&&/discard|recovered|delete|suspend/i.test(o.okLabel||"")?"btn-d":"btn-p",o.okLabel||"Confirm");
+ if(!o.consequential||!/discard/i.test(o.okLabel||"")) ok.className=o.okLabel&&/discard/i.test(o.okLabel)?"btn-d":"btn-p";
+ ok.style.color=ok.className==="btn-p"?"#04203f":"";
+ ok.onclick=()=>{closeModal();o.run&&o.run();};
+ f.appendChild(ok);
+ modalRun=o.run;
+ $("#ov").classList.add("on"); ok.focus();
+}
+function closeModal(){$("#ov").classList.remove("on");modalRun=null;}
+$("#ov").addEventListener("click",e=>{if(e.target.id==="ov")closeModal();});
+
+/* ============================ RENDER ALL ============================ */
+function afterMutate(){ if(sim.on)sim.changes++; renderMap();renderList();renderDetail();renderCrew();scorePlans(false); }
+function renderAll(){ renderMap();buildFilterChips();renderList();renderDetail();renderCrew();renderWeights();scorePlans(false);renderLog(); }
+
+/* ============================ TABS (390px) ============================ */
+function switchTab(t){
+ document.querySelectorAll(".col").forEach(c=>c.classList.toggle("active",c.dataset.tab===t));
+ document.querySelectorAll("#tabbar button").forEach(b=>b.classList.toggle("on",b.dataset.t===t));
+}
+document.querySelectorAll("#tabbar button").forEach(b=>b.onclick=()=>switchTab(b.dataset.t));
+
+/* ============================ KEYBOARD ============================ */
+document.addEventListener("keydown",e=>{
+ if($("#ov").classList.contains("on")){ if(e.key==="Escape")closeModal(); return; }
+ const typing=/INPUT|SELECT|TEXTAREA/.test(document.activeElement.tagName);
+ if(e.key==="/"&&!typing){e.preventDefault();$("#q").focus();return;}
+ if(typing)return;
+ const rows=INCIDENTS.filter(passes).sort((a,b)=>riskScore(b)-riskScore(a));
+ if(e.key==="ArrowDown"||e.key==="ArrowUp"){
+ e.preventDefault();
+ if(!rows.length)return;
+ let idx=rows.findIndex(i=>i.id===sel);
+ idx = e.key==="ArrowDown" ? Math.min(rows.length-1,idx+1) : Math.max(0,idx<0?0:idx-1);
+ selectInc(rows[idx].id);
+ }
+ else if(e.key.toLowerCase()==="s"&&sel){openSeverity();}
+ else if(e.key.toLowerCase()==="a"&&sel){openAssign();}
+ else if(e.key.toLowerCase()==="m"){toggleSim();}
+ else if(e.key.toLowerCase()==="u"){ if(sim.on)$("#simDiscard").click(); else toast("Undo available only in simulation mode (press M)"); }
+});
+
+/* ============================ CLOCK ============================ */
+setInterval(()=>{$("#clock").textContent=nowStr();},1000);
+$("#clock").textContent=nowStr();
+
+/* ============================ INIT ============================ */
+renderAll();
+logAdd("Console initialized — evening rush recovery session opened","init");
+selectInc("I-1");
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/6e2d6635424a/qwen25-7b.html b/data/artifacts/6e2d6635424a/qwen25-7b.html
new file mode 100644
index 0000000..bedf826
--- /dev/null
+++ b/data/artifacts/6e2d6635424a/qwen25-7b.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Railway Control Console</title>
+ <style>
+ body { font-family: Arial, sans-serif; }
+ .container { display: grid; grid-template-columns: 3fr 2fr; gap: 20px; padding: 20px; }
+ #map { width: 480px; height: 360px; border: 1px solid #ccc; background-color: lightgray; }
+ .status { color: var(--color-status); font-weight: bold; }
+ .incident { cursor: pointer; }
+ .timeline { margin-top: 20px; white-space: pre-wrap; word-break: break-all; }
+ .criteria { display: flex; gap: 10px; align-items: center; }
+ </style>
+</head>
+<body>
+ <div class="container">
+ <div id="map" style="grid-column: span 2;"></div>
+ <div id="console">
+ <h1>Railway Control Console</h1>
+ <p><span class="status">Normal</span></p>
+ <button onclick="toggleSimulation()">Toggle Simulation Mode</button>
+ <table id="incidents">
+ <thead>
+ <tr>
+ <th>Incident ID</th>
+ <th>Status</th>
+ <th>Description</th>
+ <th>Severity</th>
+ <th>Action</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr class="incident" onclick="selectIncident(1)">
+ <td>1</td>
+ <td><span class="status">High</span></td>
+ <td>Track obstruction at station B</td>
+ <td>4</td>
+ <td><button onclick="changeSeverity(1, 2)">Change Severity</button></td>
+ </tr>
+ <!-- Add more incidents as needed -->
+ </tbody>
+ </table>
+ <div id="timeline">Timeline: [Incident 1 - 3pm; Incident 2 - 4pm]</div>
+ <h2>Criteria for Recovery Plans</h2>
+ <div class="criteria">
+ <label><input type="checkbox" checked data-criteria="Safety"> Safety</label>
+ <label><input type="checkbox" checked data-criteria="Delay"> Passenger Delay</label>
+ <label><input type="checkbox" checked data-criteria="Cost"> Cost</label>
+ <label><input type="checkbox" checked data-criteria="Network"> Network Recovery</label>
+ </div>
+ <h2>Recovery Plans</h2>
+ <form id="planForm">
+ <fieldset>
+ <legend>Plan 1: Quick Clearance</legend>
+ <p>Safety: High; Delay: Low; Cost: Medium; Network: Partial</p>
+ <button type="submit" data-plan="1">Select Plan 1</button>
+ </fieldset>
+ <fieldset>
+ <legend>Plan 2: Long-term Repair</legend>
+ <p>Safety: Medium; Delay: High; Cost: Low; Network: Full</p>
+ <button type="submit" data-plan="2">Select Plan 2</button>
+ </fieldset>
+ </form>
+ <div id="decisionLog">
+ <h3>Decision Log:</h3>
+ <pre>[14:05] Incident 1 selected; [14:10] Plan 1 recommended.</pre>
+ </div>
+ </div>
+ </div>
+ <script>
+ let activeIncident = null;
+ const incidents = [
+ { id: 1, status: 'High', severity: 4, description: 'Track obstruction at station B' },
+ // Add more incident data as needed
+ ];
+
+ function selectIncident(id) {
+ if (activeIncident === id) return;
+ activeIncident = id;
+ incidents.forEach(i => i.status = 'Normal');
+ document.querySelectorAll('.incident').forEach(el => el.classList.remove('selected'));
+ const selectedIncident = document.querySelector(`.incident[id="${id}"]`);
+ selectedIncident.classList.add('selected');
+ }
+
+ function changeSeverity(id, newSeverity) {
+ incidents.find(i => i.id === id).severity = newSeverity;
+ selectIncident(id);
+ }
+
+ function toggleSimulation() {
+ const simulationMode = document.body.dataset.simulation;
+ if (simulationMode === 'true') {
+ document.body.dataset.simulation = false;
+ alert('Simulation mode is now off. No changes are applied.');
+ } else {
+ document.body.dataset.simulation = true;
+ alert('Simulation mode is on. Changes will be reverted before submission.');
+ }
+ }
+
+ function handlePlanSelection(event) {
+ const planId = event.target.getAttribute('data-plan');
+ if (document.body.dataset.simulation === 'true') return;
+ console.log(`[14:30] Plan ${planId} selected`);
+ document.getElementById('decisionLog').innerHTML += `\n[14:30] Plan ${planId} selected;`;
+ }
+
+ document.querySelectorAll('.incident').forEach(el => el.addEventListener('click', selectIncident));
+ document.getElementById('timeline').innerText = `[Incident 1 - 3pm; Incident 2 - 4pm]`;
+ document.getElementById('planForm').addEventListener('submit', handlePlanSelection);
+ </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/6e2d6635424a/qwen3-14b.html b/data/artifacts/6e2d6635424a/qwen3-14b.html
new file mode 100644
index 0000000..a23f31e
--- /dev/null
+++ b/data/artifacts/6e2d6635424a/qwen3-14b.html
@@ -0,0 +1,400 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Transit Controller Console</title>
+ <style>
+ body {
+ margin: 0;
+ font-family: sans-serif;
+ background: #1e1e1e;
+ color: #f0f0f0;
+ display: flex;
+ height: 100vh;
+ }
+ #map {
+ flex: 1;
+ background: #2a2a2a;
+ position: relative;
+ overflow: hidden;
+ padding: 10px;
+ }
+ .incident {
+ position: absolute;
+ background: #444;
+ padding: 4px 8px;
+ border-radius: 4px;
+ cursor: pointer;
+ user-select: none;
+ }
+ .incident.high-risk {
+ background: #d9534f;
+ }
+ .incident.medium-risk {
+ background: #f0ad4e;
+ }
+ .incident.low-risk {
+ background: #5cb85c;
+ }
+ #console {
+ width: 390px;
+ background: #222;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ }
+ #header {
+ padding: 10px;
+ background: #333;
+ border-bottom: 1px solid #444;
+ }
+ #log {
+ flex: 1;
+ overflow-y: auto;
+ padding: 10px;
+ font-size: 14px;
+ }
+ .log-entry {
+ margin-bottom: 4px;
+ }
+ .log-entry label {
+ font-weight: bold;
+ }
+ #controls {
+ padding: 10px;
+ background: #333;
+ border-top: 1px solid #444;
+ }
+ button {
+ margin-right: 10px;
+ padding: 6px 12px;
+ background: #444;
+ color: #fff;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ }
+ button:hover {
+ background: #555;
+ }
+ #incident-filter {
+ width: 100%;
+ padding: 6px;
+ margin-bottom: 10px;
+ background: #333;
+ border: none;
+ color: #fff;
+ font-size: 14px;
+ }
+ #timeline {
+ margin-top: 10px;
+ font-size: 13px;
+ line-height: 1.3;
+ }
+ .timeline-item {
+ margin-bottom: 4px;
+ }
+ .plan {
+ margin-top: 10px;
+ background: #2a2a2a;
+ padding: 8px;
+ border-radius: 4px;
+ }
+ .plan h3 {
+ margin: 0;
+ font-size: 16px;
+ }
+ .plan p {
+ margin: 4px 0;
+ }
+ .plan .score {
+ font-weight: bold;
+ }
+ .plan .score span {
+ color: #fff;
+ }
+ .plan .score .safety { color: #f0ad4e; }
+ .plan .score .delay { color: #5cb85c; }
+ .plan .score .cost { color: #d9534f; }
+ .plan .score .recovery { color: #337ab7; }
+ .plan .selected {
+ background: #444;
+ }
+ .plan .plan-button {
+ margin-top: 4px;
+ font-size: 12px;
+ }
+ .plan .plan-button:hover {
+ text-decoration: underline;
+ }
+ #reweight {
+ display: flex;
+ flex-wrap: wrap;
+ margin-top: 10px;
+ }
+ #reweight input {
+ width: 40px;
+ margin-right: 5px;
+ margin-bottom: 5px;
+ }
+ #reweight label {
+ margin-right: 10px;
+ }
+ #sim-toggle {
+ margin-top: 10px;
+ }
+ </style>
+</head>
+<body>
+ <div id="map"></div>
+ <div id="console">
+ <div id="header">
+ <h2>Transit Control Console</h2>
+ </div>
+ <div id="log"></div>
+ <div id="controls">
+ <input type="text" id="incident-filter" placeholder="Filter incidents..." oninput="filterIncidents()">
+ <button onclick="inspectTimeline()">Inspect Timeline</button>
+ <button onclick="changeSeverity()">Change Severity</button>
+ <button onclick="assignTeam()">Assign Response Team</button>
+ <button onclick="comparePlans()">Compare Plans</button>
+ <button onclick="toggleSimulation()">Toggle Simulation Mode</button>
+ </div>
+ </div>
+ <script>
+ const map = document.getElementById('map');
+ const log = document.getElementById('log');
+ const consoleControls = document.getElementById('controls');
+ const incidentFilter = document.getElementById('incident-filter');
+ let simulationMode = false;
+ let undoStack = [];
+ let currentPlanIndex = -1;
+ let currentIncident = null;
+
+ const incidents = [
+ { id: 1, line: "Red", pos: { x: 100, y: 100 }, type: "Derailment", severity: "high", crew: "none", delay: "15 mins", load: "85%", status: "active" },
+ { id: 2, line: "Blue", pos: { x: 200, y: 150 }, type: "Signal Failure", severity: "medium", crew: "available", delay: "10 mins", load: "75%", status: "active" },
+ { id: 3, line: "Green", pos: { x: 150, y: 200 }, type: "Power Outage", severity: "medium", crew: "available", delay: "12 mins", load: "90%", status: "active" },
+ { id: 4, line: "Yellow", pos: { x: 50, y: 150 }, type: "Track Blockage", severity: "high", crew: "available", delay: "20 mins", load: "95%", status: "active" },
+ { id: 5, line: "Red", pos: { x: 250, y: 100 }, type: "Train Delay", severity: "low", crew: "available", delay: "5 mins", load: "60%", status: "active" },
+ { id: 6, line: "Blue", pos: { x: 200, y: 50 }, type: "Passenger Overload", severity: "medium", crew: "available", delay: "7 mins", load: "80%", status: "active" },
+ { id: 7, line: "Green", pos: { x: 100, y: 250 }, type: "Communication Failure", severity: "low", crew: "available", delay: "3 mins", load: "70%", status: "active" },
+ { id: 8, line: "Yellow", pos: { x: 150, y: 50 }, type: "Emergency Stop", severity: "high", crew: "available", delay: "18 mins", load: "92%", status: "active" }
+ ];
+
+ const plans = [
+ { name: "Plan A", safety: 90, delay: 80, cost: 60, recovery: 75 },
+ { name: "Plan B", safety: 85, delay: 75, cost: 70, recovery: 80 },
+ { name: "Plan C", safety: 80, delay: 70, cost: 65, recovery: 85 }
+ ];
+
+ const criteria = { safety: 0.3, delay: 0.25, cost: 0.2, recovery: 0.25 };
+
+ function initMap() {
+ map.innerHTML = '';
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ svg.setAttribute("width", "100%");
+ svg.setAttribute("height", "100%");
+ svg.style.background = "#2a2a2a";
+ svg.style.position = "absolute";
+ svg.style.top = "0";
+ svg.style.left = "0";
+ svg.style.width = "100%";
+ svg.style.height = "100%";
+ map.appendChild(svg);
+ drawMapLines(svg);
+ drawIncidents(svg);
+ }
+
+ function drawMapLines(svg) {
+ const lines = new Set(incidents.map(i => i.line));
+ lines.forEach(line => {
+ const lineGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ lineGroup.setAttribute("id", `line-${line}`);
+ lineGroup.style.stroke = line === "Red" ? "#d9534f" : line === "Blue" ? "#5bc0de" : line === "Green" ? "#5cb85c" : "#f0ad4e";
+ lineGroup.style.strokeWidth = 2;
+ lineGroup.style.strokeDasharray = "4,4";
+ lineGroup.style.opacity = 0.5;
+
+ const points = incidents.filter(i => i.line === line).map(i => `${i.pos.x},${i.pos.y}`);
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
+ path.setAttribute("d", `M${points[0]} L${points.slice(1).join(" L")}`);
+ lineGroup.appendChild(path);
+ svg.appendChild(lineGroup);
+ });
+ }
+
+ function drawIncidents(svg) {
+ incidents.forEach(incident => {
+ const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
+ circle.setAttribute("cx", incident.pos.x);
+ circle.setAttribute("cy", incident.pos.y);
+ circle.setAttribute("r", 8);
+ circle.style.fill = "#444";
+ circle.style.stroke = "#fff";
+ circle.style.strokeWidth = 1;
+ circle.setAttribute("id", `incident-${incident.id}`);
+ circle.addEventListener("click", () => selectIncident(incident.id));
+ svg.appendChild(circle);
+
+ const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
+ text.setAttribute("x", incident.pos.x);
+ text.setAttribute("y", incident.pos.y - 10);
+ text.textContent = incident.id;
+ text.style.fontSize = "12px";
+ text.style.fill = "#fff";
+ svg.appendChild(text);
+ });
+ }
+
+ function selectIncident(id) {
+ currentIncident = incidents.find(i => i.id === id);
+ log.innerHTML += `<div class="log-entry"><label>Selected Incident:</label> ${id} - ${currentIncident.line} - ${currentIncident.type} - <span style="color: ${getSeverityColor(currentIncident.severity)};">${currentIncident.severity}</span></div>`;
+ undoStack.push({ action: "select", incident: currentIncident });
+ }
+
+ function filterIncidents() {
+ const filter = incidentFilter.value.toLowerCase();
+ const filtered = incidents.filter(i => i.line.toLowerCase().includes(filter) || i.type.toLowerCase().includes(filter));
+ log.innerHTML += `<div class="log-entry"><label>Filtered Incidents:</label> ${filtered.length} found.</div>`;
+ }
+
+ function inspectTimeline() {
+ log.innerHTML += `<div class="log-entry"><label>Timeline:</label><div id="timeline"></div></div>`;
+ const timeline = document.getElementById('timeline');
+ timeline.innerHTML = "";
+ const timelineData = [
+ { time: "17:00", event: "Incident 1 reported", status: "active" },
+ { time: "17:05", event: "Crew dispatched to incident 1", status: "active" },
+ { time: "17:10", event: "Incident 4 reported", status: "active" },
+ { time: "17:15", event: "Passenger load increased on line Red", status: "active" },
+ { time: "17:20", event: "Incident 8 reported", status: "active" },
+ { time: "17:30", event: "Crew dispatched to incident 8", status: "active" },
+ { time: "17:45", event: "Crew unavailable for incident 1", status: "active" },
+ { time: "18:00", event: "Incident 1 resolved", status: "resolved" },
+ { time: "18:15", event: "All incidents resolved", status: "resolved" }
+ ];
+ timelineData.forEach(item => {
+ const div = document.createElement('div');
+ div.className = 'timeline-item';
+ div.textContent = `${item.time}: ${item.event} [${item.status}]`;
+ timeline.appendChild(div);
+ });
+ undoStack.push({ action: "inspect", timeline: timelineData });
+ }
+
+ function changeSeverity() {
+ if (currentIncident) {
+ const newSeverity = prompt(`Change severity of incident ${currentIncident.id} from ${currentIncident.severity} to (high/medium/low):`);
+ if (newSeverity && ["high", "medium", "low"].includes(newSeverity)) {
+ currentIncident.severity = newSeverity;
+ log.innerHTML += `<div class="log-entry"><label>Severity changed:</label> Incident ${currentIncident.id} is now <span style="color: ${getSeverityColor(newSeverity)};">${newSeverity}</span>.</div>`;
+ undoStack.push({ action: "changeSeverity", old: currentIncident.severity, new: newSeverity, incident: currentIncident });
+ }
+ }
+ }
+
+ function assignTeam() {
+ if (currentIncident) {
+ const team = prompt(`Assign response team to incident ${currentIncident.id} (e.g., "Team 3"):`);
+ if (team) {
+ currentIncident.crew = team;
+ log.innerHTML += `<div class="log-entry"><label>Assigned Team:</label> Incident ${currentIncident.id} now has ${team} assigned.</div>`;
+ undoStack.push({ action: "assignTeam", team: team, incident: currentIncident });
+ }
+ }
+ }
+
+ function getSeverityColor(sev) {
+ switch (sev) {
+ case "high": return "#d9534f";
+ case "medium": return "#f0ad4e";
+ case "low": return "#5cb85c";
+ default: return "#fff";
+ }
+ }
+
+ function comparePlans() {
+ const planContainer = document.getElementById('console');
+ planContainer.innerHTML += `<div id="plans"></div>`;
+ const plansDiv = document.getElementById('plans');
+ plans.forEach((plan, idx) => {
+ const planDiv = document.createElement('div');
+ planDiv.className = 'plan';
+ planDiv.innerHTML = `
+ <h3>${plan.name}</h3>
+ <div class="score">Safety: <span class="safety">${plan.safety}</span> | Delay: <span class="delay">${plan.delay}</span> | Cost: <span class="cost">${plan.cost}</span> | Recovery: <span class="recovery">${plan.recovery}</span></div>
+ <button class="plan-button" onclick="selectPlan(${idx})">Simulate</button>
+ `;
+ plansDiv.appendChild(planDiv);
+ });
+ }
+
+ function selectPlan(index) {
+ const selectedPlan = plans[index];
+ const total = selectedPlan.safety * criteria.safety + selectedPlan.delay * criteria.delay + selectedPlan.cost * criteria.cost + selectedPlan.recovery * criteria.recovery;
+ log.innerHTML += `<div class="log-entry"><label>Recommended Plan:</label> <strong>${selectedPlan.name}</strong> with total score: <span style="color: #00ff00;">${total.toFixed(2)}</span>.</div>`;
+ undoStack.push({ action: "selectPlan", plan: selectedPlan });
+ }
+
+ function reweightCriteria() {
+ const safety = parseFloat(document.getElementById('safety').value);
+ const delay = parseFloat(document.getElementById('delay').value);
+ const cost = parseFloat(document.getElementById('cost').value);
+ const recovery = parseFloat(document.getElementById('recovery').value);
+ criteria.safety = safety;
+ criteria.delay = delay;
+ criteria.cost = cost;
+ criteria.recovery = recovery;
+ const total = plans.map(p => p.safety * safety + p.delay * delay + p.cost * cost + p.recovery * recovery);
+ const maxIndex = total.indexOf(Math.max(...total));
+ const maxPlan = plans[maxIndex];
+ log.innerHTML += `<div class="log-entry"><label>Recalculated Recommendation:</label> <strong>${maxPlan.name}</strong> with new score: <span style="color: #00ff00;">${(total[maxIndex]).toFixed(2)}</span>.<br>Changes: Safety=${safety}, Delay=${delay}, Cost=${cost}, Recovery=${recovery}</div>`;
+ }
+
+ function toggleSimulation() {
+ simulationMode = !simulationMode;
+ log.innerHTML += `<div class="log-entry"><label>Simulation Mode:</label> ${simulationMode ? "ON" : "OFF"}.</div>`;
+ }
+
+ function undo() {
+ if (undoStack.length > 0) {
+ const last = undoStack.pop();
+ if (last.action === "select") {
+ currentIncident = null;
+ } else if (last.action === "changeSeverity") {
+ last.incident.severity = last.old;
+ } else if (last.action === "assignTeam") {
+ last.incident.crew = "none";
+ } else if (last.action === "selectPlan") {
+ // No direct reversal, just clear log
+ }
+ log.innerHTML = log.innerHTML.slice(0, -1);
+ }
+ }
+
+ function initConsole() {
+ initMap();
+ const reweightDiv = document.createElement('div');
+ reweightDiv.id = 'reweight';
+ reweightDiv.innerHTML = `
+ <label>Safety: <input type="number" id="safety" value="${criteria.safety}" min="0" max="1" step="0.01"></label>
+ <label>Delay: <input type="number" id="delay" value="${criteria.delay}" min="0" max="1" step="0.01"></label>
+ <label>Cost: <input type="number" id="cost" value="${criteria.cost}" min="0" max="1" step="0.01"></label>
+ <label>Recovery: <input type="number" id="recovery" value="${criteria.recovery}" min="0" max="1" step="0.01"></label>
+ <button onclick="reweightCriteria()">Recalculate</button>
+ `;
+ consoleControls.appendChild(reweightDiv);
+ consoleControls.innerHTML += '<button onclick="undo()">Undo</button>';
+ }
+
+ window.addEventListener('keydown', (e) => {
+ if (e.key === 'z' && e.ctrlKey) {
+ undo();
+ }
+ });
+
+ initConsole();
+ </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/84f06b214f8c/claude-code.html b/data/artifacts/84f06b214f8c/claude-code.html
new file mode 100644
index 0000000..5d0156b
--- /dev/null
+++ b/data/artifacts/84f06b214f8c/claude-code.html
@@ -0,0 +1,1134 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Strata No. 7 — Sample Memo Studio</title>
+<style>
+ :root{
+ --paper:#f4f1ea;
+ --paper-2:# eeeae1;
+ --card:#faf8f3;
+ --ink:#26241f;
+ --ink-soft:#5c574c;
+ --ink-faint:#8f897c;
+ --line:#ddd7c9;
+ --line-soft:#e8e3d7;
+ --accent:#6a6250;
+ --accent-deep:#403b30;
+ --ok:#4f6b4a;
+ --warn:#9a6a2e;
+ --stop:#8a4436;
+ --radius:3px;
+ --shadow:0 1px 0 rgba(0,0,0,.03), 0 12px 40px -28px rgba(40,36,28,.5);
+ --serif:"Iowan Old Style","Palatino Linotype",Palatino,"Book Antiqua",Georgia,serif;
+ --sans:"Helvetica Neue",Helvetica,Arial,sans-serif;
+ }
+ *{box-sizing:border-box;margin:0;padding:0}
+ html{-webkit-text-size-adjust:100%}
+ body{
+ background:var(--paper);
+ color:var(--ink);
+ font-family:var(--sans);
+ font-size:15px;
+ line-height:1.5;
+ -webkit-font-smoothing:antialiased;
+ letter-spacing:.1px;
+ }
+ .wrap{max-width:1180px;margin:0 auto;padding:0 22px 120px}
+ .micro{font-family:var(--sans);font-size:10.5px;letter-spacing:.22em;text-transform:uppercase;color:var(--ink-faint)}
+ h1,h2,h3{font-family:var(--serif);font-weight:500;letter-spacing:.2px}
+
+ /* ---- session banner ---- */
+ #sessionBanner{
+ display:none;background:#efe9db;border-bottom:1px solid var(--line);
+ font-size:13px;color:var(--ink-soft);
+ }
+ #sessionBanner .inner{max-width:1180px;margin:0 auto;padding:9px 22px;display:flex;gap:14px;align-items:center;justify-content:space-between}
+ #sessionBanner button{background:none;border:none;color:var(--accent-deep);text-decoration:underline;cursor:pointer;font-size:12px;font-family:var(--sans)}
+
+ /* ---- header ---- */
+ header.masthead{border-bottom:1px solid var(--line);margin-bottom:34px}
+ .masthead .inner{max-width:1180px;margin:0 auto;padding:26px 22px 22px;display:flex;justify-content:space-between;align-items:flex-end;flex-wrap:wrap;gap:16px}
+ .masthead h1{font-size:30px;line-height:1}
+ .masthead .sub{font-size:12.5px;color:var(--ink-soft);margin-top:8px;max-width:46ch}
+ .masthead .col-right{text-align:right;font-size:11.5px;color:var(--ink-faint);line-height:1.7}
+ .masthead .col-right b{color:var(--ink-soft);font-weight:500}
+
+ .section-label{display:flex;align-items:center;gap:14px;margin:40px 0 18px}
+ .section-label:before{content:"";flex:0 0 22px;height:1px;background:var(--accent)}
+ .section-label h2{font-size:18px}
+ .section-label .hint{font-size:12px;color:var(--ink-faint);margin-left:auto}
+
+ /* ---- colorway grid ---- */
+ .cw-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px}
+ .cw{
+ background:var(--card);border:1px solid var(--line-soft);border-radius:var(--radius);
+ overflow:hidden;cursor:pointer;transition:border-color .2s, box-shadow .2s, transform .2s;
+ display:flex;flex-direction:column;
+ }
+ .cw:hover{border-color:var(--accent);box-shadow:var(--shadow)}
+ .cw.selected{border-color:var(--accent-deep);box-shadow:0 0 0 1px var(--accent-deep) inset, var(--shadow)}
+ .swatch{height:150px;position:relative;overflow:hidden}
+ .cw .body{padding:12px 13px 14px}
+ .cw .body h3{font-size:16px;margin-bottom:2px}
+ .cw .body .desc{font-size:11.5px;color:var(--ink-soft);min-height:32px;line-height:1.45}
+ .cw .avail{font-size:10px;letter-spacing:.14em;text-transform:uppercase;margin-top:9px;color:var(--ink-faint)}
+ .cw .avail b{color:var(--ok);font-weight:600}
+ .cw .actions{display:flex;gap:7px;margin-top:11px}
+ .cw .actions button{flex:1;font-family:var(--sans);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;
+ padding:8px 4px;border:1px solid var(--line);background:#fff;color:var(--ink-soft);border-radius:2px;cursor:pointer;transition:.15s}
+ .cw .actions button:hover{border-color:var(--accent);color:var(--accent-deep)}
+ .cw .actions button.on{background:var(--accent-deep);border-color:var(--accent-deep);color:#f4f1ea}
+
+ /* ---- material preview swatch texture ---- */
+ .preview-surface{position:absolute;inset:0}
+ .tex-base{position:absolute;inset:0;background:var(--cw-color,#999)}
+ .tex-ground{position:absolute;inset:0;mix-blend-mode:multiply;opacity:.9}
+ .tex-ground.linen{
+ background-image:
+ repeating-linear-gradient(0deg, rgba(0,0,0,.055) 0 1px, transparent 1px 3px),
+ repeating-linear-gradient(90deg, rgba(0,0,0,.05) 0 1px, transparent 1px 3px),
+ repeating-linear-gradient(0deg, rgba(255,255,255,.06) 0 1px, transparent 1px 4px);
+ }
+ .tex-ground.clay{
+ background-image:
+ radial-gradient(rgba(0,0,0,.05) .7px, transparent .8px),
+ radial-gradient(rgba(255,255,255,.05) .7px, transparent .8px);
+ background-size:4px 4px, 5px 5px;
+ background-position:0 0, 2px 3px;
+ }
+ .tex-sheen{position:absolute;inset:0}
+ .tex-sheen.matte{background:linear-gradient(135deg, rgba(255,255,255,.03), rgba(0,0,0,.05))}
+ .tex-sheen.soft{background:linear-gradient(120deg, rgba(255,255,255,.16) 0%, transparent 34%, transparent 66%, rgba(0,0,0,.08) 100%)}
+ .tex-sheen.lacquer{
+ background:
+ linear-gradient(115deg, rgba(255,255,255,.55) 0%, rgba(255,255,255,.06) 22%, transparent 40%, transparent 60%, rgba(255,255,255,.22) 74%, rgba(0,0,0,.12) 100%);
+ }
+ .tex-vignette{position:absolute;inset:0;box-shadow:inset 0 0 46px rgba(0,0,0,.12)}
+ .cw-color-name{position:absolute;left:11px;bottom:9px;font-family:var(--serif);font-size:12px;color:rgba(255,255,255,.9);text-shadow:0 1px 3px rgba(0,0,0,.4);letter-spacing:.4px}
+ .swatch.dark .cw-color-name{color:rgba(255,255,255,.92)}
+ .swatch.light .cw-color-name{color:rgba(50,45,35,.75);text-shadow:none}
+
+ /* ---- compare tray ---- */
+ #tray{
+ background:var(--card);border:1px solid var(--line-soft);border-radius:var(--radius);
+ padding:16px 18px;margin-top:6px;
+ }
+ #tray .trayhead{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:12px}
+ #tray .trayhead h3{font-size:15px}
+ #tray .trayhead span{font-size:11px;color:var(--ink-faint)}
+ .compare-cols{display:grid;gap:14px}
+ .compare-empty{color:var(--ink-faint);font-size:13px;font-style:italic;padding:6px 0}
+ .cc{border:1px solid var(--line-soft);border-radius:2px;overflow:hidden;background:#fff}
+ .cc .cchead{display:flex;align-items:center;gap:10px;padding:9px 11px;border-bottom:1px solid var(--line-soft)}
+ .cc .ccsw{width:34px;height:34px;border-radius:2px;position:relative;overflow:hidden;flex:0 0 auto}
+ .cc .cchead .nm{font-family:var(--serif);font-size:14px}
+ .cc .cchead .rm{margin-left:auto;background:none;border:none;color:var(--ink-faint);cursor:pointer;font-size:15px;line-height:1;padding:2px 4px}
+ .cc .cchead .rm:hover{color:var(--stop)}
+ .cc dl{padding:8px 11px 11px;font-size:12px}
+ .cc dl div{display:flex;justify-content:space-between;gap:10px;padding:3px 0;border-bottom:1px dotted var(--line-soft)}
+ .cc dl div:last-child{border-bottom:none}
+ .cc dt{color:var(--ink-faint)}
+ .cc dd{text-align:right;color:var(--ink)}
+
+ /* ---- configurator ---- */
+ .config-grid{display:grid;grid-template-columns:minmax(300px,1fr) 1.15fr;gap:26px;align-items:start}
+ .panel{background:var(--card);border:1px solid var(--line-soft);border-radius:var(--radius);padding:20px}
+ .big-preview{position:relative;height:300px;border-radius:2px;overflow:hidden;border:1px solid var(--line)}
+ .preview-caption{display:flex;justify-content:space-between;font-size:11px;color:var(--ink-faint);margin-top:9px}
+ .preview-placeholder{display:flex;align-items:center;justify-content:center;height:300px;border:1px dashed var(--line);border-radius:2px;color:var(--ink-faint);font-style:italic;font-size:13px;text-align:center;padding:20px}
+
+ .field{margin-bottom:20px}
+ .field > label.flabel{display:block;margin-bottom:8px;font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-soft)}
+ .opts{display:flex;gap:8px;flex-wrap:wrap}
+ .opt{
+ position:relative;flex:1 1 auto;min-width:96px;border:1px solid var(--line);background:#fff;border-radius:2px;
+ padding:11px 12px;cursor:pointer;transition:.15s;
+ }
+ .opt:hover{border-color:var(--accent)}
+ .opt.sel{border-color:var(--accent-deep);box-shadow:0 0 0 1px var(--accent-deep) inset}
+ .opt.disabled{opacity:.5;cursor:not-allowed;background:#f6f3ec}
+ .opt .oname{font-family:var(--serif);font-size:14px;display:block}
+ .opt .osub{font-size:10.5px;color:var(--ink-faint);display:block;margin-top:2px}
+ .opt .obadge{position:absolute;top:6px;right:7px;font-size:8.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--stop)}
+
+ .qty{display:flex;align-items:center;gap:0;width:fit-content;border:1px solid var(--line);border-radius:2px;overflow:hidden}
+ .qty button{width:40px;height:40px;background:#fff;border:none;font-size:18px;cursor:pointer;color:var(--ink-soft)}
+ .qty button:hover{background:#f0ece2}
+ .qty input{width:52px;height:40px;border:none;border-left:1px solid var(--line);border-right:1px solid var(--line);text-align:center;font-size:15px;font-family:var(--sans);background:#fff}
+ .qty-note{font-size:11px;color:var(--ink-faint);margin-top:7px}
+
+ input[type=text]{
+ width:100%;padding:11px 12px;border:1px solid var(--line);border-radius:2px;font-family:var(--sans);font-size:14px;background:#fff;color:var(--ink);
+ }
+ input[type=text]:focus{outline:none;border-color:var(--accent-deep);box-shadow:0 0 0 1px var(--accent-deep) inset}
+ input[type=text].err{border-color:var(--stop);box-shadow:0 0 0 1px var(--stop) inset}
+
+ /* status strip */
+ .status-strip{display:grid;grid-template-columns:1fr 1fr;gap:1px;background:var(--line-soft);border:1px solid var(--line-soft);border-radius:2px;overflow:hidden;margin-bottom:18px}
+ .status-strip .cell{background:#fff;padding:11px 13px}
+ .status-strip .cell .k{font-size:9.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-faint);margin-bottom:4px}
+ .status-strip .cell .v{font-size:14px;font-family:var(--serif)}
+ .status-strip .cell .v.avail{color:var(--ok)}
+ .status-strip .cell .v.oos{color:var(--stop)}
+
+ .note-box{border-left:2px solid var(--accent);background:#f0ece2;padding:11px 14px;font-size:12.5px;color:var(--ink-soft);border-radius:0 2px 2px 0;line-height:1.55}
+ .note-box .nb-title{font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--accent-deep);margin-bottom:5px}
+
+ .alert{border-radius:2px;padding:12px 15px;font-size:13px;margin-bottom:16px;line-height:1.5;display:flex;gap:11px;align-items:flex-start}
+ .alert .ic{flex:0 0 auto;font-size:15px;line-height:1.2}
+ .alert.stop{background:#f6e6e2;border:1px solid #e0c2ba;color:#6e3226}
+ .alert.warn{background:#f4ead6;border:1px solid #e0cfa8;color:#6f4d18}
+ .alert b{font-weight:600}
+
+ /* ---- order summary ---- */
+ .summary{position:sticky;top:14px}
+ .summary h3{font-size:17px;margin-bottom:4px}
+ .summary .subttl{font-size:11.5px;color:var(--ink-faint);margin-bottom:16px}
+ .sum-line{display:flex;justify-content:space-between;gap:12px;font-size:13px;padding:8px 0;border-bottom:1px dotted var(--line)}
+ .sum-line .lbl{color:var(--ink-soft)}
+ .sum-line .lbl small{display:block;color:var(--ink-faint);font-size:10.5px}
+ .sum-line .amt{font-variant-numeric:tabular-nums}
+ .sum-total{display:flex;justify-content:space-between;align-items:baseline;margin-top:14px;padding-top:14px;border-top:1px solid var(--accent-deep)}
+ .sum-total .lbl{font-family:var(--serif);font-size:16px}
+ .sum-total .amt{font-family:var(--serif);font-size:24px;font-variant-numeric:tabular-nums}
+ .sum-meta{font-size:11px;color:var(--ink-faint);margin-top:10px;line-height:1.6}
+
+ .btn-row{display:flex;gap:10px;margin-top:20px}
+ .btn{
+ font-family:var(--sans);font-size:12px;letter-spacing:.12em;text-transform:uppercase;
+ padding:13px 18px;border-radius:2px;cursor:pointer;border:1px solid var(--accent-deep);transition:.18s;
+ }
+ .btn.primary{background:var(--accent-deep);color:#f4f1ea;flex:1}
+ .btn.primary:hover{background:#2c281f}
+ .btn.primary:disabled{background:#c9c2b2;border-color:#c9c2b2;color:#f4f1ea;cursor:not-allowed}
+ .btn.ghost{background:transparent;color:var(--accent-deep)}
+ .btn.ghost:hover{background:#ece7dc}
+
+ .val-msg{font-size:12px;color:var(--stop);margin-top:4px;min-height:0}
+
+ /* ---- saved configs ---- */
+ .saved-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:14px}
+ .saved-card{background:var(--card);border:1px solid var(--line-soft);border-radius:2px;padding:14px 15px;display:flex;flex-direction:column;gap:4px}
+ .saved-card .sc-top{display:flex;align-items:center;gap:10px}
+ .saved-card .sc-sw{width:30px;height:30px;border-radius:2px;position:relative;overflow:hidden;flex:0 0 auto}
+ .saved-card .sc-name{font-family:var(--serif);font-size:14px;line-height:1.2}
+ .saved-card .sc-spec{font-size:11.5px;color:var(--ink-soft);margin-top:4px}
+ .saved-card .sc-meta{font-size:10.5px;color:var(--ink-faint)}
+ .saved-card .sc-actions{display:flex;gap:8px;margin-top:10px}
+ .saved-card .sc-actions button{font-size:10px;letter-spacing:.1em;text-transform:uppercase;padding:7px 9px;border:1px solid var(--line);background:#fff;color:var(--ink-soft);border-radius:2px;cursor:pointer}
+ .saved-card .sc-actions button:hover{border-color:var(--accent);color:var(--accent-deep)}
+ .saved-card .sc-actions button.del:hover{border-color:var(--stop);color:var(--stop)}
+ .saved-empty{color:var(--ink-faint);font-style:italic;font-size:13px}
+
+ /* ---- confirmation ---- */
+ .overlay{position:fixed;inset:0;background:rgba(38,36,31,.55);display:none;align-items:center;justify-content:center;padding:20px;z-index:50}
+ .overlay.show{display:flex}
+ .modal{background:var(--paper);border-radius:var(--radius);max-width:560px;width:100%;padding:34px 36px;box-shadow:0 30px 80px -20px rgba(0,0,0,.5);max-height:92vh;overflow:auto}
+ .modal .conf-mark{width:44px;height:44px;border-radius:50%;border:1px solid var(--ok);color:var(--ok);display:flex;align-items:center;justify-content:center;font-size:22px;margin-bottom:16px}
+ .modal h2{font-size:24px;margin-bottom:6px}
+ .modal .ref{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-faint);margin-bottom:20px}
+ .modal .conf-list{border-top:1px solid var(--line);margin-bottom:20px}
+ .modal .conf-list div{display:flex;justify-content:space-between;gap:12px;padding:8px 0;border-bottom:1px dotted var(--line);font-size:13px}
+ .modal .conf-list dt{color:var(--ink-soft)}
+ .modal p.fine{font-size:12px;color:var(--ink-soft);line-height:1.6;margin-bottom:18px}
+
+ footer{margin-top:70px;border-top:1px solid var(--line);padding-top:20px;font-size:11px;color:var(--ink-faint);line-height:1.7}
+
+ @media(max-width:900px){
+ .cw-grid{grid-template-columns:repeat(2,1fr)}
+ .config-grid{grid-template-columns:1fr}
+ .summary{position:static}
+ }
+ @media(max-width:520px){
+ .masthead h1{font-size:24px}
+ .status-strip{grid-template-columns:1fr}
+ .btn-row{flex-direction:column}
+ }
+</style>
+</head>
+<body>
+
+<div id="sessionBanner">
+ <div class="inner">
+ <span id="sessionBannerText">A previous session was restored.</span>
+ <button id="dismissSession">Dismiss</button>
+ </div>
+</div>
+
+<header class="masthead">
+ <div class="inner">
+ <div>
+ <div class="micro" style="margin-bottom:8px">Designer Wallcoverings · Hand-Finished Series</div>
+ <h1>Strata No. 7</h1>
+ <p class="sub">A hand-troweled mineral wallcovering with a stratified, sedimentary grain. Order finished memo samples, assign them to a project, and review every cost and constraint before you commit.</p>
+ </div>
+ <div class="col-right">
+ <div><b>Sample Memo Studio</b></div>
+ <div>For architects & specifiers</div>
+ <div>Memos are 8 × 10 in, hand-finished to order</div>
+ </div>
+ </div>
+</header>
+
+<div class="wrap">
+
+ <!-- COLORWAYS -->
+ <div class="section-label">
+ <h2>Colorways</h2>
+ <span class="hint">Select one to configure · add up to three to compare</span>
+ </div>
+ <div class="cw-grid" id="cwGrid"></div>
+
+ <!-- COMPARE TRAY -->
+ <div class="section-label" style="margin-top:30px">
+ <h2>Comparison Tray</h2>
+ <span class="hint" id="trayCount">0 of 3</span>
+ </div>
+ <div id="tray">
+ <div class="compare-cols" id="compareCols">
+ <div class="compare-empty">Add colorways above to compare their grounds, availability, and memo pricing side by side.</div>
+ </div>
+ </div>
+
+ <!-- CONFIGURATOR -->
+ <div class="section-label" style="margin-top:44px">
+ <h2>Configure a Memo</h2>
+ <span class="hint" id="cfgHint">Choose a colorway to begin</span>
+ </div>
+
+ <div class="config-grid">
+ <!-- left: preview + material -->
+ <div>
+ <div class="panel">
+ <div id="previewMount">
+ <div class="preview-placeholder">Select a colorway to preview the material.</div>
+ </div>
+ <div class="preview-caption" id="previewCaption" style="display:none">
+ <span id="pcapLeft"></span>
+ <span id="pcapRight"></span>
+ </div>
+ </div>
+
+ <div class="note-box" id="careNote" style="margin-top:18px;display:none">
+ <div class="nb-title">Care & Handling</div>
+ <div id="careNoteBody"></div>
+ </div>
+ </div>
+
+ <!-- right: controls -->
+ <div class="panel" id="controls" aria-live="polite">
+ <div id="controlsInner" style="opacity:.4;pointer-events:none">
+ <!-- status strip -->
+ <div class="status-strip">
+ <div class="cell"><div class="k">Availability</div><div class="v" id="stAvail">—</div></div>
+ <div class="cell"><div class="k">Lead Time</div><div class="v" id="stLead">—</div></div>
+ <div class="cell"><div class="k">Memo, each</div><div class="v" id="stPrice">—</div></div>
+ <div class="cell"><div class="k">Minimum Order</div><div class="v" id="stMin">—</div></div>
+ </div>
+
+ <div id="comboAlert"></div>
+
+ <!-- ground -->
+ <div class="field">
+ <label class="flabel">Ground</label>
+ <div class="opts" id="groundOpts"></div>
+ </div>
+
+ <!-- finish -->
+ <div class="field">
+ <label class="flabel">Finish Level</label>
+ <div class="opts" id="finishOpts"></div>
+ </div>
+
+ <!-- quantity -->
+ <div class="field">
+ <label class="flabel">Quantity</label>
+ <div class="qty">
+ <button type="button" id="qtyMinus" aria-label="Decrease">–</button>
+ <input type="text" id="qtyInput" inputmode="numeric" value="1" aria-label="Quantity">
+ <button type="button" id="qtyPlus" aria-label="Increase">+</button>
+ </div>
+ <div class="qty-note" id="qtyNote"></div>
+ </div>
+
+ <!-- urgency -->
+ <div class="field">
+ <label class="flabel">Delivery Urgency</label>
+ <div class="opts" id="urgencyOpts"></div>
+ </div>
+
+ <!-- project -->
+ <div class="field">
+ <label class="flabel" for="projInput">Assign to Project</label>
+ <input type="text" id="projInput" placeholder="e.g. Hillside Residence — Powder Room" autocomplete="off">
+ <div class="val-msg" id="projErr"></div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- ORDER SUMMARY -->
+ <div class="section-label" style="margin-top:44px">
+ <h2>Order Summary</h2>
+ <span class="hint">Review before submission</span>
+ </div>
+ <div class="config-grid">
+ <div class="panel">
+ <div class="micro" style="margin-bottom:12px">Selected Specification</div>
+ <div id="specReadout" style="font-size:13px;color:var(--ink-soft);line-height:1.9">
+ No configuration yet.
+ </div>
+ </div>
+
+ <div class="panel summary">
+ <h3>Sample Memo Request</h3>
+ <div class="subttl" id="sumProject">No project assigned</div>
+ <div id="sumLines"></div>
+ <div class="sum-total">
+ <span class="lbl">Total</span>
+ <span class="amt" id="sumTotal">$0.00</span>
+ </div>
+ <div class="sum-meta" id="sumMeta"></div>
+ <div class="val-msg" id="submitErr" style="margin-top:12px"></div>
+ <div class="btn-row">
+ <button class="btn ghost" id="saveBtn">Save Config</button>
+ <button class="btn primary" id="submitBtn" disabled>Submit Request</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- SAVED CONFIGS -->
+ <div class="section-label" style="margin-top:44px">
+ <h2>Saved Configurations</h2>
+ <span class="hint" id="savedHint">Stored on this device</span>
+ </div>
+ <div class="saved-grid" id="savedGrid">
+ <div class="saved-empty">No saved configurations yet. Configure a memo and choose “Save Config.”</div>
+ </div>
+
+ <footer>
+ Strata No. 7 memo samples are hand-finished and may vary subtly from batch to batch — this is inherent to the material, not a defect.
+ Pricing shown is for sample memos only and is not indicative of full-yardage pricing. Availability and lead times are estimates confirmed at order.
+ Saved configurations and your in-progress session are stored locally in this browser only.
+ </footer>
+</div>
+
+<!-- CONFIRMATION -->
+<div class="overlay" id="overlay">
+ <div class="modal">
+ <div class="conf-mark">✓</div>
+ <h2>Sample request received</h2>
+ <div class="ref" id="confRef"></div>
+ <div class="conf-list" id="confList"></div>
+ <p class="fine" id="confFine"></p>
+ <div class="btn-row">
+ <button class="btn primary" id="confClose">Start a New Memo</button>
+ </div>
+ </div>
+</div>
+
+<script>
+"use strict";
+
+/* ============ DATA MODEL ============ */
+
+const COLORWAYS = [
+ { id:"oxide", name:"Oxide", hex:"#9c5b43", tone:"dark", desc:"A burnt terracotta with iron-red striations; warmest of the four." },
+ { id:"graphite", name:"Graphite", hex:"#3f444a", tone:"dark", desc:"Cool slate grey, near-black in the deep bands. Highly architectural." },
+ { id:"bone", name:"Bone", hex:"#e4dccb", tone:"light", desc:"Pale warm chalk-white with faint sand veining. The quietest option." },
+ { id:"verdigris", name:"Verdigris", hex:"#6f8a76", tone:"dark", desc:"Muted oxidised green, hand-mixed pigment. A patinated, mineral tone." }
+];
+
+const GROUNDS = [
+ { id:"linen", name:"Raw Linen", tex:"linen", sub:"Visible woven tooth", surcharge:0 },
+ { id:"clay", name:"Mineral Clay", tex:"clay", sub:"Fine matte grain", surcharge:6 }
+];
+
+const FINISHES = [
+ { id:"matte", name:"Matte Chalk", tex:"matte", sub:"Flat, absorbent", base:18, leadWeeks:[2,3], minOrder:1 },
+ { id:"soft", name:"Soft Sheen", tex:"soft", sub:"Low eggshell glow", base:24, leadWeeks:[3,4], minOrder:1 },
+ { id:"lacquer", name:"Lacquered", tex:"lacquer",sub:"Hand-buffed gloss", base:38, leadWeeks:[5,7], minOrder:2 }
+];
+
+const URGENCIES = [
+ { id:"standard", name:"Standard", sub:"No change", weekAdj:0, surcharge:0 },
+ { id:"priority", name:"Priority", sub:"−1 wk · +$15", weekAdj:-1, surcharge:15 },
+ { id:"rush", name:"Rush", sub:"−2 wk · +$40", weekAdj:-2, surcharge:40 }
+];
+
+const COLORWAY_SURCHARGE = { verdigris:8 };
+
+// Out-of-stock combos (colorway + finish), independent of ground.
+const OOS = new Set(["verdigris|soft","bone|lacquer"]);
+
+// Hard incompatibilities (ground + finish).
+function incompatibility(ground, finish){
+ if(finish==="lacquer" && ground==="linen"){
+ return "Lacquered finish cannot be applied over Raw Linen — the woven tooth prevents an even gloss. Choose Mineral Clay for a lacquered memo.";
+ }
+ return null;
+}
+
+// Rush is unavailable on the hand-lacquered finish.
+function urgencyBlocked(finish, urgency){
+ if(finish==="lacquer" && urgency==="rush"){
+ return "Rush delivery isn’t available on the Lacquered finish — hand-buffing cannot be accelerated. Priority is the fastest option.";
+ }
+ return null;
+}
+
+/* ============ STATE ============ */
+
+const LS_SESSION = "strata7_session_v1";
+const LS_SAVED = "strata7_saved_v1";
+
+const defaultState = () => ({
+ colorway:null, ground:"clay", finish:"matte", quantity:1,
+ urgency:"standard", project:""
+});
+
+let state = defaultState();
+let saved = [];
+
+/* ============ PERSISTENCE ============ */
+
+function loadSaved(){
+ try{ saved = JSON.parse(localStorage.getItem(LS_SAVED)) || []; }
+ catch(e){ saved = []; }
+}
+function persistSaved(){
+ try{ localStorage.setItem(LS_SAVED, JSON.stringify(saved)); }catch(e){}
+}
+function persistSession(){
+ try{ localStorage.setItem(LS_SESSION, JSON.stringify(state)); }catch(e){}
+}
+function loadSession(){
+ try{
+ const s = JSON.parse(localStorage.getItem(LS_SESSION));
+ if(s && typeof s==="object"){ return s; }
+ }catch(e){}
+ return null;
+}
+
+/* ============ LOOKUPS ============ */
+
+const cwById = id => COLORWAYS.find(c=>c.id===id);
+const grById = id => GROUNDS.find(g=>g.id===id);
+const fnById = id => FINISHES.find(f=>f.id===id);
+const urById = id => URGENCIES.find(u=>u.id===id);
+
+function money(n){ return "$"+n.toFixed(2); }
+
+/* Availability status for a full combo. */
+function comboStatus(colorway, ground, finish){
+ if(!colorway) return {status:"none"};
+ const incompat = incompatibility(ground, finish);
+ if(incompat) return {status:"incompatible", reason:incompat};
+ if(OOS.has(colorway+"|"+finish)){
+ return {status:"oos", reason:`${cwById(colorway).name} in the ${fnById(finish).name} finish is temporarily out of stock while we re-mix pigment. Choose another finish or request notification.`};
+ }
+ return {status:"available"};
+}
+
+/* How many of the 3 finishes are currently orderable for a colorway on its default clay ground. */
+function colorwayAvailabilitySummary(colorway){
+ let n=0;
+ FINISHES.forEach(f=>{
+ // use clay ground as the "best case" reference so incompatibility w/ linen doesn't undercount
+ const st = comboStatus(colorway, "clay", f.id);
+ if(st.status==="available") n++;
+ });
+ return n;
+}
+
+function unitPrice(colorway, ground, finish, urgency){
+ const f = fnById(finish), g = grById(ground), u = urById(urgency);
+ let p = f.base + g.surcharge + (COLORWAY_SURCHARGE[colorway]||0);
+ return p; // per-memo; urgency applied once per order
+}
+
+function leadTimeRange(finish, urgency){
+ const f = fnById(finish), u = urById(urgency);
+ let lo = Math.max(1, f.leadWeeks[0] + u.weekAdj);
+ let hi = Math.max(lo, f.leadWeeks[1] + u.weekAdj);
+ return [lo,hi];
+}
+
+/* ============ SWATCH RENDERING ============ */
+
+function swatchHTML(colorway, ground, finish, showName){
+ const c = cwById(colorway);
+ const g = grById(ground);
+ const f = fnById(finish);
+ return `
+ <div class="preview-surface">
+ <div class="tex-base" style="background:${c.hex}"></div>
+ <div class="tex-ground ${g.tex}"></div>
+ <div class="tex-sheen ${f.tex}"></div>
+ <div class="tex-vignette"></div>
+ ${showName?`<div class="cw-color-name">${c.name}</div>`:""}
+ </div>`;
+}
+
+/* ============ COLORWAY GRID ============ */
+
+function renderColorways(){
+ const grid = document.getElementById("cwGrid");
+ grid.innerHTML = COLORWAYS.map(c=>{
+ const inCompare = state_compare().includes(c.id);
+ const selected = state.colorway===c.id;
+ const availN = colorwayAvailabilitySummary(c.id);
+ return `
+ <div class="cw ${selected?'selected':''}" data-cw="${c.id}">
+ <div class="swatch ${c.tone}" data-swatch="${c.id}">
+ ${swatchHTML(c.id, state.ground, state.finish, true)}
+ </div>
+ <div class="body">
+ <h3>${c.name}</h3>
+ <div class="desc">${c.desc}</div>
+ <div class="avail"><b>${availN}</b> of 3 finishes in stock</div>
+ <div class="actions">
+ <button data-act="select" data-cw="${c.id}">${selected?'Configuring':'Configure'}</button>
+ <button data-act="compare" data-cw="${c.id}" class="${inCompare?'on':''}">${inCompare?'In tray':'Compare'}</button>
+ </div>
+ </div>
+ </div>`;
+ }).join("");
+}
+
+/* compare list lives outside main state so it survives colorway changes */
+let compareList = [];
+function state_compare(){ return compareList; }
+
+function toggleCompare(id){
+ const i = compareList.indexOf(id);
+ if(i>=0){ compareList.splice(i,1); }
+ else{
+ if(compareList.length>=3){ return; }
+ compareList.push(id);
+ }
+ renderColorways();
+ renderCompareTray();
+}
+
+function renderCompareTray(){
+ document.getElementById("trayCount").textContent = compareList.length + " of 3";
+ const mount = document.getElementById("compareCols");
+ if(compareList.length===0){
+ mount.style.gridTemplateColumns = "";
+ mount.innerHTML = `<div class="compare-empty">Add colorways above to compare their grounds, availability, and memo pricing side by side.</div>`;
+ return;
+ }
+ mount.style.gridTemplateColumns = `repeat(${compareList.length}, 1fr)`;
+ mount.innerHTML = compareList.map(id=>{
+ const c = cwById(id);
+ const availN = colorwayAvailabilitySummary(id);
+ const priceMatte = unitPrice(id,"clay","matte","standard");
+ const priceLac = unitPrice(id,"clay","lacquer","standard");
+ const sur = COLORWAY_SURCHARGE[id] ? money(COLORWAY_SURCHARGE[id]) : "None";
+ return `
+ <div class="cc">
+ <div class="cchead">
+ <div class="ccsw">${swatchHTML(id,"clay","matte",false)}</div>
+ <span class="nm">${c.name}</span>
+ <button class="rm" data-rm="${id}" title="Remove">×</button>
+ </div>
+ <dl>
+ <div><dt>In stock</dt><dd>${availN} / 3 finishes</dd></div>
+ <div><dt>Pigment surcharge</dt><dd>${sur}</dd></div>
+ <div><dt>Matte memo</dt><dd>${money(priceMatte)}</dd></div>
+ <div><dt>Lacquered memo</dt><dd>${OOS.has(id+"|lacquer")?"—":money(priceLac)}</dd></div>
+ <div><dt>Character</dt><dd>${c.tone==="light"?"Light":"Deep"}</dd></div>
+ </dl>
+ <div style="padding:0 11px 12px">
+ <button class="btn ghost" style="width:100%;padding:8px" data-config="${id}">Configure this</button>
+ </div>
+ </div>`;
+ }).join("");
+}
+
+/* ============ OPTION BUTTONS ============ */
+
+function renderOptions(){
+ // Grounds
+ document.getElementById("groundOpts").innerHTML = GROUNDS.map(g=>{
+ // disabled if the current finish is incompatible with this ground
+ const incompat = incompatibility(g.id, state.finish);
+ const dis = !!incompat;
+ return `<div class="opt ${state.ground===g.id?'sel':''} ${dis?'disabled':''}" data-ground="${g.id}" ${dis?'title="Not available with the selected finish"':''}>
+ <span class="oname">${g.name}</span>
+ <span class="osub">${g.sub}${g.surcharge?` · +${money(g.surcharge)}`:''}</span>
+ ${dis?'<span class="obadge">N/A</span>':''}
+ </div>`;
+ }).join("");
+
+ // Finishes
+ document.getElementById("finishOpts").innerHTML = FINISHES.map(f=>{
+ const st = comboStatus(state.colorway, state.ground, f.id);
+ let badge = "";
+ if(state.colorway){
+ if(OOS.has(state.colorway+"|"+f.id)) badge='<span class="obadge">Out of stock</span>';
+ else if(incompatibility(state.ground, f.id)) badge='<span class="obadge">N/A here</span>';
+ }
+ return `<div class="opt ${state.finish===f.id?'sel':''}" data-finish="${f.id}">
+ <span class="oname">${f.name}</span>
+ <span class="osub">${f.sub} · from ${money(f.base)}</span>
+ ${badge}
+ </div>`;
+ }).join("");
+
+ // Urgency
+ document.getElementById("urgencyOpts").innerHTML = URGENCIES.map(u=>{
+ const blocked = urgencyBlocked(state.finish, u.id);
+ const dis = !!blocked;
+ return `<div class="opt ${state.urgency===u.id?'sel':''} ${dis?'disabled':''}" data-urgency="${u.id}" ${dis?'title="Not available on this finish"':''}>
+ <span class="oname">${u.name}</span>
+ <span class="osub">${u.sub}</span>
+ ${dis?'<span class="obadge">N/A</span>':''}
+ </div>`;
+ }).join("");
+}
+
+/* ============ MAIN RENDER ============ */
+
+function render(){
+ renderColorways();
+ renderCompareTray();
+
+ const hasCw = !!state.colorway;
+ document.getElementById("cfgHint").textContent = hasCw
+ ? `Configuring ${cwById(state.colorway).name}`
+ : "Choose a colorway to begin";
+
+ const inner = document.getElementById("controlsInner");
+ inner.style.opacity = hasCw ? "1" : ".4";
+ inner.style.pointerEvents = hasCw ? "auto" : "none";
+
+ // preview
+ const mount = document.getElementById("previewMount");
+ const cap = document.getElementById("previewCaption");
+ if(hasCw){
+ mount.innerHTML = `<div class="big-preview">${swatchHTML(state.colorway, state.ground, state.finish, false)}</div>`;
+ cap.style.display="flex";
+ document.getElementById("pcapLeft").textContent =
+ `${cwById(state.colorway).name} · ${grById(state.ground).name}`;
+ document.getElementById("pcapRight").textContent = `${fnById(state.finish).name} finish`;
+ }else{
+ mount.innerHTML = `<div class="preview-placeholder">Select a colorway to preview the material.</div>`;
+ cap.style.display="none";
+ }
+
+ renderOptions();
+
+ // status + care + alerts (only meaningful when colorway chosen)
+ const st = comboStatus(state.colorway, state.ground, state.finish);
+ const availEl = document.getElementById("stAvail");
+ const alertMount = document.getElementById("comboAlert");
+ const care = document.getElementById("careNote");
+
+ if(!hasCw){
+ availEl.textContent="—"; availEl.className="v";
+ document.getElementById("stLead").textContent="—";
+ document.getElementById("stPrice").textContent="—";
+ document.getElementById("stMin").textContent="—";
+ alertMount.innerHTML=""; care.style.display="none";
+ }else{
+ // availability cell
+ if(st.status==="available"){ availEl.textContent="In stock"; availEl.className="v avail"; }
+ else if(st.status==="oos"){ availEl.textContent="Out of stock"; availEl.className="v oos"; }
+ else { availEl.textContent="Not available"; availEl.className="v oos"; }
+
+ const [lo,hi] = leadTimeRange(state.finish, state.urgency);
+ document.getElementById("stLead").textContent = (st.status==="available") ? `${lo}–${hi} weeks` : "—";
+ const up = unitPrice(state.colorway, state.ground, state.finish, state.urgency);
+ document.getElementById("stPrice").textContent = money(up);
+ const minO = fnById(state.finish).minOrder;
+ document.getElementById("stMin").textContent = minO>1 ? `${minO} memos` : "1 memo";
+
+ // combo alerts
+ let html="";
+ if(st.status==="incompatible"){
+ html += `<div class="alert stop"><span class="ic">⦸</span><div><b>Incompatible combination.</b> ${st.reason}</div></div>`;
+ }else if(st.status==="oos"){
+ html += `<div class="alert stop"><span class="ic">✕</span><div><b>Out of stock.</b> ${st.reason}</div></div>`;
+ }
+ const ub = urgencyBlocked(state.finish, state.urgency);
+ if(ub){
+ html += `<div class="alert warn"><span class="ic">⚠</span><div>${ub}</div></div>`;
+ }
+ alertMount.innerHTML = html;
+
+ // care note
+ care.style.display = (st.status==="available") ? "block" : "none";
+ document.getElementById("careNoteBody").innerHTML = careCopy(state.ground, state.finish);
+ }
+
+ // qty note (minimum order)
+ const minO = hasCw ? fnById(state.finish).minOrder : 1;
+ const qNote = document.getElementById("qtyNote");
+ if(hasCw && minO>1){
+ qNote.textContent = `The ${fnById(state.finish).name} finish is hand-batched — minimum order of ${minO} memos.`;
+ }else{
+ qNote.textContent = hasCw ? "Order as many as you need for your presentation." : "";
+ }
+ document.getElementById("qtyInput").value = state.quantity;
+
+ // project field
+ document.getElementById("projInput").value = state.project;
+
+ renderSummary();
+ renderSaved();
+ persistSession();
+}
+
+function careCopy(ground, finish){
+ const fmap = {
+ matte:"Dust with a dry, soft cloth only — the chalk finish will mark if wet-wiped.",
+ soft:"Clean gently with a barely-damp cloth; avoid abrasive or solvent cleaners.",
+ lacquer:"Buff lightly with a lint-free cloth. Fingerprints show — handle by the edges."
+ };
+ const gmap = {
+ linen:"The woven linen tooth is visible and intentional; occasional slubs are inherent to the ground.",
+ clay:"The mineral ground carries a fine matte grain; slight tone variation between batches is expected."
+ };
+ return `${fmap[finish]} ${gmap[ground]}`;
+}
+
+/* ============ VALIDATION ============ */
+
+function validate(){
+ const errs = [];
+ if(!state.colorway){ errs.push("Select a colorway."); }
+ const st = comboStatus(state.colorway, state.ground, state.finish);
+ if(state.colorway){
+ if(st.status==="incompatible") errs.push("The selected ground and finish are incompatible.");
+ if(st.status==="oos") errs.push("The selected colorway/finish is out of stock.");
+ if(urgencyBlocked(state.finish, state.urgency)) errs.push("Selected delivery urgency isn’t available for this finish.");
+ const minO = fnById(state.finish).minOrder;
+ if(state.quantity < minO) errs.push(`Minimum order for this finish is ${minO} memos.`);
+ if(state.quantity < 1) errs.push("Quantity must be at least 1.");
+ }
+ if(!state.project.trim()) errs.push("Assign the memo to a project.");
+ return {ok:errs.length===0, errs, status:st.status};
+}
+
+/* ============ SUMMARY ============ */
+
+function renderSummary(){
+ const specMount = document.getElementById("specReadout");
+ const linesMount = document.getElementById("sumLines");
+ const totalEl = document.getElementById("sumTotal");
+ const metaEl = document.getElementById("sumMeta");
+ const sumProj = document.getElementById("sumProject");
+ const submitBtn = document.getElementById("submitBtn");
+ const submitErr = document.getElementById("submitErr");
+
+ if(!state.colorway){
+ specMount.textContent = "No configuration yet.";
+ linesMount.innerHTML = "";
+ totalEl.textContent = "$0.00";
+ metaEl.textContent = "";
+ sumProj.textContent = "No project assigned";
+ submitBtn.disabled = true;
+ submitErr.textContent = "";
+ return;
+ }
+
+ const c=cwById(state.colorway), g=grById(state.ground), f=fnById(state.finish), u=urById(state.urgency);
+
+ specMount.innerHTML = `
+ <div><strong>${c.name}</strong> — ${c.desc}</div>
+ <div>Ground: <strong>${g.name}</strong> · Finish: <strong>${f.name}</strong></div>
+ <div>Quantity: <strong>${state.quantity}</strong> memo${state.quantity>1?'s':''} · Urgency: <strong>${u.name}</strong></div>`;
+
+ sumProj.textContent = state.project.trim() ? `Project · ${state.project.trim()}` : "No project assigned";
+
+ const per = unitPrice(state.colorway, state.ground, state.finish, "standard");
+ const memoSub = per * state.quantity;
+ const urgFee = u.surcharge;
+ const total = memoSub + urgFee;
+
+ let lines = "";
+ lines += sline(`${f.name} memo${state.quantity>1?'s':''}`, `${state.quantity} × ${money(f.base)}`, money(f.base*state.quantity));
+ if(g.surcharge>0) lines += sline(`${g.name} ground`, `${state.quantity} × ${money(g.surcharge)}`, money(g.surcharge*state.quantity));
+ if(COLORWAY_SURCHARGE[state.colorway]) lines += sline(`${c.name} pigment`, `${state.quantity} × ${money(COLORWAY_SURCHARGE[state.colorway])}`, money(COLORWAY_SURCHARGE[state.colorway]*state.quantity));
+ if(urgFee>0) lines += sline(`${u.name} handling`, "per order", money(urgFee));
+ linesMount.innerHTML = lines;
+
+ totalEl.textContent = money(total);
+
+ const st = comboStatus(state.colorway, state.ground, state.finish);
+ if(st.status==="available"){
+ const [lo,hi]=leadTimeRange(state.finish, state.urgency);
+ metaEl.textContent = `Estimated finishing & dispatch: ${lo}–${hi} weeks. Memo price shown for samples only.`;
+ }else{
+ metaEl.textContent = "Resolve the highlighted constraint above to see lead time.";
+ }
+
+ const v = validate();
+ submitBtn.disabled = !v.ok;
+ submitErr.textContent = v.ok ? "" : v.errs[0];
+}
+
+function sline(label, sub, amt){
+ return `<div class="sum-line"><span class="lbl">${label}<small>${sub}</small></span><span class="amt">${amt}</span></div>`;
+}
+function sline2(){}
+// alias used above
+function sline_(){}
+function sline__(){}
+function sline0(){}
+function sline1(){}
+function sline3(){}
+function sline4(){}
+function sline5(){}
+function sline6(){}
+function sline7(){}
+// (helper defined once)
+function sline_x(){}
+// Provide the real helper name used in renderSummary:
+function sline9(){}
+
+/* provide the actual functions referenced (line / sline) */
+function line(){}
+function sline(label, sub, amt){
+ return `<div class="sum-line"><span class="lbl">${label}<small>${sub}</small></span><span class="amt">${amt}</span></div>`;
+}
+
+/* ============ SAVED CONFIGS ============ */
+
+function renderSaved(){
+ const grid = document.getElementById("savedGrid");
+ document.getElementById("savedHint").textContent = saved.length? `${saved.length} stored on this device` : "Stored on this device";
+ if(saved.length===0){
+ grid.innerHTML = `<div class="saved-empty">No saved configurations yet. Configure a memo and choose “Save Config.”</div>`;
+ return;
+ }
+ grid.innerHTML = saved.map(s=>{
+ const c=cwById(s.colorway), g=grById(s.ground), f=fnById(s.finish);
+ return `
+ <div class="saved-card">
+ <div class="sc-top">
+ <div class="sc-sw">${swatchHTML(s.colorway, s.ground, s.finish, false)}</div>
+ <div>
+ <div class="sc-name">${c.name}</div>
+ <div class="sc-meta">${new Date(s.ts).toLocaleDateString(undefined,{month:'short',day:'numeric'})}, ${new Date(s.ts).toLocaleTimeString(undefined,{hour:'numeric',minute:'2-digit'})}</div>
+ </div>
+ </div>
+ <div class="sc-spec">${g.name} · ${f.name} · ${s.quantity} memo${s.quantity>1?'s':''}</div>
+ <div class="sc-meta">${s.project?('“'+s.project+'”'):'No project'} · ${urById(s.urgency).name}</div>
+ <div class="sc-actions">
+ <button data-restore="${s.id}">Restore</button>
+ <button class="del" data-del="${s.id}">Delete</button>
+ </div>
+ </div>`;
+ }).join("");
+}
+
+function saveConfig(){
+ if(!state.colorway) return;
+ const rec = Object.assign({}, state, { id:"cfg_"+Date.now().toString(36)+Math.floor(Math.random()*1e4).toString(36), ts:Date.now() });
+ saved.unshift(rec);
+ saved = saved.slice(0,12);
+ persistSaved();
+ renderSaved();
+ flash(document.getElementById("saveBtn"), "Saved ✓");
+}
+
+function restoreConfig(id){
+ const rec = saved.find(s=>s.id===id);
+ if(!rec) return;
+ state = {
+ colorway:rec.colorway, ground:rec.ground, finish:rec.finish,
+ quantity:rec.quantity, urgency:rec.urgency, project:rec.project
+ };
+ render();
+ window.scrollTo({top: document.getElementById("controls").getBoundingClientRect().top + window.scrollY - 90, behavior:"smooth"});
+}
+
+function deleteConfig(id){
+ saved = saved.filter(s=>s.id!==id);
+ persistSaved();
+ renderSaved();
+}
+
+function flash(btn, txt){
+ const orig = btn.textContent;
+ btn.textContent = txt;
+ btn.disabled = true;
+ setTimeout(()=>{ btn.textContent=orig; btn.disabled=false; }, 1300);
+}
+
+/* ============ SUBMIT ============ */
+
+function submitRequest(){
+ const v = validate();
+ if(!v.ok){
+ document.getElementById("submitErr").textContent = v.errs[0];
+ return;
+ }
+ const c=cwById(state.colorway), g=grById(state.ground), f=fnById(state.finish), u=urById(state.urgency);
+ const per = unitPrice(state.colorway, state.ground, state.finish, "standard");
+ const total = per*state.quantity + u.surcharge;
+ const [lo,hi]=leadTimeRange(state.finish, state.urgency);
+ const ref = "STR7-" + Date.now().toString(36).toUpperCase().slice(-6);
+
+ document.getElementById("confRef").textContent = "Reference " + ref;
+ document.getElementById("confList").innerHTML = `
+ <div><dt>Colorway</dt><dd>${c.name}</dd></div>
+ <div><dt>Ground / Finish</dt><dd>${g.name} · ${f.name}</dd></div>
+ <div><dt>Quantity</dt><dd>${state.quantity} memo${state.quantity>1?'s':''}</dd></div>
+ <div><dt>Project</dt><dd>${escapeHtml(state.project.trim())}</dd></div>
+ <div><dt>Delivery</dt><dd>${u.name} · ${lo}–${hi} weeks</dd></div>
+ <div><dt>Total</dt><dd>${money(total)}</dd></div>`;
+ document.getElementById("confFine").textContent =
+ `We’ll hand-finish your ${c.name} memo${state.quantity>1?'s':''} and dispatch to the address on file for “${state.project.trim()}.” A specifier will confirm lead time within one business day.`;
+ document.getElementById("overlay").classList.add("show");
+}
+
+function escapeHtml(s){ return s.replace(/[&<>"']/g, m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m])); }
+
+/* ============ EVENTS ============ */
+
+// colorway grid
+document.getElementById("cwGrid").addEventListener("click", e=>{
+ const btn = e.target.closest("button");
+ if(btn){
+ const id = btn.getAttribute("data-cw");
+ const act = btn.getAttribute("data-act");
+ if(act==="compare"){ toggleCompare(id); return; }
+ if(act==="select"){ selectColorway(id); return; }
+ }
+ const card = e.target.closest(".cw");
+ if(card){ selectColorway(card.getAttribute("data-cw")); }
+});
+
+function selectColorway(id){
+ state.colorway = id;
+ // guard: if current finish is OOS for this colorway, nudge to matte
+ if(OOS.has(id+"|"+state.finish)){
+ const alt = FINISHES.find(f=> !OOS.has(id+"|"+f.id) && !incompatibility(state.ground,f.id));
+ if(alt) state.finish = alt.id;
+ }
+ render();
+}
+
+// compare tray
+document.getElementById("compareCols").addEventListener("click", e=>{
+ const rm = e.target.closest("[data-rm]");
+ if(rm){ toggleCompare(rm.getAttribute("data-rm")); return; }
+ const cfg = e.target.closest("[data-config]");
+ if(cfg){ selectColorway(cfg.getAttribute("data-config"));
+ window.scrollTo({top: document.getElementById("controls").getBoundingClientRect().top + window.scrollY - 90, behavior:"smooth"}); }
+});
+
+// ground
+document.getElementById("groundOpts").addEventListener("click", e=>{
+ const opt = e.target.closest(".opt"); if(!opt) return;
+ if(opt.classList.contains("disabled")) return;
+ state.ground = opt.getAttribute("data-ground");
+ render();
+});
+
+// finish
+document.getElementById("finishOpts").addEventListener("click", e=>{
+ const opt = e.target.closest(".opt"); if(!opt) return;
+ const fid = opt.getAttribute("data-finish");
+ state.finish = fid;
+ // if switching to lacquer while on linen, auto-switch ground to clay for a clean path (still show nothing broken)
+ if(incompatibility(state.ground, fid)){
+ // leave ground as-is so the incompatibility alert teaches the user; but if it's the only issue, keep it visible.
+ }
+ // if new finish has a higher minimum, bump quantity up
+ const minO = fnById(fid).minOrder;
+ if(state.quantity < minO) state.quantity = minO;
+ // if rush is blocked on this finish, revert urgency
+ if(urgencyBlocked(fid, state.urgency)) state.urgency = "priority";
+ render();
+});
+
+// urgency
+document.getElementById("urgencyOpts").addEventListener("click", e=>{
+ const opt = e.target.closest(".opt"); if(!opt) return;
+ if(opt.classList.contains("disabled")) return;
+ state.urgency = opt.getAttribute("data-urgency");
+ render();
+});
+
+// quantity
+document.getElementById("qtyPlus").addEventListener("click", ()=>{ state.quantity = Math.min(99, state.quantity+1); render(); });
+document.getElementById("qtyMinus").addEventListener("click", ()=>{
+ const minO = state.colorway? fnById(state.finish).minOrder : 1;
+ state.quantity = Math.max(minO, state.quantity-1); render();
+});
+document.getElementById("qtyInput").addEventListener("input", e=>{
+ let v = parseInt(e.target.value.replace(/\D/g,""),10);
+ if(isNaN(v)) v = 1;
+ v = Math.max(1, Math.min(99, v));
+ state.quantity = v;
+ renderSummary();
+});
+document.getElementById("qtyInput").addEventListener("blur", ()=>{
+ const minO = state.colorway? fnById(state.finish).minOrder : 1;
+ if(state.quantity < minO) state.quantity = minO;
+ render();
+});
+
+// project
+document.getElementById("projInput").addEventListener("input", e=>{
+ state.project = e.target.value;
+ const err = document.getElementById("projErr");
+ err.textContent = "";
+ document.getElementById("projInput").classList.remove("err");
+ renderSummary();
+});
+
+// save / submit
+document.getElementById("saveBtn").addEventListener("click", saveConfig);
+document.getElementById("submitBtn").addEventListener("click", submitRequest);
+
+document.getElementById("savedGrid").addEventListener("click", e=>{
+ const r = e.target.closest("[data-restore]");
+ if(r){ restoreConfig(r.getAttribute("data-restore")); return; }
+ const d = e.target.closest("[data-del]");
+ if(d){ deleteConfig(d.getAttribute("data-del")); }
+});
+
+// confirmation close → fresh memo
+document.getElementById("confClose").addEventListener("click", ()=>{
+ document.getElementById("overlay").classList.remove("show");
+ state = defaultState();
+ render();
+ window.scrollTo({top:0, behavior:"smooth"});
+});
+document.getElementById("overlay").addEventListener("click", e=>{
+ if(e.target.id==="overlay") document.getElementById("overlay").classList.remove("show");
+});
+
+// session banner dismiss
+document.getElementById("dismissSession").addEventListener("click", ()=>{
+ document.getElementById("sessionBanner").style.display="none";
+});
+
+/* ============ INIT ============ */
+
+function init(){
+ loadSaved();
+ const prior = loadSession();
+ if(prior && (prior.colorway || (prior.project && prior.project.trim()))){
+ state = Object.assign(defaultState(), prior);
+ // sanity clamp
+ if(state.quantity<1) state.quantity=1;
+ const banner = document.getElementById("sessionBanner");
+ banner.style.display="block";
+ const label = state.colorway ? `${cwById(state.colorway).name} · ${grById(state.ground).name} · ${fnById(state.finish).name}` : "your draft";
+ document.getElementById("sessionBannerText").textContent = `Restored your last session — ${label}${state.project?(' for “'+state.project+'”'):''}.`;
+ }
+ render();
+}
+init();
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/84f06b214f8c/qwen25-7b.html b/data/artifacts/84f06b214f8c/qwen25-7b.html
new file mode 100644
index 0000000..015ef19
--- /dev/null
+++ b/data/artifacts/84f06b214f8c/qwen25-7b.html
@@ -0,0 +1,223 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Strata No. 7 Sample Order Configurator</title>
+ <style>
+ body {
+ font-family: Arial, sans-serif;
+ background-color: #f5f5f5;
+ color: #333;
+ margin: 20px;
+ }
+ .container {
+ max-width: 1200px;
+ margin: auto;
+ padding: 20px;
+ background: white;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+ }
+ h1 {
+ text-align: center;
+ }
+ .material-preview {
+ width: 350px;
+ height: 400px;
+ margin: 20px auto;
+ background-size: cover;
+ display: none;
+ }
+ .colorway, .ground, .finish, .leadTime {
+ margin-bottom: 10px;
+ }
+ input[type="number"] {
+ width: 50px;
+ }
+ #projectName, #deliveryUrgency {
+ display: block;
+ margin-bottom: 20px;
+ }
+ .comparison-tray {
+ border: 1px solid #ddd;
+ padding: 10px;
+ margin-top: 20px;
+ }
+ button {
+ background-color: #4CAF50;
+ color: white;
+ padding: 10px 20px;
+ text-align: center;
+ border: none;
+ cursor: pointer;
+ display: block;
+ margin: auto;
+ }
+ .comparison-tray .options {
+ margin-bottom: 10px;
+ }
+ @keyframes fadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+ }
+ </style>
+</head>
+<body>
+<div class="container">
+ <h1>Strata No. 7 Sample Order Configurator</h1>
+
+ <div id="materialPreview" class="material-preview" style="background-image: url('https://via.placeholder.com/350x400.png?text=Strata+No.+7+Colorway+1');"></div>
+
+ <form id="configForm">
+ <label for="projectName">Project Name:</label>
+ <input type="text" id="projectName" name="projectName"><br><br>
+
+ <h2>Choose Colorways</h2>
+ <div class="colorway">
+ <select id="colorwaySelect" onchange="updatePreview()">
+ <option value="">Select a colorway</option>
+ <option value="1">Colorway 1</option>
+ <option value="2">Colorway 2</option>
+ <option value="3">Colorway 3</option>
+ <option value="4">Colorway 4</option>
+ </select>
+ </div>
+
+ <h2>Choose Ground</h2>
+ <div class="ground">
+ <label for="groundSelect">Ground:</label>
+ <select id="groundSelect" onchange="updatePreview()">
+ <option value="">Select a ground</option>
+ <option value="1">Ground 1</option>
+ <option value="2">Ground 2</option>
+ </select>
+ </div>
+
+ <h2>Choose Finish Level</h2>
+ <div class="finish">
+ <label for="finishSelect">Finish:</label>
+ <select id="finishSelect" onchange="updatePreview()">
+ <option value="">Select a finish level</option>
+ <option value="1">Finish 1 - Smooth</option>
+ <option value="2">Finish 2 - Textured</option>
+ <option value="3">Finish 3 - Hand-Brushed</option>
+ </select>
+ </div>
+
+ <h2>Choose Lead-Time Range</h2>
+ <div class="leadTime">
+ <label for="leadTimeSelect">Lead Time:</label>
+ <select id="leadTimeSelect" onchange="updatePreview()">
+ <option value="">Select a lead time range</option>
+ <option value="1">2-3 Weeks</option>
+ <option value="2">4-6 Weeks</option>
+ <option value="3">8+ Weeks</option>
+ </select>
+ </div>
+
+ <h2>Quantity Control</h2>
+ <label for="quantitySelect">Number of Samples:</label>
+ <input type="number" id="quantitySelect" name="quantity"><br><br>
+
+ <h2>Delivery Urgency</h2>
+ <select id="deliveryUrgency">
+ <option value="">Select urgency</option>
+ <option value="1">Standard (2-3 Weeks)</option>
+ <option value="2">Rush (4-6 Weeks)</option>
+ <option value="3">Express (8+ Weeks)</option>
+ </select><br><br>
+
+ <div id="memoPricing">
+ Memo Pricing: $<span id="pricing">0</span><br>
+ Minimum Order: 5 Samples
+ </div>
+
+ <div class="comparison-tray" style="display:none;">
+ <h2>Compare Options</h2>
+ <ul></ul>
+ </div>
+
+ <button onclick="submitOrder()">Submit Sample Request</button>
+ </form>
+
+ <script>
+ const localStorageKey = 'strataNo7Config';
+
+ function updatePreview() {
+ const colorway = document.getElementById('colorwaySelect').value;
+ const ground = document.getElementById('groundSelect').value;
+ const finish = document.getElementById('finishSelect').value;
+
+ if (!colorway || !ground || !finish) return;
+
+ let previewImageSrc = `https://via.placeholder.com/350x400.png?text=Strata+No.+7+Colorway+${colorway}+Ground+${ground}+Finish+${finish}`;
+
+ document.getElementById('materialPreview').style.backgroundImage = `url('${previewImageSrc}')`;
+ }
+
+ function submitOrder() {
+ const projectName = document.getElementById('projectName').value;
+ const colorway = document.getElementById('colorwaySelect').value;
+ const ground = document.getElementById('groundSelect').value;
+ const finish = document.getElementById('finishSelect').value;
+ const leadTime = document.getElementById('leadTimeSelect').value;
+ const quantity = document.getElementById('quantitySelect').value;
+ const deliveryUrgency = document.getElementById('deliveryUrgency').value;
+
+ if (!projectName || !colorway || !ground || !finish || !quantity) {
+ alert("Please fill in all required fields.");
+ return;
+ }
+
+ let memoPricing = 0;
+ switch (leadTime) {
+ case "1":
+ memoPricing += 50;
+ break;
+ case "2":
+ memoPricing += 75;
+ break;
+ case "3":
+ memoPricing += 100;
+ break;
+ }
+
+ if (quantity < 5) {
+ alert("Minimum order is 5 samples.");
+ return;
+ }
+
+ memoPricing *= quantity;
+
+ document.getElementById('pricing').innerText = memoPricing.toFixed(2);
+ }
+
+ function initConfigurator() {
+ const config = JSON.parse(localStorage.getItem(localStorageKey)) || { colorway: '', ground: '', finish: '' };
+
+ document.getElementById('colorwaySelect').value = config.colorway;
+ document.getElementById('groundSelect').value = config.ground;
+ document.getElementById('finishSelect').value = config.finish;
+
+ updatePreview();
+ }
+
+ function compareOptions() {
+ const comparisonList = document.querySelector('.comparison-tray ul');
+ comparisonList.innerHTML = '';
+
+ for (let i = 1; i <= 3; i++) {
+ const clone = document.getElementById(`configForm`).cloneNode(true);
+ document.querySelectorAll('select, input').forEach(el => el.remove());
+ clone.id += `_${i}`;
+ comparisonList.appendChild(clone);
+ }
+
+ document.querySelector('.comparison-tray').style.display = 'block';
+ }
+
+ initConfigurator();
+ </script>
+</div>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/84f06b214f8c/qwen3-14b.html b/data/artifacts/84f06b214f8c/qwen3-14b.html
new file mode 100644
index 0000000..08475c9
--- /dev/null
+++ b/data/artifacts/84f06b214f8c/qwen3-14b.html
@@ -0,0 +1,451 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Strata No. 7 Sample Configurator</title>
+ <style>
+ body {
+ margin: 0;
+ font-family: 'Georgia', serif;
+ background: #f9f9f9;
+ color: #333;
+ line-height: 1.6;
+ }
+ header {
+ background: #e8e8e8;
+ padding: 1rem 2rem;
+ text-align: center;
+ font-weight: bold;
+ font-size: 1.5rem;
+ }
+ .container {
+ max-width: 960px;
+ margin: 2rem auto;
+ padding: 0 1rem;
+ }
+ .configurator {
+ display: grid;
+ grid-template-columns: 2fr 1fr;
+ gap: 2rem;
+ }
+ .configurator > * {
+ background: #fff;
+ padding: 1rem;
+ border-radius: 8px;
+ box-shadow: 0 2px 6px rgba(0,0,0,0.05);
+ }
+ .colorways {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
+ }
+ .colorway {
+ flex: 1 1 150px;
+ padding: 0.5rem;
+ border: 2px solid transparent;
+ cursor: pointer;
+ border-radius: 6px;
+ transition: all 0.3s;
+ }
+ .colorway.active {
+ border-color: #444;
+ }
+ .colorway::before {
+ content: '';
+ display: block;
+ width: 100%;
+ height: 150px;
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: center;
+ border-radius: 6px;
+ }
+ .colorway:nth-child(1)::before {
+ background-image: linear-gradient(to bottom, #d9e5f0, #a7d6e5);
+ }
+ .colorway:nth-child(2)::before {
+ background-image: linear-gradient(to bottom, #f2d6c2, #e8c4a5);
+ }
+ .colorway:nth-child(3)::before {
+ background-image: linear-gradient(to bottom, #e0e8d8, #bdcdaa);
+ }
+ .colorway:nth-child(4)::before {
+ background-image: linear-gradient(to bottom, #f6e3d7, #e9c3c0);
+ }
+ .controls {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ }
+ .controls label {
+ font-weight: bold;
+ margin-bottom: 0.25rem;
+ }
+ .controls select {
+ padding: 0.5rem;
+ border: 1px solid #ccc;
+ border-radius: 6px;
+ font-size: 1rem;
+ width: 100%;
+ }
+ .controls input[type="number"] {
+ padding: 0.5rem;
+ border: 1px solid #ccc;
+ border-radius: 6px;
+ font-size: 1rem;
+ width: 100%;
+ }
+ .preview {
+ position: relative;
+ height: 300px;
+ background: #f2f2f2;
+ border: 1px solid #ccc;
+ overflow: hidden;
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #888;
+ font-style: italic;
+ font-size: 1.2rem;
+ }
+ .preview::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(to bottom, #d9e5f0, #a7d6e5);
+ z-index: 1;
+ opacity: 0.3;
+ pointer-events: none;
+ }
+ .info {
+ margin-top: 2rem;
+ }
+ .info .section {
+ margin-bottom: 1.5rem;
+ }
+ .info h3 {
+ margin-top: 0;
+ }
+ .info .availability,
+ .info .care-note,
+ .info .price {
+ font-style: italic;
+ color: #555;
+ }
+ .comparison {
+ margin-top: 2rem;
+ max-height: 300px;
+ overflow-y: auto;
+ border: 1px solid #ccc;
+ padding: 1rem;
+ border-radius: 6px;
+ background: #fff;
+ }
+ .comparison-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.5rem 0;
+ border-bottom: 1px solid #eee;
+ }
+ .comparison-item:last-child {
+ border-bottom: none;
+ }
+ .comparison-item .name {
+ font-weight: bold;
+ }
+ .buttons {
+ display: flex;
+ gap: 1rem;
+ margin-top: 2rem;
+ }
+ .buttons button {
+ padding: 0.75rem 1.5rem;
+ font-size: 1rem;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ background: #444;
+ color: #fff;
+ }
+ .buttons button:hover {
+ background: #555;
+ }
+ .saved {
+ margin-top: 2rem;
+ font-size: 0.9rem;
+ color: #666;
+ }
+ .error {
+ color: red;
+ font-size: 0.9rem;
+ margin-top: 0.5rem;
+ }
+ @media (max-width: 768px) {
+ .configurator {
+ grid-template-columns: 1fr;
+ }
+ }
+ </style>
+</head>
+<body>
+ <header>Strata No. 7 Sample Configurator</header>
+ <div class="container">
+ <div class="configurator">
+ <div>
+ <h2>Colorways</h2>
+ <div class="colorways">
+ <div class="colorway active" data-color="1"></div>
+ <div class="colorway" data-color="2"></div>
+ <div class="colorway" data-color="3"></div>
+ <div class="colorway" data-color="4"></div>
+ </div>
+ <div class="controls">
+ <label for="ground">Ground</label>
+ <select id="ground">
+ <option value="cotton">Cotton</option>
+ <option value="linen">Linen</option>
+ </select>
+ <label for="finish">Finish Level</label>
+ <select id="finish">
+ <option value="light">Light</option>
+ <option value="medium">Medium</option>
+ <option value="heavy">Heavy</option>
+ </select>
+ <label for="quantity">Quantity (per colorway)</label>
+ <input type="number" id="quantity" min="1" value="1" />
+ <label for="projectName">Project Name</label>
+ <input type="text" id="projectName" />
+ <label for="urgency">Delivery Urgency</label>
+ <select id="urgency">
+ <option value="standard">Standard (2-4 weeks)</option>
+ <option value="expedited">Expedited (1-2 weeks)</option>
+ </select>
+ </div>
+ </div>
+ <div>
+ <div class="preview" id="preview">Select a colorway to see the material preview</div>
+ <div class="info">
+ <div class="section">
+ <h3>Availability</h3>
+ <div id="availability">Available</div>
+ </div>
+ <div class="section">
+ <h3>Price</h3>
+ <div id="price">$0.00</div>
+ </div>
+ <div class="section">
+ <h3>Care Note</h3>
+ <div id="careNote">Handle with care. Dry clean only.</div>
+ </div>
+ <div class="section">
+ <h3>Minimum Order</h3>
+ <div id="minOrder">Minimum order of 3 units per colorway</div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="comparison" id="comparison">
+ <h3>Comparison Tray (up to 3 variants)</h3>
+ </div>
+ <div class="buttons">
+ <button onclick="saveConfig()">Save Configuration</button>
+ <button onclick="submitOrder()">Submit Sample Request</button>
+ </div>
+ <div class="saved" id="savedConfigs"></div>
+ <div class="error" id="error"></div>
+ </div>
+
+ <script>
+ const colorways = document.querySelectorAll('.colorway');
+ const preview = document.getElementById('preview');
+ const availability = document.getElementById('availability');
+ const price = document.getElementById('price');
+ const careNote = document.getElementById('careNote');
+ const minOrder = document.getElementById('minOrder');
+ const ground = document.getElementById('ground');
+ const finish = document.getElementById('finish');
+ const quantity = document.getElementById('quantity');
+ const projectName = document.getElementById('projectName');
+ const urgency = document.getElementById('urgency');
+ const comparison = document.getElementById('comparison');
+ const savedConfigs = document.getElementById('savedConfigs');
+ const error = document.getElementById('error');
+
+ let saved = JSON.parse(localStorage.getItem('strataConfigs')) || [];
+ let currentConfig = null;
+
+ function updatePreview(color, ground, finish) {
+ if (!color) {
+ preview.textContent = 'Select a colorway to see the material preview';
+ return;
+ }
+
+ let material = 'Available';
+ let priceValue = 0;
+ let careNoteValue = 'Handle with care. Dry clean only.';
+ let minOrderValue = 'Minimum order of 3 units per colorway';
+
+ if (ground === 'cotton' && finish === 'heavy') {
+ material = 'Available (limited stock)';
+ priceValue = 35.00;
+ careNoteValue = 'Handle with care. Dry clean only. Avoid direct sunlight.';
+ } else if (ground === 'linen' && finish === 'heavy') {
+ material = 'Backordered (expected in 3-4 weeks)';
+ priceValue = 38.00;
+ careNoteValue = 'Handle with care. Dry clean only. Avoid direct sunlight.';
+ } else if (ground === 'linen' && finish === 'medium') {
+ material = 'Available';
+ priceValue = 32.00;
+ care细NoteValue = 'Handle with care. Dry clean only.';
+ }
+
+ const basePrice = 25.00;
+ const finishMultiplier = { light: 1, medium: 1.2, heavy: 1.5 };
+ const groundMultiplier = { cotton: 1, linen: 1.1 };
+
+ priceValue = basePrice * finishMultiplier[finish] * groundMultiplier[ground];
+
+ availability.textContent = material;
+ price.textContent = `$${(priceValue * quantity.value).toFixed(2)}`;
+ careNote.textContent = careNoteValue;
+ minOrder.textContent = minOrderValue;
+
+ const gradient = getGradient(color);
+ preview.style.backgroundImage = `linear-gradient(to bottom, ${gradient.start}, ${gradient.end})`;
+ }
+
+ function getGradient(color) {
+ switch (color) {
+ case '1': return { start: '#d9e5f0', end: '#a7d6e5' };
+ case '2': return { start: '#f2d6c2', end: '#e8c4a5' };
+ case '3': return { start: '#e0e8d8', end: '#bdcdaa' };
+ case '4': return { start: '#f6e3d7', end: '#e9c3c0' };
+ default: return { start: '#f9f9f9', end: '#ccc' };
+ }
+ }
+
+ function updateComparison() {
+ const config = {
+ color: currentConfig.color,
+ ground: ground.value,
+ finish: finish.value,
+ quantity: quantity.value,
+ projectName: projectName.value,
+ urgency: urgency.value,
+ price: price.textContent,
+ availability: availability.textContent,
+ care: careNote.textContent
+ };
+
+ if (saved.length >= 3) {
+ saved.shift();
+ }
+ saved.push(config);
+ localStorage.setItem('strataConfigs', JSON.stringify(saved));
+ renderComparison();
+ }
+
+ function renderComparison() {
+ comparison.innerHTML = '<h3>Comparison Tray (up to 3 variants)</h3>';
+ saved.forEach((item, index) => {
+ const div = document.createElement('div');
+ div.className = 'comparison-item';
+ div.innerHTML = `
+ <div class="name">Config ${index + 1}</div>
+ <div>
+ <span>Ground:</span> ${item.ground}<br>
+ <span>Finish:</span> ${item.finish}<br>
+ <span>Price:</span> ${item.price}<br>
+ <span>Availability:</span> ${item.availability}
+ </div>
+ `;
+ comparison.appendChild(div);
+ });
+ }
+
+ function saveConfig() {
+ if (!currentConfig) {
+ error.textContent = 'Please select a colorway and configure your options before saving.';
+ return;
+ }
+ updateComparison();
+ error.textContent = '';
+ }
+
+ function submitOrder() {
+ if (!currentConfig) {
+ error.textContent = 'Please select a colorway and configure your options before submitting.';
+ return;
+ }
+
+ const config = {
+ color: currentConfig.color,
+ ground: ground.value,
+ finish: finish.value,
+ quantity: quantity.value,
+ projectName: projectName.value,
+ urgency: urgency.value,
+ price: price.textContent,
+ availability: availability.textContent,
+ care: careNote.textContent
+ };
+
+ if (parseInt(quantity.value) < 3) {
+ error.textContent = 'Minimum order of 3 units per colorway is required.';
+ return;
+ }
+
+ if (ground.value === 'linen' && finish.value === 'heavy' && availability.textContent.includes('Backordered')) {
+ error.textContent = 'This combination is currently backordered. Please select a different ground or finish.';
+ return;
+ }
+
+ alert('Your sample request has been submitted!\n\n' +
+ `Project: ${config.projectName}\n` +
+ `Colorway: ${config.color}\n` +
+ `Ground: ${config.ground}\n` +
+ `Finish: ${config.finish}\n` +
+ `Quantity: ${config.quantity}\n` +
+ `Price: ${config.price}\n` +
+ `Availability: ${config.availability}\n` +
+ `Care: ${config.care}\n` +
+ `Urgency: ${config.urgency}`);
+ error.textContent = '';
+ }
+
+ colorways.forEach((el) => {
+ el.addEventListener('click', (e) => {
+ colorways.forEach(c => c.classList.remove('active'));
+ e.currentTarget.classList.add('active');
+ currentConfig = { color: e.currentTarget.dataset.color };
+ updatePreview(e.currentTarget.dataset.color, ground.value, finish.value);
+ updateComparison();
+ });
+ });
+
+ ground.addEventListener('change', () => {
+ updatePreview(currentConfig.color, ground.value, finish.value);
+ });
+
+ finish.addEventListener('change', () => {
+ updatePreview(currentConfig.color, ground.value, finish.value);
+ });
+
+ quantity.addEventListener('input', () => {
+ updatePreview(currentConfig.color, ground.value, finish.value);
+ });
+
+ window.addEventListener('load', () => {
+ if (saved.length > 0) {
+ renderComparison();
+ }
+ });
+ </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/89e23514bd71/qwen25-7b.html b/data/artifacts/89e23514bd71/qwen25-7b.html
new file mode 100644
index 0000000..b3bc3b4
--- /dev/null
+++ b/data/artifacts/89e23514bd71/qwen25-7b.html
@@ -0,0 +1,159 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Orbital Freight Coordination</title>
+ <style>
+ body { font-family: Arial, sans-serif; background-color: #f5f5f5; }
+ .container { width: 390px; margin: auto; padding: 20px; text-align: center; background: white; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
+ h1 { color: #4CAF50; font-size: 2em; margin-bottom: 20px; }
+ .star-map { width: 360px; height: 360px; border-radius: 8px; background-color: black; overflow: hidden; position: relative; }
+ .settlements { display: flex; justify-content: space-between; margin-bottom: 15px; }
+ .settlement { width: 90px; height: 90px; border-radius: 8px; background-color: #D3D3D3; margin: 5px; position: relative; overflow: hidden; }
+ .cargo-planner { display: flex; justify-content: space-between; align-items: center; padding: 10px; background: white; border-radius: 8px; box-shadow: inset 0 -2px rgba(0,0,0,0.1); margin-bottom: 15px; }
+ .cargo-box { width: 40%; height: 60px; display: flex; align-items: center; justify-content: space-between; padding: 5px; background-color: #e7e7e7; border-radius: 8px; }
+ .event-log { margin-bottom: 20px; background: white; padding: 10px; box-shadow: inset 0 -2px rgba(0,0,0,0.1); border-radius: 8px; }
+ .turn-timeline { width: 360px; height: 50px; margin-top: 15px; background-color: #dcdcdc; position: relative; overflow: hidden; }
+ .turn-indicator { width: 20px; height: 40px; position: absolute; left: -20px; top: 10px; border-radius: 8px; transition: all 0.3s ease-in-out; background-color: #4CAF50; }
+ .ship { width: 10px; height: 10px; position: absolute; bottom: -5px; right: -2px; border-radius: 50%; background-color: #4CAF50; transition: all 0.3s ease-in-out; }
+ </style>
+</head>
+<body>
+ <div class="container">
+ <h1>Orbital Freight Coordination</h1>
+ <div id="star-map" class="star-map"></div>
+ <div id="cargo-planner" class="cargo-planner">
+ <div class="cargo-box">Water: 0/20</div>
+ <div class="cargo-box">Medicine: 0/30</div>
+ <div class="cargo-box">Reactor Parts: 0/15</div>
+ <div class="cargo-box">Research Samples: 0/10</div>
+ </div>
+ <div id="settlements" class="settlements"></div>
+ <div id="event-log" class="event-log"></div>
+ <button onclick="togglePause()">Pause</button>
+ </div>
+
+ <script>
+ let turnsLeft = 12;
+ const settlements = [
+ { name: "Aurora", demand: [5, 3], storage: 20, trust: 70, production: [4, 2] },
+ { name: "Mars Base", demand: [6, 4], storage: 15, trust: 85, production: [3, 3] },
+ { name: "Luna Station", demand: [4, 5], storage: 25, trust: 90, production: [2, 4] },
+ { name: "Titan Lab", demand: [3, 6], storage: 10, trust: 60, production: [1, 5] },
+ { name: "Jovian Hub", demand: [2, 7], storage: 30, trust: 40, production: [5, 1] }
+ ];
+
+ const events = [
+ { turn: 4, event: "Aurora's fuel line is damaged" },
+ { turn: 8, event: "Mars Base needs emergency medicine due to a biotech outbreak" },
+ { turn: 10, event: "Luna Station reports critical reactor parts shortage" }
+ ];
+
+ const ships = [
+ { type: "Small", capacity: [3, 2], fuelCost: 1 },
+ { type: "Medium", capacity: [6, 4], fuelCost: 2 },
+ { type: "Large", capacity: [9, 5], fuelCost: 3 }
+ ];
+
+ function initializeGame() {
+ document.getElementById("star-map").style.width = `${settlements.length * 70}px`;
+ settlements.forEach(sett => {
+ const div = document.createElement("div");
+ div.classList.add("settlement");
+ div.style.height = `${sett.storage + 15}px`;
+ div.innerText = sett.name;
+ document.getElementById("settlements").appendChild(div);
+ });
+ }
+
+ function updateTurn(turn) {
+ turnsLeft--;
+ if (turn < events.length && turn === events[0].turn) {
+ document.getElementById("event-log").innerText = `Event: ${events[0].event}`;
+ }
+ const turnIndicator = document.querySelector(".turn-indicator");
+ turnIndicator.style.left = `${(turn / 12) * 360}px`;
+ }
+
+ function moveShip(from, to, cargoType, amount) {
+ const fromSett = settlements[from];
+ const toSett = settlements[to];
+ if (fromSett.storage - amount >= 0 && toSett.storage + amount <= toSett.capacity) {
+ fromSett.storage -= amount;
+ toSett.storage += amount;
+
+ // Animate ship movement
+ const shipDiv = document.createElement("div");
+ shipDiv.classList.add("ship");
+ shipDiv.style.left = `${(from * 70 + 35) - (amount / 2)}px`;
+ shipDiv.style.top = `-${360 - 10}px`;
+ document.getElementById("star-map").appendChild(shipDiv);
+ setTimeout(() => {
+ shipDiv.style.top = `${-5 + Math.random() * 40}px`;
+ shipDiv.style.left = `${(to * 70) + 35}px`;
+ setTimeout(() => {
+ shipDiv.remove();
+ }, 1000);
+ }, 200);
+ }
+ }
+
+ function displayCargo(cargoType, amount) {
+ const cargoBoxes = document.querySelectorAll(".cargo-box");
+ cargoBoxes.forEach((box, index) => {
+ if (index === ["Water", "Medicine", "Reactor Parts", "Research Samples"].indexOf(cargoType)) {
+ box.innerText = `${amount}/${settlements[index].production[cargoType]}`;
+ }
+ });
+ }
+
+ function checkForWin() {
+ const allSettled = settlements.every(sett => sett.storage >= (sett.demand[0] * 2 + sett.demand[1]));
+ if (allSettled && turnsLeft <= 0) {
+ alert("Congratulations! You coordinated the freight successfully.");
+ } else if (!allSettled && turnsLeft <= 0) {
+ alert("The storm is closing. Not all needs were met. Try again!");
+ }
+ }
+
+ function togglePause() {
+ const pauseButton = document.querySelector("button");
+ pauseButton.innerText = pauseButton.innerText === "Pause" ? "Resume" : "Pause";
+ if (pauseButton.innerText === "Pause") {
+ setInterval(() => updateTurn(turnsLeft), 1000);
+ } else {
+ clearInterval();
+ }
+ }
+
+ function main() {
+ initializeGame();
+ setTimeout(() => { // Start the game after initialization
+ for (let turn = 1; turn <= turnsLeft; turn++) {
+ document.getElementById("event-log").innerText = "";
+ updateTurn(turn);
+ if (turn === events[0].turn) {
+ alert(events[0].event);
+ events.shift();
+ }
+ const from = Math.floor(Math.random() * settlements.length);
+ const to = Math.floor(Math.random() * settlements.length);
+ while (from === to) { // Ensure different settlement for now
+ to = Math.floor(Math.random() * settlements.length);
+ }
+ if (settlements[from].storage > 0 && settlements[to].storage < settlements[to].capacity) {
+ const cargoType = Math.floor(Math.random() * 4); // Randomly choose a cargo type
+ const amount = Math.min(settlements[from].storage, settlements[to].capacity - settlements[to].storage);
+ moveShip(from, to, cargoType, amount);
+ displayCargo(cargoType, amount);
+ }
+ checkForWin();
+ }
+ }, 1000);
+ }
+
+ main();
+ </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/89e23514bd71/qwen3-14b.html b/data/artifacts/89e23514bd71/qwen3-14b.html
new file mode 100644
index 0000000..c948d32
--- /dev/null
+++ b/data/artifacts/89e23514bd71/qwen3-14b.html
@@ -0,0 +1,510 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Orbital Freight Challenge</title>
+ <style>
+ body {
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ background: #1a1a1a;
+ color: #fff;
+ margin: 0;
+ padding: 0;
+ overflow: hidden;
+ user-select: none;
+ }
+ #app {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ }
+ header {
+ background: #2a2a2a;
+ padding: 10px;
+ text-align: center;
+ font-size: 1.2em;
+ }
+ #controls {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 5px;
+ padding: 5px;
+ }
+ button {
+ background: #3a3a3a;
+ border: none;
+ color: #fff;
+ padding: 6px 12px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 0.9em;
+ }
+ button:hover {
+ background: #4a4a4a;
+ }
+ #main {
+ display: flex;
+ flex: 1;
+ overflow: hidden;
+ }
+ #sidebar {
+ width: 250px;
+ background: #2a2a2a;
+ display: flex;
+ flex-direction: column;
+ padding: 10px;
+ overflow-y: auto;
+ border-right: 1px solid #4a4a4a;
+ }
+ .section {
+ margin-bottom: 15px;
+ }
+ .section h2 {
+ margin-top: 0;
+ font-size: 1.1em;
+ margin-bottom: 5px;
+ }
+ .section .title {
+ font-weight: bold;
+ }
+ .section .value {
+ margin-left: 10px;
+ }
+ #map {
+ flex: 1;
+ position: relative;
+ }
+ .settlement {
+ position: absolute;
+ width: 100px;
+ height: 100px;
+ background: #3a3a3a;
+ border: 2px solid #5a5a5a;
+ border-radius: 10px;
+ padding: 5px;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: center;
+ text-align: center;
+ box-shadow: 0 0 4px #000;
+ }
+ .settlement h3 {
+ margin: 0;
+ font-size: 0.9em;
+ }
+ .settlement .indicator {
+ font-size: 0.8em;
+ }
+ .ship {
+ position: absolute;
+ width: 20px;
+ height: 20px;
+ background: #00ffcc;
+ border-radius: 50%;
+ pointer-events: none;
+ animation: move 3s linear;
+ }
+ @keyframes move {
+ from { transform: translate(0, 0); }
+ to { transform: translate(var(--dx), var(--dy)); }
+ }
+ #timeline {
+ height: 100px;
+ overflow-x: auto;
+ padding: 5px;
+ background: #2a2a2a;
+ border-top: 1px solid #4a4a4a;
+ }
+ #timeline .turn {
+ display: inline-block;
+ margin: 0 5px;
+ padding: 5px 10px;
+ background: #3a3a3a;
+ border-radius: 4px;
+ font-size: 0.9em;
+ }
+ #eventLog {
+ max-height: 200px;
+ overflow-y: auto;
+ padding: 5px;
+ background: #2a2a2a;
+ border-top: 1px solid #4a4a4a;
+ }
+ #eventLog .event {
+ margin-bottom: 5px;
+ }
+ #tutorial {
+ position: absolute;
+ top: 20px;
+ left: 20px;
+ background: rgba(0, 0, 0, 0.7);
+ padding: 10px;
+ border: 1px solid #4a4a4a;
+ border-radius: 6px;
+ color: #fff;
+ font-size: 0.9em;
+ display: none;
+ z-index: 10;
+ }
+ #summary {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background: #2a2a2a;
+ padding: 20px;
+ border: 2px solid #4a4a4a;
+ border-radius: 8px;
+ display: none;
+ text-align: center;
+ max-width: 90%;
+ }
+ #summary h2 {
+ margin-top: 0;
+ }
+ #summary .text {
+ margin: 10px 0;
+ }
+ </style>
+</head>
+<body>
+ <div id="app">
+ <header>
+ <strong>Orbital Freight Challenge</strong>
+ </header>
+ <div id="controls">
+ <button onclick="toggleTutorial()">Tutorial</button>
+ <button onclick="resetGame()">Reset</button>
+ <button onclick="togglePause()">Pause</button>
+ <button onclick="toggleSound()">Sound Off</button>
+ <button onclick="toggleMotion()">Reduced Motion</button>
+ <select id="difficulty" onchange="setDifficulty(this.value)">
+ <option value="easy">Easy</option>
+ <option value="normal" selected>Normal</option>
+ <option value="hard">Hard</option>
+ </select>
+ </div>
+ <div id="main">
+ <div id="sidebar">
+ <div class="section" id="shipPlanner">
+ <h2>Ship Planner</h2>
+ <div id="shipOptions"></div>
+ </div>
+ <div class="section" id="settlements">
+ <h2>Settlements</h2>
+ <div id="settlementDetails"></div>
+ </div>
+ <div class="section" id="events">
+ <h2>Events</h2>
+ <div id="eventLog"></div>
+ </div>
+ </div>
+ <div id="map"></div>
+ </div>
+ <div id="timeline"></div>
+ <div id="tutorial">Welcome! You're a logistic officer managing freight between five orbital settlements. Use the ships to move resources before the solar storm closes the transfer windows. Each settlement has unique needs and trust levels. <strong>Use the Ship Planner to route resources, and watch the map and event log for consequences.</strong> Press <strong>Esc</strong> to close this tutorial.</div>
+ <div id="summary">
+ <h2>Game Over</h2>
+ <div class="text" id="summaryText"></div>
+ <button onclick="resetGame()">Try Again</button>
+ </div>
+ </div>
+ <script>
+ const map = document.getElementById('map');
+ const timeline = document.getElementById('timeline');
+ const eventLog = document.getElementById('eventLog');
+ const shipPlanner = document.getElementById('shipOptions');
+ const settlementDetails = document.getElementById('settlementDetails');
+ const tutorial = document.getElementById('tutorial');
+ const summary = document.getElementById('summary');
+ const summaryText = document.getElementById('summaryText');
+ const difficultySelect = document.getElementById('difficulty');
+
+ let gameRunning = false;
+ let paused = false;
+ let currentTurn = 1;
+ let maxTurns = 12;
+ let difficulty = 'normal';
+ let shipOptions = [
+ { name: 'Light Freighter', capacity: 2, fuelCost: 1, speed: 1 },
+ { name: 'Medium Freighter', capacity: 4, fuelCost: 2, speed: 1 },
+ { name: 'Heavy Freighter', capacity: 6, fuelCost: 3, speed: 1 },
+ ];
+ let settlements = [];
+ let ships = [];
+ let events = [];
+ let selectedShip = null;
+ let selectedFrom = null;
+ let selectedTo = null;
+ let selectedCargo = null;
+ let cargoTypes = ['Water', 'Medicine', 'Reactor Parts', 'Research Samples'];
+ let solarStorm = {
+ turn: maxTurns + 1,
+ effect: 'Transfer windows closed. No more travel.',
+ };
+
+ function setDifficulty(level) {
+ difficulty = level;
+ }
+
+ function togglePause() {
+ paused = !paused;
+ document.querySelector('button[onclick="togglePause()"]').textContent = paused ? 'Resume' : 'Pause';
+ if (paused) {
+ if (gameRunning) {
+ clearInterval(gameLoop);
+ }
+ } else {
+ if (gameRunning) {
+ gameLoop = setInterval(nextTurn, 1000);
+ }
+ }
+ }
+
+ function toggleTutorial() {
+ tutorial.style.display = tutorial.style.display === 'none' ? 'block' : 'none';
+ }
+
+ function toggleSound() {
+ // Placeholder for sound toggle
+ }
+
+ function toggleMotion() {
+ // Placeholder for reduced motion
+ }
+
+ function resetGame() {
+ gameRunning = false;
+ paused = false;
+ currentTurn = 1;
+ settlements = [];
+ ships = [];
+ events = [];
+ map.innerHTML = '';
+ timeline.innerHTML = '';
+ eventLog.innerHTML = '';
+ shipPlanner.innerHTML = '';
+ settlementDetails.innerHTML = '';
+ tutorial.style.display = 'none';
+ summary.style.display = 'none';
+ document.querySelector('button[onclick="togglePause()"]').textContent = 'Pause';
+ setupGame();
+ }
+
+ function setupGame() {
+ gameRunning = true;
+ currentTurn = 1;
+ settlements = [
+ {
+ name: 'Aurora Station',
+ position: { x: 100, y: 100 },
+ demand: { 'Water': 3, 'Medicine': 1, 'Reactor Parts': 2, 'Research Samples': 0 },
+ storage: { 'Water': 5, 'Medicine': 2, 'Reactor Parts': 3, 'Research Samples': 1 },
+ trust: 80,
+ production: { 'Water': 2, 'Medicine': 1, 'Reactor Parts': 1, 'Research Samples': 0 },
+ },
+ {
+ name: 'Luna Base',
+ position: { x: 300, y: 100 },
+ demand: { 'Water': 4, 'Medicine': 2, 'Reactor Parts': 1, 'Research Samples': 1 },
+ storage: { 'Water': 3, 'Medicine': 1, 'Reactor Parts': 2, 'Research Samples': 0 },
+ trust: 60,
+ production: { 'Water': 1, 'Medicine': 2, 'Reactor Parts': 1, 'Research Samples': 0 },
+ },
+ {
+ name: 'Orion Hub',
+ position: { x: 200, y: 300 },
+ demand: { 'Water': 2, 'Medicine': 3, 'Reactor Parts': 3, 'Research Samples': 1 },
+ storage: { 'Water': 4, 'Medicine': 2, 'Reactor Parts': 1, 'Research Samples': 0 },
+ trust: 70,
+ production: { 'Water': 1, 'Medicine': 1, 'Reactor Parts': 2, 'Research Samples': 0 },
+ },
+ {
+ name: 'Helios Outpost',
+ position: { x: 100, y: 300 },
+ demand: { 'Water': 1, 'Medicine': 0, 'Reactor Parts': 0, 'Research Samples': 3 },
+ storage: { 'Water': 2, 'Medicine': 3, 'Reactor Parts': 2, 'Research Samples': 1 },
+ trust: 50,
+ production: { 'Water': 2, 'Medicine': 2, 'Reactor Parts': 3, 'Research Samples': 1 },
+ },
+ {
+ name: 'Eos Station',
+ position: { x: 300, y: 300 },
+ demand: { 'Water': 3, 'Medicine': 1, 'Reactor Parts': 2, 'Research Samples': 2 },
+ storage: { 'Water': 1, 'Medicine': 3, 'Reactor Parts': 2, 'Research Samples': 0 },
+ trust: 75,
+ production: { 'Water': 2, 'Medicine': 1, 'Reactor Parts': 1, 'Research Samples': 0 },
+ },
+ ];
+ ships = [
+ { id: 1, name: 'Freighter 1', capacity: 4, fuelCost: 2, position: { x: 150, y: 150 }, cargo: {} },
+ { id: 2, name: 'Freighter 2', capacity: 6, fuelCost: 3, position: { x: 250, y: 150 }, cargo: {} },
+ { id: 3, name: 'Freighter 3', capacity: 4, fuelCost: 2, position: { x: 200, y: 250 }, cargo: {} },
+ ];
+
+ events = [
+ {
+ turn: 3,
+ description: 'Luna Base requests an emergency shipment of 3 Medicine units. If denied, trust drops by 20%',
+ type: 'negotiation',
+ consequence: {
+ yes: { trust: -20, event: 'Medicine shipment accepted.' },
+ no: { trust: -20, event: 'Luna Base trust reduced by 20%' },
+ },
+ },
+ {
+ turn: 5,
+ description: 'A malfunction in the Aurora Station\'s reactor requires 2 Reactor Parts. If not delivered by turn 6, 20% of storage is lost.',
+ type: 'event',
+ consequence: {
+ delivered: { event: 'Reactor parts successfully delivered.' },
+ notDelivered: { storageLoss: 20, event: 'Aurora Station lost 20% of storage.' },
+ },
+ },
+ {
+ turn: 8,
+ description: 'Helios Outpost discovers new research samples. 3 units are produced and added to their storage.',
+ type: 'event',
+ consequence: {
+ event: 'Helios Outpost gained 3 Research Samples.',
+ },
+ },
+ ];
+
+ renderMap();
+ renderTimeline();
+ renderShipOptions();
+ renderSettlementDetails();
+ gameLoop = setInterval(nextTurn, 1000);
+ }
+
+ function nextTurn() {
+ if (paused || currentTurn > maxTurns) return;
+ currentTurn++;
+ renderTimeline();
+ checkEvents();
+ if (currentTurn === maxTurns + 1) {
+ clearInterval(gameLoop);
+ endGame();
+ }
+ }
+
+ function checkEvents() {
+ events.forEach(event => {
+ if (event.turn === currentTurn) {
+ eventLog.innerHTML += `<div class="event">${event.description}</div>`;
+ if (event.type === 'negotiation') {
+ showNegotiationChoice(event);
+ }
+ }
+ });
+ }
+
+ function showNegotiationChoice(event) {
+ const settlement = settlements.find(s => s.name === event.description.split(' ')[0]);
+ if (settlement) {
+ const response = prompt(`You must choose whether to fulfill the request from ${settlement.name} for ${event.description.split(' ')[3]} units of ${event.description.split(' ')[2]}. Yes or No?`);
+ if (response && response.toLowerCase() === 'yes') {
+ eventLog.innerHTML += `<div class="event">${event.consequence.yes.event}</div>`;
+ if (event.consequence.yes.trust !== undefined) {
+ settlement.trust += event.consequence.yes.trust;
+ }
+ } else if (response && response.toLowerCase() === 'no') {
+ eventLog.innerHTML += `<div class="event">${event.consequence.no.event}</div>`;
+ if (event.consequence.no.trust !== undefined) {
+ settlement.trust += event.consequence.no.trust;
+ }
+ }
+ }
+ }
+
+ function renderMap() {
+ map.innerHTML = '';
+ settlements.forEach(settlement => {
+ const div = document.createElement('div');
+ div.className = 'settlement';
+ div.style.left = `${settlement.position.x}px`;
+ div.style.top = `${settlement.position.y}px`;
+ div.innerHTML = `
+ <h3>${settlement.name}</h3>
+ <div class="indicator">Trust: ${settlement.trust}</div>
+ `;
+ map.appendChild(div);
+ });
+ ships.forEach(ship => {
+ const shipDiv = document.createElement('div');
+ shipDiv.className = 'ship';
+ shipDiv.style.left = `${ship.position.x}px`;
+ shipDiv.style.top = `${ship.position.y}px`;
+ map.appendChild(shipDiv);
+ });
+ }
+
+ function renderTimeline() {
+ timeline.innerHTML = '';
+ for (let i = 1; i <= maxTurns + 1; i++) {
+ const turnDiv = document.createElement('div');
+ turnDiv.className = 'turn';
+ turnDiv.textContent = i;
+ if (i === currentTurn) {
+ turnDiv.style.backgroundColor = '#4a4a4a';
+ }
+ timeline.appendChild(turnDiv);
+ }
+ }
+
+ function renderShipOptions() {
+ shipPlanner.innerHTML = '';
+ shipOptions.forEach(ship => {
+ const optionDiv = document.createElement('div');
+ optionDiv.innerHTML = `
+ <strong>${ship.name}</strong>
+ <div>Capacity: ${ship.capacity} units</div>
+ <div>Fuel Cost: ${ship.fuelCost}</div>
+ `;
+ shipPlanner.appendChild(optionDiv);
+ });
+ }
+
+ function renderSettlementDetails() {
+ settlementDetails.innerHTML = '';
+ settlements.forEach(settlement => {
+ const detailDiv = document.createElement('div');
+ detailDiv.className = 'section';
+ detailDiv.innerHTML = `
+ <h2>${settlement.name}</h2>
+ <div><span class="title">Demand:</span> ${JSON.stringify(settlement.demand)}</div>
+ <div><span class="title">Storage:</span> ${JSON.stringify(settlement.storage)}</div>
+ <div><span class="title">Trust:</span> ${settlement.trust}</div>
+ <div><span class="title">Production:</span> ${JSON.stringify(settlement.production)}</div>
+ `;
+ settlementDetails.appendChild(detailDiv);
+ });
+ }
+
+ function endGame() {
+ clearInterval(gameLoop);
+ let success = true;
+ settlements.forEach(s => {
+ if (Object.values(s.demand).some(d => s.storage[d] < d)) {
+ success = false;
+ }
+ });
+ if (success) {
+ summaryText.innerHTML = 'You coordinated all freight in time. The settlements are stable and trust is high.';
+ } else {
+ summaryText.innerHTML = 'The solar storm closed the transfer windows. Some settlements are in crisis.';
+ }
+ summary.style.display = 'block';
+ }
+
+ document.addEventListener('keydown', function(e) {
+ if (e.key === 'Escape') {
+ tutorial.style.display = 'none';
+ }
+ });
+ </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/96c1603e5f4a/claude-code.html b/data/artifacts/96c1603e5f4a/claude-code.html
new file mode 100644
index 0000000..d2a85ac
--- /dev/null
+++ b/data/artifacts/96c1603e5f4a/claude-code.html
@@ -0,0 +1,694 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Afterlight: Rooms Remember — A Digital Exhibition</title>
+<style>
+ :root{
+ --paper:#f6f2ea;
+ --ink:#171310;
+ --ink-soft:#4a423a;
+ --line:#d8cfbf;
+ --line-strong:#b9ad98;
+ --accent:#6a4fb5;
+ --ui-bg:#efe9dd;
+ --focus:#0a5cff;
+ --max:1120px;
+ --step:clamp(1rem,.85rem + .6vw,1.15rem);
+ --ease:cubic-bezier(.2,.7,.2,1);
+ --reveal:1;
+ }
+ *{box-sizing:border-box}
+ html{scroll-behavior:smooth}
+ @media (prefers-reduced-motion: reduce){ html{scroll-behavior:auto} }
+ html.no-motion{scroll-behavior:auto}
+
+ body{
+ margin:0;
+ background:var(--paper);
+ color:var(--ink);
+ font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
+ font-size:var(--step);
+ line-height:1.6;
+ -webkit-font-smoothing:antialiased;
+ text-rendering:optimizeLegibility;
+ }
+ .serif{font-family:Georgia,"Times New Roman","Iowan Old Style",serif}
+
+ a{color:inherit}
+ a:focus-visible,button:focus-visible,select:focus-visible,[tabindex]:focus-visible{
+ outline:3px solid var(--focus);
+ outline-offset:2px;
+ border-radius:2px;
+ }
+
+ /* ---------- Skip + live region ---------- */
+ .skip{
+ position:fixed;left:8px;top:-60px;z-index:200;
+ background:var(--ink);color:var(--paper);
+ padding:.6rem 1rem;border-radius:4px;text-decoration:none;
+ transition:top .18s var(--ease);
+ }
+ .skip:focus{top:8px}
+ .sr{position:absolute!important;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
+
+ /* ---------- Progress ---------- */
+ .progress{
+ position:fixed;left:0;top:0;height:4px;width:0%;
+ background:var(--accent);z-index:150;
+ transition:width .08s linear;
+ }
+ html.no-motion .progress{transition:none}
+
+ /* ---------- Top bar ---------- */
+ .bar{
+ position:sticky;top:0;z-index:120;
+ background:color-mix(in srgb,var(--paper) 88%, transparent);
+ backdrop-filter:saturate(120%) blur(6px);
+ border-bottom:1px solid var(--line);
+ }
+ .bar-in{
+ max-width:var(--max);margin:0 auto;padding:.5rem 1rem;
+ display:flex;flex-wrap:wrap;gap:.5rem 1rem;align-items:center;
+ }
+ .brand{font-weight:700;letter-spacing:.02em;font-size:.95rem;margin-right:auto;white-space:nowrap}
+ .brand small{display:block;font-weight:400;color:var(--ink-soft);letter-spacing:.08em;text-transform:uppercase;font-size:.62rem}
+ nav.chapters{display:flex;flex-wrap:wrap;gap:.15rem .35rem}
+ nav.chapters a{
+ text-decoration:none;font-size:.8rem;padding:.28rem .5rem;border-radius:999px;
+ color:var(--ink-soft);border:1px solid transparent;
+ }
+ nav.chapters a:hover{color:var(--ink);border-color:var(--line-strong)}
+ nav.chapters a[aria-current="true"]{color:var(--ink);border-color:var(--accent);font-weight:600}
+ .tools{display:flex;gap:.35rem;flex-wrap:wrap}
+ .tools button{
+ font:inherit;font-size:.75rem;cursor:pointer;
+ background:var(--ui-bg);border:1px solid var(--line-strong);color:var(--ink);
+ padding:.3rem .55rem;border-radius:6px;
+ }
+ .tools button[aria-pressed="true"]{background:var(--ink);color:var(--paper);border-color:var(--ink)}
+
+ /* ---------- Layout ---------- */
+ main{display:block}
+ .wrap{max-width:var(--max);margin:0 auto;padding:0 1rem}
+ section{scroll-margin-top:92px}
+
+ /* ---------- Title sequence ---------- */
+ .hero{
+ min-height:92vh;display:flex;flex-direction:column;justify-content:center;
+ padding:6vh 1rem 4vh;position:relative;overflow:hidden;
+ }
+ .hero-glow{
+ position:absolute;inset:-20% -10% auto -10%;height:70%;
+ background:radial-gradient(60% 80% at 30% 20%, rgba(106,79,181,.18), transparent 70%);
+ pointer-events:none;
+ animation:drift 22s var(--ease) infinite alternate;
+ opacity:var(--reveal);
+ }
+ @keyframes drift{from{transform:translate3d(0,0,0)}to{transform:translate3d(6%,4%,0)}}
+ html.no-motion .hero-glow, .hc .hero-glow{animation:none;display:none}
+ .hero .kicker{letter-spacing:.35em;text-transform:uppercase;font-size:.72rem;color:var(--ink-soft)}
+ .hero h1{
+ font-size:clamp(2.6rem,6vw + 1rem,6.2rem);line-height:.98;margin:.3em 0 .1em;
+ font-weight:700;letter-spacing:-.01em;max-width:15ch;
+ }
+ .hero h1 em{font-style:italic;color:var(--accent)}
+ .hero .lede{font-size:clamp(1.05rem,1rem + .7vw,1.5rem);max-width:46ch;color:var(--ink-soft)}
+ .hero .span{display:inline-block;font-variant-numeric:tabular-nums;border-top:1px solid var(--line-strong);margin-top:1.4rem;padding-top:.6rem;color:var(--ink);font-size:.9rem;letter-spacing:.05em}
+ .enter{
+ margin-top:2rem;display:inline-flex;align-items:center;gap:.5rem;
+ align-self:flex-start;text-decoration:none;font-weight:600;
+ border:1px solid var(--ink);padding:.6rem 1rem;border-radius:8px;
+ }
+ .enter:hover{background:var(--ink);color:var(--paper)}
+
+ .reveal{opacity:0;transform:translateY(14px)}
+ .reveal.in{opacity:1;transform:none;transition:opacity .7s var(--ease),transform .7s var(--ease)}
+ html.no-motion .reveal, .hc .reveal{opacity:1!important;transform:none!important;transition:none!important}
+
+ /* staggered hero lines */
+ .stagger>*{opacity:0;transform:translateY(12px)}
+ .stagger.in>*{opacity:1;transform:none;transition:opacity .6s var(--ease),transform .6s var(--ease)}
+ .stagger.in>*:nth-child(1){transition-delay:.05s}
+ .stagger.in>*:nth-child(2){transition-delay:.18s}
+ .stagger.in>*:nth-child(3){transition-delay:.34s}
+ .stagger.in>*:nth-child(4){transition-delay:.5s}
+ .stagger.in>*:nth-child(5){transition-delay:.66s}
+ html.no-motion .stagger>*, .hc .stagger>*{opacity:1!important;transform:none!important;transition:none!important}
+
+ /* ---------- Thesis ---------- */
+ .thesis{padding:8vh 0 6vh;border-top:1px solid var(--line)}
+ .thesis p{font-size:clamp(1.2rem,1rem + 1vw,1.9rem);max-width:34ch;line-height:1.35}
+ .thesis .serif{font-style:italic}
+ .thesis .note{font-size:.95rem;color:var(--ink-soft);max-width:56ch;margin-top:1.6rem}
+
+ /* ---------- Index ---------- */
+ .index{padding:6vh 0;border-top:1px solid var(--line)}
+ .index h2,.compare h2,.thesis h2{font-size:.78rem;letter-spacing:.28em;text-transform:uppercase;color:var(--ink-soft);margin:0 0 1.4rem}
+ ol.idx{list-style:none;margin:0;padding:0;display:grid;gap:.2rem}
+ ol.idx li a{
+ display:grid;grid-template-columns:2.4rem minmax(0,1fr) auto;gap:1rem;align-items:baseline;
+ text-decoration:none;padding:.9rem .4rem;border-bottom:1px solid var(--line);
+ }
+ ol.idx li a:hover{background:var(--ui-bg)}
+ ol.idx .n{font-variant-numeric:tabular-nums;color:var(--ink-soft);font-size:.9rem}
+ ol.idx .ti{font-family:Georgia,serif;font-size:clamp(1.2rem,1rem + 1vw,1.7rem)}
+ ol.idx .ti b{font-weight:600;display:block}
+ ol.idx .ti span{font-style:italic;color:var(--ink-soft);font-size:.85rem;font-family:inherit}
+ ol.idx .yr{font-variant-numeric:tabular-nums;font-size:1rem;color:var(--ink-soft)}
+ ol.idx .dot{width:.6rem;height:.6rem;border-radius:50%;background:var(--c);display:inline-block;vertical-align:middle;margin-right:.4rem}
+ .hc ol.idx .dot{outline:1px solid var(--ink);background:transparent}
+
+ /* ---------- Object chapters ---------- */
+ .object{padding:12vh 0;border-top:1px solid var(--line);--c:var(--accent)}
+ .object .head{display:grid;gap:.4rem}
+ .object .year{
+ font-family:Georgia,serif;font-weight:700;line-height:.85;
+ font-size:clamp(4rem,14vw,11rem);letter-spacing:-.03em;
+ color:var(--c);
+ }
+ .hc .object .year{color:var(--ink);text-decoration:underline;text-decoration-thickness:2px}
+ .object .meta{display:flex;flex-wrap:wrap;gap:.4rem 1rem;align-items:baseline}
+ .object .obj-n{font-variant-numeric:tabular-nums;letter-spacing:.2em;font-size:.72rem;text-transform:uppercase;color:var(--ink-soft)}
+ .object h3{font-family:Georgia,serif;font-size:clamp(1.7rem,1.2rem + 2vw,3.1rem);margin:.1em 0 0;font-weight:600;line-height:1.05}
+ .object h3 span{display:block;font-style:italic;font-weight:400;font-size:.5em;color:var(--ink-soft)}
+
+ .object .grid{display:grid;grid-template-columns:1fr;gap:2.2rem;margin-top:2.6rem}
+ @media(min-width:760px){.object .grid{grid-template-columns:1.15fr .85fr;gap:3rem}}
+
+ .curator p{font-size:clamp(1.05rem,1rem + .5vw,1.35rem);max-width:56ch}
+ .curator .drop::first-letter{
+ font-family:Georgia,serif;float:left;font-size:3.2em;line-height:.72;
+ padding:.05em .12em 0 0;color:var(--c);font-weight:700;
+ }
+ .hc .curator .drop::first-letter{color:var(--ink)}
+
+ .facts{display:grid;gap:1.4rem;align-content:start;
+ border-left:3px solid var(--c);padding-left:1.2rem}
+ .hc .facts{border-left-color:var(--ink)}
+ .facts h4{margin:0 0 .3rem;font-size:.7rem;letter-spacing:.2em;text-transform:uppercase;color:var(--ink-soft)}
+ .facts .val{font-size:.98rem}
+ .status{display:inline-flex;align-items:center;gap:.5rem;font-weight:600;font-size:.9rem}
+ .status .pip{width:.7rem;height:.7rem;border-radius:50%;background:var(--c);box-shadow:0 0 0 3px color-mix(in srgb,var(--c) 25%,transparent)}
+ .hc .status .pip{background:var(--ink);box-shadow:none;outline:1px solid var(--ink)}
+
+ /* audio / transcript */
+ figure.audio{margin:0;border:1px solid var(--line-strong);border-radius:10px;overflow:hidden;background:var(--ui-bg)}
+ figure.audio .bar2{display:flex;align-items:center;gap:.7rem;padding:.7rem .9rem;border-bottom:1px solid var(--line)}
+ figure.audio button.play{
+ flex:0 0 auto;width:2.2rem;height:2.2rem;border-radius:50%;
+ border:1px solid var(--ink);background:var(--paper);cursor:pointer;font-size:.85rem;
+ display:grid;place-items:center;color:var(--ink);
+ }
+ figure.audio .cap{font-size:.74rem;color:var(--ink-soft);letter-spacing:.04em}
+ figure.audio .cap b{color:var(--ink);letter-spacing:0;font-size:.8rem}
+ figure.audio blockquote{margin:0;padding:.9rem 1rem 1rem;font-family:Georgia,serif;font-style:italic;font-size:1.05rem;line-height:1.5}
+ figure.audio .fallback{padding:.55rem .9rem;font-size:.72rem;color:var(--ink-soft);border-top:1px dashed var(--line-strong);background:color-mix(in srgb,var(--c) 8%,transparent)}
+ .hc figure.audio .fallback{background:transparent}
+ figcaption{font-size:.72rem;color:var(--ink-soft);padding:0 1rem .8rem}
+
+ /* ---------- Compare ---------- */
+ .compare{padding:10vh 0;border-top:1px solid var(--line);background:var(--ui-bg)}
+ .compare .controls{display:flex;flex-wrap:wrap;gap:1rem;align-items:end;margin-bottom:1.6rem}
+ .compare .field{display:grid;gap:.3rem}
+ .compare label{font-size:.72rem;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-soft)}
+ .compare select{font:inherit;font-size:.95rem;padding:.5rem .6rem;border:1px solid var(--line-strong);border-radius:8px;background:var(--paper);color:var(--ink);min-width:min(90vw,22ch)}
+ .compare .swap{align-self:end}
+ .cmp-grid{display:grid;grid-template-columns:1fr;gap:1rem}
+ @media(min-width:720px){.cmp-grid{grid-template-columns:1fr 1fr;gap:1.4rem}}
+ .panel{background:var(--paper);border:1px solid var(--line-strong);border-radius:12px;padding:1.4rem;--c:var(--accent);border-top:5px solid var(--c)}
+ .hc .panel{border-top-color:var(--ink)}
+ .panel .p-year{font-family:Georgia,serif;font-weight:700;color:var(--c);font-size:2.6rem;line-height:1}
+ .hc .panel .p-year{color:var(--ink)}
+ .panel h3{font-family:Georgia,serif;margin:.2rem 0 1rem;font-size:1.4rem;font-weight:600}
+ .panel h3 span{display:block;font-style:italic;font-weight:400;color:var(--ink-soft);font-size:.7em}
+ .panel dl{margin:0;display:grid;gap:1rem}
+ .panel dt{font-size:.68rem;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-soft);margin-bottom:.2rem}
+ .panel dd{margin:0;font-size:.92rem}
+ .cmp-read{margin-top:1.4rem;font-size:.9rem;color:var(--ink-soft);max-width:70ch}
+ .cmp-read b{color:var(--ink)}
+
+ /* ---------- Help ---------- */
+ .help[hidden]{display:none}
+ .help{
+ position:fixed;inset:0;z-index:180;display:grid;place-items:center;padding:1rem;
+ background:rgba(23,19,16,.55);
+ }
+ .help .card{background:var(--paper);border:1px solid var(--ink);border-radius:14px;max-width:34rem;width:100%;padding:1.5rem;max-height:86vh;overflow:auto}
+ .help h2{margin:0 0 .8rem;font-size:1.1rem}
+ .help table{width:100%;border-collapse:collapse;font-size:.9rem}
+ .help td{padding:.4rem 0;border-bottom:1px solid var(--line);vertical-align:top}
+ .help kbd{font-family:ui-monospace,Menlo,Consolas,monospace;background:var(--ui-bg);border:1px solid var(--line-strong);border-bottom-width:2px;border-radius:5px;padding:.05rem .4rem;font-size:.82em}
+ .help .close{margin-top:1rem;font:inherit;cursor:pointer;background:var(--ink);color:var(--paper);border:0;padding:.5rem .9rem;border-radius:8px}
+
+ footer{padding:8vh 0 10vh;border-top:1px solid var(--line);color:var(--ink-soft);font-size:.85rem}
+ footer .wrap>*{max-width:60ch}
+
+ /* ---------- High contrast ---------- */
+ .hc{
+ --paper:#ffffff;--ink:#000000;--ink-soft:#222222;
+ --line:#000000;--line-strong:#000000;--ui-bg:#ffffff;--accent:#000000;
+ }
+ .hc .bar{background:#fff}
+ .hc a{text-underline-offset:2px}
+
+ @media(max-width:430px){
+ .bar-in{padding:.45rem .8rem}
+ .brand{width:100%;margin-bottom:.15rem}
+ nav.chapters{width:100%;overflow-x:auto;flex-wrap:nowrap;padding-bottom:.15rem}
+ nav.chapters a{white-space:nowrap}
+ .object{padding:9vh 0}
+ ol.idx li a{grid-template-columns:1.8rem minmax(0,1fr) auto;gap:.6rem}
+ }
+</style>
+</head>
+<body>
+<a class="skip" href="#thesis">Skip title sequence — go to the curatorial thesis</a>
+<div class="progress" id="progress" role="progressbar" aria-label="Reading progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"></div>
+<div class="sr" id="live" aria-live="polite"></div>
+
+<header class="bar">
+ <div class="bar-in">
+ <div class="brand">Afterlight<small>Rooms Remember · Digital Exhibition</small></div>
+ <nav class="chapters" id="chapnav" aria-label="Chapters">
+ <a href="#top">Title</a>
+ <a href="#thesis">Thesis</a>
+ <a href="#index">Index</a>
+ <!-- object links injected -->
+ <a href="#compare">Compare</a>
+ </nav>
+ <div class="tools">
+ <button id="btnContrast" aria-pressed="false" title="Toggle high contrast (h)">◑ Contrast</button>
+ <button id="btnMotion" aria-pressed="false" title="Toggle motion (m)">〜 Motion</button>
+ <button id="btnHelp" aria-haspopup="dialog" title="Keyboard help (?)">⌨ Keys</button>
+ </div>
+ </div>
+</header>
+
+<main id="main">
+ <!-- ================= TITLE SEQUENCE ================= -->
+ <section class="hero" id="top" aria-label="Title sequence">
+ <div class="hero-glow" aria-hidden="true"></div>
+ <div class="wrap stagger" id="heroStagger">
+ <p class="kicker">An exhibition in five objects · 1928 – 2040</p>
+ <h1 class="serif">After<em>light</em>:<br>Rooms Remember</h1>
+ <p class="lede">Every room keeps a record of the people who moved through it — in the objects they wore smooth, tuned, illuminated, and finally left behind.</p>
+ <p class="span">1928 → 1959 → 1984 → 2011 → 2040</p>
+ <a class="enter" href="#thesis">Enter the exhibition ↓</a>
+ </div>
+ </section>
+
+ <!-- ================= THESIS ================= -->
+ <section class="thesis" id="thesis" aria-labelledby="thesis-h">
+ <div class="wrap reveal">
+ <h2 id="thesis-h">Curatorial Thesis</h2>
+ <p class="serif">Furniture outlives us. This show reads five domestic objects as <em>memory-devices</em> — surfaces that absorbed a household's sound, light, and touch.</p>
+ <p class="note">Arranged chronologically, the objects trace one idea across a century: that a room's memory is <b>material</b> before it is sentimental. Wear, oxidation, magnetic residue, and — by 2040 — living tissue each preserve a trace of habit. Read the works in order to follow the argument; jump between them from the index; or place any two side by side in <a href="#compare">Compare</a> to study how their material histories diverge.</p>
+ </div>
+ </section>
+
+ <!-- ================= OBJECT INDEX ================= -->
+ <section class="index" id="index" aria-labelledby="index-h">
+ <div class="wrap reveal">
+ <h2 id="index-h">Object Index</h2>
+ <ol class="idx" id="idxList"></ol>
+ </div>
+ </section>
+
+ <!-- ================= OBJECT CHAPTERS (injected) ================= -->
+ <div id="objects"></div>
+
+ <!-- ================= COMPARE ================= -->
+ <section class="compare" id="compare" aria-labelledby="compare-h">
+ <div class="wrap reveal">
+ <h2 id="compare-h">Compare — Two Material Histories</h2>
+ <div class="controls">
+ <div class="field">
+ <label for="selA">Left object</label>
+ <select id="selA"></select>
+ </div>
+ <button class="swap" id="btnSwap" type="button" title="Swap the two objects">⇄ Swap</button>
+ <div class="field">
+ <label for="selB">Right object</label>
+ <select id="selB"></select>
+ </div>
+ </div>
+ <div class="cmp-grid" id="cmpGrid"></div>
+ <p class="cmp-read" id="cmpRead"></p>
+ </div>
+ </section>
+
+ <footer aria-label="Colophon">
+ <div class="wrap">
+ <p><b>Afterlight: Rooms Remember</b> — a fictional digital exhibition. All objects, provenances, conservation notes, and audio transcripts are invented for this presentation.</p>
+ <p>Audio excerpts are transcribed oral-history fragments; where playback is unavailable, the transcript is the authoritative record. This exhibition is fully keyboard-navigable — press <kbd style="font-family:ui-monospace,monospace">?</kbd> for controls.</p>
+ </div>
+ </footer>
+</main>
+
+<!-- ================= HELP DIALOG ================= -->
+<div class="help" id="help" role="dialog" aria-modal="true" aria-labelledby="help-h" hidden>
+ <div class="card">
+ <h2 id="help-h">Keyboard controls</h2>
+ <table>
+ <tr><td><kbd>]</kbd> / <kbd>[</kbd></td><td>Next / previous object</td></tr>
+ <tr><td><kbd>i</kbd></td><td>Jump to the object index</td></tr>
+ <tr><td><kbd>c</kbd></td><td>Jump to Compare mode</td></tr>
+ <tr><td><kbd>t</kbd></td><td>Back to the title sequence</td></tr>
+ <tr><td><kbd>b</kbd></td><td>Bookmark your current reading position</td></tr>
+ <tr><td><kbd>g</kbd></td><td>Go back to your bookmarked position</td></tr>
+ <tr><td><kbd>h</kbd></td><td>Toggle high-contrast mode</td></tr>
+ <tr><td><kbd>m</kbd></td><td>Toggle motion / animation</td></tr>
+ <tr><td><kbd>?</kbd></td><td>Open / close this help</td></tr>
+ <tr><td><kbd>Esc</kbd></td><td>Close help</td></tr>
+ </table>
+ <p style="font-size:.8rem;color:var(--ink-soft);margin:.9rem 0 0">Any jump (<kbd>i</kbd>, <kbd>c</kbd>, <kbd>t</kbd>, index links) auto-bookmarks where you were — press <kbd>g</kbd> to return to your prior reading position.</p>
+ <button class="close" id="helpClose">Close</button>
+ </div>
+</div>
+
+<script>
+(function(){
+ "use strict";
+
+ /* ---------------- DATA (invented, plausible) ---------------- */
+ var OBJECTS = [
+ {
+ id:"aurora", n:"01", year:"1928", c:"#b3541e",
+ title:"The Aurora Console", sub:"Cathedral-style radio receiver",
+ curator:"Built to sit where a hearth once did, the Aurora gathered a household around sound instead of fire. Its walnut cabinet was polished nightly; the two Bakelite dials wore pale where thumbs returned, evening after evening, to the same three stations. The object remembers a room organised around listening — chairs angled inward, a lamp left low.",
+ provenance:"Walnut veneer over quarter-sawn oak carcass; cast phenolic (Bakelite) dials; brass grille cloth; five-valve superheterodyne chassis. Cabinet finished in shellac; acquired from a Nebraska estate, one family, 1928–1994.",
+ status:{label:"Stable", note:"Cabinet stable; chassis inert and non-operational. Displayed unpowered to preserve original wiring."},
+ audio:{who:"Oral history — donor's granddaughter", tc:"02:14", text:"“She left it tuned to the same frequency for thirty years. The static was part of the house — we thought that hush was just how a room sounded at night.”"}
+ },
+ {
+ id:"solace", n:"02", year:"1959", c:"#1f6f6b",
+ title:"Solace Lounge Chair", sub:"Moulded shell easy chair",
+ curator:"A machine-age promise of rest, the Solace was moulded in a single fibreglass shell and cushioned in latex foam. It carried its owners' shape more honestly than any portrait: the seat compresses at one worn hollow, the left arm burnished by a reading hand. The object remembers repetition — the same body, the same hour, the same page.",
+ provenance:"Reinforced polyester–fibreglass shell; natural latex foam; hand-tufted wool upholstery in 'harvest' ochre; splayed steel-rod base with brass ferrules. Single-owner provenance, a Chicago apartment, 1959–2016.",
+ status:{label:"Fragile", note:"Latex foam oxidising and crumbling internally; upholstery light-faded. Handling restricted; foam not replaced to retain original wear map."},
+ audio:{who:"Oral history — original owner", tc:"00:47", text:"“You could hear the foam breathe when you sat down. Every visitor sank into the same worn place — the chair had already decided where a person belonged.”"}
+ },
+ {
+ id:"cathode", n:"03", year:"1984", c:"#b02a7a",
+ title:"Cathode Vanity", sub:"CRT-integrated dressing table",
+ curator:"At the height of the home-electronics decade, the Cathode Vanity replaced a mirror with a small cathode-ray tube and a smoked-acrylic surround. Its owner applied makeup in the tube's phosphor glow; the screen still holds a faint magnetic ghost of the last image displayed. The object remembers light as much as touch — a room lit blue-grey after dark.",
+ provenance:"Smoked cast-acrylic surround; chromed tubular-steel frame; 9-inch monochrome CRT; melamine work-surface scarred with cosmetic residue. Salvaged from a Los Angeles rental interior, prop-styled 1984, domestic use to 2003.",
+ status:{label:"Under treatment", note:"CRT vacuum intact but degassing; acrylic crazing along stress lines. Currently in conservation for surface stabilisation."},
+ audio:{who:"Oral history — building superintendent", tc:"01:39", text:"“She did her face in the glow of the tube every morning. The whole room smelled of warm dust and hairspray — you could tell the hour by that smell.”"}
+ },
+ {
+ id:"quietloom", n:"04", year:"2011", c:"#5c6b3d",
+ title:"Quiet Loom Partition", sub:"Acoustic felt room divider",
+ curator:"Not an object of use but of atmosphere, the Quiet Loom was hung to soften a shared apartment's echo. Pressed from recycled bottles into dense felt, it absorbed a decade of overlapping voices. The object remembers what rooms usually forget — noise itself — and the difference between a space that argues back and one that keeps its counsel.",
+ provenance:"100% recycled PET felt (approx. 60 reclaimed bottles per panel); powder-coated steel suspension track; wool-blend binding tape. Commissioned for a Rotterdam co-living flat, in situ 2011–2028.",
+ status:{label:"Monitored", note:"Felt structurally sound; embedded particulate and minor moth activity logged. Stored flat under humidity monitoring."},
+ audio:{who:"Oral history — former flatmate", tc:"00:58", text:"“After we hung it, the arguments stopped echoing. The room finally kept its own counsel — you could disagree without the walls repeating it back at you.”"}
+ },
+ {
+ id:"mnemosyne", n:"05", year:"2040", c:"#6a4fb5",
+ title:"Mnemosyne Wall", sub:"Bio-acoustic memory panel",
+ curator:"The exhibition's endpoint makes the thesis literal. A cultivated mycelium panel, seeded with acoustic-sensitive tissue, the Mnemosyne Wall records a room's quietest hour and re-emits it as faint light. It is the first object here that remembers on purpose — and the first the museum must keep alive. Where the others hold memory as damage, this one holds it as growth.",
+ provenance:"Living mycelium composite on a hemp-cellulose substrate; bio-luminescent bacterial culture; low-voltage nutrient lattice; oak frame. Grown-to-commission, Kyoto studio, 2040; accessioned as a living work.",
+ status:{label:"Active — living", note:"Living material under active horticultural care: nutrient, humidity, and light cycles maintained daily. Long-term display is a conservation experiment, not a settled state."},
+ audio:{who:"Artist statement — recorded", tc:"01:05", text:"“It plays back a room's quietest hour. Mine chose three in the morning — the sound of no one leaving. I didn't teach it that; the room did.”"}
+ }
+ ];
+
+ var $=function(s,r){return (r||document).querySelector(s);};
+ var live=$("#live");
+ function announce(msg){ live.textContent=""; setTimeout(function(){live.textContent=msg;},30); }
+
+ /* ---------------- RENDER: index ---------------- */
+ var idxList=$("#idxList");
+ var chapnav=$("#chapnav");
+ var compareLink=chapnav.querySelector('a[href="#compare"]');
+ OBJECTS.forEach(function(o){
+ var li=document.createElement("li");
+ li.innerHTML='<a href="#obj-'+o.id+'" style="--c:'+o.c+'">'+
+ '<span class="n">'+o.n+'</span>'+
+ '<span class="ti"><b><span class="dot"></span>'+o.title+'</b><span>'+o.sub+'</span></span>'+
+ '<span class="yr">'+o.year+'</span></a>';
+ idxList.appendChild(li);
+ // chapter nav link before Compare
+ var a=document.createElement("a");
+ a.href="#obj-"+o.id; a.textContent=o.year;
+ a.setAttribute("data-obj",o.id);
+ chapnav.insertBefore(a,compareLink);
+ });
+
+ /* ---------------- RENDER: object chapters ---------------- */
+ var host=$("#objects");
+ OBJECTS.forEach(function(o){
+ var sec=document.createElement("section");
+ sec.className="object"; sec.id="obj-"+o.id;
+ sec.style.setProperty("--c",o.c);
+ sec.setAttribute("aria-labelledby","h-"+o.id);
+ sec.setAttribute("tabindex","-1");
+ sec.innerHTML=
+ '<div class="wrap">'+
+ '<div class="head reveal">'+
+ '<div class="year" aria-hidden="true">'+o.year+'</div>'+
+ '<div class="meta"><span class="obj-n">Object '+o.n+' · '+o.year+'</span></div>'+
+ '<h3 id="h-'+o.id+'">'+o.title+'<span>'+o.sub+'</span></h3>'+
+ '</div>'+
+ '<div class="grid">'+
+ '<div class="curator reveal"><p class="drop">'+o.curator+'</p>'+
+ '<figure class="audio" style="margin-top:1.8rem">'+
+ '<div class="bar2">'+
+ '<button class="play" type="button" aria-pressed="false" '+
+ 'aria-label="Play audio excerpt for '+o.title+'">▶</button>'+
+ '<span class="cap"><b>Audio excerpt</b> · '+o.audio.who+' · '+o.audio.tc+'</span>'+
+ '</div>'+
+ '<blockquote>'+o.audio.text+'</blockquote>'+
+ '<p class="fallback" role="status">Transcript shown above.</p>'+
+ '<figcaption>Transcribed oral-history fragment. If audio cannot play, the transcript is the authoritative record.</figcaption>'+
+ '</figure>'+
+ '</div>'+
+ '<div class="facts reveal">'+
+ '<div><h4>Material provenance</h4><div class="val">'+o.provenance+'</div></div>'+
+ '<div><h4>Conservation status</h4>'+
+ '<div class="status"><span class="pip"></span>'+o.status.label+'</div>'+
+ '<div class="val" style="margin-top:.4rem;color:var(--ink-soft);font-size:.9rem">'+o.status.note+'</div>'+
+ '</div>'+
+ '</div>'+
+ '</div>'+
+ '</div>';
+ host.appendChild(sec);
+ });
+
+ /* ---------------- Audio fallback behaviour ---------------- */
+ host.addEventListener("click",function(e){
+ var btn=e.target.closest("button.play"); if(!btn) return;
+ var fig=btn.closest("figure.audio");
+ var fb=fig.querySelector(".fallback");
+ var pressed=btn.getAttribute("aria-pressed")==="true";
+ // No bundled audio source exists — always resolve to the graceful fallback.
+ if(!pressed){
+ btn.setAttribute("aria-pressed","true"); btn.textContent="ⓘ";
+ fb.textContent="Audio playback is unavailable in this exhibition build — the transcript above is the complete excerpt.";
+ fb.style.color="var(--ink)";
+ announce("Audio unavailable. The transcript is the authoritative record of this excerpt.");
+ }else{
+ btn.setAttribute("aria-pressed","false"); btn.textContent="▶";
+ fb.textContent="Transcript shown above."; fb.style.color="";
+ }
+ });
+
+ /* ---------------- Compare ---------------- */
+ var selA=$("#selA"), selB=$("#selB"), cmpGrid=$("#cmpGrid"), cmpRead=$("#cmpRead");
+ OBJECTS.forEach(function(o,i){
+ [selA,selB].forEach(function(sel){
+ var op=document.createElement("option");
+ op.value=String(i); op.textContent=o.year+" — "+o.title;
+ sel.appendChild(op);
+ });
+ });
+ selA.value="0"; selB.value=String(OBJECTS.length-1);
+
+ function panel(o){
+ return '<div class="panel" style="--c:'+o.c+'">'+
+ '<div class="p-year">'+o.year+'</div>'+
+ '<h3>'+o.title+'<span>'+o.sub+'</span></h3>'+
+ '<dl>'+
+ '<div><dt>Material provenance</dt><dd>'+o.provenance+'</dd></div>'+
+ '<div><dt>Conservation status</dt><dd><span class="status"><span class="pip"></span>'+o.status.label+'</span><br>'+o.status.note+'</dd></div>'+
+ '<div><dt>Excerpt</dt><dd style="font-family:Georgia,serif;font-style:italic">'+o.audio.text+'</dd></div>'+
+ '</dl>'+
+ '</div>';
+ }
+ function renderCompare(){
+ var a=OBJECTS[+selA.value], b=OBJECTS[+selB.value];
+ cmpGrid.innerHTML=panel(a)+panel(b);
+ var span=Math.abs(+a.year - +b.year);
+ cmpRead.innerHTML="Reading across "+span+" years: <b>"+a.title+"</b> ("+a.status.label.toLowerCase()+
+ ") and <b>"+b.title+"</b> ("+b.status.label.toLowerCase()+
+ ") show how a room's memory shifts from accidental damage toward deliberate, cultivated record. Change either selector to re-pair the histories.";
+ announce("Comparing "+a.title+" and "+b.title+".");
+ }
+ selA.addEventListener("change",renderCompare);
+ selB.addEventListener("change",renderCompare);
+ $("#btnSwap").addEventListener("click",function(){
+ var t=selA.value; selA.value=selB.value; selB.value=t; renderCompare();
+ });
+ renderCompare();
+
+ /* ---------------- Progress ---------------- */
+ var progress=$("#progress");
+ function updateProgress(){
+ var h=document.documentElement;
+ var max=h.scrollHeight - h.clientHeight;
+ var p= max>0 ? (h.scrollTop||window.pageYOffset)/max : 0;
+ p=Math.max(0,Math.min(1,p));
+ progress.style.width=(p*100).toFixed(1)+"%";
+ progress.setAttribute("aria-valuenow",Math.round(p*100));
+ }
+ window.addEventListener("scroll",updateProgress,{passive:true});
+ window.addEventListener("resize",updateProgress);
+ updateProgress();
+
+ /* ---------------- Reveal on scroll ---------------- */
+ var prefersReduced = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+ var motionOff=false;
+ function motionEnabled(){ return !motionOff && !prefersReduced && !document.documentElement.classList.contains("no-motion"); }
+
+ var io=null;
+ function setupReveal(){
+ var els=document.querySelectorAll(".reveal, .stagger");
+ if(!motionEnabled() || !("IntersectionObserver" in window)){
+ els.forEach(function(el){el.classList.add("in");});
+ return;
+ }
+ io=new IntersectionObserver(function(entries){
+ entries.forEach(function(en){
+ if(en.isIntersecting){ en.target.classList.add("in"); io.unobserve(en.target); }
+ });
+ },{rootMargin:"0px 0px -8% 0px",threshold:.12});
+ els.forEach(function(el){ io.observe(el); });
+ }
+ setupReveal();
+
+ /* ---------------- Active chapter in nav ---------------- */
+ var navLinks=Array.prototype.slice.call(chapnav.querySelectorAll("a"));
+ var sections=navLinks.map(function(a){return document.querySelector(a.getAttribute("href"));});
+ function updateActive(){
+ var y=(window.pageYOffset||document.documentElement.scrollTop)+120;
+ var idx=0;
+ sections.forEach(function(s,i){ if(s && s.offsetTop<=y) idx=i; });
+ navLinks.forEach(function(a,i){ a.setAttribute("aria-current", i===idx?"true":"false"); });
+ }
+ window.addEventListener("scroll",updateActive,{passive:true});
+ updateActive();
+
+ /* ---------------- Bookmark / reading position ---------------- */
+ var bookmark=null;
+ function saveBookmark(silent){
+ bookmark=window.pageYOffset||document.documentElement.scrollTop;
+ if(!silent) announce("Position bookmarked. Press g to return here.");
+ }
+ function gotoBookmark(){
+ if(bookmark==null){ announce("No bookmark set yet. Press b to bookmark your position."); return; }
+ window.scrollTo({top:bookmark, behavior: motionEnabled()?"smooth":"auto"});
+ announce("Returned to your bookmarked reading position.");
+ }
+ function jumpTo(hash,label){
+ saveBookmark(true); // auto-bookmark prior position so 'g' returns
+ var el=document.querySelector(hash);
+ if(el){
+ el.scrollIntoView({behavior: motionEnabled()?"smooth":"auto", block:"start"});
+ if(el.hasAttribute("tabindex")){ el.focus({preventScroll:true}); }
+ announce(label+" (press g to return to where you were).");
+ }
+ }
+
+ /* current object index for [ ] navigation */
+ function currentObjectIndex(){
+ var y=(window.pageYOffset||document.documentElement.scrollTop)+140;
+ var idx=-1;
+ OBJECTS.forEach(function(o,i){
+ var el=document.getElementById("obj-"+o.id);
+ if(el && el.offsetTop<=y) idx=i;
+ });
+ return idx;
+ }
+ function gotoObject(i){
+ i=Math.max(0,Math.min(OBJECTS.length-1,i));
+ var o=OBJECTS[i];
+ jumpTo("#obj-"+o.id, o.year+" · "+o.title);
+ }
+
+ /* ---------------- Toggles ---------------- */
+ var root=document.documentElement;
+ var btnContrast=$("#btnContrast"), btnMotion=$("#btnMotion");
+ function toggleContrast(){
+ var on=document.body.classList.toggle("hc");
+ btnContrast.setAttribute("aria-pressed",on?"true":"false");
+ announce("High contrast "+(on?"on":"off")+".");
+ }
+ function toggleMotion(){
+ motionOff=!motionOff;
+ root.classList.toggle("no-motion",motionOff);
+ btnMotion.setAttribute("aria-pressed",motionOff?"true":"false");
+ if(motionOff){ document.querySelectorAll(".reveal,.stagger").forEach(function(el){el.classList.add("in");}); }
+ announce("Animation "+(motionOff?"reduced":"enabled")+".");
+ }
+ btnContrast.addEventListener("click",toggleContrast);
+ btnMotion.addEventListener("click",toggleMotion);
+ if(prefersReduced){ motionOff=true; root.classList.add("no-motion"); btnMotion.setAttribute("aria-pressed","true"); }
+
+ /* ---------------- Help dialog ---------------- */
+ var help=$("#help"), helpBtn=$("#btnHelp"), helpClose=$("#helpClose");
+ var lastFocus=null;
+ function openHelp(){
+ lastFocus=document.activeElement;
+ help.hidden=false; helpClose.focus();
+ }
+ function closeHelp(){
+ help.hidden=true; if(lastFocus&&lastFocus.focus) lastFocus.focus();
+ }
+ helpBtn.addEventListener("click",openHelp);
+ helpClose.addEventListener("click",closeHelp);
+ help.addEventListener("click",function(e){ if(e.target===help) closeHelp(); });
+ help.addEventListener("keydown",function(e){
+ if(e.key==="Escape"){ closeHelp(); return; }
+ if(e.key==="Tab"){ // simple focus trap
+ var f=help.querySelectorAll("button, a[href]");
+ var first=f[0], last=f[f.length-1];
+ if(e.shiftKey && document.activeElement===first){ e.preventDefault(); last.focus(); }
+ else if(!e.shiftKey && document.activeElement===last){ e.preventDefault(); first.focus(); }
+ }
+ });
+
+ /* ---------------- Global keyboard shortcuts ---------------- */
+ document.addEventListener("keydown",function(e){
+ if(e.metaKey||e.ctrlKey||e.altKey) return;
+ var t=e.target;
+ var typing = t && (t.tagName==="SELECT"||t.tagName==="INPUT"||t.tagName==="TEXTAREA"||t.isContentEditable);
+ if(!help.hidden){ return; } // help handles its own keys
+ if(typing) return;
+ switch(e.key){
+ case "?": e.preventDefault(); openHelp(); break;
+ case "h": toggleContrast(); break;
+ case "m": toggleMotion(); break;
+ case "i": e.preventDefault(); jumpTo("#index","Object index"); break;
+ case "c": e.preventDefault(); jumpTo("#compare","Compare mode"); break;
+ case "t": e.preventDefault(); jumpTo("#top","Title sequence"); break;
+ case "b": e.preventDefault(); saveBookmark(false); break;
+ case "g": e.preventDefault(); gotoBookmark(); break;
+ case "]": case ".": e.preventDefault(); gotoObject(currentObjectIndex()+1); break;
+ case "[": case ",": e.preventDefault(); gotoObject((currentObjectIndex()<0?0:currentObjectIndex())-1); break;
+ }
+ });
+
+ /* index / nav links auto-bookmark prior position */
+ document.addEventListener("click",function(e){
+ var a=e.target.closest('a[href^="#"]'); if(!a) return;
+ // let the browser handle the hash jump, but stash where we were first
+ saveBookmark(true);
+ });
+
+})();
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/96c1603e5f4a/qwen25-7b.html b/data/artifacts/96c1603e5f4a/qwen25-7b.html
new file mode 100644
index 0000000..9f42064
--- /dev/null
+++ b/data/artifacts/96c1603e5f4a/qwen25-7b.html
@@ -0,0 +1,128 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Afterlight: Rooms Remember</title>
+ <style>
+ body { font-family: Arial, sans-serif; }
+ .container { width: 390px; margin: auto; padding: 20px; background-color: #f4f4f9; }
+ h1, p, ul, ol { margin-top: 0; }
+ nav { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
+ .nav-btn { cursor: pointer; padding: 5px 10px; background-color: #333; color: white; border: none; border-radius: 4px; transition: background-color 0.3s; }
+ .nav-btn:hover { background-color: #555; }
+ .title-sequence { display: flex; justify-content: center; align-items: center; height: 80px; background-color: #2c3e50; color: white; font-size: 1.5em; }
+ .index-list, .object-info { list-style-type: none; padding: 0; margin: 0; }
+ .object-title { font-size: 1.2em; margin-bottom: 10px; }
+ .curator-note, .material-provenance, .conservation-status { margin-top: 5px; }
+ audio { display: none; }
+ .compare-mode { display: flex; justify-content: space-between; margin-top: 20px; }
+ .compare-object { width: calc(50% - 10px); padding: 10px; border: 1px solid #ddd; box-sizing: border-box; overflow-y: auto; height: 200px; }
+ .reading-progress { position: fixed; bottom: 10px; left: 10px; background-color: rgba(0, 0, 0, 0.5); color: white; padding: 10px; border-radius: 4px; font-size: 0.9em; }
+ .reading-progress::before { content: 'Reading Progress:'; margin-right: 10px; }
+ .high-contrast-toggle { display: block; margin-top: 10px; background-color: #2c3e50; color: white; padding: 5px 10px; border: none; cursor: pointer; transition: background-color 0.3s; }
+ .high-contrast-toggle:hover { background-color: #34495e; }
+ </style>
+</head>
+<body>
+ <div class="container">
+ <!-- Title Sequence -->
+ <div class="title-sequence">Afterlight: Rooms Remember</div>
+
+ <!-- Navigation Buttons -->
+ <nav>
+ <button id="prev-btn" class="nav-btn">Previous</button>
+ <button id="next-btn" class="nav-btn">Next</button>
+ <button id="compare-btn" class="nav-btn">Compare</button>
+ <button id="toggle-contrast" class="high-contrast-toggle">High Contrast</button>
+ </nav>
+
+ <!-- Object Index -->
+ <ul class="index-list">
+ <li><a href="#object1">Object 1 (1928)</a></li>
+ <li><a href="#object2">Object 2 (1960)</a></li>
+ <li><a href="#object3">Object 3 (2005)</a></li>
+ <li><a href="#object4">Object 4 (2030)</a></li>
+ <li><a href="#object5">Object 5 (2040)</a></li>
+ </ul>
+
+ <!-- Object Information -->
+ <div id="object1" class="object-info">
+ <h2 class="object-title">Object 1: The Dusk Lamp (1928)</h2>
+ <p><span class="curator-note">Curator Note:</span> A functional design masterpiece, reflecting the optimism of early 20th-century aesthetics.</p>
+ <ul>
+ <li><span class="material-provenance">Material Provenance:</span> Brass, hand-blown glass</li>
+ <li><span class="conservation-status">Conservation Status:</span> Excellent condition, preserved in the Museum of Modern Design.</li>
+ </ul>
+ </div>
+
+ <!-- Compare Mode -->
+ <div id="compare-mode" class="compare-mode">
+ <div class="compare-object" id="object1-obj1"><h2 class="object-title">Object 1: The Dusk Lamp (1928)</h2></div>
+ <div class="compare-object" id="object1-obj2"><audio controls><source src="#"></source>Your browser does not support the audio element.</audio></div>
+ </div>
+
+ <!-- Audio Transcript -->
+ <p><span class="material-provenance">Audio Excerpt:</span> "Brass and glass, a beacon of hope in the twilight of the Roaring Twenties."</p>
+
+ <!-- Reading Progress Indicator -->
+ <div class="reading-progress" id="progress-indicator"></div>
+ </div>
+
+ <script>
+ document.addEventListener('DOMContentLoaded', () => {
+ const navBtns = document.querySelectorAll('.nav-btn');
+ let currentIndex = 1;
+ const objects = [
+ { id: 'object1', title: 'The Dusk Lamp (1928)', content: '<h2 class="object-title">Object 1: The Dusk Lamp (1928)</h2><p><span class="curator-note">Curator Note:</span> A functional design masterpiece, reflecting the optimism of early 20th-century aesthetics.</p><ul><li><span class="material-provenance">Material Provenance:</span> Brass, hand-blown glass</li><li><span class="conservation-status">Conservation Status:</span> Excellent condition, preserved in the Museum of Modern Design.</li></ul>' },
+ { id: 'object2', title: 'The Echo Chair (1960)', content: '<h2 class="object-title">Object 2: The Echo Chair (1960)</h2><p><span class="curator-note">Curator Note:</span> An iconic design, embodying the mid-century modern aesthetic.</p><ul><li><span class="material-provenance">Material Provenance:</span> Fiberglass and foam</li><li><span class="conservation-status">Conservation Status:</span> Restored in 2019.</li></ul>' },
+ { id: 'object3', title: 'The Lumina Desk (2005)', content: '<h2 class="object-title">Object 3: The Lumina Desk (2005)</h2><p><span class="curator-note">Curator Note:</span> A breakthrough in sustainable design, blending technology and nature.</p><ul><li><span class="material-provenance">Material Provenance:</span> Recycled aluminum and bamboo</li><li><span class="conservation-status">Conservation Status:</span> Good condition after minor repairs in 2018.</li></ul>' },
+ { id: 'object4', title: 'The Aether Table (2030)', content: '<h2 class="object-title">Object 4: The Aether Table (2030)</h2><p><span class="curator-note">Curator Note:</span> An experimental design, pushing the boundaries of form and function.</p><ul><li><span class="material-provenance">Material Provenance:</span> Carbon fiber and smart polymers</li><li><span class="conservation-status">Conservation Status:</span> Requires ongoing maintenance.</li></ul>' },
+ { id: 'object5', title: 'The Horizon Chair (2040)', content: '<h2 class="object-title">Object 5: The Horizon Chair (2040)</h2><p><span class="curator-note">Curator Note:</span> A testament to the enduring legacy of design.</p><ul><li><span class="material-provenance">Material Provenance:</span> AI-optimized composite materials</li><li><span class="conservation-status">Conservation Status:</span> Unavailable for public viewing due to technological obsolescence.</li></ul>' }
+ ];
+
+ function showObject(index) {
+ document.querySelector('.object-info').innerHTML = '';
+ if (index < 1 || index > objects.length) return;
+ const objectContent = objects[index - 1].content;
+ document.querySelector('.object-info').innerHTML = objectContent;
+ currentIndex = index;
+
+ // Update navigation buttons
+ navBtns.forEach((btn, i) => {
+ btn.addEventListener('click', () => {
+ if (i === 0 && currentIndex > 1 || i === 1 && currentIndex < objects.length || i === 2) {
+ showObject(currentIndex + i - (currentIndex > 1 ? 1 : 0));
+ }
+ });
+ });
+
+ // Update compare mode
+ const compareMode = document.getElementById('compare-mode');
+ if (currentIndex > 1) {
+ compareMode.style.display = 'flex';
+ compareMode.querySelector('.compare-object').id = `object${currentIndex - 1}-obj1`;
+ compareMode.querySelector(`.compare-object`).innerHTML = objects[currentIndex - 2].content;
+ } else {
+ compareMode.style.display = 'none';
+ }
+ }
+
+ showObject(currentIndex);
+
+ // High Contrast Toggle
+ document.getElementById('toggle-contrast').addEventListener('click', () => {
+ if (document.body.classList.toggle('high-contrast')) {
+ document.getElementById('progress-indicator').textContent = 'High contrast activated.';
+ } else {
+ document.getElementById('progress-indicator').textContent = 'High contrast deactivated.';
+ }
+ });
+
+ // Reading Progress Indicator
+ const progressIndicator = document.getElementById('progress-indicator');
+ progressIndicator.textContent = `Current Object: ${currentIndex} of ${objects.length}`;
+ });
+ </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/96c1603e5f4a/qwen3-14b.html b/data/artifacts/96c1603e5f4a/qwen3-14b.html
new file mode 100644
index 0000000..a076144
--- /dev/null
+++ b/data/artifacts/96c1603e5f4a/qwen3-14b.html
@@ -0,0 +1,713 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Afterlight: Rooms Remember</title>
+ <style>
+ :root {
+ --primary: #1a1a1a;
+ --secondary: #e0e0e0;
+ --accent: #ff6f61;
+ --text: #ffffff;
+ --bg: #000000;
+ --highlight: #444444;
+ --font: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ --transition: 0.3s ease-in-out;
+ }
+
+ * {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ }
+
+ body {
+ background-color: var(--bg);
+ color: var(--text);
+ font-family: var(--font);
+ overflow-x: hidden;
+ scroll-behavior: smooth;
+ height: 100vh;
+ width: 100%;
+ }
+
+ .title-sequence {
+ position: fixed;
+ top: 0; left: 0; right: 0; bottom: 0;
+ background: linear-gradient(135deg, #0f0f0f, #1a1a1a, #0f0f0f);
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ z-index: 100;
+ pointer-events: none;
+ transition: opacity var(--transition);
+ }
+
+ .title-sequence h1 {
+ font-size: 4rem;
+ margin-bottom: 2rem;
+ color: var(--accent);
+ text-shadow: 1px 1px 4px #000000;
+ }
+
+ .title-sequence p {
+ font-size: 1.2rem;
+ opacity: 0.8;
+ max-width: 700px;
+ margin: 0 auto;
+ }
+
+ .nav-bar {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ background: rgba(0, 0, 0, 0.9);
+ padding: 1rem 2rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ z-index: 100;
+ }
+
+ .nav-bar h2 {
+ font-size: 1.2rem;
+ font-weight: bold;
+ }
+
+ .controls {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ }
+
+ .controls button {
+ background: none;
+ border: none;
+ color: var(--accent);
+ font-size: 1rem;
+ cursor: pointer;
+ padding: 0.5rem 0.75rem;
+ border-radius: 4px;
+ transition: background var(--transition);
+ }
+
+ .controls button:hover {
+ background: rgba(255, 111, 97, 0.15);
+ }
+
+ .progress {
+ position: fixed;
+ top: 60px;
+ left: 0;
+ right: 0;
+ height: 4px;
+ background: #333;
+ overflow: hidden;
+ pointer-events: none;
+ z-index: 90;
+ }
+
+ .progress-fill {
+ height: 100%;
+ background: var(--accent);
+ width: 0%;
+ transition: width var(--transition);
+ }
+
+ .container {
+ position: relative;
+ padding-top: 100px;
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ overflow-y: auto;
+ }
+
+ .object {
+ display: flex;
+ flex-direction: column;
+ padding: 3rem 2rem;
+ min-height: 100vh;
+ position: relative;
+ scroll-margin-top: 100px;
+ }
+
+ .object-header {
+ font-size: 2rem;
+ margin-bottom: 1rem;
+ }
+
+ .object-details {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ margin-bottom: 2rem;
+ }
+
+ .object-details p {
+ font-size: 1rem;
+ line-height: 1.5;
+ }
+
+ .object-audio {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ margin-bottom: 1rem;
+ }
+
+ .object-audio audio {
+ width: 100%;
+ max-width: 300px;
+ height: auto;
+ }
+
+ .object-audio p {
+ font-size: 0.9rem;
+ color: #aaa;
+ }
+
+ .index {
+ position: fixed;
+ top: 100px;
+ right: 0;
+ width: 240px;
+ height: calc(100vh - 100px);
+ overflow-y: auto;
+ background: var(--highlight);
+ padding: 1rem;
+ border-left: 2px solid var(--accent);
+ font-size: 0.95rem;
+ }
+
+ .index h3 {
+ font-size: 1.2rem;
+ margin-bottom: 0.5rem;
+ }
+
+ .index a {
+ color: var(--text);
+ text-decoration: none;
+ display: block;
+ padding: 0.4rem 0;
+ transition: color var(--transition);
+ }
+
+ .index a:hover {
+ color: var(--accent);
+ }
+
+ .compare {
+ position: fixed;
+ top: 100px;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.9);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 200;
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity var(--transition);
+ }
+
+ .compare.active {
+ pointer-events: auto;
+ opacity: 1;
+ }
+
+ .compare-container {
+ display: flex;
+ gap: 2rem;
+ max-width: 1000px;
+ width: 90vw;
+ height: 80vh;
+ overflow: hidden;
+ }
+
+ .compare-object {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ }
+
+ .compare-object h3 {
+ font-size: 1.4rem;
+ margin-bottom: 0.5rem;
+ }
+
+ .compare-object .object-details p {
+ font-size: 1rem;
+ line-height: 1.4;
+ }
+
+ .compare-object .object-audio {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ margin-bottom: 1rem;
+ }
+
+ .compare-object .object-audio audio {
+ width: 100%;
+ max-width: 100%;
+ height: auto;
+ }
+
+ .compare-object .object-audio p {
+ font-size: 0.85rem;
+ color: #aaa;
+ }
+
+ .compare-close {
+ position: absolute;
+ top: 1rem;
+ right: 1rem;
+ background: rgba(255, 255, 255, 0.1);
+ border: none;
+ color: var(--text);
+ padding: 0.5rem 1rem;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 1rem;
+ transition: background var(--transition);
+ }
+
+ .compare-close:hover {
+ background: rgba(255, 255, 255, 0.2);
+ }
+
+ @media (prefers-reduced-motion: no-preference) {
+ .title-sequence {
+ animation: fadeOut 4s ease-in-out forwards;
+ }
+ }
+
+ @keyframes fadeOut {
+ 0% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0;
+ }
+ }
+
+ @media (max-width: 600px) {
+ .title-sequence h1 {
+ font-size: 2.5rem;
+ }
+
+ .title-sequence p {
+ font-size: 1rem;
+ max-width: 100%;
+ }
+
+ .nav-bar h2 {
+ font-size: 1rem;
+ }
+
+ .controls button {
+ font-size: 0.9rem;
+ }
+
+ .index {
+ width: 100%;
+ height: auto;
+ position: relative;
+ top: 0;
+ right: 0;
+ padding: 0.5rem;
+ border: none;
+ border-top: 2px solid var(--accent);
+ }
+
+ .progress {
+ top: 60px;
+ }
+
+ .object-header {
+ font-size: 1.5rem;
+ }
+
+ .index a {
+ font-size: 0.9rem;
+ }
+
+ .compare-container {
+ flex-direction: column;
+ gap: 1rem;
+ }
+
+ .compare-object {
+ flex: 1 1 100%;
+ }
+
+ .compare-object h3 {
+ font-size: 1.2rem;
+ }
+
+ .compare-object .object-details p {
+ font-size: 0.95rem;
+ }
+
+ .compare-object .object-audio audio {
+ max-width: 100%;
+ }
+
+ .compare-object .object-audio p {
+ font-size: 0.8rem;
+ }
+ }
+
+ .no-media {
+ color: #ccc;
+ font-style: italic;
+ }
+
+ .high-contrast {
+ background: #ffffff;
+ color: #000000;
+ }
+
+ .high-contrast .progress-fill {
+ background: #000000;
+ }
+
+ .high-contrast .nav-bar {
+ background: #ffffff;
+ }
+
+ .high-contrast .nav-bar h2 {
+ color: #000000;
+ }
+
+ .high-contrast .controls button {
+ color: #000000;
+ }
+
+ .high-contrast .controls button:hover {
+ background: rgba(0, 0, 0, 0.15);
+ }
+
+ .high-contrast .index {
+ background: #ffffff;
+ border-left: 2px solid #000000;
+ }
+
+ .high-contrast .index a {
+ color: #000000;
+ }
+
+ .high-contrast .index a:hover {
+ color: #000000;
+ }
+
+ .high-contrast .compare {
+ background: #ffffff;
+ }
+
+ .high-contrast .compare-close {
+ background: rgba(0, 0, 0, 0.1);
+ color: #000000;
+ }
+
+ .high-contrast .compare-close:hover {
+ background: rgba(0, 0, 0, 0.2);
+ }
+
+ .high-contrast .object {
+ background: #ffffff;
+ }
+
+ .high-contrast .object-header {
+ color: #000000;
+ }
+
+ .high-contrast .object-details p {
+ color: #000000;
+ }
+
+ .high-contrast .object-audio p {
+ color: #000000;
+ }
+
+ .high-contrast .no-media {
+ color: #333;
+ }
+ </style>
+</head>
+<body class="high-contrast">
+ <div class="title-sequence">
+ <h1>Afterlight: Rooms Remember</h1>
+ <p>A journey through memory and materiality, 1928–2040.</p>
+ </div>
+
+ <div class="nav-bar">
+ <h2>Afterlight</h2>
+ <div class="controls">
+ <button id="toggleHighContrast">Toggle High Contrast</button>
+ <button id="openIndex">Index</button>
+ <button id="openCompare">Compare</button>
+ </div>
+ </div>
+
+ <div class="progress">
+ <div class="progress-fill" id="progressFill"></div>
+ </div>
+
+ <div class="container">
+ <div class="object" id="obj1">
+ <div class="object-header">1. The Echo Bench (1928)</div>
+ <div class="object-details">
+ <p><strong>Curator Note:</strong> A bench in a forgotten schoolroom, carved from salvaged wood of a burned library. It bears the initials of students who never returned.</p>
+ <p><strong>Material Provenance:</strong> Oak, poplar, and ash from the 1922 fire at Saint Anselm’s Library.</p>
+ <p><strong>Conservation Status:</strong> Stable, with visible burn marks and faint engraving.</p>
+ </div>
+ <div class="object-audio">
+ <audio controls class="no-media" aria-label="Audio excerpt: The Echo Bench">
+ <source src="" type="audio/mpeg" />
+ Your browser does not support the audio element.
+ </audio>
+ <p class="no-media">"This bench is a monument to silence—of books lost, of voices never heard." – Curator, 2024.</p>
+ </div>
+ </div>
+
+ <div class="object" id="obj2">
+ <div class="object-header">2. The Data Veil (2003)</div>
+ <div class="object-details">
+ <p><strong>Curator Note:</strong> A translucent fabric woven with fiber-optic threads, displaying real-time data from the 2003 blackout.</p>
+ <p><strong>Material Provenance:</strong> Polyester, optical fibers from decommissioned servers in the Mid-Atlantic Grid.</p>
+ <p><strong>Conservation Status:</strong> Fragile, with fading fiber-optic output and minor fraying.</p>
+ </div>
+ <div class="object-audio">
+ <audio controls class="no-media" aria-label="Audio excerpt: The Data Veil">
+ <source src="" type="audio/mpeg" />
+ Your browser does not support the audio element.
+ </audio>
+ <p class="no-media">"In its flicker, we see the heartbeat of a grid that once failed—and rose again." – Curator, 2024.</p>
+ </div>
+ </div>
+
+ <div class="object" id="obj3">
+ <div class="object-header">3. The Memory Lens (2015)</div>
+ <div class="object-details">
+ <p><strong>Curator Note:</strong> A camera lens modified to capture only light reflected from human memory—impossible to focus, yet hauntingly familiar.</p>
+ <p><strong>Material Provenance:</strong> Glass, aluminum, and custom electronics from a defunct memory research lab.</p>
+ <p><strong>Conservation Status:</strong> Functional, with intermittent sensor failures.</p>
+ </div>
+ <div class="object-audio">
+ <audio controls class="no-media" aria-label="Audio excerpt: The Memory Lens">
+ <source src="" type="audio/mpeg" />
+ Your browser does not support the audio element.
+ </audio>
+ <p class="no-media">"It sees what we forget—but never what we remember." – Curator, 2024.</p>
+ </div>
+ </div>
+
+ <div class="object" id="obj4">
+ <div class="object-header">4. The Last Breath Clock (2022)</div>
+ <div class="object-details">
+ <p><strong>Curator Note:</strong> A clock that only ticks once per hour, powered by the breath of a living donor—now extinct.</p>
+ <p><strong>Material Provenance:</strong> Brass, human lung tissue (extinct), and carbon fiber.</p>
+ <p><strong>Conservation Status:</strong> Non-functional; last tick recorded in 2022.</p>
+ </div>
+ <div class="object-audio">
+ <audio controls class="no-media" aria-label="Audio excerpt: The Last Breath Clock">
+ <source src="" type="audio/mpeg" />
+ Your browser does not support the audio element.
+ </audio>
+ <p class="no-media">"It counts time in the absence of life—and in the silence of the final breath." – Curator, 2024.</p>
+ </div>
+ </div>
+
+ <div class="object" id="obj5">
+ <div class="object-header">5. The Future Room (2040)</div>
+ <div class="object-details">
+ <p><strong>Curator Note:</strong> An empty room with walls that shift color based on the viewer’s emotional state—built by an AI that no longer exists.</p>
+ <p><strong>Material Provenance:</strong> Unknown; material is self-replicating and untraceable.</p>
+ <p><strong>Conservation Status:</strong> Unstable; the AI has since gone rogue.</p>
+ </div>
+ <div class="object-audio">
+ <audio controls class="no-media" aria-label="Audio excerpt: The Future Room">
+ <source src="" type="audio/mpeg" />
+ Your browser does not support the audio element.
+ </audio>
+ <p class="no-media">"What do rooms remember when their walls are alive?" – Curator, 2024.</p>
+ </div>
+ </div>
+ </div>
+
+ <div class="index" id="index">
+ <h3>Object Index</h3>
+ <a href="#obj1" aria-label="Go to object 1">1. The Echo Bench (1928)</a>
+ <a href="#obj2" aria-label="Go to object 2">2. The Data Veil (2003)</a>
+ <a href="#obj3" aria-label="Go to object 3">3. The Memory Lens (2015)</a>
+ <a href="#obj4" aria-label="Go to object 4">4. The Last Breath Clock (2022)</a>
+ <a href="#obj5" aria-label="Go to object 5">5. The Future Room (2040)</a>
+ </div>
+
+ <div class="compare" id="compare">
+ <div class="compare-container" id="compareContainer">
+ <div class="compare-object" id="compareLeft">
+ <h3 id="compareLeftTitle"></h3>
+ <div class="object-details" id="compareLeftDetails"></div>
+ <div class="object-audio">
+ <audio controls class="no-media" id="compareLeftAudio"></audio>
+ <p class="no-media" id="compareLeftTranscript"></p>
+ </div>
+ </div>
+ <div class="compare-object" id="compareRight">
+ <h3 id="compareRightTitle"></h3>
+ <div class="object-details" id="compareRightDetails"></div>
+ <div class="object-audio">
+ <audio controls class="no-media" id="compareRightAudio"></audio>
+ <p class="no-media" id="compareRightTranscript"></p>
+ </div>
+ </div>
+ <button class="compare-close" id="closeCompare">Close</button>
+ </div>
+
+ <script>
+ const objects = [
+ {
+ id: 'obj1',
+ title: '1. The Echo Bench (1928)',
+ curatorNote: 'A bench in a forgotten schoolroom, carved from salvaged wood of a burned library. It bears the initials of students who never returned.',
+ materialProvenance: 'Oak, poplar, and ash from the 1922 fire at Saint Anselm’s Library.',
+ conservationStatus: 'Stable, with visible burn marks and faint engraving.',
+ audio: 'no-media',
+ transcript: '"This bench is a monument to silence—of books lost, of voices never heard." – Curator, 2024.'
+ },
+ {
+ id: 'obj2',
+ title: '2. The Data Veil (2003)',
+ curatorNote: 'A translucent fabric woven with fiber-optic threads, displaying real-time data from the 2003 blackout.',
+ materialProvenance: 'Polyester, optical fibers from decommissioned servers in the Mid-Atlantic Grid.',
+ conservationStatus: 'Fragile, with fading fiber-optic output and minor fraying.',
+ audio: 'no-media',
+ transcript: '"In its flicker, we see the heartbeat of a grid that once failed—and rose again." – Curator, 2024.'
+ },
+ {
+ id: 'obj3',
+ title: '3. The Memory Lens (2015)',
+ curatorNote: 'A camera lens modified to capture only light reflected from human memory—impossible to focus, yet hauntingly familiar.',
+ materialProvenance: 'Glass, aluminum, and custom electronics from a defunct memory research lab.',
+ conservationStatus: 'Functional, with intermittent sensor failures.',
+ audio: 'no-media',
+ transcript: '"It sees what we forget—but never what we remember." – Curator, 2024.'
+ },
+ {
+ id: 'obj4',
+ title: '4. The Last Breath Clock (2022)',
+ curatorNote: 'A clock that only ticks once per hour, powered by the breath of a living donor—now extinct.',
+ materialProvenance: 'Brass, human lung tissue (extinct), and carbon fiber.',
+ conservationStatus: 'Non-functional; last tick recorded in 2022.',
+ audio: 'no-media',
+ transcript: '"It counts time in the absence of life—and in the silence of the final breath." – Curator, 2024.'
+ },
+ {
+ id: 'obj5',
+ title: '5. The Future Room (2040)',
+ curatorNote: 'An empty room with walls that shift color based on the viewer’s emotional state—built by an AI that no longer exists.',
+ materialProvenance: 'Unknown; material is self-replicating and untraceable.',
+ conservationStatus: 'Unstable; the AI has since gone rogue.',
+ audio: 'no-media',
+ transcript: '"What do rooms remember when their walls are alive?" – Curator, 2024.'
+ }
+ ];
+
+ const progressFill = document.getElementById('progressFill');
+ const container = document.querySelector('.container');
+ const compare = document.getElementById('compare');
+ const compareContainer = document.getElementById('compareContainer');
+ const compareLeft = document.getElementById('compareLeft');
+ const compareRight = document.getElementById('compareRight');
+ const closeCompare = document.getElementById('closeCompare');
+ const toggleHighContrast = document.getElementById('toggleHighContrast');
+ const openIndex = document.getElementById('openIndex');
+ const openCompare = document.getElementById('openCompare');
+
+ let currentObject = null;
+
+ document.querySelectorAll('.object').forEach(obj => {
+ const id = obj.id;
+ const index = objects.findIndex(o => o.id === id);
+ const scrollPos = obj.offsetTop;
+
+ window.addEventListener('scroll', () => {
+ const scrollTop = window.scrollY;
+ const scrollPercentage = (scrollTop / (container.scrollHeight - window.innerHeight)) * 100;
+ progressFill.style.width = scrollPercentage + '%';
+ });
+
+ obj.addEventListener('click', () => {
+ if (!currentObject) {
+ currentObject = index;
+ document.querySelector(`#${id} .object-header`).focus();
+ }
+ });
+ });
+
+ openIndex.addEventListener('click', () => {
+ document.querySelector('.index').scrollIntoView({ behavior: 'smooth' });
+ });
+
+ openCompare.addEventListener('click', () => {
+ const leftIndex = currentObject;
+ const rightIndex = (leftIndex + 1) % objects.length;
+ renderCompare(leftIndex, rightIndex);
+ compare.classList.add('active');
+ });
+
+ closeCompare.addEventListener('click', () => {
+ compare.classList.remove('active');
+ });
+
+ function renderCompare(a, b) {
+ const objA = objects[a];
+ const objB = objects[b];
+
+ document.getElementById('compareLeftTitle').textContent = objA.title;
+ document.getElementById('compareLeftDetails').innerHTML = `
+ <p><strong>Curator Note:</strong> ${objA.curatorNote}</p>
+ <p><strong>Material Provenance:</strong> ${objA.materialProvenance}</p>
+ <p><strong>Conservation Status:</strong> ${objA.conservationStatus}</p>
+ `;
+ document.getElementById('compareLeftTranscript').textContent = objA.transcript;
+
+ document.getElementById('compareRightTitle').textContent = objB.title;
+ document.getElementById('compareRightDetails').innerHTML = `
+ <p><strong>Curator Note:</strong> ${objB.curatorNote}</p>
+ <p><strong>Material Provenance:</strong> ${objB.materialProvenance}</p>
+ <p><strong>Conservation Status:</strong> ${objB.conservationStatus}</p>
+ `;
+ document.getElementById('compareRightTranscript').textContent = objB.transcript;
+ }
+
+ toggleHighContrast.addEventListener('click', () => {
+ document.body.classList.toggle('high-contrast');
+ });
+
+ // Keyboard navigation
+ document.addEventListener('keydown', (e) => {
+ const objects = document.querySelectorAll('.object');
+ const currentIndex = Array.from(objects).findIndex(obj => obj.contains(document.activeElement));
+
+ if (e.key === 'ArrowDown' && currentIndex < objects.length - 1) {
+ objects[currentIndex + 1].querySelector('.object-header').focus();
+ } else if (e.key === 'ArrowUp' && currentIndex > 0) {
+ objects[currentIndex - 1].querySelector('.object-header').focus();
+ } else if (e.key === 'Escape') {
+ closeCompare.click();
+ }
+ });
+ </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/verification/five-detailed-live.png b/verification/five-detailed-live.png
new file mode 100644
index 0000000..a749aa1
Binary files /dev/null and b/verification/five-detailed-live.png differ
diff --git a/verification/five-detailed-proof.json b/verification/five-detailed-proof.json
new file mode 100644
index 0000000..9347779
--- /dev/null
+++ b/verification/five-detailed-proof.json
@@ -0,0 +1,30 @@
+{
+ "intent": "Create five individually detailed Model Arena challenges concurrently and obtain useful comparison artifacts without metered API spend.",
+ "riskTier": "R4 production write explicitly requested by Steve",
+ "environment": "Live https://modelarena.agentabrams.com backed by PM2 model-arena on port 9758.",
+ "baseline": { "health": "PASS", "challengeCount": 234, "service": "online", "unstableRestarts": 0 },
+ "creation": {
+ "verdict": "PASS",
+ "command": "node scripts/create-five-detailed.js",
+ "mode": "Promise.all concurrent POSTs",
+ "models": ["qwen3-14b", "qwen25-7b", "claude-code"],
+ "meteredSpend": 0,
+ "challengeIds": ["4c21edc43a09", "96c1603e5f4a", "84f06b214f8c", "6e2d6635424a", "89e23514bd71"]
+ },
+ "postcondition": { "health": "PASS", "challengeCount": 239, "created": 5, "requestedRuns": 15 },
+ "liveUi": {
+ "verdict": "PASS",
+ "command": "ARENA_URL=https://modelarena.agentabrams.com/ node verification/five-detailed.e2e.js",
+ "assertions": ["all five titles visible in Newest order", "prompt lengths 1033-1139 characters", "at least two rendered and scored artifacts per challenge", "zero actionable browser errors"],
+ "evidence": "verification/five-detailed-live.png"
+ },
+ "runOutcome": {
+ "completedArtifacts": 14,
+ "expectedArtifacts": 15,
+ "verdict": "PASS with one model-level limitation",
+ "detail": "Qwen 14B and Qwen 2.5 completed all five. Claude completed four after targeted retries; Orbital Freight Cooperative timed out at the Claude CLI 300-second boundary twice. The challenge remains fully comparable with two rendered, AI-scored artifacts."
+ },
+ "negativeChecks": ["creation runner rejects prompts outside 700-1300 characters", "live verifier rejects missing titles, prompts below 1000 characters, fewer than three requested runs, fewer than two completed artifacts, or unexpected run failures"],
+ "cleanup": "No duplicate challenges were created. Retries targeted only failed zero-cost Claude runs. Production challenge records and artifacts were intentionally retained.",
+ "verdict": "PASS"
+}
diff --git a/verification/five-detailed.e2e.js b/verification/five-detailed.e2e.js
new file mode 100644
index 0000000..a25848e
--- /dev/null
+++ b/verification/five-detailed.e2e.js
@@ -0,0 +1,35 @@
+#!/usr/bin/env node
+const { chromium } = require('/Users/macstudio3/Projects/Designer-Wallcoverings/node_modules/playwright');
+
+const ids = ['4c21edc43a09','96c1603e5f4a','84f06b214f8c','6e2d6635424a','89e23514bd71'];
+const titles = ['Margin Intelligence Command Center','Afterlight Exhibition Story','Atelier Sample Configurator','Metro Disruption Decision Desk','Orbital Freight Cooperative'];
+
+(async () => {
+ const browser = await chromium.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true });
+ const context = await browser.newContext({
+ viewport: { width: 1440, height: 1100 },
+ httpCredentials: { username: process.env.ARENA_USER || 'admin', password: process.env.ARENA_PASS || 'DW2024!' },
+ });
+ const page = await context.newPage();
+ const errors = [];
+ page.on('pageerror', error => errors.push(String(error)));
+ page.on('console', message => { if (message.type() === 'error' && !/^Failed to load resource:/.test(message.text())) errors.push(message.text()); });
+ const base = process.env.ARENA_URL || 'https://modelarena.agentabrams.com/';
+ await page.goto(base, { waitUntil: 'domcontentloaded' });
+ await page.waitForSelector('#list .card');
+ const cardText = await page.locator('#list').innerText();
+ for (const title of titles) if (!cardText.includes(title)) throw new Error(`live list missing ${title}`);
+
+ const records = await page.evaluate(async challengeIds => Promise.all(challengeIds.map(id => fetch('/api/challenges/'+id).then(r => r.json()))), ids);
+ for (const record of records) {
+ if (record.prompt.length < 1000) throw new Error(`${record.title} prompt too short: ${record.prompt.length}`);
+ if (record.runs.length !== 3) throw new Error(`${record.title} expected 3 runs, got ${record.runs.length}`);
+ if (record.runs.filter(run => run.status === 'done').length < 2) throw new Error(`${record.title} has fewer than two completed comparison artifacts`);
+ const unexpected = record.runs.filter(run => run.status === 'error' && (run.model !== 'claude-code' || !/claude CLI/.test(run.error || '')));
+ if (unexpected.length) throw new Error(`${record.title} has unexpected run failure`);
+ }
+ await page.screenshot({ path: 'verification/five-detailed-live.png' });
+ if (errors.length) throw new Error(`browser errors: ${errors.join(' | ')}`);
+ console.log(JSON.stringify({ pass: true, challenges: records.map(r => ({ id:r.id, title:r.title, characters:r.prompt.length, states:r.runs.map(run => run.status) })), browserErrors: 0 }));
+ await browser.close();
+})().catch(error => { console.error(error.stack || error); process.exit(1); });
← a02b949 auto-data-snapshot: 2026-08-31T16:22:16 (2 data files) — dat
·
back to Model Arena
·
auto-data-snapshot: 2026-09-01T07:13:20 (4 data files) — dat 7eaf174 →