@kinetixui/angular 0.24.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 ADDED
@@ -0,0 +1,782 @@
1
+ /**
2
+ * @kinetixui/angular — component styles.
3
+ *
4
+ * Every declaration here resolves to a custom property generated by `pnpm build:tokens` from the DTCG source
5
+ * in `tokens/`. There is deliberately no Angular-specific token system and no copied hex value: this file only
6
+ * spends tokens, it never defines one. Load the contract first, then this:
7
+ *
8
+ * @import "@kinetixui/tokens/css";
9
+ * @import "@kinetixui/tokens/css/extras";
10
+ * @import "@kinetixui/angular/styles.css";
11
+ *
12
+ * Both token sheets, not just the first. `globals.css` carries colour, spacing and radius; the elevation and
13
+ * typography scales this file spends — `--shadow-focus`, `--shadow-sm`, `--text-body-md` and the rest of the
14
+ * `--text-*` ramp — live in `extras.css`. Importing only `globals.css` leaves focus rings and every type size
15
+ * undefined, which renders as components that look almost right, so the external-consumer test in
16
+ * `scripts/angular-package/` asserts the documented imports are the ones that actually work.
17
+ *
18
+ * `@kinetixui/tokens` is a peer dependency for this reason: the custom properties are a single global
19
+ * contract, and a second copy of it in the tree would be waste at best and a version skew at worst.
20
+ *
21
+ * Colour tokens are HSL channel triplets ("221 83% 53%"), so they are always used as hsl(var(--token)) — the
22
+ * same convention the React package and the registry's globals.css use, which is what lets `/` alpha work.
23
+ *
24
+ * Why plain CSS rather than Tailwind: @kinetixui/ui is a Tailwind consumer, but an Angular application should
25
+ * not be forced into Tailwind to use a button. The class names are stable public API, documented alongside the
26
+ * components, so an application can restyle or extend them.
27
+ *
28
+ * Dark mode is the same `.dark` class contract as the rest of KinetixUI — nothing here switches on a theme; the
29
+ * tokens do, which is the entire point of the architecture.
30
+ */
31
+
32
+ /* ── button ─────────────────────────────────────────────────────────────── */
33
+ .kx-btn {
34
+ display: inline-flex;
35
+ align-items: center;
36
+ justify-content: center;
37
+ gap: var(--spacing-2);
38
+ border-radius: var(--radius-control);
39
+ border: var(--border-width-default) solid transparent;
40
+ font-family: var(--font-family-sans);
41
+ font-weight: var(--font-weight-medium);
42
+ white-space: nowrap;
43
+ cursor: pointer;
44
+ text-decoration: none;
45
+ transition:
46
+ background-color var(--duration-fast) var(--easing-standard),
47
+ color var(--duration-fast) var(--easing-standard),
48
+ border-color var(--duration-fast) var(--easing-standard),
49
+ box-shadow var(--duration-fast) var(--easing-standard);
50
+ }
51
+ .kx-btn:focus-visible {
52
+ outline: none;
53
+ box-shadow: var(--shadow-focus);
54
+ }
55
+ /* :disabled covers <button>; [aria-disabled] covers the anchor form, which cannot be disabled */
56
+ .kx-btn:disabled,
57
+ .kx-btn[aria-disabled='true'] {
58
+ pointer-events: none;
59
+ opacity: var(--opacity-disabled);
60
+ }
61
+ .kx-btn > svg {
62
+ pointer-events: none;
63
+ flex-shrink: 0;
64
+ inline-size: 18px;
65
+ block-size: 18px;
66
+ }
67
+
68
+ /* sizes — padding from the spacing scale, type from the label scale (matches the React matrix) */
69
+ .kx-btn--sm { padding: var(--spacing-2) var(--spacing-3); font: var(--text-label-sm); }
70
+ .kx-btn--md { padding: var(--spacing-3) var(--spacing-4); font: var(--text-label-md); }
71
+ .kx-btn--lg { padding: var(--spacing-3) var(--spacing-6); font: var(--text-label-lg); }
72
+ .kx-btn--icon { padding: var(--spacing-3); font: var(--text-label-md); }
73
+
74
+ /* corners */
75
+ .kx-btn--sharp { border-radius: var(--radius-none); }
76
+ .kx-btn--pill { border-radius: var(--radius-full); }
77
+
78
+ /*
79
+ * Variant fills. The hover/active alphas are bounded by contrast, not taste — text on the dimmed fill has to
80
+ * stay at or above 4.5:1, which is why Primary steps 90%/85% and Secondary swaps fill and text instead of
81
+ * dimming (90% measured 3.97:1). `pnpm check:contrast` enforces the same pairs for the React package.
82
+ */
83
+ .kx-btn--primary {
84
+ background-color: hsl(var(--action));
85
+ color: hsl(var(--action-foreground));
86
+ }
87
+ .kx-btn--primary:hover { background-color: hsl(var(--action) / 0.9); }
88
+ .kx-btn--primary:active { background-color: hsl(var(--action) / 0.85); }
89
+
90
+ .kx-btn--secondary {
91
+ background-color: hsl(var(--secondary));
92
+ color: hsl(var(--secondary-foreground));
93
+ }
94
+ .kx-btn--secondary:hover,
95
+ .kx-btn--secondary:active {
96
+ background-color: hsl(var(--secondary-foreground));
97
+ color: hsl(var(--secondary));
98
+ }
99
+
100
+ .kx-btn--outline {
101
+ border-color: hsl(var(--input));
102
+ background-color: transparent;
103
+ color: hsl(var(--foreground));
104
+ }
105
+ /* the edge strengthens to --ring on hover so the state stays visible on the darkest surfaces (WCAG 2.2 SC 1.4.11) */
106
+ .kx-btn--outline:hover { border-color: hsl(var(--ring)); background-color: hsl(var(--accent)); }
107
+ .kx-btn--outline:active { border-color: hsl(var(--action)); background-color: hsl(var(--accent)); color: hsl(var(--action)); }
108
+
109
+ .kx-btn--destructive {
110
+ background-color: hsl(var(--destructive));
111
+ color: hsl(var(--destructive-foreground));
112
+ }
113
+ .kx-btn--destructive:hover { filter: brightness(0.95); }
114
+ .kx-btn--destructive:active { filter: brightness(0.9); }
115
+ .kx-btn--destructive:focus-visible { box-shadow: var(--shadow-focus-destructive); }
116
+
117
+ /* the accent fill alone is under 3:1 on dark, so hover also draws an inset --ring outline */
118
+ .kx-btn--ghost { background-color: transparent; color: hsl(var(--foreground)); }
119
+ .kx-btn--ghost:hover { background-color: hsl(var(--accent)); box-shadow: inset 0 0 0 1px hsl(var(--ring)); }
120
+ .kx-btn--ghost:active { background-color: hsl(var(--accent)); color: hsl(var(--action)); box-shadow: none; }
121
+
122
+ .kx-btn--link {
123
+ background-color: transparent;
124
+ color: hsl(var(--link));
125
+ border-radius: var(--radius-none);
126
+ padding-inline: 0;
127
+ text-underline-offset: 4px;
128
+ }
129
+ .kx-btn--link:hover { text-decoration: underline; color: hsl(var(--foreground)); }
130
+
131
+ /* ── badge ──────────────────────────────────────────────────────────────── */
132
+ .kx-badge {
133
+ display: inline-flex;
134
+ align-items: center;
135
+ gap: var(--spacing-1);
136
+ border: var(--border-width-default) solid transparent;
137
+ border-radius: var(--radius-full);
138
+ padding: 2px 10px;
139
+ font: var(--text-label-sm);
140
+ font-weight: var(--font-weight-medium);
141
+ white-space: nowrap;
142
+ }
143
+ .kx-badge--default { background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground)); }
144
+ .kx-badge--secondary { background-color: hsl(var(--secondary)); color: hsl(var(--secondary-foreground)); }
145
+ .kx-badge--outline { color: hsl(var(--foreground)); border-color: hsl(var(--border)); }
146
+ .kx-badge--destructive { background-color: hsl(var(--destructive)); color: hsl(var(--destructive-foreground)); }
147
+ .kx-badge--success { background-color: hsl(var(--success)); color: hsl(var(--success-foreground)); }
148
+ .kx-badge--warning { background-color: hsl(var(--warning)); color: hsl(var(--warning-foreground)); }
149
+
150
+ /* ── card ───────────────────────────────────────────────────────────────── */
151
+ .kx-card {
152
+ display: block;
153
+ background-color: hsl(var(--card));
154
+ color: hsl(var(--card-foreground));
155
+ border: var(--border-width-default) solid hsl(var(--border));
156
+ border-radius: var(--radius-container);
157
+ box-shadow: var(--shadow-sm);
158
+ }
159
+ .kx-card-header { display: flex; flex-direction: column; gap: var(--spacing-1); padding: var(--spacing-6); }
160
+ .kx-card-title { font: var(--text-title-md); font-weight: var(--font-weight-semibold); margin: 0; }
161
+ .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); }
163
+ .kx-card-footer { display: flex; align-items: center; gap: var(--spacing-2); padding: 0 var(--spacing-6) var(--spacing-6); }
164
+
165
+ /* ── label ──────────────────────────────────────────────────────────────── */
166
+ .kx-label {
167
+ display: inline-block;
168
+ font: var(--text-label-md);
169
+ font-weight: var(--font-weight-medium);
170
+ color: hsl(var(--foreground));
171
+ }
172
+ /* the control is disabled, not the label — so the label follows via the group, never its own attribute */
173
+ .kx-label[data-disabled='true'] { opacity: var(--opacity-disabled); cursor: not-allowed; }
174
+
175
+ /* ── input / textarea ───────────────────────────────────────────────────── */
176
+ .kx-input {
177
+ display: block;
178
+ inline-size: 100%;
179
+ min-block-size: var(--interaction-target-minimum);
180
+ padding: var(--spacing-2) var(--spacing-3);
181
+ background-color: hsl(var(--background));
182
+ color: hsl(var(--foreground));
183
+ border: var(--border-width-default) solid hsl(var(--input));
184
+ border-radius: var(--radius-field);
185
+ font: var(--text-body-md);
186
+ font-family: var(--font-family-sans);
187
+ transition: border-color var(--duration-fast) var(--easing-standard), box-shadow var(--duration-fast) var(--easing-standard);
188
+ }
189
+ .kx-input::placeholder { color: hsl(var(--muted-foreground)); }
190
+ .kx-input:focus-visible { outline: none; border-color: hsl(var(--ring)); box-shadow: var(--shadow-focus); }
191
+ .kx-input:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; }
192
+ /* aria-invalid is the source of truth for the error state — the styling follows the semantics, not the reverse */
193
+ .kx-input[aria-invalid='true'] { border-color: hsl(var(--destructive)); }
194
+ .kx-input[aria-invalid='true']:focus-visible { box-shadow: var(--shadow-focus-destructive); }
195
+
196
+ /* ── checkbox / switch ──────────────────────────────────────────────────── */
197
+ .kx-checkbox {
198
+ display: inline-grid;
199
+ place-items: center;
200
+ inline-size: 18px;
201
+ block-size: 18px;
202
+ flex-shrink: 0;
203
+ padding: 0;
204
+ background-color: hsl(var(--background));
205
+ color: hsl(var(--primary-foreground));
206
+ border: var(--border-width-default) solid hsl(var(--input));
207
+ border-radius: var(--radius-sm);
208
+ cursor: pointer;
209
+ transition: background-color var(--duration-fast) var(--easing-standard), border-color var(--duration-fast) var(--easing-standard);
210
+ }
211
+ .kx-checkbox:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
212
+ .kx-checkbox[aria-checked='true'],
213
+ .kx-checkbox[aria-checked='mixed'] { background-color: hsl(var(--primary)); border-color: hsl(var(--primary)); }
214
+ .kx-checkbox:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; }
215
+ .kx-checkbox > svg { inline-size: 14px; block-size: 14px; pointer-events: none; }
216
+
217
+ .kx-switch {
218
+ display: inline-flex;
219
+ align-items: center;
220
+ inline-size: 44px;
221
+ block-size: 24px;
222
+ flex-shrink: 0;
223
+ padding: 2px;
224
+ background-color: hsl(var(--tertiary));
225
+ border: var(--border-width-default) solid transparent;
226
+ border-radius: var(--radius-full);
227
+ cursor: pointer;
228
+ transition: background-color var(--duration-base) var(--easing-standard);
229
+ }
230
+ .kx-switch:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
231
+ .kx-switch[aria-checked='true'] { background-color: hsl(var(--primary)); }
232
+ .kx-switch:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; }
233
+ .kx-switch__thumb {
234
+ inline-size: 18px;
235
+ block-size: 18px;
236
+ border-radius: var(--radius-full);
237
+ background-color: hsl(var(--static-white));
238
+ box-shadow: var(--shadow-sm);
239
+ transition: translate var(--duration-base) var(--easing-standard);
240
+ }
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; }
244
+
245
+ /* ── separator ──────────────────────────────────────────────────────────── */
246
+ .kx-separator { display: block; flex-shrink: 0; background-color: hsl(var(--border)); border: 0; margin: 0; }
247
+ .kx-separator--horizontal { block-size: 1px; inline-size: 100%; }
248
+ .kx-separator--vertical { inline-size: 1px; align-self: stretch; }
249
+
250
+ /* ── progress ───────────────────────────────────────────────────────────── */
251
+ .kx-progress {
252
+ display: block;
253
+ overflow: hidden;
254
+ block-size: var(--spacing-2);
255
+ inline-size: 100%;
256
+ background-color: hsl(var(--tertiary));
257
+ border-radius: var(--radius-full);
258
+ }
259
+ .kx-progress__bar {
260
+ block-size: 100%;
261
+ background-color: hsl(var(--primary));
262
+ border-radius: inherit;
263
+ transition: inline-size var(--duration-base) var(--easing-standard);
264
+ }
265
+ /* an indeterminate bar animates; a reduced-motion user gets a static filled track instead */
266
+ @media (prefers-reduced-motion: reduce) {
267
+ .kx-progress__bar { transition: none; }
268
+ }
269
+
270
+ /* ── alert ──────────────────────────────────────────────────────────────── */
271
+ .kx-alert {
272
+ display: grid;
273
+ grid-template-columns: auto 1fr;
274
+ gap: var(--spacing-1) var(--spacing-3);
275
+ padding: var(--spacing-4);
276
+ border: var(--border-width-default) solid hsl(var(--border));
277
+ border-radius: var(--radius-container);
278
+ background-color: hsl(var(--background));
279
+ color: hsl(var(--foreground));
280
+ }
281
+ .kx-alert:not(:has(.kx-alert__icon)) { grid-template-columns: 1fr; }
282
+ .kx-alert__icon { grid-row: span 2; inline-size: 18px; block-size: 18px; }
283
+ .kx-alert-title { font: var(--text-label-lg); font-weight: var(--font-weight-semibold); margin: 0; }
284
+ .kx-alert-description { font: var(--text-body-sm); color: hsl(var(--muted-foreground)); margin: 0; }
285
+ .kx-alert--destructive { border-color: hsl(var(--destructive)); color: hsl(var(--destructive)); }
286
+ .kx-alert--destructive .kx-alert-description { color: hsl(var(--destructive)); }
287
+ .kx-alert--success { border-color: hsl(var(--success)); }
288
+ .kx-alert--warning { border-color: hsl(var(--warning)); }
289
+ .kx-alert--info { border-color: hsl(var(--info)); }
290
+
291
+ /* ── tabs ───────────────────────────────────────────────────────────────── */
292
+ .kx-tabs { display: block; }
293
+ .kx-tab-list {
294
+ display: inline-flex;
295
+ align-items: center;
296
+ gap: var(--spacing-1);
297
+ border-block-end: var(--border-width-default) solid hsl(var(--border));
298
+ }
299
+ .kx-tab {
300
+ appearance: none;
301
+ background: transparent;
302
+ border: 0;
303
+ border-block-end: var(--border-width-focus) solid transparent;
304
+ margin-block-end: -1px;
305
+ padding: var(--spacing-2) var(--spacing-3);
306
+ font: var(--text-label-md);
307
+ font-weight: var(--font-weight-medium);
308
+ color: hsl(var(--muted-foreground));
309
+ cursor: pointer;
310
+ transition: color var(--duration-fast) var(--easing-standard), border-color var(--duration-fast) var(--easing-standard);
311
+ }
312
+ .kx-tab:hover { color: hsl(var(--foreground)); }
313
+ .kx-tab:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
314
+ .kx-tab[aria-selected='true'] { color: hsl(var(--foreground)); border-block-end-color: hsl(var(--primary)); }
315
+ .kx-tab:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; }
316
+ .kx-tab-panel { display: block; padding-block-start: var(--spacing-4); }
317
+ .kx-tab-panel:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
318
+
319
+ /* ── utility: visually hidden ───────────────────────────────────────────── */
320
+ /* Announced, never seen. Used by the spinner's label and by the avatar-group overflow marker, whose visible
321
+ "+4" is decorative punctuation and would otherwise be read as "plus four". */
322
+ .kx-sr-only {
323
+ position: absolute;
324
+ inline-size: 1px;
325
+ block-size: 1px;
326
+ padding: 0;
327
+ margin: -1px;
328
+ overflow: hidden;
329
+ clip-path: inset(50%);
330
+ white-space: nowrap;
331
+ border: 0;
332
+ }
333
+
334
+ /* ── aspect ratio ───────────────────────────────────────────────────────── */
335
+ .kx-aspect-ratio { display: block; inline-size: 100%; overflow: hidden; aspect-ratio: var(--kx-aspect-ratio, 1); }
336
+ .kx-aspect-ratio > * { inline-size: 100%; block-size: 100%; object-fit: cover; }
337
+
338
+ /* ── avatar ─────────────────────────────────────────────────────────────── */
339
+ .kx-avatar {
340
+ position: relative;
341
+ display: inline-flex;
342
+ align-items: center;
343
+ justify-content: center;
344
+ flex-shrink: 0;
345
+ inline-size: var(--spacing-10);
346
+ block-size: var(--spacing-10);
347
+ overflow: hidden;
348
+ border-radius: var(--radius-full);
349
+ background-color: hsl(var(--muted));
350
+ }
351
+ .kx-avatar__image { inline-size: 100%; block-size: 100%; object-fit: cover; aspect-ratio: 1; }
352
+ .kx-avatar__fallback {
353
+ display: flex;
354
+ align-items: center;
355
+ justify-content: center;
356
+ inline-size: 100%;
357
+ block-size: 100%;
358
+ font: var(--text-label-md);
359
+ color: hsl(var(--muted-foreground));
360
+ }
361
+ .kx-avatar--collapsed { display: none; }
362
+
363
+ .kx-avatar-group { display: flex; align-items: center; }
364
+ /* the ring is the page background, so the stack reads as separate discs on any surface */
365
+ .kx-avatar-group > .kx-avatar,
366
+ .kx-avatar-group .kx-avatar-group__overflow {
367
+ box-shadow: 0 0 0 2px hsl(var(--background));
368
+ }
369
+ .kx-avatar-group > .kx-avatar + .kx-avatar,
370
+ .kx-avatar-group__overflow { margin-inline-start: calc(var(--spacing-2) * -1); }
371
+
372
+ /* ── keyboard keys ──────────────────────────────────────────────────────── */
373
+ .kx-kbd {
374
+ display: inline-flex;
375
+ align-items: center;
376
+ justify-content: center;
377
+ min-inline-size: var(--spacing-5);
378
+ block-size: var(--spacing-5);
379
+ padding-inline: var(--spacing-1);
380
+ border: var(--border-width-default) solid hsl(var(--input));
381
+ border-radius: var(--radius-sm);
382
+ background-color: hsl(var(--muted));
383
+ font: var(--text-label-sm);
384
+ font-family: var(--font-family-sans);
385
+ color: hsl(var(--muted-foreground));
386
+ }
387
+ .kx-kbd-group { display: inline-flex; align-items: center; gap: var(--spacing-1); }
388
+
389
+ /* ── skeleton ───────────────────────────────────────────────────────────── */
390
+ .kx-skeleton {
391
+ display: block;
392
+ inline-size: 100%;
393
+ block-size: var(--spacing-4);
394
+ border-radius: var(--radius-sm);
395
+ background-color: hsl(var(--muted));
396
+ animation: kx-skeleton-pulse 1.6s var(--easing-standard) infinite;
397
+ }
398
+ @keyframes kx-skeleton-pulse {
399
+ 50% { opacity: var(--opacity-muted); }
400
+ }
401
+
402
+ /* ── spinner ────────────────────────────────────────────────────────────── */
403
+ .kx-spinner {
404
+ display: inline-block;
405
+ border: 2px solid currentColor;
406
+ border-block-start-color: transparent;
407
+ border-radius: var(--radius-full);
408
+ animation: kx-spin 0.7s var(--easing-linear) infinite;
409
+ }
410
+ .kx-spinner--sm { inline-size: var(--spacing-4); block-size: var(--spacing-4); }
411
+ .kx-spinner--md { inline-size: var(--spacing-6); block-size: var(--spacing-6); }
412
+ .kx-spinner--lg { inline-size: var(--spacing-8); block-size: var(--spacing-8); }
413
+ .kx-spinner--default { color: hsl(var(--action)); }
414
+ .kx-spinner--muted { color: hsl(var(--muted-foreground)); }
415
+ .kx-spinner--onColor { color: hsl(var(--action-foreground)); }
416
+ @keyframes kx-spin {
417
+ to { transform: rotate(360deg); }
418
+ }
419
+
420
+ /* ── tag ────────────────────────────────────────────────────────────────── */
421
+ .kx-tag {
422
+ display: inline-flex;
423
+ align-items: center;
424
+ gap: var(--spacing-1);
425
+ padding: var(--spacing-1) var(--spacing-2);
426
+ border: var(--border-width-default) solid transparent;
427
+ border-radius: var(--radius-sm);
428
+ font: var(--text-label-md);
429
+ font-family: var(--font-family-sans);
430
+ }
431
+ .kx-tag--default { background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground)); }
432
+ .kx-tag--secondary { background-color: hsl(var(--secondary)); color: hsl(var(--secondary-foreground)); }
433
+ .kx-tag--destructive { background-color: hsl(var(--destructive-foreground)); color: hsl(var(--destructive)); }
434
+ .kx-tag--warning { background-color: hsl(var(--warning-foreground)); color: hsl(var(--warning)); }
435
+ .kx-tag--outline { border-color: hsl(var(--input)); color: hsl(var(--foreground)); }
436
+ .kx-tag__remove {
437
+ display: inline-flex;
438
+ appearance: none;
439
+ background: transparent;
440
+ border: 0;
441
+ padding: 0;
442
+ color: inherit;
443
+ cursor: pointer;
444
+ opacity: var(--opacity-muted);
445
+ border-radius: var(--radius-sm);
446
+ transition: opacity var(--duration-fast) var(--easing-standard);
447
+ }
448
+ .kx-tag__remove:hover { opacity: var(--opacity-visible); }
449
+ .kx-tag__remove:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
450
+ .kx-tag__remove > svg { inline-size: 14px; block-size: 14px; }
451
+
452
+ /* ── quote ──────────────────────────────────────────────────────────────── */
453
+ .kx-quote { display: flex; flex-direction: column; gap: var(--spacing-4); margin: 0; font-family: var(--font-family-sans); }
454
+ .kx-quote__body { margin: 0; font: var(--text-title-md); font-weight: var(--font-weight-medium); color: hsl(var(--foreground)); }
455
+ .kx-quote__caption { display: flex; align-items: center; gap: var(--spacing-3); }
456
+ .kx-quote__author { display: block; font: var(--text-body-md); font-weight: var(--font-weight-medium); color: hsl(var(--foreground)); }
457
+ .kx-quote__author-title { display: block; font: var(--text-body-sm); color: hsl(var(--muted-foreground)); }
458
+
459
+ /* ── metric ─────────────────────────────────────────────────────────────── */
460
+ .kx-metric {
461
+ display: flex;
462
+ flex-direction: column;
463
+ gap: var(--spacing-2);
464
+ padding: var(--spacing-4);
465
+ border: var(--border-width-default) solid hsl(var(--input));
466
+ border-radius: var(--radius-md);
467
+ font-family: var(--font-family-sans);
468
+ }
469
+ .kx-metric__head { display: flex; align-items: center; justify-content: space-between; }
470
+ .kx-metric__label { margin: 0; font: var(--text-body-sm); color: hsl(var(--muted-foreground)); }
471
+ .kx-metric__body { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--spacing-2); }
472
+ .kx-metric__value { margin: 0; font: var(--text-headline-sm); font-weight: var(--font-weight-medium); color: hsl(var(--foreground)); }
473
+ .kx-metric__trend { display: inline-flex; align-items: center; gap: var(--spacing-1); font: var(--text-label-md); font-weight: var(--font-weight-medium); }
474
+ .kx-metric__trend[data-trend='up'] { color: hsl(var(--success)); }
475
+ .kx-metric__trend[data-trend='down'] { color: hsl(var(--destructive)); }
476
+ .kx-metric__trend[data-trend='neutral'] { color: hsl(var(--muted-foreground)); }
477
+
478
+ /* ── empty state ────────────────────────────────────────────────────────── */
479
+ .kx-empty {
480
+ display: flex;
481
+ flex-direction: column;
482
+ align-items: center;
483
+ justify-content: center;
484
+ gap: var(--spacing-6);
485
+ padding: var(--spacing-10);
486
+ text-align: center;
487
+ font-family: var(--font-family-sans);
488
+ }
489
+ .kx-empty__header { display: flex; flex-direction: column; align-items: center; gap: var(--spacing-2); }
490
+ .kx-empty__media { display: flex; flex-shrink: 0; align-items: center; justify-content: center; }
491
+ .kx-empty__media--icon {
492
+ inline-size: var(--spacing-10);
493
+ block-size: var(--spacing-10);
494
+ border-radius: var(--radius-full);
495
+ background-color: hsl(var(--muted));
496
+ color: hsl(var(--muted-foreground));
497
+ }
498
+ .kx-empty__title { display: block; font: var(--text-title-sm); font-weight: var(--font-weight-medium); color: hsl(var(--foreground)); }
499
+ .kx-empty__description { display: block; font: var(--text-body-md); color: hsl(var(--muted-foreground)); }
500
+ .kx-empty__content { display: flex; align-items: center; gap: var(--spacing-2); }
501
+
502
+ /* ── textarea ───────────────────────────────────────────────────────────── */
503
+ .kx-textarea {
504
+ display: block;
505
+ inline-size: 100%;
506
+ min-block-size: 100px;
507
+ resize: vertical;
508
+ padding: var(--spacing-3);
509
+ border: var(--border-width-default) solid hsl(var(--input));
510
+ border-radius: var(--radius-field);
511
+ background-color: hsl(var(--background));
512
+ font: var(--text-body-md);
513
+ font-family: var(--font-family-sans);
514
+ color: hsl(var(--foreground));
515
+ outline: none;
516
+ transition: border-color var(--duration-fast) var(--easing-standard), box-shadow var(--duration-fast) var(--easing-standard);
517
+ }
518
+ .kx-textarea::placeholder { color: hsl(var(--muted-foreground)); }
519
+ .kx-textarea:focus-visible { border-color: hsl(var(--action)); box-shadow: var(--shadow-focus); }
520
+ .kx-textarea:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; resize: none; }
521
+ .kx-textarea[aria-invalid='true'] { border-color: hsl(var(--destructive)); }
522
+ .kx-textarea[aria-invalid='true']:focus-visible { box-shadow: var(--shadow-focus-destructive); }
523
+
524
+ /* ── native select ──────────────────────────────────────────────────────── */
525
+ .kx-native-select {
526
+ inline-size: 100%;
527
+ padding: var(--spacing-3);
528
+ padding-inline-end: var(--spacing-8);
529
+ border: var(--border-width-default) solid hsl(var(--input));
530
+ background-color: hsl(var(--background));
531
+ font: var(--text-body-md);
532
+ font-family: var(--font-family-sans);
533
+ color: hsl(var(--foreground));
534
+ outline: none;
535
+ transition: border-color var(--duration-fast) var(--easing-standard), box-shadow var(--duration-fast) var(--easing-standard);
536
+ }
537
+ .kx-native-select--sharp { border-radius: var(--radius-none); }
538
+ .kx-native-select--default { border-radius: var(--radius-field); }
539
+ .kx-native-select--rounded { border-radius: var(--radius-md); }
540
+ .kx-native-select--pill { border-radius: var(--radius-full); padding-inline-start: var(--spacing-4); }
541
+ .kx-native-select:focus-visible { border-color: hsl(var(--action)); box-shadow: var(--shadow-focus); }
542
+ .kx-native-select:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; }
543
+ .kx-native-select[aria-invalid='true'] { border-color: hsl(var(--destructive)); }
544
+
545
+ /* ── radio group ────────────────────────────────────────────────────────── */
546
+ .kx-radio-group { display: flex; flex-direction: column; gap: var(--spacing-2); }
547
+ .kx-radio-group[aria-orientation='horizontal'] { flex-direction: row; gap: var(--spacing-4); }
548
+ .kx-radio { display: flex; align-items: center; gap: var(--spacing-2); }
549
+ /* the native control, kept in the accessibility tree and in the tab order — only its appearance is ours */
550
+ .kx-radio__input {
551
+ appearance: none;
552
+ margin: 0;
553
+ inline-size: var(--spacing-5);
554
+ block-size: var(--spacing-5);
555
+ flex-shrink: 0;
556
+ border: var(--border-width-default) solid hsl(var(--input));
557
+ border-radius: var(--radius-full);
558
+ background-color: hsl(var(--background));
559
+ cursor: pointer;
560
+ transition: border-color var(--duration-fast) var(--easing-standard), box-shadow var(--duration-fast) var(--easing-standard);
561
+ }
562
+ /* the dot is an inset ring rather than a pseudo-element, so it scales with the control */
563
+ .kx-radio__input:checked {
564
+ border-color: hsl(var(--primary));
565
+ box-shadow: inset 0 0 0 4px hsl(var(--primary));
566
+ }
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); }
569
+ .kx-radio__input:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; }
570
+ .kx-radio__label { font: var(--text-body-md); font-family: var(--font-family-sans); color: hsl(var(--foreground)); cursor: pointer; }
571
+ .kx-radio__input:disabled + .kx-radio__label { opacity: var(--opacity-disabled); cursor: not-allowed; }
572
+
573
+ /* ── slider ─────────────────────────────────────────────────────────────── */
574
+ .kx-slider { display: block; inline-size: 100%; }
575
+ .kx-slider__input {
576
+ appearance: none;
577
+ inline-size: 100%;
578
+ block-size: var(--spacing-5);
579
+ margin: 0;
580
+ background: transparent;
581
+ cursor: pointer;
582
+ }
583
+ /* the filled portion is a gradient stop driven by --kx-slider-fill, so the track follows the real value */
584
+ .kx-slider__input::-webkit-slider-runnable-track {
585
+ block-size: var(--spacing-2);
586
+ 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
+ );
592
+ }
593
+ .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);
600
+ border-radius: var(--radius-full);
601
+ background-color: hsl(var(--primary));
602
+ }
603
+ .kx-slider__input::-webkit-slider-thumb {
604
+ appearance: none;
605
+ margin-block-start: -6px;
606
+ inline-size: var(--spacing-5);
607
+ block-size: var(--spacing-5);
608
+ border: var(--border-width-default) solid hsl(var(--primary));
609
+ border-radius: var(--radius-full);
610
+ background-color: hsl(var(--background));
611
+ }
612
+ .kx-slider__input::-moz-range-thumb {
613
+ inline-size: var(--spacing-5);
614
+ block-size: var(--spacing-5);
615
+ border: var(--border-width-default) solid hsl(var(--primary));
616
+ border-radius: var(--radius-full);
617
+ background-color: hsl(var(--background));
618
+ }
619
+ .kx-slider__input:focus-visible { outline: none; box-shadow: var(--shadow-focus); border-radius: var(--radius-full); }
620
+ .kx-slider__input:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; }
621
+
622
+ /* ── number input ───────────────────────────────────────────────────────── */
623
+ .kx-number-input {
624
+ display: inline-flex;
625
+ 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
+ }
631
+ .kx-number-input:focus-within { border-color: hsl(var(--action)); box-shadow: var(--shadow-focus); }
632
+ .kx-number-input__input {
633
+ inline-size: var(--spacing-16);
634
+ min-inline-size: 0;
635
+ border: 0;
636
+ padding: var(--spacing-3) var(--spacing-2);
637
+ background: transparent;
638
+ font: var(--text-body-md);
639
+ font-family: var(--font-family-sans);
640
+ color: hsl(var(--foreground));
641
+ text-align: center;
642
+ outline: none;
643
+ -moz-appearance: textfield;
644
+ }
645
+ /* the browser's own spinners are replaced by the buttons either side */
646
+ .kx-number-input__input::-webkit-outer-spin-button,
647
+ .kx-number-input__input::-webkit-inner-spin-button { appearance: none; margin: 0; }
648
+ .kx-number-input__step {
649
+ appearance: none;
650
+ border: 0;
651
+ padding-inline: var(--spacing-3);
652
+ background-color: hsl(var(--muted));
653
+ color: hsl(var(--muted-foreground));
654
+ font: var(--text-body-md);
655
+ cursor: pointer;
656
+ }
657
+ .kx-number-input__step:hover { color: hsl(var(--foreground)); }
658
+ .kx-number-input__step:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; }
659
+
660
+ /* ── password input ─────────────────────────────────────────────────────── */
661
+ .kx-password-input {
662
+ display: inline-flex;
663
+ align-items: stretch;
664
+ 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
+ }
670
+ .kx-password-input:focus-within { border-color: hsl(var(--action)); box-shadow: var(--shadow-focus); }
671
+ .kx-password-input__input {
672
+ flex: 1;
673
+ min-inline-size: 0;
674
+ border: 0;
675
+ padding: var(--spacing-3);
676
+ background: transparent;
677
+ font: var(--text-body-md);
678
+ font-family: var(--font-family-sans);
679
+ color: hsl(var(--foreground));
680
+ outline: none;
681
+ }
682
+ .kx-password-input__reveal {
683
+ appearance: none;
684
+ border: 0;
685
+ padding-inline: var(--spacing-3);
686
+ background-color: hsl(var(--muted));
687
+ color: hsl(var(--muted-foreground));
688
+ font: var(--text-label-md);
689
+ font-family: var(--font-family-sans);
690
+ cursor: pointer;
691
+ }
692
+ .kx-password-input__reveal:hover { color: hsl(var(--foreground)); }
693
+ .kx-password-input__reveal:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
694
+
695
+ /* ── field ──────────────────────────────────────────────────────────────── */
696
+ .kx-field { display: flex; flex-direction: column; gap: var(--spacing-2); }
697
+ .kx-field__description { margin: 0; font: var(--text-body-sm); font-family: var(--font-family-sans); color: hsl(var(--muted-foreground)); }
698
+ .kx-field__message { display: block; font: var(--text-body-sm); font-family: var(--font-family-sans); }
699
+ .kx-field__message--error { color: hsl(var(--destructive)); }
700
+ .kx-field__message--success { color: hsl(var(--success)); }
701
+ .kx-field__message--warning { color: hsl(var(--warning)); }
702
+ .kx-field__message--info { color: hsl(var(--info)); }
703
+
704
+ /* ── toggle ─────────────────────────────────────────────────────────────── */
705
+ .kx-toggle {
706
+ display: inline-flex;
707
+ align-items: center;
708
+ justify-content: center;
709
+ gap: var(--spacing-2);
710
+ appearance: none;
711
+ border: var(--border-width-default) solid transparent;
712
+ border-radius: var(--radius-md);
713
+ background-color: transparent;
714
+ font: var(--text-label-md);
715
+ font-family: var(--font-family-sans);
716
+ font-weight: var(--font-weight-medium);
717
+ color: hsl(var(--foreground));
718
+ cursor: pointer;
719
+ transition: background-color var(--duration-fast) var(--easing-standard), color var(--duration-fast) var(--easing-standard);
720
+ }
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); }
724
+ .kx-toggle--outline { border-color: hsl(var(--input)); }
725
+ .kx-toggle:hover { background-color: hsl(var(--muted)); }
726
+ .kx-toggle:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
727
+ .kx-toggle[aria-pressed='true'] { background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground)); }
728
+ .kx-toggle:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; pointer-events: none; }
729
+
730
+ /* ── toggle group ───────────────────────────────────────────────────────── */
731
+ .kx-toggle-group { display: inline-flex; align-items: center; gap: var(--spacing-1); }
732
+ .kx-toggle-group__item { display: inline-flex; }
733
+ /* the single-select form is a real radio: kept in the tree and in the tab order, painted through its label */
734
+ .kx-toggle-group__radio { position: absolute; opacity: 0; pointer-events: none; }
735
+ .kx-toggle-group__radio:checked + .kx-toggle { background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground)); }
736
+ .kx-toggle-group__radio:focus-visible + .kx-toggle { box-shadow: var(--shadow-focus); }
737
+ .kx-toggle-group__radio:disabled + .kx-toggle { opacity: var(--opacity-disabled); cursor: not-allowed; }
738
+
739
+ /* ── segmented control ──────────────────────────────────────────────────── */
740
+ .kx-segmented {
741
+ display: inline-flex;
742
+ align-items: stretch;
743
+ padding: var(--spacing-1);
744
+ border-radius: var(--radius-md);
745
+ background-color: hsl(var(--muted));
746
+ }
747
+ .kx-segmented__item { display: inline-flex; }
748
+ .kx-segmented__radio { position: absolute; opacity: 0; pointer-events: none; }
749
+ .kx-segmented__label {
750
+ display: inline-flex;
751
+ align-items: center;
752
+ justify-content: center;
753
+ padding: var(--spacing-2) var(--spacing-4);
754
+ border-radius: var(--radius-sm);
755
+ font: var(--text-label-md);
756
+ font-family: var(--font-family-sans);
757
+ font-weight: var(--font-weight-medium);
758
+ color: hsl(var(--muted-foreground));
759
+ cursor: pointer;
760
+ transition: background-color var(--duration-fast) var(--easing-standard), color var(--duration-fast) var(--easing-standard);
761
+ }
762
+ .kx-segmented__radio:checked + .kx-segmented__label {
763
+ background-color: hsl(var(--background));
764
+ color: hsl(var(--foreground));
765
+ box-shadow: var(--shadow-sm);
766
+ }
767
+ .kx-segmented__radio:focus-visible + .kx-segmented__label { box-shadow: var(--shadow-focus); }
768
+ .kx-segmented__radio:disabled + .kx-segmented__label { opacity: var(--opacity-disabled); cursor: not-allowed; }
769
+
770
+ /* ── motion ─────────────────────────────────────────────────────────────── */
771
+ /* The spinner keeps turning, slowly, rather than stopping: a frozen spinner reads as a hung page. The
772
+ skeleton's pulse and the hover transitions stop, because neither carries information. */
773
+ @media (prefers-reduced-motion: reduce) {
774
+ .kx-spinner { animation-duration: 3s; }
775
+ .kx-skeleton { animation: none; }
776
+ .kx-toggle,
777
+ .kx-segmented__label,
778
+ .kx-textarea,
779
+ .kx-native-select,
780
+ .kx-radio__input,
781
+ .kx-tag__remove { transition: none; }
782
+ }