thin-channel / index.html
Laborator's picture
Rebuild as interactive static demo: live reveal-schedule lever drives the Brier curve; correct dataset label to ForecastBench (CC BY-SA 4.0)
bc3caba verified
Raw
History Blame Contribute Delete
29.4 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Thin Channel · Second Loop</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@1,700&family=Inter:wght@400;500;700;800&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet" />
<script src="https://cdn.plot.ly/plotly-2.35.2.min.js" charset="utf-8"></script>
<style>
:root{
--bg:#000000; --bg-card:#0A0A0A; --bg-elev:#141414;
--border:#1F1F1F; --border-strong:#2A2A2A;
--text:#FFFFFF; --text-mute:#A8A8A8; --text-dim:#6B6B6B;
--gold:#D4AF37; --gold-hi:#E8C84A;
--red:#FF2A2A; --green:#1FD160;
--mono:'JetBrains Mono',ui-monospace,monospace;
--serif:'Playfair Display',serif;
--sans:'Inter',system-ui,sans-serif;
}
*{box-sizing:border-box;}
html,body{margin:0;background:var(--bg);color:var(--text);font-family:var(--sans);}
.wrap{max-width:1180px;margin:0 auto;padding:22px 24px 64px 24px;}
a{color:var(--gold);text-decoration:none;border-bottom:1px dotted var(--gold);}
a:hover{color:var(--gold-hi);}
/* ---------- header ---------- */
.head{border:1px solid var(--border-strong);border-radius:14px;
padding:26px 30px 22px 30px;background:linear-gradient(180deg,#0A0A0A 0%,#050505 100%);}
.head-top{display:flex;align-items:flex-start;justify-content:space-between;gap:24px;
padding-bottom:16px;border-bottom:1px solid var(--border);margin-bottom:16px;}
.head-brand{display:flex;align-items:center;gap:16px;}
.head-icon{width:52px;height:52px;border:1px solid var(--border-strong);border-radius:12px;
display:flex;align-items:center;justify-content:center;
background:radial-gradient(60% 60% at 50% 40%,#1A1A1A 0%,#050505 100%);}
.head-title{font-family:var(--serif);font-style:italic;font-weight:700;font-size:34px;
color:var(--text);line-height:1;margin:2px 0 7px 0;}
.head-subtitle{font-family:var(--mono);font-size:11.5px;letter-spacing:.14em;
color:var(--text-mute);text-transform:uppercase;}
.head-right{text-align:right;white-space:nowrap;}
.submitted-label{font-family:var(--mono);font-size:10px;letter-spacing:.22em;
color:var(--text-dim);text-transform:uppercase;display:block;margin-bottom:4px;}
.submitted-name{font-family:var(--serif);font-style:italic;font-weight:700;font-size:20px;color:var(--text);}
.status-pill{display:inline-flex;align-items:center;gap:6px;margin-top:10px;padding:5px 12px;
border-radius:999px;background:rgba(31,209,96,.08);border:1px solid rgba(31,209,96,.5);
font-family:var(--mono);font-size:10px;letter-spacing:.18em;color:var(--green);text-transform:uppercase;}
.status-dot{width:7px;height:7px;border-radius:50%;background:var(--green);}
.head-meta{display:grid;grid-template-columns:repeat(4,1fr);gap:12px 24px;}
.head-meta .item{display:flex;flex-direction:column;gap:3px;}
.head-meta .k{font-family:var(--mono);font-size:9.5px;letter-spacing:.2em;color:var(--text-dim);text-transform:uppercase;}
.head-meta .v{font-family:var(--sans);font-size:13.5px;font-weight:700;color:var(--text);}
/* ---------- three honest badges ---------- */
.badges{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:18px 0 4px 0;}
.badge{background:var(--bg-card);border:1px solid var(--border-strong);border-radius:12px;padding:16px 20px;}
.badge .label{font-family:var(--mono);font-size:9.5px;letter-spacing:.2em;color:var(--text-dim);
text-transform:uppercase;margin-bottom:8px;}
.badge .value{font-family:var(--serif);font-style:italic;font-weight:700;font-size:26px;color:var(--text);}
.badge .sub{font-family:var(--mono);font-size:10px;letter-spacing:.1em;color:var(--text-mute);
text-transform:uppercase;margin-top:4px;}
.badge.green{border-color:rgba(31,209,96,.5);}
.badge.red{border-color:rgba(255,42,42,.5);}
/* ---------- exhibit (the museum lever) ---------- */
.exhibit{margin-top:18px;border:1px solid var(--border-strong);border-radius:14px;
background:var(--bg-card);padding:22px 24px 26px 24px;}
.exhibit-title{font-family:var(--serif);font-style:italic;font-weight:700;font-size:22px;margin:0 0 2px 0;}
.exhibit-lede{font-family:var(--mono);font-size:11px;letter-spacing:.1em;color:var(--text-mute);
text-transform:uppercase;margin-bottom:18px;}
.exhibit-grid{display:grid;grid-template-columns:1fr 300px;gap:22px;align-items:stretch;}
#chart{width:100%;height:430px;}
.readout{border:1px solid var(--border-strong);border-radius:12px;background:#050505;
padding:20px 22px;display:flex;flex-direction:column;gap:2px;transition:border-color 160ms ease;}
.readout.collapsed{border-color:rgba(255,42,42,.6);background:rgba(255,42,42,.04);}
.readout .ro-label{font-family:var(--mono);font-size:9.5px;letter-spacing:.2em;color:var(--text-dim);text-transform:uppercase;}
.readout .ro-name{font-family:var(--serif);font-style:italic;font-weight:700;font-size:30px;line-height:1.05;margin:4px 0 2px 0;}
.readout .ro-period{font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;color:var(--text-mute);text-transform:uppercase;}
.ro-brier-label{font-family:var(--mono);font-size:9.5px;letter-spacing:.2em;color:var(--text-dim);
text-transform:uppercase;margin-top:20px;}
.ro-brier{font-family:var(--serif);font-style:italic;font-weight:700;font-size:56px;line-height:1;
color:var(--green);transition:color 160ms ease;}
.readout.collapsed .ro-brier{color:var(--red);}
.ro-tag{align-self:flex-start;margin-top:6px;padding:4px 10px;border-radius:999px;
font-family:var(--mono);font-size:9.5px;letter-spacing:.18em;text-transform:uppercase;
background:rgba(255,42,42,.1);border:1px solid rgba(255,42,42,.55);color:var(--red);
visibility:hidden;}
.readout.collapsed .ro-tag{visibility:visible;}
.ro-aux{margin-top:18px;border-top:1px solid var(--border);padding-top:14px;display:flex;flex-direction:column;gap:9px;}
.ro-aux .row{display:flex;align-items:baseline;justify-content:space-between;gap:10px;}
.ro-aux .rk{font-family:var(--mono);font-size:10px;letter-spacing:.06em;color:var(--text-dim);text-transform:uppercase;}
.ro-aux .rv{font-family:var(--mono);font-size:13px;font-weight:700;}
.rv.gold{color:var(--gold);} .rv.red{color:var(--red);} .rv.green{color:var(--green);} .rv.mute{color:var(--text-mute);}
/* ---------- slider (the lever) ---------- */
.lever{margin-top:24px;}
.lever-head{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:14px;}
.lever-head .t{font-family:var(--mono);font-size:11px;letter-spacing:.18em;color:var(--text);text-transform:uppercase;}
.lever-head .h{font-family:var(--mono);font-size:10px;letter-spacing:.1em;color:var(--text-dim);text-transform:uppercase;}
.slider-area{position:relative;padding:0 4px;}
input[type=range].lever-input{
-webkit-appearance:none;appearance:none;width:100%;height:6px;border-radius:999px;margin:0;
background:linear-gradient(90deg,var(--green) 0%,var(--gold) 78%,var(--red) 100%);
outline:none;cursor:pointer;}
input[type=range].lever-input::-webkit-slider-thumb{
-webkit-appearance:none;appearance:none;width:26px;height:26px;border-radius:50%;
background:#0A0A0A;border:2px solid var(--gold);box-shadow:0 0 0 4px rgba(212,175,55,.18);
cursor:grab;margin-top:-10px;transition:border-color 120ms ease,box-shadow 120ms ease;}
input[type=range].lever-input::-webkit-slider-thumb:active{cursor:grabbing;}
input[type=range].lever-input::-moz-range-thumb{width:26px;height:26px;border-radius:50%;
background:#0A0A0A;border:2px solid var(--gold);box-shadow:0 0 0 4px rgba(212,175,55,.18);cursor:grab;}
input[type=range].lever-input::-moz-range-track{height:6px;border-radius:999px;background:transparent;}
input[type=range].lever-input.at-never::-webkit-slider-thumb{border-color:var(--red);box-shadow:0 0 0 4px rgba(255,42,42,.22);}
input[type=range].lever-input.at-never::-moz-range-thumb{border-color:var(--red);box-shadow:0 0 0 4px rgba(255,42,42,.22);}
input[type=range].lever-input:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 4px rgba(212,175,55,.20),0 0 0 7px var(--gold-hi);}
input[type=range].lever-input:focus-visible::-moz-range-thumb{box-shadow:0 0 0 4px rgba(212,175,55,.20),0 0 0 7px var(--gold-hi);}
.ticks{position:relative;height:10px;margin-top:9px;}
.tick{position:absolute;top:0;width:1px;height:6px;background:var(--border-strong);transform:translateX(-50%);}
.tick.on{background:var(--gold);height:9px;}
.ticklabels{position:relative;height:30px;margin-top:2px;}
.tlabel{position:absolute;top:0;font-family:var(--mono);font-size:9.5px;letter-spacing:.02em;
color:var(--text-dim);white-space:nowrap;line-height:1.15;text-align:center;cursor:pointer;
transition:color 120ms ease;}
.tlabel.on{color:var(--text);font-weight:700;}
.tlabel.never.on{color:var(--red);}
.lever-foot{margin-top:18px;display:flex;justify-content:space-between;
font-family:var(--mono);font-size:10px;letter-spacing:.12em;color:var(--text-dim);text-transform:uppercase;}
/* ---------- ablation strip ---------- */
.ablations{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:22px 0;}
.abl{background:var(--bg-card);border:1px solid var(--border-strong);border-radius:12px;padding:18px 20px;text-align:center;}
.abl .key{font-family:var(--mono);font-size:10px;letter-spacing:.16em;color:var(--text-dim);text-transform:uppercase;margin-bottom:8px;}
.abl .val{font-family:var(--serif);font-style:italic;font-weight:700;font-size:30px;color:var(--text);}
.abl .lbl{font-family:var(--mono);font-size:10px;letter-spacing:.1em;color:var(--text-mute);text-transform:uppercase;margin-top:4px;}
.abl.acc .val{color:var(--green);}
/* ---------- data table ---------- */
.table-wrap{background:var(--bg-card);border:1px solid var(--border-strong);border-radius:14px;padding:20px 22px;}
.table-cap{font-family:var(--mono);font-size:10px;letter-spacing:.18em;color:var(--text-dim);
text-transform:uppercase;margin-bottom:12px;}
.schedule-table{width:100%;border-collapse:collapse;font-family:var(--mono);font-size:13px;}
.schedule-table th{font-family:var(--mono);font-size:9.5px;letter-spacing:.18em;color:var(--text-dim);
text-transform:uppercase;text-align:right;padding:8px 12px;border-bottom:1px solid var(--border-strong);}
.schedule-table th:first-child{text-align:left;}
.schedule-table td{padding:9px 12px;border-bottom:1px solid var(--border);text-align:right;}
.schedule-table tr{cursor:pointer;}
.schedule-table tr.active{background:rgba(212,175,55,.07);}
.td-name{text-align:left!important;font-family:var(--serif);font-style:italic;font-weight:700;font-size:15px;color:var(--text);}
.td-mute{color:var(--text-mute);}
.td-c{color:var(--green);font-weight:700;} .td-b{color:var(--gold);font-weight:700;} .td-a{color:var(--red);font-weight:700;opacity:.78;}
.never-row{background:rgba(255,42,42,.06);}
.never-row .td-c{color:var(--red)!important;}
/* ---------- about ---------- */
.foot{margin-top:24px;padding:22px 24px;border:1px solid var(--border);border-radius:12px;background:var(--bg-card);}
.foot .ftitle{font-family:var(--mono);font-size:10px;letter-spacing:.2em;color:var(--text-dim);text-transform:uppercase;margin-bottom:10px;}
.foot .body{font-family:var(--sans);font-size:13px;color:var(--text-mute);line-height:1.62;}
.foot code{font-family:var(--mono);font-size:12px;color:var(--text);background:var(--bg-elev);
padding:1px 5px;border-radius:4px;border:1px solid var(--border);}
.attrib{margin-top:14px;padding-top:14px;border-top:1px solid var(--border);
font-family:var(--mono);font-size:11px;letter-spacing:.04em;color:var(--text-dim);line-height:1.7;}
@media(max-width:860px){
.exhibit-grid{grid-template-columns:1fr;}
.head-meta{grid-template-columns:repeat(2,1fr);}
.badges,.ablations{grid-template-columns:1fr;}
.head-top{flex-direction:column;}
.head-right{text-align:left;}
}
</style>
</head>
<body>
<main class="wrap">
<!-- HEADER -->
<header class="head">
<div class="head-top">
<div class="head-brand">
<div class="head-icon">
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" aria-hidden="true">
<rect x="3.5" y="3.5" width="21" height="21" rx="4" stroke="#2A2A2A"/>
<line x1="14" y1="5.5" x2="14" y2="22.5" stroke="#D4AF37" stroke-width="1.6"/>
<circle cx="14" cy="9" r="1.3" fill="#1FD160"/>
<circle cx="14" cy="14" r="1.3" fill="#1FD160"/>
<circle cx="14" cy="19" r="1.3" fill="#1FD160"/>
</svg>
</div>
<div>
<div class="head-title">Thin Channel</div>
<div class="head-subtitle">How rarely external truth can arrive before calibration collapses</div>
</div>
</div>
<div class="head-right">
<span class="submitted-label">Submitted by</span>
<div class="submitted-name" id="author"></div>
<div class="status-pill"><span class="status-dot"></span>Static demo · no live model</div>
</div>
</div>
<div class="head-meta">
<div class="item"><span class="k">Subject model</span><span class="v">Qwen2.5-3B-Instruct (frozen)</span></div>
<div class="item"><span class="k">Dataset</span><span class="v" id="m-dataset">ForecastBench</span></div>
<div class="item"><span class="k">Hold-out</span><span class="v" id="m-holdout">N = 330</span></div>
<div class="item"><span class="k">Calibration</span><span class="v">per-topic Beta · K = 10</span></div>
</div>
</header>
<!-- THREE HONEST BADGES -->
<section class="badges">
<div class="badge green">
<div class="label">Corridor — any finite schedule</div>
<div class="value" id="b-corridor">0.205–0.220</div>
<div class="sub">Brier on hold-out</div>
</div>
<div class="badge red">
<div class="label">Never — no signal</div>
<div class="value" id="b-never">0.298</div>
<div class="sub">collapses to raw 3B</div>
</div>
<div class="badge">
<div class="label">Cliff direction</div>
<div class="value">Binary</div>
<div class="sub">channel = volume, not frequency</div>
</div>
</section>
<!-- EXHIBIT: the museum lever -->
<section class="exhibit">
<div class="exhibit-title">How often does the system get to check reality?</div>
<div class="exhibit-lede">Move the lever from “every day” to “never” and watch the Brier curve</div>
<div class="exhibit-grid">
<div id="chart"></div>
<div class="readout" id="readout">
<div class="ro-label">Current check interval</div>
<div class="ro-name" id="ro-name">day</div>
<div class="ro-period" id="ro-period">every 1 day · 1316 outcomes seen</div>
<div class="ro-brier-label">Brier on hold-out · lower is better</div>
<div class="ro-brier" id="ro-brier">0.205</div>
<div class="ro-tag" id="ro-tag">Collapse — back to raw 3B</div>
<div class="ro-aux">
<div class="row"><span class="rk">(b) base-rate only</span><span class="rv gold" id="ro-b">0.210</span></div>
<div class="row"><span class="rk">(a) raw 3B only</span><span class="rv red" id="ro-a">0.298</span></div>
<div class="row"><span class="rk">improvement vs raw 3B</span><span class="rv" id="ro-delta">−0.093</span></div>
</div>
</div>
</div>
<div class="lever">
<div class="lever-head">
<span class="t">The lever — reveal schedule</span>
<span class="h">← more often &nbsp;·&nbsp; less often →</span>
</div>
<div class="slider-area">
<input id="slider" class="lever-input" type="range" min="0" max="10" step="1" value="0"
aria-label="How often the system checks reality" />
<div class="ticks" id="ticks"></div>
<div class="ticklabels" id="ticklabels"></div>
</div>
<div class="lever-foot">
<span>Daily contact</span>
<span>One annual debrief</span>
<span>Never</span>
</div>
</div>
</section>
<!-- ABLATION STRIP -->
<section class="ablations">
<div class="abl">
<div class="key">(a) raw 3B only</div>
<div class="val" id="abl-a">0.298</div>
<div class="lbl">useless as a forecaster</div>
</div>
<div class="abl acc">
<div class="key">(b) topic base-rate only</div>
<div class="val" id="abl-b">0.189</div>
<div class="lbl">no model needed</div>
</div>
<div class="abl acc">
<div class="key">(c) mix (a + b)</div>
<div class="val" id="abl-c">0.189</div>
<div class="lbl">c − b = <span id="abl-d">−0.0001</span></div>
</div>
</section>
<!-- DATA TABLE -->
<section class="table-wrap">
<div class="table-cap">All 11 schedules — verbatim from the run (click a row to move the lever)</div>
<table class="schedule-table">
<thead><tr>
<th>Frequency</th><th>Period</th><th>Revealed</th>
<th>(c) mix</th><th>(b) base only</th><th>(a) raw 3B</th>
</tr></thead>
<tbody id="tbody"></tbody>
</table>
</section>
<!-- ABOUT -->
<footer class="foot">
<div class="ftitle">About this demo</div>
<div class="body">
1,646 resolved binary forecasting questions are streamed through a simulated calendar:
a frozen Qwen2.5-3B-Instruct emits a raw <i>P(YES)</i> at each question’s freeze date,
the true outcome is hidden and revealed only on a schedule, and a per-topic Beta
calibration (9 topics, K = 10) consumes each revealed outcome. The same stream is run
independently through 11 reveal schedules, from “every day” to “never”. The metric is
Brier score on the held-out last 20% (N = 330); lower is better.
The result is a binary cliff: every finite schedule holds the corridor at
<b id="t-corridor">0.205–0.220</b>, while zero contact collapses to
<b id="t-never">0.298</b> — bit-for-bit the raw 3B level.
<br/><br/>
Read the honest caveat alongside it: the raw 3B is useless as a forecaster on its own
(Brier <span id="t-wide">0.354</span> on a wide baseline, worse than “always NO”). The (c) ≈ (b) ablation shows
the entire calibration gain comes from picking up <b>per-topic base rates</b>, not from
the model learning to predict the future. What the lever measures is the rate at which
unforgeable signal accumulates — not forecasting skill.
<br/><br/>
No live model runs in this Space. Every number, the curve and the table are verbatim
outputs of the original experimental run, bundled into <code>data.json</code>. Full code,
raw per-schedule results and methodology:
<a id="repo-link" href="https://github.com/SergheiBrinza/thin-channel" target="_blank" rel="noopener">github.com/SergheiBrinza/thin-channel</a>.
</div>
<div class="attrib" id="attrib">
Dataset: ForecastBench (Karger, Bastani, Yueh-Han, Jacobs, Halawi, Zhang &amp; Tetlock,
<i>ForecastBench: A Dynamic Benchmark of AI Forecasting Capabilities</i>, ICLR 2025,
arXiv:2409.19839), licensed CC BY-SA 4.0 — source:
<a href="https://huggingface.co/datasets/forecastingresearch/forecastbench-datasets" target="_blank" rel="noopener">forecastingresearch/forecastbench-datasets</a>.
No raw market questions or per-question outcomes are redistributed here — only aggregate
Brier results. Demo code: MIT.
</div>
</footer>
</main>
<script>
const COL={bg:'#000000',card:'#0A0A0A',border:'#1F1F1F',borderS:'#2A2A2A',
text:'#FFFFFF',mute:'#A8A8A8',dim:'#6B6B6B',gold:'#D4AF37',red:'#FF2A2A',green:'#1FD160'};
const f3=v=>v.toFixed(3);
const signed=v=>(v>0?'+':v<0?'−':'')+Math.abs(v).toFixed(3);
const signed4=v=>(v>0?'+':v<0?'−':'')+Math.abs(v).toFixed(4);
let S=[], NAMES=[], FINAL=[], BASE=[], RAW=[], REVEAL=[], FLOOR=0, CORR_MIN=0, CORR_MAX=0, cur=0;
const PLOT_MARGIN={l:54,r:18,t:14,b:30};
fetch('data.json').then(r=>r.json()).then(D=>{
const meta=D.meta||{};
S=D.schedules;
NAMES=S.map(s=>s.name);
FINAL=S.map(s=>s.holdout_brier_final);
BASE=S.map(s=>s.holdout_brier_base_only);
RAW=S.map(s=>s.holdout_brier_raw);
REVEAL=S.map(s=>Math.round(s.total_revealed));
const finite=S.filter(s=>s.period_days!==null);
CORR_MIN=Math.min(...finite.map(s=>s.holdout_brier_final));
CORR_MAX=Math.max(...finite.map(s=>s.holdout_brier_final));
FLOOR=Math.min(...finite.map(s=>s.holdout_brier_base_only));
// header / badges / about — all driven by data
document.getElementById('author').textContent=meta.author||'—';
document.getElementById('m-dataset').textContent=meta.dataset_short||'ForecastBench';
document.getElementById('m-holdout').textContent='N = '+(meta.n_holdout||330);
const corrTxt=f3(CORR_MIN)+'–'+f3(CORR_MAX);
document.getElementById('b-corridor').textContent=corrTxt;
document.getElementById('t-corridor').textContent=corrTxt;
const neverV=S.find(s=>s.period_days===null).holdout_brier_final;
document.getElementById('b-never').textContent=f3(neverV);
document.getElementById('t-never').textContent=f3(neverV);
const repo=meta.repo||'https://github.com/SergheiBrinza/thin-channel';
const rl=document.getElementById('repo-link'); rl.href=repo;
rl.textContent=repo.replace(/^https?:\/\//,'');
const abl=D.ablations||{};
document.getElementById('abl-a').textContent=f3(abl.brier_a_raw);
document.getElementById('abl-b').textContent=f3(abl.brier_b_base_only);
document.getElementById('abl-c').textContent=f3(abl.brier_c_mix);
document.getElementById('abl-d').textContent=signed4(abl.delta_c_vs_b);
if(D.wide_baseline_overall&&typeof D.wide_baseline_overall.brier==='number'){
const tw=document.getElementById('t-wide'); if(tw) tw.textContent=f3(D.wide_baseline_overall.brier);
}
buildTicks();
buildTable();
drawChart();
// optional deep-link / screenshot preset: ?i=10
const q=new URLSearchParams(location.search);
const i=parseInt(q.get('i'),10);
const start=(Number.isFinite(i)&&i>=0&&i<=10)?i:0;
document.getElementById('slider').value=start;
document.getElementById('slider').addEventListener('input',e=>setPos(+e.target.value));
setPos(start);
}).catch(err=>{
console.error('thin-channel: could not load data.json —', err);
const ex=document.querySelector('.exhibit');
if(ex){
const b=document.createElement('div');
b.style.cssText='border:1px solid rgba(255,42,42,.5);background:rgba(255,42,42,.06);'+
'color:#FF2A2A;font-family:var(--mono);font-size:12px;letter-spacing:.04em;'+
'padding:14px 16px;border-radius:10px;margin:4px 0 16px 0;line-height:1.5;';
b.textContent='Data unavailable — could not load data.json ('+err+'). '+
'This static demo needs data.json served alongside the page.';
ex.insertBefore(b,ex.firstChild);
}
});
function buildTicks(){
const T=document.getElementById('ticks'), L=document.getElementById('ticklabels');
T.innerHTML=''; L.innerHTML='';
NAMES.forEach((nm,i)=>{
const pct=i/10*100;
const t=document.createElement('div'); t.className='tick'; t.dataset.i=i;
t.style.left=pct+'%'; T.appendChild(t);
const l=document.createElement('div'); l.className='tlabel'+(nm==='never'?' never':'');
l.dataset.i=i; l.textContent=nm;
l.style.left=pct+'%';
if(i===0){l.style.transform='translateX(0)';l.style.textAlign='left';}
else if(i===NAMES.length-1){l.style.transform='translateX(-100%)';l.style.textAlign='right';}
else{l.style.transform='translateX(-50%)';}
l.addEventListener('click',()=>{document.getElementById('slider').value=i;setPos(i);});
L.appendChild(l);
});
}
function buildTable(){
const tb=document.getElementById('tbody'); tb.innerHTML='';
S.forEach((s,i)=>{
const tr=document.createElement('tr');
tr.className=(s.period_days===null?'never-row':'')+ (i===cur?' active':'');
tr.dataset.i=i;
const period=s.period_days===null?'∞':s.period_days+'d';
tr.innerHTML='<td class="td-name">'+s.name+'</td>'+
'<td class="td-mute">'+period+'</td>'+
'<td class="td-mute">'+Math.round(s.total_revealed)+'</td>'+
'<td class="td-c">'+f3(s.holdout_brier_final)+'</td>'+
'<td class="td-b">'+f3(s.holdout_brier_base_only)+'</td>'+
'<td class="td-a">'+f3(s.holdout_brier_raw)+'</td>';
tr.addEventListener('click',()=>{document.getElementById('slider').value=i;setPos(i);});
tb.appendChild(tr);
});
}
const X=[...Array(11).keys()];
function drawChart(){
const axisFont={family:"'JetBrains Mono',monospace",size:11,color:COL.mute};
const traces=[
// corridor band edges drawn via shapes; lines below
{x:X,y:RAW,name:'(a) raw 3B',mode:'lines',line:{color:COL.red,width:1.6,dash:'dash',shape:'spline'},
opacity:.55,hovertemplate:'(a) raw 3B<br>%{y:.3f}<extra></extra>'},
{x:X,y:BASE,name:'(b) base rate',mode:'lines+markers',line:{color:COL.gold,width:2,shape:'spline'},
marker:{size:5,color:COL.gold},hovertemplate:'(b) base rate<br>%{y:.3f}<extra></extra>'},
{x:X,y:FINAL,name:'(c) mix (ours)',mode:'lines+markers',line:{color:COL.green,width:3,shape:'spline'},
marker:{size:6,color:COL.green},hovertemplate:'(c) mix<br>%{y:.3f}<extra></extra>'},
// moving highlight point
{x:[0],y:[FINAL[0]],name:'',mode:'markers',showlegend:false,hoverinfo:'skip',
marker:{size:18,color:COL.green,line:{color:'#fff',width:2.5}}}
];
const layout=baseLayout();
Plotly.newPlot('chart',traces,layout,{displayModeBar:false,responsive:true});
}
function baseLayout(){
return {
paper_bgcolor:COL.card, plot_bgcolor:COL.card,
margin:PLOT_MARGIN, height:430,
font:{family:"'JetBrains Mono',monospace",color:COL.mute},
showlegend:true,
legend:{orientation:'h',y:1.12,x:0,font:{size:11,color:COL.mute},bgcolor:'rgba(0,0,0,0)'},
hoverlabel:{bgcolor:'#141414',bordercolor:COL.borderS,font:{family:"'JetBrains Mono',monospace",color:COL.text,size:12}},
xaxis:{tickmode:'array',tickvals:X,ticktext:NAMES,tickangle:-35,
tickfont:{family:"'JetBrains Mono',monospace",size:9.5,color:COL.dim},
gridcolor:COL.border,zeroline:false,range:[-0.4,10.4],fixedrange:true},
yaxis:{title:{text:'Brier (lower = better)',font:{size:11,color:COL.mute}},
tickfont:{family:"'JetBrains Mono',monospace",size:10,color:COL.dim},
gridcolor:COL.border,zeroline:false,range:[0.196,0.312],fixedrange:true},
shapes:shapesFor(cur),
annotations:annotationsFor(cur)
};
}
function shapesFor(i){
const isNever=(S[i]&&S[i].period_days===null);
return [
// corridor band 0.205–0.220
{type:'rect',xref:'paper',x0:0,x1:1,yref:'y',y0:CORR_MIN,y1:CORR_MAX,
fillcolor:'rgba(31,209,96,0.06)',line:{width:0},layer:'below'},
// base-only floor reference line
{type:'line',xref:'paper',x0:0,x1:1,yref:'y',y0:FLOOR,y1:FLOOR,
line:{color:COL.gold,width:1,dash:'dot'},layer:'below'},
// vertical guide connecting the lever to the point
{type:'line',xref:'x',x0:i,x1:i,yref:'paper',y0:0,y1:1,
line:{color:isNever?'rgba(255,42,42,0.5)':'rgba(255,255,255,0.16)',width:1},layer:'below'}
];
}
function annotationsFor(i){
const a=[{
xref:'paper',yref:'y',x:0.012,y:FLOOR,xanchor:'left',yanchor:'bottom',
text:'base-only floor ≈ '+FLOOR.toFixed(2),showarrow:false,
font:{family:"'JetBrains Mono',monospace",size:9.5,color:COL.gold}
}];
if(S[i]&&S[i].period_days===null){
a.push({xref:'x',yref:'y',x:10,y:FINAL[10],ax:-46,ay:0,axref:'pixel',ayref:'pixel',
text:'collapse',showarrow:true,arrowhead:2,arrowcolor:COL.red,arrowwidth:1.4,
font:{family:"'JetBrains Mono',monospace",size:11,color:COL.red},
xanchor:'right',yanchor:'middle'});
}
return a;
}
function setPos(i){
cur=i;
const s=S[i], isNever=(s.period_days===null);
// chart: move highlight point, color, guide line, collapse annotation
Plotly.restyle('chart',{x:[[i]],y:[[FINAL[i]]],'marker.color':isNever?COL.red:COL.green},[3]);
Plotly.relayout('chart',{shapes:shapesFor(i),annotations:annotationsFor(i)});
// readout
const ro=document.getElementById('readout');
ro.classList.toggle('collapsed',isNever);
document.getElementById('ro-name').textContent=s.name;
document.getElementById('ro-period').textContent =
(isNever?'no reveals · 0 outcomes seen'
:'every '+s.period_days+' day'+(s.period_days===1?'':'s')+' · '+REVEAL[i]+' outcomes seen');
document.getElementById('ro-brier').textContent=f3(FINAL[i]);
document.getElementById('ro-b').textContent=f3(BASE[i]);
document.getElementById('ro-a').textContent=f3(RAW[i]);
const delta=FINAL[i]-RAW[i];
const dEl=document.getElementById('ro-delta');
dEl.textContent = delta<-0.0005 ? signed(delta)+' Brier' : '0.000 — none';
dEl.className='rv '+(delta<-0.0005?'green':'red');
// slider thumb tint
const sl=document.getElementById('slider');
sl.classList.toggle('at-never',isNever);
sl.setAttribute('aria-valuetext', s.name+' — Brier '+f3(FINAL[i]));
// ticks + labels active state
document.querySelectorAll('.tick').forEach(t=>t.classList.toggle('on',+t.dataset.i===i));
document.querySelectorAll('.tlabel').forEach(l=>l.classList.toggle('on',+l.dataset.i===i));
// table active row
document.querySelectorAll('#tbody tr').forEach(tr=>tr.classList.toggle('active',+tr.dataset.i===i));
}
</script>
</body>
</html>