@apliteni/apliteni-ui 0.53.3 → 0.54.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.
@@ -54,3 +54,15 @@
54
54
 
55
55
 
56
56
  <!-- rule: both-themes -->
57
+
58
+ ## Keep text off grey fills
59
+
60
+ <!-- rule: text-surface -->
61
+
62
+ **Rule:** Put text on the page or card surface, never on a grey fill.
63
+
64
+ **Why:** Grey blocks make reading areas look disabled.
65
+
66
+ **Do:** Draw inputs with an edge. Keep grey fills for non-text states, such as disabled tracks and dividers.
67
+
68
+ **Don't:** Put text on grey fields, chips, headers or footers.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.53.3",
3
+ "version": "0.54.0",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
@@ -250,8 +250,8 @@
250
250
  font-family: var(--font-sans);
251
251
  font-size: var(--text-base);
252
252
  color: var(--strong);
253
- background: var(--surface-2);
254
- border: 1px solid var(--border);
253
+ background: var(--field-bg);
254
+ border: 1px solid var(--field-edge);
255
255
  border-radius: var(--radius-md);
256
256
  padding: 12px 15px;
257
257
  width: 100%;
@@ -265,7 +265,7 @@
265
265
  .ui-input:hover,
266
266
  .ui-textarea:hover,
267
267
  .ui-select:hover {
268
- border-color: var(--border-strong);
268
+ border-color: var(--field-edge-hover);
269
269
  }
270
270
  .ui-input:focus-visible,
271
271
  .ui-textarea:focus-visible,
@@ -461,16 +461,16 @@
461
461
  align-items: center;
462
462
  gap: 8px;
463
463
  padding: 24px;
464
- border: 1px dashed var(--border);
464
+ border: 1px dashed var(--field-edge);
465
465
  border-radius: var(--radius-md);
466
- background: var(--surface-2);
466
+ background: var(--field-bg);
467
467
  color: var(--text);
468
468
  font-size: var(--text-base);
469
469
  text-align: center;
470
470
  overflow-wrap: anywhere;
471
471
  }
472
472
  .ui-file:hover {
473
- border-color: var(--border-strong);
473
+ border-color: var(--field-edge-hover);
474
474
  }
475
475
  .ui-file:focus-within,
476
476
  .ui-file.is-dragging {
@@ -31,7 +31,7 @@
31
31
  (0,2,0) wins without !important — the argument `.ui-nav .ui-nav__item` makes
32
32
  in nav.css. */
33
33
  .ui-back[href] { color: var(--text); }
34
- .ui-back[href]:hover { color: var(--strong); background: var(--surface-2); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
34
+ .ui-back[href]:hover { color: var(--strong); background: var(--surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
35
35
  .ui-back:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring); }
36
36
 
37
37
  /* 16px, with the stroke raised to match: the glyph set draws at 1.7 on a 24-unit
@@ -15,14 +15,14 @@
15
15
  padding: 3px 9px;
16
16
  border-radius: var(--radius-pill);
17
17
  color: var(--text);
18
- background: var(--surface-2); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */
18
+ background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */
19
19
  white-space: nowrap;
20
20
  }
21
21
  /* Status chips take the --chip-* pairs, so the light app's solid fills live in
22
22
  tokens.css rather than in a theme override down here: on white the glow-tints
23
23
  wash out and the ink has to deepen to clear AA, and that is a token decision. */
24
24
  .ui-badge--live { color: var(--chip-success-ink); background: var(--chip-success-fill); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ }
25
- .ui-badge--soon { color: var(--purple-mid); background: var(--glow-purple); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */ }
25
+ .ui-badge--soon { color: var(--accent); background: var(--surface); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */ }
26
26
  .ui-badge--info { color: var(--chip-info-ink); background: var(--chip-info-fill); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ }
27
27
  .ui-badge--warn { color: var(--chip-warn-ink); background: var(--chip-warn-fill); }
28
28
  .ui-badge--danger { color: var(--chip-danger-ink); background: var(--chip-danger-fill); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ }
@@ -32,12 +32,7 @@
32
32
  (paid / verified / pending / failed …). Same chip, mapped to meaning. */
33
33
  .ui-badge--success { color: var(--chip-success-ink); background: var(--chip-success-fill); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ }
34
34
  .ui-badge--pending { color: var(--chip-warn-ink); background: var(--chip-warn-fill); }
35
- .ui-badge--neutral { color: var(--text); background: var(--surface-2); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ }
36
-
37
- /* The two neutral chips are the exception: their light fill is already in the
38
- surface scale (--surface-3), and only archive uses muted state ink. */
39
- :root[data-theme="light"] .ui-badge--neutral { background: var(--surface-3); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ }
40
- :root[data-theme="light"] .ui-badge--archive { /* muted-ink: state — archived record */ color: var(--muted); background: var(--surface-3); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ }
35
+ .ui-badge--neutral { color: var(--text); background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ }
41
36
 
42
37
  /* Pill: same shape, sentence case, a touch larger — a metadata chip */
43
38
  .ui-pill {
@@ -47,7 +42,7 @@
47
42
  /* rank: chip */
48
43
  font-size: var(--text-xs);
49
44
  color: var(--text);
50
- background: var(--surface-2); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */
45
+ background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */
51
46
  border-radius: var(--radius-pill);
52
47
  padding: 4px 11px;
53
48
  font-weight: var(--weight-semibold);
@@ -56,7 +51,7 @@
56
51
  same pair. On its own it was --green on --glow-green, which is 3.97:1 over
57
52
  white — under AA, while the badge beside it cleared at 4.64:1. See issue #131. */
58
53
  .ui-pill--live { color: var(--chip-success-ink); background: var(--chip-success-fill); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ }
59
- .ui-pill--soon { color: var(--purple-mid); background: var(--glow-purple); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */ }
54
+ .ui-pill--soon { color: var(--accent); background: var(--surface); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */ }
60
55
 
61
56
  /* Status dot */
62
57
  .ui-dot {
@@ -19,8 +19,10 @@
19
19
  border-radius: var(--radius-sm);
20
20
  padding: var(--btn-pad-y) var(--btn-pad-x);
21
21
  cursor: pointer;
22
- border: 1px solid var(--border);
23
- background: var(--surface-2); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
22
+ /* Light needs a stronger line here: --border on this fill measures 1.047:1.
23
+ why: docs/specification.md#elevation */
24
+ border: 1px solid var(--control-edge);
25
+ background: var(--surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
24
26
  color: var(--text);
25
27
  white-space: nowrap;
26
28
  transition: background var(--dur-fast) var(--ease),
@@ -32,7 +34,7 @@
32
34
 
33
35
  .ui-btn:hover {
34
36
  border-color: var(--accent);
35
- background: color-mix(in srgb, var(--accent) 14%, var(--surface-2)); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
37
+ background: color-mix(in srgb, var(--accent) 14%, var(--surface)); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
36
38
  color: var(--strong);
37
39
  }
38
40
  .ui-btn:active { transform: translateY(1px); }
@@ -59,7 +61,7 @@
59
61
  color: var(--text);
60
62
  }
61
63
  .ui-btn--ghost:hover {
62
- background: var(--surface-2); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
64
+ background: var(--surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
63
65
  border-color: transparent;
64
66
  color: var(--strong);
65
67
  }
@@ -108,7 +110,7 @@
108
110
  }
109
111
  /* With no box, the ink is read on whatever is behind it, so the ghost takes an ink
110
112
  set for the dullest ground: in dark 7.49 / 6.31 / 7.00 / 5.44 on --bg / --surface /
111
- --surface-2 / --surface-3, in light 5.70 / 6.17 / 5.20 / 5.39. The enabled ghost now uses body
113
+ --surface-2 / --surface-3, in light 5.85 / 6.50 / 5.49 / 5.70. The enabled ghost now uses body
112
114
  ink, keeping its words readable without changing the disabled pair. #273 cut this ink to rescue two
113
115
  dark pairs from under #220's floor; #295 re-picked --muted against the elevation
114
116
  ladder and lifted both clear, so it is the margin now rather than the rescue.
@@ -135,7 +137,7 @@
135
137
  .ui-btn[aria-busy="true"],
136
138
  .ui-btn[aria-busy="true"]:hover,
137
139
  .ui-btn[aria-busy="true"]:active {
138
- background: var(--surface-2); /* ring-gap: inherit — busy paint retains the button's containing gap. */
140
+ background: var(--surface); /* ring-gap: inherit — busy paint retains the button's containing gap. */
139
141
  border-color: var(--border);
140
142
  color: var(--text);
141
143
  filter: none;
@@ -158,7 +160,7 @@
158
160
  .ui-btn[aria-busy="true"].ui-btn--danger,
159
161
  .ui-btn[aria-busy="true"].ui-btn--danger:hover,
160
162
  .ui-btn[aria-busy="true"].ui-btn--danger:active {
161
- background: var(--surface-2); /* ring-gap: inherit — busy paint retains the button's containing gap. */
163
+ background: var(--surface); /* ring-gap: inherit — busy paint retains the button's containing gap. */
162
164
  border-color: var(--border);
163
165
  color: var(--text);
164
166
  }
@@ -7,7 +7,7 @@
7
7
  gap: 12px;
8
8
  padding: 15px 17px;
9
9
  border-radius: var(--radius-md);
10
- background: var(--surface-2); --ring-gap: var(--surface-2);
10
+ background: var(--surface); --ring-gap: var(--surface);
11
11
  font-size: calc(var(--text-sm) + 0.5px);
12
12
  line-height: 1.55;
13
13
  color: var(--text);
@@ -56,7 +56,7 @@
56
56
  .ui-toast--danger { --toast-accent: var(--pink); --toast-action-ink: var(--chip-danger-ink); --toast-glow: var(--glow-pink); --toast-on: var(--danger-contrast); --toast-solid: var(--signal-solid-danger); }
57
57
  .ui-toast--warn { --toast-accent: var(--amber); --toast-action-ink: var(--chip-warn-ink); --toast-glow: color-mix(in srgb, var(--amber) 14%, transparent); --toast-on: var(--signal-contrast); --toast-solid: var(--signal-solid-warn); }
58
58
  .ui-toast--info { --toast-accent: var(--cyan); --toast-action-ink: var(--chip-info-ink); --toast-glow: var(--glow-cyan); --toast-on: var(--signal-contrast); --toast-solid: var(--signal-solid-info); }
59
- .ui-toast--neutral { --toast-accent: var(--muted); --toast-action-ink: var(--text); --toast-glow: var(--surface-2); --toast-on: var(--signal-solid-ink); --toast-solid: var(--signal-solid-neutral); }
59
+ .ui-toast--neutral { --toast-accent: var(--muted); --toast-action-ink: var(--text); --toast-glow: var(--surface); --toast-on: var(--signal-solid-ink); --toast-solid: var(--signal-solid-neutral); }
60
60
 
61
61
  .ui-toast__icon { width: 22px; height: 22px; flex: none; display: grid; place-items: center; background: transparent; color: var(--toast-action-ink); }
62
62
  .ui-toast__icon svg { width: var(--space-4); height: var(--space-4); stroke: currentColor; fill: none; stroke-width: 2.25; } /* 16px × 2.25 / 24 = 1.5 CSS px. */
@@ -9,7 +9,9 @@
9
9
  background: var(--surface); --ring-gap: var(--surface);
10
10
  border: 1px solid var(--border);
11
11
  border-radius: var(--radius-xl);
12
- padding: var(--space-6) 26px;
12
+ /* Was `var(--space-6) 26px`; 26 is off the scale and broke the column's text edge.
13
+ why: docs/specification.md#spacing-and-rhythm */
14
+ padding: var(--space-6);
13
15
  position: relative;
14
16
  }
15
17
 
@@ -22,17 +24,25 @@
22
24
  (No `.ui-card + .ui-card` margin: it leaks into grid/flex rows and misaligns.) */
23
25
  .ui-card-stack { display: flex; flex-direction: column; gap: var(--space-5); }
24
26
 
25
- .ui-card--pad-sm { padding: var(--space-5); }
27
+ /* A padding modifier moves the vertical rhythm and keeps the text edge, so cards of
28
+ different padding line up in one column. --pad-lg is the centred landing card, never
29
+ stacked with plain ones, and keeps its own inset.
30
+ why: docs/specification.md#spacing-and-rhythm */
31
+ .ui-card--pad-sm { padding: var(--space-5) var(--space-6); }
26
32
  .ui-card--pad-lg { padding: var(--space-8) var(--space-10); }
27
33
 
28
34
  /* An accent-tinted card, e.g. a highlighted plan / success surface. The accent edge is
29
- the card's own hairline re-coloured: an inset ring over it would draw two. #295 */
35
+ the card's own hairline re-coloured: an inset ring over it would draw two. #295
36
+ The strength is --card-tint, per theme: light mixes down from white, so a tint that
37
+ works in dark sinks a light card below the page. Each variant edges itself in its own
38
+ colour. why: docs/specification.md#elevation */
30
39
  .ui-card--accent {
31
- background: color-mix(in srgb, var(--accent) 9%, var(--surface)); --ring-gap: color-mix(in srgb, var(--accent) 9%, var(--surface));
40
+ background: color-mix(in srgb, var(--accent) var(--card-tint), var(--surface)); --ring-gap: color-mix(in srgb, var(--accent) var(--card-tint), var(--surface));
32
41
  border-color: color-mix(in srgb, var(--accent) 22%, transparent);
33
42
  }
34
43
  .ui-card--live {
35
- background: color-mix(in srgb, var(--green) 10%, var(--surface)); --ring-gap: color-mix(in srgb, var(--green) 10%, var(--surface));
44
+ background: color-mix(in srgb, var(--green) var(--card-tint), var(--surface)); --ring-gap: color-mix(in srgb, var(--green) var(--card-tint), var(--surface));
45
+ border-color: color-mix(in srgb, var(--green) 22%, transparent);
36
46
  }
37
47
 
38
48
  /* Interactive card (link / button semantics) */
@@ -4,7 +4,7 @@
4
4
  * ========================================================================== */
5
5
 
6
6
  .ui-snippet {
7
- background: var(--surface-2); --ring-gap: var(--surface-2);
7
+ background: var(--surface); --ring-gap: var(--surface);
8
8
  border-radius: var(--radius-md);
9
9
  overflow: hidden;
10
10
  }
@@ -71,7 +71,7 @@
71
71
  .ui-code {
72
72
  font-family: var(--font-mono);
73
73
  font-size: 0.9em;
74
- background: var(--surface-2); --ring-gap: var(--surface-2);
74
+ background: var(--surface); --ring-gap: var(--surface);
75
75
  padding: 2px 6px;
76
76
  border-radius: var(--radius-xs);
77
77
  color: var(--text);
@@ -178,7 +178,7 @@
178
178
  background: none;
179
179
  }
180
180
  .ui-cmdk__item[hidden] { display: none; } /* motion: still — a row the query no longer answers is gone on the next keystroke */
181
- .ui-cmdk__item.is-active { background: var(--surface-3); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
181
+ .ui-cmdk__item.is-active { outline: 1px solid var(--control-edge); outline-offset: -1px; background: var(--surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
182
182
  .ui-cmdk__item.is-active .ui-cmdk__label { color: var(--strong); }
183
183
 
184
184
  .ui-cmdk__ic {
@@ -239,9 +239,9 @@
239
239
  .ui-cmdk__item.is-danger.is-active .ui-cmdk__ic { color: var(--pink); }
240
240
 
241
241
  /* A disabled row is painted, never faded. It takes --disabled-ink-bare, the ink
242
- for a control with no box of its own: --disabled-surface IS --surface-2,
243
- which is the panel behind it, so a disabled row paints no box a reader can
244
- see and its label is read on whatever the panel is.
242
+ for a control with no box of its own. Disabled rows keep the panel's
243
+ surface and their labels remain readable on that ground without
244
+ adding a grey box behind the text.
245
245
  why: docs/specification.md#text-ink */
246
246
  .ui-cmdk__item.is-disabled { cursor: default; /* muted-ink: state — unavailable control */ color: var(--disabled-ink-bare); }
247
247
  .ui-cmdk__item.is-disabled .ui-cmdk__label,
@@ -256,9 +256,9 @@
256
256
  padding: 2px 5px;
257
257
  border: 1px solid var(--border);
258
258
  border-radius: var(--radius-xs);
259
- /* A key cap reads as raised, so it takes the step above the panel rather than
260
- the card step below it. why: docs/specification.md#elevation */
261
- background: var(--surface-3); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */
259
+ /* Key caps use an edge on the card surface, keeping the shortcut readable.
260
+ why: docs/specification.md#elevation */
261
+ background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */
262
262
  font-family: var(--font-sans);
263
263
  font-size: var(--text-xs);
264
264
  line-height: 1.5;
@@ -304,7 +304,7 @@
304
304
  background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */
305
305
  border: 1px solid var(--border);
306
306
  }
307
- .ui-cmdk--roomy .ui-cmdk__item.is-active .ui-cmdk__ic { background: var(--surface-2); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ }
307
+ .ui-cmdk--roomy .ui-cmdk__item.is-active .ui-cmdk__ic { background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ }
308
308
 
309
309
  /* -- Narrow --------------------------------------------------------------- */
310
310
  /* At the kit's narrowest step the palette is the page: full width, off the top
@@ -151,9 +151,9 @@
151
151
  color: var(--text);
152
152
  transition: background var(--dur-fast) var(--ease);
153
153
  }
154
- /* A row in a raised panel lifts: the step above it, never the card step below. why: docs/specification.md#elevation */
155
- .ui-dropdown__item:hover { background: var(--surface-3); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
156
- .ui-dropdown__item:focus-visible { outline: 2px solid transparent; background: var(--surface-3); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ box-shadow: var(--ring); }
154
+ /* Rows keep a reading surface; hover adds an edge. why: docs/specification.md#elevation */
155
+ .ui-dropdown__item:hover { outline: 1px solid var(--control-edge); outline-offset: -1px; background: var(--surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
156
+ .ui-dropdown__item:focus-visible { outline: 2px solid transparent; background: var(--surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ box-shadow: var(--ring); }
157
157
  .ui-dropdown__main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
158
158
  .ui-dropdown__label { font-family: var(--font-sans); font-size: calc(var(--text-sm) - 0.5px); color: var(--strong); font-weight: 600; letter-spacing: 0.01em; }
159
159
  .ui-dropdown__desc { font-size: var(--text-xs); color: var(--text); line-height: 1.35; }
@@ -180,14 +180,14 @@
180
180
  flex: none;
181
181
  color: var(--text);
182
182
  /* A chip is the top step. why: docs/specification.md#elevation */
183
- background: var(--surface-3); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */
183
+ background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */
184
184
  }
185
185
  .ui-dropdown__badge.is-live { color: var(--chip-success-ink); background: var(--chip-success-fill); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ }
186
186
  /* The accent counter keeps the badge's flat surface and changes only the ink — the shape
187
- src/styles/nav.css:163 `.ui-nav__item.is-active .ui-nav__badge.is-accent` already holds.
187
+ src/styles/nav.css:158 `.ui-nav__item.is-active .ui-nav__badge.is-accent` already holds.
188
188
  The accent wash goes on a base surface, never a raised one, and #295 made this panel a
189
189
  raised one. why: docs/specification.md#elevation */
190
- .ui-dropdown__badge.is-accent { color: var(--accent); background: var(--surface-3); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ }
190
+ .ui-dropdown__badge.is-accent { color: var(--accent); background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ }
191
191
 
192
192
  /* Disabled + danger rows */
193
193
  .ui-dropdown__item.is-disabled { /* muted-ink: state — unavailable control */ color: var(--disabled-ink); cursor: not-allowed; pointer-events: none; }
@@ -257,9 +257,9 @@
257
257
  font: inherit;
258
258
  font-size: calc(var(--text-sm) - 0.5px);
259
259
  color: var(--strong);
260
- /* A field is the sunken step, the same one .ui-input takes. why: docs/specification.md#elevation */
261
- background: var(--surface-2); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
262
- border: 1px solid var(--border);
260
+ /* The ground and line .ui-input takes. why: docs/specification.md#elevation */
261
+ background: var(--field-bg); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
262
+ border: 1px solid var(--field-edge);
263
263
  border-radius: var(--radius-sm);
264
264
  padding: 8px 12px 8px 34px;
265
265
  /* The height Chromium paints (38.25px), stated so the target-size gate, which
@@ -267,14 +267,12 @@
267
267
  min-height: 38px;
268
268
  transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
269
269
  }
270
- /* Light sinks the field one rung less far: its panel is the only pure white the
271
- kit paints, so --surface-2 falls two rungs under it rather than one. The
272
- numbers are in the spec and the gate is stories/dropdown-field-ground.test.js.
273
- why: docs/specification.md#elevation */
274
- :root[data-theme="light"] .ui-dropdown__search-input { background: var(--bg); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
270
+ /* The light-only `background: var(--bg)` override this field used to carry is gone:
271
+ --field-bg says the same thing for every field. Gated by
272
+ stories/dropdown-field-ground.test.js. why: docs/specification.md#elevation */
275
273
 
276
274
  .ui-dropdown__search-input::placeholder { /* muted-ink: placeholder — empty search field */ color: var(--muted); }
277
- .ui-dropdown__search-input:hover { border-color: var(--border-strong); }
275
+ .ui-dropdown__search-input:hover { border-color: var(--field-edge-hover); }
278
276
  .ui-dropdown__search-input:focus-visible { outline: 2px solid transparent; border-color: var(--accent); box-shadow: var(--ring); }
279
277
 
280
278
  .ui-dropdown__list { max-height: 300px; overflow-y: auto; }
@@ -284,7 +282,7 @@
284
282
 
285
283
  /* The row Enter would pick. Focus stays in the field, which carries the ring,
286
284
  so this row takes the hover fill and a bar rather than a second ring. */
287
- .ui-dropdown__item.is-active { background: var(--surface-3); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ box-shadow: inset 2px 0 0 var(--accent); }
285
+ .ui-dropdown__item.is-active { background: var(--surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ box-shadow: inset 2px 0 0 var(--accent); }
288
286
 
289
287
  /* No match — a nudge and no action, per Guidelines / Microcopy. Empty while
290
288
  anything matches, so it takes no room. */
@@ -76,16 +76,16 @@
76
76
  .ui-fbc__head { display: flex; align-items: center; gap: 12px; padding: 17px 20px 0; }
77
77
  .ui-fbc__chip { display: inline-flex; align-items: center; gap: 8px; color: var(--accent); font-size: var(--text-sm); font-weight: var(--weight-semibold); letter-spacing: .01em; white-space: nowrap; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
78
78
  .ui-fbc__chip svg { width: 14px; height: 14px; flex: none; opacity: .85; stroke-width: 2.7; }
79
- .ui-fbc__x { margin-left: auto; background: var(--surface-2); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ /* muted-ink: glyph — dismiss icon */ color: var(--dim); border: 0; width: 28px; height: 28px; border-radius: 8px; cursor: pointer; display: grid; place-items: center; flex: none; font: inherit; }
79
+ .ui-fbc__x { margin-left: auto; background: var(--surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ /* muted-ink: glyph — dismiss icon */ color: var(--dim); border: 0; width: 28px; height: 28px; border-radius: 8px; cursor: pointer; display: grid; place-items: center; flex: none; font: inherit; }
80
80
  .ui-fbc__x:hover { color: var(--strong); }
81
81
  .ui-fbc__x svg { width: 14px; height: 14px; stroke-width: 2.7; }
82
- .ui-fbc__quote { margin: 14px 20px 0; background: var(--surface-2); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ border-radius: 12px; padding: 12px 15px; display: flex; gap: 11px; }
82
+ .ui-fbc__quote { margin: 14px 20px 0; background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ border-radius: 12px; padding: 12px 15px; display: flex; gap: 11px; }
83
83
  .ui-fbc__qm { color: var(--accent); font-size: var(--text-xl); font-weight: var(--weight-bold); line-height: .9; flex: none; opacity: .55; }
84
84
  .ui-fbc__quote q { quotes: none; color: var(--text); font-style: italic; font-size: calc(var(--text-sm) + 0.5px); line-height: 1.55; }
85
85
  .ui-fbc__body { padding: 14px 20px 0; }
86
86
  .ui-fbcomposer textarea {
87
87
  width: 100%; min-height: 96px; resize: vertical; font-family: var(--font-sans); font-size: var(--text-base); line-height: 1.55;
88
- color: var(--strong); background: var(--surface-2); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ border: 0; border-radius: 12px; padding: 13px 15px; transition: box-shadow var(--dur-fast) var(--ease);
88
+ color: var(--strong); background: var(--surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ border: 1px solid var(--field-edge); border-radius: 12px; padding: 13px 15px; transition: box-shadow var(--dur-fast) var(--ease);
89
89
  }
90
90
  .ui-fbcomposer textarea::placeholder { /* muted-ink: placeholder — empty feedback field */ color: var(--muted); }
91
91
  .ui-fbcomposer textarea:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring); }
@@ -93,7 +93,7 @@
93
93
 
94
94
  .ui-fbbtn { font-family: var(--font-sans); font-size: calc(var(--text-sm) + 0.5px); font-weight: var(--weight-semibold); border-radius: 10px; padding: 9px 17px; cursor: pointer; border: 0; transition: filter var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease); }
95
95
  .ui-fbbtn.ghost { background: transparent; color: var(--text); }
96
- .ui-fbbtn.ghost:hover { color: var(--strong); background: var(--surface-2); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
96
+ .ui-fbbtn.ghost:hover { color: var(--strong); background: var(--surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
97
97
  .ui-fbbtn.primary { background: var(--accent-strong); color: var(--accent-contrast); display: inline-flex; align-items: center; gap: 9px; }
98
98
  .ui-fbbtn.primary:hover { filter: brightness(1.08); }
99
99
  /* Same flat pair as .ui-btn:disabled, for the same reason (#220). */
@@ -111,7 +111,7 @@
111
111
  .ui-fbc__done { padding: 30px 26px 24px; text-align: center; background: radial-gradient(112% 60% at 50% 0%, var(--glow-green), transparent 62%); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */ }
112
112
  .ui-fbc__ck { width: 88px; height: 88px; margin: 0 auto 13px; display: grid; place-items: center; }
113
113
  .ui-fbck { width: 88px; height: 88px; overflow: visible; display: block; transform-origin: center; animation: uiFbCkSpring 1s cubic-bezier(.22,1,.3,1) both; } /* motion: choreographed — the frame the mark and the comet are timed against */
114
- .ui-fbck-t { fill: none; stroke: var(--surface-2); stroke-width: 9; stroke-linecap: round; stroke-linejoin: round; }
114
+ .ui-fbck-t { fill: none; stroke: var(--surface); stroke-width: 9; stroke-linecap: round; stroke-linejoin: round; }
115
115
  .ui-fbck-m { fill: none; stroke: var(--green); stroke-width: 9; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 118; stroke-dashoffset: 118; animation: uiFbCkDraw .6s cubic-bezier(.5,0,.3,1) .1s forwards; /* motion: choreographed — the mark draws while the spring is still settling */ }
116
116
  .ui-fbck-s { fill: none; stroke: var(--sheen); stroke-width: 9; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 15 400; stroke-dashoffset: 133; filter: drop-shadow(0 0 8px var(--green)) drop-shadow(0 0 15px var(--green)); opacity: 0; animation: uiFbCkComet .6s cubic-bezier(.5,0,.3,1) .1s forwards; /* motion: choreographed — the comet rides the mark, so it shares its duration and delay exactly */ }
117
117
  .ui-fbc__done h4 { color: var(--strong); font-size: calc(var(--text-lg) + 1px); font-weight: var(--weight-semibold); letter-spacing: -.01em; margin-bottom: 7px; }
@@ -23,8 +23,10 @@
23
23
  font-family: var(--font-sans);
24
24
  font-size: var(--text-base);
25
25
  color: var(--strong);
26
- background: var(--surface-2); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
27
- border: 1px solid var(--border);
26
+ /* Fields use the card surface and an edge in both themes.
27
+ why: docs/specification.md#elevation */
28
+ background: var(--field-bg); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
29
+ border: 1px solid var(--field-edge);
28
30
  border-radius: var(--radius-md);
29
31
  padding: 12px 15px;
30
32
  width: 100%;
@@ -34,7 +36,7 @@
34
36
 
35
37
  .ui-input:hover,
36
38
  .ui-textarea:hover,
37
- .ui-select:hover { border-color: var(--border-strong); }
39
+ .ui-select:hover { border-color: var(--field-edge-hover); }
38
40
 
39
41
  .ui-input:focus-visible,
40
42
  .ui-textarea:focus-visible,
@@ -27,8 +27,8 @@
27
27
  .ui-app-page { --ui-app-top: 52px; }
28
28
  .ui-app-page .topbar { position: sticky; top: 0; z-index: var(--z-sticky); }
29
29
 
30
- /* --surface-2, not --bg-elevated: elevated resolves to --bg itself in the light
31
- theme, an exact no-op that left the border as the whole of the rail.
30
+ /* The rail uses the card surface so its labels stay on a reading ground.
31
+ Its border separates navigation from the page in both themes.
32
32
  overflow-y, because the box is pinned at the viewport's height for the whole
33
33
  scroll of .ui-app — without it a row past that height paints below the fold
34
34
  and no amount of document scrolling brings it back. The rail's own padding is
@@ -39,7 +39,7 @@
39
39
  gap: var(--space-5);
40
40
  padding: var(--space-5) var(--space-4);
41
41
  border-right: var(--ui-rail-rule) solid var(--border);
42
- background: var(--surface-2); --ring-gap: var(--surface-2);
42
+ background: var(--surface); --ring-gap: var(--surface);
43
43
  position: sticky;
44
44
  top: var(--ui-app-top, 0px);
45
45
  height: calc(100vh - var(--ui-app-top, 0px));
@@ -59,11 +59,11 @@
59
59
  .ui-app__rail > .ui-app__head,
60
60
  .ui-app__rail > .ui-app__user,
61
61
  .ui-app__rail > .ui-app__foot { width: var(--ui-nav-col); flex: none; }
62
- /* The rail's hover WAS --surface-2, so moving the rail onto it would have made
63
- hovering a row do nothing. The danger row and the current row are left out by
64
- name, because this rule outranks the states nav.css writes for them.
62
+ /* Hover keeps the reading surface and uses the edge declared in nav.css.
63
+ The danger row and the current row are left out by name because this
64
+ rule outranks the states nav.css writes for them.
65
65
  */
66
- .ui-app__rail .ui-nav__item:not(.is-danger):not(.is-active):hover { background: var(--surface-3); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
66
+ .ui-app__rail .ui-nav__item:not(.is-danger):not(.is-active):hover { background: var(--surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
67
67
  .ui-app__brand {
68
68
  display: flex;
69
69
  align-items: center;
@@ -109,7 +109,7 @@
109
109
  smallest of three weak signals doing the most work, so it grows with the row. */
110
110
  /* Re-tuned at #295, which took the light rail down a step with the page. Below 720px
111
111
  the glyph is the whole row, so it is held at text grade rather than at 1.4.11's bar —
112
- stories/apps/shell-states.test.js:1403 `a resting glyph is dimmer but still legible`. */
112
+ stories/apps/shell-states.test.js:1377 `a resting glyph is dimmer but still legible`. */
113
113
  .ui-app__rail .ui-nav__ic svg { opacity: 0.66; }
114
114
  .ui-app__rail .ui-nav__item.is-active .ui-nav__ic svg,
115
115
  .ui-app__rail .ui-nav__item.is-current .ui-nav__ic svg { opacity: 1; stroke: currentColor; } /* motion: still — the glyph brightens with its row, whose own highlight already transitions */
@@ -153,7 +153,7 @@
153
153
  width: var(--ui-app-av); height: var(--ui-app-av); flex: none;
154
154
  margin-inline-start: calc((var(--ui-nav-strip) - var(--ui-app-av)) / 2);
155
155
  border-radius: var(--radius-pill);
156
- background: var(--surface-3); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */
156
+ background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */
157
157
  color: var(--strong);
158
158
  display: grid; place-items: center;
159
159
  font-size: calc(var(--text-xs) + 1px);
@@ -187,7 +187,7 @@
187
187
  font-weight: inherit;
188
188
  letter-spacing: normal;
189
189
  }
190
- .ui-app .ui-app__user-trigger:hover { background: var(--surface-3); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
190
+ .ui-app .ui-app__user-trigger:hover { background: var(--surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
191
191
  .ui-app__user-trigger .ui-app__who { flex: 1 1 auto; text-align: left; }
192
192
  /* The panel is portalled onto <body>, so nothing here may hang off .ui-app: by the
193
193
  time it is open it is no longer inside the rail that drew it.
@@ -269,7 +269,7 @@
269
269
  why: docs/specification.md#elevation */
270
270
  :where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__item:is(:hover, :focus-visible) > .ui-nav__label,
271
271
  :where(.ui-app.is-collapsed) .ui-app__rail .ui-app__user-trigger:is(:hover, :focus-visible) > .ui-app__who,
272
- .ui-app__rail .ui-app__fold:is(:hover, :focus-visible) > .ui-nav__label { position: fixed; left: calc(var(--ui-rail-w) + var(--space-2)); z-index: var(--z-dropdown); width: max-content; max-width: 15rem; margin-block: -7px; padding: 6px 10px; border: 1px solid var(--border-strong); box-shadow: inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop); border-radius: var(--radius-xs); background: var(--surface-3); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ color: var(--strong); opacity: 1; pointer-events: none; }
272
+ .ui-app__rail .ui-app__fold:is(:hover, :focus-visible) > .ui-nav__label { position: fixed; left: calc(var(--ui-rail-w) + var(--space-2)); z-index: var(--z-dropdown); width: max-content; max-width: 15rem; margin-block: -7px; padding: 6px 10px; border: 1px solid var(--border-strong); box-shadow: inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop); border-radius: var(--radius-xs); background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ color: var(--strong); opacity: 1; pointer-events: none; }
273
273
  @supports (anchor-name: --a) and (anchor-scope: --a) {
274
274
  .ui-app.is-collapsed,
275
275
  .ui-app:has(.ui-app__fold) { anchor-scope: --ui-rail; }
@@ -352,15 +352,7 @@
352
352
  font-size: var(--text-sm);
353
353
  text-align: start;
354
354
  color: var(--text);
355
- /* --surface, not the sunken --surface-2 every other field in the kit takes:
356
- those sit on a card and this one sits on the band, and the band IS --bg, the
357
- bottom of the ladder — so a sunken fill has nowhere to go but down into its
358
- own ground. Measured in light, the field was 1.10:1 BELOW the band and its
359
- key cap 1.04:1 against the field it sits in; they are 1.08 above and 1.14 now.
360
- Dark trades the other way and is stated rather than hidden: the cap goes
361
- 1.29 -> 1.16, because --surface-3 and --surface are adjacent rungs there.
362
- Artur chose this over three alternatives on #318; the survey of what seven
363
- other systems do is docs/reviews/318-search-field.md.
355
+ /* The band search uses the same reading surface as other text controls.
364
356
  why: docs/specification.md#elevation */
365
357
  background: var(--surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
366
358
  border: 1px solid var(--border);
@@ -467,7 +459,7 @@
467
459
  .ui-app__rail .ui-nav__item,
468
460
  .ui-app__rail .ui-app__user-trigger { min-height: 44px; }
469
461
  .ui-app__rail .ui-nav__item:is(:hover, :focus-visible) > .ui-nav__label,
470
- .ui-app__rail .ui-app__user-trigger:is(:hover, :focus-visible) > .ui-app__who { position: fixed; left: calc(var(--ui-rail-w) + var(--space-2)); z-index: var(--z-dropdown); width: max-content; max-width: 15rem; margin-block: -7px; padding: 6px 10px; border: 1px solid var(--border-strong); box-shadow: inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop); border-radius: var(--radius-xs); background: var(--surface-3); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ color: var(--strong); opacity: 1; pointer-events: none; }
462
+ .ui-app__rail .ui-app__user-trigger:is(:hover, :focus-visible) > .ui-app__who { position: fixed; left: calc(var(--ui-rail-w) + var(--space-2)); z-index: var(--z-dropdown); width: max-content; max-width: 15rem; margin-block: -7px; padding: 6px 10px; border: 1px solid var(--border-strong); box-shadow: inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop); border-radius: var(--radius-xs); background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ color: var(--strong); opacity: 1; pointer-events: none; }
471
463
  @supports (anchor-name: --a) and (anchor-scope: --a) {
472
464
  .ui-app { anchor-scope: --ui-rail; }
473
465
  .ui-app__rail { anchor-name: --ui-rail; }
@@ -69,9 +69,9 @@
69
69
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
70
70
  }
71
71
  .ui-nav .ui-nav__item { color: var(--text); } /* (0,2,0): a host sheet's a:link is (0,1,1) — why: docs/specification.md#the-page-shell */
72
- .ui-nav__item:hover { background: var(--surface-2); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ color: var(--strong); }
72
+ .ui-nav__item:hover { outline: 1px solid var(--control-edge); outline-offset: -1px; background: var(--surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ color: var(--strong); }
73
73
  .ui-nav__item.is-active {
74
- background: var(--surface-3); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
74
+ background: var(--surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
75
75
  color: var(--strong);
76
76
  font-weight: 500;
77
77
  }
@@ -116,15 +116,10 @@
116
116
  background and pull its label toward the rail behind it (#220). */
117
117
  .ui-nav__item.is-disabled { /* muted-ink: state — unavailable control */ color: var(--disabled-ink); cursor: default; pointer-events: none; }
118
118
 
119
- /* The current row under the pointer. It rests on --surface-3, which is also what
120
- a resting row hovers to, so the one row with no hover response was the row the
121
- reader is standing on. There is no surface above --surface-3, so the step is
122
- mixed: --text is near-white in dark and near-black in light, and both themes
123
- raise a surface by moving it away from the page. Written after the danger
124
- rules so it cannot out-rank them — sign out is never the current row, but a
125
- rule that could take its wash would be one to argue with later. */
119
+ /* Hover adds an edge without changing the current row's reading surface.
120
+ The danger row keeps its own state treatment. */
126
121
  .ui-nav__item.is-active:not(.is-danger):hover {
127
- background: color-mix(in srgb, var(--text) 10%, var(--surface-3)); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
122
+ background: var(--surface); outline: 1px solid var(--control-edge); outline-offset: -1px; /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
128
123
  }
129
124
 
130
125
  /* Per-item badge / counter */
@@ -140,7 +135,7 @@
140
135
  font-size: var(--text-xs);
141
136
  font-weight: 600;
142
137
  line-height: 1;
143
- background: var(--surface-3); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */
138
+ background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */
144
139
  color: var(--text);
145
140
  /* The count fades with the words. It is never lost: sidebarNav() spells it
146
141
  into the row's aria-label, so "Pending 3" is what a reader hears at either
@@ -150,17 +145,17 @@
150
145
  .ui-nav__badge.is-accent { background: var(--glow-purple); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */ color: var(--accent); }
151
146
  .ui-nav__badge.is-live { background: var(--chip-success-fill); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ color: var(--chip-success-ink); }
152
147
  .ui-nav__badge.is-danger { background: var(--glow-pink); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */ color: var(--pink); }
153
- .ui-nav__item.is-active .ui-nav__badge.is-neutral { background: var(--surface-3); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ color: var(--strong); }
148
+ .ui-nav__item.is-active .ui-nav__badge.is-neutral { background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ color: var(--strong); }
154
149
  /* Same shape, one row up: on an active row the accent counter takes the row's
155
150
  OWN surface instead of laying its wash over it. An active row is already
156
- painted --surface-3, so --glow-purple there is two tints stacked. Every
151
+ painted --surface, so an opaque counter avoids stacking tinted fills. Every
157
152
  figure here is measured against the tokens this kit now ships. The accent on
158
153
  that pair falls under WCAG AA in four of the eight theme x accent cells —
159
154
  4.08 in dark Nebula, where the flat surface reads 4.92 — and no alpha reaches
160
155
  it, the largest that clears being 0.06 in dark Nebula and 0.02 in light
161
156
  Emerald. So the accent wash is painted on a base surface, never a raised one.
162
157
  See #157 and docs/specification.md#colour-and-contrast. */
163
- .ui-nav__item.is-active .ui-nav__badge.is-accent { background: var(--surface-3); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ color: var(--accent); }
158
+ .ui-nav__item.is-active .ui-nav__badge.is-accent { background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ color: var(--accent); }
164
159
 
165
160
  /* Collapsible group */
166
161
  .ui-nav__group { display: flex; flex-direction: column; }
@@ -294,7 +289,7 @@
294
289
 
295
290
  /* pill variant */
296
291
  .ui-nav--tabs.is-pill .ui-nav__tab { border-radius: var(--radius-pill); padding: 8px 15px; }
297
- .ui-nav--tabs.is-pill .ui-nav__tab:hover { background: var(--surface-2); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
292
+ .ui-nav--tabs.is-pill .ui-nav__tab:hover { background: var(--surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
298
293
  .ui-nav--tabs.is-pill .ui-nav__tab.is-active {
299
294
  background: var(--glow-purple); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */
300
295
  color: var(--accent);
@@ -304,7 +299,9 @@
304
299
  .ui-nav__tab .ui-nav__badge { height: 18px; }
305
300
 
306
301
  /* -- Breadcrumbs --------------------------------------------------------- */
307
- .ui-nav__crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: 2px; }
302
+ /* The chip hangs out by its own padding so the first LABEL lands on the column's edge,
303
+ level with the page title under it. */
304
+ .ui-nav__crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: 2px; margin-inline-start: -6px; }
308
305
  .ui-nav__crumb-item { display: inline-flex; align-items: center; }
309
306
  .ui-nav__crumb-item + .ui-nav__crumb-item::before {
310
307
  content: "";
@@ -329,7 +326,7 @@
329
326
  does above: a linked crumb is an <a>, and a host stylesheet's `a:link` is
330
327
  (0,1,1), which outranks a bare (0,1,0) class. */
331
328
  .ui-nav .ui-nav__crumb { color: var(--text); }
332
- a.ui-nav__crumb:hover { color: var(--strong); background: var(--surface-2); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
329
+ a.ui-nav__crumb:hover { color: var(--strong); background: var(--surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
333
330
  a.ui-nav__crumb:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring); }
334
331
  .ui-nav__crumb.is-current { color: var(--strong); font-weight: 500; }
335
332
  .ui-nav__crumb .ui-nav__ic svg { width: 15px; height: 15px; stroke: currentColor; fill: none; stroke-width: 2.5; }
@@ -69,7 +69,7 @@
69
69
  rule about an ENABLED control out of the disabled-rule sweep in
70
70
  stories/guidelines/accessibility-floor.test.js, which reads selector text. */
71
71
  .ui-pager__page.is-current:not([disabled]) {
72
- background: var(--surface-3); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
72
+ background: var(--surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
73
73
  border-color: var(--border);
74
74
  color: var(--strong);
75
75
  font-weight: var(--weight-semibold);
@@ -1,11 +1,15 @@
1
1
  /* ============================================================================
2
2
  * Segmented control — .ui-seg (the deck's pill switch: theme, Deck/Text, plan)
3
- * The selected button rides on a raised surface with a soft shadow.
3
+ * The selected button keeps the reading surface and gains an accent edge.
4
4
  * ========================================================================== */
5
5
 
6
6
  .ui-seg {
7
7
  display: inline-flex;
8
- background: var(--surface-2); --ring-gap: var(--surface-2);
8
+ /* `inline-flex` sizes the track to its pills, but a flex or grid parent stretches a
9
+ child whatever its display says — and .ui-app__body is a flex column. The control
10
+ states its own alignment; --block and --underline ask for the width back. */
11
+ align-self: start;
12
+ background: var(--surface); --ring-gap: var(--surface);
9
13
  border-radius: var(--radius-2xl);
10
14
  padding: 3px;
11
15
  gap: 2px;
@@ -30,11 +34,12 @@
30
34
  .ui-seg button[aria-pressed="true"],
31
35
  .ui-seg button[aria-selected="true"] {
32
36
  color: var(--strong);
33
- background: var(--seg-active-bg); /* the step is the lift; nothing casts. #295 */
37
+ background: var(--surface); /* ring-gap: inherit — the button ring meets its containing track. */
38
+ outline: 1px solid var(--accent);
34
39
  }
35
40
 
36
41
  .ui-seg--sm button { padding: 5px 13px; font-size: calc(var(--text-xs) + 1px); }
37
- .ui-seg--block { display: flex; }
42
+ .ui-seg--block { display: flex; align-self: stretch; }
38
43
  .ui-seg--block button { flex: 1; }
39
44
 
40
45
  /* Accent picker — round colour swatches, active gets a ring */
@@ -56,7 +61,7 @@
56
61
  }
57
62
  .ui-accent-picker button:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring); }
58
63
 
59
- .ui-seg--underline { background: transparent; --ring-gap: inherit; border-radius: 0; border-bottom: 1px solid var(--border); padding: var(--space-1); overflow: auto; max-width: 100%; }
64
+ .ui-seg--underline { align-self: stretch; background: transparent; --ring-gap: inherit; border-radius: 0; border-bottom: 1px solid var(--border); padding: var(--space-1); overflow: auto; max-width: 100%; }
60
65
  .ui-seg--underline button { border-radius: var(--radius-xs); border-bottom: 2px solid transparent; padding: var(--space-3); white-space: nowrap; }
61
66
  .ui-seg--underline button.is-active,
62
67
  .ui-seg--underline button[aria-pressed="true"] { background: transparent; border-bottom-color: var(--accent); }
@@ -54,7 +54,7 @@
54
54
  * Cell helpers: .ui-table__num (right, tabular), .ui-table__code.
55
55
  * why: docs/specification.md#spacing-and-rhythm
56
56
  * held by: stories/table-rhythm.test.js */
57
- .ui-table--dense th { padding: 0 var(--space-3) var(--space-2) 0; }
57
+ .ui-table--dense th { padding: 0 var(--space-3) var(--space-2); }
58
58
  .ui-table--dense td { padding: var(--space-2) var(--space-3); font-size: var(--text-sm); white-space: nowrap; }
59
59
  .ui-table--dense th:first-child,
60
60
  .ui-table--dense td:first-child { padding-left: var(--space-3); }
@@ -65,10 +65,11 @@
65
65
  .ui-table--zebra td { border-bottom: 0; }
66
66
  .ui-table--zebra tbody tr:nth-child(even) td { background: var(--surface-2); --ring-gap: var(--surface-2); }
67
67
  /* The stripe stays full-bleed (the #71 inset above is hover only), but it only reads right
68
- * if the end cells are inset from the tint's own edge, and `.ui-table td` sets
68
+ * if the end cells are inset from the row's own edge, and `.ui-table td` sets
69
69
  * `padding-left: 0`. Only `--dense` supplied that inset, so `--zebra` without `--dense` —
70
- * `DataTable`'s composition — sat its checkboxes at x=0 of the stripe. Head and body are
71
- * inset together, or the columns stop lining up. */
70
+ * `DataTable`'s composition — sat its checkboxes at x=0 of the row. Head and body are
71
+ * inset together, or the columns stop lining up. Held by src/styles/table.test.js, and
72
+ * kept off the card's text edge by the bleed rule at the end of this file. */
72
73
  .ui-table--zebra th:first-child,
73
74
  .ui-table--zebra td:first-child { padding-left: var(--space-3); }
74
75
  .ui-table--zebra th:last-child,
@@ -76,6 +77,19 @@
76
77
  .ui-table--zebra.ui-table--hover tbody tr:hover td,
77
78
  .ui-table--zebra tbody tr:hover td { --ring-gap: inherit; background: color-mix(in srgb, var(--accent) 8%, transparent); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */ }
78
79
 
80
+ /* A link in a cell is a value that happens to open something: the row's ink, underlined
81
+ on hover — what .ui-identity below has always done. Without it, base.css's bare
82
+ `a { color: var(--accent) }` reached the ledger's IDs.
83
+ why: guidelines/density-and-accents.md#give-accents-a-job */
84
+ .ui-table :where(td, th) a:not(.ui-btn):not(.ui-identity) {
85
+ color: inherit;
86
+ text-decoration: none;
87
+ }
88
+ .ui-table :where(td, th) a:not(.ui-btn):not(.ui-identity):hover {
89
+ color: var(--strong);
90
+ text-decoration: underline;
91
+ }
92
+
79
93
  .ui-table__num { text-align: right; font-variant-numeric: tabular-nums; }
80
94
  .ui-table th.ui-table__num { text-align: right; }
81
95
  .ui-table__num--strong { color: var(--strong); font-weight: var(--weight-semibold); }
@@ -93,9 +107,20 @@
93
107
  background: var(--table-bg); --ring-gap: var(--table-bg);
94
108
  }
95
109
  .ui-table tbody tr:hover { outline: 1px solid var(--border-strong); outline-offset: -1px; }
110
+
111
+ /* The end-cell inset above gives a row's highlight room; the bleed is what stops the grid
112
+ paying for it. The table hangs out by the same --space-3, into the card's 20-24px
113
+ padding and never past it, so the columns start on the card's own text edge and the
114
+ scroll region stays empty at rest. why: docs/specification.md#spacing-and-rhythm */
115
+ .ui-card > :where(.ui-table--dense, .ui-table--zebra) {
116
+ width: calc(100% + 2 * var(--space-3));
117
+ margin-inline: calc(-1 * var(--space-3));
118
+ }
96
119
  .ui-table td.ui-table__num { text-align: right; }
97
120
  .ui-table--compact td { padding: var(--space-1) var(--space-3); font-size: var(--text-sm); line-height: var(--leading-snug); height: 33px; }
98
121
  .ui-table--compact th { padding: var(--space-2) var(--space-3); }
122
+ .ui-table--compact th:last-child,
123
+ .ui-table--compact td:last-child { padding-right: var(--space-3); }
99
124
  .ui-table-scroll { overflow: auto; max-height: var(--ui-table-height, 70vh); padding: 0 var(--space-1); }
100
125
  .ui-table-scroll:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring); }
101
126
  .ui-table--sticky { border-collapse: separate; border-spacing: 0; }
@@ -14,7 +14,7 @@
14
14
  /* One number for the mark-to-readout distance, read by both placements here
15
15
  and by the wiring's flip test, so above and below cannot drift apart. */
16
16
  --ui-tip-gap: 8px;
17
- --ui-tip-bg: var(--surface-3);
17
+ --ui-tip-bg: var(--bg-elevated);
18
18
  position: absolute;
19
19
  top: var(--ui-tip-y, 0px);
20
20
  left: var(--ui-tip-x, 50%);
@@ -47,9 +47,9 @@
47
47
  visibility var(--dur-fast) linear;
48
48
  }
49
49
 
50
- /* No light override: it re-pointed the readout at --surface, which #295 made the card
51
- step, so a readout over a card was painted the card's own colour. --surface-3 is the
52
- readout's step in both themes. why: docs/specification.md#elevation */
50
+ /* The readout uses the floating panel surface in both themes. Its edge
51
+ and shadow separate it from the card underneath.
52
+ why: docs/specification.md#elevation */
53
53
 
54
54
  /* Below the mark: the wiring's flip when above would be clipped, or the
55
55
  author's own choice. */
@@ -48,7 +48,7 @@
48
48
  display: none;
49
49
  align-items: center;
50
50
  gap: 2px;
51
- background: var(--surface-2); --ring-gap: var(--surface-2);
51
+ background: var(--surface); --ring-gap: var(--surface);
52
52
  border: 1px solid var(--border);
53
53
  border-radius: 24px;
54
54
  padding: 3px;
@@ -179,10 +179,10 @@
179
179
  font: inherit;
180
180
  transition: background var(--dur-fast) var(--ease);
181
181
  }
182
- /* The menu is raised, so a hovered row takes the step ABOVE it — see
182
+ /* Menu rows use the reading surface and an edge on hover — see
183
183
  src/styles/dropdown.css:155 `.ui-dropdown__item:hover`.
184
184
  why: docs/specification.md#elevation */
185
- .vopt:hover { background: var(--surface-3); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
185
+ .vopt:hover { outline: 1px solid var(--control-edge); outline-offset: -1px; background: var(--surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
186
186
  .vopt .vname { font-family: var(--font-sans); font-size: calc(var(--text-sm) - 0.5px); color: var(--strong); font-weight: 600; letter-spacing: 0.01em; }
187
187
  .vopt .vmeta { font-size: var(--text-xs); color: var(--text); margin-top: 2px; }
188
188
  .vopt .vbadge {
@@ -196,7 +196,7 @@
196
196
  margin-top: 1px;
197
197
  }
198
198
  .vbadge.live { color: var(--chip-success-ink); background: var(--chip-success-fill); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ }
199
- .vbadge.arch { /* muted-ink: state — archived version */ color: var(--muted); background: var(--surface-3); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ }
199
+ .vbadge.arch { /* muted-ink: state — archived version */ color: var(--muted); background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ }
200
200
  /* The switcher keeps its glyph and drops its label where the shell folds — the
201
201
  same step the rail folds at: docs/specification.md#breakpoints. */
202
202
  @media (max-width: 720px) { .vsw__btn .lbl { display: none; } }
@@ -258,7 +258,7 @@
258
258
  .amenu .ahead .anm { color: var(--strong); font-size: var(--text-sm); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
259
259
  .amenu .ahead .aem { color: var(--text); font-size: calc(var(--text-xs) + 0.5px); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
260
260
  .amenu a { display: flex; align-items: center; gap: 11px; padding: 10px 15px; color: var(--text); text-decoration: none; font-family: var(--font-sans); font-size: var(--text-sm); cursor: pointer; }
261
- .amenu a:hover { background: var(--surface-3); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
261
+ .amenu a:hover { outline: 1px solid var(--control-edge); outline-offset: -1px; background: var(--surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
262
262
  .amenu a.cur { color: var(--accent); }
263
263
  .amenu a svg { width: 19px; height: 19px; flex: none; stroke: currentColor; fill: none; stroke-width: 2; opacity: 0.85; }
264
264
  .amenu .asep { height: 1px; background: var(--border); margin: 5px 0; }
@@ -139,10 +139,10 @@
139
139
  line and nothing else in the kit does. why: docs/specification.md#elevation */
140
140
  --bg: #0e0d14;
141
141
  --table-bg: var(--bg);
142
- --surface-2: #161520; /* SUNKEN — fields, tracks, disabled, code */
142
+ --surface-2: #161520; /* non-text sunken marks and tracks */
143
143
  --surface: #211e2d; /* the card */
144
144
  --bg-elevated: #2a2639; /* FLOATING — menus, panels, drawer, modal, toasts */
145
- --surface-3: #2d293c; /* the top step — the hover readout, chips, the nav rail */
145
+ --surface-3: #2d293c; /* non-text quiet fills */
146
146
  --seg-active-bg: #383350; /* raised pill — clearly above the inset track in dark */
147
147
  /* The raised handle on a control track (switch knob). White in both themes and under
148
148
  every accent: it reads on the plain track AND on the accent fill of an on control. */
@@ -150,6 +150,16 @@
150
150
  --border: #332f45;
151
151
  --border-strong: #453f5c;
152
152
 
153
+ /* Fields use the card surface and an edge in both themes. why: docs/specification.md#elevation */
154
+ --field-bg: var(--surface);
155
+ --field-edge: var(--border);
156
+ --field-edge-hover: var(--border-strong);
157
+ --control-edge: var(--border);
158
+
159
+ /* How much of its own colour a tinted card mixes in. Dark mixes upward off a mid-grey
160
+ card, so it can afford this much. why: docs/specification.md#elevation */
161
+ --card-tint: 9%;
162
+
153
163
  --text: #e9e7f0;
154
164
  --strong: #ffffff;
155
165
  --dim: #c6c2d6;
@@ -164,7 +174,7 @@
164
174
  ink read on the surface beside it. Held by guidelines/accessibility-floor.test.js.
165
175
  why: docs/specification.md#colour-and-contrast */
166
176
  --disabled-ink: var(--muted);
167
- --disabled-surface: var(--surface-2);
177
+ --disabled-surface: var(--surface);
168
178
  --disabled-border: var(--border);
169
179
  /* The ink of a disabled control that paints no box of its own — a ghost button. With
170
180
  no surface beside it, it is read on whatever is behind it, so it is set to clear the
@@ -281,29 +291,47 @@
281
291
  --pink: #a92d59;
282
292
  --amber: #b5730a;
283
293
 
284
- /* The same ladder, run the only way light can run it: nothing is brighter than the
285
- white a card already was, so light moves DOWN and keeps white for the top step.
294
+ /* The same ladder, run the only way light can run it: nothing is brighter than
295
+ white, so light moves DOWN from it. #307 ran it downwards from the CARD, which
296
+ put the page at #eef0f5 and left the card at #f8f9fc — a grey canvas under a
297
+ surface only 1.08:1 above it, so the grey was what the screen read as and the
298
+ card never arrived. The card takes the white instead, and the page is the
299
+ lightest ground that still leaves it a step: 1.11:1, on a page lighter and less
300
+ blue than the one it replaces. Light spends its white on the surface you read
301
+ ON, and separates the panel that floats over it by role — its two-step edge and
302
+ its drop — rather than by a rung it has nothing left to pay for. See issue #448.
286
303
  why: docs/specification.md#elevation */
287
- --bg: #eef0f5;
304
+ --bg: #f2f3f6;
288
305
  --table-bg: #ffffff;
289
- --surface-2: #e3e6ee; /* SUNKEN — fields, tracks, disabled, code */
290
- --surface: #f8f9fc; /* the card */
306
+ --surface-2: #e9ecf3; /* non-text sunken marks and tracks */
307
+ --surface: #ffffff; /* the card */
291
308
  --bg-elevated: #ffffff; /* FLOATING — menus, panels, drawer, modal, toasts */
292
- --surface-3: #e7eaf1; /* the hover readout, chips, the nav rail */
309
+ --surface-3: #eef0f5; /* non-text quiet fills */
293
310
  --seg-active-bg: #ffffff; /* white pill raised above the grey track in light */
294
311
  --control-knob: #ffffff;
295
312
  --border: #e4e7ee;
296
313
  --border-strong: #cdd2dc;
297
314
 
315
+ /* Light draws a field with its EDGE, because since #448 it has no well left to draw it
316
+ with: a card and a panel are both white, the sunken step falls two rungs under either,
317
+ and --border on it measures 1.047:1. why: docs/specification.md#elevation */
318
+ --field-bg: var(--surface);
319
+ --field-edge: var(--border-strong);
320
+ --field-edge-hover: var(--muted);
321
+ --control-edge: var(--border-strong);
322
+
323
+ /* Light mixes DOWN from white, so tint costs lightness: 5% is the most a card can spend
324
+ and stay above the page. why: docs/specification.md#elevation */
325
+ --card-tint: 5%;
326
+
298
327
  --text: #1a1e27;
299
328
  --strong: #0c0e13;
300
329
  --dim: #3a404b;
301
330
  --muted: #5c6270;
302
331
 
303
- /* Same three aliases as dark. Light moves the disabled surface AWAY from white for the
304
- same reason dark moves it away from the card: a control that is off should sink. */
332
+ /* Disabled text stays on the card surface; its ink marks the unavailable state. */
305
333
  --disabled-ink: var(--muted);
306
- --disabled-surface: var(--surface-2);
334
+ --disabled-surface: var(--surface);
307
335
  --disabled-border: var(--border);
308
336
  --disabled-ink-bare: #585e6c;
309
337
 
@@ -361,8 +389,8 @@
361
389
  --shadow-ink: #1e1e32;
362
390
  --sheen: #ffffff;
363
391
 
364
- /* 18% / 10% against dark's 62% / 50%. Light's ink is #1e1e32 on a card at
365
- #f8f9fc, and 18% is what puts the drop core at #d1d2d8 — 1.44, the strongest
392
+ /* 18% / 10% against dark's 62% / 50%. Light's ink is #1e1e32 on a card that is
393
+ now #ffffff, and 18% is what puts the drop core at #d7d7da — 1.44, the strongest
366
394
  separation either theme gets. why: docs/specification.md#elevation */
367
395
  --elev-drop:
368
396
  0 14px 30px -12px color-mix(in srgb, var(--shadow-ink) 18%, transparent),