@kinetixui/angular 0.24.0 → 0.25.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/styles.css CHANGED
@@ -29,6 +29,16 @@
29
29
  * tokens do, which is the entire point of the architecture.
30
30
  */
31
31
 
32
+ /* ── box model ──────────────────────────────────────────────────────────── */
33
+ /* Every size in this file is a border-box size, as it is in React (Tailwind's preflight sets it globally). An
34
+ Angular application has no such reset by default, and under content-box a 44px input with its padding and
35
+ border rendered 62px tall — and then did not grow at 200% text, because the extra 18px was already spent.
36
+ Scoped to KinetixUI's own classes and wrapped in :where() so it carries no specificity: an application's
37
+ own box-sizing rule, and any part here that opts out (the timeline marker), still win. */
38
+ :where([class^='kx-'], [class*=' kx-']),
39
+ :where([class^='kx-'], [class*=' kx-'])::before,
40
+ :where([class^='kx-'], [class*=' kx-'])::after { box-sizing: border-box; }
41
+
32
42
  /* ── button ─────────────────────────────────────────────────────────────── */
33
43
  .kx-btn {
34
44
  display: inline-flex;
@@ -152,14 +162,61 @@
152
162
  display: block;
153
163
  background-color: hsl(var(--card));
154
164
  color: hsl(var(--card-foreground));
155
- border: var(--border-width-default) solid hsl(var(--border));
165
+ /* The surface model (TOKENS.md): a soft edge at half the strength of the controls' stroke, and the md
166
+ elevation step for lift — the same resting card React renders. */
167
+ border: var(--border-width-default) solid hsl(var(--border) / 0.5);
156
168
  border-radius: var(--radius-container);
157
- box-shadow: var(--shadow-sm);
169
+ box-shadow: var(--shadow-md);
170
+ }
171
+ /* The interactive card (TOKENS.md, "Card"): only on an element that IS interactive — `a[href]` or `button`
172
+ carrying kxCard — so a static card can never look clickable.
173
+ hover a pointer that can hover: edge to full `--border`, lifted to `--shadow-lg`
174
+ pressed `:active`: down to `--shadow-sm` with a `--muted` wash, so it differs from hover in depth AND fill
175
+ selected `aria-pressed="true"` or `aria-current`: a 2px `--primary` edge (the border plus an inset 1px) —
176
+ weight as well as hue — alongside the elevation of whichever state it is in
177
+ focus the shared focus ring replaces the elevation and is the strongest edge the card shows
178
+ box-shadow, border-color and background-color over `--duration-fast`; nothing moves or resizes. */
179
+ :where(a[href], button).kx-card {
180
+ display: block;
181
+ appearance: none;
182
+ padding: 0;
183
+ font: inherit;
184
+ text-align: start;
185
+ text-decoration: none;
186
+ cursor: pointer;
187
+ outline: none;
188
+ transition:
189
+ box-shadow var(--duration-fast) var(--easing-standard),
190
+ border-color var(--duration-fast) var(--easing-standard),
191
+ background-color var(--duration-fast) var(--easing-standard);
192
+ }
193
+ @media (hover: hover) {
194
+ :is(a[href], button).kx-card:hover { border-color: hsl(var(--border)); box-shadow: var(--shadow-lg); }
195
+ }
196
+ :is(a[href], button).kx-card:active { box-shadow: var(--shadow-sm); background-color: hsl(var(--muted) / 0.4); }
197
+ :is(a[href], button).kx-card:is([aria-pressed='true'], [aria-current]:not([aria-current='false'])) {
198
+ border-color: hsl(var(--primary));
199
+ box-shadow: inset 0 0 0 1px hsl(var(--primary)), var(--shadow-md);
200
+ }
201
+ @media (hover: hover) {
202
+ :is(a[href], button).kx-card:is([aria-pressed='true'], [aria-current]:not([aria-current='false'])):hover {
203
+ box-shadow: inset 0 0 0 1px hsl(var(--primary)), var(--shadow-lg);
204
+ }
205
+ }
206
+ :is(a[href], button).kx-card:is([aria-pressed='true'], [aria-current]:not([aria-current='false'])):active {
207
+ box-shadow: inset 0 0 0 1px hsl(var(--primary)), var(--shadow-sm);
208
+ }
209
+ /* declared last at the highest specificity above, so no hover or selected shadow can hide it */
210
+ :is(a[href], button).kx-card:is(*, [aria-pressed], [aria-current]):focus-visible { box-shadow: var(--shadow-focus); }
211
+ button.kx-card:disabled { pointer-events: none; opacity: var(--opacity-disabled); }
212
+ @media (prefers-reduced-motion: reduce) {
213
+ :where(a[href], button).kx-card { transition: none; }
158
214
  }
159
215
  .kx-card-header { display: flex; flex-direction: column; gap: var(--spacing-1); padding: var(--spacing-6); }
160
216
  .kx-card-title { font: var(--text-title-md); font-weight: var(--font-weight-semibold); margin: 0; }
161
217
  .kx-card-description { font: var(--text-body-sm); color: hsl(var(--muted-foreground)); margin: 0; }
162
- .kx-card-content { padding: 0 var(--spacing-6) var(--spacing-6); }
218
+ /* a custom element is inline by default, and an inline box drops its inline padding around the blocks inside it */
219
+ .kx-card-content { display: block; padding: 0 var(--spacing-6) var(--spacing-6); }
163
220
  .kx-card-footer { display: flex; align-items: center; gap: var(--spacing-2); padding: 0 var(--spacing-6) var(--spacing-6); }
164
221
 
165
222
  /* ── label ──────────────────────────────────────────────────────────────── */
@@ -172,7 +229,16 @@
172
229
  /* the control is disabled, not the label — so the label follows via the group, never its own attribute */
173
230
  .kx-label[data-disabled='true'] { opacity: var(--opacity-disabled); cursor: not-allowed; }
174
231
 
175
- /* ── input / textarea ───────────────────────────────────────────────────── */
232
+ /* ── input / textarea / native select ───────────────────────────────────── */
233
+ /* Text-entry state contract (TOKENS.md, "Text entry and navigation"), shared with React:
234
+ rest edge `--muted-foreground` at 80% (3.4:1 light / 5.4:1 dark on a card; `--input` was 2.2 / 2.4)
235
+ hover a pointer that can hover: full `--muted-foreground` — never over focus, invalid, read-only or
236
+ disabled, so hover cannot take over a stronger state
237
+ focus `--action` edge + `--shadow-focus`, the strongest signal
238
+ invalid `aria-invalid="true"` → `--destructive` edge and ring
239
+ read-only an inset `--muted` fill and full-strength text; no hover
240
+ disabled `--opacity-disabled`, inert
241
+ Motion: border-color, box-shadow and background-color over `--duration-instant`; none under reduce. */
176
242
  .kx-input {
177
243
  display: block;
178
244
  inline-size: 100%;
@@ -180,11 +246,14 @@
180
246
  padding: var(--spacing-2) var(--spacing-3);
181
247
  background-color: hsl(var(--background));
182
248
  color: hsl(var(--foreground));
183
- border: var(--border-width-default) solid hsl(var(--input));
249
+ border: var(--border-width-default) solid hsl(var(--muted-foreground) / 0.8);
184
250
  border-radius: var(--radius-field);
185
251
  font: var(--text-body-md);
186
252
  font-family: var(--font-family-sans);
187
- transition: border-color var(--duration-fast) var(--easing-standard), box-shadow var(--duration-fast) var(--easing-standard);
253
+ transition:
254
+ border-color var(--duration-instant) var(--easing-standard),
255
+ box-shadow var(--duration-instant) var(--easing-standard),
256
+ background-color var(--duration-instant) var(--easing-standard);
188
257
  }
189
258
  .kx-input::placeholder { color: hsl(var(--muted-foreground)); }
190
259
  .kx-input:focus-visible { outline: none; border-color: hsl(var(--ring)); box-shadow: var(--shadow-focus); }
@@ -192,55 +261,93 @@
192
261
  /* aria-invalid is the source of truth for the error state — the styling follows the semantics, not the reverse */
193
262
  .kx-input[aria-invalid='true'] { border-color: hsl(var(--destructive)); }
194
263
  .kx-input[aria-invalid='true']:focus-visible { box-shadow: var(--shadow-focus-destructive); }
264
+ .kx-input[readonly],
265
+ .kx-textarea[readonly] { background-color: hsl(var(--muted)); }
266
+ @media (hover: hover) {
267
+ .kx-input:enabled:not(:focus):not([readonly]):not([aria-invalid='true']):hover,
268
+ .kx-textarea:enabled:not(:focus):not([readonly]):not([aria-invalid='true']):hover,
269
+ .kx-native-select:enabled:not(:focus):not([aria-invalid='true']):hover { border-color: hsl(var(--muted-foreground)); }
270
+ }
195
271
 
196
272
  /* ── checkbox / switch ──────────────────────────────────────────────────── */
273
+ /* Selection-control state contract (TOKENS.md, "Selection controls"), shared with React:
274
+ rest the unchecked box's edge is `--muted-foreground` — it is the only thing that says a checkbox is
275
+ there, so it clears SC 1.4.11's 3:1, which `--input` (2.2:1) does not
276
+ hover a pointer that can hover: a `--foreground` state layer at 8% around the control; an unchecked
277
+ box's edge darkens to `--foreground`, a checked fill steps to `--primary` at 90%
278
+ pressed the state layer deepens to 14%
279
+ focus the shared focus shadow, declared after hover and pressed at the same specificity so neither
280
+ can cover it
281
+ disabled the hover and pressed layers are keyed on `:enabled`, so a disabled control never answers */
197
282
  .kx-checkbox {
198
283
  display: inline-grid;
199
284
  place-items: center;
200
- inline-size: 18px;
201
- block-size: 18px;
285
+ /* rem, not px: a selection control has no text of its own, so its box is what must grow at 200% text */
286
+ inline-size: 1.125rem;
287
+ block-size: 1.125rem;
202
288
  flex-shrink: 0;
203
289
  padding: 0;
204
290
  background-color: hsl(var(--background));
205
- color: hsl(var(--primary-foreground));
206
- border: var(--border-width-default) solid hsl(var(--input));
291
+ color: hsl(var(--action-foreground));
292
+ border: var(--border-width-default) solid hsl(var(--muted-foreground));
207
293
  border-radius: var(--radius-sm);
208
294
  cursor: pointer;
209
- transition: background-color var(--duration-fast) var(--easing-standard), border-color var(--duration-fast) var(--easing-standard);
295
+ transition:
296
+ background-color var(--duration-instant) var(--easing-standard),
297
+ border-color var(--duration-instant) var(--easing-standard),
298
+ box-shadow var(--duration-instant) var(--easing-standard);
210
299
  }
211
- .kx-checkbox:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
212
300
  .kx-checkbox[aria-checked='true'],
213
- .kx-checkbox[aria-checked='mixed'] { background-color: hsl(var(--primary)); border-color: hsl(var(--primary)); }
301
+ .kx-checkbox[aria-checked='mixed'] { background-color: hsl(var(--action)); border-color: hsl(var(--action)); }
302
+ @media (hover: hover) {
303
+ .kx-checkbox:enabled:hover { box-shadow: 0 0 0 5px hsl(var(--foreground) / 0.08); }
304
+ .kx-checkbox:enabled:hover:not([aria-checked='true'], [aria-checked='mixed']) { border-color: hsl(var(--foreground)); }
305
+ .kx-checkbox:enabled:hover:is([aria-checked='true'], [aria-checked='mixed']) { background-color: hsl(var(--action) / 0.9); }
306
+ }
307
+ .kx-checkbox:enabled:active { box-shadow: 0 0 0 5px hsl(var(--foreground) / 0.14); }
308
+ .kx-checkbox:enabled:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
214
309
  .kx-checkbox:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; }
215
- .kx-checkbox > svg { inline-size: 14px; block-size: 14px; pointer-events: none; }
310
+ .kx-checkbox > svg { inline-size: 0.875rem; block-size: 0.875rem; pointer-events: none; }
216
311
 
217
312
  .kx-switch {
218
313
  display: inline-flex;
219
314
  align-items: center;
220
- inline-size: 44px;
221
- block-size: 24px;
315
+ inline-size: 2.75rem;
316
+ block-size: 1.5rem;
222
317
  flex-shrink: 0;
223
- padding: 2px;
318
+ padding: 0.125rem;
224
319
  background-color: hsl(var(--tertiary));
225
320
  border: var(--border-width-default) solid transparent;
226
321
  border-radius: var(--radius-full);
227
322
  cursor: pointer;
228
- transition: background-color var(--duration-base) var(--easing-standard);
323
+ transition:
324
+ background-color var(--duration-base) var(--easing-standard),
325
+ box-shadow var(--duration-instant) var(--easing-standard);
229
326
  }
230
- .kx-switch:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
231
- .kx-switch[aria-checked='true'] { background-color: hsl(var(--primary)); }
327
+ .kx-switch[aria-checked='true'] { background-color: hsl(var(--action)); }
328
+ @media (hover: hover) {
329
+ .kx-switch:enabled:hover { box-shadow: 0 0 0 5px hsl(var(--foreground) / 0.08); }
330
+ .kx-switch:enabled:hover[aria-checked='true'] { background-color: hsl(var(--action) / 0.9); }
331
+ }
332
+ .kx-switch:enabled:active { box-shadow: 0 0 0 5px hsl(var(--foreground) / 0.14); }
333
+ .kx-switch:enabled:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
232
334
  .kx-switch:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; }
233
335
  .kx-switch__thumb {
234
- inline-size: 18px;
235
- block-size: 18px;
336
+ position: relative;
337
+ inset-inline-start: 0;
338
+ inline-size: 1.125rem;
339
+ block-size: 1.125rem;
236
340
  border-radius: var(--radius-full);
237
341
  background-color: hsl(var(--static-white));
238
342
  box-shadow: var(--shadow-sm);
239
- transition: translate var(--duration-base) var(--easing-standard);
343
+ transition: inset-inline-start var(--duration-base) var(--easing-standard);
240
344
  }
241
- /* `translate` with a logical sign flip: in RTL the thumb travels the other way without a second rule */
242
- .kx-switch[aria-checked='true'] .kx-switch__thumb { translate: 20px 0; }
243
- [dir='rtl'] .kx-switch[aria-checked='true'] .kx-switch__thumb { translate: -20px 0; }
345
+ /* The thumb travels along the inline axis of the switch's OWN direction: `inset-inline-start` is logical, so
346
+ the browser resolves it against the nearest `dir`. It used to be `translate` flipped by `[dir='rtl'] …`,
347
+ which matches ANY rtl ancestor — a switch in an LTR section of an RTL page was flipped too and its thumb
348
+ landed outside the track (check:selection-visual, "direction"). A logical property needs no selector, so
349
+ no build step can rewrite the direction test into a guess (see the React Switch). */
350
+ .kx-switch[aria-checked='true'] .kx-switch__thumb { inset-inline-start: 1.25rem; }
244
351
 
245
352
  /* ── separator ──────────────────────────────────────────────────────────── */
246
353
  .kx-separator { display: block; flex-shrink: 0; background-color: hsl(var(--border)); border: 0; margin: 0; }
@@ -292,6 +399,9 @@
292
399
  .kx-tabs { display: block; }
293
400
  .kx-tab-list {
294
401
  display: inline-flex;
402
+ /* wraps inside its own width rather than running off a phone at 200% text (React's TabsList does the same) */
403
+ flex-wrap: wrap;
404
+ max-inline-size: 100%;
295
405
  align-items: center;
296
406
  gap: var(--spacing-1);
297
407
  border-block-end: var(--border-width-default) solid hsl(var(--border));
@@ -306,10 +416,21 @@
306
416
  font: var(--text-label-md);
307
417
  font-weight: var(--font-weight-medium);
308
418
  color: hsl(var(--muted-foreground));
419
+ border-start-start-radius: var(--radius-sm);
420
+ border-start-end-radius: var(--radius-sm);
309
421
  cursor: pointer;
310
- transition: color var(--duration-fast) var(--easing-standard), border-color var(--duration-fast) var(--easing-standard);
422
+ transition:
423
+ color var(--duration-fast) var(--easing-standard),
424
+ background-color var(--duration-fast) var(--easing-standard),
425
+ border-color var(--duration-fast) var(--easing-standard);
426
+ }
427
+ /* Navigation state contract (TOKENS.md, "Text entry and navigation"). Angular's strip is underlined rather
428
+ than React's segmented well — the shape differs, the semantics do not: hover is the selection controls'
429
+ 8% `--foreground` layer on an unselected, enabled tab (a disabled one used to answer with a colour change);
430
+ selected is `--foreground` text AND the 2px `--primary` indicator; focus is the ring. */
431
+ @media (hover: hover) {
432
+ .kx-tab:enabled:not([aria-selected='true']):hover { color: hsl(var(--foreground)); background-color: hsl(var(--foreground) / 0.08); }
311
433
  }
312
- .kx-tab:hover { color: hsl(var(--foreground)); }
313
434
  .kx-tab:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
314
435
  .kx-tab[aria-selected='true'] { color: hsl(var(--foreground)); border-block-end-color: hsl(var(--primary)); }
315
436
  .kx-tab:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; }
@@ -506,14 +627,17 @@
506
627
  min-block-size: 100px;
507
628
  resize: vertical;
508
629
  padding: var(--spacing-3);
509
- border: var(--border-width-default) solid hsl(var(--input));
630
+ border: var(--border-width-default) solid hsl(var(--muted-foreground) / 0.8);
510
631
  border-radius: var(--radius-field);
511
632
  background-color: hsl(var(--background));
512
633
  font: var(--text-body-md);
513
634
  font-family: var(--font-family-sans);
514
635
  color: hsl(var(--foreground));
515
636
  outline: none;
516
- transition: border-color var(--duration-fast) var(--easing-standard), box-shadow var(--duration-fast) var(--easing-standard);
637
+ transition:
638
+ border-color var(--duration-instant) var(--easing-standard),
639
+ box-shadow var(--duration-instant) var(--easing-standard),
640
+ background-color var(--duration-instant) var(--easing-standard);
517
641
  }
518
642
  .kx-textarea::placeholder { color: hsl(var(--muted-foreground)); }
519
643
  .kx-textarea:focus-visible { border-color: hsl(var(--action)); box-shadow: var(--shadow-focus); }
@@ -526,14 +650,20 @@
526
650
  inline-size: 100%;
527
651
  padding: var(--spacing-3);
528
652
  padding-inline-end: var(--spacing-8);
529
- border: var(--border-width-default) solid hsl(var(--input));
653
+ border: var(--border-width-default) solid hsl(var(--muted-foreground) / 0.8);
530
654
  background-color: hsl(var(--background));
531
655
  font: var(--text-body-md);
532
656
  font-family: var(--font-family-sans);
533
657
  color: hsl(var(--foreground));
534
658
  outline: none;
535
- transition: border-color var(--duration-fast) var(--easing-standard), box-shadow var(--duration-fast) var(--easing-standard);
659
+ transition:
660
+ border-color var(--duration-instant) var(--easing-standard),
661
+ box-shadow var(--duration-instant) var(--easing-standard);
536
662
  }
663
+ /* The placeholder is the empty-valued option: while it is chosen the field reads as unanswered. The options
664
+ in the open picker keep the value colour. */
665
+ .kx-native-select:has(option[value='']:checked) { color: hsl(var(--muted-foreground)); }
666
+ .kx-native-select option { color: hsl(var(--foreground)); }
537
667
  .kx-native-select--sharp { border-radius: var(--radius-none); }
538
668
  .kx-native-select--default { border-radius: var(--radius-field); }
539
669
  .kx-native-select--rounded { border-radius: var(--radius-md); }
@@ -541,6 +671,9 @@
541
671
  .kx-native-select:focus-visible { border-color: hsl(var(--action)); box-shadow: var(--shadow-focus); }
542
672
  .kx-native-select:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; }
543
673
  .kx-native-select[aria-invalid='true'] { border-color: hsl(var(--destructive)); }
674
+ /* Input and Textarea already turned the ring destructive on an invalid field; the select drew the default
675
+ ring, so a focused invalid select lost its error colour to the focus colour. */
676
+ .kx-native-select[aria-invalid='true']:focus-visible { box-shadow: var(--shadow-focus-destructive); }
544
677
 
545
678
  /* ── radio group ────────────────────────────────────────────────────────── */
546
679
  .kx-radio-group { display: flex; flex-direction: column; gap: var(--spacing-2); }
@@ -550,68 +683,92 @@
550
683
  .kx-radio__input {
551
684
  appearance: none;
552
685
  margin: 0;
553
- inline-size: var(--spacing-5);
554
- block-size: var(--spacing-5);
686
+ /* 1.25rem is --spacing-5 at the default text size; the spacing scale is px, so it would not grow at 200% */
687
+ inline-size: 1.25rem;
688
+ block-size: 1.25rem;
555
689
  flex-shrink: 0;
556
- border: var(--border-width-default) solid hsl(var(--input));
690
+ border: var(--border-width-default) solid hsl(var(--muted-foreground));
557
691
  border-radius: var(--radius-full);
558
692
  background-color: hsl(var(--background));
559
693
  cursor: pointer;
560
- transition: border-color var(--duration-fast) var(--easing-standard), box-shadow var(--duration-fast) var(--easing-standard);
694
+ /* the selection-control contract (see checkbox): a 3:1 edge, an 8% / 14% state layer, focus on top */
695
+ --kx-radio-dot: 0 0 #0000;
696
+ --kx-radio-layer: 0 0 #0000;
697
+ box-shadow: var(--kx-radio-dot), var(--kx-radio-layer);
698
+ transition: border-color var(--duration-instant) var(--easing-standard), box-shadow var(--duration-instant) var(--easing-standard);
561
699
  }
562
- /* the dot is an inset ring rather than a pseudo-element, so it scales with the control */
700
+ /* the dot is an inset ring rather than a pseudo-element, so it scales with the control — and it is its own
701
+ custom property so the hover and pressed layers add to it instead of replacing it */
563
702
  .kx-radio__input:checked {
564
- border-color: hsl(var(--primary));
565
- box-shadow: inset 0 0 0 4px hsl(var(--primary));
703
+ border-color: hsl(var(--action));
704
+ --kx-radio-dot: inset 0 0 0 0.25rem hsl(var(--action));
705
+ }
706
+ @media (hover: hover) {
707
+ .kx-radio__input:enabled:hover { --kx-radio-layer: 0 0 0 5px hsl(var(--foreground) / 0.08); }
708
+ .kx-radio__input:enabled:hover:not(:checked) { border-color: hsl(var(--foreground)); }
566
709
  }
567
- .kx-radio__input:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
568
- .kx-radio__input:checked:focus-visible { box-shadow: inset 0 0 0 4px hsl(var(--primary)), var(--shadow-focus); }
710
+ .kx-radio__input:enabled:active { --kx-radio-layer: 0 0 0 5px hsl(var(--foreground) / 0.14); }
711
+ .kx-radio__input:enabled:focus-visible { outline: none; --kx-radio-layer: var(--shadow-focus); }
569
712
  .kx-radio__input:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; }
570
713
  .kx-radio__label { font: var(--text-body-md); font-family: var(--font-family-sans); color: hsl(var(--foreground)); cursor: pointer; }
571
714
  .kx-radio__input:disabled + .kx-radio__label { opacity: var(--opacity-disabled); cursor: not-allowed; }
572
715
 
573
716
  /* ── slider ─────────────────────────────────────────────────────────────── */
574
- .kx-slider { display: block; inline-size: 100%; }
717
+ .kx-slider { position: relative; display: block; inline-size: 100%; }
718
+ /* The visible track and fill sit behind a transparent native track. They are placed with logical properties,
719
+ so the fill grows from the inline start of the slider's own direction (see KxSlider). */
720
+ .kx-slider__track {
721
+ position: absolute;
722
+ inset-inline: 0;
723
+ inset-block-start: 50%;
724
+ block-size: 0.5rem;
725
+ margin-block-start: -0.25rem;
726
+ overflow: hidden;
727
+ border-radius: var(--radius-full);
728
+ background-color: hsl(var(--muted));
729
+ pointer-events: none;
730
+ }
731
+ .kx-slider__fill {
732
+ position: absolute;
733
+ inset-block: 0;
734
+ inset-inline-start: 0;
735
+ inline-size: var(--kx-slider-fill, 0%);
736
+ background-color: hsl(var(--primary));
737
+ }
738
+ .kx-slider:has(.kx-slider__input:disabled) .kx-slider__track { opacity: var(--opacity-disabled); }
575
739
  .kx-slider__input {
740
+ position: relative;
741
+ display: block;
576
742
  appearance: none;
577
743
  inline-size: 100%;
578
- block-size: var(--spacing-5);
744
+ /* rem throughout (the px spacing scale's 20 / 8 at the default text size), so the hit area grows with text */
745
+ block-size: 1.25rem;
579
746
  margin: 0;
580
747
  background: transparent;
581
748
  cursor: pointer;
582
749
  }
583
- /* the filled portion is a gradient stop driven by --kx-slider-fill, so the track follows the real value */
584
750
  .kx-slider__input::-webkit-slider-runnable-track {
585
- block-size: var(--spacing-2);
751
+ block-size: 0.5rem;
586
752
  border-radius: var(--radius-full);
587
- background: linear-gradient(
588
- to right,
589
- hsl(var(--primary)) var(--kx-slider-fill, 0%),
590
- hsl(var(--muted)) var(--kx-slider-fill, 0%)
591
- );
753
+ background: transparent;
592
754
  }
593
755
  .kx-slider__input::-moz-range-track {
594
- block-size: var(--spacing-2);
595
- border-radius: var(--radius-full);
596
- background-color: hsl(var(--muted));
597
- }
598
- .kx-slider__input::-moz-range-progress {
599
- block-size: var(--spacing-2);
756
+ block-size: 0.5rem;
600
757
  border-radius: var(--radius-full);
601
- background-color: hsl(var(--primary));
758
+ background: transparent;
602
759
  }
603
760
  .kx-slider__input::-webkit-slider-thumb {
604
761
  appearance: none;
605
- margin-block-start: -6px;
606
- inline-size: var(--spacing-5);
607
- block-size: var(--spacing-5);
762
+ margin-block-start: -0.375rem;
763
+ inline-size: 1.25rem;
764
+ block-size: 1.25rem;
608
765
  border: var(--border-width-default) solid hsl(var(--primary));
609
766
  border-radius: var(--radius-full);
610
767
  background-color: hsl(var(--background));
611
768
  }
612
769
  .kx-slider__input::-moz-range-thumb {
613
- inline-size: var(--spacing-5);
614
- block-size: var(--spacing-5);
770
+ inline-size: 1.25rem;
771
+ block-size: 1.25rem;
615
772
  border: var(--border-width-default) solid hsl(var(--primary));
616
773
  border-radius: var(--radius-full);
617
774
  background-color: hsl(var(--background));
@@ -619,16 +776,47 @@
619
776
  .kx-slider__input:focus-visible { outline: none; box-shadow: var(--shadow-focus); border-radius: var(--radius-full); }
620
777
  .kx-slider__input:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; }
621
778
 
779
+ /* ── composite fields: number input, password input ─────────────────────── */
780
+ /* The text-entry state contract (TOKENS.md, "Composite fields"), shared with React's NumberInput and
781
+ InputGroup: the wrapper is ONE field, and it reads its state from the input inside it.
782
+ rest edge `--muted-foreground` at 80% (3:1 on a card; `--input` was 2.2:1)
783
+ hover full `--muted-foreground` — never over focus, invalid or disabled
784
+ focus the INPUT focused → `--action` edge + `--shadow-focus`. A focused part button (the reveal toggle)
785
+ draws its own ring and leaves the wrapper at rest, so the ring says which part has focus
786
+ invalid `aria-invalid="true"` on the inner input (the `aria-invalid` input) → `--destructive`, kept under
787
+ the pointer and when focused
788
+ read-only `readonly` on the inner input (kx-number-input's `readonly`) → the inset `--muted` fill, no hover
789
+ disabled `--opacity-disabled` on the whole field, inert */
790
+ .kx-number-input,
791
+ .kx-password-input {
792
+ border: var(--border-width-default) solid hsl(var(--muted-foreground) / 0.8);
793
+ border-radius: var(--radius-field);
794
+ background-color: hsl(var(--background));
795
+ overflow: hidden;
796
+ transition:
797
+ border-color var(--duration-instant) var(--easing-standard),
798
+ box-shadow var(--duration-instant) var(--easing-standard);
799
+ }
800
+ @media (hover: hover) {
801
+ .kx-number-input:not(:has(input:is(:focus, :disabled, [readonly], [aria-invalid='true']))):hover,
802
+ .kx-password-input:not(:has(input:is(:focus, :disabled, [readonly], [aria-invalid='true']))):hover { border-color: hsl(var(--muted-foreground)); }
803
+ }
804
+ .kx-number-input:has(.kx-number-input__input[readonly]) { background-color: hsl(var(--muted)); }
805
+ .kx-number-input:has(.kx-number-input__input:focus),
806
+ .kx-password-input:has(.kx-password-input__input:focus) { border-color: hsl(var(--action)); box-shadow: var(--shadow-focus); }
807
+ /* after focus and at least as specific, so a focused invalid field keeps its destructive edge */
808
+ .kx-number-input:has(.kx-number-input__input[aria-invalid='true']),
809
+ .kx-password-input:has(.kx-password-input__input[aria-invalid='true']) { border-color: hsl(var(--destructive)); }
810
+ .kx-number-input:has(.kx-number-input__input[aria-invalid='true']:focus),
811
+ .kx-password-input:has(.kx-password-input__input[aria-invalid='true']:focus) { box-shadow: var(--shadow-focus-destructive); }
812
+ .kx-number-input:has(input:disabled),
813
+ .kx-password-input:has(input:disabled) { opacity: var(--opacity-disabled); cursor: not-allowed; }
814
+
622
815
  /* ── number input ───────────────────────────────────────────────────────── */
623
816
  .kx-number-input {
624
817
  display: inline-flex;
625
818
  align-items: stretch;
626
- border: var(--border-width-default) solid hsl(var(--input));
627
- border-radius: var(--radius-field);
628
- background-color: hsl(var(--background));
629
- overflow: hidden;
630
819
  }
631
- .kx-number-input:focus-within { border-color: hsl(var(--action)); box-shadow: var(--shadow-focus); }
632
820
  .kx-number-input__input {
633
821
  inline-size: var(--spacing-16);
634
822
  min-inline-size: 0;
@@ -654,20 +842,16 @@
654
842
  font: var(--text-body-md);
655
843
  cursor: pointer;
656
844
  }
657
- .kx-number-input__step:hover { color: hsl(var(--foreground)); }
658
- .kx-number-input__step:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; }
845
+ .kx-number-input__step:enabled:hover { color: hsl(var(--foreground)); }
846
+ /* the field's own opacity already says disabled — a step dimmed again inside it would read as more disabled */
847
+ .kx-number-input__step:disabled { cursor: not-allowed; }
659
848
 
660
849
  /* ── password input ─────────────────────────────────────────────────────── */
661
850
  .kx-password-input {
662
851
  display: inline-flex;
663
852
  align-items: stretch;
664
853
  inline-size: 100%;
665
- border: var(--border-width-default) solid hsl(var(--input));
666
- border-radius: var(--radius-field);
667
- background-color: hsl(var(--background));
668
- overflow: hidden;
669
854
  }
670
- .kx-password-input:focus-within { border-color: hsl(var(--action)); box-shadow: var(--shadow-focus); }
671
855
  .kx-password-input__input {
672
856
  flex: 1;
673
857
  min-inline-size: 0;
@@ -689,8 +873,179 @@
689
873
  font-family: var(--font-family-sans);
690
874
  cursor: pointer;
691
875
  }
692
- .kx-password-input__reveal:hover { color: hsl(var(--foreground)); }
693
- .kx-password-input__reveal:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
876
+ .kx-password-input__reveal:enabled:hover { color: hsl(var(--foreground)); }
877
+ /* inset: the wrapper clips with overflow:hidden, so an outer ring would be cut off at the field's edge */
878
+ .kx-password-input__reveal:focus-visible { outline: none; box-shadow: inset 0 0 0 2px hsl(var(--ring)); }
879
+
880
+ /* ── input group ────────────────────────────────────────────────────────── */
881
+ /* The same composite-field contract as the number and password inputs above, read from the group's input:
882
+ rest `--muted-foreground` at 80%; hover the full colour, never over focus, invalid, read-only or disabled;
883
+ the INPUT focused → `--action` edge and `--shadow-focus` on the shell (a focused add-on button draws its own
884
+ inset ring instead and leaves the shell at rest); `aria-invalid` → `--destructive`, kept under the pointer
885
+ and focus; `readonly` → the inset `--muted` fill; a disabled input → the whole group at `--opacity-disabled`. */
886
+ .kx-input-group {
887
+ display: flex;
888
+ align-items: center;
889
+ inline-size: 100%;
890
+ overflow: hidden;
891
+ border: var(--border-width-default) solid hsl(var(--muted-foreground) / 0.8);
892
+ border-radius: var(--radius-field);
893
+ background-color: hsl(var(--background));
894
+ font-family: var(--font-family-sans);
895
+ transition:
896
+ border-color var(--duration-instant) var(--easing-standard),
897
+ box-shadow var(--duration-instant) var(--easing-standard),
898
+ background-color var(--duration-instant) var(--easing-standard);
899
+ }
900
+ @media (hover: hover) {
901
+ .kx-input-group:not(:has(.kx-input-group__input:is(:focus, :disabled, [readonly], [aria-invalid='true']))):hover { border-color: hsl(var(--muted-foreground)); }
902
+ }
903
+ .kx-input-group:has(.kx-input-group__input[readonly]) { background-color: hsl(var(--muted)); }
904
+ .kx-input-group:has(.kx-input-group__input:focus) { border-color: hsl(var(--action)); box-shadow: var(--shadow-focus); }
905
+ .kx-input-group:has(.kx-input-group__input[aria-invalid='true']) { border-color: hsl(var(--destructive)); }
906
+ .kx-input-group:has(.kx-input-group__input[aria-invalid='true']:focus) { box-shadow: var(--shadow-focus-destructive); }
907
+ /* The field is disabled when its input is. An add-on button disabled on its own (Copy with nothing to copy) dims
908
+ only itself: the input beside it still takes typing, so the group must not read as disabled. */
909
+ .kx-input-group:has(.kx-input-group__input:disabled) { opacity: var(--opacity-disabled); cursor: not-allowed; }
910
+ .kx-input-group:not(:has(.kx-input-group__input:disabled)) .kx-input-group__button:disabled { opacity: var(--opacity-disabled); }
911
+ .kx-input-group__input {
912
+ flex: 1 1 0;
913
+ /* an <input>'s min-content is its intrinsic width (~20 characters), and that is what it contributes when
914
+ an auto-sized track sizes the group; a zero base keeps the group from widening its column past a phone
915
+ viewport at 200% text. It still grows to fill the field. */
916
+ inline-size: 0;
917
+ min-inline-size: 0;
918
+ border: 0;
919
+ padding: var(--spacing-3);
920
+ background: transparent;
921
+ font: var(--text-body-md);
922
+ font-family: var(--font-family-sans);
923
+ color: hsl(var(--foreground));
924
+ outline: none;
925
+ }
926
+ .kx-input-group__input::placeholder { color: hsl(var(--muted-foreground)); }
927
+ .kx-input-group__input:disabled { cursor: not-allowed; }
928
+ .kx-input-group__addon {
929
+ display: flex;
930
+ flex-shrink: 0;
931
+ align-items: center;
932
+ gap: var(--spacing-2);
933
+ color: hsl(var(--muted-foreground));
934
+ }
935
+ .kx-input-group__addon--start { padding-inline-start: var(--spacing-3); }
936
+ .kx-input-group__addon--end { padding-inline-end: var(--spacing-3); }
937
+ .kx-input-group__addon > svg { inline-size: 1rem; block-size: 1rem; flex-shrink: 0; }
938
+ .kx-input-group__text {
939
+ padding-inline: var(--spacing-3);
940
+ font: var(--text-body-md);
941
+ font-family: var(--font-family-sans);
942
+ color: hsl(var(--muted-foreground));
943
+ white-space: nowrap;
944
+ user-select: none;
945
+ }
946
+ /* the divider is internal structure, deliberately quieter than the shell's edge */
947
+ .kx-input-group__button {
948
+ display: flex;
949
+ flex-shrink: 0;
950
+ align-self: stretch;
951
+ align-items: center;
952
+ gap: var(--spacing-2);
953
+ appearance: none;
954
+ margin: 0;
955
+ border: 0;
956
+ border-inline-start: var(--border-width-default) solid hsl(var(--input));
957
+ padding-inline: var(--spacing-3);
958
+ background-color: hsl(var(--muted));
959
+ font: var(--text-label-md);
960
+ font-family: var(--font-family-sans);
961
+ color: hsl(var(--foreground));
962
+ cursor: pointer;
963
+ outline: none;
964
+ transition: background-color var(--duration-instant) var(--easing-standard), box-shadow var(--duration-instant) var(--easing-standard);
965
+ }
966
+ /* the selection controls' `--foreground` state layer, as an inset shadow so it composes with the focus ring */
967
+ @media (hover: hover) {
968
+ .kx-input-group__button:enabled:hover { box-shadow: inset 0 0 0 100vmax hsl(var(--foreground) / 0.08); }
969
+ }
970
+ .kx-input-group__button:focus-visible { box-shadow: inset 0 0 0 2px hsl(var(--ring)); }
971
+ .kx-input-group__button:disabled { cursor: not-allowed; }
972
+
973
+ /* ── one-time code ──────────────────────────────────────────────────────── */
974
+ /* The cells are decoration of one transparent input laid over them (KxInputOtp). Every cell owns its whole
975
+ border and each after the first in a group is pulled back a pixel, so a group that wraps at 200% text is
976
+ still closed at the end it starts from (React's InputOTP learned this the hard way). The cells follow the
977
+ composite-field contract, read from the input: rest `--muted-foreground` at 80%; hover the full colour, not
978
+ while focused, invalid or disabled; the ACTIVE cell (where the next character goes) takes `--action` and the
979
+ focus shadow, drawn above its neighbours; `aria-invalid` → `--destructive` edges; disabled → dimmed, inert. */
980
+ .kx-input-otp {
981
+ position: relative;
982
+ display: inline-flex;
983
+ max-inline-size: 100%;
984
+ }
985
+ .kx-input-otp__cells { display: flex; flex-wrap: wrap; align-items: center; gap: var(--spacing-2); }
986
+ .kx-input-otp__group { display: flex; flex-wrap: wrap; align-items: center; }
987
+ .kx-input-otp__slot {
988
+ position: relative;
989
+ display: flex;
990
+ align-items: center;
991
+ justify-content: center;
992
+ inline-size: 2.25rem;
993
+ block-size: 2.25rem;
994
+ border: var(--border-width-default) solid hsl(var(--muted-foreground) / 0.8);
995
+ background-color: hsl(var(--background));
996
+ font: var(--text-body-md);
997
+ font-family: var(--font-family-sans);
998
+ color: hsl(var(--foreground));
999
+ transition:
1000
+ border-color var(--duration-instant) var(--easing-standard),
1001
+ box-shadow var(--duration-instant) var(--easing-standard);
1002
+ }
1003
+ .kx-input-otp__slot:not(:first-child) { margin-inline-start: -1px; }
1004
+ .kx-input-otp__slot:first-child { border-start-start-radius: var(--radius-md); border-end-start-radius: var(--radius-md); }
1005
+ .kx-input-otp__slot:last-child { border-start-end-radius: var(--radius-md); border-end-end-radius: var(--radius-md); }
1006
+ @media (hover: hover) {
1007
+ .kx-input-otp:not(:has(.kx-input-otp__input:is(:focus, :disabled, [aria-invalid='true']))):hover .kx-input-otp__slot { border-color: hsl(var(--muted-foreground)); }
1008
+ }
1009
+ .kx-input-otp:has(.kx-input-otp__input[aria-invalid='true']) .kx-input-otp__slot { border-color: hsl(var(--destructive)); }
1010
+ .kx-input-otp__slot[data-active] { z-index: 1; border-color: hsl(var(--action)); box-shadow: var(--shadow-focus); }
1011
+ .kx-input-otp:has(.kx-input-otp__input[aria-invalid='true']) .kx-input-otp__slot[data-active] { border-color: hsl(var(--destructive)); box-shadow: var(--shadow-focus-destructive); }
1012
+ .kx-input-otp:has(.kx-input-otp__input:disabled) { opacity: var(--opacity-disabled); cursor: not-allowed; }
1013
+ .kx-input-otp__caret { position: absolute; inline-size: 1px; block-size: 1rem; background-color: hsl(var(--foreground)); pointer-events: none; }
1014
+ .kx-input-otp__separator { inline-size: var(--spacing-3); block-size: 2px; border-radius: var(--radius-full); background-color: hsl(var(--muted-foreground)); }
1015
+ /* the real input: over the cells, receiving every keystroke, paste and autofill, painting nothing itself */
1016
+ .kx-input-otp__input {
1017
+ position: absolute;
1018
+ inset: 0;
1019
+ inline-size: 100%;
1020
+ block-size: 100%;
1021
+ margin: 0;
1022
+ padding: 0;
1023
+ border: 0;
1024
+ background: transparent;
1025
+ color: transparent;
1026
+ caret-color: transparent;
1027
+ font: var(--text-body-md);
1028
+ letter-spacing: -0.5em;
1029
+ outline: none;
1030
+ }
1031
+ .kx-input-otp__input::selection { background: transparent; }
1032
+ .kx-input-otp__input:disabled { cursor: not-allowed; }
1033
+
1034
+ /* ── rating ─────────────────────────────────────────────────────────────── */
1035
+ /* Each star is a real radio, visually hidden and kept in the tab order, painted through its label (KxRating).
1036
+ The stars are rem-sized, so they grow with the reader's text; a chosen or previewed star is `--warning`
1037
+ filled, an empty one a `--muted-foreground` outline (3:1 against the page, SC 1.4.11). */
1038
+ .kx-rating { display: inline-flex; align-items: center; gap: 0.125rem; }
1039
+ .kx-rating__item { position: relative; display: inline-flex; padding: 0.125rem; border-radius: var(--radius-sm); }
1040
+ label.kx-rating__item { cursor: pointer; }
1041
+ .kx-rating__radio { position: absolute; inset: 0; margin: 0; opacity: 0; pointer-events: none; }
1042
+ .kx-rating__star { inline-size: 1.25rem; block-size: 1.25rem; fill: transparent; stroke: hsl(var(--muted-foreground)); stroke-width: 1.5; stroke-linejoin: round; }
1043
+ .kx-rating--sm .kx-rating__star { inline-size: 1rem; block-size: 1rem; }
1044
+ .kx-rating--lg .kx-rating__star { inline-size: 1.5rem; block-size: 1.5rem; }
1045
+ .kx-rating__star--on { fill: hsl(var(--warning)); stroke: hsl(var(--warning)); }
1046
+ .kx-rating__item:has(.kx-rating__radio:focus-visible) { box-shadow: var(--shadow-focus); }
1047
+ .kx-rating[aria-disabled='true'] { opacity: var(--opacity-disabled); }
1048
+ .kx-rating[aria-disabled='true'] .kx-rating__item { cursor: not-allowed; }
694
1049
 
695
1050
  /* ── field ──────────────────────────────────────────────────────────────── */
696
1051
  .kx-field { display: flex; flex-direction: column; gap: var(--spacing-2); }
@@ -718,9 +1073,10 @@
718
1073
  cursor: pointer;
719
1074
  transition: background-color var(--duration-fast) var(--easing-standard), color var(--duration-fast) var(--easing-standard);
720
1075
  }
721
- .kx-toggle--sm { min-inline-size: var(--spacing-8); block-size: var(--spacing-8); padding-inline: var(--spacing-2); }
722
- .kx-toggle--md { min-inline-size: var(--spacing-10); block-size: var(--spacing-10); padding-inline: var(--spacing-3); }
723
- .kx-toggle--lg { min-inline-size: var(--spacing-12); block-size: var(--spacing-12); padding-inline: var(--spacing-4); }
1076
+ /* the square is rem (--spacing-8 / 10 / 12 at the default text size): it holds the label, so it grows with it */
1077
+ .kx-toggle--sm { min-inline-size: 2rem; block-size: 2rem; padding-inline: var(--spacing-2); }
1078
+ .kx-toggle--md { min-inline-size: 2.5rem; block-size: 2.5rem; padding-inline: var(--spacing-3); }
1079
+ .kx-toggle--lg { min-inline-size: 3rem; block-size: 3rem; padding-inline: var(--spacing-4); }
724
1080
  .kx-toggle--outline { border-color: hsl(var(--input)); }
725
1081
  .kx-toggle:hover { background-color: hsl(var(--muted)); }
726
1082
  .kx-toggle:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
@@ -739,10 +1095,13 @@
739
1095
  /* ── segmented control ──────────────────────────────────────────────────── */
740
1096
  .kx-segmented {
741
1097
  display: inline-flex;
1098
+ flex-wrap: wrap;
1099
+ max-inline-size: 100%;
742
1100
  align-items: stretch;
743
1101
  padding: var(--spacing-1);
744
1102
  border-radius: var(--radius-md);
745
- background-color: hsl(var(--muted));
1103
+ /* an inset well; the chosen segment is a small raised surface on it (TOKENS.md, "Surface model") */
1104
+ background-color: hsl(var(--surface-grouped));
746
1105
  }
747
1106
  .kx-segmented__item { display: inline-flex; }
748
1107
  .kx-segmented__radio { position: absolute; opacity: 0; pointer-events: none; }
@@ -757,12 +1116,27 @@
757
1116
  font-weight: var(--font-weight-medium);
758
1117
  color: hsl(var(--muted-foreground));
759
1118
  cursor: pointer;
760
- transition: background-color var(--duration-fast) var(--easing-standard), color var(--duration-fast) var(--easing-standard);
1119
+ transition:
1120
+ background-color var(--duration-fast) var(--easing-standard),
1121
+ color var(--duration-fast) var(--easing-standard),
1122
+ box-shadow var(--duration-fast) var(--easing-standard);
761
1123
  }
1124
+ /* chosen: surface + half-strength edge + depth + text — the Card's raised contract at control scale, so it
1125
+ sits ABOVE its track in both themes (a `--background` segment sank below a dark `--muted` track) */
762
1126
  .kx-segmented__radio:checked + .kx-segmented__label {
763
- background-color: hsl(var(--background));
1127
+ background-color: hsl(var(--card));
1128
+ color: hsl(var(--foreground));
1129
+ box-shadow: inset 0 0 0 1px hsl(var(--border) / 0.5), var(--shadow-sm);
1130
+ }
1131
+ @media (hover: hover) {
1132
+ .kx-segmented__radio:enabled:not(:checked) + .kx-segmented__label:hover {
1133
+ background-color: hsl(var(--foreground) / 0.08);
1134
+ color: hsl(var(--foreground));
1135
+ }
1136
+ }
1137
+ .kx-segmented__radio:enabled:not(:checked) + .kx-segmented__label:active {
1138
+ background-color: hsl(var(--foreground) / 0.14);
764
1139
  color: hsl(var(--foreground));
765
- box-shadow: var(--shadow-sm);
766
1140
  }
767
1141
  .kx-segmented__radio:focus-visible + .kx-segmented__label { box-shadow: var(--shadow-focus); }
768
1142
  .kx-segmented__radio:disabled + .kx-segmented__label { opacity: var(--opacity-disabled); cursor: not-allowed; }
@@ -778,5 +1152,1268 @@
778
1152
  .kx-textarea,
779
1153
  .kx-native-select,
780
1154
  .kx-radio__input,
1155
+ .kx-checkbox,
1156
+ .kx-switch,
1157
+ .kx-switch__thumb,
1158
+ .kx-input,
1159
+ .kx-number-input,
1160
+ .kx-password-input,
1161
+ .kx-input-group,
1162
+ .kx-input-group__button,
1163
+ .kx-input-otp__slot,
1164
+ .kx-tab,
781
1165
  .kx-tag__remove { transition: none; }
782
1166
  }
1167
+
1168
+ /* ══ content wave ═════════════════════════════════════════════════════════
1169
+ banner · button group · circular progress · code block · description list
1170
+ fab · image · inform · list · marquee · page header · timeline
1171
+
1172
+ Logical properties throughout (padding-inline, inset-inline-start, margin-inline)
1173
+ so every surface mirrors under dir="rtl" without a second rule.
1174
+ ════════════════════════════════════════════════════════════════════════ */
1175
+
1176
+ /* ── banner + inform: one intent palette, two containers ────────────────── */
1177
+ .kx-banner,
1178
+ .kx-inform {
1179
+ display: flex;
1180
+ align-items: flex-start;
1181
+ gap: var(--spacing-2);
1182
+ font: var(--text-body-sm);
1183
+ font-family: var(--font-family-sans);
1184
+ }
1185
+ .kx-banner {
1186
+ width: 100%;
1187
+ border-block-end: var(--border-width-default) solid transparent;
1188
+ padding: var(--spacing-3) var(--spacing-4);
1189
+ }
1190
+ .kx-banner--sticky { position: sticky; inset-block-start: 0; z-index: var(--z-index-sticky, 40); }
1191
+ .kx-inform { border-radius: var(--radius-md); padding: var(--spacing-3); }
1192
+
1193
+ .kx-banner--information { background-color: hsl(var(--info) / 0.1); color: hsl(var(--on-info-container)); border-color: hsl(var(--info) / 0.2); }
1194
+ .kx-banner--warning { background-color: hsl(var(--warning) / 0.15); color: hsl(var(--warning)); border-color: hsl(var(--warning) / 0.25); }
1195
+ .kx-banner--success { background-color: hsl(var(--success) / 0.15); color: hsl(var(--success)); border-color: hsl(var(--success) / 0.25); }
1196
+ .kx-banner--error { background-color: hsl(var(--destructive) / 0.1); color: hsl(var(--destructive)); border-color: hsl(var(--destructive) / 0.2); }
1197
+ .kx-banner--action { background-color: hsl(var(--foreground)); color: hsl(var(--background)); }
1198
+ .kx-inform--information { background-color: hsl(var(--info) / 0.1); color: hsl(var(--on-info-container)); }
1199
+ .kx-inform--warning { background-color: hsl(var(--warning) / 0.15); color: hsl(var(--warning)); }
1200
+ .kx-inform--success { background-color: hsl(var(--success) / 0.15); color: hsl(var(--success)); }
1201
+ .kx-inform--error { background-color: hsl(var(--destructive) / 0.1); color: hsl(var(--destructive)); }
1202
+ .kx-inform--action { background-color: hsl(var(--foreground)); color: hsl(var(--background)); }
1203
+
1204
+ .kx-banner__body {
1205
+ display: flex;
1206
+ flex: 1 1 0%;
1207
+ flex-wrap: wrap;
1208
+ align-items: center;
1209
+ justify-content: center;
1210
+ gap: var(--spacing-1) var(--spacing-3);
1211
+ min-width: 0;
1212
+ margin-inline: auto;
1213
+ max-width: 56rem;
1214
+ text-align: center;
1215
+ }
1216
+ .kx-inform__body { flex: 1 1 0%; min-width: 0; }
1217
+ .kx-inform__message { margin: 0; }
1218
+ .kx-inform__message:not(:only-child) { margin-block-end: var(--spacing-2); }
1219
+
1220
+ .kx-banner [kxBannerAction],
1221
+ .kx-inform [kxInformAction] {
1222
+ display: inline-flex;
1223
+ align-items: center;
1224
+ gap: var(--spacing-1);
1225
+ background: none;
1226
+ border: 0;
1227
+ padding: 0;
1228
+ color: inherit;
1229
+ font: var(--text-label-md);
1230
+ font-weight: var(--font-weight-medium);
1231
+ text-underline-offset: 2px;
1232
+ cursor: pointer;
1233
+ }
1234
+ .kx-banner [kxBannerAction]:hover,
1235
+ .kx-banner [kxBannerAction]:focus-visible,
1236
+ .kx-inform [kxInformAction]:hover,
1237
+ .kx-inform [kxInformAction]:focus-visible { text-decoration: underline; outline: none; }
1238
+
1239
+ .kx-banner__dismiss,
1240
+ .kx-inform__dismiss {
1241
+ flex-shrink: 0;
1242
+ background: none;
1243
+ border: 0;
1244
+ padding: 2px;
1245
+ margin: -2px;
1246
+ border-radius: 2px;
1247
+ color: inherit;
1248
+ opacity: var(--opacity-muted);
1249
+ cursor: pointer;
1250
+ transition: opacity var(--duration-fast) var(--easing-standard);
1251
+ }
1252
+ .kx-banner__dismiss:hover,
1253
+ .kx-banner__dismiss:focus-visible,
1254
+ .kx-inform__dismiss:hover,
1255
+ .kx-inform__dismiss:focus-visible { opacity: var(--opacity-visible); outline: var(--border-width-default) solid currentColor; }
1256
+ @media (prefers-reduced-motion: reduce) {
1257
+ .kx-banner__dismiss, .kx-inform__dismiss { transition: none; }
1258
+ }
1259
+
1260
+ /* ── button group ───────────────────────────────────────────────────────── */
1261
+ .kx-button-group { display: flex; width: fit-content; align-items: stretch; }
1262
+ .kx-button-group > * { position: relative; }
1263
+ .kx-button-group > *:focus-visible, .kx-button-group > *:focus-within { z-index: 10; }
1264
+ .kx-button-group--horizontal { flex-direction: row; }
1265
+ /* -1px on the inline start of every child but the first: one shared border, not two stacked. */
1266
+ .kx-button-group--horizontal > *:not(:first-child) { margin-inline-start: -1px; border-start-start-radius: 0; border-end-start-radius: 0; }
1267
+ .kx-button-group--horizontal > *:not(:last-child) { border-start-end-radius: 0; border-end-end-radius: 0; }
1268
+ .kx-button-group--vertical { flex-direction: column; }
1269
+ .kx-button-group--vertical > *:not(:first-child) { margin-block-start: -1px; border-start-start-radius: 0; border-start-end-radius: 0; }
1270
+ .kx-button-group--vertical > *:not(:last-child) { border-end-start-radius: 0; border-end-end-radius: 0; }
1271
+
1272
+ .kx-button-group-separator { flex-shrink: 0; align-self: stretch; background-color: hsl(var(--border)); }
1273
+ .kx-button-group-separator--vertical { width: 1px; margin-block: var(--spacing-1); }
1274
+ .kx-button-group-separator--horizontal { height: 1px; margin-inline: var(--spacing-1); }
1275
+
1276
+ .kx-button-group-text {
1277
+ display: flex;
1278
+ align-items: center;
1279
+ gap: var(--spacing-1);
1280
+ border: var(--border-width-default) solid hsl(var(--input));
1281
+ border-radius: var(--radius-control);
1282
+ background-color: hsl(var(--muted));
1283
+ padding-inline: var(--spacing-3);
1284
+ color: hsl(var(--foreground));
1285
+ font: var(--text-label-md);
1286
+ }
1287
+
1288
+ /* ── circular progress ──────────────────────────────────────────────────── */
1289
+ .kx-circular-progress { position: relative; display: inline-grid; place-items: center; font-family: var(--font-family-sans); }
1290
+ .kx-circular-progress svg { grid-area: 1 / 1; transform: rotate(-90deg); }
1291
+ .kx-circular-progress__track { stroke: hsl(var(--muted)); }
1292
+ .kx-circular-progress__indicator {
1293
+ stroke: hsl(var(--action));
1294
+ transition: stroke-dashoffset var(--duration-base) var(--easing-standard);
1295
+ }
1296
+ .kx-circular-progress__value { grid-area: 1 / 1; font: var(--text-label-sm); color: hsl(var(--foreground)); }
1297
+ /* No value means indeterminate: spin the quarter arc rather than show a bar stuck at zero. */
1298
+ .kx-circular-progress:not([aria-valuenow]) svg { animation: kx-spin 1s linear infinite; }
1299
+ @media (prefers-reduced-motion: reduce) {
1300
+ .kx-circular-progress__indicator { transition: none; }
1301
+ /* 3s, as the spinner: it keeps turning so the page does not read as hung, but no faster than the 3s
1302
+ reduced-motion floor (check:angular-browser, "reduced motion") — it was 2.4s */
1303
+ .kx-circular-progress:not([aria-valuenow]) svg { animation-duration: 3s; }
1304
+ }
1305
+
1306
+ /* ── code block ─────────────────────────────────────────────────────────── */
1307
+ .kx-code-block {
1308
+ display: block;
1309
+ overflow: hidden;
1310
+ border: var(--border-width-default) solid hsl(var(--input));
1311
+ border-radius: var(--radius-md);
1312
+ background-color: hsl(var(--muted));
1313
+ font-family: var(--font-family-sans);
1314
+ }
1315
+ .kx-code-block__header {
1316
+ display: flex;
1317
+ align-items: center;
1318
+ justify-content: space-between;
1319
+ border-block-end: var(--border-width-default) solid hsl(var(--input));
1320
+ background-color: hsl(var(--background));
1321
+ padding-inline: var(--spacing-2);
1322
+ }
1323
+ .kx-code-block__tabs { display: flex; }
1324
+ .kx-code-block__tab {
1325
+ border: 0;
1326
+ border-block-end: 2px solid transparent;
1327
+ background: none;
1328
+ padding: var(--spacing-2) var(--spacing-3);
1329
+ color: hsl(var(--muted-foreground));
1330
+ font: var(--text-body-sm);
1331
+ cursor: pointer;
1332
+ }
1333
+ .kx-code-block__tab:hover { color: hsl(var(--foreground)); }
1334
+ .kx-code-block__tab--active { border-block-end-color: hsl(var(--action)); color: hsl(var(--foreground)); }
1335
+ .kx-code-block__tab:focus-visible { outline: var(--border-width-default) solid hsl(var(--ring)); outline-offset: -2px; }
1336
+ .kx-code-block__copy {
1337
+ border: 0;
1338
+ background: none;
1339
+ border-radius: 2px;
1340
+ padding: var(--spacing-1);
1341
+ color: hsl(var(--muted-foreground));
1342
+ cursor: pointer;
1343
+ }
1344
+ .kx-code-block__copy:hover { color: hsl(var(--foreground)); }
1345
+ .kx-code-block__copy:focus-visible { outline: var(--border-width-default) solid currentColor; }
1346
+ /* The default glyphs, and whatever an application projects in their place (`[kxDismissIcon]`,
1347
+ `[kxCopyIcon]` — an <svg>, or an icon component whose host wraps one). Sized here rather than on the SVG
1348
+ so a projected replacement gets the same box without carrying a size of its own, and so a consumer
1349
+ restyling the package can reach them. Until this rule named the projected slots, only the built-in glyph
1350
+ was sized: a projected <svg> with no width rendered at the browser's default 300×150. */
1351
+ .kx-dismiss-glyph,
1352
+ [kxDismissIcon],
1353
+ [kxDismissIcon] > svg { width: 0.875rem; height: 0.875rem; display: block; flex-shrink: 0; }
1354
+ .kx-copy-glyph,
1355
+ [kxCopyIcon],
1356
+ [kxCopyIcon] > svg { width: 1rem; height: 1rem; display: block; flex-shrink: 0; }
1357
+
1358
+ .kx-code-block__body { position: relative; }
1359
+ /* No filename and no tabs means no header to hold the copy button, so it sits over the code. */
1360
+ .kx-code-block__copy--floating { position: absolute; inset-block-start: var(--spacing-2); inset-inline-end: var(--spacing-2); }
1361
+
1362
+ .kx-code-block__pre {
1363
+ overflow-x: auto;
1364
+ margin: 0;
1365
+ padding: var(--spacing-4);
1366
+ font: var(--text-body-sm);
1367
+ font-family: var(--font-family-mono, ui-monospace, monospace);
1368
+ color: hsl(var(--foreground));
1369
+ }
1370
+ .kx-code-block__pre:focus-visible { outline: var(--border-width-default) solid hsl(var(--ring)); outline-offset: -2px; }
1371
+
1372
+ /* ── description list ───────────────────────────────────────────────────── */
1373
+ .kx-description-list {
1374
+ margin: 0;
1375
+ border: var(--border-width-default) solid hsl(var(--border));
1376
+ border-radius: var(--radius-lg);
1377
+ background-color: hsl(var(--muted) / 0.2);
1378
+ }
1379
+ .kx-description-list__item { display: flex; gap: var(--spacing-3); padding: var(--spacing-2) var(--spacing-3); }
1380
+ .kx-description-list__item:not(:first-child) { border-block-start: var(--border-width-default) solid hsl(var(--border)); }
1381
+ .kx-description-list__item--row { align-items: baseline; }
1382
+ .kx-description-list__item--stacked { flex-direction: column; }
1383
+ .kx-description-list__term {
1384
+ flex-shrink: 0;
1385
+ font-family: var(--font-family-mono, ui-monospace, monospace);
1386
+ font-size: 0.625rem;
1387
+ text-transform: uppercase;
1388
+ letter-spacing: 0.14em;
1389
+ color: hsl(var(--muted-foreground));
1390
+ }
1391
+ .kx-description-list__item--row .kx-description-list__term { width: 6rem; }
1392
+ .kx-description-list__value { margin: 0; min-width: 0; font: var(--text-body-sm); color: hsl(var(--foreground)); }
1393
+
1394
+ /* ── fab ────────────────────────────────────────────────────────────────── */
1395
+ .kx-fab {
1396
+ display: inline-flex;
1397
+ flex-shrink: 0;
1398
+ align-items: center;
1399
+ justify-content: center;
1400
+ gap: var(--spacing-2);
1401
+ border: 0;
1402
+ border-radius: var(--radius-full);
1403
+ box-shadow: var(--shadow-lg);
1404
+ font-family: var(--font-family-sans);
1405
+ font-weight: var(--font-weight-medium);
1406
+ cursor: pointer;
1407
+ transition: background-color var(--duration-fast) var(--easing-standard);
1408
+ }
1409
+ .kx-fab:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
1410
+ .kx-fab:disabled { pointer-events: none; opacity: var(--opacity-disabled); }
1411
+ .kx-fab--default { width: 3.5rem; height: 3.5rem; }
1412
+ .kx-fab--sm { width: 2.75rem; height: 2.75rem; }
1413
+ .kx-fab--extended { width: auto; padding-inline: var(--spacing-5); }
1414
+ .kx-fab--Primary { background-color: hsl(var(--action)); color: hsl(var(--action-foreground)); }
1415
+ .kx-fab--Primary:hover { background-color: hsl(var(--action) / 0.9); }
1416
+ .kx-fab--Secondary { background-color: hsl(var(--secondary)); color: hsl(var(--secondary-foreground)); }
1417
+ .kx-fab--Secondary:hover { background-color: hsl(var(--secondary) / 0.85); }
1418
+ @media (prefers-reduced-motion: reduce) { .kx-fab { transition: none; } }
1419
+
1420
+ /* ── image ──────────────────────────────────────────────────────────────── */
1421
+ .kx-image { position: relative; display: block; inline-size: 100%; overflow: hidden; background-color: hsl(var(--muted)); aspect-ratio: var(--kx-image-ratio, 1); }
1422
+ .kx-image--rounded { border-radius: var(--radius-md); }
1423
+ .kx-image__img {
1424
+ width: 100%;
1425
+ height: 100%;
1426
+ object-fit: cover;
1427
+ opacity: var(--opacity-hidden);
1428
+ transition: opacity var(--duration-base) var(--easing-standard);
1429
+ }
1430
+ .kx-image__img--loaded { opacity: var(--opacity-visible); }
1431
+ .kx-image__fallback { position: absolute; inset: 0; display: grid; place-items: center; color: hsl(var(--muted-foreground)); }
1432
+ @media (prefers-reduced-motion: reduce) { .kx-image__img { transition: none; } }
1433
+
1434
+ /* ── list ───────────────────────────────────────────────────────────────── */
1435
+ .kx-list { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; }
1436
+ .kx-list__item { display: block; }
1437
+ .kx-list__item:not(:first-child) { border-block-start: var(--border-width-default) solid hsl(var(--border)); }
1438
+ .kx-list__row {
1439
+ display: flex;
1440
+ align-items: center;
1441
+ gap: var(--spacing-3);
1442
+ width: 100%;
1443
+ padding: var(--spacing-3);
1444
+ font-family: var(--font-family-sans);
1445
+ text-align: start;
1446
+ }
1447
+ .kx-list__row--pressable {
1448
+ border: 0;
1449
+ background: none;
1450
+ color: inherit;
1451
+ font: inherit;
1452
+ cursor: pointer;
1453
+ }
1454
+ /* The ring, not only the fill: the accent background is under 3:1 against the surface in dark mode, so
1455
+ hover and focus must not rely on it alone to be perceivable. */
1456
+ .kx-list__row--pressable:hover,
1457
+ .kx-list__row--pressable:focus-visible {
1458
+ background-color: hsl(var(--accent));
1459
+ outline: var(--border-width-default) solid hsl(var(--ring));
1460
+ outline-offset: -1px;
1461
+ }
1462
+ .kx-list__row--pressable:disabled { pointer-events: none; }
1463
+ .kx-list__item--disabled { opacity: var(--opacity-disabled); }
1464
+ .kx-list__leading:empty, .kx-list__trailing:empty { display: none; }
1465
+ .kx-list__leading, .kx-list__trailing { display: flex; flex-shrink: 0; align-items: center; gap: var(--spacing-2); }
1466
+ .kx-list__text { display: flex; flex-direction: column; min-width: 0; flex: 1 1 0%; }
1467
+ .kx-list__title { font: var(--text-body-md); color: hsl(var(--foreground)); }
1468
+ .kx-list__description { font: var(--text-body-sm); color: hsl(var(--muted-foreground)); }
1469
+
1470
+ /* ── marquee ────────────────────────────────────────────────────────────── */
1471
+ .kx-marquee { display: flex; overflow: hidden; width: 100%; }
1472
+ .kx-marquee__track { display: flex; flex-shrink: 0; animation: kx-marquee-scroll linear infinite; }
1473
+ .kx-marquee__group { display: flex; flex-shrink: 0; align-items: center; gap: var(--spacing-4); padding-inline-end: var(--spacing-4); }
1474
+ .kx-marquee--pause-on-hover:hover .kx-marquee__track { animation-play-state: paused; }
1475
+ @keyframes kx-marquee-scroll {
1476
+ from { transform: translateX(0); }
1477
+ to { transform: translateX(-50%); }
1478
+ }
1479
+ /* RTL: the same keyframes would scroll the wrong way, so the whole track is mirrored. */
1480
+ [dir='rtl'] .kx-marquee__track { animation-name: kx-marquee-scroll-rtl; }
1481
+ @keyframes kx-marquee-scroll-rtl {
1482
+ from { transform: translateX(0); }
1483
+ to { transform: translateX(50%); }
1484
+ }
1485
+ /* Continuous motion is WCAG 2.2.2's subject. With reduce, the animation stops and the strip stays
1486
+ scrollable, so nothing becomes unreachable. */
1487
+ @media (prefers-reduced-motion: reduce) {
1488
+ .kx-marquee { overflow-x: auto; }
1489
+ .kx-marquee__track { animation: none; }
1490
+ .kx-marquee__group[aria-hidden='true'] { display: none; }
1491
+ }
1492
+
1493
+ /* ── page header ────────────────────────────────────────────────────────── */
1494
+ .kx-page-header {
1495
+ display: flex;
1496
+ flex-direction: column;
1497
+ gap: var(--spacing-4);
1498
+ border-block-end: var(--border-width-default) solid hsl(var(--border));
1499
+ padding-block-end: var(--spacing-6);
1500
+ font-family: var(--font-family-sans);
1501
+ }
1502
+ .kx-page-header__breadcrumb:empty { display: none; }
1503
+ .kx-page-header__breadcrumb { font: var(--text-body-sm); }
1504
+ .kx-page-header__main { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: var(--spacing-4); }
1505
+ .kx-page-header__titles { display: flex; flex-direction: column; gap: var(--spacing-1); min-width: 0; }
1506
+ .kx-page-header__title { font: var(--text-headline-sm); font-weight: var(--font-weight-medium); color: hsl(var(--foreground)); }
1507
+ .kx-page-header__description { margin: 0; font: var(--text-body-md); color: hsl(var(--muted-foreground)); }
1508
+ .kx-page-header__actions:empty { display: none; }
1509
+ .kx-page-header__actions { display: flex; flex-shrink: 0; flex-wrap: wrap; align-items: center; gap: var(--spacing-2); }
1510
+
1511
+ /* ── timeline ───────────────────────────────────────────────────────────── */
1512
+ .kx-timeline { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; }
1513
+ .kx-timeline__item { position: relative; display: flex; gap: var(--spacing-3); padding-block-end: var(--spacing-5); }
1514
+ /* The connecting line sits on the inline start edge, so it mirrors with the marker under dir="rtl". */
1515
+ .kx-timeline__item:not(:last-child)::before {
1516
+ content: '';
1517
+ position: absolute;
1518
+ inset-block: 1.25rem auto;
1519
+ height: calc(100% - 1.25rem);
1520
+ inset-inline-start: 0.4375rem;
1521
+ width: 1px;
1522
+ background-color: hsl(var(--border));
1523
+ }
1524
+ .kx-timeline__marker {
1525
+ position: relative;
1526
+ z-index: 1;
1527
+ flex-shrink: 0;
1528
+ display: grid;
1529
+ place-items: center;
1530
+ width: 0.875rem;
1531
+ height: 0.875rem;
1532
+ margin-block-start: 0.375rem;
1533
+ border-radius: var(--radius-full);
1534
+ border: 2px solid hsl(var(--background));
1535
+ box-sizing: content-box;
1536
+ }
1537
+ .kx-timeline__marker--default { background-color: hsl(var(--muted-foreground)); }
1538
+ .kx-timeline__marker--active { background-color: hsl(var(--action)); }
1539
+ .kx-timeline__marker--success { background-color: hsl(var(--success)); }
1540
+ .kx-timeline__marker--error { background-color: hsl(var(--destructive)); }
1541
+ .kx-timeline__content { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
1542
+ .kx-timeline__title { font: var(--text-body-md); color: hsl(var(--foreground)); }
1543
+ .kx-timeline__description { font: var(--text-body-sm); color: hsl(var(--muted-foreground)); }
1544
+
1545
+ /* ══ navigation and disclosure ═════════════════════════════════════════════
1546
+ accordion · collapsible · breadcrumb · pagination · table of contents
1547
+ tab bar · stepper · navigation bar · app bar · footer
1548
+
1549
+ One state contract for every destination (TOKENS.md, "Navigation and disclosure"):
1550
+ rest muted text, no surface — navigation recedes behind content
1551
+ hover `--foreground` text on the 8% `--foreground` layer the tabs and selection controls use
1552
+ pressed `:active`, the 12% layer: deeper than hover, so a press is seen before the page changes
1553
+ current `--foreground` text AND a shape cue (an indicator bar, an outlined raised surface, or a pill)
1554
+ — never hue alone
1555
+ focus the shared `--shadow-focus` ring, which no other state draws, so focus out-draws hover
1556
+ disabled `--opacity-disabled`, and no answer to the pointer
1557
+ Colour, background and shadow transition over `--duration-fast`; nothing moves or resizes, and under
1558
+ reduced motion nothing transitions (below). Logical properties throughout, so every row and indent
1559
+ mirrors with the direction its own element resolves to.
1560
+ ════════════════════════════════════════════════════════════════════════ */
1561
+
1562
+ /* ── disclosure: the shared expand / collapse ───────────────────────────── */
1563
+ /* A one-row grid whose track goes 0fr → 1fr: the block size interpolates to the content's natural height
1564
+ with no measured height variable (disclosure.ts, "Motion"). Visibility switches on at the start of an
1565
+ expansion and off at the end of a collapse, so closed content is out of the tab order and the
1566
+ accessibility tree, but is seen shrinking rather than vanishing. */
1567
+ .kx-disclosure {
1568
+ display: grid;
1569
+ grid-template-rows: 0fr;
1570
+ visibility: hidden;
1571
+ transition:
1572
+ grid-template-rows var(--duration-fast) var(--easing-exit),
1573
+ visibility 0s linear var(--duration-fast);
1574
+ }
1575
+ .kx-disclosure[data-state='open'] {
1576
+ grid-template-rows: 1fr;
1577
+ visibility: visible;
1578
+ transition:
1579
+ grid-template-rows var(--duration-fast) var(--easing-enter),
1580
+ visibility 0s linear 0s;
1581
+ }
1582
+ /* clip, not hidden: the height must clip while it animates, but a focus ring on content flush with the edge
1583
+ (4px: --shadow-focus's outer spread) must not be cut */
1584
+ .kx-disclosure__body { min-block-size: 0; overflow: clip; overflow-clip-margin: 4px; }
1585
+
1586
+ /* ── accordion ──────────────────────────────────────────────────────────── */
1587
+ /* Items are divided, not boxed: one rule between them, no border around the whole, no shadow. */
1588
+ .kx-accordion { display: block; font-family: var(--font-family-sans); }
1589
+ .kx-accordion__item { display: block; border-block-end: var(--border-width-default) solid hsl(var(--border)); }
1590
+ .kx-accordion__heading { display: flex; margin: 0; }
1591
+ .kx-accordion__trigger {
1592
+ display: flex;
1593
+ flex: 1 1 auto;
1594
+ align-items: center;
1595
+ justify-content: space-between;
1596
+ gap: var(--spacing-3);
1597
+ min-block-size: 2.75rem;
1598
+ margin: 0;
1599
+ padding: var(--spacing-3) var(--spacing-2);
1600
+ border: 0;
1601
+ border-radius: var(--radius-sm);
1602
+ background: transparent;
1603
+ color: hsl(var(--foreground));
1604
+ font: var(--text-label-lg);
1605
+ font-weight: var(--font-weight-medium);
1606
+ text-align: start;
1607
+ cursor: pointer;
1608
+ transition:
1609
+ background-color var(--duration-fast) var(--easing-standard),
1610
+ box-shadow var(--duration-fast) var(--easing-standard);
1611
+ }
1612
+ .kx-accordion__label { min-inline-size: 0; overflow-wrap: break-word; }
1613
+ @media (hover: hover) {
1614
+ .kx-accordion__trigger:enabled:not([aria-disabled='true']):hover { background-color: hsl(var(--foreground) / 0.08); }
1615
+ }
1616
+ .kx-accordion__trigger:enabled:not([aria-disabled='true']):active { background-color: hsl(var(--foreground) / 0.12); }
1617
+ .kx-accordion__trigger:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
1618
+ .kx-accordion__trigger[aria-disabled='true'] { cursor: default; }
1619
+ .kx-accordion__trigger:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; }
1620
+ .kx-accordion__chevron {
1621
+ flex-shrink: 0;
1622
+ inline-size: 1rem;
1623
+ block-size: 1rem;
1624
+ color: hsl(var(--muted-foreground));
1625
+ transition: transform var(--duration-fast) var(--easing-standard);
1626
+ }
1627
+ .kx-accordion__trigger[aria-expanded='true'] .kx-accordion__chevron { transform: rotate(180deg); color: hsl(var(--foreground)); }
1628
+ .kx-accordion__inner {
1629
+ padding: 0 var(--spacing-2) var(--spacing-4);
1630
+ font: var(--text-body-md);
1631
+ color: hsl(var(--muted-foreground));
1632
+ }
1633
+
1634
+ /* ── collapsible ────────────────────────────────────────────────────────── */
1635
+ .kx-collapsible { display: block; }
1636
+ .kx-collapsible__content > .kx-disclosure__body > :first-child { margin-block-start: var(--spacing-2); }
1637
+
1638
+ /* ── breadcrumb ─────────────────────────────────────────────────────────── */
1639
+ .kx-breadcrumb { font-family: var(--font-family-sans); }
1640
+ .kx-breadcrumb__list {
1641
+ display: flex;
1642
+ flex-wrap: wrap;
1643
+ align-items: center;
1644
+ gap: var(--spacing-1) var(--spacing-2);
1645
+ margin: 0;
1646
+ padding: 0;
1647
+ list-style: none;
1648
+ font: var(--text-body-sm);
1649
+ color: hsl(var(--muted-foreground));
1650
+ }
1651
+ .kx-breadcrumb__item { display: inline-flex; align-items: center; min-inline-size: 0; overflow-wrap: break-word; }
1652
+ .kx-breadcrumb__link {
1653
+ border-radius: var(--radius-sm);
1654
+ color: hsl(var(--muted-foreground));
1655
+ text-decoration: underline;
1656
+ text-decoration-color: transparent;
1657
+ text-underline-offset: 3px;
1658
+ transition:
1659
+ color var(--duration-fast) var(--easing-standard),
1660
+ text-decoration-color var(--duration-fast) var(--easing-standard),
1661
+ box-shadow var(--duration-fast) var(--easing-standard);
1662
+ }
1663
+ @media (hover: hover) {
1664
+ .kx-breadcrumb__link:hover { color: hsl(var(--foreground)); text-decoration-color: currentColor; }
1665
+ }
1666
+ .kx-breadcrumb__link:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
1667
+ /* current: full-strength text AND weight — the one crumb that is not a link reads as where you are */
1668
+ .kx-breadcrumb__page { color: hsl(var(--foreground)); font-weight: var(--font-weight-medium); }
1669
+ .kx-breadcrumb__separator { display: inline-flex; font-size: 1.25em; line-height: 1; color: hsl(var(--muted-foreground)); }
1670
+
1671
+ /* ── shared destination: pagination, app bar ────────────────────────────── */
1672
+ .kx-pagination__link,
1673
+ .kx-app-bar__link {
1674
+ display: inline-flex;
1675
+ align-items: center;
1676
+ justify-content: center;
1677
+ gap: var(--spacing-1);
1678
+ min-block-size: 2.25rem;
1679
+ margin: 0;
1680
+ border: var(--border-width-default) solid transparent;
1681
+ border-radius: var(--radius-control);
1682
+ background: transparent;
1683
+ color: hsl(var(--muted-foreground));
1684
+ font: var(--text-label-lg);
1685
+ font-weight: var(--font-weight-medium);
1686
+ text-decoration: none;
1687
+ white-space: nowrap;
1688
+ cursor: pointer;
1689
+ transition:
1690
+ color var(--duration-fast) var(--easing-standard),
1691
+ background-color var(--duration-fast) var(--easing-standard),
1692
+ border-color var(--duration-fast) var(--easing-standard),
1693
+ box-shadow var(--duration-fast) var(--easing-standard);
1694
+ }
1695
+ @media (hover: hover) {
1696
+ :is(.kx-pagination__link, .kx-app-bar__link):not(:disabled, [aria-disabled='true']):hover {
1697
+ color: hsl(var(--foreground));
1698
+ background-color: hsl(var(--foreground) / 0.08);
1699
+ }
1700
+ }
1701
+ :is(.kx-pagination__link, .kx-app-bar__link):not(:disabled, [aria-disabled='true']):active { background-color: hsl(var(--foreground) / 0.12); }
1702
+ :is(.kx-pagination__link, .kx-app-bar__link):focus-visible { outline: none; box-shadow: var(--shadow-focus); }
1703
+ :is(.kx-pagination__link, .kx-app-bar__link):is(:disabled, [aria-disabled='true']) { opacity: var(--opacity-disabled); cursor: not-allowed; }
1704
+
1705
+ /* ── pagination ─────────────────────────────────────────────────────────── */
1706
+ .kx-pagination { display: flex; justify-content: center; inline-size: 100%; font-family: var(--font-family-sans); }
1707
+ .kx-pagination__content { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: var(--spacing-1); margin: 0; padding: 0; list-style: none; }
1708
+ .kx-pagination__item { display: flex; }
1709
+ .kx-pagination__link { min-inline-size: 2.25rem; padding-inline: var(--spacing-2); }
1710
+ .kx-pagination__step { padding-inline: var(--spacing-3); }
1711
+ .kx-pagination__glyph { font-size: 1.25em; line-height: 1; }
1712
+ /* current: an outlined surface — an edge at 3:1 (the field edge, `--muted-foreground` at 80%), a fill and a
1713
+ small lift; not a colour. `--input` (2.2:1 light, 2.7:1 dark) measured under SC 1.4.11's 3:1. */
1714
+ .kx-pagination__link[aria-current='page'] {
1715
+ border-color: hsl(var(--muted-foreground) / 0.8);
1716
+ background-color: hsl(var(--background));
1717
+ color: hsl(var(--foreground));
1718
+ box-shadow: var(--shadow-sm);
1719
+ }
1720
+ .kx-pagination__ellipsis { display: inline-flex; align-items: center; justify-content: center; min-inline-size: 2.25rem; min-block-size: 2.25rem; color: hsl(var(--muted-foreground)); }
1721
+
1722
+ /* ── table of contents ──────────────────────────────────────────────────── */
1723
+ .kx-toc { font-family: var(--font-family-sans); }
1724
+ .kx-toc__list { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; border-inline-start: var(--border-width-default) solid hsl(var(--border)); }
1725
+ .kx-toc__link {
1726
+ display: block;
1727
+ margin-inline-start: -1px;
1728
+ padding-block: var(--spacing-1);
1729
+ /* each level indents 0.75rem further from the inline start; rem, so the indent grows with the text */
1730
+ padding-inline: calc(0.75rem + var(--kx-toc-level, 0) * 0.75rem) var(--spacing-2);
1731
+ border-inline-start: var(--border-width-focus) solid transparent;
1732
+ border-start-end-radius: var(--radius-sm);
1733
+ border-end-end-radius: var(--radius-sm);
1734
+ font: var(--text-body-sm);
1735
+ color: hsl(var(--muted-foreground));
1736
+ text-decoration: none;
1737
+ overflow-wrap: break-word;
1738
+ transition:
1739
+ color var(--duration-fast) var(--easing-standard),
1740
+ background-color var(--duration-fast) var(--easing-standard),
1741
+ border-color var(--duration-fast) var(--easing-standard),
1742
+ box-shadow var(--duration-fast) var(--easing-standard);
1743
+ }
1744
+ @media (hover: hover) {
1745
+ .kx-toc__link:not([aria-current]):hover { color: hsl(var(--foreground)); background-color: hsl(var(--foreground) / 0.08); }
1746
+ }
1747
+ .kx-toc__link:not([aria-current]):active { background-color: hsl(var(--foreground) / 0.12); }
1748
+ .kx-toc__link:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
1749
+ /* current: the 2px `--primary` bar on the inline-start edge, foreground text, medium weight */
1750
+ .kx-toc__link[aria-current] { border-inline-start-color: hsl(var(--primary)); color: hsl(var(--foreground)); font-weight: var(--font-weight-medium); }
1751
+
1752
+ /* ── tab bar ────────────────────────────────────────────────────────────── */
1753
+ .kx-tab-bar {
1754
+ display: flex;
1755
+ align-items: stretch;
1756
+ inline-size: 100%;
1757
+ border-block-start: var(--border-width-default) solid hsl(var(--border));
1758
+ background-color: hsl(var(--background));
1759
+ font-family: var(--font-family-sans);
1760
+ }
1761
+ .kx-tab-bar__item {
1762
+ display: flex;
1763
+ flex: 1 1 0%;
1764
+ min-inline-size: 0;
1765
+ flex-direction: column;
1766
+ align-items: center;
1767
+ justify-content: center;
1768
+ gap: var(--spacing-1);
1769
+ margin: 0;
1770
+ padding: var(--spacing-2) var(--spacing-1);
1771
+ border: 0;
1772
+ border-radius: var(--radius-control);
1773
+ background: transparent;
1774
+ color: hsl(var(--muted-foreground));
1775
+ font: var(--text-label-md);
1776
+ font-weight: var(--font-weight-medium);
1777
+ text-align: center;
1778
+ text-decoration: none;
1779
+ cursor: pointer;
1780
+ transition:
1781
+ color var(--duration-fast) var(--easing-standard),
1782
+ box-shadow var(--duration-fast) var(--easing-standard);
1783
+ }
1784
+ .kx-tab-bar__icon {
1785
+ position: relative;
1786
+ display: inline-flex;
1787
+ align-items: center;
1788
+ justify-content: center;
1789
+ inline-size: 4rem;
1790
+ max-inline-size: 100%;
1791
+ block-size: 2rem;
1792
+ border-radius: var(--radius-full);
1793
+ transition: background-color var(--duration-fast) var(--easing-standard);
1794
+ }
1795
+ .kx-tab-bar__icon > :not(.kx-tab-bar__badge) { inline-size: 1.5rem; block-size: 1.5rem; }
1796
+ .kx-tab-bar__label { max-inline-size: 100%; overflow-wrap: break-word; }
1797
+ .kx-tab-bar__badge {
1798
+ position: absolute;
1799
+ inset-block-start: 0;
1800
+ inset-inline-end: 0.75rem;
1801
+ display: inline-flex;
1802
+ align-items: center;
1803
+ justify-content: center;
1804
+ min-inline-size: 1.125rem;
1805
+ block-size: 1.125rem;
1806
+ padding-inline: 0.25rem;
1807
+ border-radius: var(--radius-full);
1808
+ background-color: hsl(var(--destructive));
1809
+ color: hsl(var(--destructive-foreground));
1810
+ font: var(--text-label-sm);
1811
+ font-weight: var(--font-weight-medium);
1812
+ }
1813
+ @media (hover: hover) {
1814
+ .kx-tab-bar__item:not([aria-current], :disabled, [aria-disabled='true']):hover { color: hsl(var(--foreground)); }
1815
+ .kx-tab-bar__item:not([aria-current], :disabled, [aria-disabled='true']):hover .kx-tab-bar__icon { background-color: hsl(var(--foreground) / 0.08); }
1816
+ }
1817
+ .kx-tab-bar__item:not(:disabled, [aria-disabled='true']):active .kx-tab-bar__icon { background-color: hsl(var(--foreground) / 0.12); }
1818
+ .kx-tab-bar__item:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
1819
+ /* current: full-strength label, an action-coloured icon AND a pill behind it. The label is `--foreground`, not
1820
+ `--action`: in dark, `--action` text (7.9:1) is weaker than the resting `--muted-foreground` (9.0:1), so the
1821
+ current destination would read quieter than the others. */
1822
+ .kx-tab-bar__item[aria-current='page'] { color: hsl(var(--foreground)); }
1823
+ .kx-tab-bar__item[aria-current='page'] .kx-tab-bar__icon { color: hsl(var(--action)); background-color: hsl(var(--action) / 0.14); }
1824
+ .kx-tab-bar__item:is(:disabled, [aria-disabled='true']) { opacity: var(--opacity-disabled); cursor: not-allowed; }
1825
+
1826
+ /* ── stepper ────────────────────────────────────────────────────────────── */
1827
+ .kx-stepper { display: flex; margin: 0; padding: 0; list-style: none; font-family: var(--font-family-sans); }
1828
+ .kx-stepper--horizontal { flex-wrap: wrap; align-items: flex-start; inline-size: 100%; }
1829
+ .kx-stepper--vertical { flex-direction: column; }
1830
+ .kx-stepper__step { display: flex; min-inline-size: 0; }
1831
+ .kx-stepper--horizontal .kx-stepper__step { flex: 1 1 0%; flex-direction: column; align-items: center; gap: var(--spacing-2); text-align: center; }
1832
+ .kx-stepper--vertical .kx-stepper__step { gap: var(--spacing-3); }
1833
+ .kx-stepper__track { display: flex; align-items: center; }
1834
+ .kx-stepper--horizontal .kx-stepper__track { inline-size: 100%; justify-content: center; position: relative; }
1835
+ .kx-stepper--vertical .kx-stepper__track { flex-direction: column; align-self: stretch; }
1836
+ .kx-stepper__marker {
1837
+ position: relative;
1838
+ z-index: 1;
1839
+ display: inline-flex;
1840
+ flex-shrink: 0;
1841
+ align-items: center;
1842
+ justify-content: center;
1843
+ inline-size: 1.75rem;
1844
+ block-size: 1.75rem;
1845
+ border: var(--border-width-default) solid hsl(var(--input));
1846
+ border-radius: var(--radius-full);
1847
+ background-color: hsl(var(--background));
1848
+ color: hsl(var(--muted-foreground));
1849
+ font: var(--text-label-md);
1850
+ font-weight: var(--font-weight-medium);
1851
+ }
1852
+ .kx-stepper__marker > svg { inline-size: 1rem; block-size: 1rem; }
1853
+ /* complete: a filled action disc with a check; current: a 2px action ring around the number */
1854
+ .kx-stepper__step[data-status='complete'] .kx-stepper__marker { border-color: hsl(var(--action)); background-color: hsl(var(--action)); color: hsl(var(--action-foreground)); }
1855
+ .kx-stepper__step[data-status='current'] .kx-stepper__marker { border: var(--border-width-focus) solid hsl(var(--action)); color: hsl(var(--action)); }
1856
+ /* the connector runs from this marker to the next one, on the inline axis or the block axis */
1857
+ .kx-stepper__connector { background-color: hsl(var(--border)); }
1858
+ .kx-stepper__step[data-status='complete'] .kx-stepper__connector { background-color: hsl(var(--action)); }
1859
+ /* Horizontal, each step draws its own halves of the line — marker to its inline-end edge, and (after the first)
1860
+ its inline-start edge to the marker — so nothing reaches into a neighbour's box, and a step's box holds
1861
+ everything it paints, at any text size. The leading half is coloured once this step is reached. */
1862
+ .kx-stepper--horizontal .kx-stepper__connector { position: absolute; inset-inline-start: calc(50% + 0.875rem + var(--spacing-1)); inset-inline-end: 0; inset-block-start: 50%; block-size: 1px; }
1863
+ .kx-stepper--horizontal .kx-stepper__step:not(:first-child) .kx-stepper__track::before {
1864
+ content: '';
1865
+ position: absolute;
1866
+ inset-inline-start: 0;
1867
+ inset-inline-end: calc(50% + 0.875rem + var(--spacing-1));
1868
+ inset-block-start: 50%;
1869
+ block-size: 1px;
1870
+ background-color: hsl(var(--border));
1871
+ }
1872
+ .kx-stepper--horizontal .kx-stepper__step:is([data-status='complete'], [data-status='current']):not(:first-child) .kx-stepper__track::before { background-color: hsl(var(--action)); }
1873
+ .kx-stepper--vertical .kx-stepper__connector { flex: 1 1 auto; inline-size: 1px; min-block-size: var(--spacing-6); margin-block: var(--spacing-1); }
1874
+ .kx-stepper__text { display: flex; flex-direction: column; gap: 2px; min-inline-size: 0; overflow-wrap: break-word; }
1875
+ .kx-stepper--horizontal .kx-stepper__text { padding-inline: var(--spacing-1); }
1876
+ .kx-stepper--vertical .kx-stepper__text { padding-block-end: var(--spacing-6); }
1877
+ .kx-stepper__label { font: var(--text-label-lg); font-weight: var(--font-weight-medium); color: hsl(var(--foreground)); }
1878
+ .kx-stepper__step[data-status='upcoming'] .kx-stepper__label { color: hsl(var(--muted-foreground)); }
1879
+ .kx-stepper__description { font: var(--text-body-sm); color: hsl(var(--muted-foreground)); }
1880
+ /* A horizontal stepper reflows into a list when its own width is under 20em of its own text — a phone, or any
1881
+ width once the reader doubles the text size — instead of breaking its labels mid-word. em, not px, so the
1882
+ switch follows the text size; a container query, so it follows the space the stepper is given. The
1883
+ connectors go: list order and the markers carry the sequence. */
1884
+ .kx-stepper--horizontal { container-type: inline-size; }
1885
+ @container (max-width: 20em) {
1886
+ .kx-stepper--horizontal .kx-stepper__step { flex: 1 1 100%; flex-direction: row; align-items: flex-start; gap: var(--spacing-3); text-align: start; padding-block-end: var(--spacing-3); }
1887
+ .kx-stepper--horizontal .kx-stepper__track { inline-size: auto; }
1888
+ .kx-stepper--horizontal .kx-stepper__connector,
1889
+ .kx-stepper--horizontal .kx-stepper__track::before { display: none; }
1890
+ }
1891
+
1892
+ /* ── navigation bar ─────────────────────────────────────────────────────── */
1893
+ /* Wraps rather than squeezing: when the actions leave the titles less than about five characters (two text
1894
+ actions at 200% text on a phone), the actions move to a second row at the inline end instead of the title
1895
+ being broken one letter per line (found composing it with a sheet and a drawer, Wave C1). */
1896
+ .kx-navigation-bar {
1897
+ display: flex;
1898
+ flex-wrap: wrap;
1899
+ align-items: center;
1900
+ gap: var(--spacing-2);
1901
+ inline-size: 100%;
1902
+ min-block-size: 3.5rem;
1903
+ padding: var(--spacing-2);
1904
+ border-block-end: var(--border-width-default) solid hsl(var(--border));
1905
+ background-color: hsl(var(--background));
1906
+ font-family: var(--font-family-sans);
1907
+ }
1908
+ .kx-navigation-bar__leading { display: flex; flex-shrink: 0; align-items: center; min-inline-size: 2.25rem; }
1909
+ .kx-navigation-bar__leading:empty { min-inline-size: 0; }
1910
+ .kx-navigation-bar__back {
1911
+ display: inline-flex;
1912
+ align-items: center;
1913
+ justify-content: center;
1914
+ inline-size: 2.25rem;
1915
+ block-size: 2.25rem;
1916
+ margin: 0;
1917
+ padding: 0;
1918
+ border: 0;
1919
+ border-radius: var(--radius-full);
1920
+ background: transparent;
1921
+ color: hsl(var(--foreground));
1922
+ cursor: pointer;
1923
+ transition:
1924
+ background-color var(--duration-fast) var(--easing-standard),
1925
+ box-shadow var(--duration-fast) var(--easing-standard);
1926
+ }
1927
+ .kx-navigation-bar__glyph { font-size: 1.75rem; line-height: 1; transform: translateY(-0.06em); }
1928
+ @media (hover: hover) {
1929
+ .kx-navigation-bar__back:hover { background-color: hsl(var(--foreground) / 0.08); }
1930
+ }
1931
+ .kx-navigation-bar__back:active { background-color: hsl(var(--foreground) / 0.12); }
1932
+ .kx-navigation-bar__back:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
1933
+ .kx-navigation-bar__titles { display: flex; flex: 1 1 5em; flex-direction: column; min-inline-size: 0; overflow-wrap: break-word; }
1934
+ .kx-navigation-bar__title { font: var(--text-body-lg); font-weight: var(--font-weight-medium); color: hsl(var(--foreground)); }
1935
+ .kx-navigation-bar__info { font: var(--text-body-sm); color: hsl(var(--muted-foreground)); }
1936
+ .kx-navigation-bar__actions { display: flex; flex-shrink: 0; align-items: center; gap: var(--spacing-1); margin-inline-start: auto; }
1937
+ .kx-navigation-bar__actions:empty { display: none; }
1938
+
1939
+ /* ── app bar ────────────────────────────────────────────────────────────── */
1940
+ /* A grid, so the one <nav> can sit inline between brand and actions on a wide screen and span a row of its
1941
+ own under the bar on a narrow one — without a second copy of the links. The DOM (and so Tab) order is brand,
1942
+ menu button, nav, actions, and the narrow layout follows it: the button ends the first row, the opened links
1943
+ come under it, and the actions take the last row — so focus never moves backwards across the screen. */
1944
+ .kx-app-bar {
1945
+ display: grid;
1946
+ grid-template-columns: minmax(0, 1fr) auto;
1947
+ grid-template-areas: 'brand toggle' 'nav nav' 'actions actions';
1948
+ align-items: center;
1949
+ column-gap: var(--spacing-3);
1950
+ inline-size: 100%;
1951
+ padding-inline: var(--spacing-4);
1952
+ border-block-end: var(--border-width-default) solid hsl(var(--border));
1953
+ background-color: hsl(var(--background));
1954
+ font-family: var(--font-family-sans);
1955
+ }
1956
+ .kx-app-bar--sticky { position: sticky; inset-block-start: 0; z-index: var(--z-index-sticky); }
1957
+ .kx-app-bar__brand {
1958
+ grid-area: brand;
1959
+ display: inline-flex;
1960
+ align-items: center;
1961
+ gap: var(--spacing-2);
1962
+ min-block-size: 3.5rem;
1963
+ min-inline-size: 0;
1964
+ border-radius: var(--radius-sm);
1965
+ color: hsl(var(--foreground));
1966
+ font: var(--text-body-lg);
1967
+ font-weight: var(--font-weight-semibold);
1968
+ text-decoration: none;
1969
+ overflow-wrap: break-word;
1970
+ }
1971
+ a.kx-app-bar__brand:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
1972
+ .kx-app-bar__actions { grid-area: actions; display: flex; flex-wrap: wrap; align-items: center; gap: var(--spacing-2); padding-block-end: var(--spacing-2); }
1973
+ .kx-app-bar__toggle {
1974
+ grid-area: toggle;
1975
+ display: inline-flex;
1976
+ align-items: center;
1977
+ justify-content: center;
1978
+ inline-size: 2.25rem;
1979
+ block-size: 2.25rem;
1980
+ margin: 0;
1981
+ padding: 0;
1982
+ border: 0;
1983
+ border-radius: var(--radius-control);
1984
+ background: transparent;
1985
+ color: hsl(var(--foreground));
1986
+ cursor: pointer;
1987
+ transition:
1988
+ background-color var(--duration-fast) var(--easing-standard),
1989
+ box-shadow var(--duration-fast) var(--easing-standard);
1990
+ }
1991
+ .kx-app-bar__toggle > svg { inline-size: 1.25rem; block-size: 1.25rem; }
1992
+ @media (hover: hover) {
1993
+ .kx-app-bar__toggle:hover { background-color: hsl(var(--foreground) / 0.08); }
1994
+ }
1995
+ .kx-app-bar__toggle:active { background-color: hsl(var(--foreground) / 0.12); }
1996
+ .kx-app-bar__toggle:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
1997
+ .kx-app-bar__toggle[aria-expanded='true'] { background-color: hsl(var(--foreground) / 0.08); }
1998
+ .kx-app-bar__nav { grid-area: nav; }
1999
+ .kx-app-bar__links { display: flex; flex-direction: column; gap: var(--spacing-1); padding-block: var(--spacing-2) var(--spacing-3); }
2000
+ .kx-app-bar__links > .kx-app-bar__link { justify-content: flex-start; padding-inline: var(--spacing-3); white-space: normal; }
2001
+ /* current: foreground text on the accent surface AND a 2px `--primary` indicator flush with the block-end edge:
2002
+ the link's own (otherwise transparent) border plus 1px inset, because an inset shadow alone starts inside the
2003
+ border and would float 1px above the edge */
2004
+ .kx-app-bar__link[aria-current='page'] {
2005
+ color: hsl(var(--foreground));
2006
+ background-color: hsl(var(--accent));
2007
+ border-block-end-color: hsl(var(--primary));
2008
+ box-shadow: inset 0 calc(1px - var(--border-width-focus)) 0 hsl(var(--primary));
2009
+ }
2010
+ .kx-app-bar__link[aria-current='page']:focus-visible { box-shadow: inset 0 calc(1px - var(--border-width-focus)) 0 hsl(var(--primary)), var(--shadow-focus); }
2011
+ @media (min-width: 48rem) {
2012
+ .kx-app-bar { grid-template-columns: auto minmax(0, 1fr) auto; grid-template-areas: 'brand nav actions'; column-gap: var(--spacing-6); }
2013
+ .kx-app-bar__actions { padding-block-end: 0; }
2014
+ .kx-app-bar__toggle { display: none; }
2015
+ /* the disclosure is a narrow-screen behaviour; wide, the links are simply there */
2016
+ .kx-app-bar__nav.kx-disclosure { display: block; visibility: visible; transition: none; }
2017
+ .kx-app-bar__links { flex-direction: row; flex-wrap: wrap; align-items: center; padding-block: var(--spacing-2); }
2018
+ .kx-app-bar__links > .kx-app-bar__link { padding-inline: var(--spacing-3); white-space: nowrap; }
2019
+ }
2020
+
2021
+ /* ── footer ─────────────────────────────────────────────────────────────── */
2022
+ .kx-footer {
2023
+ display: block;
2024
+ inline-size: 100%;
2025
+ padding: var(--spacing-10) var(--spacing-6);
2026
+ border-block-start: var(--border-width-default) solid hsl(var(--border));
2027
+ background-color: hsl(var(--background));
2028
+ font-family: var(--font-family-sans);
2029
+ }
2030
+ .kx-footer__columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 10rem), 1fr)); gap: var(--spacing-8) var(--spacing-6); }
2031
+ .kx-footer__column { display: flex; flex-direction: column; gap: var(--spacing-3); min-inline-size: 0; }
2032
+ .kx-footer__title { font: var(--text-label-lg); font-weight: var(--font-weight-medium); color: hsl(var(--foreground)); }
2033
+ .kx-footer__links { display: flex; flex-direction: column; align-items: flex-start; gap: var(--spacing-2); }
2034
+ .kx-footer__link {
2035
+ border-radius: var(--radius-sm);
2036
+ color: hsl(var(--muted-foreground));
2037
+ font: var(--text-body-sm);
2038
+ text-decoration: underline;
2039
+ text-decoration-color: transparent;
2040
+ text-underline-offset: 3px;
2041
+ overflow-wrap: break-word;
2042
+ transition:
2043
+ color var(--duration-fast) var(--easing-standard),
2044
+ text-decoration-color var(--duration-fast) var(--easing-standard),
2045
+ box-shadow var(--duration-fast) var(--easing-standard);
2046
+ }
2047
+ @media (hover: hover) {
2048
+ .kx-footer__link:hover { color: hsl(var(--foreground)); text-decoration-color: currentColor; }
2049
+ }
2050
+ .kx-footer__link:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
2051
+ .kx-footer__bottom {
2052
+ display: flex;
2053
+ flex-wrap: wrap;
2054
+ align-items: center;
2055
+ justify-content: space-between;
2056
+ gap: var(--spacing-3);
2057
+ margin-block-start: var(--spacing-8);
2058
+ padding-block-start: var(--spacing-6);
2059
+ border-block-start: var(--border-width-default) solid hsl(var(--border));
2060
+ font: var(--text-body-sm);
2061
+ color: hsl(var(--muted-foreground));
2062
+ }
2063
+
2064
+ /* Disclosure and navigation motion carries no information a reduced-motion reader needs: the expanded or
2065
+ collapsed state, the chevron's direction and every state colour are rendered directly. */
2066
+ @media (prefers-reduced-motion: reduce) {
2067
+ .kx-disclosure,
2068
+ .kx-disclosure[data-state='open'],
2069
+ .kx-accordion__trigger,
2070
+ .kx-accordion__chevron,
2071
+ .kx-breadcrumb__link,
2072
+ .kx-pagination__link,
2073
+ .kx-app-bar__link,
2074
+ .kx-app-bar__toggle,
2075
+ .kx-toc__link,
2076
+ .kx-tab-bar__item,
2077
+ .kx-tab-bar__icon,
2078
+ .kx-navigation-bar__back,
2079
+ .kx-footer__link { transition: none; }
2080
+ }
2081
+
2082
+ /* ── overlays: dialog family and floating surfaces ───────────────────────── */
2083
+ /* TOKENS.md, "Overlays". Every surface here renders in the browser's top layer (overlay.ts), so none of them
2084
+ carries a z-index: stacking is the order surfaces opened in. Both box types arrive with user-agent styles
2085
+ (`dialog` and `[popover]` are centred, bordered, painted `Canvas`); each is reset to the token contract.
2086
+
2087
+ Elevation (TOKENS.md, "Elevation contract"): a floating surface is `shadow-lg`, one step above a resting
2088
+ card's `md`, so a popover over a card reads as above it; a modal surface is `shadow-xl` over a scrim.
2089
+
2090
+ Motion: the open state is reached through `@starting-style` and left through `transition-behavior:
2091
+ allow-discrete` on `display` and `overlay`, so a closing surface keeps painting for its exit while it is
2092
+ already closed to the accessibility tree, the keyboard and the pointer. */
2093
+
2094
+ .kx-overlay-root { display: contents; }
2095
+
2096
+ /* modal surfaces: dialog, alert dialog, modal, sheet, drawer */
2097
+ .kx-dialog {
2098
+ position: fixed;
2099
+ margin: auto;
2100
+ padding: var(--spacing-6);
2101
+ inline-size: min(32rem, 100% - 2 * var(--spacing-4));
2102
+ max-inline-size: none;
2103
+ max-block-size: calc(100% - 2 * var(--spacing-4));
2104
+ overflow: auto;
2105
+ overscroll-behavior: contain;
2106
+ container-type: inline-size;
2107
+ border: var(--border-width-default) solid hsl(var(--border));
2108
+ border-radius: var(--radius-surface);
2109
+ background-color: hsl(var(--popover));
2110
+ color: hsl(var(--popover-foreground));
2111
+ box-shadow: var(--shadow-xl);
2112
+ font-family: var(--font-family-sans);
2113
+ outline: none;
2114
+ /* closed, and where opening starts from */
2115
+ opacity: 0;
2116
+ scale: 0.95;
2117
+ transition:
2118
+ opacity var(--duration-instant) var(--easing-exit),
2119
+ scale var(--duration-instant) var(--easing-exit),
2120
+ overlay var(--duration-instant) var(--easing-exit) allow-discrete,
2121
+ display var(--duration-instant) var(--easing-exit) allow-discrete;
2122
+ }
2123
+ .kx-dialog:not([open]) { pointer-events: none; }
2124
+ .kx-dialog[open] {
2125
+ display: flex;
2126
+ flex-direction: column;
2127
+ gap: var(--spacing-4);
2128
+ opacity: 1;
2129
+ scale: 1;
2130
+ transition-duration: var(--duration-fast);
2131
+ transition-timing-function: var(--easing-enter);
2132
+ }
2133
+ @starting-style {
2134
+ .kx-dialog[open] { opacity: 0; scale: 0.95; }
2135
+ }
2136
+ /* The scrim: the `scrim` role (black at 40% in light, 60% in dark) — not `foreground`, which is near-white in
2137
+ dark and would lift the page behind the dialog toward the dialog's own surface instead of setting it back.
2138
+ The role carries its own alpha. The fallback keeps a browser that does not pass custom properties through to
2139
+ `::backdrop` drawing the light scrim rather than nothing. */
2140
+ .kx-dialog::backdrop {
2141
+ background-color: hsl(var(--scrim, 0 0% 0% / 0.4));
2142
+ opacity: 0;
2143
+ transition:
2144
+ opacity var(--duration-instant) var(--easing-exit),
2145
+ overlay var(--duration-instant) var(--easing-exit) allow-discrete,
2146
+ display var(--duration-instant) var(--easing-exit) allow-discrete;
2147
+ }
2148
+ .kx-dialog[open]::backdrop { opacity: 1; transition-duration: var(--duration-fast); transition-timing-function: var(--easing-enter); }
2149
+ @starting-style {
2150
+ .kx-dialog[open]::backdrop { opacity: 0; }
2151
+ }
2152
+
2153
+ .kx-dialog__header { display: flex; flex-direction: column; gap: var(--spacing-2); }
2154
+ .kx-dialog:has(> .kx-dialog__close) .kx-dialog__header { padding-inline-end: var(--spacing-8); }
2155
+ .kx-dialog__title { margin: 0; font: var(--text-title-dialog); color: hsl(var(--popover-foreground)); overflow-wrap: break-word; }
2156
+ .kx-dialog__description { margin: 0; font: var(--text-body-md); color: hsl(var(--muted-foreground)); overflow-wrap: break-word; }
2157
+ /* Actions toward the inline end, in DOM order. In a narrow surface they stack — in the same order, so the
2158
+ order on screen is the order Tab takes (React reverses them visually; this does not). */
2159
+ .kx-dialog__footer {
2160
+ display: flex;
2161
+ flex-wrap: wrap;
2162
+ justify-content: flex-end;
2163
+ gap: var(--spacing-2);
2164
+ margin-block-start: var(--spacing-2);
2165
+ }
2166
+ @container (max-width: 24rem) {
2167
+ .kx-dialog__footer { flex-direction: column; align-items: stretch; }
2168
+ .kx-dialog__footer > .kx-btn { white-space: normal; }
2169
+ }
2170
+
2171
+ /* The built-in close button: last in the DOM, drawn at the inline-end of the top edge. */
2172
+ .kx-dialog__close {
2173
+ position: absolute;
2174
+ inset-block-start: var(--spacing-4);
2175
+ inset-inline-end: var(--spacing-4);
2176
+ display: inline-grid;
2177
+ place-items: center;
2178
+ inline-size: 2rem;
2179
+ block-size: 2rem;
2180
+ padding: 0;
2181
+ border: 0;
2182
+ border-radius: var(--radius-control);
2183
+ background: transparent;
2184
+ color: hsl(var(--muted-foreground));
2185
+ cursor: pointer;
2186
+ transition:
2187
+ background-color var(--duration-fast) var(--easing-standard),
2188
+ color var(--duration-fast) var(--easing-standard),
2189
+ box-shadow var(--duration-fast) var(--easing-standard);
2190
+ }
2191
+ .kx-dialog__close svg { inline-size: 1rem; block-size: 1rem; }
2192
+ @media (hover: hover) {
2193
+ .kx-dialog__close:hover { background-color: hsl(var(--foreground) / 0.08); color: hsl(var(--foreground)); }
2194
+ }
2195
+ .kx-dialog__close:active { background-color: hsl(var(--foreground) / 0.12); }
2196
+ .kx-dialog__close:focus-visible { outline: none; box-shadow: var(--shadow-focus); color: hsl(var(--foreground)); }
2197
+
2198
+ /* modal: header (title + close), divider, body, divider, footer — React's Modal */
2199
+ .kx-modal { padding: 0; gap: 0; inline-size: min(30rem, 100% - 2 * var(--spacing-4)); }
2200
+ .kx-modal[open] { gap: 0; }
2201
+ .kx-modal__header {
2202
+ display: flex;
2203
+ align-items: center;
2204
+ gap: var(--spacing-3);
2205
+ padding: var(--spacing-5) var(--spacing-6) var(--spacing-3);
2206
+ }
2207
+ .kx-modal__title { flex: 1; min-inline-size: 0; }
2208
+ .kx-modal__close { position: static; flex: none; }
2209
+ .kx-modal__body {
2210
+ display: flex;
2211
+ flex-direction: column;
2212
+ gap: var(--spacing-3);
2213
+ padding: var(--spacing-5) var(--spacing-6);
2214
+ border-block: var(--border-width-default) solid hsl(var(--border));
2215
+ }
2216
+ .kx-modal__footer {
2217
+ display: flex;
2218
+ flex-wrap: wrap;
2219
+ justify-content: flex-end;
2220
+ gap: var(--spacing-3);
2221
+ padding: var(--spacing-4) var(--spacing-6);
2222
+ }
2223
+ @container (max-width: 24rem) {
2224
+ .kx-modal__footer { flex-direction: column; align-items: stretch; }
2225
+ .kx-modal__footer > .kx-btn { white-space: normal; }
2226
+ }
2227
+
2228
+ /* sheet: a panel against one edge. `start` / `end` are logical, so the edge is chosen by the direction the
2229
+ sheet's own element resolves to, with no direction selector anywhere. The inline sides slide by animating
2230
+ their own logical margin (out past the edge by exactly their size); top and bottom slide by translation. */
2231
+ .kx-sheet {
2232
+ --kx-sheet-size: min(24rem, 100% - 3rem);
2233
+ margin: 0;
2234
+ inset: auto;
2235
+ max-inline-size: none;
2236
+ max-block-size: none;
2237
+ border-radius: 0;
2238
+ border-width: 0;
2239
+ opacity: 1;
2240
+ scale: none;
2241
+ transition:
2242
+ margin var(--duration-base) var(--easing-exit),
2243
+ translate var(--duration-base) var(--easing-exit),
2244
+ overlay var(--duration-base) var(--easing-exit) allow-discrete,
2245
+ display var(--duration-base) var(--easing-exit) allow-discrete;
2246
+ }
2247
+ .kx-sheet[open] { opacity: 1; scale: none; transition-duration: var(--duration-slow); transition-timing-function: var(--easing-enter); }
2248
+ .kx-sheet:is([data-side='start'], [data-side='end']) {
2249
+ inset-block: 0;
2250
+ block-size: 100%;
2251
+ inline-size: var(--kx-sheet-size);
2252
+ padding-block: max(var(--spacing-6), env(safe-area-inset-top)) max(var(--spacing-6), env(safe-area-inset-bottom));
2253
+ }
2254
+ .kx-sheet[data-side='end'] {
2255
+ inset-inline-end: 0;
2256
+ border-inline-start-width: var(--border-width-default);
2257
+ margin-inline-end: calc(-1 * var(--kx-sheet-size));
2258
+ }
2259
+ .kx-sheet[data-side='start'] {
2260
+ inset-inline-start: 0;
2261
+ border-inline-end-width: var(--border-width-default);
2262
+ margin-inline-start: calc(-1 * var(--kx-sheet-size));
2263
+ }
2264
+ .kx-sheet[data-side='end'][open] { margin-inline-end: 0; }
2265
+ .kx-sheet[data-side='start'][open] { margin-inline-start: 0; }
2266
+ @starting-style {
2267
+ .kx-sheet[data-side='end'][open] { margin-inline-end: calc(-1 * var(--kx-sheet-size)); }
2268
+ .kx-sheet[data-side='start'][open] { margin-inline-start: calc(-1 * var(--kx-sheet-size)); }
2269
+ }
2270
+ .kx-sheet:is([data-side='top'], [data-side='bottom']) {
2271
+ inset-inline: 0;
2272
+ inline-size: 100%;
2273
+ max-block-size: 85dvh;
2274
+ }
2275
+ .kx-sheet[data-side='top'] {
2276
+ inset-block-start: 0;
2277
+ border-block-end-width: var(--border-width-default);
2278
+ translate: 0 -100%;
2279
+ padding-block-start: max(var(--spacing-6), env(safe-area-inset-top));
2280
+ }
2281
+ .kx-sheet[data-side='bottom'] {
2282
+ inset-block-end: 0;
2283
+ border-block-start-width: var(--border-width-default);
2284
+ translate: 0 100%;
2285
+ padding-block-end: max(var(--spacing-6), env(safe-area-inset-bottom));
2286
+ }
2287
+ .kx-sheet:is([data-side='top'], [data-side='bottom'])[open] { translate: 0 0; }
2288
+ @starting-style {
2289
+ .kx-sheet[data-side='top'][open] { translate: 0 -100%; }
2290
+ .kx-sheet[data-side='bottom'][open] { translate: 0 100%; }
2291
+ }
2292
+
2293
+ /* drawer: a bottom sheet with rounded top corners and a grab handle */
2294
+ .kx-drawer {
2295
+ padding-block-start: var(--spacing-3);
2296
+ border-start-start-radius: var(--radius-xxl);
2297
+ border-start-end-radius: var(--radius-xxl);
2298
+ }
2299
+ .kx-drawer__handle {
2300
+ flex: none;
2301
+ align-self: center;
2302
+ inline-size: 6.25rem;
2303
+ block-size: var(--spacing-2);
2304
+ border-radius: var(--radius-full);
2305
+ background-color: hsl(var(--muted-foreground) / 0.4);
2306
+ }
2307
+
2308
+ /* floating surfaces: popover, hover card, tooltip — placed by overlay.ts, which publishes the room each one
2309
+ has as --kx-available-width / --kx-available-height. Larger text grows the surface until it meets that
2310
+ room, and then it scrolls inside itself rather than running off the screen. */
2311
+ .kx-floating {
2312
+ position: fixed;
2313
+ inset: auto;
2314
+ margin: 0;
2315
+ max-inline-size: var(--kx-available-width, calc(100vw - 2 * var(--spacing-2)));
2316
+ max-block-size: var(--kx-available-height, calc(100vh - 2 * var(--spacing-2)));
2317
+ overflow: auto;
2318
+ overscroll-behavior: contain;
2319
+ padding: var(--spacing-4);
2320
+ border: var(--border-width-default) solid hsl(var(--border));
2321
+ border-radius: var(--radius-control);
2322
+ background-color: hsl(var(--popover));
2323
+ color: hsl(var(--popover-foreground));
2324
+ box-shadow: var(--shadow-lg);
2325
+ font: var(--text-body-md);
2326
+ font-family: var(--font-family-sans);
2327
+ overflow-wrap: break-word;
2328
+ outline: none;
2329
+ opacity: 0;
2330
+ translate: var(--kx-from, 0 0);
2331
+ transition:
2332
+ opacity var(--duration-instant) var(--easing-exit),
2333
+ translate var(--duration-instant) var(--easing-exit),
2334
+ overlay var(--duration-instant) var(--easing-exit) allow-discrete,
2335
+ display var(--duration-instant) var(--easing-exit) allow-discrete;
2336
+ }
2337
+ /* where it settles from: 4px back toward its trigger, on the side it actually opened on (after direction and
2338
+ collisions — `data-placement` is that resolved, physical side) */
2339
+ .kx-floating[data-placement='bottom'] { --kx-from: 0 -4px; }
2340
+ .kx-floating[data-placement='top'] { --kx-from: 0 4px; }
2341
+ .kx-floating[data-placement='left'] { --kx-from: 4px 0; }
2342
+ .kx-floating[data-placement='right'] { --kx-from: -4px 0; }
2343
+ .kx-floating:popover-open {
2344
+ opacity: 1;
2345
+ translate: 0 0;
2346
+ transition-duration: var(--duration-fast);
2347
+ transition-timing-function: var(--easing-enter);
2348
+ }
2349
+ @starting-style {
2350
+ .kx-floating:popover-open { opacity: 0; translate: var(--kx-from, 0 0); }
2351
+ }
2352
+ .kx-popover { inline-size: 18rem; }
2353
+ .kx-hover-card { inline-size: 16rem; }
2354
+ /* content stacks with the token gap; the surface owns the spacing, so its children carry no margins of their own */
2355
+ .kx-popover:popover-open,
2356
+ .kx-hover-card:popover-open { display: flex; flex-direction: column; gap: var(--spacing-2); }
2357
+ .kx-popover > *,
2358
+ .kx-hover-card > * { margin: 0; }
2359
+ /* The tooltip is the inverse surface (`action`), small and quick: it labels, it does not hold content. Its
2360
+ transparent edge costs nothing here and becomes a visible outline under a forced palette, where its fill is
2361
+ flattened to Canvas and the edge is the only thing that says where it is. */
2362
+ .kx-tooltip {
2363
+ inline-size: max-content;
2364
+ max-inline-size: min(20rem, var(--kx-available-width, 20rem));
2365
+ padding: var(--spacing-1) var(--spacing-3);
2366
+ border-color: transparent;
2367
+ background-color: hsl(var(--action));
2368
+ color: hsl(var(--action-foreground));
2369
+ box-shadow: none;
2370
+ font: var(--text-body-sm);
2371
+ font-family: var(--font-family-sans);
2372
+ }
2373
+ .kx-tooltip:popover-open { transition-duration: var(--duration-instant); }
2374
+
2375
+ /* Overlay motion tells the reader where a surface came from; under reduced motion it simply appears and
2376
+ disappears, in the same open and closed states (the discrete `display` / `overlay` hand-off goes with it). */
2377
+ @media (prefers-reduced-motion: reduce) {
2378
+ .kx-dialog,
2379
+ .kx-dialog::backdrop,
2380
+ .kx-dialog__close,
2381
+ .kx-floating { transition: none; }
2382
+ }
2383
+
2384
+ /* ── forced colours ─────────────────────────────────────────────────────── */
2385
+ /* Every focus ring above is a box-shadow, and a forced palette (Windows contrast themes, `forced-colors:
2386
+ active`) removes box-shadow entirely. With one on, the keyboard had no visible position on any control
2387
+ (check:angular-browser, "forced colours": every button, field, tab, toggle and selection control). An
2388
+ outline survives the mode and is painted in a system colour, so it is the indicator here; nothing changes
2389
+ outside it. Parts clipped by their field's `overflow: hidden` (composite inputs, the reveal toggle) draw it
2390
+ inset, and the visually hidden radios of a segment or single-select toggle group draw it on their label. */
2391
+ @media (forced-colors: active) {
2392
+ :is(
2393
+ .kx-btn, .kx-fab, .kx-input, .kx-textarea, .kx-native-select, .kx-checkbox, .kx-switch, .kx-radio__input,
2394
+ .kx-slider__input, .kx-tab, .kx-tab-panel, .kx-toggle, .kx-tag__remove, .kx-banner [kxBannerAction],
2395
+ .kx-inform [kxInformAction], a.kx-card, button.kx-card, .kx-accordion__trigger, .kx-breadcrumb__link,
2396
+ .kx-pagination__link, .kx-toc__link, .kx-tab-bar__item, .kx-navigation-bar__back, .kx-app-bar__toggle,
2397
+ .kx-app-bar__link, a.kx-app-bar__brand, .kx-footer__link, .kx-dialog__close
2398
+ ):focus-visible,
2399
+ :is(.kx-segmented__radio, .kx-toggle-group__radio):focus-visible + label,
2400
+ .kx-rating__item:has(.kx-rating__radio:focus-visible),
2401
+ .kx-input-otp:has(.kx-input-otp__input:focus-visible) .kx-input-otp__slot[data-active] {
2402
+ outline: 2px solid Highlight;
2403
+ outline-offset: 2px;
2404
+ }
2405
+ :is(.kx-number-input__input, .kx-password-input__input, .kx-password-input__reveal, .kx-input-group__input, .kx-input-group__button):focus-visible {
2406
+ outline: 2px solid Highlight;
2407
+ outline-offset: -2px;
2408
+ }
2409
+ /* a current destination drawn with a background or an inset shadow would lose its cue to the forced palette;
2410
+ an outline in a system colour keeps "you are here" visible */
2411
+ :is(.kx-app-bar__link, .kx-pagination__link)[aria-current='page'],
2412
+ .kx-tab-bar__item[aria-current='page'] .kx-tab-bar__icon {
2413
+ outline: 2px solid CanvasText;
2414
+ outline-offset: -2px;
2415
+ }
2416
+ /* the slider's track and fill are plain elements, whose backgrounds the mode would flatten to Canvas */
2417
+ .kx-slider__track { forced-color-adjust: none; background-color: GrayText; }
2418
+ .kx-slider__fill { background-color: Highlight; }
2419
+ }