cogworker 0.1.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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/lib/cogworker/attempts.rb +47 -0
  3. data/lib/cogworker/basic_fetch.rb +25 -2
  4. data/lib/cogworker/heartbeat.rb +4 -2
  5. data/lib/cogworker/history/storage.rb +58 -22
  6. data/lib/cogworker/history.rb +57 -7
  7. data/lib/cogworker/manager.rb +4 -0
  8. data/lib/cogworker/periodic/ticker.rb +16 -1
  9. data/lib/cogworker/process.rb +13 -5
  10. data/lib/cogworker/processor.rb +4 -0
  11. data/lib/cogworker/queue.rb +17 -0
  12. data/lib/cogworker/redis_keys.rb +5 -0
  13. data/lib/cogworker/throughput.rb +51 -0
  14. data/lib/cogworker/version.rb +1 -1
  15. data/lib/cogworker/web/assets/nocturne/fonts/inter-latin.woff2 +0 -0
  16. data/lib/cogworker/web/assets/nocturne/styles.css +582 -0
  17. data/lib/cogworker/web/assets/phosphor/Phosphor.woff2 +0 -0
  18. data/lib/cogworker/web/assets/phosphor/style.css +4627 -0
  19. data/lib/cogworker/web/layout.rb +232 -141
  20. data/lib/cogworker/web/routes/history.rb +86 -32
  21. data/lib/cogworker/web/routes/jobs.rb +469 -0
  22. data/lib/cogworker/web/routes/overview.rb +746 -0
  23. data/lib/cogworker/web/routes/schedules.rb +197 -0
  24. data/lib/cogworker/web/routes/workers.rb +227 -0
  25. data/lib/cogworker/web.rb +4 -3
  26. metadata +11 -9
  27. data/lib/cogworker/web/assets/tailwind.css +0 -1
  28. data/lib/cogworker/web/routes/busy.rb +0 -99
  29. data/lib/cogworker/web/routes/dead.rb +0 -93
  30. data/lib/cogworker/web/routes/periodic.rb +0 -67
  31. data/lib/cogworker/web/routes/queues.rb +0 -101
  32. data/lib/cogworker/web/routes/retries.rb +0 -89
  33. data/lib/cogworker/web/routes/scheduled.rb +0 -49
  34. data/lib/cogworker/web/routes/stats.rb +0 -298
@@ -0,0 +1,582 @@
1
+ /* Nocturne — design-system tokens and component classes. This file is the source of truth for the system's look; retune it here and see readme.md. */
2
+ /* Self-hosted Inter (latin subset, weights 400/500/600/700 in one variable
3
+ woff2 — Google serves the same file for each weight, so one @font-face
4
+ per weight is correct, not a duplicate download) instead of the
5
+ upstream fonts.googleapis.com @import: this gem's Web UI ships with zero
6
+ external network dependency (see "Fully offline" in CLAUDE.md), same
7
+ policy as htmx/Tailwind/AG Grid/Chart.js. */
8
+ @font-face {
9
+ font-family: 'Inter';
10
+ font-style: normal;
11
+ font-weight: 400;
12
+ font-display: swap;
13
+ src: url('fonts/inter-latin.woff2') format('woff2');
14
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
15
+ }
16
+ @font-face {
17
+ font-family: 'Inter';
18
+ font-style: normal;
19
+ font-weight: 500;
20
+ font-display: swap;
21
+ src: url('fonts/inter-latin.woff2') format('woff2');
22
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
23
+ }
24
+ @font-face {
25
+ font-family: 'Inter';
26
+ font-style: normal;
27
+ font-weight: 600;
28
+ font-display: swap;
29
+ src: url('fonts/inter-latin.woff2') format('woff2');
30
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
31
+ }
32
+ @font-face {
33
+ font-family: 'Inter';
34
+ font-style: normal;
35
+ font-weight: 700;
36
+ font-display: swap;
37
+ src: url('fonts/inter-latin.woff2') format('woff2');
38
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
39
+ }
40
+
41
+ :root {
42
+ --color-bg: #161826;
43
+ --color-surface: #232532;
44
+ --color-text: #e9e9ed;
45
+ --color-accent: #9184d9; /* review round 7 (Barron): the accent moves to
46
+ the product's blurple — OKLCH hue 289.2, the hue of the app's own Pro
47
+ accent (#9a8fe0, --om-accent-pro) — at the same lightness the steel
48
+ accent held (L 0.660, so the documented ratios survive) and chroma
49
+ in the product blurple's own range (its token sits at C 0.118; the
50
+ accent takes 0.125, from the old steel's 0.030):
51
+ the accent now reads as an accent rather than another neutral. */
52
+ --color-accent-2: #a7a1db; /* the machine-derived stand-in follows: same
53
+ hue, same L 0.734, chroma at the same 2:3 proportion it held */
54
+ --color-divider: color-mix(in srgb, #e9e9ed 16%, transparent);
55
+
56
+ /* Tonal ramps — generated in OKLCH on one shared lightness scale, so the
57
+ same step of any role matches the others in visual value. */
58
+ --color-neutral-100: #f3f5fe;
59
+ --color-neutral-200: #e4e7f5;
60
+ --color-neutral-300: #cfd3e5;
61
+ --color-neutral-400: #b2b6ca;
62
+ --color-neutral-500: #9397ab;
63
+ --color-neutral-600: #75798c;
64
+ --color-neutral-700: #595d6c;
65
+ --color-neutral-800: #3f424d;
66
+ --color-neutral-900: #292b31;
67
+
68
+ --color-accent-100: #f5f4ff;
69
+ --color-accent-200: #e7e5fe;
70
+ --color-accent-300: #d2cefd;
71
+ --color-accent-400: #b5abfc;
72
+ --color-accent-500: #968ae0;
73
+ --color-accent-600: #796cbf;
74
+ --color-accent-700: #5d5294;
75
+ --color-accent-800: #423a6a;
76
+ --color-accent-900: #2b2741;
77
+
78
+ --color-accent-2-100: #f5f4ff;
79
+ --color-accent-2-200: #e7e5fe;
80
+ --color-accent-2-300: #d2cefd;
81
+ --color-accent-2-400: #b5afe8;
82
+ --color-accent-2-500: #9690c9;
83
+ --color-accent-2-600: #7972a9;
84
+ --color-accent-2-700: #5c5783;
85
+ --color-accent-2-800: #423e5d;
86
+ --color-accent-2-900: #2b293a;
87
+
88
+ /* Semantic (warning/danger/success) ramps — nocturne's own token set is
89
+ mono (neutral + one accent, see readme's Color section); the three
90
+ hues below were picked ad hoc in the "Relay" concept mock for
91
+ retrying/failed-dead/healthy status (amber/rose/mint, chosen there as
92
+ literal hex, not tokens) and are formalized here as full ramps so
93
+ usage sites reference a role, not a hardcoded hex. Built the same way
94
+ as the accent-2 stand-in: each ramp keeps the mock's hue, is anchored
95
+ at the ramp's shared step-500 lightness, and reuses the accent ramp's
96
+ own chroma taper (proportional to its C at each step) scaled to that
97
+ hue's measured chroma — so a semantic ramp carries the same "vivid,
98
+ tapering at the extremes" character as the brand accent, unlike the
99
+ low-chroma neutral ramp. --color-warning/-danger/-success (the bare,
100
+ unindexed role, parallel to --color-accent) keep the mock's original
101
+ hex exactly, so nothing that already matched the concept shifts. */
102
+ --color-warning: #d9b884;
103
+ --color-warning-100: #f9f5ef;
104
+ --color-warning-200: #f0e7d9;
105
+ --color-warning-300: #e3d2b7;
106
+ --color-warning-400: #d0b283;
107
+ --color-warning-500: #b29360;
108
+ --color-warning-600: #937543;
109
+ --color-warning-700: #725931;
110
+ --color-warning-800: #513f22;
111
+ --color-warning-900: #332a1a;
112
+
113
+ --color-danger: #d98494;
114
+ --color-danger-100: #fef2f4;
115
+ --color-danger-200: #fae1e4;
116
+ --color-danger-300: #f5c6cd;
117
+ --color-danger-400: #ed9dab;
118
+ --color-danger-500: #d07c8c;
119
+ --color-danger-600: #af5e6e;
120
+ --color-danger-700: #874754;
121
+ --color-danger-800: #61323c;
122
+ --color-danger-900: #3c2327;
123
+
124
+ --color-success: #7fc0a0;
125
+ --color-success-100: #f0f8f3;
126
+ --color-success-200: #dcede4;
127
+ --color-success-300: #bdddcc;
128
+ --color-success-400: #8cc7a9;
129
+ --color-success-500: #68a889;
130
+ --color-success-600: #4a896c;
131
+ --color-success-700: #376a52;
132
+ --color-success-800: #274b3a;
133
+ --color-success-900: #1d3027;
134
+
135
+ /* Deck section-divider ground — review round 3: saturation as presence.
136
+ Dividers take a saturated deep indigo (hue 277, the neutral ramp's own
137
+ family; C 0.094 — written when the accent held C 0.030, before the
138
+ round-7 blurple raised it past the ground's) with a lighter bloom and
139
+ a ghost tint of the same hue, while the slide grounds stay
140
+ desaturated. Deck-scale fills only — not interface colors. */
141
+ --color-section: #262a60;
142
+ --color-section-glow: #353b80;
143
+ --color-section-ghost: #4c5397;
144
+
145
+ --font-heading: "Inter", system-ui, sans-serif;
146
+ --font-heading-weight: 500;
147
+ --font-body: "Inter", system-ui, sans-serif;
148
+
149
+ --space-1: 2.8px;
150
+ --space-2: 5.6px;
151
+ --space-3: 8.4px;
152
+ --space-4: 11.2px;
153
+ --space-6: 16.8px;
154
+ --space-8: 22.4px;
155
+
156
+ --radius-sm: 4px;
157
+ --radius-md: 8px;
158
+ --radius-lg: 14px;
159
+
160
+ /* Elevation — derived from the ground: soft ink-tinted shadows on a
161
+ light theme, a hairline edge + ambient darkness on a dark one. */
162
+ --shadow-sm: 0 0 0 1px #3f424d;
163
+ --shadow-md: 0 0 0 1px #595d6c, 0 6px 18px rgba(0,0,0,0.55);
164
+ --shadow-lg: 0 0 0 1px #9397ab, 0 16px 40px rgba(0,0,0,0.65);
165
+ }
166
+
167
+ /* Light variant — not part of the exported design system (nocturne's own
168
+ readme describes a dark-only "ground"); authored here so Cogworker's Web
169
+ UI can keep following the OS's `prefers-color-scheme`, same as it did
170
+ under the previous Tailwind theme, instead of dropping light mode.
171
+ Every *ramp* step keeps its shared, role-independent lightness schedule
172
+ (100 lightest … 900 darkest) but is reassigned to the mirror-image step
173
+ of its dark-theme value (light's 100 = dark's 900, light's 200 = dark's
174
+ 800, … light's 500 stays put). This way every component rule in this
175
+ file that reaches for a specific step — written with a dark ground in
176
+ mind, e.g. "800 for a subtle tinted border" — keeps producing the
177
+ equivalently subtle result on a light ground too, with no component CSS
178
+ duplicated or forked per theme; only these token values change. The
179
+ four *role* tokens (bg/surface/text/divider) and the two brand-accent
180
+ anchors aren't ramp steps, so they're set directly below, not mirrored:
181
+ bg/surface stay near-white rather than literally inverting to mid-grey,
182
+ text goes near-black, and --color-accent/--color-accent-2 anchor at
183
+ what was the dark ramp's own 600 step — a darker point on the same hue,
184
+ needed for the ~3:1 contrast readme's Color section calls for once the
185
+ ground is light instead of dark (the untouched, lighter #9184d9 reads
186
+ fine on #161826 but washes out against a near-white surface). Nothing
187
+ here (never pure black/white, low chroma outside the accent, no bolded
188
+ headings) breaks the system's own Do/Don't rules. */
189
+ @media (prefers-color-scheme: light) {
190
+ :root {
191
+ --color-bg: #f5f6fe;
192
+ --color-surface: #fdfdff;
193
+ --color-text: #1a1a1f;
194
+ --color-accent: #796cbf;
195
+ --color-accent-2: #7972a9;
196
+ --color-divider: color-mix(in srgb, #1a1a1f 12%, transparent);
197
+
198
+ --color-neutral-100: #292b31;
199
+ --color-neutral-200: #3f424d;
200
+ --color-neutral-300: #595d6c;
201
+ --color-neutral-400: #75798c;
202
+ --color-neutral-500: #9397ab;
203
+ --color-neutral-600: #b2b6ca;
204
+ --color-neutral-700: #cfd3e5;
205
+ --color-neutral-800: #e4e7f5;
206
+ --color-neutral-900: #f3f5fe;
207
+
208
+ --color-accent-100: #2b2741;
209
+ --color-accent-200: #423a6a;
210
+ --color-accent-300: #5d5294;
211
+ --color-accent-400: #796cbf;
212
+ --color-accent-500: #968ae0;
213
+ --color-accent-600: #b5abfc;
214
+ --color-accent-700: #d2cefd;
215
+ --color-accent-800: #e7e5fe;
216
+ --color-accent-900: #f5f4ff;
217
+
218
+ --color-accent-2-100: #2b293a;
219
+ --color-accent-2-200: #423e5d;
220
+ --color-accent-2-300: #5c5783;
221
+ --color-accent-2-400: #7972a9;
222
+ --color-accent-2-500: #9690c9;
223
+ --color-accent-2-600: #b5afe8;
224
+ --color-accent-2-700: #d2cefd;
225
+ --color-accent-2-800: #e7e5fe;
226
+ --color-accent-2-900: #f5f4ff;
227
+
228
+ --color-warning: #937543;
229
+ --color-warning-100: #332a1a;
230
+ --color-warning-200: #513f22;
231
+ --color-warning-300: #725931;
232
+ --color-warning-400: #937543;
233
+ --color-warning-500: #b29360;
234
+ --color-warning-600: #d0b283;
235
+ --color-warning-700: #e3d2b7;
236
+ --color-warning-800: #f0e7d9;
237
+ --color-warning-900: #f9f5ef;
238
+
239
+ --color-danger: #af5e6e;
240
+ --color-danger-100: #3c2327;
241
+ --color-danger-200: #61323c;
242
+ --color-danger-300: #874754;
243
+ --color-danger-400: #af5e6e;
244
+ --color-danger-500: #d07c8c;
245
+ --color-danger-600: #ed9dab;
246
+ --color-danger-700: #f5c6cd;
247
+ --color-danger-800: #fae1e4;
248
+ --color-danger-900: #fef2f4;
249
+
250
+ --color-success: #4a896c;
251
+ --color-success-100: #1d3027;
252
+ --color-success-200: #274b3a;
253
+ --color-success-300: #376a52;
254
+ --color-success-400: #4a896c;
255
+ --color-success-500: #68a889;
256
+ --color-success-600: #8cc7a9;
257
+ --color-success-700: #bdddcc;
258
+ --color-success-800: #dcede4;
259
+ --color-success-900: #f0f8f3;
260
+
261
+ --shadow-sm: 0 0 0 1px #e4e7f5;
262
+ --shadow-md: 0 0 0 1px #cfd3e5, 0 6px 18px rgba(0,0,0,0.12);
263
+ --shadow-lg: 0 0 0 1px #9397ab, 0 16px 40px rgba(0,0,0,0.18);
264
+ }
265
+ }
266
+
267
+ body {
268
+ background: var(--color-bg);
269
+ color: var(--color-text);
270
+ font-family: var(--font-body);
271
+ }
272
+ h1, h2, h3, h4 { font-family: var(--font-heading); font-weight: var(--font-heading-weight); }
273
+
274
+ .lighten{mix-blend-mode:lighten;background-color:transparent}
275
+
276
+ /* ══════════════════════════════════════════════════════════════════════════
277
+ Components — built with the tokens above. Plain CSS
278
+ on plain HTML: no JavaScript, no build step. Each class is documented in
279
+ readme.md and demonstrated in foundations/ and components/.
280
+ ══════════════════════════════════════════════════════════════════════ */
281
+
282
+ *, *::before, *::after { box-sizing: border-box; }
283
+ body { margin: 0; font-size: 15px; line-height: 1.55; font-weight: 400; }
284
+ h1, h2, h3, h4, h5, h6 {
285
+ font-family: var(--font-heading); font-weight: var(--font-heading-weight);
286
+ line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2);
287
+ }
288
+ h1 { font-size: 42px; }
289
+ h2 { font-size: 32px; }
290
+ h3 { font-size: 25px; }
291
+ h4 { font-size: 20px; }
292
+ h5 { font-size: 16px; }
293
+ h6 { font-size: 13px; }
294
+ h6 { letter-spacing: 0.08em; text-transform: uppercase; }
295
+ p { margin: 0 0 var(--space-3); }
296
+ a { color: var(--color-accent); text-underline-offset: 3px; }
297
+ img { display: block; max-width: 100%; }
298
+ figure { margin: 0; }
299
+ figcaption {
300
+ font-size: 11px; margin-top: var(--space-1);
301
+ color: color-mix(in srgb, var(--color-text) 55%, transparent);
302
+ }
303
+ .text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }
304
+ /* Not part of the exported system — the "Relay" concept mock defined this
305
+ inline in its own <style> block for JIDs/class names; formalized here so
306
+ every consuming page shares one definition instead of repeating it. */
307
+ .mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; }
308
+ :focus { outline: none; }
309
+ :focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
310
+ ::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }
311
+
312
+ /* — rules — */
313
+ /* Rules fade to transparent at both ends instead of stopping cleanly — a
314
+ Nocturne signature. The ramp is 48px an end (one deck baseline unit),
315
+ painted from the same divider token. Freestanding rules fade; box
316
+ outlines, in-control separators, and short accent marks stay solid. */
317
+ .hr {
318
+ height: 1px; border: 0; margin: var(--space-4) 0;
319
+ background: linear-gradient(to right,
320
+ transparent, var(--color-divider) 48px,
321
+ var(--color-divider) calc(100% - 48px), transparent);
322
+ }
323
+
324
+ /* — buttons — */
325
+ .btn {
326
+ display: inline-flex; align-items: center; justify-content: center; gap: 6px;
327
+ cursor: pointer; text-decoration: none; white-space: nowrap;
328
+ font-family: var(--font-heading); font-weight: var(--font-heading-weight);
329
+ font-size: 14px; line-height: 1.2; color: var(--color-text); /* matches the .input's 14px —
330
+ the pair sits side by side in sign-up rows */
331
+ background: transparent; border: 1px solid transparent;
332
+ padding: var(--space-2) calc(var(--space-3) * 1.2);
333
+ border-radius: var(--radius-md);
334
+ }
335
+ .btn svg { display: block; }
336
+ .btn:disabled { opacity: 0.45; cursor: not-allowed; }
337
+ .btn-primary { color: var(--color-accent); border-color: var(--color-accent); }
338
+ .btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }
339
+ .btn-primary:active { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }
340
+ .btn-secondary { border-color: var(--color-divider); }
341
+ .btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
342
+ .btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }
343
+ .btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); }
344
+ .btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }
345
+ .btn-ghost:active { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }
346
+ .btn-icon { width: 36px; height: 36px; padding: 0; }
347
+ .btn-block { width: 100%; margin-top: var(--space-2); }
348
+ /* Not part of the exported system (only .btn-primary/-secondary/-ghost/-icon/
349
+ -block are documented) — added for actions that are specifically a warning,
350
+ a destructive/danger action, or a positive/resume action (Cogworker's
351
+ Busy/Retries/Dead/Overview/Workers tabs have these), following the exact
352
+ same outline-only shape as .btn-primary, just anchored on the warning/
353
+ danger/success ramps instead of the accent. */
354
+ .btn-warning { color: var(--color-warning); border-color: var(--color-warning); }
355
+ .btn-warning:hover { background: color-mix(in srgb, var(--color-warning) 12%, transparent); }
356
+ .btn-warning:active { background: color-mix(in srgb, var(--color-warning) 22%, transparent); }
357
+ .btn-danger { color: var(--color-danger); border-color: var(--color-danger); }
358
+ .btn-danger:hover { background: color-mix(in srgb, var(--color-danger) 12%, transparent); }
359
+ .btn-danger:active { background: color-mix(in srgb, var(--color-danger) 22%, transparent); }
360
+ .btn-success { color: var(--color-success); border-color: var(--color-success); }
361
+ .btn-success:hover { background: color-mix(in srgb, var(--color-success) 12%, transparent); }
362
+ .btn-success:active { background: color-mix(in srgb, var(--color-success) 22%, transparent); }
363
+
364
+ /* — forms — */
365
+ .field > label {
366
+ display: block; font-size: 12px; margin-bottom: 5px;
367
+ color: color-mix(in srgb, var(--color-text) 70%, transparent);
368
+ }
369
+ .input {
370
+ width: 100%; min-height: 36px; padding: 6px 10px; font: inherit;
371
+ font-size: 14px; color: var(--color-text); caret-color: var(--color-accent);
372
+ background: var(--color-surface);
373
+ border: 1px solid var(--color-divider); border-radius: var(--radius-md);
374
+ }
375
+ .input:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }
376
+ .input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }
377
+ textarea.input { min-height: 90px; resize: vertical; }
378
+ .radio { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; }
379
+ .radio input, .seg-opt input {
380
+ position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;
381
+ }
382
+ .radio .dot {
383
+ width: 16px; height: 16px; flex: none; border-radius: 50%;
384
+ border: 1.5px solid var(--color-divider);
385
+ }
386
+ .radio:hover .dot { border-color: var(--color-accent); }
387
+ .radio input:checked + .dot {
388
+ border-color: var(--color-accent); background: var(--color-accent);
389
+ box-shadow: inset 0 0 0 4px var(--color-bg);
390
+ }
391
+ .radio input:focus-visible + .dot { outline: 2px solid var(--color-accent); outline-offset: 2px; }
392
+ /* `align-self: flex-start` guards against a real bug: when a `.seg` ends
393
+ up a *direct* child of a `display: flex; flex-direction: column` ancestor
394
+ (e.g. History's status filter, a direct child of `<main>` — unlike every
395
+ other `.seg` usage, nested a level or two deeper inside its own
396
+ `page_header` div) the flex spec "blockifies" its `inline-flex` to plain
397
+ `flex`, and the ancestor's default `align-items: stretch` then stretches
398
+ it to the ancestor's full width — a mostly-empty, oddly wide bordered box
399
+ with the actual segment labels bunched at the left. `align-self` opts
400
+ this element back out of stretching on the cross axis regardless of
401
+ where it ends up, and is a no-op everywhere `.seg` isn't a flex item at
402
+ all (nested usages), so it's safe to set unconditionally. */
403
+ .seg {
404
+ display: inline-flex; overflow: hidden; align-self: flex-start;
405
+ border: 1px solid var(--color-divider); border-radius: var(--radius-md);
406
+ }
407
+ .seg-opt {
408
+ display: inline-flex; align-items: center; gap: 6px;
409
+ padding: 7px 12px; font-size: 13px; cursor: pointer;
410
+ }
411
+ .seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }
412
+ /* `.seg` clips its children to its own rounded corner via `overflow:
413
+ hidden` — a checked end segment's own corner is otherwise square, so its
414
+ `box-shadow: inset` ring gets cut off right at the parent's curve instead
415
+ of following it, a visible break in the ring exactly at the rounded
416
+ corner. Matching the end segment's own radius to the parent's (minus the
417
+ 1px border) keeps the ring's corner following the same curve as the clip. */
418
+ .seg-opt:first-child { border-radius: calc(var(--radius-md) - 1px) 0 0 calc(var(--radius-md) - 1px); }
419
+ .seg-opt:last-child { border-radius: 0 calc(var(--radius-md) - 1px) calc(var(--radius-md) - 1px) 0; }
420
+ .seg-opt:only-child { border-radius: calc(var(--radius-md) - 1px); }
421
+ .seg-opt:has(input:checked) { color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); }
422
+ .seg-opt:not(:has(input:checked)):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
423
+ .seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }
424
+
425
+ /* — cards — */
426
+ .card {
427
+ display: flex; flex-direction: column; gap: var(--space-2);
428
+ padding: var(--space-3); border-radius: var(--radius-md); background: var(--color-surface);
429
+ }
430
+ .card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); }
431
+ .card-title {
432
+ font-family: var(--font-heading); font-weight: var(--font-heading-weight);
433
+ font-size: 17px; line-height: 1.2;
434
+ }
435
+ .card-body { margin: 0; font-size: 13px; opacity: 0.8; flex: 1; }
436
+ .card-meta {
437
+ display: flex; align-items: center; gap: 6px; font-size: 11px;
438
+ color: color-mix(in srgb, var(--color-text) 50%, transparent);
439
+ }
440
+ .elev-sm { box-shadow: var(--shadow-sm); }
441
+ .elev-md { box-shadow: var(--shadow-md); }
442
+ .elev-lg { box-shadow: var(--shadow-lg); }
443
+
444
+ /* — tags — */
445
+ .tag {
446
+ display: inline-flex; align-items: center; font-size: 11px;
447
+ letter-spacing: 0.02em; padding: 3px 10px;
448
+ border-radius: calc(var(--radius-md) * 0.75);
449
+ }
450
+ .tag-accent { background: var(--color-accent-800); color: var(--color-accent-100); }
451
+ .tag-accent-2 { background: var(--color-accent-2-800); color: var(--color-accent-2-100); }
452
+ .tag-neutral { background: var(--color-neutral-800); color: var(--color-neutral-100); }
453
+ .tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }
454
+ /* Same addition as .btn-warning/-danger above, mirroring .tag-accent's own
455
+ shape (dark-ramp-800 fill, light-ramp-100 text) for the three semantic
456
+ ramps instead of the brand accent. */
457
+ .tag-warning { background: var(--color-warning-800); color: var(--color-warning-100); }
458
+ .tag-danger { background: var(--color-danger-800); color: var(--color-danger-100); }
459
+ .tag-success { background: var(--color-success-800); color: var(--color-success-100); }
460
+
461
+ /* — navigation — */
462
+ .nav {
463
+ display: flex; align-items: center; gap: var(--space-4);
464
+ padding: var(--space-3) var(--space-4);
465
+ border-bottom: none;
466
+ }
467
+ .nav-brand {
468
+ font-family: var(--font-heading); font-weight: var(--font-heading-weight);
469
+ font-size: 18px; margin-right: auto;
470
+ }
471
+ .nav a { color: inherit; text-decoration: none; font-size: 14px; }
472
+ .nav a:hover, .nav a[aria-current='page'] { color: var(--color-accent); }
473
+
474
+ /* — layout — */
475
+ /* Not part of the exported design system — the mock's own canvas has no
476
+ equivalent of a real app's page content ever needing to run edge to edge.
477
+ `.cw-main--wide` (toggled by `Layout.wide_layout_toggle_button`/
478
+ `wide_layout_script`) lifts the cap for anyone working with wide tables
479
+ (History's AG Grid, the Overview queue-first layout) on a large monitor.
480
+ `contain: inline-size` fixes a real bug: `body`'s own `min-width: max-
481
+ content` (see `Layout.wrap`'s comment) is meant to be driven purely by
482
+ the header — but without this, `.cw-main`'s *own* max-content
483
+ contribution (a wide table/card grid, capped at 1480px by the max-width
484
+ above) also factors into that same calculation, and since 1480px is
485
+ bigger than the header's own true minimum (~1090px), `body` ends up
486
+ floored at 1480 instead — the header (and `main` riding along with it)
487
+ then sit frozen at 1480px for the *entire* practical resize range
488
+ instead of continuously tracking the window down to their real minimum,
489
+ caught by hand: dragging the window narrower did nothing at all until
490
+ crossing below 1480px, only then jumping straight to the scrollbar.
491
+ `contain: inline-size` tells the browser this element's own width
492
+ sizing doesn't depend on (and its content-based size shouldn't leak
493
+ into) its container's — main still gets its width entirely normally,
494
+ from the flex `align-items: stretch` cross-size body ends up resolving
495
+ to (via the header) and its own `max-width` above; it just stops being
496
+ counted as a contributor to what that resolved size floors out at. */
497
+ .cw-main { max-width: 1480px; width: 100%; margin: 0 auto; box-sizing: border-box; contain: inline-size; }
498
+ .cw-main.cw-main--wide { max-width: none; }
499
+
500
+ /* — tables — */
501
+ .table { width: 100%; border-collapse: collapse; font-size: 14px; }
502
+ .table th {
503
+ text-align: left; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;
504
+ color: color-mix(in srgb, var(--color-text) 60%, transparent);
505
+ padding: var(--space-2); border-bottom: 1px solid transparent; /* keeps layout; the row paints the fading rule */
506
+ }
507
+ .table td {
508
+ padding: var(--space-2);
509
+ border-bottom: 1px solid transparent;
510
+ }
511
+ /* Row rules as row-level strips so the end-fade spans the row, not each cell. */
512
+ .table thead tr {
513
+ background: linear-gradient(to right,
514
+ transparent, var(--color-divider) 48px,
515
+ var(--color-divider) calc(100% - 48px), transparent) no-repeat bottom / 100% 1px;
516
+ }
517
+ .table tbody tr {
518
+ background: linear-gradient(to right,
519
+ transparent, color-mix(in srgb, var(--color-text) 8%, transparent) 48px,
520
+ color-mix(in srgb, var(--color-text) 8%, transparent) calc(100% - 48px), transparent) no-repeat bottom / 100% 1px;
521
+ }
522
+ .table tbody tr:hover {
523
+ /* the hover tint layers over the row rule, which keeps painting */
524
+ background:
525
+ linear-gradient(color-mix(in srgb, var(--color-text) 4%, transparent),
526
+ color-mix(in srgb, var(--color-text) 4%, transparent)) no-repeat 0 0 / 100% 100%,
527
+ linear-gradient(to right,
528
+ transparent, color-mix(in srgb, var(--color-text) 8%, transparent) 48px,
529
+ color-mix(in srgb, var(--color-text) 8%, transparent) calc(100% - 48px), transparent) no-repeat bottom / 100% 1px;
530
+ }
531
+
532
+ /* — dialog — */
533
+ .dialog-backdrop {
534
+ position: fixed; inset: 0; display: grid; place-items: center;
535
+ padding: var(--space-4);
536
+ background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent);
537
+ }
538
+ /* `color: var(--color-text)` here is as load-bearing as `border: none`
539
+ below: a native `<dialog>`'s UA stylesheet also sets `color: CanvasText`
540
+ directly on the element, and since that's a declared value (not merely
541
+ inherited), it wins over the light `--color-text` that would otherwise
542
+ flow down from `body` — without overriding it here, every descendant
543
+ that doesn't set its own `color` (the title, the `<pre>` backtrace body)
544
+ renders near-black text on `--color-surface`'s dark background, unreadable. */
545
+ .dialog {
546
+ width: min(440px, 100%); display: flex; flex-direction: column; gap: var(--space-3);
547
+ padding: var(--space-4); border-radius: var(--radius-lg); border: none;
548
+ background: var(--color-surface); color: var(--color-text); box-shadow: var(--shadow-lg);
549
+ }
550
+ .dialog-title {
551
+ font-family: var(--font-heading); font-weight: var(--font-heading-weight);
552
+ font-size: 20px;
553
+ }
554
+ .dialog-body { font-size: 14px; opacity: 0.85; }
555
+ .dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-2); }
556
+ /* Not part of the exported system — the native <dialog>/showModal() a
557
+ consuming page reaches for (e.g. History's backtrace viewer) paints its
558
+ own browser-default ::backdrop, not the .dialog-backdrop div above (that
559
+ one's for a JS-managed overlay <div>). Same tint, so either path looks
560
+ like the same system. .dialog itself needs one explicit override to work
561
+ as a native <dialog>'s own box styling, not just "unmodified" — see the
562
+ `border: none` above: `<dialog>` ships a UA-default `border-style: solid`
563
+ that neither `.dialog`'s `border-radius` nor `box-shadow` overrides on
564
+ their own, so without resetting it a real browser paints its own default
565
+ border (often a stark, unthemed gray/silver rectangle, square-cornered
566
+ on top of the rounded card underneath) around every native dialog on the
567
+ page — a real bug, caught by hand on History's backtrace viewer. */
568
+ dialog.dialog::backdrop {
569
+ background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent);
570
+ }
571
+ /* `.dialog`'s own `display: flex` (needed for the JS-managed
572
+ `.dialog-backdrop` div pattern above) has the same specificity as the
573
+ browser's built-in `dialog:not([open]) { display: none }` — and author
574
+ styles win a same-specificity tie over user-agent ones, so without this,
575
+ `.dialog` applied straight to a native `<dialog>` element defeats its
576
+ own native hide-until-`showModal()` behavior and renders permanently
577
+ open. Element-qualifying restores it without touching `.dialog`'s
578
+ existing div-overlay behavior elsewhere. */
579
+ dialog.dialog:not([open]) {
580
+ display: none;
581
+ }
582
+