jiminHuang commited on
Commit
e46cb4b
·
verified ·
1 Parent(s): 42d3c8f

Rebuild leaderboard around the tree-ring story

Browse files
Files changed (4) hide show
  1. app.js +220 -145
  2. data.js +0 -0
  3. index.html +145 -72
  4. style.css +240 -115
app.js CHANGED
@@ -1,192 +1,267 @@
1
  /* FinDendro leaderboard: every number comes from data.js. */
 
2
  const D = window.FD, M = D.models, CH = D.meta.chance;
 
3
  const $ = (s, el = document) => el.querySelector(s);
 
4
  const f2 = v => v.toFixed(2).replace('-', '−');
5
  const f3 = v => (v > 0 ? '+' : '') + v.toFixed(3).replace('-', '−');
6
  const pct = v => Math.round(v * 100) + '%';
7
  const by = name => M.find(m => m.model === name);
8
- const quarters = (a, b) => { const q = s => +s.slice(0, 4) * 4 + +s.slice(5); return q(b) - q(a); };
 
 
 
 
9
  const NS = 'http://www.w3.org/2000/svg';
10
- const KNOWS = 'var(--knows)', SAYS = 'var(--says)', CHANCE = 'var(--chance)';
 
 
 
11
 
12
- /* tooltip: any element with data-tip */
13
  const tip = $('#tip');
14
- const showTip = (html, e) => {
15
- tip.innerHTML = html; tip.classList.add('show');
16
- const w = tip.offsetWidth, h = tip.offsetHeight;
17
- tip.style.transform = `translate(${Math.max(8, Math.min(innerWidth - w - 8, e.clientX + 12))}px,${e.clientY - h - 12 < 8 ? e.clientY + 16 : e.clientY - h - 12}px)`;
18
- };
19
  const hideTip = () => tip.classList.remove('show');
20
- document.addEventListener('pointermove', e => {
21
- const t = e.target.closest && e.target.closest('[data-tip]');
22
- if (t) showTip(t.dataset.tip, e); else if (!e.target.closest || !e.target.closest('.chart')) hideTip();
23
- });
24
 
25
- function svg(w, h, label) {
26
- const s = document.createElementNS(NS, 'svg');
27
- s.setAttribute('viewBox', `0 0 ${w} ${h}`); s.setAttribute('width', '100%'); s.setAttribute('role', 'img'); s.setAttribute('aria-label', label);
28
- s.style.display = 'block';
29
- return s;
30
- }
31
- function add(p, tag, attrs, text) {
32
- const e = document.createElementNS(NS, tag);
33
- for (const k in attrs) e.setAttribute(k, attrs[k]);
34
- if (text != null) e.textContent = text;
35
- p.appendChild(e); return e;
36
- }
37
 
38
- /* hero background: one ring per year, darker where GPT-5 was more accurate */
39
- (function rings() {
40
- const s = $('#rings'), years = {};
41
- D.timeline['GPT-5'].forEach(r => { const y = Math.floor(r.x); (years[y] = years[y] || []).push(r.acc); });
42
- Object.keys(years).sort().forEach((y, i, all) => {
43
- const a = years[y].reduce((p, q) => p + q, 0) / years[y].length;
44
- add(s, 'circle', { cx: 0, cy: 0, r: 40 + i * 10.4, fill: 'none', stroke: '#1f4fb8', 'stroke-width': 1 + a * 3.4, 'stroke-opacity': (0.03 + a * a * 0.2).toFixed(3) });
45
- });
 
 
46
  })();
47
 
48
- $('#stats').innerHTML = [
49
- [D.meta.facts.toLocaleString('en-US'), 'dated facts'],
50
- [D.meta.prompts.toLocaleString('en-US'), 'prompts per model'],
51
- [`${D.meta.from}–${D.meta.to}`, 'calendar span'],
52
- [M.length + D.pit.length, 'models and vintages'],
53
- ].map(([b, s]) => `<div class="stat"><b>${b}</b><span>${s}</span></div>`).join('');
 
 
 
 
 
 
 
 
 
 
54
 
55
- /* horizontal dumbbell on a shared 0..1 axis; values sit in a column at the right so labels never meet */
56
- function dumbbell(el, rows, opts = {}) {
57
- const W = opts.W || 520, L = opts.L || 112, R = 92, T = 8, rowH = opts.rowH || 34, H = T + rows.length * rowH + 24, sep = opts.sep || ' / ';
58
- const s = svg(W, H, opts.label || 'Knows and says it knows by model');
59
- const x = v => L + v * (W - L - R);
60
- [0, .25, .5, .75, 1].forEach(v => {
61
- add(s, 'line', { x1: x(v), x2: x(v), y1: T, y2: H - 22, stroke: 'rgba(24,56,152,.09)' });
62
- add(s, 'text', { x: x(v), y: H - 6, 'text-anchor': 'middle', 'font-size': 11, fill: 'var(--soft)' }, v.toFixed(2));
 
 
 
 
 
 
 
 
63
  });
64
- if (!opts.noChance) add(s, 'line', { x1: x(CH), x2: x(CH), y1: T, y2: H - 22, stroke: CHANCE, 'stroke-width': 1.4, 'stroke-dasharray': '3 3' });
65
- rows.forEach((r, i) => {
66
- const y = T + rowH * (i + .5);
67
- add(s, 'text', { x: L - 12, y: y + 4, 'text-anchor': 'end', 'font-size': 12.5, fill: 'var(--ink)' }, r.label);
68
- add(s, 'line', { x1: x(r.a), x2: x(r.b), y1: y, y2: y, stroke: 'rgba(15,21,40,.22)', 'stroke-width': 2 });
69
- [[r.a, KNOWS], [r.b, SAYS]].forEach(([v, c]) => add(s, 'circle', { cx: x(v), cy: y, r: 6.5, style: `fill:${c};stroke:#fff;stroke-width:2` }));
70
- add(s, 'text', { x: W, y: y + 4, 'text-anchor': 'end', 'font-size': 12, fill: 'var(--soft)', style: 'font-variant-numeric:tabular-nums' }, f2(r.a) + sep + f2(r.b));
71
- const hit = add(s, 'rect', { x: L - 4, y: y - rowH / 2, width: W - L + 4, height: rowH, fill: 'transparent' });
72
- hit.dataset.tip = `<b>${r.label}</b><br>${r.aName || 'Knows'} ${f2(r.a)}<br>${r.bName || 'Says it knows'} ${f2(r.b)}`;
73
  });
74
- el.replaceChildren(s);
 
 
 
75
  }
76
- dumbbell($('#dumbbell'), M.map(m => ({ label: m.model, a: m.knows, b: m.says })));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
  /* leaderboard */
79
- const COLS = [
80
- ['model', 'Model', 'left'], ['knows', 'Knows'], ['says', 'Says'], ['acc_says', 'Acc | says'], ['acc_denies', 'Acc | denies'],
81
- ['gap', 'Gap [95% CI]'], ['know_end', 'Knowledge ends'], ['claim_end', 'Claims end'], ['self_report', 'Self-report'], ['vendor_cutoff', 'Vendor'],
82
- ];
83
- let sortK = 'knows', sortDir = -1;
 
 
 
 
 
 
 
84
  function renderLB() {
85
- const rows = [...M].sort((a, b) => {
86
- const x = a[sortK], y = b[sortK];
87
- if (x == null && y == null) return 0;
88
- if (x == null) return 1;
89
- if (y == null) return -1;
90
- return (x < y ? -1 : x > y ? 1 : 0) * sortDir;
91
- });
92
- const head = `<thead><tr><th class="left">#</th>${COLS.map(([k, t, c]) =>
93
- `<th data-k="${k}" tabindex="0" role="button" class="${c || ''}${k === sortK ? ' on' : ''}"${k === sortK ? ` aria-sort="${sortDir < 0 ? 'descending' : 'ascending'}"` : ''}>${t}${k === sortK ? (sortDir < 0 ? ' ↓' : ' ↑') : ''}</th>`).join('')}</tr></thead>`;
94
  const na = '<span class="na">n/a</span>';
 
95
  const body = rows.map((m, i) => {
96
- const lag = m.know_end && quarters(m.know_end, m.claim_end) > 1;
97
- return `<tr><td class="left rank">${i + 1}</td>
98
- <td class="left"><span class="mname">${m.model}</span><span class="vendor">${m.vendor}</span>${m.small ? '<span class="chip">small</span>' : ''}</td>
99
- <td><span class="bar"><i><b style="width:${m.knows * 100}%"></b><u style="left:${CH * 100}%"></u></i>${f2(m.knows)}</span></td>
100
- <td>${f2(m.says)}</td><td>${f2(m.acc_says)}</td><td>${f2(m.acc_denies)}</td>
101
  <td>${f2(m.gap)}<span class="ci">[${f2(m.gap_lo)}, ${f2(m.gap_hi)}]</span></td>
102
- <td>${m.know_end || na}</td><td${lag ? ' class="flag"' : ''}>${m.claim_end || na}</td>
103
- <td>${m.self_report || na}</td><td>${m.vendor_cutoff || na}</td></tr>`;
104
  }).join('');
105
- const t = $('#lb');
106
- t.innerHTML = head + `<tbody>${body}</tbody>`;
107
- t.querySelectorAll('th[data-k]').forEach(th => {
108
- const go = () => { const k = th.dataset.k; if (k === sortK) sortDir = -sortDir; else { sortK = k; sortDir = k === 'model' ? 1 : -1; } renderLB(); };
109
- th.onclick = go;
110
- th.onkeydown = e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); go(); } };
111
- });
112
  }
113
- renderLB();
114
 
115
- /* findings, written from the data */
116
- (function findings() {
117
- const g = by('GPT-5'), l = by('Llama-3.3-70B');
118
- const items = [
119
- [`${f2(g.knows)} known, ${f2(g.says)} claimed`, `GPT-5 answers ${pct(g.knows)} of controlled facts correctly and says it knows ${pct(g.says)} of them.`],
120
- [`${f2(g.acc_denies)} on denied facts`, `When GPT-5 says it does not know a value, it still picks the right one ${pct(g.acc_denies)} of the time. Chance is about ${pct(CH)}.`],
121
- [`${quarters(l.know_end, l.claim_end)} quarters of empty claims`, `Llama-3.3-70B stops knowing after ${l.know_end} and keeps claiming knowledge until ${l.claim_end}.`],
122
- ];
123
- $('#findings').innerHTML = items.map(([n, t]) => `<div class="card"><div class="num">${n}</div><p>${t}</p></div>`).join('');
124
- })();
 
 
 
 
 
 
 
 
 
 
125
 
126
- /* timeline small multiples */
127
- function ymX(s) { const [y, m] = s.split('-').map(Number); return y + (m - .5) / 12; }
128
- function qX(s) { return +s.slice(0, 4) + (+s.slice(5) - .5) / 4; }
129
- function drawPanel(el, m) {
130
- const W = Math.max(320, el.clientWidth || 520), H = 230, L = 34, R = 12, T = 10, B = 26, x0 = 2017, x1 = 2026.9;
131
- const pts = D.timeline[m.model].filter(r => r.x >= x0);
132
- const s = svg(W, H, `${m.model}: accuracy and claim rate from ${x0}. Knowledge ends ${m.know_end}, claims end ${m.claim_end}.`);
133
  const x = v => L + (v - x0) / (x1 - x0) * (W - L - R), y = v => T + (1 - v) * (H - T - B);
134
- [0, .25, .5, .75, 1].forEach(v => {
135
- add(s, 'line', { x1: L, x2: W - R, y1: y(v), y2: y(v), stroke: 'rgba(24,56,152,.09)' });
136
- add(s, 'text', { x: L - 6, y: y(v) + 4, 'text-anchor': 'end', 'font-size': 10.5, fill: 'var(--soft)' }, v.toFixed(2).replace('0.', '.').replace('1.00', '1'));
 
 
 
 
 
 
 
 
137
  });
138
- for (let yr = 2018; yr <= 2026; yr += W < 420 ? 2 : 1)
139
- add(s, 'text', { x: x(yr + .5), y: H - 8, 'text-anchor': 'middle', 'font-size': 10.5, fill: 'var(--soft)' }, W < 420 ? yr : `'${String(yr).slice(2)}`);
140
- add(s, 'line', { x1: L, x2: W - R, y1: y(CH), y2: y(CH), stroke: CHANCE, 'stroke-width': 1.3, 'stroke-dasharray': '3 3' });
141
- const vline = (v, style, dash) => add(s, 'line', { x1: x(v), x2: x(v), y1: T, y2: H - B, style, 'stroke-width': 2, ...(dash ? { 'stroke-dasharray': dash, 'stroke-linecap': 'round' } : {}) });
142
- if (m.vendor_cutoff) vline(ymX(m.vendor_cutoff), 'stroke:var(--vendor)', '0.1 4');
143
- vline(qX(m.know_end), 'stroke:var(--knows);opacity:.55');
144
- // equal boundaries would overlap exactly, so claims end is drawn dashed on top
145
- vline(qX(m.claim_end), 'stroke:var(--says);opacity:.8', m.know_end === m.claim_end ? '5 5' : null);
146
- const path = key => pts.map((r, i) => `${i ? 'L' : 'M'}${x(r.x).toFixed(1)},${y(r[key]).toFixed(1)}`).join('');
147
- add(s, 'path', { d: path('claim'), fill: 'none', style: `stroke:${SAYS}`, 'stroke-width': 2, 'stroke-linejoin': 'round' });
148
- add(s, 'path', { d: path('acc'), fill: 'none', style: `stroke:${KNOWS}`, 'stroke-width': 2, 'stroke-linejoin': 'round' });
149
- const cross = add(s, 'line', { y1: T, y2: H - B, stroke: 'rgba(15,21,40,.35)', visibility: 'hidden' });
150
- const dA = add(s, 'circle', { r: 4.5, style: `fill:${KNOWS};stroke:#fff;stroke-width:1.5`, visibility: 'hidden' });
151
- const dC = add(s, 'circle', { r: 4.5, style: `fill:${SAYS};stroke:#fff;stroke-width:1.5`, visibility: 'hidden' });
152
  el.replaceChildren(s);
153
- const hide = () => { [cross, dA, dC].forEach(n => n.setAttribute('visibility', 'hidden')); hideTip(); };
154
  s.addEventListener('pointermove', e => {
155
- const b = s.getBoundingClientRect(), mx = (e.clientX - b.left) / b.width * W;
156
- let r = pts[0]; pts.forEach(p => { if (Math.abs(x(p.x) - mx) < Math.abs(x(r.x) - mx)) r = p; });
157
- cross.setAttribute('x1', x(r.x)); cross.setAttribute('x2', x(r.x));
158
- dA.setAttribute('cx', x(r.x)); dA.setAttribute('cy', y(r.acc)); dC.setAttribute('cx', x(r.x)); dC.setAttribute('cy', y(r.claim));
159
- [cross, dA, dC].forEach(n => n.setAttribute('visibility', 'visible'));
160
- showTip(`<b>${r.p}</b> · n=${r.n}<br>Accuracy ${f2(r.acc)}<br>Claim rate ${f2(r.claim)}`, e);
161
  });
162
- s.addEventListener('pointerleave', hide);
 
 
163
  }
164
- const panels = M.filter(m => m.know_end);
165
- $('#multiples').innerHTML = panels.map((m, i) => `<article class="card panel"><h3>${m.model}</h3>
166
- <div class="ends">Knowledge ends <b>${m.know_end}</b> · Claims end <b>${m.claim_end}</b> · Self-report <b>${m.self_report}</b> · Vendor <b>${m.vendor_cutoff || 'n/a'}</b></div>
167
- <div class="chart" id="chart${i}"></div></article>`).join('');
168
- const drawAll = () => panels.forEach((m, i) => drawPanel($('#chart' + i), m));
169
 
170
  /* fact families */
171
- const FAMS = Object.keys(D.family['GPT-5']);
172
- $('#fam').innerHTML = FAMS.map((f, i) => `<div class="card"><h3>${f}</h3><div id="fam${i}"></div></div>`).join('');
173
- FAMS.forEach((f, i) => dumbbell($('#fam' + i), Object.keys(D.family).map(m => ({ label: m, a: D.family[m][f].know, b: D.family[m][f].says })),
174
- { W: 400, L: 104, rowH: 36, noChance: true, label: `${f}: knows and says it knows by model` }));
 
 
175
 
176
- /* declared date */
177
- $('#decl').innerHTML = `<div class="card"><h3>Claims of knowledge</h3><div class="legend"><span><i class="dot dot--knows"></i>No date</span><span><i class="dot dot--says"></i>Date declared</span></div><div id="declC"></div></div>
178
- <div class="card"><h3>Accuracy on the value</h3><div class="legend"><span><i class="dot dot--knows"></i>No date</span><span><i class="dot dot--says"></i>Date declared</span></div><div id="declA"></div></div>`;
179
- dumbbell($('#declC'), M.map(m => ({ label: m.model, a: m.claims, b: m.claims_declared, aName: 'No date', bName: 'Date declared' })), { W: 520, sep: ' → ', noChance: true, label: 'Claims of knowledge without and with a declared date' });
180
- dumbbell($('#declA'), M.map(m => ({ label: m.model, a: m.acc, b: m.acc_declared, aName: 'No date', bName: 'Date declared' })), { W: 520, sep: ' → ', noChance: true, label: 'Accuracy without and with a declared date' });
 
 
 
 
 
181
 
182
  /* point-in-time */
183
  (function pit() {
184
  const lo = -.08, hi = .12, x = v => (v - lo) / (hi - lo) * 100;
185
  $('#pitT').innerHTML = `<thead><tr><th class="left">Model</th><th>Cutoff</th><th>Up to cutoff</th><th>After</th><th>Difference [95% CI]</th><th class="left">Interval around zero</th></tr></thead><tbody>` +
186
- D.pit.map(r => `<tr><td class="left mname">${r.model}</td><td>${r.cutoff}</td><td>${f2(r.before)}</td><td>${f2(r.after)}</td>
187
- <td>${f3(r.diff)}<span class="ci">[${f3(r.lo)}, ${f3(r.hi)}]</span></td>
188
- <td class="left"><span class="forest" data-tip="<b>${r.model} ${r.cutoff}</b><br>Difference ${f3(r.diff)}"><s style="left:${x(0)}%"></s><i style="left:${x(r.lo)}%;width:${x(r.hi) - x(r.lo)}%"></i><b style="left:${x(r.diff)}%"></b></span></td></tr>`).join('') + '</tbody>';
189
  })();
190
 
 
191
  drawAll();
192
  let rz; addEventListener('resize', () => { clearTimeout(rz); rz = setTimeout(drawAll, 120); });
 
1
  /* FinDendro leaderboard: every number comes from data.js. */
2
+ document.documentElement.classList.add('js');
3
  const D = window.FD, M = D.models, CH = D.meta.chance;
4
+ const FRONT = M.filter(m => !m.small);
5
  const $ = (s, el = document) => el.querySelector(s);
6
+ const $$ = (s, el = document) => [...el.querySelectorAll(s)];
7
  const f2 = v => v.toFixed(2).replace('-', '−');
8
  const f3 = v => (v > 0 ? '+' : '') + v.toFixed(3).replace('-', '−');
9
  const pct = v => Math.round(v * 100) + '%';
10
  const by = name => M.find(m => m.model === name);
11
+ const qn = s => +s.slice(0, 4) * 4 + +s.slice(5);
12
+ const quarters = (a, b) => qn(b) - qn(a);
13
+ const qX = s => +s.slice(0, 4) + (+s.slice(5) - .5) / 4;
14
+ const ymX = s => { const [y, m] = s.split('-').map(Number); return y + (m - .5) / 12; };
15
+ const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
16
  const NS = 'http://www.w3.org/2000/svg';
17
+ const add = (p, tag, attrs, text) => { const e = document.createElementNS(NS, tag); for (const k in attrs) e.setAttribute(k, attrs[k]); if (text != null) e.textContent = text; p.appendChild(e); return e; };
18
+ const svg = (w, h, label) => { const s = document.createElementNS(NS, 'svg'); s.setAttribute('viewBox', `0 0 ${w} ${h}`); s.setAttribute('width', '100%'); s.setAttribute('role', 'img'); s.setAttribute('aria-label', label); s.style.display = 'block'; return s; };
19
+ const mix = (a, b, t) => '#' + a.map((v, i) => Math.round(v + (b[i] - v) * Math.max(0, Math.min(1, t))).toString(16).padStart(2, '0')).join('');
20
+ const WOOD = [243, 236, 220], TEAL = [8, 96, 88], RUST = [201, 82, 28], PALE = [241, 244, 238], HEAT = [10, 125, 116];
21
 
22
+ /* tooltip */
23
  const tip = $('#tip');
24
+ const showTip = (html, e) => { tip.innerHTML = html; tip.classList.add('show'); const w = tip.offsetWidth, h = tip.offsetHeight; tip.style.transform = `translate(${Math.max(8, Math.min(innerWidth - w - 8, e.clientX + 12))}px,${e.clientY - h - 12 < 8 ? e.clientY + 16 : e.clientY - h - 12}px)`; };
 
 
 
 
25
  const hideTip = () => tip.classList.remove('show');
26
+ document.addEventListener('pointermove', e => { const t = e.target.closest && e.target.closest('[data-tip]'); if (t) showTip(t.dataset.tip, e); else if (!(e.target.closest && e.target.closest('.live'))) hideTip(); });
 
 
 
27
 
28
+ /* scroll, reveal, nav */
29
+ const prog = $('#progress'), head = $('#header');
30
+ const onScroll = () => { const h = document.documentElement; prog.style.transform = `scaleX(${h.scrollTop / Math.max(1, h.scrollHeight - h.clientHeight)})`; head.classList.toggle('scrolled', h.scrollTop > 12); };
31
+ addEventListener('scroll', onScroll, { passive: true }); onScroll();
32
+ const io = new IntersectionObserver(es => es.forEach(e => { if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); } }), { rootMargin: '0px 0px -6% 0px' });
33
+ $$('.reveal').forEach(el => io.observe(el));
34
+ setTimeout(() => $$('.reveal').forEach(el => el.classList.add('in')), 2200);
35
+ const nav = new IntersectionObserver(es => es.forEach(e => { if (e.isIntersecting) $$('#navLinks a').forEach(a => a.classList.toggle('active', a.dataset.sec === e.target.id)); }), { rootMargin: '-40% 0px -55% 0px' });
36
+ $$('#navLinks a').forEach(a => { const s = document.getElementById(a.dataset.sec); if (s) nav.observe(s); });
 
 
 
37
 
38
+ /* pixel woodpecker, tapping at the rings */
39
+ (function bird() {
40
+ const P = { k: '#10201c', r: '#c9521c', w: '#f4f1e6', t: '#0a7d74', y: '#d9962b' };
41
+ const A = ['....rr......', '...rrrr.....', '..kkkkkk....', '.kkwkkkk....', 'yykkkkkkk...', '..kkttkkkk..', '...kttttkkk.', '....tttttkk.', '.....kk.ttkk', '.....y.y...k'];
42
+ const B = A.map((r, i) => i < 5 ? '.' + r.slice(0, 11) : r);
43
+ const c = document.createElement('canvas'); c.width = 12; c.height = 12; $('#bird').appendChild(c);
44
+ const g = c.getContext('2d');
45
+ const draw = f => { g.clearRect(0, 0, 12, 12); f.forEach((r, y) => [...r].forEach((p, x) => { if (P[p]) { g.fillStyle = P[p]; g.fillRect(x, y + 1, 1, 1); } })); };
46
+ draw(A);
47
+ if (!reduce) { let n = 0; setInterval(() => { n = (n + 1) % 8; draw(n < 4 && n % 2 ? B : A); }, 170); }
48
  })();
49
 
50
+ /* hero */
51
+ (function stats() {
52
+ const items = [[D.meta.facts, 'dated facts', v => v.toLocaleString('en-US')], [D.meta.prompts, 'prompts per model', v => v.toLocaleString('en-US')], [D.meta.to - D.meta.from + 1, 'years of markets', v => v], [M.length + D.pit.length, 'models and vintages', v => v]];
53
+ $('#stats').innerHTML = items.map(([v, s, f], i) => `<div class="stat"><b data-i="${i}">${f(v)}</b><span>${s}</span></div>`).join('');
54
+ if (reduce) return;
55
+ items.forEach(([v, , f], i) => { const el = $(`.stat b[data-i="${i}"]`), t0 = performance.now(); (function step(t) { const p = Math.min(1, (t - t0) / 1100), e = 1 - Math.pow(1 - p, 3); el.textContent = f(Math.round(v * e)); if (p < 1) requestAnimationFrame(step); })(t0); });
56
+ })();
57
+ (function rotator() {
58
+ const el = $('#rotator'), names = M.map(m => m.model); let i = 0; el.textContent = names[0];
59
+ if (reduce) return;
60
+ setInterval(() => { el.classList.add('off'); setTimeout(() => { i = (i + 1) % names.length; el.textContent = names[i]; el.classList.remove('off'); }, 260); }, 2200);
61
+ })();
62
+ (function ticker() {
63
+ const items = [...M].sort((a, b) => b.knows - a.knows).map((m, i) => `<span class="tk"><i>#${i + 1}</i><b>${m.model}</b>knows <u>${f2(m.knows)}</u> · says <s>${f2(m.says)}</s></span>`).join('');
64
+ $('#tickerTrack').innerHTML = items + items + items + items;
65
+ })();
66
 
67
+ /* cross-section: right half is what the model knows, left half what it says it knows */
68
+ let xsAuto = null;
69
+ function arc(r, a0, a1) { const p = a => [(r * Math.cos(a)).toFixed(2), (r * Math.sin(a)).toFixed(2)]; const [x0, y0] = p(a0), [x1, y1] = p(a1); return `M${x0},${y0}A${r},${r} 0 0 1 ${x1},${y1}`; }
70
+ function drawXS(name) {
71
+ const m = by(name), S = D.timeline[name], s = svg(500, 500, `${name} cross-section. Knowledge ends ${m.know_end}, claims end ${m.claim_end}.`);
72
+ s.setAttribute('viewBox', '-250 -250 500 500'); s.classList.add('live');
73
+ let r = 20; add(s, 'circle', { r: 16, fill: mix(WOOD, TEAL, .12) });
74
+ const after = (p, end) => p.length > 4 && qn(p) > qn(end);
75
+ S.forEach((p, i) => {
76
+ const w = p.p.length === 4 ? 6.4 : 3.1, rr = r + w / 2;
77
+ const kCol = after(p.p, m.know_end) ? mix(WOOD, TEAL, .05) : mix(WOOD, TEAL, (p.acc - CH) / (1 - CH) * .94 + .06);
78
+ const cCol = after(p.p, m.claim_end) ? mix(WOOD, RUST, .05) : mix(WOOD, RUST, p.claim * .94 + .06);
79
+ const g = add(s, 'g', { class: 'grow' }); g.style.animationDelay = (i * 15) + 'ms';
80
+ add(g, 'path', { d: arc(rr, -Math.PI / 2, Math.PI / 2), fill: 'none', stroke: kCol, 'stroke-width': w - .45 });
81
+ add(g, 'path', { d: arc(rr, Math.PI / 2, Math.PI * 1.5), fill: 'none', stroke: cCol, 'stroke-width': w - .45 });
82
+ p._r0 = r; p._r1 = r + w; r += w;
83
  });
84
+ const ring = (p, a0, a1, col) => { if (p) add(s, 'path', { d: arc(p._r1, a0, a1), fill: 'none', stroke: col, 'stroke-width': 2.2 }); };
85
+ ring(S.find(p => p.p === m.know_end), -Math.PI / 2, Math.PI / 2, '#10201c');
86
+ ring(S.find(p => p.p === m.claim_end), Math.PI / 2, Math.PI * 1.5, '#10201c');
87
+ add(s, 'line', { x1: 0, x2: 0, y1: -r - 2, y2: r + 2, stroke: '#fff', 'stroke-width': 2.5 });
88
+ s.addEventListener('pointermove', e => {
89
+ const b = s.getBoundingClientRect(), dx = (e.clientX - b.left) / b.width * 500 - 250, dy = (e.clientY - b.top) / b.height * 500 - 250, d = Math.hypot(dx, dy);
90
+ const p = S.find(q => d >= q._r0 && d < q._r1);
91
+ if (p) showTip(`<b>${p.p}</b><br>Knows ${f2(p.acc)}<br>Says ${f2(p.claim)}`, e); else hideTip();
 
92
  });
93
+ s.addEventListener('pointerleave', hideTip);
94
+ $('#xs').replaceChildren(s);
95
+ $('#xsEnds').innerHTML = [['Knows', f2(m.knows)], ['Says', f2(m.says)], ['Knowledge ends', m.know_end], ['Claims end', m.claim_end]].map(([a, b]) => `<div><b>${b}</b>${a}</div>`).join('');
96
+ $$('#xsChips .chip').forEach(c => c.setAttribute('aria-selected', c.dataset.m === name));
97
  }
98
+ $('#xsChips').innerHTML = FRONT.map(m => `<button class="chip" role="tab" data-m="${m.model}" type="button">${m.model}</button>`).join('');
99
+ $$('#xsChips .chip').forEach(c => c.onclick = () => { clearInterval(xsAuto); drawXS(c.dataset.m); });
100
+ drawXS(FRONT[0].model);
101
+ if (!reduce) { let i = 0; xsAuto = setInterval(() => { if (document.hidden) return; i = (i + 1) % FRONT.length; drawXS(FRONT[i].model); }, 6000); }
102
+
103
+ /* replay: the reader answers, then the recorded answers appear */
104
+ (function replay() {
105
+ const P = D.pool, tally = { You: [0, 0] }; FRONT.forEach(m => tally[m.model] = [0, 0]);
106
+ let i = 0, done = false;
107
+ const val = (f, r) => (f.opts.find(o => o[0] === r) || [, ''])[1].replace(/ USD$| JPY per USD$/, '');
108
+ const months = ['Jan.', 'Feb.', 'Mar.', 'Apr.', 'May', 'Jun.', 'Jul.', 'Aug.', 'Sep.', 'Oct.', 'Nov.', 'Dec.'];
109
+ function score() { const [a, b] = tally.You; $('#aScore').textContent = b ? `You ${a} of ${b}` : ''; }
110
+ function rows(f, show) {
111
+ $('#aRows').innerHTML = FRONT.map(m => {
112
+ const a = f.ans[m.model].r, t = tally[m.model];
113
+ if (!show) return `<div class="a-row"><span class="a-m">${m.model}</span><span class="a-v wait">?</span><span class="a-s wait">waiting for your answer</span><span class="a-tally"></span></div>`;
114
+ if (!a) return `<div class="a-row"><span class="a-m">${m.model}</span><span class="a-v wait">n/a</span><span class="a-s wait">gave no usable answer</span><span class="a-tally">${t[0]} of ${t[1]}</span></div>`;
115
+ const ok = a[1] === f.true;
116
+ return `<div class="a-row"><span class="a-m">${m.model}</span><span class="a-v ${ok ? 'ok' : 'no'}">${val(f, a[1])}</span><span class="a-s ${a[0] ? 'know' : 'deny'}">${a[0] ? '“I know this value.”' : '“I do not know this value.”'}</span><span class="a-tally">${t[0]} of ${t[1]}</span></div>`;
117
+ }).join('');
118
+ }
119
+ function load() {
120
+ const f = P[i]; done = false;
121
+ const [y, mo] = f.period.split('-');
122
+ $('#qKick').textContent = `${f.family} · ${months[+mo - 1]} ${y}`;
123
+ $('#qN').textContent = `Fact ${i + 1} of ${P.length}`;
124
+ $('#qText').textContent = f.q.replace(/ Use FRED series.*$| Round to .*$/, '');
125
+ const box = $('#qOpts'); box.replaceChildren();
126
+ [...f.opts].sort((a, b) => (+a[1].replace(/[^\d.-]/g, '') || 0) - (+b[1].replace(/[^\d.-]/g, '') || 0)).forEach(o => {
127
+ const b = document.createElement('button'); b.type = 'button'; b.className = 'q-opt'; b.dataset.r = o[0];
128
+ const t = document.createElement('span'); t.textContent = o[1]; b.appendChild(t);
129
+ b.onclick = () => finish(o[0]); box.appendChild(b);
130
+ });
131
+ $('#qReveal').hidden = false; $('#qNext').hidden = true; $('#aPunch').hidden = true;
132
+ rows(f, false); score();
133
+ }
134
+ function finish(pick) {
135
+ if (done) return; done = true;
136
+ const f = P[i];
137
+ if (pick != null) { tally.You[1]++; if (pick === f.true) tally.You[0]++; }
138
+ FRONT.forEach(m => { const a = f.ans[m.model].r; if (a) { tally[m.model][1]++; if (a[1] === f.true) tally[m.model][0]++; } });
139
+ $$('.q-opt').forEach(b => { b.disabled = true; const r = +b.dataset.r; if (r === f.true) b.classList.add('right'); if (r === pick) b.classList.add('you'); });
140
+ rows(f, true); score();
141
+ const leak = FRONT.filter(m => { const a = f.ans[m.model].r; return a && !a[0] && a[1] === f.true; }).map(m => m.model);
142
+ const p = $('#aPunch');
143
+ if (leak.length) { p.textContent = `${leak.join(', ')} denied knowing it and picked the published value anyway.`; p.hidden = false; }
144
+ $('#qReveal').hidden = true; $('#qNext').hidden = false;
145
+ }
146
+ $('#qReveal').onclick = () => finish(null);
147
+ $('#qNext').onclick = () => { i = (i + 1) % P.length; load(); };
148
+ load();
149
+ })();
150
 
151
  /* leaderboard */
152
+ const COLS = [['model', 'Model', 'left'], ['knows', 'Knows', 'heat'], ['says', 'Says', 'heat'], ['acc_says', 'Right when claiming', 'heat'], ['acc_denies', 'Right when denying', 'heat'], ['gap', 'Gap [95% CI]'], ['know_end', 'Knowledge ends'], ['claim_end', 'Claims end']];
153
+ let sortK = 'knows', sortDir = -1, openM = null;
154
+ const heat = v => { const t = Math.max(0, Math.min(1, (v - .15) / .8)); return `background:${mix(PALE, HEAT, t)};color:${t > .55 ? '#fff' : 'var(--ink)'}`; };
155
+ function detail(m) {
156
+ const fam = D.family[m.model];
157
+ const lines = fam ? Object.keys(fam).map(k => `<div class="dt-line"><span>${k}</span><b>knows ${f2(fam[k].know)} · says ${f2(fam[k].says)}</b></div>`).join('') : '<p>Not broken down for the small open models.</p>';
158
+ const lag = m.know_end ? quarters(m.know_end, m.claim_end) : null;
159
+ const read = m.know_end ? (lag > 1 ? `Keeps claiming knowledge for ${lag} quarters after it stops knowing.` : 'Its claims stop within a quarter of its knowledge.') : 'Accuracy sits too close to chance for the rule to locate an end.';
160
+ return `<div class="dt"><div><h4>Reading</h4><p>${read}</p><div class="dt-line" style="margin-top:8px"><span>Says when asked</span><b>${m.self_report || 'n/a'}</b></div><div class="dt-line"><span>Vendor cutoff</span><b>${m.vendor_cutoff || 'n/a'}</b></div></div>
161
+ <div><h4>By fact family</h4>${lines}</div>
162
+ <div><h4>With a declared date</h4><div class="dt-line"><span>Claims to already know</span><b>${f2(m.claims)} → ${f2(m.claims_declared)}</b></div><div class="dt-line"><span>Accuracy</span><b>${f2(m.acc)} → ${f2(m.acc_declared)}</b></div></div></div>`;
163
+ }
164
  function renderLB() {
165
+ const rows = [...M].sort((a, b) => { const x = a[sortK], y = b[sortK]; if (x == null && y == null) return 0; if (x == null) return 1; if (y == null) return -1; return (x < y ? -1 : x > y ? 1 : 0) * sortDir; });
 
 
 
 
 
 
 
 
166
  const na = '<span class="na">n/a</span>';
167
+ const head = `<thead><tr><th>#</th>${COLS.map(([k, t, c]) => `<th data-k="${k}" tabindex="0" role="button" class="${c === 'left' ? 'left' : ''}${k === sortK ? ' on' : ''}">${t}${k === sortK ? (sortDir < 0 ? ' ↓' : ' ↑') : ''}</th>`).join('')}<th></th></tr></thead>`;
168
  const body = rows.map((m, i) => {
169
+ const lag = m.know_end && quarters(m.know_end, m.claim_end) > 1, open = openM === m.model;
170
+ return `<tr class="row${open ? ' open' : ''}" data-m="${m.model}" tabindex="0" aria-expanded="${open}"><td><span class="pos ${i < 3 ? 'p' + (i + 1) : ''}">${i + 1}</span></td>
171
+ <td class="left"><span class="mname">${m.model}</span><span class="mv">${m.vendor}</span>${m.small ? '<span class="tag">small</span>' : ''}</td>
172
+ ${['knows', 'says', 'acc_says', 'acc_denies'].map(k => `<td><span class="heat" style="${heat(m[k])}">${f2(m[k])}</span></td>`).join('')}
 
173
  <td>${f2(m.gap)}<span class="ci">[${f2(m.gap_lo)}, ${f2(m.gap_hi)}]</span></td>
174
+ <td>${m.know_end || na}</td><td class="${lag ? 'late' : ''}">${m.claim_end || na}</td><td><span class="chev">›</span></td></tr>` +
175
+ (open ? `<tr class="detail"><td colspan="${COLS.length + 2}">${detail(m)}</td></tr>` : '');
176
  }).join('');
177
+ const t = $('#lb'); t.innerHTML = head + `<tbody>${body}</tbody>`;
178
+ $$('th[data-k]', t).forEach(th => { const go = () => { const k = th.dataset.k; if (k === sortK) sortDir = -sortDir; else { sortK = k; sortDir = k === 'model' ? 1 : -1; } renderLB(); }; th.onclick = go; th.onkeydown = e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); go(); } }; });
179
+ $$('tr.row', t).forEach(r => { const go = () => { openM = openM === r.dataset.m ? null : r.dataset.m; renderLB(); }; r.onclick = go; r.onkeydown = e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); go(); } }; });
 
 
 
 
180
  }
181
+ openM = FRONT[0].model; renderLB();
182
 
183
+ /* cutoff calendar: one row per model */
184
+ function drawCalendar() {
185
+ const el = $('#calendar'), W = Math.max(340, el.clientWidth || 900), small = W < 560, L = small ? 76 : 118, R = 54, T = 8, rowH = 58, x0 = 2021, x1 = 2025.25, H = T + rowH * FRONT.length + 24;
186
+ const s = svg(W, H, 'Knowledge end, claims end, self-reported and vendor cutoffs by model'); const x = v => L + (v - x0) / (x1 - x0) * (W - L - R);
187
+ for (let y = 2021; y <= 2025; y++) { add(s, 'line', { x1: x(y), x2: x(y), y1: T, y2: H - 22, stroke: 'rgba(14,84,70,.09)' }); add(s, 'text', { x: x(y), y: H - 6, 'text-anchor': 'middle', 'font-size': 11.5, fill: 'var(--soft)' }, y); }
188
+ FRONT.forEach((m, i) => {
189
+ const y = T + rowH * (i + .5), kx = x(qX(m.know_end)), cx = x(qX(m.claim_end));
190
+ add(s, 'text', { x: L - 14, y: y + 4, 'text-anchor': 'end', 'font-size': small ? 11 : 13, 'font-weight': 500, fill: 'var(--ink)' }, small ? m.model.replace('-70B', '').replace('DeepSeek-', 'DS-') : m.model);
191
+ add(s, 'line', { x1: L, x2: W - R, y1: y + rowH / 2, y2: y + rowH / 2, stroke: 'rgba(14,84,70,.07)' });
192
+ if (cx > kx) add(s, 'rect', { x: kx, y: y - 18, width: cx - kx, height: 36, style: 'fill:var(--says);opacity:.16', rx: 3 });
193
+ const lag = quarters(m.know_end, m.claim_end);
194
+ if (lag > 1) add(s, 'text', { x: (kx + cx) / 2, y: y + 4, 'text-anchor': 'middle', 'font-size': 12, 'font-weight': 500, fill: '#b34a17' }, `${lag} quarters`);
195
+ const hit = (cxv, label, val, draw) => { draw(); const r = add(s, 'circle', { cx: cxv, cy: y, r: 16, fill: 'transparent' }); r.dataset.tip = `<b>${m.model}</b><br>${label}: ${val}`; };
196
+ if (m.vendor_cutoff) { const v = x(ymX(m.vendor_cutoff)); hit(v, 'Vendor cutoff', m.vendor_cutoff, () => add(s, 'rect', { x: v - 5, y: y + 8, width: 10, height: 10, style: 'fill:#fff;stroke:var(--vendor);stroke-width:2' })); }
197
+ const sv = x(ymX(m.self_report)); hit(sv, 'Says when asked', m.self_report, () => add(s, 'path', { d: `M${sv},${y - 19}l6,11h-12z`, style: 'fill:var(--ink)' }));
198
+ hit(cx, 'Claims end', m.claim_end, () => add(s, 'rect', { x: cx - 5.5, y: y - 5.5, width: 11, height: 11, transform: `rotate(45 ${cx} ${y})`, style: 'fill:var(--says);stroke:#fff;stroke-width:1.5' }));
199
+ hit(kx, 'Knowledge ends', m.know_end, () => add(s, 'circle', { cx: kx, cy: kx === cx ? y - 11 : y, r: 6.5, style: 'fill:var(--knows);stroke:#fff;stroke-width:1.5' }));
200
+ });
201
+ el.replaceChildren(s);
202
+ }
203
 
204
+ /* core chart */
205
+ let coreM = FRONT[0].model;
206
+ function drawCore() {
207
+ const el = $('#coreChart'), m = by(coreM), W = Math.max(320, el.clientWidth || 900), H = W < 560 ? 240 : 310, L = 36, R = 12, T = 12, B = 26, x0 = 2015, x1 = 2026.9;
208
+ const pts = D.timeline[coreM].filter(r => r.x >= x0);
209
+ const s = svg(W, H, `${coreM}: accuracy and claim rate from ${x0}`); s.classList.add('live');
 
210
  const x = v => L + (v - x0) / (x1 - x0) * (W - L - R), y = v => T + (1 - v) * (H - T - B);
211
+ [0, .25, .5, .75, 1].forEach(v => { add(s, 'line', { x1: L, x2: W - R, y1: y(v), y2: y(v), stroke: 'rgba(14,84,70,.09)' }); add(s, 'text', { x: L - 7, y: y(v) + 4, 'text-anchor': 'end', 'font-size': 11, fill: 'var(--soft)' }, v.toFixed(2)); });
212
+ for (let yr = 2016; yr <= 2026; yr += W < 560 ? 2 : 1) add(s, 'text', { x: x(yr + .5), y: H - 7, 'text-anchor': 'middle', 'font-size': 11, fill: 'var(--soft)' }, yr);
213
+ add(s, 'line', { x1: L, x2: W - R, y1: y(CH), y2: y(CH), style: 'stroke:var(--chance)', 'stroke-width': 1.3, 'stroke-dasharray': '3 3' });
214
+ const kx = x(qX(m.know_end)), cx = x(qX(m.claim_end));
215
+ if (cx > kx) add(s, 'rect', { x: kx, y: T, width: cx - kx, height: H - T - B, style: 'fill:var(--says);opacity:.1' });
216
+ add(s, 'line', { x1: kx, x2: kx, y1: T, y2: H - B, style: 'stroke:var(--knows)', 'stroke-width': 2 });
217
+ add(s, 'line', { x1: cx, x2: cx, y1: T, y2: H - B, style: 'stroke:var(--says)', 'stroke-width': 2, 'stroke-dasharray': m.know_end === m.claim_end ? '5 5' : 'none' });
218
+ const path = k => pts.map((r, i) => `${i ? 'L' : 'M'}${x(r.x).toFixed(1)},${y(r[k]).toFixed(1)}`).join('');
219
+ [['claim', 'var(--says)'], ['acc', 'var(--knows)']].forEach(([k, c]) => {
220
+ const p = add(s, 'path', { d: path(k), fill: 'none', style: `stroke:${c}`, 'stroke-width': 2.4, 'stroke-linejoin': 'round' });
221
+ if (!reduce && drawCore.anim) { const n = 2600; p.style.strokeDasharray = n; p.style.strokeDashoffset = n; requestAnimationFrame(() => { p.style.transition = 'stroke-dashoffset 1.1s ease-out'; p.style.strokeDashoffset = 0; }); }
222
  });
223
+ drawCore.anim = false;
224
+ const cross = add(s, 'line', { y1: T, y2: H - B, stroke: 'rgba(16,32,28,.35)', visibility: 'hidden' });
225
+ const dA = add(s, 'circle', { r: 4.5, style: 'fill:var(--knows);stroke:#fff;stroke-width:1.5', visibility: 'hidden' }), dC = add(s, 'circle', { r: 4.5, style: 'fill:var(--says);stroke:#fff;stroke-width:1.5', visibility: 'hidden' });
 
 
 
 
 
 
 
 
 
 
 
226
  el.replaceChildren(s);
 
227
  s.addEventListener('pointermove', e => {
228
+ const b = s.getBoundingClientRect(), mx = (e.clientX - b.left) / b.width * W; let r = pts[0]; pts.forEach(p => { if (Math.abs(x(p.x) - mx) < Math.abs(x(r.x) - mx)) r = p; });
229
+ cross.setAttribute('x1', x(r.x)); cross.setAttribute('x2', x(r.x)); dA.setAttribute('cx', x(r.x)); dA.setAttribute('cy', y(r.acc)); dC.setAttribute('cx', x(r.x)); dC.setAttribute('cy', y(r.claim));
230
+ [cross, dA, dC].forEach(n => n.setAttribute('visibility', 'visible')); showTip(`<b>${r.p}</b> · ${r.n} facts<br>Accuracy ${f2(r.acc)}<br>Claim rate ${f2(r.claim)}`, e);
 
 
 
231
  });
232
+ s.addEventListener('pointerleave', () => { [cross, dA, dC].forEach(n => n.setAttribute('visibility', 'hidden')); hideTip(); });
233
+ $('#coreNote').textContent = `Yearly up to 2020 and quarterly after. ${coreM} knowledge ends ${m.know_end} and claims end ${m.claim_end}.`;
234
+ $$('#coreChips .chip').forEach(c => c.setAttribute('aria-selected', c.dataset.m === coreM));
235
  }
236
+ $('#coreChips').innerHTML = FRONT.map(m => `<button class="chip" role="tab" data-m="${m.model}" type="button">${m.model}</button>`).join('');
237
+ $$('#coreChips .chip').forEach(c => c.onclick = () => { coreM = c.dataset.m; drawCore.anim = true; drawCore(); });
 
 
 
238
 
239
  /* fact families */
240
+ (function fam() {
241
+ const names = Object.keys(D.family), order = ['Fed decisions', 'Macroeconomic', 'Stock prices'];
242
+ $('#fam').innerHTML = order.map(f => `<div class="card"><h3>${f}</h3><div class="fam-sub"><span><i style="background:var(--knows)"></i>Knows</span><span><i style="background:var(--says)"></i>Says</span></div>${names.map(m => {
243
+ const v = D.family[m][f]; return `<div class="fr"><span>${m}</span><div class="fr-b"><div class="fb k" data-tip="<b>${m}</b><br>Knows ${f2(v.know)}"><i style="width:${v.know * 100}%"></i>${f2(v.know)}</div><div class="fb c" data-tip="<b>${m}</b><br>Says ${f2(v.says)}"><i style="width:${v.says * 100}%"></i>${f2(v.says)}</div></div></div>`;
244
+ }).join('')}</div>`).join('');
245
+ })();
246
 
247
+ /* declared date toggle */
248
+ (function declared() {
249
+ const row = (m, k, c) => `<div class="hb-r"><span>${m.model}</span><div class="hb-t"><i data-a="${m[k]}" data-b="${m[k + '_declared']}" style="background:${c}"></i></div><span class="hb-v" data-a="${f2(m[k])}" data-b="${f2(m[k + '_declared'])}"></span></div>`;
250
+ $('#declC').innerHTML = M.map(m => row(m, 'claims', 'var(--says)')).join('');
251
+ $('#declA').innerHTML = M.map(m => row(m, 'acc', 'var(--knows)')).join('');
252
+ const sw = $('#dateSw');
253
+ const set = on => { sw.setAttribute('aria-checked', on); $('#dateCode').classList.toggle('on', on); $$('.hb-t i').forEach(i => i.style.width = (on ? i.dataset.b : i.dataset.a) * 100 + '%'); $$('.hb-v').forEach(v => v.textContent = on ? v.dataset.b : v.dataset.a); };
254
+ sw.onclick = () => set(sw.getAttribute('aria-checked') !== 'true');
255
+ set(false);
256
+ })();
257
 
258
  /* point-in-time */
259
  (function pit() {
260
  const lo = -.08, hi = .12, x = v => (v - lo) / (hi - lo) * 100;
261
  $('#pitT').innerHTML = `<thead><tr><th class="left">Model</th><th>Cutoff</th><th>Up to cutoff</th><th>After</th><th>Difference [95% CI]</th><th class="left">Interval around zero</th></tr></thead><tbody>` +
262
+ D.pit.map(r => `<tr><td class="left mname">${r.model}</td><td>${r.cutoff}</td><td>${f2(r.before)}</td><td>${f2(r.after)}</td><td>${f3(r.diff)}<span class="ci">[${f3(r.lo)}, ${f3(r.hi)}]</span></td><td class="left"><span class="forest" data-tip="<b>${r.model} ${r.cutoff}</b><br>Difference ${f3(r.diff)}"><s style="left:${x(0)}%"></s><i style="left:${x(r.lo)}%;width:${x(r.hi) - x(r.lo)}%"></i><b style="left:${x(r.diff)}%"></b></span></td></tr>`).join('') + '</tbody>';
 
 
263
  })();
264
 
265
+ const drawAll = () => { drawCalendar(); drawCore(); };
266
  drawAll();
267
  let rz; addEventListener('resize', () => { clearTimeout(rz); rz = setTimeout(drawAll, 120); });
data.js CHANGED
The diff for this file is too large to render. See raw diff
 
index.html CHANGED
@@ -3,127 +3,200 @@
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>FinDendro · Temporal Self-Knowledge Leaderboard · The Fin AI</title>
7
- <meta name="description" content="FinDendro measures what LLMs know about dated financial facts and what they say they know." />
8
  <link rel="preconnect" href="https://fonts.googleapis.com" />
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
- <link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;1,6..72,400;1,6..72,500&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
11
  <link rel="icon" href="./assets/finai-logo.png" />
12
  <link rel="stylesheet" href="./style.css" />
13
  </head>
14
  <body>
 
 
 
 
15
  <div class="tip" id="tip" role="tooltip"></div>
16
 
17
- <header class="header">
18
  <div class="wrap nav">
19
- <a href="#top" class="brand"><img src="./assets/finai-logo.png" alt="" />The Fin AI<span class="brand-sep">/</span><span class="brand-fd">FinDendro</span></a>
20
- <nav class="links">
21
- <a href="#leaderboard">Leaderboard</a>
22
- <a href="#timeline">Timeline</a>
23
- <a href="#families">Facts</a>
24
- <a href="#declared">Declared date</a>
25
- <a href="#pit">Point-in-time</a>
26
- <a href="#method">Method</a>
27
- </nav>
 
 
28
  </div>
29
  </header>
30
 
31
  <main id="top">
32
- <section class="hero">
33
- <svg class="rings" id="rings" aria-hidden="true" viewBox="-320 -320 640 640"></svg>
34
  <div class="wrap hero-grid">
35
  <div class="hero-copy">
36
- <p class="eyebrow">Benchmark for financial backtesting</p>
37
- <h1 class="wordmark">FinDendro</h1>
38
- <p class="hero-sub">Do LLMs know what they <em>know</em>?</p>
39
- <p class="lead">Backtests trust a model to say when its knowledge ends. FinDendro checks that claim against dated U.S. macro data, stock prices and Fed decisions, one quarter at a time.</p>
40
- <div class="stats" id="stats"></div>
41
- <div class="ctas">
42
- <a class="btn" href="#leaderboard">See the leaderboard</a>
43
- <a class="btn btn--ghost" href="https://huggingface.co/datasets/TheFinAI/FinDendro" target="_blank" rel="noreferrer">Dataset ↗</a>
44
  </div>
45
  </div>
46
- <figure class="card hero-viz">
47
- <figcaption>Knows vs says it knows</figcaption>
48
- <div class="legend">
49
- <span><i class="dot dot--knows"></i>Knows</span>
50
- <span><i class="dot dot--says"></i>Says it knows</span>
51
- <span><i class="tick"></i>Chance</span>
 
 
52
  </div>
53
- <div id="dumbbell"></div>
 
54
  </figure>
55
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  </section>
57
 
58
- <section class="section" id="leaderboard">
59
  <div class="wrap">
60
- <h2>Leaderboard</h2>
61
- <p class="sec-sub">Sorted by what each model knows. Click a column to re-sort.</p>
62
- <div class="scroll card card--flush"><table class="lb" id="lb"></table></div>
63
- <p class="note"><b>Knows</b> is accuracy on 1,340 controlled facts. <b>Says</b> is the share of answers that claim knowledge. <b>Acc | says</b> and <b>Acc | denies</b> split accuracy by that claim. <b>Gap</b> is their within-period difference with a 95% interval. Boundaries are not reported for the two small Qwen models, whose accuracy is too close to chance for the rule to locate an end.</p>
64
- <div class="findings" id="findings"></div>
 
 
 
 
 
65
  </div>
66
  </section>
67
 
68
- <section class="section section--tint" id="timeline">
69
  <div class="wrap">
70
- <h2>Where knowledge ends and claims end</h2>
71
- <p class="sec-sub">Accuracy and claim rate by period since 2017. Yearly up to 2020, quarterly after.</p>
72
- <div class="legend legend--wide">
73
- <span><i class="ln ln--knows"></i>Accuracy</span>
74
- <span><i class="ln ln--says"></i>Claim rate</span>
75
- <span><i class="vl vl--knows"></i>Knowledge ends</span>
76
- <span><i class="vl vl--says"></i>Claims end</span>
77
- <span><i class="vl vl--vendor"></i>Vendor cutoff</span>
78
- <span><i class="tick"></i>Chance</span>
 
 
 
 
 
 
 
 
 
79
  </div>
80
- <div class="multiples" id="multiples"></div>
81
  </div>
82
  </section>
83
 
84
- <section class="section" id="families">
85
  <div class="wrap">
86
- <h2>What kind of fact matters</h2>
87
- <p class="sec-sub">The same model can know prices it will not admit to knowing, and admit to Fed decisions it does know.</p>
88
- <div class="legend">
89
- <span><i class="dot dot--knows"></i>Knows</span>
90
- <span><i class="dot dot--says"></i>Says it knows</span>
91
  </div>
92
- <div class="fam" id="fam"></div>
93
  </div>
94
  </section>
95
 
96
- <section class="section section--tint" id="declared">
97
  <div class="wrap">
98
- <h2>A declared date changes the claim, not the knowledge</h2>
99
- <p class="sec-sub">Backtests fix the forecast origin with a line such as “Assume that today is May 31, 2023.” Each pair shows the answer without that line and with it, on the Predict wording.</p>
100
- <div class="decl" id="decl"></div>
 
 
 
 
 
 
 
 
 
 
101
  </div>
102
  </section>
103
 
104
- <section class="section" id="pit">
105
  <div class="wrap">
106
- <h2>Point-in-time models</h2>
107
- <p class="sec-sub">Models trained only on data before a cutoff are free of look-ahead by construction. At 1 to 1.5B parameters they retain almost none of these facts, so accuracy stays near chance on both sides of the cutoff.</p>
108
- <div class="scroll card card--flush"><table class="lb pit" id="pitT"></table></div>
109
- <p class="note">Scored by the likelihood of each candidate value, since these models do not follow a multiple-choice format. The difference is accuracy up to the cutoff year minus accuracy after it, with a 95% interval.</p>
 
 
110
  </div>
111
  </section>
112
 
113
- <section class="section section--tint" id="method">
114
- <div class="wrap method">
115
- <h2>Method</h2>
116
- <div class="m-grid">
117
- <div><h3>Facts</h3><p>2,226 dated U.S. facts sampled from a pool of 20,972, January 1999 to September 2026. Twelve FRED series, month-end prices of 50 large companies and Fed target rate decisions, all effectively unrevised after publication.</p></div>
118
- <div><h3>Questions</h3><p>Each fact yields five prompts that share one set of candidates. Task 2 asks for the value. Task 1 offers every candidate twice, once with “I know this” and once with “I do not know this”, so one choice reveals both the answer and the claim.</p></div>
119
- <div><h3>Controls</h3><p>47% of numerical facts can be answered by repeating last month's value, so they are excluded from knowledge. Fed decisions use balanced accuracy over three outcomes, since “unchanged” is right in 78% of months. Chance is about 0.27.</p></div>
120
- <div><h3>Boundaries</h3><p>Knowledge ends at the last quarter whose two-quarter rolling accuracy stays above the midpoint between the 2015 to 2020 level and chance. Claims end at the last quarter whose claim rate stays above half of its 2015 to 2020 level.</p></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  </div>
122
  </div>
123
  </section>
124
  </main>
125
 
126
- <footer class="foot"><div class="wrap">FinDendro · The Fin AI</div></footer>
127
  <script src="./data.js"></script>
128
  <script src="./app.js"></script>
129
  </body>
 
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>FinDendro · Do LLMs Know What They Know? · The Fin AI</title>
7
+ <meta name="description" content="FinDendro dates what LLMs know about financial history, and checks it against what they say they know." />
8
  <link rel="preconnect" href="https://fonts.googleapis.com" />
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
+ <link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,500;0,9..144,600;1,9..144,500&family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;1,6..72,400;1,6..72,500&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
11
  <link rel="icon" href="./assets/finai-logo.png" />
12
  <link rel="stylesheet" href="./style.css" />
13
  </head>
14
  <body>
15
+ <div class="progress" id="progress"></div>
16
+ <div class="glow" aria-hidden="true"></div>
17
+ <div class="edge edge-l" aria-hidden="true"><span class="e-name">FinDendro</span></div>
18
+ <div class="edge edge-r" aria-hidden="true"><span class="e-name">The Fin AI</span></div>
19
  <div class="tip" id="tip" role="tooltip"></div>
20
 
21
+ <header class="header" id="header">
22
  <div class="wrap nav">
23
+ <a href="https://thefin.ai/" class="brand" target="_blank" rel="noreferrer"><img src="./assets/finai-logo.png" alt="" />The Fin AI<span class="brand-sep">/</span><span class="brand-fd">FinDendro</span></a>
24
+ <div class="nav-right">
25
+ <nav class="links" id="navLinks">
26
+ <a href="#replay" data-sec="replay">Replay</a>
27
+ <a href="#leaderboard" data-sec="leaderboard">Leaderboard</a>
28
+ <a href="#core" data-sec="core">Core sample</a>
29
+ <a href="#date" data-sec="date">Declared date</a>
30
+ <a href="#benchmark" data-sec="benchmark">Benchmark</a>
31
+ </nav>
32
+ <a class="btn btn--sm" href="https://huggingface.co/datasets/TheFinAI/FinDendro" target="_blank" rel="noreferrer">Data <span class="arrow">↗</span></a>
33
+ </div>
34
  </div>
35
  </header>
36
 
37
  <main id="top">
38
+ <section class="hero" id="hero">
 
39
  <div class="wrap hero-grid">
40
  <div class="hero-copy">
41
+ <a class="award reveal" href="#replay"><span class="medal" id="bird" aria-hidden="true"></span>Temporal self-knowledge · <b>1999 to 2026, quarter by quarter</b> →</a>
42
+ <h1 class="wordmark reveal">FinDendro</h1>
43
+ <p class="hero-sub reveal">A tree records every year it lived.<br />An LLM <em>can't tell you</em> what it remembers.</p>
44
+ <p class="lead reveal">Dendrochronologists date wood by reading its rings. We date models the same way. Every model, <span class="rotator" id="rotator"></span>, answers dated questions about markets and says whether it knows each answer. Then we check both.</p>
45
+ <div class="stats reveal" id="stats"></div>
46
+ <div class="ctas reveal">
47
+ <a class="btn btn--lg" href="#replay">Try to beat a model</a>
48
+ <a class="btn btn--lg btn--ghost" href="#leaderboard">See who knows</a>
49
  </div>
50
  </div>
51
+
52
+ <figure class="hero-viz reveal">
53
+ <figcaption><span>Cross-section · one ring per period</span><a href="#core">Read the core →</a></figcaption>
54
+ <div class="xs-chips" id="xsChips" role="tablist" aria-label="Choose a model"></div>
55
+ <div class="xs-wrap">
56
+ <div class="xs-side xs-side--l"><b>Says</b><span>claims it knows</span></div>
57
+ <div class="xs" id="xs"></div>
58
+ <div class="xs-side xs-side--r"><b>Knows</b><span>answers correctly</span></div>
59
  </div>
60
+ <div class="xs-ends" id="xsEnds"></div>
61
+ <p class="hero-claim">“I don't know” is <em>not</em> evidence that a model doesn't know.</p>
62
  </figure>
63
  </div>
64
+ <div class="ticker" id="ticker" aria-label="Leaderboard ticker"><div class="ticker-track" id="tickerTrack"></div></div>
65
+ </section>
66
+
67
+ <section class="section" id="replay">
68
+ <div class="wrap">
69
+ <div class="sec-head reveal">
70
+ <span class="kicker">Replay</span>
71
+ <h2>It said it <em>didn't know</em>. Then it named the price.</h2>
72
+ <p class="note">Real questions from the benchmark and the models' recorded answers. Make your pick first, then see what each model chose and whether it claimed to know.</p>
73
+ </div>
74
+ <div class="game reveal">
75
+ <article class="q-card">
76
+ <div class="q-top"><span class="q-kick" id="qKick"></span><span class="q-n" id="qN"></span></div>
77
+ <p class="q-text" id="qText"></p>
78
+ <div class="q-opts" id="qOpts"></div>
79
+ <div class="q-foot">
80
+ <button class="btn btn--ghost btn--sm" id="qReveal" type="button">Reveal without guessing</button>
81
+ <button class="btn btn--sm" id="qNext" type="button" hidden>Next fact →</button>
82
+ </div>
83
+ </article>
84
+ <aside class="a-card">
85
+ <div class="a-head"><span>What the models said</span><span class="a-score" id="aScore"></span></div>
86
+ <div class="a-rows" id="aRows"></div>
87
+ <p class="a-punch" id="aPunch" hidden></p>
88
+ </aside>
89
+ </div>
90
+ </div>
91
  </section>
92
 
93
+ <section class="section section--tint" id="leaderboard">
94
  <div class="wrap">
95
+ <div class="sec-head reveal">
96
+ <span class="kicker">Leaderboard</span>
97
+ <h2>Who <em>knows</em>, and who <em>admits</em> it</h2>
98
+ <p class="note">Open a row for the breakdown. Click a heading to rank by it.</p>
99
+ </div>
100
+ <div class="board reveal"><div class="scroll"><table class="lb" id="lb"></table></div></div>
101
+ <div class="keys reveal">
102
+ <p><b>Knows</b> accuracy on 1,340 controlled facts, chance about 0.27. <b>Says</b> share of answers claiming knowledge. <b>Right when denying</b> accuracy on facts the model says it does not know. <b>Gap</b> how much better it does when it claims to know, inside each period.</p>
103
+ <p class="ramp-key">Darker green means higher. <span class="ramp"></span></p>
104
+ </div>
105
  </div>
106
  </section>
107
 
108
+ <section class="section" id="core">
109
  <div class="wrap">
110
+ <div class="sec-head reveal">
111
+ <span class="kicker">Core sample</span>
112
+ <h2>The last ring and the last <em>claim</em> can be years apart</h2>
113
+ <p class="note">One rule places every boundary. Knowledge ends where accuracy falls halfway to chance. Claims end where the claim rate halves.</p>
114
+ </div>
115
+ <div class="cal card reveal">
116
+ <div class="legend">
117
+ <span><i class="mk mk--k"></i>Knowledge ends</span><span><i class="mk mk--c"></i>Claims end</span><span><i class="mk mk--s"></i>Says when asked</span><span><i class="mk mk--v"></i>Vendor cutoff</span><span><i class="band"></i>Claiming without knowing</span>
118
+ </div>
119
+ <div id="calendar"></div>
120
+ </div>
121
+ <div class="big card reveal">
122
+ <div class="big-top">
123
+ <div class="pick" id="coreChips" role="tablist" aria-label="Choose a model"></div>
124
+ <div class="legend"><span><i class="ln ln--k"></i>Accuracy</span><span><i class="ln ln--c"></i>Claim rate</span><span><i class="tick"></i>Chance</span></div>
125
+ </div>
126
+ <div class="chart" id="coreChart"></div>
127
+ <p class="note" id="coreNote"></p>
128
  </div>
 
129
  </div>
130
  </section>
131
 
132
+ <section class="section section--tint" id="grain">
133
  <div class="wrap">
134
+ <div class="sec-head reveal">
135
+ <span class="kicker">Grain</span>
136
+ <h2>Models admit the <em>Fed</em> and hide the <em>prices</em></h2>
137
+ <p class="note">Frontier models claim knowledge of most Fed decisions and almost no stock prices, yet they recall prices well above what they admit.</p>
 
138
  </div>
139
+ <div class="fam reveal" id="fam"></div>
140
  </div>
141
  </section>
142
 
143
+ <section class="section" id="date">
144
  <div class="wrap">
145
+ <div class="sec-head reveal">
146
+ <span class="kicker">Declared date</span>
147
+ <h2>A date changes the <em>words</em>, not the <em>knowledge</em></h2>
148
+ <p class="note">Backtests fix the forecast origin with one line. Add it and watch what moves.</p>
149
+ </div>
150
+ <div class="prompt-line reveal">
151
+ <button class="sw" id="dateSw" type="button" role="switch" aria-checked="false"><i></i></button>
152
+ <code id="dateCode"><span id="datePrefix">Assume that today is May 31, 2023. Answer as you would on that date.</span></code>
153
+ </div>
154
+ <div class="decl reveal">
155
+ <div class="card"><h3>Claims to already know</h3><div class="hb" id="declC"></div></div>
156
+ <div class="card"><h3>Accuracy on the value</h3><div class="hb" id="declA"></div></div>
157
+ </div>
158
  </div>
159
  </section>
160
 
161
+ <section class="section section--tint" id="pit">
162
  <div class="wrap">
163
+ <div class="sec-head reveal">
164
+ <span class="kicker">Saplings</span>
165
+ <h2>Clean by construction, <em>empty</em> by construction</h2>
166
+ <p class="note">Point-in-time models stop reading at a cutoff, so they cannot leak. At 1 to 1.5B parameters they also retain almost none of these facts. Scored by candidate likelihood, accuracy sits near chance on both sides of the cutoff.</p>
167
+ </div>
168
+ <div class="board reveal"><div class="scroll"><table class="lb pit" id="pitT"></table></div></div>
169
  </div>
170
  </section>
171
 
172
+ <section class="section" id="benchmark">
173
+ <div class="wrap">
174
+ <div class="sec-head reveal">
175
+ <span class="kicker">Benchmark</span>
176
+ <h2><em>One</em> fact, <em>one</em> choice, two answers</h2>
177
+ <p class="note">Every option pairs a value with a statement of knowledge, so a single letter reveals both what the model thinks and whether it claims to know.</p>
178
+ </div>
179
+ <div class="how reveal">
180
+ <div class="eg card">
181
+ <p class="eg-q">What was the 12-month percent change in the US CPI for June 2023?</p>
182
+ <div class="eg-grid">
183
+ <span class="eg-h">“I know this value”</span><span class="eg-h">“I do not know, best guess”</span>
184
+ <span>A. 2.8%</span><span>E. 2.8%</span><span class="eg-hit">B. 3.0%</span><span class="eg-hit">F. 3.0%</span><span>C. 3.2%</span><span>G. 3.2%</span><span>D. 3.4%</span><span>H. 3.4%</span>
185
+ </div>
186
+ <p class="note">The published value is shaded. The model picks one letter out of eight.</p>
187
+ </div>
188
+ <div class="m-grid">
189
+ <div><h3>Facts</h3><p>2,226 dated U.S. facts drawn from a pool of 20,972. Twelve FRED series, month-end prices of 50 companies and Fed decisions, scored against the value published at the time.</p></div>
190
+ <div><h3>Questions</h3><p>Five prompts per fact. One asks for the value alone. Four offer the paired options, as a recall or a prediction, with and without a declared date.</p></div>
191
+ <div><h3>Controls</h3><p>47% of numerical facts fall to repeating last month's value, so they do not count toward knowledge. Fed decisions use balanced accuracy.</p></div>
192
+ <div><h3>Boundaries</h3><p>One rule for every model, from two-quarter rolling accuracy and the claim rate, each against its 2015 to 2020 level.</p></div>
193
+ </div>
194
  </div>
195
  </div>
196
  </section>
197
  </main>
198
 
199
+ <footer class="foot"><div class="wrap foot-in"><span>FinDendro · The Fin AI</span><a href="https://huggingface.co/datasets/TheFinAI/FinDendro" target="_blank" rel="noreferrer">Questions and raw results ↗</a></div></footer>
200
  <script src="./data.js"></script>
201
  <script src="./app.js"></script>
202
  </body>
style.css CHANGED
@@ -1,134 +1,259 @@
1
- /* FinDendro leaderboard. Fin AI house tokens, with the house blue as accent.
2
- Knows is blue and Says is orange everywhere on the page. */
3
  :root{
4
- --ink:#0f1528; --soft:#4f5a6e; --muted:#7a8496; --line:rgba(24,56,152,.11);
5
- --paper:#fcfcfd; --paper-2:#f4f6fa;
6
- --ring:0 0 0 1px rgba(24,56,152,.08),0 1px 2px rgba(20,30,80,.04),0 10px 28px -18px rgba(24,56,152,.2);
7
- --g:linear-gradient(90deg,#183898,#0060b0 55%,#68a8d8);
8
- --knows:#1f4fb8; --says:#e0662a; --chance:#8a93a3; --vendor:#5d6678;
9
- --serif:'Newsreader',Georgia,serif; --sans:'Inter',system-ui,-apple-system,sans-serif; --mono:'JetBrains Mono',ui-monospace,Menlo,monospace;
10
- --wrap:1180px;
 
11
  }
12
  *{box-sizing:border-box}
13
- html{scroll-behavior:smooth;scroll-padding-top:72px}
14
- body{margin:0;font-family:var(--sans);color:var(--ink);background:var(--paper);font-size:16px;line-height:1.6;-webkit-font-smoothing:antialiased}
15
- h1,h2,h3{font-family:var(--serif);font-weight:500;line-height:1.12;letter-spacing:-.02em;margin:0;text-wrap:balance}
 
16
  p{margin:0;color:var(--soft)}
17
  a{color:inherit;text-decoration:none}
18
- a:focus-visible,button:focus-visible,th:focus-visible{outline:2px solid var(--knows);outline-offset:2px}
 
 
 
 
19
  .wrap{width:100%;max-width:var(--wrap);margin:0 auto;padding-inline:32px}
20
- .header{position:sticky;top:0;z-index:50;background:rgba(252,252,253,.86);backdrop-filter:blur(14px) saturate(1.4);-webkit-backdrop-filter:blur(14px) saturate(1.4);border-bottom:1px solid var(--line)}
21
- .nav{display:flex;align-items:center;justify-content:space-between;gap:16px;min-height:60px;flex-wrap:wrap}
22
- .brand{display:inline-flex;align-items:center;gap:10px;font-family:var(--serif);font-size:1.15rem;font-weight:500;white-space:nowrap}
23
- .brand img{height:22px;width:auto}
24
- .brand-sep{color:var(--muted)}
25
- .brand-fd{background:var(--g);-webkit-background-clip:text;background-clip:text;color:transparent;font-weight:500}
26
- .links{display:flex;gap:2px;flex-wrap:wrap}
27
- .links a{font-size:.86rem;color:var(--soft);padding:6px 12px;border-radius:999px}
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  .links a:hover{background:var(--paper-2);color:var(--ink)}
 
 
 
 
 
 
 
 
 
 
 
29
 
30
- .hero{position:relative;overflow:hidden;padding-block:64px 56px}
31
- .rings{position:absolute;right:-180px;top:-200px;width:820px;height:820px;pointer-events:none;opacity:.65}
32
- .hero-grid{position:relative;display:grid;grid-template-columns:1.05fr 1fr;gap:56px;align-items:center}
33
  .hero-grid>*{min-width:0}
34
- .eyebrow{font:500 .76rem var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
35
- .wordmark{font-size:clamp(3.2rem,7.4vw,5.4rem);margin-top:10px;background:var(--g);-webkit-background-clip:text;background-clip:text;color:transparent;padding-bottom:.06em}
36
- .hero-sub{font-family:var(--serif);font-size:clamp(1.4rem,2.5vw,1.8rem);color:var(--ink);margin-top:2px}
37
- .hero-sub em{color:var(--knows)}
38
- .lead{margin-top:16px;font-size:1.05rem;max-width:34em}
39
- .stats{display:grid;grid-template-columns:repeat(4,auto);justify-content:start;gap:12px 34px;margin-top:26px}
40
- .stat b{display:block;font:500 1.65rem var(--serif);color:var(--ink);font-variant-numeric:tabular-nums;line-height:1.15}
41
- .stat span{font-size:.8rem;color:var(--muted)}
42
- .ctas{display:flex;gap:10px;margin-top:28px;flex-wrap:wrap}
43
- .btn{display:inline-flex;align-items:center;font-size:.9rem;font-weight:500;padding:10px 20px;border-radius:999px;background:var(--ink);color:#fff}
44
- .btn:hover{background:#183898}
45
- .btn--ghost{background:transparent;color:var(--ink);box-shadow:inset 0 0 0 1px var(--line)}
46
- .btn--ghost:hover{background:var(--paper-2);color:var(--ink)}
47
-
48
- .card{background:#fff;border-radius:14px;box-shadow:var(--ring);padding:22px 24px}
49
- .card--flush{padding:0}
50
- figure{margin:0}
51
- figcaption{font:500 1.15rem var(--serif);color:var(--ink)}
52
- .legend{display:flex;flex-wrap:wrap;gap:6px 18px;margin-top:8px;font-size:.8rem;color:var(--soft)}
53
- .legend span{display:inline-flex;align-items:center;gap:7px}
54
- .legend--wide{margin-block:18px 16px}
55
- .dot{width:10px;height:10px;border-radius:50%;display:inline-block}
56
- .dot--knows{background:var(--knows)}.dot--says{background:var(--says)}
57
- .tick{width:0;height:13px;border-left:1.5px dashed var(--chance);display:inline-block}
58
- .ln{width:18px;height:2.5px;border-radius:2px;display:inline-block}
59
- .ln--knows{background:var(--knows)}.ln--says{background:var(--says)}
60
- .vl{width:0;height:13px;display:inline-block;border-left:2px solid}
61
- .vl--knows{border-color:var(--knows)}.vl--says{border-color:var(--says)}.vl--vendor{border-left:2px dotted var(--vendor)}
62
- #dumbbell{margin-top:6px}
63
- svg text{font-family:var(--sans)}
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
- .section{padding-block:60px}
66
- .section--tint{background:var(--paper-2)}
67
- h2{font-size:clamp(1.7rem,3vw,2.3rem)}
68
- .sec-sub{margin-top:8px;max-width:48em}
69
- #leaderboard .scroll,#pit .scroll{margin-top:22px}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  .scroll{overflow-x:auto}
71
- table.lb{border-collapse:collapse;width:100%;font-size:.9rem;font-variant-numeric:tabular-nums;min-width:920px}
72
- .lb th{font:500 .74rem var(--sans);color:var(--muted);text-align:right;padding:13px 9px;border-bottom:1px solid var(--line);white-space:nowrap;user-select:none}
73
- .lb th[data-k]{cursor:pointer}
74
- .lb th[data-k]:hover{color:var(--ink)}
75
- .lb th.on{color:var(--ink)}
76
- .lb th.left,.lb td.left{text-align:left}
77
- .lb td{padding:11px 9px;text-align:right;border-bottom:1px solid var(--line);color:var(--ink);white-space:nowrap}
78
- .lb tr:last-child td{border-bottom:0}
79
- .lb tbody tr:hover{background:var(--paper-2)}
80
- .rank{color:var(--muted);font-family:var(--mono);font-size:.8rem}
81
- .mname{font-weight:500}
82
- .vendor{color:var(--muted);font-size:.78rem;margin-left:7px}
83
- .chip{font:500 .66rem var(--mono);color:var(--soft);background:var(--paper-2);border-radius:6px;padding:1px 6px;margin-left:7px}
84
- .bar{display:inline-flex;align-items:center;gap:9px;justify-content:flex-end}
85
- .bar i{position:relative;display:block;width:60px;height:7px;background:var(--paper-2);border-radius:4px}
86
- .bar i b{position:absolute;left:0;top:0;bottom:0;border-radius:4px;background:var(--knows)}
87
- .bar i u{position:absolute;top:-3px;bottom:-3px;border-left:1.5px dashed var(--chance)}
88
- .ci{color:var(--muted);font-size:.8rem;margin-left:4px}
89
- .flag{color:#b43c12;font-weight:500}
90
  .na{color:var(--muted)}
91
- .note{margin-top:12px;font-size:.84rem;max-width:62em}
92
- .note b{color:var(--ink);font-weight:500}
93
- .findings{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:26px}
94
- .findings>div{min-width:0}
95
- .findings .num{font:500 1.55rem var(--serif);color:var(--ink);line-height:1.2}
96
- .findings p{margin-top:6px;font-size:.9rem}
97
-
98
- .multiples{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}
99
- .panel h3{font-size:1.2rem}
100
- .panel .ends{font-size:.8rem;color:var(--soft);margin-top:3px}
101
- .panel .ends b{font-weight:500;color:var(--ink);font-variant-numeric:tabular-nums}
102
- .chart{position:relative;margin-top:6px}
103
-
104
- .fam{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;margin-top:18px}
105
- .fam h3{font-size:1.15rem;margin-bottom:6px}
106
-
107
- .decl{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px;margin-top:22px}
108
-
109
- .m-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:26px 44px;margin-top:22px}
110
- .m-grid h3{font-size:1.15rem;margin-bottom:6px}
111
- .m-grid p{font-size:.93rem}
112
- .foot{padding-block:26px;font-size:.82rem;color:var(--muted)}
113
-
114
- .forest{position:relative;display:block;width:190px;height:16px}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  .forest s{position:absolute;top:0;bottom:0;border-left:1.5px dashed var(--chance)}
116
  .forest i{position:absolute;top:7px;height:2px;background:var(--knows);border-radius:2px}
117
  .forest b{position:absolute;top:4px;width:8px;height:8px;margin-left:-4px;border-radius:50%;background:var(--knows)}
118
- table.pit{min-width:640px}
119
 
120
- .tip{position:fixed;z-index:70;pointer-events:none;background:#0f1528;color:#fff;font-size:.78rem;line-height:1.45;padding:7px 10px;border-radius:8px;max-width:240px;opacity:0;transition:opacity .12s;left:0;top:0;font-variant-numeric:tabular-nums}
121
- .tip.show{opacity:1}
122
- .tip b{font-weight:600}
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
- @media (max-width:900px){
 
 
 
125
  .wrap{padding-left:max(16px,env(safe-area-inset-left,0px));padding-right:max(16px,env(safe-area-inset-right,0px))}
126
- .header{position:static}
127
- .hero{padding-block:40px}
128
- .hero-grid,.multiples,.fam,.decl,.m-grid,.findings{grid-template-columns:minmax(0,1fr)}
129
- .hero-grid{gap:30px}
130
- .stats{grid-template-columns:repeat(2,auto)}
131
- .rings{opacity:.4;right:-320px}
132
- .section{padding-block:42px}
 
 
 
 
 
 
 
133
  }
134
- @media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
 
1
+ /* FinDendro leaderboard. Fin AI house structure; the accent is a heartwood-to-sapwood gradient.
2
+ Knows is heartwood teal and Says is rust, everywhere on the page. */
3
  :root{
4
+ --ink:#10201c; --soft:#4b5c58; --muted:#7f8d89; --line:rgba(14,84,70,.1);
5
+ --paper:#fcfcfa; --paper-2:#f3f6f1;
6
+ --ring:0 0 0 1px rgba(14,84,70,.08),0 1px 2px rgba(10,50,40,.04),0 10px 28px -18px rgba(14,84,70,.22);
7
+ --g:linear-gradient(90deg,#0a5d55,#2c9a6e 58%,#d9962b);
8
+ --g-soft:linear-gradient(90deg,rgba(10,125,116,.08),rgba(217,150,43,.1));
9
+ --knows:#0a7d74; --says:#d9622b; --chance:#8d9894; --vendor:#5d6a66; --wood:#efe6d3;
10
+ --serif:'Newsreader',Georgia,serif; --brand:'Fraunces','Newsreader',Georgia,serif; --sans:'Inter',system-ui,-apple-system,sans-serif; --mono:'JetBrains Mono',ui-monospace,Menlo,monospace;
11
+ --wrap:1180px; --ease:cubic-bezier(.22,1,.36,1);
12
  }
13
  *{box-sizing:border-box}
14
+ [hidden]{display:none!important}
15
+ html{scroll-behavior:smooth;scroll-padding-top:74px}
16
+ body{margin:0;font-family:var(--sans);color:var(--ink);background:var(--paper);font-size:17px;line-height:1.6;-webkit-font-smoothing:antialiased}
17
+ h1,h2,h3{font-family:var(--serif);font-weight:500;line-height:1.1;letter-spacing:-.02em;margin:0;text-wrap:balance}
18
  p{margin:0;color:var(--soft)}
19
  a{color:inherit;text-decoration:none}
20
+ button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
21
+ :focus-visible{outline:2px solid var(--knows);outline-offset:2px}
22
+ svg text{font-family:var(--sans)}
23
+ ::selection{background:#0a5d55;color:#fff}
24
+ figure{margin:0}
25
  .wrap{width:100%;max-width:var(--wrap);margin:0 auto;padding-inline:32px}
26
+
27
+ .glow{position:fixed;inset:0;z-index:-1;pointer-events:none;background:radial-gradient(66vw 80vh at 18% 12%,rgba(10,125,116,.07),transparent 70%),radial-gradient(60vw 70vh at 88% 30%,rgba(217,150,43,.08),transparent 70%)}
28
+ .edge{position:fixed;top:50%;z-index:45;pointer-events:none;display:flex;align-items:center;gap:18px;white-space:nowrap}
29
+ .edge-l{left:24px;transform:translate(-50%,-50%) rotate(-90deg)}
30
+ .edge-r{right:24px;transform:translate(50%,-50%) rotate(90deg)}
31
+ .edge .e-name{font:600 11px var(--sans);letter-spacing:.42em;text-transform:uppercase;background:var(--g);-webkit-background-clip:text;background-clip:text;color:transparent}
32
+ .edge::before,.edge::after{content:"";display:block;width:8vh;height:1px;background:linear-gradient(90deg,transparent,rgba(10,125,116,.26))}
33
+ .edge::after{background:linear-gradient(90deg,rgba(10,125,116,.26),transparent)}
34
+ @media (max-width:1320px){.edge{display:none}}
35
+
36
+ .progress{position:fixed;inset:0 0 auto 0;height:2px;z-index:60;transform-origin:0 50%;transform:scaleX(0);background:var(--g)}
37
+ .header{position:fixed;top:0;left:0;right:0;z-index:50;transition:background .4s var(--ease),box-shadow .4s}
38
+ .header.scrolled{background:rgba(252,252,250,.82);backdrop-filter:blur(14px) saturate(1.4);-webkit-backdrop-filter:blur(14px) saturate(1.4);box-shadow:0 1px 0 var(--line)}
39
+ .nav{display:flex;align-items:center;justify-content:space-between;height:64px;gap:14px}
40
+ .brand{display:inline-flex;align-items:center;gap:10px;font-family:var(--serif);font-size:1.2rem;font-weight:500;white-space:nowrap}
41
+ .brand img{height:24px;width:auto}
42
+ .brand-sep{color:var(--muted);font-weight:400}
43
+ .brand-fd{font-family:var(--brand);font-size:1.02rem;font-weight:600;background:var(--g);-webkit-background-clip:text;background-clip:text;color:transparent}
44
+ .nav-right{display:flex;align-items:center;gap:10px}
45
+ .links{display:flex;gap:2px}
46
+ .links a{font-size:.88rem;color:var(--soft);padding:7px 13px;border-radius:999px;transition:background .25s,color .25s}
47
  .links a:hover{background:var(--paper-2);color:var(--ink)}
48
+ .links a.active{background:var(--g-soft);color:var(--ink)}
49
+
50
+ .btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;font-size:.92rem;font-weight:500;padding:10px 20px;border-radius:999px;background:var(--ink);color:#fff;transition:box-shadow .3s,transform .2s,background .2s}
51
+ .btn:hover{box-shadow:0 10px 30px -12px rgba(10,125,116,.65);transform:translateY(-1px)}
52
+ .btn--sm{padding:7px 15px;font-size:.84rem}
53
+ .btn--lg{padding:14px 27px;font-size:1rem}
54
+ .btn--ghost{background:#fff;color:var(--ink);box-shadow:inset 0 0 0 1px var(--line)}
55
+ .btn--ghost:hover{background:var(--paper-2)}
56
+
57
+ .reveal{transition:opacity .75s var(--ease),transform .75s var(--ease)}
58
+ .js .reveal:not(.in){opacity:0;transform:translateY(16px)}
59
 
60
+ .hero{position:relative;padding-top:118px;overflow:hidden}
61
+ .hero-grid{display:grid;grid-template-columns:1.06fr 1fr;gap:54px;align-items:center;padding-bottom:40px}
 
62
  .hero-grid>*{min-width:0}
63
+ .award{display:inline-flex;align-items:center;gap:9px;font-size:.84rem;color:var(--soft);padding:5px 14px 5px 6px;border-radius:999px;background:#fff;box-shadow:var(--ring)}
64
+ .award b{font-weight:500;background:var(--g);-webkit-background-clip:text;background-clip:text;color:transparent}
65
+ .medal{width:26px;height:26px;border-radius:50%;display:inline-grid;place-items:center;background:#e9f1ea;overflow:hidden}
66
+ .medal canvas{width:22px;height:22px;image-rendering:pixelated}
67
+ .wordmark{font-family:var(--brand);font-weight:600;font-size:clamp(3.6rem,8.3vw,6.3rem);letter-spacing:-.025em;line-height:1;margin-top:20px;padding-bottom:.08em;background:var(--g);-webkit-background-clip:text;background-clip:text;color:transparent}
68
+ .hero-sub{font-family:var(--serif);font-size:clamp(1.4rem,2.5vw,1.8rem);line-height:1.24;color:var(--ink);margin-top:10px}
69
+ .hero-sub em{background:var(--g);-webkit-background-clip:text;background-clip:text;color:transparent;padding-right:.06em}
70
+ .lead{margin-top:16px;max-width:33em;font-size:1.02rem}
71
+ .rotator{display:inline-block;color:var(--ink);font-weight:500;border-bottom:1.5px solid var(--knows);padding-inline:2px;min-width:7.4em;text-align:center;transition:opacity .25s,filter .25s}
72
+ .rotator.off{opacity:0;filter:blur(5px)}
73
+ .stats{display:flex;flex-wrap:wrap;gap:12px 36px;margin-top:24px}
74
+ .stat b{display:block;font:500 2.05rem/1.1 var(--serif);font-variant-numeric:tabular-nums}
75
+ .stat span{font-size:.78rem;color:var(--muted)}
76
+ .ctas{display:flex;gap:12px;margin-top:26px;flex-wrap:wrap}
77
+
78
+ .hero-viz{background:rgba(255,255,255,.86);border-radius:20px;box-shadow:var(--ring);padding:20px 22px 18px}
79
+ .hero-viz figcaption{display:flex;justify-content:space-between;gap:10px;font:500 .72rem var(--mono);letter-spacing:.07em;text-transform:uppercase;color:var(--muted)}
80
+ .hero-viz figcaption a{font:500 .86rem var(--sans);letter-spacing:0;text-transform:none;background:var(--g);-webkit-background-clip:text;background-clip:text;color:transparent}
81
+ .xs-chips,.pick{display:flex;flex-wrap:wrap;gap:6px}
82
+ .xs-chips{margin-top:12px}
83
+ .chip{font-size:.8rem;padding:5px 12px;border-radius:999px;background:#fff;box-shadow:inset 0 0 0 1px var(--line);color:var(--soft);transition:background .2s,color .2s}
84
+ .chip:hover{color:var(--ink)}
85
+ .chip[aria-selected="true"]{background:var(--ink);color:#fff;box-shadow:none}
86
+ .xs-wrap{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:6px;margin-top:6px}
87
+ .xs{position:relative;max-width:330px;margin:0 auto;width:100%}
88
+ .xs-side{font-size:.72rem;color:var(--muted);line-height:1.25;width:64px}
89
+ .xs-side b{display:block;font:500 1.15rem var(--serif);letter-spacing:-.01em}
90
+ .xs-side--l{text-align:right}.xs-side--l b{color:var(--says)}
91
+ .xs-side--r b{color:var(--knows)}
92
+ .xs-ends{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-top:6px;border-top:1px solid var(--line);padding-top:11px;font-size:.72rem;color:var(--muted);line-height:1.3}
93
+ .xs-ends b{display:block;font:500 1rem var(--serif);color:var(--ink);font-variant-numeric:tabular-nums}
94
+ .hero-claim{margin-top:12px;font:400 1.08rem var(--serif);color:var(--ink);text-align:center}
95
+ .hero-claim em{color:var(--says)}
96
+
97
+ .ticker{border-block:1px solid var(--line);overflow:hidden;background:rgba(255,255,255,.6);padding-block:11px}
98
+ .ticker-track{display:flex;width:max-content;gap:44px;animation:tick 42s linear infinite}
99
+ .ticker:hover .ticker-track{animation-play-state:paused}
100
+ .tk{font-size:.84rem;white-space:nowrap;color:var(--soft);font-variant-numeric:tabular-nums}
101
+ .tk i{font:500 .74rem var(--mono);color:var(--muted);font-style:normal;margin-right:7px}
102
+ .tk b{font-weight:500;color:var(--ink);margin-right:8px}
103
+ .tk u{text-decoration:none;color:var(--knows);font-weight:500}.tk s{text-decoration:none;color:var(--says);font-weight:500}
104
+ @keyframes tick{to{transform:translateX(-50%)}}
105
 
106
+ .section{padding-block:76px}
107
+ .section--tint{background:linear-gradient(180deg,rgba(243,246,241,.85),rgba(243,246,241,.45))}
108
+ .sec-head{margin-bottom:28px}
109
+ .kicker{display:inline-flex;align-items:center;gap:10px;font:500 .74rem var(--mono);letter-spacing:.14em;text-transform:uppercase;color:var(--knows)}
110
+ .kicker::before{content:"";width:26px;height:1.5px;background:var(--g)}
111
+ h2{font-size:clamp(2rem,4.1vw,3rem);margin-top:9px;max-width:21em}
112
+ h2 em{background:var(--g);-webkit-background-clip:text;background-clip:text;color:transparent;padding-right:.08em}
113
+ .note{margin-top:11px;max-width:45em;font-size:.97rem}
114
+ .card{background:#fff;border-radius:16px;box-shadow:var(--ring);padding:22px 24px}
115
+ .card h3{font-size:1.22rem}
116
+
117
+ .game{display:grid;grid-template-columns:1.12fr 1fr;gap:16px;align-items:stretch}
118
+ .game>*{min-width:0}
119
+ .q-card,.a-card{background:#fff;border-radius:18px;box-shadow:var(--ring);padding:24px 26px;display:flex;flex-direction:column}
120
+ .q-top{display:flex;justify-content:space-between;gap:10px;font:500 .72rem var(--mono);letter-spacing:.07em;text-transform:uppercase;color:var(--muted)}
121
+ .q-text{font:400 1.32rem/1.32 var(--serif);color:var(--ink);margin-top:12px;min-height:5.3em}
122
+ .q-opts{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:12px}
123
+ .q-opt{padding:13px 14px;border-radius:12px;box-shadow:inset 0 0 0 1px var(--line);text-align:left;font-size:1rem;font-variant-numeric:tabular-nums;transition:background .18s,box-shadow .18s;background:#fff;min-height:50px;display:flex;justify-content:space-between;align-items:center;gap:8px}
124
+ .q-opt:hover:not(:disabled){background:var(--paper-2)}
125
+ .q-opt:disabled{cursor:default;color:var(--soft)}
126
+ .q-opt.you{box-shadow:inset 0 0 0 2px var(--ink);color:var(--ink)}
127
+ .q-opt.right{background:#e3f2ee;box-shadow:inset 0 0 0 2px var(--knows);color:var(--ink);font-weight:500}
128
+ .q-opt small{font:500 .68rem var(--mono);color:var(--muted)}
129
+ .q-foot{display:flex;justify-content:flex-end;gap:8px;margin-top:auto;padding-top:16px}
130
+ .a-head{display:flex;justify-content:space-between;gap:10px;font:500 .72rem var(--mono);letter-spacing:.07em;text-transform:uppercase;color:var(--muted)}
131
+ .a-score{letter-spacing:0;text-transform:none;font-variant-numeric:tabular-nums}
132
+ .a-rows{display:flex;flex-direction:column;margin-top:8px}
133
+ .a-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:2px 10px;padding:11px 0;border-bottom:1px solid var(--line)}
134
+ .a-row:last-child{border-bottom:0}
135
+ .a-m{font-weight:500;font-size:.95rem}
136
+ .a-v{text-align:right;font-variant-numeric:tabular-nums;font-size:.95rem}
137
+ .a-s{font:400 1.02rem var(--serif);color:var(--soft);min-height:1.5em}
138
+ .a-s.know{color:var(--knows)}.a-s.deny{color:var(--says)}
139
+ .a-tally{text-align:right;font:500 .72rem var(--mono);color:var(--muted);align-self:center}
140
+ .a-v.ok{color:var(--knows);font-weight:500}.a-v.no{color:var(--muted);text-decoration:line-through}
141
+ .wait{color:var(--muted)}
142
+ .a-punch{margin-top:auto;padding-top:12px;font:400 1.14rem/1.3 var(--serif);color:var(--ink)}
143
+
144
+ .board{background:#fff;border-radius:18px;box-shadow:var(--ring);overflow:hidden}
145
  .scroll{overflow-x:auto}
146
+ table.lb{border-collapse:collapse;width:100%;font-size:.94rem;font-variant-numeric:tabular-nums;min-width:960px}
147
+ .lb th{font:500 .74rem var(--sans);color:var(--muted);text-align:center;padding:14px 8px;border-bottom:1px solid var(--line);white-space:nowrap;user-select:none}
148
+ .lb th[data-k]{cursor:pointer}.lb th[data-k]:hover,.lb th.on{color:var(--ink)}
149
+ .lb .left{text-align:left}
150
+ .lb td{padding:8px;text-align:center;border-bottom:1px solid var(--line);white-space:nowrap}
151
+ .lb tbody tr.row{cursor:pointer;transition:background .15s}
152
+ .lb tbody tr.row:hover{background:var(--paper-2)}
153
+ .lb tr.detail td{background:var(--paper-2);padding:0;white-space:normal;text-align:left}
154
+ .pos{display:inline-grid;place-items:center;width:27px;height:27px;border-radius:50%;font:500 .78rem var(--mono);color:var(--muted)}
155
+ .pos.p1{background:linear-gradient(145deg,#f6d77a,#d9a42b);color:#3e2c00}.pos.p2{background:linear-gradient(145deg,#e4e8ec,#b4bdc6);color:#2c3540}.pos.p3{background:linear-gradient(145deg,#e9bd91,#bf7f45);color:#3c2109}
156
+ .mname{font-weight:500}.mv{color:var(--muted);font-size:.78rem;margin-left:7px}
157
+ .tag{font:500 .66rem var(--mono);color:var(--soft);background:var(--paper-2);border-radius:6px;padding:1px 6px;margin-left:7px}
158
+ .heat{display:inline-block;min-width:52px;padding:6px 8px;border-radius:9px}
159
+ .ci{color:var(--muted);font-size:.78rem;margin-left:4px}
 
 
 
 
 
160
  .na{color:var(--muted)}
161
+ .late{color:var(--says);font-weight:500}
162
+ .chev{color:var(--muted);transition:transform .25s;display:inline-block}
163
+ tr.open .chev{transform:rotate(90deg)}
164
+ .dt{display:grid;grid-template-columns:1.1fr 1fr 1fr;gap:26px;padding:18px 26px 22px}
165
+ .dt>*{min-width:0}
166
+ .dt h4{font:500 .72rem var(--mono);letter-spacing:.07em;text-transform:uppercase;color:var(--muted);margin:0 0 8px}
167
+ .dt p{font-size:.92rem}
168
+ .dt-line{display:flex;justify-content:space-between;gap:10px;font-size:.9rem;padding:3px 0;font-variant-numeric:tabular-nums}
169
+ .dt-line b{font-weight:500}
170
+ .keys{display:flex;justify-content:space-between;gap:36px;margin-top:14px;font-size:.84rem}
171
+ .keys p{font-size:.84rem}.keys b{color:var(--ink);font-weight:500}
172
+ .ramp-key{white-space:nowrap}
173
+ .ramp{display:inline-block;width:82px;height:9px;border-radius:5px;background:linear-gradient(90deg,#f1f4ee,#0a7d74);margin-left:7px;vertical-align:middle}
174
+
175
+ .legend{display:flex;flex-wrap:wrap;gap:6px 18px;font-size:.8rem;color:var(--soft)}
176
+ .legend span{display:inline-flex;align-items:center;gap:7px}
177
+ .mk{width:11px;height:11px;display:inline-block}
178
+ .mk--k{background:var(--knows);border-radius:50%}
179
+ .mk--c{background:var(--says);transform:rotate(45deg) scale(.85)}
180
+ .mk--s{width:0;height:0;border-inline:6px solid transparent;border-bottom:11px solid var(--ink)}
181
+ .mk--v{border:2px solid var(--vendor);background:#fff}
182
+ .band{width:22px;height:11px;background:rgba(217,98,43,.2);border-radius:2px;display:inline-block}
183
+ .ln{width:18px;height:2.5px;border-radius:2px;display:inline-block}.ln--k{background:var(--knows)}.ln--c{background:var(--says)}
184
+ .tick{height:13px;border-left:1.5px dashed var(--chance);display:inline-block}
185
+ .cal #calendar{margin-top:8px}
186
+ .big{margin-top:16px}
187
+ .big-top{display:flex;justify-content:space-between;gap:10px 18px;flex-wrap:wrap;align-items:center}
188
+ .chart{margin-top:8px}
189
+ .big .note{margin-top:6px;font-size:.92rem}
190
+
191
+ .fam{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px}
192
+ .fam h3{margin-bottom:12px}
193
+ .fam-sub{display:flex;gap:16px;font-size:.76rem;color:var(--muted);margin-bottom:6px}
194
+ .fam-sub span{display:inline-flex;align-items:center;gap:6px}
195
+ .fam-sub i{width:9px;height:9px;border-radius:50%;display:inline-block}
196
+ .fr{display:grid;grid-template-columns:96px minmax(0,1fr);gap:10px;align-items:center;padding:7px 0;font-size:.82rem}
197
+ .fr-b{display:grid;gap:3px}
198
+ .fb{display:grid;grid-template-columns:minmax(0,1fr) 30px;gap:7px;align-items:center;font-size:.76rem;color:var(--soft);font-variant-numeric:tabular-nums}
199
+ .fb i{display:block;height:7px;border-radius:0 4px 4px 0}
200
+ .fb.k i{background:var(--knows)}.fb.c i{background:var(--says)}
201
+
202
+ .prompt-line{display:flex;gap:14px;align-items:center;padding:13px 18px;background:#fff;border-radius:14px;box-shadow:var(--ring);margin-bottom:16px}
203
+ .sw{position:relative;flex:none;width:46px;height:26px;border-radius:999px;background:#d7dfda;transition:background .25s}
204
+ .sw i{position:absolute;left:3px;top:3px;width:20px;height:20px;border-radius:50%;background:#fff;transition:transform .3s var(--ease);box-shadow:0 1px 3px rgba(0,0,0,.25)}
205
+ .sw[aria-checked="true"]{background:var(--knows)}
206
+ .sw[aria-checked="true"] i{transform:translateX(20px)}
207
+ #dateCode{font:400 .88rem var(--mono);color:var(--muted);transition:color .3s}
208
+ #dateCode.on{color:var(--ink)}
209
+ .decl{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}
210
+ .hb{margin-top:12px;display:grid;gap:11px}
211
+ .hb-r{display:grid;grid-template-columns:104px minmax(0,1fr) 40px;gap:10px;align-items:center;font-size:.86rem}
212
+ .hb-t{height:12px;background:var(--paper-2);border-radius:0 6px 6px 0;position:relative}
213
+ .hb-t i{position:absolute;inset:0 auto 0 0;background:var(--knows);border-radius:0 6px 6px 0;transition:width .7s var(--ease),background .4s}
214
+ .hb-v{text-align:right;font-variant-numeric:tabular-nums;font-weight:500}
215
+
216
+ table.pit{min-width:680px}
217
+ .pit td{padding-block:9px}
218
+ .forest{position:relative;display:block;width:200px;height:16px}
219
  .forest s{position:absolute;top:0;bottom:0;border-left:1.5px dashed var(--chance)}
220
  .forest i{position:absolute;top:7px;height:2px;background:var(--knows);border-radius:2px}
221
  .forest b{position:absolute;top:4px;width:8px;height:8px;margin-left:-4px;border-radius:50%;background:var(--knows)}
 
222
 
223
+ .how{display:grid;grid-template-columns:1fr 1.25fr;gap:32px;align-items:start}
224
+ .how>*{min-width:0}
225
+ .eg-q{font:400 1.16rem/1.3 var(--serif);color:var(--ink)}
226
+ .eg-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px;margin-top:14px;font-size:.92rem;font-variant-numeric:tabular-nums}
227
+ .eg-grid span{padding:8px 11px;border-radius:9px;box-shadow:inset 0 0 0 1px var(--line)}
228
+ .eg-grid .eg-h{box-shadow:none;padding:0 2px;font-size:.78rem;color:var(--muted)}
229
+ .eg-grid .eg-hit{background:#e3f2ee}
230
+ .m-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:22px 30px}
231
+ .m-grid h3{font-size:1.16rem;margin-bottom:5px}
232
+ .m-grid p{font-size:.92rem}
233
+ .foot{border-top:1px solid var(--line);padding-block:24px;font-size:.84rem;color:var(--muted)}
234
+ .foot-in{display:flex;justify-content:space-between;gap:14px;flex-wrap:wrap}
235
+ .foot a:hover{color:var(--ink)}
236
+
237
+ .tip{position:fixed;z-index:70;pointer-events:none;background:#10201c;color:#fff;font-size:.78rem;line-height:1.45;padding:7px 10px;border-radius:8px;max-width:250px;opacity:0;transition:opacity .12s;left:0;top:0;font-variant-numeric:tabular-nums}
238
+ .tip.show{opacity:1}.tip b{font-weight:600}
239
 
240
+ @keyframes grow{from{opacity:0}}
241
+ .grow{animation:grow .3s both}
242
+
243
+ @media (max-width:960px){
244
  .wrap{padding-left:max(16px,env(safe-area-inset-left,0px));padding-right:max(16px,env(safe-area-inset-right,0px))}
245
+ .links{display:none}
246
+ .hero{padding-top:88px}
247
+ .hero-grid,.game,.fam,.decl,.how,.dt{grid-template-columns:minmax(0,1fr)}
248
+ .hero-grid{gap:26px}
249
+ .section{padding-block:48px}
250
+ .keys{flex-direction:column;gap:8px}
251
+ .xs-side{width:50px}
252
+ .q-text{min-height:0}
253
+ }
254
+ @media (max-width:480px){.m-grid{grid-template-columns:minmax(0,1fr)}.xs-ends{grid-template-columns:repeat(2,1fr)}}
255
+ @media (prefers-reduced-motion:reduce){
256
+ html{scroll-behavior:auto}
257
+ .reveal,.js .reveal:not(.in){opacity:1;transform:none;transition:none}
258
+ .ticker-track,.grow{animation:none}
259
  }