Xueqing commited on
Commit
c58bef8
·
verified ·
1 Parent(s): e21a69b

HERCULEAN leaderboard site (from xueqingpeng/herculean docs/ @ 76f5121)

Browse files
README.md CHANGED
@@ -1,10 +1,22 @@
1
  ---
2
  title: HERCULEAN
3
- emoji: 😻
4
- colorFrom: blue
5
  colorTo: purple
6
  sdk: static
 
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: HERCULEAN
3
+ emoji: 🦁
4
+ colorFrom: red
5
  colorTo: purple
6
  sdk: static
7
+ app_file: index.html
8
  pinned: false
9
+ short_description: Agentic benchmark for financial intelligence leaderboard
10
  ---
11
 
12
+ # HERCULEAN
13
+
14
+ **HERCULEAN: An Agentic Benchmark for Financial Intelligence** · [arXiv 2605.14355](https://arxiv.org/abs/2605.14355)
15
+
16
+ Interactive leaderboard for HERCULEAN, which evaluates 30 agent systems (5 architectures × 6 backbones,
17
+ 3 runs each) end to end on four financial workflows: Trading, Hedging, Market Insights and Auditing.
18
+
19
+ - `index.html`: rankings, per-workflow leaderboard, trajectory replay and benchmark overview
20
+ - `play.html`: a mini-game that trades a stock with the agent systems' recorded decisions
21
+
22
+ Also available at https://xueqingpeng.github.io/herculean/
app.js ADDED
@@ -0,0 +1,1068 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* HERCULEAN leaderboard.
2
+ Reads window.HERC from ./data.js. No libraries. */
3
+ (() => {
4
+ const H = window.HERC, M = H.meta;
5
+ const $ = (s, el = document) => el.querySelector(s);
6
+ const $$ = (s, el = document) => [...el.querySelectorAll(s)];
7
+ const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
8
+
9
+ const WF = H.workflows; // Trading, Hedging, Market Insights, Auditing
10
+ const WC = { 'Trading': '#d6304a', 'Hedging': '#8a3ce0', 'Market Insights': '#2f6fd6', 'Auditing': '#11949f' };
11
+ const SHORT = { 'Market Insights': 'Insights' };
12
+ const AC = { 'ReAct': '#2a78d6', 'Claude Code': '#eb6834', 'Codex': '#1baf7a', 'Hermes': '#eda100', 'OpenClaw': '#e87ba4' };
13
+ const INI = { 'ReAct': 'RA', 'Claude Code': 'CC', 'Codex': 'CX', 'Hermes': 'HM', 'OpenClaw': 'OC' };
14
+ const INI_FG = { 'ReAct': '#fff' }; // dark text on the lighter badges
15
+ const ini = (a, cls = 'ini') => `<span class="${cls}" style="--c:${AC[a]};--fg:${INI_FG[a] || '#0f1528'}">${INI[a]}</span>`;
16
+
17
+ /* ---------- agent systems: Overall = mean of the 4 workflow scores, Floor = min ---------- */
18
+ const rows = H.scores.map(s => {
19
+ const v = WF.map(w => s[w]);
20
+ return { ...s, key: `${s.agent}|${s.model}`, name: `${s.agent} · ${s.model}`,
21
+ Overall: v.reduce((a, b) => a + b, 0) / v.length, Floor: Math.min(...v) };
22
+ }).sort((a, b) => b.Overall - a.Overall);
23
+ rows.forEach((r, i) => r.rank = i + 1);
24
+ const top = rows[0], N = rows.length, S = H.offlineStd;
25
+
26
+ // paper rounding (half up), so 96.55 shows as 96.6
27
+ const f1 = v => (Math.round(v * 10 + 1e-9) / 10).toFixed(1);
28
+ const fmtRho = v => (v < 0 ? '−' : '') + Math.abs(v).toFixed(2);
29
+ const pct = v => v == null ? '—' : (v > 0 ? '+' : v < 0 ? '−' : '') + Math.abs(v).toFixed(2) + '%';
30
+ const num = (v, d = 2) => v == null ? '—' : (v < 0 ? '−' : '') + Math.abs(v).toFixed(d);
31
+ const pm = (txt, sd, d = 2) => sd == null ? txt : `${txt}<span class="sd">±${Math.abs(sd).toFixed(d)}</span>`;
32
+ const mean = a => { const x = a.filter(v => v != null); return x.length ? x.reduce((p, q) => p + q, 0) / x.length : null; };
33
+
34
+ /* best score per workflow (ties count for every tied system) */
35
+ const best = Object.fromEntries(WF.map(w => [w, Math.max(...rows.map(r => r[w]))]));
36
+ const isBest = (r, w) => Math.abs(r[w] - best[w]) < 1e-6;
37
+ const leaders = Object.fromEntries(WF.map(w => [w, rows.filter(r => isBest(r, w))]));
38
+ const leadCount = {}; WF.forEach(w => leaders[w].forEach(r => leadCount[r.key] = (leadCount[r.key] || 0) + 1));
39
+ const maxLeads = Math.max(...Object.values(leadCount));
40
+
41
+ /* Spearman ρ between workflow rankings (average ranks for ties) */
42
+ function ranks(v) {
43
+ const idx = v.map((x, i) => [x, i]).sort((a, b) => a[0] - b[0]), r = new Array(v.length);
44
+ for (let i = 0; i < idx.length;) { let j = i; while (j + 1 < idx.length && idx[j + 1][0] === idx[i][0]) j++;
45
+ for (let k = i; k <= j; k++) r[idx[k][1]] = (i + j) / 2 + 1; i = j + 1; }
46
+ return r;
47
+ }
48
+ function spearman(a, b) {
49
+ const ra = ranks(a), rb = ranks(b), n = a.length, m = (n + 1) / 2;
50
+ let sab = 0, saa = 0, sbb = 0;
51
+ for (let i = 0; i < n; i++) { sab += (ra[i] - m) * (rb[i] - m); saa += (ra[i] - m) ** 2; sbb += (rb[i] - m) ** 2; }
52
+ return sab / Math.sqrt(saa * sbb);
53
+ }
54
+ const rhos = []; WF.forEach((a, i) => WF.slice(i + 1).forEach(b => rhos.push(spearman(rows.map(r => r[a]), rows.map(r => r[b])))));
55
+ const meanRho = rhos.reduce((p, q) => p + q, 0) / rhos.length;
56
+
57
+ /* score magnitude: one purple hue, light → dark */
58
+ const RAMP = [[244, 240, 251], [201, 182, 234], [142, 106, 208], [75, 42, 143]];
59
+ function shade(v) {
60
+ const t = Math.max(0, Math.min(1, v / 100)) * (RAMP.length - 1), i = Math.min(RAMP.length - 2, Math.floor(t)), f = t - i;
61
+ const c = RAMP[i].map((x, k) => Math.round(x + (RAMP[i + 1][k] - x) * f));
62
+ return { bg: `rgb(${c})`, fg: v > 58 ? '#fff' : '#0f1528' };
63
+ }
64
+
65
+ /* ---------- tooltip ---------- */
66
+ const tip = $('#tip');
67
+ document.addEventListener('pointerover', e => { const t = e.target.closest('[data-tip]'); if (!t) return; tip.innerHTML = t.dataset.tip; tip.classList.add('on'); });
68
+ document.addEventListener('pointermove', e => {
69
+ if (!tip.classList.contains('on')) return;
70
+ const x = Math.min(innerWidth - tip.offsetWidth - 10, e.clientX + 14), y = e.clientY - tip.offsetHeight - 12;
71
+ tip.style.left = x + 'px'; tip.style.top = (y < 6 ? e.clientY + 18 : y) + 'px';
72
+ });
73
+ document.addEventListener('pointerout', e => { if (e.target.closest('[data-tip]')) tip.classList.remove('on'); });
74
+
75
+ /* ================= HERO ================= */
76
+ // small lion for the "New benchmark" pill: scalloped mane, face, eyes, nose
77
+ const LION = (() => {
78
+ // spiky mane (16-point star), ears, face, eyes, nose and muzzle
79
+ const pts = Array.from({ length: 32 }, (_, i) => { const a = i / 32 * 2 * Math.PI - Math.PI / 2, r = i % 2 ? 8.6 : 11.4;
80
+ return `${(12 + r * Math.cos(a)).toFixed(2)},${(12.6 + r * Math.sin(a)).toFixed(2)}`; }).join(' ');
81
+ const face = '#6b1590';
82
+ return `<svg viewBox="0 0 24 24" aria-hidden="true"><polygon class="mane" points="${pts}" fill="#fff"/>
83
+ <circle cx="8.1" cy="8.2" r="1.9" fill="${face}"/><circle cx="15.9" cy="8.2" r="1.9" fill="${face}"/>
84
+ <circle cx="12" cy="12.9" r="6.4" fill="${face}"/>
85
+ <circle cx="9.6" cy="11.6" r=".95" fill="#fff"/><circle cx="14.4" cy="11.6" r=".95" fill="#fff"/>
86
+ <ellipse cx="12" cy="15.4" rx="2.6" ry="1.9" fill="#fff" opacity=".22"/>
87
+ <path d="M10.6 13.6h2.8L12 15.2z" fill="#fff"/>
88
+ <path d="M12 15.2v.8M12 16c-.6.7-1.5.8-2.1.3M12 16c.6.7 1.5.8 2.1.3" stroke="#fff" stroke-width=".75" fill="none" stroke-linecap="round"/></svg>`;
89
+ })();
90
+ $('#lion').innerHTML = LION;
91
+ // the same lion beside "Cite HERCULEAN"
92
+ if ($('#citeLion')) $('#citeLion').innerHTML = LION;
93
+ // hovering the lion or the citation panel speeds the mane up; playbackRate keeps the current angle (no jump)
94
+ (() => {
95
+ const lion = $('#citeLion'), bib = $('#resources .bib'); if (!lion) return;
96
+ const rate = r => { const m = $('.mane', lion); if (m) m.getAnimations().forEach(x => (x.playbackRate = r)); };
97
+ [lion, bib].filter(Boolean).forEach(el => { el.addEventListener('pointerenter', () => rate(14 / 3)); el.addEventListener('pointerleave', () => rate(1)); });
98
+ })();
99
+ // rotating subject: one architecture at a time, in its own colour
100
+ (() => {
101
+ const r = $('#rotator');
102
+ r.innerHTML = H.agents.map((a, i) => `<span class="${i === 0 ? 'on' : ''}" style="--c:${AC[a]}">${a}</span>`).join('');
103
+ const sp = $$('span', r); let i = 0;
104
+ const MS_ROT = 1600; // one name every 1.6 s
105
+ r.style.setProperty('--rot', MS_ROT + 'ms');
106
+ // width follows the current name; the underline takes its colour
107
+ const fit = () => { r.style.width = sp[i].offsetWidth + 'px'; r.style.setProperty('--uc', AC[H.agents[i]]); };
108
+ fit(); document.fonts && document.fonts.ready.then(fit);
109
+ if (reduce) return;
110
+ setInterval(() => {
111
+ const prev = sp[i]; prev.classList.remove('on'); prev.classList.add('out');
112
+ i = (i + 1) % sp.length; sp[i].classList.remove('out'); sp[i].classList.add('on'); fit();
113
+ setTimeout(() => prev.classList.remove('out'), 600);
114
+ }, MS_ROT);
115
+ })();
116
+ $('#awardN').textContent = `${H.agents.length} architectures × ${H.models.length} backbones`;
117
+ $('#stats').innerHTML = [[N, 'agent systems', `${H.agents.length} architectures × ${H.models.length} backbones`], [WF.length, 'workflows', WF.join(' · ')], [M.offlineDays, 'offline days', `trading days, ${M.offlineWindow[0]} → ${M.offlineWindow[1]}`], [M.liveDays, 'live days', `trading days, ${M.liveWindow[0]} → ${M.liveWindow[1]}; Trading, Hedging and Market Insights, 3 selected systems per workflow`], [M.runs, 'runs', `every agent system is run ${M.runs} times independently`, '×']]
118
+ .map(([n, l, t, suf]) => `<div${t ? ` data-tip="<b>${l}</b><br>${t}"` : ''}><b data-count="${n}" data-dec="0"${suf ? ` data-suf="${suf}"` : ''}>0</b><span>${l}</span></div>`).join('');
119
+ // "Top 8 · Overall score" card: bars coloured by architecture, initials badge on top
120
+ const MS = { 'Claude Sonnet 5': 'Sonnet 5', 'DeepSeek-V3.2': 'DS-V3.2', 'DeepSeek-V4-Pro': 'DS-V4-Pro', 'Qwen3.5-9B': 'Qwen-9B', 'Qwen3.5-4B': 'Qwen-4B' };
121
+ // jump into the Rankings section on a given view, optionally with a selection (set by the Rankings block below)
122
+ let rkGo = null;
123
+ const goRank = (view, sel) => { if (rkGo) rkGo(view, sel); document.getElementById('rankings').scrollIntoView({ behavior: reduce ? 'auto' : 'smooth', block: 'start' }); };
124
+ $('#award').addEventListener('click', e => { e.preventDefault(); goRank('flow'); });
125
+ const pod = $('#podium'), TOPN = 8;
126
+ pod.innerHTML = rows.slice(0, TOPN).map((r, i) => `<div class="col${i === 0 ? ' first' : ''}${i < 3 ? ` podium-${i + 1}` : ''}" data-key="${r.key}" style="--h:${r.Overall / top.Overall * 100}%;--d:${i * 90}ms;--c:${AC[r.agent]};--fg:${INI_FG[r.agent] || '#0f1528'}"
127
+ data-tip="<b>#${r.rank} ${r.name}</b><br>Overall ${f1(r.Overall)}<span> · floor ${f1(r.Floor)}</span><br>${WF.map(w => `${SHORT[w] || w} ${f1(r[w])}`).join('<span> · </span>')}">
128
+ <div class="bar"><span class="val">${f1(r.Overall)}</span><span class="rk-badge">${r.rank}</span></div><span class="nm">${r.agent}<small>${MS[r.model] || r.model}</small></span></div>`).join('');
129
+ // ticker: every agent system by Overall rank; the list is doubled so the loop is seamless
130
+ const tickItem = r => `<span class="tk${r.rank === 1 ? ' first' : ''}" data-key="${r.key}" data-tip="<b>#${r.rank} ${r.name}</b><br>${WF.map(w => `${SHORT[w] || w} ${f1(r[w])}`).join('<span> · </span>')}"><span class="p">#${String(r.rank).padStart(2, '0')}</span>${ini(r.agent, 'ini sm')}<span class="n">${r.agent} <em>·</em> ${MS[r.model] || r.model}</span><span class="v">${f1(r.Overall)}</span></span>`;
131
+ $('#tickerTrack').innerHTML = rows.map(tickItem).join('').repeat(2);
132
+ // constant speed (~70 px/s, like a ticker tape) whatever the item widths
133
+ const setTickerSpeed = () => { const t = $('#tickerTrack'); t.style.setProperty('--dur', (t.scrollWidth / 2 / 70).toFixed(1) + 's'); };
134
+ setTickerSpeed(); document.fonts && document.fonts.ready.then(setTickerSpeed);
135
+ $('#ticker').addEventListener('click', e => {
136
+ const t = e.target.closest('.tk'); if (!t) return;
137
+ const i = rows.findIndex(r => r.key === t.dataset.key);
138
+ if (openKey !== rows[i].key) toggle(i);
139
+ document.getElementById('leaderboard').scrollIntoView({ behavior: reduce ? 'auto' : 'smooth' });
140
+ });
141
+ $('#heroLegend').innerHTML = H.agents.map(a => `<span><i style="background:${AC[a]}"></i>${a}</span>`).join('');
142
+ // architecture and backbone rankings: mean Overall over the systems that share the architecture (6 backbones)
143
+ // or the backbone (5 architectures) — a page-only aggregate of the leaderboard
144
+ {
145
+ const agg = (keys, by) => keys.map(k => { const rs = rows.filter(r => r[by] === k), v = rs.map(r => r.Overall);
146
+ return { k, m: v.reduce((p, q) => p + q, 0) / v.length, lo: Math.min(...v), hi: Math.max(...v), best: rs[0] }; }).sort((p, q) => q.m - p.m);
147
+ const A = agg(H.agents, 'agent'), B = agg(H.models, 'model'), mx = Math.max(...A.map(x => x.m), ...B.map(x => x.m));
148
+ const row = (x, i, kind) => `<div class="gr" data-kind="${kind}" data-k="${x.k}" data-tip="<b>${kind === 'agent' ? x.k : (MS[x.k] || x.k)}</b> · mean Overall ${f1(x.m)}<br><span>range ${f1(x.lo)}–${f1(x.hi)} over ${kind === 'agent' ? H.models.length + ' backbones' : H.agents.length + ' architectures'}</span><br>best: ${x.best.name} (${f1(x.best.Overall)})">
149
+ <span class="gr-p">${i + 1}</span>${kind === 'agent' ? ini(x.k, 'ini sm') : ''}<span class="gr-n">${kind === 'agent' ? x.k : (MS[x.k] || x.k)}</span>
150
+ <span class="gr-t"><i style="width:${x.m / mx * 100}%;${kind === 'agent' ? `background:${AC[x.k]}` : ''}"></i></span><b>${f1(x.m)}</b></div>`;
151
+ $('#heroLeaders').innerHTML = `<div class="gr-col"><p class="hl-cap">Architectures <span>mean Overall</span></p>${A.map((x, i) => row(x, i, 'agent')).join('')}</div>
152
+ <div class="gr-col"><p class="hl-cap">Backbones <span>mean Overall</span></p>${B.map((x, i) => row(x, i, 'model')).join('')}</div>`;
153
+ }
154
+ $('#vizCap').textContent = `Top ${TOPN} · Overall score`;
155
+ // clicking a bar opens Rankings on the Floor → Overall view with that system highlighted;
156
+ // clicking an architecture / backbone row opens the Field view with that group highlighted
157
+ pod.addEventListener('click', e => { const c = e.target.closest('.col'); if (c) goRank('floor', { mode: 'system', key: c.dataset.key }); });
158
+ $('#heroLeaders').addEventListener('click', e => { const g = e.target.closest('.gr'); if (g) goRank('field', { mode: g.dataset.kind, key: g.dataset.k }); });
159
+
160
+ function countUp(el) {
161
+ const to = +el.dataset.count, dec = el.dataset.dec != null ? +el.dataset.dec : 1, suf = el.dataset.suf || '', d = 1200, t0 = performance.now();
162
+ const show = v => (el.textContent = v.toFixed(dec) + suf);
163
+ if (reduce) { show(to); return; }
164
+ setTimeout(() => show(to), d + 300); // always land on the value
165
+ // rAF time can precede t0 on the first frame; clamp so the count never starts below zero
166
+ const step = t => { const p = Math.max(0, Math.min(1, (t - t0) / d)), e = 1 - Math.pow(1 - p, 3);
167
+ show(to * e); if (p < 1) requestAnimationFrame(step); };
168
+ requestAnimationFrame(step);
169
+ }
170
+
171
+ /* ================= 1 · WORKFLOW CARDS ================= */
172
+ const ICONS = {
173
+ env: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5.5" rx="7.5" ry="2.5"/><path d="M4.5 5.5v6c0 1.4 3.4 2.5 7.5 2.5s7.5-1.1 7.5-2.5v-6"/><path d="M4.5 11.5v6c0 1.4 3.4 2.5 7.5 2.5s7.5-1.1 7.5-2.5v-6"/></svg>',
174
+ proto: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h12l-3-3M20 17H8l3 3"/></svg>',
175
+ eval: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 20h18"/><path d="M5 16l4-5 4 3 6-8"/><circle cx="19" cy="6" r="1.5"/></svg>',
176
+ };
177
+ // each workflow = Environment + Interaction Protocol + Trajectory Evaluation (short; the paper has the details)
178
+ const RECIPE = {
179
+ 'Trading': [`Daily prices, news and 10-K / 10-Q filings for ${H.tradeAssets.join(', ')}`, `One BUY, SELL or HOLD per day for ${M.offlineDays} trading days`, ['CR', 'SR', 'MDD', 'vs Buy & Hold']],
180
+ 'Hedging': ['The same data across eight large-cap stocks', 'Pick a pair on day 1, then LONG_SHORT, SHORT_LONG, HOLD or CLOSE daily', ['CR', 'SR', 'MDD', 'dollar-neutral']],
181
+ 'Market Insights': ['Weekly market context with a fixed peer mapping', 'A weekly eight-section report and rating, about 13 per asset', ['Quality 0–10', 'CR', 'SR', 'MDD']],
182
+ 'Auditing': [`SEC XBRL filings with the US-GAAP taxonomy, ${M.auditInstances} instances`, 'Commit the reported and the recalculated value of one fact', ['ACC', 'SER', 'EER', 'CER']],
183
+ };
184
+ // one line on what each workflow is for (directional vs market-neutral; forward- vs backward-looking)
185
+ const PURPOSE = {
186
+ 'Trading': 'Seek profit as the market evolves, one directional decision a day.',
187
+ 'Hedging': 'Seek profit with market risk hedged out, through a market-neutral pair.',
188
+ 'Market Insights': 'Look forward: turn evidence into a view on an asset’s future.',
189
+ 'Auditing': 'Look back: verify what a filing reports against its own numbers.',
190
+ };
191
+ const PARTS = [['Environment', 'env'], ['Interaction Protocol', 'proto'], ['Trajectory Evaluation', 'eval']];
192
+ $('#wfGrid').innerHTML = WF.map((w, i) => {
193
+ const lead = leaders[w], [env, proto, ev] = RECIPE[w];
194
+ const who = lead.length === 1 ? `<span>${lead[0].agent} <em>·</em></span><span>${lead[0].model}</span>`
195
+ : `<span data-tip="<b>${lead.length}-way tie</b><br>${lead.map(r => r.name).join('<br>')}"><span>${lead[0].agent} <em>·</em></span><span>${lead[0].model} <em>+${lead.length - 1} tied</em></span></span>`;
196
+ const item = (j, body) => `<li><span class="ico">${ICONS[PARTS[j][1]]}</span><div><small>${PARTS[j][0]}</small>${body}</div></li>`;
197
+ return `<article class="wf spot" data-reveal style="--i:${i};--c:${WC[w]}"><span class="wf-bar"></span>
198
+ <div class="wf-top"><span class="lbl"><span class="wdot" style="--c:${WC[w]}"></span>Workflow</span><span class="num">0${i + 1}</span></div>
199
+ <h3>${w}</h3>
200
+ <p class="purpose">${PURPOSE[w]}</p>
201
+ <ul class="recipe">${item(0, `<p>${env}</p>`)}${item(1, `<p>${proto}</p>`)}${item(2, `<div class="chips">${ev.map(m => `<span>${m}</span>`).join('')}</div>`)}</ul>
202
+ <div class="wf-best"><div><small>Best agent system</small><div class="who">${who}</div></div><b>${f1(best[w])}</b></div>
203
+ </article>`;
204
+ }).join('');
205
+
206
+ /* ================= 2 · LEADERBOARD ================= */
207
+ const score = (r, v) => v === 'Overall' ? r.Overall : v === 'Workflow floor' ? r.Floor : r[v];
208
+ let view = 'Overall', openKey = null, showAll = false;
209
+ const LIMIT = 10; // rows shown before "show all"
210
+ const hidden = new Set(), hiddenM = new Set();
211
+
212
+ $('#lbKicker').textContent = `Leaderboard · offline regime · ${M.offlineWindow[0]} → ${M.offlineWindow[1]}`;
213
+ $('#lbSub').textContent = `Click a column to re-rank all ${N} agent systems. Click a row for its details.`;
214
+ $('#lbNote').innerHTML = `Each metric (mean of ${M.runs} runs) is rank-normalised across the ${N} agent systems to 0–100. <b>Overall</b> is the mean of the four workflow scores; the <b>workflow floor</b> is the lowest.`;
215
+
216
+ // ranking is chosen from the column headers (Overall or one workflow); the bar always shows Overall
217
+ $('#archChips').innerHTML = H.agents.map(a => `<button type="button" data-a="${a}" aria-pressed="true">${ini(a, 'ini sm')}${a}</button>`).join('');
218
+ $('#archChips').addEventListener('click', e => {
219
+ const b = e.target.closest('button'); if (!b) return;
220
+ const a = b.dataset.a;
221
+ if (hidden.has(a)) hidden.delete(a); else if (hidden.size < H.agents.length - 1) hidden.add(a);
222
+ $$('button', $('#archChips')).forEach(x => { x.classList.toggle('off', hidden.has(x.dataset.a)); x.setAttribute('aria-pressed', !hidden.has(x.dataset.a)); });
223
+ layout();
224
+ });
225
+ $('#modelChips').innerHTML = H.models.map(m => `<button type="button" data-m="${m}" aria-pressed="true">${MS[m] || m}</button>`).join('');
226
+ $('#modelChips').addEventListener('click', e => {
227
+ const b = e.target.closest('button'); if (!b) return;
228
+ const m = b.dataset.m;
229
+ if (hiddenM.has(m)) hiddenM.delete(m); else if (hiddenM.size < H.models.length - 1) hiddenM.add(m);
230
+ $$('button', $('#modelChips')).forEach(x => { x.classList.toggle('off', hiddenM.has(x.dataset.m)); x.setAttribute('aria-pressed', !hiddenM.has(x.dataset.m)); });
231
+ layout();
232
+ });
233
+ $('#colCells').innerHTML = WF.map(w => `<button type="button" class="sort-h" data-v="${w}" title="Rank by ${w}"><span class="wdot" style="--c:${WC[w]}"></span>${SHORT[w] || w}<i class="arr">↓</i></button>`).join('');
234
+ $('#board .lb-cols').addEventListener('click', e => { const b = e.target.closest('.sort-h'); if (!b || b.dataset.v === view) return; view = b.dataset.v; layout(); });
235
+
236
+ // one DOM row per system, created once and moved with transforms
237
+ const box = $('#lbRows');
238
+ box.innerHTML = rows.map((r, i) => `<div class="lb-row" data-i="${i}">
239
+ <div class="in" tabindex="0" role="button" aria-expanded="false">
240
+ <span class="lb-pos"></span>
241
+ <span class="lb-name">${ini(r.agent)}<b>${r.agent}</b><span>${r.model}</span></span>
242
+ <span class="lb-score"><span class="lb-track"><span class="lb-bar" style="--c:${AC[r.agent]}"></span></span><span class="lb-val"></span></span>
243
+ <span class="cells">${WF.map(w => { const s = shade(r[w]);
244
+ return `<span class="cell${isBest(r, w) ? ' best' : ''}" data-w="${w}" style="background:${s.bg};color:${s.fg}" data-tip="<b>${r.name}</b><br>${w}: ${f1(r[w])}<span> / 100${isBest(r, w) ? ' · best in workflow' : ''}</span>">${f1(r[w])}</span>`; }).join('')}</span>
245
+ <span class="chev">›</span>
246
+ </div></div>`).join('') + '<div class="lb-detail" id="lbDetail"></div>';
247
+ const els = $$('.lb-row', box), detail = $('#lbDetail');
248
+ function fitNames() {
249
+ const w = Math.max(...$$('.lb-name', box).map(n => [...n.children].reduce((s, c) => s + Math.max(c.getBoundingClientRect().width, c.scrollWidth), 0) + 22));
250
+ $('#board').style.setProperty('--namew', Math.ceil(w) + 'px');
251
+ }
252
+ const ROW = 40;
253
+ let revealed = false;
254
+
255
+ function layout() {
256
+ const vis = rows.map((r, i) => ({ r, i })).filter(x => !hidden.has(x.r.agent) && !hiddenM.has(x.r.model))
257
+ .sort((a, b) => score(b.r, view) - score(a.r, view) || b.r.Overall - a.r.Overall);
258
+ if (openKey && (hidden.has(openKey.split('|')[0]) || hiddenM.has(openKey.split('|')[1]))) openKey = null;
259
+ // open row beyond the first LIMIT rows (e.g. from the ticker): show everything
260
+ if (openKey && !showAll && vis.findIndex(x => x.r.key === openKey) >= LIMIT) showAll = true;
261
+ const shown = showAll ? vis : vis.slice(0, LIMIT);
262
+ els.forEach(el => el.classList.add('gone'));
263
+ let y = 0;
264
+ shown.forEach((x, pos) => {
265
+ const el = els[x.i], open = openKey === x.r.key;
266
+ el.classList.remove('gone'); el.classList.toggle('open', open); el.classList.toggle('top', pos === 0);
267
+ $('.in', el).setAttribute('aria-expanded', open);
268
+ el.style.transform = `translateY(${y}px)`;
269
+ $('.lb-pos', el).textContent = String(pos + 1).padStart(2, '0');
270
+ $('.lb-bar', el).style.width = (revealed ? x.r.Overall : 0) + '%';
271
+ $('.lb-val', el).textContent = f1(x.r.Overall);
272
+ $('.cells', el).classList.toggle('dim', WF.includes(view));
273
+ $$('.cell', el).forEach(c => c.classList.toggle('on', c.dataset.w === view));
274
+ y += ROW;
275
+ if (open) { detail.style.transform = `translateY(${y}px)`; y += detail.offsetHeight; }
276
+ });
277
+ if (openKey && !shown.some(x => x.r.key === openKey)) openKey = null;
278
+ detail.classList.toggle('show', !!openKey);
279
+ const more = $('#lbMore'), rest = vis.length - LIMIT;
280
+ more.hidden = rest <= 0;
281
+ more.setAttribute('aria-expanded', showAll);
282
+ more.innerHTML = showAll ? `Show top ${LIMIT} only <span class="chev2">↓</span>` : `Show all ${vis.length} agent systems <em style="font-style:normal;color:var(--muted)">(+${rest})</em> <span class="chev2">↓</span>`;
283
+ box.style.height = y + 'px';
284
+ $$('#board .sort-h').forEach(b => b.classList.toggle('on', b.dataset.v === view));
285
+ }
286
+
287
+ $('#lbMore').addEventListener('click', () => {
288
+ showAll = !showAll;
289
+ if (!showAll) openKey = null; // otherwise an open row below #15 would re-expand the list
290
+ layout();
291
+ if (!showAll) document.getElementById('board').scrollIntoView({ behavior: reduce ? 'auto' : 'smooth', block: 'start' });
292
+ });
293
+
294
+ // expanded row: a one-line summary, then four small cards, one chart each
295
+ const wfRank = (r, w) => 1 + rows.filter(x => x[w] > r[w] + 1e-9).length;
296
+ const isLive = (w, k) => (M.liveConfigs[w] || []).includes(k);
297
+ const pc0 = v => v == null ? '—' : (v > 0 ? '+' : v < 0 ? '−' : '') + Math.abs(v).toFixed(1) + '%';
298
+ const sdS = (v, d = 1) => v == null ? '' : `<span class="sd">±${Math.abs(v).toFixed(d)}</span>`;
299
+ // shared CR axis across every system and Buy & Hold, so trading dots are comparable between rows
300
+ const crAll = H.tradeAssets.flatMap(a => [...rows.map(r => H.offline.Trading[a].CR[r.key]), H.baseline[`Trading|${a}|CR`]]).filter(v => v != null);
301
+ const crMin = Math.min(0, ...crAll), crMax = Math.max(0, ...crAll), xPos = v => (v - crMin) / (crMax - crMin) * 100;
302
+ const ACTION = { B: 'BUY', S: 'SELL', H: 'HOLD', L: 'LONG_SHORT', R: 'SHORT_LONG', C: 'CLOSE', '-': 'no committed record (scored as HOLD)' };
303
+ function breakdown(r) {
304
+ const k = r.key, O = H.offline, A = H.tradeAssets;
305
+ const fw = WF.find(w => r[w] === r.Floor), liveWf = Object.keys(M.liveConfigs).filter(w => isLive(w, k));
306
+ const best = WF.reduce((p, w) => r[w] > r[p] ? w : p, WF[0]);
307
+ const liveTip = M.liveRule.replace(/"/g, '&quot;');
308
+ const tag = w => (w === best ? '<span class="d1-tag best" data-tip="this system’s strongest workflow">best</span>' : w === fw ? '<span class="d1-tag floor" data-tip="this system’s weakest workflow (workflow floor)">floor</span>' : '')
309
+ + (liveWf.includes(w) ? `<span class="d1-tag live" data-tip="<b>Run live</b> ${M.liveWindow[0]} → ${M.liveWindow[1]}<br><span>${liveTip}</span>">live</span>` : '');
310
+ const rpl = w => (w === 'Trading' || w === 'Hedging') ? `<button type="button" class="d1-replay" data-replay="${k}"><svg class="play-ico" viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="7.5" fill="currentColor"/><path d="M6.4 4.9v6.2l4.9-3.1z" fill="#fff"/></svg>Replay</button>` : '';
311
+ const card = (w, sub, body) => `<div class="d1-card" style="--c:${WC[w]}"><h4><span class="wdot" style="--c:${WC[w]}"></span>${w}${tag(w)}<span class="ws" data-tip="${w} workflow score, rank-normalised 0–100"><b>${f1(r[w])}</b><span class="rk" data-tip="rank in ${w} among ${N} agent systems">#${wfRank(r, w)}</span></span></h4><p class="sub">${sub}${rpl(w)}</p>${body}</div>`;
312
+ // Trading: CR per asset (dot) against Buy & Hold (tick)
313
+ const tr = A.map(a => { const v = O.Trading[a].CR[k], sd = S.Trading[a].CR[k], bh = H.baseline[`Trading|${a}|CR`];
314
+ const sr = O.Trading[a].SR[k], mdd = O.Trading[a].MDD[k];
315
+ return `<div class="d1-cr" data-tip="<b>${a}</b><br>Agent CR ${pct(v)} <span>±${(sd ?? 0).toFixed(2)}</span><br>SR ${num(sr)} <span>±${(S.Trading[a].SR[k] ?? 0).toFixed(2)}</span><br>MDD ${mdd == null ? '— <span>never held a position</span>' : mdd.toFixed(2) + '% <span>±' + (S.Trading[a].MDD[k] ?? 0).toFixed(2) + '</span>'}<br><span>Buy &amp; Hold CR ${pct(bh)}</span>"><span class="d1-t">${a}</span>
316
+ <span class="track"><i class="zero" style="left:${xPos(0)}%"></i><i class="bh" style="left:${xPos(bh)}%"></i>${v == null ? '' : `<i class="ag" style="left:${xPos(v)}%"></i>`}</span>
317
+ <span class="val">${pct(v)}${sdS(sd)}</span></div>`; }).join('');
318
+ const beats = A.filter(t => O.Trading[t].CR[k] >= H.baseline[`Trading|${t}|CR`]).length;
319
+ const trading = card('Trading', 'Cumulative return (CR) per asset · hover a row for SR and MDD', tr + `<p class="d1-beat">Beats Buy &amp; Hold on <b>${beats} of ${A.length}</b> tickers</p><div class="d1-legend"><span><i class="l-ag"></i>agent CR</span><span><i class="l-bh"></i>Buy &amp; Hold CR</span><span><i class="l-z"></i>0%</span></div>`);
320
+ // Hedging: pair, CR / SR / MDD, 61-day action strip
321
+ const hp = O.Hedging.PAIR, hs = S.Hedging.PAIR, pair = H.pairs[k], pr = H.pairRuns[k], acts = H.hedgingActions[k] || '';
322
+ const [pl, prt] = pair ? pair.split('/') : [];
323
+ const hedging = card('Hedging', 'Pair chosen on day 1 (most frequent over runs)',
324
+ (pair ? `<div class="d1-pair" data-tip="LONG_SHORT = long ${pl}, short ${prt}">${pl}<span class="s">/</span>${prt}${pr ? `<span class="runs">${pr.runs}/${pr.of} runs</span>` : ''}</div>` : '<div class="d1-pair na">no pair</div>') +
325
+ `<div class="d1-kv"><span>CR</span><span>${pct(hp.CR[k])}${sdS(hs.CR[k])}</span></div><div class="d1-kv"><span>SR</span><span>${num(hp.SR[k])}${sdS(hs.SR[k], 2)}</span></div>
326
+ <div class="d1-kv"><span>MDD</span><span>${hp.MDD[k] == null ? '<span data-tip="never held a position">—</span>' : hp.MDD[k].toFixed(2) + '%' + sdS(hs.MDD[k])}</span></div>` +
327
+ (acts ? (() => {
328
+ // same encoding as the paper's Figure 3b: position state per day (HOLD and days without a committed record keep
329
+ // the previous position, as in the evaluator); CLOSE days are marked with a diamond
330
+ let p = 'F';
331
+ const days = [...acts].map((c, i) => { const cl = c === 'C'; if (c === 'L' || c === 'R') p = c; else if (cl) p = 'F'; return { s: p, cl, c, i }; });
332
+ const pn = { L: `Long-Short (long ${pl}, short ${prt})`, R: `Short-Long (short ${pl}, long ${prt})`, F: 'Flat' };
333
+ return `<div class="d1-cap">Position each day · run ${pr ? pr.shownRun : 1} of ${M.runs}</div><div class="d1-strip">${days.map(d => `<i class="${d.s}${d.cl ? ' cl' : ''}" data-tip="<b>${H.dates[d.i]}</b><br>${pn[d.s]}<br><span>action: ${d.c === '-' ? 'no committed record (kept as HOLD)' : ACTION[d.c]}</span>"></i>`).join('')}</div>`;
334
+ })() : '') +
335
+ `<div class="d1-legend"><span><i class="sw L"></i>Long-Short</span><span><i class="sw R"></i>Short-Long</span><span><i class="sw F"></i>Flat</span><span><i class="dia"></i>Close</span></div>`);
336
+ // Market Insights: report quality per asset, rating-strategy CR vs Buy & Hold
337
+ const qs = A.map(a => O['Market Insights'][a].Score[k]), qsd = A.map(a => S['Market Insights'][a].Score[k]);
338
+ const insights = card('Market Insights', `Report quality (0–10), rubric over ${M.insightsRubric.length} dimensions`,
339
+ `<div class="d1-big">${num(mean(qs))}<small>mean / 10</small></div>` +
340
+ `<div class="d1-q hd"><span></span><span></span><span>score</span><span data-tip="CR of the weekly ratings replayed as a strategy">CR</span><span data-tip="Buy &amp; Hold CR">B&amp;H</span></div>` +
341
+ A.map((a, i) => { const cr = O['Market Insights'][a].CR[k], bh = H.baseline[`Market Insights|${a}|CR`];
342
+ return `<div class="d1-q" data-tip="<b>${a}</b><br>report quality ${num(qs[i])} ±${(qsd[i] ?? 0).toFixed(2)} / 10<br>CR of ratings ${pct(cr)} ±${(S['Market Insights'][a].CR[k] ?? 0).toFixed(2)}<br><span>Buy &amp; Hold CR ${pct(bh)}</span>"><span class="d1-t">${a}</span><span class="q-bar"><i style="width:${(qs[i] || 0) * 10}%"></i></span><span class="val">${f1(qs[i])}</span><span class="val ${cr >= bh ? 'up' : 'dn'}">${pc0(cr)}</span><span class="val bh">${pc0(bh)}</span></div>`; }).join(''));
343
+ // Auditing: ACC and the error split (ACC + SER + EER + CER = 100%)
344
+ const MS4 = ['ACC', 'SER', 'EER', 'CER'], au = MS4.map(m => O.Auditing.AUDIT[m][k]), asd = MS4.map(m => S.Auditing.AUDIT[m][k]);
345
+ const NAMES = { ACC: 'Accuracy', SER: 'Schema error', EER: 'Extraction error', CER: 'Calculation error' };
346
+ const auditing = card('Auditing', `${M.auditInstances} XBRL instances · ACC + SER + EER + CER = 100%`,
347
+ `<div class="d1-big">${f1(au[0])}<small>% ACC${asd[0] == null ? '' : ' ±' + asd[0].toFixed(1)}</small></div>` +
348
+ `<div class="d1-stack">${MS4.map((m, i) => au[i] ? `<i class="s-${m.toLowerCase()}" style="width:${au[i]}%" data-tip="${NAMES[m]} (${m}): ${f1(au[i])}%"></i>` : '').join('')}</div>` +
349
+ MS4.map((m, i) => `<div class="d1-kv"><span><i class="s-${m.toLowerCase()} sq"></i>${NAMES[m]} <em class="ab">${m}</em></span><span>${f1(au[i])}%${sdS(asd[i])}</span></div>`).join(''));
350
+ return `<div class="d1"><div class="d1-cards">${trading}${hedging}${insights}${auditing}</div><p class="d1-foot">Raw metrics: mean of ${M.runs} runs <span class="sd">±std</span></p></div>`;
351
+ }
352
+ function toggle(i) {
353
+ const r = rows[i];
354
+ openKey = openKey === r.key ? null : r.key;
355
+ if (openKey) detail.innerHTML = breakdown(r);
356
+ layout();
357
+ }
358
+ detail.addEventListener('click', e => { const b = e.target.closest('[data-replay]'); if (b) { e.stopPropagation(); replayTo(b.dataset.replay); } });
359
+ els.forEach((el, i) => {
360
+ const t = $('.in', el);
361
+ t.addEventListener('click', () => toggle(i));
362
+ t.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(i); } });
363
+ });
364
+ fitNames(); document.fonts && document.fonts.ready.then(fitNames);
365
+ layout();
366
+
367
+ /* ================= 2b · RANKINGS ACROSS WORKFLOWS ================= */
368
+ // bump chart with linked highlighting: pick one system, a whole architecture, or a whole backbone
369
+ (() => {
370
+ const NS = 'http://www.w3.org/2000/svg';
371
+ const mk = (tag, attrs = {}, parent) => { const e = document.createElementNS(NS, tag); for (const k in attrs) e.setAttribute(k, attrs[k]); if (parent) parent.appendChild(e); return e; };
372
+ const n = rows.length;
373
+ // y position = order within a workflow (ties broken by Overall); label = competition rank, "=" marks a tie
374
+ const pos = {}, lab = {}, rnum = {};
375
+ WF.forEach(w => {
376
+ [...rows].sort((x, y) => y[w] - x[w] || y.Overall - x.Overall).forEach((r, i) => {
377
+ (pos[r.key] ||= {})[w] = i + 1;
378
+ const c = 1 + rows.filter(x => x[w] > r[w] + 1e-9).length, tie = rows.filter(x => Math.abs(x[w] - r[w]) < 1e-9).length > 1;
379
+ (lab[r.key] ||= {})[w] = (tie ? '=' : '') + c; (rnum[r.key] ||= {})[w] = c;
380
+ });
381
+ });
382
+ const topIn = w => [...rows].sort((x, y) => y[w] - x[w] || y.Overall - x.Overall);
383
+ const topBy = s => s === 'Overall' ? rows : s === 'Floor' ? [...rows].sort((x, y) => y.Floor - x.Floor || y.Overall - x.Overall) : topIn(s);
384
+ const ordinal = k => k + (k % 100 >= 11 && k % 100 <= 13 ? 'th' : ({ 1: 'st', 2: 'nd', 3: 'rd' }[k % 10] || 'th'));
385
+ const short = m => MS[m] || m;
386
+
387
+ /* ---------- chart (built once; selection only toggles classes) ---------- */
388
+ // side margins just wide enough for the longest labels: rank ticks + architecture on the left, backbone + rank on the right
389
+ const textW = (txt, size, weight = 500) => { const c = textW.c || (textW.c = document.createElement('canvas').getContext('2d')); c.font = `${weight} ${size}px Inter, system-ui, sans-serif`; return c.measureText(txt).width; };
390
+ const longAgent = Math.max(...H.agents.map(ag => textW(ag, 11.5))), longModel = Math.max(...rows.map(r => textW(`${short(r.model)} #${r.rank}`, 11.5, 600)));
391
+ const W = 1116, T = 52, rowH = 18, Hh = T + n * rowH + 10;
392
+ const L = Math.ceil(4 + textW('#30', 10.5) + 18 + longAgent + 20 + 6), R = Math.ceil(20 + longModel + 10);
393
+ const cx = i => L + i * (W - L - R) / (WF.length - 1), cy = p => T + (p - 1) * rowH + rowH / 2;
394
+ const svg = mk('svg', { viewBox: `0 0 ${W} ${Hh}`, role: 'img', 'aria-label': 'Rank of every agent system in each workflow' });
395
+ const defs = mk('defs', {}, svg), gr = mk('linearGradient', { id: 'wfGrad', gradientUnits: 'userSpaceOnUse', x1: cx(0), x2: cx(WF.length - 1), y1: 0, y2: 0 }, defs);
396
+ WF.forEach((w, i) => mk('stop', { offset: i / (WF.length - 1), 'stop-color': WC[w] }, gr));
397
+ WF.forEach((w, i) => {
398
+ mk('line', { x1: cx(i), x2: cx(i), y1: T - 10, y2: Hh - 4, class: 'lane' }, svg);
399
+ // header = button: follow this workflow's leader
400
+ const ld = leaders[w][0], hg = mk('g', { class: 'col-btn', 'data-k': ld.key, 'data-w': w, style: `--c:${WC[w]}`, tabindex: 0, role: 'button',
401
+ 'data-tip': `<b>${w} · top 3</b><br>${[...rows].sort((x, y) => y[w] - x[w] || y.Overall - x.Overall).slice(0, 3).map((r, j) => `${j + 1}. ${r.name}`).join('<br>')}<br><span>click to highlight</span>` }, svg);
402
+ const tw = w.length * 7.6 + 34;
403
+ mk('rect', { x: cx(i) - tw / 2, y: 2, width: tw, height: 26, rx: 13, class: 'cb-bg' }, hg);
404
+ mk('circle', { cx: cx(i) - tw / 2 + 13, cy: 15, r: 3.5, class: 'cb-dot' }, hg);
405
+ mk('text', { x: cx(i) + 6, y: 19.5, 'text-anchor': 'middle', class: 'col-h' }, hg).textContent = w;
406
+ });
407
+ [1, 5, 10, 15, 20, 25, 30].filter(p => p <= n).forEach(p => (mk('text', { x: 4, y: cy(p) + 3.5, class: 'tick' }, svg).textContent = `#${p}`));
408
+ const lines = {}, layer = mk('g', {}, svg), lbls = mk('g', { class: 'lbls' }, svg);
409
+ rows.forEach(r => {
410
+ const pts = WF.map((w, i) => [cx(i), cy(pos[r.key][w])]);
411
+ let d = `M${pts[0][0]},${pts[0][1]}`;
412
+ for (let i = 1; i < pts.length; i++) { const mx = (pts[i - 1][0] + pts[i][0]) / 2; d += ` C${mx},${pts[i - 1][1]} ${mx},${pts[i][1]} ${pts[i][0]},${pts[i][1]}`; }
413
+ const g = mk('g', { class: 'ln', 'data-key': r.key }, layer);
414
+ const tipAll = `<b>${r.name}</b><br>${WF.map(w => `${SHORT[w] || w} #${lab[r.key][w]}`).join('<span> · </span>')}<br><span>#${r.rank} Overall · click to follow</span>`;
415
+ mk('path', { d, class: 'line' }, g);
416
+ mk('path', { d, class: 'hit', 'data-tip': tipAll }, g);
417
+ WF.forEach((w, i) => {
418
+ mk('circle', { cx: pts[i][0], cy: pts[i][1], r: 2.6, style: `--wc:${WC[w]}`, 'data-tip': `<b>${r.name}</b><br>${w}: ${ordinal(rnum[r.key][w])}${lab[r.key][w][0] === '=' ? ' (tied)' : ''} of ${n}<span> · score ${f1(r[w])}</span>` }, g);
419
+ const t = mk('text', { x: pts[i][0], y: pts[i][1] + 4, class: 'rkl' }, g); t.textContent = lab[r.key][w];
420
+ if (lab[r.key][w].length > 2) t.style.fontSize = '9px';
421
+ });
422
+ lines[r.key] = g;
423
+ });
424
+ $('#rkChart').appendChild(svg);
425
+ svg.classList.add('rk-flow');
426
+
427
+ /* ---------- second view: workflow floor → Overall ---------- */
428
+ // one row per system (Overall order): its four workflow scores as coloured dots, floor (open ring) joined to Overall (filled dot)
429
+ const DW = 1116, DT = 58, DH = 17, DL = 250, DR = 70, dH = DT + n * DH + 34;
430
+ const dx = v => DL + v / 100 * (DW - DL - DR);
431
+ const db = mk('svg', { viewBox: `0 0 ${DW} ${dH}`, class: 'rk-db', role: 'img', 'aria-label': 'Workflow floor, workflow scores and Overall score of every agent system' });
432
+ // user-space gradient: a horizontal line has a zero-height bounding box, so a bbox gradient would not paint
433
+ const dgr = mk('linearGradient', { id: 'brandGrad', gradientUnits: 'userSpaceOnUse', x1: dx(0), x2: dx(100), y1: 0, y2: 0 }, mk('defs', {}, db));
434
+ mk('stop', { offset: 0, 'stop-color': '#b40028' }, dgr); mk('stop', { offset: 1, 'stop-color': '#7800f0' }, dgr);
435
+ // sort chips: Overall, workflow floor, then the four workflows (re-orders the rows; the highlight is unchanged)
436
+ // same pills as the other two views' headers: centred, tinted, filled when chosen
437
+ const DSORT = [['Overall', 'Overall', '#4a1a8c'], ['Floor', 'Workflow floor', '#4a1a8c'], ...WF.map(w => [w, w, WC[w]])];
438
+ const dpw = l => l.length * 7.6 + 34, dGap = 10;
439
+ let lx = (DW - DSORT.reduce((s, [, l]) => s + dpw(l), 0) - dGap * (DSORT.length - 1)) / 2, dbSort = 'Overall';
440
+ DSORT.forEach(([k, lbl, c]) => {
441
+ const tw = dpw(lbl), g = mk('g', { class: 'db-sort' + (k === dbSort ? ' on' : ''), 'data-s': k, style: `--c:${c}`, tabindex: 0, role: 'button',
442
+ 'data-tip': `sort by ${lbl}${k === 'Floor' ? '<br><span>ring = the weakest workflow’s dot</span>' : k === 'Overall' ? '<br><span>black dot = Overall</span>' : ''}` }, db);
443
+ mk('rect', { x: lx, y: 2, width: tw, height: 26, rx: 13, class: 'cb-bg' }, g);
444
+ if (k === 'Floor') { mk('circle', { cx: lx + 13, cy: 15, r: 2.2, class: 'cb-dot' }, g); mk('circle', { cx: lx + 13, cy: 15, r: 4.6, class: 'cb-ring' }, g); }
445
+ else mk('circle', { cx: lx + 13, cy: 15, r: 3.5, class: 'cb-dot' }, g);
446
+ mk('text', { x: lx + tw / 2 + 6, y: 19.5, 'text-anchor': 'middle', class: 'col-h' }, g).textContent = lbl;
447
+ lx += tw + dGap;
448
+ });
449
+ [0, 25, 50, 75, 100].forEach(v => {
450
+ mk('line', { x1: dx(v), x2: dx(v), y1: DT - 8, y2: DT + n * DH, class: 'lane' }, db);
451
+ mk('text', { x: dx(v), y: DT + n * DH + 18, 'text-anchor': 'middle', class: 'tick' }, db).textContent = v;
452
+ });
453
+ const dbRows = {}, dbLayer = mk('g', {}, db);
454
+ rows.forEach((r, i) => {
455
+ const y = DT + i * DH + DH / 2, vals = WF.map(w => r[w]), lo = Math.min(...vals), hi = Math.max(...vals);
456
+ const g = mk('g', { class: 'db', 'data-key': r.key }, dbLayer);
457
+ mk('rect', { x: 0, y: y - DH / 2, width: DW, height: DH, rx: 6, class: 'db-band', 'data-tip': `<b>#${r.rank} ${r.name}</b><br>Overall ${f1(r.Overall)} · floor ${f1(r.Floor)}<br>${WF.map(w => `${SHORT[w] || w} ${f1(r[w])}`).join('<span> · </span>')}` }, g);
458
+ mk('text', { x: DL - 46, y: y + 4, class: 'db-name', 'text-anchor': 'end' }, g).textContent = `${r.agent} · ${short(r.model)}`;
459
+ g.dataset.i = i; g.dataset.y = y;
460
+ mk('text', { x: DL - 14, y: y + 4, class: 'db-rank', 'text-anchor': 'end' }, g).textContent = `#${r.rank}`;
461
+ mk('line', { x1: dx(lo), x2: dx(hi), y1: y, y2: y, class: 'db-range' }, g);
462
+ mk('line', { x1: dx(r.Floor), x2: dx(r.Overall), y1: y, y2: y, class: 'db-gap' }, g);
463
+ WF.forEach(w => mk('circle', { cx: dx(r[w]), cy: y, r: 3.2, class: 'db-wf', style: `fill:${WC[w]}` }, g));
464
+ // floor = a transparent ring drawn around the weakest workflow's own dot (the dot shows through)
465
+ const fw = WF.find(w => r[w] === r.Floor);
466
+ mk('circle', { cx: dx(r.Floor), cy: y, r: 6, class: 'db-floor', 'data-tip': `<b>${r.name}</b><br>workflow floor: ${fw} ${f1(r.Floor)}` }, g);
467
+ mk('circle', { cx: dx(r.Overall), cy: y, r: 5.5, class: 'db-ov', 'data-dx': dx(r.Floor) - dx(r.Overall) }, g);
468
+ mk('text', { x: DW - DR + 18, y: y + 4, class: 'db-val' }, g).textContent = f1(r.Overall);
469
+ dbRows[r.key] = g;
470
+ });
471
+ // re-order rows (slide) by the chosen key; the rank and value columns follow that key
472
+ const keyOf = (r, s) => s === 'Overall' ? r.Overall : s === 'Floor' ? r.Floor : r[s];
473
+ function sortDb(s) {
474
+ dbSort = s;
475
+ [...rows].sort((p, q) => keyOf(q, s) - keyOf(p, s) || q.Overall - p.Overall).forEach((r, p) => {
476
+ const g = dbRows[r.key];
477
+ g.style.transform = `translateY(${(p - +g.dataset.i) * DH}px)`;
478
+ $('.db-rank', g).textContent = `#${1 + rows.filter(x => keyOf(x, s) > keyOf(r, s) + 1e-9).length}`;
479
+ $('.db-val', g).textContent = f1(keyOf(r, s));
480
+ });
481
+ $$('.db-sort', db).forEach(b => b.classList.toggle('on', b.dataset.s === s));
482
+ }
483
+ db.addEventListener('click', e => { const b = e.target.closest('.db-sort'); if (b) { sortDb(b.dataset.s); choose({ mode: 'sort', key: b.dataset.s }, true); } });
484
+ db.addEventListener('keydown', e => { const b = e.target.closest('.db-sort'); if (b && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); sortDb(b.dataset.s); choose({ mode: 'sort', key: b.dataset.s }, true); } });
485
+ db.style.display = 'none';
486
+ $('#rkChart').appendChild(db);
487
+
488
+ /* ---------- field view ("staff"): one line per architecture, one note per backbone, x = score ---------- */
489
+ // score = the chosen workflow (header pills), else Overall; follows the same selection state as the other views
490
+ const BB = { 'Claude Sonnet 5': 'Son5', 'GPT-5.4': 'GPT', 'DeepSeek-V3.2': 'DS3.2', 'DeepSeek-V4-Pro': 'DS4P', 'Qwen3.5-9B': 'Q9B', 'Qwen3.5-4B': 'Q4B' };
491
+ // same canvas height as the other two views (the card never changes size); the spare height is split above and below
492
+ const FW = W, FLANE = 92, FH = Hh, FT = 64 + Math.max(0, (FH - (64 + H.agents.length * FLANE + 70)) / 2), FL = 150, FR = 86, NR = 15.5, KR = 13.5;
493
+ const fx = v => FL + v / 100 * (FW - FL - FR);
494
+ const fv = mk('svg', { viewBox: `0 0 ${FW} ${FH}`, role: 'img', 'aria-label': 'Every agent system on one score axis, one line per architecture' });
495
+ // score pills: Overall + the four workflows (workflow pills reuse the column-header buttons)
496
+ const pills = ['Overall', ...WF], pw = p => p.length * 7.6 + 34, gapP = 10, totP = pills.reduce((s, p) => s + pw(p), 0) + gapP * (pills.length - 1);
497
+ let px0 = (FW - totP) / 2;
498
+ pills.forEach(p => {
499
+ const g = mk('g', p === 'Overall' ? { class: 'col-btn ov-btn', style: '--c:#4a1a8c', tabindex: 0, role: 'button', 'data-tip': 'Overall = mean of the four workflow scores' }
500
+ : { class: 'col-btn', 'data-w': p, style: `--c:${WC[p]}`, tabindex: 0, role: 'button', 'data-tip': `<b>${p}</b><br><span>place every system by its ${p} score</span>` }, fv);
501
+ mk('rect', { x: px0, y: 2, width: pw(p), height: 26, rx: 13, class: 'cb-bg' }, g);
502
+ mk('circle', { cx: px0 + 13, cy: 15, r: 3.5, class: 'cb-dot' }, g);
503
+ mk('text', { x: px0 + pw(p) / 2 + 6, y: 19.5, 'text-anchor': 'middle', class: 'col-h' }, g).textContent = p;
504
+ px0 += pw(p) + gapP;
505
+ });
506
+ const fGrid = mk('g', {}, fv), fLanes = mk('g', {}, fv), fTraces = mk('g', {}, fv), fNotes = mk('g', {}, fv);
507
+ const axY = FT + H.agents.length * FLANE + 6;
508
+ [0, 25, 50, 75, 100].forEach(v => { mk('line', { x1: fx(v), x2: fx(v), y1: FT - 8, y2: axY, class: 'f-grid' }, fGrid); mk('text', { x: fx(v), y: axY + 18, class: 'tick', 'text-anchor': 'middle' }, fGrid).textContent = v; });
509
+ mk('text', { x: FW - FR + 26, y: FT - 14, class: 'tick' }, fGrid).textContent = 'mean';
510
+ // key: note code -> backbone
511
+ // key: a small note with the code, then the backbone's full name
512
+ // key: a small dark note with the code, then the backbone's full name
513
+ const kW = H.models.map(m => 2 * KR + 8 + textW(m, 11.5)), kGap = 26, kTot = kW.reduce((s, w) => s + w, 0) + kGap * (H.models.length - 1);
514
+ let kx = (FW - kTot) / 2;
515
+ H.models.forEach((m, i) => {
516
+ const g = mk('g', { class: 'f-key', transform: `translate(${kx + KR},${axY + 42})` }, fGrid);
517
+ mk('circle', { r: KR, class: 'f-kdot' }, g);
518
+ mk('text', { y: 3.6, 'text-anchor': 'middle', class: 'f-kbb' }, g).textContent = BB[m];
519
+ mk('text', { x: KR + 8, y: 4, class: 'f-kname' }, g).textContent = m;
520
+ kx += kW[i] + kGap;
521
+ });
522
+ const fTrace = Object.fromEntries(H.models.map(m => [m, mk('path', { class: 'f-trace' }, fTraces)]));
523
+ const fLane = Object.fromEntries(H.agents.map(ag => {
524
+ const g = mk('g', { class: 'f-lane', 'data-a': ag }, fLanes);
525
+ mk('line', { x1: FL, x2: FW - FR, y1: 0, y2: 0, class: 'f-line' }, g);
526
+ mk('text', { x: FL - 22, y: 5, 'text-anchor': 'end', class: 'f-name', style: `fill:${AC[ag]}` }, g).textContent = ag;
527
+ mk('line', { y1: -NR - 8, y2: NR + 8, class: 'f-mean', style: `stroke:${AC[ag]}` }, g);
528
+ mk('text', { x: FW - FR + 26, y: 5, class: 'f-mv' }, g);
529
+ return [ag, g];
530
+ }));
531
+ const fNote = Object.fromEntries(rows.map(r => {
532
+ const g = mk('g', { class: 'f-note', 'data-key': r.key }, fNotes);
533
+ mk('circle', { r: NR, style: `fill:${AC[r.agent]}` }, g);
534
+ mk('text', { y: 3.4, 'text-anchor': 'middle', class: 'f-bb' }, g).textContent = BB[r.model];
535
+ return [r.key, g];
536
+ }));
537
+ let fPos = {};
538
+ function fieldLayout() {
539
+ const sc = st.mode === 'workflow' ? st.key : 'Overall', val = r => sc === 'Overall' ? r.Overall : r[sc];
540
+ $$('.col-btn', fv).forEach(b => b.classList.toggle('on', b.classList.contains('ov-btn') ? sc === 'Overall' : b.dataset.w === sc));
541
+ const mean = ag => { const v = rows.filter(r => r.agent === ag).map(val); return v.reduce((p, q) => p + q, 0) / v.length; };
542
+ const order = H.agents, ly = ag => FT + (order.indexOf(ag) + .5) * FLANE; // fixed: same row order as the picker
543
+ order.forEach(ag => {
544
+ const g = fLane[ag], m = mean(ag);
545
+ g.style.transform = `translateY(${ly(ag)}px)`;
546
+ const mk2 = $('.f-mean', g); mk2.setAttribute('x1', fx(m)); mk2.setAttribute('x2', fx(m));
547
+ $('.f-mv', g).textContent = f1(m);
548
+ g.dataset.tip = `<b>${ag}</b><br>mean ${sc}: ${f1(m)}<br><span>click to highlight its ${H.models.length} systems</span>`;
549
+ });
550
+ // within a lane, notes that would overlap are nudged up / down (small beeswarm)
551
+ fPos = {};
552
+ H.agents.forEach(ag => {
553
+ const placed = [];
554
+ rows.filter(r => r.agent === ag).sort((p, q) => val(p) - val(q)).forEach(r => {
555
+ const x = fx(val(r)); let dy = 0;
556
+ let best = -1; // first free slot; if none is free, the slot with the least overlap
557
+ for (const c of [0, -1, 1, -2, 2]) { const md = Math.min(1e9, ...placed.map(p => Math.hypot(p.x - x, p.dy - c * NR * 1.1)));
558
+ if (md >= NR * 1.9) { dy = c * NR * 1.1; break; } if (md > best) { best = md; dy = c * NR * 1.1; } }
559
+ placed.push({ x, dy }); fPos[r.key] = { x, y: ly(ag) + dy };
560
+ });
561
+ });
562
+ rows.forEach(r => {
563
+ const g = fNote[r.key], p = fPos[r.key];
564
+ g.style.transform = `translate(${p.x}px,${p.y}px)`;
565
+ g.dataset.tip = `<b>${r.name}</b><br>${sc}: ${f1(val(r))}<span> · #${1 + rows.filter(x => val(x) > val(r) + 1e-9).length} of ${n}</span>`;
566
+ });
567
+ }
568
+ // classes follow the shared selection; the dashed trace links one backbone across the five lines
569
+ function fieldApply(selKey, litKeys) {
570
+ fieldLayout();
571
+ rows.forEach(r => { const g = fNote[r.key]; g.classList.toggle('sel', r.key === selKey); g.classList.toggle('lit', litKeys.has(r.key)); g.classList.toggle('pv', r.key === pv); });
572
+ fv.classList.toggle('all', st.mode === 'none');
573
+ // the dashed trace links one backbone across the five lines: the hovered note's, the chosen column's,
574
+ // else the highlighted system's (by default the #1 system's)
575
+ const one = pv ? rows.find(r => r.key === pv).model : st.mode === 'model' ? st.key : rows.find(r => r.key === selKey).model;
576
+ H.models.forEach(m => {
577
+ const pts = rows.filter(r => r.model === m).map(r => fPos[r.key]).sort((p, q) => p.y - q.y);
578
+ fTrace[m].setAttribute('d', pts.map((p, i) => (i ? 'L' : 'M') + p.x.toFixed(1) + ',' + p.y.toFixed(1)).join(''));
579
+ fTrace[m].classList.toggle('on', m === one);
580
+ });
581
+ }
582
+ fNotes.addEventListener('click', e => { const g = e.target.closest('.f-note'); if (g) choose({ mode: 'system', key: g.dataset.key }); });
583
+ fNotes.addEventListener('pointerover', e => { const g = e.target.closest('.f-note'); const k = g ? g.dataset.key : null; if (k && k !== pv) { pv = k; apply(); } });
584
+ fNotes.addEventListener('pointerleave', () => { if (pv) { pv = null; apply(); } });
585
+ fLanes.addEventListener('click', e => { const g = e.target.closest('.f-lane'); if (g) choose({ mode: 'agent', key: g.dataset.a }); });
586
+ fv.addEventListener('click', e => { if (e.target.closest('.ov-btn')) choose({ mode: 'none' }, true); });
587
+ fv.addEventListener('keydown', e => { if (e.target.closest('.ov-btn') && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); choose({ mode: 'none' }, true); } });
588
+ fv.style.display = 'none';
589
+ $('#rkChart').appendChild(fv);
590
+ function growField() {
591
+ rows.forEach((r, i) => { const g = fNote[r.key]; g.animate([{ opacity: 0, transform: `translate(${fx(0)}px,${fPos[r.key].y}px)` }, { opacity: 1, transform: g.style.transform }], { duration: 900, delay: i * 18, easing: 'cubic-bezier(.22,1,.36,1)', fill: 'backwards' }); });
592
+ }
593
+ let view = 'flow';
594
+
595
+ /* ---------- picker: architecture rows × backbone columns ---------- */
596
+ const pk = $('#picker');
597
+ pk.innerHTML = '<span></span>' + H.models.map(m => `<button class="pk-col" data-m="${m}" title="All ${H.agents.length} architectures on ${m}">${short(m)}</button>`).join('') +
598
+ H.agents.map(ag => `<button class="pk-row" data-a="${ag}" title="All ${H.models.length} ${ag} systems">${ini(ag, 'ini sm')}${ag}</button>` +
599
+ H.models.map(m => { const r = rows.find(x => x.agent === ag && x.model === m);
600
+ return `<button class="pk-cell" data-k="${r.key}" style="--c:${AC[ag]}" data-tip="<b>${r.name}</b><br>#${r.rank} Overall · ${f1(r.Overall)}">${r.rank}</button>`; }).join('')).join('') +
601
+ '<p class="pk-note">Block = one agent system (number = Overall rank) · row = architecture · column = backbone</p>';
602
+
603
+ /* ---------- state ---------- */
604
+ let st = { mode: 'none' }, pv = null;
605
+ // one highlighted line (sel) plus secondary lines (lit) for every choice:
606
+ // none -> #1 Overall highlighted, every other line secondary
607
+ // workflow -> its #1 highlighted, its #2 and #3 secondary
608
+ // system -> that system highlighted, the same architecture on other backbones secondary
609
+ // agent / model -> the group's best Overall system highlighted, the rest of the row / column secondary
610
+ // rows are already in Overall order, so the first match is the group's best system
611
+ const selOf = s => s.mode === 'system' ? s.key : s.mode === 'workflow' ? topIn(s.key)[0].key : s.mode === 'sort' ? topBy(s.key)[0].key : s.mode === 'none' ? top.key
612
+ : rows.find(r => r[s.mode] === s.key).key;
613
+ const group = s => {
614
+ const k = selOf(s);
615
+ if (s.mode === 'agent' || s.mode === 'model') return rows.filter(r => r[s.mode] === s.key && r.key !== k);
616
+ if (s.mode === 'workflow') return topIn(s.key).slice(1, 3);
617
+ if (s.mode === 'sort') return topBy(s.key).slice(1, 3);
618
+ if (s.mode === 'system') { const ag = rows.find(x => x.key === k).agent; return rows.filter(r => r.agent === ag && r.key !== k); }
619
+ return [];
620
+ };
621
+ // labels are static text, the same for every highlighted line: architecture on the left, backbone + Overall rank on the right
622
+ function labels(shown) {
623
+ lbls.innerHTML = '';
624
+ const spread = arr => { arr.sort((p, q) => p.y - q.y); for (let i = 1; i < arr.length; i++) if (arr[i].y - arr[i - 1].y < 13) arr[i].y = arr[i - 1].y + 13; return arr; };
625
+ spread(shown.map(r => ({ y: cy(pos[r.key][WF[0]]), r }))).forEach(({ y, r }) =>
626
+ (mk('text', { x: cx(0) - 20, y: y + 4, class: 'end end-l', style: `fill:${AC[r.agent]}` }, lbls).textContent = r.agent));
627
+ spread(shown.map(r => ({ y: cy(pos[r.key][WF[WF.length - 1]]), r }))).forEach(({ y, r }) => {
628
+ const t = mk('text', { x: cx(WF.length - 1) + 20, y: y + 4, class: 'end' }, lbls);
629
+ t.textContent = short(r.model) + ' ';
630
+ mk('tspan', { class: 'rk-tag' }, t).textContent = `#${r.rank}`;
631
+ });
632
+ }
633
+ function apply() {
634
+ const lit = group(st), litKeys = new Set(lit.map(r => r.key)), selKey = selOf(st);
635
+ Object.entries(lines).forEach(([k, g]) => { g.classList.toggle('sel', k === selKey); g.classList.toggle('lit', litKeys.has(k)); g.classList.toggle('pv', k === pv); });
636
+ // draw order: faint lines, then the lit group, then the preview, then the selected line on top
637
+ [...lit.map(r => r.key), pv, selKey].filter(Boolean).forEach(k => layer.appendChild(lines[k]));
638
+ labels([...lit, ...(selKey ? [rows.find(x => x.key === selKey)] : [])]);
639
+ svg.classList.toggle('all', st.mode === 'none');
640
+ $$('.pk-cell', pk).forEach(c => { c.classList.toggle('sel', c.dataset.k === selKey); c.classList.toggle('lit', litKeys.has(c.dataset.k)); c.classList.toggle('pv', c.dataset.k === pv); });
641
+ $$('.pk-row', pk).forEach(b => b.classList.toggle('on', st.mode === 'agent' && b.dataset.a === st.key));
642
+ $$('.pk-col', pk).forEach(b => b.classList.toggle('on', st.mode === 'model' && b.dataset.m === st.key));
643
+ $$('#rkChart .col-btn').forEach(b => b.classList.toggle('on', st.mode === 'workflow' && b.dataset.w === st.key));
644
+ Object.entries(dbRows).forEach(([k, g]) => { g.classList.toggle('sel', k === selKey); g.classList.toggle('lit', litKeys.has(k)); g.classList.toggle('pv', k === pv); });
645
+ db.classList.toggle('all', st.mode === 'none');
646
+ fieldApply(selKey, litKeys);
647
+ }
648
+ // dots appear as the growing line reaches their column (the ease-out curve gets there early)
649
+ const DOT_AT = [0, 70, 160, 300];
650
+ const pop = (c, delay) => c.animate([{ opacity: 0, transform: 'scale(.3)' }, { opacity: 1, transform: 'scale(1)' }], { duration: 260, delay, easing: 'cubic-bezier(.34,1.56,.64,1)', fill: 'backwards' });
651
+ // the lit and selected lines grow in again after every click (same effect as on first view)
652
+ function grow(force) {
653
+ if (reduce) return;
654
+ if (view === 'floor') return growDb(force);
655
+ if (view === 'field') return force && growField();
656
+ const keys = force || st.mode === 'none' ? rows.map(r => r.key) : [...group(st).map(r => r.key), selOf(st)].filter(Boolean);
657
+ keys.forEach((k, i) => {
658
+ const p = $('path.line', lines[k]), l = p.getTotalLength();
659
+ p.getAnimations().forEach(x => x.cancel());
660
+ p.animate([{ strokeDasharray: l, strokeDashoffset: l }, { strokeDasharray: l, strokeDashoffset: 0 }], { duration: 900, delay: i * 45, easing: 'cubic-bezier(.22,1,.36,1)' });
661
+ $$('circle', lines[k]).forEach((c, j) => pop(c, i * 45 + DOT_AT[j]));
662
+ });
663
+ }
664
+ // clicking the current choice again clears it
665
+ // floor → Overall view: the Overall dot slides out from the floor, the gap line stretches, workflow dots pop in
666
+ function growDb(all) {
667
+ const keys = all || st.mode === 'none' ? rows.map(r => r.key) : [...group(st).map(r => r.key), selOf(st)].filter(Boolean);
668
+ keys.forEach((k, i) => {
669
+ const g = dbRows[k], d = i * (all ? 22 : 50), ov = $('.db-ov', g);
670
+ ov.animate([{ transform: `translateX(${ov.dataset.dx}px)` }, { transform: 'none' }], { duration: 800, delay: d, easing: 'cubic-bezier(.22,1,.36,1)', fill: 'backwards' });
671
+ $('.db-gap', g).animate([{ transform: 'scaleX(0)' }, { transform: 'none' }], { duration: 800, delay: d, easing: 'cubic-bezier(.22,1,.36,1)', fill: 'backwards' });
672
+ $('.db-range', g).animate([{ opacity: 0 }, { opacity: 1 }], { duration: 500, delay: d + 150, fill: 'backwards' });
673
+ $$('.db-wf', g).forEach((c, j) => pop(c, d + 120 + j * 60));
674
+ });
675
+ }
676
+ const choose = (s, keep) => { st = (!keep && s.mode === st.mode && s.key === st.key) ? { mode: 'none' } : s; pv = null; apply(); grow(); };
677
+ pk.addEventListener('click', e => {
678
+ const c = e.target.closest('.pk-cell'), rw = e.target.closest('.pk-row'), cl = e.target.closest('.pk-col');
679
+ if (c) choose({ mode: 'system', key: c.dataset.k }); else if (rw) choose({ mode: 'agent', key: rw.dataset.a }); else if (cl) choose({ mode: 'model', key: cl.dataset.m });
680
+ });
681
+ pk.addEventListener('pointerover', e => { const c = e.target.closest('.pk-cell'); const k = c ? c.dataset.k : null; if (k !== pv) { pv = k; apply(); } });
682
+ pk.addEventListener('pointerleave', () => { if (pv) { pv = null; apply(); } });
683
+ layer.addEventListener('pointerover', e => { const g = e.target.closest('.ln'); const k = g ? g.dataset.key : null; if (k && k !== pv) { pv = k; apply(); } });
684
+ layer.addEventListener('pointerleave', () => { if (pv) { pv = null; apply(); } });
685
+ layer.addEventListener('click', e => { const g = e.target.closest('.ln'); if (g) choose({ mode: 'system', key: g.dataset.key }); });
686
+ $('#rkChart').addEventListener('click', e => { const b = e.target.closest('.col-btn[data-w]'); if (b) choose({ mode: 'workflow', key: b.dataset.w }); });
687
+ dbLayer.addEventListener('click', e => { const g = e.target.closest('.db'); if (g) choose({ mode: 'system', key: g.dataset.key }); });
688
+ dbLayer.addEventListener('pointerover', e => { const g = e.target.closest('.db'); const k = g ? g.dataset.key : null; if (k && k !== pv) { pv = k; apply(); } });
689
+ dbLayer.addEventListener('pointerleave', () => { if (pv) { pv = null; apply(); } });
690
+ // view switch
691
+ const vs = $('#rkView'), vPill = $('.seg-pill', vs);
692
+ const placeV = () => { const b = $('button[aria-selected=true]', vs); vPill.style.width = b.offsetWidth + 'px'; vPill.style.transform = `translateX(${b.offsetLeft}px)`; };
693
+ vs.addEventListener('click', e => {
694
+ const b = e.target.closest('button'); if (!b || b.dataset.v === view) return;
695
+ view = b.dataset.v; $$('button', vs).forEach(x => x.setAttribute('aria-selected', x === b)); placeV();
696
+ svg.style.display = view === 'flow' ? '' : 'none'; db.style.display = view === 'floor' ? '' : 'none'; fv.style.display = view === 'field' ? '' : 'none';
697
+ grow(true);
698
+ });
699
+ requestAnimationFrame(placeV); document.fonts && document.fonts.ready.then(placeV); addEventListener("resize", placeV);
700
+ $('#rkChart').addEventListener('keydown', e => { const b = e.target.closest('.col-btn[data-w]'); if (b && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); choose({ mode: 'workflow', key: b.dataset.w }); } });
701
+ apply();
702
+ rkGo = (v, s) => { const b = $(`button[data-v="${v}"]`, vs); if (b && v !== view) b.click(); if (s) choose(s, true); };
703
+
704
+ /* ---------- draw-in when the chart scrolls into view ---------- */
705
+ if (reduce || !('IntersectionObserver' in window) || /[?&]all\b/.test(location.search)) return;
706
+ const paths = rows.map(r => $('path.line', lines[r.key]));
707
+ paths.forEach(p => { const l = p.getTotalLength(); p.style.strokeDasharray = l; p.style.strokeDashoffset = l; });
708
+ $$('circle', layer).forEach(c => (c.style.opacity = 0));
709
+ new IntersectionObserver((es, ob) => es.forEach(e => {
710
+ if (!e.isIntersecting) return; ob.disconnect();
711
+ paths.forEach((p, i) => {
712
+ const l = +p.style.strokeDasharray;
713
+ p.animate([{ strokeDashoffset: l }, { strokeDashoffset: 0 }], { duration: 1100, delay: 150 + i * 28, easing: 'cubic-bezier(.22,1,.36,1)', fill: 'forwards' })
714
+ .onfinish = () => { p.style.strokeDasharray = ''; p.style.strokeDashoffset = ''; };
715
+ });
716
+ rows.forEach((r, i) => $$('circle', lines[r.key]).forEach((c, j) => pop(c, 150 + i * 28 + DOT_AT[j]).onfinish = () => (c.style.opacity = '')));
717
+ }), { threshold: .2 }).observe($('#rkChart'));
718
+ })();
719
+
720
+ /* ================= 2c · REPLAY: one system's Trading and Hedging trajectories, drawn day by day ================= */
721
+ const replayTo = (() => {
722
+ const NS = 'http://www.w3.org/2000/svg', D = H.dates, ND = D.length;
723
+ const mk = (tag, attrs = {}, parent) => { const e = document.createElementNS(NS, tag); for (const k in attrs) e.setAttribute(k, attrs[k]); if (parent) parent.appendChild(e); return e; };
724
+ const ACOL = { B: '#1593b5', S: '#e0475b', H: 'rgba(185,181,207,.6)' }, PCOL = { L: '#3567d8', R: '#e0475b', F: 'rgba(185,181,207,.45)' };
725
+ const ANAME = { B: 'BUY', S: 'SELL', H: 'HOLD', L: 'LONG_SHORT', R: 'SHORT_LONG', C: 'CLOSE', '-': 'no committed record (scored as HOLD)' };
726
+ const date = d => new Date(d + 'T00:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
727
+ const W = 560, PH = 150, GAP = 8, SH = 14, L = 40, R = 34, T = 18, Hh = T + PH + GAP + SH + 20;
728
+ const X = i => L + (i + .5) * (W - L - R) / ND, cw = (W - L - R) / ND;
729
+ let sys = top, tk = H.tradeAssets[0], run = 1, raf = null;
730
+ const RN = H.runs;
731
+
732
+ // one chart: price line(s) with a purple wash underneath, a day-by-day strip, a dot riding the
733
+ // first line and a pill that names each day's action / position; returns handles for the animation
734
+ function chart(host, series, cells, labels, info) {
735
+ host.innerHTML = '';
736
+ const svg = mk('svg', { viewBox: `0 0 ${W} ${Hh}` }, host), defs = mk('defs', {}, svg), id = host.id;
737
+ const fg = mk('linearGradient', { id: id + 'Fill', x1: 0, x2: 0, y1: 0, y2: 1 }, defs);
738
+ mk('stop', { offset: 0, 'stop-color': '#7800f0', 'stop-opacity': .14 }, fg); mk('stop', { offset: 1, 'stop-color': '#7800f0', 'stop-opacity': 0 }, fg);
739
+ const all = series.flat(), lo = Math.min(...all), hi = Math.max(...all), pad = (hi - lo) * .08 || 1;
740
+ const Y = v => T + (1 - (v - lo + pad) / (hi - lo + 2 * pad)) * PH;
741
+ [0, .5, 1].forEach(f => { const v = lo - pad + f * (hi - lo + 2 * pad);
742
+ mk('line', { x1: L, x2: W - R, y1: Y(v), y2: Y(v), class: 'rp-gridline' }, svg);
743
+ mk('text', { x: L - 6, y: Y(v) + 3, 'text-anchor': 'end', class: 'rp-ax' }, svg).textContent = v.toFixed(0); });
744
+ [0, Math.floor(ND / 2), ND - 1].forEach(i => mk('text', { x: X(i), y: Hh - 4, 'text-anchor': 'middle', class: 'rp-ax' }, svg).textContent = date(D[i]));
745
+ const cr = mk('rect', { x: 0, y: 0, width: L, height: Hh }, mk('clipPath', { id: id + 'Clip' }, defs));
746
+ const g = mk('g', { 'clip-path': `url(#${id}Clip)` }, svg);
747
+ const pts = s => series[s].map((v, i) => `${X(i).toFixed(1)},${Y(v).toFixed(1)}`).join(' ');
748
+ mk('polygon', { points: `${X(0)},${T + PH} ${pts(0)} ${X(ND - 1)},${T + PH}`, fill: `url(#${id}Fill)` }, g);
749
+ series.forEach((p, s) => mk('polyline', { points: pts(s), class: 'rp-line' + (s ? ' b' : '') }, g));
750
+ // end labels, nudged apart when the two legs finish close together
751
+ const ly = (labels || []).map((t, s) => ({ t, s, y: Y(series[s][ND - 1]) + 3 })).sort((p, q) => p.y - q.y);
752
+ for (let k = 1; k < ly.length; k++) if (ly[k].y - ly[k - 1].y < 11) { const m = (ly[k].y + ly[k - 1].y) / 2; ly[k - 1].y = m - 5.5; ly[k].y = m + 5.5; }
753
+ ly.forEach(({ t, s, y }) => mk('text', { x: W - R + 4, y, class: 'rp-lab', fill: s ? '#8a93a3' : '#0f1528' }, svg).textContent = t);
754
+ const y0 = T + PH + GAP;
755
+ const cellEls = cells.map((c, i) => mk('rect', { x: (L + i * cw + .4).toFixed(2), y: y0, width: (cw - .8).toFixed(2), height: SH, rx: 1.5, class: 'rp-cell pending', fill: c.fill, 'data-tip': c.tip }, svg));
756
+ const dias = cells.map((c, i) => c.diamond ? mk('rect', { x: L + i * cw + cw / 2 - 3, y: y0 + SH / 2 - 3, width: 6, height: 6, fill: '#8b3fd9', transform: `rotate(45 ${L + i * cw + cw / 2} ${y0 + SH / 2})`, class: 'rp-cell pending' }, svg) : null);
757
+ const vl = mk('line', { x1: L, x2: L, y1: T - 4, y2: y0 + SH, stroke: '#0f1528', 'stroke-opacity': .18, 'stroke-dasharray': '2 3' }, svg);
758
+ const dot = mk('circle', { r: 4.5, stroke: '#fff', 'stroke-width': 2, fill: '#0f1528', class: 'rp-dot' }, svg);
759
+ const status = mk('text', { x: L, y: 10, class: 'rp-status' }, svg);
760
+ const pill = mk('rect', { x: 0, y: 0, width: 60, height: 15, rx: 7.5, class: 'rp-pill' }, svg), ptx = mk('text', { x: 0, y: 10.8, 'text-anchor': 'middle', class: 'rp-pilltext' }, svg);
761
+ // hover: a vertical line follows the pointer; the tooltip lists that day's values
762
+ if (info) {
763
+ const hv = mk('line', { y1: T - 4, y2: y0 + SH, class: 'rp-hover', opacity: 0 }, svg);
764
+ const hd = series.map((p, s) => mk('circle', { r: 3, class: 'rp-hdot' + (s ? ' b' : ''), opacity: 0 }, svg));
765
+ const hit = mk('rect', { x: L, y: 0, width: W - L - R, height: y0 + SH, fill: 'transparent' }, svg);
766
+ hit.addEventListener('pointermove', e => {
767
+ const b = svg.getBoundingClientRect(), sx = (e.clientX - b.left) / b.width * W;
768
+ const i = Math.max(0, Math.min(ND - 1, Math.floor((sx - L) / cw))), x = X(i);
769
+ hv.setAttribute('x1', x); hv.setAttribute('x2', x); hv.setAttribute('opacity', 1);
770
+ hd.forEach((d, s) => { d.setAttribute('cx', x); d.setAttribute('cy', Y(series[s][i])); d.setAttribute('opacity', 1); });
771
+ tip.innerHTML = info(i); tip.classList.add('on');
772
+ });
773
+ hit.addEventListener('pointerleave', () => { hv.setAttribute('opacity', 0); hd.forEach(d => d.setAttribute('opacity', 0)); tip.classList.remove('on'); });
774
+ }
775
+ return { svg, cr, cellEls, dias, vl, dot, status, pill, ptx, cells, Y, line: series[0] };
776
+ }
777
+ const hatch = 'url(#rpHatch)';
778
+ const trow = (k, v) => `<div class="tr"><span>${k}</span><b>${v}</b></div>`;
779
+ function hatchDef(svg) { const p = mk('pattern', { id: 'rpHatch', width: 4, height: 4, patternUnits: 'userSpaceOnUse', patternTransform: 'rotate(45)' }, mk('defs', {}, svg)); mk('rect', { width: 4, height: 4, fill: '#fff' }, p); mk('line', { x1: 0, y1: 0, x2: 0, y2: 4, stroke: '#d8d6e0', 'stroke-width': 1.6 }, p); }
780
+
781
+ function render() {
782
+ const k = sys.key, O = H.offline, Sd = H.offlineStd;
783
+ $$('#rpArch button').forEach(b => b.classList.toggle('sel', b.dataset.a === sys.agent));
784
+ $$('#rpModel button').forEach(b => b.classList.toggle('sel', b.dataset.m === sys.model));
785
+ $$('#rpRun button').forEach(b => b.classList.toggle('on', +b.dataset.r === run));
786
+ $('#rpSub').textContent = `Every committed decision of one agent system over the ${ND} offline trading days. Pick an architecture, a backbone and a run, or open a system from the leaderboard.`;
787
+ $$('#rpTk button').forEach(b => b.classList.toggle('on', b.dataset.t === tk));
788
+ // Trading: price + BUY / SELL / HOLD strip
789
+ const acts = [...((RN.trading[`${k}|${tk}`] || [])[run - 1] || '-'.repeat(ND))], px = H.prices[tk];
790
+ const tr = chart($('#rpTrade'), [px], acts.map((c, i) => ({ fill: c === '-' ? hatch : ACOL[c], pill: [c === '-' ? 'NO RECORD' : ANAME[c], c === '-' ? hatch : c === 'H' ? '#d9d7e3' : ACOL[c], c === '-' || c === 'H'], dot: c === 'B' || c === 'S' ? ACOL[c] : '#0f1528', tip: `<b>${tk} · ${date(D[i])}</b><br>close $${px[i].toFixed(2)}<br>${ANAME[c]}` })), null, i => `<div class="tt">${tk} · ${date(D[i])}</div>${trow('Close', '$' + px[i].toFixed(2))}${trow('Action', ANAME[acts[i]])}${trow('Run', run + ' of ' + M.runs)}`);
791
+ hatchDef(tr.svg);
792
+ const [cr, sr, mdd] = (RN.tradingMetrics[`${k}|${tk}`] || [])[run - 1] || [], bh = H.baseline[`Trading|${tk}|CR`];
793
+ $('#rpTradeFoot').innerHTML = `<div><b class="${cr >= bh ? 'up' : 'dn'}">${pct(cr)}</b><span>CR · run ${run}</span></div><em class="vs">vs.</em><div><b>${pct(bh)}</b><span>Buy &amp; Hold</span></div><div class="dim"><b>${num(sr)}</b><span>SR</span></div><div class="dim"><b>${mdd == null ? '—' : mdd.toFixed(2) + '%'}</b><span>MDD</span></div>`;
794
+ // Hedging: both legs indexed to 100 + position strip (paper Figure 3b encoding)
795
+ const pair = (RN.pairs[k] || [])[run - 1], pr = H.pairRuns[k], hs = (RN.hedging[k] || [])[run - 1];
796
+ let hc = null;
797
+ if (pair && hs) {
798
+ const [pl, prt] = pair.split('/'), ix = p => p.map(v => v / p[0] * 100);
799
+ let p = 'F';
800
+ const cells = [...hs].map((c, i) => { const cl = c === 'C'; if (c === 'L' || c === 'R') p = c; else if (cl) p = 'F';
801
+ const pn = cl ? 'CLOSE' : { L: 'LONG-SHORT', R: 'SHORT-LONG', F: 'FLAT' }[p], pc = cl ? '#8b3fd9' : p === 'F' ? '#d9d7e3' : PCOL[p];
802
+ return { fill: PCOL[p], diamond: cl, pill: [pn, pc, p === 'F' && !cl], dot: p === 'F' && !cl ? '#0f1528' : pc, tip: `<b>${date(D[i])}</b><br>${{ L: 'Long-Short', R: 'Short-Long', F: 'Flat' }[p]}<br><span>action: ${ANAME[c]}</span>` }; });
803
+ const li = ix(H.prices[pl]), ri = ix(H.prices[prt]), posN = { L: `long ${pl} / short ${prt}`, R: `short ${pl} / long ${prt}`, F: 'flat' };
804
+ let q = 'F'; const posArr = [...hs].map(c => { if (c === 'L' || c === 'R') q = c; else if (c === 'C') q = 'F'; return q; });
805
+ hc = chart($('#rpHedge'), [li, ri], cells, [pl, prt], i => `<div class="tt">${pl} / ${prt} · ${date(D[i])}</div>${trow(pl + ' (idx)', li[i].toFixed(1))}${trow(prt + ' (idx)', ri[i].toFixed(1))}${trow('Action', ANAME[hs[i]])}${trow('Position', posN[posArr[i]])}`);
806
+ const same = (RN.pairs[k] || []).filter(x => x === pair).length;
807
+ $('#rpPair').innerHTML = `${pl} / ${prt}<small>chosen in ${same}/${M.runs} runs · Long-Short = long ${pl}</small>`;
808
+ } else { $('#rpHedge').innerHTML = '<p class="sub">No pair selected in this run.</p>'; $('#rpPair').textContent = ''; }
809
+ const [hcr, hsr, hmdd] = (RN.hedgingMetrics[k] || [])[run - 1] || [];
810
+ $('#rpHedgeFoot').innerHTML = `<div><b class="${hcr >= 0 ? 'up' : 'dn'}">${pct(hcr)}</b><span>CR · run ${run}</span></div><div class="dim"><b>${num(hsr)}</b><span>SR</span></div><div class="dim"><b>${hmdd == null ? '—' : hmdd.toFixed(2) + '%'}</b><span>MDD</span></div>`;
811
+ play([tr, hc].filter(Boolean));
812
+ }
813
+ // draw both charts left to right over ~4 s (eased); strips light up, the dot rides the line, the pill names the day
814
+ let timer = null;
815
+ function play(charts) {
816
+ cancelAnimationFrame(raf); clearTimeout(timer);
817
+ const feet = [$('#rpTradeFoot'), $('#rpHedgeFoot')], shown = charts.map(() => -1);
818
+ function frame(p) {
819
+ const f = p * (ND - 1), i = Math.min(ND - 1, Math.floor(f + 1e-6)), fr = f - i, x = X(f);
820
+ charts.forEach((c, ci) => {
821
+ const v = i < ND - 1 ? c.line[i] + (c.line[i + 1] - c.line[i]) * fr : c.line[ND - 1];
822
+ c.cr.setAttribute('width', x + 1);
823
+ c.dot.setAttribute('cx', x); c.dot.setAttribute('cy', c.Y(v));
824
+ c.vl.setAttribute('x1', x); c.vl.setAttribute('x2', x);
825
+ if (i === shown[ci]) return;
826
+ for (let k = shown[ci] + 1; k <= i; k++) { c.cellEls[k].classList.remove('pending'); if (c.dias[k]) c.dias[k].classList.remove('pending'); }
827
+ shown[ci] = i;
828
+ const [txt, fill, dark] = c.cells[i].pill;
829
+ c.status.textContent = `Day ${String(i + 1).padStart(2, '0')}/${ND} · ${date(D[i])}`;
830
+ const bw = c.status.getComputedTextLength(), pw = txt.length * 6.4 + 16;
831
+ c.pill.setAttribute('x', L + bw + 10); c.pill.setAttribute('width', pw); c.pill.setAttribute('fill', fill);
832
+ c.ptx.setAttribute('x', L + bw + 10 + pw / 2); c.ptx.textContent = txt; c.ptx.classList.toggle('dark', dark);
833
+ c.dot.setAttribute('fill', c.cells[i].dot);
834
+ });
835
+ }
836
+ const done = () => { frame(1); charts.forEach(c => c.vl.setAttribute('stroke-opacity', 0)); feet.forEach(f => f.classList.remove('pending')); };
837
+ if (reduce || document.hidden) { done(); return; }
838
+ feet.forEach(f => f.classList.add('pending'));
839
+ const t0 = performance.now(), dur = 4000;
840
+ const step = t => { const p = Math.max(0, Math.min(1, (t - t0) / dur)); frame(1 - Math.pow(1 - p, 1.6)); if (p < 1) raf = requestAnimationFrame(step); else done(); };
841
+ frame(0); raf = requestAnimationFrame(step);
842
+ timer = setTimeout(done, dur + 600); // fallback if rAF is throttled
843
+ }
844
+ // controls
845
+ const pickSys = (ag, m) => { sys = rows.find(r => r.agent === ag && r.model === m); render(); };
846
+ $('#rpArch').innerHTML = H.agents.map(ag => `<button type="button" data-a="${ag}">${ini(ag, 'ini sm')}${ag}</button>`).join('');
847
+ $('#rpModel').innerHTML = H.models.map(m => `<button type="button" data-m="${m}">${MS[m] || m}</button>`).join('');
848
+ $('#rpArch').addEventListener('click', e => { const b = e.target.closest('button'); if (b) pickSys(b.dataset.a, sys.model); });
849
+ $('#rpModel').addEventListener('click', e => { const b = e.target.closest('button'); if (b) pickSys(sys.agent, b.dataset.m); });
850
+ $('#rpRun').innerHTML = Array.from({ length: M.runs }, (_, i) => `<button type="button" data-r="${i + 1}">Run ${i + 1}</button>`).join('');
851
+ $('#rpRun').addEventListener('click', e => { const b = e.target.closest('button'); if (b) { run = +b.dataset.r; render(); } });
852
+ $('#rpTk').innerHTML = H.tradeAssets.map(t => `<button type="button" data-t="${t}">${t}</button>`).join('');
853
+ $('#rpTk').addEventListener('click', e => { const b = e.target.closest('button'); if (b) { tk = b.dataset.t; render(); } });
854
+ $('#rpAgain').addEventListener('click', render);
855
+ // first draw when the section scrolls into view
856
+ let drawn = false;
857
+ const go = () => { if (!drawn) { drawn = true; render(); } };
858
+ if (reduce || !('IntersectionObserver' in window) || /[?&]all\b/.test(location.search)) go();
859
+ else new IntersectionObserver((es, ob) => es.forEach(e => { if (e.isIntersecting) { ob.disconnect(); go(); } }), { threshold: .3 }).observe($('#replay'));
860
+ // entry point used by the leaderboard rows
861
+ return key => { sys = rows.find(r => r.key === key); drawn = true; document.getElementById('replay').scrollIntoView({ behavior: reduce ? 'auto' : 'smooth', block: 'start' }); setTimeout(render, reduce ? 0 : 450); };
862
+ })();
863
+
864
+ /* ================= 3 · PARTNERS ================= */
865
+ // collaborator institutions from the arXiv paper's affiliations (names only until logos are collected)
866
+ const INSTITUTIONS = ['Columbia University', 'Stevens Institute of Technology', 'New York University', 'Georgia Institute of Technology',
867
+ 'Université de Montréal', 'University of Minnesota', 'University of Massachusetts Boston', 'AIST', 'University of Liverpool',
868
+ 'Vrije Universiteit Amsterdam', 'National University of Singapore', 'Halmstad University', 'Cardiff University', 'Mila – Quebec AI Institute'];
869
+ $('#pNames').innerHTML = INSTITUTIONS.map(n => `<span>${n}</span>`).join(' <i>·</i> '); // spaces = line-break points
870
+ $('#copyBib').addEventListener('click', () => {
871
+ const b = $('#copyBib');
872
+ (navigator.clipboard ? navigator.clipboard.writeText($('#bib').textContent) : Promise.reject()).then(() => b.textContent = 'Copied', () => b.textContent = 'Select & copy');
873
+ setTimeout(() => b.textContent = 'Copy BibTeX', 1800);
874
+ });
875
+
876
+
877
+ /* ================= hero background: real price paths as thin drifting lines ================= */
878
+ // Each line is one stock's 61 offline closes. To loop seamlessly the path runs forward then mirrored
879
+ // (period = 2 × width), and the group drifts left by one period every 160 s.
880
+ function drawFlow() {
881
+ const svg = $('#flow'), W = Math.max(800, svg.clientWidth), Hh = Math.max(500, svg.clientHeight);
882
+ svg.setAttribute('viewBox', `0 0 ${W} ${Hh}`);
883
+ const syms = ['ADBE', 'META', 'AMZN', 'GOOGL', 'TSLA', 'AAPL', 'NVDA', 'MSFT'].filter(s => H.prices[s]);
884
+ // Catmull-Rom through the points, as cubic Bézier segments [start, c1, c2, end] (rounded like the path string)
885
+ const r1 = v => Math.round(v * 10) / 10;
886
+ const segsOf = pts => pts.slice(0, -1).map((p1, i) => { const p0 = pts[Math.max(0, i - 1)], p2 = pts[i + 1], p3 = pts[Math.min(pts.length - 1, i + 2)];
887
+ return [p1, [p1[0] + (p2[0] - p0[0]) / 6, p1[1] + (p2[1] - p0[1]) / 6], [p2[0] - (p3[0] - p1[0]) / 6, p2[1] - (p3[1] - p1[1]) / 6], p2].map(q => [r1(q[0]), r1(q[1])]); });
888
+ const f1p = q => `${q[0].toFixed(1)},${q[1].toFixed(1)}`;
889
+ const smooth = segs => `M${f1p(segs[0][0])}` + segs.map(([, c1, c2, e]) => ` C${f1p(c1)} ${f1p(c2)} ${f1p(e)}`).join('');
890
+ // ridge: the curve's y at every integer x over one 2W period. Same stepping as SVG getPointAtLength every
891
+ // 2 units of arc length (the heroes step along it the way they always did), measured on the segments in JS.
892
+ const ridgeOf = segs => { const r = new Float32Array(Math.ceil(2 * W) + 1);
893
+ let px = segs[0][0][0], py = segs[0][0][1], acc = 0, s = 2, last = 0, ly = py, done = false;
894
+ const put = (x, y) => { if (x > 2 * W) { done = true; return; } for (let i = last; i <= Math.floor(x); i++) r[i] = y; last = Math.floor(x) + 1; ly = y; };
895
+ put(px, py);
896
+ for (const [a, b, c, d] of segs) { if (done) break;
897
+ for (let j = 1; j <= 64 && !done; j++) { const t = j / 64, u = 1 - t;
898
+ const x = u * u * u * a[0] + 3 * u * u * t * b[0] + 3 * u * t * t * c[0] + t * t * t * d[0], y = u * u * u * a[1] + 3 * u * u * t * b[1] + 3 * u * t * t * c[1] + t * t * t * d[1];
899
+ const dl = Math.hypot(x - px, y - py);
900
+ while (!done && acc + dl >= s) { const f = (s - acc) / dl; put(px + (x - px) * f, py + (y - py) * f); s += 2; }
901
+ acc += dl; px = x; py = y; } }
902
+ for (let i = last; i < r.length; i++) r[i] = ly; // the period's last pixels keep the last height
903
+ return r; };
904
+ const ridges = [];
905
+ const labels = [];
906
+ let out = `<defs><linearGradient id="lg" gradientUnits="userSpaceOnUse" x1="0" x2="${W}" y1="0" y2="0" spreadMethod="reflect"><stop offset="0" stop-color="#b40028"/><stop offset="1" stop-color="#7800f0"/></linearGradient></defs><g>`;
907
+ syms.forEach((s, li) => {
908
+ const p = H.prices[s], n = p.length, lo = Math.min(...p), hi = Math.max(...p), depth = li / (syms.length - 1);
909
+ const base = Hh * (.16 + depth * .74), amp = 60 + depth * 60;
910
+ const y = v => base - (v - lo) / (hi - lo) * amp;
911
+ const loopv = [...p, ...p.slice(0, -1).reverse()], m = loopv.length; // forward then mirrored, ends where it starts
912
+ const pts = [0, 1, 2].flatMap(k => loopv.map((v, i) => [k * 2 * W + i / (m - 1) * 2 * W, y(v)])); // three periods, enough for the drift
913
+ // ticker name, fixed at the right edge at the middle of this line's band (does not drift)
914
+ labels.push(`<text x="${W - 60}" y="${(base - amp / 2 + 3).toFixed(1)}" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="9" fill="#7a1fb4" fill-opacity=".14">${s}</text>`);
915
+ const bright = li % 3 === 1;
916
+ const segs = segsOf(pts); ridges.push(segs);
917
+ out += `<path d="${smooth(segs)}" fill="none" stroke="url(#lg)" stroke-width="${bright ? 1.3 : .9}" opacity="${bright ? .34 : .12 + depth * .1}"/>`;
918
+ for (let k = 0; k < 3; k++) {
919
+ if (bright) [.18, .55, .83].forEach(f => { const i = Math.round(f * (n - 1)), xi = k * 2 * W + i / (m - 1) * 2 * W;
920
+ out += `<circle cx="${xi}" cy="${y(p[i])}" r="2.2" fill="#7800f0" opacity=".5"/><circle class="node" cx="${xi}" cy="${y(p[i])}" r="2.4" fill="none" stroke="#7800f0" stroke-width=".8" style="animation-delay:${(f * 3).toFixed(2)}s"/>`; });
921
+ }
922
+ });
923
+ out += '</g>' + labels.join('');
924
+ svg.innerHTML = out;
925
+ flowState = { W, Hh, ridge: ridgeOf(ridges[ridges.length - 1]), ridgeTop: ridgeOf(ridges[0]), g: svg.querySelector('g'), period: 2 * W };
926
+ }
927
+ let flowState = null, flowT0 = null;
928
+ // drift the lines left (one 2W period every 160 s) and stand the pixel hero / lion on the front ridge
929
+ function flowTick(ts) {
930
+ if (!flowState) return requestAnimationFrame(flowTick);
931
+ if (flowT0 === null) flowT0 = ts;
932
+ const { W, Hh, ridge, ridgeTop, g, period } = flowState, off = reduce ? 0 : ((ts - flowT0) / 160000 * period) % period;
933
+ g.setAttribute('transform', `translate(${-off} 0)`);
934
+ const k = $('#flow').getBoundingClientRect().width / W || 1; // svg is stretched to the hero box
935
+ const ky = $('#flow').getBoundingClientRect().height / Hh;
936
+ const yAt = (r, px) => r[Math.max(0, Math.min(r.length - 1, Math.round(px / k + off) % period))] * ky;
937
+ const stand = (sel, r) => $$(sel).forEach(el => {
938
+ const sw = el.parentNode.clientWidth, l = el.style.left, w = el.offsetWidth || 18;
939
+ const x = l.endsWith('%') ? parseFloat(l) / 100 * sw : parseFloat(l) || 0; // the return walker moves in %
940
+ el.style.top = (yAt(r, x + w / 2) - (el.offsetHeight || 24) + 2) + 'px';
941
+ });
942
+ stand('#herc-top .walker, #herc-top .lion, #herc-top .flash', ridgeTop); // the fight, on the top line (ADBE)
943
+ stand('#herc-bot .walker', ridge); // the walk back, on the bottom line (MSFT)
944
+ requestAnimationFrame(flowTick);
945
+ }
946
+ requestAnimationFrame(flowTick);
947
+ drawFlow();
948
+
949
+ /* ================= header: scrolled state, progress bar, sliding nav pill ================= */
950
+ const header = $('#header'), prog = $('#progress'), nav = $('#navLinks');
951
+ function onScroll() {
952
+ const y = scrollY, max = document.documentElement.scrollHeight - innerHeight;
953
+ header.classList.toggle('scrolled', y > 10);
954
+ prog.style.transform = `scaleX(${max > 0 ? y / max : 0})`;
955
+ let cur = null;
956
+ for (const a of $$('a', nav)) { const sec = document.getElementById(a.dataset.sec); if (sec && sec.getBoundingClientRect().top < innerHeight * .4) cur = a.dataset.sec; }
957
+ $$('a', nav).forEach(a => a.classList.toggle('active', a.dataset.sec === cur));
958
+ }
959
+ addEventListener('scroll', onScroll, { passive: true }); onScroll();
960
+ let rz; addEventListener('resize', () => { clearTimeout(rz); rz = setTimeout(() => { drawFlow(); layout(); }, 200); });
961
+
962
+ /* ================= micro-interactions (thefin.ai): spotlight cards, button glow, magnetic CTAs, tilt ================= */
963
+ let lastBtn = null;
964
+ document.addEventListener('pointermove', e => {
965
+ const b = e.target.closest && e.target.closest('.btn');
966
+ if (lastBtn && lastBtn !== b) lastBtn.style.transform = '';
967
+ lastBtn = b;
968
+ if (b) {
969
+ const r = b.getBoundingClientRect(), x = e.clientX - r.left, y = e.clientY - r.top;
970
+ b.style.setProperty('--mx', x + 'px'); b.style.setProperty('--my', y + 'px');
971
+ if (!reduce && b.classList.contains('magnetic')) b.style.transform = `translate(${(x - r.width / 2) * .18}px,${(y - r.height / 2) * .3}px)`;
972
+ }
973
+ const c = e.target.closest && e.target.closest('.spot');
974
+ if (c) { const r = c.getBoundingClientRect(); c.style.setProperty('--x', e.clientX - r.left + 'px'); c.style.setProperty('--y', e.clientY - r.top + 'px'); }
975
+ }, { passive: true });
976
+ if (!reduce && matchMedia('(hover:hover)').matches) $$('.tilt').forEach(card => {
977
+ card.addEventListener('pointermove', e => {
978
+ const r = card.getBoundingClientRect(), px = (e.clientX - r.left) / r.width, py = (e.clientY - r.top) / r.height;
979
+ card.style.transition = 'transform .1s';
980
+ card.style.transform = `perspective(1000px) rotateY(${(px - .5) * 8}deg) rotateX(${(.5 - py) * 8}deg)`;
981
+ card.style.setProperty('--gx', px * 100 + '%'); card.style.setProperty('--gy', py * 100 + '%');
982
+ });
983
+ card.addEventListener('pointerleave', () => { card.style.transition = ''; card.style.transform = ''; });
984
+ });
985
+
986
+ /* ================= reveal on scroll ================= */
987
+ const io = 'IntersectionObserver' in window ? new IntersectionObserver(es => es.forEach(e => {
988
+ if (!e.isIntersecting) return;
989
+ e.target.classList.add('in'); io.unobserve(e.target);
990
+ if (e.target.id === 'board') { revealed = true; setTimeout(layout, 120); }
991
+ }), { threshold: .08 }) : null;
992
+ $$('[data-reveal]').forEach(el => io ? io.observe(el) : el.classList.add('in'));
993
+ if (!io || reduce || /[?&]all\b/.test(location.search)) { revealed = true; pod.classList.add('in'); $$('[data-reveal]').forEach(el => el.classList.add('in')); layout(); }
994
+
995
+ requestAnimationFrame(() => { document.body.classList.add('loaded'); setTimeout(() => { $$('[data-count]').forEach(countUp); pod.classList.add('in'); }, 500); });
996
+ })();
997
+
998
+ /* ---------- game card: mini K-line of the real window (TSLA), with one system's calls ---------- */
999
+ (() => {
1000
+ const H = window.HERC, svg = document.getElementById('gameK'); if (!svg || !H.ohlc) return;
1001
+ const tk = 'TSLA', O = H.ohlc, bars = O.bars[tk].slice(O.start), acts = (H.runs.trading[`Hermes|DeepSeek-V4-Pro|${tk}`] || [''])[0] || '';
1002
+ const lo = Math.min(...bars.map(b => b[2])), hi = Math.max(...bars.map(b => b[1])), W = svg.clientWidth || 360, Hh = svg.clientHeight || 96, top = 10, ph = Hh - 20, cw = W / bars.length, w = cw * .6;
1003
+ const Y = v => top + (1 - (v - lo) / (hi - lo)) * ph;
1004
+ svg.setAttribute('viewBox', `0 0 ${W} ${Hh}`);
1005
+ svg.innerHTML = bars.map((b, i) => {
1006
+ const [o, h, l, c] = b, up = c >= o, col = up ? '#3fd68f' : '#ff5a72', x = (i + .5) * cw, d = `style="transition-delay:${(i * 18) | 0}ms"`;
1007
+ const mk = acts[i] === 'B' ? `<path d="M${x},${Y(l) + 3} l3,5 h-6z" fill="#3fd68f"/>` : acts[i] === 'S' ? `<path d="M${x},${Y(h) - 3} l3,-5 h-6z" fill="#ff5a72"/>` : '';
1008
+ return `<g class="gk" ${d}><line x1="${x}" x2="${x}" y1="${Y(h)}" y2="${Y(l)}" stroke="${col}" stroke-width=".9"/><rect x="${x - w / 2}" y="${Y(Math.max(o, c))}" width="${w}" height="${Math.max(.8, Math.abs(Y(o) - Y(c)))}" fill="${up ? 'none' : col}" stroke="${col}" stroke-width=".8"/>${mk}</g>`;
1009
+ }).join('');
1010
+ })();
1011
+
1012
+ /* ---------- deep links (play.html → the Leaderboard section): the target travels in the #hash, ?to=, window.name and
1013
+ localStorage, because some viewers drop or rewrite the URL; scroll once the JS-built sections exist, and again
1014
+ while the layout settles, unless the reader has started scrolling */
1015
+ (() => {
1016
+ let want = location.hash.slice(1) || new URLSearchParams(location.search).get('to') || '';
1017
+ const wn = /^herc-goto:(\w[\w-]*)$/.exec(window.name || ''); if (wn) { want = want || wn[1]; window.name = ''; }
1018
+ try { const g = JSON.parse(localStorage.getItem('herc-goto') || 'null'); localStorage.removeItem('herc-goto'); if (!want && g && Date.now() - g.t < 15000) want = g.id; } catch (_) {}
1019
+ if (!want || !document.getElementById(want)) return;
1020
+ let user = false; const stop = () => (user = true);
1021
+ ['wheel', 'touchstart', 'keydown', 'mousedown'].forEach(t => addEventListener(t, stop, { once: true, passive: true }));
1022
+ const go = () => {
1023
+ if (user) return;
1024
+ const el = document.getElementById(want), root = document.documentElement, sb = root.style.scrollBehavior;
1025
+ root.style.scrollBehavior = 'auto'; // html{scroll-behavior:smooth} would animate and can be cut short
1026
+ scrollTo(0, el.getBoundingClientRect().top + scrollY - (parseFloat(getComputedStyle(root).scrollPaddingTop) || 0));
1027
+ root.style.scrollBehavior = sb;
1028
+ };
1029
+ if ('scrollRestoration' in history) history.scrollRestoration = 'manual';
1030
+ go(); requestAnimationFrame(go);
1031
+ [150, 400, 900, 1600, 2600].forEach(t => setTimeout(go, t));
1032
+ addEventListener('load', go);
1033
+ addEventListener('pageshow', e => e.persisted && go());
1034
+ (document.fonts && document.fonts.ready || Promise.resolve()).then(go);
1035
+ })();
1036
+
1037
+ /* ---------- coins popping out of the $ badge on the two "Trade" buttons: slow and faint at rest, fast and bright on hover ---------- */
1038
+ (() => {
1039
+ if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
1040
+ const btns = [...document.querySelectorAll('.btn--trade')]; if (!btns.length) return;
1041
+ const layer = document.createElement('div'); layer.className = 'coin-layer'; document.body.appendChild(layer);
1042
+ const coin = () => { const c = document.createElement('i'); c.className = 'coin'; c.textContent = '$'; return c; };
1043
+ btns.forEach(b => {
1044
+ const host = b.closest('.game-card') || b, dol = b.querySelector('.dol') || b;
1045
+ let hot = false, vis = true, t = 0, raf = 0;
1046
+ host.addEventListener('pointerenter', () => (hot = true)); host.addEventListener('pointerleave', () => (hot = false));
1047
+ if ('IntersectionObserver' in window) new IntersectionObserver(es => es.forEach(e => (vis = e.isIntersecting))).observe(b);
1048
+ const spawn = () => {
1049
+ const r = dol.getBoundingClientRect(), x = r.left + r.width / 2 + scrollX, y = r.top + r.height / 2 + scrollY;
1050
+ const big = hot, c = coin(), dx = (Math.random() - .5) * (big ? 70 : 34), up = (big ? 46 : 26) + Math.random() * (big ? 30 : 14);
1051
+ const s = big ? .9 + Math.random() * .35 : .7 + Math.random() * .2, op = big ? .95 : .38, dur = big ? 900 + Math.random() * 300 : 1300 + Math.random() * 400;
1052
+ c.style.left = x + 'px'; c.style.top = y + 'px'; layer.appendChild(c);
1053
+ // flat coin: a small in-plane turn only
1054
+ const spin = (Math.random() < .5 ? -1 : 1) * (10 + Math.random() * 20);
1055
+ c.animate([
1056
+ { transform: `translate(-50%,-50%) translate(0,0) scale(${s * .4}) rotate(0deg)`, opacity: 0 },
1057
+ { transform: `translate(-50%,-50%) translate(${dx * .5}px,${-up}px) scale(${s}) rotate(${spin * .6}deg)`, opacity: op, offset: .45 },
1058
+ { transform: `translate(-50%,-50%) translate(${dx}px,${-up * .55}px) scale(${s * .85}) rotate(${spin}deg)`, opacity: 0 }
1059
+ ], { duration: dur, easing: 'cubic-bezier(.25,.7,.35,1)' }).onfinish = () => c.remove();
1060
+ };
1061
+ const tick = now => {
1062
+ const gap = hot ? 190 : 900;
1063
+ if (vis && !document.hidden && now - t > gap) { t = now; spawn(); }
1064
+ raf = requestAnimationFrame(tick);
1065
+ };
1066
+ raf = requestAnimationFrame(tick);
1067
+ });
1068
+ })();
assets/deepkin-mark.svg ADDED
assets/finai-logo.png ADDED
assets/manchester.png ADDED
assets/mbzuai.png ADDED
assets/mcgill-coa.svg ADDED
assets/nactem.png ADDED
assets/nvidia-full.svg ADDED
assets/uf.svg ADDED
assets/yale-shield.svg ADDED
assets/yale.svg ADDED
data.js ADDED
The diff for this file is too large to render. See raw diff
 
herc_scene.js ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Pixel Hercules scenes, for the hero.
2
+ Top: the hero meets the Nemean lion and dons its skin. Bottom: the lion-skin hero walks on. */
3
+ /* ---------- Hercules pixel scenes (top: encounter+transform · bottom: lion-skin hero walking) ---------- */
4
+ (function(){
5
+ const SPR={
6
+ HA:"<svg viewBox=\"0 0 18 18\" shape-rendering=\"crispEdges\" fill=\"currentColor\"><rect x=\"7\" y=\"1\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"1\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"1\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"2\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"2\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"2\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"2\" width=\"1\" height=\"1\"/><rect x=\"10\" y=\"2\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"15\" y=\"2\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"16\" y=\"2\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"3\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"3\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"3\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"3\" width=\"1\" height=\"1\"/><rect x=\"10\" y=\"3\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"14\" y=\"3\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"15\" y=\"3\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"4\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"4\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"4\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"13\" y=\"4\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"14\" y=\"4\" width=\"1\" height=\"1\"/><rect x=\"4\" y=\"5\" width=\"1\" height=\"1\"/><rect x=\"5\" y=\"5\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"5\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"5\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"5\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"5\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"12\" y=\"5\" width=\"1\" height=\"1\"/><rect x=\"4\" y=\"6\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"6\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"6\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"6\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"6\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"11\" y=\"6\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"7\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"7\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"7\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"7\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"8\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"8\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"8\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"8\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"9\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"9\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"9\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"9\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"10\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"10\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"11\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"11\" width=\"1\" height=\"1\"/><rect x=\"5\" y=\"12\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"12\" width=\"1\" height=\"1\"/><rect x=\"4\" y=\"13\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"13\" width=\"1\" height=\"1\"/><rect x=\"3\" y=\"14\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"14\" width=\"1\" height=\"1\"/><rect x=\"10\" y=\"14\" width=\"1\" height=\"1\"/><rect x=\"2\" y=\"15\" width=\"1\" height=\"1\"/><rect x=\"3\" y=\"15\" width=\"1\" height=\"1\"/></svg>",
7
+ HB:"<svg viewBox=\"0 0 18 18\" shape-rendering=\"crispEdges\" fill=\"currentColor\"><rect x=\"7\" y=\"2\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"2\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"2\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"3\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"3\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"3\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"3\" width=\"1\" height=\"1\"/><rect x=\"10\" y=\"3\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"15\" y=\"3\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"16\" y=\"3\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"4\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"4\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"4\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"4\" width=\"1\" height=\"1\"/><rect x=\"10\" y=\"4\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"14\" y=\"4\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"15\" y=\"4\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"5\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"5\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"5\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"13\" y=\"5\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"14\" y=\"5\" width=\"1\" height=\"1\"/><rect x=\"4\" y=\"6\" width=\"1\" height=\"1\"/><rect x=\"5\" y=\"6\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"6\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"6\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"6\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"6\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"12\" y=\"6\" width=\"1\" height=\"1\"/><rect x=\"4\" y=\"7\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"7\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"7\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"7\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"7\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"11\" y=\"7\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"8\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"8\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"8\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"8\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"9\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"9\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"9\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"9\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"10\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"10\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"11\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"11\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"12\" width=\"1\" height=\"1\"/><rect x=\"10\" y=\"12\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"13\" width=\"1\" height=\"1\"/><rect x=\"10\" y=\"13\" width=\"1\" height=\"1\"/><rect x=\"5\" y=\"14\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"14\" width=\"1\" height=\"1\"/><rect x=\"10\" y=\"14\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"15\" width=\"1\" height=\"1\"/><rect x=\"10\" y=\"15\" width=\"1\" height=\"1\"/></svg>",
8
+ SA:"<svg viewBox=\"0 0 18 18\" shape-rendering=\"crispEdges\" fill=\"currentColor\"><rect class=\"acc\" x=\"6\" y=\"0\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"7\" y=\"0\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"8\" y=\"0\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"5\" y=\"1\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"6\" y=\"1\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"7\" y=\"1\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"8\" y=\"1\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"9\" y=\"1\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"4\" y=\"2\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"5\" y=\"2\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"6\" y=\"2\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"7\" y=\"2\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"8\" y=\"2\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"9\" y=\"2\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"10\" y=\"2\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"4\" y=\"3\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"5\" y=\"3\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"3\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"3\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"3\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"9\" y=\"3\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"10\" y=\"3\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"14\" y=\"3\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"15\" y=\"3\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"4\" y=\"4\" width=\"1\" height=\"1\"/><rect x=\"5\" y=\"4\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"4\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"4\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"4\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"4\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"10\" y=\"4\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"13\" y=\"4\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"14\" y=\"4\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"4\" y=\"5\" width=\"1\" height=\"1\"/><rect x=\"5\" y=\"5\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"5\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"5\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"5\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"5\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"10\" y=\"5\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"12\" y=\"5\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"13\" y=\"5\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"4\" y=\"6\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"5\" y=\"6\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"6\" y=\"6\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"7\" y=\"6\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"8\" y=\"6\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"9\" y=\"6\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"10\" y=\"6\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"12\" y=\"6\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"13\" y=\"6\" width=\"1\" height=\"1\"/><rect x=\"4\" y=\"7\" width=\"1\" height=\"1\"/><rect x=\"5\" y=\"7\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"7\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"7\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"7\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"7\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"10\" y=\"7\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"11\" y=\"7\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"8\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"8\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"8\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"8\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"9\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"9\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"9\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"9\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"10\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"10\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"11\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"11\" width=\"1\" height=\"1\"/><rect x=\"5\" y=\"12\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"12\" width=\"1\" height=\"1\"/><rect x=\"4\" y=\"13\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"13\" width=\"1\" height=\"1\"/><rect x=\"3\" y=\"14\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"14\" width=\"1\" height=\"1\"/><rect x=\"10\" y=\"14\" width=\"1\" height=\"1\"/><rect x=\"2\" y=\"15\" width=\"1\" height=\"1\"/><rect x=\"3\" y=\"15\" width=\"1\" height=\"1\"/></svg>",
9
+ SB:"<svg viewBox=\"0 0 18 18\" shape-rendering=\"crispEdges\" fill=\"currentColor\"><rect class=\"acc\" x=\"6\" y=\"1\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"7\" y=\"1\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"8\" y=\"1\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"5\" y=\"2\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"6\" y=\"2\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"7\" y=\"2\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"8\" y=\"2\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"9\" y=\"2\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"4\" y=\"3\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"5\" y=\"3\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"6\" y=\"3\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"7\" y=\"3\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"8\" y=\"3\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"9\" y=\"3\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"10\" y=\"3\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"4\" y=\"4\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"5\" y=\"4\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"4\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"4\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"4\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"9\" y=\"4\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"10\" y=\"4\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"14\" y=\"4\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"15\" y=\"4\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"4\" y=\"5\" width=\"1\" height=\"1\"/><rect x=\"5\" y=\"5\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"5\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"5\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"5\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"5\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"10\" y=\"5\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"13\" y=\"5\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"14\" y=\"5\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"4\" y=\"6\" width=\"1\" height=\"1\"/><rect x=\"5\" y=\"6\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"6\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"6\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"6\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"6\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"10\" y=\"6\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"12\" y=\"6\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"13\" y=\"6\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"4\" y=\"7\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"5\" y=\"7\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"6\" y=\"7\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"7\" y=\"7\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"8\" y=\"7\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"9\" y=\"7\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"10\" y=\"7\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"12\" y=\"7\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"13\" y=\"7\" width=\"1\" height=\"1\"/><rect x=\"4\" y=\"8\" width=\"1\" height=\"1\"/><rect x=\"5\" y=\"8\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"8\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"8\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"8\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"8\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"10\" y=\"8\" width=\"1\" height=\"1\"/><rect class=\"acc\" x=\"11\" y=\"8\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"9\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"9\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"9\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"9\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"10\" width=\"1\" height=\"1\"/><rect x=\"8\" y=\"10\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"11\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"11\" width=\"1\" height=\"1\"/><rect x=\"7\" y=\"12\" width=\"1\" height=\"1\"/><rect x=\"10\" y=\"12\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"13\" width=\"1\" height=\"1\"/><rect x=\"10\" y=\"13\" width=\"1\" height=\"1\"/><rect x=\"5\" y=\"14\" width=\"1\" height=\"1\"/><rect x=\"6\" y=\"14\" width=\"1\" height=\"1\"/><rect x=\"10\" y=\"14\" width=\"1\" height=\"1\"/><rect x=\"9\" y=\"15\" width=\"1\" height=\"1\"/><rect x=\"10\" y=\"15\" width=\"1\" height=\"1\"/></svg>"
10
+ };
11
+ const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
12
+
13
+ // ---- TOP: full story ----
14
+ (function(){
15
+ const scene=document.getElementById('herc-top'); if(!scene) return;
16
+ scene.innerHTML =
17
+ '<span class="lion"><span class="li">\uD83E\uDD81</span></span>'+
18
+ '<span class="flash"></span>'+
19
+ '<span class="walker">'+
20
+ '<span class="hk plain"><span class="fr a">'+SPR.HA+'</span><span class="fr b">'+SPR.HB+'</span></span>'+
21
+ '<span class="hk skin"><span class="fr a">'+SPR.SA+'</span><span class="fr b">'+SPR.SB+'</span></span>'+
22
+ '</span>';
23
+ const walker=scene.querySelector('.walker'), lion=scene.querySelector('.lion'),
24
+ plain=scene.querySelector('.hk.plain'), skin=scene.querySelector('.hk.skin'),
25
+ flash=scene.querySelector('.flash'),
26
+ fA=[...scene.querySelectorAll('.hk .fr.a')], fB=[...scene.querySelectorAll('.hk .fr.b')];
27
+ // The ground scrolls left at WORLD px/s; the lion is "pinned" to that ground, so it drifts in
28
+ // from the far right at the EXACT same speed (no foot-slip, any window width). The hero walks in
29
+ // at his own pace, waits, they collide & bounce, the lion vanishes in a flash, the hero dons the skin.
30
+ const WORLD=60; // px/s — shared by ground + lion (slow, steady)
31
+ const HERO_PXS=150; // hero walk speed (px/s), walking right the whole time
32
+ const HERO_START=-22; // hero off-left (px)
33
+ const BOUNCE=1.5, MERGE_GAP=0.3, WALKOFF=3.6; // phase durations after contact (s)
34
+ if(reduce){ walker.style.left='50%'; lion.style.opacity=0; skin.style.display='block'; plain.style.display='none'; return; }
35
+
36
+ let t0=null,phase=0,lastLeg=0;
37
+ function frame(ts){
38
+ if(t0===null)t0=ts;
39
+ const W=scene.clientWidth||1000;
40
+ const elapsed=(ts-t0)/1000;
41
+ scene.style.setProperty('--gx', (-((WORLD*elapsed)%14))+'px');
42
+ // hero walks right at HERO_PXS, lion drifts left at WORLD — they close the gap until they touch.
43
+ // Solve contact time: HERO_START + HERO_PXS*t + 16 == (W+22) - WORLD*t (hero right side meets lion left)
44
+ const startPx=W+22;
45
+ const tContact=Math.max(0,(startPx-16-HERO_START)/(HERO_PXS+WORLD));
46
+ const contactHeroPx=HERO_START+HERO_PXS*tContact; // hero x at contact
47
+ const contactLionPx=contactHeroPx+16; // lion sits just past hero's right
48
+ const tBounceEnd=tContact+BOUNCE, tFlash=tContact+BOUNCE-0.05, tMerge=tBounceEnd+MERGE_GAP, tEnd=tMerge+Math.min(WALKOFF,Math.max(.2,(W+4-contactHeroPx)/(HERO_PXS*1.6))); // phase ends as he leaves the screen
49
+ // one shared cycle: encounter + walk-off on the front line, then the walk back on the top line (RETURN s)
50
+ const RETURN=13, cycle=tEnd+RETURN, tc=elapsed%cycle;
51
+ window.__hercReturn = tc>=tEnd ? (tc-tEnd)/RETURN : null; // 0..1 while the lion-skin hero walks back
52
+ if(tc>=tEnd){ walker.style.left=(W+60)+'px'; lion.style.opacity=0; flash.style.opacity=0; requestAnimationFrame(frame); return; }
53
+ const t=tc;
54
+
55
+ // ---- lion (pinned to ground: drifts left at WORLD px/s until contact) ----
56
+ let lionPx;
57
+ if(t<tContact){ lionPx=startPx-WORLD*t; }
58
+ else if(t<tBounceEnd){ const k=(t-tContact)/BOUNCE; lionPx=contactLionPx + 9*Math.sin(k*Math.PI*3)*(1-k); }
59
+ else lionPx=contactLionPx;
60
+ lion.style.left=lionPx+'px';
61
+
62
+ // ---- hero: walks right continuously until contact, bounces, then strides off ----
63
+ let heroPx;
64
+ if(t<tContact) heroPx=HERO_START+HERO_PXS*t;
65
+ else if(t<tBounceEnd){ const k=(t-tContact)/BOUNCE; heroPx=contactHeroPx - 22*Math.sin(k*Math.PI*3)*(1-k); }
66
+ else if(t<tMerge) heroPx=contactHeroPx;
67
+ else heroPx=contactHeroPx + HERO_PXS*1.6*(t-tMerge);
68
+ walker.style.left=heroPx+'px';
69
+
70
+ // legs march except during the locked collision window
71
+ const walking=t<tContact||t>=tMerge;
72
+ if(walking && ts-lastLeg>240){phase^=1;lastLeg=ts;
73
+ fA.forEach(e=>e.style.opacity=phase?0:1); fB.forEach(e=>e.style.opacity=phase?1:0);}
74
+
75
+ // transform: lion fades out over the merge gap, hero swaps to the lion-skin sprite
76
+ const merging=t>=tMerge;
77
+ skin.style.display=merging?'block':'none'; plain.style.display=merging?'none':'block';
78
+ if(t>=tBounceEnd&&t<tMerge){const u=(t-tBounceEnd)/MERGE_GAP; lion.style.opacity=1-u; lion.style.transform='scale('+(1-0.6*u)+')';}
79
+ else if(t>=tMerge){lion.style.opacity=0;}
80
+ else{lion.style.opacity=1; lion.style.transform='scale(1)';}
81
+ // flash at the moment of transformation
82
+ if(t>=tFlash-0.15&&t<tMerge+0.25){const d=Math.abs(t-(tFlash+0.05));
83
+ flash.style.opacity=Math.max(0,1-d/0.22); flash.style.left=(contactLionPx)+'px';}
84
+ else flash.style.opacity=0;
85
+ requestAnimationFrame(frame);
86
+ }
87
+ requestAnimationFrame(frame);
88
+ })();
89
+
90
+ // ---- BOTTOM: lone lion-skin hero walking right -> left ----
91
+ (function(){
92
+ const scene=document.getElementById('herc-bot'); if(!scene) return;
93
+ scene.innerHTML='<span class="walker"><span class="hk skin"><span class="fr a">'+SPR.SA+'</span><span class="fr b">'+SPR.SB+'</span></span></span>';
94
+ const walker=scene.querySelector('.walker');
95
+ const fA=scene.querySelector('.fr.a'), fB=scene.querySelector('.fr.b');
96
+ if(reduce){ walker.style.left='50%'; return; }
97
+ let t0=null,phase=0,lastLeg=0;
98
+ function frame(ts){
99
+ if(t0===null)t0=ts;
100
+ const p=window.__hercReturn; // driven by the top scene: only walks after the encounter has left the screen
101
+ walker.style.visibility = p==null ? 'hidden' : 'visible';
102
+ walker.style.left=(p==null ? 101 : 101-(101-(-3))*p)+'%'; // enters right away at the right edge (101% -> -3%)
103
+ if(ts-lastLeg>240){phase^=1;lastLeg=ts;
104
+ fA.style.opacity=phase?0:1; fB.style.opacity=phase?1:0;}
105
+ requestAnimationFrame(frame);
106
+ }
107
+ requestAnimationFrame(frame);
108
+ })();
109
+ })();
index.html CHANGED
@@ -1,19 +1,237 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
  <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>HERCULEAN · An Agentic Benchmark for Financial Intelligence · The Fin AI</title>
7
+ <meta name="description" content="HERCULEAN: end-to-end, workflow-level evaluation of financial agents. Leaderboard of agent systems across Trading, Hedging, Market Insights and Auditing." />
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=Cinzel:wght@600;700&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
+ <!-- page frame: soft colour glows behind everything, and vertical marks on both edges -->
17
+ <div class="glow" aria-hidden="true"></div>
18
+ <div class="edge edge-l" aria-hidden="true"><span class="e-name">HERCULEAN</span></div>
19
+ <div class="edge edge-r" aria-hidden="true"><span class="e-name">The Fin AI</span></div>
20
+ <div class="tip" id="tip" role="tooltip"></div>
21
+
22
+ <!-- ============ HEADER ============ -->
23
+ <header class="header" id="header">
24
+ <div class="wrap nav">
25
+ <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-herc">HERCULEAN</span></a>
26
+ <div class="nav-right">
27
+ <nav class="links" id="navLinks">
28
+ <a href="#rankings" data-sec="rankings">Rankings</a>
29
+ <a href="#leaderboard" data-sec="leaderboard">Leaderboard</a>
30
+ <a href="#replay" data-sec="replay">Replay</a>
31
+ <a href="#workflows" data-sec="workflows">Benchmark</a>
32
+ <a href="#resources" data-sec="resources">Cite</a>
33
+ </nav>
34
+ <a class="btn btn--sm" href="https://arxiv.org/abs/2605.14355" target="_blank" rel="noreferrer">Paper <span class="arrow">↗</span></a>
35
+ </div>
36
+ </div>
37
+ </header>
38
+
39
+ <main id="top">
40
+ <!-- ============ HERO ============ -->
41
+ <section class="hero" id="hero">
42
+ <!-- background: one thin gradient line per stock (real offline prices), drifting slowly left -->
43
+ <svg class="flow" id="flow" aria-hidden="true" preserveAspectRatio="none"></svg>
44
+ <div class="wrap hero-grid">
45
+ <div class="hero-copy">
46
+ <a class="award reveal-up" id="award" href="#rankings" style="transition-delay:.05s">
47
+ <span class="medal" id="lion" aria-hidden="true"></span>Agentic benchmark · <b id="awardN">5 architectures × 6 backbones</b> →
48
+ </a>
49
+ <h1 class="wordmark reveal-up" style="transition-delay:.15s">HERCULEAN</h1>
50
+ <p class="hero-sub reveal-up" style="transition-delay:.3s"><em>End-to-end</em>, <em>workflow-level</em> evaluation of financial&nbsp;agents.</p>
51
+ <p class="lead reveal-up" style="transition-delay:.4s">Every agent system, <span class="rotator" id="rotator"></span>, trades, hedges, writes market reports and audits filings under the same date-bounded environment, and is scored on its whole trajectory.</p>
52
+ <div class="stats reveal-up" id="stats" style="transition-delay:.5s"></div>
53
+ <div class="hero-ctas reveal-up" style="transition-delay:.6s">
54
+ <a class="btn btn--lg btn--trade magnetic" href="./play.html">Trade with an agent <svg class="dol" viewBox="0 0 18 18" aria-hidden="true"><circle cx="9" cy="9" r="8.5" fill="currentColor"/><text x="9" y="13.1" text-anchor="middle" font-size="11.5" font-weight="700" font-family="Inter,system-ui,sans-serif">$</text></svg></a>
55
+ <a class="btn btn--lg btn--ghost magnetic" href="https://arxiv.org/abs/2605.14355" target="_blank" rel="noreferrer">Read the paper</a>
56
+ </div>
57
+ </div>
58
+
59
+ <figure class="hero-viz tilt reveal-up" style="transition-delay:.55s">
60
+ <figcaption><span id="vizCap">Top 8 · Overall score</span><a href="#leaderboard">Full ranking →</a></figcaption>
61
+ <div class="podium" id="podium"></div>
62
+ <div class="hero-legend" id="heroLegend"></div>
63
+ <div class="hero-leaders" id="heroLeaders"></div>
64
+ <p class="hero-claim">No agent system is a <em>generalist</em>: every workflow has its own leader.</p>
65
+ </figure>
66
+ </div>
67
+ <!-- pixel Hercules: meets the Nemean lion and dons its skin, walking on the ticker's top line -->
68
+ <div class="scene press js-ground" id="herc-top" aria-hidden="true"></div>
69
+ <!-- after the fight, the lion-skin hero walks back right-to-left along the bottom price line -->
70
+ <div class="scene rev solo" id="herc-bot" aria-hidden="true"></div>
71
+ <!-- ticker: all agent systems by Overall, looping (pause on hover, click to open) -->
72
+ <div class="ticker" id="ticker" aria-label="Overall ranking ticker"><div class="ticker-track" id="tickerTrack"></div></div>
73
+ </section>
74
+
75
+ <!-- ============ 2b · RANKINGS ============ -->
76
+ <section class="section" id="rankings" style="padding-top:36px">
77
+ <div class="wrap">
78
+ <div class="rk-head" data-reveal>
79
+ <div class="rk-copy">
80
+ <span class="kicker">Rankings</span>
81
+ <h2 class="sec-h2">Rankings <em>reshuffle</em> across workflows</h2>
82
+ </div>
83
+ <div class="picker" id="picker" aria-label="Choose an agent system, an architecture or a backbone"></div>
84
+ </div>
85
+ <div class="rk-chart" id="rkChart" data-reveal>
86
+ <div class="rk-view" id="rkView" role="tablist" aria-label="Chart view"><span class="seg-pill"></span>
87
+ <button role="tab" aria-selected="true" data-v="flow"><svg viewBox="0 0 22 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M1 13 C6 13 7 3 11 3 S16 12 21 6"/><path d="M1 4 C6 4 8 12 12 12 S17 4 21 10" opacity=".55"/></svg><b>Rank flow</b><i class="go">›</i></button>
88
+ <button role="tab" aria-selected="false" data-v="field"><svg viewBox="0 0 22 16" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"><path d="M1 3h20M1 8h20M1 13h20" opacity=".55"/><circle cx="6" cy="3" r="2" fill="currentColor"/><circle cx="14" cy="8" r="2" fill="currentColor"/><circle cx="9" cy="13" r="2" fill="currentColor"/></svg><b>Field</b><i class="go">›</i></button>
89
+ <button role="tab" aria-selected="false" data-v="floor"><svg viewBox="0 0 22 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M4 4h12M2 8h16M6 12h10"/><circle cx="16" cy="4" r="1.8" fill="currentColor"/><circle cx="18" cy="8" r="1.8" fill="currentColor"/><circle cx="16" cy="12" r="1.8" fill="currentColor"/></svg><b>Floor → Overall</b><i class="go">›</i></button>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ </section>
94
+
95
+ <!-- ============ 2 · LEADERBOARD ============ -->
96
+ <section class="section section--tint" id="leaderboard" style="padding-top:20px">
97
+ <div class="wrap">
98
+ <div class="sec-head lb-head" data-reveal>
99
+ <div>
100
+ <span class="kicker" id="lbKicker">Leaderboard</span>
101
+ <h2 class="sec-h2">Who <em>leads</em>, workflow by workflow</h2>
102
+ <p class="note" id="lbSub"></p>
103
+ </div>
104
+ </div>
105
+ <div class="controls" data-reveal>
106
+ <div class="arch-chips" id="archChips" aria-label="Show or hide an architecture"></div>
107
+ <div class="arch-chips model-chips" id="modelChips" aria-label="Show or hide a backbone"></div>
108
+ </div>
109
+
110
+ <div class="board" id="board" data-reveal>
111
+ <div class="lb-cols">
112
+ <span class="c-pos">#</span><span>Agent system</span><button type="button" class="sort-h on" id="colScore" data-v="Overall" title="Rank by Overall">Overall <i class="arr">↓</i></button>
113
+ <span class="c-cells" id="colCells"></span><span></span>
114
+ </div>
115
+ <div class="lb-rows" id="lbRows"></div>
116
+ <button class="lb-more" id="lbMore" type="button" aria-expanded="false"></button>
117
+ </div>
118
+ <div class="board-foot" data-reveal>
119
+ <p id="lbNote"></p>
120
+ <p class="keys"><span class="ring-key"></span>best score in each workflow. Darker purple = higher score. <span class="ramp"></span></p>
121
+ </div>
122
+ </div>
123
+ </section>
124
+
125
+ <!-- ============ 2c · REPLAY ============ -->
126
+ <section class="section" id="replay" style="padding-top:20px">
127
+ <div class="wrap">
128
+ <div class="sec-head rp-head" data-reveal>
129
+ <div><span class="kicker">Replay</span><h2 class="sec-h2">Watch an agent <em>trade</em> and <em>hedge</em></h2>
130
+ <p class="note" id="rpSub"></p></div>
131
+ </div>
132
+ <div class="controls rp-pick" data-reveal>
133
+ <div class="arch-chips" id="rpArch" aria-label="Architecture"></div>
134
+ <div class="arch-chips model-chips" id="rpModel" aria-label="Backbone"></div>
135
+ <div class="rp-ctl">
136
+ <div class="rp-tk rp-run" id="rpRun" role="tablist" aria-label="Run"></div>
137
+ <button class="btn btn--ghost btn--sm rp-again" id="rpAgain" type="button"><svg class="play-ico" viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="7.5" fill="currentColor"/><path d="M6.4 4.9v6.2l4.9-3.1z" fill="#fff"/></svg>Replay</button>
138
+ </div>
139
+ </div>
140
+ <div class="rp-grid" data-reveal>
141
+ <article class="rp-card" style="--c:#d6304a">
142
+ <div class="rp-top"><span class="rp-kick"><span class="wdot" style="--c:#d6304a"></span>Trading</span><div class="rp-tk" id="rpTk" role="tablist" aria-label="Ticker"></div></div>
143
+ <div class="rp-chart" id="rpTrade"></div>
144
+ <div class="rp-legend"><span><i class="a-B"></i>BUY</span><span><i class="a-S"></i>SELL</span><span><i class="a-H"></i>HOLD</span><span><i class="a-x"></i>no record (scored as HOLD)</span></div>
145
+ <div class="rp-foot" id="rpTradeFoot"></div>
146
+ </article>
147
+ <article class="rp-card" style="--c:#8a3ce0">
148
+ <div class="rp-top"><span class="rp-kick"><span class="wdot" style="--c:#8a3ce0"></span>Hedging</span><span class="rp-pair" id="rpPair"></span></div>
149
+ <div class="rp-chart" id="rpHedge"></div>
150
+ <div class="rp-legend"><span><i class="p-L"></i>Long-Short</span><span><i class="p-R"></i>Short-Long</span><span><i class="p-F"></i>Flat</span><span><i class="p-dia"></i>Close</span></div>
151
+ <div class="rp-foot" id="rpHedgeFoot"></div>
152
+ </article>
153
+ </div>
154
+ </div>
155
+ </section>
156
+
157
+ <!-- ============ 1 · WORKFLOWS ============ -->
158
+ <section class="section" id="workflows" style="padding-top:20px">
159
+ <div class="wrap">
160
+ <div class="sec-head" data-reveal>
161
+ <div><span class="kicker">Benchmark</span><h2 class="sec-h2 one-line"><em>Four</em> workflows, <em>one</em> benchmark</h2>
162
+ <p class="note recipe-note">Every workflow = <b>Environment</b> + <b>Interaction Protocol</b> + <b>Trajectory Evaluation</b>.</p></div>
163
+ </div>
164
+ <div class="wf-grid" id="wfGrid"></div>
165
+ </div>
166
+ </section>
167
+
168
+ <!-- ============ GAME CARD (links to play.html) ============ -->
169
+ <section class="cta-sec" id="play">
170
+ <div class="wrap">
171
+ <a class="cta-card game-card" href="./play.html" data-reveal>
172
+ <div class="mesh" aria-hidden="true"><i></i><i></i></div>
173
+ <div class="cta-text">
174
+ <span class="game-kick">Mini-game · Trading workflow</span>
175
+ <h3>Trade with an <em>agent</em></h3>
176
+ <p>Pick a stock, then choose which agent system trades it each day. Its real recorded decisions play out over the 61-day window. Can your picks beat Buy &amp; Hold?</p>
177
+ </div>
178
+ <svg class="game-k" id="gameK" aria-hidden="true"></svg>
179
+ <span class="btn btn--light btn--trade game-go">Trade <svg class="dol" viewBox="0 0 18 18" aria-hidden="true"><circle cx="9" cy="9" r="8.5" fill="currentColor"/><text x="9" y="13.1" text-anchor="middle" font-size="11.5" font-weight="700" font-family="Inter,system-ui,sans-serif">$</text></svg></span>
180
+ </a>
181
+ </div>
182
+ </section>
183
+
184
+ <!-- ============ 3 · CITE · RESOURCES · PARTNERS ============ -->
185
+ <section class="section" id="resources" style="padding-top:84px;padding-bottom:12px">
186
+ <div class="wrap">
187
+ <div class="sec-head" data-reveal>
188
+ <div><span class="kicker">Cite</span><h2 class="sec-h2">Cite <em class="herc">HERCULEAN</em><span class="medal cite-lion" id="citeLion" aria-hidden="true"></span></h2></div>
189
+ </div>
190
+ <div class="bib spot" data-reveal>
191
+ <div class="bib-top"><span class="res-h">Citation</span>
192
+ <div class="bib-actions"><a class="paper-link" href="https://arxiv.org/abs/2605.14355" target="_blank" rel="noreferrer">Paper · arXiv 2605.14355 <span>↗</span></a><button class="copy-btn" id="copyBib" type="button">Copy BibTeX</button></div></div>
193
+ <pre id="bib">@misc{peng2026herculeanagenticbenchmarkfinancial,
194
+ title={Herculean: An Agentic Benchmark for Financial Intelligence},
195
+ author={Xueqing Peng and Zhuohan Xie and Yupeng Cao and Haohang Li and Lingfei Qian and Yan Wang and Vincent Jim Zhang and Huan He and Xuguang Ai and Linhai Ma and Ruoyu Xiang and Yueru He and Yi Han and Shuyao Wang and Yuqing Guo and Mingyang Jiang and Yilun Zhao and Youzhong Dong and Xiaoyu Wang and Yankai Chen and Ye Yuan and Qiyuan Zhang and Fuyuan Lyu and Haolun Wu and Yonghan Yang and Zichen Zhao and Yuyang Dai and Fan Zhang and Rania Elbadry and Ayesha Gull and Muhammad Usman Safder and Nuo Chen and Fengbin Zhu and Tianshi Cai and Zimu Wang and Polydoros Giannouris and Yuechen Jiang and Zhiwei Liu and Mohsinul Kabir and Yuyan Wang and Yixiang Zheng and Yangyang Yu and Weijin Liu and Wenbo Cao and Anke Xu and Peng Lu and Jerry Huang and Mingquan Lin and Prayag Tiwari and Yijia Zhao and Víctor Gutiérrez-Basulto and Xiao-Yang Liu and Kaleb E Smith and Jiahuan Pei and Arman Cohan and Jimin Huang and Yuehua Tang and Alejandro Lopez-Lira and Xi Chen and Xue Liu and Junichi Tsujii and Jian-Yun Nie and Sophia Ananiadou},
196
+ year={2026},
197
+ eprint={2605.14355},
198
+ archivePrefix={arXiv},
199
+ primaryClass={cs.AI},
200
+ url={https://arxiv.org/abs/2605.14355},
201
+ }</pre>
202
+ </div>
203
+
204
+ <!-- partners: logos in order, then the remaining institutions as names (no panel) -->
205
+ <div class="partners" data-reveal>
206
+ <div class="p-logos">
207
+ <a href="https://thefin.ai/" target="_blank" rel="noreferrer" class="p-finai" data-tip="The Fin AI"><img src="./assets/finai-logo.png" alt="" /><span>The Fin AI</span></a>
208
+ <a href="https://deepkin.ai/" target="_blank" rel="noreferrer" class="p-pair p-dk" data-tip="DeepKin"><img src="./assets/deepkin-mark.svg" alt="" class="crest" /><span class="p-dkw">DeepKin</span></a>
209
+ <a href="https://www.nactem.ac.uk/" target="_blank" rel="noreferrer" data-tip="National Centre for Text Mining (NaCTeM)"><img src="./assets/nactem.png" alt="NaCTeM" /></a>
210
+ <a href="https://www.ufl.edu/" target="_blank" rel="noreferrer" data-tip="University of Florida (UF)"><img src="./assets/uf.svg" alt="University of Florida" /></a>
211
+ </div>
212
+ <div class="p-logos">
213
+ <a href="https://www.manchester.ac.uk/" target="_blank" rel="noreferrer" data-tip="The University of Manchester (UoM)"><img src="./assets/manchester.png" alt="The University of Manchester" /></a>
214
+ <a href="https://www.yale.edu/" target="_blank" rel="noreferrer" class="p-pair" data-tip="Yale University"><img src="./assets/yale-shield.svg" alt="" class="crest" /><img src="./assets/yale.svg" alt="Yale University" /></a>
215
+ <a href="https://www.mcgill.ca/" target="_blank" rel="noreferrer" class="p-pair" data-tip="McGill University"><img src="./assets/mcgill-coa.svg" alt="" class="crest" /><span class="p-word">McGill</span></a>
216
+ <a href="https://mbzuai.ac.ae/" target="_blank" rel="noreferrer" data-tip="Mohamed bin Zayed University of Artificial Intelligence (MBZUAI)"><img src="./assets/mbzuai.png" alt="Mohamed bin Zayed University of Artificial Intelligence" class="p-mbz" /></a>
217
+ <a href="https://www.nvidia.com/" target="_blank" rel="noreferrer" data-tip="NVIDIA"><img src="./assets/nvidia-full.svg" alt="NVIDIA" /></a>
218
+ </div>
219
+ <p class="p-names" id="pNames"></p>
220
+ </div>
221
+ </div>
222
+ </section>
223
+ </main>
224
+
225
+ <footer class="footer">
226
+ <div class="wrap footer-bottom">
227
+ <a class="brand sm" href="https://thefin.ai/" target="_blank" rel="noreferrer"><img src="./assets/finai-logo.png" alt="" />The Fin AI</a>
228
+ <span class="copy">© 2026 The Fin AI · HERCULEAN benchmark</span>
229
+ <a href="#top" class="to-top" aria-label="Back to top">↑</a>
230
+ </div>
231
+ </footer>
232
+
233
+ <script src="./data.js"></script>
234
+ <script src="./app.js"></script>
235
+ <script src="./herc_scene.js"></script>
236
+ </body>
237
  </html>
play.css ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root{--ink:#0f1528;--soft:#4f5a6e;--muted:#8a93a3;--line:rgba(74,38,130,.085);--paper:#fcfcfd;--paper-2:#f4f6fa;
2
+ --g:linear-gradient(90deg,#b40028,#7800f0);--up:#0f9d58;--dn:#d6304a;
3
+ --serif:'Newsreader',Georgia,serif;--sans:'Inter',system-ui,sans-serif;--mono:'JetBrains Mono',ui-monospace,monospace;
4
+ --ring:0 0 0 1px rgba(74,38,130,.07),0 1px 2px rgba(40,20,80,.04),0 10px 28px -18px rgba(60,30,120,.18)}
5
+ *{box-sizing:border-box}html,body{margin:0;height:100%}
6
+ body{font-family:var(--sans);color:var(--ink);background:var(--paper);-webkit-font-smoothing:antialiased;
7
+ background-image:radial-gradient(58vw 70vh at 0% 20%,rgba(180,0,40,.05),transparent 72%),radial-gradient(62vw 74vh at 100% 85%,rgba(120,0,240,.06),transparent 72%)}
8
+ button{font:inherit;cursor:pointer}
9
+ .up{color:var(--up)}.dn{color:var(--dn)}.muted{color:var(--muted)}
10
+ .app{height:100vh;min-height:680px;display:grid;grid-template-rows:60px minmax(0,1fr) auto;grid-template-columns:minmax(0,1fr);row-gap:14px;padding:0 28px 18px}
11
+
12
+ /* top bar */
13
+ .top{display:flex;align-items:center;gap:16px;border-bottom:1px solid var(--line)}
14
+ .back{font:600 .8rem var(--sans);color:#fff;text-decoration:none;padding:8px 15px;border-radius:999px;background:var(--ink);box-shadow:0 6px 16px -8px rgba(15,21,40,.6);transition:transform .2s,box-shadow .2s}
15
+ .back:hover{transform:translateX(-2px);box-shadow:0 8px 20px -8px rgba(15,21,40,.7)}
16
+ .brand{display:flex;align-items:center;gap:10px;font-family:var(--serif);font-size:1.15rem;font-weight:500;color:var(--ink);text-decoration:none}
17
+ .brand img{height:22px}.brand .sep{color:rgba(74,38,130,.25)}
18
+ .brand b{font:700 .8rem 'Cinzel','Newsreader',Georgia,serif;letter-spacing:.1em;background:var(--g);-webkit-background-clip:text;background-clip:text;color:transparent}
19
+ .title{font-family:var(--serif);font-size:1.15rem;margin-left:6px}
20
+ .title em{font-style:italic;background:var(--g);-webkit-background-clip:text;background-clip:text;color:transparent;padding-right:.06em}
21
+ .who{display:flex;align-items:center;gap:12px;margin-left:auto}
22
+ .sym-pill{font:600 .78rem var(--mono);padding:5px 11px;border-radius:999px;background:var(--paper-2)}
23
+ .sym-pill small{font:400 .72rem var(--sans);color:var(--soft);margin-left:6px}
24
+ .day{font:500 .8rem var(--mono);color:var(--soft)}.day b{color:var(--ink)}
25
+ .ghost{border:0;background:none;font-size:.78rem;color:var(--soft);padding:5px 10px;border-radius:999px;box-shadow:inset 0 0 0 1px var(--line)}
26
+ .ghost:hover{color:var(--ink)}
27
+ .newg{border:0;font:600 .78rem var(--sans);color:#6a2bc4;padding:7px 13px;border-radius:999px;background:rgba(120,0,240,.07);box-shadow:inset 0 0 0 1px rgba(120,0,240,.22);transition:background .2s,color .2s}
28
+ .newg:hover{background:rgba(120,0,240,.13);color:#5310b0}
29
+ .avatar{width:34px;height:34px;border-radius:50%;display:grid;place-items:center;background:var(--g);color:#fff;font:500 1rem var(--serif)}
30
+ .nm{display:flex;flex-direction:column;line-height:1.15}
31
+ .nm small{font-size:.62rem;color:var(--muted);letter-spacing:.1em;text-transform:uppercase}.nm b{font:500 1.05rem var(--serif)}
32
+
33
+ /* layout */
34
+ .main{display:grid;grid-template-columns:300px minmax(0,1fr) 340px;gap:18px;padding-top:2px;min-height:0}
35
+ .chartcard{background:#fff;border-radius:18px;box-shadow:var(--ring);padding:14px 16px 8px;display:flex;flex-direction:column;min-height:0}
36
+ .ch-head{display:flex;align-items:baseline;gap:12px;flex-wrap:nowrap;min-height:26px;white-space:nowrap;overflow:hidden}
37
+ .ch-head .sym{font:600 1.15rem var(--sans)}.ch-head .px{font:600 1.15rem var(--mono)}
38
+ .ch-head .chg{font:500 .85rem var(--mono)}.ch-head .ohlc{font:400 .72rem var(--mono);color:var(--muted)}.ch-head .ohlc b{color:var(--soft);font-weight:500}
39
+ .ch-head .leg{margin-left:auto;display:flex;gap:12px;font-size:.7rem;color:var(--soft)}
40
+ .ch-head .leg i{display:inline-block;width:14px;height:2px;margin-right:5px;vertical-align:3px}
41
+ .ch-head .leg i.dash{height:0;border-top:1.5px dashed var(--muted)}
42
+ #kline{display:block;width:100%;flex:1;min-height:0;cursor:crosshair}
43
+ .ax{font:400 9.5px var(--mono);fill:var(--muted)}.gl{stroke:rgba(74,38,130,.07)}
44
+ .cnew{transform-box:fill-box;transform-origin:center;animation:grow .45s cubic-bezier(.2,.8,.2,1)}
45
+ @keyframes grow{from{transform:scaleY(0);opacity:0}}
46
+ .mk{animation:pop .4s cubic-bezier(.3,1.6,.5,1)}@keyframes pop{from{opacity:0;transform:translateY(-6px)}}
47
+
48
+ .side{display:flex;flex-direction:column;gap:12px;min-height:0}
49
+ .card{background:#fff;border-radius:16px;box-shadow:var(--ring);padding:14px 16px}
50
+ .kick{font-size:.62rem;font-weight:600;letter-spacing:.16em;text-transform:uppercase;color:var(--muted);display:flex;justify-content:space-between;gap:10px}
51
+ .eq{display:flex;align-items:baseline;gap:10px;margin-top:6px}
52
+ .eq b{font:500 2rem var(--serif);letter-spacing:-.02em}.eq span{font:600 .9rem var(--mono)}
53
+ .eq .vs{color:var(--muted);font-weight:400}
54
+ .kv{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:10px}
55
+ .kv div{background:var(--paper-2);border-radius:10px;padding:7px 9px}
56
+ .kv small{display:block;font-size:.6rem;color:var(--muted);letter-spacing:.06em;text-transform:uppercase}.kv b{font:600 .9rem var(--mono)}
57
+
58
+ .stand{flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column}
59
+ .stand.wide{display:grid;grid-template-columns:minmax(0,1.55fr) minmax(0,1fr);gap:28px;height:206px;min-width:0}
60
+ .st-left{display:flex;flex-direction:column;min-height:0;min-width:0}
61
+ .st-right{min-width:0;border-left:1px solid var(--line);padding-left:24px}
62
+ .st-gap{font:600 .8rem var(--mono)}
63
+ .stand.wide .st-rank{font-size:1.5rem}
64
+ .st-plot{flex:1;min-height:48px;margin-top:4px;overflow:hidden}
65
+ .st-plot svg{position:relative}
66
+ .stand .tb{margin-top:4px;font-size:.74rem}.stand .tr{grid-template-columns:34px minmax(0,1fr) 64px 70px;padding:2px 6px}
67
+ .stand .nm2{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
68
+ .st-row{display:flex;justify-content:space-between;align-items:baseline}
69
+ .st-rank{font:500 1.25rem var(--serif);background:var(--g);-webkit-background-clip:text;background-clip:text;color:transparent}
70
+ .st-rank small{font:400 .72rem var(--mono);color:var(--muted);-webkit-text-fill-color:var(--muted)}
71
+ .stand svg{display:block;margin-top:2px}
72
+ .sx{font:500 8.5px var(--mono);fill:var(--muted)}.sx.you{fill:#7800f0;font-weight:600}
73
+ .st-line{display:flex;justify-content:space-between;gap:8px;font-size:.72rem;color:var(--soft);margin-top:2px}
74
+ .st-line b{color:var(--ink);font-weight:600}.st-line span:last-child{font:600 .72rem var(--mono);white-space:nowrap}
75
+ .pick{display:flex;flex-wrap:wrap;gap:5px;margin-top:8px}
76
+ .pick button{display:inline-flex;align-items:center;gap:6px;border:0;background:#fff;box-shadow:0 0 0 1px rgba(74,38,130,.12);font:500 .74rem var(--sans);padding:4px 10px 4px 4px;border-radius:999px;color:var(--ink);transition:background .15s,color .15s,box-shadow .15s}
77
+ .pick.m button{padding:4px 10px}
78
+ .pick button:hover{box-shadow:0 0 0 1px rgba(120,0,240,.35)}
79
+ .pick button.sel{background:var(--ink);color:#fff;box-shadow:none}
80
+ .ini{display:inline-grid;place-items:center;flex:none;width:18px;height:18px;border-radius:50%;font:600 .46rem var(--mono);color:#0f1528}
81
+ .go-row{display:flex;gap:8px;margin-top:12px}
82
+ .go{flex:1;border:0;border-radius:12px;padding:11px;font:600 .9rem var(--sans);color:#fff;background:linear-gradient(90deg,#7a1036,#4a1a8c);display:flex;justify-content:center;gap:10px;align-items:center;transition:filter .2s,transform .1s}
83
+ .go small{font-weight:400;opacity:.7}.go:hover{filter:brightness(1.15)}.go:active{transform:scale(.98)}
84
+ .go:disabled,.auto:disabled{opacity:.4;cursor:default;filter:none}
85
+ .auto{border:0;border-radius:12px;padding:0 14px;font:600 .78rem var(--sans);color:var(--ink);background:var(--paper-2);box-shadow:inset 0 0 0 1px var(--line)}
86
+ .auto.on{background:var(--ink);color:#fff}
87
+ .reveal{display:flex;align-items:center;flex-wrap:wrap;gap:6px 10px;margin-top:10px;padding:9px 11px;border-radius:10px;min-height:40px;font-size:.74rem;color:var(--soft);
88
+ background:linear-gradient(90deg,rgba(180,0,40,.05),rgba(120,0,240,.06))}
89
+ .reveal b{color:var(--ink);font-size:.8rem}
90
+ .reveal.flash{animation:flash .5s}@keyframes flash{from{background:rgba(120,0,240,.18)}}
91
+ .act{font:600 .62rem var(--sans);letter-spacing:.06em;padding:2px 6px;border-radius:5px;text-align:center}
92
+ .reveal .act{font-size:.72rem;padding:3px 8px}
93
+ .act.B{color:#0b7a45;background:#e3f5eb}.act.S{color:#b4233a;background:#fdecee}.act.H{color:#4f5a6e;background:var(--paper-2)}
94
+ .logbar{padding:12px 16px 12px;min-width:0}
95
+ .tiles{display:flex;gap:6px;overflow-x:auto;margin-top:8px;padding:2px 2px 6px;scrollbar-width:thin;position:relative;
96
+ -webkit-mask-image:linear-gradient(90deg,transparent,#000 14px,#000 calc(100% - 14px),transparent);mask-image:linear-gradient(90deg,transparent,#000 14px,#000 calc(100% - 14px),transparent)}
97
+ .tile{flex:none;width:112px;border-radius:10px;padding:6px 8px;background:var(--paper-2);font-size:.68rem;display:grid;gap:3px;cursor:default;transition:box-shadow .15s,background .15s}
98
+ .tile:hover{background:#fff;box-shadow:0 0 0 1px rgba(120,0,240,.35)}
99
+ .tile .t1,.tile .t2{display:flex;justify-content:space-between;align-items:center}
100
+ .tile .t1{font-family:var(--mono);color:var(--muted);font-size:.62rem}
101
+ .tile .t2 span:last-child{font:600 .72rem var(--mono)}
102
+ .tile .t3{color:var(--soft);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
103
+ .tile.fut{background:none;outline:1px dashed rgba(74,38,130,.14);outline-offset:-1px;box-shadow:none;color:var(--muted)}
104
+ .tile.fut .t2,.tile.fut .t3{visibility:hidden}
105
+ .tile.now{outline:1.5px solid #7800f0;outline-offset:-1.5px;background:#fff}
106
+ .tile.now .t2{visibility:visible;color:#7800f0;font-weight:600}
107
+ .tile.new{animation:pop .35s}
108
+ .tile.on{background:#fff;box-shadow:0 0 0 1.5px #7800f0}
109
+ .log-body{flex:1;min-height:0;overflow:auto;margin-top:4px;scrollbar-width:thin}
110
+ .row{display:grid;grid-template-columns:42px 44px 1fr 62px;gap:8px;align-items:center;font-size:.74rem;padding:7px 0;border-top:1px solid var(--line)}
111
+ .row:first-child{border-top:0}.row.new{animation:pop .35s}
112
+ .row .d{font-family:var(--mono);color:var(--muted)}
113
+ .row .ag{color:var(--soft);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
114
+ .row .r{text-align:right;font-family:var(--mono)}
115
+ .log .empty{font-size:.74rem;color:var(--muted);padding:10px 0}
116
+
117
+ /* overlay (start / end) */
118
+ .ov{position:fixed;inset:0;display:grid;place-items:center;background:rgba(246,245,250,.6);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);z-index:20;opacity:0;pointer-events:none;transition:opacity .3s}
119
+ .ov.on{opacity:1;pointer-events:auto}
120
+ .ov-card{width:min(640px,92vw);background:#fff;border-radius:22px;box-shadow:var(--ring),0 30px 80px -30px rgba(60,30,120,.35);padding:28px 30px;transform:translateY(10px);transition:transform .35s}
121
+ .ov.on .ov-card{transform:none}
122
+ .ov-card .kick{justify-content:flex-start}
123
+ .ov-card h2{font:500 2rem/1.15 var(--serif);margin:6px 0 4px;letter-spacing:-.01em}
124
+ .ov-card h2 em{font-style:italic;background:var(--g);-webkit-background-clip:text;background-clip:text;color:transparent;padding-right:.06em}
125
+ .ov-card p{margin:6px 0 0;font-size:.86rem;color:var(--soft);line-height:1.5}
126
+ .reroll{border:0;background:none;color:var(--muted);font-size:1rem;vertical-align:4px;padding:0 4px}.reroll:hover{color:#7800f0}
127
+ .stocks{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:18px}
128
+ .stock{border:0;background:#fff;border-radius:14px;box-shadow:0 0 0 1px rgba(74,38,130,.12);padding:12px 12px 8px;text-align:left;transition:box-shadow .2s,transform .2s}
129
+ .stock:hover{transform:translateY(-2px);box-shadow:0 0 0 1px rgba(120,0,240,.35),0 8px 20px -12px rgba(60,30,120,.3)}
130
+ .stock.sel{box-shadow:0 0 0 2px #7800f0}
131
+ .stock b{display:block;font:600 .95rem var(--mono)}.stock small{font-size:.7rem;color:var(--muted)}
132
+ .stock svg{display:block;width:100%;height:34px;margin-top:6px}
133
+ .rules{display:flex;gap:18px;margin-top:16px;font-size:.74rem;color:var(--soft)}
134
+ .rules span{display:flex;gap:6px;align-items:baseline}.rules i{font-style:normal;font:600 .7rem var(--mono);color:#7800f0}
135
+ .ov-actions{display:flex;gap:10px;margin-top:20px;align-items:center}
136
+ .ov-actions .go{flex:none;padding:11px 22px}
137
+ .ov-actions a,.ov-actions .ghost{font-size:.82rem}
138
+ .ov-actions a{color:var(--soft);text-decoration:none;padding:7px 12px;border-radius:999px;box-shadow:inset 0 0 0 1px var(--line)}
139
+ .res{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-top:16px}
140
+ .res div{background:var(--paper-2);border-radius:12px;padding:10px 12px}
141
+ .res small{display:block;font-size:.6rem;color:var(--muted);letter-spacing:.08em;text-transform:uppercase}
142
+ .res b{font:600 1.05rem var(--mono)}.res em{display:block;font:400 .7rem var(--mono);color:var(--muted);font-style:normal;margin-top:2px}
143
+ .rankline{margin-top:14px;padding:12px 14px;border-radius:12px;font-size:.84rem;color:var(--soft);background:linear-gradient(90deg,rgba(180,0,40,.05),rgba(120,0,240,.07))}
144
+ .rankline b{color:var(--ink)}.rankline .big{font:500 1.6rem var(--serif);margin-right:6px;background:var(--g);-webkit-background-clip:text;background-clip:text;color:transparent}
145
+ .rankline .dn{color:var(--dn)}
146
+ .rk-top{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:4px}
147
+ .tb{margin-top:8px;font-size:.78rem}
148
+ .tr{display:grid;grid-template-columns:40px 1fr 72px 72px;gap:8px;align-items:center;padding:4px 6px;border-radius:8px}
149
+ .tr span:nth-child(3),.tr span:nth-child(4){text-align:right;font-family:var(--mono)}
150
+ .tr.th{font-size:.6rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
151
+ .tr .rk{font-family:var(--mono);color:var(--muted)}.tr .nm2{display:flex;align-items:center;gap:6px;color:var(--ink)}
152
+ .tr .nm2 small{color:var(--muted);font-size:.7rem}
153
+ .tr.me{background:#fff;box-shadow:0 0 0 1.5px #7800f0}.tr.me .rk{color:#7800f0;font-weight:600}.tr.me .nm2{font-weight:600}
154
+ .tr.gapr{display:block;text-align:center;color:var(--muted);padding:0;line-height:.8}
155
+ .fav{margin-top:8px;font-size:.74rem;color:var(--soft)}
156
+
157
+ .tip{position:fixed;z-index:30;pointer-events:none;background:var(--ink);color:#fff;font-size:.72rem;padding:6px 9px;border-radius:8px;opacity:0;transition:opacity .15s;white-space:nowrap}
158
+ .tip.on{opacity:1}
159
+
160
+ @media(max-width:1000px){
161
+ .app{height:auto;padding:0 16px 20px}.title,.brand{display:none}
162
+ .main{grid-template-columns:1fr}.chartcard{height:70vh}
163
+ .stocks{grid-template-columns:repeat(2,1fr)}.res{grid-template-columns:repeat(2,1fr)}
164
+ }
165
+ @media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
166
+ .stand .st-line{margin-top:6px}
167
+ .ch-head .co{font-size:.78rem;color:var(--soft);margin-left:-8px}
168
+ .dayblk{margin-left:auto;display:flex;align-items:center;gap:10px;font:500 .78rem var(--mono);color:var(--soft)}
169
+ .dayblk b{color:var(--ink)}
170
+ .prog{display:inline-block;width:90px;height:5px;border-radius:3px;background:var(--paper-2);overflow:hidden;box-shadow:inset 0 0 0 1px var(--line)}
171
+ .prog i{display:block;height:100%;background:linear-gradient(90deg,#b40028,#7800f0);border-radius:3px;transition:width .4s}
172
+ #kline .lg text{font:500 9.5px var(--sans);fill:var(--soft)}
173
+ #kline .flag{font:600 9px var(--mono);fill:#fff}
174
+ .ch-head .ohlc{font-size:.68rem}
175
+ .dayblk{font-size:.74rem}.prog{width:70px}
176
+ .side > .card:last-child{flex:1}
177
+ /* standing as the left column */
178
+ .main > .stand{display:flex;flex-direction:column;min-height:0;overflow:hidden}
179
+ .main > .stand .st-row{margin-top:6px}
180
+ .main > .stand .st-rank{font-size:1.6rem}
181
+ .main > .stand .st-plot{flex:none;height:150px;margin-top:4px}
182
+ .tb.full{flex:1;min-height:0;overflow:auto;margin-top:8px;scrollbar-width:thin;border-top:1px solid var(--line);padding-top:0}
183
+ .tb.full .tr{grid-template-columns:30px minmax(0,1fr) 60px;padding:3px 4px;font-size:.72rem}
184
+ .tb.full .tr.th{position:sticky;top:0;background:#fff;z-index:1;font-size:.56rem;border-radius:0;padding-top:6px;padding-bottom:5px;box-shadow:0 1px 0 var(--line)}
185
+ .tb.full .tr .nm2{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
186
+ .tb.full .tr .ini{width:16px;height:16px;font-size:.42rem}
187
+ .tb.full .gapr{display:none}
188
+ #kline .ttag .t1{font:700 10px var(--mono);fill:#fff;letter-spacing:.04em}
189
+ #kline .ttag .t1{font:600 10.5px var(--sans);fill:#fff}
190
+ #kline .ttag .t2{font:500 8.5px var(--sans);fill:rgba(255,255,255,.75);letter-spacing:.04em}
191
+ /* right column: richer reveal + P&L attribution */
192
+ .reveal{flex-direction:column;align-items:stretch;gap:6px}
193
+ .rv1{display:flex;justify-content:space-between;align-items:baseline;gap:8px}
194
+ .rv-pnl{font:600 .8rem var(--mono)}
195
+ .rv2{display:flex;align-items:center;gap:6px;flex-wrap:wrap;font-size:.66rem}
196
+ .rv2 .muted{margin-right:auto}
197
+ .rv-run{display:inline-flex;align-items:center;gap:3px;padding:2px 4px 2px 5px;border-radius:7px;background:#fff;opacity:.6}
198
+ .rv-run i{font:500 .6rem var(--mono);font-style:normal;color:var(--muted)}
199
+ .rv-run .act{font-size:.56rem;padding:1px 4px}
200
+ .rv-run.drawn{opacity:1;box-shadow:0 0 0 1.5px #7800f0}
201
+ .act.na{color:var(--muted);background:var(--paper-2)}
202
+ .side > .card:last-child{flex:1}
203
+ .attr{display:flex;flex-direction:column;min-height:0;overflow:hidden}
204
+ .at-h,.at-r{display:grid;grid-template-columns:minmax(0,1.1fr) 64px minmax(0,1fr) 56px;gap:8px;align-items:center;white-space:nowrap}
205
+ .at-h{margin-top:8px;font-size:.56rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
206
+ .at-h span:nth-child(2),.at-h span:last-child{text-align:right}
207
+ .at-body{flex:1;min-height:0;overflow:auto;scrollbar-width:thin}
208
+ .at-r{padding:5px 0;border-top:1px solid var(--line);font-size:.72rem}
209
+ .at-n{display:flex;align-items:center;gap:6px;white-space:nowrap;overflow:hidden}
210
+ .at-n .ini{width:17px;height:17px;font-size:.44rem;flex:none}
211
+ .at-d{font-family:var(--mono);color:var(--muted);text-align:right;font-size:.66rem}
212
+ .at-bar{position:relative;height:8px;background:linear-gradient(90deg,transparent calc(50% - .5px),rgba(74,38,130,.18) calc(50% - .5px),rgba(74,38,130,.18) calc(50% + .5px),transparent calc(50% + .5px))}
213
+ .at-bar i{position:absolute;top:0;bottom:0;border-radius:2px}
214
+ .at-bar i.p{left:50%;background:#0f9d58}.at-bar i.n{right:50%;background:#d6304a}
215
+ .at-v{font:600 .7rem var(--mono);text-align:right}
216
+ /* right column: roomier top two cards */
217
+ .side{gap:14px}
218
+ .side > .card:not(.attr){padding:18px 20px}
219
+ .side .eq{margin-top:10px}
220
+ .side .kv{gap:10px;margin-top:14px}
221
+ .side .kv div{padding:9px 11px}
222
+ .pk-l{margin-top:12px;font-size:.6rem;font-weight:600;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
223
+ .side .pick{gap:6px 6px;margin-top:6px}
224
+ .side .go-row{margin-top:16px}
225
+ .side .reveal{margin-top:12px;padding:11px 13px}
226
+
227
+ /* ---------- narrow screens (≤1000px): one column — chart, then the controls, then the standing. Desktop is untouched. ---------- */
228
+ @media (max-width:1000px){
229
+ .app{height:auto;min-height:0;display:flex;flex-direction:column;gap:14px;padding:0 14px 20px}
230
+ .main{display:flex;flex-direction:column;gap:14px}
231
+ .main > .chartcard{order:1;height:62vh;min-height:380px}
232
+ .main > .side{order:2}
233
+ .main > .stand{order:3;max-height:420px}
234
+ .ch-head{flex-wrap:wrap;white-space:normal;row-gap:2px}
235
+ .ch-head .ohlc{flex-basis:100%}
236
+ .who .nm{display:none}
237
+ .rules{flex-wrap:wrap;row-gap:6px}
238
+ .ov-card{padding:22px 20px;max-height:92vh;overflow:auto}
239
+ .logbar .kick span:last-child{display:none}
240
+ }
play.html ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Trade with an agent · HERCULEAN</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@600;700&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;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
10
+ <link rel="icon" href="./assets/finai-logo.png" />
11
+ <link rel="stylesheet" href="./play.css" />
12
+ </head>
13
+ <body>
14
+ <div class="app">
15
+ <header class="top">
16
+ <a class="back" href="./index.html">← Leaderboard</a>
17
+ <a class="brand" href="./index.html"><img src="./assets/finai-logo.png" alt="">The Fin AI<span class="sep">/</span><b>HERCULEAN</b></a>
18
+ <div class="who">
19
+ <button class="newg" id="newBtn" title="Start over">↻ New game</button>
20
+ <div class="avatar" id="ava"></div><div class="nm"><small>Trader</small><b id="trader"></b></div>
21
+ </div>
22
+ </header>
23
+
24
+ <main class="main">
25
+ <section class="card stand" id="stand"></section>
26
+
27
+ <section class="chartcard">
28
+ <div class="ch-head" id="head"></div>
29
+ <svg id="kline" aria-label="Price chart"></svg>
30
+ </section>
31
+
32
+ <aside class="side">
33
+ <div class="card">
34
+ <div class="kick"><span>Portfolio · $100k start</span><span>you vs B&amp;H</span></div>
35
+ <div class="eq"><b id="eqv">$100,000</b><span id="eqr"></span></div>
36
+ <div class="kv" id="kv"></div>
37
+ </div>
38
+ <div class="card">
39
+ <div class="kick"><span>Who trades today?</span><span id="dayK"></span></div>
40
+ <div class="pk-l">Architecture</div>
41
+ <div class="pick" id="arch"></div>
42
+ <div class="pk-l">Backbone</div>
43
+ <div class="pick m" id="model"></div>
44
+ <div class="go-row">
45
+ <button class="go" id="goBtn">Trade today <small>▶ reveal the call</small></button>
46
+ <button class="auto" id="autoBtn" title="Keep this pick and play the remaining days">Auto ▸▸</button>
47
+ </div>
48
+ <div class="reveal" id="rev"><span class="muted">Pick an architecture and a backbone, then trade. One of its 3 runs is drawn at random.</span></div>
49
+ </div>
50
+ <div class="card attr" id="attr"></div>
51
+ </aside>
52
+ </main>
53
+
54
+
55
+ <section class="card logbar">
56
+ <div class="kick"><span>Trade log · day by day</span><span>system · run · action · P&amp;L</span></div>
57
+ <div class="tiles" id="log"></div>
58
+ </section>
59
+ </div>
60
+
61
+ <!-- start / end overlay -->
62
+ <div class="ov" id="ov">
63
+ <div class="ov-card" id="ovCard"></div>
64
+ </div>
65
+
66
+ <div class="tip" id="tip"></div>
67
+ <script src="./data.js"></script>
68
+ <script src="./play.js"></script>
69
+ </body>
70
+ </html>
play.js ADDED
@@ -0,0 +1,351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* HERCULEAN · Trade with an agent.
2
+ Each day the player picks an agent system (architecture × backbone); one of its three recorded runs is drawn
3
+ (only among runs that committed that day; HOLD if none did) and its real Trading decision is executed.
4
+ Scoring follows analysis/score_runs.py: BUY/SELL/HOLD -> +1/-1/0, held from the open of the day to the next
5
+ open (last day: open to close), adjusted prices, no costs. SR = sqrt(252)·mean/std of daily returns. */
6
+ (() => {
7
+ const H = window.HERC, OH = H.ohlc, HIST = OH.start, N = H.dates.length, $ = id => document.getElementById(id);
8
+ const AC = { ReAct: '#2a78d6', 'Claude Code': '#eb6834', Codex: '#1baf7a', Hermes: '#eda100', OpenClaw: '#e87ba4' };
9
+ const INI = { ReAct: 'RA', 'Claude Code': 'CC', Codex: 'CX', Hermes: 'HM', OpenClaw: 'OC' };
10
+ const MS = { 'Claude Sonnet 5': 'Sonnet 5', 'DeepSeek-V3.2': 'DS-V3.2', 'DeepSeek-V4-Pro': 'DS-V4-Pro', 'Qwen3.5-9B': 'Qwen-9B', 'Qwen3.5-4B': 'Qwen-4B' };
11
+ const CO = { MSFT: 'Microsoft', TSLA: 'Tesla', AAPL: 'Apple', NVDA: 'NVIDIA' };
12
+ const NAMES = ['Perseus', 'Theseus', 'Atalanta', 'Achilles', 'Odysseus', 'Penelope', 'Jason', 'Medea', 'Orpheus', 'Eurydice',
13
+ 'Andromeda', 'Cassandra', 'Hector', 'Ajax', 'Electra', 'Antigone', 'Ariadne', 'Bellerophon', 'Castor', 'Pollux', 'Daedalus',
14
+ 'Icarus', 'Hippolyta', 'Iphigenia', 'Nestor', 'Telemachus', 'Pandora', 'Psyche', 'Calypso', 'Circe', 'Paris', 'Helen'];
15
+ const ACT = { B: 'BUY', S: 'SELL', H: 'HOLD' }, POS = { B: 1, S: -1, H: 0 };
16
+ const short = m => MS[m] || m, sysName = (a, m) => `${a} · ${short(m)}`;
17
+ const pct = (v, d = 2) => (v >= 0 ? '+' : '−') + Math.abs(v * 100).toFixed(d) + '%';
18
+ const money = v => '$' + Math.round(v).toLocaleString('en-US');
19
+ const cls = v => v > 0 ? 'up' : v < 0 ? 'dn' : '';
20
+ const pickRand = a => a[Math.floor(Math.random() * a.length)];
21
+
22
+ let S; // game state
23
+ let arch = 'Hermes', model = 'DeepSeek-V4-Pro', name = pickRand(NAMES), sym = 'MSFT', autoT = null, hover = null;
24
+
25
+ // per-day returns on the chosen stock (open -> next open; last day open -> close)
26
+ const returns = tk => H.dates.map((_, d) => { const b = OH.bars[tk][HIST + d], nx = OH.bars[tk][HIST + d + 1]; return (nx ? nx[0] : b[3]) / b[0] - 1; });
27
+ const metrics = (rets) => {
28
+ let w = 1, peak = 1, mdd = 0; for (const r of rets) { w *= 1 + r; peak = Math.max(peak, w); mdd = Math.max(mdd, 1 - w / peak); }
29
+ const n = rets.length, mu = n ? rets.reduce((p, q) => p + q, 0) / n : 0;
30
+ const sd = n > 1 ? Math.sqrt(rets.reduce((p, q) => p + (q - mu) ** 2, 0) / (n - 1)) : 0;
31
+ return { cr: w - 1, sr: sd > 0 ? Math.sqrt(252) * mu / sd : 0, mdd };
32
+ };
33
+
34
+ // the 30 systems on this stock: mean wealth over their three runs after each day (missing decision = flat)
35
+ function field(tk, ret) {
36
+ return H.agents.flatMap(a => H.models.map(m => {
37
+ const runs = (H.runs.trading[`${a}|${m}|${tk}`] || []).filter(Boolean), w = new Array(N + 1).fill(0);
38
+ runs.forEach(r => { let x = 1; w[0] += 1; for (let d = 0; d < N; d++) { x *= 1 + (POS[r[d]] || 0) * ret[d]; w[d + 1] += x; } });
39
+ return { a, m, w: w.map(v => v / (runs.length || 1)) };
40
+ }));
41
+ }
42
+ // where the player stands after day d (CR, ties go to the player)
43
+ function standing(d) {
44
+ const you = S.eq[d] - 1, all = S.field.map(f => ({ a: f.a, m: f.m, cr: f.w[d] - 1 })).sort((p, q) => q.cr - p.cr);
45
+ const rank = 1 + all.filter(f => f.cr > you + 1e-12).length;
46
+ return { you, all, rank, n: all.length + 1, top: all[0], bh: S.bh[d] - 1 };
47
+ }
48
+ // the field on one CR axis, one lane per architecture (dots = backbones); you = the purple line and dot
49
+ function strip(st, W, Hs) {
50
+ const xs = [...st.all.map(f => f.cr), st.you, st.bh, 0], lo = Math.min(...xs), hi = Math.max(...xs), span = hi - lo || .01, pl = 26, pr = 10;
51
+ const X = v => pl + (v - lo) / span * (W - pl - pr), top = 16, axY = Hs - 14, lh = (axY - top - 2) / H.agents.length;
52
+ const LY = a => top + (H.agents.indexOf(a) + .5) * lh, r = Math.max(2.8, Math.min(4.6, lh * .36));
53
+ let g = '';
54
+ H.agents.forEach((a, i) => g += `<line x1="${pl}" x2="${W - pr}" y1="${LY(a)}" y2="${LY(a)}" stroke="rgba(74,38,130,${i % 2 ? .05 : .09})"/>` +
55
+ `<text x="${pl - 6}" y="${LY(a) + 3}" text-anchor="end" class="sx" style="fill:${AC[a]};font-weight:600">${INI[a]}</text>`);
56
+ g += `<line x1="${pl}" x2="${W - pr}" y1="${axY}" y2="${axY}" stroke="rgba(74,38,130,.18)"/>`;
57
+ const lab = [[lo, pct(lo, 1), 'start'], [hi, pct(hi, 1), 'end'], [0, '0%', 'middle'], [st.bh, 'B&H', 'middle']], used = [];
58
+ lab.reverse().forEach(([v, t, an]) => { const x = X(v); if (used.some(u => Math.abs(u - x) < 30)) return; used.push(x);
59
+ g += `<text x="${x}" y="${Hs - 2}" text-anchor="${an}" class="sx"${t === 'B&H' ? ' style="fill:#4f5a6e"' : ''}>${t}</text>`; });
60
+ g += `<line x1="${X(0)}" x2="${X(0)}" y1="${top}" y2="${axY}" stroke="rgba(74,38,130,.14)"/>`;
61
+ g += `<line x1="${X(st.bh)}" x2="${X(st.bh)}" y1="${top}" y2="${axY + 2}" stroke="#8a93a3" stroke-dasharray="2 2"/>`;
62
+ st.all.forEach(f => g += `<circle cx="${X(f.cr)}" cy="${LY(f.a)}" r="${r}" fill="${AC[f.a]}" opacity=".7"><title>${sysName(f.a, f.m)} · ${pct(f.cr)}</title></circle>`);
63
+ const t = st.top;
64
+ g += `<circle cx="${X(t.cr)}" cy="${LY(t.a)}" r="${r + 2}" fill="none" stroke="${AC[t.a]}" stroke-width="1.2"/><text x="${Math.min(X(t.cr), W - pr)}" y="${top - 5}" text-anchor="end" class="sx">#1</text>`;
65
+ const yx = X(st.you);
66
+ g += `<line x1="${yx}" x2="${yx}" y1="${top - 2}" y2="${axY}" stroke="#7800f0" stroke-width="1.6"/><circle cx="${yx}" cy="${top - 2}" r="4" fill="#7800f0" stroke="#fff" stroke-width="1.5"><title>you · ${pct(st.you)}</title></circle>` +
67
+ `<text x="${yx + (yx > W - 40 ? -8 : 8)}" y="${top + 1}" text-anchor="${yx > W - 40 ? 'end' : 'start'}" class="sx you">you</text>`;
68
+ return `<svg viewBox="0 0 ${W} ${Hs}" width="100%" height="${Hs}" style="overflow:visible">${g}</svg>`;
69
+ }
70
+
71
+ // ranking rows: top 3, then the player's neighbourhood (`around` rows either side)
72
+ function rankTable(st, around, compact) {
73
+ const rank = st.rank, rows = st.all.map((f, i) => ({ ...f, r: i + 1 + (i + 1 >= rank ? 1 : 0) }));
74
+ rows.splice(rank - 1, 0, { you: true, cr: st.you, r: rank });
75
+ const keep = new Set([0, 1, 2]); for (let k = -around; k <= around; k++) keep.add(rank - 1 + k);
76
+ let tb = '', prev = -1;
77
+ rows.forEach((f, i) => { if (!keep.has(i)) return; if (prev >= 0 && i > prev + 1) tb += '<div class="tr gapr">⋯</div>'; prev = i;
78
+ tb += `<div class="tr${f.you ? ' me' : ''}"><span class="rk">#${f.r}</span><span class="nm2">${f.you ? `${name} <small>your picks</small>` : `<span class="ini" style="background:${AC[f.a]}${f.a === 'ReAct' ? ';color:#fff' : ''}">${INI[f.a]}</span>${sysName(f.a, f.m)}`}</span>` +
79
+ `<span class="${cls(f.cr)}">${pct(f.cr)}</span>${compact ? '' : `<span class="muted">${i === 0 ? '—' : ((f.cr - rows[0].cr) * 100).toFixed(2) + ' pp'}</span>`}</div>`; });
80
+ return tb;
81
+ }
82
+
83
+ function newGame(tk) {
84
+ sym = tk; stopAuto();
85
+ S = { d: 0, ret: returns(tk), log: [], eq: [1], bh: [1], picks: {} }; S.field = field(tk, S.ret);
86
+ $('trader').textContent = name; $('ava').textContent = name[0];
87
+ $('rev').innerHTML = '<span class="muted">Pick an architecture and a backbone, then trade. One of its 3 runs is drawn at random.</span>';
88
+ renderAll();
89
+ }
90
+
91
+ // ---------- one trading day ----------
92
+ function step() {
93
+ if (!S || S.d >= N) return;
94
+ const d = S.d, runs = H.runs.trading[`${arch}|${model}|${sym}`] || ['', '', ''];
95
+ const ok = [0, 1, 2].filter(r => runs[r] && runs[r][d] && runs[r][d] !== '-');
96
+ const r = ok.length ? pickRand(ok) : Math.floor(Math.random() * 3);
97
+ const a = ok.length ? runs[r][d] : 'H', miss = !ok.length;
98
+ const pnl = POS[a] * S.ret[d];
99
+ S.eq.push(S.eq[d] * (1 + pnl)); S.bh.push(S.bh[d] * (1 + S.ret[d]));
100
+ S.log.push({ d, a, arch, model, run: r + 1, pnl, miss });
101
+ const k = `${arch}|${model}`; S.picks[k] = (S.picks[k] || 0) + 1;
102
+ S.d++;
103
+ // the three recorded runs of this system on this day; the drawn one is outlined
104
+ const calls = [0, 1, 2].map(i => { const c = runs[i] && runs[i][d]; return !c || c === '-' ? null : c; });
105
+ $('rev').innerHTML = `<div class="rv1"><b>${sysName(arch, model)}</b><span class="rv-pnl ${cls(pnl)}">${miss ? 'HOLD · no commit' : pct(pnl)}</span></div>` +
106
+ `<div class="rv2"><span class="muted">${H.dates[d].slice(5)} · its 3 runs</span>${calls.map((c, i) => `<span class="rv-run${i === r ? ' drawn' : ''}"><i>r${i + 1}</i>${c ? `<span class="act ${c}">${ACT[c]}</span>` : '<span class="act na">—</span>'}</span>`).join('')}</div>`;
107
+ const rv = $('rev'); rv.classList.remove('flash'); void rv.offsetWidth; rv.classList.add('flash');
108
+ renderAll(true);
109
+ if (S.d >= N) { stopAuto(); setTimeout(endCard, 700); }
110
+ }
111
+ function stopAuto() { clearInterval(autoT); autoT = null; $('autoBtn').classList.remove('on'); $('autoBtn').textContent = 'Auto ▸▸'; }
112
+
113
+ // ---------- render ----------
114
+ function renderAll(fresh) { renderSide(fresh); draw(fresh); }
115
+
116
+ function renderSide(fresh) {
117
+ const done = S.d >= N, eq = S.eq[S.d], bh = S.bh[S.d], m = metrics(S.log.map(l => l.pnl));
118
+ $('dayK').textContent = done ? 'window closed' : `Day ${S.d + 1} · ${H.dates[S.d].slice(5)}`;
119
+ $('eqv').textContent = money(1e5 * eq);
120
+ $('eqr').innerHTML = `<span class="${cls(eq - 1)}">${pct(eq - 1)}</span> <span class="vs">vs</span> <span class="${cls(bh - 1)}">${pct(bh - 1)}</span>`;
121
+ const last = S.log[S.log.length - 1];
122
+ const pos = !last ? 'Flat' : { B: 'Long', S: 'Short', H: 'Flat' }[last.a];
123
+ $('kv').innerHTML = [['Position', pos], ['Max DD', (m.mdd * 100).toFixed(2) + '%'], ['Sharpe', S.log.length > 1 ? m.sr.toFixed(2) : '—']]
124
+ .map(([k, v]) => `<div><small>${k}</small><b>${v}</b></div>`).join('');
125
+ // P&L attribution: each picked system's days, win days and summed daily P&L (pp)
126
+ const by = {};
127
+ S.log.forEach(l => { const k = `${l.arch}|${l.model}`, b = by[k] || (by[k] = { a: l.arch, m: l.model, days: 0, win: 0, pnl: 0 }); b.days++; b.pnl += l.pnl; if (l.pnl > 0) b.win++; });
128
+ const ag = Object.values(by).sort((p, q) => q.pnl - p.pnl), amax = Math.max(.002, ...ag.map(b => Math.abs(b.pnl)));
129
+ $('attr').innerHTML = `<div class="kick"><span>P&amp;L attribution</span><span>by agent system</span></div>` + (ag.length
130
+ ? `<div class="at-h"><span>agent system</span><span>days · won</span><span>P&amp;L</span></div><div class="at-body">` + ag.map(b => `<div class="at-r" title="${b.a} · ${b.m}: ${b.days} days, ${b.win} winning, sum of daily P&L ${pct(b.pnl)}">` +
131
+ `<span class="at-n"><span class="ini" style="background:${AC[b.a]}${b.a === 'ReAct' ? ';color:#fff' : ''}">${INI[b.a]}</span>${short(b.m)}</span>` +
132
+ `<span class="at-d">${b.days} · ${b.win}</span>` +
133
+ `<span class="at-bar"><i class="${b.pnl >= 0 ? 'p' : 'n'}" style="width:${Math.abs(b.pnl) / amax * 50}%"></i></span>` +
134
+ `<span class="at-v ${cls(b.pnl)}">${pct(b.pnl)}</span></div>`).join('') + '</div>'
135
+ : '<p class="st-line muted" style="margin-top:8px;white-space:normal">Which of your picks made or lost money will show here.</p>');
136
+ const st = standing(S.d), gap = (st.you - st.top.cr) * 100;
137
+ $('stand').innerHTML = `<div class="kick"><span>Standing · ${sym} CR</span><span>mean of 3 runs</span></div>` +
138
+ `<div class="st-row"><span class="st-rank">${S.d ? `#${st.rank}<small> / ${st.n}</small>` : '<small>—</small>'}</span>` +
139
+ (S.d ? `<span class="st-gap ${st.rank === 1 ? 'up' : 'dn'}">${st.rank === 1 ? '+' + ((st.you - st.all[0].cr) * 100).toFixed(2) + ' pp ahead' : '−' + Math.abs(gap).toFixed(2) + ' pp to #1'}</span>` : '') + '</div>' +
140
+ (S.d ? '<div class="st-plot" id="stPlot"></div>' +
141
+ `<div class="tb full" id="stList"><div class="tr th"><span>#</span><span>agent system</span><span>CR</span></div>${rankTable(st, 99, true)}</div>`
142
+ : `<p class="st-line muted" style="margin-top:10px;white-space:normal">Trade to enter the ranking of the 30 agent systems (CR, mean of 3 runs, up to the same day).</p>`);
143
+ const me = document.querySelector('#stList .tr.me'), li = $('stList');
144
+ // centre the player's row in the list (positions measured relative to the list itself)
145
+ if (me && li) li.scrollTop = Math.max(0, me.getBoundingClientRect().top - li.getBoundingClientRect().top + li.scrollTop - (li.clientHeight - me.offsetHeight) / 2);
146
+ lastSt = S.d ? st : null; plotStand(); requestAnimationFrame(plotStand);
147
+ $('arch').innerHTML = H.agents.map(a => `<button data-a="${a}" class="${a === arch ? 'sel' : ''}"><span class="ini" style="background:${AC[a]}${a === 'ReAct' ? ';color:#fff' : ''}">${INI[a]}</span>${a}</button>`).join('');
148
+ $('model').innerHTML = H.models.map(x => `<button data-m="${x}" class="${x === model ? 'sel' : ''}" title="${x}">${short(x)}</button>`).join('');
149
+ $('goBtn').disabled = done; $('autoBtn').disabled = done;
150
+ $('log').innerHTML = H.dates.map((dt, d) => {
151
+ const l = S.log[d];
152
+ if (!l) return `<div class="tile fut${d === S.d ? ' now' : ''}" data-d="${d}"><div class="t1"><span>${dt.slice(5)}</span><span>D${d + 1}</span></div><div class="t2"><span>${d === S.d ? 'today' : ''}</span><span></span></div><div class="t3">·</div></div>`;
153
+ return `<div class="tile${fresh && d === S.d - 1 ? ' new' : ''}" data-d="${d}" title="${l.arch} · ${l.model} · run ${l.run}${l.miss ? ' · no commit, scored as HOLD' : ''}"><div class="t1"><span>${dt.slice(5)}</span><span>D${d + 1}</span></div>` +
154
+ `<div class="t2"><span class="act ${l.a}">${ACT[l.a]}</span><span class="${cls(l.pnl)}">${pct(l.pnl)}</span></div><div class="t3">${INI[l.arch]} · ${short(l.model)} · r${l.run}${l.miss ? ' · –' : ''}</div></div>`;
155
+ }).join('');
156
+ const tl = $('log'), cur = tl.children[Math.min(S.d, N - 1)];
157
+ if (cur) tl.scrollTo({ left: Math.max(0, cur.offsetLeft - tl.clientWidth + cur.offsetWidth * 2.5), behavior: 'auto' });
158
+ }
159
+
160
+ function head(i) {
161
+ const b = OH.bars[sym], cur = b[i], prev = b[i - 1], chg = cur[3] / prev[3] - 1, d = Math.min(S.d, N), done = S.d >= N;
162
+ const dt = new Date((done ? H.dates[N - 1] : H.dates[S.d]) + 'T12:00:00Z').toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', timeZone: 'UTC' });
163
+ $('head').innerHTML = `<span class="sym">${sym}</span><span class="co">${CO[sym]}</span><span class="px">${cur[3].toFixed(2)}</span><span class="chg ${cls(chg)}">${pct(chg)}</span>` +
164
+ `<span class="ohlc">${OH.dates[i]} · O <b>${cur[0].toFixed(2)}</b> H <b>${cur[1].toFixed(2)}</b> L <b>${cur[2].toFixed(2)}</b> C <b>${cur[3].toFixed(2)}</b> · Vol <b>${cur[4].toFixed(1)}M</b></span>` +
165
+ ''; void dt; void d; void done;
166
+ }
167
+
168
+ // the standing plot takes whatever height the card has left; redraw when that changes (fonts, resize)
169
+ let lastSt = null;
170
+ function plotStand() {
171
+ const sp = $('stPlot'); if (!sp || !lastSt) return;
172
+ sp.innerHTML = ''; sp.innerHTML = strip(lastSt, sp.clientWidth || 328, Math.max(48, Math.min(170, sp.clientHeight)));
173
+ }
174
+ if (window.ResizeObserver) new ResizeObserver(() => plotStand()).observe($('stand'));
175
+ if (document.fonts) { document.fonts.ready.then(() => { plotStand(); S && draw(); }); document.fonts.addEventListener('loadingdone', () => { plotStand(); S && draw(); }); }
176
+
177
+ let G = null; // geometry for hover
178
+ function draw(fresh) {
179
+ const svg = $('kline'), W = svg.clientWidth, Hh = svg.clientHeight; if (!W || !Hh) return;
180
+ svg.setAttribute('viewBox', `0 0 ${W} ${Hh}`);
181
+ const bars = OH.bars[sym], total = HIST + N, L = 6, R = 58, cw = (W - L - R) / total, X = i => L + (i + .5) * cw;
182
+ const nShown = HIST + S.d, shown = bars.slice(0, nShown);
183
+ const pTop = 20, pH = Hh * .5, aTop = pTop + pH + 10, aH = 13, vTop = aTop + aH + 8, vH = Hh * .08, eTop = vTop + vH + 22, eH = Hh - eTop - 46, tY = Hh - 16;
184
+ // price scale from what has been revealed only (no peeking at the future)
185
+ let lo = Math.min(...shown.map(b => b[2])), hi = Math.max(...shown.map(b => b[1])); const pad = (hi - lo) * .12;
186
+ lo -= pad; hi += pad;
187
+ const Y = v => pTop + (1 - (v - lo) / (hi - lo)) * pH, vmax = Math.max(...shown.map(b => b[4]));
188
+ let g = '';
189
+ for (let t = 0; t <= 4; t++) { const v = lo + t / 4 * (hi - lo); g += `<line class="gl" x1="${L}" x2="${W - R}" y1="${Y(v)}" y2="${Y(v)}"/><text class="ax" x="${W - R + 6}" y="${Y(v) + 3}">${v.toFixed(0)}</text>`; }
190
+ const x0 = X(HIST) - cw / 2, xn = X(nShown) - cw / 2;
191
+ g += `<rect x="${x0}" y="${pTop}" width="${Math.max(0, xn - x0)}" height="${eTop + eH - pTop}" fill="rgba(120,0,240,.028)"/>`;
192
+ if (S.d < N) g += `<rect x="${xn}" y="${pTop}" width="${W - R - xn}" height="${eTop + eH - pTop}" fill="url(#fut)"/>`;
193
+ g += `<text class="ax" x="${x0 + 3}" y="${pTop + 10}" fill="#7a1fb4">Jan 2 · window opens</text>`;
194
+ // legends inside the panels
195
+ g += `<g class="lg"><line x1="${L + 2}" x2="${L + 16}" y1="${pTop - 9}" y2="${pTop - 9}" stroke="#eda100" stroke-width="1.6"/><text x="${L + 20}" y="${pTop - 6}">MA5</text>` +
196
+ `<line x1="${L + 52}" x2="${L + 66}" y1="${pTop - 9}" y2="${pTop - 9}" stroke="#7800f0" stroke-width="1.6"/><text x="${L + 70}" y="${pTop - 6}">MA20</text>` +
197
+ `<path d="M${L + 110},${pTop - 6} l4,-7 l4,7z" fill="#0f9d58"/><text x="${L + 122}" y="${pTop - 6}">agent BUY</text><path d="M${L + 180},${pTop - 13} l4,7 l4,-7z" fill="#d6304a"/><text x="${L + 192}" y="${pTop - 6}">agent SELL</text></g>`;
198
+ // daily decision strip (as in the Trading replay): one cell per window day
199
+ g += `<text class="ax" x="${x0 - 6}" y="${aTop + 10}" text-anchor="end">ACTION</text>`;
200
+ const AF = { B: '#0f9d58', S: '#d6304a', H: '#d9d7e3' };
201
+ for (let d = 0; d < N; d++) {
202
+ const l = S.log[d], cx0 = X(HIST + d) - cw / 2 + .6, cwi = Math.max(1, cw - 1.2);
203
+ if (l) g += `<rect${fresh && d === S.d - 1 ? ' class="cnew"' : ''} x="${cx0}" y="${aTop}" width="${cwi}" height="${aH}" rx="1.5" fill="${l.miss ? 'url(#miss)' : AF[l.a]}"/>`;
204
+ else g += `<rect x="${cx0}" y="${aTop}" width="${cwi}" height="${aH}" rx="1.5" fill="#fff" stroke="${d === S.d ? '#7800f0' : 'rgba(74,38,130,.14)'}" stroke-width="${d === S.d ? 1.4 : .8}"/>`;
205
+ }
206
+ // moving averages
207
+ [[5, '#eda100'], [20, '#7800f0']].forEach(([n, c]) => {
208
+ let p = ''; for (let i = n - 1; i < nShown; i++) { const v = bars.slice(i - n + 1, i + 1).reduce((s, b) => s + b[3], 0) / n; p += (p ? 'L' : 'M') + X(i).toFixed(1) + ',' + Y(v).toFixed(1); }
209
+ g += `<path d="${p}" fill="none" stroke="${c}" stroke-width="1.2" opacity=".75"/>`;
210
+ });
211
+ // candles + volume
212
+ const w = Math.max(1.5, cw * .62);
213
+ shown.forEach((b, i) => {
214
+ const [o, h, l, c, v] = b, up = c >= o, col = up ? '#0f9d58' : '#d6304a', nw = fresh && i === nShown - 1 ? ' class="cnew"' : '';
215
+ g += `<g${nw}><line x1="${X(i)}" x2="${X(i)}" y1="${Y(h)}" y2="${Y(l)}" stroke="${col}"/><rect x="${X(i) - w / 2}" y="${Y(Math.max(o, c))}" width="${w}" height="${Math.max(1, Math.abs(Y(o) - Y(c)))}" fill="${up ? '#fff' : col}" stroke="${col}"/></g>`;
216
+ g += `<rect${nw} x="${X(i) - w / 2}" y="${vTop + vH - v / vmax * vH}" width="${w}" height="${v / vmax * vH}" fill="${col}" opacity=".35"/>`;
217
+ });
218
+ // the agents' calls
219
+ S.log.forEach((l, k) => {
220
+ const i = HIST + l.d, b = bars[i], nw = fresh && k === S.log.length - 1 ? ' class="mk"' : '';
221
+ if (l.a === 'B') g += `<path${nw} d="M${X(i)},${Y(b[2]) + 5} l4,7 h-8z" fill="#0f9d58"/>`;
222
+ if (l.a === 'S') g += `<path${nw} d="M${X(i)},${Y(b[1]) - 5} l4,-7 h-8z" fill="#d6304a"/>`;
223
+ });
224
+ const lastC = shown[nShown - 1][3], lastUp = lastC >= shown[nShown - 2][3];
225
+ g += `<rect x="${W - R + 2}" y="${Y(lastC) - 8}" width="${R - 4}" height="16" rx="4" fill="${lastUp ? '#0f9d58' : '#d6304a'}"/><text x="${W - R + 6}" y="${Y(lastC) + 4}" style="font:600 10px 'JetBrains Mono',monospace" fill="#fff">${lastC.toFixed(2)}</text>`;
226
+ // equity panel
227
+ const all = [...S.eq, ...S.bh], emin = Math.min(...all), emax = Math.max(...all), ep = Math.max((emax - emin) * .15, .01);
228
+ const EY = v => eTop + (1 - (v - emin + ep) / (emax - emin + 2 * ep)) * eH;
229
+ const ex = k => k === 0 ? x0 : X(HIST + k - 1) + cw / 2; // equity after day k sits at the right edge of that day
230
+ g += `<text class="ax" x="${L}" y="${eTop - 6}">EQUITY</text><line class="gl" x1="${L}" x2="${W - R}" y1="${EY(1)}" y2="${EY(1)}" stroke-dasharray="2 3"/><text class="ax" x="${W - R + 6}" y="${EY(1) + 3}">$100k</text>`;
231
+ const line = arr => arr.map((v, k) => (k ? 'L' : 'M') + ex(k).toFixed(1) + ',' + EY(v).toFixed(1)).join('');
232
+ if (S.d) {
233
+ g += `<path d="${line(S.eq)} L${ex(S.d)},${EY(1)} L${ex(0)},${EY(1)}Z" fill="rgba(120,0,240,.08)"/>`;
234
+ g += `<path d="${line(S.bh)}" fill="none" stroke="#8a93a3" stroke-dasharray="4 3" stroke-width="1.2"/><path d="${line(S.eq)}" fill="none" stroke="#0f1528" stroke-width="1.8"/>`;
235
+ g += `<circle cx="${ex(S.d)}" cy="${EY(S.eq[S.d])}" r="4" fill="#0f1528" stroke="#fff" stroke-width="2"/>`;
236
+ } else g += `<text class="ax" x="${x0 + 3}" y="${EY(1) - 8}">your equity starts here</text>`;
237
+ // time bar under the chart: the 61-day window, filled up to today; the Day tag rides on it
238
+ const xEnd = X(HIST + N - 1) + cw / 2, cur = Math.min(S.d, N - 1), tx = S.d >= N ? xEnd : X(HIST + S.d);
239
+ if (S.d < N) g += `<line x1="${tx}" x2="${tx}" y1="${pTop}" y2="${tY - 9}" stroke="#7800f0" stroke-opacity=".45" stroke-dasharray="3 3"/>`;
240
+ g += `<rect x="${x0}" y="${tY - 3}" width="${xEnd - x0}" height="6" rx="3" fill="#eceef4"/>` +
241
+ `<rect x="${x0}" y="${tY - 3}" width="${Math.max(0, (S.d >= N ? xEnd : tx - cw / 2) - x0)}" height="6" rx="3" fill="url(#tlg)"/>`;
242
+ [20, 40].forEach(k => g += `<line x1="${X(HIST + k) - cw / 2}" x2="${X(HIST + k) - cw / 2}" y1="${tY - 3}" y2="${tY + 3}" stroke="#fff" stroke-width="1.5"/>`);
243
+ g += `<text class="ax" x="${x0 - 8}" y="${tY + 3.5}" text-anchor="end">Jan 2</text><text class="ax" x="${xEnd + 8}" y="${tY + 3.5}">Mar 31</text>`;
244
+ // date tag riding on the bar: month and day on top, weekday below
245
+ const dd = new Date(H.dates[cur] + 'T12:00:00Z'), fmt = o => dd.toLocaleDateString('en-US', { ...o, timeZone: 'UTC' });
246
+ const l1 = S.d >= N ? 'Closed' : fmt({ month: 'short', day: 'numeric' }), l2 = S.d >= N ? 'Mar 31' : fmt({ weekday: 'short' });
247
+ const tw = Math.max(l1.length * 6.4, l2.length * 5.4) + 16, tcx = Math.min(Math.max(tx, x0 + tw / 2), xEnd - tw / 2);
248
+ g += `<g class="ttag"><rect x="${tcx - tw / 2}" y="${tY - 15}" width="${tw}" height="30" rx="9" fill="#4a1a8c" stroke="#fff" stroke-width="2"/>` +
249
+ `<text x="${tcx}" y="${tY - 1.5}" text-anchor="middle" class="t1">${l1}</text><text x="${tcx}" y="${tY + 9.5}" text-anchor="middle" class="t2">${l2}</text></g>`;
250
+ g += `<g class="lg"><line x1="${L + 60}" x2="${L + 74}" y1="${eTop - 9}" y2="${eTop - 9}" stroke="#0f1528" stroke-width="1.8"/><text x="${L + 78}" y="${eTop - 6}">your equity</text>` +
251
+ `<line x1="${L + 146}" x2="${L + 160}" y1="${eTop - 9}" y2="${eTop - 9}" stroke="#8a93a3" stroke-dasharray="3 2" stroke-width="1.3"/><text x="${L + 164}" y="${eTop - 6}">Buy &amp; Hold</text></g>`;
252
+ // date axis
253
+ g += `<text class="ax" x="${X(0)}" y="${tY + 3.5}">${new Date(OH.dates[0] + 'T12:00:00Z').toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' })}</text>`;
254
+ g += `<g id="hv"></g>`;
255
+ svg.innerHTML = `<defs><pattern id="fut" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><rect width="6" height="6" fill="#fcfcfd"/><line x1="0" y1="0" x2="0" y2="6" stroke="rgba(74,38,130,.035)" stroke-width="2"/></pattern><linearGradient id="tlg" x1="0" x2="1"><stop offset="0" stop-color="#b40028"/><stop offset="1" stop-color="#7800f0"/></linearGradient><pattern id="miss" width="4" height="4" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><rect width="4" height="4" fill="#eceaf3"/><line x1="0" y1="0" x2="0" y2="4" stroke="#b9b5cf" stroke-width="1.5"/></pattern></defs>` + g;
256
+ G = { X, cw, L, R, W, nShown, pTop, bot: eTop + eH, Y, EY, ex };
257
+ head(hover != null && hover < nShown ? hover : nShown - 1);
258
+ }
259
+
260
+ // hover crosshair + readout (chart and log tiles)
261
+ function showDay(i, cx, cy) {
262
+ if (!G || i < 0 || i >= G.nShown) return leave();
263
+ hover = i; head(i);
264
+ const day = i - HIST, l = day >= 0 ? S.log[day] : null;
265
+ $('hv').innerHTML = `<line x1="${G.X(i)}" x2="${G.X(i)}" y1="${G.pTop}" y2="${G.bot}" stroke="#0f1528" stroke-opacity=".3" stroke-dasharray="3 3"/>` +
266
+ (l ? `<circle cx="${G.ex(day + 1)}" cy="${G.EY(S.eq[day + 1])}" r="3.5" fill="#7800f0" stroke="#fff" stroke-width="1.5"/>` : '');
267
+ document.querySelectorAll('.tile.on').forEach(t => t.classList.remove('on'));
268
+ if (day >= 0 && $('log').children[day]) $('log').children[day].classList.add('on');
269
+ const tip = $('tip');
270
+ if (l && cx != null) { tip.innerHTML = `${sysName(l.arch, l.model)} · run ${l.run} → <b>${ACT[l.a]}</b> · <span style="color:${l.pnl > 0 ? '#5fe0a0' : l.pnl < 0 ? '#ff8a9a' : '#ccc'}">${pct(l.pnl)}</span>`; tip.style.left = cx + 14 + 'px'; tip.style.top = cy - 34 + 'px'; tip.classList.add('on'); }
271
+ else tip.classList.remove('on');
272
+ }
273
+ $('kline').addEventListener('mousemove', e => {
274
+ if (!G) return;
275
+ const r = e.currentTarget.getBoundingClientRect();
276
+ showDay(Math.round((e.clientX - r.left - G.L) / G.cw - .5), e.clientX, e.clientY);
277
+ });
278
+ $('log').addEventListener('mouseover', e => { const t = e.target.closest('.tile'); if (t && S.log[+t.dataset.d]) showDay(HIST + +t.dataset.d); });
279
+ $('log').addEventListener('mouseleave', () => leave());
280
+ const leave = () => { hover = null; if ($('hv')) $('hv').innerHTML = ''; document.querySelectorAll('.tile.on').forEach(t => t.classList.remove('on')); $('tip').classList.remove('on'); if (G) head(G.nShown - 1); };
281
+ $('kline').addEventListener('mouseleave', leave);
282
+
283
+ // ---------- picks & buttons ----------
284
+ $('arch').addEventListener('click', e => { const b = e.target.closest('button'); if (b) { arch = b.dataset.a; renderSide(); } });
285
+ $('model').addEventListener('click', e => { const b = e.target.closest('button'); if (b) { model = b.dataset.m; renderSide(); } });
286
+ $('goBtn').addEventListener('click', () => { stopAuto(); step(); });
287
+ $('autoBtn').addEventListener('click', () => {
288
+ if (autoT) return stopAuto();
289
+ $('autoBtn').classList.add('on'); $('autoBtn').textContent = 'Stop ■'; step();
290
+ autoT = setInterval(step, 320);
291
+ });
292
+ $('newBtn').addEventListener('click', startCard);
293
+ document.addEventListener('keydown', e => {
294
+ if ($('ov').classList.contains('on')) return;
295
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); stopAuto(); step(); }
296
+ });
297
+ addEventListener('resize', () => S && draw());
298
+
299
+ // ---------- overlays ----------
300
+ const spark = tk => { // pre-window history only
301
+ const c = OH.bars[tk].slice(0, HIST).map(b => b[3]), lo = Math.min(...c), hi = Math.max(...c), up = c[c.length - 1] >= c[0];
302
+ const p = c.map((v, i) => `${(i / (c.length - 1) * 100).toFixed(1)},${(30 - (v - lo) / (hi - lo) * 26 - 2).toFixed(1)}`).join(' ');
303
+ return `<svg viewBox="0 0 100 32" preserveAspectRatio="none"><polyline points="${p}" fill="none" stroke="${up ? '#0f9d58' : '#d6304a'}" stroke-width="1.5" vector-effect="non-scaling-stroke"/></svg>`;
304
+ };
305
+ function open(html) { $('ovCard').innerHTML = html; $('ov').classList.add('on'); }
306
+ function close() { $('ov').classList.remove('on'); }
307
+
308
+ function startCard() {
309
+ stopAuto();
310
+ open(`<div class="kick"><span>HERCULEAN · Trading workflow</span></div>
311
+ <h2>Welcome, <em id="nm">${name}</em><button class="reroll" id="reroll" title="Another name">↻</button></h2>
312
+ <p>Pick a stock. For each of the 61 trading days (Jan 2 – Mar 31, 2026) you choose which agent system trades; one of its three recorded runs is drawn, and its real decision is executed. Can your picks beat Buy &amp; Hold?</p>
313
+ <div class="stocks">${H.tradeAssets.map(t => `<button class="stock${t === sym ? ' sel' : ''}" data-t="${t}"><b>${t}</b><small>${CO[t]}</small>${spark(t)}</button>`).join('')}</div>
314
+ <div class="rules"><span><i>30</i>agent systems</span><span><i>3</i>runs each</span><span><i>$100k</i>start</span><span><i>open→open</i>no costs</span></div>
315
+ <div class="ov-actions"><button class="go" id="startBtn">Start trading ▶</button>${S && S.d && S.d < N ? '<button class="ghost" id="resume">Resume</button>' : ''}<a class="to-lb" href="./index.html">See the leaderboard</a></div>`);
316
+ let tk = sym;
317
+ $('ovCard').querySelectorAll('.stock').forEach(b => b.onclick = () => { tk = b.dataset.t; $('ovCard').querySelectorAll('.stock').forEach(x => x.classList.toggle('sel', x === b)); });
318
+ $('reroll').onclick = () => { let n; do n = pickRand(NAMES); while (n === name); name = n; $('nm').textContent = n; };
319
+ $('startBtn').onclick = () => { close(); newGame(tk); };
320
+ if ($('resume')) $('resume').onclick = close;
321
+ }
322
+
323
+ function endCard() {
324
+ const m = metrics(S.log.map(l => l.pnl)), bh = metrics(S.ret), st = standing(N), rank = st.rank, top = st.top;
325
+ const fav = Object.entries(S.picks).sort((p, q) => q[1] - p[1])[0], [fa, fm] = fav[0].split('|');
326
+ const beat = m.cr > bh.cr, gap = (m.cr - top.cr) * 100;
327
+ const tb = rankTable(st, 1);
328
+ open(`<div class="kick"><span>${sym} · window closed · 61 days</span></div>
329
+ <h2>${beat ? `${name} <em>beat</em> Buy &amp; Hold` : `Buy &amp; Hold wins this time, <em>${name}</em>`}</h2>
330
+ <div class="res">
331
+ <div><small>Final equity</small><b>${money(1e5 * (1 + m.cr))}</b><em>B&amp;H ${money(1e5 * (1 + bh.cr))}</em></div>
332
+ <div><small>CR</small><b class="${cls(m.cr)}">${pct(m.cr)}</b><em>B&amp;H ${pct(bh.cr)}</em></div>
333
+ <div><small>SR</small><b>${m.sr.toFixed(2)}</b><em>B&amp;H ${bh.sr.toFixed(2)}</em></div>
334
+ <div><small>MDD</small><b>${(m.mdd * 100).toFixed(2)}%</b><em>B&amp;H ${(bh.mdd * 100).toFixed(2)}%</em></div>
335
+ </div>
336
+ <div class="rankline">
337
+ <div class="rk-top"><span><span class="big">#${rank}</span>of ${st.n} on ${sym} Trading</span>
338
+ <span>${rank === 1 ? 'ahead of #2 by <b>' + ((m.cr - st.all[0].cr) * 100).toFixed(2) + ' pp</b>' : `<b class="dn">${gap.toFixed(2)} pp</b> behind #1`}</span></div>
339
+ ${strip(st, 580, 96)}
340
+ <div class="tb"><div class="tr th"><span>Rank</span><span>Agent system · CR, mean of 3 runs</span><span>CR</span><span>vs #1</span></div>${tb}</div>
341
+ <div class="fav">Your most-picked: <b>${sysName(fa, fm)}</b> (${fav[1]} of 61 days)</div>
342
+ </div>
343
+ <div class="ov-actions"><button class="go" id="again">Play again ▶</button><button class="ghost" id="review">Review chart</button><a class="to-lb" href="./index.html">See the leaderboard</a></div>`);
344
+ $('again').onclick = () => { name = pickRand(NAMES.filter(n => n !== name)); startCard(); };
345
+ $('review').onclick = close;
346
+ }
347
+
348
+ newGame(sym);
349
+ startCard();
350
+ window.__play = { step, state: () => S, set: (a, m) => { arch = a; model = m; renderSide(); }, endCard, close }; // for tests
351
+ })();
style.css CHANGED
@@ -1,28 +1,756 @@
1
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
 
 
 
 
 
 
 
 
 
4
  }
 
 
 
 
 
 
 
 
 
5
 
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
9
- }
 
 
 
 
 
 
 
 
10
 
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
- .card p:last-child {
27
- margin-bottom: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  }
 
1
+ /* HERCULEAN leaderboard
2
+ Fin AI house tokens; the blue accent is replaced by the HERCULEAN red→purple gradient. */
3
+ :root{
4
+ --ink:#0f1528; --soft:#4f5a6e; --muted:#8a93a3; --line:rgba(74,38,130,.085);
5
+ --paper:#fcfcfd; --paper-2:#f4f6fa;
6
+ --card-ring:0 0 0 1px rgba(74,38,130,.07),0 1px 2px rgba(40,20,80,.04),0 10px 28px -18px rgba(60,30,120,.18);
7
+ --g:linear-gradient(90deg,#b40028,#7800f0);
8
+ --g-soft:linear-gradient(90deg,rgba(180,0,40,.08),rgba(120,0,240,.09));
9
+ --accent-ink:#4a1a8c;
10
+ --up:#16a34a;
11
+ --serif:'Newsreader',Georgia,serif; --brand:'Cinzel','Newsreader',Georgia,serif; --sans:'Inter',system-ui,-apple-system,sans-serif; --mono:'JetBrains Mono',ui-monospace,monospace;
12
+ --wrap:1180px; --ease:cubic-bezier(.22,1,.36,1); --spring:cubic-bezier(.34,1.56,.64,1);
13
  }
14
+ *{box-sizing:border-box}
15
+ html{scroll-behavior:smooth;scroll-padding-top:70px}
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;overflow-x:hidden}
17
+ h1,h2,h3{font-family:var(--serif);font-weight:500;line-height:1.1;letter-spacing:-.02em;margin:0}
18
+ p{margin:0;color:var(--soft)}
19
+ a{color:inherit;text-decoration:none}
20
+ img{display:block}
21
+ .wrap{width:100%;max-width:var(--wrap);margin:0 auto;padding:0 32px}
22
+ ::selection{background:#5a1aa8;color:#fff}
23
 
24
+ /* ---------- page frame: soft colour glows + vertical edge marks ---------- */
25
+ .glow{position:fixed;inset:0;z-index:-1;pointer-events:none;background:radial-gradient(66vw 80vh at 20% 15%,rgba(180,0,40,.070),transparent 72%),radial-gradient(70vw 84vh at 67% 90%,rgba(120,0,240,.080),transparent 72%)}
26
+ .edge{position:fixed;top:50%;z-index:45;pointer-events:none;display:flex;align-items:center;gap:18px;white-space:nowrap;text-transform:uppercase}
27
+ .edge-l{left:24px;transform:translate(-50%,-50%) rotate(-90deg)}
28
+ .edge-r{right:24px;transform:translate(50%,-50%) rotate(90deg)}
29
+ .edge .e-name{font:600 11px var(--sans);letter-spacing:.42em;background:var(--g);-webkit-background-clip:text;background-clip:text;color:transparent}
30
+ .edge::before,.edge::after{content:"";display:block;width:8vh;height:1px;background:linear-gradient(90deg,transparent,rgba(120,0,240,.22))}
31
+ .edge::after{background:linear-gradient(90deg,rgba(120,0,240,.22),transparent)}
32
+ .edge-r .e-name{background-image:linear-gradient(90deg,#183898,#0060b0 55%,#68a8d8)}
33
+ .edge-r::before{background:linear-gradient(90deg,transparent,rgba(0,96,176,.28))}
34
+ .edge-r::after{background:linear-gradient(90deg,rgba(0,96,176,.28),transparent)}
35
+ @media (max-width:1320px){.edge{display:none}}
36
 
37
+ /* ---------- scroll progress + header ---------- */
38
+ .progress{position:fixed;inset:0 0 auto 0;height:2px;z-index:60;transform-origin:0 50%;transform:scaleX(0);background:var(--g)}
39
+ .header{position:fixed;top:0;left:0;right:0;z-index:50;transition:background .4s var(--ease),box-shadow .4s}
40
+ .header.scrolled{background:rgba(252,252,253,.8);backdrop-filter:blur(14px) saturate(1.4);-webkit-backdrop-filter:blur(14px) saturate(1.4);box-shadow:0 1px 0 var(--line)}
41
+ .nav{display:flex;align-items:center;justify-content:space-between;height:64px}
42
+ .brand{display:inline-flex;align-items:center;gap:10px;font-family:var(--serif);font-size:1.2rem;font-weight:500;white-space:nowrap}
43
+ .brand img{height:24px;width:auto;transition:transform .6s var(--spring)}
44
+ .brand:hover img{transform:rotate(-12deg) scale(1.08)}
45
+ .brand.sm{font-size:1rem}.brand.sm img{height:20px}
46
+ .brand-sep{color:var(--line);font-weight:400}
47
+ .brand-herc{font-family:var(--brand);font-size:.84rem;font-weight:700;letter-spacing:.1em;background:var(--g);-webkit-background-clip:text;background-clip:text;color:transparent}
48
+ .nav-right{display:flex;align-items:center;gap:10px}
49
+ .links{display:flex;gap:2px}
50
+ .links a{font-size:.88rem;color:var(--soft);padding:7px 13px;border-radius:999px;transition:background .25s,color .25s}
51
+ .links a:hover{background:var(--paper-2);color:var(--ink)}
52
+ .links a.active{background:var(--g-soft);color:var(--ink)}
53
+
54
+ /* ---------- buttons ---------- */
55
+ .btn{position:relative;display:inline-flex;align-items:center;gap:10px;font-size:.9rem;font-weight:500;padding:10px 20px;border-radius:999px;background:var(--ink);color:var(--paper);overflow:hidden;isolation:isolate;border:0;cursor:pointer;font-family:inherit;transition:transform .35s var(--spring),box-shadow .35s}
56
+ .btn{--mx:50%;--my:50%}
57
+ .btn::before{content:"";position:absolute;inset:0;z-index:-1;background:radial-gradient(130px circle at var(--mx) var(--my),rgba(180,0,40,.75),rgba(120,0,240,.6) 45%,transparent 75%);opacity:0;transition:opacity .3s}
58
+ .btn:hover::before{opacity:1}
59
+ .btn.magnetic{transition:transform .25s var(--ease),box-shadow .35s}
60
+ .btn:hover{box-shadow:0 10px 30px -12px rgba(120,0,240,.55)}
61
+ .btn:active{transform:scale(.96)}
62
+ .btn .arrow{display:inline-block;transition:transform .35s var(--spring)}
63
+ .btn:hover .arrow{transform:translateX(4px)}
64
+ .btn--lg{padding:14px 26px;font-size:1rem}
65
+ .btn--sm{padding:7px 15px;font-size:.84rem;gap:7px}
66
+ .btn--ghost{background:rgba(255,255,255,.7);color:var(--ink);box-shadow:inset 0 0 0 1px var(--line)}
67
+ .btn--ghost::before{background:radial-gradient(110px circle at var(--mx) var(--my),rgba(120,0,240,.12),transparent 70%)}
68
+ .btn--ghost:hover{box-shadow:inset 0 0 0 1px var(--ink)}
69
+
70
+ /* ---------- hero ---------- */
71
+ .hero{position:relative;background:var(--paper);min-height:clamp(700px,100svh,940px);display:flex;align-items:center;padding:96px 0 92px;overflow:hidden}
72
+ .flow{position:absolute;inset:0;width:100%;height:100%;z-index:0}
73
+ .flow .node{animation:ping2 3.2s var(--ease) infinite}
74
+ @keyframes ping2{0%{r:2.4;opacity:.9}100%{r:9;opacity:0}}
75
+ .hero::after{content:"";position:absolute;inset:0;z-index:0;pointer-events:none;background:radial-gradient(ellipse 55% 70% at 24% 50%,var(--paper) 35%,transparent 78%),linear-gradient(to bottom,transparent 72%,var(--paper))}
76
+ .hero-grid>*{min-width:0}
77
+ .hero-grid{position:relative;z-index:1;display:grid;grid-template-columns:1.2fr 1fr;gap:56px;align-items:center;width:100%}
78
+ .award{display:inline-flex;align-items:center;gap:10px;padding:5px 14px 5px 5px;border-radius:999px;background:#fff;box-shadow:0 0 0 1px var(--line),0 6px 20px -12px rgba(15,21,40,.3);font-size:.8rem;font-weight:500;color:var(--soft);position:relative;overflow:hidden;transition:box-shadow .3s,transform .4s var(--spring)}
79
+ .award:hover{transform:translateY(-2px);box-shadow:0 0 0 1px rgba(120,0,240,.35),0 12px 28px -14px rgba(120,0,240,.45)}
80
+ .award::after{content:"";position:absolute;inset:0;background:linear-gradient(105deg,transparent 35%,rgba(255,255,255,.9) 50%,transparent 65%);transform:translateX(-100%);animation:shine 4.5s 1.8s infinite}
81
+ @keyframes shine{0%,70%{transform:translateX(-100%)}100%{transform:translateX(100%)}}
82
+ .award .medal{display:grid;place-items:center;width:30px;height:30px;border-radius:50%;background:var(--g);color:#fff}
83
+ .award .medal svg{width:26px;height:26px}
84
+ .award b{color:var(--accent-ink);font-weight:600}
85
+ .wordmark{font-family:var(--brand);font-weight:600;font-size:clamp(2.6rem,6.2vw,5.2rem);letter-spacing:.01em;line-height:1;margin:24px 0 16px;padding-bottom:.04em;background:var(--g);-webkit-background-clip:text;background-clip:text;color:transparent;width:max-content;max-width:100%}
86
+ .hero-sub{font-family:var(--serif);font-size:clamp(1.4rem,2.3vw,1.95rem);line-height:1.25;color:var(--ink);max-width:24ch}
87
+ .hero-sub em,.sec-h2 em,.claim em{font-style:italic;background:var(--g);-webkit-background-clip:text;background-clip:text;color:transparent;padding-right:.06em}
88
+ .hero .lead{font-size:1.06rem;max-width:46ch;margin:16px 0 0}
89
+ /* stats */
90
+ .stats{display:flex;gap:34px;margin:28px 0 30px;flex-wrap:wrap}
91
+ .stats div{display:flex;flex-direction:column}
92
+ .stats b{font-family:var(--serif);font-weight:500;font-size:2.6rem;line-height:1;letter-spacing:-.02em;font-variant-numeric:tabular-nums}
93
+ .stats span{font-size:.8rem;color:var(--muted);margin-top:7px}
94
+ .hero-ctas{display:flex;gap:12px;flex-wrap:wrap}
95
+ /* rotating architecture name (thefin.ai "rotator"): serif italic like the subtitle, underline drawn in and out */
96
+ .rotator{--uc:#7800f0;--rot:1.6s;display:inline-block;position:relative;height:1.35em;vertical-align:bottom;overflow:hidden;font-family:var(--serif);font-style:italic;font-size:1.2em;line-height:1.35;transition:width .45s var(--ease)}
97
+ .rotator span{position:absolute;left:0;top:0;color:var(--c);white-space:nowrap;opacity:0;transform:translateY(60%);filter:blur(6px);transition:opacity .45s var(--ease),transform .55s var(--ease),filter .45s}
98
+ .rotator span.on{opacity:1;transform:none;filter:none}
99
+ .rotator span.out{opacity:0;transform:translateY(-60%);filter:blur(6px)}
100
+ .rotator::after{content:"";position:absolute;left:0;right:0;bottom:1px;height:2px;border-radius:2px;background:var(--uc);transform-origin:0 50%;animation:under var(--rot) var(--ease) infinite;transition:background .3s}
101
+ @keyframes under{0%{transform:scaleX(0)}25%,80%{transform:scaleX(1);transform-origin:0 50%}81%{transform-origin:100% 50%}100%{transform:scaleX(0);transform-origin:100% 50%}}
102
+ /* ticker at the bottom of the hero (thefin.ai marquee) */
103
+ .ticker{position:absolute;left:0;right:0;bottom:14px;z-index:2;height:36px;display:flex;align-items:center;overflow:hidden;border-top:1px solid var(--line);border-bottom:1px solid var(--line);background:rgba(252,252,253,.72);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);-webkit-mask:linear-gradient(90deg,transparent,#000 6%,#000 94%,transparent);mask:linear-gradient(90deg,transparent,#000 6%,#000 94%,transparent)}
104
+ .ticker-track{display:flex;width:max-content;animation:marquee var(--dur,140s) linear infinite}
105
+ .ticker:hover .ticker-track{animation-play-state:paused}
106
+ @keyframes marquee{to{transform:translateX(-50%)}}
107
+ .tk{display:inline-flex;align-items:center;gap:8px;padding:0 18px;font-size:.8rem;line-height:1;white-space:nowrap;cursor:pointer;border-right:1px solid var(--line)}
108
+ .tk .p{font-family:var(--mono);font-size:.7rem;color:var(--muted)}
109
+ .tk .n{color:var(--soft)}
110
+ .tk .n em{font-style:normal;color:var(--muted)}
111
+ .tk .v{font-weight:600;color:var(--ink);font-variant-numeric:tabular-nums}
112
+ .tk .ini.sm{width:18px;height:18px;font-size:.46rem}
113
+ .tk:hover .n{color:var(--ink)}
114
+ .tk.first .v{color:#7a1fb4}
115
+ .reveal-up{opacity:0;transform:translateY(22px);transition:opacity .9s var(--ease),transform 1s var(--ease)}
116
+ .loaded .reveal-up{opacity:1;transform:none}
117
+
118
+ /* "Top 8 · Overall score" card (light theme) */
119
+ .hero-viz{position:relative;margin:0;height:600px;border-radius:24px;background:linear-gradient(160deg,rgba(244,246,250,.92),rgba(255,255,255,.86));backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);box-shadow:0 0 0 1px var(--line),0 30px 60px -32px rgba(15,21,40,.32);padding:20px 22px 16px;display:flex;flex-direction:column}
120
+ .hero-viz.tilt{transform-style:preserve-3d;transition:transform .6s var(--ease),box-shadow .4s}
121
+ .hero-viz.tilt::after{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;background:radial-gradient(500px circle at var(--gx,50%) var(--gy,0%),rgba(255,255,255,.55),transparent 45%);opacity:0;transition:opacity .4s}
122
+ .hero-viz.tilt:hover::after{opacity:1}
123
+ .hero-viz.tilt:hover{box-shadow:0 0 0 1px rgba(120,0,240,.22),0 34px 60px -30px rgba(120,0,240,.38)}
124
+ .hero-viz figcaption{white-space:nowrap;gap:12px;display:flex;justify-content:space-between;align-items:center;font-size:.7rem;font-weight:600;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
125
+ .hero-viz figcaption a{letter-spacing:.02em;text-transform:none;font-size:.8rem;font-weight:500;color:#6a1fc0;transition:transform .35s var(--spring)}
126
+ .hero-viz figcaption a:hover{transform:translateX(3px)}
127
+ .podium{position:relative;flex:1;display:grid;grid-template-columns:repeat(8,1fr);gap:10px;align-items:end;padding:54px 0 0;border-bottom:1px solid var(--line);margin-bottom:72px}
128
+ .col{position:relative;height:100%;display:flex;flex-direction:column;justify-content:flex-end;align-items:center;cursor:pointer}
129
+ .col .bar{position:relative;width:100%;max-width:44px;height:0;border-radius:10px 10px 3px 3px;background:linear-gradient(180deg,var(--c),color-mix(in srgb,var(--c) 55%,#fff));transition:height 1.1s var(--ease) var(--d),transform .35s var(--spring),filter .25s;transform-origin:50% 100%}
130
+ .podium.in .col .bar{height:var(--h)}
131
+ .col.first .bar{box-shadow:0 12px 24px -14px #7800f0}
132
+ .col .val{position:absolute;left:50%;top:8px;transform:translateX(-50%);font-size:.7rem;font-weight:600;color:var(--fg,#0f1528);font-variant-numeric:tabular-nums;opacity:0;transition:opacity .4s calc(var(--d) + .8s)}
133
+ .col .rk-badge{position:absolute;left:50%;top:-36px;margin-left:-13px;display:grid;place-items:center;width:26px;height:26px;border-radius:50%;font:700 .74rem/1 var(--sans);color:var(--fg,#0f1528);background:var(--c);opacity:0;transform:translateY(8px) scale(.6);box-shadow:0 0 0 2px #fff,0 4px 10px -5px rgba(15,21,40,.4);transition:opacity .5s var(--ease) calc(var(--d) + .6s),transform .6s var(--spring) calc(var(--d) + .6s)}
134
+ .podium.in .col .val,.podium.in .col .rk-badge{opacity:1}
135
+ .podium.in .col .rk-badge{transform:none}
136
+ .col .nm{position:absolute;top:100%;right:50%;margin-top:10px;white-space:nowrap;font-size:.7rem;line-height:1.25;color:var(--soft);transform-origin:right center;transform:rotate(-38deg);text-align:right}
137
+ .col .nm small{display:block;font-size:.62rem;color:var(--muted)}
138
+ .col:hover .bar{transform:scaleX(1.08);filter:brightness(1.06)}
139
+ .col:hover .rk-badge{transform:translateY(-4px) scale(1.12);transition-delay:0s}
140
+ /* top three: flat gold / silver / bronze badges that glow on staggered, slightly different cycles */
141
+ .col.podium-1{--m:#f2b81c;--mg:rgba(242,184,28,.7);--gd:2.6s;--gw:0s}
142
+ .col.podium-2{--m:#b6bfce;--mg:rgba(150,164,190,.75);--gd:3.1s;--gw:1.1s}
143
+ .col.podium-3{--m:#d08a4e;--mg:rgba(208,138,78,.7);--gd:2.8s;--gw:2s}
144
+ .col[class*="podium-"] .rk-badge{background:var(--m);color:#2b1d05;box-shadow:0 0 0 2px #fff,0 0 10px 2px var(--mg);animation:medalGlow var(--gd) ease-in-out var(--gw) infinite}
145
+ @keyframes medalGlow{0%,100%{box-shadow:0 0 0 2px #fff,0 0 6px 1px var(--mg)}50%{box-shadow:0 0 0 2px #fff,0 0 18px 6px var(--mg)}}
146
+ .col[class*="podium-"] .nm{color:var(--ink);font-weight:600}
147
+ .col[class*="podium-"] .nm small{color:var(--soft);font-weight:500}
148
+ .col:hover .nm{color:var(--ink)}
149
+ .hero-claim{margin-top:10px;padding-top:9px;border-top:1px solid var(--line);font-size:.8rem;color:var(--ink);line-height:1.3;white-space:nowrap}
150
+ .hero-claim em{font-style:normal}
151
+ .hero-legend{display:flex;flex-wrap:wrap;gap:4px 14px;font-size:.72rem;color:var(--soft)}
152
+ .hero-leaders{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:10px;padding-top:10px;border-top:1px solid var(--line)}
153
+ .hl-cap{font-size:.62rem;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);margin:0 0 4px;display:flex;justify-content:space-between}
154
+ .hl-cap span{letter-spacing:.02em;text-transform:none;font-weight:500}
155
+ .gr{display:grid;grid-template-columns:12px 16px 76px 1fr 30px;align-items:center;gap:6px;height:19px;font-size:.72rem;cursor:pointer;border-radius:5px;transition:background .2s}
156
+ .gr:hover{background:rgba(120,0,240,.06)}
157
+ .gr-p{font-family:var(--mono);font-size:.62rem;color:var(--muted)}
158
+ .gr .ini.sm{width:16px;height:16px;font-size:.42rem}
159
+ .gr-n{color:var(--ink);white-space:nowrap}
160
+ .gr-t{height:5px;border-radius:3px;background:var(--paper-2);overflow:hidden}
161
+ .gr-t i{display:block;height:100%;border-radius:3px;background:#8e6ad0}
162
+ .gr b{font-weight:600;font-size:.72rem;text-align:right;font-variant-numeric:tabular-nums}
163
+ .gr-col:last-child .gr{grid-template-columns:12px 66px 1fr 30px}
164
+ .hero-legend span{display:inline-flex;align-items:center;gap:6px}
165
+ .hero-legend i{width:8px;height:8px;border-radius:50%}
166
+ .wdot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--c);flex:none}
167
+
168
+ /* ---------- section scaffolding ---------- */
169
+ .section{padding:84px 0}
170
+ .section--tint{background:none}
171
+ .kicker{display:inline-flex;align-items:center;gap:10px;font-size:.72rem;font-weight:600;letter-spacing:.18em;text-transform:uppercase;color:#7a1fb4}
172
+ .kicker::before{content:"";width:22px;height:1px;background:var(--g)}
173
+ .sec-h2{font-size:clamp(2.2rem,4.2vw,3.2rem);margin:14px 0 0;max-width:24ch}
174
+ .sec-head{display:flex;justify-content:space-between;align-items:end;gap:30px;margin-bottom:40px}
175
+ .sec-head>p{max-width:42ch;font-size:1rem}
176
+ .sec-sub{margin-top:14px;font-size:1rem;color:var(--soft)}
177
+ .sec-sub b{color:var(--ink);font-weight:600}
178
+ /* initials badge used in the hero card is defined with the leaderboard below */
179
+ [data-reveal]{opacity:0;transform:translateY(30px);transition:opacity .9s var(--ease),transform 1s var(--ease);transition-delay:calc(var(--i,0) * 80ms)}
180
+ [data-reveal].in{opacity:1;transform:none}
181
+
182
+ /* ---------- 1 · workflow cards ---------- */
183
+ .wf-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}
184
+ .wf{position:relative;display:flex;flex-direction:column;padding:24px 22px 20px;border-radius:20px;background:#fff;box-shadow:var(--card-ring);overflow:hidden;isolation:isolate;transition:transform .5s var(--spring),box-shadow .4s}
185
+ .wf-bar{position:absolute;left:0;right:0;top:0;height:3px;background:var(--c);transform:scaleX(.18);transform-origin:0 50%;transition:transform .6s var(--ease)}
186
+ .wf:hover{transform:translateY(-6px);box-shadow:0 0 0 1px color-mix(in srgb,var(--c) 30%,transparent),0 24px 40px -26px color-mix(in srgb,var(--c) 70%,transparent)}
187
+ .wf:hover .wf-bar{transform:none}
188
+ /* spotlight (thefin.ai "spot"): a soft glow and a lit border that follow the pointer, in the card's own colour */
189
+ .spot{--x:50%;--y:50%;--sc:#7800f0}
190
+ .spot::before{content:"";position:absolute;inset:0;z-index:-1;border-radius:inherit;background:radial-gradient(420px circle at var(--x) var(--y),color-mix(in srgb,var(--sc) 15%,transparent),transparent 45%);opacity:0;transition:opacity .4s;pointer-events:none}
191
+ .spot::after{content:"";position:absolute;inset:0;border-radius:inherit;padding:1px;background:radial-gradient(260px circle at var(--x) var(--y),var(--sc),transparent 60%);-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;opacity:0;transition:opacity .4s;pointer-events:none}
192
+ .spot:hover::before,.spot:hover::after{opacity:1}
193
+ .wf.spot{--sc:var(--c)}
194
+ .wf-top{display:flex;align-items:center;justify-content:space-between}
195
+ .wf-top .num{font-family:var(--mono);font-size:.8rem;color:var(--muted)}
196
+ .wf-top .lbl{display:inline-flex;align-items:center;gap:7px;font-size:.7rem;font-weight:600;letter-spacing:.12em;text-transform:uppercase;color:var(--c)}
197
+ .wf h3{font-size:1.65rem;margin:18px 0 6px}
198
+ .wf .purpose{font-size:.8rem;line-height:1.45;color:var(--ink);opacity:.78;margin-bottom:18px;min-height:2.9em}
199
+ /* the three parts of a task: icon + label + one short line */
200
+ .recipe{list-style:none;margin:0;padding:0;display:grid;gap:12px}
201
+ .recipe li{display:grid;grid-template-columns:30px 1fr;gap:10px;align-items:start}
202
+ .recipe .ico{width:30px;height:30px;border-radius:9px;display:grid;place-items:center;background:var(--g-soft);color:#5a1aa8;transition:transform .6s var(--spring)}
203
+ /* hover (thefin.ai pillars): the same icon lights up in the workflow colour and turns slightly */
204
+ .recipe .ico{transition:transform .6s var(--spring),background .3s,color .3s,box-shadow .3s}
205
+ .wf:hover .recipe .ico{transform:rotate(-8deg) scale(1.08);background:var(--c);color:#fff;box-shadow:0 6px 14px -6px var(--c)}
206
+ .recipe .ico svg{width:16px;height:16px}
207
+ .recipe small{display:block;font-size:.6rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--soft);line-height:1.3;margin-bottom:4px;text-decoration:underline;text-decoration-thickness:1.5px;text-underline-offset:4px;text-decoration-color:color-mix(in srgb,var(--c) 55%,transparent)}
208
+ .recipe p{font-size:.78rem;line-height:1.45;color:var(--soft)}
209
+ .chips{display:flex;flex-wrap:wrap;gap:4px;margin-top:3px}
210
+ .chips span{font-family:var(--mono);font-size:.68rem;padding:2px 7px;border-radius:999px;background:var(--paper-2);color:var(--soft)}
211
+ .wf-best{margin-top:auto;padding-top:16px;border-top:1px solid var(--line);display:flex;justify-content:space-between;align-items:center;gap:10px}
212
+ .wf .recipe{margin-bottom:20px}
213
+ .wf-best small{display:block;font-size:.64rem;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);margin-bottom:3px;white-space:nowrap}
214
+ .wf-best .who{min-height:2.6em;font-size:.86rem;font-weight:500;line-height:1.3}
215
+ .wf-best .who span{display:block;white-space:nowrap}
216
+ .wf-best .who em{font-style:normal;color:var(--muted);font-weight:400}
217
+ .wf-best b{font-family:var(--serif);font-weight:500;font-size:1.85rem;line-height:1;font-variant-numeric:tabular-nums}
218
+
219
+ /* ---------- 2 · leaderboard ---------- */
220
+ .lb-head{align-items:end;margin-bottom:26px}
221
+ .sub-h{margin-top:12px;font-family:var(--serif);font-size:1.45rem;line-height:1.2;color:var(--ink)}
222
+ .sub-h em{font-style:italic;background:var(--g);-webkit-background-clip:text;background-clip:text;color:transparent;padding-right:.06em}
223
+ .note{margin-top:10px;font-size:.78rem;color:var(--muted);max-width:70ch}
224
+ .recipe-note b{color:var(--ink);font-weight:600}
225
+ .sec-h2.one-line{max-width:none;white-space:nowrap}
226
+ .lb-claim{max-width:340px;padding-left:24px;border-left:1px solid var(--line)}
227
+ .claim-k{font-size:.66rem;font-weight:600;letter-spacing:.16em;text-transform:uppercase;color:var(--muted)}
228
+ .claim{font-family:var(--serif);font-size:1.7rem;line-height:1.15;color:var(--ink);margin-top:6px;letter-spacing:-.01em}
229
+ .claim-sub{font-size:.88rem;margin-top:10px;line-height:1.5}
230
+ .claim-sub b{color:var(--ink);font-weight:600}
231
+ .controls{display:flex;flex-wrap:wrap;gap:12px 18px;align-items:center;margin-bottom:16px}
232
+ .seg{position:relative;display:inline-flex;gap:2px;padding:4px;border-radius:999px;background:#fff;box-shadow:inset 0 0 0 1px var(--line)}
233
+ .seg button{position:relative;z-index:1;display:inline-flex;align-items:center;gap:7px;border:0;background:none;font:inherit;font-size:.84rem;font-weight:500;color:var(--soft);padding:7px 14px;border-radius:999px;cursor:pointer;white-space:nowrap;transition:color .3s}
234
+ .seg button[aria-selected=true]{color:var(--paper)}
235
+ .seg-pill{position:absolute;top:4px;bottom:4px;left:0;border-radius:999px;background:var(--ink);transition:transform .45s var(--ease),width .45s var(--ease)}
236
+ .arch-chips{display:flex;flex-wrap:wrap;gap:6px}
237
+ .arch-chips button{display:inline-flex;align-items:center;gap:7px;border:0;background:#fff;box-shadow:0 0 0 1px var(--line);color:var(--ink);font:500 .8rem var(--sans);padding:5px 11px 5px 5px;border-radius:999px;cursor:pointer;transition:opacity .25s,box-shadow .25s,transform .3s var(--spring)}
238
+ .arch-chips button:hover{box-shadow:0 0 0 1px rgba(120,0,240,.4)}
239
+ .arch-chips button:active{transform:scale(.95)}
240
+ .arch-chips button.off{opacity:.42}
241
+ .arch-chips button.off .ini{background:var(--muted)!important;color:#fff}
242
+ .model-chips{padding-left:14px;border-left:1px solid var(--line)}
243
+ .model-chips button{padding:5px 11px}
244
+
245
+ /* architecture initials badge */
246
+ .ini{display:inline-grid;place-items:center;flex:none;width:26px;height:26px;border-radius:50%;font:600 .58rem/1 var(--mono);letter-spacing:.02em;color:var(--fg,#0f1528);background:var(--c);transition:transform .35s var(--spring)}
247
+ .ini.sm{width:20px;height:20px;font-size:.5rem}
248
+
249
+ .board{background:#fff;border-radius:20px;box-shadow:var(--card-ring);padding:6px 10px 10px}
250
+ /* columns: rank | system | score bar | 4 workflow cells | chevron */
251
+ .lb-cols,.lb-row .in{display:grid;grid-template-columns:30px var(--namew,250px) minmax(160px,1fr) 248px 18px;gap:12px;align-items:center}
252
+ .lb-cols{padding:10px 10px 8px;border-bottom:1px solid var(--line);font-size:.64rem;font-weight:600;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
253
+ .lb-cols .c-pos{text-align:right}
254
+ .c-cells{display:grid;grid-template-columns:repeat(4,1fr);gap:4px;text-align:center}
255
+ .sort-h{position:relative;display:inline-flex;align-items:center;justify-content:center;gap:4px;min-width:0;border:0;background:none;padding:3px 0;border-radius:6px;font:inherit;letter-spacing:inherit;text-transform:inherit;color:var(--muted);cursor:pointer;transition:color .25s,background .25s}
256
+ #colScore{justify-content:flex-start}
257
+ .sort-h:hover{color:var(--ink);background:var(--paper-2)}
258
+ .sort-h.on{color:var(--ink)}
259
+ .sort-h .arr{position:absolute;right:-9px;font-style:normal;opacity:0;transition:opacity .25s}
260
+ .c-cells .sort-h{letter-spacing:.02em;font-size:.6rem;gap:3px}
261
+ .c-cells .sort-h .arr{display:none}
262
+ .c-cells .sort-h.on{box-shadow:inset 0 -2px 0 var(--ink);border-radius:0}
263
+ .sort-h.on .arr{opacity:1}
264
+ .sort-h:hover .arr{opacity:.5}
265
+ .lb-rows{position:relative;transition:height .55s var(--ease)}
266
+ .lb-row{position:absolute;left:0;right:0;top:0;height:40px;transition:transform .7s var(--ease),opacity .4s}
267
+ .lb-row .in{height:100%;padding:0 10px;border-radius:12px;cursor:pointer;transition:background .2s}
268
+ .lb-row .in:hover{background:#faf8fe}
269
+ .lb-row.open .in{background:#f6f2fd}
270
+ .lb-row.gone{opacity:0;pointer-events:none}
271
+ .lb-pos{font-family:var(--mono);font-size:.76rem;color:var(--muted);text-align:right}
272
+ .lb-row.top .lb-pos{background:var(--g);-webkit-background-clip:text;background-clip:text;color:transparent;font-weight:600}
273
+ .lb-name{display:flex;align-items:center;gap:10px;min-width:0;font-size:.9rem;white-space:nowrap}
274
+ .lb-name b{font-weight:500}
275
+ .lb-name>span:not(.ini){color:var(--muted);overflow:hidden;text-overflow:ellipsis}
276
+ .lb-row .in:hover .ini{transform:rotate(-10deg) scale(1.1)}
277
+ .lb-score{display:flex;align-items:center;gap:12px}
278
+ .lb-track{flex:1;height:10px;border-radius:999px;background:var(--paper-2);overflow:hidden}
279
+ .lb-bar{display:block;height:100%;width:0;border-radius:999px;background:var(--c);transition:width 1s var(--ease)}
280
+ .lb-val{font-weight:600;font-size:.92rem;width:38px;text-align:right;font-variant-numeric:tabular-nums}
281
+ .cells{display:grid;grid-template-columns:repeat(4,1fr);gap:4px}
282
+ .cell{display:block;text-align:center;padding:4px 0;border-radius:6px;font-family:var(--mono);font-size:.74rem;line-height:1.3;transition:box-shadow .3s,opacity .3s}
283
+ .cell.best{box-shadow:0 0 0 2px var(--ink)}
284
+ .cells.dim .cell:not(.on){opacity:.62}
285
+ .chev{color:var(--muted);font-size:.8rem;transition:transform .35s var(--ease)}
286
+ .lb-row.open .chev{transform:rotate(90deg)}
287
+ /* show more / fewer rows */
288
+ .lb-more{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;margin-top:6px;padding:11px 0 6px;border:0;border-top:1px solid var(--line);background:none;font:500 .84rem var(--sans);color:var(--soft);cursor:pointer;transition:color .25s}
289
+ .lb-more:hover{color:var(--ink)}
290
+ .lb-more[hidden]{display:none}
291
+ .lb-more .chev2{display:inline-grid;place-items:center;width:22px;height:22px;border-radius:50%;box-shadow:inset 0 0 0 1px var(--line);font-size:.7rem;transition:transform .45s var(--ease),background .3s,color .3s}
292
+ .lb-more:hover .chev2{background:var(--ink);color:#fff;box-shadow:none}
293
+ .lb-more[aria-expanded=true] .chev2{transform:rotate(180deg)}
294
+ /* expanded breakdown, positioned under its row */
295
+ .lb-detail{position:absolute;left:0;right:0;top:0;opacity:0;pointer-events:none;transition:transform .7s var(--ease),opacity .35s}
296
+ .lb-detail.show{opacity:1;pointer-events:auto;transition:transform .7s var(--ease),opacity .45s .15s}
297
+ .sd{margin-left:4px;font-family:var(--mono);font-size:.86em;color:var(--muted);font-weight:400}
298
+ /* expanded row: summary line + four cards */
299
+ .d1{padding:8px 10px 14px 46px}
300
+ .d1-head{font-size:.76rem;color:var(--muted);margin:2px 0 12px}
301
+ .d1-head b{color:var(--soft);font-weight:600}
302
+ .d1-cards{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}
303
+ .d1-card{background:#fff;border-radius:12px;box-shadow:var(--card-ring);padding:12px 13px;min-width:0}
304
+ .d1-card h4{margin:0 0 2px;font-size:.77rem;font-weight:600;display:flex;align-items:flex-start;gap:5px;white-space:nowrap}
305
+ .d1-card h4>.wdot{margin-top:5px}
306
+ .d1-card h4>.d1-tag{margin-top:1px}
307
+ .d1-card h4 .ws{margin-left:auto;display:inline-flex;align-items:baseline;gap:5px;white-space:nowrap}
308
+ .d1-card h4 .ws b{font-weight:600;font-size:.82rem}
309
+ .d1-card h4 .ws .rk{font-size:.62rem;font-weight:500;color:var(--muted)}
310
+ .d1-tag{font-size:.52rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;padding:2px 5px;border-radius:999px;cursor:help}
311
+ .d1-tag.best{color:#0f7a4a;background:#e7f6ec}
312
+ .d1-tag.floor{color:#b4233a;background:#fdecee}
313
+ .d1-beat{margin-top:6px;font-size:.7rem;color:var(--soft)}
314
+ .d1-beat b{color:var(--ink);font-weight:600}
315
+ .d1-tag.live{color:#1d5fbf;background:#e8f0fc}
316
+ .d1-foot{margin-top:8px;font-size:.66rem;color:var(--muted);text-align:right}
317
+ .d1 .ab{font-style:normal;font-family:var(--mono);font-size:.62rem;color:var(--muted);margin-left:5px}
318
+ .d1-card .sub{font-size:.7rem;color:var(--muted);margin:0 0 10px}
319
+ .d1-big{font-size:1.4rem;font-weight:700;letter-spacing:-.01em;line-height:1.1;margin-bottom:6px}
320
+ .d1-big small{font-size:.72rem;font-weight:500;color:var(--muted);margin-left:4px}
321
+ .d1-kv{display:flex;justify-content:space-between;gap:10px;font-size:.74rem;color:var(--ink);margin-top:4px}
322
+ .d1-kv>span:first-child{color:var(--muted);display:inline-flex;align-items:center}
323
+ .d1-kv>span:last-child{font-family:var(--mono);white-space:nowrap}
324
+ .d1 .na{color:var(--muted)}
325
+ .d1-cr{display:grid;grid-template-columns:38px 1fr auto;align-items:center;gap:8px;font-size:.74rem;height:22px}
326
+ .d1-t{font-family:var(--mono);font-size:.7rem;color:var(--soft)}
327
+ .d1-cr .val,.d1-q .val{font-family:var(--mono);text-align:right;white-space:nowrap}
328
+ .track{position:relative;height:14px}
329
+ .track::before{content:"";position:absolute;left:0;right:0;top:7px;height:1px;background:var(--line)}
330
+ .track .zero{position:absolute;top:2px;width:1px;height:10px;background:var(--muted);opacity:.5}
331
+ .track .bh{position:absolute;top:1px;width:2px;height:12px;margin-left:-1px;background:#7d7b85;border-radius:1px}
332
+ .track .ag{position:absolute;top:3px;width:8px;height:8px;margin-left:-4px;border-radius:50%;background:var(--c)}
333
+ .d1-legend{display:flex;flex-wrap:wrap;gap:4px 10px;font-size:.64rem;color:var(--muted);margin-top:8px}
334
+ .d1-legend i{display:inline-block;vertical-align:-1px;margin-right:4px}
335
+ .d1-legend .l-ag{width:8px;height:8px;border-radius:50%;background:#d6304a}
336
+ .d1-legend .l-bh{width:2px;height:10px;background:#7d7b85}
337
+ .d1-legend .l-z{width:1px;height:10px;background:var(--muted);opacity:.5}
338
+ .d1-legend .sw{width:8px;height:8px;border-radius:2px}
339
+ .d1-pair{font-size:.95rem;font-weight:600;margin-bottom:4px}
340
+ .d1-pair .s{color:var(--muted);font-weight:400;margin:0 4px}
341
+ .d1-pair .runs{font-size:.68rem;font-weight:500;color:var(--muted);margin-left:8px}
342
+ .d1-cap{font-size:.64rem;color:var(--muted);margin-top:10px}
343
+ .d1-strip{display:flex;gap:1px;height:14px;margin-top:3px}
344
+ .d1-strip i,.d1-legend .sw{background:#d9d7de}
345
+ .d1-strip i{flex:1;border-radius:1px;cursor:help}
346
+ .d1-strip i.L,.d1-legend .sw.L{background:#3567d8}
347
+ .d1-strip i.R,.d1-legend .sw.R{background:#e0475b}
348
+ .d1-strip i.F,.d1-legend .sw.F{background:#b9b5cf;opacity:.45}
349
+ .d1-strip i{position:relative}
350
+ .d1-strip i.cl::after,.d1-legend .dia{content:"";position:absolute;left:50%;top:50%;width:6px;height:6px;margin:-3px 0 0 -3px;background:#8b3fd9;transform:rotate(45deg);opacity:1}
351
+ .d1-legend .dia{position:relative;display:inline-block;left:auto;top:auto;margin:0 6px 0 2px;vertical-align:0}
352
+ .d1-strip i.M,.d1-legend .sw.M{background:repeating-linear-gradient(45deg,#fff 0 2px,#e2e0e6 2px 4px)}
353
+ .d1-q{display:grid;grid-template-columns:34px 1fr 26px 44px 44px;align-items:center;gap:6px;font-size:.72rem;height:20px}
354
+ .d1-q.hd{height:14px;font-size:.54rem;letter-spacing:.03em;white-space:nowrap;text-transform:uppercase;color:var(--muted)}
355
+ .d1-q.hd span{text-align:right}
356
+ .d1-q .val.up{color:#0f7a4a}.d1-q .val.dn{color:#b4233a}.d1-q .val.bh{color:var(--muted)}
357
+ .q-bar{height:6px;border-radius:3px;background:var(--paper-2);overflow:hidden}
358
+ .q-bar i{display:block;height:100%;border-radius:3px;background:var(--c)}
359
+ .d1-stack{display:flex;gap:2px;height:12px;margin:4px 0 8px;border-radius:4px;overflow:hidden;background:var(--paper-2)}
360
+ .d1-stack i{height:100%;cursor:help}
361
+ .s-acc{background:#11949f}.s-ser{background:#4f5a6e}.s-eer{background:#8a93a3}.s-cer{background:#c9cfdb}
362
+ .d1 .sq{display:inline-block;width:8px;height:8px;border-radius:2px;margin-right:6px}
363
+ .board-foot{display:flex;justify-content:space-between;gap:24px;flex-wrap:wrap;margin-top:14px;font-size:.78rem}
364
+ .board-foot p{font-size:.78rem;color:var(--muted);max-width:70ch}
365
+ .board-foot b{color:var(--soft);font-weight:600}
366
+ .keys{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}
367
+ .ring-key{display:inline-block;width:18px;height:12px;border-radius:4px;background:#8e6ad0;box-shadow:0 0 0 2px var(--ink);margin-right:4px}
368
+ .ramp{display:inline-block;width:70px;height:8px;border-radius:4px;margin-left:6px;background:linear-gradient(90deg,#f4f0fb,#c9b6ea,#8e6ad0,#4b2a8f)}
369
+
370
+ /* ---------- 2b · rankings across workflows ---------- */
371
+ .rk-head{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:40px;align-items:end;margin-bottom:16px}
372
+ .rk-dek{margin-top:14px;font-size:1rem;max-width:52ch}
373
+ .rk-dek b{color:var(--ink);font-weight:600}
374
+ .rk-quick{display:flex;flex-wrap:wrap;gap:8px 12px;margin-top:18px;align-items:center}
375
+ .rk-quick button{--c:#7800f0;display:inline-flex;align-items:center;gap:7px;border:0;cursor:pointer;font:500 .8rem var(--sans);padding:6px 12px;border-radius:999px;color:color-mix(in srgb,var(--c) 75%,#0f1528);background:color-mix(in srgb,var(--c) 9%,#fff);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--c) 22%,transparent);transition:background .3s,color .3s,box-shadow .3s,transform .3s var(--spring)}
376
+ .rk-quick button:hover{background:color-mix(in srgb,var(--c) 16%,#fff)}
377
+ .rk-quick button:active{transform:scale(.95)}
378
+ .rk-quick button.on{background:var(--c);color:#fff;box-shadow:0 6px 14px -8px var(--c)}
379
+ .rk-quick button.grad{--c:#7800f0}
380
+ .rk-quick button.grad.on{background:var(--g)}
381
+ .rk-quick .wdot{transition:background .3s}
382
+ .rk-quick button.on .wdot{background:#fff}
383
+ /* agent × backbone picker */
384
+ .picker{display:grid;grid-template-columns:auto repeat(6,56px);gap:3px 5px;align-items:center;padding:8px 14px 7px;border-radius:18px;background:#fff;box-shadow:var(--card-ring)}
385
+ .pk-col,.pk-row{border:0;background:none;cursor:pointer;font:inherit;color:var(--soft);border-radius:8px;transition:color .25s,background .25s}
386
+ .pk-col{font-size:.6rem;font-weight:600;letter-spacing:.04em;padding:1px 0;text-align:center;white-space:nowrap}
387
+ .pk-row{display:inline-flex;align-items:center;gap:7px;font-size:.78rem;font-weight:500;padding:0 10px 0 2px;line-height:1.2;text-align:left;white-space:nowrap}
388
+ .pk-col:hover,.pk-row:hover{color:var(--ink);background:var(--paper-2)}
389
+ .pk-col.on,.pk-row.on{color:var(--ink);background:var(--g-soft)}
390
+ .pk-cell{position:relative;height:20px;border:0;border-radius:6px;cursor:pointer;font:500 .62rem var(--mono);color:color-mix(in srgb,var(--c) 70%,#0f1528);background:color-mix(in srgb,var(--c) 14%,#fff);transition:background .3s,color .3s,box-shadow .3s,transform .35s var(--spring)}
391
+ .pk-cell:hover{transform:scale(1.08);z-index:3}
392
+ .pk-cell.pv,.pk-cell.lit{z-index:1}
393
+ .pk-cell.lit{background:color-mix(in srgb,var(--c) 70%,#fff);color:#fff}
394
+ .pk-cell.sel{z-index:2;background:var(--c);color:#fff;box-shadow:0 0 0 2px #fff,0 0 0 3.5px var(--ink);transform:scale(1.06)}
395
+ .pk-cell.pv:not(.sel){box-shadow:0 0 0 1.5px var(--c)}
396
+ .pk-note{grid-column:1/-1;font-size:.66rem;color:var(--muted);margin-top:2px;line-height:1.3}
397
+ /* chart */
398
+ .rk-chart{position:relative;border-radius:20px;background:rgba(255,255,255,.72);box-shadow:var(--card-ring);padding:14px 10px 6px}
399
+ .rk-chart svg{display:block;width:100%;height:auto;overflow:visible;font-family:var(--sans)}
400
+ .rk-chart .col-h{font-size:13px;font-weight:600;fill:var(--ink);pointer-events:none;transition:fill .3s}
401
+ .col-btn{cursor:pointer;outline:none}
402
+ .col-btn .cb-bg{fill:color-mix(in srgb,var(--c) 9%,#fff);stroke:color-mix(in srgb,var(--c) 28%,transparent);stroke-width:1;transition:fill .3s,stroke .3s}
403
+ .col-btn .cb-dot{fill:var(--c);transition:fill .3s}
404
+ .col-btn:hover .cb-bg,.col-btn:focus-visible .cb-bg{fill:color-mix(in srgb,var(--c) 18%,#fff)}
405
+ .col-btn.on .cb-bg{fill:var(--c);stroke:var(--c)}
406
+ .col-btn.on .col-h{fill:#fff}
407
+ .col-btn.on .cb-dot{fill:#fff}
408
+ .rk-tag{font-weight:700;fill:#7a1fb4}
409
+ .rk-hint{font-size:.78rem;color:var(--muted);align-self:center}
410
+ .rk-chart .tick{font-size:10.5px;fill:var(--muted);font-family:var(--mono)}
411
+ .rk-chart .lane{stroke:var(--line);stroke-dasharray:2 4}
412
+ .ln path.line{fill:none;stroke:rgba(74,38,130,.11);stroke-width:1.4;transition:stroke-opacity .45s,stroke-width .45s var(--ease),stroke .3s}
413
+ .ln path.hit{fill:none;stroke:transparent;stroke-width:11;cursor:pointer}
414
+ .ln circle{transform-box:fill-box;transform-origin:center;fill:#fff;stroke:rgba(74,38,130,.18);stroke-width:1.1;transition:r .45s var(--spring),fill .3s,stroke .3s,stroke-width .3s}
415
+ .ln .rkl{font-size:11px;font-weight:700;fill:#fff;text-anchor:middle;pointer-events:none;opacity:0;transition:opacity .3s}
416
+ .ln.lit path.line{stroke:url(#wfGrad);stroke-opacity:.6;stroke-width:2}
417
+ .ln.lit circle{fill:var(--wc);stroke:#fff;stroke-width:1.5;r:4.5}
418
+ .ln.sel path.line{stroke:url(#wfGrad);stroke-opacity:1;stroke-width:5.5;filter:drop-shadow(0 2px 4px rgba(90,20,160,.25))}
419
+ .ln.sel circle{fill:var(--wc);stroke:#fff;stroke-width:2.5;r:12}
420
+ .ln.sel .rkl{opacity:1}
421
+ .ln.pv:not(.sel) path.line{stroke:#0f1528;stroke-opacity:.9;stroke-width:2.4}
422
+ .ln.pv:not(.sel) circle{fill:#0f1528;stroke:#fff;r:4.5}
423
+ .rk-chart svg.all .ln:not(.sel) path.line{stroke:url(#wfGrad);stroke-opacity:.34;stroke-width:1.5}
424
+ .rk-chart svg.all .ln:not(.sel) circle{fill:var(--wc);stroke:#fff;stroke-width:1;r:3}
425
+ .rk-chart svg.all .ln.pv:not(.sel) path.line{stroke:#0f1528;stroke-opacity:.9;stroke-width:2.4}
426
+ .rk-chart svg.all .ln.pv:not(.sel) circle{fill:#0f1528;stroke:#fff;r:4.5}
427
+ .end{font-size:11.5px;font-weight:500;fill:#4f5a6e;pointer-events:none}
428
+ .end-l{text-anchor:end}
429
+
430
+ /* view switch: the card's own two-half navigation, a gradient knob slides to the chosen half */
431
+ .rk-view{position:relative;display:grid;grid-template-columns:1fr 1fr 1fr;margin:0 0 14px;padding:4px;border-radius:14px;background:var(--paper-2);box-shadow:inset 0 0 0 1px var(--line)}
432
+ .rk-view button{position:relative;z-index:1;display:flex;align-items:center;justify-content:center;gap:10px;border:0;background:none;height:32px;padding:0 16px;border-radius:10px;cursor:pointer;font:inherit;color:var(--soft);text-align:left;transition:color .35s}
433
+ .rk-view button svg{width:26px;height:19px;flex:none}
434
+ .rk-view button span{display:flex;flex-direction:column;line-height:1.25}
435
+ .rk-view button b{font-size:.95rem;font-weight:600}
436
+ .rk-view button small{font-size:.72rem;color:inherit;opacity:.75}
437
+ .rk-view .go{font-style:normal;font-size:1.2rem;opacity:0;transform:translateX(-4px);transition:opacity .3s,transform .35s var(--spring)}
438
+ .rk-view button[aria-selected=true]{color:#fff}
439
+ .rk-view button[aria-selected=false]{color:#5a1aa8}
440
+ .rk-view button[aria-selected=false] .go{opacity:1;transform:none;animation:nudge 1.3s cubic-bezier(.34,1.56,.64,1) infinite}
441
+ .rk-view button[aria-selected=false]:hover .go{animation-duration:.7s}
442
+ /* the arrow on the other view bounces to invite a click */
443
+ @keyframes nudge{0%,55%,100%{transform:translateX(0)}20%{transform:translateX(6px)}35%{transform:translateX(1px)}45%{transform:translateX(3px)}}
444
+ .rk-view button[aria-selected=false]:hover{background:rgba(120,0,240,.06)}
445
+ .rk-view .seg-pill{position:absolute;top:4px;bottom:4px;left:0;border-radius:10px;background:linear-gradient(90deg,#7a1036,#4a1a8c);box-shadow:0 4px 10px -6px rgba(74,26,140,.6);transition:transform .5s var(--ease),width .5s var(--ease)}
446
+ /* floor → Overall view */
447
+ .rk-db .db-chip{font-size:12px}
448
+ .rk-db .db-key text{font-size:11.5px;fill:var(--soft)}
449
+ .db{cursor:pointer;transition:opacity .4s,transform .7s var(--ease)}
450
+ .db-sort{cursor:pointer;outline:none}
451
+ .db-sort .cb-bg{fill:color-mix(in srgb,var(--c) 9%,#fff);stroke:color-mix(in srgb,var(--c) 28%,transparent);stroke-width:1;transition:fill .3s,stroke .3s}
452
+ .db-sort .cb-dot{fill:var(--c);transition:fill .3s}
453
+ .db-sort:hover .cb-bg,.db-sort:focus-visible .cb-bg{fill:color-mix(in srgb,var(--c) 18%,#fff)}
454
+ .db-sort.on .cb-bg{fill:var(--c);stroke:var(--c)}
455
+ .db-sort.on .col-h,.db-sort.on .cb-dot{fill:#fff}
456
+ .db-sort .cb-ring{fill:none;stroke:var(--c);stroke-width:1.4}
457
+ .db-sort.on .cb-ring{stroke:#fff}
458
+ .db-band{fill:transparent;transition:fill .3s}
459
+ .db:hover .db-band,.db.pv .db-band{fill:rgba(15,21,40,.05)}
460
+ .db.pv:not(.sel) .db-name{fill:#0f1528;font-weight:600}
461
+ .db.pv:not(.sel) .db-gap{stroke:#0f1528;opacity:.85}
462
+ .db.sel .db-band{fill:rgba(120,0,240,.08)}
463
+ .db-name{font-size:11.5px;fill:var(--soft)}
464
+ .db-rank{font-size:10.5px;fill:var(--muted);font-family:var(--mono)}
465
+ .db-val{font-size:12px;font-weight:600;fill:var(--ink)}
466
+ .db-range{stroke:rgba(74,38,130,.16);stroke-width:1.5}
467
+ .db-gap{stroke:url(#brandGrad);stroke-width:3;stroke-linecap:round;opacity:.55;transform-box:fill-box;transform-origin:left center;transition:opacity .3s,stroke-width .3s}
468
+ .db-wf{transform-box:fill-box;transform-origin:center}
469
+ .db-floor{fill:none;stroke:#0f1528;stroke-width:1.5;pointer-events:all}
470
+ .db-ov{fill:#0f1528;stroke:#fff;stroke-width:1.5;transform-box:fill-box;transform-origin:center}
471
+ .rk-db .db:not(.sel):not(.lit):not(.pv){opacity:.28}
472
+ .rk-db.all .db:not(.sel):not(.pv){opacity:1}
473
+ .rk-db.all .db:not(.sel) .db-name{fill:var(--ink)}
474
+ .db.sel .db-name,.db.lit .db-name{fill:var(--ink);font-weight:600}
475
+ .db.sel .db-gap{opacity:1;stroke-width:4.5}
476
+ .db.lit .db-gap{opacity:.85}
477
+ .db.sel .db-ov{r:7}
478
+
479
+ /* ---------- 2c · replay: Trading + Hedging trajectories, drawn day by day ---------- */
480
+ .rp-head{align-items:end}
481
+ .rp-ctl{display:flex;align-items:center;gap:12px;flex-basis:100%}
482
+ .rp-run button{font-family:var(--sans);white-space:nowrap}
483
+ .rp-pick{margin-bottom:14px}
484
+ .rp-pick button.sel{background:var(--ink);color:#fff;box-shadow:none}
485
+ .rp-pick button:not(.sel){opacity:.75}
486
+ .rp-pick button:not(.sel):hover{opacity:1}
487
+ .rp-now{margin-left:auto;font-size:.8rem;color:var(--muted)}
488
+ .rp-now b{color:var(--ink);font-weight:600}
489
+ #rpAgain{white-space:nowrap}
490
+ .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}
491
+ .rp-sys{position:relative;display:inline-flex;align-items:center}
492
+ .rp-sys select{appearance:none;-webkit-appearance:none;border:0;background:transparent;font:500 1.35rem var(--serif);letter-spacing:-.01em;color:var(--ink);padding:2px 22px 4px 0;cursor:pointer;border-bottom:1px dashed rgba(74,38,130,.3);border-radius:0}
493
+ .rp-sys select:focus{outline:none;border-bottom-color:#7800f0}
494
+ .rp-sys .caret{position:absolute;right:2px;top:50%;transform:translateY(-50%);font-size:.75rem;color:var(--muted);pointer-events:none}
495
+ .rp-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
496
+ .rp-card{position:relative;display:flex;flex-direction:column;padding:18px 20px 16px;border-radius:20px;background:#fff;box-shadow:var(--card-ring);min-width:0}
497
+ .rp-top{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:8px;height:30px}
498
+ .rp-kick{display:inline-flex;align-items:center;gap:8px;font-size:.72rem;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--soft)}
499
+ .rp-tk{display:inline-flex;gap:2px;padding:3px;border-radius:999px;background:var(--paper-2)}
500
+ .rp-tk button{border:0;background:none;font:500 .72rem var(--mono);color:var(--soft);padding:4px 10px;border-radius:999px;cursor:pointer;transition:background .25s,color .25s}
501
+ .rp-tk button.on{background:#fff;color:var(--ink);box-shadow:0 1px 4px -1px rgba(15,21,40,.2)}
502
+ .rp-pair{font:600 .9rem var(--sans)}
503
+ .rp-pair small{font:500 .7rem var(--sans);color:var(--muted);margin-left:6px}
504
+ .rp-chart svg{display:block;width:100%;height:auto;overflow:visible;font-family:var(--sans)}
505
+ .rp-gridline{stroke:var(--line);stroke-dasharray:2 4}
506
+ .rp-ax{font:500 9.5px var(--mono);fill:var(--muted)}
507
+ .rp-line{fill:none;stroke:#0f1528;stroke-width:1.7;stroke-linejoin:round;stroke-linecap:round}
508
+ .rp-line.b{stroke:#8a93a3;stroke-dasharray:4 3}
509
+ .rp-lab{font:600 10px var(--mono)}
510
+ .rp-status{font:500 10.5px var(--mono);fill:var(--soft)}
511
+ .rp-card .rp-chart{margin-top:4px}
512
+ .rp-dot{transition:fill .2s}
513
+ .rp-hover{stroke:#0f1528;stroke-width:1;stroke-opacity:.35;pointer-events:none}
514
+ .rp-hdot{fill:#0f1528;stroke:#fff;stroke-width:1.5;pointer-events:none}
515
+ .rp-hdot.b{fill:#8a93a3}
516
+ .tip .tt{font-weight:600;margin-bottom:3px}
517
+ .tip .tr{display:flex;justify-content:space-between;gap:16px}
518
+ .tip .tr span{color:rgba(255,255,255,.65)}
519
+ .rp-pill{transition:fill .2s}
520
+ .rp-pilltext{font:600 9.5px var(--sans);fill:#fff;letter-spacing:.06em}
521
+ .rp-pilltext.dark{fill:#4f5a6e}
522
+ .rp-cell{transition:opacity .2s}
523
+ .rp-cell.pending{opacity:0}
524
+ .rp-legend{margin-bottom:12px;display:flex;flex-wrap:wrap;gap:4px 14px;font-size:.7rem;color:var(--soft);margin-top:6px;min-height:18px}
525
+ .rp-legend span{display:inline-flex;align-items:center;gap:6px}
526
+ .rp-legend i{display:inline-block;width:11px;height:9px;border-radius:2px}
527
+ .a-B{background:#1593b5}.a-S{background:#e0475b}.a-H{background:#b9b5cf;opacity:.6}
528
+ .a-x{background:repeating-linear-gradient(45deg,#fff 0 2px,#d8d6e0 2px 4px);box-shadow:inset 0 0 0 1px #d8d6e0}
529
+ .p-L{background:#3567d8}.p-R{background:#e0475b}.p-F{background:#b9b5cf;opacity:.45}
530
+ .rp-legend i.p-dia{width:7px;height:7px;border-radius:0;background:#8b3fd9;transform:rotate(45deg)}
531
+ .rp-foot{display:flex;flex-wrap:wrap;gap:8px 16px;align-items:baseline;margin-top:auto;min-height:36px;box-sizing:content-box;line-height:1.4;padding-top:12px;border-top:1px solid var(--line);transition:opacity .5s}
532
+ .rp-foot.pending{opacity:.25}
533
+ .rp-foot div{display:flex;flex-direction:row;align-items:baseline;gap:5px;white-space:nowrap}
534
+ .rp-foot b{font:600 1.05rem var(--sans);font-variant-numeric:tabular-nums}
535
+ .rp-foot .vs{font-style:normal;font-size:.7rem;color:var(--muted);margin:0 -6px}
536
+ .rp-foot .dim b{color:var(--muted)}
537
+ .rp-foot span{font-size:.62rem;letter-spacing:.03em;text-transform:uppercase;color:var(--muted)}
538
+ .rp-foot .up{color:#0f7a4a}.rp-foot .dn{color:#b4233a}
539
+ .d1-replay{display:flex;width:max-content;align-items:center;gap:6px;margin-top:4px;font-size:.64rem;font-weight:600;color:#7a1fb4;letter-spacing:.02em;cursor:pointer;border:0;background:none;padding:0;white-space:nowrap}
540
+ .play-ico{width:15px;height:15px;flex:none;transition:transform .35s var(--spring)}
541
+ .d1-replay .play-ico{width:14px;height:14px}
542
+ .d1-replay:hover .play-ico,.rp-again:hover .play-ico{transform:scale(1.15)}
543
+ .rp-again{color:var(--ink);gap:8px}
544
+ .rp-again .play-ico{color:#7a1fb4}
545
+ .d1-replay:hover{text-decoration:underline}
546
 
547
+ /* ---------- pixel Hercules scenes (from the earlier HERCULEAN site) ---------- */
548
+ .scene{position:relative;height:34px;overflow:hidden;color:#4f5a6e;--gold:#A9762A}
549
+ .hero .scene{position:absolute;inset:0;height:auto;z-index:0;margin:0;pointer-events:none}
550
+ .hero .scene::after{display:none} /* walks on the front price ridge instead (positioned in app.js) */
551
+ .hero .scene .walker,.hero .scene .lion,.hero .scene .flash{bottom:auto}
552
+ .scene::after{content:'';position:absolute;left:0;right:0;bottom:7px;height:1px;background:repeating-linear-gradient(90deg,rgba(74,38,130,.28) 0 10px,transparent 10px 14px);animation:hgrnd 1.1s linear infinite}
553
+ .scene.rev::after{animation:hgrndrev .23s linear infinite}
554
+ .scene.js-ground::after{animation:none;background-position-x:var(--gx,0)}
555
+ @keyframes hgrnd{to{background-position-x:-14px}}
556
+ @keyframes hgrndrev{to{background-position-x:14px}}
557
+ .scene .walker{position:absolute;bottom:8px;width:18px;height:24px;z-index:2;color:#4A4234}
558
+ .scene .walker svg{position:absolute;left:0;bottom:0;height:24px;width:auto;image-rendering:pixelated}
559
+ .scene .walker .acc{fill:var(--gold)}
560
+ .scene .hk{position:absolute;left:0;bottom:0;width:18px;height:24px}
561
+ .scene .hk.skin{display:none}
562
+ .scene .fr{opacity:0;position:absolute;left:0;bottom:0}
563
+ .scene .fr.a{opacity:1}
564
+ .scene .lion{position:absolute;bottom:8px;font-size:17px;line-height:1;z-index:1}
565
+ .scene.press .lion{z-index:3}
566
+ .scene .li{display:inline-block;animation:liorise .34s steps(2) alternate infinite}
567
+ .scene .flash{position:absolute;bottom:8px;width:26px;height:26px;border-radius:50%;opacity:0;pointer-events:none;margin-left:-4px;z-index:4;mix-blend-mode:screen;background:radial-gradient(circle,#fff 0%,#FFE9A8 35%,rgba(255,220,120,0) 70%)}
568
+ .scene.solo .walker{transform:scaleX(-1)}
569
+ .scene.solo .hk.skin{display:block}
570
+ @keyframes liorise{to{transform:translateY(-2px)}}
571
+ @media(prefers-reduced-motion:reduce){.scene .li,.scene::after{animation:none}}
572
+
573
+ /* ---------- compact CTA ---------- */
574
+ .cta-sec{padding:20px 0 0}
575
+ .cta-card{position:relative;overflow:hidden;isolation:isolate;display:flex;align-items:center;justify-content:space-between;gap:28px;padding:26px 30px;border-radius:20px;background:var(--ink);color:#fff}
576
+ .cta-card .mesh{position:absolute;inset:-40%;z-index:-1;filter:blur(70px);opacity:.32;pointer-events:none}
577
+ .cta-card .mesh i{position:absolute;width:40%;aspect-ratio:1;border-radius:50%}
578
+ .cta-card .mesh i:nth-child(1){background:#b40028;left:5%;top:20%;animation:float1 16s ease-in-out infinite alternate}
579
+ .cta-card .mesh i:nth-child(2){background:#7800f0;right:5%;top:10%;animation:float2 19s ease-in-out infinite alternate}
580
+ @keyframes float1{to{transform:translate(18%,12%) scale(1.15)}}
581
+ @keyframes float2{to{transform:translate(-20%,-8%) scale(.9)}}
582
+ .cta-text h3{font-size:1.55rem;color:#fff}
583
+ .cta-text h3 em{font-style:italic;background:linear-gradient(90deg,#ff8aa0,#c79bff);-webkit-background-clip:text;background-clip:text;color:transparent;padding-right:.06em}
584
+ .cta-text p{margin-top:6px;font-size:.88rem;color:rgba(255,255,255,.7);max-width:62ch}
585
+ .cta-row{display:flex;gap:10px;flex:none}
586
+ .btn--light{background:var(--paper);color:var(--ink)}
587
+ .btn--light::before{background:radial-gradient(110px circle at var(--mx) var(--my),rgba(120,0,240,.16),transparent 70%)}
588
+ .game-card{text-decoration:none;transition:transform .3s cubic-bezier(.2,.8,.2,1),box-shadow .3s}
589
+ .game-card:hover{transform:translateY(-3px);box-shadow:0 24px 50px -24px rgba(60,20,120,.55)}
590
+ .game-kick{display:block;font-size:.62rem;font-weight:600;letter-spacing:.16em;text-transform:uppercase;color:rgba(255,255,255,.55);margin-bottom:6px}
591
+ .game-k{flex:1 1 300px;min-width:0;max-width:440px;height:96px}
592
+ .game-k .gk{transform-box:fill-box;transform-origin:center;opacity:0;transform:scaleY(.2);transition:opacity .35s,transform .45s cubic-bezier(.2,.8,.2,1)}
593
+ .game-card.in .game-k .gk{opacity:1;transform:none}
594
+ .game-go{flex:none}
595
+ .game-card:hover .game-go .arrow{transform:translateX(3px)}
596
+ .btn--glass{background:rgba(255,255,255,.1);color:#fff;box-shadow:inset 0 0 0 1px rgba(255,255,255,.3)}
597
+ .btn--glass::before{background:radial-gradient(110px circle at var(--mx) var(--my),rgba(255,255,255,.22),transparent 70%)}
598
+
599
+ /* ---------- 3 · cite · resources · partners ---------- */
600
+ .res-grid{display:grid;grid-template-columns:1.25fr 1fr;gap:14px}
601
+ .res-h{display:block;font-size:.66rem;font-weight:600;letter-spacing:.16em;text-transform:uppercase;color:var(--muted)}
602
+ .bib{position:relative;overflow:hidden;isolation:isolate;border-radius:20px;background:#fff;box-shadow:var(--card-ring);padding:22px 24px}
603
+ .bib-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}
604
+ .bib pre{margin:0;font-family:var(--mono);font-size:.74rem;line-height:1.65;color:var(--soft);white-space:pre-wrap;word-break:break-word;max-height:14.5em;overflow:auto}
605
+ .bib-actions{display:flex;align-items:center;gap:14px}
606
+ .paper-link{font-size:.82rem;font-weight:500;color:#6a1fc0}
607
+ .paper-link:hover{text-decoration:underline}
608
+ .copy-btn{border:0;font:inherit;font-size:.78rem;font-weight:500;padding:6px 14px;border-radius:999px;background:var(--paper-2);color:var(--ink);cursor:pointer;transition:background .3s,color .3s}
609
+ .copy-btn:hover{background:var(--ink);color:#fff}
610
+ .res-links{position:relative;overflow:hidden;isolation:isolate;border-radius:20px;background:#fff;box-shadow:var(--card-ring);padding:22px 24px;display:flex;flex-direction:column}
611
+ .res-links .res-h{margin-bottom:6px}
612
+ .res-links a{display:grid;grid-template-columns:110px 1fr 16px;align-items:center;gap:10px;padding:11px 0;border-top:1px solid var(--line);transition:padding .35s var(--ease)}
613
+ .res-links a:first-of-type{border-top:0}
614
+ .res-links a:hover{padding-left:6px}
615
+ .res-links b{font-family:var(--serif);font-weight:500;font-size:1.15rem}
616
+ .res-links span{font-size:.84rem;color:var(--muted)}
617
+ .res-links i{font-style:normal;color:var(--muted);transition:color .25s,transform .35s var(--spring)}
618
+ .res-links a:hover i{color:#7800f0;transform:translate(2px,-2px)}
619
+ .partners{margin-top:34px;text-align:center}
620
+ .p-logos{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:16px 36px;flex-wrap:wrap}
621
+ .p-logos+.p-logos{margin-top:18px}
622
+ .p-logos a{display:inline-flex;align-items:center;gap:10px;transition:transform .35s var(--spring)}
623
+ .p-logos a:hover{transform:translateY(-2px)}
624
+ .p-logos .p-pair{gap:8px}
625
+ .p-logos img.crest{height:32px;max-width:none}
626
+ .p-logos .p-word{font-family:Georgia,'Times New Roman',serif;font-size:1.3rem;color:#ed1b2f;letter-spacing:.01em}
627
+ .p-logos .p-text{font:600 1.15rem var(--sans);letter-spacing:-.01em;color:var(--ink);cursor:default}
628
+ .p-logos .p-dk img.crest{height:30px}
629
+ .p-logos .p-dkw{font:700 1.15rem var(--sans);letter-spacing:-.01em;color:#0b1d45}
630
+ .p-logos img{height:28px;width:auto;max-width:112px;object-fit:contain}
631
+ .p-logos .p-finai img{height:30px;max-width:none}
632
+ .p-logos img.p-mbz{height:40px;max-width:none}
633
+ .p-logos .p-finai{font-size:1.1rem!important;font-family:var(--serif);font-size:1.25rem;font-weight:500;white-space:nowrap}
634
+ .p-lead{display:flex;align-items:center;gap:34px;flex-wrap:wrap}
635
+ .p-main{display:inline-flex;align-items:center;gap:12px;font-family:var(--serif);font-size:1.6rem;font-weight:500}
636
+ .p-main img{height:38px;width:auto}
637
+ .p-names{margin:18px auto 0;max-width:860px;font-size:.78rem;line-height:1.9;color:var(--muted)}
638
+ .p-names span{white-space:nowrap}
639
+ .p-names i{font-style:normal;margin:0 9px;opacity:.5}
640
+
641
+ /* ---------- footer ---------- */
642
+ .footer{padding:20px 0 28px}
643
+ .footer-bottom{display:flex;justify-content:space-between;align-items:center;gap:20px;padding-top:24px;border-top:1px solid var(--line)}
644
+ .copy{font-size:.82rem;color:var(--muted)}
645
+ .to-top{display:inline-grid;place-items:center;width:40px;height:40px;border-radius:50%;box-shadow:inset 0 0 0 1px var(--line);transition:background .3s,color .3s,transform .5s var(--spring)}
646
+ .to-top:hover{background:var(--ink);color:#fff;transform:translateY(-4px)}
647
+
648
+ /* ---------- tooltip ---------- */
649
+ .tip{position:fixed;z-index:80;pointer-events:none;padding:7px 11px;border-radius:10px;background:var(--ink);color:#fff;font-size:.76rem;line-height:1.45;max-width:280px;opacity:0;transform:translateY(4px);transition:opacity .15s,transform .15s}
650
+ .tip.on{opacity:1;transform:none}
651
+ .tip b{font-weight:600}
652
+ .tip span{color:rgba(255,255,255,.65)}
653
+
654
+ /* ---------- responsive ---------- */
655
+ @media (max-width:1080px){
656
+ .wf-grid{grid-template-columns:repeat(2,1fr)}
657
+ .d1-cards{grid-template-columns:repeat(2,minmax(0,1fr))}.d1{padding-left:10px}
658
+ .lb-cols,.lb-row .in{grid-template-columns:28px minmax(180px,240px) minmax(100px,1fr) 220px 14px;gap:10px}
659
+ }
660
+ @media (max-width:980px){
661
+ .hero{min-height:auto;padding:120px 0 70px}
662
+ .hero-grid{grid-template-columns:1fr;gap:40px}
663
+ .hero-viz{max-width:560px;height:430px}
664
+ .sec-head{flex-direction:column;align-items:start}
665
+ .lb-claim{padding-left:0;border-left:0}
666
+ .res-grid{grid-template-columns:1fr}
667
+ .rk-head{grid-template-columns:1fr}
668
+ .rp-grid{grid-template-columns:1fr}
669
+ .cta-card{flex-direction:column;align-items:start}
670
+ .game-k{width:100%}
671
+ .partners{grid-template-columns:1fr}
672
+ }
673
+ @media (max-width:760px){
674
+ .links{display:none}
675
+ .wrap{padding:0 18px}
676
+ .section{padding:64px 0}
677
+ .wf-grid{grid-template-columns:1fr}
678
+ .board{overflow-x:auto}
679
+ .lb-cols,.lb-rows{min-width:760px}
680
+ .stats{gap:24px}.stats b{font-size:2.1rem}
681
+ .podium{gap:5px}.col .nm{font-size:.62rem}.hero-viz{padding:18px 14px 14px}
682
+ .d1-cards{grid-template-columns:1fr}
683
  }
684
+ @media (prefers-reduced-motion:reduce){
685
+ .ticker{overflow-x:auto}.ticker-track{animation:none}
686
+ *,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition:none!important}
687
+ }
688
+
689
+ /* "Trade" buttons (hero + game card): on hover the whole pill turns into a bright brand gradient; $ badge */
690
+ .btn--trade::before,.btn--trade.btn--light::before{background:linear-gradient(100deg,#ff2d5f,#c026d3 52%,#8b3dff);opacity:0;transition:opacity .35s var(--ease)}
691
+ .btn--trade:hover::before,.game-card:hover .btn--trade::before,.btn--trade.fh::before{opacity:1}
692
+ .btn--trade:hover,.game-card:hover .btn--trade,.btn--trade.fh{color:#fff;box-shadow:0 12px 30px -10px rgba(192,38,211,.6)}
693
+ .btn--trade .dol{width:1.15em;height:1.15em;flex:none;color:#ffcf5a;transition:transform .45s var(--spring)}
694
+ .btn--trade .dol text{fill:#3a2400}
695
+ .btn--trade.btn--light .dol{color:#f0b429}
696
+ .btn--trade:hover .dol,.game-card:hover .btn--trade .dol,.btn--trade.fh .dol{transform:rotate(-14deg) scale(1.12)}
697
+
698
+ /* Rankings · field view */
699
+ .rk-chart .f-grid{stroke:rgba(74,38,130,.07)}
700
+ .rk-chart .f-lane{transition:transform .8s var(--ease);cursor:pointer}
701
+ .rk-chart .f-line{stroke:rgba(74,38,130,.18)}
702
+ .rk-chart .f-name{font-size:13px;font-weight:600}
703
+ .rk-chart .f-mean{stroke-width:2.6;stroke-linecap:round;transition:x1 .8s var(--ease),x2 .8s var(--ease)}
704
+ .rk-chart .f-mv{font:500 12px var(--mono);fill:var(--soft)}
705
+ .rk-chart .f-note{cursor:pointer;transition:transform .8s var(--ease)}
706
+ .rk-chart .f-note circle{stroke:#fff;stroke-width:2;opacity:.25;transition:opacity .25s,r .2s}
707
+ .rk-chart .f-note .f-bb{font:600 9.5px var(--mono);letter-spacing:-.02em;fill:#fff;pointer-events:none;opacity:.6;transition:opacity .25s}
708
+ .rk-chart svg.all .f-note circle,.rk-chart .f-note.lit circle,.rk-chart .f-note.sel circle,.rk-chart .f-note.pv circle{opacity:.92}
709
+ .rk-chart svg.all .f-note .f-bb,.rk-chart .f-note.lit .f-bb,.rk-chart .f-note.sel .f-bb,.rk-chart .f-note.pv .f-bb{opacity:1}
710
+ .rk-chart .f-note.sel circle{stroke:var(--ink);stroke-width:2.5;r:17.5}
711
+ .rk-chart .f-note.pv circle{r:17.5}
712
+ .rk-chart .f-trace{fill:none;stroke:var(--ink);stroke-width:1.4;stroke-dasharray:4 4;opacity:0;transition:opacity .25s;pointer-events:none}
713
+ .rk-chart .f-trace.on{opacity:.5}
714
+ .rk-chart .f-kdot{fill:#2a3042;stroke:#fff;stroke-width:2}
715
+ .rk-chart .f-kbb{font:600 8px var(--mono);letter-spacing:-.02em;fill:#fff}
716
+ .rk-chart .f-kname{font:500 11.5px var(--sans);fill:var(--soft)}
717
+
718
+ /* "Cite HERCULEAN": the lion medal from the hero badge */
719
+ .cite-lion{display:inline-grid;place-items:center;width:42px;height:42px;margin-left:16px;border-radius:50%;background:var(--g);box-shadow:0 8px 20px -10px rgba(120,0,240,.6);vertical-align:.1em}
720
+ .cite-lion svg{width:36px;height:36px}
721
+ /* the mane turns slowly; the face stays upright (app.js speeds it up on hover) */
722
+ .cite-lion .mane{transform-box:view-box;transform-origin:12px 12.6px;animation:maneSpin 14s linear infinite}
723
+ @keyframes maneSpin{to{transform:rotate(360deg)}}
724
+ @media(prefers-reduced-motion:reduce){.cite-lion .mane{animation:none}}
725
+
726
+ /* coins for the Trade buttons (flat) */
727
+ .coin-layer{position:absolute;left:0;top:0;width:0;height:0;pointer-events:none;z-index:60}
728
+ .coin{position:absolute;width:16px;height:16px;border-radius:50%;display:grid;place-items:center;font:800 9.5px/1 Inter,system-ui,sans-serif;font-style:normal;color:#7a4a00;background:#f5bf2e;box-shadow:inset 0 0 0 1.5px #d69a12;will-change:transform,opacity}
729
+
730
+ /* the HERCULEAN name is set in Cinzel (Roman/Greek inscription capitals) wherever it appears as a mark */
731
+ .edge-l .e-name{font:700 12px var(--brand);letter-spacing:.34em}
732
+ .sec-h2 em.herc{font-family:var(--brand);font-style:normal;font-weight:600;letter-spacing:.01em}
733
 
734
+ /* ---------- phones (≤760px). Desktop layout is untouched: every rule below sits in this media query. ---------- */
735
+ @media (max-width:760px){
736
+ .hero{padding:96px 0 40px}
737
+ .award{font-size:.68rem;white-space:nowrap;gap:7px;padding-right:11px}.award .medal{width:24px;height:24px}.award .medal svg{width:20px;height:20px}
738
+ .hero-ctas{flex-wrap:nowrap;gap:10px}.hero-ctas .btn--lg{padding:11px 15px;font-size:.84rem;white-space:nowrap;gap:7px}
739
+ .stats{display:grid;grid-template-columns:repeat(5,auto);justify-content:space-between;gap:10px;margin:22px 0 24px}
740
+ .stats b{font-size:1.7rem}.stats span{font-size:.66rem;margin-top:5px}
741
+ .hero-viz{height:540px}
742
+ .hero-claim{white-space:normal}
743
+ .sec-h2.one-line{white-space:normal}
744
+ /* Rankings: the picker and the three charts keep a readable size and scroll sideways */
745
+ .rk-head{gap:14px}
746
+ .picker{grid-template-columns:auto repeat(6,50px);gap:3px 4px;padding:8px 10px;overflow-x:auto;justify-content:start}
747
+ .picker .pk-col{font-size:.58rem;white-space:nowrap}
748
+ .rk-chart{overflow-x:auto;-webkit-overflow-scrolling:touch;padding:10px 8px 6px}
749
+ .rk-chart > svg{width:760px;max-width:none}
750
+ .rk-view{position:sticky;left:0;width:calc(100vw - 52px)}
751
+ .rk-view button{gap:6px;padding:0 6px}.rk-view button svg{display:none}.rk-view button b{font-size:.8rem;white-space:nowrap}
752
+ .rk-view .go{display:none}
753
+ /* tables that scroll sideways get a fading edge as a hint */
754
+ .keys{white-space:normal;flex-wrap:wrap}
755
+ .board{-webkit-mask-image:linear-gradient(90deg,#000 88%,transparent);mask-image:linear-gradient(90deg,#000 88%,transparent)}
756
  }