GitHub Actions commited on
Commit
df975ba
·
1 Parent(s): ba6f9e5

sync from abhijitramesh/webgpu-bench@aac06bd55b

Browse files
Files changed (8) hide show
  1. css/style.css +558 -139
  2. index.html +40 -10
  3. js/app.js +88 -25
  4. js/charts.js +17 -7
  5. js/run/controller.js +95 -16
  6. js/tables.js +7 -3
  7. methodology.html +2 -2
  8. run.html +27 -9
css/style.css CHANGED
@@ -28,28 +28,40 @@ html { scroll-padding-top: 80px; }
28
  --foreground-muted: #71717a;
29
  --foreground-subtle: #a1a1aa;
30
 
31
- --success: #16a34a;
32
- --success-bg: rgba(22, 163, 74, 0.1);
33
  --error: #dc2626;
34
- --error-bg: rgba(220, 38, 38, 0.1);
35
- --warning: #d97706;
36
- --info: #2563eb;
37
-
38
- /* Accent colors (theme-aware) */
39
- --accent-blue: #2563eb;
40
- --accent-blue-bg: rgba(37, 99, 235, 0.1);
41
- --accent-purple: #7c3aed;
42
- --accent-purple-bg: rgba(124, 58, 237, 0.1);
43
- --accent-amber: #d97706;
44
- --accent-amber-bg: rgba(217, 119, 6, 0.1);
45
- --accent-violet: #6d28d9;
46
- --accent-violet-bg: rgba(109, 40, 217, 0.1);
47
-
48
- --shadow-dropdown: 0 8px 24px rgba(0, 0, 0, 0.12);
 
 
 
 
 
 
 
 
 
 
 
49
 
50
  /* Non-color tokens (same in both themes) */
51
- --font-sans: 'Manrope', system-ui, -apple-system, sans-serif;
52
- --font-mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
 
53
  --radius-sm: 6px;
54
  --radius-md: 8px;
55
  --radius-lg: 12px;
@@ -60,7 +72,7 @@ html { scroll-padding-top: 80px; }
60
  [data-theme="dark"] {
61
  color-scheme: dark;
62
 
63
- --background: #09090b;
64
  --surface-0: #0f0f12;
65
  --surface-1: #18181b;
66
  --surface-2: #1c1c20;
@@ -75,22 +87,29 @@ html { scroll-padding-top: 80px; }
75
  --foreground-muted: #a1a1aa;
76
  --foreground-subtle: #71717a;
77
 
78
- --success: #22c55e;
79
- --success-bg: rgba(34, 197, 94, 0.12);
80
- --error: #ef4444;
81
- --error-bg: rgba(239, 68, 68, 0.12);
82
- --warning: #eab308;
83
- --info: #3b82f6;
 
 
 
 
 
84
 
85
  --accent-blue: #60a5fa;
86
- --accent-blue-bg: rgba(59, 130, 246, 0.12);
87
  --accent-purple: #a78bfa;
88
- --accent-purple-bg: rgba(139, 92, 246, 0.12);
89
  --accent-amber: #fbbf24;
90
- --accent-amber-bg: rgba(245, 158, 11, 0.12);
91
  --accent-violet: #c084fc;
92
- --accent-violet-bg: rgba(168, 85, 247, 0.12);
93
 
 
 
94
  --shadow-dropdown: 0 8px 24px rgba(0, 0, 0, 0.4);
95
  }
96
 
@@ -103,16 +122,30 @@ body {
103
  font-family: var(--font-sans);
104
  font-size: 14px;
105
  line-height: 1.6;
 
 
106
  -webkit-font-smoothing: antialiased;
107
  -moz-osx-font-smoothing: grayscale;
108
  }
109
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  a {
111
  color: var(--info);
112
  text-decoration: none;
113
  transition: color var(--transition-fast);
114
  }
115
- a:hover { color: #60a5fa; }
116
 
117
  /* === SCROLLBAR === */
118
  ::-webkit-scrollbar { width: 8px; height: 8px; }
@@ -244,52 +277,164 @@ a:hover { color: #60a5fa; }
244
  HERO
245
  ============================================================= */
246
  .hero {
247
- border-bottom: 1px solid var(--border);
248
  background: var(--background);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  }
250
  .hero-inner {
251
- display: flex;
 
 
252
  align-items: center;
253
- justify-content: space-between;
254
- gap: 24px;
255
- padding-top: 32px;
256
- padding-bottom: 32px;
257
- flex-wrap: wrap;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  }
259
- .hero-copy { flex: 1 1 420px; min-width: 0; }
260
  .hero-title {
261
- margin: 0 0 8px;
262
- font-size: 28px;
263
- font-weight: 800;
264
- letter-spacing: -0.02em;
 
265
  color: var(--foreground);
266
  }
267
  .hero-lede {
268
  margin: 0;
269
- font-size: 15px;
270
  line-height: 1.55;
271
  color: var(--foreground-secondary);
272
- max-width: 62ch;
273
  }
274
  .hero-meta {
275
- margin: 10px 0 0;
276
  font-size: 12px;
277
  color: var(--foreground-muted);
278
  font-family: var(--font-mono);
279
  }
280
  .hero-lede code {
281
  font-family: var(--font-mono);
282
- font-size: 13px;
283
  background: var(--surface-2);
284
  padding: 1px 6px;
285
  border-radius: var(--radius-sm);
286
  color: var(--foreground);
287
  }
288
- .hero-actions { display: flex; gap: 10px; flex-shrink: 0; }
289
  .hero-actions .btn { text-decoration: none; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  @media (max-width: 640px) {
291
- .hero-inner { padding-top: 20px; padding-bottom: 20px; }
292
- .hero-title { font-size: 22px; }
293
  .hero-lede { font-size: 14px; }
294
  }
295
 
@@ -532,6 +677,13 @@ a:hover { color: #60a5fa; }
532
  .section-nav {
533
  background: var(--background);
534
  border-bottom: 1px solid var(--border);
 
 
 
 
 
 
 
535
  }
536
  .section-nav-track {
537
  display: flex;
@@ -539,27 +691,40 @@ a:hover { color: #60a5fa; }
539
  overflow-x: auto;
540
  scrollbar-width: none;
541
  -webkit-overflow-scrolling: touch;
 
542
  }
543
  .section-nav-track::-webkit-scrollbar { display: none; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
544
 
545
  .section-nav-item {
546
  padding: 10px 16px;
547
  background: none;
548
  border: none;
549
- border-bottom: 2px solid transparent;
550
  color: var(--foreground-muted);
551
  font-family: var(--font-sans);
552
  font-size: 13px;
553
  font-weight: 500;
554
  cursor: pointer;
555
  white-space: nowrap;
556
- transition: color var(--transition-fast), border-color var(--transition-fast);
557
  }
558
  .section-nav-item:hover { color: var(--foreground); }
559
- .section-nav-item.active {
560
- color: var(--foreground);
561
- border-bottom-color: var(--foreground);
562
- }
563
 
564
  /* =============================================================
565
  SECTIONS & LAYOUT
@@ -619,49 +784,79 @@ a:hover { color: #60a5fa; }
619
  ============================================================= */
620
  .summary-grid {
621
  display: grid;
622
- grid-template-columns: repeat(3, 1fr);
623
- gap: 16px;
624
  }
625
  .stat-card {
626
  display: flex;
627
  align-items: flex-start;
628
- gap: 16px;
629
  background: var(--surface-1);
630
  border: 1px solid var(--border);
631
  border-radius: var(--radius-lg);
632
- padding: 20px 24px;
633
- transition: border-color var(--transition-base);
 
 
 
 
 
 
634
  }
635
- .stat-card:hover { border-color: var(--border-hover); }
636
 
637
  .stat-card-icon {
638
  display: flex;
639
  align-items: center;
640
  justify-content: center;
641
- width: 40px;
642
- height: 40px;
643
  border-radius: var(--radius-md);
644
  flex-shrink: 0;
645
  }
646
  .stat-card-icon--machines { background: var(--accent-blue-bg); color: var(--accent-blue); }
647
  .stat-card-icon--benchmarks { background: var(--accent-violet-bg); color: var(--accent-violet); }
648
  .stat-card-icon--pass { background: var(--success-bg); color: var(--success); }
 
 
 
649
 
 
650
  .stat-card-label {
651
  display: block;
652
- font-size: 12px;
 
653
  font-weight: 500;
 
 
654
  color: var(--foreground-muted);
655
- margin-bottom: 2px;
656
  }
657
  .stat-card-value {
658
- display: block;
659
- font-size: 28px;
660
- font-weight: 800;
 
 
661
  font-family: var(--font-mono);
662
  color: var(--foreground);
663
- letter-spacing: -0.02em;
664
- line-height: 1.1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
665
  }
666
 
667
  /* =============================================================
@@ -763,7 +958,12 @@ a:hover { color: #60a5fa; }
763
  .results-table thead th[aria-sort="ascending"] .th-sort-indicator,
764
  .results-table thead th[aria-sort="descending"] .th-sort-indicator {
765
  opacity: 1;
 
 
 
 
766
  color: var(--foreground);
 
767
  }
768
  .results-table thead th:hover .th-sort-indicator { opacity: 0.85; }
769
 
@@ -787,8 +987,11 @@ a:hover { color: #60a5fa; }
787
  border-bottom: none;
788
  }
789
 
790
- /* Row status indicator */
791
- .row-pass td:first-child { box-shadow: inset 3px 0 0 var(--success); }
 
 
 
792
  .row-fail td:first-child { box-shadow: inset 3px 0 0 var(--error); }
793
 
794
  /* =============================================================
@@ -804,19 +1007,19 @@ a:hover { color: #60a5fa; }
804
  text-transform: uppercase;
805
  letter-spacing: 0.02em;
806
  }
807
- .badge--pass { background: var(--success-bg); color: #166534; }
808
- .badge--fail { background: var(--error-bg); color: #991b1b; }
809
- .badge--yes { background: var(--accent-blue-bg); color: #1d4ed8; }
810
  .badge--no { background: var(--surface-3); color: var(--foreground-secondary); }
811
- .badge--webgpu { background: var(--accent-purple-bg); color: #5b21b6; }
812
- .badge--cpu { background: var(--accent-amber-bg); color: #92400e; }
813
 
814
- [data-theme="dark"] .badge--pass { color: #4ade80; }
815
- [data-theme="dark"] .badge--fail { color: #f87171; }
816
- [data-theme="dark"] .badge--yes { color: #60a5fa; }
817
  [data-theme="dark"] .badge--no { color: var(--foreground-muted); }
818
- [data-theme="dark"] .badge--webgpu { color: #c4b5fd; }
819
- [data-theme="dark"] .badge--cpu { color: #fbbf24; }
820
 
821
  /* =============================================================
822
  ERROR CELLS & CATEGORIES
@@ -854,10 +1057,19 @@ a:hover { color: #60a5fa; }
854
  position: relative;
855
  height: 340px;
856
  min-width: 0;
857
- transition: border-color var(--transition-base);
 
 
 
 
 
 
 
 
 
858
  }
859
- .chart-box canvas { max-width: 100%; }
860
- .chart-box:hover { border-color: var(--border-hover); }
861
 
862
  .subsection-title { font-size: 16px; font-weight: 600; color: var(--foreground); margin: 0; }
863
  .metric-selector { display: flex; align-items: center; gap: 8px; }
@@ -870,11 +1082,22 @@ a:hover { color: #60a5fa; }
870
 
871
  .chart-empty {
872
  position: absolute;
873
- top: 50%;
874
- left: 50%;
875
- transform: translate(-50%, -50%);
876
- color: var(--foreground-secondary);
877
- font-size: 13px;
 
 
 
 
 
 
 
 
 
 
 
878
  }
879
 
880
  /* =============================================================
@@ -894,41 +1117,87 @@ a:hover { color: #60a5fa; }
894
  }
895
  .machine-card:hover { border-color: var(--border-hover); }
896
 
897
- /* "Add your machine" call-to-action card — same shape as machine cards
898
- but dashed/muted, and it's a link so the whole card is clickable. */
 
899
  .machine-card-add {
 
900
  display: flex;
901
  flex-direction: column;
902
- border-style: dashed;
903
- background: transparent;
 
 
 
904
  color: var(--foreground-secondary);
905
  text-decoration: none;
906
- gap: 12px;
 
 
 
 
 
 
 
 
 
 
907
  }
908
  .machine-card-add:hover {
909
- border-color: var(--foreground-muted);
910
- background: var(--surface-1);
911
  color: var(--foreground);
 
 
 
912
  }
 
 
913
  .machine-card-add .machine-card-header {
914
  margin-bottom: 0;
915
  padding-bottom: 0;
916
  border-bottom: none;
917
  }
918
- .machine-card-add .machine-card-icon { color: var(--foreground-muted); }
919
- .machine-card-add:hover .machine-card-icon { color: var(--foreground); }
920
  .machine-card-add-blurb {
921
  font-size: 13px;
922
  line-height: 1.5;
923
  color: var(--foreground-secondary);
924
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
925
  .machine-card-add-cta {
926
  display: inline-flex;
 
 
927
  font-size: 13px;
928
  font-weight: 600;
929
  color: var(--foreground);
930
  margin-top: auto;
931
  }
 
 
 
 
 
 
932
 
933
  .machine-card-header {
934
  display: flex;
@@ -1012,7 +1281,6 @@ a:hover { color: #60a5fa; }
1012
  ============================================================= */
1013
  @media (max-width: 1024px) {
1014
  .charts-grid { grid-template-columns: 1fr; }
1015
- .summary-grid { grid-template-columns: 1fr 1fr; }
1016
  }
1017
 
1018
  @media (max-width: 768px) {
@@ -1041,9 +1309,8 @@ a:hover { color: #60a5fa; }
1041
  .table-card { border-radius: var(--radius-md); }
1042
  .results-wrapper { max-height: 450px; }
1043
 
1044
- .summary-grid { grid-template-columns: 1fr; gap: 12px; }
1045
- .stat-card { padding: 16px 20px; }
1046
- .stat-card-value { font-size: 24px; }
1047
 
1048
  .charts-grid { gap: 12px; }
1049
  .chart-box { height: 280px; padding: 16px; }
@@ -1144,33 +1411,47 @@ a:hover { color: #60a5fa; }
1144
 
1145
  .methodology-content h1 {
1146
  margin: 0 0 32px;
1147
- font-size: 28px;
1148
- font-weight: 800;
1149
  color: var(--foreground);
1150
- letter-spacing: -0.02em;
 
1151
  }
1152
 
1153
  .methodology-content h2 {
1154
- margin-top: 48px;
1155
  margin-bottom: 16px;
1156
- font-size: 20px;
1157
  font-weight: 700;
1158
  color: var(--foreground);
1159
- letter-spacing: -0.01em;
1160
  }
1161
  .methodology-content h1 + h2 { margin-top: 0; }
1162
 
1163
  .methodology-content h3 {
1164
  margin-top: 28px;
1165
  margin-bottom: 8px;
1166
- font-size: 15px;
 
1167
  font-weight: 600;
 
 
1168
  color: var(--foreground-muted);
1169
  }
1170
- .methodology-content p,
 
 
 
 
 
 
 
 
 
 
1171
  .methodology-content li {
1172
- line-height: 1.8;
1173
- margin-bottom: 8px;
1174
  color: var(--foreground-secondary);
1175
  font-size: 14px;
1176
  }
@@ -1255,8 +1536,12 @@ a:hover { color: #60a5fa; }
1255
  .btn-primary {
1256
  background: var(--foreground);
1257
  color: var(--background);
 
 
 
 
 
1258
  }
1259
- .btn-primary:hover:not(:disabled) { background: var(--foreground-secondary); }
1260
 
1261
  .btn-secondary {
1262
  background: transparent;
@@ -1281,15 +1566,15 @@ a:hover { color: #60a5fa; }
1281
  letter-spacing: 0.02em;
1282
  font-weight: 600;
1283
  }
1284
- .run-mode-localhost { background: var(--accent-blue-bg); color: #1d4ed8; }
1285
- .run-mode-space { background: var(--accent-purple-bg); color: #5b21b6; }
1286
  .run-mode-pages { background: var(--surface-3); color: var(--foreground-secondary); }
1287
- .run-mode-file { background: var(--accent-amber-bg); color: #92400e; }
1288
 
1289
- [data-theme="dark"] .run-mode-localhost { color: #60a5fa; }
1290
- [data-theme="dark"] .run-mode-space { color: #c4b5fd; }
1291
  [data-theme="dark"] .run-mode-pages { color: var(--foreground-muted); }
1292
- [data-theme="dark"] .run-mode-file { color: #fbbf24; }
1293
 
1294
  /* Pages-mode read-only banner. */
1295
  .run-pages-banner {
@@ -1304,8 +1589,7 @@ a:hover { color: #60a5fa; }
1304
  border-radius: var(--radius-md);
1305
  font-size: 13px;
1306
  }
1307
- .run-pages-banner a { font-weight: 600; color: #1d4ed8; text-decoration: underline; }
1308
- [data-theme="dark"] .run-pages-banner a { color: #93c5fd; }
1309
 
1310
  /* HF hub sign-in/submit row (space surface only). */
1311
  .hub-row { margin-bottom: 20px; }
@@ -1352,6 +1636,44 @@ a:hover { color: #60a5fa; }
1352
  gap: 12px;
1353
  flex-wrap: wrap;
1354
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1355
  .run-hide-label {
1356
  display: inline-flex;
1357
  align-items: center;
@@ -1395,17 +1717,17 @@ a:hover { color: #60a5fa; }
1395
  font-size: 12px;
1396
  }
1397
 
1398
- /* Sticky action bar with Download / Run always reachable inside the
1399
- long model list. */
 
 
1400
  .run-action-bar {
1401
- position: sticky;
1402
- top: 0;
1403
- z-index: 40;
1404
- margin: 0 0 20px;
1405
- padding: 12px 32px;
1406
- background: var(--background);
1407
- border-bottom: 1px solid var(--border);
1408
- box-shadow: 0 1px 0 var(--border);
1409
  }
1410
  .run-action-bar-inner {
1411
  display: flex;
@@ -1414,10 +1736,72 @@ a:hover { color: #60a5fa; }
1414
  gap: 16px;
1415
  flex-wrap: wrap;
1416
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1417
  @media (max-width: 640px) {
1418
- .run-action-bar { padding: 12px 16px; }
1419
  .run-action-bar-inner { gap: 10px; }
1420
- .run-actions { margin-left: 0; }
 
1421
  }
1422
 
1423
  /* Family cards + variant rows. */
@@ -1482,6 +1866,20 @@ a:hover { color: #60a5fa; }
1482
  cursor: pointer;
1483
  user-select: none;
1484
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1485
  .run-family-stats {
1486
  color: var(--foreground-muted);
1487
  font-size: 12px;
@@ -1489,11 +1887,10 @@ a:hover { color: #60a5fa; }
1489
  }
1490
  .run-family-warning {
1491
  margin-left: auto;
1492
- color: #92400e;
1493
  font-size: 11px;
1494
  font-weight: 600;
1495
  }
1496
- [data-theme="dark"] .run-family-warning { color: #fbbf24; }
1497
 
1498
  .run-variant-list {
1499
  display: flex;
@@ -1547,11 +1944,33 @@ a:hover { color: #60a5fa; }
1547
  .badge--warn { background: var(--accent-amber-bg); color: var(--accent-amber); }
1548
 
1549
  /* Progress table — piggybacks on .results-table styling. */
1550
- .run-progress-table { font-variant-numeric: tabular-nums; }
1551
  .run-progress-table th.num,
1552
  .run-progress-table td.num { text-align: right; }
1553
  .run-row-queued { color: var(--foreground-subtle); }
1554
- .run-row-running td { background: var(--accent-blue-bg); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1555
  .run-row-ok { color: var(--foreground-secondary); }
1556
  .run-row-error td { color: var(--error); }
1557
  .run-row-error td.err { font-family: var(--font-mono); font-size: 12px; }
 
28
  --foreground-muted: #71717a;
29
  --foreground-subtle: #a1a1aa;
30
 
31
+ --success: #0fa968;
32
+ --success-bg: rgba(15, 169, 104, 0.16);
33
  --error: #dc2626;
34
+ --error-bg: rgba(220, 38, 38, 0.14);
35
+ --warning: #b45309;
36
+ --info: #1d4ed8;
37
+
38
+ /* Signature plasma green — used for brand accent, live state, running row,
39
+ sortable-active marker, and primary CTA glow. The single load-bearing
40
+ color in the palette; everything else stays neutral. */
41
+ --signal: #0fa968;
42
+ --signal-strong: #0a8a55;
43
+ --signal-bg: rgba(15, 169, 104, 0.14);
44
+ --signal-glow: 0 0 0 4px rgba(15, 169, 104, 0.18);
45
+
46
+ /* Accent colors (theme-aware) kept for badge variation only. */
47
+ --accent-blue: #1d4ed8;
48
+ --accent-blue-bg: rgba(29, 78, 216, 0.12);
49
+ --accent-purple: #6d28d9;
50
+ --accent-purple-bg: rgba(109, 40, 217, 0.12);
51
+ --accent-amber: #b45309;
52
+ --accent-amber-bg: rgba(180, 83, 9, 0.14);
53
+ --accent-violet: #5b21b6;
54
+ --accent-violet-bg: rgba(91, 33, 182, 0.12);
55
+
56
+ /* Layered shadow vocabulary — light mode loses depth without these. */
57
+ --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.04), 0 1px 1px rgba(15, 23, 42, 0.03);
58
+ --shadow-md: 0 4px 12px rgba(15, 23, 42, 0.06), 0 2px 4px rgba(15, 23, 42, 0.04);
59
+ --shadow-dropdown: 0 8px 24px rgba(15, 23, 42, 0.10);
60
 
61
  /* Non-color tokens (same in both themes) */
62
+ --font-sans: 'Bricolage Grotesque', system-ui, -apple-system, sans-serif;
63
+ --font-mono: 'Geist Mono', 'SF Mono', 'JetBrains Mono', monospace;
64
+ --font-serif: 'Instrument Serif', 'Source Serif 4', Georgia, serif;
65
  --radius-sm: 6px;
66
  --radius-md: 8px;
67
  --radius-lg: 12px;
 
72
  [data-theme="dark"] {
73
  color-scheme: dark;
74
 
75
+ --background: #08080a;
76
  --surface-0: #0f0f12;
77
  --surface-1: #18181b;
78
  --surface-2: #1c1c20;
 
87
  --foreground-muted: #a1a1aa;
88
  --foreground-subtle: #71717a;
89
 
90
+ --success: #22e09a;
91
+ --success-bg: rgba(34, 224, 154, 0.14);
92
+ --error: #f87171;
93
+ --error-bg: rgba(239, 68, 68, 0.14);
94
+ --warning: #fbbf24;
95
+ --info: #60a5fa;
96
+
97
+ --signal: #22e09a;
98
+ --signal-strong: #5cffaa;
99
+ --signal-bg: rgba(34, 224, 154, 0.12);
100
+ --signal-glow: 0 0 0 4px rgba(34, 224, 154, 0.20);
101
 
102
  --accent-blue: #60a5fa;
103
+ --accent-blue-bg: rgba(96, 165, 250, 0.14);
104
  --accent-purple: #a78bfa;
105
+ --accent-purple-bg: rgba(167, 139, 250, 0.14);
106
  --accent-amber: #fbbf24;
107
+ --accent-amber-bg: rgba(251, 191, 36, 0.14);
108
  --accent-violet: #c084fc;
109
+ --accent-violet-bg: rgba(192, 132, 252, 0.14);
110
 
111
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.02) inset;
112
+ --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(255, 255, 255, 0.02) inset;
113
  --shadow-dropdown: 0 8px 24px rgba(0, 0, 0, 0.4);
114
  }
115
 
 
122
  font-family: var(--font-sans);
123
  font-size: 14px;
124
  line-height: 1.6;
125
+ font-feature-settings: 'ss01', 'cv02', 'cv11';
126
+ font-variant-numeric: tabular-nums slashed-zero;
127
  -webkit-font-smoothing: antialiased;
128
  -moz-osx-font-smoothing: grayscale;
129
  }
130
 
131
+ /* Bricolage Grotesque optical sizing — tighten for headings, relax for body. */
132
+ .hero-title,
133
+ .run-hero-title,
134
+ .methodology-content h1,
135
+ .methodology-content h2,
136
+ .section-header h1,
137
+ .section-header h2 {
138
+ font-family: var(--font-sans);
139
+ font-optical-sizing: auto;
140
+ font-variation-settings: 'opsz' 96;
141
+ }
142
+
143
  a {
144
  color: var(--info);
145
  text-decoration: none;
146
  transition: color var(--transition-fast);
147
  }
148
+ a:hover { color: var(--info); }
149
 
150
  /* === SCROLLBAR === */
151
  ::-webkit-scrollbar { width: 8px; height: 8px; }
 
277
  HERO
278
  ============================================================= */
279
  .hero {
280
+ position: relative;
281
  background: var(--background);
282
+ overflow: hidden;
283
+ /* No bottom border — filter-bar's top border is the visual divider. */
284
+ }
285
+ /* Subtle radial mesh — only present on the hero. Hints at the GPU subject
286
+ matter without committing to a heavy gradient aesthetic elsewhere. */
287
+ .hero::before {
288
+ content: '';
289
+ position: absolute;
290
+ inset: -10% -10% auto auto;
291
+ width: 520px;
292
+ height: 520px;
293
+ background:
294
+ radial-gradient(circle at 70% 30%, var(--signal-bg), transparent 60%),
295
+ radial-gradient(circle at 30% 80%, var(--accent-violet-bg), transparent 60%);
296
+ filter: blur(40px);
297
+ opacity: 0.7;
298
+ pointer-events: none;
299
  }
300
  .hero-inner {
301
+ position: relative;
302
+ display: grid;
303
+ grid-template-columns: minmax(0, 1fr) auto;
304
  align-items: center;
305
+ gap: 40px;
306
+ padding-top: 56px;
307
+ padding-bottom: 48px;
308
+ }
309
+ .hero-copy { min-width: 0; }
310
+ .hero-eyebrow {
311
+ display: inline-flex;
312
+ align-items: center;
313
+ gap: 8px;
314
+ margin: 0 0 16px;
315
+ padding: 4px 10px 4px 8px;
316
+ background: var(--signal-bg);
317
+ border-radius: 9999px;
318
+ font-family: var(--font-mono);
319
+ font-size: 11px;
320
+ font-weight: 600;
321
+ letter-spacing: 0.08em;
322
+ text-transform: uppercase;
323
+ color: var(--signal-strong);
324
+ }
325
+ [data-theme="dark"] .hero-eyebrow { color: var(--signal); }
326
+ .hero-eyebrow-dot {
327
+ width: 6px;
328
+ height: 6px;
329
+ border-radius: 50%;
330
+ background: var(--signal);
331
+ box-shadow: 0 0 0 0 var(--signal);
332
+ animation: live-pulse 2s ease-out infinite;
333
+ }
334
+ @keyframes live-pulse {
335
+ 0% { box-shadow: 0 0 0 0 rgba(15, 169, 104, 0.5); }
336
+ 70% { box-shadow: 0 0 0 8px rgba(15, 169, 104, 0); }
337
+ 100% { box-shadow: 0 0 0 0 rgba(15, 169, 104, 0); }
338
+ }
339
+ [data-theme="dark"] .hero-eyebrow-dot {
340
+ animation-name: live-pulse-dark;
341
+ }
342
+ @keyframes live-pulse-dark {
343
+ 0% { box-shadow: 0 0 0 0 rgba(34, 224, 154, 0.6); }
344
+ 70% { box-shadow: 0 0 0 8px rgba(34, 224, 154, 0); }
345
+ 100% { box-shadow: 0 0 0 0 rgba(34, 224, 154, 0); }
346
  }
 
347
  .hero-title {
348
+ margin: 0 0 14px;
349
+ font-size: clamp(36px, 5vw, 56px);
350
+ font-weight: 700;
351
+ letter-spacing: -0.04em;
352
+ line-height: 0.98;
353
  color: var(--foreground);
354
  }
355
  .hero-lede {
356
  margin: 0;
357
+ font-size: 16px;
358
  line-height: 1.55;
359
  color: var(--foreground-secondary);
360
+ max-width: 56ch;
361
  }
362
  .hero-meta {
363
+ margin: 14px 0 0;
364
  font-size: 12px;
365
  color: var(--foreground-muted);
366
  font-family: var(--font-mono);
367
  }
368
  .hero-lede code {
369
  font-family: var(--font-mono);
370
+ font-size: 14px;
371
  background: var(--surface-2);
372
  padding: 1px 6px;
373
  border-radius: var(--radius-sm);
374
  color: var(--foreground);
375
  }
376
+ .hero-actions { display: flex; gap: 10px; flex-shrink: 0; margin-top: 24px; }
377
  .hero-actions .btn { text-decoration: none; }
378
+
379
+ /* Right-side stat block — surfaces the headline metric. Count-up tween is
380
+ driven by JS; the markup ships with `data-target` and a starting "0.0". */
381
+ .hero-stat {
382
+ display: flex;
383
+ flex-direction: column;
384
+ gap: 4px;
385
+ padding: 20px 28px;
386
+ border: 1px solid var(--border);
387
+ border-radius: var(--radius-lg);
388
+ background: var(--surface-1);
389
+ box-shadow: var(--shadow-md);
390
+ min-width: 240px;
391
+ }
392
+ .hero-stat-label {
393
+ font-family: var(--font-mono);
394
+ font-size: 11px;
395
+ font-weight: 600;
396
+ letter-spacing: 0.08em;
397
+ text-transform: uppercase;
398
+ color: var(--foreground-muted);
399
+ }
400
+ .hero-stat-value {
401
+ display: inline-flex;
402
+ align-items: baseline;
403
+ gap: 8px;
404
+ color: var(--foreground);
405
+ margin: 4px 0;
406
+ }
407
+ .hero-stat-num {
408
+ font-family: var(--font-mono);
409
+ font-size: 44px;
410
+ font-weight: 600;
411
+ letter-spacing: -0.03em;
412
+ color: var(--signal-strong);
413
+ line-height: 1;
414
+ }
415
+ [data-theme="dark"] .hero-stat-num { color: var(--signal); }
416
+ .hero-stat-unit {
417
+ font-family: var(--font-mono);
418
+ font-size: 14px;
419
+ color: var(--foreground-muted);
420
+ font-weight: 500;
421
+ }
422
+ .hero-stat-meta {
423
+ font-family: var(--font-mono);
424
+ font-size: 11px;
425
+ color: var(--foreground-muted);
426
+ white-space: nowrap;
427
+ overflow: hidden;
428
+ text-overflow: ellipsis;
429
+ }
430
+
431
+ @media (max-width: 768px) {
432
+ .hero-inner { grid-template-columns: 1fr; gap: 24px; padding-top: 32px; padding-bottom: 28px; }
433
+ .hero-stat { min-width: 0; }
434
+ .hero-stat-num { font-size: 36px; }
435
+ }
436
  @media (max-width: 640px) {
437
+ .hero-title { font-size: 36px; }
 
438
  .hero-lede { font-size: 14px; }
439
  }
440
 
 
677
  .section-nav {
678
  background: var(--background);
679
  border-bottom: 1px solid var(--border);
680
+ position: relative;
681
+ /* Right-edge fade so the user knows there's more to scroll on mobile. */
682
+ -webkit-mask-image: linear-gradient(to right, black 90%, transparent 100%);
683
+ mask-image: linear-gradient(to right, black 90%, transparent 100%);
684
+ }
685
+ @media (min-width: 901px) {
686
+ .section-nav { -webkit-mask-image: none; mask-image: none; }
687
  }
688
  .section-nav-track {
689
  display: flex;
 
691
  overflow-x: auto;
692
  scrollbar-width: none;
693
  -webkit-overflow-scrolling: touch;
694
+ position: relative;
695
  }
696
  .section-nav-track::-webkit-scrollbar { display: none; }
697
+ /* Sliding indicator — JS sets `--indicator-x` and `--indicator-w` to the
698
+ active button's offsetLeft/offsetWidth. Falls back to 0 (hidden) before
699
+ first paint. */
700
+ .section-nav-track::after {
701
+ content: '';
702
+ position: absolute;
703
+ left: 0;
704
+ bottom: 0;
705
+ height: 2px;
706
+ width: var(--indicator-w, 0px);
707
+ transform: translateX(var(--indicator-x, 0));
708
+ background: var(--signal);
709
+ border-radius: 1px 1px 0 0;
710
+ transition: transform 280ms cubic-bezier(0.4, 0, 0.2, 1), width 280ms cubic-bezier(0.4, 0, 0.2, 1);
711
+ pointer-events: none;
712
+ }
713
 
714
  .section-nav-item {
715
  padding: 10px 16px;
716
  background: none;
717
  border: none;
 
718
  color: var(--foreground-muted);
719
  font-family: var(--font-sans);
720
  font-size: 13px;
721
  font-weight: 500;
722
  cursor: pointer;
723
  white-space: nowrap;
724
+ transition: color var(--transition-fast);
725
  }
726
  .section-nav-item:hover { color: var(--foreground); }
727
+ .section-nav-item.active { color: var(--foreground); }
 
 
 
728
 
729
  /* =============================================================
730
  SECTIONS & LAYOUT
 
784
  ============================================================= */
785
  .summary-grid {
786
  display: grid;
787
+ grid-template-columns: repeat(5, minmax(0, 1fr));
788
+ gap: 12px;
789
  }
790
  .stat-card {
791
  display: flex;
792
  align-items: flex-start;
793
+ gap: 14px;
794
  background: var(--surface-1);
795
  border: 1px solid var(--border);
796
  border-radius: var(--radius-lg);
797
+ padding: 18px 20px;
798
+ box-shadow: var(--shadow-sm);
799
+ transition: border-color var(--transition-base), box-shadow var(--transition-base), transform var(--transition-base);
800
+ }
801
+ .stat-card:hover {
802
+ border-color: var(--border-hover);
803
+ box-shadow: var(--shadow-md);
804
+ transform: translateY(-1px);
805
  }
 
806
 
807
  .stat-card-icon {
808
  display: flex;
809
  align-items: center;
810
  justify-content: center;
811
+ width: 36px;
812
+ height: 36px;
813
  border-radius: var(--radius-md);
814
  flex-shrink: 0;
815
  }
816
  .stat-card-icon--machines { background: var(--accent-blue-bg); color: var(--accent-blue); }
817
  .stat-card-icon--benchmarks { background: var(--accent-violet-bg); color: var(--accent-violet); }
818
  .stat-card-icon--pass { background: var(--success-bg); color: var(--success); }
819
+ .stat-card-icon--decode { background: var(--signal-bg); color: var(--signal-strong); }
820
+ [data-theme="dark"] .stat-card-icon--decode { color: var(--signal); }
821
+ .stat-card-icon--size { background: var(--accent-amber-bg); color: var(--accent-amber); }
822
 
823
+ .stat-card-content { min-width: 0; flex: 1; }
824
  .stat-card-label {
825
  display: block;
826
+ font-family: var(--font-mono);
827
+ font-size: 11px;
828
  font-weight: 500;
829
+ letter-spacing: 0.06em;
830
+ text-transform: uppercase;
831
  color: var(--foreground-muted);
832
+ margin-bottom: 4px;
833
  }
834
  .stat-card-value {
835
+ display: inline-flex;
836
+ align-items: baseline;
837
+ gap: 4px;
838
+ font-size: 26px;
839
+ font-weight: 600;
840
  font-family: var(--font-mono);
841
  color: var(--foreground);
842
+ letter-spacing: -0.03em;
843
+ line-height: 1.05;
844
+ }
845
+ .stat-card-unit {
846
+ font-size: 12px;
847
+ font-weight: 500;
848
+ color: var(--foreground-muted);
849
+ letter-spacing: 0;
850
+ }
851
+
852
+ @media (max-width: 1100px) {
853
+ .summary-grid { grid-template-columns: repeat(3, 1fr); }
854
+ }
855
+ @media (max-width: 720px) {
856
+ .summary-grid { grid-template-columns: repeat(2, 1fr); }
857
+ }
858
+ @media (max-width: 480px) {
859
+ .summary-grid { grid-template-columns: 1fr; }
860
  }
861
 
862
  /* =============================================================
 
958
  .results-table thead th[aria-sort="ascending"] .th-sort-indicator,
959
  .results-table thead th[aria-sort="descending"] .th-sort-indicator {
960
  opacity: 1;
961
+ color: var(--signal);
962
+ }
963
+ .results-table thead th[aria-sort="ascending"],
964
+ .results-table thead th[aria-sort="descending"] {
965
  color: var(--foreground);
966
+ box-shadow: inset 0 -2px 0 var(--signal);
967
  }
968
  .results-table thead th:hover .th-sort-indicator { opacity: 0.85; }
969
 
 
987
  border-bottom: none;
988
  }
989
 
990
+ /* Row status indicator — purely decorative; the badge in the Status column
991
+ carries the same info accessibly. The stripe is hidden from AT via
992
+ the parent row's `aria-label` (added in tables.js render) plus the
993
+ badge being the canonical source of truth. */
994
+ .row-pass td:first-child { box-shadow: inset 3px 0 0 var(--signal); }
995
  .row-fail td:first-child { box-shadow: inset 3px 0 0 var(--error); }
996
 
997
  /* =============================================================
 
1007
  text-transform: uppercase;
1008
  letter-spacing: 0.02em;
1009
  }
1010
+ .badge--pass { background: var(--signal-bg); color: var(--signal-strong); }
1011
+ .badge--fail { background: var(--error-bg); color: #b91c1c; }
1012
+ .badge--yes { background: var(--accent-blue-bg); color: var(--accent-blue); }
1013
  .badge--no { background: var(--surface-3); color: var(--foreground-secondary); }
1014
+ .badge--webgpu { background: var(--accent-purple-bg); color: var(--accent-purple); }
1015
+ .badge--cpu { background: var(--accent-amber-bg); color: var(--accent-amber); }
1016
 
1017
+ [data-theme="dark"] .badge--pass { color: var(--signal); }
1018
+ [data-theme="dark"] .badge--fail { color: var(--error); }
1019
+ [data-theme="dark"] .badge--yes { color: var(--accent-blue); }
1020
  [data-theme="dark"] .badge--no { color: var(--foreground-muted); }
1021
+ [data-theme="dark"] .badge--webgpu { color: var(--accent-purple); }
1022
+ [data-theme="dark"] .badge--cpu { color: var(--accent-amber); }
1023
 
1024
  /* =============================================================
1025
  ERROR CELLS & CATEGORIES
 
1057
  position: relative;
1058
  height: 340px;
1059
  min-width: 0;
1060
+ box-shadow: var(--shadow-sm);
1061
+ transition: border-color var(--transition-base), box-shadow var(--transition-base);
1062
+ /* Faint dot grid — visible mostly through empty-state placeholders, and
1063
+ adds texture to charts with sparse data points. */
1064
+ background-image: radial-gradient(circle, rgba(15, 23, 42, 0.06) 1px, transparent 1px);
1065
+ background-size: 18px 18px;
1066
+ background-position: 12px 12px;
1067
+ }
1068
+ [data-theme="dark"] .chart-box {
1069
+ background-image: radial-gradient(circle, rgba(255, 255, 255, 0.04) 1px, transparent 1px);
1070
  }
1071
+ .chart-box canvas { max-width: 100%; position: relative; z-index: 1; }
1072
+ .chart-box:hover { border-color: var(--border-hover); box-shadow: var(--shadow-md); }
1073
 
1074
  .subsection-title { font-size: 16px; font-weight: 600; color: var(--foreground); margin: 0; }
1075
  .metric-selector { display: flex; align-items: center; gap: 8px; }
 
1082
 
1083
  .chart-empty {
1084
  position: absolute;
1085
+ inset: 0;
1086
+ display: flex;
1087
+ align-items: center;
1088
+ justify-content: center;
1089
+ text-align: center;
1090
+ padding: 24px;
1091
+ color: var(--foreground-muted);
1092
+ font-family: var(--font-mono);
1093
+ font-size: 12px;
1094
+ letter-spacing: 0.02em;
1095
+ z-index: 2;
1096
+ }
1097
+ .chart-empty::before {
1098
+ content: '— ';
1099
+ color: var(--foreground-subtle);
1100
+ margin-right: 8px;
1101
  }
1102
 
1103
  /* =============================================================
 
1117
  }
1118
  .machine-card:hover { border-color: var(--border-hover); }
1119
 
1120
+ /* "Add your machine" call-to-action card — strengthened version: subtle
1121
+ signal-tinted gradient ring, copyable command line, real arrow that
1122
+ slides on hover. */
1123
  .machine-card-add {
1124
+ position: relative;
1125
  display: flex;
1126
  flex-direction: column;
1127
+ border-style: solid;
1128
+ border-color: var(--border);
1129
+ background:
1130
+ linear-gradient(var(--surface-1), var(--surface-1)) padding-box,
1131
+ linear-gradient(135deg, var(--signal-bg), transparent 60%, var(--accent-violet-bg)) border-box;
1132
  color: var(--foreground-secondary);
1133
  text-decoration: none;
1134
+ gap: 14px;
1135
+ overflow: hidden;
1136
+ }
1137
+ .machine-card-add::before {
1138
+ content: '';
1139
+ position: absolute;
1140
+ inset: 0;
1141
+ background: radial-gradient(circle at 0% 0%, var(--signal-bg), transparent 60%);
1142
+ opacity: 0;
1143
+ transition: opacity var(--transition-base);
1144
+ pointer-events: none;
1145
  }
1146
  .machine-card-add:hover {
1147
+ border-color: transparent;
 
1148
  color: var(--foreground);
1149
+ text-decoration: none;
1150
+ transform: translateY(-1px);
1151
+ box-shadow: var(--shadow-md);
1152
  }
1153
+ .machine-card-add:hover::before { opacity: 1; }
1154
+ .machine-card-add > * { position: relative; }
1155
  .machine-card-add .machine-card-header {
1156
  margin-bottom: 0;
1157
  padding-bottom: 0;
1158
  border-bottom: none;
1159
  }
1160
+ .machine-card-add .machine-card-icon { color: var(--signal-strong); }
1161
+ [data-theme="dark"] .machine-card-add .machine-card-icon { color: var(--signal); }
1162
  .machine-card-add-blurb {
1163
  font-size: 13px;
1164
  line-height: 1.5;
1165
  color: var(--foreground-secondary);
1166
  }
1167
+ .machine-card-add-cmd {
1168
+ display: block;
1169
+ margin-top: 4px;
1170
+ padding: 8px 10px;
1171
+ background: var(--surface-0);
1172
+ border: 1px solid var(--border);
1173
+ border-radius: var(--radius-sm);
1174
+ font-family: var(--font-mono);
1175
+ font-size: 11.5px;
1176
+ color: var(--foreground);
1177
+ white-space: nowrap;
1178
+ overflow: hidden;
1179
+ text-overflow: ellipsis;
1180
+ }
1181
+ .machine-card-add-cmd::before {
1182
+ content: '$ ';
1183
+ color: var(--foreground-subtle);
1184
+ user-select: none;
1185
+ }
1186
  .machine-card-add-cta {
1187
  display: inline-flex;
1188
+ align-items: center;
1189
+ gap: 6px;
1190
  font-size: 13px;
1191
  font-weight: 600;
1192
  color: var(--foreground);
1193
  margin-top: auto;
1194
  }
1195
+ .machine-card-add-cta svg {
1196
+ transition: transform var(--transition-base);
1197
+ }
1198
+ .machine-card-add:hover .machine-card-add-cta svg {
1199
+ transform: translateX(3px);
1200
+ }
1201
 
1202
  .machine-card-header {
1203
  display: flex;
 
1281
  ============================================================= */
1282
  @media (max-width: 1024px) {
1283
  .charts-grid { grid-template-columns: 1fr; }
 
1284
  }
1285
 
1286
  @media (max-width: 768px) {
 
1309
  .table-card { border-radius: var(--radius-md); }
1310
  .results-wrapper { max-height: 450px; }
1311
 
1312
+ .stat-card { padding: 16px 18px; }
1313
+ .stat-card-value { font-size: 22px; }
 
1314
 
1315
  .charts-grid { gap: 12px; }
1316
  .chart-box { height: 280px; padding: 16px; }
 
1411
 
1412
  .methodology-content h1 {
1413
  margin: 0 0 32px;
1414
+ font-size: clamp(36px, 5vw, 52px);
1415
+ font-weight: 700;
1416
  color: var(--foreground);
1417
+ letter-spacing: -0.04em;
1418
+ line-height: 1;
1419
  }
1420
 
1421
  .methodology-content h2 {
1422
+ margin-top: 56px;
1423
  margin-bottom: 16px;
1424
+ font-size: 22px;
1425
  font-weight: 700;
1426
  color: var(--foreground);
1427
+ letter-spacing: -0.02em;
1428
  }
1429
  .methodology-content h1 + h2 { margin-top: 0; }
1430
 
1431
  .methodology-content h3 {
1432
  margin-top: 28px;
1433
  margin-bottom: 8px;
1434
+ font-family: var(--font-mono);
1435
+ font-size: 12px;
1436
  font-weight: 600;
1437
+ text-transform: uppercase;
1438
+ letter-spacing: 0.06em;
1439
  color: var(--foreground-muted);
1440
  }
1441
+ /* Body paragraphs in Instrument Serif — gives Methodology editorial weight
1442
+ that separates it from the dashboard's UI typography. List items stay
1443
+ sans-serif for tighter scanning. */
1444
+ .methodology-content p {
1445
+ font-family: var(--font-serif);
1446
+ font-size: 19px;
1447
+ line-height: 1.55;
1448
+ margin-bottom: 12px;
1449
+ color: var(--foreground-secondary);
1450
+ letter-spacing: -0.005em;
1451
+ }
1452
  .methodology-content li {
1453
+ line-height: 1.7;
1454
+ margin-bottom: 6px;
1455
  color: var(--foreground-secondary);
1456
  font-size: 14px;
1457
  }
 
1536
  .btn-primary {
1537
  background: var(--foreground);
1538
  color: var(--background);
1539
+ position: relative;
1540
+ }
1541
+ .btn-primary:hover:not(:disabled) {
1542
+ background: var(--foreground-secondary);
1543
+ box-shadow: var(--signal-glow);
1544
  }
 
1545
 
1546
  .btn-secondary {
1547
  background: transparent;
 
1566
  letter-spacing: 0.02em;
1567
  font-weight: 600;
1568
  }
1569
+ .run-mode-localhost { background: var(--accent-blue-bg); color: var(--accent-blue); }
1570
+ .run-mode-space { background: var(--accent-purple-bg); color: var(--accent-purple); }
1571
  .run-mode-pages { background: var(--surface-3); color: var(--foreground-secondary); }
1572
+ .run-mode-file { background: var(--accent-amber-bg); color: var(--accent-amber); }
1573
 
1574
+ [data-theme="dark"] .run-mode-localhost { color: var(--accent-blue); }
1575
+ [data-theme="dark"] .run-mode-space { color: var(--accent-purple); }
1576
  [data-theme="dark"] .run-mode-pages { color: var(--foreground-muted); }
1577
+ [data-theme="dark"] .run-mode-file { color: var(--accent-amber); }
1578
 
1579
  /* Pages-mode read-only banner. */
1580
  .run-pages-banner {
 
1589
  border-radius: var(--radius-md);
1590
  font-size: 13px;
1591
  }
1592
+ .run-pages-banner a { font-weight: 600; color: var(--accent-blue); text-decoration: underline; }
 
1593
 
1594
  /* HF hub sign-in/submit row (space surface only). */
1595
  .hub-row { margin-bottom: 20px; }
 
1636
  gap: 12px;
1637
  flex-wrap: wrap;
1638
  }
1639
+
1640
+ /* Family search — sits at the start of the run-filters row, expands to
1641
+ fill available space so the user has a generous typing target. */
1642
+ .filter-group--search { flex: 1 1 220px; min-width: 200px; }
1643
+ .run-search-wrapper {
1644
+ position: relative;
1645
+ display: flex;
1646
+ align-items: center;
1647
+ }
1648
+ .run-search-icon {
1649
+ position: absolute;
1650
+ left: 10px;
1651
+ color: var(--foreground-muted);
1652
+ pointer-events: none;
1653
+ }
1654
+ .run-search-input {
1655
+ width: 100%;
1656
+ height: 36px;
1657
+ padding: 0 12px 0 32px;
1658
+ background: var(--surface-1);
1659
+ border: 1px solid var(--border);
1660
+ border-radius: var(--radius-md);
1661
+ color: var(--foreground);
1662
+ font-family: var(--font-sans);
1663
+ font-size: 13px;
1664
+ font-weight: 500;
1665
+ transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
1666
+ }
1667
+ .run-search-input::placeholder { color: var(--foreground-muted); }
1668
+ .run-search-input:hover { border-color: var(--border-hover); }
1669
+ .run-search-input:focus {
1670
+ outline: none;
1671
+ border-color: var(--ring);
1672
+ box-shadow: 0 0 0 2px rgba(82, 82, 91, 0.3);
1673
+ }
1674
+ /* Hide the native "x" clear button on type=search inputs — we don't want
1675
+ the rendering inconsistency between Webkit and Gecko. */
1676
+ .run-search-input::-webkit-search-cancel-button { -webkit-appearance: none; }
1677
  .run-hide-label {
1678
  display: inline-flex;
1679
  align-items: center;
 
1717
  font-size: 12px;
1718
  }
1719
 
1720
+ /* Action bar with Download / Run / Abort / Purge — sits between the model
1721
+ list and the Progress section so the controls are visually adjacent to
1722
+ the status they drive. Wrapped in a card so it reads as a deliberate
1723
+ pre-launch surface, not a leftover toolbar. */
1724
  .run-action-bar {
1725
+ margin: 24px 0 16px;
1726
+ padding: 14px 18px;
1727
+ background: var(--surface-1);
1728
+ border: 1px solid var(--border);
1729
+ border-radius: var(--radius-lg);
1730
+ box-shadow: var(--shadow-sm);
 
 
1731
  }
1732
  .run-action-bar-inner {
1733
  display: flex;
 
1736
  gap: 16px;
1737
  flex-wrap: wrap;
1738
  }
1739
+
1740
+ /* Budget meter — visualizes "largest selected model" against the device
1741
+ memory budget so users can tell at-a-glance if their queue will fit. */
1742
+ .run-budget {
1743
+ display: flex;
1744
+ flex-direction: column;
1745
+ gap: 6px;
1746
+ flex: 1 1 280px;
1747
+ min-width: 220px;
1748
+ max-width: 480px;
1749
+ }
1750
+ .run-budget-row {
1751
+ display: flex;
1752
+ align-items: baseline;
1753
+ justify-content: space-between;
1754
+ gap: 12px;
1755
+ }
1756
+ .run-budget-label {
1757
+ font-family: var(--font-mono);
1758
+ font-size: 11px;
1759
+ font-weight: 600;
1760
+ letter-spacing: 0.06em;
1761
+ text-transform: uppercase;
1762
+ color: var(--foreground-muted);
1763
+ }
1764
+ .run-budget-text {
1765
+ font-family: var(--font-mono);
1766
+ font-size: 13px;
1767
+ color: var(--foreground-secondary);
1768
+ }
1769
+ .run-budget-text strong {
1770
+ color: var(--foreground);
1771
+ font-weight: 600;
1772
+ }
1773
+ .run-budget-size {
1774
+ color: var(--foreground);
1775
+ font-weight: 500;
1776
+ }
1777
+ .run-budget-bar {
1778
+ height: 6px;
1779
+ background: var(--surface-3);
1780
+ border-radius: 3px;
1781
+ overflow: hidden;
1782
+ }
1783
+ .run-budget-bar-fill {
1784
+ height: 100%;
1785
+ width: 0;
1786
+ background: var(--signal);
1787
+ border-radius: 3px;
1788
+ transition: width 240ms cubic-bezier(0.4, 0, 0.2, 1), background 200ms ease;
1789
+ }
1790
+ .run-budget[data-tone="warn"] .run-budget-bar-fill { background: var(--accent-amber); }
1791
+ .run-budget[data-tone="over"] .run-budget-bar-fill { background: var(--error); }
1792
+ .run-budget-meta {
1793
+ font-family: var(--font-mono);
1794
+ font-size: 11px;
1795
+ color: var(--foreground-muted);
1796
+ letter-spacing: 0.01em;
1797
+ min-height: 14px;
1798
+ }
1799
+
1800
  @media (max-width: 640px) {
1801
+ .run-action-bar { padding: 12px 14px; }
1802
  .run-action-bar-inner { gap: 10px; }
1803
+ .run-actions { margin-left: 0; width: 100%; }
1804
+ .run-budget { max-width: none; }
1805
  }
1806
 
1807
  /* Family cards + variant rows. */
 
1866
  cursor: pointer;
1867
  user-select: none;
1868
  }
1869
+ .run-family-params {
1870
+ display: inline-flex;
1871
+ align-items: center;
1872
+ padding: 2px 8px;
1873
+ background: var(--signal-bg);
1874
+ color: var(--signal-strong);
1875
+ font-family: var(--font-mono);
1876
+ font-size: 11px;
1877
+ font-weight: 600;
1878
+ letter-spacing: 0.02em;
1879
+ border-radius: 9999px;
1880
+ flex-shrink: 0;
1881
+ }
1882
+ [data-theme="dark"] .run-family-params { color: var(--signal); }
1883
  .run-family-stats {
1884
  color: var(--foreground-muted);
1885
  font-size: 12px;
 
1887
  }
1888
  .run-family-warning {
1889
  margin-left: auto;
1890
+ color: var(--accent-amber);
1891
  font-size: 11px;
1892
  font-weight: 600;
1893
  }
 
1894
 
1895
  .run-variant-list {
1896
  display: flex;
 
1944
  .badge--warn { background: var(--accent-amber-bg); color: var(--accent-amber); }
1945
 
1946
  /* Progress table — piggybacks on .results-table styling. */
1947
+ .run-progress-table { font-variant-numeric: tabular-nums slashed-zero; }
1948
  .run-progress-table th.num,
1949
  .run-progress-table td.num { text-align: right; }
1950
  .run-row-queued { color: var(--foreground-subtle); }
1951
+ .run-row-running td {
1952
+ background: var(--signal-bg);
1953
+ position: relative;
1954
+ }
1955
+ /* Animated 1px stripe across the running row's leading cell — signals
1956
+ liveness without consuming attention the way a flashing background would. */
1957
+ .run-row-running td:first-child::before {
1958
+ content: '';
1959
+ position: absolute;
1960
+ left: 0;
1961
+ right: 0;
1962
+ top: 0;
1963
+ height: 2px;
1964
+ background: linear-gradient(90deg, transparent 0%, var(--signal) 50%, transparent 100%);
1965
+ background-size: 50% 100%;
1966
+ background-repeat: no-repeat;
1967
+ animation: running-stripe 1.4s linear infinite;
1968
+ pointer-events: none;
1969
+ }
1970
+ @keyframes running-stripe {
1971
+ from { background-position: -50% 0; }
1972
+ to { background-position: 150% 0; }
1973
+ }
1974
  .run-row-ok { color: var(--foreground-secondary); }
1975
  .run-row-error td { color: var(--error); }
1976
  .run-row-error td.err { font-family: var(--font-mono); font-size: 12px; }
index.html CHANGED
@@ -8,7 +8,7 @@
8
  <title>WebGPU Bench</title>
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
- <link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
12
  <link rel="stylesheet" href="css/style.css">
13
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
14
  </head>
@@ -22,7 +22,7 @@
22
  <nav class="header-nav" aria-label="Primary">
23
  <a href="run.html" class="header-link">Run</a>
24
  <a href="methodology.html" class="header-link">Methodology</a>
25
- <button id="theme-toggle" class="header-link theme-toggle-btn" type="button" title="Toggle theme">
26
  <svg class="icon-sun" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
27
  <svg class="icon-moon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
28
  </button>
@@ -51,14 +51,26 @@
51
  <section class="hero">
52
  <div class="container hero-inner">
53
  <div class="hero-copy">
 
 
 
 
54
  <h1 class="hero-title">WebGPU Bench</h1>
55
  <p class="hero-lede">Real-browser benchmarks for <code>llama.cpp</code>'s WebGPU backend. GGUF models loaded via WebAssembly, measured across Chrome and Safari.</p>
56
  <p class="hero-meta" id="hero-meta" hidden></p>
 
 
 
 
57
  </div>
58
- <div class="hero-actions">
59
- <a href="run.html" class="btn btn-primary">Run on your machine</a>
60
- <a href="methodology.html" class="btn btn-secondary">Methodology</a>
61
- </div>
 
 
 
 
62
  </div>
63
  </section>
64
 
@@ -127,8 +139,8 @@
127
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>
128
  </div>
129
  <div class="stat-card-content">
130
- <span class="stat-card-label">Machines Tested</span>
131
- <span class="stat-card-value" id="stat-machines">0</span>
132
  </div>
133
  </div>
134
  <div class="stat-card">
@@ -137,7 +149,7 @@
137
  </div>
138
  <div class="stat-card-content">
139
  <span class="stat-card-label">Benchmarks</span>
140
- <span class="stat-card-value" id="stat-benchmarks">0</span>
141
  </div>
142
  </div>
143
  <div class="stat-card">
@@ -146,7 +158,25 @@
146
  </div>
147
  <div class="stat-card-content">
148
  <span class="stat-card-label">Pass Rate</span>
149
- <span class="stat-card-value" id="stat-pass-rate">0%</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  </div>
151
  </div>
152
  </div>
 
8
  <title>WebGPU Bench</title>
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,500;12..96,600;12..96,700;12..96,800&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
12
  <link rel="stylesheet" href="css/style.css">
13
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
14
  </head>
 
22
  <nav class="header-nav" aria-label="Primary">
23
  <a href="run.html" class="header-link">Run</a>
24
  <a href="methodology.html" class="header-link">Methodology</a>
25
+ <button id="theme-toggle" class="header-link theme-toggle-btn" type="button" title="Toggle theme" aria-label="Toggle dark mode">
26
  <svg class="icon-sun" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
27
  <svg class="icon-moon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
28
  </button>
 
51
  <section class="hero">
52
  <div class="container hero-inner">
53
  <div class="hero-copy">
54
+ <span class="hero-eyebrow" id="hero-live" hidden>
55
+ <span class="hero-eyebrow-dot" aria-hidden="true"></span>
56
+ <span id="hero-live-text">Live</span>
57
+ </span>
58
  <h1 class="hero-title">WebGPU Bench</h1>
59
  <p class="hero-lede">Real-browser benchmarks for <code>llama.cpp</code>'s WebGPU backend. GGUF models loaded via WebAssembly, measured across Chrome and Safari.</p>
60
  <p class="hero-meta" id="hero-meta" hidden></p>
61
+ <div class="hero-actions">
62
+ <a href="run.html" class="btn btn-primary">Run on your machine</a>
63
+ <a href="methodology.html" class="btn btn-secondary">Methodology</a>
64
+ </div>
65
  </div>
66
+ <aside class="hero-stat" id="hero-stat" aria-live="polite" hidden>
67
+ <span class="hero-stat-label">Top decode</span>
68
+ <span class="hero-stat-value">
69
+ <span class="hero-stat-num" id="hero-top-decode" data-target="0">0.0</span>
70
+ <span class="hero-stat-unit">tok/s</span>
71
+ </span>
72
+ <span class="hero-stat-meta" id="hero-top-meta">—</span>
73
+ </aside>
74
  </div>
75
  </section>
76
 
 
139
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>
140
  </div>
141
  <div class="stat-card-content">
142
+ <span class="stat-card-label">Machines</span>
143
+ <span class="stat-card-value" id="stat-machines" data-target="0">0</span>
144
  </div>
145
  </div>
146
  <div class="stat-card">
 
149
  </div>
150
  <div class="stat-card-content">
151
  <span class="stat-card-label">Benchmarks</span>
152
+ <span class="stat-card-value" id="stat-benchmarks" data-target="0">0</span>
153
  </div>
154
  </div>
155
  <div class="stat-card">
 
158
  </div>
159
  <div class="stat-card-content">
160
  <span class="stat-card-label">Pass Rate</span>
161
+ <span class="stat-card-value"><span id="stat-pass-rate" data-target="0">0</span><span class="stat-card-unit">%</span></span>
162
+ </div>
163
+ </div>
164
+ <div class="stat-card">
165
+ <div class="stat-card-icon stat-card-icon--decode">
166
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 17 9 11 13 15 21 7"/><polyline points="14 7 21 7 21 14"/></svg>
167
+ </div>
168
+ <div class="stat-card-content">
169
+ <span class="stat-card-label">Best Decode</span>
170
+ <span class="stat-card-value"><span id="stat-best-decode" data-target="0">0.0</span><span class="stat-card-unit">tok/s</span></span>
171
+ </div>
172
+ </div>
173
+ <div class="stat-card">
174
+ <div class="stat-card-icon stat-card-icon--size">
175
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>
176
+ </div>
177
+ <div class="stat-card-content">
178
+ <span class="stat-card-label">Largest Run</span>
179
+ <span class="stat-card-value"><span id="stat-largest" data-target="0">0</span><span class="stat-card-unit">MB</span></span>
180
  </div>
181
  </div>
182
  </div>
js/app.js CHANGED
@@ -78,12 +78,21 @@ function render() {
78
  const filters = getFilters();
79
  const filtered = filterResults(appData.results, filters);
80
 
81
- // Summary cards
 
82
  const passed = filtered.filter(r => r.status === 'done');
83
- document.getElementById('stat-machines').textContent = appData.meta.machines.length;
84
- document.getElementById('stat-benchmarks').textContent = filtered.length;
85
- const passRate = filtered.length > 0 ? ((passed.length / filtered.length) * 100).toFixed(0) : '0';
86
- document.getElementById('stat-pass-rate').textContent = `${passRate}%`;
 
 
 
 
 
 
 
 
87
 
88
  // Results count
89
  const countEl = document.getElementById('results-count');
@@ -94,15 +103,16 @@ function render() {
94
  : `${filtered.length} of ${total}`;
95
  }
96
 
97
- // Reset button — always visible so users understand filters can be
98
- // cleared; disabled when nothing to reset.
 
99
  const resetBtn = document.getElementById('filter-reset');
100
  if (resetBtn) {
101
  const activeCount = (filters.machine !== 'all' ? 1 : 0) + (filters.browser !== 'all' ? 1 : 0) +
102
  (filters.model !== 'all' ? 1 : 0) + (filters.backend !== 'all' ? 1 : 0) +
103
  (filters.status !== 'all' ? 1 : 0) + (filters.quants.size > 0 ? 1 : 0);
104
  resetBtn.disabled = activeCount === 0;
105
- resetBtn.style.display = '';
106
  const label = resetBtn.querySelector('.filter-reset-label') || resetBtn;
107
  if (label !== resetBtn) {
108
  label.textContent = activeCount ? `Reset (${activeCount})` : 'Reset';
@@ -160,22 +170,63 @@ function renderCpuGpuSection(filtered, metric) {
160
 
161
  function renderHeroMeta(data) {
162
  const el = document.getElementById('hero-meta');
163
- if (!el) return;
 
164
  const generated = data?.meta?.generatedAt;
165
  const machineCount = data?.meta?.machines?.length || 0;
166
  const resultCount = data?.results?.length || 0;
167
- const parts = [];
168
- if (machineCount > 0) parts.push(`${machineCount} machine${machineCount === 1 ? '' : 's'}`);
169
- if (resultCount > 0) parts.push(`${resultCount} benchmark${resultCount === 1 ? '' : 's'}`);
170
- if (generated) {
171
- const d = new Date(generated);
172
- const rel = formatRelativeTime(d);
173
- parts.push(`updated ${rel}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  }
175
- if (parts.length === 0) return;
176
- el.textContent = parts.join(' · ');
177
- el.hidden = false;
178
- if (generated) el.title = new Date(generated).toLocaleString();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  }
180
 
181
  function formatRelativeTime(date) {
@@ -194,6 +245,7 @@ function initSectionNav() {
194
  const nav = document.getElementById('section-nav');
195
  if (!nav) return;
196
 
 
197
  const buttons = nav.querySelectorAll('.section-nav-item');
198
  const sections = [];
199
 
@@ -216,6 +268,14 @@ function initSectionNav() {
216
  });
217
  });
218
 
 
 
 
 
 
 
 
 
219
  // Scroll spy: instead of IntersectionObserver (which fires inconsistently
220
  // when multiple sections overlap the observer band), compute the
221
  // currently-active section on scroll by comparing each section's top to
@@ -225,13 +285,14 @@ function initSectionNav() {
225
  let ticking = false;
226
  const updateActive = () => {
227
  const anchor = stickyHead.offsetHeight + 16;
228
- let activeId = sections[0].section.id;
229
- for (const { section } of sections) {
230
- const top = section.getBoundingClientRect().top;
231
- if (top - anchor <= 0) activeId = section.id;
232
  else break;
233
  }
234
- buttons.forEach(b => b.classList.toggle('active', b.dataset.section === activeId));
 
235
  };
236
  const onScroll = () => {
237
  if (ticking) return;
@@ -241,6 +302,8 @@ function initSectionNav() {
241
  window.addEventListener('scroll', onScroll, { passive: true });
242
  window.addEventListener('resize', onScroll);
243
  updateActive();
 
 
244
  }
245
 
246
  init();
 
78
  const filters = getFilters();
79
  const filtered = filterResults(appData.results, filters);
80
 
81
+ // Summary cards — counts tween from previous value to new on filter changes
82
+ // and from 0 on first paint (since `data-value` defaults to "0").
83
  const passed = filtered.filter(r => r.status === 'done');
84
+ animateCount(document.getElementById('stat-machines'), appData.meta.machines.length, { decimals: 0 });
85
+ animateCount(document.getElementById('stat-benchmarks'), filtered.length, { decimals: 0 });
86
+ const passRate = filtered.length > 0 ? (passed.length / filtered.length) * 100 : 0;
87
+ animateCount(document.getElementById('stat-pass-rate'), passRate, { decimals: 0 });
88
+
89
+ const decodeVals = passed.map(r => r.decode_tok_s).filter(v => v != null);
90
+ const bestDecode = decodeVals.length ? Math.max(...decodeVals) : 0;
91
+ animateCount(document.getElementById('stat-best-decode'), bestDecode, { decimals: 1 });
92
+
93
+ const sizes = passed.map(r => r.sizeMB).filter(v => v != null);
94
+ const largest = sizes.length ? Math.max(...sizes) : 0;
95
+ animateCount(document.getElementById('stat-largest'), largest, { decimals: 0 });
96
 
97
  // Results count
98
  const countEl = document.getElementById('results-count');
 
103
  : `${filtered.length} of ${total}`;
104
  }
105
 
106
+ // Reset button — only present when at least one filter is active. Hiding
107
+ // (rather than disabling) removes a permanent ghost button from the bar
108
+ // and makes the appearance signal "you can undo your filter."
109
  const resetBtn = document.getElementById('filter-reset');
110
  if (resetBtn) {
111
  const activeCount = (filters.machine !== 'all' ? 1 : 0) + (filters.browser !== 'all' ? 1 : 0) +
112
  (filters.model !== 'all' ? 1 : 0) + (filters.backend !== 'all' ? 1 : 0) +
113
  (filters.status !== 'all' ? 1 : 0) + (filters.quants.size > 0 ? 1 : 0);
114
  resetBtn.disabled = activeCount === 0;
115
+ resetBtn.hidden = activeCount === 0;
116
  const label = resetBtn.querySelector('.filter-reset-label') || resetBtn;
117
  if (label !== resetBtn) {
118
  label.textContent = activeCount ? `Reset (${activeCount})` : 'Reset';
 
170
 
171
  function renderHeroMeta(data) {
172
  const el = document.getElementById('hero-meta');
173
+ const liveEl = document.getElementById('hero-live');
174
+ const liveText = document.getElementById('hero-live-text');
175
  const generated = data?.meta?.generatedAt;
176
  const machineCount = data?.meta?.machines?.length || 0;
177
  const resultCount = data?.results?.length || 0;
178
+
179
+ if (el) {
180
+ const parts = [];
181
+ if (machineCount > 0) parts.push(`${machineCount} machine${machineCount === 1 ? '' : 's'}`);
182
+ if (resultCount > 0) parts.push(`${resultCount} benchmark${resultCount === 1 ? '' : 's'}`);
183
+ if (generated) parts.push(`updated ${formatRelativeTime(new Date(generated))}`);
184
+ if (parts.length > 0) {
185
+ el.textContent = parts.join(' · ');
186
+ el.hidden = false;
187
+ if (generated) el.title = new Date(generated).toLocaleString();
188
+ }
189
+ }
190
+
191
+ if (liveEl && liveText && generated) {
192
+ liveText.textContent = `Live · ${formatRelativeTime(new Date(generated))}`;
193
+ liveEl.hidden = false;
194
+ }
195
+
196
+ // Hero stat: top decode tok/s with machine + model context.
197
+ const passed = (data?.results || []).filter(r => r.status === 'done' && r.decode_tok_s != null);
198
+ const heroStatEl = document.getElementById('hero-stat');
199
+ const heroNumEl = document.getElementById('hero-top-decode');
200
+ const heroMetaEl = document.getElementById('hero-top-meta');
201
+ if (heroStatEl && heroNumEl && heroMetaEl && passed.length > 0) {
202
+ const top = passed.reduce((a, b) => (a.decode_tok_s > b.decode_tok_s ? a : b));
203
+ heroStatEl.hidden = false;
204
+ heroMetaEl.textContent = `${top.machineSlug || top.machine || '—'} · ${top.model || ''} ${top.variant || ''}`.trim();
205
+ animateCount(heroNumEl, top.decode_tok_s, { decimals: 1, duration: 800 });
206
  }
207
+ }
208
+
209
+ /* Tween numeric content from 0 to a target. CSS-only via @property would
210
+ need server-side @property registration to work in older Safari; keep
211
+ this 12-line JS tween for predictability. */
212
+ export function animateCount(el, target, { decimals = 0, duration = 600 } = {}) {
213
+ if (!el) return;
214
+ const start = parseFloat(el.dataset.value || '0') || 0;
215
+ const end = Number(target) || 0;
216
+ if (start === end) {
217
+ el.textContent = end.toFixed(decimals);
218
+ return;
219
+ }
220
+ const startTime = performance.now();
221
+ const ease = (t) => 1 - Math.pow(1 - t, 3);
222
+ function step(now) {
223
+ const t = Math.min(1, (now - startTime) / duration);
224
+ const v = start + (end - start) * ease(t);
225
+ el.textContent = v.toFixed(decimals);
226
+ if (t < 1) requestAnimationFrame(step);
227
+ else el.dataset.value = String(end);
228
+ }
229
+ requestAnimationFrame(step);
230
  }
231
 
232
  function formatRelativeTime(date) {
 
245
  const nav = document.getElementById('section-nav');
246
  if (!nav) return;
247
 
248
+ const track = nav.querySelector('.section-nav-track');
249
  const buttons = nav.querySelectorAll('.section-nav-item');
250
  const sections = [];
251
 
 
268
  });
269
  });
270
 
271
+ // Drive the sliding indicator from the active button's geometry. Track
272
+ // is the positioned ancestor; offsetLeft/offsetWidth are relative to it.
273
+ const moveIndicator = (btn) => {
274
+ if (!track || !btn) return;
275
+ track.style.setProperty('--indicator-x', `${btn.offsetLeft}px`);
276
+ track.style.setProperty('--indicator-w', `${btn.offsetWidth}px`);
277
+ };
278
+
279
  // Scroll spy: instead of IntersectionObserver (which fires inconsistently
280
  // when multiple sections overlap the observer band), compute the
281
  // currently-active section on scroll by comparing each section's top to
 
285
  let ticking = false;
286
  const updateActive = () => {
287
  const anchor = stickyHead.offsetHeight + 16;
288
+ let active = sections[0];
289
+ for (const entry of sections) {
290
+ const top = entry.section.getBoundingClientRect().top;
291
+ if (top - anchor <= 0) active = entry;
292
  else break;
293
  }
294
+ buttons.forEach(b => b.classList.toggle('active', b === active.btn));
295
+ moveIndicator(active.btn);
296
  };
297
  const onScroll = () => {
298
  if (ticking) return;
 
302
  window.addEventListener('scroll', onScroll, { passive: true });
303
  window.addEventListener('resize', onScroll);
304
  updateActive();
305
+ // Re-measure once fonts settle — Bricolage Grotesque shifts widths.
306
+ document.fonts?.ready?.then(() => updateActive()).catch(() => {});
307
  }
308
 
309
  init();
js/charts.js CHANGED
@@ -1,18 +1,27 @@
1
  import { BROWSER_COLORS, quantSortKey, groupBy, formatTokS } from './utils.js';
2
 
3
- // Global Chart.js theme
4
- Chart.defaults.font.family = "'Manrope', system-ui, -apple-system, sans-serif";
 
 
 
5
  Chart.defaults.color = '#a1a1aa';
 
6
  Chart.defaults.plugins.tooltip.backgroundColor = 'rgba(15, 15, 18, 0.95)';
7
  Chart.defaults.plugins.tooltip.borderColor = '#27272a';
8
  Chart.defaults.plugins.tooltip.borderWidth = 1;
9
  Chart.defaults.plugins.tooltip.cornerRadius = 8;
10
  Chart.defaults.plugins.tooltip.padding = { top: 8, bottom: 8, left: 12, right: 12 };
11
- Chart.defaults.plugins.tooltip.titleFont = { weight: '600', size: 13 };
12
- Chart.defaults.plugins.tooltip.bodyFont = { family: "'JetBrains Mono', monospace", size: 12 };
13
- Chart.defaults.plugins.legend.labels.boxWidth = 12;
14
- Chart.defaults.plugins.legend.labels.boxHeight = 12;
 
 
15
  Chart.defaults.plugins.legend.labels.padding = 16;
 
 
 
16
 
17
  const chartInstances = new Map();
18
 
@@ -26,9 +35,10 @@ function destroyChart(id) {
26
  function themeColors() {
27
  const dark = document.documentElement.getAttribute('data-theme') === 'dark';
28
  return {
29
- grid: dark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)',
30
  text: dark ? '#a1a1aa' : '#71717a',
31
  title: dark ? '#e4e4e7' : '#09090b',
 
32
  };
33
  }
34
 
 
1
  import { BROWSER_COLORS, quantSortKey, groupBy, formatTokS } from './utils.js';
2
 
3
+ // Global Chart.js theme — uses the site's font tokens and a calm tooltip
4
+ // silhouette. Colors are pulled from CSS variables at render time so the
5
+ // theme toggle works without rebuilding chart instances.
6
+ Chart.defaults.font.family = "'Bricolage Grotesque', system-ui, -apple-system, sans-serif";
7
+ Chart.defaults.font.size = 12;
8
  Chart.defaults.color = '#a1a1aa';
9
+ Chart.defaults.borderColor = 'rgba(255,255,255,0.06)';
10
  Chart.defaults.plugins.tooltip.backgroundColor = 'rgba(15, 15, 18, 0.95)';
11
  Chart.defaults.plugins.tooltip.borderColor = '#27272a';
12
  Chart.defaults.plugins.tooltip.borderWidth = 1;
13
  Chart.defaults.plugins.tooltip.cornerRadius = 8;
14
  Chart.defaults.plugins.tooltip.padding = { top: 8, bottom: 8, left: 12, right: 12 };
15
+ Chart.defaults.plugins.tooltip.titleFont = { weight: '600', size: 12, family: "'Bricolage Grotesque', system-ui, sans-serif" };
16
+ Chart.defaults.plugins.tooltip.bodyFont = { family: "'Geist Mono', 'SF Mono', monospace", size: 12 };
17
+ Chart.defaults.plugins.tooltip.displayColors = true;
18
+ Chart.defaults.plugins.tooltip.boxPadding = 6;
19
+ Chart.defaults.plugins.legend.labels.boxWidth = 8;
20
+ Chart.defaults.plugins.legend.labels.boxHeight = 8;
21
  Chart.defaults.plugins.legend.labels.padding = 16;
22
+ Chart.defaults.plugins.legend.labels.font = { family: "'Geist Mono', monospace", size: 11 };
23
+ Chart.defaults.elements.bar.borderRadius = 4;
24
+ Chart.defaults.elements.bar.borderSkipped = false;
25
 
26
  const chartInstances = new Map();
27
 
 
35
  function themeColors() {
36
  const dark = document.documentElement.getAttribute('data-theme') === 'dark';
37
  return {
38
+ grid: dark ? 'rgba(255,255,255,0.04)' : 'rgba(15, 23, 42, 0.05)',
39
  text: dark ? '#a1a1aa' : '#71717a',
40
  title: dark ? '#e4e4e7' : '#09090b',
41
+ signal: dark ? '#22e09a' : '#0fa968',
42
  };
43
  }
44
 
js/run/controller.js CHANGED
@@ -299,11 +299,17 @@ function renderModels() {
299
  nameLabel.htmlFor = selectAllId;
300
  nameLabel.textContent = family;
301
 
 
 
 
 
 
 
302
  const stats = document.createElement('span');
303
  stats.className = 'run-family-stats';
304
  stats.textContent = `${variants.length} variants · ${fitsCount} fit · ${quickFitCount} quick`;
305
 
306
- header.append(toggleBtn, selectAll, nameLabel, stats);
307
 
308
  if (/^granite-4/i.test(family)) {
309
  const w = document.createElement('span');
@@ -362,7 +368,12 @@ function updateFamilySelectAllState(family) {
362
  `.run-family[data-family="${cssEscape(family)}"]`,
363
  );
364
  if (!familyEl) return;
365
- const rows = familyEl.querySelectorAll('.run-variant-select');
 
 
 
 
 
366
  const all = rows.length;
367
  const checked = [...rows].filter(cb => cb.checked).length;
368
  const selectAll = familyEl.querySelector('.run-family-select-all');
@@ -396,6 +407,18 @@ function formatSize(mb) {
396
  return `${mb.toFixed(0)} MB`;
397
  }
398
 
 
 
 
 
 
 
 
 
 
 
 
 
399
  function escapeText(s) {
400
  return String(s).replace(/[&<>]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));
401
  }
@@ -457,6 +480,32 @@ function wireFilters() {
457
  });
458
  }
459
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
  function wireBatchSelect() {
461
  const apply = (pred) => {
462
  document.querySelectorAll('.run-variant-select').forEach(cb => {
@@ -547,21 +596,50 @@ function updateButtons() {
547
  // workflow.)
548
  const rn = $('btn-run'); if (rn) rn.disabled = state.running || checked.length === 0;
549
  const ab = $('btn-abort'); if (ab) { ab.disabled = !state.running; ab.hidden = !state.running; }
550
- const status = $('queue-status');
551
- if (status) {
552
- if (checked.length === 0) {
553
- status.textContent = '';
554
- } else {
555
- const toDownload = checked.filter(v => !isCached(v));
556
- const dlMB = toDownload.reduce((a, v) => a + (v.sizeMB || 0), 0);
557
- const parts = [
558
- `${checked.length} selected`,
559
- `${cachedChecked.length} cached`,
560
- ];
561
- if (dlMB > 0) parts.push(`~${formatSize(dlMB)} to download`);
562
- status.textContent = parts.join(' · ');
563
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
564
  }
 
 
 
565
  }
566
 
567
  // ──────────────── progress table ────────────────
@@ -1424,6 +1502,7 @@ export async function mountRunSection() {
1424
  renderModels();
1425
  wireSelectionHandlers();
1426
  wireFilters();
 
1427
  wireBatchSelect();
1428
  wireIterationsInput();
1429
  wireRunHandlers();
 
299
  nameLabel.htmlFor = selectAllId;
300
  nameLabel.textContent = family;
301
 
302
+ const paramChip = document.createElement('span');
303
+ paramChip.className = 'run-family-params';
304
+ const params = parseParamSize(family);
305
+ if (params) paramChip.textContent = params;
306
+ else paramChip.hidden = true;
307
+
308
  const stats = document.createElement('span');
309
  stats.className = 'run-family-stats';
310
  stats.textContent = `${variants.length} variants · ${fitsCount} fit · ${quickFitCount} quick`;
311
 
312
+ header.append(toggleBtn, selectAll, nameLabel, paramChip, stats);
313
 
314
  if (/^granite-4/i.test(family)) {
315
  const w = document.createElement('span');
 
368
  `.run-family[data-family="${cssEscape(family)}"]`,
369
  );
370
  if (!familyEl) return;
371
+ // Only count fit variants — the parent checkbox is intentionally limited
372
+ // to toggling fits (non-fits would OOM). If we counted non-fits here too,
373
+ // the parent could never reach "all checked" for any mixed family, which
374
+ // wedges its underlying `checked` at false and turns subsequent clicks
375
+ // into no-ops (see SmolLM3-3B: 21 fit / 24 variants).
376
+ const rows = familyEl.querySelectorAll('.run-variant-row:not(.is-non-fit) .run-variant-select');
377
  const all = rows.length;
378
  const checked = [...rows].filter(cb => cb.checked).length;
379
  const selectAll = familyEl.querySelector('.run-family-select-all');
 
407
  return `${mb.toFixed(0)} MB`;
408
  }
409
 
410
+ /* Pull a parameter-count hint (e.g. "1B", "270M", "0.6B") from a family
411
+ name. Most family names embed this near the end (Llama-3.2-1B-Instruct,
412
+ gemma-3-270m-it). Returns the LAST `<digits>[Bb|Mm]` token in the name,
413
+ uppercased. Returns null if no match — chip is then hidden. */
414
+ function parseParamSize(name) {
415
+ if (!name) return null;
416
+ const matches = String(name).match(/(\d+\.?\d*)\s*[BbMm](?![A-Za-z])/g);
417
+ if (!matches?.length) return null;
418
+ const last = matches[matches.length - 1];
419
+ return last.toUpperCase().replace(/\s+/g, '');
420
+ }
421
+
422
  function escapeText(s) {
423
  return String(s).replace(/[&<>]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));
424
  }
 
480
  });
481
  }
482
 
483
+ function wireFamilySearch() {
484
+ const input = $('family-search');
485
+ if (!input) return;
486
+ // Live-filter family cards on input. Match against the lowercased family
487
+ // name; auto-expand any family that matches a non-empty query so the user
488
+ // sees the relevant variants without an extra click.
489
+ input.addEventListener('input', () => {
490
+ const q = input.value.trim().toLowerCase();
491
+ document.querySelectorAll('.run-family').forEach(el => {
492
+ const family = (el.dataset.family || '').toLowerCase();
493
+ const match = q === '' || family.includes(q);
494
+ el.hidden = !match;
495
+ // Expand on match-with-query so variants are visible without a click.
496
+ if (q !== '' && match) {
497
+ const list = el.querySelector('.run-variant-list');
498
+ const toggle = el.querySelector('.run-family-toggle');
499
+ if (list && toggle) {
500
+ list.hidden = false;
501
+ toggle.setAttribute('aria-expanded', 'true');
502
+ el.classList.add('is-open');
503
+ }
504
+ }
505
+ });
506
+ });
507
+ }
508
+
509
  function wireBatchSelect() {
510
  const apply = (pred) => {
511
  document.querySelectorAll('.run-variant-select').forEach(cb => {
 
596
  // workflow.)
597
  const rn = $('btn-run'); if (rn) rn.disabled = state.running || checked.length === 0;
598
  const ab = $('btn-abort'); if (ab) { ab.disabled = !state.running; ab.hidden = !state.running; }
599
+ renderBudgetMeter(checked, cachedChecked);
600
+ }
601
+
602
+ /* Show selected size as a fill bar against the device's max model size.
603
+ Three states drive the fill color: under (signal green), nearing (amber
604
+ 70%), over (red 100%). When nothing is selected, hide the whole
605
+ widget so the action bar isn't dominated by an empty meter. */
606
+ function renderBudgetMeter(checked, cachedChecked) {
607
+ const widget = $('run-budget');
608
+ const fill = $('run-budget-fill');
609
+ const text = $('run-budget-text');
610
+ const meta = $('run-budget-meta');
611
+ if (!widget || !fill || !text || !meta) return;
612
+
613
+ if (checked.length === 0) {
614
+ widget.hidden = true;
615
+ return;
616
+ }
617
+ widget.hidden = false;
618
+
619
+ const totalMB = checked.reduce((a, v) => a + (v.sizeMB || 0), 0);
620
+ const toDownload = checked.filter(v => !isCached(v));
621
+ const dlMB = toDownload.reduce((a, v) => a + (v.sizeMB || 0), 0);
622
+ const budgetMB = state.budget?.budgetMB || 0;
623
+
624
+ // Largest single model is what really matters for the device — total is
625
+ // download size, not peak memory. Show both.
626
+ const largest = checked.reduce((m, v) => Math.max(m, v.sizeMB || 0), 0);
627
+ const pct = budgetMB > 0 ? Math.min(100, (largest / budgetMB) * 100) : 0;
628
+
629
+ fill.style.width = `${pct}%`;
630
+ let tone = 'ok';
631
+ if (budgetMB > 0 && largest > budgetMB) tone = 'over';
632
+ else if (budgetMB > 0 && largest / budgetMB >= 0.7) tone = 'warn';
633
+ widget.dataset.tone = tone;
634
+
635
+ text.innerHTML = `<strong>${checked.length}</strong> selected · <span class="run-budget-size">${formatSize(totalMB)}</span> total`;
636
+ const metaParts = [];
637
+ if (largest > 0 && budgetMB > 0) {
638
+ metaParts.push(`largest ${formatSize(largest)} / budget ${formatSize(budgetMB)}`);
639
  }
640
+ if (cachedChecked.length > 0) metaParts.push(`${cachedChecked.length} cached`);
641
+ if (dlMB > 0) metaParts.push(`~${formatSize(dlMB)} to download`);
642
+ meta.textContent = metaParts.join(' · ');
643
  }
644
 
645
  // ──────────────── progress table ────────────────
 
1502
  renderModels();
1503
  wireSelectionHandlers();
1504
  wireFilters();
1505
+ wireFamilySearch();
1506
  wireBatchSelect();
1507
  wireIterationsInput();
1508
  wireRunHandlers();
js/tables.js CHANGED
@@ -217,11 +217,15 @@ export function renderMachineInfo(machines) {
217
  const addYourMachineCard = `
218
  <a class="machine-card machine-card-add" href="run.html">
219
  <div class="machine-card-header">
220
- <svg class="machine-card-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
221
  <h3>Add your machine</h3>
222
  </div>
223
- <p class="machine-card-add-blurb">Run the benchmark in your browser and contribute results to the leaderboard.</p>
224
- <span class="machine-card-add-cta">Open Run page →</span>
 
 
 
 
225
  </a>`;
226
 
227
  if (machines.length === 0) {
 
217
  const addYourMachineCard = `
218
  <a class="machine-card machine-card-add" href="run.html">
219
  <div class="machine-card-header">
220
+ <svg class="machine-card-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
221
  <h3>Add your machine</h3>
222
  </div>
223
+ <p class="machine-card-add-blurb">Run benchmarks directly in your browser. Results post to the leaderboard.</p>
224
+ <code class="machine-card-add-cmd">npm run bench:quick</code>
225
+ <span class="machine-card-add-cta">
226
+ Open Run page
227
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>
228
+ </span>
229
  </a>`;
230
 
231
  if (machines.length === 0) {
methodology.html CHANGED
@@ -8,7 +8,7 @@
8
  <title>Methodology — WebGPU Bench</title>
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
- <link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
12
  <link rel="stylesheet" href="css/style.css">
13
  </head>
14
  <body>
@@ -21,7 +21,7 @@
21
  <nav class="header-nav" aria-label="Primary">
22
  <a href="index.html" class="header-link">Dashboard</a>
23
  <a href="run.html" class="header-link">Run</a>
24
- <button id="theme-toggle" class="header-link theme-toggle-btn" type="button" title="Toggle theme">
25
  <svg class="icon-sun" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
26
  <svg class="icon-moon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
27
  </button>
 
8
  <title>Methodology — WebGPU Bench</title>
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,500;12..96,600;12..96,700;12..96,800&family=Geist+Mono:wght@400;500;600&family=Instrument+Serif:ital@0;1&display=swap" rel="stylesheet">
12
  <link rel="stylesheet" href="css/style.css">
13
  </head>
14
  <body>
 
21
  <nav class="header-nav" aria-label="Primary">
22
  <a href="index.html" class="header-link">Dashboard</a>
23
  <a href="run.html" class="header-link">Run</a>
24
+ <button id="theme-toggle" class="header-link theme-toggle-btn" type="button" title="Toggle theme" aria-label="Toggle dark mode">
25
  <svg class="icon-sun" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
26
  <svg class="icon-moon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
27
  </button>
run.html CHANGED
@@ -8,7 +8,7 @@
8
  <title>Run — WebGPU Bench</title>
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
- <link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
12
  <link rel="stylesheet" href="css/style.css">
13
  <!-- Import map so `@huggingface/hub` resolves in the browser via esm.sh.
14
  Must appear before any <script type="module">. -->
@@ -30,7 +30,7 @@
30
  <nav class="header-nav" aria-label="Primary">
31
  <a href="index.html" class="header-link">Dashboard</a>
32
  <a href="methodology.html" class="header-link">Methodology</a>
33
- <button id="theme-toggle" class="header-link theme-toggle-btn" type="button" title="Toggle theme">
34
  <svg class="icon-sun" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
35
  <svg class="icon-moon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
36
  </button>
@@ -113,6 +113,13 @@
113
  <!-- Hide filters, iterations, actions -->
114
  <div class="filter-bar run-controls">
115
  <div class="filter-bar-inner run-filters">
 
 
 
 
 
 
 
116
  <div class="filter-group">
117
  <span class="filter-label">Hide</span>
118
  <div class="run-filters-checks">
@@ -136,10 +143,26 @@
136
  </div>
137
  </div>
138
 
139
- <!-- Sticky action bar: always-reachable Download/Run -->
 
 
 
 
 
 
140
  <div class="run-action-bar">
141
  <div class="run-action-bar-inner">
142
- <span id="queue-status" class="run-queue-status"></span>
 
 
 
 
 
 
 
 
 
 
143
  <div class="run-actions">
144
  <button class="btn btn-secondary" id="btn-download" type="button" disabled>Download selected</button>
145
  <button class="btn btn-primary" id="btn-run" type="button" disabled>Run benchmarks</button>
@@ -149,11 +172,6 @@
149
  </div>
150
  </div>
151
 
152
- <!-- Family / variant list -->
153
- <div id="run-models" class="run-models-stack">
154
- <div class="empty-state">Loading models…</div>
155
- </div>
156
-
157
  <!-- Progress -->
158
  <div class="section-header">
159
  <h2 class="subsection-title">Progress</h2>
 
8
  <title>Run — WebGPU Bench</title>
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,500;12..96,600;12..96,700;12..96,800&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
12
  <link rel="stylesheet" href="css/style.css">
13
  <!-- Import map so `@huggingface/hub` resolves in the browser via esm.sh.
14
  Must appear before any <script type="module">. -->
 
30
  <nav class="header-nav" aria-label="Primary">
31
  <a href="index.html" class="header-link">Dashboard</a>
32
  <a href="methodology.html" class="header-link">Methodology</a>
33
+ <button id="theme-toggle" class="header-link theme-toggle-btn" type="button" title="Toggle theme" aria-label="Toggle dark mode">
34
  <svg class="icon-sun" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
35
  <svg class="icon-moon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
36
  </button>
 
113
  <!-- Hide filters, iterations, actions -->
114
  <div class="filter-bar run-controls">
115
  <div class="filter-bar-inner run-filters">
116
+ <div class="filter-group filter-group--search">
117
+ <label class="filter-label" for="family-search">Search</label>
118
+ <div class="run-search-wrapper">
119
+ <svg class="run-search-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
120
+ <input type="search" id="family-search" class="run-search-input" placeholder="Filter models…" autocomplete="off" spellcheck="false">
121
+ </div>
122
+ </div>
123
  <div class="filter-group">
124
  <span class="filter-label">Hide</span>
125
  <div class="run-filters-checks">
 
143
  </div>
144
  </div>
145
 
146
+ <!-- Family / variant list -->
147
+ <div id="run-models" class="run-models-stack">
148
+ <div class="empty-state">Loading models…</div>
149
+ </div>
150
+
151
+ <!-- Action bar: lives just above Progress so the Run/Abort/Download
152
+ controls are co-located with the live status they affect. -->
153
  <div class="run-action-bar">
154
  <div class="run-action-bar-inner">
155
+ <div class="run-budget" id="run-budget" hidden>
156
+ <div class="run-budget-row">
157
+ <span class="run-budget-label">Selected</span>
158
+ <span class="run-budget-text" id="run-budget-text">—</span>
159
+ </div>
160
+ <div class="run-budget-bar" role="progressbar" aria-labelledby="run-budget-text">
161
+ <div class="run-budget-bar-fill" id="run-budget-fill"></div>
162
+ </div>
163
+ <div class="run-budget-meta" id="run-budget-meta"></div>
164
+ </div>
165
+ <span id="queue-status" class="run-queue-status" hidden></span>
166
  <div class="run-actions">
167
  <button class="btn btn-secondary" id="btn-download" type="button" disabled>Download selected</button>
168
  <button class="btn btn-primary" id="btn-run" type="button" disabled>Run benchmarks</button>
 
172
  </div>
173
  </div>
174
 
 
 
 
 
 
175
  <!-- Progress -->
176
  <div class="section-header">
177
  <h2 class="subsection-title">Progress</h2>