ereniko commited on
Commit
19c8b69
·
verified ·
1 Parent(s): e991d46

Upload 3 files

Browse files
Files changed (3) hide show
  1. index.html +84 -61
  2. script.js +279 -81
  3. style.css +236 -18
index.html CHANGED
@@ -30,6 +30,87 @@
30
  <!-- populated by script.js -->
31
  </section>
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  <section class="submit-panel" aria-label="Submission rules">
34
  <button class="submit-toggle" id="submit-toggle" aria-expanded="false" aria-controls="submit-body">
35
  <span class="submit-toggle-left">
@@ -41,18 +122,17 @@
41
 
42
  <div class="submit-body" id="submit-body" hidden>
43
  <p class="submit-intro">
44
- There's no submission form; the leaderboard <em>is</em> <code>models.json</code>.
45
  Add your entry and open a PR against this Space.
46
  </p>
47
 
48
  <h3>Rules</h3>
49
  <ul class="rules-list">
50
  <li>Model must be public on the Hugging Face Hub, with a link in <code>url</code>.</li>
51
- <li>Evaluated on at least one Bench Labs benchmark using the method described in that dataset's README (lm-eval loglikelihood for <code>bench-mid-6-2026</code>, normalized exact-match for <code>bench-effortless-6-2026</code> / <code>bench-easy-6-2026</code>, blind panel rank-order for <code>bench-AGI</code>).</li>
52
  <li>Include <code>n</code> (sample count) for every benchmark you report a score for.</li>
53
  <li>If reporting a <code>bench-mid-6-2026</code> result, include the full <code>categories</code> breakdown, not just the headline score — this is what powers the detail panel.</li>
54
  <li>Link to your eval run or write-up in <code>eval_source</code> (a blog post, a script, a notebook — anything reproducible).</li>
55
- <li>No self-graded <code>bench-AGI</code> scores: the grader panel must not include the model being evaluated.</li>
56
  <li>One model per entry. If you have multiple checkpoints, submit them separately with distinct <code>id</code>s.</li>
57
  </ul>
58
 
@@ -76,8 +156,7 @@
76
  "runs": {
77
  "bench-effortless-6-2026": { "score": 0.0, "n": 240, "notes": "" },
78
  "bench-easy-6-2026": { "score": 0.0, "n": 300, "notes": "" },
79
- "bench-mid-6-2026": { "score": null, "n": null, "notes": "" },
80
- "bench-AGI": { "score": null, "n": null, "notes": "" }
81
  }
82
  }</code></pre>
83
 
@@ -88,62 +167,6 @@
88
  </div>
89
  </section>
90
 
91
- <div class="controls">
92
- <div class="search-pill">
93
- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="6"/><line x1="20" y1="20" x2="15.5" y2="15.5"/></svg>
94
- <input type="search" id="model-search" placeholder="Search models or orgs…" aria-label="Search models">
95
- </div>
96
-
97
- <div class="metric-select-wrap">
98
- <select class="pill-select" id="metric-select" aria-label="Metric"></select>
99
- <span class="select-arrow">▼</span>
100
- </div>
101
-
102
- <div class="sort-select-wrap">
103
- <select class="pill-select" id="sort-select" aria-label="Sort order">
104
- <option value="score-desc">Score: High → Low</option>
105
- <option value="score-asc">Score: Low → High</option>
106
- <option value="params-asc">Params: Small → Large</option>
107
- <option value="params-desc">Params: Large → Small</option>
108
- <option value="name-asc">Name: A → Z</option>
109
- </select>
110
- <span class="select-arrow">▼</span>
111
- </div>
112
- </div>
113
-
114
- <div class="table-scroll">
115
- <table class="leaderboard" id="leaderboard-table">
116
- <thead>
117
- <tr>
118
- <th data-key="rank" style="width:46px;">#</th>
119
- <th data-key="name">Model</th>
120
- <th data-key="params">Params</th>
121
- <th data-key="score">Score <span class="sort-arrow">▼</span></th>
122
- </tr>
123
- </thead>
124
- <tbody id="leaderboard-body">
125
- <!-- populated by script.js -->
126
- </tbody>
127
- </table>
128
- <div class="empty-state" id="empty-state" hidden>
129
- <svg width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.5" y2="16.5"/></svg>
130
- <p>No models match that search on this benchmark yet.</p>
131
- </div>
132
- </div>
133
-
134
- <section class="detail-panel" id="detail-panel" aria-live="polite">
135
- <div class="detail-panel-header">
136
- <div>
137
- <h2 id="detail-title">Model</h2>
138
- <p id="detail-subtitle">Per-category breakdown</p>
139
- </div>
140
- <button class="detail-close" id="detail-close" aria-label="Close detail panel">
141
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
142
- </button>
143
- </div>
144
- <div class="cat-grid" id="detail-cats"></div>
145
- </section>
146
-
147
  <footer class="page-footer">
148
  <span>Data from <a href="models.json">models.json</a> · loaded client-side, no build step.</span>
149
  <span>
 
30
  <!-- populated by script.js -->
31
  </section>
32
 
33
+ <div class="controls">
34
+ <div class="filter-wrap">
35
+ <button
36
+ type="button"
37
+ class="search-circle-btn filter-btn tooltip"
38
+ id="params-filter-btn"
39
+ aria-label="Filter by parameter count"
40
+ aria-expanded="false"
41
+ data-tooltip="Filter by parameter count">
42
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
43
+ <line x1="4" y1="6" x2="20" y2="6"/><circle cx="9" cy="6" r="2"/>
44
+ <line x1="4" y1="12" x2="20" y2="12"/><circle cx="16" cy="12" r="2"/>
45
+ <line x1="4" y1="18" x2="20" y2="18"/><circle cx="7" cy="18" r="2"/>
46
+ </svg>
47
+ </button>
48
+
49
+ <div class="filter-popup" id="params-filter-popup" hidden>
50
+ <div class="filter-popup-title">Parameters (B)</div>
51
+ <div class="range-slider" id="params-range-slider">
52
+ <div class="range-slider-track"></div>
53
+ <div class="range-slider-fill" id="params-range-fill"></div>
54
+ <input type="range" id="params-range-min" class="range-thumb range-thumb-min" min="0" max="100" step="0.1" value="0" aria-label="Minimum parameters in billions">
55
+ <input type="range" id="params-range-max" class="range-thumb range-thumb-max" min="0" max="100" step="0.1" value="100" aria-label="Maximum parameters in billions">
56
+ </div>
57
+ <div class="range-slider-values">
58
+ <span id="params-range-min-label">0B</span>
59
+ <span id="params-range-max-label">100B+</span>
60
+ </div>
61
+ <button type="button" class="filter-reset" id="params-filter-reset">Reset</button>
62
+ </div>
63
+ </div>
64
+
65
+ <div class="search-pill">
66
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="6"/><line x1="20" y1="20" x2="15.5" y2="15.5"/></svg>
67
+ <input type="search" id="model-search" placeholder="Search models or orgs…" aria-label="Search models">
68
+ </div>
69
+
70
+ <div class="sort-select-wrap">
71
+ <select class="pill-select" id="sort-select" aria-label="Sort order">
72
+ <option value="score-desc">Score: High → Low</option>
73
+ <option value="score-asc">Score: Low → High</option>
74
+ <option value="params-asc">Params: Small → Large</option>
75
+ <option value="params-desc">Params: Large → Small</option>
76
+ <option value="name-asc">Name: A → Z</option>
77
+ </select>
78
+ <span class="select-arrow">▼</span>
79
+ </div>
80
+ </div>
81
+
82
+ <div class="table-scroll">
83
+ <table class="leaderboard" id="leaderboard-table">
84
+ <thead id="leaderboard-head">
85
+ <!-- populated by script.js: rank/model/params + one column per metric for the active benchmark -->
86
+ </thead>
87
+ <tbody id="leaderboard-body">
88
+ <!-- populated by script.js -->
89
+ </tbody>
90
+ </table>
91
+ <div class="empty-state" id="empty-state" hidden>
92
+ <svg width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.5" y2="16.5"/></svg>
93
+ <p>No models match your search or filters on this benchmark yet.</p>
94
+ </div>
95
+ <div class="unavailable-state" id="unavailable-state" hidden>
96
+ <svg width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M12 9v4M12 17h.01M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z"/></svg>
97
+ <p id="unavailable-text">This benchmark is unavailable.</p>
98
+ </div>
99
+ </div>
100
+
101
+ <section class="detail-panel" id="detail-panel" aria-live="polite">
102
+ <div class="detail-panel-header">
103
+ <div>
104
+ <h2 id="detail-title">Model</h2>
105
+ <p id="detail-subtitle">Per-category breakdown</p>
106
+ </div>
107
+ <button class="detail-close" id="detail-close" aria-label="Close detail panel">
108
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
109
+ </button>
110
+ </div>
111
+ <div class="cat-grid" id="detail-cats"></div>
112
+ </section>
113
+
114
  <section class="submit-panel" aria-label="Submission rules">
115
  <button class="submit-toggle" id="submit-toggle" aria-expanded="false" aria-controls="submit-body">
116
  <span class="submit-toggle-left">
 
122
 
123
  <div class="submit-body" id="submit-body" hidden>
124
  <p class="submit-intro">
125
+ There's no submission form the leaderboard <em>is</em> <code>models.json</code>.
126
  Add your entry and open a PR against this Space.
127
  </p>
128
 
129
  <h3>Rules</h3>
130
  <ul class="rules-list">
131
  <li>Model must be public on the Hugging Face Hub, with a link in <code>url</code>.</li>
132
+ <li>Evaluated on at least one Bench Labs benchmark using the method described in that dataset's README (lm-eval loglikelihood for <code>bench-mid-6-2026</code>, normalized exact-match for <code>bench-effortless-6-2026</code> / <code>bench-easy-6-2026</code>).</li>
133
  <li>Include <code>n</code> (sample count) for every benchmark you report a score for.</li>
134
  <li>If reporting a <code>bench-mid-6-2026</code> result, include the full <code>categories</code> breakdown, not just the headline score — this is what powers the detail panel.</li>
135
  <li>Link to your eval run or write-up in <code>eval_source</code> (a blog post, a script, a notebook — anything reproducible).</li>
 
136
  <li>One model per entry. If you have multiple checkpoints, submit them separately with distinct <code>id</code>s.</li>
137
  </ul>
138
 
 
156
  "runs": {
157
  "bench-effortless-6-2026": { "score": 0.0, "n": 240, "notes": "" },
158
  "bench-easy-6-2026": { "score": 0.0, "n": 300, "notes": "" },
159
+ "bench-mid-6-2026": { "score": null, "n": null, "notes": "" }
 
160
  }
161
  }</code></pre>
162
 
 
167
  </div>
168
  </section>
169
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  <footer class="page-footer">
171
  <span>Data from <a href="models.json">models.json</a> · loaded client-side, no build step.</span>
172
  <span>
script.js CHANGED
@@ -6,19 +6,34 @@
6
  activeBenchmark: null,
7
  search: "",
8
  sort: "score-desc",
 
 
9
  };
10
 
11
  const els = {};
12
 
 
 
 
 
 
 
 
 
 
 
 
13
  document.addEventListener("DOMContentLoaded", init);
14
 
15
  async function init() {
16
  els.tierStrip = document.getElementById("tier-strip");
17
- els.metricSelect = document.getElementById("metric-select");
18
  els.sortSelect = document.getElementById("sort-select");
19
  els.searchInput = document.getElementById("model-search");
 
20
  els.tbody = document.getElementById("leaderboard-body");
21
  els.emptyState = document.getElementById("empty-state");
 
 
22
  els.table = document.getElementById("leaderboard-table");
23
  els.detailPanel = document.getElementById("detail-panel");
24
  els.detailTitle = document.getElementById("detail-title");
@@ -27,6 +42,14 @@
27
  els.detailClose = document.getElementById("detail-close");
28
  els.submitToggle = document.getElementById("submit-toggle");
29
  els.submitBody = document.getElementById("submit-body");
 
 
 
 
 
 
 
 
30
 
31
  bindSubmitPanel();
32
 
@@ -39,18 +62,20 @@
39
  return;
40
  }
41
 
42
- state.activeBenchmark = state.data.benchmarks[0]?.id || null;
 
 
43
 
 
44
  buildTierStrip();
45
- buildMetricSelect();
46
  bindControls();
 
47
  render();
48
  }
49
 
50
  function renderLoadError(err) {
51
- els.tbody = document.getElementById("leaderboard-body");
52
  els.tbody.innerHTML =
53
- '<tr><td colspan="4" style="padding:32px;text-align:center;color:var(--muted);">' +
54
  "Couldn't load <code>models.json</code> (" + escapeHtml(String(err.message || err)) +
55
  "). Make sure it sits next to index.html." +
56
  "</td></tr>";
@@ -77,13 +102,19 @@
77
  .forEach((bench) => {
78
  const card = document.createElement("button");
79
  card.type = "button";
80
- card.className = "tier-card" + (bench.id === state.activeBenchmark ? " active" : "");
 
 
 
81
  card.dataset.benchmarkId = bench.id;
82
 
83
  const rowsLabel = bench.rows != null ? bench.rows + " rows" : "open-ended";
 
 
 
84
 
85
  card.innerHTML =
86
- '<span class="tier-num">Tier ' + bench.tier + " · " + escapeHtml(bench.label) + "</span>" +
87
  "<h3>" + escapeHtml(bench.id) + "</h3>" +
88
  "<p>" + escapeHtml(bench.description) + "</p>" +
89
  '<div class="tier-meta">' +
@@ -95,7 +126,6 @@
95
  state.activeBenchmark = bench.id;
96
  closeDetail();
97
  buildTierStrip();
98
- buildMetricSelect();
99
  render();
100
  });
101
 
@@ -106,43 +136,6 @@
106
  els.tierStrip.appendChild(frag);
107
  }
108
 
109
- // -------------------- METRIC SELECT --------------------
110
-
111
- function buildMetricSelect() {
112
- const bench = getActiveBenchmark();
113
- if (!bench) return;
114
-
115
- // Collect every metric key present across model runs for this benchmark,
116
- // beyond the headline "score", so users can pivot (acc vs acc_norm etc).
117
- const keys = new Set(["score"]);
118
- state.data.models.forEach((m) => {
119
- const run = m.runs?.[bench.id];
120
- if (!run) return;
121
- Object.keys(run).forEach((k) => {
122
- if (typeof run[k] === "number" && k !== "n" && k !== "stderr") keys.add(k);
123
- });
124
- });
125
-
126
- const labels = {
127
- score: "Headline score",
128
- acc: "acc",
129
- acc_norm: "acc_norm",
130
- soft_score: "soft_score",
131
- soft_score_norm: "soft_score_norm",
132
- stderr: "stderr",
133
- };
134
-
135
- const prev = els.metricSelect.value;
136
- els.metricSelect.innerHTML = "";
137
- Array.from(keys).forEach((k) => {
138
- const opt = document.createElement("option");
139
- opt.value = k;
140
- opt.textContent = labels[k] || k;
141
- els.metricSelect.appendChild(opt);
142
- });
143
- if (keys.has(prev)) els.metricSelect.value = prev;
144
- }
145
-
146
  // -------------------- CONTROLS --------------------
147
 
148
  function bindControls() {
@@ -156,24 +149,114 @@
156
  render();
157
  });
158
 
159
- els.metricSelect.addEventListener("change", render);
 
160
 
161
- els.table.querySelectorAll("thead th[data-key]").forEach((th) => {
162
- th.addEventListener("click", () => {
163
- const key = th.dataset.key;
164
- if (key === "score") {
165
- state.sort = state.sort === "score-desc" ? "score-asc" : "score-desc";
166
- } else if (key === "params") {
167
- state.sort = state.sort === "params-asc" ? "params-desc" : "params-asc";
168
- } else if (key === "name") {
169
- state.sort = "name-asc";
170
- }
171
- els.sortSelect.value = state.sort;
172
- render();
173
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  });
175
 
176
- els.detailClose.addEventListener("click", closeDetail);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  }
178
 
179
  // -------------------- HELPERS --------------------
@@ -182,6 +265,26 @@
182
  return state.data.benchmarks.find((b) => b.id === state.activeBenchmark) || null;
183
  }
184
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  function getMetricValue(run, metricKey) {
186
  if (!run) return null;
187
  const v = run[metricKey];
@@ -214,11 +317,22 @@
214
  const bench = getActiveBenchmark();
215
  if (!bench) return;
216
 
217
- const metricKey = els.metricSelect.value || "score";
 
 
 
 
 
 
 
 
 
 
 
218
 
219
  let rows = state.data.models.map((m) => {
220
  const run = m.runs?.[bench.id] || null;
221
- const value = getMetricValue(run, metricKey);
222
  return { model: m, run, value };
223
  });
224
 
@@ -230,28 +344,95 @@
230
  );
231
  }
232
 
 
 
 
 
 
 
 
 
 
 
 
233
  rows = sortRows(rows, state.sort);
234
 
235
  els.tbody.innerHTML = "";
236
  if (rows.length === 0) {
237
  els.emptyState.hidden = false;
 
238
  els.table.hidden = true;
239
  return;
240
  }
241
  els.emptyState.hidden = true;
 
242
  els.table.hidden = false;
243
 
244
  const frag = document.createDocumentFragment();
245
  rows.forEach((r, idx) => {
246
- frag.appendChild(buildRow(r, idx + 1, bench));
247
  });
248
  els.tbody.appendChild(frag);
249
 
250
  updateSortIndicators();
251
  }
252
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  function sortRows(rows, sort) {
254
- const withScore = (r) => (r.value == null ? -Infinity : r.value);
255
  const withParams = (r) => (r.model.params_b == null ? Infinity : r.model.params_b);
256
 
257
  switch (sort) {
@@ -280,7 +461,8 @@
280
  }
281
 
282
  function updateSortIndicators() {
283
- els.table.querySelectorAll("thead th[data-key]").forEach((th) => {
 
284
  th.classList.remove("sorted");
285
  const arrow = th.querySelector(".sort-arrow");
286
  if (!arrow) return;
@@ -296,7 +478,7 @@
296
  });
297
  }
298
 
299
- function buildRow(r, rank, bench) {
300
  const tr = document.createElement("tr");
301
  const { model, run, value } = r;
302
 
@@ -322,21 +504,37 @@
322
  paramsTd.textContent = model.params_b != null ? model.params_b + "B" : "—";
323
  tr.appendChild(paramsTd);
324
 
325
- // score
326
- const scoreTd = document.createElement("td");
327
- scoreTd.className = "score-cell";
328
- if (value == null) {
329
- scoreTd.innerHTML = '<span class="score-na">not evaluated</span>';
330
- } else {
331
- const pct = Math.max(0, Math.min(100, value * 100));
332
- const cls = scoreClass(value);
333
- scoreTd.innerHTML =
334
- '<span class="score-pill">' +
335
- '<span class="score-num ' + cls + '">' + fmtScore(value) + "</span>" +
336
- '<span class="score-bar-track"><span class="score-bar-fill" style="width:' + pct.toFixed(1) + '%"></span></span>' +
337
- "</span>";
338
- }
339
- tr.appendChild(scoreTd);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
 
341
  if (run && run.categories) {
342
  tr.style.cursor = "pointer";
 
6
  activeBenchmark: null,
7
  search: "",
8
  sort: "score-desc",
9
+ paramsBounds: { min: 0, max: 100 }, // full data range, computed on load
10
+ paramsFilter: { min: 0, max: 100 }, // current filter selection
11
  };
12
 
13
  const els = {};
14
 
15
+ // Per-benchmark metric columns, in display order. "score" always maps to
16
+ // the headline number every run object carries; the rest are the extra
17
+ // numeric fields lm-eval-style runs report (acc, acc_norm, etc).
18
+ const METRIC_LABELS = {
19
+ score: "Score",
20
+ acc: "acc",
21
+ acc_norm: "acc_norm",
22
+ soft_score: "soft_score",
23
+ soft_score_norm: "soft_score_norm",
24
+ };
25
+
26
  document.addEventListener("DOMContentLoaded", init);
27
 
28
  async function init() {
29
  els.tierStrip = document.getElementById("tier-strip");
 
30
  els.sortSelect = document.getElementById("sort-select");
31
  els.searchInput = document.getElementById("model-search");
32
+ els.thead = document.getElementById("leaderboard-head");
33
  els.tbody = document.getElementById("leaderboard-body");
34
  els.emptyState = document.getElementById("empty-state");
35
+ els.unavailableState = document.getElementById("unavailable-state");
36
+ els.unavailableText = document.getElementById("unavailable-text");
37
  els.table = document.getElementById("leaderboard-table");
38
  els.detailPanel = document.getElementById("detail-panel");
39
  els.detailTitle = document.getElementById("detail-title");
 
42
  els.detailClose = document.getElementById("detail-close");
43
  els.submitToggle = document.getElementById("submit-toggle");
44
  els.submitBody = document.getElementById("submit-body");
45
+ els.filterBtn = document.getElementById("params-filter-btn");
46
+ els.filterPopup = document.getElementById("params-filter-popup");
47
+ els.rangeMin = document.getElementById("params-range-min");
48
+ els.rangeMax = document.getElementById("params-range-max");
49
+ els.rangeFill = document.getElementById("params-range-fill");
50
+ els.rangeMinLabel = document.getElementById("params-range-min-label");
51
+ els.rangeMaxLabel = document.getElementById("params-range-max-label");
52
+ els.filterReset = document.getElementById("params-filter-reset");
53
 
54
  bindSubmitPanel();
55
 
 
62
  return;
63
  }
64
 
65
+ // Land on the first available (non-unavailable) benchmark if possible.
66
+ const firstAvailable = state.data.benchmarks.find((b) => !b.unavailable);
67
+ state.activeBenchmark = (firstAvailable || state.data.benchmarks[0])?.id || null;
68
 
69
+ initParamsBounds();
70
  buildTierStrip();
 
71
  bindControls();
72
+ bindParamsFilter();
73
  render();
74
  }
75
 
76
  function renderLoadError(err) {
 
77
  els.tbody.innerHTML =
78
+ '<tr><td colspan="6" style="padding:32px;text-align:center;color:var(--muted);">' +
79
  "Couldn't load <code>models.json</code> (" + escapeHtml(String(err.message || err)) +
80
  "). Make sure it sits next to index.html." +
81
  "</td></tr>";
 
102
  .forEach((bench) => {
103
  const card = document.createElement("button");
104
  card.type = "button";
105
+ card.className =
106
+ "tier-card" +
107
+ (bench.id === state.activeBenchmark ? " active" : "") +
108
+ (bench.unavailable ? " is-unavailable" : "");
109
  card.dataset.benchmarkId = bench.id;
110
 
111
  const rowsLabel = bench.rows != null ? bench.rows + " rows" : "open-ended";
112
+ const badge = bench.unavailable
113
+ ? '<span class="tier-badge-unavailable">Unavailable</span>'
114
+ : "";
115
 
116
  card.innerHTML =
117
+ '<span class="tier-num">Tier ' + bench.tier + " · " + escapeHtml(bench.label) + badge + "</span>" +
118
  "<h3>" + escapeHtml(bench.id) + "</h3>" +
119
  "<p>" + escapeHtml(bench.description) + "</p>" +
120
  '<div class="tier-meta">' +
 
126
  state.activeBenchmark = bench.id;
127
  closeDetail();
128
  buildTierStrip();
 
129
  render();
130
  });
131
 
 
136
  els.tierStrip.appendChild(frag);
137
  }
138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  // -------------------- CONTROLS --------------------
140
 
141
  function bindControls() {
 
149
  render();
150
  });
151
 
152
+ els.detailClose.addEventListener("click", closeDetail);
153
+ }
154
 
155
+ // -------------------- PARAMS FILTER --------------------
156
+
157
+ // Derive slider bounds from the actual dataset so the range always matches
158
+ // whatever models are in models.json (no hardcoded cap).
159
+ function initParamsBounds() {
160
+ const values = state.data.models
161
+ .map((m) => m.params_b)
162
+ .filter((v) => typeof v === "number");
163
+
164
+ let min = 0;
165
+ let max = values.length ? Math.max(...values) : 100;
166
+ // round the ceiling up a bit so the top model's dot isn't pinned to the
167
+ // very edge of the track, and so the slider still makes sense with 1 model
168
+ max = Math.max(1, Math.ceil(max * 1.1 * 10) / 10);
169
+
170
+ state.paramsBounds = { min, max };
171
+ state.paramsFilter = { min, max };
172
+
173
+ els.rangeMin.min = String(min);
174
+ els.rangeMin.max = String(max);
175
+ els.rangeMin.step = "0.1";
176
+ els.rangeMin.value = String(min);
177
+
178
+ els.rangeMax.min = String(min);
179
+ els.rangeMax.max = String(max);
180
+ els.rangeMax.step = "0.1";
181
+ els.rangeMax.value = String(max);
182
+ }
183
+
184
+ function bindParamsFilter() {
185
+ if (!els.filterBtn) return;
186
+
187
+ els.filterBtn.addEventListener("click", (e) => {
188
+ e.stopPropagation();
189
+ const open = els.filterBtn.getAttribute("aria-expanded") === "true";
190
+ setFilterPopupOpen(!open);
191
  });
192
 
193
+ document.addEventListener("click", (e) => {
194
+ if (els.filterPopup.hidden) return;
195
+ if (els.filterPopup.contains(e.target) || els.filterBtn.contains(e.target)) return;
196
+ setFilterPopupOpen(false);
197
+ });
198
+
199
+ document.addEventListener("keydown", (e) => {
200
+ if (e.key === "Escape" && !els.filterPopup.hidden) setFilterPopupOpen(false);
201
+ });
202
+
203
+ const onSlide = () => {
204
+ let lo = parseFloat(els.rangeMin.value);
205
+ let hi = parseFloat(els.rangeMax.value);
206
+ // keep the two thumbs from crossing
207
+ if (lo > hi) {
208
+ [lo, hi] = [hi, lo];
209
+ }
210
+ state.paramsFilter = { min: lo, max: hi };
211
+ updateRangeVisuals();
212
+ render();
213
+ };
214
+
215
+ els.rangeMin.addEventListener("input", onSlide);
216
+ els.rangeMax.addEventListener("input", onSlide);
217
+
218
+ els.filterReset.addEventListener("click", () => {
219
+ const { min, max } = state.paramsBounds;
220
+ state.paramsFilter = { min, max };
221
+ els.rangeMin.value = String(min);
222
+ els.rangeMax.value = String(max);
223
+ updateRangeVisuals();
224
+ render();
225
+ });
226
+
227
+ updateRangeVisuals();
228
+ }
229
+
230
+ function setFilterPopupOpen(open) {
231
+ els.filterPopup.hidden = !open;
232
+ els.filterBtn.setAttribute("aria-expanded", String(open));
233
+ }
234
+
235
+ function isParamsFilterActive() {
236
+ const { min, max } = state.paramsBounds;
237
+ return state.paramsFilter.min > min || state.paramsFilter.max < max;
238
+ }
239
+
240
+ function updateRangeVisuals() {
241
+ const { min, max } = state.paramsBounds;
242
+ const span = Math.max(0.0001, max - min);
243
+ const loPct = ((state.paramsFilter.min - min) / span) * 100;
244
+ const hiPct = ((state.paramsFilter.max - min) / span) * 100;
245
+
246
+ els.rangeFill.style.left = loPct.toFixed(2) + "%";
247
+ els.rangeFill.style.right = (100 - hiPct).toFixed(2) + "%";
248
+
249
+ const fmt = (v) => {
250
+ const isMax = v >= max;
251
+ const rounded = Math.round(v * 10) / 10;
252
+ return (Number.isInteger(rounded) ? rounded.toFixed(0) : rounded.toFixed(1)) + "B" + (isMax ? "+" : "");
253
+ };
254
+ els.rangeMinLabel.textContent = fmt(state.paramsFilter.min);
255
+ els.rangeMaxLabel.textContent = fmt(state.paramsFilter.max);
256
+
257
+ const active = isParamsFilterActive();
258
+ els.filterBtn.classList.toggle("has-active-filter", active);
259
+ els.filterReset.disabled = !active;
260
  }
261
 
262
  // -------------------- HELPERS --------------------
 
265
  return state.data.benchmarks.find((b) => b.id === state.activeBenchmark) || null;
266
  }
267
 
268
+ // Every metric column to show for this benchmark: "score" first, then any
269
+ // other numeric fields present on any model's run object (acc, acc_norm...).
270
+ function getMetricColumns(bench) {
271
+ const keys = ["score"];
272
+ const seen = new Set(keys);
273
+ state.data.models.forEach((m) => {
274
+ const run = m.runs?.[bench.id];
275
+ if (!run) return;
276
+ Object.keys(run).forEach((k) => {
277
+ if (k === "n" || k === "stderr" || k === "notes" || k === "categories") return;
278
+ if (typeof run[k] !== "number") return;
279
+ if (!seen.has(k)) {
280
+ seen.add(k);
281
+ keys.push(k);
282
+ }
283
+ });
284
+ });
285
+ return keys;
286
+ }
287
+
288
  function getMetricValue(run, metricKey) {
289
  if (!run) return null;
290
  const v = run[metricKey];
 
317
  const bench = getActiveBenchmark();
318
  if (!bench) return;
319
 
320
+ if (bench.unavailable) {
321
+ els.table.hidden = true;
322
+ els.emptyState.hidden = true;
323
+ els.unavailableState.hidden = false;
324
+ els.unavailableText.textContent =
325
+ bench.unavailable_reason || (bench.id + " is currently unavailable.");
326
+ closeDetail();
327
+ return;
328
+ }
329
+
330
+ const metricCols = getMetricColumns(bench);
331
+ buildHead(metricCols);
332
 
333
  let rows = state.data.models.map((m) => {
334
  const run = m.runs?.[bench.id] || null;
335
+ const value = getMetricValue(run, "score");
336
  return { model: m, run, value };
337
  });
338
 
 
344
  );
345
  }
346
 
347
+ if (isParamsFilterActive()) {
348
+ const { min, max } = state.paramsFilter;
349
+ const { max: dataMax } = state.paramsBounds;
350
+ rows = rows.filter((r) => {
351
+ const p = r.model.params_b;
352
+ if (p == null) return false; // unknown size excluded once a filter is set
353
+ if (max >= dataMax) return p >= min; // top thumb at the ceiling = open-ended "+"
354
+ return p >= min && p <= max;
355
+ });
356
+ }
357
+
358
  rows = sortRows(rows, state.sort);
359
 
360
  els.tbody.innerHTML = "";
361
  if (rows.length === 0) {
362
  els.emptyState.hidden = false;
363
+ els.unavailableState.hidden = true;
364
  els.table.hidden = true;
365
  return;
366
  }
367
  els.emptyState.hidden = true;
368
+ els.unavailableState.hidden = true;
369
  els.table.hidden = false;
370
 
371
  const frag = document.createDocumentFragment();
372
  rows.forEach((r, idx) => {
373
+ frag.appendChild(buildRow(r, idx + 1, bench, metricCols));
374
  });
375
  els.tbody.appendChild(frag);
376
 
377
  updateSortIndicators();
378
  }
379
 
380
+ function buildHead(metricCols) {
381
+ const tr = document.createElement("tr");
382
+
383
+ const thRank = document.createElement("th");
384
+ thRank.dataset.key = "rank";
385
+ thRank.style.width = "46px";
386
+ thRank.textContent = "#";
387
+ tr.appendChild(thRank);
388
+
389
+ const thName = document.createElement("th");
390
+ thName.dataset.key = "name";
391
+ thName.textContent = "Model";
392
+ tr.appendChild(thName);
393
+
394
+ const thParams = document.createElement("th");
395
+ thParams.dataset.key = "params";
396
+ thParams.innerHTML = "Params <span class=\"sort-arrow\">▼</span>";
397
+ tr.appendChild(thParams);
398
+
399
+ const thN = document.createElement("th");
400
+ thN.textContent = "n";
401
+ tr.appendChild(thN);
402
+
403
+ metricCols.forEach((key, i) => {
404
+ const th = document.createElement("th");
405
+ th.dataset.key = i === 0 ? "score" : "metric-" + key;
406
+ const label = METRIC_LABELS[key] || key;
407
+ th.innerHTML = escapeHtml(label) + (i === 0 ? ' <span class="sort-arrow">▼</span>' : "");
408
+ tr.appendChild(th);
409
+ });
410
+
411
+ els.thead.innerHTML = "";
412
+ els.thead.appendChild(tr);
413
+
414
+ // (re)bind header click-to-sort now that the head was rebuilt
415
+ els.thead.querySelectorAll("th[data-key]").forEach((th) => {
416
+ th.addEventListener("click", () => {
417
+ const key = th.dataset.key;
418
+ if (key === "score") {
419
+ state.sort = state.sort === "score-desc" ? "score-asc" : "score-desc";
420
+ } else if (key === "params") {
421
+ state.sort = state.sort === "params-asc" ? "params-desc" : "params-asc";
422
+ } else if (key === "name") {
423
+ state.sort = "name-asc";
424
+ } else {
425
+ return; // secondary metric columns are informational, not sortable
426
+ }
427
+ els.sortSelect.value = state.sort;
428
+ render();
429
+ });
430
+ });
431
+
432
+ updateSortIndicators();
433
+ }
434
+
435
  function sortRows(rows, sort) {
 
436
  const withParams = (r) => (r.model.params_b == null ? Infinity : r.model.params_b);
437
 
438
  switch (sort) {
 
461
  }
462
 
463
  function updateSortIndicators() {
464
+ if (!els.thead) return;
465
+ els.thead.querySelectorAll("th[data-key]").forEach((th) => {
466
  th.classList.remove("sorted");
467
  const arrow = th.querySelector(".sort-arrow");
468
  if (!arrow) return;
 
478
  });
479
  }
480
 
481
+ function buildRow(r, rank, bench, metricCols) {
482
  const tr = document.createElement("tr");
483
  const { model, run, value } = r;
484
 
 
504
  paramsTd.textContent = model.params_b != null ? model.params_b + "B" : "—";
505
  tr.appendChild(paramsTd);
506
 
507
+ // n (sample count)
508
+ const nTd = document.createElement("td");
509
+ nTd.className = "n-cell";
510
+ nTd.textContent = run && run.n != null ? String(run.n) : "—";
511
+ tr.appendChild(nTd);
512
+
513
+ // one cell per metric column
514
+ metricCols.forEach((key, i) => {
515
+ const td = document.createElement("td");
516
+ const v = getMetricValue(run, key);
517
+
518
+ if (i === 0) {
519
+ // primary "score" column: bar + colored number
520
+ td.className = "score-cell";
521
+ if (v == null) {
522
+ td.innerHTML = '<span class="score-na">not evaluated</span>';
523
+ } else {
524
+ const pct = Math.max(0, Math.min(100, v * 100));
525
+ const cls = scoreClass(v);
526
+ td.innerHTML =
527
+ '<span class="score-pill">' +
528
+ '<span class="score-num ' + cls + '">' + fmtScore(v) + "</span>" +
529
+ '<span class="score-bar-track"><span class="score-bar-fill" style="width:' + pct.toFixed(1) + '%"></span></span>' +
530
+ "</span>";
531
+ }
532
+ } else {
533
+ td.className = "metric-cell";
534
+ td.innerHTML = v == null ? '<span class="metric-na">—</span>' : fmtScore(v);
535
+ }
536
+ tr.appendChild(td);
537
+ });
538
 
539
  if (run && run.categories) {
540
  tr.style.cursor = "pointer";
style.css CHANGED
@@ -116,23 +116,6 @@ a:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; border-
116
  to { transform: translateX(120vw) scale(var(--cloud-scale, 1)); }
117
  }
118
 
119
- .grass {
120
- position: fixed;
121
- bottom: -2px; left: 0;
122
- width: 100%; height: 70px;
123
- z-index: 50;
124
- pointer-events: none;
125
- }
126
-
127
- @keyframes blade-sway {
128
- from { transform: rotate(-2.4deg); }
129
- to { transform: rotate(3.2deg); }
130
- }
131
-
132
- @media (prefers-reduced-motion: reduce) {
133
- .grass path { animation: none !important; }
134
- }
135
-
136
  /* -------------------- LAYOUT -------------------- */
137
 
138
  .container {
@@ -410,10 +393,188 @@ a:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; border-
410
 
411
  .search-pill svg { color: var(--muted); flex-shrink: 0; margin-right: 2px; }
412
 
413
- .metric-select-wrap, .sort-select-wrap {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  position: relative;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
  }
416
 
 
 
 
 
417
  .pill-select {
418
  appearance: none;
419
  -webkit-appearance: none;
@@ -554,6 +715,29 @@ table.leaderboard td {
554
  white-space: nowrap;
555
  }
556
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
557
  .score-pill {
558
  display: inline-flex;
559
  align-items: center;
@@ -605,6 +789,38 @@ table.leaderboard td {
605
 
606
  .empty-state svg { margin-bottom: 14px; color: var(--accent-soft); }
607
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
608
  /* -------------------- DETAIL PANEL -------------------- */
609
 
610
  .detail-panel {
@@ -721,6 +937,8 @@ table.leaderboard td {
721
  @media (max-width: 640px) {
722
  .container { padding: 40px 16px 32px; }
723
  .controls { flex-direction: column; align-items: stretch; }
 
 
724
  table.leaderboard { min-width: 560px; }
725
  }
726
 
 
116
  to { transform: translateX(120vw) scale(var(--cloud-scale, 1)); }
117
  }
118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  /* -------------------- LAYOUT -------------------- */
120
 
121
  .container {
 
393
 
394
  .search-pill svg { color: var(--muted); flex-shrink: 0; margin-right: 2px; }
395
 
396
+ .sort-select-wrap {
397
+ position: relative;
398
+ }
399
+
400
+ /* -------------------- FILTER BUTTON + POPUP -------------------- */
401
+
402
+ .filter-wrap {
403
+ position: relative;
404
+ flex-shrink: 0;
405
+ }
406
+
407
+ .search-circle-btn {
408
+ flex-shrink: 0;
409
+ width: 42px;
410
+ height: 42px;
411
+ border-radius: 50%;
412
+ border: 1px solid var(--border);
413
+ background: var(--surface);
414
+ color: var(--accent);
415
+ display: flex;
416
+ align-items: center;
417
+ justify-content: center;
418
+ cursor: pointer;
419
+ transition: background var(--transition), border-color var(--transition), transform var(--transition), color var(--transition);
420
+ backdrop-filter: blur(20px) saturate(1.2);
421
+ -webkit-backdrop-filter: blur(20px) saturate(1.2);
422
+ box-shadow: var(--shadow-soft);
423
+ font-family: inherit;
424
+ }
425
+
426
+ .search-circle-btn:hover {
427
+ background: #fff;
428
+ border-color: var(--accent);
429
+ transform: scale(1.05);
430
+ }
431
+
432
+ .search-circle-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
433
+
434
+ .filter-btn.has-active-filter {
435
+ background: var(--accent);
436
+ border-color: var(--accent);
437
+ color: #fff;
438
+ }
439
+
440
+ .filter-btn[aria-expanded="true"] {
441
+ background: #fff;
442
+ border-color: var(--accent);
443
+ color: var(--accent);
444
+ }
445
+
446
+ .filter-btn.has-active-filter[aria-expanded="true"] {
447
+ background: var(--accent);
448
+ color: #fff;
449
+ }
450
+
451
+ .filter-popup {
452
+ position: absolute;
453
+ top: calc(100% + 10px);
454
+ left: 0;
455
+ z-index: 200;
456
+ width: 260px;
457
+ padding: 16px;
458
+ background: var(--surface-2);
459
+ border: 1px solid var(--border);
460
+ border-radius: 16px;
461
+ backdrop-filter: blur(20px) saturate(1.2);
462
+ -webkit-backdrop-filter: blur(20px) saturate(1.2);
463
+ box-shadow: var(--shadow-hover);
464
+ }
465
+
466
+ .filter-popup-title {
467
+ font-size: 0.72rem;
468
+ text-transform: uppercase;
469
+ letter-spacing: 0.08em;
470
+ color: var(--muted);
471
+ font-weight: 600;
472
+ margin-bottom: 16px;
473
+ }
474
+
475
+ /* dual-thumb range slider */
476
+ .range-slider {
477
  position: relative;
478
+ height: 20px;
479
+ margin: 4px 2px 10px;
480
+ }
481
+
482
+ .range-slider-track {
483
+ position: absolute;
484
+ top: 50%;
485
+ left: 0;
486
+ right: 0;
487
+ height: 4px;
488
+ transform: translateY(-50%);
489
+ background: rgba(120, 160, 200, 0.22);
490
+ border-radius: 999px;
491
+ }
492
+
493
+ .range-slider-fill {
494
+ position: absolute;
495
+ top: 50%;
496
+ height: 4px;
497
+ transform: translateY(-50%);
498
+ background: linear-gradient(90deg, var(--accent-soft), var(--accent));
499
+ border-radius: 999px;
500
+ left: 0%;
501
+ right: 0%;
502
+ }
503
+
504
+ .range-thumb {
505
+ position: absolute;
506
+ top: 50%;
507
+ left: 0;
508
+ width: 100%;
509
+ margin: 0;
510
+ transform: translateY(-50%);
511
+ -webkit-appearance: none;
512
+ appearance: none;
513
+ background: transparent;
514
+ pointer-events: none;
515
+ height: 20px;
516
+ }
517
+
518
+ .range-thumb::-webkit-slider-thumb {
519
+ -webkit-appearance: none;
520
+ appearance: none;
521
+ pointer-events: auto;
522
+ width: 18px;
523
+ height: 18px;
524
+ border-radius: 50%;
525
+ background: #fff;
526
+ border: 2px solid var(--accent);
527
+ box-shadow: 0 2px 6px rgba(74, 144, 194, 0.35);
528
+ cursor: pointer;
529
+ transition: transform 0.15s ease;
530
+ }
531
+
532
+ .range-thumb::-webkit-slider-thumb:hover { transform: scale(1.15); }
533
+ .range-thumb::-webkit-slider-thumb:active { transform: scale(1.05); }
534
+
535
+ .range-thumb::-moz-range-thumb {
536
+ pointer-events: auto;
537
+ width: 16px;
538
+ height: 16px;
539
+ border-radius: 50%;
540
+ background: #fff;
541
+ border: 2px solid var(--accent);
542
+ box-shadow: 0 2px 6px rgba(74, 144, 194, 0.35);
543
+ cursor: pointer;
544
+ }
545
+
546
+ .range-thumb::-webkit-slider-runnable-track { background: transparent; }
547
+ .range-thumb::-moz-range-track { background: transparent; }
548
+
549
+ .range-thumb:focus-visible::-webkit-slider-thumb { outline: 2px solid var(--accent); outline-offset: 2px; }
550
+
551
+ .range-slider-values {
552
+ display: flex;
553
+ justify-content: space-between;
554
+ font-size: 0.78rem;
555
+ color: var(--heading);
556
+ font-weight: 600;
557
+ font-variant-numeric: tabular-nums;
558
+ margin-bottom: 14px;
559
+ }
560
+
561
+ .filter-reset {
562
+ width: 100%;
563
+ padding: 8px 10px;
564
+ border-radius: 999px;
565
+ border: 1px solid var(--border-soft);
566
+ background: rgba(255,255,255,0.5);
567
+ color: var(--text);
568
+ font-size: 0.78rem;
569
+ font-family: inherit;
570
+ cursor: pointer;
571
+ transition: background var(--transition), border-color var(--transition), color var(--transition);
572
  }
573
 
574
+ .filter-reset:hover { background: rgba(255,255,255,0.85); border-color: rgba(74,144,194,0.4); }
575
+ .filter-reset:disabled { opacity: 0.5; cursor: default; }
576
+ .filter-reset:disabled:hover { background: rgba(255,255,255,0.5); border-color: var(--border-soft); }
577
+
578
  .pill-select {
579
  appearance: none;
580
  -webkit-appearance: none;
 
715
  white-space: nowrap;
716
  }
717
 
718
+ .metric-cell {
719
+ font-variant-numeric: tabular-nums;
720
+ white-space: nowrap;
721
+ font-size: 0.88rem;
722
+ color: var(--text);
723
+ }
724
+
725
+ .metric-cell.is-primary { font-weight: 700; font-size: 0.95rem; }
726
+
727
+ .metric-cell .metric-na {
728
+ color: var(--muted);
729
+ font-style: italic;
730
+ font-weight: 400;
731
+ font-size: 0.82rem;
732
+ }
733
+
734
+ .n-cell {
735
+ font-variant-numeric: tabular-nums;
736
+ color: var(--muted);
737
+ font-size: 0.8rem;
738
+ white-space: nowrap;
739
+ }
740
+
741
  .score-pill {
742
  display: inline-flex;
743
  align-items: center;
 
789
 
790
  .empty-state svg { margin-bottom: 14px; color: var(--accent-soft); }
791
 
792
+ .unavailable-state {
793
+ padding: 60px 24px;
794
+ text-align: center;
795
+ color: var(--muted);
796
+ }
797
+
798
+ .unavailable-state svg { margin-bottom: 14px; color: #d99a3e; }
799
+ .unavailable-state p { max-width: 420px; margin: 0 auto; line-height: 1.6; }
800
+
801
+ .tier-card.is-unavailable {
802
+ opacity: 0.72;
803
+ cursor: pointer;
804
+ }
805
+
806
+ .tier-card.is-unavailable:hover { opacity: 0.9; }
807
+
808
+ .tier-badge-unavailable {
809
+ display: inline-flex;
810
+ align-items: center;
811
+ gap: 4px;
812
+ margin-left: 8px;
813
+ padding: 2px 8px;
814
+ border-radius: 999px;
815
+ background: rgba(217, 154, 62, 0.16);
816
+ color: #b5791f;
817
+ font-size: 0.62rem;
818
+ font-weight: 700;
819
+ text-transform: uppercase;
820
+ letter-spacing: 0.06em;
821
+ vertical-align: middle;
822
+ }
823
+
824
  /* -------------------- DETAIL PANEL -------------------- */
825
 
826
  .detail-panel {
 
937
  @media (max-width: 640px) {
938
  .container { padding: 40px 16px 32px; }
939
  .controls { flex-direction: column; align-items: stretch; }
940
+ .filter-wrap { align-self: flex-start; }
941
+ .filter-popup { left: 0; right: auto; width: min(260px, calc(100vw - 64px)); }
942
  table.leaderboard { min-width: 560px; }
943
  }
944