@apliteni/apliteni-ui 0.36.0 → 0.37.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.
package/README.md CHANGED
@@ -151,6 +151,13 @@ Each accent re-points only the accent family (`--accent`, `--purple*`, `--glow-p
151
151
  stay put — so **every accent works in both themes** and every component follows with no
152
152
  component-level change.
153
153
 
154
+ Focus uses a surface-coloured gap, a solid band and a decorative glow. Tune
155
+ `--ring-width`, `--ring-color`, `--ring-gap-width` and `--ring-gap` at the root;
156
+ `--ring` remains the composed shadow. Kit surfaces recompose it to match their
157
+ background, so an ancestor's custom `--ring` must also be applied on those surfaces.
158
+ The page shell inherits root overrides; it does not introduce another composition.
159
+ See [the focus-ring contract](docs/specification.md#the-focus-ring).
160
+
154
161
  Both attributes are overrides, not requirements: with neither present the kit paints dark
155
162
  Nebula, and `data-accent` alone paints that accent on the dark theme. An absent `data-theme`
156
163
  is *not* "follow the system" — the kit ships no `prefers-color-scheme` rule, so a host that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.36.0",
3
+ "version": "0.37.0",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
@@ -44,6 +44,7 @@
44
44
  .rx-modal {
45
45
  width: min(480px, 92vw);
46
46
  background: var(--bg-elevated);
47
+ --ring-gap: var(--bg-elevated);
47
48
  border: 1px solid var(--border-strong);
48
49
  box-shadow: inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop);
49
50
  border-radius: var(--radius-xl);
@@ -92,8 +93,8 @@
92
93
  color: inherit;
93
94
  }
94
95
  .rx-sort:focus-visible {
95
- outline: 2px solid var(--accent);
96
- outline-offset: 2px;
96
+ outline: 2px solid transparent;
97
+ box-shadow: var(--ring);
97
98
  border-radius: 4px;
98
99
  }
99
100
  .rx-caret {
@@ -31,8 +31,8 @@
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); }
35
- .ui-back:focus-visible { outline: none; box-shadow: var(--ring); }
34
+ .ui-back[href]:hover { color: var(--strong); background: var(--surface-2); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
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
38
38
  box, which is 1.13 CSS px at this size and under the kit's 1.5 for a stroked
@@ -15,29 +15,29 @@
15
15
  padding: 3px 9px;
16
16
  border-radius: var(--radius-pill);
17
17
  color: var(--text);
18
- background: var(--surface-2);
18
+ background: var(--surface-2); /* 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
- .ui-badge--live { color: var(--chip-success-ink); background: var(--chip-success-fill); }
25
- .ui-badge--soon { color: var(--purple-mid); background: var(--glow-purple); }
26
- .ui-badge--info { color: var(--chip-info-ink); background: var(--chip-info-fill); }
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. */ }
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
- .ui-badge--danger { color: var(--chip-danger-ink); background: var(--chip-danger-fill); }
29
- .ui-badge--archive { /* muted-ink: state — archived record */ color: var(--muted); background: var(--surface); }
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. */ }
29
+ .ui-badge--archive { /* muted-ink: state — archived record */ color: var(--muted); background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ }
30
30
 
31
31
  /* Semantic status variants — for record/row status in data tables
32
32
  (paid / verified / pending / failed …). Same chip, mapped to meaning. */
33
- .ui-badge--success { color: var(--chip-success-ink); background: var(--chip-success-fill); }
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); }
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
36
 
37
37
  /* The two neutral chips are the exception: their light fill is already in the
38
38
  surface scale (--surface-3), and only archive uses muted state ink. */
39
- :root[data-theme="light"] .ui-badge--neutral { background: var(--surface-3); }
40
- :root[data-theme="light"] .ui-badge--archive { /* muted-ink: state — archived record */ color: var(--muted); background: var(--surface-3); }
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. */ }
41
41
 
42
42
  /* Pill: same shape, sentence case, a touch larger — a metadata chip */
43
43
  .ui-pill {
@@ -47,7 +47,7 @@
47
47
  /* rank: chip */
48
48
  font-size: var(--text-xs);
49
49
  color: var(--text);
50
- background: var(--surface-2);
50
+ background: var(--surface-2); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */
51
51
  border-radius: var(--radius-pill);
52
52
  padding: 4px 11px;
53
53
  font-weight: var(--weight-semibold);
@@ -55,8 +55,8 @@
55
55
  /* The live pill is the same status paint as the live badge, so it takes the
56
56
  same pair. On its own it was --green on --glow-green, which is 3.97:1 over
57
57
  white — under AA, while the badge beside it cleared at 4.64:1. See issue #131. */
58
- .ui-pill--live { color: var(--chip-success-ink); background: var(--chip-success-fill); }
59
- .ui-pill--soon { color: var(--purple-mid); background: var(--glow-purple); }
58
+ .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. */ }
60
60
 
61
61
  /* Status dot */
62
62
  .ui-dot {
@@ -15,7 +15,7 @@ html {
15
15
  }
16
16
 
17
17
  body {
18
- background: var(--bg);
18
+ background: var(--bg); /* ring-gap: inherit — page background inherits root overrides. */
19
19
  color: var(--text);
20
20
  font-family: var(--font-sans);
21
21
  font-weight: var(--weight-normal);
@@ -27,7 +27,7 @@ body {
27
27
  }
28
28
 
29
29
  ::selection {
30
- background: color-mix(in srgb, var(--accent) 30%, transparent);
30
+ background: color-mix(in srgb, var(--accent) 30%, transparent); /* ring-gap: inherit — selection paint is not a focus container. */
31
31
  color: var(--strong);
32
32
  }
33
33
 
@@ -87,7 +87,7 @@ strong {
87
87
 
88
88
  /* Spotlight — a soft radial from the top-centre (auth cards, focused heroes). */
89
89
  .ui-bg-spotlight::before {
90
- background: radial-gradient(620px 340px at 50% -12%, var(--glow-purple), transparent 70%);
90
+ background: radial-gradient(620px 340px at 50% -12%, var(--glow-purple), transparent 70%); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */
91
91
  }
92
92
  /* Grid — a faint token-coloured grid, masked to fade at the edges. */
93
93
  .ui-bg-grid::before {
@@ -107,7 +107,7 @@ strong {
107
107
  }
108
108
  /* Accent wash — a gentle top-down tint in the current accent. */
109
109
  .ui-bg-wash::before {
110
- background: linear-gradient(180deg, var(--glow-purple), transparent 45%);
110
+ background: linear-gradient(180deg, var(--glow-purple), transparent 45%); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */
111
111
  }
112
112
 
113
113
  /* Layout helpers used by the example apps */
@@ -142,6 +142,6 @@ svg:where(:not([width]):not([height])) {
142
142
  .ui-input:focus-visible,
143
143
  .ui-seg button:focus-visible,
144
144
  .ui-switch:focus-visible {
145
- outline: none;
145
+ outline: 2px solid transparent;
146
146
  box-shadow: var(--ring);
147
147
  }
@@ -20,7 +20,7 @@
20
20
  padding: var(--btn-pad-y) var(--btn-pad-x);
21
21
  cursor: pointer;
22
22
  border: 1px solid var(--border);
23
- background: var(--surface-2);
23
+ background: var(--surface-2); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
24
24
  color: var(--text);
25
25
  white-space: nowrap;
26
26
  transition: background var(--dur-fast) var(--ease),
@@ -32,7 +32,7 @@
32
32
 
33
33
  .ui-btn:hover {
34
34
  border-color: var(--accent);
35
- background: color-mix(in srgb, var(--accent) 14%, var(--surface-2));
35
+ background: color-mix(in srgb, var(--accent) 14%, var(--surface-2)); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
36
36
  color: var(--strong);
37
37
  }
38
38
  .ui-btn:active { transform: translateY(1px); }
@@ -59,7 +59,7 @@
59
59
  color: var(--text);
60
60
  }
61
61
  .ui-btn--ghost:hover {
62
- background: var(--surface-2);
62
+ background: var(--surface-2); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
63
63
  border-color: transparent;
64
64
  color: var(--strong);
65
65
  }
@@ -68,7 +68,7 @@
68
68
  .ui-btn--danger:hover {
69
69
  color: var(--pink);
70
70
  border-color: var(--pink);
71
- background: color-mix(in srgb, var(--pink) 10%, transparent);
71
+ background: color-mix(in srgb, var(--pink) 10%, transparent); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
72
72
  }
73
73
 
74
74
  /* -- Sizes -------------------------------------------------------------- */
@@ -93,7 +93,7 @@
93
93
  makes this read as inert; the quiet ink is the rest. #220 */
94
94
  .ui-btn:disabled,
95
95
  .ui-btn[aria-disabled="true"] {
96
- background: var(--disabled-surface);
96
+ background: var(--disabled-surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
97
97
  border-color: var(--disabled-border);
98
98
  /* muted-ink: state — unavailable control */ color: var(--disabled-ink);
99
99
  cursor: not-allowed;
@@ -137,7 +137,7 @@
137
137
  left: 10px; right: 10px; bottom: 5px;
138
138
  height: 3px;
139
139
  border-radius: 999px;
140
- background: color-mix(in srgb, var(--accent) 22%, transparent);
140
+ background: color-mix(in srgb, var(--accent) 22%, transparent); /* ring-gap: inherit — decorative paint has no nested focus target. */
141
141
  overflow: hidden;
142
142
  /* Clean entrance: grow + fade in on state change, once. */
143
143
  transform-origin: center;
@@ -156,7 +156,7 @@
156
156
  /* On the filled primary the surface IS the accent, so shimmer in the contrast
157
157
  colour instead — otherwise the bars vanish into the button. */
158
158
  .ui-btn--primary .ui-btn__bars {
159
- background: color-mix(in srgb, var(--accent-contrast) 26%, transparent);
159
+ background: color-mix(in srgb, var(--accent-contrast) 26%, transparent); /* ring-gap: inherit — decorative paint has no nested focus target. */
160
160
  }
161
161
  .ui-btn--primary .ui-btn__bars i {
162
162
  background: linear-gradient(90deg,
@@ -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);
10
+ background: var(--surface-2); --ring-gap: var(--surface-2);
11
11
  font-size: calc(var(--text-sm) + 0.5px);
12
12
  line-height: 1.55;
13
13
  color: var(--text);
@@ -17,13 +17,13 @@
17
17
  .ui-callout b { color: var(--text); font-weight: var(--weight-semibold); }
18
18
  .ui-callout code { font-family: var(--font-mono); font-size: 0.92em; color: var(--text); }
19
19
 
20
- .ui-callout--info { background: var(--glow-cyan); }
20
+ .ui-callout--info { --ring-gap: inherit; background: var(--glow-cyan); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */ }
21
21
  .ui-callout--info .ui-callout__icon { color: var(--chip-info-ink); } /* the chip inks, not the raw signals: in light the raw signal cleared the graphic bar by a tenth on its own wash and read as a smudge, and a stroke is read the way text is. The two are the same value in dark by construction, so only light moves. #206 */
22
- .ui-callout--success { background: var(--glow-green); }
22
+ .ui-callout--success { --ring-gap: inherit; background: var(--glow-green); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */ }
23
23
  .ui-callout--success .ui-callout__icon { color: var(--chip-success-ink); }
24
- .ui-callout--warn { background: color-mix(in srgb, var(--amber) 13%, transparent); }
24
+ .ui-callout--warn { --ring-gap: inherit; background: color-mix(in srgb, var(--amber) 13%, transparent); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */ }
25
25
  .ui-callout--warn .ui-callout__icon { color: var(--chip-warn-ink); }
26
- .ui-callout--danger { background: var(--glow-pink); }
26
+ .ui-callout--danger { --ring-gap: inherit; background: var(--glow-pink); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */ }
27
27
  .ui-callout--danger .ui-callout__icon { color: var(--chip-danger-ink); }
28
28
 
29
29
  /* ----------------------------------------------------------------------------
@@ -85,7 +85,7 @@
85
85
  the ink is dark and darkens it where the ink is bright — the hovered pair is
86
86
  at or above the resting pair in both. */
87
87
  .ui-toast__action { flex: none; align-self: center; background: none; border: 0; padding: 4px 9px; border-radius: var(--radius-xs); font: inherit; font-weight: var(--weight-medium); color: var(--toast-action-ink); cursor: pointer; }
88
- .ui-toast__action:hover { background: color-mix(in srgb, var(--bg) 55%, transparent); }
88
+ .ui-toast__action:hover { background: color-mix(in srgb, var(--bg) 55%, transparent); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
89
89
 
90
90
  .ui-toast__close { flex: none; align-self: flex-start; background: none; border: 0; /* muted-ink: glyph — dismiss icon */ color: var(--muted); cursor: pointer; padding: 2px; display: grid; place-items: center; position: relative; font: inherit; }
91
91
  .ui-toast__close svg { width: 15px; height: 15px; stroke: currentColor; fill: none; stroke-width: 2.5; }
@@ -117,7 +117,7 @@
117
117
  /* style: soft — tinted surface. The shadow used to be its edge, so it takes the
118
118
  status-tinted hairline the outline style carries, at half the weight. #295 */
119
119
  .ui-toast--soft {
120
- background: var(--toast-glow);
120
+ background: var(--toast-glow); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */
121
121
  border: 1px solid color-mix(in srgb, var(--toast-accent) 22%, transparent);
122
122
  --elev-edge: color-mix(in srgb, var(--toast-accent) 11%, transparent);
123
123
  }
@@ -130,8 +130,8 @@
130
130
  clear each other — and four of the ten status x theme combinations did not.
131
131
  Gated for all ten in stories/signal-contrast.test.js. See #149. */
132
132
  .ui-toast--solid {
133
- --toast-ink: var(--signal-solid-ink);
134
- background: var(--toast-solid);
133
+ --toast-ink: var(--signal-solid-ink); --ring-color: var(--toast-ink);
134
+ background: var(--toast-solid); --ring-gap: var(--toast-solid);
135
135
  color: var(--toast-ink);
136
136
  /* No inner line: a solid toast has no outer one either. It keeps the drop. */
137
137
  --elev-edge: transparent;
@@ -143,18 +143,18 @@
143
143
  there, and 2.25 on the dark danger fill — the pair was correct and the
144
144
  dilution then spent it. */
145
145
  .ui-toast--solid .ui-toast__text { color: var(--toast-ink); }
146
- .ui-toast--solid .ui-toast__icon { background: color-mix(in srgb, var(--toast-ink) 22%, transparent); color: var(--toast-ink); }
146
+ .ui-toast--solid .ui-toast__icon { background: color-mix(in srgb, var(--toast-ink) 22%, transparent); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ color: var(--toast-ink); }
147
147
  .ui-toast--solid .ui-toast__action { color: var(--toast-ink); }
148
148
  /* Hover darkens the FILL rather than lightening the ink. Washing the ink over
149
149
  itself was the one surface #149 did not reach, and it worked against the pair
150
150
  it was drawn from: it pulled the fill towards the ink it has to clear. */
151
- .ui-toast--solid .ui-toast__action:hover { background: color-mix(in srgb, var(--signal-contrast) 14%, transparent); }
151
+ .ui-toast--solid .ui-toast__action:hover { background: color-mix(in srgb, var(--signal-contrast) 14%, transparent); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
152
152
  .ui-toast--solid .ui-toast__close { /* muted-ink: glyph — dismiss icon on solid toast */ color: color-mix(in srgb, var(--toast-ink) 68%, transparent); }
153
153
  .ui-toast--solid .ui-toast__close:hover { color: var(--toast-ink); }
154
- .ui-toast--solid .ui-toast__timer { background: color-mix(in srgb, var(--toast-ink) 55%, transparent); }
154
+ .ui-toast--solid .ui-toast__timer { background: color-mix(in srgb, var(--toast-ink) 55%, transparent); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ }
155
155
 
156
156
  /* style: outline — elevated card with a status-tinted border */
157
- .ui-toast--outline { background: var(--bg-elevated); border: 1px solid color-mix(in srgb, var(--toast-accent) 40%, transparent); --elev-edge: color-mix(in srgb, var(--toast-accent) 20%, transparent); }
157
+ .ui-toast--outline { background: var(--bg-elevated); --ring-gap: var(--bg-elevated); border: 1px solid color-mix(in srgb, var(--toast-accent) 40%, transparent); --elev-edge: color-mix(in srgb, var(--toast-accent) 20%, transparent); }
158
158
 
159
159
  /* compact — single line, no body text */
160
160
  .ui-toast--compact { align-items: center; padding-top: 10px; padding-bottom: 10px; }
@@ -178,7 +178,7 @@
178
178
  border-radius: var(--radius-lg);
179
179
  padding: 30px;
180
180
  text-align: center;
181
- background: var(--glow-green);
181
+ background: var(--glow-green); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */
182
182
  }
183
183
  .ui-success__check {
184
184
  width: 48px; height: 48px;
@@ -6,7 +6,7 @@
6
6
  /* Flat since #284 and edged in both themes since #295: the surface is the step, the
7
7
  line is what makes it legible. why: docs/specification.md#elevation */
8
8
  .ui-card {
9
- background: var(--surface);
9
+ background: var(--surface); --ring-gap: var(--surface);
10
10
  border: 1px solid var(--border);
11
11
  border-radius: var(--radius-xl);
12
12
  padding: var(--space-6) 26px;
@@ -28,11 +28,11 @@
28
28
  /* An accent-tinted card, e.g. a highlighted plan / success surface. The accent edge is
29
29
  the card's own hairline re-coloured: an inset ring over it would draw two. #295 */
30
30
  .ui-card--accent {
31
- background: color-mix(in srgb, var(--accent) 9%, var(--surface));
31
+ background: color-mix(in srgb, var(--accent) 9%, var(--surface)); --ring-gap: color-mix(in srgb, var(--accent) 9%, var(--surface));
32
32
  border-color: color-mix(in srgb, var(--accent) 22%, transparent);
33
33
  }
34
34
  .ui-card--live {
35
- background: color-mix(in srgb, var(--green) 10%, var(--surface));
35
+ background: color-mix(in srgb, var(--green) 10%, var(--surface)); --ring-gap: color-mix(in srgb, var(--green) 10%, var(--surface));
36
36
  }
37
37
 
38
38
  /* Interactive card (link / button semantics) */
@@ -89,7 +89,7 @@
89
89
  border-radius: var(--radius-md);
90
90
  display: grid;
91
91
  place-items: center;
92
- background: var(--glow-purple);
92
+ background: var(--glow-purple); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */
93
93
  color: var(--accent);
94
94
  flex: none;
95
95
  }
@@ -4,7 +4,7 @@
4
4
  * ========================================================================== */
5
5
 
6
6
  .ui-snippet {
7
- background: var(--surface-2);
7
+ background: var(--surface-2); --ring-gap: var(--surface-2);
8
8
  border-radius: var(--radius-md);
9
9
  overflow: hidden;
10
10
  }
@@ -61,7 +61,7 @@
61
61
 
62
62
  /* Reveal: a live secret/token, tinted green */
63
63
  .ui-snippet--reveal {
64
- background: color-mix(in srgb, var(--green) 10%, var(--surface));
64
+ background: color-mix(in srgb, var(--green) 10%, var(--surface)); --ring-gap: color-mix(in srgb, var(--green) 10%, var(--surface));
65
65
  }
66
66
  .ui-snippet--reveal .ui-snippet__bar span { color: var(--green); }
67
67
  .ui-snippet--reveal pre { color: var(--green); word-break: break-all; white-space: pre-wrap; }
@@ -70,7 +70,7 @@
70
70
  .ui-code {
71
71
  font-family: var(--font-mono);
72
72
  font-size: 0.9em;
73
- background: var(--surface-2);
73
+ background: var(--surface-2); --ring-gap: var(--surface-2);
74
74
  padding: 2px 6px;
75
75
  border-radius: var(--radius-xs);
76
76
  color: var(--text);
@@ -68,7 +68,7 @@
68
68
  display: flex;
69
69
  flex-direction: column;
70
70
  font-family: var(--font-sans);
71
- background: var(--cmdk-surface);
71
+ background: var(--cmdk-surface); --ring-gap: var(--cmdk-surface);
72
72
  /* The two-step edge and the drop: --border-strong outside, --border one pixel
73
73
  inside it, then the broad faint drop. why: docs/specification.md#elevation */
74
74
  border: 1px solid var(--border-strong);
@@ -86,7 +86,7 @@
86
86
  box-shadow list replaces the whole list, so `box-shadow: var(--ring)` alone
87
87
  took the panel's edge and its drop off for as long as it held focus.
88
88
  why: docs/specification.md#elevation */
89
- .ui-cmdk__panel:focus-visible { outline: none; box-shadow: var(--ring), inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop); }
89
+ .ui-cmdk__panel:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring), inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop); }
90
90
 
91
91
  /* -- The text box --------------------------------------------------------- */
92
92
 
@@ -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); }
181
+ .ui-cmdk__item.is-active { background: var(--surface-3); /* 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 {
@@ -258,7 +258,7 @@
258
258
  border-radius: var(--radius-xs);
259
259
  /* A key cap reads as raised, so it takes the step above the panel rather than
260
260
  the card step below it. why: docs/specification.md#elevation */
261
- background: var(--surface-3);
261
+ background: var(--surface-3); /* 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;
@@ -301,10 +301,10 @@
301
301
  align-items: center;
302
302
  justify-content: center;
303
303
  border-radius: var(--radius-sm);
304
- background: var(--surface);
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); }
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. */ }
308
308
 
309
309
  /* -- Narrow --------------------------------------------------------------- */
310
310
  /* At the kit's narrowest step the palette is the page: full width, off the top
@@ -61,7 +61,7 @@
61
61
  flex-direction: column;
62
62
  gap: var(--space-2);
63
63
  padding: var(--space-5);
64
- background: var(--confirm-surface);
64
+ background: var(--confirm-surface); --ring-gap: var(--confirm-surface);
65
65
  /* The two-step edge and the drop: --border-strong outside, --border one pixel
66
66
  inside it, then the broad faint drop. why: docs/specification.md#elevation */
67
67
  border: 1px solid var(--border-strong);
@@ -78,7 +78,7 @@
78
78
  box-shadow list replaces the whole list, so `box-shadow: var(--ring)` alone
79
79
  took the panel's edge and its drop off for as long as it held focus.
80
80
  why: docs/specification.md#elevation */
81
- .ui-confirm__panel:focus-visible { outline: none; box-shadow: var(--ring), inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop); }
81
+ .ui-confirm__panel:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring), inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop); }
82
82
 
83
83
  /* -- Slots ----------------------------------------------------------------- */
84
84
  /* The question and the answers keep their size; the consequence between them is
@@ -57,7 +57,7 @@
57
57
  position: absolute;
58
58
  display: flex;
59
59
  flex-direction: column;
60
- background: var(--drawer-surface);
60
+ background: var(--drawer-surface); --ring-gap: var(--drawer-surface);
61
61
  /* A drawer is flush to a screen edge, so it has one edge rather than four and the
62
62
  two-step edge runs along that one: --border-strong on the edge rule below, the
63
63
  inner line one pixel inside it. This is the one floating surface that writes no
@@ -115,7 +115,7 @@
115
115
  box-shadow list replaces the whole list, so a bare `box-shadow: var(--ring)` would
116
116
  take the drawer's edge and its drop off for as long as it held focus.
117
117
  why: docs/specification.md#elevation */
118
- .ui-drawer__panel:focus-visible { outline: none; box-shadow: var(--ring), var(--drawer-line), var(--elev-drop); }
118
+ .ui-drawer__panel:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring), var(--drawer-line), var(--elev-drop); }
119
119
 
120
120
  /* -- Sizes ----------------------------------------------------------------- */
121
121
  /* left|right → width; top|bottom → height. Same names, axis chosen by side. */
@@ -169,8 +169,8 @@
169
169
  background var(--dur-fast) var(--ease),
170
170
  border-color var(--dur-fast) var(--ease);
171
171
  }
172
- .ui-drawer__close:hover { color: var(--strong); background: var(--surface-3); }
173
- .ui-drawer__close:focus-visible { outline: none; box-shadow: var(--ring); border-color: var(--accent); }
172
+ .ui-drawer__close:hover { color: var(--strong); background: var(--surface-3); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
173
+ .ui-drawer__close:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring); border-color: var(--accent); }
174
174
  .ui-drawer__close svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 2.1; }
175
175
 
176
176
  .ui-drawer__body {
@@ -191,7 +191,7 @@
191
191
  gap: 10px;
192
192
  padding: 16px 20px;
193
193
  border-top: 1px solid var(--border);
194
- background: var(--surface-2);
194
+ background: var(--surface-2); --ring-gap: var(--surface-2);
195
195
  flex: none;
196
196
  }
197
197
 
@@ -17,14 +17,14 @@
17
17
  font-size: calc(var(--text-sm) - 0.5px);
18
18
  font-weight: 500;
19
19
  color: var(--text);
20
- background: var(--surface);
20
+ background: var(--surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
21
21
  border: 1px solid var(--border);
22
22
  border-radius: var(--radius-pill);
23
23
  padding: 7px 11px 7px 13px;
24
24
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
25
25
  }
26
26
  .ui-dropdown__trigger:hover { border-color: var(--accent); }
27
- .ui-dropdown__trigger:focus-visible { outline: none; box-shadow: var(--ring); border-color: var(--accent); }
27
+ .ui-dropdown__trigger:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring); border-color: var(--accent); }
28
28
  .ui-dropdown__pre { color: var(--text); }
29
29
  .ui-dropdown__value { color: var(--strong); font-weight: 600; letter-spacing: 0.01em; }
30
30
 
@@ -70,7 +70,7 @@
70
70
  min-width: 240px;
71
71
  /* The floating step, not the sunken one — a panel held open over a card cannot be
72
72
  painted the colour of an inset. why: docs/specification.md#elevation */
73
- background: var(--bg-elevated);
73
+ background: var(--bg-elevated); --ring-gap: var(--bg-elevated);
74
74
  /* The two-step edge and the drop: --border-strong outside, --border one pixel
75
75
  inside it, then the broad faint drop. why: docs/specification.md#elevation */
76
76
  border: 1px solid var(--border-strong);
@@ -152,8 +152,8 @@
152
152
  transition: background var(--dur-fast) var(--ease);
153
153
  }
154
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); }
156
- .ui-dropdown__item:focus-visible { outline: none; background: var(--surface-3); box-shadow: var(--ring); }
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); }
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);
183
+ background: var(--surface-3); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */
184
184
  }
185
- .ui-dropdown__badge.is-live { color: var(--chip-success-ink); background: var(--chip-success-fill); }
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
187
  src/styles/nav.css:163 `.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); }
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. */ }
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; }
@@ -258,7 +258,7 @@
258
258
  font-size: calc(var(--text-sm) - 0.5px);
259
259
  color: var(--strong);
260
260
  /* A field is the sunken step, the same one .ui-input takes. why: docs/specification.md#elevation */
261
- background: var(--surface-2);
261
+ background: var(--surface-2); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
262
262
  border: 1px solid var(--border);
263
263
  border-radius: var(--radius-sm);
264
264
  padding: 8px 12px 8px 34px;
@@ -271,11 +271,11 @@
271
271
  kit paints, so --surface-2 falls two rungs under it rather than one. The
272
272
  numbers are in the spec and the gate is stories/dropdown-field-ground.test.js.
273
273
  why: docs/specification.md#elevation */
274
- :root[data-theme="light"] .ui-dropdown__search-input { background: var(--bg); }
274
+ :root[data-theme="light"] .ui-dropdown__search-input { background: var(--bg); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
275
275
 
276
276
  .ui-dropdown__search-input::placeholder { /* muted-ink: placeholder — empty search field */ color: var(--muted); }
277
277
  .ui-dropdown__search-input:hover { border-color: var(--border-strong); }
278
- .ui-dropdown__search-input:focus-visible { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
278
+ .ui-dropdown__search-input:focus-visible { outline: 2px solid transparent; border-color: var(--accent); box-shadow: var(--ring); }
279
279
 
280
280
  .ui-dropdown__list { max-height: 300px; overflow-y: auto; }
281
281
  /* A row is `display: flex`, which outranks the UA's [hidden], so a row the
@@ -284,7 +284,7 @@
284
284
 
285
285
  /* The row Enter would pick. Focus stays in the field, which carries the ring,
286
286
  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); box-shadow: inset 2px 0 0 var(--accent); }
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); }
288
288
 
289
289
  /* No match — a nudge and no action, per Guidelines / Microcopy. Empty while
290
290
  anything matches, so it takes no room. */
@@ -66,7 +66,7 @@
66
66
  /* The composer panel (bottom-centre) */
67
67
  .ui-fbcomposer {
68
68
  position: fixed; z-index: 71; left: 50%; bottom: 26px; transform: translateX(-50%) translateY(14px);
69
- width: min(520px, calc(100vw - 32px)); background: var(--bg-elevated); border-radius: 18px;
69
+ width: min(520px, calc(100vw - 32px)); background: var(--bg-elevated); --ring-gap: var(--bg-elevated); border-radius: 18px;
70
70
  /* The shadow was its only edge until #295; now it takes the hairline. */
71
71
  border: 1px solid var(--border); opacity: 0; pointer-events: none;
72
72
  transition: opacity var(--dur-med) var(--ease), transform var(--dur-med) var(--ease); overflow: hidden;
@@ -76,29 +76,29 @@
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); /* 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-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; }
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); border-radius: 12px; padding: 12px 15px; display: flex; gap: 11px; }
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; }
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); border: 0; border-radius: 12px; padding: 13px 15px; transition: box-shadow var(--dur-fast) var(--ease);
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);
89
89
  }
90
90
  .ui-fbcomposer textarea::placeholder { /* muted-ink: placeholder — empty feedback field */ color: var(--muted); }
91
- .ui-fbcomposer textarea:focus { outline: none; box-shadow: 0 0 0 2px var(--accent); }
91
+ .ui-fbcomposer textarea:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring); }
92
92
  .ui-fbc__foot { display: flex; align-items: center; justify-content: flex-end; gap: 12px; padding: 16px 20px 18px; }
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); }
96
+ .ui-fbbtn.ghost:hover { color: var(--strong); background: var(--surface-2); /* 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). */
100
100
  .ui-fbbtn.primary:disabled {
101
- background: var(--disabled-surface);
101
+ background: var(--disabled-surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
102
102
  /* muted-ink: state — unavailable control */ color: var(--disabled-ink);
103
103
  cursor: default;
104
104
  filter: none;
@@ -108,7 +108,7 @@
108
108
  @keyframes uiFbSpin { to { transform: rotate(360deg); } }
109
109
 
110
110
  /* Success state */
111
- .ui-fbc__done { padding: 30px 26px 24px; text-align: center; background: radial-gradient(112% 60% at 50% 0%, var(--glow-green), transparent 62%); }
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
114
  .ui-fbck-t { fill: none; stroke: var(--surface-2); stroke-width: 9; stroke-linecap: round; stroke-linejoin: round; }
@@ -123,7 +123,7 @@
123
123
  @keyframes uiFbCkSpring { 0% { transform: scale(1.32); } 62% { transform: scale(.965); } 82% { transform: scale(1.02); } 100% { transform: scale(1); } }
124
124
 
125
125
  /* Error banner */
126
- .ui-fbc__err { margin: 14px 20px 0; background: color-mix(in srgb, var(--pink) 14%, transparent); color: var(--pink); font-size: var(--text-sm); padding: 11px 15px; border-radius: 11px; display: none; }
126
+ .ui-fbc__err { margin: 14px 20px 0; background: color-mix(in srgb, var(--pink) 14%, transparent); /* ring-gap: inherit — text-only validation message has no nested focus target. */ color: var(--pink); font-size: var(--text-sm); padding: 11px 15px; border-radius: 11px; display: none; }
127
127
  .ui-fbc__err.show { display: block; }
128
128
  /* A failed send's line fades in; wireFeedback() adds the class as it shows it. */
129
129
  .ui-fbc__err.is-entering { animation: m-fade var(--dur-fast) var(--ease-out) both; }