Rebuild leaderboard around the tree-ring story
Browse files
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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
const NS = 'http://www.w3.org/2000/svg';
|
| 10 |
-
const
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
/* tooltip
|
| 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 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
if (text != null) e.textContent = text;
|
| 35 |
-
p.appendChild(e); return e;
|
| 36 |
-
}
|
| 37 |
|
| 38 |
-
/*
|
| 39 |
-
(function
|
| 40 |
-
const
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
});
|
|
|
|
|
|
|
| 46 |
})();
|
| 47 |
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
[D.meta.
|
| 51 |
-
[`${
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
-
/*
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
const
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
});
|
| 64 |
-
if (
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 75 |
}
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
/* leaderboard */
|
| 79 |
-
const COLS = [
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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="
|
| 98 |
-
<td class="left"><span class="mname">${m.model}</span><span class="
|
| 99 |
-
<td><span class="
|
| 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 ? '
|
| 103 |
-
<
|
| 104 |
}).join('');
|
| 105 |
-
const t = $('#lb');
|
| 106 |
-
t.
|
| 107 |
-
|
| 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 |
-
/*
|
| 116 |
-
|
| 117 |
-
const
|
| 118 |
-
const
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
-
/*
|
| 127 |
-
|
| 128 |
-
function
|
| 129 |
-
|
| 130 |
-
const
|
| 131 |
-
const
|
| 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 |
-
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
});
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
add(s, '
|
| 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 |
-
|
| 157 |
-
cross.setAttribute('
|
| 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',
|
|
|
|
|
|
|
| 163 |
}
|
| 164 |
-
|
| 165 |
-
$('#
|
| 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 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
|
|
|
|
|
|
| 175 |
|
| 176 |
-
/* declared date */
|
| 177 |
-
|
| 178 |
-
<div class="
|
| 179 |
-
|
| 180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 ·
|
| 7 |
-
<meta name="description" content="FinDendro
|
| 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="
|
| 20 |
-
<
|
| 21 |
-
<
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
| 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 |
-
<
|
| 37 |
-
<h1 class="wordmark">FinDendro</h1>
|
| 38 |
-
<p class="hero-sub">
|
| 39 |
-
<p class="lead">
|
| 40 |
-
<div class="stats" id="stats"></div>
|
| 41 |
-
<div class="ctas">
|
| 42 |
-
<a class="btn" href="#
|
| 43 |
-
<a class="btn btn--ghost" href="
|
| 44 |
</div>
|
| 45 |
</div>
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
<
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
<
|
|
|
|
|
|
|
| 52 |
</div>
|
| 53 |
-
<div id="
|
|
|
|
| 54 |
</figure>
|
| 55 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
</section>
|
| 57 |
|
| 58 |
-
<section class="section" id="leaderboard">
|
| 59 |
<div class="wrap">
|
| 60 |
-
<
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
</div>
|
| 66 |
</section>
|
| 67 |
|
| 68 |
-
<section class="section
|
| 69 |
<div class="wrap">
|
| 70 |
-
<
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
<
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
<
|
| 77 |
-
|
| 78 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
</div>
|
| 80 |
-
<div class="multiples" id="multiples"></div>
|
| 81 |
</div>
|
| 82 |
</section>
|
| 83 |
|
| 84 |
-
<section class="section" id="
|
| 85 |
<div class="wrap">
|
| 86 |
-
<
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
<
|
| 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
|
| 97 |
<div class="wrap">
|
| 98 |
-
<
|
| 99 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
</div>
|
| 102 |
</section>
|
| 103 |
|
| 104 |
-
<section class="section" id="pit">
|
| 105 |
<div class="wrap">
|
| 106 |
-
<
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
| 110 |
</div>
|
| 111 |
</section>
|
| 112 |
|
| 113 |
-
<section class="section
|
| 114 |
-
<div class="wrap
|
| 115 |
-
<
|
| 116 |
-
|
| 117 |
-
<
|
| 118 |
-
<
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 2 |
-
Knows is
|
| 3 |
:root{
|
| 4 |
-
--ink:#
|
| 5 |
-
--paper:#
|
| 6 |
-
--ring:0 0 0 1px rgba(
|
| 7 |
-
--g:linear-gradient(90deg,#
|
| 8 |
-
--
|
| 9 |
-
--
|
| 10 |
-
--
|
|
|
|
| 11 |
}
|
| 12 |
*{box-sizing:border-box}
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
|
|
|
| 16 |
p{margin:0;color:var(--soft)}
|
| 17 |
a{color:inherit;text-decoration:none}
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
.wrap{width:100%;max-width:var(--wrap);margin:0 auto;padding-inline:32px}
|
| 20 |
-
|
| 21 |
-
.
|
| 22 |
-
.
|
| 23 |
-
.
|
| 24 |
-
.
|
| 25 |
-
.
|
| 26 |
-
.
|
| 27 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
.links a:hover{background:var(--paper-2);color:var(--ink)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
-
.hero{position:relative;
|
| 31 |
-
.
|
| 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 |
-
.
|
| 35 |
-
.
|
| 36 |
-
.
|
| 37 |
-
.
|
| 38 |
-
.
|
| 39 |
-
.
|
| 40 |
-
.
|
| 41 |
-
.
|
| 42 |
-
.
|
| 43 |
-
.
|
| 44 |
-
.
|
| 45 |
-
.
|
| 46 |
-
.
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
.
|
| 50 |
-
|
| 51 |
-
figcaption{font:500
|
| 52 |
-
.
|
| 53 |
-
.
|
| 54 |
-
.
|
| 55 |
-
.
|
| 56 |
-
.
|
| 57 |
-
.
|
| 58 |
-
.
|
| 59 |
-
.
|
| 60 |
-
.
|
| 61 |
-
.
|
| 62 |
-
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
-
.section{padding-block:
|
| 66 |
-
.section--tint{background:
|
| 67 |
-
|
| 68 |
-
.
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
.scroll{overflow-x:auto}
|
| 71 |
-
table.lb{border-collapse:collapse;width:100%;font-size:.
|
| 72 |
-
.lb th{font:500 .74rem var(--sans);color:var(--muted);text-align:
|
| 73 |
-
.lb th[data-k]{cursor:pointer}
|
| 74 |
-
.lb
|
| 75 |
-
.lb
|
| 76 |
-
.lb
|
| 77 |
-
.lb
|
| 78 |
-
.lb tr
|
| 79 |
-
.
|
| 80 |
-
.
|
| 81 |
-
.mname{font-weight:500}
|
| 82 |
-
.
|
| 83 |
-
.
|
| 84 |
-
.
|
| 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 |
-
.
|
| 92 |
-
.
|
| 93 |
-
.
|
| 94 |
-
.
|
| 95 |
-
.
|
| 96 |
-
.
|
| 97 |
-
|
| 98 |
-
.
|
| 99 |
-
.
|
| 100 |
-
.
|
| 101 |
-
.
|
| 102 |
-
.
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
.
|
| 106 |
-
|
| 107 |
-
.
|
| 108 |
-
|
| 109 |
-
.
|
| 110 |
-
.
|
| 111 |
-
.
|
| 112 |
-
.
|
| 113 |
-
|
| 114 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
.
|
| 121 |
-
.
|
| 122 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
-
@
|
|
|
|
|
|
|
|
|
|
| 125 |
.wrap{padding-left:max(16px,env(safe-area-inset-left,0px));padding-right:max(16px,env(safe-area-inset-right,0px))}
|
| 126 |
-
.
|
| 127 |
-
.hero{padding-
|
| 128 |
-
.hero-grid,.
|
| 129 |
-
.hero-grid{gap:
|
| 130 |
-
.
|
| 131 |
-
.
|
| 132 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
}
|
|
|