YICHEN013 commited on
Commit
e3521a3
·
verified ·
1 Parent(s): 50f656f

Align leaderboard with the InferenceNet Challenge website

Browse files

Bring results into the first screen with the Challenge website’s light/dark styling, a five-metric table and the shared mountain visualization. Add an all-results view while preserving paired-only comparisons, DSH provenance and expandable scoring evidence.

Validated all 13 runs, five sorting metrics, 6 baseline / 7 harness / 6 paired views, search, Chinese/English, light/dark themes and narrow layouts. Published data, scoring and result archives are unchanged.

Files changed (5) hide show
  1. index.html +40 -22
  2. leaderboard.css +17 -7
  3. leaderboard.js +110 -29
  4. mountain.css +145 -0
  5. mountain.js +499 -0
index.html CHANGED
@@ -4,30 +4,33 @@
4
  <meta charset="utf-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
  <meta name="description" content="InferenceNet research leaderboard: single-pass generation, DeepAgents and an independent DeepSeek Harness result on 1,000 econometric replication tasks.">
7
- <meta name="color-scheme" content="dark">
8
- <title>InferenceNet · Agent & Harness Leaderboard</title>
9
  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c1219'/%3E%3Ctext x='5' y='23' font-family='sans-serif' font-size='21' fill='%2378dbc0'%3EiN%3C/text%3E%3C/svg%3E">
10
  <link rel="stylesheet" href="./leaderboard.css">
11
- <script src="./leaderboard.js" defer></script>
 
12
  </head>
13
  <body>
14
  <a class="skip" href="#rankings">Skip to rankings</a>
15
  <main class="page">
16
  <nav class="topbar" aria-label="Main navigation">
17
- <a class="brand" href="https://easonai-5589.github.io/inferencenet-challenge/" target="_blank" rel="noopener noreferrer"><span class="brand-mark" aria-hidden="true">iN</span>InferenceNet</a>
18
  <div class="nav-links">
 
19
  <a href="https://huggingface.co/datasets/CamoAiLab/InferenceNet" target="_blank" rel="noopener noreferrer" data-i18n="dataset">Dataset ↗</a>
20
  <a href="https://huggingface.co/spaces/CamoAiLab/InferenceNet-Leaderboard/tree/main/results" target="_blank" rel="noopener noreferrer" data-i18n="archive">Results archive ↗</a>
21
  <button id="language" type="button" aria-label="Switch to Chinese">中文</button>
 
22
  </div>
23
  </nav>
24
 
25
  <header class="hero">
26
- <p class="eyebrow"><span class="dot" aria-hidden="true"></span><span data-i18n="edition">RESEARCH LEADERBOARD · SEPTEMBER 2026</span></p>
27
- <h1 data-i18n="title">Models and harnesses.<br><span>Real research, reproduced.</span></h1>
28
- <p class="intro" data-i18n="intro">Explore single-pass agents, DeepAgents and a separate DeepSeek Harness result on real econometric replication tasks.</p>
29
  <div class="facts" aria-label="Dataset coverage">
30
- <span><strong id="model-count">—</strong><span data-i18n="models">requested model aliases</span></span>
31
  <span><strong id="group-count">—</strong><span data-i18n="groups">experiment groups</span></span>
32
  <span><strong id="task-count">—</strong><span data-i18n="tasks">tasks per group</span></span>
33
  <span><strong id="record-count">—</strong><span data-i18n="records">task records</span></span>
@@ -35,27 +38,23 @@
35
  </div>
36
  </header>
37
 
38
- <section class="protocols" aria-label="Experimental settings">
39
- <div><p class="protocol-tag baseline"><span aria-hidden="true">A</span><strong data-i18n="single">Single-pass Agent</strong></p><p data-i18n="singleDesc">One code-generation call, followed by execution. No iterative agent loop.</p></div>
40
- <div><p class="protocol-tag harness"><span aria-hidden="true">B</span><strong data-i18n="deepagents">DeepAgents</strong></p><p data-i18n="harnessDesc">DeepAgents plans, uses tools and revises code. Up to 6 model calls, plus final execution.</p></div>
41
- <div><p class="protocol-tag independent"><span aria-hidden="true">C</span><strong data-i18n="dsh">DeepSeek Harness · separate run</strong></p><p data-i18n="dshDesc">Official DSH SDK, requested v4-pro API alias. A fixed technical recovery selection; underlying model weights are unverified.</p></div>
42
- </section>
43
 
44
  <section id="rankings" aria-labelledby="rankings-title">
45
- <div class="section-head"><div><p class="eyebrow" data-i18n="resultsLabel">01 / RESULTS</p><h2 id="rankings-title" data-i18n="rankings">Agent & Harness leaderboard</h2></div><span class="badge" data-i18n="research">Research results · provisional</span></div>
46
- <p class="scope-note" data-i18n="scope">Locally scored, archived results. Official scorer parity is not yet verified. Different budgets and run protocols mean the lift is descriptive, not an equal-cost comparison.</p>
47
- <aside id="independent-notice" class="independent-notice" hidden><strong data-i18n="dshNoticeTitle">DSH is an independent result.</strong><p data-i18n="dshNotice">The request used deepseek-v4-pro; the underlying model weights were not verified. The fixed 1,000-task selection includes 87 technical replacement attempts (27 with changed input-schema prompts), with 30 task outcomes still unknown. It is excluded from the paired DeepAgents lift.</p><a id="dsh-protocol" data-i18n="dshProtocol" target="_blank" rel="noopener noreferrer">Read run protocol & evidence ↗</a></aside>
48
 
49
  <div class="toolbar">
50
  <div class="view-switch" role="group" aria-label="Leaderboard view">
 
51
  <button type="button" data-view="compare" aria-pressed="false" data-i18n="compare">Baseline / DeepAgents pairs</button>
52
  <button type="button" data-view="baseline" aria-pressed="false" data-i18n="single">Single-pass Agent</button>
53
- <button type="button" data-view="harness" aria-pressed="true" data-i18n="harness">All harness runs</button>
54
  </div>
55
  <a class="download" href="./leaderboard.csv" download data-i18n="download">Download CSV ↓</a>
56
  </div>
57
  <div class="filters">
58
- <label class="metric-control"><span data-i18n="metric">Metric</span><select id="metric">
59
  <optgroup label="Local paper profile" data-group="local">
60
  <option value="perfect" data-i18n="perfect">Full replication · local</option>
61
  </optgroup>
@@ -69,7 +68,15 @@
69
  <label id="sort-control"><span data-i18n="sort">Sort by</span><select id="sort"><option value="deepagents" data-i18n="harnessScore">DeepAgents score</option><option value="baseline" data-i18n="singleScore">Single-pass score</option><option value="gain" data-i18n="lift">Lift</option></select></label>
70
  <label class="search-control"><span data-i18n="search">Find a model</span><input id="search" type="search" placeholder="Search models…" autocomplete="off"></label>
71
  </div>
72
- <p id="metric-description" class="metric-description"></p>
 
 
 
 
 
 
 
 
73
  <p id="load-state" role="status" class="load-state">Loading archived results…</p>
74
  <div id="error" class="error" role="alert" hidden><p data-i18n="error">Results could not be loaded. Please retry or open the results archive.</p><button type="button" id="retry" data-i18n="retry">Retry</button></div>
75
  <div id="table-wrap" class="table-wrap" tabindex="0" aria-label="Scrollable leaderboard" hidden>
@@ -77,22 +84,33 @@
77
  </div>
78
  <p id="empty" class="load-state" hidden data-i18n="empty">No matching models.</p>
79
  <div class="table-foot"><span id="result-count" aria-live="polite"></span><span data-i18n="denominator">Every score uses all 1,000 tasks. Unknown and invalid outcomes earn no successes.</span></div>
 
80
  </section>
81
 
82
- <section class="method" aria-labelledby="method-title">
83
- <div class="section-head"><div><p class="eyebrow" data-i18n="methodLabel">02 / HOW TO READ THIS</p><h2 id="method-title" data-i18n="methodTitle">A comparison you can inspect.</h2></div><a href="./legacy.html" data-i18n="historical">Historical leaderboard ↗</a></div>
 
 
 
 
 
 
 
 
84
  <div class="method-grid">
85
  <article><h3 data-i18n="fixedTasks">Fixed task set</h3><p data-i18n="fixedDesc">The same Selected_1000 task IDs and dataset revision are used for every group. Each score is successes ÷ 1,000; failed or unknown tasks stay in the denominator.</p></article>
86
  <article><h3 data-i18n="separateMetrics">Two scoring profiles</h3><p data-i18n="profilesDesc">Full replication is the local paper metric. The four HF-style metrics reproduce the published descriptions locally; they are not yet verified against the official scorer.</p></article>
87
  <article><h3 data-i18n="budgetTitle">Read lift with the budgets</h3><p data-i18n="budgetDesc">Baseline: 1 model call. DeepAgents and DSH: up to 6 model calls and 4 trial tool calls per attempt. DSH adds 87 recovery attempts; full environment and reasoning parity is unverified. DSH has no paired lift; these are not equal-cost causal estimates.</p></article>
88
  </div>
89
- <details class="evidence"><summary data-i18n="evidence">Evidence status & metric definitions</summary>
90
  <p data-i18n="unknownDesc">“Metric unknown” means the selected metric cannot be assessed, including failed execution or unavailable outputs. It is different from a task whose final status is unknown. Neither is dropped or counted as a success.</p>
91
  <ul id="definitions"></ul>
92
  <div class="evidence-table-wrap"><table class="evidence-table"><caption data-i18n="evidenceCaption">Status from each accepted archive</caption><thead><tr><th data-i18n="model">Model / API alias</th><th data-i18n="protocol">Protocol</th><th data-i18n="evidenceCol">Evidence</th></tr></thead><tbody id="evidence-body"></tbody></table></div>
93
  <p data-i18n="verification">HF-style counts are checked against archived task flags. Full-replication counts for Sol and Opus come from accepted summaries; all other displayed runs, including DSH, have per-task local flags. This refresh does not rerun or rescore any experiment.</p>
94
  <p data-i18n="archiveNote">Per-run archive files remain frozen, including their original publication metadata. This page is a new display derived from those archives. GPT-5.5 and unarchived experiments are not included in this edition.</p>
 
95
  </details>
 
96
  </section>
97
  <footer><span>InferenceNet <span class="muted">/</span> <span data-i18n="footer">Econometric replication, measured.</span></span><div><a id="revision-link" href="https://huggingface.co/spaces/CamoAiLab/InferenceNet-Leaderboard/tree/main/results" target="_blank" rel="noopener noreferrer" data-i18n="source">Source snapshot ↗</a><a href="./leaderboard-data.json" data-i18n="json">Data JSON ↗</a><a href="https://easonai-5589.github.io/inferencenet-challenge/" target="_blank" rel="noopener noreferrer" data-i18n="project">Project ↗</a></div></footer>
98
  </main>
 
4
  <meta charset="utf-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
  <meta name="description" content="InferenceNet research leaderboard: single-pass generation, DeepAgents and an independent DeepSeek Harness result on 1,000 econometric replication tasks.">
7
+ <meta name="color-scheme" content="light dark">
8
+ <title>Leaderboard — InferenceNet Challenge</title>
9
  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c1219'/%3E%3Ctext x='5' y='23' font-family='sans-serif' font-size='21' fill='%2378dbc0'%3EiN%3C/text%3E%3C/svg%3E">
10
  <link rel="stylesheet" href="./leaderboard.css">
11
+ <link rel="stylesheet" href="./mountain.css">
12
+ <script type="module" src="./leaderboard.js"></script>
13
  </head>
14
  <body>
15
  <a class="skip" href="#rankings">Skip to rankings</a>
16
  <main class="page">
17
  <nav class="topbar" aria-label="Main navigation">
18
+ <a class="brand" href="https://easonai-5589.github.io/inferencenet-challenge/" target="_blank" rel="noopener noreferrer">InferenceNet <small>CHALLENGE</small></a>
19
  <div class="nav-links">
20
+ <a href="https://easonai-5589.github.io/inferencenet-challenge/" target="_blank" rel="noopener noreferrer" data-i18n="website">Project website ↗</a>
21
  <a href="https://huggingface.co/datasets/CamoAiLab/InferenceNet" target="_blank" rel="noopener noreferrer" data-i18n="dataset">Dataset ↗</a>
22
  <a href="https://huggingface.co/spaces/CamoAiLab/InferenceNet-Leaderboard/tree/main/results" target="_blank" rel="noopener noreferrer" data-i18n="archive">Results archive ↗</a>
23
  <button id="language" type="button" aria-label="Switch to Chinese">中文</button>
24
+ <button id="theme" type="button" aria-pressed="false"><span class="theme-icon" aria-hidden="true"></span><span id="theme-label">Light</span></button>
25
  </div>
26
  </nav>
27
 
28
  <header class="hero">
29
+ <p class="eyebrow"><span data-i18n="edition">LEADERBOARD · SEPTEMBER 2026</span></p>
30
+ <h1 data-i18n="title">InferenceNet Challenge Leaderboard</h1>
31
+ <p class="intro" data-i18n="intro">Models and harnesses on 1,000 econometric replication tasks.</p>
32
  <div class="facts" aria-label="Dataset coverage">
33
+ <span><strong id="model-count">—</strong><span data-i18n="models">model aliases</span></span>
34
  <span><strong id="group-count">—</strong><span data-i18n="groups">experiment groups</span></span>
35
  <span><strong id="task-count">—</strong><span data-i18n="tasks">tasks per group</span></span>
36
  <span><strong id="record-count">—</strong><span data-i18n="records">task records</span></span>
 
38
  </div>
39
  </header>
40
 
 
 
 
 
 
41
 
42
  <section id="rankings" aria-labelledby="rankings-title">
43
+ <div class="section-head"><div><p class="eyebrow" data-i18n="resultsLabel">01 / RESULTS</p><h2 id="rankings-title" class="sr-only" data-i18n="rankings">Agent & Harness leaderboard</h2></div><span class="badge" data-i18n="research">Research results · provisional</span></div>
44
+
45
+
46
 
47
  <div class="toolbar">
48
  <div class="view-switch" role="group" aria-label="Leaderboard view">
49
+ <button type="button" data-view="all" aria-pressed="true" data-i18n="all">All results</button>
50
  <button type="button" data-view="compare" aria-pressed="false" data-i18n="compare">Baseline / DeepAgents pairs</button>
51
  <button type="button" data-view="baseline" aria-pressed="false" data-i18n="single">Single-pass Agent</button>
52
+ <button type="button" data-view="harness" aria-pressed="false" data-i18n="harness">All harness runs</button>
53
  </div>
54
  <a class="download" href="./leaderboard.csv" download data-i18n="download">Download CSV ↓</a>
55
  </div>
56
  <div class="filters">
57
+ <label class="metric-control"><span data-i18n="metric">Rank by</span><select id="metric">
58
  <optgroup label="Local paper profile" data-group="local">
59
  <option value="perfect" data-i18n="perfect">Full replication · local</option>
60
  </optgroup>
 
68
  <label id="sort-control"><span data-i18n="sort">Sort by</span><select id="sort"><option value="deepagents" data-i18n="harnessScore">DeepAgents score</option><option value="baseline" data-i18n="singleScore">Single-pass score</option><option value="gain" data-i18n="lift">Lift</option></select></label>
69
  <label class="search-control"><span data-i18n="search">Find a model</span><input id="search" type="search" placeholder="Search models…" autocomplete="off"></label>
70
  </div>
71
+ <div class="display-tools">
72
+ <p id="metric-description" class="metric-description"></p>
73
+ <div class="display-switch" role="group" aria-label="Display format">
74
+ <button type="button" data-display="table" aria-pressed="true" data-i18n="tableView">Table</button>
75
+ <button type="button" data-display="mountain" aria-pressed="false" data-i18n="mountainView">Mountain view</button>
76
+ </div>
77
+ </div>
78
+ <p id="independent-summary" class="independent-summary" hidden><span class="independent-dot" aria-hidden="true"></span><span data-i18n="dshShort">DSH is an independent run with technical recovery, not a paired lift.</span> <a href="#protocol-details" id="show-protocol" data-i18n="detailsLink">Details ↗</a></p>
79
+ <figure id="ranking-mountain" class="mountain" hidden></figure>
80
  <p id="load-state" role="status" class="load-state">Loading archived results…</p>
81
  <div id="error" class="error" role="alert" hidden><p data-i18n="error">Results could not be loaded. Please retry or open the results archive.</p><button type="button" id="retry" data-i18n="retry">Retry</button></div>
82
  <div id="table-wrap" class="table-wrap" tabindex="0" aria-label="Scrollable leaderboard" hidden>
 
84
  </div>
85
  <p id="empty" class="load-state" hidden data-i18n="empty">No matching models.</p>
86
  <div class="table-foot"><span id="result-count" aria-live="polite"></span><span data-i18n="denominator">Every score uses all 1,000 tasks. Unknown and invalid outcomes earn no successes.</span></div>
87
+ <p class="publication-note" data-i18n="publicationNote">Research snapshot · local scoring; official scorer parity unverified. Run budgets differ.</p>
88
  </section>
89
 
90
+ <section class="method" aria-label="Evaluation details">
91
+ <details id="protocol-details" class="evidence">
92
+ <summary data-i18n="evaluationDetails">Evaluation protocol, definitions & evidence</summary>
93
+ <p class="scope-note" data-i18n="scope">Locally scored, archived results. Official scorer parity is not yet verified. Different budgets and run protocols mean the lift is descriptive, not an equal-cost comparison.</p>
94
+ <section class="protocols" aria-label="Experimental settings">
95
+ <div><p class="protocol-tag baseline"><span aria-hidden="true">A</span><strong data-i18n="single">Single-pass Agent</strong></p><p data-i18n="singleDesc">One code-generation call, followed by execution. No iterative agent loop.</p></div>
96
+ <div><p class="protocol-tag harness"><span aria-hidden="true">B</span><strong data-i18n="deepagents">DeepAgents</strong></p><p data-i18n="harnessDesc">DeepAgents plans, uses tools and revises code. Up to 6 model calls, plus final execution.</p></div>
97
+ <div><p class="protocol-tag independent"><span aria-hidden="true">C</span><strong data-i18n="dsh">DeepSeek Harness · separate run</strong></p><p data-i18n="dshDesc">Official DSH SDK, requested v4-pro API alias. A fixed technical recovery selection; underlying model weights are unverified.</p></div>
98
+ </section>
99
+ <aside id="independent-notice" class="independent-notice" hidden><strong data-i18n="dshNoticeTitle">DSH is an independent result.</strong><p data-i18n="dshNotice">The request used deepseek-v4-pro; the underlying model weights were not verified. The fixed 1,000-task selection includes 87 technical replacement attempts (27 with changed input-schema prompts), with 30 task outcomes still unknown. It is excluded from the paired DeepAgents lift.</p><a id="dsh-protocol" data-i18n="dshProtocol" target="_blank" rel="noopener noreferrer">Read run protocol & evidence ↗</a></aside>
100
  <div class="method-grid">
101
  <article><h3 data-i18n="fixedTasks">Fixed task set</h3><p data-i18n="fixedDesc">The same Selected_1000 task IDs and dataset revision are used for every group. Each score is successes ÷ 1,000; failed or unknown tasks stay in the denominator.</p></article>
102
  <article><h3 data-i18n="separateMetrics">Two scoring profiles</h3><p data-i18n="profilesDesc">Full replication is the local paper metric. The four HF-style metrics reproduce the published descriptions locally; they are not yet verified against the official scorer.</p></article>
103
  <article><h3 data-i18n="budgetTitle">Read lift with the budgets</h3><p data-i18n="budgetDesc">Baseline: 1 model call. DeepAgents and DSH: up to 6 model calls and 4 trial tool calls per attempt. DSH adds 87 recovery attempts; full environment and reasoning parity is unverified. DSH has no paired lift; these are not equal-cost causal estimates.</p></article>
104
  </div>
105
+ <h3 class="evidence-heading" data-i18n="evidence">Evidence status & metric definitions</h3>
106
  <p data-i18n="unknownDesc">“Metric unknown” means the selected metric cannot be assessed, including failed execution or unavailable outputs. It is different from a task whose final status is unknown. Neither is dropped or counted as a success.</p>
107
  <ul id="definitions"></ul>
108
  <div class="evidence-table-wrap"><table class="evidence-table"><caption data-i18n="evidenceCaption">Status from each accepted archive</caption><thead><tr><th data-i18n="model">Model / API alias</th><th data-i18n="protocol">Protocol</th><th data-i18n="evidenceCol">Evidence</th></tr></thead><tbody id="evidence-body"></tbody></table></div>
109
  <p data-i18n="verification">HF-style counts are checked against archived task flags. Full-replication counts for Sol and Opus come from accepted summaries; all other displayed runs, including DSH, have per-task local flags. This refresh does not rerun or rescore any experiment.</p>
110
  <p data-i18n="archiveNote">Per-run archive files remain frozen, including their original publication metadata. This page is a new display derived from those archives. GPT-5.5 and unarchived experiments are not included in this edition.</p>
111
+
112
  </details>
113
+ <a class="historical" href="./legacy.html" data-i18n="historical">Historical leaderboard ↗</a>
114
  </section>
115
  <footer><span>InferenceNet <span class="muted">/</span> <span data-i18n="footer">Econometric replication, measured.</span></span><div><a id="revision-link" href="https://huggingface.co/spaces/CamoAiLab/InferenceNet-Leaderboard/tree/main/results" target="_blank" rel="noopener noreferrer" data-i18n="source">Source snapshot ↗</a><a href="./leaderboard-data.json" data-i18n="json">Data JSON ↗</a><a href="https://easonai-5589.github.io/inferencenet-challenge/" target="_blank" rel="noopener noreferrer" data-i18n="project">Project ↗</a></div></footer>
116
  </main>
leaderboard.css CHANGED
@@ -1,7 +1,17 @@
1
- :root{color-scheme:dark;--bg:#0c1219;--surface:#111c26;--line:#27343f;--text:#eef4f5;--muted:#a3b3c0;--base:#92b7e6;--accent:#78dbc0;--amber:#e9c078;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans SC",sans-serif;font-synthesis:none}
2
- *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font-size:15px;line-height:1.6}a{color:inherit;text-decoration:none}a:hover{text-decoration:underline;text-underline-offset:4px}button,input,select{font:inherit}button,a,input,select,summary{touch-action:manipulation}button{cursor:pointer}button:disabled{cursor:default}button:focus-visible,a:focus-visible,select:focus-visible,input:focus-visible,summary:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--accent);outline-offset:4px}button,input,select{border:1px solid var(--line);background:var(--surface);color:var(--text);border-radius:6px}button{padding:8px 14px}p,h1,h2,h3{margin:0}h1,h2,h3{line-height:1.2;font-weight:600}h2{font-size:26px;letter-spacing:-.6px}h3{font-size:16px}strong{font-weight:600}.page{max-width:1200px;margin:auto;padding:0 36px}.topbar{display:flex;align-items:center;justify-content:space-between;gap:20px;min-height:88px;border-bottom:1px solid var(--line)}.brand{font-size:20px;font-weight:650;letter-spacing:-.6px;display:flex;align-items:center;gap:11px}.brand-mark{font-size:15px;letter-spacing:-1.5px;border:1px solid #497364;border-radius:8px;width:32px;height:32px;display:grid;place-items:center;color:var(--accent)}.nav-links{display:flex;align-items:center;gap:26px;font-size:13px;color:var(--muted)}#language{padding:5px 12px;font-size:12px;background:transparent}.hero{padding:52px 0 0}.eyebrow{font-size:11px;letter-spacing:1.7px;color:var(--muted);font-weight:650}.hero>.eyebrow{display:flex;align-items:center;gap:8px;color:var(--accent)}.dot{width:6px;height:6px;border-radius:50%;background:var(--accent)}h1{font-size:clamp(34px,4.8vw,54px);letter-spacing:-2px;margin:18px 0 16px}h1 span{color:var(--accent)}.intro{max-width:660px;color:var(--muted);font-size:17px;line-height:1.7}.facts{display:flex;gap:32px;align-items:center;flex-wrap:wrap;padding:24px 0;margin-top:14px;border-bottom:1px solid var(--line);font-size:12px;color:var(--muted)}.facts>span{display:flex;align-items:baseline;gap:8px}.facts strong{color:var(--text);font-size:23px;font-variant-numeric:tabular-nums;letter-spacing:-.7px}.facts .update{margin-left:auto}.protocols{display:grid;grid-template-columns:1fr 1fr;border-bottom:1px solid var(--line);gap:40px;padding:24px 0}.protocols>div+div{border-left:1px solid var(--line);padding-left:36px}.protocol-tag{display:flex;align-items:center;gap:10px;font-size:14px}.protocol-tag>span{display:grid;place-items:center;border:1px solid currentColor;width:22px;height:22px;border-radius:5px;font-size:10px}.baseline{color:var(--base)}.harness{color:var(--accent)}.protocols p+p{font-size:13px;color:var(--muted);margin-top:8px;max-width:450px}#rankings{padding-top:40px;scroll-margin-top:20px}.section-head{display:flex;align-items:center;justify-content:space-between;gap:20px}.section-head .eyebrow{margin-bottom:10px}.badge{color:var(--amber);background:#272419;border:1px solid #4b422e;border-radius:4px;padding:5px 9px;font-size:11px;white-space:nowrap}.scope-note{color:var(--muted);font-size:12px;max-width:840px;margin-top:14px}.toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:24px;padding-bottom:16px;border-bottom:1px solid var(--line)}.view-switch{display:flex;gap:4px;flex-wrap:wrap;background:#101922;border:1px solid var(--line);padding:4px;border-radius:8px}.view-switch button{background:transparent;border-color:transparent;color:var(--muted);font-size:13px;padding:7px 15px}.view-switch button[aria-pressed=true]{background:#273b3a;border-color:#49665c;color:var(--text)}.view-switch button:hover{color:var(--text)}.download{font-size:12px;color:var(--accent);white-space:nowrap}.filters{display:flex;align-items:flex-end;gap:16px;padding:18px 0 10px}.filters label{display:flex;flex-direction:column;gap:6px;min-width:0}.filters label>span{font-size:11px;color:var(--muted)}input,select{height:39px;padding:6px 10px;font-size:12px;max-width:100%}.metric-control{min-width:270px!important}.search-control{margin-left:auto;width:210px}input::placeholder{color:#8b9aa8}.metric-description{min-height:36px;color:var(--muted);font-size:12px;padding:4px 0 14px}.profile{font-family:ui-monospace,monospace;color:var(--accent);font-size:11px;margin-right:8px}.table-wrap{overflow:auto;border:1px solid var(--line);border-radius:8px;background:#0e1821}table{border-collapse:collapse;width:100%;font-variant-numeric:tabular-nums;text-align:left;white-space:nowrap}th{font-weight:500;background:#15212b;color:var(--muted);padding:14px 18px;font-size:11px;border-bottom:1px solid var(--line)}td{padding:20px 18px;border-bottom:1px solid #23313b;font-size:13px}tbody tr:last-child td{border-bottom:none}tbody tr:hover{background:#15242c}.rank{width:45px;color:var(--muted);font-size:12px}.model-cell{min-width:225px}.model-name{font-size:14px;font-weight:550}.model-meta{display:flex;gap:7px;align-items:center;margin-top:5px;color:var(--muted);font-size:10px}.tag{display:inline-block;font-size:9px;color:var(--amber);border:1px solid #4d442e;padding:0 4px;border-radius:3px}.score-cell{min-width:155px;width:21%}.score{font-size:19px;letter-spacing:-.3px;font-weight:550}.score small{font-size:11px;font-weight:400;margin-left:2px}.track{height:4px;background:#283643;border-radius:2px;margin:7px 0 5px;max-width:150px}.fill{height:100%;border-radius:2px;background:var(--base)}.harness .fill{background:var(--accent)}.counts{font-size:10px;color:var(--muted)}.unknown{display:block;font-size:10px;color:#96a8b4;margin-top:2px}.gain{color:var(--accent);font-size:16px}.gain.negative{color:#f59d9d}.gain small{font-size:10px;margin-left:3px}.source-cell{font-size:11px;color:var(--muted)}.source-cell a{display:block;margin:3px 0}.table-foot{display:flex;justify-content:space-between;gap:24px;color:var(--muted);font-size:10px;margin-top:12px}.table-foot>span:last-child{text-align:right;max-width:620px}.load-state{padding:32px;text-align:center;color:var(--muted)}.error{padding:24px;border:1px solid #a65f5f;border-radius:8px;color:#f6b4b4}.error button{margin-top:12px}.method{padding-top:48px}.method .section-head>a{font-size:12px;color:var(--muted)}.method-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:34px;margin:24px 0 26px}.method p{font-size:12px;color:var(--muted);margin-top:11px;line-height:1.8}.evidence{border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:18px 0}.evidence summary{font-size:13px;cursor:pointer;color:var(--text)}.evidence li{font-size:12px;color:var(--muted);margin:7px 0}.evidence-table-wrap{overflow:auto;margin-top:22px}.evidence-table th,.evidence-table td{font-size:11px;padding:12px}.evidence-table caption{text-align:left;margin:12px 0;font-size:12px;color:var(--muted)}.evidence-table td span{display:block;font-size:10px;color:var(--muted)}footer{display:flex;justify-content:space-between;gap:20px;padding:28px 0 36px;font-size:11px;color:var(--muted)}footer>div{display:flex;gap:20px}.muted{color:#526271;margin:0 5px}[hidden]{display:none!important}.sr-only,.skip:not(:focus){position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}.skip:focus{display:block;padding:10px}noscript{padding:30px;display:block}
3
- @media(max-width:760px){.page{padding:0 20px}.topbar{min-height:76px}.nav-links{gap:12px}.nav-links a{font-size:11px}.brand{font-size:18px}.brand-mark{display:none}.hero{padding-top:34px}h1{letter-spacing:-1.3px}.intro{font-size:15px}.facts{gap:18px}.facts .update{width:100%;margin-left:0}.protocols{gap:18px}.protocols>div+div{padding-left:18px}.protocols p+p{font-size:12px}.protocol-tag{font-size:12px}h2{font-size:22px}.section-head{align-items:flex-start;flex-direction:column;gap:12px}.toolbar{align-items:flex-start;flex-direction:column}.view-switch{width:100%}.view-switch button{flex:1;font-size:11px;padding:8px}.filters{flex-wrap:wrap;gap:12px}.metric-control{width:100%;min-width:0!important}.filters label:not(.metric-control){flex:1}.search-control{margin-left:0;width:auto}.metric-description{padding-bottom:16px}.method-grid{grid-template-columns:1fr;gap:20px}.method{padding-top:36px}.table-foot{flex-direction:column;gap:5px}.table-foot>span:last-child{text-align:left}.table-wrap td{padding:17px 14px}.method .section-head{gap:14px}footer{flex-direction:column;gap:12px}}
4
- tbody th[scope=row]{background:transparent;color:var(--text);border-bottom-color:#23313b}tbody tr:last-child th{border-bottom:none}
5
- @media(prefers-reduced-motion:no-preference){button,a{transition:background .12s,color .12s}}
6
- .protocols{grid-template-columns:repeat(3,1fr);gap:24px}.protocols>div+div{padding-left:24px}.independent{color:var(--amber)}.independent .fill{background:var(--amber)}.independent-notice{margin-top:18px;padding:16px 20px;border:1px solid #4b422e;border-left:3px solid var(--amber);border-radius:6px;background:#1c1e1b;font-size:12px;color:var(--muted)}.independent-notice strong{color:var(--amber)}.independent-notice p{margin:5px 0 7px;max-width:960px}.independent-notice a{color:var(--amber)}.run-protocol{display:block;font-size:10px;margin-top:5px}.model-name{white-space:normal;max-width:355px}.model-meta{flex-wrap:wrap}.facts{gap:24px}.facts .update{font-size:11px}
7
- @media(max-width:760px){.protocols{grid-template-columns:1fr;gap:18px}.protocols>div+div{border-left:0;border-top:1px solid var(--line);padding:18px 0 0}.facts{gap:14px 24px}.independent-notice{padding:14px}.view-switch button{flex-basis:100%}.model-cell{min-width:225px}.model-name{max-width:275px}}
 
 
 
 
 
 
 
 
 
 
 
1
+ /* InferenceNet Challenge design tokens, shared with the project website. */
2
+ :root{color-scheme:light;--paper:#fff;--surface:#fff;--ink:#191919;--soft:#555;--muted:#666;--line:#dedede;--strong-line:#bbb;--tint:#f5f5f5;--selected:#f1f2fa;--independent:#79590d;--independent-bg:#faf6ea;--sans:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",Arial,sans-serif;--mono:ui-monospace,SFMono-Regular,Consolas,monospace}
3
+ html[data-theme=dark]{color-scheme:dark;--paper:#111;--surface:#181818;--ink:#eee;--soft:#bdbdbd;--muted:#aaa;--line:#363636;--strong-line:#666;--tint:#222;--selected:#252736;--independent:#e6c772;--independent-bg:#28251c}
4
+ *{box-sizing:border-box}html{scroll-behavior:smooth;scroll-padding-top:28px;-webkit-text-size-adjust:100%}body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--sans);font-size:15px;line-height:1.65;-webkit-font-smoothing:antialiased}a{color:inherit;text-decoration:none}a:hover{text-decoration:underline;text-underline-offset:4px}button,input,select{font:inherit;color:inherit}button{cursor:pointer}button:disabled{opacity:.4;cursor:not-allowed}[hidden]{display:none!important}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,summary:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--ink);outline-offset:3px}h1,h2,h3,p,figure{margin:0}button{background:var(--paper);border:1px solid var(--line);border-radius:3px}button:hover{background:var(--tint)}.page{width:min(1320px,calc(100% - 80px));margin:auto}.skip{position:fixed;top:-100px;left:16px;padding:12px;background:var(--ink);color:var(--paper);z-index:10}.skip:focus{top:12px}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
5
+ .topbar{display:flex;align-items:center;justify-content:space-between;gap:24px;min-height:82px;border-bottom:1px solid var(--line)}.brand{font-size:24px;font-weight:650;letter-spacing:-1px;white-space:nowrap}.brand small{font-family:var(--mono);font-size:9px;font-weight:400;letter-spacing:1px;margin-left:10px;vertical-align:middle}.nav-links{display:flex;align-items:center;gap:24px;font-size:13px;color:var(--soft)}.nav-links button{min-height:38px;padding:7px 12px;color:var(--ink)}#theme{display:flex;gap:8px;align-items:center}.theme-icon{width:14px;height:14px;border:1.5px solid currentColor;border-radius:50%;background:linear-gradient(90deg,currentColor 50%,transparent 50%)}
6
+ .hero{padding:37px 0 27px;border-bottom:1px solid var(--line)}.eyebrow{font-size:10px;font-weight:600;letter-spacing:.09em;color:var(--muted);text-transform:uppercase}.hero h1{font-size:clamp(28px,3.3vw,42px);font-weight:650;letter-spacing:-.035em;line-height:1.2;margin-top:13px}.intro{font-size:16px;color:var(--soft);margin-top:12px}.facts{display:flex;flex-wrap:wrap;align-items:baseline;gap:10px 25px;margin-top:22px;font-size:12px;color:var(--muted)}.facts>span{display:flex;gap:6px;align-items:baseline}.facts strong{font-weight:650;font-size:15px;color:var(--ink);font-variant-numeric:tabular-nums}.facts .update{margin-left:auto;font-size:11px}
7
+ #rankings{padding-top:25px}.section-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:15px}.badge{font-size:10px;color:var(--muted);padding:3px 7px;border:1px solid var(--line);border-radius:3px}.toolbar{display:flex;justify-content:space-between;align-items:center;gap:14px}.view-switch{display:flex;gap:20px;flex-wrap:wrap}.view-switch button{font-size:13px;padding:9px 0;border:0;border-radius:0;border-bottom:2px solid transparent;color:var(--soft);background:none}.view-switch button[aria-pressed=true]{color:var(--ink);border-bottom-color:var(--ink);font-weight:650}.download{font-size:12px;text-decoration:underline;text-underline-offset:4px;white-space:nowrap}.filters{display:flex;align-items:end;gap:16px;margin-top:19px}.filters label{display:flex;align-items:center;gap:9px;font-size:12px;color:var(--soft)}select,input{border:1px solid var(--strong-line);background:var(--paper);min-height:38px;border-radius:3px;padding:6px 10px;font-size:12px;max-width:100%}.search-control{margin-left:auto}.search-control input{width:180px}.display-tools{display:flex;gap:24px;justify-content:space-between;align-items:center;margin:14px 0 8px}.metric-description{font-size:11px;color:var(--muted);max-width:850px}.display-switch{display:flex;flex-shrink:0}.display-switch button{font-size:11px;padding:5px 11px;border-radius:0}.display-switch button:first-child{border-radius:3px 0 0 3px}.display-switch button:last-child{border-radius:0 3px 3px 0;border-left:0}.display-switch button[aria-pressed=true]{background:var(--ink);color:var(--paper);border-color:var(--ink)}.independent-summary{font-size:11px;color:var(--soft);margin:8px 0 15px;display:flex;align-items:baseline;gap:7px;flex-wrap:wrap}.independent-summary>a{color:var(--independent);text-decoration:underline;text-underline-offset:3px}.independent-dot{width:5px;height:5px;border-radius:50%;background:var(--independent);align-self:center}.load-state,.error{padding:35px 0;color:var(--muted)}.error button{padding:6px 15px;margin-top:10px}
8
+ .table-wrap{overflow:auto;border-block:1px solid var(--line);overscroll-behavior-x:contain;max-width:100%;margin-top:12px}table{width:100%;border-collapse:separate;border-spacing:0;text-align:left;font-size:13px;font-variant-numeric:tabular-nums}th,td{padding:17px 12px;border-bottom:1px solid var(--line);vertical-align:middle}thead th{color:var(--soft);font-size:11px;font-weight:550;background:var(--paper);line-height:1.5}thead button{border:0;padding:0;text-align:inherit;font-size:inherit;background:transparent!important;white-space:normal}tbody tr:last-child>*{border-bottom:0}.table-wrap table{min-width:1080px}.rank{width:45px;text-align:center;color:var(--muted)}.rank-number{display:inline-flex;align-items:center;justify-content:center;width:25px;height:25px;border-radius:50%;font-size:12px}.rank-1{background:#eadcaa;color:#4d3c08;font-weight:650}.rank-2{background:#e7e8eb;color:#454850;font-weight:650}.rank-3{background:#eedecf;color:#6a4933;font-weight:650}.model-cell{font-size:13px;font-weight:600;white-space:nowrap;min-width:175px;letter-spacing:-.02em}.model-heading{min-width:175px}.numeric{text-align:right;min-width:120px}.selected-metric{background:var(--selected);font-weight:650}.metric-value{position:relative;display:inline-block;cursor:help}.numeric small{font-size:10px;margin-left:2px}.metric-tooltip{display:none;position:absolute;z-index:5;bottom:calc(100% + 8px);right:-10px;min-width:170px;max-width:220px;white-space:normal;padding:7px 10px;border:1px solid var(--strong-line);border-radius:4px;color:var(--ink);background:var(--surface);font-size:11px;font-weight:400;text-align:left;box-shadow:0 3px 10px #0001}.metric-value:hover .metric-tooltip,.metric-value:focus .metric-tooltip{display:block}.protocol-cell{min-width:120px}.protocol-label{font-size:11px;display:inline-block;border:1px solid var(--line);border-radius:3px;padding:2px 7px;white-space:nowrap;background:var(--surface)}.no-harness{color:var(--muted);border-color:transparent;background:none;padding-left:0}.independent-tag{display:block;font-size:9px;color:var(--independent);margin-top:3px;white-space:nowrap}.source-cell{font-size:10px;white-space:nowrap;color:var(--muted)}.source-cell a{display:block}.evidence-col{min-width:80px}tbody tr:hover>td:not(.selected-metric),tbody tr:hover>th{background:var(--tint)}.table-foot{display:flex;justify-content:space-between;gap:18px;font-size:11px;color:var(--muted);padding:13px 0}.table-foot #result-count{white-space:nowrap}.score-cell{text-align:right;min-width:150px}.score{font-size:17px;font-weight:600}.counts,.unknown{display:block;font-size:10px;color:var(--muted);font-weight:400}.track{display:none}.gain{font-size:17px;color:#2e6b50}.gain small{display:block;font-size:10px;color:var(--muted)}.negative{color:#9c3e35}
9
+ #ranking-mountain{margin-top:14px}.mountain-head{display:none!important}.mountain-caption .mountain-legend{display:none}.mountain-pill{font-size:11px!important}.mountain-pill-label{max-width:235px}.method{margin-top:30px;border-top:1px solid var(--line);padding-top:18px}.evidence summary{font-size:13px;cursor:pointer;font-weight:550;list-style-position:inside;padding:7px 0}.evidence p{font-size:12px;color:var(--soft);margin:12px 0;line-height:1.8}.scope-note{max-width:1050px}.protocols{display:grid;grid-template-columns:repeat(3,1fr);gap:28px;margin-top:22px}.protocol-tag{display:flex;gap:10px;align-items:center;color:var(--ink)!important}.protocol-tag>span{font-family:var(--mono);font-size:10px;border:1px solid var(--line);padding:0 5px}.protocol-tag strong{font-weight:600}.independent-notice{padding:15px 20px;margin:24px 0;border-left:2px solid var(--independent);background:var(--independent-bg);font-size:12px}.independent-notice p{margin:7px 0}.independent-notice a{text-decoration:underline;text-underline-offset:3px}.method-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:28px;margin-top:24px}.method-grid h3,.evidence-heading{font-size:13px;font-weight:600}.evidence-heading{margin-top:25px}.evidence ul{font-size:12px;color:var(--soft);padding-left:18px;line-height:1.8}.evidence li{margin:8px 0}.evidence-table-wrap{overflow:auto;margin:20px 0}.evidence-table{font-size:11px;min-width:650px}.evidence-table caption{text-align:left;color:var(--muted);padding-bottom:10px}.evidence-table td span{display:block;font-size:10px;color:var(--muted)}.evidence-table th,.evidence-table td{padding:10px}.historical{display:inline-block;font-size:11px;color:var(--muted);margin-top:14px}footer{display:flex;justify-content:space-between;gap:20px;border-top:1px solid var(--line);padding:25px 0;margin-top:30px;font-size:11px;color:var(--muted)}footer>div{display:flex;gap:20px}.muted{margin:0 5px}noscript{padding:20px}
10
+ @media(min-width:1800px){.page{width:min(1440px,calc(100% - 120px))}.hero{padding-top:45px}.hero h1{font-size:46px}th,td{padding-block:19px}}
11
+ @media(max-width:1000px){.page{width:calc(100% - 48px)}.nav-links{gap:14px}.brand small{display:none}.filters{flex-wrap:wrap}.search-control{margin-left:auto}.filters label{flex-direction:column;align-items:stretch;gap:4px}.metric-control{flex:1}.table-foot{flex-direction:column;gap:4px}.protocols,.method-grid{grid-template-columns:1fr}.protocols{gap:5px}.method-grid{gap:12px}.nav-links>a:nth-child(3){display:none}}
12
+ @media(max-width:620px){.page{width:calc(100% - 32px)}.topbar{flex-wrap:wrap;gap:12px;padding:18px 0;min-height:76px}.brand{font-size:21px}.nav-links{gap:8px;font-size:11px;flex-wrap:wrap;width:100%}.nav-links button{padding:5px 9px;min-height:34px}#language{margin-left:auto}.hero{padding-top:28px}.hero h1{font-size:29px}.intro{font-size:14px}.facts{gap:8px 15px;margin-top:18px}.facts .update{margin-left:0;width:100%}.toolbar{align-items:flex-start;flex-direction:column;gap:12px}.view-switch{gap:18px}.view-switch button{font-size:12px;padding:6px 0}.download{align-self:flex-end;margin-top:-3px}.filters{gap:12px}.filters label{font-size:11px}.search-control{width:100%;margin:0}.search-control input{width:100%}.display-tools{align-items:flex-start;gap:12px}.display-switch{align-self:center}.metric-description{font-size:10px}.section-head{margin-bottom:10px}.independent-summary{font-size:10px}.table-wrap table{min-width:1000px}.model-cell{min-width:160px}.numeric{min-width:108px}.table-wrap th,.table-wrap td{padding:14px 10px}footer{flex-direction:column;gap:12px}footer>div{flex-wrap:wrap;gap:14px}.mountain-pill-label{max-width:none}.mountain-caption{font-size:10px!important}}
13
+ @media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}*{transition:none!important}}
14
+
15
+ .publication-note{font-size:11px;color:var(--muted);margin-top:2px}
16
+
17
+ .evidence-gap{display:block;font-size:9px;color:var(--independent);margin-top:3px}
leaderboard.js CHANGED
@@ -1,3 +1,4 @@
 
1
  'use strict';
2
  const $ = id => document.getElementById(id);
3
  const metricKeys = ['perfect', 'partial_replication', 'compilation_success', 'coefficient_direction', 'significance_level'];
@@ -21,9 +22,9 @@ Object.assign(en, {
21
  });
22
  const zh = {
23
  dataset: '数据集 ↗', archive: '结果存档 ↗', edition: '研究榜单 · 2026 年 9 月',
24
- title: '模型与 Harness,<br><span>复现真实研究。</span>',
25
- intro: '查看单次 Agent、DeepAgents 和独立 DeepSeek Harness 实验在真实计量经济学复现任务上的表现。',
26
- models: '种请求模型别名', groups: '组实验', tasks: '题 / 组', records: '条任务记录', published: '发布于 {date}',
27
  single: '单次 Agent', harness: '全部 Harness 实验', deepagents: 'DeepAgents',
28
  dsh: 'DeepSeek Harness · 独立实验',
29
  dshDesc: '使用官方 DSH SDK,请求 v4-pro API 别名。按固定规则纳入技术补测,底层模型权重未经核实。',
@@ -66,13 +67,50 @@ const zh = {
66
  let language = 'en';
67
  try { language = localStorage.getItem('inferencenet-language') === 'zh' ? 'zh' : 'en'; } catch (_) {}
68
  let data = null;
69
- let view = 'harness';
 
 
 
70
  let sortBy = 'deepagents';
71
  const t = key => (language === 'zh' ? zh[key] : en[key]) ?? en[key] ?? key;
72
  const esc = value => String(value).replace(/[&<>"']/g, char => ({'&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;'}[char]));
73
  const sourceURL = (path, tree = false) => `https://huggingface.co/spaces/CamoAiLab/InferenceNet-Leaderboard/${tree ? 'tree' : 'blob'}/${data.source_revision}/${path}`;
74
  const external = (url, label) => `<a href="${esc(url)}" target="_blank" rel="noopener noreferrer">${esc(label)} ↗</a>`;
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  function applyLanguage() {
77
  document.documentElement.lang = language === 'zh' ? 'zh-CN' : 'en';
78
  document.querySelectorAll('[data-i18n]').forEach(el => { el.innerHTML = t(el.dataset.i18n); });
@@ -83,6 +121,7 @@ function applyLanguage() {
83
  document.querySelector('[data-group=local]').label = t('groupLocal');
84
  document.querySelector('[data-group=hf]').label = t('groupHF');
85
  $('definitions').innerHTML = metricKeys.map(key => `<li><strong>${esc(t(key))}:</strong> ${esc(t(key + 'Desc'))}</li>`).join('');
 
86
  render();
87
  }
88
 
@@ -113,12 +152,36 @@ function validate(candidate) {
113
  // Pairing is intentionally limited to the two historical arms. Independent
114
  // harness entries have their own rank and can never enter a paired lift.
115
  function rankedEntries(snapshot, selectedView, key, selectedSort, query) {
116
- const entries = selectedView === 'harness'
117
- ? snapshot.models.flatMap(model => Object.entries(model.arms).filter(([id]) => id !== 'baseline').map(([armId, arm]) => ({model, armId, arm, name: arm.display_name || model.name, value: arm.metrics[key].count})))
118
- : snapshot.models.map(model => ({model, armId: selectedView === 'compare' ? null : 'baseline', arm: model.arms.baseline, name: model.name,
119
- value: selectedView === 'baseline' ? model.arms.baseline.metrics[key].count : selectedSort === 'gain' ? model.arms.deepagents.metrics[key].count - model.arms.baseline.metrics[key].count : model.arms[selectedSort].metrics[key].count}));
 
 
120
  const search = query.toLowerCase().trim();
121
- return {total: entries.length, entries: entries.filter(entry => `${entry.name} ${entry.model.id} ${entry.armId || ''}`.toLowerCase().includes(search)).sort((a, b) => b.value - a.value || a.name.localeCompare(b.name))};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  }
123
 
124
  function scoreCell(arm, key, style) {
@@ -135,45 +198,42 @@ function evidenceState(arm) {
135
 
136
  function render() {
137
  const key = $('metric').value;
138
- $('metric-description').innerHTML = `<span class="profile">${esc(t(key === 'perfect' ? 'localProfile' : 'hfProfile'))}</span>${esc(t(key + 'Desc'))}`;
139
  if (!data) return;
140
- const count = (m, arm) => m.arms[arm].metrics[key].count;
141
  const {total, entries} = rankedEntries(data, view, key, sortBy, $('search').value);
142
  $('published-date').textContent = t('published').replace('{date}', data.published_date);
143
  $('sort-control').hidden = view !== 'compare';
144
- $('table-wrap').hidden = entries.length === 0;
145
  $('empty').hidden = entries.length !== 0;
146
  $('result-count').textContent = t(view === 'compare' ? 'matched' : 'matchedRuns').replace('{n}', entries.length).replace('{total}', total);
147
- $('caption').textContent = `${t(view === 'compare' ? 'compare' : view === 'baseline' ? 'single' : 'harness')} — ${t(key)}`;
148
  const th = (text, cls = '') => `<th scope="col" class="${cls}">${esc(text)}</th>`;
149
- $('table-head').innerHTML = '<tr>' + th(t('rank'), 'rank') + th(t('model')) + (view === 'compare' ? th(t('single'), 'baseline') + th(t('deepagents'), 'harness') + th(t('lift')) : th(t('score'), view === 'baseline' ? 'baseline' : 'harness')) + th(t('evidenceCol')) + '</tr>';
150
- let lastValue = null, rank = 0;
151
- $('table-body').innerHTML = entries.map((entry, index) => {
152
- const {model, arm, armId, name, value: currentValue} = entry;
153
- if (currentValue !== lastValue) rank = index + 1;
154
- lastValue = currentValue;
155
  const arms = model.arms;
156
- const gap = view === 'compare' ? !arms.baseline.complete || !arms.deepagents.complete : !arm.complete;
157
- const badge = gap ? `<span class="tag">${esc(t('review'))}</span>` : '';
158
- const independent = armId === 'dsh';
159
- const protocol = view === 'harness' ? `<span class="run-protocol ${independent ? 'independent' : 'harness'}">${esc(independent ? t('independentRun') : 'DeepAgents')}</span>` : '';
160
- const identity = `<td class="rank">${String(rank).padStart(2, '0')}</td><th scope="row" class="model-cell"><div class="model-name">${esc(name)}</div>${protocol}<div class="model-meta">1,000 ${language === 'zh' ? '题 / 组' : 'tasks / group'} ${badge}</div></th>`;
161
  let scores = '', sources = '';
162
  if (view === 'compare') {
163
- const gain = (count(model, 'deepagents') - count(model, 'baseline')) / 10;
164
- scores = scoreCell(arms.baseline, key, 'baseline') + scoreCell(arms.deepagents, key, 'harness') + `<td class="gain ${gain < 0 ? 'negative' : ''}">${gain > 0 ? '+' : ''}${gain.toFixed(1)}<small>${esc(t('pp'))}</small></td>`;
165
  sources = external(sourceURL(arms.baseline.path, true), 'A') + external(sourceURL(arms.deepagents.path, true), 'B');
166
  } else {
167
- scores = scoreCell(arm, key, view === 'baseline' ? 'baseline' : independent ? 'independent' : 'harness');
168
- sources = external(sourceURL(arm.path + '/results.jsonl'), t('recordsLink')) + external(sourceURL(arm.path + '/config.json'), t('config'));
169
- if (independent) sources += external(sourceURL(arm.protocol_url), t('protocol'));
170
  }
171
  return `<tr data-model="${esc(model.id)}" data-arm="${esc(armId || 'paired')}">${identity}${scores}<td class="source-cell">${sources}</td></tr>`;
172
  }).join('');
173
  $('evidence-body').innerHTML = data.models.flatMap(m => Object.entries(m.arms).map(([id, arm]) => `<tr><th scope="row">${esc(arm.display_name || m.name)}</th><td>${esc(id === 'baseline' ? t('single') : id === 'dsh' ? t('dsh') : 'DeepAgents')}</td>${evidenceState(arm)}</tr>`)).join('');
174
  const dsh = data.models.find(m => m.id === 'deepseek-v4-pro')?.arms.dsh;
175
  $('independent-notice').hidden = !dsh;
 
176
  if (dsh) $('dsh-protocol').href = sourceURL(dsh.protocol_url);
 
 
177
  }
178
 
179
  async function loadData() {
@@ -182,6 +242,8 @@ async function loadData() {
182
  $('error').hidden = true;
183
  $('load-state').hidden = false;
184
  $('table-wrap').hidden = true;
 
 
185
  $('empty').hidden = true;
186
  $('result-count').textContent = '';
187
  $('evidence-body').replaceChildren();
@@ -208,6 +270,7 @@ async function loadData() {
208
 
209
  document.querySelectorAll('[data-view]').forEach(button => button.addEventListener('click', () => {
210
  view = button.dataset.view;
 
211
  document.querySelectorAll('[data-view]').forEach(el => el.setAttribute('aria-pressed', String(el === button)));
212
  render();
213
  }));
@@ -220,5 +283,23 @@ $('language').addEventListener('click', () => {
220
  try { localStorage.setItem('inferencenet-language', language); } catch (_) {}
221
  applyLanguage();
222
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  applyLanguage();
224
  loadData();
 
1
+ import { mountain } from './mountain.js';
2
  'use strict';
3
  const $ = id => document.getElementById(id);
4
  const metricKeys = ['perfect', 'partial_replication', 'compilation_success', 'coefficient_direction', 'significance_level'];
 
22
  });
23
  const zh = {
24
  dataset: '数据集 ↗', archive: '结果存档 ↗', edition: '研究榜单 · 2026 年 9 月',
25
+ title: 'InferenceNet 挑战赛排行榜',
26
+ intro: '模型与 Harness 在 1,000 道计量经济学复现任务上的表现。',
27
+ models: '种模型别名', groups: '组实验', tasks: '题 / 组', records: '条任务记录', published: '发布于 {date}',
28
  single: '单次 Agent', harness: '全部 Harness 实验', deepagents: 'DeepAgents',
29
  dsh: 'DeepSeek Harness · 独立实验',
30
  dshDesc: '使用官方 DSH SDK,请求 v4-pro API 别名。按固定规则纳入技术补测,底层模型权重未经核实。',
 
67
  let language = 'en';
68
  try { language = localStorage.getItem('inferencenet-language') === 'zh' ? 'zh' : 'en'; } catch (_) {}
69
  let data = null;
70
+ let view = 'all';
71
+ let display = 'table';
72
+ let theme = 'light';
73
+ try { theme = localStorage.getItem('inferencenet-theme') === 'dark' ? 'dark' : 'light'; } catch (_) {}
74
  let sortBy = 'deepagents';
75
  const t = key => (language === 'zh' ? zh[key] : en[key]) ?? en[key] ?? key;
76
  const esc = value => String(value).replace(/[&<>"']/g, char => ({'&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;'}[char]));
77
  const sourceURL = (path, tree = false) => `https://huggingface.co/spaces/CamoAiLab/InferenceNet-Leaderboard/${tree ? 'tree' : 'blob'}/${data.source_revision}/${path}`;
78
  const external = (url, label) => `<a href="${esc(url)}" target="_blank" rel="noopener noreferrer">${esc(label)} ↗</a>`;
79
 
80
+
81
+ Object.assign(en, {
82
+ publicationNote: 'Research snapshot · local scoring; official scorer parity unverified. Run budgets differ.',
83
+ all: 'All results', website: 'Project website ↗', tableView: 'Table', mountainView: 'Mountain view',
84
+ evaluationDetails: 'Evaluation protocol, definitions & evidence', detailsLink: 'Details ↗',
85
+ dshShort: 'DSH: model weights unverified; 87 technical replacements, 30 unknown task outcomes. Excluded from paired lift.',
86
+ harnessColumn: 'Harness', noHarness: 'None', independentLabel: 'Independent',
87
+ light: 'Light', dark: 'Dark', fullShort: 'Full replication', partialShort: 'Partial replication',
88
+ executionShort: 'Execution success', directionShort: 'Coefficient direction', significanceShort: 'Significance level',
89
+ mountainTitle: 'The climb to full replication', summit: '100% · every task succeeds',
90
+ mountainNote: 'Height represents the selected score. Run protocols and budgets differ.',
91
+ scoreDetails: 'Successes / tasks; metric unknown', comparisonNote: 'Paired view includes baseline and DeepAgents only. DSH remains a separate result.',
92
+ });
93
+ Object.assign(zh, {
94
+ publicationNote: '研究快照 · 本地评分,尚未验证与官方评分器一致;各组运行预算存在差异。',
95
+ all: '全部结果', website: '项目官网 ↗', tableView: '成绩表', mountainView: '登山图',
96
+ evaluationDetails: '评测协议、指标定义与证据', detailsLink: '查看详情 ↗',
97
+ dshShort: 'DSH:模型权重未核实,含 87 次技术补测替换、30 题状态未知;不纳入配对提升。',
98
+ harnessColumn: 'Harness', noHarness: '无', independentLabel: '独立实验',
99
+ light: '浅色', dark: '深色', fullShort: '完整复现率', partialShort: '部分复现率',
100
+ executionShort: '执行成功率', directionShort: '系数方向', significanceShort: '显著性水平',
101
+ mountainTitle: '向完整复现迈进', summit: '100% · 每道任务均成功',
102
+ mountainNote: '高度表示所选指标得分。各组运行协议与预算存在差异。',
103
+ scoreDetails: '成功数 / 题数;指标未知', comparisonNote: '配对视图仅比较单次组与 DeepAgents;DSH 单独展示。',
104
+ metric: '排名指标', single: '单次生成', harness: '模型 + Harness', compare: '配对提升',
105
+ });
106
+ const shortKeys = ['fullShort', 'partialShort', 'executionShort', 'directionShort', 'significanceShort'];
107
+ function applyTheme() {
108
+ document.documentElement.dataset.theme = theme;
109
+ $('theme').setAttribute('aria-pressed', String(theme === 'dark'));
110
+ $('theme-label').textContent = t(theme);
111
+ $('theme').setAttribute('aria-label', language === 'zh' ? `切换到${theme === 'light' ? '深色' : '浅色'}主题` : `Switch to ${theme === 'light' ? 'dark' : 'light'} theme`);
112
+ }
113
+
114
  function applyLanguage() {
115
  document.documentElement.lang = language === 'zh' ? 'zh-CN' : 'en';
116
  document.querySelectorAll('[data-i18n]').forEach(el => { el.innerHTML = t(el.dataset.i18n); });
 
121
  document.querySelector('[data-group=local]').label = t('groupLocal');
122
  document.querySelector('[data-group=hf]').label = t('groupHF');
123
  $('definitions').innerHTML = metricKeys.map(key => `<li><strong>${esc(t(key))}:</strong> ${esc(t(key + 'Desc'))}</li>`).join('');
124
+ applyTheme();
125
  render();
126
  }
127
 
 
152
  // Pairing is intentionally limited to the two historical arms. Independent
153
  // harness entries have their own rank and can never enter a paired lift.
154
  function rankedEntries(snapshot, selectedView, key, selectedSort, query) {
155
+ const entries = selectedView === 'compare'
156
+ ? snapshot.models.map(model => ({model, armId: null, arm: model.arms.baseline, name: model.name,
157
+ value: selectedSort === 'gain' ? model.arms.deepagents.metrics[key].count - model.arms.baseline.metrics[key].count : model.arms[selectedSort].metrics[key].count}))
158
+ : snapshot.models.flatMap(model => Object.entries(model.arms)
159
+ .filter(([id]) => selectedView === 'all' || (selectedView === 'baseline' ? id === 'baseline' : id !== 'baseline'))
160
+ .map(([armId, arm]) => ({model, armId, arm, name: armId === 'dsh' ? 'DeepSeek API · v4-pro alias' : model.name, value: arm.metrics[key].count})));
161
  const search = query.toLowerCase().trim();
162
+ const filtered = entries.filter(entry => `${entry.name} ${entry.model.id} ${entry.armId || ''} ${entry.arm.display_name || ''}`.toLowerCase().includes(search))
163
+ .sort((a, b) => b.value - a.value || a.name.localeCompare(b.name) || String(a.armId).localeCompare(String(b.armId)));
164
+ let last = null, rank = 0;
165
+ filtered.forEach((entry, i) => { if (entry.value !== last) rank = i + 1; last = entry.value; entry.rank = rank; });
166
+ return {total: entries.length, entries: filtered};
167
+ }
168
+ function protocolName(armId) { return armId === 'baseline' ? t('noHarness') : armId === 'dsh' ? 'DSH' : 'DeepAgents'; }
169
+ function compactScore(arm, key, active) {
170
+ const m = arm.metrics[key];
171
+ const description = `${m.count} / 1,000 ${t('passed')}; ${t('metricUnknown')}: ${m.unknown}`;
172
+ return `<td class="numeric ${active ? 'selected-metric' : ''}"><span tabindex="0" class="metric-value" aria-label="${esc(m.score.toFixed(1) + '%; ' + description)}">${m.score.toFixed(1)}<small>%</small><span class="metric-tooltip" role="tooltip">${esc(description)}</span></span></td>`;
173
+ }
174
+ function renderMountain(entries, key) {
175
+ const el = $('ranking-mountain');
176
+ el.hidden = display !== 'mountain' || entries.length === 0 || view === 'compare';
177
+ if (el.hidden) return;
178
+ // Mountain markers use unique ordinal IDs, with tied scores at the same altitude.
179
+ mountain(el, {
180
+ title: t('mountainTitle'), metric: t(shortKeys[metricKeys.indexOf(key)]), unit: '%', max: 100,
181
+ peakLabel: t('summit'), caption: t('mountainNote'),
182
+ items: entries.map((entry, i) => ({rank: i + 1, label: `${entry.name} · ${protocolName(entry.armId)}${entry.armId === 'dsh' ? ' *' : ''}`, value: entry.arm.metrics[key].score, kind: 'internal'})),
183
+ locale: language,
184
+ }, {table: false, aspect: 0.44, compactAspect: 0.75});
185
  }
186
 
187
  function scoreCell(arm, key, style) {
 
198
 
199
  function render() {
200
  const key = $('metric').value;
201
+ $('metric-description').textContent = t(key + 'Desc');
202
  if (!data) return;
 
203
  const {total, entries} = rankedEntries(data, view, key, sortBy, $('search').value);
204
  $('published-date').textContent = t('published').replace('{date}', data.published_date);
205
  $('sort-control').hidden = view !== 'compare';
206
+ $('table-wrap').hidden = entries.length === 0 || (display === 'mountain' && view !== 'compare');
207
  $('empty').hidden = entries.length !== 0;
208
  $('result-count').textContent = t(view === 'compare' ? 'matched' : 'matchedRuns').replace('{n}', entries.length).replace('{total}', total);
209
+ $('caption').textContent = `${t(view === 'compare' ? 'compare' : view === 'all' ? 'all' : view === 'baseline' ? 'single' : 'harness')} — ${t(key)}`;
210
  const th = (text, cls = '') => `<th scope="col" class="${cls}">${esc(text)}</th>`;
211
+ $('table-head').innerHTML = '<tr>' + th(t('rank'), 'rank') + th(t('model'), 'model-heading') + (view === 'compare'
212
+ ? th(t('single'), 'numeric') + th('DeepAgents', 'numeric') + th(t('lift'), 'numeric')
213
+ : th(t('harnessColumn')) + metricKeys.map((metric, i) => `<th scope="col" class="numeric ${metric === key ? 'selected-metric' : ''}" aria-sort="${metric === key ? 'descending' : 'none'}"><button type="button" data-metric="${metric}" title="${esc(t(metric + 'Desc'))}">${esc(t(shortKeys[i]))}${metric === key ? ' ↓' : ''}</button></th>`).join('')) + th(t('evidenceCol'), 'evidence-col') + '</tr>';
214
+ $('table-body').innerHTML = entries.map(entry => {
215
+ const {model, arm, armId, name, rank} = entry;
 
216
  const arms = model.arms;
217
+ const identity = `<td class="rank"><span class="rank-number rank-${rank}">${rank}</span></td><th scope="row" class="model-cell">${esc(name)}</th>`;
 
 
 
 
218
  let scores = '', sources = '';
219
  if (view === 'compare') {
220
+ const gain = (arms.deepagents.metrics[key].count - arms.baseline.metrics[key].count) / 10;
221
+ scores = scoreCell(arms.baseline, key, 'baseline') + scoreCell(arms.deepagents, key, 'harness') + `<td class="gain numeric ${gain < 0 ? 'negative' : ''}">${gain > 0 ? '+' : ''}${gain.toFixed(1)}<small>${esc(t('pp'))}</small></td>`;
222
  sources = external(sourceURL(arms.baseline.path, true), 'A') + external(sourceURL(arms.deepagents.path, true), 'B');
223
  } else {
224
+ const badge = armId === 'dsh' ? `<a class="independent-tag" href="#protocol-details">${esc(t('independentLabel'))} ↗</a>` : '';
225
+ scores = `<td class="protocol-cell"><span class="protocol-label ${armId === 'baseline' ? 'no-harness' : ''}">${esc(protocolName(armId))}</span>${badge}</td>` + metricKeys.map(metric => compactScore(arm, metric, metric === key)).join('');
226
+ sources = external(sourceURL(arm.path + '/results.jsonl'), t('recordsLink')) + (!arm.complete ? `<span class="evidence-gap">${esc(t('review'))}</span>` : '');
227
  }
228
  return `<tr data-model="${esc(model.id)}" data-arm="${esc(armId || 'paired')}">${identity}${scores}<td class="source-cell">${sources}</td></tr>`;
229
  }).join('');
230
  $('evidence-body').innerHTML = data.models.flatMap(m => Object.entries(m.arms).map(([id, arm]) => `<tr><th scope="row">${esc(arm.display_name || m.name)}</th><td>${esc(id === 'baseline' ? t('single') : id === 'dsh' ? t('dsh') : 'DeepAgents')}</td>${evidenceState(arm)}</tr>`)).join('');
231
  const dsh = data.models.find(m => m.id === 'deepseek-v4-pro')?.arms.dsh;
232
  $('independent-notice').hidden = !dsh;
233
+ $('independent-summary').hidden = !dsh || view === 'baseline' || view === 'compare';
234
  if (dsh) $('dsh-protocol').href = sourceURL(dsh.protocol_url);
235
+ document.querySelector('[data-display="mountain"]').disabled = view === 'compare';
236
+ renderMountain(entries, key);
237
  }
238
 
239
  async function loadData() {
 
242
  $('error').hidden = true;
243
  $('load-state').hidden = false;
244
  $('table-wrap').hidden = true;
245
+ $('ranking-mountain').hidden = true;
246
+ $('independent-summary').hidden = true;
247
  $('empty').hidden = true;
248
  $('result-count').textContent = '';
249
  $('evidence-body').replaceChildren();
 
270
 
271
  document.querySelectorAll('[data-view]').forEach(button => button.addEventListener('click', () => {
272
  view = button.dataset.view;
273
+ if (view === 'compare') setDisplay('table');
274
  document.querySelectorAll('[data-view]').forEach(el => el.setAttribute('aria-pressed', String(el === button)));
275
  render();
276
  }));
 
283
  try { localStorage.setItem('inferencenet-language', language); } catch (_) {}
284
  applyLanguage();
285
  });
286
+ function setDisplay(next) {
287
+ display = next;
288
+ document.querySelectorAll('[data-display]').forEach(el => el.setAttribute('aria-pressed', String(el.dataset.display === next)));
289
+ render();
290
+ }
291
+ document.querySelectorAll('[data-display]').forEach(button => button.addEventListener('click', () => setDisplay(button.dataset.display)));
292
+ $('table-head').addEventListener('click', event => {
293
+ const button = event.target.closest('[data-metric]');
294
+ if (button) { $('metric').value = button.dataset.metric; render(); }
295
+ });
296
+ document.addEventListener('click', event => {
297
+ if (event.target.closest('a[href="#protocol-details"]')) $('protocol-details').open = true;
298
+ });
299
+ $('theme').addEventListener('click', () => {
300
+ theme = theme === 'light' ? 'dark' : 'light';
301
+ try { localStorage.setItem('inferencenet-theme', theme); } catch (_) {}
302
+ applyTheme();
303
+ });
304
  applyLanguage();
305
  loadData();
mountain.css ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Mountain-climb leaderboard. Companion to mountain.js. Uses the site tokens from
2
+ * styles.css (--paper, --surface, --ink, --soft, --muted, --line, --strong-line, --tint, --sans, --mono)
3
+ * and defines every colour the drawing needs on the component itself, for light and dark. */
4
+ .mountain{
5
+ --mtn-sky:#e9eef5;--mtn-sky-low:#ffffff;
6
+ --mtn-far:#e3e8ef;--mtn-mid:#d2dae4;--mtn-near:#b9c4d1;--mtn-shade:#6f7f93;--mtn-foot:#a2aebd;
7
+ --mtn-snow:#ffffff;--mtn-cap:#ffffff;--mtn-outline:#4b5665;--mtn-haze:#ffffff;
8
+ --mtn-line:#7d8794;--mtn-route:#2f3742;--mtn-flag:#d6382a;--mtn-pole:#262a30;
9
+ --mtn-gold:#e8b421;--mtn-silver:#c9d0da;--mtn-bronze:#c98a52;--mtn-on-gold:#1a1a1a;
10
+ --mtn-marker:#28313c;--mtn-marker-text:#ffffff;--mtn-marker-ring:#ffffff;--mtn-focus:#191919;
11
+ --mtn-pill:#20272f;--mtn-pill-text:#ffffff;--mtn-pill-edge:rgba(255,255,255,.55);--mtn-badge:rgba(255,255,255,.16);
12
+ --mtn-text:#191919;--mtn-muted:#5d6672;--mtn-halo:rgba(255,255,255,.9);--mtn-trail:#1f262e;
13
+ display:block;margin:0;border:1px solid var(--line);border-radius:8px;background:var(--surface);color:var(--ink);overflow:hidden;font-family:var(--sans,Inter,Arial,sans-serif)}
14
+ html[data-theme="dark"] .mountain{
15
+ --mtn-sky:#0d121a;--mtn-sky-low:#171b22;
16
+ --mtn-far:#1e2632;--mtn-mid:#293340;--mtn-near:#38434f;--mtn-shade:#141a22;--mtn-foot:#28313c;
17
+ --mtn-snow:#d5dde7;--mtn-cap:#c9d2dd;--mtn-outline:#8892a0;--mtn-haze:#171b22;
18
+ --mtn-line:#6c7684;--mtn-route:#dfe5ec;--mtn-flag:#e4493a;--mtn-pole:#e8e8e8;
19
+ --mtn-gold:#f2c13d;--mtn-silver:#aeb7c3;--mtn-bronze:#cf925c;--mtn-on-gold:#1a1a1a;
20
+ --mtn-marker:#e9edf2;--mtn-marker-text:#151515;--mtn-marker-ring:#111111;--mtn-focus:#eeeeee;
21
+ --mtn-pill:#e9ebee;--mtn-pill-text:#151515;--mtn-pill-edge:rgba(0,0,0,.55);--mtn-badge:rgba(0,0,0,.12);
22
+ --mtn-text:#eeeeee;--mtn-muted:#a5adb8;--mtn-halo:rgba(17,17,17,.9);--mtn-trail:#e6eaef}
23
+
24
+ .mountain>script{display:none}
25
+ .mountain-canvas{display:block}
26
+
27
+ /* Header and caption. */
28
+ .mountain-head{display:flex;justify-content:space-between;align-items:flex-start;gap:16px;padding:18px 22px 14px;border-bottom:1px solid var(--line)}
29
+ .mountain-title{margin:0;font-size:17px;font-weight:600;letter-spacing:-.015em;line-height:1.35;color:var(--ink)}
30
+ .mountain-subtitle{margin:4px 0 0;font-size:13px;line-height:1.6;color:var(--muted)}
31
+ .mountain-stamp{margin:0;font-family:var(--mono,ui-monospace,monospace);font-size:12px;line-height:1.6;color:var(--muted);white-space:nowrap}
32
+ .mountain-caption{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px 20px;padding:12px 22px 14px;border-top:1px solid var(--line);font-size:12px;line-height:1.6;color:var(--muted)}
33
+ .mountain-note{margin:0}
34
+ .mountain-legend{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:6px 16px}
35
+ .mountain-legend li{display:inline-flex;align-items:center;gap:7px;white-space:nowrap}
36
+ .mtn-key{display:inline-block;width:11px;height:11px;border-radius:50%;background:var(--mtn-marker);border:2px solid var(--mtn-marker-ring);box-sizing:content-box}
37
+ .mtn-key--gold{background:var(--mtn-gold)}
38
+ .mtn-key--internal{border:1.5px dashed var(--mtn-marker);background:transparent;width:10px;height:10px}
39
+ .mtn-key--trail{width:20px;height:0;border:0;border-top:2px dashed var(--mtn-trail);border-radius:0;background:transparent}
40
+
41
+ /* Stage: the drawing plus the pill overlay. */
42
+ .mountain-stage{position:relative;display:block;width:100%;line-height:0}
43
+ .mountain-svg{display:block;width:100%;height:auto;font-family:inherit;font-variant-numeric:tabular-nums}
44
+ .mountain-svg text{font-family:inherit;line-height:1}
45
+
46
+ /* Terrain. */
47
+ .mtn-sky-top{stop-color:var(--mtn-sky)}
48
+ .mtn-sky-low{stop-color:var(--mtn-sky-low)}
49
+ .mtn-haze-top{stop-color:var(--mtn-haze);stop-opacity:0}
50
+ .mtn-haze-low{stop-color:var(--mtn-haze);stop-opacity:.55}
51
+ .mtn-far{fill:var(--mtn-far)}
52
+ .mtn-mid{fill:var(--mtn-mid)}
53
+ .mtn-cap{fill:var(--mtn-cap);opacity:.85}
54
+ .mtn-near{fill:var(--mtn-near)}
55
+ .mtn-snow{fill:var(--mtn-snow)}
56
+ .mtn-shade{fill:var(--mtn-shade);opacity:.28}
57
+ .mtn-stria{fill:none;stroke:var(--mtn-outline);stroke-width:1;opacity:.14;stroke-linejoin:round}
58
+ .mtn-foot{fill:var(--mtn-foot)}
59
+ .mtn-outline{fill:none;stroke:var(--mtn-outline);stroke-width:1;stroke-linejoin:round;opacity:.42}
60
+ .mtn-outline--foot{opacity:.22}
61
+ .mtn-contour{stroke:var(--mtn-line);stroke-width:1;stroke-dasharray:2 7;stroke-linecap:round;opacity:.38}
62
+ .mtn-contour-label{fill:var(--mtn-muted);font-size:12px;paint-order:stroke;stroke:var(--mtn-halo);stroke-width:3px;stroke-linejoin:round}
63
+ .mtn-base{stroke:var(--mtn-line);stroke-width:1}
64
+ .mtn-route{stroke:var(--mtn-route);stroke-width:2.2;stroke-dasharray:0 7;stroke-linecap:round;opacity:.72}
65
+ .mtn-pole{stroke:var(--mtn-pole);stroke-width:2;stroke-linecap:round}
66
+ .mtn-flag{fill:var(--mtn-flag)}
67
+ .mtn-peak-label{fill:var(--mtn-text);font-size:12px;font-weight:500;paint-order:stroke;stroke:var(--mtn-halo);stroke-width:3px;stroke-linejoin:round}
68
+ .mtn-stamp{fill:var(--mtn-muted);font-family:var(--mono,ui-monospace,monospace);font-size:12px}
69
+
70
+ /* Connectors, trails. */
71
+ .mtn-connector{stroke:var(--mtn-line);stroke-width:1;opacity:.5;transition:opacity .15s,stroke-width .15s}
72
+ .mtn-connector.is-active{stroke:var(--mtn-focus);stroke-width:1.5;opacity:1}
73
+ .mtn-trail-path{stroke:var(--mtn-trail);stroke-width:1.75;stroke-dasharray:6 5;stroke-linecap:round}
74
+ .mtn-trail-head{fill:var(--mtn-trail)}
75
+ .mtn-trail-label{fill:var(--mtn-text);font-size:12px;font-weight:600;paint-order:stroke;stroke:var(--mtn-halo);stroke-width:4px;stroke-linejoin:round}
76
+
77
+ /* Markers. */
78
+ .mtn-marker{cursor:default}
79
+ .mtn-marker-dot{fill:var(--mtn-marker);stroke:var(--mtn-marker-ring);stroke-width:2;transition:stroke-width .15s}
80
+ .mtn-marker--leader .mtn-marker-dot{fill:var(--mtn-gold);stroke-width:2.5}
81
+ .mtn-marker--silver .mtn-marker-dot{stroke:var(--mtn-silver);stroke-width:3.5}
82
+ .mtn-marker--bronze .mtn-marker-dot{stroke:var(--mtn-bronze);stroke-width:3.5}
83
+ .mtn-marker--waypoint .mtn-marker-dot{fill:var(--mtn-sky-low);stroke:var(--mtn-marker);stroke-width:1.5}
84
+ .mtn-marker-num{fill:var(--mtn-marker-text);font-size:12px;font-weight:700;pointer-events:none}
85
+ .mtn-marker--leader .mtn-marker-num{fill:var(--mtn-on-gold);font-size:14px}
86
+ .mtn-marker-halo{fill:none;stroke:var(--mtn-focus);stroke-width:2;opacity:0;transition:opacity .15s}
87
+ .mtn-marker--leader .mtn-marker-halo{stroke:var(--mtn-gold);stroke-width:1.5;opacity:.55}
88
+ .mtn-marker.is-active .mtn-marker-halo{stroke:var(--mtn-focus);stroke-width:2;opacity:1}
89
+ .mtn-marker-ring{fill:none;stroke:var(--mtn-marker);stroke-width:1.25;stroke-dasharray:3 3}
90
+ .mtn-marker-val{fill:var(--mtn-text);font-size:12px;font-weight:600;paint-order:stroke;stroke:var(--mtn-halo);stroke-width:4px;stroke-linejoin:round;opacity:0;transition:opacity .15s;pointer-events:none}
91
+ .mtn-marker.is-active .mtn-marker-val{opacity:1}
92
+
93
+ /* Pills: absolute overlay in the wide layout, ranks stacked left (even) and right (odd). */
94
+ .mountain-pills{list-style:none;margin:0;padding:0;position:absolute;inset:0;pointer-events:none;line-height:1}
95
+ .mountain-pill{position:absolute;display:inline-flex;align-items:center;gap:8px;height:28px;padding:0 11px 0 4px;border-radius:14px;background:var(--mtn-pill);color:var(--mtn-pill-text);border:1px solid var(--mtn-pill-edge);font-size:13px;font-weight:600;white-space:nowrap;pointer-events:auto;cursor:default;box-sizing:border-box;transition:transform .15s,border-color .15s}
96
+ .mountain-pill--right{flex-direction:row-reverse;padding:0 4px 0 11px}
97
+ .mountain-pill:hover,.mountain-pill.is-active{border-color:var(--mtn-focus);transform:translateY(-1px)}
98
+ .mountain-pill:focus{outline:none}
99
+ .mountain-pill:focus-visible{outline:2px solid var(--mtn-focus);outline-offset:3px}
100
+ .mountain-pill--internal .mountain-badge{box-shadow:inset 0 0 0 1.5px var(--mtn-pill-edge)}
101
+ .mountain-badge{display:inline-grid;place-items:center;width:20px;height:20px;border-radius:50%;background:var(--mtn-badge);font-size:12px;font-weight:700;font-variant-numeric:tabular-nums;flex:none}
102
+ .mountain-badge--gold{background:var(--mtn-gold);color:var(--mtn-on-gold)}
103
+ .mountain-badge--silver{background:var(--mtn-silver);color:var(--mtn-on-gold)}
104
+ .mountain-badge--bronze{background:var(--mtn-bronze);color:var(--mtn-on-gold)}
105
+ .mountain-pill-value{display:none}
106
+ /* Narrow desktop (700–1000px container): smaller pills leave the drawing more width. */
107
+ .mountain--narrow .mountain-pill{font-size:12px;padding:0 9px 0 4px}
108
+ .mountain--narrow .mountain-pill--right{padding:0 4px 0 9px}
109
+ .mountain-vh{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}
110
+
111
+ /* Compact layout (container narrower than the breakpoint): 4:3 drawing, pills listed below. */
112
+ .mountain--compact .mountain-head{flex-direction:column;gap:2px;padding:16px 16px 12px}
113
+ .mountain--compact .mountain-stamp{white-space:normal;margin-top:6px}
114
+ .mountain--compact .mountain-caption{padding:12px 16px}
115
+ .mountain--compact .mountain-pills{position:static;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;padding:14px 16px 4px;pointer-events:auto;border-top:1px solid var(--line)}
116
+ .mountain--compact .mountain-pill{position:static;display:flex;flex-direction:row;height:auto;min-height:32px;padding:5px 10px 5px 5px;border-radius:16px;white-space:normal;font-size:12px;line-height:1.3;gap:7px}
117
+ .mountain--compact .mountain-pill-label{flex:1 1 auto;min-width:0;overflow-wrap:anywhere}
118
+ .mountain--compact .mountain-pill-value{display:inline;font-variant-numeric:tabular-nums;font-weight:500;opacity:.8;flex:none}
119
+ .mountain--compact .mountain-pill:hover,.mountain--compact .mountain-pill.is-active{transform:none}
120
+ @media(max-width:420px){.mountain--compact .mountain-pills{grid-template-columns:1fr}} /* one column on phones: labels stay on one or two lines */
121
+
122
+ /* Ranked table (no-JS fallback and screen readers). An authored table with more than
123
+ * three columns goes inside <div class="mountain-table-wrap"> so it scrolls sideways on phones. */
124
+ .mountain-table-wrap{max-width:100%;overflow-x:auto;overscroll-behavior-x:contain}
125
+ .mountain-details{border-top:1px solid var(--line);font-size:13px}
126
+ .mountain-details summary{padding:12px 22px;cursor:pointer;color:var(--soft);font-weight:500}
127
+ .mountain-details summary:hover{background:var(--tint)}
128
+ .mountain-details summary:focus-visible{outline:2px solid var(--ink);outline-offset:-3px}
129
+ .mountain-table{width:100%;border-collapse:collapse;font-size:13px;font-variant-numeric:tabular-nums;text-align:left}
130
+ .mountain-table caption{padding:0 22px 8px;text-align:left;font-size:12px;color:var(--muted)}
131
+ .mountain-table th,.mountain-table td{padding:9px 12px;border-top:1px solid var(--line);vertical-align:top}
132
+ .mountain-table thead th{font-size:12px;font-weight:600;color:var(--soft);background:var(--tint)}
133
+ .mountain-table tbody th{font-weight:500}
134
+ .mountain-table tr>*:first-child{padding-left:22px;width:44px;color:var(--muted)}
135
+ .mountain-table tr>*:last-child{padding-right:22px;text-align:right}
136
+ .mountain:not(.mountain--ready) .mountain-canvas{display:none}
137
+ .mountain:not(.mountain--ready):not(.mountain--error):not(:has(.mountain-table)){display:none}
138
+ .mountain:not(.mountain--ready) .mountain-table caption{padding-top:12px}
139
+ .mountain--error .mountain-canvas::before{content:"Leaderboard data could not be read.";display:block;padding:18px 22px;font-size:13px;color:var(--muted)}
140
+
141
+ @media(prefers-reduced-motion:reduce){.mountain *{transition:none!important}}
142
+ @media(forced-colors:active){
143
+ .mountain-pill{border-color:ButtonText;forced-color-adjust:none;background:ButtonFace;color:ButtonText}
144
+ .mtn-marker-dot{stroke:CanvasText}
145
+ }
mountain.js ADDED
@@ -0,0 +1,499 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Reused from the InferenceNet Challenge website; local adaptation adds Chinese labels.
2
+ /* Mountain-climb leaderboard: every entry is a climber whose altitude is its score,
3
+ * the flag marks the unconquered summit (score = max). ES module, zero dependencies.
4
+ *
5
+ * Markup:
6
+ * <figure class="mountain" data-mountain>
7
+ * <script type="application/json">{ "title": "…", "items": [ … ] }</script>
8
+ * <div class="mountain-canvas"></div>
9
+ * </figure>
10
+ * <script type="module" src="mountain.js"></script>
11
+ *
12
+ * The module mounts every [data-mountain] element once the DOM is ready; calling
13
+ * mountMountains() again (or mountain(el, data) twice) re-renders in place. */
14
+
15
+ const NS = "http://www.w3.org/2000/svg";
16
+ const DEFAULTS = {
17
+ compactBreakpoint: 700, // px; below this the pills move under the drawing
18
+ headingLevel: 3,
19
+ table: true, // emit a ranked <table class="mountain-table"> when the page has none
20
+ minGap: 28, // px; minimum centre distance between two markers
21
+ markerRadius: 11,
22
+ leaderRadius: 15,
23
+ aspect: 0.6, // drawing height / width (1200 × 720)
24
+ compactAspect: 0.75 // 4:3 on small screens
25
+ };
26
+
27
+ /* Terrain in unit coordinates: [x / width, altitude above the base line, 0..1 = summit]. */
28
+ const PEAK = [[-0.06, 0], [0.02, 0.06], [0.07, 0.17], [0.11, 0.14], [0.17, 0.29], [0.21, 0.26], [0.26, 0.42], [0.30, 0.39], [0.345, 0.55], [0.375, 0.53], [0.42, 0.73], [0.445, 0.71], [0.475, 0.88], [0.5, 1], [0.525, 0.87], [0.555, 0.84], [0.59, 0.72], [0.62, 0.69], [0.665, 0.52], [0.70, 0.47], [0.75, 0.34], [0.79, 0.30], [0.84, 0.18], [0.885, 0.15], [0.94, 0.06], [1.06, 0]];
29
+ const SNOW = [[0.42, 0.73], [0.445, 0.71], [0.475, 0.88], [0.5, 1], [0.525, 0.87], [0.555, 0.84], [0.59, 0.72], [0.60, 0.69], [0.592, 0.63], [0.578, 0.66], [0.567, 0.585], [0.552, 0.63], [0.541, 0.555], [0.531, 0.60], [0.52, 0.505], [0.506, 0.57], [0.495, 0.495], [0.484, 0.56], [0.471, 0.525], [0.461, 0.60], [0.451, 0.565], [0.441, 0.63], [0.431, 0.61], [0.425, 0.68]];
30
+ const SNOW_TONGUES = [
31
+ [[0.462, 0.605], [0.482, 0.575], [0.478, 0.46], [0.470, 0.43], [0.464, 0.47]],
32
+ [[0.512, 0.545], [0.531, 0.565], [0.535, 0.43], [0.526, 0.37], [0.519, 0.44]],
33
+ [[0.556, 0.61], [0.573, 0.585], [0.592, 0.50], [0.585, 0.465], [0.571, 0.53]],
34
+ [[0.388, 0.52], [0.404, 0.565], [0.416, 0.535], [0.412, 0.485], [0.398, 0.475]],
35
+ [[0.628, 0.60], [0.646, 0.585], [0.662, 0.525], [0.652, 0.50], [0.636, 0.535]]
36
+ ];
37
+ const SHADE = [[0.5, 1], [0.515, 0.80], [0.53, 0.62], [0.56, 0.45], [0.585, 0.28], [0.62, 0.12], [0.66, 0], [1.06, 0], [0.94, 0.06], [0.885, 0.15], [0.84, 0.18], [0.79, 0.30], [0.75, 0.34], [0.70, 0.47], [0.665, 0.52], [0.62, 0.69], [0.59, 0.72], [0.555, 0.84], [0.525, 0.87]];
38
+ const STRIAE = [
39
+ [[0.36, 0.53], [0.33, 0.40], [0.29, 0.30], [0.23, 0.16]],
40
+ [[0.41, 0.66], [0.39, 0.50], [0.35, 0.36], [0.31, 0.20]],
41
+ [[0.66, 0.50], [0.69, 0.36], [0.74, 0.22], [0.80, 0.10]],
42
+ [[0.60, 0.66], [0.64, 0.48], [0.68, 0.30], [0.71, 0.14]]
43
+ ];
44
+ const FAR = [[-0.06, 0], [0.0, 0.22], [0.05, 0.18], [0.10, 0.36], [0.15, 0.30], [0.20, 0.44], [0.25, 0.40], [0.31, 0.52], [0.36, 0.48], [0.42, 0.56], [0.5, 0.6], [0.58, 0.55], [0.64, 0.58], [0.70, 0.48], [0.75, 0.52], [0.80, 0.40], [0.86, 0.44], [0.92, 0.28], [0.97, 0.32], [1.06, 0.16], [1.06, 0]];
45
+ const MID = [[-0.06, 0], [0.0, 0.10], [0.06, 0.20], [0.10, 0.18], [0.16, 0.36], [0.20, 0.30], [0.24, 0.33], [0.29, 0.24], [0.34, 0.28], [0.38, 0.18], [0.42, 0.24], [0.47, 0.12], [0.53, 0.14], [0.58, 0.08], [0.63, 0.16], [0.68, 0.22], [0.72, 0.20], [0.77, 0.34], [0.82, 0.42], [0.86, 0.32], [0.90, 0.36], [0.95, 0.22], [1.0, 0.28], [1.06, 0.12], [1.06, 0]];
46
+ const MID_CAPS = [
47
+ [[0.135, 0.29], [0.16, 0.36], [0.185, 0.31], [0.175, 0.30], [0.16, 0.32], [0.148, 0.30]],
48
+ [[0.795, 0.37], [0.82, 0.42], [0.845, 0.36], [0.835, 0.35], [0.82, 0.37], [0.807, 0.355]]
49
+ ];
50
+ const FOOT = [[-0.06, 0], [0.04, 0.05], [0.12, 0.03], [0.20, 0.075], [0.28, 0.04], [0.36, 0.06], [0.45, 0.02], [0.55, 0.05], [0.64, 0.03], [0.73, 0.08], [0.82, 0.045], [0.9, 0.07], [1.06, 0.03], [1.06, 0]];
51
+ const ROUTE = [[0.34, 0], [0.30, 0.10], [0.39, 0.20], [0.33, 0.33], [0.43, 0.44], [0.40, 0.56], [0.47, 0.68], [0.455, 0.80], [0.49, 0.92], [0.5, 1]];
52
+ /* Horizontal spread sequence for markers of one side, indexed by order within that side. */
53
+ const SPREAD = [0.45, 0.78, 0.22, 0.6, 0.92, 0.35, 0.7, 0.12, 0.52, 0.85, 0.3, 0.65];
54
+
55
+ let counter = 0;
56
+ const instances = new WeakMap();
57
+
58
+ /* ---------- small helpers ---------- */
59
+ const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
60
+ const num = (v) => (Math.round(v * 10) / 10).toFixed(1).replace(/\.0$/, "");
61
+ const fmt = (v, unit) => {
62
+ const s = num(Number(v) || 0);
63
+ return unit === "%" ? `${s}%` : unit ? `${s} ${unit}` : s;
64
+ };
65
+ function h(tag, attrs, parent, text) {
66
+ const n = document.createElement(tag);
67
+ for (const k in attrs) if (attrs[k] != null) n.setAttribute(k, attrs[k]);
68
+ if (text != null) n.textContent = text;
69
+ if (parent) parent.appendChild(n);
70
+ return n;
71
+ }
72
+ function s(tag, attrs, parent, text) {
73
+ const n = document.createElementNS(NS, tag);
74
+ for (const k in attrs) if (attrs[k] != null) n.setAttribute(k, attrs[k]);
75
+ if (text != null) n.textContent = text;
76
+ if (parent) parent.appendChild(n);
77
+ return n;
78
+ }
79
+ const px = (v) => (Math.round(v * 10) / 10).toString();
80
+
81
+ function frame(W, compact, o) {
82
+ const aspect = compact ? o.compactAspect : W < 1000 ? Math.max(o.aspect, 0.7) : o.aspect;
83
+ const H = Math.round(W * aspect);
84
+ const base = H - (compact ? 26 : 30);
85
+ const summit = Math.round(H * (compact ? 0.27 : 0.13));
86
+ return { W, H, base, summit, range: base - summit, cx: W / 2, compact };
87
+ }
88
+ const pt = (f, p) => [p[0] * f.W, f.base - p[1] * f.range];
89
+ const pts = (f, list) => list.map((p) => pt(f, p).map(px).join(",")).join(" ");
90
+
91
+ /* Catmull-Rom spline through the points, as a cubic path. */
92
+ function smoothPath(list) {
93
+ let d = `M${px(list[0][0])},${px(list[0][1])}`;
94
+ for (let i = 0; i < list.length - 1; i++) {
95
+ const p0 = list[i - 1] || list[i], p1 = list[i], p2 = list[i + 1], p3 = list[i + 2] || p2;
96
+ const c1 = [p1[0] + (p2[0] - p0[0]) / 6, p1[1] + (p2[1] - p0[1]) / 6];
97
+ const c2 = [p2[0] - (p3[0] - p1[0]) / 6, p2[1] - (p3[1] - p1[1]) / 6];
98
+ d += ` C${px(c1[0])},${px(c1[1])} ${px(c2[0])},${px(c2[1])} ${px(p2[0])},${px(p2[1])}`;
99
+ }
100
+ return d;
101
+ }
102
+
103
+ /* Horizontal extent of a closed polygon (pixel coordinates) along the line y. */
104
+ function extentAt(poly, y) {
105
+ let lo = Infinity, hi = -Infinity;
106
+ for (let i = 0; i < poly.length; i++) {
107
+ const a = poly[i], b = poly[(i + 1) % poly.length];
108
+ if ((a[1] <= y && b[1] > y) || (b[1] <= y && a[1] > y)) {
109
+ const x = a[0] + ((y - a[1]) * (b[0] - a[0])) / (b[1] - a[1]);
110
+ lo = Math.min(lo, x); hi = Math.max(hi, x);
111
+ }
112
+ }
113
+ return lo <= hi ? [lo, hi] : null;
114
+ }
115
+
116
+ /* Deterministic collision resolution: sweep x outward in 6px steps inside [lo, hi],
117
+ * then allow small vertical nudges; never places two markers closer than gap. */
118
+ function resolve(x, y, lo, hi, placed, gap) {
119
+ const free = (X, Y) => placed.every((p) => Math.hypot(p.x - X, p.y - Y) >= gap);
120
+ const dys = [0, -3, 3, -6, 6, -9, 9, -12, 12];
121
+ for (const dy of dys) {
122
+ for (let d = 0; d <= Math.max(hi - lo, 1) + 6; d += 6) {
123
+ for (const X of d === 0 ? [x] : [x + d, x - d]) {
124
+ if (X < lo - 0.5 || X > hi + 0.5) continue;
125
+ if (free(X, y + dy)) return { x: X, y: y + dy };
126
+ }
127
+ }
128
+ }
129
+ let best = { x, y }, score = -1; // last resort: the least crowded spot
130
+ for (let X = lo; X <= hi; X += 4) {
131
+ const m = Math.min(...placed.map((p) => Math.hypot(p.x - X, p.y - y)), Infinity);
132
+ if (m > score) { score = m; best = { x: X, y }; }
133
+ }
134
+ return best;
135
+ }
136
+
137
+ function normalise(data) {
138
+ const d = Object.assign({ unit: "", max: 100, items: [], trails: [] }, data || {});
139
+ const max = Number(d.max) > 0 ? Number(d.max) : 100;
140
+ const items = (d.items || []).map((it) => ({
141
+ label: String(it.label ?? it.name ?? ""), value: Number(it.value) || 0,
142
+ group: it.group ?? "", kind: it.kind || "public", rank: it.rank
143
+ }));
144
+ items.sort((a, b) => (b.value - a.value) || String(a.label).localeCompare(String(b.label)));
145
+ items.forEach((it, i) => { if (!Number.isFinite(Number(it.rank))) it.rank = i + 1; });
146
+ items.sort((a, b) => a.rank - b.rank);
147
+ return { ...d, max, items, trails: Array.isArray(d.trails) ? d.trails : [] };
148
+ }
149
+
150
+ /* ---------- component ---------- */
151
+ export function mountain(el, data, options = {}) {
152
+ let inst = instances.get(el);
153
+ if (inst) { inst.update(data, options); return inst; }
154
+ const o = Object.assign({}, DEFAULTS, options);
155
+ const id = el.id || `mountain-${++counter}`;
156
+ let d = normalise(data);
157
+ let canvas = el.querySelector(":scope > .mountain-canvas") || h("div", { class: "mountain-canvas" }, el);
158
+ let lastWidth = -1, raf = 0, ro = null;
159
+ el.classList.add("mountain");
160
+
161
+ function render() {
162
+ el.classList.add("mountain--ready"); // reveals the canvas before it is measured
163
+ const width = canvas.clientWidth;
164
+ if (!width) return; // hidden (closed tab, display:none): the ResizeObserver re-schedules when visible
165
+ lastWidth = width;
166
+ const compact = width < o.compactBreakpoint;
167
+ el.classList.toggle("mountain--compact", compact);
168
+ el.classList.toggle("mountain--narrow", !compact && width < 1000); // shorter pills keep the drawing wide
169
+ canvas.replaceChildren();
170
+
171
+ /* Header */
172
+ const head = h("div", { class: "mountain-head" }, canvas);
173
+ const headText = h("div", { class: "mountain-head-text" }, head);
174
+ if (d.title) h(`h${o.headingLevel}`, { class: "mountain-title" }, headText, d.title);
175
+ if (d.subtitle) h("p", { class: "mountain-subtitle" }, headText, d.subtitle);
176
+ if (compact && d.date) h("p", { class: "mountain-stamp" }, head, d.date);
177
+
178
+ /* Stage: drawing + pills */
179
+ const stage = h("div", { class: "mountain-stage" }, canvas);
180
+ /* Trails need vertical room on phones: a taller compact drawing keeps labels off the markers. */
181
+ const f = frame(width, compact, d.trails.length ? { ...o, compactAspect: Math.max(o.compactAspect, 0.9) } : o);
182
+ const svg = s("svg", {
183
+ class: "mountain-svg", viewBox: `0 0 ${f.W} ${f.H}`, width: f.W, height: f.H,
184
+ role: "img", "aria-labelledby": `${id}-title ${id}-desc`, focusable: "false"
185
+ }, stage);
186
+ const leader = d.items[0];
187
+ const isZh = d.locale === "zh";
188
+ const n = d.items.length;
189
+ s("title", { id: `${id}-title` }, svg, `${d.title || "Leaderboard"}: ${n} entries climbing by ${d.metric || "score"}.`);
190
+ s("desc", { id: `${id}-desc` }, svg, leader
191
+ ? `Altitude is ${d.metric || "score"}; the summit flag marks ${fmt(d.max, d.unit)}. Leader: ${leader.label} at ${fmt(leader.value, d.unit)}. Full ranking in the table below.`
192
+ : "No entries yet.");
193
+ svg.setAttribute("aria-label", `${d.title || "Leaderboard"}: ${n} entries climbing by ${d.metric || "score"}${leader ? `; leader ${leader.label} at ${fmt(leader.value, d.unit)}` : ""}.`);
194
+
195
+ const pills = h("ol", { class: "mountain-pills", "aria-label": `Ranked entries by ${d.metric || "score"}` }, stage);
196
+ const pillOf = new Map();
197
+ d.items.forEach((it) => {
198
+ const right = it.rank % 2 === 1;
199
+ const li = h("li", {
200
+ class: `mountain-pill mountain-pill--${right ? "right" : "left"}${it.kind === "internal" ? " mountain-pill--internal" : ""}`,
201
+ "data-rank": it.rank, "aria-describedby": `${id}-d${it.rank}`
202
+ }, pills);
203
+ const badge = h("span", { class: `mountain-badge${it.rank <= 3 ? ` mountain-badge--${["gold", "silver", "bronze"][it.rank - 1]}` : ""}`, "aria-hidden": "true" }, li, String(it.rank));
204
+ badge.dataset.rank = it.rank;
205
+ h("span", { class: "mountain-vh" }, li, `Rank ${it.rank}: `);
206
+ h("span", { class: "mountain-pill-label" }, li, it.label);
207
+ h("span", { class: "mountain-pill-value" }, li, fmt(it.value, d.unit));
208
+ pillOf.set(it.rank, li);
209
+ });
210
+
211
+ /* Column geometry from the measured pills (desktop only). */
212
+ const gutter = 16;
213
+ let leftEdge = 0, rightEdge = f.W, maxL = 0, maxR = 0, nL = 0, nR = 0;
214
+ if (!compact) {
215
+ pillOf.forEach((li, rank) => {
216
+ const w = li.offsetWidth;
217
+ if (rank % 2 === 1) { maxR = Math.max(maxR, w); nR++; } else { maxL = Math.max(maxL, w); nL++; }
218
+ });
219
+ leftEdge = gutter + maxL; rightEdge = f.W - gutter - maxR;
220
+ }
221
+
222
+ /* Marker placement. */
223
+ const peakPx = PEAK.map((p) => pt(f, p));
224
+ const r0 = compact ? Math.max(9, o.markerRadius - 2) : o.markerRadius;
225
+ const rLead = compact ? Math.max(12, o.leaderRadius - 2) : o.leaderRadius;
226
+ const placed = [];
227
+ const byLabel = new Map();
228
+ const trailTo = new Map(d.trails.map((t) => [t.from, t.to]));
229
+ const sideCount = { 1: 0, "-1": 0 };
230
+ d.items.forEach((it) => {
231
+ const side = it.rank % 2 === 1 ? 1 : -1;
232
+ const k = sideCount[side]++;
233
+ const r = it.rank === 1 ? rLead : r0;
234
+ const y = Math.max(f.summit + 1, f.base - clamp(it.value / d.max, 0, 1) * f.range);
235
+ const ext = extentAt(peakPx, y) || [f.cx - r, f.cx + r];
236
+ let lo, hi;
237
+ if (side > 0) { lo = Math.max(f.cx + 0.03 * f.W, ext[0] + r + 4); hi = Math.min(ext[1] - r - 4, rightEdge - r - 14, f.W - r - 6); }
238
+ else { hi = Math.min(f.cx - 0.03 * f.W, ext[1] - r - 4); lo = Math.max(ext[0] + r + 4, leftEdge + r + 14, r + 6); }
239
+ if (lo > hi) { const m = clamp((lo + hi) / 2, leftEdge + r + 14, rightEdge - r - 14); lo = hi = m; }
240
+ let x = lo + SPREAD[k % SPREAD.length] * (hi - lo);
241
+ const to = trailTo.get(it.label);
242
+ if (to && byLabel.has(to)) x = clamp(byLabel.get(to).x + side * 0.07 * f.W, lo, hi);
243
+ const p = resolve(x, y, lo, hi, placed, o.minGap);
244
+ const m = { ...p, r, item: it, side };
245
+ placed.push(m); byLabel.set(it.label, m);
246
+ });
247
+ /* Trail endpoints that are not ranked entries become unlabeled waypoints. */
248
+ d.trails.forEach((t) => {
249
+ [["from", t.fromValue], ["to", t.toValue]].forEach(([key, v]) => {
250
+ const label = t[key];
251
+ if (byLabel.has(label) || !Number.isFinite(Number(v))) return;
252
+ const other = byLabel.get(t[key === "from" ? "to" : "from"]);
253
+ const side = other ? other.side : 1;
254
+ const y = Math.max(f.summit + 1, f.base - clamp(Number(v) / d.max, 0, 1) * f.range);
255
+ const ext = extentAt(peakPx, y) || [f.cx - r0, f.cx + r0];
256
+ const lo = Math.max(ext[0] + r0 + 4, leftEdge + r0 + 14), hi = Math.min(ext[1] - r0 - 4, rightEdge - r0 - 14);
257
+ const x0 = other ? clamp(other.x + side * 0.08 * f.W, lo, hi) : f.cx;
258
+ const p = resolve(x0, y, Math.min(lo, hi), Math.max(lo, hi), placed, o.minGap);
259
+ const m = { ...p, r: r0 - 2, item: { label, value: Number(v), kind: t.kind || "internal", rank: null }, side, synthetic: true };
260
+ placed.push(m); byLabel.set(label, m);
261
+ });
262
+ });
263
+
264
+ /* Pill positions: bottom-aligned stacks on both sides, ranks ascending downwards. */
265
+ const pillH = 28;
266
+ const cols = { 1: [], "-1": [] };
267
+ if (!compact) {
268
+ const nSide = Math.max(nL, nR, 1);
269
+ const bottom = f.base - 6;
270
+ const pitch = clamp(Math.floor((bottom - f.H * 0.38) / nSide), 30, 34);
271
+ const top = bottom - nSide * pitch;
272
+ const idx = { 1: 0, "-1": 0 };
273
+ d.items.forEach((it) => {
274
+ const side = it.rank % 2 === 1 ? 1 : -1;
275
+ const li = pillOf.get(it.rank);
276
+ const w = li.offsetWidth;
277
+ const yTop = top + idx[side]++ * pitch + (pitch - pillH) / 2;
278
+ li.style.top = `${px(yTop)}px`;
279
+ li.style.left = side > 0 ? `${px(f.W - gutter - w)}px` : `${px(gutter)}px`;
280
+ cols[side].push({ rank: it.rank, cy: yTop + pillH / 2, inner: side > 0 ? f.W - gutter - w - 3 : gutter + w + 3, top: yTop, bottom: yTop + pillH });
281
+ });
282
+ }
283
+
284
+ /* ---------- drawing ---------- */
285
+ const defs = s("defs", {}, svg);
286
+ const sky = s("linearGradient", { id: `${id}-sky`, x1: "0", y1: "0", x2: "0", y2: "1" }, defs);
287
+ s("stop", { offset: "0", class: "mtn-sky-top" }, sky);
288
+ s("stop", { offset: "1", class: "mtn-sky-low" }, sky);
289
+ const haze = s("linearGradient", { id: `${id}-haze`, x1: "0", y1: "0", x2: "0", y2: "1" }, defs);
290
+ s("stop", { offset: "0", class: "mtn-haze-top" }, haze);
291
+ s("stop", { offset: "1", class: "mtn-haze-low" }, haze);
292
+
293
+ s("rect", { class: "mtn-sky", x: 0, y: 0, width: f.W, height: f.base, fill: `url(#${id}-sky)` }, svg);
294
+ const scene = s("g", { class: "mtn-scene", "aria-hidden": "true" }, svg);
295
+ s("polygon", { class: "mtn-far", points: pts(f, FAR) }, scene);
296
+ s("polygon", { class: "mtn-mid", points: pts(f, MID) }, scene);
297
+ MID_CAPS.forEach((c) => s("polygon", { class: "mtn-cap", points: pts(f, c) }, scene));
298
+ s("rect", { class: "mtn-haze", x: 0, y: f.base - f.range * 0.42, width: f.W, height: f.range * 0.42 + 1, fill: `url(#${id}-haze)` }, scene);
299
+ s("polygon", { class: "mtn-near", points: pts(f, PEAK) }, scene);
300
+ s("polygon", { class: "mtn-snow", points: pts(f, SNOW) }, scene);
301
+ SNOW_TONGUES.forEach((t) => s("polygon", { class: "mtn-snow", points: pts(f, t) }, scene));
302
+ s("polygon", { class: "mtn-shade", points: pts(f, SHADE) }, scene);
303
+ STRIAE.forEach((l) => s("polyline", { class: "mtn-stria", points: pts(f, l) }, scene));
304
+ s("polygon", { class: "mtn-foot", points: pts(f, FOOT) }, scene);
305
+ s("polygon", { class: "mtn-outline", points: pts(f, PEAK) }, scene);
306
+ s("polygon", { class: "mtn-outline mtn-outline--foot", points: pts(f, FOOT) }, scene);
307
+
308
+ /* Contour lines every 25 % with a label where the pill columns leave room. */
309
+ const contours = s("g", { class: "mtn-contours" }, scene);
310
+ const labelFont = 12;
311
+ [0.25, 0.5, 0.75].forEach((a) => {
312
+ const y = f.base - a * f.range;
313
+ s("line", { class: "mtn-contour", x1: 0, y1: px(y), x2: f.W, y2: px(y) }, contours);
314
+ const text = `${num(a * d.max)}${d.unit === "%" ? "%" : ""}`;
315
+ const collides = (col) => col.some((c) => y - labelFont - 4 < c.bottom && y + 2 > c.top - 4);
316
+ if (!collides(cols[1])) s("text", { class: "mtn-contour-label", x: f.W - 10, y: px(y - 5), "text-anchor": "end" }, contours, text);
317
+ else if (!collides(cols["-1"])) s("text", { class: "mtn-contour-label", x: 10, y: px(y - 5) }, contours, text);
318
+ });
319
+ s("line", { class: "mtn-base", x1: 0, y1: f.base + 0.5, x2: f.W, y2: f.base + 0.5 }, scene);
320
+ s("text", { class: "mtn-contour-label", x: f.W - 10, y: f.base + 16, "text-anchor": "end" }, scene, `0${d.unit === "%" ? "%" : ""} · ${isZh ? "起点" : "base camp"}`);
321
+
322
+ /* Route and flag. */
323
+ s("path", { class: "mtn-route", d: smoothPath(ROUTE.map((p) => pt(f, p))), fill: "none" }, scene);
324
+ const top = pt(f, [0.5, 1]);
325
+ const poleH = compact ? Math.min(34, Math.max(26, f.summit - 30)) : Math.max(34, f.H * 0.075);
326
+ s("line", { class: "mtn-pole", x1: px(top[0]), y1: px(top[1]), x2: px(top[0]), y2: px(top[1] - poleH) }, scene);
327
+ const fw = Math.max(22, f.W * 0.028), fh = fw * 0.62, fx = top[0] + 1, fy = top[1] - poleH;
328
+ s("path", { class: "mtn-flag", d: `M${px(fx)},${px(fy)} Q${px(fx + fw * 0.55)},${px(fy + fh * 0.2)} ${px(fx + fw)},${px(fy + fh * 0.5)} Q${px(fx + fw * 0.55)},${px(fy + fh * 0.8)} ${px(fx)},${px(fy + fh)} Z` }, scene);
329
+ if (d.peakLabel) {
330
+ if (compact) s("text", { class: "mtn-peak-label", x: px(f.cx), y: 18, "text-anchor": "middle" }, scene, d.peakLabel);
331
+ else s("text", { class: "mtn-peak-label", x: px(fx + fw + 10), y: px(fy + 12) }, scene, d.peakLabel);
332
+ }
333
+ if (!compact && d.date) s("text", { class: "mtn-stamp", x: f.W - 16, y: 26, "text-anchor": "end" }, scene, d.date);
334
+
335
+ /* Connectors (pill → marker); straight, from the pill's inner edge, so they never cross a pill. */
336
+ const connectors = s("g", { class: "mtn-connectors" }, scene);
337
+ const connOf = new Map();
338
+ if (!compact) {
339
+ [1, -1].forEach((side) => cols[side].forEach((c) => {
340
+ const m = placed.find((p) => p.item.rank === c.rank);
341
+ if (!m) return;
342
+ const dx = m.x - c.inner, dy = m.y - c.cy, len = Math.hypot(dx, dy) || 1;
343
+ const ex = m.x - (dx / len) * (m.r + 3), ey = m.y - (dy / len) * (m.r + 3);
344
+ connOf.set(c.rank, s("line", { class: "mtn-connector", x1: px(c.inner), y1: px(c.cy), x2: px(ex), y2: px(ey) }, connectors));
345
+ }));
346
+ }
347
+
348
+ /* Trails: dashed climbing arrow from the baseline marker to the improved one. */
349
+ const trails = s("g", { class: "mtn-trails" }, scene);
350
+ d.trails.forEach((t) => {
351
+ const a = byLabel.get(t.from), b = byLabel.get(t.to);
352
+ if (!a || !b) return;
353
+ const dx = b.x - a.x, dy = b.y - a.y, len = Math.hypot(dx, dy) || 1;
354
+ let nx = -dy / len, ny = dx / len;
355
+ const mid = [(a.x + b.x) / 2, (a.y + b.y) / 2];
356
+ const bow = Math.min(0.22 * len, 60);
357
+ /* Bow (and label) on the outer side of the trail: for a steep trail that is the side away from the summit line. */
358
+ const side = a.side || 1;
359
+ const inward = Math.abs(nx) >= 0.45 ? nx * side < 0 : ((mid[0] + nx * bow) - f.cx) * side < 0;
360
+ if (inward) { nx = -nx; ny = -ny; }
361
+ const c = [mid[0] + nx * bow, mid[1] + ny * bow];
362
+ const sa = [c[0] - a.x, c[1] - a.y], la = Math.hypot(...sa) || 1;
363
+ const sb = [c[0] - b.x, c[1] - b.y], lb = Math.hypot(...sb) || 1;
364
+ const p0 = [a.x + (sa[0] / la) * (a.r + 5), a.y + (sa[1] / la) * (a.r + 5)];
365
+ const p1 = [b.x + (sb[0] / lb) * (b.r + 3.5), b.y + (sb[1] / lb) * (b.r + 3.5)]; // arrowhead touches the ring
366
+ const g = s("g", { class: "mtn-trail" }, trails);
367
+ s("path", { class: "mtn-trail-path", d: `M${px(p0[0])},${px(p0[1])} Q${px(c[0])},${px(c[1])} ${px(p1[0])},${px(p1[1])}`, fill: "none" }, g);
368
+ const tx = p1[0] - c[0], ty = p1[1] - c[1], tl = Math.hypot(tx, ty) || 1, ux = tx / tl, uy = ty / tl;
369
+ const tip = p1, back = [tip[0] - ux * 9, tip[1] - uy * 9];
370
+ s("polygon", { class: "mtn-trail-head", points: `${px(tip[0])},${px(tip[1])} ${px(back[0] - uy * 4)},${px(back[1] + ux * 4)} ${px(back[0] + uy * 4)},${px(back[1] - ux * 4)}` }, g);
371
+ if (t.label) {
372
+ const q = [0.25 * p0[0] + 0.5 * c[0] + 0.25 * p1[0], 0.25 * p0[1] + 0.5 * c[1] + 0.25 * p1[1]];
373
+ /* On a steep trail the label hangs off its outer side (anchored at the inner edge) so it never crosses back over the trail or the markers. */
374
+ const steep = Math.abs(nx) >= 0.45, off = steep ? 9 : 14;
375
+ s("text", { class: "mtn-trail-label", x: px(q[0] + nx * off), y: px(q[1] + ny * off + 4), "text-anchor": steep ? (nx > 0 ? "start" : "end") : "middle" }, g, t.label);
376
+ }
377
+ });
378
+
379
+ /* Markers. Drawn last so they sit above route, trails and snow. */
380
+ const markers = s("g", { class: "mtn-markers" }, svg);
381
+ const markerOf = new Map();
382
+ placed.forEach((m) => {
383
+ const it = m.item;
384
+ const cls = ["mtn-marker"];
385
+ if (it.rank === 1) cls.push("mtn-marker--leader");
386
+ if (it.rank === 2) cls.push("mtn-marker--silver");
387
+ if (it.rank === 3) cls.push("mtn-marker--bronze");
388
+ if (it.kind === "internal") cls.push("mtn-marker--internal");
389
+ if (m.synthetic) cls.push("mtn-marker--waypoint");
390
+ const g = s("g", { class: cls.join(" "), transform: `translate(${px(m.x)} ${px(m.y)})` }, markers);
391
+ s("circle", { class: "mtn-marker-halo", r: m.r + 6 }, g);
392
+ if (it.kind === "internal") s("circle", { class: "mtn-marker-ring", r: m.r + 4.5 }, g);
393
+ s("circle", { class: "mtn-marker-dot", r: m.r }, g);
394
+ if (!m.synthetic) {
395
+ s("text", { class: "mtn-marker-num", "text-anchor": "middle", dy: "0.36em" }, g, String(it.rank));
396
+ s("text", { class: "mtn-marker-val", "text-anchor": "middle", y: -(m.r + 8) }, g, fmt(it.value, d.unit));
397
+ s("desc", { id: `${id}-d${it.rank}` }, g, `${fmt(it.value, d.unit)} ${d.metric || ""}; rank ${it.rank} of ${n}${it.group ? `; ${it.group}` : ""}${it.kind === "internal" ? "; internal study" : ""}.`);
398
+ markerOf.set(it.rank, g);
399
+ } else {
400
+ s("title", {}, g, `${it.label}: ${fmt(it.value, d.unit)}`);
401
+ }
402
+ });
403
+
404
+ /* Hover / focus linking between pills and markers. */
405
+ pillOf.forEach((li, rank) => {
406
+ const g = markerOf.get(rank), line = connOf.get(rank);
407
+ const set = (on) => { li.classList.toggle("is-active", on); g && g.classList.toggle("is-active", on); line && line.classList.toggle("is-active", on); };
408
+ li.addEventListener("mouseenter", () => set(true));
409
+ li.addEventListener("mouseleave", () => set(false));
410
+ if (g) { g.addEventListener("mouseenter", () => set(true)); g.addEventListener("mouseleave", () => set(false)); }
411
+ });
412
+
413
+ /* Caption + legend. */
414
+ const caption = h("div", { class: "mountain-caption" }, canvas);
415
+ const noteParts = [];
416
+ if (d.metric) noteParts.push(isZh ? `高度 = ${d.metric}${d.unit ? `(${d.unit})` : ""};顶峰 = ${fmt(d.max, d.unit)}` : `Altitude = ${d.metric}${d.unit ? ` (${d.unit})` : ""}; summit = ${fmt(d.max, d.unit)}`);
417
+ if (d.date) noteParts.push(d.date);
418
+ if (d.caption) noteParts.push(d.caption);
419
+ h("p", { class: "mountain-note" }, caption, noteParts.join(" · "));
420
+ const legend = h("ul", { class: "mountain-legend", "aria-label": "Legend" }, caption);
421
+ const key = (cls, text) => { const li = h("li", {}, legend); h("i", { class: `mtn-key ${cls}`, "aria-hidden": "true" }, li); li.appendChild(document.createTextNode(text)); };
422
+ if (n) key("mtn-key--gold", "rank 1");
423
+ if (d.items.some((i) => i.kind !== "internal")) key("mtn-key--public", "public result");
424
+ if (d.items.some((i) => i.kind === "internal") || d.trails.length) key("mtn-key--internal", "internal study");
425
+ if (d.trails.length) key("mtn-key--trail", d.trailLegend || "gain with harness");
426
+
427
+ /* Ranked table for no-JS and screen readers: keep the page's own, or emit one. */
428
+ let table = el.querySelector(":scope > .mountain-table, :scope > .mountain-details .mountain-table, :scope > .mountain-table-wrap .mountain-table");
429
+ if (!table && o.table) {
430
+ table = h("table", { class: "mountain-table" });
431
+ h("caption", {}, table, `${d.title || "Leaderboard"} — ranked by ${d.metric || "score"}${d.date ? ` (${d.date})` : ""}.`);
432
+ const tr = h("tr", {}, h("thead", {}, table));
433
+ h("th", { scope: "col" }, tr, "#");
434
+ h("th", { scope: "col" }, tr, "Entry");
435
+ if (d.items.some((i) => i.group)) h("th", { scope: "col" }, tr, "Group");
436
+ h("th", { scope: "col" }, tr, d.metric || "Score");
437
+ const tb = h("tbody", {}, table);
438
+ d.items.forEach((it) => {
439
+ const row = h("tr", {}, tb);
440
+ h("td", {}, row, String(it.rank));
441
+ h("th", { scope: "row" }, row, it.label + (it.kind === "internal" ? " (internal study)" : ""));
442
+ if (d.items.some((i) => i.group)) h("td", {}, row, it.group);
443
+ h("td", {}, row, fmt(it.value, d.unit));
444
+ });
445
+ el.appendChild(table);
446
+ }
447
+ if (table && !table.closest(".mountain-details")) {
448
+ const details = h("details", { class: "mountain-details" });
449
+ h("summary", {}, details, `Ranked list · ${n} ${n === 1 ? "entry" : "entries"}`);
450
+ table.replaceWith(details);
451
+ details.appendChild(table);
452
+ }
453
+ }
454
+
455
+ function schedule() {
456
+ cancelAnimationFrame(raf);
457
+ raf = requestAnimationFrame(() => { if (canvas.clientWidth !== lastWidth) render(); });
458
+ }
459
+
460
+ inst = {
461
+ el, render,
462
+ update(next, opts) {
463
+ if (next) d = normalise(next);
464
+ if (opts) Object.assign(o, opts);
465
+ lastWidth = -1; render();
466
+ },
467
+ destroy() {
468
+ ro && ro.disconnect(); cancelAnimationFrame(raf);
469
+ instances.delete(el); el.classList.remove("mountain--ready", "mountain--compact", "mountain--narrow");
470
+ }
471
+ };
472
+ instances.set(el, inst);
473
+ render();
474
+ if (typeof ResizeObserver === "function") { ro = new ResizeObserver(schedule); ro.observe(canvas); }
475
+ else window.addEventListener("resize", schedule);
476
+ if (document.fonts && document.fonts.ready) document.fonts.ready.then(() => { lastWidth = -1; schedule(); }).catch(() => {});
477
+ return inst;
478
+ }
479
+
480
+ export function mountMountains(root = document) {
481
+ const out = [];
482
+ root.querySelectorAll("[data-mountain]").forEach((el) => {
483
+ const script = el.querySelector(':scope > script[type="application/json"]');
484
+ let data = null;
485
+ try { data = script ? JSON.parse(script.textContent) : null; }
486
+ catch (err) { el.classList.add("mountain--error"); return; }
487
+ if (!data) return;
488
+ const opts = {};
489
+ if (el.dataset.headingLevel) opts.headingLevel = Number(el.dataset.headingLevel);
490
+ if (el.dataset.compactBreakpoint) opts.compactBreakpoint = Number(el.dataset.compactBreakpoint);
491
+ out.push(mountain(el, data, opts));
492
+ });
493
+ return out;
494
+ }
495
+
496
+ if (typeof document !== "undefined") {
497
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", () => mountMountains(), { once: true });
498
+ else mountMountains();
499
+ }