@attrus-ds/core 1.0.13 → 1.0.14

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.
@@ -39,9 +39,9 @@
39
39
  flex-shrink: 0;
40
40
 
41
41
  /* Box — default size = md (40px tall) */
42
- height: 40px;
42
+ height: var(--control-h-md);
43
43
  padding: 0 var(--space-5);
44
- border: 1px solid transparent;
44
+ border: var(--border-hairline) solid transparent;
45
45
  border-radius: var(--radius-pill);
46
46
 
47
47
  /* Type */
@@ -93,7 +93,7 @@
93
93
  2. SIZES
94
94
  ---------------------------------------------------------------- */
95
95
  .btn-xs {
96
- height: 24px;
96
+ height: var(--control-h-xs);
97
97
  padding: 0 var(--space-3);
98
98
  font-size: var(--fs-sm);
99
99
  gap: var(--space-1);
@@ -101,16 +101,16 @@
101
101
  .btn-xs > i, .btn-xs > svg, .btn-xs > svg { width: var(--icon-xs); height: var(--icon-xs); }
102
102
 
103
103
  .btn-sm {
104
- height: 32px;
104
+ height: var(--control-h-sm);
105
105
  padding: 0 var(--space-4);
106
106
  font-size: var(--fs-md);
107
107
  }
108
108
  .btn-sm > i, .btn-sm > svg, .btn-sm > svg { width: var(--icon-xs); height: var(--icon-xs); }
109
109
 
110
- /* default = md (40px) — defined in .btn */
110
+ /* default = md — .btn reads var(--control-h-md) */
111
111
 
112
112
  .btn-lg {
113
- height: 48px;
113
+ height: var(--control-h-lg);
114
114
  padding: 0 var(--space-6);
115
115
  font-size: var(--fs-base);
116
116
  gap: var(--space-3);
@@ -122,10 +122,10 @@
122
122
  3. ICON-ONLY MODIFIER
123
123
  ---------------------------------------------------------------- */
124
124
  .btn-icon { padding: 0; aspect-ratio: 1 / 1; }
125
- .btn-icon.btn-xs { width: 24px; }
126
- .btn-icon.btn-sm { width: 32px; }
127
- .btn-icon { width: 40px; }
128
- .btn-icon.btn-lg { width: 48px; }
125
+ .btn-icon.btn-xs { width: var(--control-h-xs); }
126
+ .btn-icon.btn-sm { width: var(--control-h-sm); }
127
+ .btn-icon { width: var(--control-h-md); }
128
+ .btn-icon.btn-lg { width: var(--control-h-lg); }
129
129
 
130
130
 
131
131
  /* ----------------------------------------------------------------
@@ -199,6 +199,21 @@
199
199
  .btn-danger:focus-visible { box-shadow: var(--shadow-focus-danger); }
200
200
 
201
201
 
202
+ /* DANGER GHOST — destructive inside chrome (toolbars, row actions).
203
+ The outline variant carries a border and its own surface, which is right for
204
+ the primary action of a dialog but too heavy for a bar: two framed buttons on
205
+ a 52px toolbar outweigh the data below them. This keeps the danger colour as
206
+ the only signal. */
207
+ .btn-danger-ghost {
208
+ background: transparent;
209
+ color: var(--color-danger-500);
210
+ border-color: transparent;
211
+ }
212
+ .btn-danger-ghost:hover { background: var(--color-feedback-danger-background); color: var(--color-danger-600); }
213
+ .btn-danger-ghost:active { background: color-mix(in srgb, var(--color-danger-500) 16%, transparent); }
214
+ .btn-danger-ghost:focus-visible { box-shadow: var(--shadow-focus-danger); }
215
+ .btn-danger-ghost[disabled] { color: var(--color-foreground-disabled); background: transparent; }
216
+
202
217
  /* DANGER OUTLINE — destructive contextual */
203
218
  .btn-danger-outline {
204
219
  background: var(--color-background-surface);
@@ -264,21 +279,37 @@
264
279
  .btn.is-loading::after {
265
280
  content: "";
266
281
  position: absolute;
267
- top: 50%; left: 50%;
282
+ /* Centred with calc, not top:50% + negative margin: a blanket `margin: 0`
283
+ from page or reset CSS silently kills the offset and drops the spinner
284
+ below centre, while the percentage still resolves. calc cannot be reset
285
+ away, and it avoids transform, which the spin animation owns. */
286
+ /* Optical, not geometric: the ring's transparent top segment shifts the
287
+ visible arc's mass down-left of the box centre, so the box is nudged back
288
+ by half a border width. box-sizing keeps the 2px border inside the
289
+ declared size, otherwise the circle is 18px in a 14px slot. */
290
+ top: calc(50% - (var(--icon-sm) / 2) - 1px);
291
+ left: calc(50% - (var(--icon-sm) / 2) + 1px);
292
+ margin: 0;
268
293
  width: var(--icon-sm);
269
294
  height: var(--icon-sm);
270
- margin: calc(var(--icon-sm) / -2) 0 0 calc(var(--icon-sm) / -2);
295
+ box-sizing: border-box;
271
296
  border: 2px solid currentColor;
272
297
  border-top-color: transparent;
273
298
  border-radius: var(--radius-pill);
274
- animation: btn-spin var(--duration-slower) linear infinite;
275
- /* Re-introduce currentColor where the variant set it; ::after inherits the parent's color BEFORE the !important override */
299
+ animation: btn-spin 0.6s linear infinite;
276
300
  color: var(--color-foreground-default);
277
301
  }
302
+
278
303
  .btn-primary.is-loading::after,
279
304
  .btn-danger.is-loading::after,
280
305
  .btn-success.is-loading::after { color: var(--color-pure-white); }
281
306
  .btn-tertiary.is-loading::after { color: var(--color-foreground-default); }
307
+ /* On a dark surface the spinner has to invert with the button: the default ring
308
+ is --color-foreground-default, which on an inverse-tinted fill is nearly the
309
+ same value and the spinner disappears. */
310
+ .btn.on-inverse.is-loading::after,
311
+ .btn-inverse.is-loading::after { color: var(--color-foreground-inverse-default); }
312
+ .btn-inverse.is-loading::after { color: var(--color-brand-text-on-light); }
282
313
 
283
314
  @keyframes btn-spin {
284
315
  to { transform: rotate(360deg); }
@@ -296,6 +327,6 @@
296
327
  }
297
328
  .btn-group .btn:first-child { border-top-left-radius: var(--radius-pill); border-bottom-left-radius: var(--radius-pill); }
298
329
  .btn-group .btn:last-child { border-top-right-radius: var(--radius-pill); border-bottom-right-radius: var(--radius-pill); }
299
- .btn-group .btn + .btn { margin-left: -1px; }
330
+ .btn-group .btn + .btn { margin-left: calc(var(--border-hairline) * -1); }
300
331
  .btn-group .btn:focus-visible,
301
332
  .btn-group .btn:hover { z-index: 1; }
@@ -44,13 +44,34 @@
44
44
  gap: var(--space-3);
45
45
  padding: var(--space-4) var(--space-5);
46
46
  border-radius: var(--radius-md);
47
- background: color-mix(in srgb, var(--callout-tint) 8%, var(--color-background-surface));
48
- border: 1px solid color-mix(in srgb, var(--callout-tint) 22%, transparent);
47
+ /* Quieter by default: the tint carries the tone and the hairline just closes
48
+ the shape. 22% read as a hard rule around a note that should recede — the
49
+ icon and the copy are what should be seen first. */
50
+ background: color-mix(in srgb, var(--callout-tint) 5%, var(--color-background-surface));
51
+ border: 1px solid color-mix(in srgb, var(--callout-tint) 12%, transparent);
49
52
  }
50
53
 
54
+ .callout-icon > svg,
55
+ .callout-icon > i,
56
+ .callout > svg,
57
+ .callout > i {
58
+ /* Lucide emits a 24px svg with no intrinsic size; without this it overflows
59
+ the 18px slot whether it IS the slot or sits inside one. */
60
+ width: 18px;
61
+ height: 18px;
62
+ flex-shrink: 0;
63
+ }
64
+ .callout-sm .callout-icon > svg,
65
+ .callout-sm .callout-icon > i { width: 16px; height: 16px; }
66
+
51
67
  .callout > i,
52
68
  .callout > svg,
53
69
  .callout-icon {
70
+ flex-shrink: 0;
71
+ align-self: flex-start;
72
+ /* Optical: pull the 18px glyph onto the cap line of the first row of text
73
+ (line-height 15.6px), rather than centring it on the whole block. */
74
+ margin-top: -1px;
54
75
  width: 18px;
55
76
  height: 18px;
56
77
  flex-shrink: 0;
@@ -112,13 +133,22 @@
112
133
  .btn-ghost → picks up the tone for text/hover.
113
134
  .btn-danger / .btn-primary stay canonical (solid) for strong/destructive. */
114
135
  .callout-actions .btn-secondary {
115
- background: color-mix(in srgb, var(--callout-tint) 14%, var(--color-background-surface));
116
- border-color: color-mix(in srgb, var(--callout-tint) 34%, transparent);
117
- color: var(--callout-fg);
136
+ /* Kept in step with the box (5% / 12%): at 14% fill and a 34% border the
137
+ action was louder than the note holding it. The fill is what makes the
138
+ button findable here; the border only needs to close the shape. */
139
+ background: color-mix(in srgb, var(--callout-tint) 10%, var(--color-background-surface));
140
+ border-color: color-mix(in srgb, var(--callout-tint) 18%, transparent);
141
+ /* Label reads as text, not as tone. It used to inherit --callout-fg (sky by
142
+ default), which put a blue label at ~4.3:1 on a 10% fill — the box already
143
+ says what kind of note this is, so the action only has to be readable.
144
+ Tonal variants below re-assert their own fg, where the colour IS the message. */
145
+ color: var(--color-foreground-default);
118
146
  }
119
147
  .callout-actions .btn-secondary:hover {
120
- background: color-mix(in srgb, var(--callout-tint) 22%, var(--color-background-surface));
121
- border-color: color-mix(in srgb, var(--callout-tint) 50%, transparent);
148
+ /* Hover moves the fill, not the border — a border that jumps 18→50% reads as
149
+ the control changing shape rather than responding. */
150
+ background: color-mix(in srgb, var(--callout-tint) 16%, var(--color-background-surface));
151
+ border-color: color-mix(in srgb, var(--callout-tint) 28%, transparent);
122
152
  }
123
153
  .callout-actions .btn-ghost { color: var(--callout-fg); }
124
154
  .callout-actions .btn-ghost:hover { background: color-mix(in srgb, var(--callout-tint) 12%, transparent); color: var(--callout-fg); }
@@ -150,6 +180,12 @@
150
180
  .callout-danger { --callout-tint: var(--color-danger-500); --callout-fg: var(--color-feedback-danger-foreground); }
151
181
  .callout-neutral { --callout-tint: var(--color-neutral-500); --callout-fg: var(--color-foreground-muted); }
152
182
 
183
+ /* Tonal actions: on warn / success / danger the hue is the message, so the label
184
+ takes the tone back. Neutral and info keep the plain text colour. */
185
+ .callout-warn .callout-actions .btn-secondary,
186
+ .callout-success .callout-actions .btn-secondary,
187
+ .callout-danger .callout-actions .btn-secondary { color: var(--callout-fg); }
188
+
153
189
  /* ---- compact size ---- */
154
190
  .callout-sm { padding: var(--space-3) var(--space-4); gap: var(--space-2-5); }
155
191
  .callout-sm > i, .callout-sm > svg, .callout-sm .callout-icon { width: 16px; height: 16px; }
@@ -172,7 +208,7 @@
172
208
  .callout-bar .callout-title { font-size: var(--fs-sm); }
173
209
 
174
210
  /* rounded alert bar (when not edge-to-edge) */
175
- .callout-bar.callout-rounded { border-radius: var(--radius-md); border-left-width: 3px; border-top: 1px solid color-mix(in srgb, var(--callout-tint) 22%, transparent); border-right: 1px solid color-mix(in srgb, var(--callout-tint) 22%, transparent); border-bottom: 1px solid color-mix(in srgb, var(--callout-tint) 22%, transparent); }
211
+ .callout-bar.callout-rounded { border-radius: var(--radius-md); border-left-width: 3px; border-top: 1px solid color-mix(in srgb, var(--callout-tint) 12%, transparent); border-right: 1px solid color-mix(in srgb, var(--callout-tint) 12%, transparent); border-bottom: 1px solid color-mix(in srgb, var(--callout-tint) 12%, transparent); }
176
212
 
177
213
  /* ---- Inline: content-sized, single-line (badges, hero notices) ---- */
178
214
  .callout-inline { display: inline-flex; width: auto; align-items: center; }
@@ -194,3 +230,4 @@
194
230
  .callout-danger.on-inverse { --callout-fg-inv: var(--color-danger-300); }
195
231
  .callout-info.on-inverse { --callout-fg-inv: var(--color-brand-accent-300); }
196
232
  .callout-neutral.on-inverse { --callout-fg-inv: var(--color-foreground-inverse-muted); }
233
+
@@ -17,11 +17,11 @@
17
17
  align-items: center;
18
18
  gap: var(--space-3);
19
19
  width: 100%;
20
- height: 40px;
20
+ height: var(--control-h-md);
21
21
  padding: 0 var(--space-3) 0 var(--space-4);
22
22
  background: var(--color-background-surface);
23
- border: 1px solid var(--color-border-default);
24
- border-radius: var(--radius-sm);
23
+ border: var(--border-hairline) solid var(--color-border-default);
24
+ border-radius: var(--shape-control);
25
25
  font-family: var(--font-sans);
26
26
  font-size: var(--fs-sm);
27
27
  font-weight: var(--w-regular);
@@ -53,7 +53,7 @@
53
53
  font-weight: var(--w-regular);
54
54
  }
55
55
  .combobox-chevron {
56
- width: 16px; height: 16px;
56
+ width: var(--icon-sm); height: var(--icon-sm);
57
57
  stroke-width: var(--icon-stroke);
58
58
  color: var(--color-foreground-muted);
59
59
  flex-shrink: 0;
@@ -63,7 +63,7 @@
63
63
 
64
64
  /* Leading icon variant */
65
65
  .combobox-trigger-icon {
66
- width: 16px; height: 16px;
66
+ width: var(--icon-sm); height: var(--icon-sm);
67
67
  stroke-width: var(--icon-stroke);
68
68
  color: var(--color-foreground-secondary);
69
69
  flex-shrink: 0;
@@ -71,7 +71,7 @@
71
71
 
72
72
  /* Avatar inside trigger (counterparty selector pattern) */
73
73
  .combobox-avatar {
74
- width: 24px; height: 24px;
74
+ width: var(--control-h-xs); height: var(--control-h-xs);
75
75
  border-radius: var(--radius-pill);
76
76
  background: var(--color-brand-subtle-solid);
77
77
  color: var(--color-brand-text-on-light);
@@ -87,14 +87,27 @@
87
87
  position: absolute;
88
88
  top: calc(100% + var(--space-2));
89
89
  left: 0;
90
- right: 0;
91
- z-index: 7000;
90
+ /* Trigger width is the FLOOR, not the ceiling. Pinning both edges made the
91
+ popover exactly as wide as the trigger, so a 50px chip crushed its own
92
+ options with nothing to rescue them. min-width keeps the form-field look
93
+ (popover aligns with the field) while letting content set the real width. */
94
+ min-width: 100%;
95
+ width: max-content;
96
+ max-width: min(420px, calc(100vw - var(--space-6)));
97
+ /* Layer 9200 — ABOVE the scrim (9000) and the drawer panel (9100). A combobox
98
+ opened inside a drawer or modal must render over that surface, not behind
99
+ its veil; at 7000 the popover was trapped under the scrim in exactly the
100
+ case where a picker is most needed. Tooltip 8000 sits below for the same
101
+ reason it sits below a modal: it annotates, it does not collect input. */
102
+ z-index: 9200;
92
103
  background: var(--color-background-surface);
93
- border: 1px solid var(--color-border-default);
104
+ border: var(--border-hairline) solid var(--color-border-default);
94
105
  border-radius: var(--radius-md);
95
106
  box-shadow: var(--shadow-xl);
96
107
  display: none;
97
108
  flex-direction: column;
109
+ /* ~7 rows before the list scrolls. Past that the user scans instead of
110
+ reading, and the popover starts covering the field it belongs to. */
98
111
  max-height: 320px;
99
112
  }
100
113
  .combobox.is-open .combobox-popover { display: flex; }
@@ -103,16 +116,16 @@
103
116
  .combobox-search {
104
117
  position: relative;
105
118
  padding: var(--space-3);
106
- border-bottom: 1px solid var(--color-border-subtle);
119
+ border-bottom: var(--border-hairline) solid var(--color-border-subtle);
107
120
  flex-shrink: 0;
108
121
  }
109
122
  .combobox-search input {
110
123
  width: 100%;
111
124
  box-sizing: border-box;
112
- height: 32px;
125
+ height: var(--control-h-sm);
113
126
  padding: 0 var(--space-3) 0 var(--space-7);
114
127
  background: var(--color-background-canvas);
115
- border: 1px solid var(--color-border-default);
128
+ border: var(--border-hairline) solid var(--color-border-default);
116
129
  border-radius: var(--radius-xs);
117
130
  font-family: var(--font-sans);
118
131
  font-size: var(--fs-sm);
@@ -129,7 +142,7 @@
129
142
  left: calc(var(--space-3) + var(--space-2));
130
143
  top: 50%;
131
144
  transform: translateY(-50%);
132
- width: 14px; height: 14px;
145
+ width: var(--icon-xs); height: var(--icon-xs);
133
146
  stroke-width: var(--icon-stroke);
134
147
  color: var(--color-foreground-muted);
135
148
  pointer-events: none;
@@ -203,7 +216,7 @@
203
216
  }
204
217
 
205
218
  .combobox-option-check {
206
- width: 16px; height: 16px;
219
+ width: var(--icon-sm); height: var(--icon-sm);
207
220
  stroke-width: 2.4;
208
221
  color: var(--color-foreground-accent);
209
222
  flex-shrink: 0;
@@ -236,7 +249,7 @@
236
249
  /* Footer (e.g. "Create new …") */
237
250
  .combobox-footer {
238
251
  padding: var(--space-2);
239
- border-top: 1px solid var(--color-border-subtle);
252
+ border-top: var(--border-hairline) solid var(--color-border-subtle);
240
253
  flex-shrink: 0;
241
254
  }
242
255
  .combobox-footer-action {
@@ -249,7 +262,7 @@
249
262
  margin-top: 0;
250
263
  width: calc(100% - var(--space-4));
251
264
  background: var(--color-background-surface);
252
- border: 1px solid var(--color-border-default);
265
+ border: var(--border-hairline) solid var(--color-border-default);
253
266
  border-radius: var(--radius-sm);
254
267
  font-family: var(--font-sans);
255
268
  font-size: var(--fs-sm);
@@ -273,13 +286,13 @@
273
286
  }
274
287
  .combobox-footer-action svg,
275
288
  .combobox-footer-action i {
276
- width: 14px;
277
- height: 14px;
289
+ width: var(--icon-xs);
290
+ height: var(--icon-xs);
278
291
  stroke-width: var(--icon-stroke);
279
292
  color: var(--color-foreground-secondary);
280
293
  }
281
294
  .combobox-footer-action i, .combobox-footer-action svg {
282
- width: 16px; height: 16px;
295
+ width: var(--icon-sm); height: var(--icon-sm);
283
296
  stroke-width: var(--icon-stroke);
284
297
  }
285
298
 
@@ -305,15 +318,15 @@
305
318
  foot meta (below list). Exposed via Combobox props
306
319
  `filters` / `meta` / `footMeta`.
307
320
  ---------------------------------------------------------------- */
308
- .combobox-filters { display: flex; flex-wrap: wrap; gap: var(--space-2); padding: var(--space-3); border-bottom: 1px solid var(--color-border-subtle); }
309
- .combobox-filter-chip { display: inline-flex; align-items: center; gap: var(--space-2); padding: 4px var(--space-3); background: var(--color-background-surface); border: 1px solid var(--color-border-default); border-radius: var(--radius-pill); font-family: var(--font-sans); font-size: var(--fs-xs); font-weight: var(--w-regular); color: var(--color-foreground-secondary); cursor: pointer; transition: var(--transition-fast); }
321
+ .combobox-filters { display: flex; flex-wrap: wrap; gap: var(--space-2); padding: var(--space-3); border-bottom: var(--border-hairline) solid var(--color-border-subtle); }
322
+ .combobox-filter-chip { display: inline-flex; align-items: center; gap: var(--space-2); padding: var(--space-2) var(--space-3); background: var(--color-background-surface); border: var(--border-hairline) solid var(--color-border-default); border-radius: var(--radius-pill); font-family: var(--font-sans); font-size: var(--fs-xs); font-weight: var(--w-regular); color: var(--color-foreground-secondary); cursor: pointer; transition: var(--transition-fast); }
310
323
  .combobox-filter-chip:hover { background: var(--color-background-hover); border-color: var(--color-border-strong); color: var(--color-foreground-default); }
311
324
  .combobox-filter-chip.is-active { background: var(--color-background-selected); border-color: var(--color-brand-border); color: var(--color-foreground-brand); font-weight: var(--w-regular); }
312
325
  .combobox-filter-chip .count { font-family: var(--font-mono);
313
326
  font-size-adjust: var(--font-mono-adjust); font-size: var(--fs-xxs); color: var(--color-foreground-muted); }
314
327
  .combobox-filter-chip.is-active .count { color: var(--color-foreground-brand); }
315
- .combobox-meta { padding: var(--space-2) var(--space-3) var(--space-1); font-size: var(--fs-xxs); font-weight: var(--w-regular); letter-spacing: var(--ls-caps); text-transform: uppercase; color: var(--color-foreground-muted); border-bottom: 1px solid var(--color-border-subtle); }
316
- .combobox-foot-meta { padding: var(--space-2) var(--space-3); font-size: var(--fs-xs); color: var(--color-foreground-muted); border-top: 1px solid var(--color-border-subtle); }
328
+ .combobox-meta { padding: var(--space-2) var(--space-3) var(--space-1); font-size: var(--fs-xxs); font-weight: var(--w-regular); letter-spacing: var(--ls-caps); text-transform: uppercase; color: var(--color-foreground-muted); border-bottom: var(--border-hairline) solid var(--color-border-subtle); }
329
+ .combobox-foot-meta { padding: var(--space-2) var(--space-3); font-size: var(--fs-xs); color: var(--color-foreground-muted); border-top: var(--border-hairline) solid var(--color-border-subtle); }
317
330
 
318
331
  /* Reduced motion */
319
332
  @media (prefers-reduced-motion: reduce) {
@@ -100,7 +100,7 @@
100
100
  /* Box — default size = md (40px tall) */
101
101
  width: 100%;
102
102
  box-sizing: border-box;
103
- height: 40px;
103
+ height: var(--control-h-md);
104
104
  padding: 0 var(--space-4);
105
105
  border: 1px solid var(--color-border-default);
106
106
  border-radius: var(--shape-control);
@@ -170,13 +170,13 @@ textarea.input {
170
170
  3. SIZES
171
171
  ---------------------------------------------------------------- */
172
172
  .input-sm {
173
- height: 32px;
173
+ height: var(--control-h-sm);
174
174
  padding: 0 var(--space-3);
175
175
  font-size: var(--fs-sm);
176
176
  border-radius: var(--radius-xs);
177
177
  }
178
178
  .input-lg {
179
- height: 48px;
179
+ height: var(--control-h-lg);
180
180
  padding: 0 var(--space-5);
181
181
  font-size: var(--fs-lg);
182
182
  border-radius: var(--radius-md);
@@ -239,7 +239,7 @@ textarea.input.input-lg { height: auto; min-height: 120px; padding: var(--space-
239
239
  display: flex;
240
240
  align-items: stretch;
241
241
  width: 100%;
242
- height: 40px;
242
+ height: var(--control-h-md);
243
243
  border: 1px solid var(--color-border-default);
244
244
  border-radius: var(--shape-control);
245
245
  background: var(--color-background-surface);
@@ -289,7 +289,7 @@ textarea.input.input-lg { height: auto; min-height: 120px; padding: var(--space-
289
289
  display: flex;
290
290
  align-items: center;
291
291
  gap: var(--space-3);
292
- height: 40px;
292
+ height: var(--control-h-md);
293
293
  padding: 0 var(--space-4);
294
294
  border: 1px solid var(--color-border-default);
295
295
  border-radius: var(--radius-pill);
@@ -299,6 +299,17 @@ textarea.input.input-lg { height: auto; min-height: 120px; padding: var(--space-
299
299
  box-shadow var(--transition-fast),
300
300
  background var(--transition-fast);
301
301
  }
302
+ /* Dense variant. The default 40px field is right on a page; inside a 52px
303
+ toolbar beside 32px buttons it reads as the odd one out, and there was no
304
+ modifier to reach for — so callers either lived with the mismatch or set a
305
+ height inline. */
306
+ .pill-search-sm {
307
+ height: var(--control-h-sm);
308
+ padding: 0 var(--space-3);
309
+ gap: var(--space-2);
310
+ }
311
+ .pill-search-sm > input { font-size: var(--fs-sm); }
312
+ .pill-search-sm > i, .pill-search-sm > svg { width: var(--icon-xs); height: var(--icon-xs); }
302
313
  .pill-search:hover { border-color: var(--color-border-strong); }
303
314
  .pill-search:focus-within {
304
315
  border-color: var(--color-border-focus);
@@ -363,10 +374,10 @@ textarea.input.input-lg { height: auto; min-height: 120px; padding: var(--space-
363
374
  .chips {
364
375
  display: flex;
365
376
  flex-wrap: wrap;
366
- gap: 6px;
377
+ gap: var(--space-2-5);
367
378
  align-items: center;
368
- min-height: 40px;
369
- padding: 5px 8px;
379
+ min-height: var(--control-h-md);
380
+ padding: var(--space-2-5) var(--space-3);
370
381
  border: 1px solid var(--color-border-default);
371
382
  border-radius: var(--shape-control);
372
383
  background: var(--color-background-surface);
@@ -382,7 +393,7 @@ textarea.input.input-lg { height: auto; min-height: 120px; padding: var(--space-
382
393
  .chip {
383
394
  display: inline-flex;
384
395
  align-items: center;
385
- gap: 4px;
396
+ gap: var(--space-2);
386
397
  height: 26px;
387
398
  padding: 0 4px 0 10px;
388
399
  background: var(--color-background-subtle);
@@ -417,7 +428,7 @@ textarea.input.input-lg { height: auto; min-height: 120px; padding: var(--space-
417
428
  .chip-x:hover { background: var(--color-border-default); color: var(--color-foreground-default); }
418
429
  .chips > input {
419
430
  flex: 1;
420
- min-width: 80px;
431
+ min-width: calc(var(--control-h-md) * 2);
421
432
  height: 26px;
422
433
  border: 0;
423
434
  outline: none;
@@ -464,8 +475,8 @@ textarea.input.input-lg { height: auto; min-height: 120px; padding: var(--space-
464
475
  ---------------------------------------------------------------- */
465
476
  .otp { display: inline-flex; gap: var(--space-2); }
466
477
  .otp > input {
467
- width: 40px;
468
- height: 48px;
478
+ width: var(--control-h-md);
479
+ height: var(--control-h-lg);
469
480
  text-align: center;
470
481
  font-family: var(--font-mono);
471
482
  font-size-adjust: var(--font-mono-adjust);
@@ -494,6 +505,9 @@ textarea.input.input-lg { height: auto; min-height: 120px; padding: var(--space-
494
505
  }
495
506
  .switch-track {
496
507
  position: relative;
508
+ /* Literal on purpose: track 20 − thumb 16 − inset 2×2 centres the knob exactly.
509
+ These are internal geometry, not icon sizes — mapping them to --icon-* broke
510
+ the vertical centring when the token did not happen to equal 20px. */
497
511
  width: 36px;
498
512
  height: 20px;
499
513
  border-radius: var(--radius-pill);
@@ -553,6 +567,7 @@ textarea.input.input-lg { height: auto; min-height: 120px; padding: var(--space-
553
567
  }
554
568
  .check input { position: absolute; opacity: 0; pointer-events: none; }
555
569
  .check-box {
570
+ position: relative;
556
571
  width: 18px;
557
572
  height: 18px;
558
573
  border: 1px solid var(--color-border-strong);
@@ -568,12 +583,28 @@ textarea.input.input-lg { height: auto; min-height: 120px; padding: var(--space-
568
583
  border-color var(--transition-fast),
569
584
  box-shadow var(--transition-fast);
570
585
  }
571
- .check-box svg { width: 14px; height: 14px; stroke-width: 3; opacity: 0; transform: scale(0.8); transition: opacity var(--transition-fast), transform var(--transition-fast); }
586
+ .check-box svg { width: var(--icon-xs); height: var(--icon-xs); stroke-width: 3; opacity: 0; transform: scale(0.8); transition: opacity var(--transition-fast), transform var(--transition-fast); }
572
587
  .check input:checked ~ .check-box {
573
588
  background: var(--color-brand-solid);
574
589
  border-color: var(--color-brand-solid);
575
590
  }
576
591
  .check input:checked ~ .check-box svg { opacity: 1; transform: scale(1); }
592
+ /* CSS-drawn tick — used when no <svg> is supplied, so the component does not
593
+ depend on consumer markup to show its own state. */
594
+ .check-box::before {
595
+ content: '';
596
+ position: absolute;
597
+ width: 4px; height: 7.5px;
598
+ border: solid var(--color-foreground-on-solid);
599
+ border-width: 0 1.75px 1.75px 0;
600
+ transform: rotate(45deg) scale(0.8);
601
+ margin-top: -1px;
602
+ opacity: 0;
603
+ transition: opacity var(--transition-fast), transform var(--transition-fast);
604
+ }
605
+ .check input:checked ~ .check-box::before { opacity: 1; transform: rotate(45deg) scale(1); }
606
+ .check-box:has(svg)::before,
607
+ .check input:indeterminate ~ .check-box::before { content: none; }
577
608
  .check input:indeterminate ~ .check-box {
578
609
  background: var(--color-brand-solid);
579
610
  border-color: var(--color-brand-solid);
@@ -668,7 +699,7 @@ textarea.input.input-lg { height: auto; min-height: 120px; padding: var(--space-
668
699
  justify-content: space-between;
669
700
  gap: var(--space-3);
670
701
  width: 100%;
671
- height: 40px;
702
+ height: var(--control-h-md);
672
703
  padding: 0 var(--space-4);
673
704
  border: 1px solid var(--color-border-default);
674
705
  border-radius: var(--shape-control);
@@ -804,8 +835,8 @@ textarea.input.input-lg { height: auto; min-height: 120px; padding: var(--space-
804
835
 
805
836
  .file-icon {
806
837
  position: relative;
807
- width: 40px;
808
- height: 48px;
838
+ width: var(--control-h-md);
839
+ height: var(--control-h-lg);
809
840
  display: flex;
810
841
  align-items: center;
811
842
  justify-content: center;
@@ -826,7 +857,7 @@ textarea.input.input-lg { height: auto; min-height: 120px; padding: var(--space-
826
857
  transform: translateX(-50%);
827
858
  font-family: var(--font-mono);
828
859
  font-size-adjust: var(--font-mono-adjust);
829
- font-size: 8px;
860
+ font-size: var(--fs-xxs);
830
861
  font-weight: var(--w-bold);
831
862
  letter-spacing: 0.04em;
832
863
  line-height: 1;
@@ -840,7 +871,7 @@ textarea.input.input-lg { height: auto; min-height: 120px; padding: var(--space-
840
871
  min-width: 0;
841
872
  display: flex;
842
873
  flex-direction: column;
843
- gap: 2px;
874
+ gap: var(--space-1);
844
875
  }
845
876
  .file-name {
846
877
  font-family: var(--font-sans);
@@ -877,7 +908,7 @@ textarea.input.input-lg { height: auto; min-height: 120px; padding: var(--space-
877
908
  .file-status {
878
909
  display: inline-flex;
879
910
  align-items: center;
880
- gap: 4px;
911
+ gap: var(--space-2);
881
912
  padding: 3px var(--space-2);
882
913
  border-radius: var(--radius-pill);
883
914
  font-family: var(--font-mono);
@@ -906,7 +937,7 @@ textarea.input.input-lg { height: auto; min-height: 120px; padding: var(--space-
906
937
  transition: background var(--transition-fast), color var(--transition-fast);
907
938
  }
908
939
  .file-remove:hover { background: var(--color-background-subtle); color: var(--color-foreground-default); }
909
- .file-remove > svg, .file-remove > i { width: 14px; height: 14px; }
940
+ .file-remove > svg, .file-remove > i { width: var(--icon-xs); height: var(--icon-xs); }
910
941
 
911
942
 
912
943
  /* ----------------------------------------------------------------
@@ -961,8 +992,10 @@ textarea.input.input-lg { height: auto; min-height: 120px; padding: var(--space-
961
992
  ---------------------------------------------------------------- */
962
993
  /* Field + selection controls on a permanently-dark surface (hero, onboarding).
963
994
  Without these, every dark surface had to override colour inline. */
964
- .field.on-inverse .field-label { color: var(--color-foreground-inverse-default); }
965
- .field.on-inverse .field-help { color: var(--color-foreground-inverse-muted); }
995
+ /* The TSX emits a bare <label> element and a .help class — there is no
996
+ .field-label / .field-help vocabulary, so target what actually ships. */
997
+ .field.on-inverse > label { color: var(--color-foreground-inverse-default); }
998
+ .field.on-inverse .help { color: var(--color-foreground-inverse-muted); }
966
999
 
967
1000
  .switch.on-inverse .switch-track {
968
1001
  background: color-mix(in srgb, var(--color-pure-white) 18%, transparent);
@@ -284,7 +284,7 @@
284
284
  /* Mobile: footer actions stack full-width (doc §Header & Footer, variant 7) */
285
285
  @media (max-width: 560px) {
286
286
  .drawer-footer { flex-direction: column; align-items: stretch; gap: var(--space-2); padding-bottom: max(var(--space-4), env(safe-area-inset-bottom)); }
287
- .drawer-footer .btn { width: 100%; justify-content: center; }
287
+ .drawer-footer .btn { width: 100%; justify-content: center; flex-shrink: 0; }
288
288
  .drawer-footer-aux { text-align: center; margin-bottom: var(--space-1); }
289
289
  }
290
290