reviewzy 0.3.0 → 0.4.0

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.
@@ -7,7 +7,8 @@
7
7
  One height ladder shared with inputs so horizontal toolbars stay aligned
8
8
  regardless of label length, icon presence, or element type. The primary
9
9
  is outlined: accent never fills area, so coverage stays under 10 percent
10
- by construction. Press is translateY(1px), no scale, no ripple. */
10
+ by construction. Press is the jelly squish, a scale settled by a spring,
11
+ no ripple. */
11
12
  .btn {
12
13
  display: inline-flex;
13
14
  align-items: center;
@@ -25,13 +26,12 @@
25
26
  background var(--dur-tint) var(--ease),
26
27
  border-color var(--dur-tint) var(--ease),
27
28
  color var(--dur-tint) var(--ease),
28
- transform var(--dur-tint) var(--ease);
29
+ transform var(--press-dur) var(--press-spring);
29
30
  white-space: nowrap;
30
31
  text-decoration: none;
31
32
  user-select: none;
32
33
  }
33
34
  input.btn { line-height: 34px; } /* native inputs need explicit line-height */
34
- .btn:active { transform: translateY(1px); }
35
35
  .btn svg { width: 14px; height: 14px; flex-shrink: 0; }
36
36
 
37
37
  .btn-sm {
@@ -68,12 +68,14 @@ input.btn-lg { line-height: 42px; }
68
68
  the floor in all three states (rest 6.0, hover 5.0, active 4.6). */
69
69
  [data-theme="light"] .btn-primary { color: color-mix(in srgb, var(--accent) 50%, var(--text) 50%); }
70
70
  .btn-primary:hover {
71
- background: color-mix(in srgb, var(--accent) 24%, transparent);
72
- border-color: color-mix(in srgb, var(--accent) 60%, transparent);
71
+ background: color-mix(in srgb, var(--accent) 24%, var(--tint-ground));
72
+ border-color: color-mix(in srgb, var(--accent) 60%, var(--tint-ground));
73
73
  }
74
+ /* the press tint holds the dark label at 4.60:1 on surface-1 and 5.21 on --bg
75
+ (8-bit painted); 32 percent dropped it to 4.15 on the card, and 28 to 4.48 */
74
76
  .btn-primary:active {
75
- background: color-mix(in srgb, var(--accent) 32%, transparent);
76
- border-color: color-mix(in srgb, var(--accent) 60%, transparent);
77
+ background: color-mix(in srgb, var(--accent) 27%, var(--tint-ground));
78
+ border-color: color-mix(in srgb, var(--accent) 60%, var(--tint-ground));
77
79
  }
78
80
 
79
81
  /* Filled: the ONE main action on a screen, and never a second one. Outlined
@@ -84,11 +86,12 @@ input.btn-lg { line-height: 42px; }
84
86
  and --accent-press, which is what those two tokens are for. */
85
87
  .btn-fill {
86
88
  background: var(--accent);
89
+ --tint-ground: transparent;
87
90
  color: var(--on-accent);
88
91
  border-color: var(--accent);
89
92
  }
90
- .btn-fill:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
91
- .btn-fill:active { background: var(--accent-press); border-color: var(--accent-press); }
93
+ .btn-fill:hover { background: var(--accent-hover); --tint-ground: transparent; border-color: var(--accent-hover); }
94
+ .btn-fill:active { background: var(--accent-press); --tint-ground: transparent; border-color: var(--accent-press); }
92
95
  /* An accent fill swallows an accent focus ring, so the ring's own page keyline
93
96
  is what separates the two. tokens.css ships it on :focus-visible. */
94
97
 
@@ -97,20 +100,24 @@ input.btn-lg { line-height: 42px; }
97
100
  color: var(--text-2);
98
101
  border-color: var(--surface-3);
99
102
  }
100
- .btn-secondary:hover { background: var(--surface-2); color: var(--text); border-color: var(--surface-3); }
101
- .btn-secondary:active { background: var(--surface-3); }
103
+ .btn-secondary:hover { background: var(--surface-2); --tint-ground: transparent; color: var(--text); border-color: var(--surface-3); }
104
+ .btn-secondary:active { background: var(--surface-3); --tint-ground: transparent; }
102
105
 
103
106
  .btn-danger {
104
107
  background: transparent;
105
108
  color: var(--danger);
106
- border-color: color-mix(in srgb, var(--danger) 40%, transparent);
109
+ border-color: color-mix(in srgb, var(--danger) 40%, var(--tint-ground));
107
110
  }
111
+ /* label and tint are one hue, so a deeper tint pulls the label down with it:
112
+ the hover and press label steps to --danger-text, measured in tokens.css */
108
113
  .btn-danger:hover {
109
- background: color-mix(in srgb, var(--danger) 16%, transparent);
110
- border-color: color-mix(in srgb, var(--danger) 60%, transparent);
114
+ background: color-mix(in srgb, var(--danger) 16%, var(--tint-ground));
115
+ color: var(--danger-text);
116
+ border-color: color-mix(in srgb, var(--danger) 60%, var(--tint-ground));
111
117
  }
112
118
  .btn-danger:active {
113
- background: color-mix(in srgb, var(--danger) 24%, transparent);
119
+ background: color-mix(in srgb, var(--danger) 24%, var(--tint-ground));
120
+ color: var(--danger-text);
114
121
  }
115
122
 
116
123
  .btn-icon {
@@ -121,7 +128,7 @@ input.btn-lg { line-height: 42px; }
121
128
  color: var(--text-3);
122
129
  border-color: var(--line);
123
130
  }
124
- .btn-icon:hover { border-color: var(--surface-3); color: var(--text); background: var(--surface-2); }
131
+ .btn-icon:hover { border-color: var(--surface-3); color: var(--text); background: var(--surface-2); --tint-ground: transparent; }
125
132
  .btn-icon svg { width: 15px; height: 15px; }
126
133
 
127
134
  /* ---- Disabled ----
@@ -132,7 +139,7 @@ input.btn-lg { line-height: 42px; }
132
139
  `.btn:disabled` already outranks. What no placement here can beat is a state
133
140
  costume at (0,3,0), which is why the `!important` block near the end of the
134
141
  file exists.
135
- It replaces `opacity: 0.45`, her call 2026-09-02. An alpha dims the label with
142
+ It replaces `opacity: 0.45`. An alpha dims the label with
136
143
  the box, and a label dimmed that far stops clearing any floor; a colour dims
137
144
  only what should read as off. --text-disabled measures 4.50:1 on --bg and 3.89
138
145
  on --surface-1, both over the 3:1 floor for a control that cannot act.
@@ -153,12 +160,16 @@ input.btn-lg { line-height: 42px; }
153
160
  block, the primary keeps a fill under a visible edge (--surface-3 on
154
161
  --surface-2, 1.23:1 dark and 1.10 light, the same register the enabled edges
155
162
  sit in), the two outlined ones keep an edge and no fill. */
156
- .btn-fill:disabled { background: var(--surface-2); border-color: var(--surface-2); }
157
- .btn-primary:disabled { background: var(--surface-2); border-color: var(--surface-3); }
163
+ .btn-fill:disabled { background: var(--surface-2); --tint-ground: transparent; border-color: var(--surface-2); }
164
+ .btn-primary:disabled { background: var(--surface-2); --tint-ground: transparent; border-color: var(--surface-3); }
158
165
  .btn-secondary:disabled, .btn-danger:disabled { border-color: var(--line); }
159
166
 
160
167
  /* Loading spinner */
161
168
  @keyframes spin { to { transform: rotate(360deg); } }
169
+ /* the ring's loading breath: head anchored at 50, so the offset tracks L - 50 */
170
+ @keyframes ring-tail-breathe { to { stroke-dasharray: 46 100; stroke-dashoffset: -4; } }
171
+ /* its reduced-motion replacement, on the shape of the route bar's own hold */
172
+ @keyframes ring-hold { 50% { opacity: var(--ring-hold-dim); } }
162
173
  .btn-loading { position: relative; color: transparent !important; }
163
174
  .btn-loading::after {
164
175
  content: '';
@@ -170,11 +181,20 @@ input.btn-lg { line-height: 42px; }
170
181
  animation: spin 0.6s linear infinite;
171
182
  color: var(--accent-text);
172
183
  }
184
+ /* a filled button's ground is --accent, where that --accent-text ring is
185
+ invisible (1.22:1 dark rest, 1.00:1 hovered): the fill's spinner takes the
186
+ fill's own on-colour, 8.22:1 dark / 4.60:1 light */
187
+ .btn-fill.btn-loading::after { color: var(--on-accent); }
188
+ @media (prefers-reduced-motion: reduce) {
189
+ /* the busy cue replaces the motion rather than dying with it: the ring
190
+ rests, so the button still reads as busy without a spin */
191
+ .btn-loading::after { animation: none; }
192
+ }
173
193
 
174
194
  /* ---- Page skeleton ----
175
195
  Built independently by every dashboard project on the system: one
176
- centered column, section headers as a triplet, toolbars that compose.
177
- Retune a build with --page-w, not per-page overrides. */
196
+ page column, section headers as a triplet, toolbars that compose.
197
+ Under a navbar, retune a build with --page-w, not per-page overrides. */
178
198
  .page {
179
199
  max-width: var(--page-w);
180
200
  /* auto inline margins cancel a grid or flex item's stretch, so the column
@@ -185,22 +205,30 @@ input.btn-lg { line-height: 42px; }
185
205
  padding-inline: var(--space-8);
186
206
  }
187
207
  @media (max-width: 768px) { .page { padding-inline: var(--space-4); } }
208
+ /* Beside a sidebar alone a cap here centres nothing; --page-w is past the
209
+ reading-column bound shells.md names. Measured 230px of dead page each side
210
+ on the dashboard example at 1912. A bleed banner's column follows the page's. */
211
+ .sidebar ~ .page,
212
+ .sidebar ~ * .page,
213
+ .sidebar ~ * .banner-inner { max-width: none; }
188
214
 
189
215
  /* Eyebrow is .label; the triplet continues with title and lede.
190
- The title is --t-7, one step above the document h2, because a section
216
+ The title is --t-8, two steps above the document h2, because a section
191
217
  heading and a paragraph heading are not the same object: h2 stays 26px for
192
- prose. The lede is --t-4, so it stops sitting at body size and the page
218
+ prose. It steps to --t-7 under 768px as the page title steps to --t-8, so
219
+ the two stay one step apart at every width. The lede is --t-4, so it stops sitting at body size and the page
193
220
  stops reading at one volume. The eyebrow is --text-4 (5.47:1 dark, 4.94:1
194
221
  light on --bg): accent on every eyebrow down a page turns the signature
195
222
  into wallpaper, which is the failure v1 shipped. */
196
223
  .section-head { margin-bottom: var(--space-5); }
197
224
  .section-head > .label { color: var(--text-4); }
198
225
  .section-title {
199
- font: 600 var(--t-7)/1.1 var(--font-sans);
200
- letter-spacing: -0.025em;
226
+ font: 600 var(--t-8)/1.05 var(--font-sans);
227
+ letter-spacing: -0.035em;
201
228
  color: var(--text);
202
229
  margin-top: var(--space-2);
203
230
  }
231
+ @media (max-width: 768px) { .section-title { font-size: var(--t-7); } }
204
232
  .section-lede {
205
233
  font-size: var(--t-4);
206
234
  color: var(--text-3);
@@ -222,8 +250,16 @@ input.btn-lg { line-height: 42px; }
222
250
  .toolbar-center { align-items: center; }
223
251
 
224
252
  /* ---- Tabs (underline) ---- */
225
- .tabs-wrap { border-bottom: 1px solid var(--line-soft); position: relative; }
226
- .tabs { display: flex; gap: 0; }
253
+ /* The hairline and the positioning context are the ROW's, not a wrapper's:
254
+ the ink is absolutely placed against the row's own bottom edge, so it lands
255
+ on the hairline it belongs to wherever the row sits. `.tabs-wrap` keeps no
256
+ rule of its own and survives as initTabs' query hook. */
257
+ .tabs {
258
+ display: flex;
259
+ gap: 0;
260
+ position: relative;
261
+ border-bottom: 1px solid var(--line-soft);
262
+ }
227
263
  .tab {
228
264
  position: relative;
229
265
  padding: 9px 14px;
@@ -260,6 +296,7 @@ input.btn-lg { line-height: 42px; }
260
296
  transform: scaleX(0);
261
297
  transform-origin: left center;
262
298
  background: var(--accent);
299
+ --tint-ground: transparent;
263
300
  border-radius: var(--r-pill);
264
301
  pointer-events: none;
265
302
  opacity: 0;
@@ -280,8 +317,10 @@ input.btn-lg { line-height: 42px; }
280
317
  padding: 2px 5px;
281
318
  border-radius: var(--r-pill);
282
319
  background: var(--surface-2);
320
+ --tint-ground: transparent;
283
321
  color: var(--text-3);
284
322
  }
323
+ /* light takes the shared remix: --accent-text alone reads 4.35:1 over --bg */
285
324
  .tab.active .tab-badge,
286
325
  .navbar-link.active .navbar-badge { background: var(--accent-quiet); color: var(--accent-text); }
287
326
 
@@ -296,6 +335,7 @@ input.btn-lg { line-height: 42px; }
296
335
  so no-JS keeps a visible selection. */
297
336
  .tabs-boxed-wrap {
298
337
  background: var(--well);
338
+ --tint-ground: transparent;
299
339
  border-radius: var(--r-2);
300
340
  padding: 3px;
301
341
  display: inline-flex;
@@ -312,10 +352,10 @@ input.btn-lg { line-height: 42px; }
312
352
  /* concentric with the track: track radius minus its 3px padding */
313
353
  border-radius: calc(var(--r-2) - 3px);
314
354
  cursor: pointer;
315
- transition: background var(--dur-tint) var(--ease), color var(--dur-tint) var(--ease);
355
+ transition: background var(--dur-tint) var(--ease), color var(--dur-tint) var(--ease), transform var(--press-dur) var(--press-spring);
316
356
  user-select: none;
317
357
  }
318
- .tab-box:hover { background: var(--surface-2); color: var(--text-2); }
358
+ .tab-box:hover { background: var(--surface-2); --tint-ground: transparent; color: var(--text-2); }
319
359
  .tab-box.active { color: var(--text); }
320
360
  /* The segment and the pill that slides over it are ONE selection surface, so one
321
361
  rule paints it. Without ink the segment carries it; with ink the pill does and
@@ -323,6 +363,7 @@ input.btn-lg { line-height: 42px; }
323
363
  .tab-box.active,
324
364
  .tabs-boxed-ink {
325
365
  background: var(--surface-1);
366
+ --tint-ground: transparent;
326
367
  box-shadow: var(--lit-1), 0 1px 3px oklch(0.14 0.02 280 / 0.45);
327
368
  }
328
369
  [data-theme="light"] .tab-box.active,
@@ -345,6 +386,30 @@ input.btn-lg { line-height: 42px; }
345
386
  /* once the ink exists, the segment stops painting its own selection */
346
387
  .tabs-boxed-wrap.has-ink .tab-box.active { background: transparent; box-shadow: none; }
347
388
 
389
+ /* ---- Press ----
390
+ The jelly squish: hover grows, press flattens wider and
391
+ shorter, and --press-spring settles the wobble over --press-dur. Only the
392
+ transform moves on the spring: .btn keeps its background, border-color and
393
+ color on --dur-tint, .tab-box its background and color, and neither
394
+ transitions box-shadow. The hover grow is fine-pointer only; the press squash
395
+ still fires on a tap. None of it under reduced motion, where the fades stay.
396
+ The non-uniform press scale momentarily distorts a control's corner radius,
397
+ which is the point of a squish; references/motion.md sanctions it. */
398
+ @media (hover: hover) and (prefers-reduced-motion: no-preference) {
399
+ .btn:not(:disabled):hover,
400
+ .tab-box:not(:disabled):hover {
401
+ transform: scale(1.03);
402
+ }
403
+ }
404
+ /* after the hover grow: both rank (0,3,0), so source order hands a held
405
+ press the squash */
406
+ @media (prefers-reduced-motion: no-preference) {
407
+ .btn:not(:disabled):active,
408
+ .tab-box:not(:disabled):active {
409
+ transform: scale(1.05, 0.90);
410
+ }
411
+ }
412
+
348
413
  /* ---- Inputs ----
349
414
  Inputs sit below the page plane: --well, not raised. Most systems raise
350
415
  inputs; this system sinks them. Field labels are 12px 500, sentence case:
@@ -374,19 +439,31 @@ input.btn-lg { line-height: 42px; }
374
439
  grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
375
440
  column-gap: var(--space-5);
376
441
  row-gap: var(--space-4);
442
+ /* A grid stretches its items, and a stretched label paints its one line at
443
+ the TOP of a 36px control's row: measured 11px above the control's centre,
444
+ against 0.2px for the same label inside a .field, which centres its own
445
+ subgrid. The bare pair is the shape the fieldset and half the sheet use. */
446
+ align-items: center;
377
447
  }
378
448
  .form-grid .field {
379
449
  display: grid;
450
+ /* the span is restated here rather than taken from the child rule below:
451
+ subgrid needs it, and the scope is descendant so a field a wrapper div
452
+ sits between still levels with the grid's own columns */
380
453
  grid-column: 1 / -1;
381
454
  grid-template-columns: subgrid;
382
455
  align-items: center;
383
456
  }
384
457
  .form-grid .field-label { grid-column: 1; }
385
458
  .form-grid .field > :not(.field-label) { grid-column: 2; }
386
- /* The summary collects a submit's failures ABOVE the fields, so it takes the
387
- whole grid the way a .field does. Left to `auto` it is placed in the label
388
- column and measured 180px wide at a 1440 viewport. */
389
- .form-grid .form-summary { grid-column: 1 / -1; }
459
+ /* Everything that is not one of the bare label/control pairs spans both
460
+ columns: a .field, a summary, a fieldset, a paragraph, a callout. Left to
461
+ `auto` the fieldset was placed in the 180px label column and its nested
462
+ grid crushed with it, the value clipping at "oncall@acm". The pair is
463
+ restated structurally one line down, so a child joins by being a label's
464
+ sibling rather than by being named. */
465
+ .form-grid > * { grid-column: 1 / -1; }
466
+ .form-grid > .field-label + * { grid-column: 2; }
390
467
 
391
468
  /* ---- Fieldset ----
392
469
  A grouped border, not a second card: line-soft, no shadow, the legend
@@ -434,9 +511,12 @@ input.btn-lg { line-height: 42px; }
434
511
  }
435
512
  .form-summary-links a { font-size: var(--t-2); color: var(--accent-text); } /* 7.25:1 dark, 5.17:1 light on the tint */
436
513
 
437
- /* Stacked under 768px: labels above fields, the .field's own shape */
514
+ /* Stacked under 768px: labels above fields, the .field's own shape. The pair
515
+ rule above names column 2, which a one-column grid does not have, so the
516
+ control returns to flow or it lands in an implicit second column. */
438
517
  @media (max-width: 768px) {
439
518
  .form-grid { grid-template-columns: minmax(0, 1fr); }
519
+ .form-grid > .field-label + * { grid-column: auto; }
440
520
  }
441
521
 
442
522
  /* ---- Setting rows ----
@@ -458,13 +538,22 @@ input.btn-lg { line-height: 42px; }
458
538
  border-radius: 0;
459
539
  transition: background var(--dur-tint) var(--ease);
460
540
  }
461
- /* the stack is flush with its card (the card carries no vertical padding),
462
- so the edge rows take the card's own radius and the hover fill hugs the
463
- card edge exactly */
464
- .setting-row:first-child { border-radius: var(--r-2) var(--r-2) 0 0; }
465
- .setting-row:last-child { border-radius: 0 0 var(--r-2) var(--r-2); }
541
+ /* the stack is flush on both axes, because a card holding nothing but these
542
+ rows drops its own padding where `.card` is defined, so the edge rows take
543
+ the card's radius and the hover fill reaches its edge. Longhands, not the
544
+ shorthand: the two rules tie at (0,2,0) and a card with ONE row matches both,
545
+ so a shorthand pair hands it the later rule's square top corners, which used
546
+ to sit 20px inside the card and now sit on it. */
547
+ .setting-row:first-child {
548
+ border-start-start-radius: var(--r-2);
549
+ border-start-end-radius: var(--r-2);
550
+ }
551
+ .setting-row:last-child {
552
+ border-end-start-radius: var(--r-2);
553
+ border-end-end-radius: var(--r-2);
554
+ }
466
555
  .setting-row + .setting-row { border-top: 1px solid var(--line-soft); }
467
- .setting-row:hover { background: var(--surface-2); }
556
+ .setting-row:hover { background: var(--surface-2); --tint-ground: transparent; }
468
557
 
469
558
  .setting-label {
470
559
  grid-area: label;
@@ -519,12 +608,13 @@ input.btn-lg { line-height: 42px; }
519
608
  row-gap: var(--space-3);
520
609
  }
521
610
  .setting-control { justify-content: flex-start; }
522
- .setting-control .input,
611
+ .setting-control .input:not(.select-trigger),
523
612
  .setting-control .select { width: 100%; min-width: 0; }
524
613
  }
525
614
 
526
615
  .input {
527
616
  background: var(--well);
617
+ --tint-ground: transparent;
528
618
  border: 1px solid var(--line);
529
619
  border-radius: var(--r-1);
530
620
  height: var(--ctl);
@@ -552,7 +642,7 @@ input.btn-lg { line-height: 42px; }
552
642
  from what assistive tech reads. */
553
643
  .input-invalid, .input[aria-invalid="true"] {
554
644
  border-color: var(--danger);
555
- --ring-color: color-mix(in srgb, var(--danger) 30%, transparent);
645
+ --ring-color: color-mix(in srgb, var(--danger) 30%, var(--glass-ground));
556
646
  }
557
647
  .input-invalid:focus, .input[aria-invalid="true"]:focus { border-color: var(--danger); }
558
648
  /* every input-family control shares the disabled costume; before this the
@@ -590,6 +680,25 @@ select.input {
590
680
  background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%23676A74' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
591
681
  }
592
682
 
683
+ /* Every number field is a numeric readout, so the mono face and tabular
684
+ figures are the type's own costume, not the stepper's: a bare
685
+ `.input[type=number]` in a form reads the same rule. The spinners go for
686
+ the reason `.select` exists: they are UA chrome, they ignore the theme, and
687
+ they put a second pair of arrows over the ring cursor. Alignment stays with
688
+ the field: the stepper centres its value because the controls flank it, and
689
+ a form field keeps the start edge. `inputmode=numeric` is markup, not CSS. */
690
+ .input[type="number"] {
691
+ font-family: var(--font-mono);
692
+ font-variant-numeric: tabular-nums;
693
+ font-feature-settings: "tnum";
694
+ appearance: textfield;
695
+ }
696
+ .input[type="number"]::-webkit-outer-spin-button,
697
+ .input[type="number"]::-webkit-inner-spin-button {
698
+ appearance: none;
699
+ margin: 0;
700
+ }
701
+
593
702
  /* ---- Select (custom dropdown) ----
594
703
  A native <select> styles its box and nothing else: the popup is OS chrome, so
595
704
  it ignores the theme and puts the OS arrow back on screen over the ring
@@ -647,7 +756,7 @@ select.input {
647
756
  transition: --fold var(--dur-out-near) cubic-bezier(0.55, 0, 1, 0.45);
648
757
  }
649
758
  /* --dur-near, not --dur-far: the ladder puts 122px of travel at 260ms, and at
650
- 260ms this panel reads slow. Her call, 2026-08-29. */
759
+ 260ms this panel reads slow. */
651
760
  .select.open { --fold: 0; transition: --fold var(--dur-near) var(--ease-out); }
652
761
  /* an invalid field stays marked while it is open to be corrected, body and
653
762
  panel both. As a token swap it cannot be outranked by the open outline,
@@ -672,7 +781,7 @@ select.input {
672
781
  .select-trigger:focus { --edge: var(--accent); }
673
782
  .select-trigger:is(.input-invalid, [aria-invalid="true"]) { --edge: var(--danger); }
674
783
  /* `.select-value` truncates on `tokens.css`'s `.truncate` rule, which names it. */
675
- /* Square, so the sprite's 16-viewBox symbol drops in: her call 2026-09-02, over
784
+ /* Square, so the sprite's 16-viewBox symbol drops in, over
676
785
  keeping a hand-drawn 10 x 6 chevron nothing could check. 14px is the ladder's
677
786
  `.icon-sm` step, the one that sits beside 14px text. The glyph inside the box
678
787
  is 8.75 x 4.4 against the old 8 x 4, so the caret reads the same size and the
@@ -699,6 +808,7 @@ select.input {
699
808
  panel still on screen. Both were shipped, and both read as two objects. */
700
809
  .select:is(.open, .closing) > .select-trigger {
701
810
  background: color-mix(in oklab, var(--well), var(--surface-1) calc(var(--lift) * 100%));
811
+ --tint-ground: transparent;
702
812
  border-color: color-mix(in oklab, var(--open-edge), var(--edge) calc(var(--seal) * 100%));
703
813
  border-bottom-color: color-mix(in oklab, var(--line), var(--edge) calc(var(--seal) * 100%));
704
814
  box-shadow: 0 0 0 calc(var(--seal) * var(--ring)) var(--ring-color);
@@ -732,11 +842,12 @@ select.input {
732
842
  right: 0;
733
843
  z-index: 60;
734
844
  background: var(--surface-1);
845
+ --tint-ground: transparent;
735
846
  border: 1px solid var(--line);
736
847
  border-top: none; /* the trigger's own bottom border is the seam */
737
848
  border-radius: 0 0 var(--r-1) var(--r-1);
738
849
  box-shadow: var(--elev-float);
739
- padding: var(--space-1) 0;
850
+ padding: var(--space-1); /* the list panel's gutter */
740
851
  max-height: 240px;
741
852
  /* both axes named: a `visible` axis beside a scrolling one computes to `auto`,
742
853
  which would hang a horizontal scrollbar off any option wider than the trigger */
@@ -776,15 +887,17 @@ select.input {
776
887
  calc(-1 * var(--menu-bleed)));
777
888
  }
778
889
 
779
- .select-menu .menu-item { padding-inline: 12px; } /* lines up with the trigger's text */
890
+ /* 8 plus the panel's own 4px gutter: the text still lines up with the trigger's */
891
+ .select-menu .menu-item { padding-inline: 8px; }
780
892
  /* The trigger keeps DOM focus while the panel is open, so the arrow-key row
781
893
  carries its own mark. It is the row's HOVER costume, not a 2px accent edge:
782
894
  `.focus` lands on the selected row at open time, so the edge stacked against
783
895
  the panel's own accent border AND against that row's fill, which is the band
784
- plus capsule her 2026-09-02 ruling retired. The selected row keeps the fill as
785
- its one mark, so the two states never paint on one row. */
896
+ plus capsule the selected row no longer carries. The selected row keeps the
897
+ fill as its one mark, so the two states never paint on one row. */
786
898
  .select-menu .menu-item.focus:not([aria-selected="true"]) {
787
899
  background: var(--surface-2);
900
+ --tint-ground: transparent;
788
901
  color: var(--text);
789
902
  }
790
903
 
@@ -826,6 +939,16 @@ select.input {
826
939
  user-select: none;
827
940
  }
828
941
  .checkbox-label:hover, .radio-label:hover { color: var(--text); }
942
+ /* a disabled box takes its row with it, as `.input-group` takes its addons: the
943
+ label kept the pointer, its hover and its live colour, so a dead row read as
944
+ live. --text-disabled: 4.50:1 on --bg and 3.89 on --surface-1 dark, 3.76 and
945
+ 4.23 light, over the 3:1 floor for a control that cannot act. */
946
+ .checkbox-label:has(input:disabled),
947
+ .radio-label:has(input:disabled) {
948
+ color: var(--text-disabled);
949
+ cursor: not-allowed;
950
+ pointer-events: none;
951
+ }
829
952
 
830
953
  .checkbox-label input[type="checkbox"],
831
954
  .radio-label input[type="radio"] {
@@ -833,6 +956,7 @@ select.input {
833
956
  height: 16px;
834
957
  appearance: none;
835
958
  background: var(--well);
959
+ --tint-ground: transparent;
836
960
  border: 1px solid var(--line);
837
961
  border-radius: calc(var(--r-1) * 0.5);
838
962
  cursor: pointer;
@@ -848,6 +972,7 @@ select.input {
848
972
  .radio-label input[type="radio"]:checked,
849
973
  .checkbox-label input[type="checkbox"]:indeterminate {
850
974
  background: var(--accent);
975
+ --tint-ground: transparent;
851
976
  border-color: var(--accent);
852
977
  }
853
978
  /* the marks live on a pseudo that always exists: the check and the bar
@@ -860,6 +985,7 @@ select.input {
860
985
  position: absolute;
861
986
  inset: 3px;
862
987
  background: var(--on-accent);
988
+ --tint-ground: transparent;
863
989
  opacity: 0;
864
990
  transform: scale(0.4);
865
991
  transition: transform var(--dur-near) var(--ease-out), opacity var(--dur-near) var(--ease-out);
@@ -892,6 +1018,7 @@ select.input {
892
1018
  width: 38px;
893
1019
  height: 22px;
894
1020
  background: var(--surface-3);
1021
+ --tint-ground: transparent;
895
1022
  border-radius: var(--r-pill);
896
1023
  position: relative;
897
1024
  transition: background var(--dur-tint) var(--ease);
@@ -911,10 +1038,11 @@ select.input {
911
1038
  width: 16px;
912
1039
  height: 16px;
913
1040
  background: var(--text);
1041
+ --tint-ground: transparent;
914
1042
  border-radius: 50%;
915
1043
  transition: transform var(--dur-near) var(--ease);
916
1044
  }
917
- .toggle.on { background: var(--accent); }
1045
+ .toggle.on { background: var(--accent); --tint-ground: transparent; }
918
1046
  .toggle.on::after { transform: translateX(16px); }
919
1047
  /* A switch is a graphic, so the disabled costume moves its FILLS rather than a
920
1048
  text colour: the off track drops --surface-3 to --surface-2 and the on track
@@ -923,9 +1051,9 @@ select.input {
923
1051
  carries the state and has to clear 3:1 on both tracks: 4.00 and 3.47 dark, 4.65
924
1052
  and 4.75 light. --text-disabled measures 2.86 on the dark on-track and fails.
925
1053
  .on stays legible because a disabled control still reports where it stands. */
926
- .toggle:disabled { background: var(--surface-2); cursor: not-allowed; pointer-events: none; }
1054
+ .toggle:disabled { background: var(--surface-2); --tint-ground: transparent; cursor: not-allowed; pointer-events: none; }
927
1055
  .toggle:disabled.on { background: var(--accent-quiet); }
928
- .toggle:disabled::after { background: var(--text-4); }
1056
+ .toggle:disabled::after { background: var(--text-4); --tint-ground: transparent; }
929
1057
  .toggle-label { font-size: var(--t-3); color: var(--text-2); }
930
1058
 
931
1059
  /* ---- Disclosure (native details/summary) ----
@@ -975,25 +1103,33 @@ button.setting-row {
975
1103
  }
976
1104
 
977
1105
  /* ---- Card ----
978
- Rises toward the light: surface-1 over the page, a lit top edge, and a
979
- hue-matched shadow. No border, no shadow at rest beyond the edge. */
1106
+ Rises toward the light: surface-1 over the page, a crisp drawn line round
1107
+ its box, and no shadow at rest. The line is --card-line, a 1px box-shadow
1108
+ drawn outside the box so the content box never shrinks. */
980
1109
  /* One raised surface. `.card-tight` and `.card-roomy` reach `.card` alone, so a
981
1110
  stat card takes the default padding and nothing else changes. */
982
1111
  .card, .stat-card {
1112
+ position: relative;
983
1113
  background: var(--surface-1);
984
- border-radius: var(--r-2);
985
- box-shadow: var(--elev-1);
1114
+ --tint-ground: transparent;
1115
+ box-shadow: 0 0 0 1px var(--card-line);
986
1116
  padding: var(--space-5);
987
1117
  }
1118
+ /* the KPI tile takes the fourth, squircle step; every other card keeps --r-2 */
1119
+ .card { border-radius: var(--r-2); }
1120
+ .stat-card { border-radius: var(--r-4); }
988
1121
  .card-interactive {
989
1122
  cursor: pointer;
1123
+ text-decoration: none;
990
1124
  transition:
991
1125
  transform var(--dur-tint) var(--ease),
992
1126
  box-shadow var(--dur-tint) var(--ease);
993
1127
  }
1128
+ /* the lift deepens the contact shadow; while the card is raised, the line
1129
+ gives way to it */
994
1130
  .card-interactive:hover {
995
1131
  transform: translateY(-2px);
996
- box-shadow: var(--elev-2);
1132
+ box-shadow: var(--contact-2);
997
1133
  }
998
1134
  /* A card header and a modal header are one row: a space-between line under a
999
1135
  hairline. Only the padding tells them apart, so only the padding is written
@@ -1012,7 +1148,7 @@ button.setting-row {
1012
1148
  margin-bottom: var(--space-3);
1013
1149
  }
1014
1150
  /* A status tag anchors to the card's TOP right, never to the middle of the
1015
- header row. Her call 2026-09-02, and it settles `.stat-card` and the timeline
1151
+ header row, and it settles `.stat-card` and the timeline
1016
1152
  entry the same way for whenever either grows one. A header pairing a tag with
1017
1153
  a title PLUS a subtitle centres the tag against both lines, which is what put
1018
1154
  it mid-card.
@@ -1048,6 +1184,7 @@ button.setting-row {
1048
1184
  .tag {
1049
1185
  display: inline-flex;
1050
1186
  align-items: center;
1187
+ text-decoration: none;
1051
1188
  gap: 5px;
1052
1189
  font: 500 var(--t-1)/1 var(--font-sans);
1053
1190
  padding: 3px 8px;
@@ -1065,6 +1202,7 @@ button.setting-row {
1065
1202
  .tag-default,
1066
1203
  .tag-data {
1067
1204
  background: var(--surface-2);
1205
+ --tint-ground: transparent;
1068
1206
  color: var(--text-3);
1069
1207
  border-color: var(--line);
1070
1208
  }
@@ -1073,19 +1211,19 @@ button.setting-row {
1073
1211
  color: var(--accent-text);
1074
1212
  }
1075
1213
  .tag-success {
1076
- background: color-mix(in srgb, var(--success) 14%, transparent);
1214
+ background: color-mix(in srgb, var(--success) 14%, var(--tint-ground));
1077
1215
  color: color-mix(in srgb, var(--success) 80%, var(--text) 20%);
1078
1216
  }
1079
1217
  .tag-warning {
1080
- background: color-mix(in srgb, var(--warning) 14%, transparent);
1218
+ background: color-mix(in srgb, var(--warning) 14%, var(--tint-ground));
1081
1219
  color: color-mix(in srgb, var(--warning) 80%, var(--text) 20%);
1082
1220
  }
1083
1221
  .tag-danger {
1084
- background: color-mix(in srgb, var(--danger) 14%, transparent);
1222
+ background: color-mix(in srgb, var(--danger) 14%, var(--tint-ground));
1085
1223
  color: color-mix(in srgb, var(--danger) 80%, var(--text) 20%);
1086
1224
  }
1087
1225
  .tag-info {
1088
- background: color-mix(in srgb, var(--info) 14%, transparent);
1226
+ background: color-mix(in srgb, var(--info) 14%, var(--tint-ground));
1089
1227
  color: color-mix(in srgb, var(--info) 80%, var(--text) 20%);
1090
1228
  }
1091
1229
  [data-theme="light"] .tag-success { color: color-mix(in srgb, var(--success) 70%, var(--text) 30%); }
@@ -1096,12 +1234,21 @@ button.setting-row {
1096
1234
  /* ---- Table ---- */
1097
1235
  .table-wrap {
1098
1236
  background: var(--surface-1);
1237
+ --tint-ground: transparent;
1099
1238
  border-radius: var(--r-2);
1100
1239
  box-shadow: var(--elev-1);
1101
1240
  /* A too-wide table scrolls in its own box; clipping it amputates columns
1102
1241
  silently. Seven projects fixed this same defect independently. */
1103
1242
  overflow-x: auto;
1104
1243
  overflow-y: hidden;
1244
+ /* The global focus ring floats 3px outside the cell box, so a landing that
1245
+ stops a cell flush with the scroller edge amputates the first column's
1246
+ arc into the clip. scroll-padding gives scrollIntoView landings room to
1247
+ stop short of the edge (measured: 0px without, 8px with, chrome 152).
1248
+ Tab focus does not use it: chrome leaves a partially visible cell
1249
+ unscrolled, so a control flush with a clipped edge wants an inset ring
1250
+ instead. */
1251
+ scroll-padding-inline: 8px;
1105
1252
  }
1106
1253
  .table-wrap table {
1107
1254
  width: 100%;
@@ -1111,8 +1258,9 @@ button.setting-row {
1111
1258
  .table-wrap thead tr {
1112
1259
  border-bottom: 1px solid var(--line-soft);
1113
1260
  background: var(--well);
1261
+ --tint-ground: transparent;
1114
1262
  }
1115
- [data-theme="light"] .table-wrap thead tr { background: var(--surface-2); }
1263
+ [data-theme="light"] .table-wrap thead tr { background: var(--surface-2); --tint-ground: transparent; }
1116
1264
  /* THE tracked-caps eyebrow, and the three places it is sanctioned: a table
1117
1265
  header, a navigation-list group label, a weekday name. Not a merge into
1118
1266
  `tokens.css`'s `.label`, which declares 11px/1.4 where all three declare /1 and
@@ -1146,19 +1294,21 @@ button.setting-row {
1146
1294
  the 3px it had grown. What holds the sortable and plain labels together is
1147
1295
  `vertical-align` on the button, one screen down. A DESCENDANT selector, so the
1148
1296
  half pixel reaches the label in both places: moving only the plain one would
1149
- split the row again. Measured across the sheet's three tables, -0.88 / -0.62 /
1150
- -0.25 before and +0.12 / -0.12 / +0.25 after, with all five labels in the
1151
- sortable row on one range top and one ink top. What is left is DEVICE-PIXEL
1152
- PHASE and no stylesheet reaches it: two tables with the same content box read
1153
- 0.74px apart purely because they land on different fractional page positions. */
1154
- .table-wrap thead th .th-label { position: relative; top: 0.5px; }
1297
+ split the row again. With all five labels in the sortable row on one range top
1298
+ and one ink top, what is left is DEVICE-PIXEL PHASE and no stylesheet reaches
1299
+ it: a table's reading swings about a pixel with its fractional page position,
1300
+ measured -0.12 and +0.62 on one dashboard table 13.4px apart. So the nudge
1301
+ CENTRES that swing rather than fitting one table: at 0.5px the nine tables on
1302
+ disk read -0.25 to +0.62, at 0.25px -0.50 to +0.12, at the 0.5px budget's
1303
+ edge and never past it. */
1304
+ .table-wrap thead th .th-label { position: relative; top: 0.25px; }
1155
1305
  .table-wrap tbody tr {
1156
1306
  border-bottom: 1px solid var(--line-soft);
1157
1307
  /* the transition lives with the selection rules in the table-patterns
1158
1308
  block, which restate this selector at equal specificity later */
1159
1309
  }
1160
1310
  .table-wrap tbody tr:last-child { border-bottom: none; }
1161
- .table-wrap tbody tr:hover { background: var(--surface-2); }
1311
+ .table-wrap tbody tr:hover { background: var(--surface-2); --tint-ground: transparent; }
1162
1312
  .table-wrap tbody td {
1163
1313
  padding: 10px var(--space-4);
1164
1314
  color: var(--text-2);
@@ -1190,6 +1340,7 @@ button.setting-row {
1190
1340
  code {
1191
1341
  font: 400 var(--t-2)/1 var(--font-mono);
1192
1342
  background: var(--well);
1343
+ --tint-ground: transparent;
1193
1344
  border: 1px solid var(--line-soft);
1194
1345
  padding: 2px 6px;
1195
1346
  border-radius: var(--r-1);
@@ -1201,6 +1352,7 @@ code {
1201
1352
  kbd {
1202
1353
  font: 500 var(--t-1)/1 var(--font-mono);
1203
1354
  background: var(--surface-1);
1355
+ --tint-ground: transparent;
1204
1356
  border: 1px solid var(--line);
1205
1357
  border-bottom-width: 2px;
1206
1358
  border-radius: var(--r-1);
@@ -1212,6 +1364,7 @@ kbd {
1212
1364
  pre {
1213
1365
  font: 400 var(--t-2)/1.5 var(--font-mono);
1214
1366
  background: var(--well);
1367
+ --tint-ground: transparent;
1215
1368
  border: 1px solid var(--line-soft);
1216
1369
  border-radius: var(--r-1);
1217
1370
  padding: var(--space-3) var(--space-4);
@@ -1235,6 +1388,7 @@ pre code {
1235
1388
  gap: var(--space-3);
1236
1389
  padding: var(--space-2) var(--space-4);
1237
1390
  background: var(--well);
1391
+ --tint-ground: transparent;
1238
1392
  border: 1px solid var(--line-soft);
1239
1393
  border-bottom: none;
1240
1394
  border-radius: var(--r-1) var(--r-1) 0 0;
@@ -1271,7 +1425,7 @@ pre code {
1271
1425
  :hover > .code-copy, .code-copy:focus-visible { opacity: 1; }
1272
1426
  .code-copy:hover, .code-copy:focus-visible {
1273
1427
  color: var(--text);
1274
- background: color-mix(in srgb, var(--surface-2) 80%, transparent);
1428
+ background: color-mix(in srgb, var(--surface-2) 80%, var(--tint-ground));
1275
1429
  }
1276
1430
  .code-copy svg { width: 14px; height: 14px; }
1277
1431
  .code-copy .icon-check { animation: check-pop var(--dur-near) var(--ease-out); }
@@ -1297,8 +1451,8 @@ pre code {
1297
1451
  /* ---- Diff marks ----
1298
1452
  The tint carries the semantics; the text keeps its own reading contrast. */
1299
1453
  .diff-add, .diff-remove { border-radius: 2px; padding: 0 1px; }
1300
- .diff-add { background: color-mix(in srgb, var(--success) 22%, transparent); }
1301
- .diff-remove { background: color-mix(in srgb, var(--danger) 22%, transparent); }
1454
+ .diff-add { background: color-mix(in srgb, var(--success) 22%, var(--tint-ground)); }
1455
+ .diff-remove { background: color-mix(in srgb, var(--danger) 22%, var(--tint-ground)); }
1302
1456
 
1303
1457
  /* ---- Stat / KPI ---- */
1304
1458
  .stat-label {
@@ -1326,8 +1480,9 @@ pre code {
1326
1480
  .stat-value,
1327
1481
  .ring-value,
1328
1482
  .claim-value { overflow-wrap: anywhere; }
1329
- .stat-value,
1483
+ .stat-value { font-size: var(--t-7); letter-spacing: -0.03em; }
1330
1484
  .ring-value { font-size: var(--t-6); }
1485
+ @media (max-width: 768px) { .stat-value { font-size: var(--t-6); } }
1331
1486
  .stat-delta {
1332
1487
  font: 400 var(--t-2)/1 var(--font-sans);
1333
1488
  margin-top: var(--space-2);
@@ -1355,22 +1510,28 @@ pre code {
1355
1510
  height: 2px;
1356
1511
  border-radius: var(--r-pill);
1357
1512
  background: var(--accent-2);
1513
+ --tint-ground: transparent;
1358
1514
  }
1359
- /* Vertical form: marks a row in a list or a band in a chart. */
1515
+ /* Vertical form: marks a row in a list or a band in a chart. The bar and its
1516
+ gap live in the container's own gutter, pulled back by exactly their width,
1517
+ so the marked line's first glyph lands where every plain line's does: with
1518
+ the padding alone the two measured 14px apart. */
1360
1519
  .annot-mark {
1361
1520
  border-inline-start: 2px solid var(--accent-2);
1362
1521
  padding-inline-start: var(--space-3);
1522
+ margin-inline-start: calc(-2px - var(--space-3));
1363
1523
  }
1364
1524
 
1365
1525
  /* ---- Sparkline ----
1366
1526
  preserveAspectRatio="none" stretches the viewBox to the card's width,
1367
1527
  which would thin and fatten a declared stroke with the card (the
1368
- scaled-stroke trap). non-scaling-stroke pins the drawn
1528
+ scaled-stroke trap, cursor.md). non-scaling-stroke pins the drawn
1369
1529
  width at 1.5 css px whatever the stretch. Line contrast on the card,
1370
- WCAG 1.4.11's 3:1 graphics floor: series-1 5.21:1 dark, 5.69:1 light
1530
+ WCAG 1.4.11's 3:1 graphics floor: series-1 5.21:1 dark, 5.81:1 light
1371
1531
  on --surface-1, measured. */
1372
1532
  .stat-spark {
1373
1533
  --spark: var(--chart-series-1);
1534
+ --series: var(--spark); /* the area and end point ui.js builds read --series */
1374
1535
  height: 28px;
1375
1536
  margin-top: var(--space-3);
1376
1537
  }
@@ -1431,11 +1592,12 @@ pre code {
1431
1592
  }
1432
1593
  .toast {
1433
1594
  background: var(--surface-1);
1595
+ --tint-ground: transparent;
1434
1596
  border-radius: var(--r-2);
1435
1597
  box-shadow: var(--elev-float);
1436
1598
  padding: var(--space-4);
1437
1599
  display: flex;
1438
- align-items: flex-start;
1600
+ align-items: center;
1439
1601
  gap: var(--space-3);
1440
1602
  pointer-events: all;
1441
1603
  animation: toast-in var(--dur-near) var(--ease-out) both;
@@ -1453,16 +1615,15 @@ pre code {
1453
1615
  .callout-icon,
1454
1616
  .form-summary-icon,
1455
1617
  .banner-icon { flex-shrink: 0; }
1456
- /* the three that sit beside a first line take a 1px optical nudge; the banner's
1457
- icon centres on the BODY instead, her call 2026-09-02, so it does not */
1458
- .toast-icon,
1459
- .callout-icon,
1460
- .form-summary-icon { margin-top: 1px; }
1461
- /* an icon dropped in without its own width lays out at 0; the slot states it */
1462
- .toast-icon svg,
1618
+ /* An icon dropped in without its own width lays out at 0, so the slot states
1619
+ it. `display: block` is what centres it: an inline svg sits on the text
1620
+ baseline, so the slot's line box runs 22.69px around a 16px glyph and the
1621
+ descender space rides the glyph 3.34px above the slot's own centre. Measured
1622
+ on every panel but the banner, which carried this one declaration alone. */
1623
+ .toast-icon > svg,
1463
1624
  .callout-icon > svg,
1464
1625
  .form-summary-icon > svg,
1465
- .banner-icon svg { width: 16px; height: 16px; }
1626
+ .banner-icon > svg { display: block; width: 16px; height: 16px; }
1466
1627
  .toast-body { flex: 1; min-width: 0; }
1467
1628
  .toast-msg { font-size: var(--t-2); color: var(--text-3); margin-top: 3px; }
1468
1629
 
@@ -1477,6 +1638,9 @@ pre code {
1477
1638
  .callout,
1478
1639
  .banner {
1479
1640
  display: flex;
1641
+ /* the icon centres on the BODY: a 1px nudge onto the
1642
+ first line put a two-line body's icon high by half the second line */
1643
+ align-items: center;
1480
1644
  gap: var(--space-3);
1481
1645
  border-radius: var(--r-2);
1482
1646
  box-shadow: var(--lit-1);
@@ -1484,7 +1648,7 @@ pre code {
1484
1648
  .form-summary,
1485
1649
  .callout { padding: 12px var(--space-4); }
1486
1650
  .callout,
1487
- .banner { background: var(--surface-1); }
1651
+ .banner { background: var(--surface-1); --tint-ground: transparent; }
1488
1652
  /* The icon carries the semantic, so the variant colours it. Leaving that to an
1489
1653
  inline style is how a callout ships with a --text icon and reads as neutral. */
1490
1654
  .callout-info > .callout-icon { color: var(--info); }
@@ -1510,6 +1674,7 @@ pre code {
1510
1674
  script sets transform-origin per side, never the centre. */
1511
1675
  .popover {
1512
1676
  background: var(--surface-1);
1677
+ --tint-ground: transparent;
1513
1678
  border-radius: var(--r-2);
1514
1679
  box-shadow: var(--elev-float);
1515
1680
  overflow: hidden;
@@ -1544,7 +1709,14 @@ pre code {
1544
1709
  display: flex;
1545
1710
  align-items: center;
1546
1711
  gap: var(--space-3);
1547
- padding: 8px 14px;
1712
+ /* 10 and not 14: the panel's own 4px gutter supplies the rest, so the text
1713
+ column sits where it always did, 14px in from the panel edge. */
1714
+ padding: 8px 10px;
1715
+ /* THE row radius, in every state. A radius that arrives with a state is a
1716
+ corner that animates, and it left a hovered row's square band meeting a
1717
+ selected row's rounded one; a state rule declaring one is that defect
1718
+ restated. */
1719
+ border-radius: var(--r-1);
1548
1720
  font-size: var(--t-3);
1549
1721
  color: var(--text-2);
1550
1722
  cursor: pointer;
@@ -1556,24 +1728,84 @@ pre code {
1556
1728
  /* THE neutral hover, and every borderless control and list row in the system
1557
1729
  takes it from here. --text on --surface-2 measures 8.56:1 dark, 11.28:1 light.
1558
1730
  It has to sit at or before this point, not with the icon buttons further down:
1559
- `.menu-item.active`, `.menu-item.danger` and `.input-step:disabled` each tie it
1560
- at (0,2,0) and have to win by coming after. `.disclosure[open] > summary` ties
1561
- it too, at (0,2,1), inertly, since both spell --text. */
1562
- .close:hover,
1731
+ `.menu-item.active` and `.input-step:disabled` each tie it at (0,2,0) and have
1732
+ to win by coming after. `.menu-item.danger` ties it too, inertly: its own
1733
+ `:hover` at (0,3,0) sets the hovered label. `.disclosure[open] > summary` ties
1734
+ it too, at (0,2,1), inertly, since both spell --text. The remove control is
1735
+ not a member: removing is destructive, and its hover is its own, declared
1736
+ with `.close` in the one-remove-control section. */
1563
1737
  .copy-chip-btn:hover,
1564
1738
  .select-clear:hover,
1565
1739
  .row-toggle:hover,
1566
1740
  .disclosure summary:hover,
1567
1741
  .input-step:hover,
1568
- .menu-item:hover { background: var(--surface-2); color: var(--text); }
1742
+ .menu-item:hover { background: var(--surface-2); --tint-ground: transparent; color: var(--text); }
1569
1743
  /* The chosen row, whether a menu marks it with `.active` or a select marks it
1570
- with aria. It has to sit AFTER `:hover`: the two tie at (0,2,0) and a hovered
1571
- chosen row must keep its accent rather than repaint grey. */
1744
+ with aria. It has to sit AFTER `:hover`, and the ORDER IS ONLY LOAD-BEARING
1745
+ FOR THE FIRST SELECTOR: `.menu-item.active` ties the hover at (0,2,0) and
1746
+ would repaint grey under the pointer from any earlier position, while the
1747
+ select's own selector measures (0,3,0) and wins from anywhere. */
1572
1748
  .menu-item.active,
1573
1749
  .select-menu .menu-item[aria-selected="true"] { color: var(--accent-text); background: var(--accent-quiet); }
1574
1750
  .menu-item.danger { color: var(--danger); }
1575
- .menu-item.danger:hover { background: color-mix(in srgb, var(--danger) 10%, transparent); }
1576
- .menu-divider { height: 1px; background: var(--line-soft); margin: 3px 0; }
1751
+ .menu-item.danger:hover { background: color-mix(in srgb, var(--danger) 10%, var(--tint-ground)); color: var(--danger-text); }
1752
+ /* The arrow-key row carries the hover costume, the same mark the select's
1753
+ listbox takes for its focused option. It has to sit after `.active`: a
1754
+ focused active row keeps its accent fill as its one mark. The danger row
1755
+ keeps its danger hover, never the neutral band. */
1756
+ .popover-menu .menu-item.focus:not(.active) {
1757
+ background: var(--surface-2);
1758
+ --tint-ground: transparent;
1759
+ color: var(--text);
1760
+ }
1761
+ .popover-menu .menu-item.danger.focus:not(.active) {
1762
+ background: color-mix(in srgb, var(--danger) 10%, var(--tint-ground));
1763
+ color: var(--danger-text);
1764
+ }
1765
+ /* 1px and not 3: the lane below contributes the other 2 on each side, so the
1766
+ painted gap around a divider stays the 3px it measured before the lane.
1767
+ `flex-shrink: 0` because the lane makes its panel a flex column and the
1768
+ select's is height-bounded: a row is floored by its own min-content, an empty
1769
+ 1px rule is floored by nothing, and it measured 0px tall with no hairline. */
1770
+ .menu-divider { height: 1px; flex-shrink: 0; background: var(--line-soft); --tint-ground: transparent; margin: 1px 0; }
1771
+
1772
+ /* ---- The list panel's one gutter ----
1773
+ A row in a list panel sits in a 4px inset with --r-1 corners in every state
1774
+ and a 2px lane to the next row, so no two bands touch and no band reaches the
1775
+ panel's own corner.
1776
+
1777
+ 4px is not a taste: on the --r-2 popover a row's corner is concentric with
1778
+ the panel's, since --r-2 - --space-1 is exactly --r-1. A clipped rounded
1779
+ panel bites r - sqrt(r^2 - (r - gap)^2) out of anything reaching its corner:
1780
+ the whole 14 at no gap, still 4.20 at 4px if the row's own corner were
1781
+ square. It is the CONCENTRICITY that takes the bite to nothing, not the
1782
+ inset alone. Both panels clip at their PADDING box, one glass border inside
1783
+ the radius they declare, so the identity lands a pixel off rather than dead
1784
+ on: 13 against the row's 10 at a 4px inset here, 9 against 10 in the select,
1785
+ whose panel rounds at --r-1. Either way the row's own corner leaves the clip
1786
+ nothing to take.
1787
+
1788
+ THE GUTTER IS KEYED ON WHAT THE PANEL HOLDS, not on `.popover-menu`: one page
1789
+ writes a bare `.popover` around its rows, and a contract a consumer can miss
1790
+ by one class is the contract that ships the defect back. `.select-menu` is
1791
+ not a `.popover`, and the palette's rows sit a level down, so those two state
1792
+ their own; the lane is shared, because the lane is one number.
1793
+
1794
+ The lane is a gap and not a margin because `.daterange-presets` turns into a
1795
+ wrapping row under 560px, where a block margin staggers the line instead of
1796
+ separating it. */
1797
+ .popover:has(> .menu-item) { padding: var(--space-1); }
1798
+ .popover:has(> .menu-item),
1799
+ .select-menu,
1800
+ .palette-group,
1801
+ .nav-group,
1802
+ .tree,
1803
+ .tree-group,
1804
+ .tree li {
1805
+ display: flex;
1806
+ flex-direction: column;
1807
+ gap: 2px;
1808
+ }
1577
1809
 
1578
1810
  /* ---- Tooltip ----
1579
1811
  The popover shell with a placement costume: surface-1 plus the float
@@ -1607,21 +1839,22 @@ pre code {
1607
1839
  .popover.tooltip { animation: tooltip-in-top var(--dur-near) var(--ease-out) both; }
1608
1840
  .popover.tooltip.below { animation-name: tooltip-in-below; }
1609
1841
  .popover.tooltip.closing { animation: tooltip-out-top var(--dur-out-near) var(--ease) both; }
1610
- .popover.tooltip.below.closing { animation-name: tooltip-out-below; }
1842
+ /* the below variant lifts as it leaves, the same move the palette exits on, so
1843
+ it takes that keyframe rather than a fourth copy of it */
1844
+ .popover.tooltip.below.closing { animation-name: lift-out; }
1611
1845
  @keyframes tooltip-in-top { from { opacity: 0; transform: translateY(4px) scale(0.98); } }
1612
1846
  @keyframes tooltip-in-below { from { opacity: 0; transform: translateY(-4px) scale(0.98); } }
1613
1847
  @keyframes tooltip-out-top { to { opacity: 0; transform: translateY(4px) scale(0.99); } }
1614
- @keyframes tooltip-out-below { to { opacity: 0; transform: translateY(-4px) scale(0.99); } }
1615
1848
 
1616
1849
  /* Reduced motion: fades stay, transforms go. Restated at each rule's own
1617
1850
  specificity; a shorter selector here is outranked and the transform
1618
1851
  comes back. */
1619
1852
  @media (prefers-reduced-motion: reduce) {
1620
- .popover.tooltip, .popover.tooltip.below { animation: tooltip-in-fade var(--dur-near) var(--ease) both; }
1621
- .popover.tooltip.closing, .popover.tooltip.below.closing { animation: tooltip-out-fade var(--dur-out-near) var(--ease) both; }
1853
+ .popover.tooltip, .popover.tooltip.below { animation: fade-in var(--dur-near) var(--ease) both; }
1854
+ .popover.tooltip.closing, .popover.tooltip.below.closing { animation: fade-out var(--dur-out-near) var(--ease) both; }
1622
1855
  }
1623
- @keyframes tooltip-in-fade { from { opacity: 0; } to { opacity: 1; } }
1624
- @keyframes tooltip-out-fade { to { opacity: 0; } }
1856
+ @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
1857
+ @keyframes fade-out { to { opacity: 0; } }
1625
1858
 
1626
1859
  /* ---- Command palette ----
1627
1860
  The popover shell dropped over the page from the top centre: a search head,
@@ -1667,12 +1900,13 @@ pre code {
1667
1900
  }
1668
1901
  /* its own exit, outranking the shell's at (0,3,0): the panel is centred, so it
1669
1902
  leaves the way it arrived rather than toward a trigger's edge */
1670
- .popover.palette.closing { animation: palette-out var(--dur-out-near) var(--ease) both; }
1671
- @keyframes palette-out { to { opacity: 0; transform: translateY(-4px) scale(0.99); } }
1903
+ .popover.palette.closing { animation: lift-out var(--dur-out-near) var(--ease) both; }
1904
+ @keyframes lift-out { to { opacity: 0; transform: translateY(-4px) scale(0.99); } }
1672
1905
 
1673
1906
  /* ---- Search head ----
1674
- 14px inline padding here, on the group labels and on `.menu-item` itself, so
1675
- the icon column and every text column share one gutter down the panel. */
1907
+ Every column down the panel starts 14px in: the head states that gutter
1908
+ itself, and the group labels and the rows reach it as their own 10px inside
1909
+ the list panel's 4px. */
1676
1910
  .palette-search {
1677
1911
  display: flex;
1678
1912
  align-items: center;
@@ -1691,8 +1925,12 @@ pre code {
1691
1925
  font: 400 var(--t-4)/1.4 var(--font-sans);
1692
1926
  color: var(--text); /* on the glass panel: 6.48:1 dark, 10.76:1 light */
1693
1927
  }
1694
- /* --text-placeholder resolves to --glass-dim inside the panel: 4.67:1 dark, 4.72 light */
1695
- .palette-input::placeholder { color: var(--text-placeholder); }
1928
+ /* the one placeholder painted straight on a panel rather than a --well, so it
1929
+ reads --text-4, the panel's dim text: on the glass panel --text-4 re-floors to
1930
+ --glass-dim (4.67:1 dark, 4.72:1 light), and on the opaque --surface-1 it holds
1931
+ 4.73:1 dark / 5.55:1 light, where the --well-tuned --text-placeholder falls to
1932
+ 3.89:1 dark. */
1933
+ .palette-input::placeholder { color: var(--text-4); }
1696
1934
  /* The dialog holds focus here for its whole life and nothing else inside it
1697
1935
  takes focus, so a ring around the only focusable element names nothing: the
1698
1936
  caret marks the field, the accent edge marks the keyboard's row. The base
@@ -1711,10 +1949,10 @@ pre code {
1711
1949
  }
1712
1950
  /* each group carries its own breathing room, so a group the query filtered out
1713
1951
  contributes none and the list needs no adjacency rule to stay even */
1714
- .palette-group { padding: var(--space-2) 0; }
1952
+ .palette-group { padding: var(--space-2) var(--space-1); } /* block rhythm, then the list panel's gutter */
1715
1953
  /* the sidebar group label's treatment: `.label` carries the caps, the tracking
1716
1954
  and --text-3, which is --glass-dim inside this panel: 4.67:1 dark, 4.72 light */
1717
- .palette-group-label { padding: var(--space-1) 14px; }
1955
+ .palette-group-label { padding: var(--space-1) 10px; }
1718
1956
 
1719
1957
  /* the sidebar's icon treatment: dimmed at rest, full on the active row.
1720
1958
  --icon-dim is 0.7 on an opaque surface and 0.90 on glass, which this panel
@@ -1724,8 +1962,8 @@ pre code {
1724
1962
  .palette-row.active > svg { opacity: 1; }
1725
1963
  .palette-name { flex: 1 1 auto; min-width: 0; } /* a flex child needs this before .truncate can bite */
1726
1964
  /* The active row is marked by the shared active-row band and nothing else, and
1727
- `.menu-item.active` supplies --accent-text. On glass that fill is --glass-quiet,
1728
- laid down opaque so it cannot composite twice: --accent-text measures 5.95:1
1965
+ `.menu-item.active` supplies --accent-text. On glass that fill is the panel's opaque
1966
+ quiet mix, laid down so it cannot composite twice: --accent-text measures 5.95:1
1729
1967
  dark and 4.89 light on it.
1730
1968
  The palette marks its arrow-key row `.active`, so it has one row state and no
1731
1969
  `.focus`: `initSelect` is the only controller that writes that class, over
@@ -1738,7 +1976,7 @@ pre code {
1738
1976
  --glass-dim 6.17:1 and 4.96:1 on the shortcut sheet's glass. */
1739
1977
  .palette-hint kbd,
1740
1978
  .palette-search kbd,
1741
- .shortcut-keys kbd { background: var(--surface-2); }
1979
+ .shortcut-keys kbd { background: var(--surface-2); --tint-ground: transparent; }
1742
1980
 
1743
1981
  /* No match: a row that names the fix, built from what was typed. */
1744
1982
  .palette-empty {
@@ -1750,9 +1988,8 @@ pre code {
1750
1988
  @media (prefers-reduced-motion: reduce) {
1751
1989
  /* restated at the exit rule's own (0,3,0). A bare `.popover` here is
1752
1990
  outranked and the panel keeps its move. */
1753
- .popover.palette.closing { animation: palette-out-fade var(--dur-out-near) var(--ease) both; }
1991
+ .popover.palette.closing { animation: fade-out var(--dur-out-near) var(--ease) both; }
1754
1992
  }
1755
- @keyframes palette-out-fade { to { opacity: 0; } }
1756
1993
 
1757
1994
  /* ---- Avatar ---- */
1758
1995
  .avatar {
@@ -1778,12 +2015,14 @@ pre code {
1778
2015
  .progress {
1779
2016
  height: 3px;
1780
2017
  background: var(--surface-2);
2018
+ --tint-ground: transparent;
1781
2019
  border-radius: var(--r-pill);
1782
2020
  overflow: hidden;
1783
2021
  }
1784
2022
  .progress-bar {
1785
2023
  height: 100%;
1786
2024
  background: var(--accent);
2025
+ --tint-ground: transparent;
1787
2026
  border-radius: var(--r-pill);
1788
2027
  transition: width var(--dur-far) var(--ease);
1789
2028
  }
@@ -1791,6 +2030,7 @@ pre code {
1791
2030
  /* ---- Skeleton (no shimmer, by spec) ---- */
1792
2031
  .skeleton {
1793
2032
  background: var(--surface-2);
2033
+ --tint-ground: transparent;
1794
2034
  border-radius: var(--r-1);
1795
2035
  }
1796
2036
 
@@ -1814,16 +2054,24 @@ pre code {
1814
2054
  .empty-title { font: 500 var(--t-4)/1.3 var(--font-sans); color: var(--text); }
1815
2055
  .empty-state .btn { margin-top: var(--space-3); }
1816
2056
 
1817
- /* ---- Spinner (loading outside buttons) ---- */
2057
+ /* ---- Spinner (loading outside buttons) ----
2058
+ The ring is a graphic, so it holds the 3:1 floor: --accent is 7.49:1 on
2059
+ --bg and 6.48 on --surface-1 dark, 4.09 / 4.60 light. --accent-line was
2060
+ 2.25 / 1.56 on --bg. */
1818
2061
  .spinner {
1819
2062
  display: inline-block;
1820
2063
  width: 14px;
1821
2064
  height: 14px;
1822
- border: 2px solid var(--accent-line);
2065
+ border: 2px solid var(--accent);
1823
2066
  border-top-color: transparent;
1824
2067
  border-radius: 50%;
1825
2068
  animation: spin 0.6s linear infinite;
1826
2069
  }
2070
+ /* rests under reduce like the button's ring; the consumers keep their own
2071
+ done-state cues beside it (a check glyph, a chevron, a dirty hairline) */
2072
+ @media (prefers-reduced-motion: reduce) {
2073
+ .spinner { animation: none; }
2074
+ }
1827
2075
 
1828
2076
  /* ---- Modal ----
1829
2077
  Four projects hand-built this against "out of scope". The entrance
@@ -1841,6 +2089,7 @@ pre code {
1841
2089
  }
1842
2090
  .modal {
1843
2091
  background: var(--surface-1);
2092
+ --tint-ground: transparent;
1844
2093
  border-radius: var(--r-3);
1845
2094
  box-shadow: var(--elev-float);
1846
2095
  width: min(560px, 100%);
@@ -1908,30 +2157,47 @@ pre code {
1908
2157
  white-space: nowrap;
1909
2158
  }
1910
2159
 
1911
- /* ---- Range slider ---- */
2160
+ /* ---- Range slider ----
2161
+ Real elements, no native range: a UA shadow thumb paints the platform pointer over
2162
+ the ring whatever cursor the page declares (the thumb
2163
+ pseudo computed `none` and the pointer still painted), the same family the native
2164
+ select and the file input retired for. The thumb rides the fill's right edge, so one
2165
+ width drives both. */
1912
2166
  .slider {
1913
- -webkit-appearance: none;
1914
- appearance: none;
2167
+ position: relative;
2168
+ display: flex;
2169
+ align-items: center;
2170
+ width: 100%;
2171
+ height: 14px;
2172
+ cursor: pointer;
2173
+ touch-action: none;
2174
+ }
2175
+ .slider-track {
2176
+ position: relative;
1915
2177
  width: 100%;
1916
2178
  height: 4px;
1917
2179
  background: var(--surface-2);
2180
+ --tint-ground: transparent;
1918
2181
  border-radius: var(--r-pill);
1919
- cursor: pointer;
1920
2182
  }
1921
- .slider::-webkit-slider-thumb {
1922
- -webkit-appearance: none;
1923
- width: 14px; height: 14px;
2183
+ .slider-fill {
2184
+ position: relative; /* the thumb's containing block: without it the thumb anchors to the track and ignores the fill width */
2185
+ height: 100%;
2186
+ width: 0;
1924
2187
  background: var(--accent);
1925
- border: none;
1926
- border-radius: 50%;
1927
- cursor: pointer;
2188
+ --tint-ground: transparent;
2189
+ border-radius: var(--r-pill);
1928
2190
  }
1929
- .slider::-moz-range-thumb {
1930
- width: 14px; height: 14px;
1931
- background: var(--accent);
1932
- border: none;
2191
+ .slider-thumb {
2192
+ position: absolute;
2193
+ right: -7px;
2194
+ top: 50%;
2195
+ transform: translateY(-50%);
2196
+ width: 14px;
2197
+ height: 14px;
1933
2198
  border-radius: 50%;
1934
- cursor: pointer;
2199
+ background: var(--accent);
2200
+ --tint-ground: transparent;
1935
2201
  }
1936
2202
  .slider:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
1937
2203
 
@@ -1958,13 +2224,19 @@ pre code {
1958
2224
  see "Reveal JS" in the skill. */
1959
2225
  .section-sticky {
1960
2226
  position: sticky;
1961
- /* the navbar's own height, or the header pins 8px inside a bar that outranks
1962
- it on z-index and loses its top edge behind it */
1963
- top: var(--navbar-h);
2227
+ /* --pin-top, never a bar's own height: it is the height of whatever bar this
2228
+ page actually holds, derived at the root beside the navbar and the topbar,
2229
+ and a sidebar shell holds none, so the header pins to the viewport top.
2230
+ It has to be the bar that is really there, or the header pins inside one
2231
+ that outranks it on z-index and loses its top edge behind it - or, on a
2232
+ page with no bar at all, hangs a bar's height below nothing. */
2233
+ top: var(--pin-top);
1964
2234
  z-index: 5;
1965
2235
  width: 100%;
1966
2236
  padding: var(--space-2) 0;
1967
- background: var(--bg);
2237
+ /* no tone at rest: a header in flow paints over whatever the page shows
2238
+ there (the sky, a band), and the bar's tone exists only while stuck */
2239
+ background: transparent;
1968
2240
  /* the hairline exists in both states so the stuck swap moves a colour,
1969
2241
  never adds a border pixel */
1970
2242
  border-bottom: 1px solid transparent;
@@ -1987,6 +2259,7 @@ pre code {
1987
2259
  hairline is what says it is there. An engine with no backdrop-filter and
1988
2260
  a viewer with reduced transparency then see one surface, not two. */
1989
2261
  background: var(--bg);
2262
+ --tint-ground: transparent;
1990
2263
  /* full-bleed: the bar spans the viewport while its toolbar stays on the
1991
2264
  page column, via the matching inline padding. --vp-w is the
1992
2265
  scrollbar-excluded viewport width the sticky controller writes;
@@ -2009,9 +2282,13 @@ pre code {
2009
2282
  .section-sticky, .section-sticky.is-stuck { transition: none; }
2010
2283
  }
2011
2284
  /* A flush card header takes the card's top radius, the same way the setting
2012
- row's edge rows do. The stuck state spans the viewport, so it drops the
2013
- radius with the rest of the full-bleed swap. */
2014
- .card > .section-sticky:first-child { border-radius: var(--r-2) var(--r-2) 0 0; }
2285
+ row's edge rows do, and paints the card tone while it rides inside the
2286
+ card; the stuck state spans the viewport as a page bar and keeps --bg. */
2287
+ .card > .section-sticky:first-child {
2288
+ background: var(--surface-1);
2289
+ --tint-ground: transparent;
2290
+ border-radius: var(--r-2) var(--r-2) 0 0;
2291
+ }
2015
2292
  .card > .section-sticky.is-stuck { border-radius: 0; }
2016
2293
  /* the header's toolbar is single-line chrome, not the labeled-field row
2017
2294
  .toolbar defaults to (flex-end aligns a stack of labelled fields at the
@@ -2038,13 +2315,14 @@ pre code {
2038
2315
  z-index: 1100;
2039
2316
  padding: var(--space-2) var(--space-4);
2040
2317
  background: var(--surface-1);
2318
+ --tint-ground: transparent;
2041
2319
  color: var(--text);
2042
2320
  border-radius: var(--r-pill);
2043
2321
  text-decoration: none;
2044
2322
  transform: translateY(-160%);
2045
2323
  /* the float shadow belongs to the revealed chip. The hidden one parks
2046
2324
  60px above the viewport, but its shadow still painted ~4 levels deep
2047
- into the top-left rows (measured 2026-08-31, her "shadow on top left
2325
+ into the top-left rows (measured 2026-08-31, the "shadow on top left
2048
2326
  side" report); visibility:hidden would drop the link from the tab
2049
2327
  order, so the shadow waits for focus instead. */
2050
2328
  box-shadow: none;
@@ -2080,7 +2358,7 @@ html:has(.reveal-hover:focus-visible) :focus-within > .reveal-hover { opacity: 1
2080
2358
  /* ---- Scrollbar ----
2081
2359
  THE NATIVE ONE IS GONE, and the reason is the cursor. A scrollbar is a UA
2082
2360
  shadow tree, so `cursor: none` does not reach it and the platform pointer
2083
- paints over the ring for as long as you drag one: her call 2026-09-02, the
2361
+ paints over the ring for as long as you drag one, the
2084
2362
  same family as the stretched file input and the native `<select>` popup,
2085
2363
  both already retired for it. `initScrollbars` draws a real element instead,
2086
2364
  which the ring paints over like any other surface.
@@ -2107,22 +2385,23 @@ html.has-drawn-scrollbars *::-webkit-scrollbar { width: 0; height: 0; }
2107
2385
  position: fixed;
2108
2386
  z-index: 999; /* over the modal, under the toast stack that can cover it */
2109
2387
  background: var(--scrollbar-thumb);
2388
+ --tint-ground: transparent;
2110
2389
  border-radius: var(--r-pill);
2111
2390
  opacity: 0;
2112
2391
  pointer-events: none;
2113
2392
  touch-action: none;
2114
2393
  /* THE RING READS ITS HOVER STATE OFF THIS DECLARATION. `cursor-rules.js`
2115
- collects every selector declaring `cursor: pointer` and the ring tightens
2116
- over that list, so the drawn thumb is a target like any other control and
2394
+ collects every selector declaring `cursor: pointer` and the ring takes its
2395
+ hover state over that list, so the drawn thumb is a target like any other control and
2117
2396
  the press state follows it through a drag. The value itself never paints
2118
- while the ring is armed: `custom-cursor` forces `cursor: none` on
2119
- everything, and the bar exists only on a page whose module ran. */
2397
+ while the ring is armed: `custom-cursor` (on v3, beside `cursor-v3` or `#cursor-paint`)
2398
+ forces `cursor: none` on everything, and the bar exists only on a page whose module ran. */
2120
2399
  cursor: pointer;
2121
2400
  transition: opacity var(--dur-tint) var(--ease), background var(--dur-tint) var(--ease);
2122
2401
  }
2123
2402
  .ui-scrollbar.is-on { opacity: 1; pointer-events: auto; }
2124
2403
  .ui-scrollbar:hover,
2125
- .ui-scrollbar.is-drag { background: var(--text-4); }
2404
+ .ui-scrollbar.is-drag { background: var(--text-4); --tint-ground: transparent; }
2126
2405
  .ui-scrollbar-y { width: 6px; }
2127
2406
  .ui-scrollbar-x { height: 6px; }
2128
2407
 
@@ -2130,6 +2409,7 @@ html.has-drawn-scrollbars *::-webkit-scrollbar { width: 0; height: 0; }
2130
2409
  hr, .divider {
2131
2410
  height: 1px;
2132
2411
  background: var(--line);
2412
+ --tint-ground: transparent;
2133
2413
  border: none;
2134
2414
  margin: var(--space-6) 0;
2135
2415
  }
@@ -2182,6 +2462,7 @@ hr, .divider {
2182
2462
  --cbtn-slot: 0px;
2183
2463
  --cbtn-rise: 0px;
2184
2464
  background: var(--surface-1);
2465
+ --tint-ground: transparent;
2185
2466
  border-right: 1px solid var(--line-soft);
2186
2467
  width: 252px;
2187
2468
  display: flex;
@@ -2211,18 +2492,50 @@ hr, .divider {
2211
2492
  margin-bottom: var(--space-3);
2212
2493
  flex-shrink: 0; /* pinned: the nav is the only item that gives */
2213
2494
  }
2214
- .logo-mark {
2215
- width: 28px;
2216
- height: 28px;
2217
- border-radius: var(--r-1);
2218
- background: var(--accent);
2219
- color: var(--on-accent);
2495
+ /* ---- Brand mark ----
2496
+ The brand's image slot: a squircle at the bar's control height, a 2px ring
2497
+ in --brand-ring (rose, the identity accent), and the striped placeholder
2498
+ when empty. It holds an <img> that fills it; the ring radius is inherited.
2499
+ The empty slot carries no text: the wordmark beside it keeps the accessible
2500
+ name, so the slot is decorative (aria-hidden on an empty one, alt="" on its
2501
+ img). The stripe reuses .img-placeholder's pattern on --well, never a new
2502
+ literal. */
2503
+ .sona {
2220
2504
  display: inline-flex;
2221
2505
  align-items: center;
2222
2506
  justify-content: center;
2223
- font: 600 13px/1 var(--font-sans);
2507
+ width: 32px;
2508
+ height: 32px;
2509
+ border-radius: var(--r-1);
2510
+ border: 2px solid var(--brand-ring);
2511
+ background-color: var(--well);
2512
+ --tint-ground: transparent;
2513
+ background-image: repeating-linear-gradient(
2514
+ -45deg,
2515
+ color-mix(in srgb, var(--text-4) 18%, var(--tint-ground)) 0 1px,
2516
+ transparent 1px 9px
2517
+ );
2224
2518
  flex-shrink: 0;
2225
2519
  }
2520
+ .sona img {
2521
+ width: 100%;
2522
+ height: 100%;
2523
+ object-fit: cover;
2524
+ border-radius: inherit;
2525
+ }
2526
+ /* the sidebar logo row has room for a larger mark */
2527
+ .sidebar-logo > .sona {
2528
+ width: 36px;
2529
+ height: 36px;
2530
+ }
2531
+ /* under 768 the navbar hides the wordmark; the sona matches the mark size it
2532
+ replaced so the nav link band keeps its width */
2533
+ @media (max-width: 768px) {
2534
+ .navbar-brand > .sona {
2535
+ width: 28px;
2536
+ height: 28px;
2537
+ }
2538
+ }
2226
2539
  .logo-name {
2227
2540
  font: 600 var(--t-4)/1.25 var(--font-sans);
2228
2541
  letter-spacing: -0.02em;
@@ -2238,6 +2551,7 @@ hr, .divider {
2238
2551
  /* inline padding lives with the rail geometry: `.sidebar .sidebar-search`
2239
2552
  derives it from --rail and outranks anything stated here */
2240
2553
  background: var(--well);
2554
+ --tint-ground: transparent;
2241
2555
  border-radius: var(--r-1);
2242
2556
  color: var(--text-3);
2243
2557
  font-size: var(--t-2);
@@ -2246,6 +2560,7 @@ hr, .divider {
2246
2560
  .sidebar-search kbd {
2247
2561
  font: 500 10px/1 var(--font-mono);
2248
2562
  background: var(--surface-1);
2563
+ --tint-ground: transparent;
2249
2564
  border: none;
2250
2565
  padding: 3px 6px;
2251
2566
  border-radius: 5px;
@@ -2282,21 +2597,20 @@ hr, .divider {
2282
2597
  user-select: none;
2283
2598
  transition: color var(--dur-tint) var(--ease), background var(--dur-tint) var(--ease);
2284
2599
  }
2285
- .nav-item:hover { color: var(--text-2); background: var(--surface-2); }
2600
+ .nav-item:hover { color: var(--text-2); background: var(--surface-2); --tint-ground: transparent; }
2286
2601
  .nav-item.active { color: var(--text); font-weight: 500; }
2287
- /* The active row's band is one rule across the three rows that paint one: the
2288
- quiet accent fill spans the row and starts at the row's own indent, rounded
2289
- like this sidebar row. Derived from .nav-item.active, her call 2026-09-01.
2290
- ONE accent mark appears per row, and it is this band: her call 2026-09-02
2291
- retired the 2px capsule the tree and the palette added on top of it, for the
2292
- reason .nav-indicator went on 2026-08-29. The sidebar's dot is not a second
2293
- mark, since it is on every row at rest and only changes shape here. */
2294
- .nav-item.active,
2295
- .tree-row[aria-selected="true"],
2296
- .palette-row.active {
2297
- background: var(--accent-quiet);
2298
- border-radius: var(--r-1);
2299
- }
2602
+ /* The active row's band is one rule across the three rows that paint one. It
2603
+ is declared down in the tree block, the last of the three hover rules it has
2604
+ to outrank: `.nav-item:hover` above, `.menu-item:hover` for the palette and
2605
+ `.tree-row:hover` below all tie it at (0,2,0), and a hovered selected row
2606
+ keeps its accent rather than repainting grey, the treatment
2607
+ `.menu-item.active` already had. The shape is NOT here: each row carries
2608
+ --r-1 at rest, so the band arrives on a corner that is already rounded and
2609
+ nothing animates. ONE accent mark appears per row, and it is this band: the
2610
+ 2px capsule the tree and the palette added on top of it is gone, for the same
2611
+ reason `.nav-indicator` was dropped. The sidebar's
2612
+ dot is not a second mark, since it is on every row at rest and only changes
2613
+ shape here. */
2300
2614
  /* The dot IS the marker: the active row's own dot grows into a capsule, and the
2301
2615
  leaving row's shrinks back, on the same frame. One element changing shape,
2302
2616
  never two swapping. r-pill instead of 50% so the grown state stays a capsule;
@@ -2307,25 +2621,31 @@ hr, .divider {
2307
2621
  border-radius: var(--r-pill);
2308
2622
  flex-shrink: 0;
2309
2623
  background: var(--text-4);
2624
+ --tint-ground: transparent;
2310
2625
  transition:
2311
2626
  height var(--dur-near) var(--ease),
2312
2627
  background var(--dur-tint) var(--ease),
2313
2628
  box-shadow var(--dur-tint) var(--ease);
2314
2629
  }
2315
- .nav-item:hover .nav-dot { background: var(--text-3); }
2316
- .nav-item.active .nav-dot { height: var(--space-4); background: var(--accent); box-shadow: var(--accent-glow); }
2630
+ .nav-item:hover .nav-dot { background: var(--text-3); --tint-ground: transparent; }
2631
+ .nav-item.active .nav-dot { height: var(--space-4); background: var(--accent); --tint-ground: transparent; box-shadow: var(--accent-glow); }
2317
2632
  .nav-item svg { width: 14px; height: 14px; flex-shrink: 0; opacity: var(--icon-dim); }
2318
2633
  .nav-item.active svg { opacity: 1; }
2319
2634
  .nav-badge {
2320
2635
  font: 500 10px/1 var(--font-mono);
2321
2636
  background: var(--surface-2);
2637
+ --tint-ground: transparent;
2322
2638
  color: var(--text-3);
2323
2639
  border-radius: var(--r-pill);
2324
2640
  padding: 3px 7px;
2325
2641
  margin-left: auto;
2326
2642
  transition: background var(--dur-tint) var(--ease), color var(--dur-tint) var(--ease);
2327
2643
  }
2328
- .nav-item.active .nav-badge { background: var(--accent-quiet); color: var(--accent-text); }
2644
+ /* on the active row the badge drops its own fill and sits on the band: a
2645
+ quiet fill on the quiet band read 4.41:1 dark on the sidebar. On the band
2646
+ it reads 5.82:1 dark and, with the shared remix, 5.48
2647
+ light. */
2648
+ .nav-item.active .nav-badge { background: transparent; color: var(--accent-text); }
2329
2649
  .sidebar-user {
2330
2650
  margin-top: var(--space-2);
2331
2651
  padding-block: var(--space-4) 0; /* inline side is a --rail function below */
@@ -2401,14 +2721,14 @@ hr, .divider {
2401
2721
  padding-block: calc(var(--cbtn-rise) * var(--rail) + var(--cbtn-slot) * (1 - var(--rail))) var(--space-4);
2402
2722
  padding-inline: calc(var(--space-4) + 3px * (1 - var(--rail)))
2403
2723
  calc(var(--space-4) + var(--cbtn-slot) * var(--rail));
2404
- border-bottom-color: color-mix(in srgb, var(--line-soft) calc(var(--rail) * 100%), transparent);
2724
+ border-bottom-color: color-mix(in srgb, var(--line-soft) calc(var(--rail) * 100%), var(--tint-ground));
2405
2725
  }
2406
2726
 
2407
2727
  /* the brand block clips at the slot's edge. The row clips at its PADDING box,
2408
2728
  which is outside the slot, so a name longer than the column paints under
2409
2729
  the chevron; and a nowrap flex item will not shrink past its text without
2410
2730
  an explicit min-width. */
2411
- .sidebar .sidebar-logo > :not(.logo-mark):not(.sidebar-collapse) {
2731
+ .sidebar .sidebar-logo > :not(.sona):not(.sidebar-collapse) {
2412
2732
  min-width: 0;
2413
2733
  overflow: hidden;
2414
2734
  }
@@ -2476,7 +2796,7 @@ hr, .divider {
2476
2796
  transition: color var(--dur-tint) var(--ease), background var(--dur-tint) var(--ease);
2477
2797
  }
2478
2798
  /* 4.73:1 dark and 5.55:1 light at rest, 6.17 and 7.00 on the hover fill */
2479
- .sidebar-collapse:hover { color: var(--text-2); background: var(--surface-2); }
2799
+ .sidebar-collapse:hover { color: var(--text-2); background: var(--surface-2); --tint-ground: transparent; }
2480
2800
  .sidebar-collapse svg {
2481
2801
  width: 14px;
2482
2802
  height: 14px;
@@ -2567,11 +2887,16 @@ hr, .divider {
2567
2887
  padding: 0 var(--space-8);
2568
2888
  margin-inline: auto;
2569
2889
  background: var(--surface-1);
2890
+ --tint-ground: transparent;
2570
2891
  border-bottom: 1px solid var(--line-soft);
2571
2892
  position: sticky;
2572
2893
  top: 0;
2573
2894
  z-index: 100;
2574
2895
  }
2896
+ /* the pin derivation: a page holding this bar starts its
2897
+ pinned things under it, and a page holding none pins to the viewport top.
2898
+ --navbar-h itself stays the bar's own height and nothing else. */
2899
+ html:has(.navbar) { --pin-top: var(--navbar-h); }
2575
2900
  /* The width change is a NAMED DEVIATION, same class as the rail collapse:
2576
2901
  a bar resizing its own row; nothing below it reflows. Arrival at the
2577
2902
  band is the entrance leg, the return to the card the 0.6x exit, each
@@ -2582,9 +2907,8 @@ hr, .divider {
2582
2907
 
2583
2908
  Both legs run the slide curve, the one the inks run. --ease-out
2584
2909
  front-loads 88 percent of its travel, which on a width change reads as
2585
- a jump followed by a linear creep (her note, 2026-08-31: "less linear
2586
- animation curve"); the slide curve starts gentle, moves quick through
2587
- the middle and lands soft. The card also floats --space-3 off the top
2910
+ a jump followed by a linear creep; the slide curve starts gentle, moves
2911
+ quick through the middle and lands soft. The card also floats --space-3 off the top
2588
2912
  edge, on transform and never on margin: a margin reflows the page
2589
2913
  below on every swap, a transform only moves the bar. Reduced motion
2590
2914
  keeps the float and snaps the moves (transition: none below). */
@@ -2617,7 +2941,7 @@ hr, .divider {
2617
2941
  /* the dock's cover strip: while the card is away, a fixed page-toned
2618
2942
  band sits over the float's gap, so the transform can glide the bar
2619
2943
  home instead of snapping and nothing paints in the strip at any
2620
- scroll speed (her fast-flick report, 2026-08-31). It lands the frame
2944
+ scroll speed. It lands the frame
2621
2945
  scrolling starts, hides behind the docked band, and is gone at rest,
2622
2946
  where the dot grid owns the strip.
2623
2947
 
@@ -2632,6 +2956,7 @@ hr, .divider {
2632
2956
  inset: 0 0 auto 0;
2633
2957
  height: calc(var(--space-3) + 1px);
2634
2958
  background: var(--bg);
2959
+ --tint-ground: transparent;
2635
2960
  z-index: 99;
2636
2961
  }
2637
2962
  }
@@ -2640,8 +2965,9 @@ hr, .divider {
2640
2965
  }
2641
2966
  .navbar-brand {
2642
2967
  display: flex;
2643
- flex-direction: column;
2644
- justify-content: center;
2968
+ flex-direction: row;
2969
+ align-items: center;
2970
+ gap: var(--space-2);
2645
2971
  padding-right: var(--space-5);
2646
2972
  margin-right: var(--space-3);
2647
2973
  border-right: 1px solid var(--line-soft);
@@ -2693,7 +3019,7 @@ hr, .divider {
2693
3019
  user-select: none;
2694
3020
  white-space: nowrap;
2695
3021
  }
2696
- .navbar-link:hover { color: var(--text); background: var(--surface-2); text-decoration: none; }
3022
+ .navbar-link:hover { color: var(--text); background: var(--surface-2); --tint-ground: transparent; text-decoration: none; }
2697
3023
  .navbar-link.active { color: var(--accent-text); font-weight: 500; }
2698
3024
  .navbar-link svg { width: 14px; height: 14px; flex-shrink: 0; opacity: var(--icon-dim); }
2699
3025
  .navbar-link.active svg { opacity: 1; }
@@ -2725,6 +3051,7 @@ hr, .divider {
2725
3051
  padding: 0 var(--space-4);
2726
3052
  height: var(--topbar-h);
2727
3053
  background: var(--surface-1);
3054
+ --tint-ground: transparent;
2728
3055
  border-bottom: 1px solid var(--line-soft);
2729
3056
  position: sticky;
2730
3057
  top: 0;
@@ -2764,10 +3091,12 @@ hr, .divider {
2764
3091
 
2765
3092
  @media (max-width: 768px) {
2766
3093
  .mobile-topbar { display: flex; }
2767
- /* the bar above a stuck header down here is the topbar, not the navbar, and
2768
- the two are different heights: pinning at --navbar-h shows 4px of page
2769
- through the gap */
2770
- .section-sticky { top: var(--topbar-h); }
3094
+ /* the topbar's own display condition: under it, a page holding the topbar
3095
+ re-derives the pin at the mobile bar's height. Scoped to the bar's
3096
+ presence and not written bare, so a narrow page holding a navbar and no
3097
+ topbar keeps the navbar's 56; ties the navbar derivation at (0,1,1) and
3098
+ wins by coming after it. */
3099
+ html:has(.mobile-topbar) { --pin-top: var(--topbar-h); }
2771
3100
  .sidebar {
2772
3101
  position: fixed;
2773
3102
  top: 0;
@@ -2828,9 +3157,6 @@ hr, .divider {
2828
3157
  /* padding alone cannot promise the floor: the tab's own line-height and font
2829
3158
  size decide the rest, and the pair measured 40.8px */
2830
3159
  .tab { padding-top: 12px; padding-bottom: 12px; min-height: var(--ctl-lg); }
2831
- /* the palette's rows are .palette-row, not .menu-item, so the row floor above
2832
- never reached them: measured 21.7px */
2833
- .palette-row { min-height: var(--ctl-lg); }
2834
3160
  /* A segment carries the floor itself: a segmented control has no row to hand
2835
3161
  it to, and the track grows with the segment, so the ink follows off its own
2836
3162
  `calc(100% - 6px)` with no rule here. Measured 26.4px. */
@@ -2856,16 +3182,16 @@ hr, .divider {
2856
3182
  Fades stay, transforms go. Nothing waits on transitionend, so no state
2857
3183
  feedback is lost and no script is stranded. */
2858
3184
  @media (prefers-reduced-motion: reduce) {
2859
- .btn:active, .card-interactive:hover, .swatch:hover { transform: none; }
2860
- .toast { animation: toast-in-fade var(--dur-near) var(--ease) both; }
2861
- .toast.removing { animation: toast-out-fade var(--dur-out-near) var(--ease) both; }
2862
- .modal { animation: modal-in-fade var(--dur-near) var(--ease) both; }
2863
- .modal-backdrop.closing .modal { animation: modal-out-fade var(--dur-out-far) var(--ease) both; }
2864
- .popover { animation: popover-in-fade var(--dur-near) var(--ease) both; }
3185
+ .card-interactive:hover, .swatch:hover { transform: none; }
3186
+ .toast { animation: fade-in var(--dur-near) var(--ease) both; }
3187
+ .toast.removing { animation: fade-out var(--dur-out-near) var(--ease) both; }
3188
+ .modal { animation: fade-in var(--dur-near) var(--ease) both; }
3189
+ .modal-backdrop.closing .modal { animation: fade-out var(--dur-out-far) var(--ease) both; }
3190
+ .popover { animation: fade-in var(--dur-near) var(--ease) both; }
2865
3191
  /* at (0,2,0), because the base exit is too and this block adds no specificity.
2866
3192
  The daterange panel rides along for the same reason it rides the base. */
2867
3193
  .popover.closing,
2868
- .daterange.closing > .daterange-panel { animation-name: tooltip-out-fade; }
3194
+ .daterange.closing > .daterange-panel { animation-name: fade-out; }
2869
3195
  .reveal { transform: none; }
2870
3196
  .zoom-img img, img.zoom-img { transform: none; }
2871
3197
  .tab-ink, .navbar-ink, .tabs-boxed-ink { transition: opacity var(--dur-tint) var(--ease); }
@@ -2887,22 +3213,20 @@ hr, .divider {
2887
3213
  base selector's own specificity so the (0,2,2) rules above cannot outrank */
2888
3214
  .checkbox-label input[type="checkbox"]::after,
2889
3215
  .radio-label input[type="radio"]::after { transition: opacity var(--dur-near) var(--ease-out); }
3216
+ /* the knob arrives; the track's own fill still fades */
3217
+ .toggle::after { transition: none; }
2890
3218
  .tab-panel.active,
2891
3219
  .form-summary { animation-name: panel-in-fade; }
2892
3220
  .code-copy .icon-check { animation: none; }
2893
3221
  }
2894
- @keyframes chip-in-fade { from { opacity: 0; } }
2895
- @keyframes chip-out-fade { to { opacity: 0; } }
3222
+ /* the reduce fallbacks are three bodies, not one per component: `panel-in-fade`
3223
+ holds the element's own end opacity, `fade-in` forces it to 1, and
3224
+ `fade-out` is the exit every one of them leaves on. */
2896
3225
  @keyframes panel-in-fade { from { opacity: 0; } }
2897
3226
  @media (prefers-reduced-motion: reduce) and (max-width: 768px) {
2898
3227
  .sidebar { transform: none; opacity: 0; transition: opacity var(--dur-out-far) var(--ease); }
2899
3228
  .sidebar.open { opacity: 1; }
2900
3229
  }
2901
- @keyframes toast-in-fade { from { opacity: 0; } to { opacity: 1; } }
2902
- @keyframes toast-out-fade { to { opacity: 0; } }
2903
- @keyframes popover-in-fade { from { opacity: 0; } to { opacity: 1; } }
2904
- @keyframes modal-in-fade { from { opacity: 0; } to { opacity: 1; } }
2905
- @keyframes modal-out-fade { to { opacity: 0; } }
2906
3230
 
2907
3231
  /* ---- Theme switch ----
2908
3232
  The dataset.theme swap crossfades the whole page. document.startViewTransition
@@ -2990,15 +3314,15 @@ html[data-theme="light"] [data-theme-toggle] .icon-sun {
2990
3314
  /* no control-height floor on a fine pointer: a header is not a control row, and
2991
3315
  the 30px one made a sortable header 50.5px against a plain one's 31.5, so the
2992
3316
  same table header rendered two heights. It is 36.5 now. The 44px tap floor
2993
- lives in the coarse block and is untouched. Her call 2026-09-03. */
3317
+ lives in the coarse block and is untouched. */
2994
3318
  transition: color var(--dur-tint) var(--ease);
2995
3319
  }
2996
3320
  .th-sort:hover { color: var(--text); }
2997
3321
 
2998
3322
  /* One fixed box holding two sprite glyphs in one grid cell, the construction the
2999
3323
  theme toggle's sun and moon already use: `#i-sort` while the column is unsorted,
3000
- the direction chevron once it is, turned for descending. Her call 2026-09-02
3001
- squared this box so a 16-viewBox symbol could drop in; the pair of hand-drawn
3324
+ the direction chevron once it is, turned for descending. The box is squared
3325
+ so a 16-viewBox symbol could drop in; the pair of hand-drawn
3002
3326
  8 x 4 chevrons it replaces was the last glyph in the system no guard could read.
3003
3327
  The box stays fixed at every state, so the glyph can never shift the column. */
3004
3328
  .th-sort-icon {
@@ -3043,30 +3367,33 @@ th.num-col .th-sort { flex-direction: row-reverse; }
3043
3367
  }
3044
3368
  .table-wrap tbody tr.row-selected {
3045
3369
  background: color-mix(in srgb, var(--accent) 14%, var(--well));
3370
+ --tint-ground: transparent;
3046
3371
  box-shadow: inset 2px 0 0 var(--accent);
3047
3372
  }
3048
3373
  [data-theme="light"] .table-wrap tbody tr.row-selected {
3049
3374
  background: color-mix(in srgb, var(--accent) 12%, var(--surface-1));
3375
+ --tint-ground: transparent;
3050
3376
  }
3051
3377
  /* selection outranks hover: the row keeps its state under the pointer */
3052
3378
  .table-wrap tbody tr.row-selected:hover {
3053
3379
  background: color-mix(in srgb, var(--accent) 14%, var(--well));
3380
+ --tint-ground: transparent;
3054
3381
  }
3055
3382
  [data-theme="light"] .table-wrap tbody tr.row-selected:hover {
3056
3383
  background: color-mix(in srgb, var(--accent) 12%, var(--surface-1));
3384
+ --tint-ground: transparent;
3057
3385
  }
3058
3386
  /* a fresh row set fades in staggered (sort and page changes); opacity only,
3059
- a transform on a table-row is one engine's bug. The controller re-adds
3060
- the class to restart it. */
3387
+ a transform on a table-row is one engine's bug. The page's render pass
3388
+ re-adds the class to restart it. */
3061
3389
  .table-wrap tbody.rows-in tr {
3062
- animation: row-in var(--dur-near) var(--ease-out) both;
3390
+ animation: panel-in-fade var(--dur-near) var(--ease-out) both;
3063
3391
  }
3064
3392
  .table-wrap tbody.rows-in tr:nth-child(2) { animation-delay: calc(var(--stagger) * 1); }
3065
3393
  .table-wrap tbody.rows-in tr:nth-child(3) { animation-delay: calc(var(--stagger) * 2); }
3066
3394
  .table-wrap tbody.rows-in tr:nth-child(4) { animation-delay: calc(var(--stagger) * 3); }
3067
3395
  .table-wrap tbody.rows-in tr:nth-child(5) { animation-delay: calc(var(--stagger) * 4); }
3068
3396
  .table-wrap tbody.rows-in tr:nth-child(6) { animation-delay: calc(var(--stagger) * 5); }
3069
- @keyframes row-in { from { opacity: 0; } }
3070
3397
 
3071
3398
  /* a secondary text cell: the preview's inline-styled region column, as a
3072
3399
  class. 5.47:1 on surface-1 dark, 5.89:1 light; on the selection tints
@@ -3079,8 +3406,10 @@ th.num-col .th-sort { flex-direction: row-reverse; }
3079
3406
 
3080
3407
  /* the row is the target: anywhere on a selection row toggles it, the JS
3081
3408
  guard skips clicks that landed on another control. Text does not select,
3082
- so a click cannot double as a drag. */
3083
- .table-wrap tbody tr:has(.cell-select) { cursor: pointer; user-select: none; }
3409
+ so a click cannot double as a drag. A row whose box is disabled is not a
3410
+ target, and keeps no pointer. */
3411
+ .table-wrap tbody tr:has(.cell-select) { user-select: none; }
3412
+ .table-wrap tbody tr:has(.cell-select input:not(:disabled)) { cursor: pointer; }
3084
3413
 
3085
3414
  /* ---- Pagination ----
3086
3415
  The count line states the slice; the strip scrolls inside itself, so at
@@ -3132,20 +3461,25 @@ th.num-col .th-sort { flex-direction: row-reverse; }
3132
3461
  @keyframes chip-in { from { opacity: 0; transform: scale(0.85); } }
3133
3462
  @keyframes chip-out { to { opacity: 0; transform: scale(0.85); } }
3134
3463
  /* The chip's × is the one remove at chip scale: the skeleton, the glyph box and
3135
- the neutral hover all come from `.close`, and these three override only the
3464
+ the remove hover all come from `.close`, and these three override only the
3136
3465
  parts that would break the pill. The scope is the chip, because the bare
3137
3466
  `.close` rules further down this file would otherwise grow the × to the 30px
3138
3467
  square; one (0,2,0) selector covers both shapes, since the svg form wears
3139
3468
  `.close` beside `.icon` and outranks the shared `.icon` box either way. The
3140
- pill row carries the 44px floor, so the glyph keeps its own box. */
3469
+ pill row carries the 44px floor, so the glyph keeps its own box.
3470
+ The hover restates the remove costume only because the chip's own ground
3471
+ moves: the fill lifts with the select's --lift, and the well reads 1.28:1
3472
+ dark / 1.23 light on the resting chip, 1.51 dark on the lifted one and
3473
+ 1.03 light there, one of the three light grounds no grey clears 1.2 on
3474
+ (the record lives with the remove skeleton rules). */
3141
3475
  .filter-chip .close {
3142
3476
  width: 16px;
3143
3477
  height: 16px;
3144
3478
  border-radius: var(--r-pill);
3145
3479
  color: inherit;
3146
- transition: background var(--dur-tint) var(--ease);
3480
+ transition: background var(--dur-tint) var(--ease), color var(--dur-tint) var(--ease);
3147
3481
  }
3148
- .filter-chip .close:hover { background: color-mix(in srgb, currentColor 18%, transparent); }
3482
+ .filter-chip .close:hover { color: var(--danger); background: var(--well); --tint-ground: transparent; }
3149
3483
  .filter-chip .close svg { width: 8px; height: 8px; }
3150
3484
 
3151
3485
  /* The picker: a filter button plus a popover of checkbox rows anchored to
@@ -3153,21 +3487,21 @@ th.num-col .th-sort { flex-direction: row-reverse; }
3153
3487
  .menu-item.checkbox-label: one target, the system's own box. Closes on
3154
3488
  outside click and Escape; the exit is the popover's missing one. */
3155
3489
  .filter-menu-wrap { position: relative; }
3156
- .filter-menu { padding: 0; }
3157
3490
  /* A trigger on the bar's end edge opens its menu off the end of the wrapper,
3158
3491
  since the panel is wider than the button it hangs from. Same edge
3159
3492
  `.btn-split-menu` anchors to by default. */
3160
3493
  .filter-menu.menu-end { left: auto; right: 0; }
3161
- /* the 20px checkbox already pads its row visually; menu padding plus the row's
3162
- own 8px read as a strip of dead panel above the first box (12.9px measured) */
3494
+ /* the checkbox already pads its row visually, so the row gives 2px back: above
3495
+ the first box the panel's gutter plus the row's own 8px measures a 15.84px
3496
+ strip of dead panel, against 13.84 at 6 */
3163
3497
  .filter-menu .menu-item { padding-block: 6px; }
3164
3498
  /* Late reduce block: the rules below this point sit after the main one, so
3165
3499
  their restatements have to as well, or the later base rule wins by order. */
3166
3500
  @media (prefers-reduced-motion: reduce) {
3167
3501
  /* rows and chips appear and go instantly; the chip's removal timer still holds it */
3168
3502
  .table-wrap tbody.rows-in tr { animation: none; }
3169
- .filter-chip { animation-name: chip-in-fade; }
3170
- .filter-chip.closing { animation-name: chip-out-fade; }
3503
+ .filter-chip { animation-name: panel-in-fade; }
3504
+ .filter-chip.closing { animation-name: fade-out; }
3171
3505
  }
3172
3506
 
3173
3507
  /* ---- Empty result inside the table ----
@@ -3192,13 +3526,11 @@ th.num-col .th-sort { flex-direction: row-reverse; }
3192
3526
  Viewport-driven, so it swaps in one frame: no animation, by design.
3193
3527
  Under the navbar's own 768px boundary, where the search is already off
3194
3528
  and the links scroll, links that carry an icon fold to it and the brand
3195
- shows its mark. */
3196
- .navbar .logo-mark { display: none; }
3529
+ keeps its mark while the wordmark hides. */
3197
3530
 
3198
3531
  @media (max-width: 768px) {
3199
3532
  .navbar { padding-inline: var(--space-4); }
3200
3533
 
3201
- .navbar .logo-mark { display: inline-flex; }
3202
3534
  .navbar .logo-name { display: none; }
3203
3535
  .navbar-brand { padding-right: var(--space-3); margin-right: var(--space-2); }
3204
3536
 
@@ -3272,10 +3604,14 @@ th.num-col .th-sort { flex-direction: row-reverse; }
3272
3604
 
3273
3605
  /* The well sinks, so the tone step is its only edge: --lit-* are the raised
3274
3606
  edges and there is no sunken one to reach for. */
3275
- .band-well { background: var(--well); }
3607
+ .band-well { background: var(--well); --tint-ground: transparent; }
3276
3608
  /* The raised band rises, so it takes the lit edge, which flips to the
3277
3609
  underside in Latte because that is where a light page shows a raised plane. */
3278
- .band-raised { background: var(--surface-1); box-shadow: var(--lit-1); }
3610
+ .band-raised { background: var(--surface-1); --tint-ground: transparent; box-shadow: var(--lit-1); }
3611
+ /* The accent band is the closing ask's ground, one per page: the sapphire turned
3612
+ up where a product page ends. It takes no edge, the tone is the step, and every
3613
+ text level and the filled action clear their floors on it (tokens.css). */
3614
+ .band-accent { background: var(--band-accent); --tint-ground: transparent; }
3279
3615
 
3280
3616
  /* Two bands of the same tone are one band, so the seam keeps one section gap
3281
3617
  instead of stacking two paddings into a dead strip. Two bands of DIFFERENT
@@ -3333,19 +3669,20 @@ th.num-col .th-sort { flex-direction: row-reverse; }
3333
3669
  what should decide, so let it shrink. */
3334
3670
  .page-header > .toolbar { flex: 0 1 auto; }
3335
3671
 
3336
- /* --t-8, one step above .section-title's --t-7, so a page and a section inside
3337
- it are never the same size. overflow-wrap because a title is content: an
3338
- unbroken machine string in one would otherwise widen the page. */
3672
+ /* --t-9, one step above .section-title's --t-8, so a page and a section inside
3673
+ it are never the same size. Tracking tightens with size, since Onest's
3674
+ default spacing opens up past 40px. overflow-wrap because a title is
3675
+ content: an unbroken machine string in one would otherwise widen the page. */
3339
3676
  .page-title {
3340
- font: 600 var(--t-8)/1.1 var(--font-sans);
3341
- letter-spacing: -0.03em;
3677
+ font: 600 var(--t-9)/1 var(--font-sans);
3678
+ letter-spacing: -0.045em;
3342
3679
  color: var(--text);
3343
3680
  overflow-wrap: anywhere;
3344
3681
  }
3345
- @media (max-width: 768px) { .page-title { font-size: var(--t-7); } }
3682
+ @media (max-width: 768px) { .page-title { font-size: var(--t-8); } }
3346
3683
 
3347
3684
  /* Metadata reads under the title, never beside it: it is the smallest type on
3348
- the screen and a 46px title beside 12px text has no shared baseline worth
3685
+ the screen and a 64px title beside 12px text has no shared baseline worth
3349
3686
  aligning. --text-3 measures 6.35:1 dark and 5.27:1 light on --bg. The 20px
3350
3687
  column gap is the separator; a middot pseudo lands at a line end or a line
3351
3688
  start the moment the row wraps. */
@@ -3462,7 +3799,7 @@ th.num-col .th-sort { flex-direction: row-reverse; }
3462
3799
  card on the page, so the highlight reads as a stronger plane change than the
3463
3800
  one the eye already tracks. */
3464
3801
  .prose mark {
3465
- background: color-mix(in srgb, var(--accent) 22%, transparent);
3802
+ background: color-mix(in srgb, var(--accent) 22%, var(--tint-ground));
3466
3803
  color: var(--text);
3467
3804
  border-radius: 2px;
3468
3805
  padding: 0 1px;
@@ -3512,9 +3849,10 @@ th.num-col .th-sort { flex-direction: row-reverse; }
3512
3849
  place-items: center;
3513
3850
  border-radius: var(--r-2);
3514
3851
  background-color: var(--well);
3852
+ --tint-ground: transparent;
3515
3853
  background-image: repeating-linear-gradient(
3516
3854
  -45deg,
3517
- color-mix(in srgb, var(--text-4) 18%, transparent) 0 1px,
3855
+ color-mix(in srgb, var(--text-4) 18%, var(--tint-ground)) 0 1px,
3518
3856
  transparent 1px 9px
3519
3857
  );
3520
3858
  }
@@ -3524,6 +3862,7 @@ th.num-col .th-sort { flex-direction: row-reverse; }
3524
3862
  font: 400 var(--t-1)/1 var(--font-mono);
3525
3863
  color: var(--text-3);
3526
3864
  background: var(--surface-1);
3865
+ --tint-ground: transparent;
3527
3866
  padding: var(--space-2) var(--space-3);
3528
3867
  border-radius: var(--r-1);
3529
3868
  }
@@ -3540,13 +3879,42 @@ th.num-col .th-sort { flex-direction: row-reverse; }
3540
3879
  carries rows, chips or numbers and there are eight of them. Roomy inside a
3541
3880
  .band-well or on --bg where the card IS the section. Neither belongs on a
3542
3881
  .band-raised: the card's fill and the band's are the same colour, so only
3543
- --elev-1 separates them, and a raised band is for type, not for cards. A
3882
+ the drawn line separates them, and a raised band is for type,
3883
+ not for cards. A
3544
3884
  card on a well band measures 1.283:1 dark and 1.230:1 light against its
3545
3885
  ground, the strongest tone step the system has. */
3546
3886
  .card.card-tight { padding: var(--space-3); }
3547
3887
  .card.card-roomy { padding: var(--space-8); }
3548
3888
  /* At 390px a roomy card spends 64 of 358 available pixels on air. */
3549
3889
  @media (max-width: 768px) { .card.card-roomy { padding: var(--space-5); } }
3890
+ /* A card holding setting rows drops its INLINE padding for the rows, and every
3891
+ other child keeps the card's 20px gutter on its own: a header's title sits
3892
+ 20px in from the card's left, a trailing save row 20px in from its right,
3893
+ and the card's block padding still holds above a title and under a save
3894
+ row: rows flush, the rest keeps the
3895
+ gutter. Only an edge row sits on the card's edge, so the hover fill
3896
+ reaches the edge the row touches and nothing else does.
3897
+
3898
+ A card holding NOTHING but rows is fully flush rather than inline-flush:
3899
+ the row's own 20px IS the gutter, its fill reaches both card edges and the
3900
+ edge rows' --r-2 corners are the card's own: the
3901
+ whole row should light up, without leaving empty space. At (0,3,0) that
3902
+ clause outranks `.card-tight` and `.card-roomy` on specificity rather than
3903
+ on where it sits.
3904
+
3905
+ Both clauses ask what PAINTS, so a `<template>` a page clones its rows
3906
+ from does not count as a child: it draws nothing, so nothing of it can meet
3907
+ the card's edge, and a rule that turned on an invisible element would be a
3908
+ defect nobody could see to diagnose. */
3909
+ .card:has(> .setting-row) { padding-inline: 0; }
3910
+ /* `.section-sticky` is excluded because it is full-bleed chrome rather than
3911
+ a gutter child: a stuck header spans the viewport and derives its own
3912
+ inline padding from the page column, so a 20px flat here would pull its
3913
+ toolbar off the column the first time the card scrolled. */
3914
+ .card:has(> .setting-row) > :not(.setting-row, .section-sticky, template, script, style) {
3915
+ padding-inline: var(--space-5);
3916
+ }
3917
+ .card:has(> .setting-row):not(:has(> :not(.setting-row, template, script, style))) { padding: 0; }
3550
3918
 
3551
3919
  /* ---- Charts ----
3552
3920
  Inline SVG, no library, no canvas. The shell is HTML and only the marks are
@@ -3576,9 +3944,9 @@ th.num-col .th-sort { flex-direction: row-reverse; }
3576
3944
  3. A rounded corner distorts with everything else, so bars are square.
3577
3945
 
3578
3946
  The four series colours are the tokens, measured on --surface-1 for WCAG
3579
- 1.4.11's 3:1 graphics floor: 5.21 / 5.27 / 7.05 / 6.45:1 dark and
3580
- 5.69 / 5.80 / 4.48 / 5.53:1 light; on --bg, 6.02 / 6.09 / 8.15 / 7.46 dark
3581
- and 5.06 / 5.16 / 3.98 / 4.92 light. Four is the ceiling: a fifth series is
3947
+ 1.4.11's 3:1 graphics floor: 5.21 / 5.27 / 6.62 / 6.45:1 dark and
3948
+ 5.81 / 5.80 / 5.12 / 5.55:1 light; on --bg, 6.02 / 6.09 / 7.65 / 7.46 dark
3949
+ and 5.17 / 5.16 / 4.56 / 4.94 light. Four is the ceiling: a fifth series is
3582
3950
  a legend problem, and yellow and red stay out because they read as status.
3583
3951
 
3584
3952
  Gridlines are scaffolding, not graphical objects: the tick labels carry the
@@ -3601,9 +3969,6 @@ th.num-col .th-sort { flex-direction: row-reverse; }
3601
3969
  --series: var(--chart-series-1);
3602
3970
  --chart-grid: var(--surface-3); /* 1.45:1 on --surface-1 dark, 1.31 light */
3603
3971
  --chart-axis: var(--text-4); /* 4.73:1 dark, 5.55 light on --surface-1 */
3604
- /* a tint that reads as a fill without competing with its own line: 1.30:1
3605
- over --surface-1 dark, 1.26:1 light, and two stacked land 1.63 and 1.58 */
3606
- --chart-area-alpha: 0.16;
3607
3972
  --chart-aspect: 16 / 6;
3608
3973
 
3609
3974
  display: grid;
@@ -3617,6 +3982,11 @@ th.num-col .th-sort { flex-direction: row-reverse; }
3617
3982
  container-type: inline-size;
3618
3983
  }
3619
3984
 
3985
+ /* A tint that reads as a fill without competing with its own line: 1.30:1 over
3986
+ --surface-1 dark, 1.26:1 light, and two stacked land 1.63 and 1.58. A
3987
+ sparkline is the other ground an area lands on, so it carries the knob too. */
3988
+ .chart, .stat-spark { --chart-area-alpha: 0.16; }
3989
+
3620
3990
  /* Each part carries its own gutter rather than the grid carrying a gap: an
3621
3991
  absent axis title then costs nothing, where a gap would leave its lane. */
3622
3992
  .chart-plot {
@@ -3653,6 +4023,8 @@ th.num-col .th-sort { flex-direction: row-reverse; }
3653
4023
  alias, so it can be snapped */
3654
4024
  shape-rendering: crispEdges;
3655
4025
  }
4026
+ /* dashed, so the fills under the lines carry the plot and the rules recede */
4027
+ .chart-grid line { stroke-dasharray: 2 5; }
3656
4028
 
3657
4029
  /* ---- Ticks ----
3658
4030
  Mono and tabular: digits line up down the y column and across a plate, and a
@@ -3774,7 +4146,8 @@ th.num-col .th-sort { flex-direction: row-reverse; }
3774
4146
 
3775
4147
  /* A flat tint, never a gradient, and never on its own: the fill sits far under
3776
4148
  the 3:1 floor by design, and the 2px line on top is what carries the value.
3777
- An area without its line is the one shape this recipe does not ship. */
4149
+ ui.js builds it under every line and every sparkline, off the line's own
4150
+ points, so no page writes one. */
3778
4151
  .chart-area {
3779
4152
  fill: var(--series);
3780
4153
  fill-opacity: var(--chart-area-alpha);
@@ -3795,6 +4168,9 @@ th.num-col .th-sort { flex-direction: row-reverse; }
3795
4168
  point is it, and a growing dot that lags the crosshair reads as a bug */
3796
4169
  .chart-point.is-active,
3797
4170
  .chart-point line.is-active { stroke-width: 11; }
4171
+ /* ui.js marks every line's last point with `.chart-end`; a sparkline's is
4172
+ scaled to its 1.5px line */
4173
+ .stat-spark .chart-point { stroke-width: 6; }
3798
4174
 
3799
4175
  /* Bars are fills, so they scale with the plot the way the data does and need
3800
4176
  no vector-effect. Square: a corner radius in a stretched viewBox turns into
@@ -3944,13 +4320,16 @@ th.num-col .th-sort { flex-direction: row-reverse; }
3944
4320
  .legend and .legend-item ship already; this adds the mark for a filled
3945
4321
  series and the interactive form. Both read --series from the same slot class
3946
4322
  the chart uses, so a legend cannot drift from the mark it names. */
3947
- .legend .legend-line { background: var(--series, currentColor); }
4323
+ .legend .legend-line { background: var(--series, currentColor); --tint-ground: transparent; }
3948
4324
  .legend-swatch {
3949
- width: 10px;
3950
- height: 10px;
4325
+ /* one width with the line mark, so the two sit level in a legend row; a
4326
+ filled block needs less height than a stroke reads long */
4327
+ width: 12px;
4328
+ height: 8px;
3951
4329
  border-radius: calc(var(--r-1) * 0.3);
3952
4330
  flex-shrink: 0;
3953
4331
  background: var(--series, currentColor);
4332
+ --tint-ground: transparent;
3954
4333
  }
3955
4334
 
3956
4335
  /* The toggle: a real button, because a legend item that filters the chart is a
@@ -3960,13 +4339,19 @@ th.num-col .th-sort { flex-direction: row-reverse; }
3960
4339
  border: none;
3961
4340
  background: none;
3962
4341
  padding: 0;
3963
- font: inherit;
4342
+ /* not the `font` shorthand: it resets font-size and beats .legend-item's
4343
+ --t-2 by source order, painting a toggle 14px beside 12px static items */
4344
+ font-family: inherit;
4345
+ font-weight: inherit;
4346
+ line-height: inherit;
4347
+ letter-spacing: inherit;
3964
4348
  color: var(--text-3); /* 5.49:1 dark, 5.92 light on --surface-1 */
3965
4349
  cursor: pointer;
3966
4350
  transition: color var(--dur-tint) var(--ease);
3967
4351
  }
3968
4352
  .legend-toggle:hover { color: var(--text); }
3969
4353
  .legend-toggle[aria-pressed="false"] { color: var(--text-4); } /* 4.73:1 dark, 5.55 light */
4354
+ .legend-toggle[aria-pressed="false"]:hover { color: var(--text-3); } /* 5.49:1 dark, 5.92 light */
3970
4355
  /* the mark hollows instead of greying: the series keeps its identity while it
3971
4356
  is switched off, and an inset ring changes no box dimension */
3972
4357
  .legend-toggle[aria-pressed="false"] .legend-swatch,
@@ -4061,7 +4446,7 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4061
4446
  /* ---- Icon tile ----
4062
4447
  The 32px soft-level square that heads a feature card. One colour drives the
4063
4448
  fill, the hairline and the glyph, so a variant is one line. 32 is the size
4064
- the router names it and the box .avatar already carries; no token holds it.
4449
+ SKILL.md names for it and the box .avatar already carries; no token holds it.
4065
4450
 
4066
4451
  The fill and the hairline are laid down OPAQUE, mixed into --surface-1 rather
4067
4452
  than tinted over whatever is behind, which is what keeps the tile off the
@@ -4091,8 +4476,9 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4091
4476
  width: 32px;
4092
4477
  height: 32px;
4093
4478
  flex-shrink: 0;
4094
- border-radius: var(--r-1);
4479
+ border-radius: var(--r-2);
4095
4480
  background: color-mix(in srgb, var(--tile) var(--tile-fill), var(--surface-1));
4481
+ --tint-ground: transparent;
4096
4482
  border: 1px solid color-mix(in srgb, var(--tile) var(--tile-line), var(--surface-1));
4097
4483
  color: var(--tile);
4098
4484
  }
@@ -4101,6 +4487,20 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4101
4487
  does, so a `<use>` that carries no class cannot lay out at 300x150 */
4102
4488
  .icon-tile > svg { width: 16px; height: 16px; }
4103
4489
 
4490
+ /* A feature grid's default mark fills deeper and edges harder: the page's
4491
+ sapphire turned up where a product page has room for it. The semantic
4492
+ variants keep the shipped mix, so their own hue still reads as the level.
4493
+ Glyph on the deeper fill, read off the painted pixel as above: 4.00 dark,
4494
+ 3.47 light; the hairline 2.05 and 1.72 on it. */
4495
+ .feature > .icon-tile:not(.icon-tile-accent-2, .icon-tile-success, .icon-tile-warning, .icon-tile-danger) {
4496
+ --tile-fill: 24%;
4497
+ --tile-line: 60%;
4498
+ }
4499
+ [data-theme="light"] .feature > .icon-tile:not(.icon-tile-accent-2, .icon-tile-success, .icon-tile-warning, .icon-tile-danger) {
4500
+ --tile-fill: 20%;
4501
+ --tile-line: 55%;
4502
+ }
4503
+
4104
4504
  .icon-tile-accent-2 { --tile: var(--accent-2); }
4105
4505
  .icon-tile-success { --tile: var(--success); }
4106
4506
  .icon-tile-warning { --tile: var(--warning); }
@@ -4164,7 +4564,7 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4164
4564
  top: 0;
4165
4565
  bottom: 0;
4166
4566
  width: 2px;
4167
- background: color-mix(in srgb, var(--text-4) 28%, transparent);
4567
+ background: color-mix(in srgb, var(--text-4) 28%, var(--tint-ground));
4168
4568
  }
4169
4569
  /* the thread begins at the first dot and ends at the last: no stub above the
4170
4570
  first entry, none hanging below the last */
@@ -4180,11 +4580,12 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4180
4580
  margin-top: var(--tl-lead);
4181
4581
  border-radius: var(--r-pill);
4182
4582
  background: var(--text-4); /* 5.47:1 dark and 4.94:1 light on --bg, 4.73 / 5.55 on --surface-1 */
4583
+ --tint-ground: transparent;
4183
4584
  }
4184
- .timeline-dot-accent { background: var(--accent); } /* 7.49 / 6.48 dark, 4.09 / 4.60 light */
4185
- .timeline-dot-success { background: var(--success); } /* 8.89 / 7.69 dark, 4.85 / 5.46 light */
4186
- .timeline-dot-warning { background: var(--warning); } /* 9.68 / 8.37 dark, 5.25 / 5.91 light */
4187
- .timeline-dot-danger { background: var(--danger); } /* 5.98 / 5.18 dark, 5.36 / 6.02 light */
4585
+ .timeline-dot-accent { background: var(--accent); --tint-ground: transparent; } /* 7.49 / 6.48 dark, 4.09 / 4.60 light */
4586
+ .timeline-dot-success { background: var(--success); --tint-ground: transparent; } /* 8.89 / 7.69 dark, 4.85 / 5.46 light */
4587
+ .timeline-dot-warning { background: var(--warning); --tint-ground: transparent; } /* 9.68 / 8.37 dark, 5.25 / 5.91 light */
4588
+ .timeline-dot-danger { background: var(--danger); --tint-ground: transparent; } /* 5.98 / 5.18 dark, 5.36 / 6.02 light */
4188
4589
 
4189
4590
  .timeline-head {
4190
4591
  display: flex;
@@ -4220,7 +4621,7 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4220
4621
  .timeline-more .timeline-dot {
4221
4622
  margin-top: 0;
4222
4623
  background: none;
4223
- border: 2px solid color-mix(in srgb, var(--text-4) 28%, transparent);
4624
+ border: 2px solid color-mix(in srgb, var(--text-4) 28%, var(--tint-ground));
4224
4625
  }
4225
4626
  .timeline > .timeline-more:last-child::before { bottom: 50%; }
4226
4627
 
@@ -4245,6 +4646,7 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4245
4646
  .accordion {
4246
4647
  interpolate-size: allow-keywords;
4247
4648
  background: var(--surface-1);
4649
+ --tint-ground: transparent;
4248
4650
  border-radius: var(--r-2);
4249
4651
  box-shadow: var(--elev-1);
4250
4652
  /* the first and last summary's hover fill hugs the card's own corners */
@@ -4257,7 +4659,11 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4257
4659
  overflow-wrap: anywhere; /* a long token cannot push the chevron off the row */
4258
4660
  }
4259
4661
  .accordion > .disclosure > .disclosure-body {
4260
- padding: 0 var(--space-5) var(--space-5);
4662
+ /* the body's ink sits centred between the label above it and the row's
4663
+ bottom edge: the summary's 12px under the label plus this 8px puts 20.8px
4664
+ of air above the text against the 21.6 below it, line-height slack
4665
+ included. Flush under the summary read top-heavy. */
4666
+ padding: var(--space-2) var(--space-5) var(--space-5);
4261
4667
  font-size: var(--t-2);
4262
4668
  color: var(--text-2); /* 7.30:1 dark, 8.36:1 light on --surface-1 */
4263
4669
  }
@@ -4330,6 +4736,7 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4330
4736
  flex-shrink: 0;
4331
4737
  border-radius: var(--r-pill);
4332
4738
  background: var(--well);
4739
+ --tint-ground: transparent;
4333
4740
  border: 1px solid var(--line);
4334
4741
  color: var(--text-4); /* 6.07:1 dark, 4.52:1 light on --well */
4335
4742
  font: 500 var(--t-2)/1 var(--font-mono);
@@ -4366,11 +4773,12 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4366
4773
  left: calc(50% + var(--step-mark) / 2 + var(--space-2));
4367
4774
  right: calc(-50% + var(--step-mark) / 2 + var(--space-2));
4368
4775
  height: 2px;
4369
- background: color-mix(in srgb, var(--text-4) 28%, transparent);
4776
+ background: color-mix(in srgb, var(--text-4) 28%, var(--tint-ground));
4370
4777
  }
4371
4778
 
4372
4779
  .step.done .step-index {
4373
4780
  background: var(--accent);
4781
+ --tint-ground: transparent;
4374
4782
  border-color: var(--accent); /* 7.49 / 6.48 dark, 4.09 / 4.60 light */
4375
4783
  color: var(--on-accent); /* 8.22:1 dark, 4.60:1 light on the fill */
4376
4784
  }
@@ -4378,7 +4786,7 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4378
4786
  /* the run got this far. Written with the child combinator, because the
4379
4787
  connector's own rule carries `:not(:last-child)` and a bare `.step.done`
4380
4788
  is outranked by it: measured, the done connector painted the neutral. */
4381
- .stepper > .step.done::after { background: var(--accent); }
4789
+ .stepper > .step.done::after { background: var(--accent); --tint-ground: transparent; }
4382
4790
 
4383
4791
  .step[aria-current="step"] .step-index {
4384
4792
  background: var(--accent-quiet);
@@ -4389,6 +4797,7 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4389
4797
 
4390
4798
  .step.error .step-index {
4391
4799
  background: var(--danger);
4800
+ --tint-ground: transparent;
4392
4801
  border-color: var(--danger);
4393
4802
  color: var(--on-danger); /* 6.58:1 dark, 6.01:1 light on the fill */
4394
4803
  }
@@ -4434,9 +4843,6 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4434
4843
 
4435
4844
  /* ---- Meter ----
4436
4845
  A level against a capacity: storage used, seats taken, rate budget spent.
4437
- Reach for `.progress` when the number climbs to 100 and the job is then
4438
- done; reach for `.meter` when the number can sit anywhere and 100 is a
4439
- problem.
4440
4846
 
4441
4847
  The fill rides scaleX, not width, so nothing here is a layout change: the
4442
4848
  track clips at its own pill radius, which is why the fill needs no radius of
@@ -4476,12 +4882,14 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4476
4882
  .meter-track {
4477
4883
  height: var(--space-2);
4478
4884
  background: var(--well);
4885
+ --tint-ground: transparent;
4479
4886
  border-radius: var(--r-pill);
4480
4887
  overflow: hidden;
4481
4888
  }
4482
4889
  .meter-fill {
4483
4890
  height: 100%;
4484
4891
  background: var(--accent); /* 8.31:1 dark, 3.74:1 light on the track */
4892
+ --tint-ground: transparent;
4485
4893
  transform-origin: left center;
4486
4894
  transform: scaleX(var(--meter-v));
4487
4895
  transition: transform var(--dur-far) var(--ease), background var(--dur-tint) var(--ease);
@@ -4499,15 +4907,16 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4499
4907
  height: 6px;
4500
4908
  border-radius: var(--r-pill);
4501
4909
  background: var(--text-3);
4910
+ --tint-ground: transparent;
4502
4911
  }
4503
4912
  .meter-hint {
4504
4913
  font: 400 var(--t-1)/1.4 var(--font-sans);
4505
4914
  color: var(--text-4); /* 5.47 / 4.73 dark, 4.94 / 5.55 light */
4506
4915
  }
4507
4916
 
4508
- .meter.warn .meter-fill { background: var(--warning); } /* 10.74:1 dark, 4.80:1 light on the track */
4917
+ .meter.warn .meter-fill { background: var(--warning); --tint-ground: transparent; } /* 10.74:1 dark, 4.80:1 light on the track */
4509
4918
  .meter.warn .meter-value { color: var(--warning); } /* 9.68 / 8.37 dark, 5.25 / 5.91 light */
4510
- .meter.over .meter-fill { background: var(--danger); } /* 6.64:1 dark, 4.89:1 light on the track */
4919
+ .meter.over .meter-fill { background: var(--danger); --tint-ground: transparent; } /* 6.64:1 dark, 4.89:1 light on the track */
4511
4920
  .meter.over .meter-value { color: var(--danger); } /* 5.98 / 5.18 dark, 5.36 / 6.02 light */
4512
4921
  /* no reading yet: the value says so and the empty track is the initial value */
4513
4922
  .meter.unknown .meter-value { color: var(--text-4); }
@@ -4528,21 +4937,27 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4528
4937
  because the live accent itself clears the track at only 3.74:1 in Latte. */
4529
4938
  .meter[aria-disabled="true"] .meter-fill {
4530
4939
  background: color-mix(in srgb, var(--accent) 50%, var(--well));
4940
+ --tint-ground: transparent;
4531
4941
  }
4532
4942
  .meter[aria-disabled="true"].warn .meter-fill {
4533
4943
  background: color-mix(in srgb, var(--warning) 44%, var(--well));
4944
+ --tint-ground: transparent;
4534
4945
  }
4535
4946
  .meter[aria-disabled="true"].over .meter-fill {
4536
4947
  background: color-mix(in srgb, var(--danger) 58%, var(--well));
4948
+ --tint-ground: transparent;
4537
4949
  }
4538
4950
  [data-theme="light"] .meter[aria-disabled="true"] .meter-fill {
4539
4951
  background: color-mix(in srgb, var(--accent) 83%, var(--well));
4952
+ --tint-ground: transparent;
4540
4953
  }
4541
4954
  [data-theme="light"] .meter[aria-disabled="true"].warn .meter-fill {
4542
4955
  background: color-mix(in srgb, var(--warning) 74%, var(--well));
4956
+ --tint-ground: transparent;
4543
4957
  }
4544
4958
  [data-theme="light"] .meter[aria-disabled="true"].over .meter-fill {
4545
4959
  background: color-mix(in srgb, var(--danger) 66%, var(--well));
4960
+ --tint-ground: transparent;
4546
4961
  }
4547
4962
 
4548
4963
  /* ---- Ring progress ----
@@ -4592,14 +5007,29 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4592
5007
  that: `r: 13.5px` measured a 33.6px ring inside its 96px box on viewBox 100 and
4593
5008
  a 21.33px stroke on viewBox 36, against a .ring-center measuring 46.8 x 41.2
4594
5009
  (62.4 diagonal). On the contract the hole measures 78.7px at --ring-size 96 and
4595
- 52.5px at 64, so the label clears the arc at both. cursor-rules.test.js asserts
4596
- the markup, since the CSS can no longer see it. */
5010
+ 52.5px at 64, so the label clears the arc at both. The markup is what holds it,
5011
+ since the CSS can no longer see it. */
4597
5012
  }
4598
5013
  .ring-track { stroke: var(--well); }
4599
5014
  .ring-fill {
4600
5015
  stroke: var(--accent); /* 8.31:1 dark, 3.74:1 light on the track */
4601
5016
  stroke-linecap: butt; /* a round cap paints a dot at zero */
5017
+ /* the gap collapses AT full, never below it: sign(1 - v) is 1 under, 0 at 1
5018
+ and negative over, so a complete ring paints closed and no partial ring
5019
+ moves. A literal `v*100 100` at v = 1 draws a dash exactly as long as the
5020
+ path, and Chrome antialiases the joint where it meets itself into a dark
5021
+ notch at twelve o'clock (measured 2 device px at 4x); `100 0` closes it
5022
+ and a negative gap paints the whole stroke the same way. Every RESTING
5023
+ pixel under 100 percent is unchanged, since the gap is literally 100 there.
5024
+ A down-sweep from a full ring is the one thing that moves: the gap
5025
+ interpolates from a negative value and clamps at 0, so the ring reads
5026
+ closed until the dash reaches 84 rather than 100, about 130ms of the 260ms
5027
+ transition. Up-sweeps are identical to the old form, both crossing at 100.
5028
+ THE FIRST DECLARATION IS THE FALLBACK, not a leftover: an engine with no
5029
+ `sign()` drops the second line and keeps the old notch, where dropping the
5030
+ pair would paint every determinate ring closed at every value. */
4602
5031
  stroke-dasharray: calc(var(--ring-v) * 100) 100;
5032
+ stroke-dasharray: calc(var(--ring-v) * 100) calc(100 * sign(1 - var(--ring-v)));
4603
5033
  transition: stroke-dasharray var(--dur-far) var(--ease), stroke var(--dur-tint) var(--ease);
4604
5034
  }
4605
5035
  .ring-center {
@@ -4623,15 +5053,42 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4623
5053
  .ring.over .ring-fill { stroke: var(--danger); }
4624
5054
  .ring.over .ring-value { color: var(--danger); }
4625
5055
  .ring.unknown .ring-value { color: var(--text-4); }
4626
- /* indeterminate: a quarter arc on the system's one loop tempo, the tempo
4627
- `.spinner` and `.btn-loading` already run */
5056
+ /* indeterminate: the tail breathes. The head is pinned to
5057
+ the rim at the turn speed and never varies; behind it the tail swings as the
5058
+ arc opens and closes, so there is always a steady point to follow. The bound
5059
+ that picks the numbers: dasharray L 100 with dashoffset d draws the arc at
5060
+ [-d, -d+L], and under the spin's theta' (71.429 u/s at 1.4s) both ends stay
5061
+ forward iff the moving end's share of |L'| stays under theta'. An ease-in-out
5062
+ alternate breath peaks at 1.7241 * A / half-period, which at these numbers
5063
+ leaves 51.7 percent of the rim to spare. The pattern must not WRAP either:
5064
+ L 100 has period L+100, longer than the path, so the dash is laid once from
5065
+ -offset and whatever falls outside [0,100] paints nothing. Anchoring the head
5066
+ at the path's midpoint keeps every amplitude here inside the path. */
5067
+ /* the trough of the reduced-motion pulse, per theme, set where the arc lands on
5068
+ the 3:1 graphics floor over --well: 0.50 measures 3.03:1 dark, 0.84 measures
5069
+ 3.02:1 light. Light needs the higher trough because --accent on --well starts
5070
+ at 3.74:1 there against 8.31:1 dark, so it has 0.74 of headroom to spend. */
5071
+ .ring { --ring-hold-dim: 0.50; }
5072
+ [data-theme="light"] .ring { --ring-hold-dim: 0.84; }
4628
5073
  .ring.loading .ring-fill {
4629
- stroke-dasharray: 25 100;
5074
+ stroke-dasharray: 16 100;
5075
+ stroke-dashoffset: -34;
4630
5076
  transform-box: view-box;
4631
5077
  transform-origin: center;
4632
- animation: spin 0.6s linear infinite;
5078
+ animation: spin 1.4s linear infinite,
5079
+ ring-tail-breathe 1.5s ease-in-out infinite alternate;
4633
5080
  transition: none;
4634
5081
  }
5082
+ /* Restated at the loading rule's own (0,3,0) and after it: a media query adds
5083
+ no specificity, so the shipped reduce blocks at (0,1,0) cannot reach this.
5084
+ The arc holds the arithmetic mid of its own breath, at its own anchor,
5085
+ pulsing to the floor rather than turning. */
5086
+ @media (prefers-reduced-motion: reduce) {
5087
+ .ring.loading .ring-fill {
5088
+ animation: ring-hold 1.4s ease-in-out infinite;
5089
+ stroke-dasharray: 31 100; stroke-dashoffset: -19;
5090
+ }
5091
+ }
4635
5092
 
4636
5093
  /* ---- Avatar, completed ----
4637
5094
  The shipped `.avatar` is the initials disc. This adds the photo, the size
@@ -4675,6 +5132,7 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4675
5132
  .avatar-group > .avatar { box-shadow: 0 0 0 2px var(--dot-ring, var(--bg)); }
4676
5133
  .avatar-more {
4677
5134
  background: var(--surface-2);
5135
+ --tint-ground: transparent;
4678
5136
  border-color: var(--line);
4679
5137
  color: var(--text-3); /* 4.65:1 dark, 4.96:1 light on --surface-2 */
4680
5138
  font-family: var(--font-mono);
@@ -4693,12 +5151,13 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4693
5151
  height: 30%;
4694
5152
  border-radius: var(--r-pill);
4695
5153
  background: var(--text-4); /* 5.47:1 dark, 4.94:1 light against a --bg ring */
5154
+ --tint-ground: transparent;
4696
5155
  box-shadow: 0 0 0 2px var(--dot-ring, var(--bg));
4697
5156
  }
4698
- .avatar-presence.online { background: var(--success); } /* 8.89 dark, 4.85 light */
4699
- .avatar-presence.away { background: var(--warning); } /* 9.68 dark, 5.25 light */
4700
- .avatar-presence.busy { background: var(--danger); } /* 5.98 dark, 5.36 light */
4701
- .avatar-presence.offline { background: var(--text-4); }
5157
+ .avatar-presence.online { background: var(--success); --tint-ground: transparent; } /* 8.89 dark, 4.85 light */
5158
+ .avatar-presence.away { background: var(--warning); --tint-ground: transparent; } /* 9.68 dark, 5.25 light */
5159
+ .avatar-presence.busy { background: var(--danger); --tint-ground: transparent; } /* 5.98 dark, 5.36 light */
5160
+ .avatar-presence.offline { background: var(--text-4); --tint-ground: transparent; }
4702
5161
 
4703
5162
  /* The unread mark for an icon button, which the navbar wanted and had no way
4704
5163
  to say. It sits outside the button's box, so it can never land on the glyph,
@@ -4715,14 +5174,12 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4715
5174
  height: 8px;
4716
5175
  border-radius: var(--r-pill);
4717
5176
  background: var(--accent); /* 7.49:1 dark, 4.09:1 light against a --bg ring */
5177
+ --tint-ground: transparent;
4718
5178
  box-shadow: 0 0 0 2px var(--dot-ring, var(--bg));
4719
5179
  }
4720
5180
 
4721
5181
  /* ---- Banner ----
4722
- One per page, at the top: a condition the whole page is under. `.callout`
4723
- explains something where it stands and stays; `.toast` announces something
4724
- that just happened and leaves; a banner states a condition and stays until
4725
- it is dismissed or resolved.
5182
+ One per page, at the top: a condition the whole page is under.
4726
5183
 
4727
5184
  The callout's material, because it is the same material: a 6 percent tint
4728
5185
  over surface-1 and the lit edge. What differs is the placement and the
@@ -4735,7 +5192,6 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4735
5192
  at --text-3 4.81 / 5.38. --text-4 was the first choice and lands at 4.14 on
4736
5193
  the dark warning tint. */
4737
5194
  .banner {
4738
- align-items: center; /* her call: the icon, the actions and the close centre on the body */
4739
5195
  flex-wrap: wrap;
4740
5196
  padding: var(--space-3) var(--space-4);
4741
5197
  }
@@ -4744,11 +5200,11 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4744
5200
  a tint declared earlier in the file would lose to it and every banner would
4745
5201
  paint plain surface-1. The validation summary is the danger one, and its tint
4746
5202
  resolves to #37313F dark, #F9F0F4 light. */
4747
- .callout-info, .banner-info { background: color-mix(in srgb, var(--info) 6%, var(--surface-1)); }
4748
- .callout-success, .banner-success { background: color-mix(in srgb, var(--success) 6%, var(--surface-1)); }
4749
- .callout-warning, .banner-warning { background: color-mix(in srgb, var(--warning) 6%, var(--surface-1)); }
5203
+ .callout-info, .banner-info { background: color-mix(in srgb, var(--info) 6%, var(--surface-1)); --tint-ground: transparent; }
5204
+ .callout-success, .banner-success { background: color-mix(in srgb, var(--success) 6%, var(--surface-1)); --tint-ground: transparent; }
5205
+ .callout-warning, .banner-warning { background: color-mix(in srgb, var(--warning) 6%, var(--surface-1)); --tint-ground: transparent; }
4750
5206
  .callout-danger, .banner-danger,
4751
- .form-summary { background: color-mix(in srgb, var(--danger) 6%, var(--surface-1)); }
5207
+ .form-summary { background: color-mix(in srgb, var(--danger) 6%, var(--surface-1)); --tint-ground: transparent; }
4752
5208
 
4753
5209
  /* Only in the bleed form. The column itself is shared with .navbar-inner. */
4754
5210
  .banner-inner { align-items: center; flex-wrap: wrap; }
@@ -4758,7 +5214,6 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4758
5214
  padding-inline: var(--space-8); /* the page column's own gutter */
4759
5215
  }
4760
5216
 
4761
- .banner-icon svg { display: block; }
4762
5217
  .banner-info .banner-icon { color: var(--info); }
4763
5218
  .banner-success .banner-icon { color: var(--success); }
4764
5219
  .banner-warning .banner-icon { color: var(--warning); }
@@ -4825,6 +5280,25 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4825
5280
  color: var(--text-3); /* 5.49:1 dark / 5.92:1 light on --surface-1, 7.05 / 4.82 on --well */
4826
5281
  }
4827
5282
  .close svg { width: 14px; height: 14px; display: block; }
5283
+ /* THE remove hover, one glyph and one grey well wherever the control sits.
5284
+ --danger on the --well fill: 6.62:1 dark, 4.89:1 light. The well reads
5285
+ 1.28:1 dark and 1.23:1 light on a card, 1.39-1.46 / 1.12-1.13 on the four
5286
+ banner tints, 1.28-1.51 on the chips. Where the ground is not a card the
5287
+ fill is named per ground below, because no single grey reads on every
5288
+ ground: --surface-2 measures 1.18:1 on a dark card and 1.08-1.10 on the
5289
+ light tints, and --well itself is 1.00 on the light well row.
5290
+
5291
+ LIGHT HOLDS THREE GROUNDS NO GREY CLEARS 1.2 ON: the four banner tints
5292
+ (well 1.12-1.13; the nearest fill, --surface-3, reads 1.19-1.21 and drops
5293
+ the glyph to 4.60), the select chip lifted by an open panel (1.03; the
5294
+ nearest, --surface-1, reads 1.19), and the glass panel's worst plate (see
5295
+ the glass rule below). --well stays because it is the one fill that holds
5296
+ the glyph's 4.5 floor everywhere in light, and every one of these grounds
5297
+ already sat under an invisible well before this rule: the neutral hover's
5298
+ --surface-2 measured 1.08-1.10 on the tints and 1.03 on the lifted chip.
5299
+ A visible well on any of the three needs a token this ramp does not have,
5300
+ which the system owns, not a fill to invent here. The hover itself lives down
5301
+ in the select-chip block, placed after the one rest rule that ties it. */
4828
5302
 
4829
5303
  /* the exit is the chip's shape at the chip's speed; the page removes the node
4830
5304
  on animationend, and that removal is the one frame the page reflows */
@@ -4856,9 +5330,11 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4856
5330
  @media (prefers-reduced-motion: reduce) {
4857
5331
  .accordion > .disclosure::details-content,
4858
5332
  .accordion > .disclosure[open]::details-content { transition: none; }
5333
+ /* the chevron still turns, it just arrives there */
5334
+ .disclosure summary svg { transition: none; }
4859
5335
  .meter-fill { transition: background var(--dur-tint) var(--ease); }
4860
5336
  .ring-fill { transition: stroke var(--dur-tint) var(--ease); }
4861
- .banner.closing { animation-name: chip-out-fade; }
5337
+ .banner.closing { animation-name: fade-out; }
4862
5338
  }
4863
5339
 
4864
5340
  /* ---- Market surfaces ----
@@ -4867,9 +5343,9 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4867
5343
  here paints a second tone: `.band` is the tone device and every one of these
4868
5344
  sits on it or on the page.
4869
5345
 
4870
- REGISTER. The demo strings are all lowercase, because these surfaces were
4871
- built for cloudy's own site. A sentence-case product page swaps the copy and
4872
- keeps the classes; nothing in this block encodes a case.
5346
+ REGISTER. The demo strings are all lowercase; a sentence-case product page
5347
+ swaps the copy and keeps the classes, and nothing in this block encodes a
5348
+ case.
4873
5349
 
4874
5350
  ONE GRID, SIX USERS. Every row here is N equal cells that collapse, and the
4875
5351
  collapse is keyed on the CELL's own minimum rather than on a viewport
@@ -4927,10 +5403,11 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4927
5403
  and not a 3.4k headline. A build that wants more raises --page-w once.
4928
5404
 
4929
5405
  The headline is `.page-title` and the sentence is `.section-lede`, the two
4930
- the system already ships: --t-8 falling to --t-7 under 768px, overflow-wrap
5406
+ the system already ships: --t-9 falling to --t-8 under 768px, overflow-wrap
4931
5407
  on the title because a headline is content, and the measure cap on the lede.
4932
- A headline twice as long wraps inside the cell and moves the media down; no
4933
- rule here reads its length. */
5408
+ The hero's lede alone steps up to --t-5, so the one sentence under a 64px
5409
+ headline does not read as a caption. A headline twice as long wraps inside
5410
+ the cell and moves the media down; no rule here reads its length. */
4934
5411
  .hero {
4935
5412
  --cell-min: 400px;
4936
5413
  --cell-cols: 2;
@@ -4947,13 +5424,17 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4947
5424
  }
4948
5425
  .hero-stack > .hero-copy > .toolbar { justify-content: center; }
4949
5426
 
4950
- /* Flow, not flex: the three gaps differ (12 under the eyebrow, 16 under the
4951
- headline, 24 over the actions) and one gap value for all three reads as a
4952
- list of four things rather than as one block with an action under it. */
5427
+ /* Flow, not flex: 16 under the eyebrow and under the headline, 24 over the
5428
+ actions, and one gap value for all three reads as a list of four things
5429
+ rather than as one block with an action under it. */
4953
5430
  .hero-copy > :first-child { margin-block-start: 0; }
4954
5431
  .hero-copy > .label { color: var(--text-4); } /* 5.47:1 dark, 4.94:1 light on --bg */
4955
- .hero-copy > .page-title { margin-block-start: var(--space-3); }
4956
- .hero-copy > .section-lede { margin-block-start: var(--space-4); }
5432
+ .hero-copy > .page-title { margin-block-start: var(--space-4); }
5433
+ .hero-copy > .section-lede {
5434
+ margin-block-start: var(--space-4);
5435
+ font-size: var(--t-5);
5436
+ line-height: 1.5;
5437
+ }
4957
5438
  .hero-copy > .toolbar { margin-block-start: var(--space-6); }
4958
5439
 
4959
5440
  .hero-media { min-width: 0; }
@@ -4998,11 +5479,21 @@ th.num-col .th-sort { flex-direction: row-reverse; }
4998
5479
  }
4999
5480
  /* A pull quote. Not `.prose blockquote`: that one is a rule in the margin of a
5000
5481
  document, this is a cell that stands alone. --text-2 measures 8.43 / 9.36 /
5001
- 7.30 dark and 7.44 / 6.79 / 8.36 light. */
5482
+ 7.30 dark and 7.44 / 6.79 / 8.36 light on --bg / --well / --surface-1. */
5002
5483
  .quote {
5003
5484
  font-size: var(--t-3);
5004
5485
  color: var(--text-2);
5005
5486
  }
5487
+ /* The opening mark sits above the line in a 22px slot, and it is a glyph, so it
5488
+ takes the accent's text token: 9.15 / 10.15 / 7.91 dark, 5.06 / 4.62 / 5.69
5489
+ light on the same three grounds. */
5490
+ .quote::before {
5491
+ content: "\201C";
5492
+ display: block;
5493
+ height: 22px;
5494
+ font: 600 var(--t-8)/1 var(--font-sans);
5495
+ color: var(--accent-text);
5496
+ }
5006
5497
  .quote footer {
5007
5498
  margin-block-start: var(--space-3);
5008
5499
  font-size: var(--t-2);
@@ -5062,7 +5553,7 @@ th.num-col .th-sort { flex-direction: row-reverse; }
5062
5553
  `.stat-card` is the dashboard tile: a labelled surface with a delta and a
5063
5554
  sparkline, eight to a screen. This is type on a band, and there is one of it.
5064
5555
 
5065
- --t-8 in mono, tabular, stepping to --t-7 under 768px exactly as
5556
+ --t-9 in mono, tabular, stepping to --t-8 under 768px exactly as
5066
5557
  `.page-title` does, so the biggest number on the page and the biggest word
5067
5558
  on it never disagree about how big that is.
5068
5559
 
@@ -5088,9 +5579,19 @@ th.num-col .th-sort { flex-direction: row-reverse; }
5088
5579
  display: flex;
5089
5580
  flex-direction: column;
5090
5581
  gap: var(--space-2);
5582
+ container-type: inline-size;
5091
5583
  }
5092
- .claim-value { font-size: var(--t-8); }
5093
- @media (max-width: 768px) { .claim-value { font-size: var(--t-7); } }
5584
+ /* the claim is the page's number, so it takes the accent's text token:
5585
+ 9.15 / 10.15 / 7.91 dark, 5.06 / 4.62 / 5.69 light on --bg / --well /
5586
+ --surface-1 */
5587
+ .claim-value { font-size: var(--t-9); letter-spacing: -0.04em; color: var(--accent-text); }
5588
+ @media (max-width: 768px) { .claim-value { font-size: var(--t-8); } }
5589
+ /* The step down is a CELL width, not only a viewport one. A mono character at
5590
+ --t-9 advances 0.56em after the tracking, so a five-character claim ("4.5:1")
5591
+ is 179.2px, measured, and four cells in a tablet column run 158.25 to 179px
5592
+ from 769 to 852px of viewport, where the claim broke after "4.5:". A sidebar
5593
+ shell narrows the same cells at any viewport. */
5594
+ @container (width < 180px) { .claim-value { font-size: var(--t-8); } }
5094
5595
  .claim-label {
5095
5596
  font-size: var(--t-3);
5096
5597
  color: var(--text-3);
@@ -5105,13 +5606,12 @@ th.num-col .th-sort { flex-direction: row-reverse; }
5105
5606
  viewport (measured), which left a 128px band of exactly the shape the
5106
5607
  media query exists to prevent.
5107
5608
 
5108
- THE MARK IS NOT ELEVATION. --elev-1 measures 24 levels under the card and
5109
- --elev-2 measures 23 (tokens.css, measured on a 300x140 card): the second is
5110
- a wider spread, not a deeper one, so swapping them marks nothing the eye can
5111
- read. The mark is three things that do read: the badge, the filled button,
5112
- and a 2px accent edge inset along the top, which is the same device
5113
- `.row-selected` and `.palette-row.active` use for the row the system is
5114
- pointing at. --accent on --surface-1 measures 6.48:1 dark and 4.60:1 light,
5609
+ THE MARK IS NOT ELEVATION. --contact-2 buys a wider spread than --contact-1,
5610
+ not a deeper one: tokens.css carries both depths and is the one place they are
5611
+ written. Swapping them marks nothing the eye can read, so the mark is three
5612
+ things that do: the badge, the filled button, and a 2px accent edge inset
5613
+ along the top, which is the same device `.row-selected` uses for the row the
5614
+ system is pointing at. --accent on --surface-1 measures 6.48:1 dark and 4.60:1 light,
5115
5615
  over the 3:1 graphics floor. The card itself is never tinted: a coloured
5116
5616
  tier and a filled button in the same cell are two marks competing. */
5117
5617
  .pricing {
@@ -5130,7 +5630,7 @@ th.num-col .th-sort { flex-direction: row-reverse; }
5130
5630
  }
5131
5631
  /* both classes on purpose: written one class longer than `.card`, so the pair
5132
5632
  cannot be decided by wherever this block lands in the file */
5133
- .tier.tier-marked { box-shadow: var(--elev-1), inset 0 2px 0 0 var(--accent); }
5633
+ .tier.tier-marked { box-shadow: var(--contact-1), inset 0 2px 0 0 var(--accent); }
5134
5634
 
5135
5635
  .tier-head {
5136
5636
  display: flex;
@@ -5242,7 +5742,7 @@ th.num-col .th-sort { flex-direction: row-reverse; }
5242
5742
  gap: var(--space-4);
5243
5743
  }
5244
5744
  @media (max-width: 768px) { .footer-brand { grid-column: 1 / -1; } }
5245
- /* the sidebar's own brand pair, on a link: `.logo-mark` and `.logo-name` are
5745
+ /* the sidebar's own brand pair, on a link: `.sona` and `.logo-name` are
5246
5746
  already the system's mark and wordmark */
5247
5747
  .footer-logo {
5248
5748
  display: inline-flex;
@@ -5345,7 +5845,7 @@ th.num-col .th-sort { flex-direction: row-reverse; }
5345
5845
  dragged out is the END, not the band: a committed end is filled, a tentative
5346
5846
  end is outlined. A second, weaker tint would have to be translucent, and a
5347
5847
  translucent tint over the glass panel composites twice - the trap
5348
- `--glass-quiet` exists to answer.
5848
+ the glass block's opaque quiet mix exists to answer.
5349
5849
 
5350
5850
  The day number is mono: it is a numeric readout, and a proportional face puts
5351
5851
  the 1s and the 8s on different centres down a column. */
@@ -5373,6 +5873,13 @@ th.num-col .th-sort { flex-direction: row-reverse; }
5373
5873
  z-index: 60;
5374
5874
  display: flex;
5375
5875
  flex-direction: column;
5876
+ /* The width clamp, for the coarse overhang bands: 44px cells put 334px of
5877
+ panel against a 320px viewport, and the document grows a scrollbar for a
5878
+ popover. The bound keeps a 40px gutter beside the trigger's own offset and
5879
+ never bites a fine pointer: the fine panel's narrowest is 276 at a 320
5880
+ viewport against a 280 bound, and the 560 cap keeps the clamp off the
5881
+ beside-form (494 at its widest) at every width that can hold it. */
5882
+ max-width: min(560px, calc(100vw - 2 * var(--space-5)));
5376
5883
  /* no background and no border here: `.popover` carries both, and the glass
5377
5884
  block restates them at (0,1,0) after this one */
5378
5885
  }
@@ -5406,19 +5913,40 @@ th.num-col .th-sort { flex-direction: row-reverse; }
5406
5913
  .daterange-presets {
5407
5914
  display: flex;
5408
5915
  flex-direction: column;
5409
- padding-block: var(--space-1);
5916
+ gap: 2px; /* the list panel's lane */
5917
+ padding: var(--space-1); /* and its gutter */
5410
5918
  border-inline-end: 1px solid var(--line-soft);
5411
- /* "last 30 days" at --t-3 inside `.menu-item`'s 14px gutters measures 132px;
5412
- 160 leaves the longest shipped preset a whole line and no more */
5413
- min-width: 160px;
5414
- flex-shrink: 0;
5919
+ /* "last 30 days" at --t-3 measures 132px from column edge to column edge,
5920
+ gutters included; 160 leaves the longest shipped preset a whole line and no
5921
+ more. It stays out of the shared lane rule: this column turns into a
5922
+ wrapping row under 560px and would take that rule's column back.
5923
+
5924
+ A WIDTH THAT MAY SHRINK, not a floor that may not. The panel is clamped,
5925
+ and `.popover` clips, so whatever the clamp takes has to come out of this
5926
+ column or out of the content: at `min-width: 160px; flex-shrink: 0` it came
5927
+ out of the content, and a preset label past the ceiling was cut inside the
5928
+ popover with no scroll and no ellipsis - measured 48px lost at a 35
5929
+ character label, identically at coarse 830, coarse 900 and fine 1440, which
5930
+ is what says it is the clamp and not any one viewport. Resting width is
5931
+ unchanged at 160. */
5932
+ width: 160px;
5933
+ min-width: 0;
5934
+ flex-shrink: 1;
5415
5935
  }
5416
5936
  /* the rows are `.menu-item` on `role="option"`, the shape `.select-menu` already
5417
5937
  uses, so the hover, the active fill and the 44px coarse floor all arrive with
5418
5938
  the class. Roving tabindex moves real focus, so the row needs no second
5419
5939
  keyboard marker. --text-2 on the panel 4.62:1 dark and 6.67:1 light; active,
5420
- --accent-text on the opaque --glass-quiet, 5.95:1 and 4.89:1. */
5421
- .daterange-preset { padding-inline: 14px; }
5940
+ --accent-text on the panel's opaque quiet mix, 5.95:1 and 4.89:1.
5941
+
5942
+ The one thing it does NOT take from `.menu-item` is nowrap: this row is in a
5943
+ column that shrinks, so it takes two lines rather than losing its end. The
5944
+ two rules tie at (0,1,0) and this one is later, which `props white-space`
5945
+ shows and `ties` cannot, since the two are different subjects. Without it a
5946
+ label past the shrunken column runs under the panel's clip and is cut with no
5947
+ ellipsis to say so - measured, "quarter to date" wanted 110px of a 97px
5948
+ button and lost 13 silently. */
5949
+ .daterange-preset { padding-inline: 10px; white-space: normal; } /* plus the column's own 4px */
5422
5950
 
5423
5951
  .daterange-cal { padding: var(--space-3); }
5424
5952
  .daterange-head {
@@ -5513,22 +6041,23 @@ th.num-col .th-sort { flex-direction: row-reverse; }
5513
6041
  background: currentColor;
5514
6042
  }
5515
6043
 
5516
- /* --accent-text on the opaque --glass-quiet: 5.95:1 dark, 4.89:1 light */
6044
+ /* --accent-text on the panel's opaque quiet mix: 5.95:1 dark, 4.89:1 light */
5517
6045
  .daterange-day.is-in-range { background: var(--accent-quiet); color: var(--accent-text); }
5518
6046
  /* --on-accent on the fill: 8.22:1 dark, 4.62:1 light */
5519
6047
  .daterange-day:is(.is-start, .is-end) {
5520
6048
  background: var(--accent);
6049
+ --tint-ground: transparent;
5521
6050
  color: var(--on-accent);
5522
6051
  font-weight: 500;
5523
6052
  }
5524
6053
  .daterange-day.is-start { border-start-start-radius: var(--r-1); border-end-start-radius: var(--r-1); }
5525
6054
  .daterange-day.is-end { border-start-end-radius: var(--r-1); border-end-end-radius: var(--r-1); }
5526
6055
  /* THE inset focus ring, and every row in the system that needs one takes it from
5527
- here. Each of these six clips at its own edge, so the page's 3px outward ring
6056
+ here. Each of these clips at its own edge, so the page's 3px outward ring
5528
6057
  is cut on a start or end edge, lands on a neighbouring cell, or rules across
5529
6058
  the body under an open summary. The base ring pairs its outline with a 1px
5530
6059
  --bg separator shadow, which at a negative offset is a stray keyline drawn
5531
- around the box, so it goes with it. Her call 2026-09-03: all six match.
6060
+ around the box, so it goes with it. All the rows here take it.
5532
6061
  Nothing here restates the selected row's edge: a selected tree row's band is
5533
6062
  the whole active mark, and an inset edge would trace its radius as a crescent.
5534
6063
 
@@ -5540,6 +6069,7 @@ th.num-col .th-sort { flex-direction: row-reverse; }
5540
6069
  .daterange-day:focus-visible,
5541
6070
  .tree-row:focus-visible,
5542
6071
  .ctxmenu > .menu-item:focus-visible,
6072
+ .popover-menu .menu-item:focus-visible,
5543
6073
  .daterange-preset:focus-visible { outline-offset: -2px; box-shadow: none; }
5544
6074
  /* the end being dragged out is outlined, not filled. The band already shows the
5545
6075
  range; a second fill would say this end is committed when it is not. The ring
@@ -5549,15 +6079,27 @@ th.num-col .th-sort { flex-direction: row-reverse; }
5549
6079
  border-start-end-radius: var(--r-1);
5550
6080
  border-end-end-radius: var(--r-1);
5551
6081
  }
6082
+ /* and it rounds that end only where the band STOPS. Rounded against a cell that
6083
+ carries the band, the ring reads as a second object cut into the run: the
6084
+ corner has to answer the neighbour, which is why the test is on the cell and
6085
+ not on the day. */
6086
+ .daterange-grid td:has(+ td > .daterange-day:is(.is-in-range, .is-start, .is-end)) > .daterange-day.is-preview-end {
6087
+ border-start-end-radius: 0;
6088
+ border-end-end-radius: 0;
6089
+ }
5552
6090
 
5553
6091
  /* hover paints the neutral cell and leaves every range state alone: the state is
5554
- already the feedback, and a hover fill over the band breaks the run. Each
5555
- restatement is one class longer than the base, so order decides nothing.
6092
+ already the feedback, and a hover fill over the band breaks the run.
6093
+ ORDER IS THE ONLY THING DECIDING THESE THREE. `:not()` carries its argument's
6094
+ specificity, so the base measures (0,3,0) and both restatements tie it there
6095
+ rather than leading it by a class: each has to stay BELOW the base, and a
6096
+ resting screenshot cannot see them swap. The state-merge check drives
6097
+ the hover and reads the paint, which is what holds it.
5556
6098
  --text on --surface-2 measures 8.56:1 dark and 11.28:1 light; --on-accent on
5557
- --accent-hover 10.04:1 and 5.90:1. */
5558
- .daterange-day:hover:not(:disabled) { background: var(--surface-2); }
6099
+ --accent-hover 10.03:1 and 5.90:1. */
6100
+ .daterange-day:hover:not(:disabled) { background: var(--surface-2); --tint-ground: transparent; }
5559
6101
  .daterange-day.is-in-range:hover { background: var(--accent-quiet); color: var(--accent-text); }
5560
- .daterange-day:is(.is-start, .is-end):hover { background: var(--accent-hover); color: var(--on-accent); }
6102
+ .daterange-day:is(.is-start, .is-end):hover { background: var(--accent-hover); --tint-ground: transparent; color: var(--on-accent); }
5561
6103
 
5562
6104
  /* on the two filled ends the inset ring takes the fill's own on-colour, which an
5563
6105
  accent ring would vanish into */
@@ -5589,11 +6131,37 @@ th.num-col .th-sort { flex-direction: row-reverse; }
5589
6131
  .daterange-presets {
5590
6132
  flex-direction: row;
5591
6133
  flex-wrap: wrap;
6134
+ /* the side column's own 160px is a COLUMN width; stacked, the row is as
6135
+ wide as the panel */
6136
+ width: auto;
5592
6137
  min-width: 0;
5593
6138
  border-inline-end: none;
5594
6139
  border-block-end: 1px solid var(--line-soft);
5595
6140
  }
5596
6141
  }
6142
+ /* The preset column gives up its whole-line width before the panel gives up
6143
+ its shape. A 44px grid makes the beside-form 494 wide, and a shell whose
6144
+ content column starts 308 in leaves less than that, so the panel overhung
6145
+ the document from 769 to 801 (measured 33 and 2).
6146
+
6147
+ THE CLAMP IS A LENGTH, NOT A CONTENT WIDTH, and that is the whole point:
6148
+ letting the column shrink without capping the panel leaves the panel exactly
6149
+ as wide as its longest preset string, so the overhang came back at a label
6150
+ two characters longer than the sheet's - measured at coarse 769, uniform
6151
+ labels, 12 characters clears and 14 overhangs by 3, 17 by 26, 20 by 46. 440
6152
+ is the bound the shell case allows (461 of column at 769, less the page's own
6153
+ gutters), so the panel is 440 whatever it holds, and the preset column's own
6154
+ `flex-shrink` and wrap pay for it.
6155
+
6156
+ TRADING WIDTH FOR HEIGHT IS WHAT THIS AVOIDS: stacking the column takes the
6157
+ panel from 383 tall to 528, and a popover that deep hanging off a control is
6158
+ the case `.daterange-up` exists to flip, in a band where the viewport is wide
6159
+ enough that nothing needed to move. Measured in a 431px frame, the stacked
6160
+ form stood 145px past it. Under 560 the panel stacks anyway and that block's
6161
+ own `min-width: 0` owns it. */
6162
+ @media (pointer: coarse) and (min-width: 561px) and (max-width: 820px) {
6163
+ .daterange-panel { max-width: min(440px, calc(100vw - 2 * var(--space-5))); }
6164
+ }
5597
6165
 
5598
6166
  /* ---- Tree navigation ----
5599
6167
  Nested rows that expand: a file tree, a nested resource list, a scope picker.
@@ -5605,12 +6173,10 @@ th.num-col .th-sort { flex-direction: row-reverse; }
5605
6173
  knows how far down it is. The indent is the group's padding, which is what
5606
6174
  lets the line sit on the parent twist's centre at every level with one value.
5607
6175
 
5608
- The selected row's band is the shared active-row band: the quiet fill, rounded
5609
- like the sidebar's, starting at the row's own edge. Its mark stays the
5610
- system's inset accent edge, and on the rounded band the 2px edge follows the
5611
- curve. The outer corners of the tree's own top and bottom rows are the one
5612
- exception: flush with a rounded card they take the card's radius, or the band
5613
- cuts it.
6176
+ The selected row's band is the shared active-row band, and the whole mark:
6177
+ the quiet fill on the --r-1 corners every row already carries, starting at
6178
+ the row's own edge. The 2px accent capsule that used to sit beside it went
6179
+ with `.nav-indicator`.
5614
6180
 
5615
6181
  The band starts at the row's own edge rather than the tree's, which is what
5616
6182
  nesting the padding buys. A band that spans the full width at every depth
@@ -5637,7 +6203,7 @@ th.num-col .th-sort { flex-direction: row-reverse; }
5637
6203
  inset-block: 0;
5638
6204
  inset-inline-start: calc(var(--space-2) + var(--tree-twist) / 2 - 1px);
5639
6205
  width: 2px;
5640
- background: color-mix(in srgb, var(--text-4) 28%, transparent);
6206
+ background: color-mix(in srgb, var(--text-4) 28%, var(--tint-ground));
5641
6207
  }
5642
6208
 
5643
6209
  .tree-row {
@@ -5648,7 +6214,7 @@ th.num-col .th-sort { flex-direction: row-reverse; }
5648
6214
  min-height: var(--ctl-sm);
5649
6215
  padding: 0 var(--space-2);
5650
6216
  border: none;
5651
- border-radius: 0;
6217
+ border-radius: var(--r-1); /* the list panel's row radius, in every state */
5652
6218
  background: none;
5653
6219
  font: 400 var(--t-3)/1.3 var(--font-sans);
5654
6220
  color: var(--text-3); /* 5.48:1 on --surface-1 and 6.34:1 on --bg dark, 5.92 / 5.28 light */
@@ -5658,13 +6224,18 @@ th.num-col .th-sort { flex-direction: row-reverse; }
5658
6224
  user-select: none;
5659
6225
  transition: background var(--dur-tint) var(--ease), color var(--dur-tint) var(--ease);
5660
6226
  }
5661
- .tree-row:hover { background: var(--surface-2); color: var(--text-2); } /* 6.17:1 dark, 7.00:1 light */
5662
- /* The top-level rows are full width, so the first and last take the card's
5663
- radius the way a setting row's edge rows do. The last row takes its bottom
5664
- radius only as a leaf: an expanded last node puts an indented row below it,
5665
- and that indented row is the visual bottom. */
5666
- .tree > li:first-child > .tree-row { border-radius: var(--r-2) var(--r-2) 0 0; }
5667
- .tree > li:last-child:not(:has(> .tree-group)) > .tree-row { border-radius: 0 0 var(--r-2) var(--r-2); }
6227
+ .tree-row:hover { background: var(--surface-2); --tint-ground: transparent; color: var(--text-2); } /* 6.17:1 dark, 7.00:1 light */
6228
+ /* The active row's band, one rule across the three rows that paint one: the
6229
+ quiet accent fill spans the row and starts at the row's own indent. Derived
6230
+ from .nav-item.active. It sits HERE, after
6231
+ `.tree-row:hover`, because the two tie at (0,2,0) and the last of the three
6232
+ member hover rules is the tree's: placed in the sidebar block as it shipped,
6233
+ a hovered selected tree row repainted its band grey (measured
6234
+ oklch(0.345 0.026 280) under the pointer). The sidebar's and the palette's
6235
+ hover rules sit above it in the file, so the one move covers all three. */
6236
+ .nav-item.active,
6237
+ .tree-row[aria-selected="true"],
6238
+ .palette-row.active { background: var(--accent-quiet); }
5668
6239
  /* The band is the shared active-row rule; this is the accent text on it.
5669
6240
  --accent-text on the quiet fill measures 5.78:1 over --surface-1 and 6.66:1
5670
6241
  over --bg, dark. */
@@ -5761,6 +6332,7 @@ th.num-col .th-sort { flex-direction: row-reverse; }
5761
6332
  flex-shrink: 0;
5762
6333
  border-radius: var(--r-pill);
5763
6334
  background: var(--status);
6335
+ --tint-ground: transparent;
5764
6336
  }
5765
6337
  .status-key-success { --status: var(--success); }
5766
6338
  .status-key-warning { --status: var(--warning); }
@@ -5801,7 +6373,7 @@ th.num-col .th-sort { flex-direction: row-reverse; }
5801
6373
  place-items: center;
5802
6374
  padding: var(--space-10) var(--space-6);
5803
6375
  }
5804
- /* Her call 2026-09-01: these screens make the dot grid the subject, and the copy
6376
+ /* these screens make the dot grid the subject, and the copy
5805
6377
  sits in a gap torn in it. The grid has to be visible for that to be true, and
5806
6378
  at the shipped resting alpha it is not: measured, a resting dot lands 5 sRGB
5807
6379
  levels off the page at 0.24 and 13 dark / 12 light at 0.60. The lift is on the
@@ -5814,10 +6386,14 @@ th.num-col .th-sort { flex-direction: row-reverse; }
5814
6386
  grid because of a 404 six levels deep inside a panel. Measured, the direct-child
5815
6387
  form matches the route and misses the specimen. */
5816
6388
  html:has(> body > .page-state) { --dot-rest: 0.60; }
6389
+ /* The same route keeps the flat page under the grid. The gap below is a
6390
+ page-toned shape, so over the sky it cut a dark cap out of the light above
6391
+ the copy; the grid is this screen's subject, and the sky is not. */
6392
+ html:has(> body > .page-state) > body::after { content: none; }
5817
6393
  /* The gap IS the design, and it is TORN rather than cut. A page-toned box with a
5818
6394
  radius paints a hard-edged rectangle across the canvas, which reads as a card
5819
- laid on the grid: her words, "a gray square that blocks the view of background
5820
- dots". The radial ramp holds the copy's own area at page tone and hands the
6395
+ laid on the grid: a gray square that blocks the view of the background dots.
6396
+ The radial ramp holds the copy's own area at page tone and hands the
5821
6397
  grid back on the way out, so there is no edge to read. A stepped, grid-aligned
5822
6398
  edge was tried at the grid's own 40px pitch (--dot-gap) and reads no
5823
6399
  differently from the plain box. */
@@ -5838,8 +6414,8 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
5838
6414
  max-width: var(--page-narrow);
5839
6415
  text-align: center;
5840
6416
  }
5841
- /* --t-7, the section title's own step. --t-8 belongs to `.page-title`, which
5842
- names a screen inside a product; this screen has no product around it.
6417
+ /* --t-7, a step under the two display sizes. --t-9 belongs to `.page-title`,
6418
+ which names a screen inside a product; this screen has no product around it.
5843
6419
  --text measures 11.67:1 on --bg and 10.09 on --surface-1 dark, 12.00 / 13.45
5844
6420
  light. */
5845
6421
  .page-state-title {
@@ -5900,6 +6476,7 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
5900
6476
  flex: 1 1 auto;
5901
6477
  height: 1px;
5902
6478
  background: var(--surface-3);
6479
+ --tint-ground: transparent;
5903
6480
  }
5904
6481
  .page-state-foot { font-size: var(--t-2); color: var(--text-3); text-align: center; } /* 5.48 / 5.92 */
5905
6482
 
@@ -5931,7 +6508,6 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
5931
6508
  z-index: 550;
5932
6509
  min-width: 180px;
5933
6510
  max-width: min(320px, calc(100vw - 2 * var(--space-4)));
5934
- padding-block: var(--space-1);
5935
6511
  translate: 0 0;
5936
6512
  transform-origin: top left;
5937
6513
  }
@@ -5997,6 +6573,7 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
5997
6573
  width: 100%;
5998
6574
  height: 100%;
5999
6575
  background: var(--accent); /* 7.48:1 dark, 4.10:1 light on --bg, over the 3:1 floor */
6576
+ --tint-ground: transparent;
6000
6577
  transform-origin: left center;
6001
6578
  }
6002
6579
  .route-progress.is-loading { visibility: visible; }
@@ -6039,8 +6616,11 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6039
6616
  gap: var(--space-2);
6040
6617
  max-width: 100%;
6041
6618
  min-width: 0;
6042
- padding-inline: var(--space-2) 2px;
6619
+ /* no end padding: the button's hover well fills the chip's end cap, and a
6620
+ 2px sliver of chip around a hovered well read as a gap in the fill */
6621
+ padding-inline: var(--space-2) 0;
6043
6622
  background: var(--well);
6623
+ --tint-ground: transparent;
6044
6624
  border: 1px solid var(--line-soft);
6045
6625
  border-radius: var(--r-1);
6046
6626
  vertical-align: middle;
@@ -6074,10 +6654,19 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6074
6654
  }
6075
6655
  /* 24px and bare, the shape `.code-copy` already is: no border, no fill at rest,
6076
6656
  below every control size. --text-3 on --well measures 7.02:1 dark and 4.82:1
6077
- light, and --text on the hover fill 8.56:1 and 11.28:1. */
6657
+ light, and --text on the hover fill 8.56:1 and 11.28:1.
6658
+ The button takes the chip's full height and its end cap, so the hover well
6659
+ meets the chip's inner edges on all three sides: a fixed 24px height left a
6660
+ 1px sliver above and below whenever the chip's row ran taller. Stretch, not
6661
+ a height, so a wrapping value grows the well with the chip; the floor holds
6662
+ the 24 for a chip whose text is shorter than that. The radius is the
6663
+ chip's own --r-1, which sits a hair outside the 1px-border arc
6664
+ an exact concentric fit would take (9px); the chip does not clip, and the
6665
+ difference is sub-pixel at 1x. */
6078
6666
  .copy-chip-btn {
6079
6667
  width: 24px;
6080
- height: 24px;
6668
+ min-height: 24px;
6669
+ align-self: stretch;
6081
6670
  border-radius: var(--r-1);
6082
6671
  color: var(--text-3);
6083
6672
  }
@@ -6143,9 +6732,10 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6143
6732
  The cue that a scrolling box holds more than it shows. The scrollers carry it
6144
6733
  themselves: `initScrollFade` writes the cue class onto every `pre`,
6145
6734
  `.table-wrap`, `.chart-scroll`, `.modal-body`, `.log-view`,
6146
- `.pagination-pages`, `.sidebar-nav`, `.navbar-nav`, `.select-menu` and
6147
- `.palette-list`, so no consumer opts in. `no-scroll-fade` on any scroller is
6148
- the one opt-out.
6735
+ `.pagination-pages`, `.sidebar-nav` and `.navbar-nav`; a consumer scroller
6736
+ opts in with `data-scroll-fade`, and floating panels (`.select-menu`,
6737
+ `.palette-list`) scroll bare. `no-scroll-fade` on any scroller is the one
6738
+ opt-out.
6149
6739
 
6150
6740
  IT IS A MASK, NOT A PAINTED RAMP. A painted ramp has to know the surface
6151
6741
  behind it, and the same scroller sits on a card, on the page and inside a
@@ -6153,7 +6743,7 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6153
6743
 
6154
6744
  THE BLEED IS LOAD-BEARING. A mask clips to its own painting area, the painting
6155
6745
  area is the border box, and a masked box therefore loses its shadow outright:
6156
- measured on a 300x140 card, --elev-1 falls from 22 levels under and 12 beside
6746
+ measured on a 300x140 card, --elev-1 falls from 23 levels under and 11 beside
6157
6747
  to 0 and 0, and it falls there for a mask that fades nothing at all.
6158
6748
  `mask-clip: no-clip` lifts the clip, but the image then has to REACH past the
6159
6749
  box or the region outside it masks to transparent just the same (measured:
@@ -6161,8 +6751,8 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6161
6751
  sized 48px past the box on every side and carries an opaque plateau in the
6162
6752
  bleed with a hard stop at the box's own edge. 48 is `.select-menu`'s own bleed
6163
6753
  and for the same reason: --elev-float's widest coat is what has to clear it.
6164
- On the shipped `.table-wrap` with the fade on, the shadow measures 25 levels
6165
- under and 13 beside against 24 and 12 on the same page's unmasked one.
6754
+ On the shipped `.table-wrap` with the fade on, the shadow measures 24 levels
6755
+ under and 12 beside against 23 and 11 on the same page's unmasked one.
6166
6756
 
6167
6757
  THE FADE IS OFF AT REST. Both windows are 0 until a controller says the box
6168
6758
  can scroll that way, so a page with no controller paints no fade at all, and
@@ -6182,9 +6772,10 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6182
6772
  --fade-start: 0px;
6183
6773
  --fade-end: 0px;
6184
6774
  --fade-bleed: 48px;
6775
+ --fade-dir: to right;
6185
6776
  /* a mask reads alpha and nothing else. #000 and #0000 are the two ends of
6186
6777
  that alpha ramp, not a colour choice, which is why no token fits here. */
6187
- --fade-mask: linear-gradient(to right,
6778
+ --fade-mask: linear-gradient(var(--fade-dir),
6188
6779
  #000 0 var(--fade-bleed),
6189
6780
  #0000 var(--fade-bleed),
6190
6781
  #000 calc(var(--fade-bleed) + var(--fade-start)),
@@ -6199,16 +6790,9 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6199
6790
  }
6200
6791
  /* the block axis, written by the controller when the element scrolls down:
6201
6792
  `.modal-body`, `.log-view`, the sheet's own column. It rides on `.scroll-fade`,
6202
- which owns the mask. */
6203
- .scroll-fade-y {
6204
- --fade-mask: linear-gradient(to bottom,
6205
- #000 0 var(--fade-bleed),
6206
- #0000 var(--fade-bleed),
6207
- #000 calc(var(--fade-bleed) + var(--fade-start)),
6208
- #000 calc(100% - var(--fade-bleed) - var(--fade-end)),
6209
- #0000 calc(100% - var(--fade-bleed)),
6210
- #000 calc(100% - var(--fade-bleed)) 100%);
6211
- }
6793
+ which owns the mask, and turns it: the axis is the only thing that differs,
6794
+ so it is the only thing this rule says. */
6795
+ .scroll-fade-y { --fade-dir: to bottom; }
6212
6796
  .scroll-fade.has-more-start { --fade-start: var(--fade); }
6213
6797
  .scroll-fade.has-more-end { --fade-end: var(--fade); }
6214
6798
  }
@@ -6237,6 +6821,34 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6237
6821
  .scroll-fade-y.has-more-start.has-more-end {
6238
6822
  box-shadow: inset 0 1px 0 var(--line), inset 0 -1px 0 var(--line);
6239
6823
  }
6824
+ /* a scroller that carries elevation restates it beside the hairline: the
6825
+ generic rules above replace `box-shadow` whole, so a scrolling `.table-wrap`
6826
+ spent its three --elev-1 coats on one inset line. At (0,3,0) these win on
6827
+ specificity whatever the order; the scrollers with no shadow of their own
6828
+ keep the generic form. */
6829
+ .table-wrap.scroll-fade.has-more-start {
6830
+ box-shadow: var(--elev-1), inset 1px 0 0 var(--line);
6831
+ }
6832
+ .table-wrap.scroll-fade.has-more-end {
6833
+ box-shadow: var(--elev-1), inset -1px 0 0 var(--line);
6834
+ }
6835
+ .table-wrap.scroll-fade.has-more-start.has-more-end {
6836
+ box-shadow: var(--elev-1), inset 1px 0 0 var(--line), inset -1px 0 0 var(--line);
6837
+ }
6838
+ /* the second scroller in the controller's list that carries its own
6839
+ elevation. It scrolls on the block axis past its own max-height, so it
6840
+ takes the -y forms, and it floats, so the coat it restates is --elev-float:
6841
+ without this a menu long enough to scroll paints no elevation at all over
6842
+ the card behind it. */
6843
+ .select-menu.scroll-fade-y.has-more-start {
6844
+ box-shadow: var(--elev-float), inset 0 1px 0 var(--line);
6845
+ }
6846
+ .select-menu.scroll-fade-y.has-more-end {
6847
+ box-shadow: var(--elev-float), inset 0 -1px 0 var(--line);
6848
+ }
6849
+ .select-menu.scroll-fade-y.has-more-start.has-more-end {
6850
+ box-shadow: var(--elev-float), inset 0 1px 0 var(--line), inset 0 -1px 0 var(--line);
6851
+ }
6240
6852
  }
6241
6853
 
6242
6854
  /* ---- Coarse pointers ----
@@ -6245,7 +6857,20 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6245
6857
  by default, and the copy button is a 24px glyph, the same shape `.code-copy`
6246
6858
  grows from. */
6247
6859
  @media (pointer: coarse) {
6248
- .daterange { --dr-cell: var(--ctl-lg); }
6860
+ /* The cell is what the panel's width IS: seven of them plus the calendar's
6861
+ own 2 * --space-3 gutter. So the panel's clamp is expressed here, on the
6862
+ one number every part of the grid derives from, rather than on a box that
6863
+ `.popover`'s `overflow: hidden` would then cut a date column out of.
6864
+ (100vw - 64px) / 7 is that clamp read back: 64 is the panel's own
6865
+ 2 * --space-5 bound plus the calendar gutter. It bites under 372px, where
6866
+ seven 44px cells plus the gutters fit no viewport - measured 44.0 at 373
6867
+ and 374, 42.3 at 360, 36.6 at 320 - so every phone at 373 and up keeps the
6868
+ floor whole, and under it the cell keeps its 44px HEIGHT and gives up
6869
+ width, which is the only thing left to give. */
6870
+ .daterange { --dr-cell: min(var(--ctl-lg), calc((100vw - 64px) / 7)); }
6871
+ /* the token carries both axes, and only the width may give: restated at the
6872
+ base rule's own (0,1,0), later in the file, so the row keeps the floor */
6873
+ .daterange-day { height: var(--ctl-lg); }
6249
6874
  .tree-row { min-height: var(--ctl-lg); }
6250
6875
  }
6251
6876
 
@@ -6257,6 +6882,9 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6257
6882
  the ladder's rule is that reduced motion replaces. */
6258
6883
  @media (prefers-reduced-motion: reduce) {
6259
6884
  .tree-twist > svg { transition: none; }
6885
+ /* the flipped menu's own (0,2,0) and (0,3,0) outrank the `.popover` fades */
6886
+ .ctxmenu.ctxmenu-flip-y { animation-name: fade-in; }
6887
+ .ctxmenu.ctxmenu-flip-y.closing { animation-name: fade-out; }
6260
6888
  .copy-chip.is-copied .copy-chip-btn > svg { animation: none; }
6261
6889
  /* keyed on the running state like the sweep it replaces, and at that rule's own
6262
6890
  (0,2,0), or reduce would start the hold on a bar that is at rest */
@@ -6295,7 +6923,7 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6295
6923
  1.23:1 light, the same step a card takes over a well band. Every "its own
6296
6924
  surface" in this block is that step.
6297
6925
 
6298
- ONE NEUTRAL HAIRLINE, `color-mix(in srgb, var(--text-4) 40%, transparent)`,
6926
+ ONE NEUTRAL HAIRLINE, `color-mix(in srgb, var(--text-4) 40%, var(--tint-ground))`,
6299
6927
  for the dropzone's dash, the sticky column's divider and the inline field's
6300
6928
  hover edge. The opaque neutrals cannot do this job: --line measures 1.11:1 on
6301
6929
  the light well and --surface-3 1.06:1, so both vanish in Latte. The mix tracks
@@ -6373,14 +7001,14 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6373
7001
 
6374
7002
  /* ---- The panel's non-row states ----
6375
7003
  No match, loading and "type to search" are all `.palette-empty`, the system's
6376
- row that names the fix. Only the gutter changes, from the palette's 14px to
6377
- the 12px `.select-menu .menu-item` already uses, so the text column lines up
6378
- with the options above it. */
7004
+ row that names the fix. Only the gutter changes, from the palette's own 14px
7005
+ to the 8 `.select-menu .menu-item` already uses inside the panel's 4px, so
7006
+ the text column lines up with the options above it. */
6379
7007
  .select-menu .palette-empty {
6380
7008
  display: flex;
6381
7009
  align-items: center;
6382
7010
  gap: var(--space-2);
6383
- padding-inline: 12px;
7011
+ padding-inline: 8px;
6384
7012
  }
6385
7013
 
6386
7014
  /* The matched substring. `<mark>` carries a UA yellow that nothing in this
@@ -6471,10 +7099,38 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6471
7099
  whole color-mix would fall back to transparent. */
6472
7100
  .select-chip {
6473
7101
  background: color-mix(in oklab, var(--surface-1), var(--surface-2) calc(var(--lift, 0) * 100%));
7102
+ --tint-ground: transparent;
6474
7103
  color: var(--text-2); /* 7.30:1 dark and 8.36:1 light at rest, 6.17 / 7.00 open */
6475
7104
  max-width: 100%;
6476
7105
  }
6477
7106
  .select-chip > .close { color: var(--text-3); } /* 5.49 / 5.92 at rest, 4.65 / 4.96 open */
7107
+ /* THE remove hover, declared here rather than with `.close` in the
7108
+ one-remove-control section because this rest rule ties it at (0,2,0) and a
7109
+ state must not lose to a rest: the order is the same treatment
7110
+ `.menu-item.active` got against `.menu-item:hover`. The chip's own
7111
+ (0,3,0) hover carries the same costume at chip scale either way, so this
7112
+ is the belt to that braces, and the braces reach the select chip because
7113
+ every select chip is spelled `tag filter-chip select-chip`: derive that set
7114
+ from the markup rather than carrying its count, which differs per tree.
7115
+ Measured 2026-09-07: with this rest colour planted BELOW the hover, winning
7116
+ the (0,2,0) tie, a hovered select chip still paints --danger through the
7117
+ filter-chip rule. A select chip written without `.filter-chip` puts it back
7118
+ on order alone. Numbers and the light-theme record live with the skeleton
7119
+ rules in the one-remove-control section. */
7120
+ .close:hover {
7121
+ color: var(--danger);
7122
+ background: var(--well);
7123
+ --tint-ground: transparent;
7124
+ }
7125
+ /* a glass panel is the one ground where --well loses and --surface-2 reads:
7126
+ 1.28:1 dark over the black plate and 1.34 over the white, against --well's
7127
+ 1.18. --danger is --glass-danger inside these panels, and holds 6.11:1 on
7128
+ the fill. Light is one of the three grounds no grey clears 1.2 on:
7129
+ --surface-2 reads 1.05 / 1.19, the same fill the neutral hover already
7130
+ painted there, and --surface-1 clears the black plate at 1.25 only to read
7131
+ 1.00 on the white one. */
7132
+ .toast .close:hover,
7133
+ .modal .close:hover { background: var(--surface-2); --tint-ground: transparent; }
6478
7134
 
6479
7135
  /* One line, the remainder folded into a count the controller writes. Same
6480
7136
  contract `.tabs-boxed-wrap.has-ink` runs under: the class is added at runtime,
@@ -6513,7 +7169,8 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6513
7169
  text-align: center;
6514
7170
  border-radius: var(--r-2);
6515
7171
  background: var(--well);
6516
- border: 1px dashed color-mix(in srgb, var(--text-4) 40%, transparent);
7172
+ --tint-ground: transparent;
7173
+ border: 1px dashed color-mix(in srgb, var(--text-4) 40%, var(--tint-ground));
6517
7174
  cursor: pointer;
6518
7175
  transition: background var(--dur-tint) var(--ease), border-color var(--dur-tint) var(--ease);
6519
7176
  }
@@ -6572,6 +7229,7 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6572
7229
  lifts a step for this state only. */
6573
7230
  .dropzone.is-over > .dropzone-target {
6574
7231
  background: color-mix(in srgb, var(--accent) 10%, var(--well));
7232
+ --tint-ground: transparent;
6575
7233
  border-color: var(--accent);
6576
7234
  border-style: dashed;
6577
7235
  }
@@ -6588,6 +7246,7 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6588
7246
  of them shipped the lift. */
6589
7247
  .dropzone.is-rejected > .dropzone-target {
6590
7248
  background: color-mix(in srgb, var(--danger) 8%, var(--well));
7249
+ --tint-ground: transparent;
6591
7250
  border-color: var(--danger);
6592
7251
  }
6593
7252
  .dropzone.is-rejected > .dropzone-target > svg { color: var(--danger); }
@@ -6599,7 +7258,7 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6599
7258
  color: var(--text-disabled);
6600
7259
  /* the dashed edge is DERIVED from the same token as the copy, so quieting the
6601
7260
  copy quiets the edge with it and the zone keeps one construction */
6602
- border-color: color-mix(in srgb, var(--text-disabled) 40%, transparent);
7261
+ border-color: color-mix(in srgb, var(--text-disabled) 40%, var(--tint-ground));
6603
7262
  cursor: not-allowed;
6604
7263
  }
6605
7264
 
@@ -6618,6 +7277,7 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6618
7277
  gap: var(--space-3);
6619
7278
  padding: var(--space-2) var(--space-3);
6620
7279
  background: var(--well);
7280
+ --tint-ground: transparent;
6621
7281
  border-radius: var(--r-1);
6622
7282
  /* the entrance carries no fill mode, so a page with no controller paints the
6623
7283
  settled row and the row is never hidden behind a class */
@@ -6649,14 +7309,18 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6649
7309
  dark and 1.23:1 light, with --accent on it at 6.48 / 4.60 over the 3:1 floor. */
6650
7310
  .dropzone-file .progress {
6651
7311
  background: var(--surface-1);
7312
+ --tint-ground: transparent;
6652
7313
  margin-top: var(--space-2);
6653
7314
  }
6654
7315
  /* The remove is `.close`, the one remove control with the coarse floor already
6655
- on it. Only the hover changes: its --surface-2 fill measures 1.03:1 on the
6656
- light well and is not there, and removing a file is destructive, so the
6657
- colour carries it. --danger reads 6.64:1 dark and 4.89:1 light on the row. */
7316
+ on it. The row's ground is --well, the one ground where the well itself is
7317
+ invisible: --well on --well measures 1.00:1 in both themes. The fill rises
7318
+ instead, the move this block already makes for the file's progress track:
7319
+ --surface-1 reads 1.28:1 dark and 1.23:1 light on the row, and --danger on
7320
+ it 5.17:1 dark and 6.00:1 light. */
6658
7321
  .dropzone-file > .close:hover {
6659
- background: none;
7322
+ background: var(--surface-1);
7323
+ --tint-ground: transparent;
6660
7324
  color: var(--danger);
6661
7325
  }
6662
7326
 
@@ -6679,27 +7343,20 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6679
7343
  border-radius: var(--r-1);
6680
7344
  --ring-color: var(--accent-line);
6681
7345
  }
6682
- /* the corners are the ladder's: --r-1 outside, square inside. Logical, so a
6683
- right-to-left page rounds the end the reader's eye actually starts at. */
7346
+ /* the corners are the ladder's: --r-1 outside, square inside. The two end radii
7347
+ are the same pair every joined group takes, so they are declared once for all
7348
+ three in the button-group block. */
6684
7349
  .input-group > * {
6685
7350
  position: relative;
6686
7351
  border-radius: 0;
6687
7352
  }
6688
- .input-group > :first-child {
6689
- border-start-start-radius: var(--r-1);
6690
- border-end-start-radius: var(--r-1);
6691
- }
6692
- .input-group > :last-child {
6693
- border-start-end-radius: var(--r-1);
6694
- border-end-end-radius: var(--r-1);
6695
- }
6696
7353
  .input-group > * + * { margin-inline-start: -1px; }
6697
7354
  .input-group > .input { flex: 1 1 auto; min-width: 0; }
6698
7355
  /* the field keeps its accent border and gives up its own halo to the group */
6699
7356
  .input-group > .input:focus { --ring: 0px; z-index: 2; }
6700
7357
  .input-group:has(> .input:focus) { box-shadow: 0 0 0 3px var(--ring-color); }
6701
7358
  .input-group:has(> .input:is(.input-invalid, [aria-invalid="true"])) {
6702
- --ring-color: color-mix(in srgb, var(--danger) 30%, transparent);
7359
+ --ring-color: color-mix(in srgb, var(--danger) 30%, var(--glass-ground));
6703
7360
  }
6704
7361
  /* a disabled field takes its addons with it: `.input:disabled` dims only itself.
6705
7362
  The addon's own icon names --text-4, so it is restated or it stays lit inside a
@@ -6717,6 +7374,7 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6717
7374
  flex-shrink: 0;
6718
7375
  padding: 0 12px; /* `.input`'s own inline padding, so the two texts line up */
6719
7376
  background: var(--surface-1);
7377
+ --tint-ground: transparent;
6720
7378
  border: 1px solid var(--line);
6721
7379
  color: var(--text-3); /* 5.49:1 dark, 5.92:1 light on --surface-1 */
6722
7380
  font: 400 var(--t-3)/1.4 var(--font-sans);
@@ -6739,6 +7397,7 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6739
7397
  flex-shrink: 0;
6740
7398
  padding: 0;
6741
7399
  background: var(--surface-1);
7400
+ --tint-ground: transparent;
6742
7401
  border: 1px solid var(--line);
6743
7402
  color: var(--text-3); /* 5.49:1 dark, 5.92:1 light on --surface-1, over the 3:1 floor */
6744
7403
  cursor: pointer;
@@ -6750,32 +7409,16 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6750
7409
  page's, and the raised z-index keeps it over both neighbours */
6751
7410
  .input-step:focus-visible { z-index: 2; }
6752
7411
 
6753
- /* A quantity is a numeric readout, so it takes the mono face and tabular
6754
- figures, and it centres because the two controls flank it. --text measures
6755
- 12.98:1 dark and 10.95:1 light on --well. */
6756
- .input-group-num > .input {
6757
- text-align: center;
6758
- font-family: var(--font-mono);
6759
- font-variant-numeric: tabular-nums;
6760
- font-feature-settings: "tnum";
6761
- }
6762
- .input-group-num > .input[type="number"] { appearance: textfield; }
6763
- .input-group-num > .input::-webkit-outer-spin-button,
6764
- .input-group-num > .input::-webkit-inner-spin-button {
6765
- appearance: none;
6766
- margin: 0;
6767
- }
7412
+ /* The value centres because the two controls flank it. The mono face and the
7413
+ spinners-off are the number field's own, declared with `.input` above.
7414
+ --text measures 12.98:1 dark and 10.95:1 light on --well. */
7415
+ .input-group-num > .input { text-align: center; }
6768
7416
 
6769
7417
  /* ============================================================
6770
7418
  Button group and split button
6771
7419
  ============================================================ */
6772
7420
 
6773
- /* `.tabs-boxed-wrap` is the SEGMENTED TAB CONTROL: a track, a sliding ink, and
6774
- it selects a panel. `.btn-group` is actions or a pressed-state toggle set
6775
- sharing one outline, and it selects nothing. Reaching for the wrong one is
6776
- the mistake this pair exists to prevent.
6777
-
6778
- The corners are the ladder's --r-1 outside and square inside; no new number
7421
+ /* The corners are the ladder's --r-1 outside and square inside; no new number
6779
7422
  appears anywhere in this block. Members shrink rather than scroll, so a group
6780
7423
  can never widen the page: put a `.truncate` span inside a label that has to
6781
7424
  give way. Past four segments the control is a `.select`. */
@@ -6791,11 +7434,18 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6791
7434
  flex: 0 1 auto;
6792
7435
  min-width: 0;
6793
7436
  }
7437
+ /* the run's two end radii, for all three joined groups. Logical, so a
7438
+ right-to-left page rounds the end the reader's eye actually starts at. It has
7439
+ to sit AFTER the `border-radius: 0` resets above: `.btn-split > .btn` ties
7440
+ `.btn-split > .btn-split-toggle` at (0,2,0), so an earlier placement squares
7441
+ the split button's own outer end. */
7442
+ .input-group > :first-child,
6794
7443
  .btn-group > .btn:first-child,
6795
7444
  .btn-split > .btn:first-child {
6796
7445
  border-start-start-radius: var(--r-1);
6797
7446
  border-end-start-radius: var(--r-1);
6798
7447
  }
7448
+ .input-group > :last-child,
6799
7449
  .btn-group > .btn:last-child,
6800
7450
  .btn-split > .btn-split-toggle {
6801
7451
  border-start-end-radius: var(--r-1);
@@ -6842,9 +7492,21 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6842
7492
  to the tag family, where each variant remixes its OWN semantic token, and only
6843
7493
  --info happens to resolve to the accent.
6844
7494
 
6845
- It lives HERE because the two `.btn-group` selectors tie the dark rule above at
6846
- (0,4,0) and have to come after it. Every other member is unopposed. */
7495
+ ITS PLACEMENT IS FREE WHILE BOTH ITS `.btn-group` SELECTORS TRAVEL TOGETHER,
7496
+ and the tie this comment used to claim is not the one that exists. Every
7497
+ selector here carries `[data-theme="light"]`, one step above the dark rule it
7498
+ remixes: the pressed pair measures (0,4,0) and (0,5,0) against the dark (0,3,0)
7499
+ and (0,4,0), and every other member gains the same step. The one real tie is
7500
+ this rule's (0,4,0) base selector against the dark rule's (0,4,0) `:hover`
7501
+ selector on a hovered pressed segment, and it is inert only because the (0,5,0)
7502
+ selector beside it wins that element outright. Split the two and the base is
7503
+ order-decided. Measured 2026-09-07 with the dark pair appended last, winning
7504
+ every tie: the light label did not move, hovered or at rest. */
6847
7505
  [data-theme="light"] .pagination .btn[aria-current="page"],
7506
+ [data-theme="light"] .tag-accent,
7507
+ [data-theme="light"] .tab.active .tab-badge,
7508
+ [data-theme="light"] .navbar-link.active .navbar-badge,
7509
+ [data-theme="light"] .nav-item.active .nav-badge,
6848
7510
  [data-theme="light"] .step[aria-current="step"] .step-index,
6849
7511
  [data-theme="light"] .avatar,
6850
7512
  [data-theme="light"] .tree-row[aria-selected="true"],
@@ -6872,13 +7534,14 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6872
7534
  says the second one opens something. currentColor rather than a token, so it
6873
7535
  tracks the hover and press fills without a rule for each. */
6874
7536
  .btn-split > .btn-fill + .btn-fill {
6875
- border-inline-start-color: color-mix(in srgb, currentColor 25%, transparent);
7537
+ border-inline-start-color: color-mix(in srgb, currentColor 25%, var(--tint-ground));
6876
7538
  }
6877
7539
  /* One control: hovering either filled half lifts the pair together. Letting
6878
7540
  `.btn-fill:hover` fire on the half under the pointer alone is what made the
6879
7541
  caret segment read as a separate, brighter button. */
6880
7542
  .btn-split:hover > .btn-fill:not(:disabled) {
6881
7543
  background: var(--accent-hover);
7544
+ --tint-ground: transparent;
6882
7545
  border-color: var(--accent-hover);
6883
7546
  }
6884
7547
  /* The outward focus ring wraps one half and reads as a second button on the
@@ -6901,7 +7564,6 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6901
7564
  .btn-split-menu {
6902
7565
  inset-inline-start: auto;
6903
7566
  inset-inline-end: 0;
6904
- padding: var(--space-1) 0;
6905
7567
  }
6906
7568
 
6907
7569
  /* ============================================================
@@ -6955,6 +7617,7 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6955
7617
  overflow: hidden;
6956
7618
  content-visibility: hidden;
6957
7619
  background: var(--well); /* 1.28:1 dark, 1.23:1 light against the table's surface-1 */
7620
+ --tint-ground: transparent;
6958
7621
  /* a detail belonging to its row, not a hole punched across the table: full
6959
7622
  bleed with square corners inside an --r-3 container read as the second */
6960
7623
  border-radius: var(--r-1);
@@ -6997,28 +7660,31 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
6997
7660
  stays behind while the cell travels. The neutral hairline reads on all four
6998
7661
  grounds - 1.92 / 1.81 / 2.03 / 1.90 dark and 1.77 / 1.69 / 1.68 / 1.70 light
6999
7662
  on --surface-1, the hover fill, the header well and the selection tint. */
7000
- .table-sticky-1 tbody tr { background: var(--surface-1); }
7663
+ .table-sticky-1 tbody tr { background: var(--surface-1); --tint-ground: transparent; }
7001
7664
  .table-sticky-1 :is(th, td):first-child {
7002
7665
  position: sticky;
7003
7666
  inset-inline-start: 0;
7004
7667
  z-index: 1;
7005
7668
  background: inherit;
7006
- box-shadow: inset -1px 0 0 0 color-mix(in srgb, var(--text-4) 40%, transparent);
7669
+ box-shadow: inset -1px 0 0 0 color-mix(in srgb, var(--text-4) 40%, var(--tint-ground));
7007
7670
  }
7008
7671
  /* the row's own selection edge is painted by the row and covered by the cell's
7009
7672
  ground, so the frozen cell carries both marks itself */
7010
7673
  .table-sticky-1 tbody tr.row-selected :is(th, td):first-child {
7011
7674
  box-shadow:
7012
7675
  inset 2px 0 0 var(--accent),
7013
- inset -1px 0 0 0 color-mix(in srgb, var(--text-4) 40%, transparent);
7676
+ inset -1px 0 0 0 color-mix(in srgb, var(--text-4) 40%, var(--tint-ground));
7014
7677
  }
7015
7678
 
7016
7679
  /* ---- Density ----
7017
7680
  Three steps, both ends off the spacing scale, around the 10px the table
7018
7681
  already ships. The control that sets them is a `.btn-group` with
7019
- `aria-pressed`, which is the other half of this block. */
7020
- .table-wrap.table-dense :is(thead th, tbody td) { padding: var(--space-1) var(--space-3); }
7021
- .table-wrap.table-roomy :is(thead th, tbody td) { padding: var(--space-4); }
7682
+ `aria-pressed`, which is the other half of this block. BODY rows only: the
7683
+ header keeps the base height at every density, so a dense table's header is
7684
+ not the shortest thing on the page and a roomy one's does not outgrow the
7685
+ card. */
7686
+ .table-wrap.table-dense tbody td { padding: var(--space-1) var(--space-3); }
7687
+ .table-wrap.table-roomy tbody td { padding: var(--space-4); }
7022
7688
 
7023
7689
  /* ---- Inline edit ----
7024
7690
  The cell holds a real field for its whole life rather than swapping one in on
@@ -7055,11 +7721,42 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
7055
7721
  border-color: transparent;
7056
7722
  color: var(--text); /* 10.12:1 dark, 13.47:1 light on --surface-1 */
7057
7723
  }
7724
+ /* The transparent edge above is (0,1,0) and sits thousands of lines below
7725
+ `.input-invalid`, which is (0,1,0) too, so it wins on source order and an
7726
+ invalid inline cell painted NO edge at all - measured `rgba(0, 0, 0, 0)` in
7727
+ both themes, with or without the unsaved state. The attribute arm of that
7728
+ rule is (0,2,0) and always did paint; restating the pair here at (0,2,0)
7729
+ makes the class form agree with it, which is what its own comment promises.
7730
+ Scoped to this field rather than reordered upstream: the invalid rule dresses
7731
+ every control in the system and this is the one whose base rule outranks it. */
7732
+ .input-inline.input-invalid,
7733
+ .input-inline[aria-invalid="true"] {
7734
+ border-color: var(--danger); /* 5.18:1 dark, 6.02:1 light on --surface-1 */
7735
+ }
7736
+ /* unsaved. The accent hairline rides the field itself, beside the value it
7737
+ marks: on the cell's edge it sat about 290px away from a numeric column's
7738
+ field and read as a stray line. A BORDER and not a shadow, so it cannot
7739
+ composite with the field's own; the hover companion sits at (0,7,1) and beats
7740
+ the (0,5,0) neutral hover edge below on specificity, so source order does not
7741
+ decide it.
7742
+
7743
+ THE INVALID CARVE-OUT IS LOAD-BEARING, and it is what a shadow did not need:
7744
+ unsaved and failed-to-save are two states of one cell, and this selector is
7745
+ longer than `.input[aria-invalid="true"]` at (0,2,0) and its `:focus` at
7746
+ (0,3,0), so without the `:not()` pair a failed save paints the accent and
7747
+ --danger never reaches the field. The old cell shadow composited beside the
7748
+ border and could not collide with it. Same pair `.input`'s own hover rule
7749
+ carries, for the same reason. A dirty field that is also [readonly] loses to
7750
+ that rule's (0,6,0) form, which is a state pair the component cannot be in. */
7751
+ .table-wrap td.cell-edit.is-dirty .input-inline:not(.input-invalid):not([aria-invalid="true"]),
7752
+ .table-wrap td.cell-edit.is-dirty .input-inline:hover:not(.input-invalid):not([aria-invalid="true"]) {
7753
+ border-color: var(--accent); /* 6.48:1 dark, 4.60:1 light on --surface-1 */
7754
+ }
7058
7755
  /* `.input:hover:not(:focus):not(.input-invalid):not([aria-invalid="true"])` is
7059
7756
  (0,5,0) and would paint --surface-3, which measures 1.06:1 on the light well.
7060
7757
  Matched here at the same (0,5,0), later in the file. */
7061
7758
  .input-inline:hover:not(:focus):not(.input-invalid):not([aria-invalid="true"]) {
7062
- border-color: color-mix(in srgb, var(--text-4) 40%, transparent);
7759
+ border-color: color-mix(in srgb, var(--text-4) 40%, var(--tint-ground));
7063
7760
  }
7064
7761
  /* A cell nobody may edit offers no affordance, and carving `[readonly]` out of
7065
7762
  the rule above is not enough: the shipped (0,5,0) rule is still there behind
@@ -7068,7 +7765,7 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
7068
7765
  .input-inline[readonly]:hover:not(:focus):not(.input-invalid):not([aria-invalid="true"]) {
7069
7766
  border-color: transparent;
7070
7767
  }
7071
- .input-inline:focus { background: var(--well); }
7768
+ .input-inline:focus { background: var(--well); --tint-ground: transparent; }
7072
7769
  .input-inline[readonly] {
7073
7770
  color: var(--text-2); /* 7.30:1 dark, 8.36:1 light on --surface-1 */
7074
7771
  cursor: default;
@@ -7088,9 +7785,6 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
7088
7785
  min-width: 3ch;
7089
7786
  margin-inline-start: auto;
7090
7787
  }
7091
- /* unsaved. The system's own active-row edge, on the cell rather than on the
7092
- field, so it cannot be outranked by the (0,6,0) hover rule above. */
7093
- .table-wrap td.cell-edit.is-dirty { box-shadow: inset 2px 0 0 var(--accent); } /* 6.48:1 dark, 4.60:1 light */
7094
7788
  /* the field shares its cell with a spinner or a retry: it gives up the 100%
7095
7789
  width and takes what the row leaves, keeping its own pulled-back padding */
7096
7790
  .cell-edit-row {
@@ -7132,8 +7826,11 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
7132
7826
  takes its size from the cell's own line box above the floor. */
7133
7827
  .input-inline { min-height: var(--ctl-lg); }
7134
7828
  /* a dense table on a phone is not a density choice, it is a miss: the variant
7135
- collapses onto the base row, the way .btn-sm collapses onto the floor */
7136
- .table-wrap.table-dense :is(thead th, tbody td) { padding: 10px var(--space-4); }
7829
+ collapses onto the base row, the way .btn-sm collapses onto the floor. BODY
7830
+ rows only, like the density rules themselves: the header takes the base
7831
+ height at every density and this block must not be the one place it does
7832
+ not, however equal the two values happen to be today. */
7833
+ .table-wrap.table-dense tbody td { padding: 10px var(--space-4); }
7137
7834
  /* the coarse floor for every bare icon button, and it has to be THIS block:
7138
7835
  each of these has a (0,1,0) box rule further down the file that an earlier
7139
7836
  coarse declaration would lose to on source order */
@@ -7166,7 +7863,7 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
7166
7863
  overlay on the field types one real example, holds it, deletes it, and moves to
7167
7864
  the next. `data-typed` on the field carries the examples; `ui.js` builds the
7168
7865
  wrapper and the overlay and owns the loop. Ported from the contact form it was
7169
- built for, cloudy's call 2026-09-01.
7866
+ built for.
7170
7867
 
7171
7868
  IT RENDERS REAL VALUES, so it is content and not a decorative hint.
7172
7869
  `--text-placeholder` is what the native placeholder takes; this sits one step
@@ -7208,13 +7905,48 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
7208
7905
  .typed-overlay { display: none; }
7209
7906
  }
7210
7907
 
7908
+ /* ---- Draw-on underline ----
7909
+ A swash that draws itself under a highlighted word once on load. The host is
7910
+ the highlighted text, made inline-block relative; the consumer pastes this
7911
+ svg inside it with the path normalized (pathLength="1"), so the css dash
7912
+ sweep draws any shape with no js and no measured length. The stroke is the
7913
+ one shape a transform cannot draw, which is why the sweep rides
7914
+ stroke-dashoffset (motion.md's exception table). Reduced motion keeps the
7915
+ full underline and drops the sweep. */
7916
+ .draw-underline {
7917
+ position: absolute;
7918
+ left: 0;
7919
+ bottom: -0.16em;
7920
+ width: 100%;
7921
+ height: 0.38em;
7922
+ overflow: visible;
7923
+ pointer-events: none;
7924
+ }
7925
+ .draw-underline path {
7926
+ fill: none;
7927
+ stroke: var(--accent-2);
7928
+ stroke-width: 3;
7929
+ stroke-linecap: round;
7930
+ vector-effect: non-scaling-stroke;
7931
+ stroke-dasharray: 1;
7932
+ stroke-dashoffset: 1;
7933
+ animation: draw-underline var(--dur-far) var(--ease-out) forwards;
7934
+ }
7935
+ @keyframes draw-underline {
7936
+ to { stroke-dashoffset: 0; }
7937
+ }
7938
+ @media (prefers-reduced-motion: reduce) {
7939
+ .draw-underline path {
7940
+ animation: none;
7941
+ stroke-dashoffset: 0;
7942
+ }
7943
+ }
7944
+
7211
7945
  /* ---- Glass ----
7212
- Real background blur on the surfaces something actually passes behind: the
7213
- mobile topbar, the stuck section header, the floating panels, the select's
7214
- own panel, and a modal or palette panel over its scrim. The navbar is
7215
- opaque in both of its states and never in the set: at the top nothing
7216
- passes behind it, and a full-width blurred band is the slab the strength
7217
- round rejected. The scrim itself stays a flat tint.
7946
+ Real background blur, on the surfaces something actually passes behind.
7947
+ Which six they are is in references/glass.md; the navbar is not one of them,
7948
+ because at the top nothing passes behind it and a full-width blurred band is
7949
+ the slab the strength round rejected. The scrim itself stays a flat tint.
7218
7950
 
7219
7951
  Two materials, one filter. Bars tint with --bg: a bar is the page held still,
7220
7952
  and on a dark page a surface-tone bar reads as an opaque slab whatever the
@@ -7250,9 +7982,9 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
7250
7982
  The fill list is explicit rather than blanket, or a bare disabled `.btn` would
7251
7983
  grow a fill it does not have at rest.
7252
7984
 
7253
- A STATIC GUARD CANNOT SEE THIS. The check that keeps it honest is
7254
- `tools/verify-disabled.py`, which mounts each state and reads the painted
7255
- colour; that gate is only as good as the specimens the pages carry.
7985
+ A STATIC GUARD CANNOT SEE THIS. The check that keeps it honest mounts each
7986
+ state and reads the painted colour; that gate is only as good as the
7987
+ specimens the pages carry.
7256
7988
 
7257
7989
  THIS IS THE ONLY DECLARATION of the disabled label colour for the four
7258
7990
  components in the list. Each of them used to restate it as well, six dead
@@ -7265,6 +7997,7 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
7265
7997
  }
7266
7998
  :is(.btn, .daterange-day, .menu-item):is(:disabled, [aria-disabled="true"]):is([aria-pressed="true"], [aria-current="page"], .is-in-range, .is-start, .is-end, .active) {
7267
7999
  background: var(--surface-2) !important;
8000
+ --tint-ground: transparent;
7268
8001
  border-color: var(--line) !important;
7269
8002
  }
7270
8003
 
@@ -7280,7 +8013,8 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
7280
8013
  /* the tokens with no headroom left on a translucent panel. --danger is
7281
8014
  3.31:1 unlifted, and a translucent accent fill laid over a translucent
7282
8015
  surface composites twice: 4.40:1 dark, 4.04 light, against 5.95 and 4.89
7283
- for the same mix laid down opaque. Numbers and method in tokens.css. */
8016
+ for the same mix laid down opaque on a panel. Numbers and method in
8017
+ tokens.css. */
7284
8018
  --text-3: var(--glass-dim);
7285
8019
  --text-4: var(--glass-dim);
7286
8020
  --text-placeholder: var(--glass-dim);
@@ -7289,21 +8023,31 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
7289
8023
  otherwise. Its own lift is the lowest value still clearing 3:1 there. */
7290
8024
  --text-disabled: var(--glass-disabled);
7291
8025
  --danger: var(--glass-danger);
7292
- --accent-quiet: var(--glass-quiet);
8026
+ --danger-text: var(--glass-danger-text);
8027
+ /* mixed HERE, on the surface, because a custom property resolves its var()
8028
+ where it is declared: a mix on :root would read the root's transparent
8029
+ --glass-ground. On a bar both land on --bg, on a panel on --surface-1. */
8030
+ --accent-quiet: color-mix(in oklab, var(--accent) var(--glass-quiet-share), var(--glass-ground));
8031
+ --accent-line: color-mix(in srgb, var(--accent) var(--glass-line-share), var(--glass-ground));
7293
8032
  /* the dimmed icon is a graphic against the same moving surface: on the dark
7294
8033
  panel 0.85 leaves a text-3 icon at 2.99:1, under the 3:1 floor, so the
7295
8034
  lift is 0.90: 3.15 dark and 3.89 light on the panel worst cases */
7296
8035
  --icon-dim: 0.90;
7297
8036
  }
7298
8037
  /* the bar is the page held still: page colour, hairline, and whatever passes
7299
- under it, ghosting through the blur. It carries only --text and --text-2,
7300
- which hold 6.21:1 on its worst case, so no dim re-floor rides on it. */
8038
+ under it, ghosting through the blur. It re-floors the same tokens as a
8039
+ panel, because the block above names it: a bar eyebrow or placeholder reads
8040
+ --glass-dim, --glass-danger and --glass-disabled there too (the sheet's
8041
+ stuck-header specimen paints a .label on it). */
8042
+ /* --tint-ground is each surface's own tone, so a hover, press or tag tint
8043
+ inside it is an opaque mix the backdrop cannot move; any opaque ground
8044
+ nested inside resets it to transparent (the --tint-ground token's note). */
7301
8045
  .mobile-topbar,
7302
- .section-sticky.is-stuck { background: var(--glass-bar-tint); }
8046
+ .section-sticky.is-stuck { background: var(--glass-bar-tint); --tint-ground: var(--bg); --glass-ground: var(--bg); }
7303
8047
  .popover,
7304
8048
  .select-menu,
7305
8049
  .toast,
7306
- .modal { background: var(--glass-tint); }
8050
+ .modal { background: var(--glass-tint); --tint-ground: var(--surface-1); --glass-ground: var(--surface-1); }
7307
8051
  /* A full-bleed bar has one edge that meets the page, so it keeps the hairline
7308
8052
  it already had and only changes its colour. A floating panel gets all four:
7309
8053
  over a photo or a card its own tone is no longer the thing that separates
@@ -7324,8 +8068,8 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
7324
8068
  .popover,
7325
8069
  .select-menu,
7326
8070
  .toast,
7327
- .modal {
7328
- background: var(--surface-1);
8071
+ .modal,
8072
+ .section-sticky.is-stuck {
7329
8073
  -webkit-backdrop-filter: none;
7330
8074
  backdrop-filter: none;
7331
8075
  /* `unset` on a custom property is `inherit`, so each surface takes the root
@@ -7337,22 +8081,23 @@ html:has(> body > .page-state) { --dot-rest: 0.60; }
7337
8081
  --text-placeholder: unset;
7338
8082
  --text-disabled: unset;
7339
8083
  --danger: unset;
8084
+ --danger-text: unset;
7340
8085
  --accent-quiet: unset;
8086
+ --accent-line: unset;
7341
8087
  --icon-dim: unset;
8088
+ --tint-ground: unset;
8089
+ --glass-ground: unset;
7342
8090
  }
8091
+ /* the surface is the only thing that differs across the six */
8092
+ .mobile-topbar,
8093
+ .popover,
8094
+ .select-menu,
8095
+ .toast,
8096
+ .modal { background: var(--surface-1); }
7343
8097
  /* the sticky header's own pre-glass surface is the page, so that is what
7344
8098
  reduce restores, with the plain hairline in place of the glass edge */
7345
8099
  .section-sticky.is-stuck {
7346
8100
  background: var(--bg); /* same tone the @supports-less fallback paints */
7347
- -webkit-backdrop-filter: none;
7348
- backdrop-filter: none;
7349
- --text-3: unset;
7350
- --text-4: unset;
7351
- --text-placeholder: unset;
7352
- --text-disabled: unset;
7353
- --danger: unset;
7354
- --accent-quiet: unset;
7355
- --icon-dim: unset;
7356
8101
  border-bottom: 1px solid var(--line-soft);
7357
8102
  }
7358
8103
  .mobile-topbar { border-bottom-color: var(--line-soft); }