@wendellresearch-ai/benchwarmers 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/AGENTS.md +147 -147
  2. package/BENCHMARKS.md +262 -262
  3. package/GATEWAY.md +272 -272
  4. package/NOTICE +28 -28
  5. package/PRIVACY.md +46 -46
  6. package/README.md +368 -314
  7. package/SECURITY.md +83 -83
  8. package/TERMS.md +22 -22
  9. package/USAGE.md +170 -163
  10. package/VERSION +1 -1
  11. package/bin/benchwarmers.js +16 -74
  12. package/cli.py +666 -0
  13. package/examples/config/gateway.json +5 -5
  14. package/examples/config/model-roles.json +11 -11
  15. package/gateway.py +755 -755
  16. package/launcher.py +120 -0
  17. package/mcp_server.py +266 -266
  18. package/metrics/alerts.py +116 -116
  19. package/metrics/aliases.py +41 -41
  20. package/metrics/bench_collections.py +141 -141
  21. package/metrics/catalog.py +295 -295
  22. package/metrics/client.py +394 -394
  23. package/metrics/cliproxy.py +191 -191
  24. package/metrics/connect.py +691 -691
  25. package/metrics/cost.py +152 -152
  26. package/metrics/decide.py +227 -227
  27. package/metrics/discover.py +318 -318
  28. package/metrics/endpoints.py +436 -436
  29. package/metrics/guard.py +617 -592
  30. package/metrics/learn.py +221 -221
  31. package/metrics/litellm_hub.py +206 -206
  32. package/metrics/model_roles.py +84 -84
  33. package/metrics/offload.py +166 -166
  34. package/metrics/owui.py +205 -205
  35. package/metrics/paths.py +60 -60
  36. package/metrics/route.py +579 -579
  37. package/metrics/scheduler.py +275 -275
  38. package/metrics/sources.py +167 -167
  39. package/metrics/wrong.py +144 -144
  40. package/package.json +68 -65
  41. package/perf/README.md +89 -89
  42. package/perf/run.py +261 -261
  43. package/scripts/auto_retire.py +367 -367
  44. package/scripts/check.py +108 -106
  45. package/scripts/check_deprecations.py +378 -378
  46. package/scripts/check_themes.py +103 -103
  47. package/scripts/init.py +124 -124
  48. package/scripts/list_served.py +78 -78
  49. package/scripts/notice.py +78 -78
  50. package/scripts/onboard.py +401 -331
  51. package/scripts/provider_tags.py +129 -129
  52. package/scripts/reprefix.py +136 -136
  53. package/scripts/sync_config.py +105 -105
  54. package/scripts/test_auth.py +255 -255
  55. package/scripts/test_cli.py +189 -0
  56. package/scripts/test_client.py +233 -233
  57. package/scripts/test_connect.py +222 -222
  58. package/scripts/test_first_run.py +180 -180
  59. package/scripts/test_hubs.py +328 -328
  60. package/scripts/test_launcher.py +67 -0
  61. package/scripts/test_llm_log.py +85 -81
  62. package/scripts/test_mcp.py +178 -178
  63. package/scripts/test_merged.py +214 -214
  64. package/scripts/test_no_owui.py +291 -291
  65. package/scripts/test_owui.py +90 -90
  66. package/scripts/test_serve_edit.py +448 -438
  67. package/scripts/test_verdict.py +98 -98
  68. package/scripts/test_vision.py +228 -228
  69. package/serve.py +3904 -3764
  70. package/suite/mine.py +154 -154
  71. package/suite/pace.py +145 -145
  72. package/suite/run.py +1046 -1042
  73. package/suite/seed.py +421 -421
  74. package/suite/tasks/claim-alias-unbound/fixtures/onboard.py +286 -286
  75. package/suite/tasks/claim-error-text-string/fixtures/decide.py +187 -187
  76. package/suite/tasks/claim-list-body/fixtures/gateway.py +235 -235
  77. package/suite/tasks/claim-member-nominated-twice/fixtures/gateway.py +284 -284
  78. package/suite/tasks/claim-nominee-uses-slot/fixtures/gateway.py +284 -284
  79. package/suite/tasks/claim-silent-exit/fixtures/gateway.py +235 -235
  80. package/suite/tasks/claim-sse-hang/fixtures/gateway.py +235 -235
  81. package/suite/tasks/claim-unknown-nominee-crash/fixtures/gateway.py +284 -284
  82. package/suite/tasks/rebench-matchms-809-bug/task.json +23 -23
  83. package/suite/tasks/rebench-matchms-809-control/task.json +27 -27
  84. package/suite/tasks/rebench-more-itertools-1028-bug/task.json +23 -23
  85. package/suite/tasks/rebench-more-itertools-1028-control/task.json +27 -27
  86. package/suite/tasks/rebench-narwhals-2864-bug/task.json +23 -23
  87. package/suite/tasks/rebench-narwhals-2864-control/task.json +27 -27
  88. package/suite/tasks/rebench-quantecon-py-769-bug/task.json +23 -23
  89. package/suite/tasks/rebench-quantecon-py-769-control/task.json +27 -27
  90. package/suite/tasks/rebench-scrapy-6867-bug/task.json +23 -23
  91. package/suite/tasks/rebench-scrapy-6867-control/task.json +27 -27
  92. package/ui/bench.css +430 -425
  93. package/ui/bench.js +1756 -1576
  94. package/ui/themes/dark.css +35 -35
  95. package/ui/themes/hardwood.css +52 -52
  96. package/ui/themes/rainforest.css +32 -32
package/ui/bench.css CHANGED
@@ -1,425 +1,430 @@
1
- /* Benchwarmers. The theme's colour tokens arrive first (ui/themes/*.css, the
2
- Wendell Research palette by default, emitted by serve.py); the values below
3
- are derived from them: grounds mixed from a theme colour, and the bench's
4
- own spacing. A theme sets colours only. */
5
-
6
- :root {
7
- --ok: var(--primary);
8
- --ok-ground: color-mix(in oklab, var(--primary) 13%, var(--paper-2));
9
- --warn-ground: color-mix(in oklab, var(--signal) 55%, var(--paper-2));
10
- --err-ground: color-mix(in oklab, var(--err) 11%, var(--paper-2));
11
- --quiet-ground: var(--band);
12
- --panel: var(--paper-2);
13
- --step-1: color-mix(in oklab, var(--band) 55%, var(--paper));
14
- color-scheme: var(--scheme, light);
15
- }
16
-
17
- * { box-sizing: border-box; }
18
- [hidden] { display: none !important; }
19
- html { -webkit-text-size-adjust: 100%; }
20
- body {
21
- margin: 0;
22
- /* A theme may draw the page's ground (Hardwood's maple court) and a line
23
- across the top (its crimson court line); the others are plain paper. */
24
- background: var(--floor, var(--paper));
25
- border-top: .4rem solid var(--court-line, transparent);
26
- color: var(--ink);
27
- font: 400 1rem/1.55 var(--font-display);
28
- -webkit-font-smoothing: antialiased;
29
- }
30
- ::selection { background: var(--signal); color: var(--ink); }
31
- a { color: var(--link); text-underline-offset: .18em; }
32
- :focus-visible { outline: 2px solid var(--link); outline-offset: 3px; border-radius: 2px; }
33
- code, .mono, td.num, .chip, .alias {
34
- font-family: var(--font-mono);
35
- font-variant-numeric: tabular-nums;
36
- }
37
- code { font-size: .875em; background: var(--step-1); padding: .05rem .3rem; border-radius: var(--radius); }
38
- h1, h2, h3 { line-height: 1.1; letter-spacing: -.02em; text-wrap: balance; }
39
-
40
- /* ---- header --------------------------------------------------------- */
41
- /* The page is wider than the theme's reading measure: with a sidebar, a
42
- 76rem page left a narrow column and wide empty margins on a large screen.
43
- Prose inside panels keeps its own measure (.panel > .sub). */
44
- /* One line: the name on its bench, the credit beside it, the actions pushed
45
- to the far end. */
46
- header {
47
- max-width: var(--page, 104rem); margin: 0 auto;
48
- padding: 1.5rem var(--gutter) 1rem;
49
- display: flex; flex-wrap: wrap; align-items: center; column-gap: 1.25rem;
50
- }
51
- header h1 { margin: 0; font-size: 2.25rem; font-weight: 800; letter-spacing: -.04em; line-height: 1; }
52
- /* The name sits on a team bench: a seat in the team's colour (the theme's
53
- brand, or --bench-seat) on two legs joined by a rail. Drawn in CSS from the
54
- font size, so it scales with the heading. With line-height 1 the letters
55
- end about .14em above the box, so the seat's top is put there: the name
56
- rests on it. */
57
- header h1 span { position: relative; display: inline-block; padding: 0 .14em .6em; }
58
- header h1 span::after { /* the seat */
59
- content: ""; position: absolute; left: -.08em; right: -.08em; bottom: .46em; height: .28em;
60
- background: var(--bench-seat, var(--brand)); border-radius: .06em;
61
- box-shadow: inset 0 .055em 0 rgb(255 255 255 / .28), 0 .05em 0 rgb(0 0 0 / .3);
62
- }
63
- header h1 span::before { /* legs and the rail between them */
64
- --leg: var(--bench-leg, var(--ink-2));
65
- content: ""; position: absolute; left: .18em; right: .18em; bottom: 0; height: .5em;
66
- background:
67
- linear-gradient(var(--leg), var(--leg)) left top / .11em 100% no-repeat,
68
- linear-gradient(var(--leg), var(--leg)) right top / .11em 100% no-repeat,
69
- linear-gradient(var(--leg), var(--leg)) center 66% / 100% .07em no-repeat;
70
- }
71
- /* The credit at the bench's feet: level with the bottom of the name's group. */
72
- header .sub { align-self: flex-end; margin: 0 auto 0 -.6rem; line-height: 1; color: var(--muted); font-size: .9375rem; }
73
- header .sub a { color: var(--ink); font-weight: 500; text-decoration-color: var(--brand); }
74
- /* Right-aligned, on the name's line or, when it does not fit, on its own. */
75
- .head-actions { display: flex; flex-wrap: wrap; gap: .75rem; align-items: center;
76
- margin-left: auto; justify-content: flex-end; }
77
-
78
- /* ---- buttons -------------------------------------------------------- */
79
- button { font: inherit; cursor: pointer; }
80
- .btn {
81
- display: inline-flex; align-items: center; gap: .4rem;
82
- padding: .5rem .9rem; border-radius: var(--radius);
83
- border: 1px solid var(--ink); background: var(--panel); color: var(--ink);
84
- font-weight: 500; font-size: .9375rem;
85
- transition: transform .15s ease, box-shadow .15s ease, background-color .15s ease;
86
- }
87
- .btn:hover { background: var(--step-1); }
88
- .btn.primary { background: var(--ink); color: var(--paper); box-shadow: 4px 4px 0 var(--brand); }
89
- .btn.primary:hover { transform: translate(-2px, -2px); box-shadow: 6px 6px 0 var(--brand); background: var(--ink); }
90
- .btn.danger { background: var(--err); border-color: var(--err); color: var(--on-err, #fff); }
91
- .btn.danger:hover { background: color-mix(in oklab, var(--err) 86%, #000); }
92
- .btn.small { padding: .25rem .6rem; font-size: .875rem; }
93
- .btn:disabled { opacity: .5; cursor: default; transform: none; box-shadow: none; }
94
- .btn.fresh { background: var(--signal); border-color: var(--ink); }
95
- .btn.fresh:hover { background: var(--signal); transform: translate(-1px, -1px); box-shadow: 3px 3px 0 var(--ink); }
96
- .linkish { background: none; border: 0; padding: 0; color: var(--link); text-decoration: underline;
97
- text-underline-offset: .18em; font-size: inherit; }
98
-
99
- /* ---- layout --------------------------------------------------------- */
100
- main { max-width: var(--page, 104rem); margin: 0 auto; padding: 0 var(--gutter) 4rem; }
101
-
102
- /* problems: what needs a person, each with the action that resolves it */
103
- .problems { background: var(--sheet, var(--panel)); border-radius: 14px; padding: 1rem 1.4rem; margin: .5rem 0 1.5rem;
104
- box-shadow: 0 1px 2px rgb(0 0 0 / .04), 0 12px 32px -14px rgb(0 0 0 / .14);
105
- -webkit-backdrop-filter: var(--glass, none); backdrop-filter: var(--glass, none); }
106
- .problems h2 { margin: 0 0 .25rem; font-size: 1.25rem; font-weight: 700; }
107
- .problems > p { margin: 0 0 .5rem; color: var(--muted); font-size: .9375rem; }
108
- .problem { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .25rem 1rem;
109
- align-items: center; padding: .55rem .75rem; margin: 0 -.75rem; border-radius: var(--radius); }
110
- .problem:hover { background: var(--step-1); }
111
- .problems details { margin-top: .5rem; }
112
- .problems details > summary { cursor: pointer; font-weight: 500; padding: .4rem 0; }
113
- .problems details > summary .muted { font-weight: 400; }
114
- .problem .what { font-weight: 500; }
115
- .problem .why { color: var(--muted); font-size: .875rem; overflow-wrap: anywhere; }
116
- .problem .acts, .endpoint .acts, .ep .acts { display: flex; gap: .5rem; flex-wrap: wrap; justify-content: flex-end; }
117
- .row-input { display: flex; gap: .5rem; align-items: center; margin-top: .35rem; }
118
- .row-input input { flex: 1; min-width: 0; margin: 0 !important; }
119
- .sub-head { font-weight: 600; margin: 1rem 0 .25rem; }
120
- .problem .chip { margin-right: .4rem; }
121
- .problems .head { display: flex; align-items: baseline; gap: .5rem; }
122
- .problems .head h2 { margin-bottom: .25rem; }
123
- .problems .sep { display: none; color: var(--muted); }
124
- /* Hidden (bench.js): one line, "Action needed · 3 · show", until something new */
125
- .problems.collapsed { padding: .5rem 1.25rem; }
126
- .problems.collapsed > :not(.head) { display: none; }
127
- .problems.collapsed .head h2 { margin: 0; font-size: 1rem; }
128
- .problems.collapsed .sep { display: inline; }
129
-
130
- /* pages: a grouped sidebar (Observe, Gateway, Control); the group names and
131
- the active page carry the theme's brand colour. A theme with a floor backs
132
- the sidebar with frosted cream (--rail) so it reads over the court. On a
133
- narrow screen the groups sit in a row above the page. */
134
- .layout { display: grid; grid-template-columns: 12rem minmax(0, 1fr); gap: 1.5rem; align-items: start; }
135
- .tabs { position: sticky; top: 1rem; display: flex; flex-direction: column; gap: 1.1rem;
136
- background: var(--rail, transparent); padding: var(--rail-pad, 0); border-radius: 14px;
137
- -webkit-backdrop-filter: var(--glass, none); backdrop-filter: var(--glass, none); }
138
- .tabs .group { display: flex; flex-direction: column; align-items: flex-start; gap: .55rem; }
139
- .tabs h2 { margin: 0 0 .2rem; font-size: .75rem; font-weight: 700; letter-spacing: .08em;
140
- text-transform: uppercase; color: color-mix(in oklab, var(--brand) 72%, var(--ink)); }
141
- /* the brand, pulled toward the ink so small text stays legible in any theme */
142
- /* No bottom padding and a tight line height, so the box ends just under the
143
- letters; the spacing between pages is the group's gap instead. */
144
- .tabs button { background: none; border: 0; padding: 0; line-height: 1.15; color: var(--muted); text-align: left;
145
- font-size: 1rem; font-weight: 500; }
146
- .tabs button:hover { color: var(--ink); }
147
- /* The active page: a highlighter stroke across the bottom of its letters,
148
- the header's old mark. */
149
- .tabs button[aria-selected="true"] { color: var(--ink); font-weight: 700;
150
- box-shadow: inset 0 -.42em 0 var(--signal); }
151
- /* Collections, the bench's own idea: a small brand sparkle after it. It
152
- hangs outside the button, so the active page's stroke still runs under
153
- the word alone. */
154
- .tabs button[data-tab="collections"] { position: relative; }
155
- .tabs button[data-tab="collections"]::after {
156
- content: ""; position: absolute; left: calc(100% + .3em); top: -.05em; width: .75em; height: .75em;
157
- background: var(--brand);
158
- clip-path: polygon(50% 0, 62% 38%, 100% 50%, 62% 62%, 50% 100%, 38% 62%, 0 50%, 38% 38%); }
159
- .tab { display: none; } .tab.active { display: block; }
160
- /* One surface for the page's content: its panels are sections of it,
161
- divided by a hairline, rather than separate boxes with gaps between. A
162
- theme with a floor makes it frosted (--sheet, --glass). */
163
- .panes { background: var(--sheet, var(--panel)); border-radius: 14px;
164
- box-shadow: 0 1px 2px rgb(0 0 0 / .04), 0 12px 32px -14px rgb(0 0 0 / .14);
165
- -webkit-backdrop-filter: var(--glass, none); backdrop-filter: var(--glass, none); }
166
- .panes .panel { background: transparent; border-radius: 0; padding: 1.4rem 1.6rem 1.5rem; }
167
- .panes .tab > .panel + .panel { border-top: 1px solid var(--line); }
168
- @media (max-width: 52rem) {
169
- .layout { grid-template-columns: minmax(0, 1fr); gap: 1rem; }
170
- .tabs { position: static; flex-direction: row; flex-wrap: wrap; gap: .5rem 1.5rem; }
171
- .tabs .group { flex-direction: row; flex-wrap: wrap; align-items: baseline; gap: .1rem 1rem; }
172
- .tabs h2 { margin: 0; }
173
- }
174
-
175
- /* settings, connections, logs, info */
176
- .themes { display: flex; flex-wrap: wrap; gap: .75rem; margin-top: .75rem; }
177
- td.set-by { font-size: .8125rem; color: var(--muted); white-space: nowrap; }
178
- .theme-card { display: grid; grid-template-columns: auto 1fr; align-items: center; gap: .35rem .5rem;
179
- padding: .6rem .8rem; border: 1px solid var(--line); border-radius: var(--radius); cursor: pointer; }
180
- .theme-card:has(input:checked) { border-color: var(--ink); box-shadow: 3px 3px 0 var(--brand); }
181
- .theme-card .swatch, .theme-card a { grid-column: 2; }
182
- .swatch { display: flex; gap: 2px; }
183
- .swatch span { width: 1rem; height: 1rem; border-radius: 2px; border: 1px solid var(--line); }
184
- .theme-card a { font-size: .8125rem; }
185
- table.facts th { text-align: left; padding-right: 1.5rem; color: var(--muted); font-weight: 500; }
186
- details.panel > summary { cursor: pointer; }
187
- details.panel[open] > summary { margin-bottom: .75rem; }
188
- .log-line { font-size: .8125rem; padding: .1rem 0; overflow-wrap: anywhere; }
189
-
190
- .panel { background: var(--panel); border-radius: var(--radius); padding: 1.1rem 1.25rem 1.25rem; }
191
- .panel > h2 { margin: 0; font-size: 1.375rem; font-weight: 700; }
192
- /* A section's heading carries a short bar of the brand colour: the accent a
193
- mostly-white theme needs, and a marker where sections meet on one surface. */
194
- .panes .panel > h2::before { content: ""; display: inline-block; width: .22em; height: .8em; margin-right: .45em;
195
- vertical-align: -.05em; border-radius: 2px; background: var(--brand); }
196
- .panel > .sub { margin: .3rem 0 1rem; color: var(--muted); font-size: .9375rem; max-width: 70ch; }
197
- .toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem 1.25rem; margin: 0 0 .75rem; }
198
- .toolbar input[type=search] { font: inherit; font-size: .9375rem; padding: .4rem .6rem; min-width: 14rem;
199
- border: 1px solid var(--line); border-radius: var(--radius); background: var(--paper); color: var(--ink); }
200
- .toolbar label { font-size: .9375rem; display: inline-flex; align-items: center; gap: .4rem; cursor: pointer; }
201
- .toolbar select { font: inherit; font-size: .9375rem; padding: .35rem .4rem; border: 1px solid var(--line);
202
- border-radius: var(--radius); background: var(--paper); color: var(--ink); }
203
- tr.pick-row { cursor: pointer; }
204
- a.dmodel { color: var(--ink); text-decoration: none; font-weight: 500; }
205
- a.dmodel:hover .alias { box-shadow: inset 0 -.3em 0 var(--signal); }
206
- /* Connections: each endpoint, its models folded under it */
207
- .ep { padding: .75rem 0; border-top: 1px solid var(--line); }
208
- .ep-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .4rem 1rem; align-items: center; }
209
- .ep .base { color: var(--muted); font-size: .875rem; overflow-wrap: anywhere; }
210
- .ep details.models > summary { cursor: pointer; color: var(--link); font-size: .9375rem; margin-top: .3rem; }
211
- .ep details.models[open] > summary { margin-bottom: .5rem; }
212
- .hint { color: var(--muted); font-size: .875rem; margin: .9rem 0 0; max-width: 75ch; }
213
-
214
- /* key: folded until asked for */
215
- details.key { margin: 1rem 0 0; }
216
- details.key > summary { cursor: pointer; color: var(--link); font-size: .9375rem; width: max-content; }
217
- details.key dl { margin: .5rem 0 0; display: grid; grid-template-columns: max-content minmax(0, 60ch);
218
- gap: .35rem 1rem; font-size: .9375rem; }
219
- details.key dt { font-weight: 700; }
220
- details.key dd { margin: 0; color: var(--ink); }
221
-
222
- /* ---- tables --------------------------------------------------------- */
223
- .scroll { overflow-x: auto; }
224
- table { border-collapse: collapse; width: 100%; font-size: .9375rem; }
225
- th, td { text-align: left; padding: .5rem .6rem; border-bottom: 1px solid var(--line); vertical-align: middle; }
226
- th { font-size: .875rem; font-weight: 700; color: var(--ink); white-space: nowrap; }
227
- th[data-sortable] { cursor: pointer; user-select: none; }
228
- th[data-sortable]::after { content: ""; display: inline-block; width: .9em; color: var(--muted); }
229
- th[aria-sort=ascending]::after { content: " \2191"; }
230
- th[aria-sort=descending]::after { content: " \2193"; }
231
- td.num, th.num { text-align: right; }
232
- /* the per-connection model cards on Connections: one fixed column plan */
233
- table.model-cards { table-layout: fixed; min-width: 52rem; }
234
- .panel:has(> table.model-cards) { overflow-x: auto; } /* scrolls on a narrow window instead of wrapping numbers */
235
- table.model-cards td.num { white-space: nowrap; }
236
- table.model-cards col.c-model { width: 24%; }
237
- table.model-cards col.c-via { width: 13%; }
238
- table.model-cards col.c-n { width: 10%; }
239
- table.model-cards col.c-input { width: 19%; }
240
- table.model-cards col.c-lim { width: 6%; }
241
- table.model-cards td { overflow-wrap: anywhere; }
242
- tbody tr:hover > td { background: var(--step-1); }
243
- tr.retired > td { color: var(--muted); }
244
- tr.retired .alias { text-decoration: line-through; }
245
- body:not(.show-retired) tr.retired { display: none; }
246
- body:not(.show-paid) tr.paid { display: none; }
247
- .muted { color: var(--muted); }
248
- .ok { color: var(--ok); } .warn { color: var(--warn); } .err { color: var(--err); }
249
-
250
- a.model { color: var(--ink); text-decoration: none; font-weight: 500; }
251
- a.model:hover .alias { box-shadow: inset 0 -.3em 0 var(--signal); }
252
- .conn { display: block; color: var(--muted); font-size: .8125rem; }
253
-
254
- /* status chips */
255
- .chip { display: inline-block; padding: .1rem .5rem; border-radius: 999px; font-size: .8125rem;
256
- font-weight: 500; white-space: nowrap; background: var(--quiet-ground); color: var(--muted); }
257
- .chip.ok { background: var(--ok-ground); color: var(--primary-deep); }
258
- .chip.warn { background: var(--warn-ground); color: var(--ink); }
259
- .chip.err { background: var(--err-ground); color: var(--err); }
260
- .chip.new { background: var(--sky); color: var(--ink-deep); }
261
- /* an endpoint a model is reached through: outlined, so it reads as a place, not a state */
262
- .chip.via { background: transparent; border: 1px solid var(--line); color: var(--ink); font-family: var(--font-mono); }
263
- button.chip { font-family: inherit; border: 0; cursor: pointer; }
264
- button.chip:hover { outline: 1px solid currentColor; }
265
-
266
- /* helped bar: share of judged offloads that helped */
267
- .helped { display: inline-flex; align-items: center; gap: .5rem; }
268
- .meter { width: 4.5rem; height: .5rem; background: var(--band); border-radius: 999px; overflow: hidden; }
269
- .meter span { display: block; height: 100%; background: var(--brand); }
270
- .meter span.low { background: var(--err); }
271
-
272
- /* ---- dialogs -------------------------------------------------------- */
273
- dialog { border: 1px solid var(--ink); border-radius: var(--radius); padding: 1.25rem 1.4rem;
274
- width: min(40rem, calc(100vw - 2rem)); max-height: calc(100vh - 2rem);
275
- background: var(--paper-2); color: var(--ink); box-shadow: 6px 6px 0 var(--ink); }
276
- dialog::backdrop { background: color-mix(in oklab, var(--ink-deep) 45%, transparent); }
277
- dialog h2 { margin: 0 0 .2rem; font-size: 1.375rem; font-weight: 700; }
278
- :is(dialog, .form-panel) .served { color: var(--muted); font-size: .9375rem; margin: 0 0 .9rem; overflow-wrap: anywhere; }
279
- :is(dialog, .form-panel) label.field { display: block; font-weight: 500; font-size: .9375rem; margin: .9rem 0 .3rem; }
280
- :is(dialog, .form-panel) input:not([type=checkbox]):not([type=radio]), :is(dialog, .form-panel) select, :is(dialog, .form-panel) textarea {
281
- width: 100%; font: inherit; padding: .45rem .6rem; color: var(--ink);
282
- border: 1px solid var(--muted); border-radius: var(--radius); background: var(--paper-2); }
283
- :is(dialog, .form-panel) .rules { font-size: .875rem; color: var(--muted); margin: .35rem 0 0; }
284
- :is(dialog, .form-panel) label.check { display: flex; align-items: center; gap: .5rem; margin-top: 1rem; cursor: pointer; }
285
- :is(dialog, .form-panel) fieldset { border: 0; padding: 0; margin: 1rem 0 0; }
286
- :is(dialog, .form-panel) .members { display: grid; grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); gap: 0 1rem;
287
- max-height: 14rem; overflow: auto; }
288
- :is(dialog, .form-panel) .members label.check { margin-top: .35rem; font-size: .875rem; }
289
- :is(dialog, .form-panel) input.member-filter { font-size: .9375rem; padding: .4rem .6rem; margin-bottom: .2rem; }
290
- :is(dialog, .form-panel) .members input:disabled + span { color: var(--muted); }
291
- :is(dialog, .form-panel) .limits { display: flex; flex-wrap: wrap; gap: .3rem .8rem; margin-top: .3rem; font-size: .875rem; }
292
- :is(dialog, .form-panel) .limits input { width: 6.5rem !important; padding: .2rem .4rem !important; }
293
- :is(dialog, .form-panel) .facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 1rem; }
294
- :is(dialog, .form-panel) .facts .src { font-size: .75rem; color: var(--muted); }
295
- :is(dialog, .form-panel) .profile { margin-top: .9rem; display: grid; gap: .3rem; font-size: .875rem; }
296
- :is(dialog, .form-panel) .profile .chip { margin-right: .25rem; }
297
- :is(dialog, .form-panel) .profile .src { font-size: .75rem; color: var(--muted); }
298
- :is(dialog, .form-panel) .settings { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 1rem; }
299
- @media (max-width: 40rem) { :is(dialog, .form-panel) .settings { grid-template-columns: minmax(0, 1fr); } }
300
- :is(dialog, .form-panel) fieldset label.check { margin-top: .4rem; flex-wrap: wrap; }
301
- :is(dialog, .form-panel) fieldset label.check .muted { font-size: .875rem; }
302
- :is(dialog, .form-panel) .errors { color: var(--err); font-size: .9375rem; margin: .8rem 0 0; }
303
- :is(dialog, .form-panel) .done { color: var(--ok); font-size: .9375rem; margin: .8rem 0 0; }
304
- :is(dialog, .form-panel) .actions { display: flex; gap: .6rem; justify-content: flex-end; margin-top: 1.25rem; flex-wrap: wrap; }
305
- .form-panel .actions { justify-content: flex-start; } /* on a page, a form's button sits under its fields */
306
- :is(dialog, .form-panel) .note { background: var(--step-1); border-radius: var(--radius); padding: .6rem .75rem; font-size: .9375rem; margin-top: .75rem; }
307
- :is(dialog, .form-panel) details.remove { margin-top: 1.25rem; padding-top: .75rem; border-top: 1px solid var(--line); }
308
- :is(dialog, .form-panel) details.remove > summary { cursor: pointer; color: var(--err); }
309
-
310
- /* endpoints */
311
- .endpoint { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .2rem 1rem; align-items: center;
312
- padding: .5rem .75rem; margin: 0 -.75rem; border-radius: var(--radius); }
313
- .endpoint:hover { background: var(--step-1); }
314
- .endpoint .base { color: var(--muted); font-size: .875rem; overflow-wrap: anywhere; }
315
- :is(dialog, .form-panel) h3.form-head { margin: 1.25rem 0 0; font-size: 1rem; font-weight: 700; }
316
- :is(dialog, .form-panel) input[readonly] { background: var(--step-1); }
317
- /* Add an endpoint's choices: API key, then each provider; the chosen one pressed */
318
- .choices { display: flex; flex-wrap: wrap; gap: .5rem; margin: .2rem 0 .4rem; }
319
- .choices .btn[aria-pressed="true"] { border-color: var(--ink); box-shadow: 3px 3px 0 var(--brand); font-weight: 600; }
320
-
321
- /* onboarding picker */
322
- .picker { max-height: 22rem; overflow: auto; margin-top: .6rem; border-top: 1px solid var(--line); }
323
- .picker details.grp { margin: 0; color: inherit; }
324
- .picker .grp > summary { position: sticky; top: 0; z-index: 1; padding: .5rem 0 .3rem; font-weight: 700;
325
- background: var(--paper-2); color: var(--muted); }
326
- .pick { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: .6rem; align-items: center;
327
- padding: .35rem .25rem; border-radius: var(--radius); cursor: pointer; }
328
- .pick:hover { background: var(--step-1); }
329
- .pick:has(input:checked) { background: var(--warn-ground); }
330
- .pick .alias { overflow-wrap: anywhere; font-size: .9375rem; }
331
- .picker .empty { color: var(--muted); padding: 1rem 0; }
332
- .picker details { margin: .5rem 0; color: var(--muted); }
333
- .picker details summary { cursor: pointer; font-size: .875rem; }
334
-
335
- /* the onboarding queue: its progress in the header, its list in the popup */
336
- .oq { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: .3rem .75rem;
337
- font-size: .875rem; color: var(--ink); }
338
- /* Short enough to sit beside the button; the whole line is its title. */
339
- .oq #oq-text { max-width: min(17rem, 100%); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
340
- /* The status as a chip whose fill wipes across as models finish (bench.js
341
- sets --fill), ending green, or red with one shake. */
342
- .oq-chip { position: relative; isolation: isolate; overflow: hidden; display: inline-flex; align-items: center;
343
- max-width: 100%; min-height: 2.1rem; padding: 0 .8rem; border: 1px solid var(--line);
344
- border-radius: var(--radius); background: var(--paper-2); }
345
- .oq-chip::before { content: ""; position: absolute; inset: 0 auto 0 0; z-index: -1; width: var(--fill, 0%);
346
- background: color-mix(in oklab, var(--brand) 22%, var(--paper-2)); transition: width .6s ease; }
347
- .oq-chip.done { border-color: var(--ok); }
348
- .oq-chip.done::before { background: var(--ok-ground); }
349
- .oq-chip.fail { border-color: var(--err); color: var(--err); animation: oq-shake .45s ease 1; }
350
- .oq-chip.fail::before { background: var(--err-ground); }
351
- @keyframes oq-shake { 20% { transform: translateX(-5px); } 45% { transform: translateX(5px); }
352
- 70% { transform: translateX(-3px); } 90% { transform: translateX(2px); } }
353
- @media (prefers-reduced-motion: reduce) {
354
- .oq-chip { animation: none; }
355
- .oq-chip::before { transition: none; }
356
- }
357
- .oq [hidden] { display: none; }
358
- ul.oq-list { list-style: none; padding: 0; margin: .75rem 0 0; display: grid; gap: .5rem;
359
- max-height: 22rem; overflow: auto; }
360
- ul.oq-list li { padding: .4rem .25rem; border-bottom: 1px solid var(--line); }
361
- ul.oq-list .what { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
362
- ul.oq-list .alias { overflow-wrap: anywhere; font-size: .9375rem; }
363
- ul.oq-list small { display: block; color: var(--muted); font-size: .875rem; overflow-wrap: anywhere; }
364
-
365
- /* onboarding progress */
366
- ol.steps { list-style: none; padding: 0; margin: 1rem 0 0; display: grid; gap: .35rem; }
367
- ol.steps li { display: grid; grid-template-columns: 1.4rem minmax(0, 1fr); gap: .5rem; align-items: start; color: var(--muted); }
368
- ol.steps li::before { content: ""; width: .75rem; height: .75rem; margin-top: .4rem; border-radius: 999px;
369
- border: 2px solid var(--muted); }
370
- ol.steps li.now { color: var(--ink); font-weight: 500; }
371
- ol.steps li.now::before { border-color: var(--ink); background: var(--signal); }
372
- ol.steps li.done { color: var(--ink); }
373
- ol.steps li.done::before { border-color: var(--brand); background: var(--brand); }
374
- ol.steps li.fail::before { border-color: var(--err); background: var(--err); }
375
- ol.steps small { display: block; grid-column: 2; color: var(--muted); font-weight: 400; font-size: .875rem; overflow-wrap: anywhere; }
376
- pre.log { background: var(--log-ground, var(--ink-deep)); color: var(--log-ink, var(--paper));
377
- font-size: .8125rem; line-height: 1.45;
378
- padding: .7rem .8rem; border-radius: var(--radius); max-height: 16rem; overflow: auto;
379
- white-space: pre-wrap; margin: .5rem 0 0; }
380
- pre.log.answer { max-height: 40vh; }
381
-
382
- /* Benchmarks charts (Observable Plot) */
383
- .chart { margin: .5rem 0 .75rem; overflow-x: auto; }
384
- .chart.muted { font-size: .875rem; }
385
- h3.chart-head { font-size: 1rem; margin: 1rem 0 .2rem; }
386
- /* the Collections tab: the dialog's form, kept to a readable width */
387
- .panel.form-panel > :not(#c-list, table) { max-width: 46rem; }
388
- /* a list of things you open (collections), with its New button above */
389
- .list-actions { display: flex; align-items: center; gap: 1rem; margin: .75rem 0; flex-wrap: wrap; }
390
- .list-actions .done { margin: 0; color: var(--ok); font-size: .9375rem; }
391
- .list-actions .errors { color: var(--err); font-size: .9375rem; }
392
- button.c-row { width: 100%; text-align: left; font: inherit; color: inherit; background: none; border: 0;
393
- border-radius: var(--radius); cursor: pointer; }
394
- button.c-row:hover, button.c-row:focus-visible { background: var(--step-1); }
395
- .c-row .open-hint { color: var(--muted); font-size: .875rem; white-space: nowrap; }
396
- /* suggested collection tags under the field */
397
- .tag-picks { display: flex; flex-wrap: wrap; gap: .35rem; margin-top: .45rem; }
398
- .tag-picks .chip { cursor: pointer; border: 0; font-family: inherit; }
399
- .tag-picks .chip:hover { outline: 1px solid currentColor; }
400
- /* a field with fixed text in front (the collection name after "bench/") */
401
- .prefixed { display: flex; align-items: stretch; }
402
- .prefixed .prefix { display: flex; align-items: center; padding: 0 .55rem; font-family: var(--font-mono);
403
- font-size: .9375rem; color: var(--muted); background: var(--step-1);
404
- border: 1px solid var(--line); border-right: 0; border-radius: var(--radius) 0 0 var(--radius); }
405
- .prefixed input { flex: 1; min-width: 0; border-top-left-radius: 0; border-bottom-left-radius: 0; }
406
- .c-settings, .limits-block { margin-top: 1.25rem; }
407
- .c-settings > summary, .limits-block > summary { cursor: pointer; }
408
- .actions .spacer { flex: 1; }
409
- .chart figure { margin: 0; }
410
- .chart [aria-label="color-legend"], .chart div[class$="-swatches"] { font-size: .8125rem; margin-bottom: .4rem; }
411
-
412
- dialog .pick { display: flex; gap: 1.5rem; }
413
-
414
- @media (max-width: 40rem) {
415
- header h1 { font-size: 1.75rem; }
416
- .problem { grid-template-columns: minmax(0, 1fr); }
417
- .problem .acts { justify-content: flex-start; }
418
- .ep-head { grid-template-columns: minmax(0, 1fr); }
419
- .ep .acts { justify-content: flex-start; }
420
- details.key dl { grid-template-columns: minmax(0, 1fr); }
421
- .toolbar input[type=search] { min-width: 0; width: 100%; }
422
- }
423
- @media (prefers-reduced-motion: reduce) {
424
- * { transition: none !important; }
425
- }
1
+ /* Benchwarmers. The theme's colour tokens arrive first (ui/themes/*.css, the
2
+ Wendell Research palette by default, emitted by serve.py); the values below
3
+ are derived from them: grounds mixed from a theme colour, and the bench's
4
+ own spacing. A theme sets colours only. */
5
+
6
+ :root {
7
+ --ok: var(--primary);
8
+ --ok-ground: color-mix(in oklab, var(--primary) 13%, var(--paper-2));
9
+ --warn-ground: color-mix(in oklab, var(--signal) 55%, var(--paper-2));
10
+ --err-ground: color-mix(in oklab, var(--err) 11%, var(--paper-2));
11
+ --quiet-ground: var(--band);
12
+ --panel: var(--paper-2);
13
+ --step-1: color-mix(in oklab, var(--band) 55%, var(--paper));
14
+ color-scheme: var(--scheme, light);
15
+ }
16
+
17
+ * { box-sizing: border-box; }
18
+ [hidden] { display: none !important; }
19
+ html { -webkit-text-size-adjust: 100%; }
20
+ body {
21
+ margin: 0;
22
+ /* A theme may draw the page's ground (Hardwood's maple court) and a line
23
+ across the top (its crimson court line); the others are plain paper. */
24
+ background: var(--floor, var(--paper));
25
+ border-top: .4rem solid var(--court-line, transparent);
26
+ color: var(--ink);
27
+ font: 400 1rem/1.55 var(--font-display);
28
+ -webkit-font-smoothing: antialiased;
29
+ }
30
+ ::selection { background: var(--signal); color: var(--ink); }
31
+ a { color: var(--link); text-underline-offset: .18em; }
32
+ :focus-visible { outline: 2px solid var(--link); outline-offset: 3px; border-radius: 2px; }
33
+ code, .mono, td.num, .chip, .alias {
34
+ font-family: var(--font-mono);
35
+ font-variant-numeric: tabular-nums;
36
+ }
37
+ code { font-size: .875em; background: var(--step-1); padding: .05rem .3rem; border-radius: var(--radius); }
38
+ h1, h2, h3 { line-height: 1.1; letter-spacing: -.02em; text-wrap: balance; }
39
+
40
+ /* ---- header --------------------------------------------------------- */
41
+ /* The page is wider than the theme's reading measure: with a sidebar, a
42
+ 76rem page left a narrow column and wide empty margins on a large screen.
43
+ Prose inside panels keeps its own measure (.panel > .sub). */
44
+ /* One line: the name on its bench, the credit beside it, the actions pushed
45
+ to the far end. */
46
+ header {
47
+ max-width: var(--page, 104rem); margin: 0 auto;
48
+ padding: 1.5rem var(--gutter) 1rem;
49
+ display: flex; flex-wrap: wrap; align-items: center; column-gap: 1.25rem;
50
+ }
51
+ header h1 { margin: 0; font-size: 2.25rem; font-weight: 800; letter-spacing: -.04em; line-height: 1; }
52
+ /* The name sits on a team bench: a seat in the team's colour (the theme's
53
+ brand, or --bench-seat) on two legs joined by a rail. Drawn in CSS from the
54
+ font size, so it scales with the heading. With line-height 1 the letters
55
+ end about .14em above the box, so the seat's top is put there: the name
56
+ rests on it. */
57
+ header h1 span { position: relative; display: inline-block; padding: 0 .14em .6em; }
58
+ header h1 span::after { /* the seat */
59
+ content: ""; position: absolute; left: -.08em; right: -.08em; bottom: .46em; height: .28em;
60
+ background: var(--bench-seat, var(--brand)); border-radius: .06em;
61
+ box-shadow: inset 0 .055em 0 rgb(255 255 255 / .28), 0 .05em 0 rgb(0 0 0 / .3);
62
+ }
63
+ header h1 span::before { /* legs and the rail between them */
64
+ --leg: var(--bench-leg, var(--ink-2));
65
+ content: ""; position: absolute; left: .18em; right: .18em; bottom: 0; height: .5em;
66
+ background:
67
+ linear-gradient(var(--leg), var(--leg)) left top / .11em 100% no-repeat,
68
+ linear-gradient(var(--leg), var(--leg)) right top / .11em 100% no-repeat,
69
+ linear-gradient(var(--leg), var(--leg)) center 66% / 100% .07em no-repeat;
70
+ }
71
+ /* The credit at the bench's feet: level with the bottom of the name's group. */
72
+ header .sub { align-self: flex-end; margin: 0 auto 0 -.6rem; line-height: 1; color: var(--muted); font-size: .9375rem; }
73
+ header .sub a { color: var(--ink); font-weight: 500; text-decoration-color: var(--brand); }
74
+ /* Right-aligned, on the name's line or, when it does not fit, on its own. */
75
+ .head-actions { display: flex; flex-wrap: wrap; gap: .75rem; align-items: center;
76
+ margin-left: auto; justify-content: flex-end; }
77
+
78
+ /* ---- buttons -------------------------------------------------------- */
79
+ button { font: inherit; cursor: pointer; }
80
+ .btn {
81
+ display: inline-flex; align-items: center; gap: .4rem;
82
+ padding: .5rem .9rem; border-radius: var(--radius);
83
+ border: 1px solid var(--ink); background: var(--panel); color: var(--ink);
84
+ font-weight: 500; font-size: .9375rem;
85
+ transition: transform .15s ease, box-shadow .15s ease, background-color .15s ease;
86
+ }
87
+ .btn:hover { background: var(--step-1); }
88
+ .btn.primary { background: var(--ink); color: var(--paper); box-shadow: 4px 4px 0 var(--brand); }
89
+ .btn.primary:hover { transform: translate(-2px, -2px); box-shadow: 6px 6px 0 var(--brand); background: var(--ink); }
90
+ .btn.danger { background: var(--err); border-color: var(--err); color: var(--on-err, #fff); }
91
+ .btn.danger:hover { background: color-mix(in oklab, var(--err) 86%, #000); }
92
+ .btn.small { padding: .25rem .6rem; font-size: .875rem; }
93
+ .btn:disabled { opacity: .5; cursor: default; transform: none; box-shadow: none; }
94
+ .btn.fresh { background: var(--signal); border-color: var(--ink); }
95
+ .btn.fresh:hover { background: var(--signal); transform: translate(-1px, -1px); box-shadow: 3px 3px 0 var(--ink); }
96
+ .linkish { background: none; border: 0; padding: 0; color: var(--link); text-decoration: underline;
97
+ text-underline-offset: .18em; font-size: inherit; }
98
+
99
+ /* ---- layout --------------------------------------------------------- */
100
+ main { max-width: var(--page, 104rem); margin: 0 auto; padding: 0 var(--gutter) 4rem; }
101
+
102
+ /* problems: what needs a person, each with the action that resolves it */
103
+ .problems { background: var(--sheet, var(--panel)); border-radius: 14px; padding: 1rem 1.4rem; margin: .5rem 0 1.5rem;
104
+ box-shadow: 0 1px 2px rgb(0 0 0 / .04), 0 12px 32px -14px rgb(0 0 0 / .14);
105
+ -webkit-backdrop-filter: var(--glass, none); backdrop-filter: var(--glass, none); }
106
+ .problems h2 { margin: 0 0 .25rem; font-size: 1.25rem; font-weight: 700; }
107
+ .problems > p { margin: 0 0 .5rem; color: var(--muted); font-size: .9375rem; }
108
+ .problem { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .25rem 1rem;
109
+ align-items: center; padding: .55rem .75rem; margin: 0 -.75rem; border-radius: var(--radius); }
110
+ .problem:hover { background: var(--step-1); }
111
+ .problems details { margin-top: .5rem; }
112
+ .problems details > summary { cursor: pointer; font-weight: 500; padding: .4rem 0; }
113
+ .problems details > summary .muted { font-weight: 400; }
114
+ .problem .what { font-weight: 500; }
115
+ .problem .why { color: var(--muted); font-size: .875rem; overflow-wrap: anywhere; }
116
+ .problem .acts, .endpoint .acts, .ep .acts { display: flex; gap: .5rem; flex-wrap: wrap; justify-content: flex-end; }
117
+ .row-input { display: flex; gap: .5rem; align-items: center; margin-top: .35rem; }
118
+ .row-input input { flex: 1; min-width: 0; margin: 0 !important; }
119
+ .sub-head { font-weight: 600; margin: 1rem 0 .25rem; }
120
+ .problem .chip { margin-right: .4rem; }
121
+ .problems .head { display: flex; align-items: baseline; gap: .5rem; }
122
+ .problems .head h2 { margin-bottom: .25rem; }
123
+ .problems .sep { display: none; color: var(--muted); }
124
+ /* Hidden (bench.js): one line, "Action needed · 3 · show", until something new */
125
+ .problems.collapsed { padding: .5rem 1.25rem; }
126
+ .problems.collapsed > :not(.head) { display: none; }
127
+ .problems.collapsed .head h2 { margin: 0; font-size: 1rem; }
128
+ .problems.collapsed .sep { display: inline; }
129
+
130
+ /* pages: a grouped sidebar (Observe, Gateway, Control); the group names and
131
+ the active page carry the theme's brand colour. A theme with a floor backs
132
+ the sidebar with frosted cream (--rail) so it reads over the court. On a
133
+ narrow screen the groups sit in a row above the page. */
134
+ .layout { display: grid; grid-template-columns: 12rem minmax(0, 1fr); gap: 1.5rem; align-items: start; }
135
+ .tabs { position: sticky; top: 1rem; display: flex; flex-direction: column; gap: 1.1rem;
136
+ background: var(--rail, transparent); padding: var(--rail-pad, 0); border-radius: 14px;
137
+ -webkit-backdrop-filter: var(--glass, none); backdrop-filter: var(--glass, none); }
138
+ .tabs .group { display: flex; flex-direction: column; align-items: flex-start; gap: .55rem; }
139
+ .tabs h2 { margin: 0 0 .2rem; font-size: .75rem; font-weight: 700; letter-spacing: .08em;
140
+ text-transform: uppercase; color: color-mix(in oklab, var(--brand) 72%, var(--ink)); }
141
+ /* the brand, pulled toward the ink so small text stays legible in any theme */
142
+ /* No bottom padding and a tight line height, so the box ends just under the
143
+ letters; the spacing between pages is the group's gap instead. */
144
+ .tabs button { background: none; border: 0; padding: 0; line-height: 1.15; color: var(--muted); text-align: left;
145
+ font-size: 1rem; font-weight: 500; }
146
+ .tabs button:hover { color: var(--ink); }
147
+ /* The active page: a highlighter stroke across the bottom of its letters,
148
+ the header's old mark. */
149
+ .tabs button[aria-selected="true"] { color: var(--ink); font-weight: 700;
150
+ box-shadow: inset 0 -.42em 0 var(--signal); }
151
+ /* Collections, the bench's own idea: a small brand sparkle after it. It
152
+ hangs outside the button, so the active page's stroke still runs under
153
+ the word alone. */
154
+ .tabs button[data-tab="collections"] { position: relative; }
155
+ .tabs button[data-tab="collections"]::after {
156
+ content: ""; position: absolute; left: calc(100% + .3em); top: -.05em; width: .75em; height: .75em;
157
+ background: var(--brand);
158
+ clip-path: polygon(50% 0, 62% 38%, 100% 50%, 62% 62%, 50% 100%, 38% 62%, 0 50%, 38% 38%); }
159
+ .tab { display: none; } .tab.active { display: block; }
160
+ /* One surface for the page's content: its panels are sections of it,
161
+ divided by a hairline, rather than separate boxes with gaps between. A
162
+ theme with a floor makes it frosted (--sheet, --glass). */
163
+ .panes { background: var(--sheet, var(--panel)); border-radius: 14px;
164
+ box-shadow: 0 1px 2px rgb(0 0 0 / .04), 0 12px 32px -14px rgb(0 0 0 / .14);
165
+ -webkit-backdrop-filter: var(--glass, none); backdrop-filter: var(--glass, none); }
166
+ .panes .panel { background: transparent; border-radius: 0; padding: 1.4rem 1.6rem 1.5rem; }
167
+ .panes .tab > .panel + .panel { border-top: 1px solid var(--line); }
168
+ @media (max-width: 52rem) {
169
+ .layout { grid-template-columns: minmax(0, 1fr); gap: 1rem; }
170
+ .tabs { position: static; flex-direction: row; flex-wrap: wrap; gap: .5rem 1.5rem; }
171
+ .tabs .group { flex-direction: row; flex-wrap: wrap; align-items: baseline; gap: .1rem 1rem; }
172
+ .tabs h2 { margin: 0; }
173
+ }
174
+
175
+ /* settings, connections, logs, info */
176
+ .themes { display: flex; flex-wrap: wrap; gap: .75rem; margin-top: .75rem; }
177
+ td.set-by { font-size: .8125rem; color: var(--muted); white-space: nowrap; }
178
+ .theme-card { display: grid; grid-template-columns: auto 1fr; align-items: center; gap: .35rem .5rem;
179
+ padding: .6rem .8rem; border: 1px solid var(--line); border-radius: var(--radius); cursor: pointer; }
180
+ .theme-card:has(input:checked) { border-color: var(--ink); box-shadow: 3px 3px 0 var(--brand); }
181
+ .theme-card .swatch, .theme-card a { grid-column: 2; }
182
+ .swatch { display: flex; gap: 2px; }
183
+ .swatch span { width: 1rem; height: 1rem; border-radius: 2px; border: 1px solid var(--line); }
184
+ .theme-card a { font-size: .8125rem; }
185
+ table.facts th { text-align: left; padding-right: 1.5rem; color: var(--muted); font-weight: 500; }
186
+ details.panel > summary { cursor: pointer; }
187
+ details.panel[open] > summary { margin-bottom: .75rem; }
188
+ .log-line { font-size: .8125rem; padding: .1rem 0; overflow-wrap: anywhere; }
189
+
190
+ .panel { background: var(--panel); border-radius: var(--radius); padding: 1.1rem 1.25rem 1.25rem; }
191
+ .panel > h2 { margin: 0; font-size: 1.375rem; font-weight: 700; }
192
+ /* A section's heading carries a short bar of the brand colour: the accent a
193
+ mostly-white theme needs, and a marker where sections meet on one surface. */
194
+ .panes .panel > h2::before { content: ""; display: inline-block; width: .22em; height: .8em; margin-right: .45em;
195
+ vertical-align: -.05em; border-radius: 2px; background: var(--brand); }
196
+ .panel > .sub { margin: .3rem 0 1rem; color: var(--muted); font-size: .9375rem; max-width: 70ch; }
197
+ .toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem 1.25rem; margin: 0 0 .75rem; }
198
+ .toolbar input[type=search] { font: inherit; font-size: .9375rem; padding: .4rem .6rem; min-width: 14rem;
199
+ border: 1px solid var(--line); border-radius: var(--radius); background: var(--paper); color: var(--ink); }
200
+ .toolbar label { font-size: .9375rem; display: inline-flex; align-items: center; gap: .4rem; cursor: pointer; }
201
+ .toolbar select { font: inherit; font-size: .9375rem; padding: .35rem .4rem; border: 1px solid var(--line);
202
+ border-radius: var(--radius); background: var(--paper); color: var(--ink); }
203
+ tr.pick-row { cursor: pointer; }
204
+ a.dmodel { color: var(--ink); text-decoration: none; font-weight: 500; }
205
+ a.dmodel:hover .alias { box-shadow: inset 0 -.3em 0 var(--signal); }
206
+ /* Connections: each endpoint, its models folded under it */
207
+ .ep { padding: .75rem 0; border-top: 1px solid var(--line); }
208
+ .ep-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .4rem 1rem; align-items: center; }
209
+ .ep .base { color: var(--muted); font-size: .875rem; overflow-wrap: anywhere; }
210
+ .ep details.models > summary { cursor: pointer; color: var(--link); font-size: .9375rem; margin-top: .3rem; }
211
+ .ep details.models[open] > summary { margin-bottom: .5rem; }
212
+ .hint { color: var(--muted); font-size: .875rem; margin: .9rem 0 0; max-width: 75ch; }
213
+
214
+ /* key: folded until asked for */
215
+ details.key { margin: 1rem 0 0; }
216
+ details.key > summary { cursor: pointer; color: var(--link); font-size: .9375rem; width: max-content; }
217
+ details.key dl { margin: .5rem 0 0; display: grid; grid-template-columns: max-content minmax(0, 60ch);
218
+ gap: .35rem 1rem; font-size: .9375rem; }
219
+ details.key dt { font-weight: 700; }
220
+ details.key dd { margin: 0; color: var(--ink); }
221
+
222
+ /* ---- tables --------------------------------------------------------- */
223
+ .scroll { overflow-x: auto; }
224
+ table { border-collapse: collapse; width: 100%; font-size: .9375rem; }
225
+ th, td { text-align: left; padding: .5rem .6rem; border-bottom: 1px solid var(--line); vertical-align: middle; }
226
+ th { font-size: .875rem; font-weight: 700; color: var(--ink); white-space: nowrap; }
227
+ th[data-sortable] { cursor: pointer; user-select: none; }
228
+ th[data-sortable]::after { content: ""; display: inline-block; width: .9em; color: var(--muted); }
229
+ th[aria-sort=ascending]::after { content: " \2191"; }
230
+ th[aria-sort=descending]::after { content: " \2193"; }
231
+ td.num, th.num { text-align: right; }
232
+ /* the per-connection model cards on Connections: one fixed column plan */
233
+ table.model-cards { table-layout: fixed; min-width: 52rem; }
234
+ .panel:has(> table.model-cards) { overflow-x: auto; } /* scrolls on a narrow window instead of wrapping numbers */
235
+ table.model-cards td.num { white-space: nowrap; }
236
+ table.model-cards col.c-model { width: 24%; }
237
+ table.model-cards col.c-via { width: 13%; }
238
+ table.model-cards col.c-n { width: 10%; }
239
+ table.model-cards col.c-input { width: 19%; }
240
+ table.model-cards col.c-lim { width: 6%; }
241
+ table.model-cards td { overflow-wrap: anywhere; }
242
+ tbody tr:hover > td { background: var(--step-1); }
243
+ tr.retired > td { color: var(--muted); }
244
+ tr.retired .alias { text-decoration: line-through; }
245
+ body:not(.show-retired) tr.retired { display: none; }
246
+ body:not(.show-paid) tr.paid { display: none; }
247
+ .muted { color: var(--muted); }
248
+ .ok { color: var(--ok); } .warn { color: var(--warn); } .err { color: var(--err); }
249
+
250
+ a.model { color: var(--ink); text-decoration: none; font-weight: 500; }
251
+ a.model:hover .alias { box-shadow: inset 0 -.3em 0 var(--signal); }
252
+ .conn { display: block; color: var(--muted); font-size: .8125rem; }
253
+
254
+ /* status chips */
255
+ .chip { display: inline-block; padding: .1rem .5rem; border-radius: 999px; font-size: .8125rem;
256
+ font-weight: 500; white-space: nowrap; background: var(--quiet-ground); color: var(--muted); }
257
+ .chip.ok { background: var(--ok-ground); color: var(--primary-deep); }
258
+ .chip.warn { background: var(--warn-ground); color: var(--ink); }
259
+ .chip.err { background: var(--err-ground); color: var(--err); }
260
+ .chip.new { background: var(--sky); color: var(--ink-deep); }
261
+ /* an endpoint a model is reached through: outlined, so it reads as a place, not a state */
262
+ .chip.via { background: transparent; border: 1px solid var(--line); color: var(--ink); font-family: var(--font-mono); }
263
+ button.chip { font-family: inherit; border: 0; cursor: pointer; }
264
+ button.chip:hover { outline: 1px solid currentColor; }
265
+
266
+ /* helped bar: share of judged offloads that helped */
267
+ .helped { display: inline-flex; align-items: center; gap: .5rem; }
268
+ .meter { width: 4.5rem; height: .5rem; background: var(--band); border-radius: 999px; overflow: hidden; }
269
+ .meter span { display: block; height: 100%; background: var(--brand); }
270
+ .meter span.low { background: var(--err); }
271
+
272
+ /* ---- dialogs -------------------------------------------------------- */
273
+ dialog { border: 1px solid var(--ink); border-radius: var(--radius); padding: 1.25rem 1.4rem;
274
+ width: min(40rem, calc(100vw - 2rem)); max-height: calc(100vh - 2rem);
275
+ background: var(--paper-2); color: var(--ink); box-shadow: 6px 6px 0 var(--ink); }
276
+ dialog::backdrop { background: color-mix(in oklab, var(--ink-deep) 45%, transparent); }
277
+ dialog h2 { margin: 0 0 .2rem; font-size: 1.375rem; font-weight: 700; }
278
+ :is(dialog, .form-panel) .served { color: var(--muted); font-size: .9375rem; margin: 0 0 .9rem; overflow-wrap: anywhere; }
279
+ :is(dialog, .form-panel) label.field { display: block; font-weight: 500; font-size: .9375rem; margin: .9rem 0 .3rem; }
280
+ :is(dialog, .form-panel) input:not([type=checkbox]):not([type=radio]), :is(dialog, .form-panel) select, :is(dialog, .form-panel) textarea {
281
+ width: 100%; font: inherit; padding: .45rem .6rem; color: var(--ink);
282
+ border: 1px solid var(--muted); border-radius: var(--radius); background: var(--paper-2); }
283
+ :is(dialog, .form-panel) .rules { font-size: .875rem; color: var(--muted); margin: .35rem 0 0; }
284
+ :is(dialog, .form-panel) label.check { display: flex; align-items: center; gap: .5rem; margin-top: 1rem; cursor: pointer; }
285
+ :is(dialog, .form-panel) fieldset { border: 0; padding: 0; margin: 1rem 0 0; }
286
+ :is(dialog, .form-panel) .members { display: grid; grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); gap: 0 1rem;
287
+ max-height: 14rem; overflow: auto; }
288
+ :is(dialog, .form-panel) .members label.check { margin-top: .35rem; font-size: .875rem; }
289
+ :is(dialog, .form-panel) input.member-filter { font-size: .9375rem; padding: .4rem .6rem; margin-bottom: .2rem; }
290
+ :is(dialog, .form-panel) .members input:disabled + span { color: var(--muted); }
291
+ /* Ruled out by the collection's tags (bench.js applyTags): greyed; a ticked
292
+ one stays tickable and is marked, so it can be taken off. */
293
+ :is(dialog, .form-panel) .members label.check.blocked:not(.clash) { cursor: not-allowed; }
294
+ :is(dialog, .form-panel) .members label.check.clash span { color: var(--warn); }
295
+ :is(dialog, .form-panel) .limits { display: flex; flex-wrap: wrap; gap: .3rem .8rem; margin-top: .3rem; font-size: .875rem; }
296
+ :is(dialog, .form-panel) .limits input { width: 6.5rem !important; padding: .2rem .4rem !important; }
297
+ :is(dialog, .form-panel) .facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 1rem; }
298
+ :is(dialog, .form-panel) .facts .src { font-size: .75rem; color: var(--muted); }
299
+ :is(dialog, .form-panel) .profile { margin-top: .9rem; display: grid; gap: .3rem; font-size: .875rem; }
300
+ :is(dialog, .form-panel) .profile .chip { margin-right: .25rem; }
301
+ :is(dialog, .form-panel) .profile .src { font-size: .75rem; color: var(--muted); }
302
+ :is(dialog, .form-panel) .settings { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 1rem; }
303
+ @media (max-width: 40rem) { :is(dialog, .form-panel) .settings { grid-template-columns: minmax(0, 1fr); } }
304
+ :is(dialog, .form-panel) fieldset label.check { margin-top: .4rem; flex-wrap: wrap; }
305
+ :is(dialog, .form-panel) fieldset label.check .muted { font-size: .875rem; }
306
+ :is(dialog, .form-panel) .errors { color: var(--err); font-size: .9375rem; margin: .8rem 0 0; }
307
+ :is(dialog, .form-panel) .done { color: var(--ok); font-size: .9375rem; margin: .8rem 0 0; }
308
+ :is(dialog, .form-panel) .actions { display: flex; gap: .6rem; justify-content: flex-end; margin-top: 1.25rem; flex-wrap: wrap; }
309
+ .form-panel .actions { justify-content: flex-start; } /* on a page, a form's button sits under its fields */
310
+ :is(dialog, .form-panel) .note { background: var(--step-1); border-radius: var(--radius); padding: .6rem .75rem; font-size: .9375rem; margin-top: .75rem; }
311
+ :is(dialog, .form-panel) details.remove { margin-top: 1.25rem; padding-top: .75rem; border-top: 1px solid var(--line); }
312
+ :is(dialog, .form-panel) details.remove > summary { cursor: pointer; color: var(--err); }
313
+
314
+ /* endpoints */
315
+ .endpoint { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .2rem 1rem; align-items: center;
316
+ padding: .5rem .75rem; margin: 0 -.75rem; border-radius: var(--radius); }
317
+ .endpoint:hover { background: var(--step-1); }
318
+ .endpoint .base { color: var(--muted); font-size: .875rem; overflow-wrap: anywhere; }
319
+ :is(dialog, .form-panel) h3.form-head { margin: 1.25rem 0 0; font-size: 1rem; font-weight: 700; }
320
+ :is(dialog, .form-panel) input[readonly] { background: var(--step-1); }
321
+ /* Add an endpoint's choices: API key, then each provider; the chosen one pressed */
322
+ .choices { display: flex; flex-wrap: wrap; gap: .5rem; margin: .2rem 0 .4rem; }
323
+ .choices .btn[aria-pressed="true"] { border-color: var(--ink); box-shadow: 3px 3px 0 var(--brand); font-weight: 600; }
324
+
325
+ /* onboarding picker */
326
+ .picker { max-height: 22rem; overflow: auto; margin-top: .6rem; border-top: 1px solid var(--line); }
327
+ .picker details.grp { margin: 0; color: inherit; }
328
+ .picker .grp > summary { position: sticky; top: 0; z-index: 1; padding: .5rem 0 .3rem; font-weight: 700;
329
+ background: var(--paper-2); color: var(--muted); }
330
+ .pick { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: .6rem; align-items: center;
331
+ padding: .35rem .25rem; border-radius: var(--radius); cursor: pointer; }
332
+ .pick:hover { background: var(--step-1); }
333
+ .pick:has(input:checked) { background: var(--warn-ground); }
334
+ .pick .alias { overflow-wrap: anywhere; font-size: .9375rem; }
335
+ .pick .chip.mod { margin-left: .4rem; font-size: .75rem; vertical-align: .1em; }
336
+ .picker .empty { color: var(--muted); padding: 1rem 0; }
337
+ .picker details { margin: .5rem 0; color: var(--muted); }
338
+ .picker details summary { cursor: pointer; font-size: .875rem; }
339
+
340
+ /* the onboarding queue: its progress in the header, its list in the popup */
341
+ .oq { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: .3rem .75rem;
342
+ font-size: .875rem; color: var(--ink); }
343
+ /* Short enough to sit beside the button; the whole line is its title. */
344
+ .oq #oq-text { max-width: min(17rem, 100%); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
345
+ /* The status as a chip whose fill wipes across as models finish (bench.js
346
+ sets --fill), ending green, or red with one shake. */
347
+ .oq-chip { position: relative; isolation: isolate; overflow: hidden; display: inline-flex; align-items: center;
348
+ max-width: 100%; min-height: 2.1rem; padding: 0 .8rem; border: 1px solid var(--line);
349
+ border-radius: var(--radius); background: var(--paper-2); }
350
+ .oq-chip::before { content: ""; position: absolute; inset: 0 auto 0 0; z-index: -1; width: var(--fill, 0%);
351
+ background: color-mix(in oklab, var(--brand) 22%, var(--paper-2)); transition: width .6s ease; }
352
+ .oq-chip.done { border-color: var(--ok); }
353
+ .oq-chip.done::before { background: var(--ok-ground); }
354
+ .oq-chip.fail { border-color: var(--err); color: var(--err); animation: oq-shake .45s ease 1; }
355
+ .oq-chip.fail::before { background: var(--err-ground); }
356
+ @keyframes oq-shake { 20% { transform: translateX(-5px); } 45% { transform: translateX(5px); }
357
+ 70% { transform: translateX(-3px); } 90% { transform: translateX(2px); } }
358
+ @media (prefers-reduced-motion: reduce) {
359
+ .oq-chip { animation: none; }
360
+ .oq-chip::before { transition: none; }
361
+ }
362
+ .oq [hidden] { display: none; }
363
+ ul.oq-list { list-style: none; padding: 0; margin: .75rem 0 0; display: grid; gap: .5rem;
364
+ max-height: 22rem; overflow: auto; }
365
+ ul.oq-list li { padding: .4rem .25rem; border-bottom: 1px solid var(--line); }
366
+ ul.oq-list .what { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
367
+ ul.oq-list .alias { overflow-wrap: anywhere; font-size: .9375rem; }
368
+ ul.oq-list small { display: block; color: var(--muted); font-size: .875rem; overflow-wrap: anywhere; }
369
+
370
+ /* onboarding progress */
371
+ ol.steps { list-style: none; padding: 0; margin: 1rem 0 0; display: grid; gap: .35rem; }
372
+ ol.steps li { display: grid; grid-template-columns: 1.4rem minmax(0, 1fr); gap: .5rem; align-items: start; color: var(--muted); }
373
+ ol.steps li::before { content: ""; width: .75rem; height: .75rem; margin-top: .4rem; border-radius: 999px;
374
+ border: 2px solid var(--muted); }
375
+ ol.steps li.now { color: var(--ink); font-weight: 500; }
376
+ ol.steps li.now::before { border-color: var(--ink); background: var(--signal); }
377
+ ol.steps li.done { color: var(--ink); }
378
+ ol.steps li.done::before { border-color: var(--brand); background: var(--brand); }
379
+ ol.steps li.fail::before { border-color: var(--err); background: var(--err); }
380
+ ol.steps small { display: block; grid-column: 2; color: var(--muted); font-weight: 400; font-size: .875rem; overflow-wrap: anywhere; }
381
+ pre.log { background: var(--log-ground, var(--ink-deep)); color: var(--log-ink, var(--paper));
382
+ font-size: .8125rem; line-height: 1.45;
383
+ padding: .7rem .8rem; border-radius: var(--radius); max-height: 16rem; overflow: auto;
384
+ white-space: pre-wrap; margin: .5rem 0 0; }
385
+ pre.log.answer { max-height: 40vh; }
386
+
387
+ /* Benchmarks charts (Observable Plot) */
388
+ .chart { margin: .5rem 0 .75rem; overflow-x: auto; }
389
+ .chart.muted { font-size: .875rem; }
390
+ h3.chart-head { font-size: 1rem; margin: 1rem 0 .2rem; }
391
+ /* the Collections tab: the dialog's form, kept to a readable width */
392
+ .panel.form-panel > :not(#c-list, table) { max-width: 46rem; }
393
+ /* a list of things you open (collections), with its New button above */
394
+ .list-actions { display: flex; align-items: center; gap: 1rem; margin: .75rem 0; flex-wrap: wrap; }
395
+ .list-actions .done { margin: 0; color: var(--ok); font-size: .9375rem; }
396
+ .list-actions .errors { color: var(--err); font-size: .9375rem; }
397
+ button.c-row { width: 100%; text-align: left; font: inherit; color: inherit; background: none; border: 0;
398
+ border-radius: var(--radius); cursor: pointer; }
399
+ button.c-row:hover, button.c-row:focus-visible { background: var(--step-1); }
400
+ .c-row .open-hint { color: var(--muted); font-size: .875rem; white-space: nowrap; }
401
+ /* suggested collection tags under the field */
402
+ .tag-picks { display: flex; flex-wrap: wrap; gap: .35rem; margin-top: .45rem; }
403
+ .tag-picks .chip { cursor: pointer; border: 0; font-family: inherit; }
404
+ .tag-picks .chip:hover { outline: 1px solid currentColor; }
405
+ /* a field with fixed text in front (the collection name after "bench/") */
406
+ .prefixed { display: flex; align-items: stretch; }
407
+ .prefixed .prefix { display: flex; align-items: center; padding: 0 .55rem; font-family: var(--font-mono);
408
+ font-size: .9375rem; color: var(--muted); background: var(--step-1);
409
+ border: 1px solid var(--line); border-right: 0; border-radius: var(--radius) 0 0 var(--radius); }
410
+ .prefixed input { flex: 1; min-width: 0; border-top-left-radius: 0; border-bottom-left-radius: 0; }
411
+ .c-settings, .limits-block { margin-top: 1.25rem; }
412
+ .c-settings > summary, .limits-block > summary { cursor: pointer; }
413
+ .actions .spacer { flex: 1; }
414
+ .chart figure { margin: 0; }
415
+ .chart [aria-label="color-legend"], .chart div[class$="-swatches"] { font-size: .8125rem; margin-bottom: .4rem; }
416
+
417
+ dialog .pick { display: flex; gap: 1.5rem; }
418
+
419
+ @media (max-width: 40rem) {
420
+ header h1 { font-size: 1.75rem; }
421
+ .problem { grid-template-columns: minmax(0, 1fr); }
422
+ .problem .acts { justify-content: flex-start; }
423
+ .ep-head { grid-template-columns: minmax(0, 1fr); }
424
+ .ep .acts { justify-content: flex-start; }
425
+ details.key dl { grid-template-columns: minmax(0, 1fr); }
426
+ .toolbar input[type=search] { min-width: 0; width: 100%; }
427
+ }
428
+ @media (prefers-reduced-motion: reduce) {
429
+ * { transition: none !important; }
430
+ }