@k2b/ui 0.15.0 → 0.16.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/dist/base.css ADDED
@@ -0,0 +1,1097 @@
1
+ /* @k2b/ui base stylesheet: classless styles for a plain HTML page.
2
+ Semantic HTML (headings, lists, tables, forms, buttons, details, dialog,
3
+ progress) looks like @k2b/ui in light and dark without classes. A few
4
+ short helper classes cover layouts that HTML has no element for.
5
+
6
+ Every rule and token sits in `@layer k2b-base`, so any unlayered rule of
7
+ the page wins without a specificity fight. Four state rules are
8
+ `!important` on purpose and stay in force: `[hidden]`, the hidden label of
9
+ a busy button, reduced motion, and what print hides.
10
+ `<html data-theme="dark">` switches the theme; the default is light. The
11
+ tokens carry the same names and values as the `.k2b-ui` scope (a parity
12
+ test keeps them equal).
13
+
14
+ Spacing: only the flow rules below set block margins. Siblings in a flow
15
+ container, `div` included, are spaced from above; no element rule sets a
16
+ top or bottom margin, so no element can cancel the rhythm. `.row`, `.grid`
17
+ and `.stat` take the flow margin from their children; custom flex or grid
18
+ layouts set `gap` and `> * { margin: 0 }` themselves. */
19
+ @layer k2b-base {
20
+ :root {
21
+ color-scheme: light;
22
+
23
+ --k2b-font-sans: "IBM Plex Sans", ui-sans-serif, system-ui, sans-serif;
24
+ --k2b-font-mono: "IBM Plex Mono", ui-monospace, monospace;
25
+ --k2b-radius-control: 0.5rem;
26
+ --k2b-radius-surface: 0.75rem;
27
+ --k2b-radius-dialog: 0.875rem;
28
+ --k2b-focus-width: 2px;
29
+ --k2b-danger-500: #ef4444;
30
+ --stdlib-chart-c1: #3b82f6;
31
+ --stdlib-chart-c2: #10b981;
32
+ --stdlib-chart-c3: #f59e0b;
33
+ --stdlib-chart-c4: #8b5cf6;
34
+ --stdlib-chart-c5: #ec4899;
35
+ --stdlib-chart-c6: #06b6d4;
36
+ --stdlib-chart-c7: #84cc16;
37
+ --stdlib-chart-c8: #f97316;
38
+
39
+ --k2b-surface: light-dark(#ffffff, #11151b);
40
+ --k2b-surface-canvas: light-dark(#fafafa, #090d12);
41
+ --k2b-surface-muted: light-dark(#f4f4f5, #20242a);
42
+ --k2b-surface-elevated: light-dark(#ffffff, #181d24);
43
+ --k2b-text: light-dark(#09090b, #fafafa);
44
+ --k2b-text-secondary: light-dark(#3f3f46, #e4e4e7);
45
+ --k2b-text-muted: light-dark(#52525b, #a1a1aa);
46
+ --k2b-placeholder-color: light-dark(#71717a, color-mix(in srgb, #a1a1aa 70%, #71717a));
47
+ --k2b-border: light-dark(#e4e4e7, #29313a);
48
+ --k2b-border-strong: light-dark(#d4d4d8, #3a4552);
49
+ --k2b-hover: light-dark(#eff6ff, #252c35);
50
+ --k2b-selected: light-dark(#dbeafe, #172554);
51
+ --k2b-action: light-dark(#1d4ed8, #93c5fd);
52
+ --k2b-action-hover: light-dark(#1e40af, #bfdbfe);
53
+ --k2b-action-solid: light-dark(#1d4ed8, #2563eb);
54
+ --k2b-action-solid-hover: light-dark(#1e40af, #1d4ed8);
55
+ --k2b-selection: light-dark(rgb(29 78 216 / 0.35), rgb(147 197 253 / 0.35));
56
+ --k2b-focus-ring: light-dark(#3b82f6, #60a5fa);
57
+ --k2b-info-surface: light-dark(#eff6ff, #172554);
58
+ --k2b-info-text: light-dark(#1d4ed8, #93c5fd);
59
+ --k2b-success-surface: light-dark(#ecfdf5, #052e26);
60
+ --k2b-success-text: light-dark(#047857, #6ee7b7);
61
+ --k2b-warning-surface: light-dark(#fffbeb, #422006);
62
+ --k2b-warning-text: light-dark(#92400e, #fcd34d);
63
+ --k2b-danger-surface: light-dark(#fef2f2, #450a0a);
64
+ --k2b-danger-text: light-dark(#b91c1c, #fca5a5);
65
+ /* light-dark() only takes colors, so the shadow switches with the theme attribute. */
66
+ --k2b-shadow-float: 0 18px 48px rgb(9 9 11 / 0.18);
67
+ }
68
+
69
+ :root[data-theme="dark"] {
70
+ color-scheme: dark;
71
+ --k2b-shadow-float: 0 22px 56px rgb(0 0 0 / 0.52);
72
+ }
73
+
74
+ *,
75
+ *::before,
76
+ *::after {
77
+ box-sizing: border-box;
78
+ }
79
+
80
+ /* The root keeps the browser's font size, so rem sizes (touch targets, spacing) match @k2b/ui. */
81
+ html {
82
+ background: var(--k2b-surface);
83
+ color: var(--k2b-text);
84
+ -webkit-text-size-adjust: 100%;
85
+ text-size-adjust: 100%;
86
+ accent-color: var(--k2b-action-solid);
87
+ }
88
+
89
+ ::selection {
90
+ background: var(--k2b-selection);
91
+ }
92
+
93
+ /* The body carries padding and the reading width, so pages without <main> look right too. */
94
+ body {
95
+ max-width: 72rem;
96
+ margin: 0 auto;
97
+ padding: 1.5rem;
98
+ font: 400 0.9375rem/1.55 var(--k2b-font-sans);
99
+ }
100
+
101
+ @media (max-width: 40rem) {
102
+ body {
103
+ padding: 1rem;
104
+ }
105
+ }
106
+
107
+ /* Containers may shrink below their content, so one wide table never widens a whole grid or page. */
108
+ :where(main, section, article, aside, header, footer, nav, form, fieldset, figure, li, div) {
109
+ min-width: 0;
110
+ }
111
+
112
+ /* ---------------------------------------------------------------- flow
113
+ The only rules that set block margins. All of them have zero
114
+ specificity; a later, more specific flow rule wins by source order. */
115
+
116
+ :where(body, main, section, article, aside, header, footer, nav, form, fieldset, dialog, details, figure, blockquote, ul, ol, li, dd, td, th, div)
117
+ > * {
118
+ margin-block: 0;
119
+ }
120
+
121
+ :where(body, main, section, article, aside, header, footer, form, fieldset, dialog, details, blockquote, div) > * + * {
122
+ margin-block-start: 1rem;
123
+ }
124
+
125
+ :where(main, body) > :where(section, article) + :where(section, article, h2) {
126
+ margin-block-start: 2.5rem;
127
+ }
128
+
129
+ :where(body, main, section, article, form, div) > * + :where(h2, h3) {
130
+ margin-block-start: 2rem;
131
+ }
132
+
133
+ :where(h1, h2, h3, h4) + * {
134
+ margin-block-start: 0.5rem;
135
+ }
136
+
137
+ :where(ul, ol):where(:not(nav > *)) > * + * {
138
+ margin-block-start: 0.25rem;
139
+ }
140
+
141
+ :where(details > summary) + * {
142
+ margin-block-start: 0.5rem;
143
+ }
144
+
145
+ :where(dialog) :where(* + footer, * + menu) {
146
+ margin-block-start: 1.25rem;
147
+ }
148
+
149
+ /* Checklist rows carry their own height; they sit flush. */
150
+ :where(ul:has(> li > input[type="checkbox"], > li > label > input[type="checkbox"])) > * + * {
151
+ margin-block-start: 0;
152
+ }
153
+
154
+ /* ---------------------------------------------------------------- type */
155
+
156
+ h1,
157
+ h2,
158
+ h3,
159
+ h4,
160
+ h5,
161
+ h6 {
162
+ font-weight: 600;
163
+ line-height: 1.25;
164
+ text-wrap: balance;
165
+ }
166
+
167
+ h1 {
168
+ font-size: 1.5rem;
169
+ }
170
+
171
+ h2 {
172
+ font-size: 1.25rem;
173
+ }
174
+
175
+ h3 {
176
+ font-size: 1.0625rem;
177
+ }
178
+
179
+ h4,
180
+ h5,
181
+ h6 {
182
+ font-size: 0.9375rem;
183
+ }
184
+
185
+ p {
186
+ text-wrap: pretty;
187
+ }
188
+
189
+ small,
190
+ figcaption,
191
+ caption {
192
+ color: var(--k2b-text-muted);
193
+ font-size: 0.8125rem;
194
+ }
195
+
196
+ a {
197
+ color: var(--k2b-action);
198
+ text-decoration: none;
199
+ border-radius: 2px;
200
+ }
201
+
202
+ a:hover {
203
+ color: var(--k2b-action-hover);
204
+ text-decoration: underline;
205
+ }
206
+
207
+ strong,
208
+ b {
209
+ font-weight: 600;
210
+ }
211
+
212
+ mark {
213
+ padding: 0 0.15em;
214
+ border-radius: 0.25rem;
215
+ color: inherit;
216
+ background: var(--k2b-warning-surface);
217
+ }
218
+
219
+ blockquote {
220
+ margin-inline: 0;
221
+ padding: 0.75rem 1rem;
222
+ border-radius: var(--k2b-radius-control);
223
+ color: var(--k2b-text-secondary);
224
+ background: var(--k2b-surface-muted);
225
+ }
226
+
227
+ hr {
228
+ height: 0.5rem;
229
+ border: 0;
230
+ }
231
+
232
+ code,
233
+ kbd,
234
+ samp,
235
+ pre {
236
+ font-family: var(--k2b-font-mono);
237
+ font-size: 0.8125rem;
238
+ }
239
+
240
+ :not(pre) > code,
241
+ kbd {
242
+ padding: 0.1em 0.35em;
243
+ border-radius: 0.3rem;
244
+ background: var(--k2b-surface-muted);
245
+ }
246
+
247
+ pre {
248
+ overflow-x: auto;
249
+ padding: 0.75rem 1rem;
250
+ border-radius: var(--k2b-radius-control);
251
+ background: var(--k2b-surface-muted);
252
+ line-height: 1.5;
253
+ }
254
+
255
+ /* ---------------------------------------------------------------- lists */
256
+
257
+ ul,
258
+ ol {
259
+ padding-inline-start: 1.25rem;
260
+ }
261
+
262
+ /* Detail views: a key/value grid. */
263
+ dl {
264
+ display: grid;
265
+ grid-template-columns: minmax(6rem, max-content) 1fr;
266
+ gap: 0.375rem 1.5rem;
267
+ }
268
+
269
+ dt {
270
+ color: var(--k2b-text-muted);
271
+ }
272
+
273
+ dd {
274
+ margin-inline-start: 0;
275
+ }
276
+
277
+ /* A list item that starts with a checkbox, or holds a label with one, is a
278
+ checklist row: no bullet, the label grows, buttons trail, done is dimmed.
279
+ Put buttons next to the label, never inside it. */
280
+ ul:has(> li > input[type="checkbox"], > li > label > input[type="checkbox"]) {
281
+ padding-inline-start: 0;
282
+ list-style: none;
283
+ }
284
+
285
+ li:has(> input[type="checkbox"], > label > input[type="checkbox"]) {
286
+ display: flex;
287
+ align-items: center;
288
+ gap: 0.625rem;
289
+ min-height: 2.5rem;
290
+ padding: 0.25rem 0.5rem;
291
+ border-radius: var(--k2b-radius-control);
292
+ list-style: none;
293
+ }
294
+
295
+ li:has(> input[type="checkbox"], > label > input[type="checkbox"]):hover {
296
+ background: var(--k2b-surface-muted);
297
+ }
298
+
299
+ li:has(> input[type="checkbox"], > label > input[type="checkbox"]) > label {
300
+ flex: 1;
301
+ min-width: 0;
302
+ padding-block: 0.25rem;
303
+ overflow-wrap: anywhere;
304
+ }
305
+
306
+ li:has(> input[type="checkbox"]:checked, > label > input[type="checkbox"]:checked) > label {
307
+ color: var(--k2b-text-muted);
308
+ text-decoration: line-through;
309
+ text-decoration-color: color-mix(in srgb, currentColor 50%, transparent);
310
+ }
311
+
312
+ /* ---------------------------------------------------------------- tables */
313
+
314
+ table {
315
+ width: 100%;
316
+ border-collapse: collapse;
317
+ font-variant-numeric: tabular-nums;
318
+ }
319
+
320
+ caption {
321
+ padding-block-end: 0.5rem;
322
+ font-weight: 600;
323
+ text-align: start;
324
+ }
325
+
326
+ th,
327
+ td {
328
+ padding: 0.5rem 0.75rem;
329
+ text-align: start;
330
+ vertical-align: middle;
331
+ }
332
+
333
+ th {
334
+ color: var(--k2b-text-muted);
335
+ font-size: 0.8125rem;
336
+ font-weight: 500;
337
+ }
338
+
339
+ :is(th, td):first-child {
340
+ padding-inline-start: 0.5rem;
341
+ }
342
+
343
+ tbody tr:hover {
344
+ background: var(--k2b-surface-muted);
345
+ }
346
+
347
+ tbody tr > :first-child {
348
+ border-start-start-radius: var(--k2b-radius-control);
349
+ border-end-start-radius: var(--k2b-radius-control);
350
+ }
351
+
352
+ tbody tr > :last-child {
353
+ border-start-end-radius: var(--k2b-radius-control);
354
+ border-end-end-radius: var(--k2b-radius-control);
355
+ }
356
+
357
+ tfoot td,
358
+ tfoot th {
359
+ font-weight: 600;
360
+ }
361
+
362
+ /* Wide tables scroll inside a <figure> instead of the page. The relative
363
+ position keeps visually hidden headers inside the scroll box. */
364
+ figure:has(> table),
365
+ .scroll {
366
+ position: relative;
367
+ overflow-x: auto;
368
+ }
369
+
370
+ /* ---------------------------------------------------------------- forms */
371
+
372
+ label {
373
+ display: grid;
374
+ gap: 0.25rem;
375
+ color: var(--k2b-text-secondary);
376
+ font-size: 0.8125rem;
377
+ font-weight: 500;
378
+ }
379
+
380
+ label:has(> input:is([type="checkbox"], [type="radio"])) {
381
+ display: flex;
382
+ align-items: center;
383
+ gap: 0.5rem;
384
+ color: var(--k2b-text);
385
+ font-size: 0.9375rem;
386
+ font-weight: 400;
387
+ }
388
+
389
+ input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"], [type="color"], [type="submit"], [type="button"], [type="reset"]),
390
+ select,
391
+ textarea {
392
+ width: 100%;
393
+ min-height: 2.25rem;
394
+ padding: 0.375rem 0.625rem;
395
+ border: 1px solid transparent;
396
+ border-radius: var(--k2b-radius-control);
397
+ color: var(--k2b-text);
398
+ background: var(--k2b-surface-muted);
399
+ font: inherit;
400
+ font-size: 0.875rem;
401
+ font-weight: 400;
402
+ line-height: 1.25rem;
403
+ transition:
404
+ background-color 150ms ease,
405
+ border-color 150ms ease;
406
+ }
407
+
408
+ textarea {
409
+ min-height: 5rem;
410
+ resize: vertical;
411
+ }
412
+
413
+ input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"], [type="color"], [type="submit"], [type="button"], [type="reset"]):hover:not(:focus, :disabled),
414
+ select:hover:not(:focus, :disabled),
415
+ textarea:hover:not(:focus, :disabled) {
416
+ border-color: var(--k2b-border);
417
+ }
418
+
419
+ /* Fields draw the focus outline inside their box, also on mouse focus.
420
+ An outline, unlike a box-shadow, also paints on a native select in
421
+ WebKit. Every other control keeps the outline outside. */
422
+ input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"], [type="color"], [type="submit"], [type="button"], [type="reset"]):focus,
423
+ select:focus,
424
+ textarea:focus {
425
+ outline: var(--k2b-focus-width) solid var(--k2b-focus-ring);
426
+ outline-offset: calc(-1 * var(--k2b-focus-width));
427
+ background: var(--k2b-surface);
428
+ }
429
+
430
+ ::placeholder {
431
+ color: var(--k2b-placeholder-color);
432
+ opacity: 1;
433
+ }
434
+
435
+ :is(input, select, textarea):user-invalid {
436
+ border-color: var(--k2b-danger-500);
437
+ }
438
+
439
+ input:is([type="checkbox"], [type="radio"]) {
440
+ flex: none;
441
+ width: 1.125rem;
442
+ height: 1.125rem;
443
+ margin: 0;
444
+ }
445
+
446
+ input[type="range"] {
447
+ width: 100%;
448
+ }
449
+
450
+ input[type="file"] {
451
+ max-width: 100%;
452
+ color: var(--k2b-text-muted);
453
+ font: inherit;
454
+ font-size: 0.8125rem;
455
+ }
456
+
457
+ input[type="file"]::file-selector-button {
458
+ margin-inline-end: 0.75rem;
459
+ padding: 0.375rem 0.75rem;
460
+ border: 0;
461
+ border-radius: var(--k2b-radius-control);
462
+ color: var(--k2b-text);
463
+ background: var(--k2b-surface-muted);
464
+ font: inherit;
465
+ font-weight: 500;
466
+ cursor: pointer;
467
+ }
468
+
469
+ fieldset {
470
+ min-width: 0;
471
+ margin-inline: 0;
472
+ padding: 0;
473
+ border: 0;
474
+ }
475
+
476
+ legend {
477
+ padding: 0 0 0.5rem;
478
+ font-weight: 600;
479
+ }
480
+
481
+ output {
482
+ font-variant-numeric: tabular-nums;
483
+ }
484
+
485
+ /* ---------------------------------------------------------------- buttons */
486
+
487
+ button,
488
+ input:is([type="submit"], [type="button"], [type="reset"]),
489
+ ::file-selector-button {
490
+ -webkit-tap-highlight-color: transparent;
491
+ }
492
+
493
+ button,
494
+ input:is([type="submit"], [type="button"], [type="reset"]) {
495
+ position: relative;
496
+ display: inline-flex;
497
+ align-items: center;
498
+ justify-content: center;
499
+ gap: 0.375rem;
500
+ min-height: 2.25rem;
501
+ padding: 0.375rem 0.875rem;
502
+ border: 1px solid transparent;
503
+ border-radius: var(--k2b-radius-control);
504
+ color: var(--k2b-text);
505
+ background: var(--k2b-surface-muted);
506
+ font: inherit;
507
+ font-size: 0.875rem;
508
+ font-weight: 500;
509
+ white-space: nowrap;
510
+ cursor: pointer;
511
+ user-select: none;
512
+ transition:
513
+ background-color 150ms ease-out,
514
+ color 150ms ease-out;
515
+ }
516
+
517
+ button:hover:not(:disabled) {
518
+ background: var(--k2b-hover);
519
+ }
520
+
521
+ /* The submit button of a form and `.primary` are filled. Cancel buttons
522
+ (`value="cancel"`) and `type="button"` stay calm. */
523
+ .primary,
524
+ form :is(button:not([type]), button[type="submit"], input[type="submit"]):not(.danger, [value="cancel"]) {
525
+ color: #ffffff;
526
+ background: var(--k2b-action-solid);
527
+ }
528
+
529
+ .primary:hover:not(:disabled),
530
+ form :is(button:not([type]), button[type="submit"]):not(.danger, [value="cancel"]):hover:not(:disabled) {
531
+ background: var(--k2b-action-solid-hover);
532
+ }
533
+
534
+ .danger {
535
+ color: var(--k2b-danger-text);
536
+ background: transparent;
537
+ }
538
+
539
+ .danger:hover:not(:disabled) {
540
+ background: var(--k2b-danger-surface);
541
+ }
542
+
543
+ /* A delete action in every list or table row stays quiet until its row is
544
+ hovered or holds the focus. Touch screens keep it quiet and visible. */
545
+ :is(li, tr):not(:hover, :focus-within) .danger {
546
+ color: var(--k2b-text-muted);
547
+ }
548
+
549
+ button:disabled,
550
+ input:disabled,
551
+ select:disabled,
552
+ textarea:disabled {
553
+ opacity: 0.45;
554
+ cursor: not-allowed;
555
+ }
556
+
557
+ /* Busy: a spinner over the hidden label, so the button keeps its size. */
558
+ button[aria-busy="true"] {
559
+ color: transparent !important;
560
+ pointer-events: none;
561
+ }
562
+
563
+ button[aria-busy="true"]::after {
564
+ content: "";
565
+ position: absolute;
566
+ inset: 0;
567
+ width: 1rem;
568
+ height: 1rem;
569
+ margin: auto;
570
+ border: 2px solid var(--k2b-text-muted);
571
+ border-right-color: transparent;
572
+ border-radius: 50%;
573
+ animation: k2b-base-spin 0.7s linear infinite;
574
+ }
575
+
576
+ .primary[aria-busy="true"]::after,
577
+ form button:not([type], .danger, [value="cancel"])[aria-busy="true"]::after,
578
+ form button[type="submit"][aria-busy="true"]::after {
579
+ border-color: #ffffff;
580
+ border-right-color: transparent;
581
+ }
582
+
583
+ @keyframes k2b-base-spin {
584
+ to {
585
+ rotate: 1turn;
586
+ }
587
+ }
588
+
589
+ /* ---------------------------------------------------------------- layout helpers */
590
+
591
+ /* Fields and buttons side by side; fields grow, everything aligns at the bottom. */
592
+ .row {
593
+ display: flex;
594
+ flex-wrap: wrap;
595
+ align-items: end;
596
+ gap: 0.5rem 0.75rem;
597
+ }
598
+
599
+ .row > * {
600
+ margin: 0;
601
+ }
602
+
603
+ .row > :is(label, input, select, textarea):not(:has(> :is([type="checkbox"], [type="radio"]))) {
604
+ flex: 1 1 12rem;
605
+ }
606
+
607
+ /* A row with a heading, directly or in a title block such as
608
+ `<div><h1>…</h1><p class="muted">…</p></div>`, is a header: title first,
609
+ actions at the end, centered. Actions that wrap below a long title on a
610
+ phone stay at the end. */
611
+ .row:has(> :is(h1, h2, h3), > * > :is(h1, h2, h3)) {
612
+ align-items: center;
613
+ }
614
+
615
+ .row > :is(h1, h2, h3, :has(> :is(h1, h2, h3))) + * {
616
+ margin-inline-start: auto;
617
+ }
618
+
619
+ /* Fields or key figures in columns of at least 14rem. */
620
+ .grid {
621
+ display: grid;
622
+ grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
623
+ gap: 1rem;
624
+ }
625
+
626
+ .grid > * {
627
+ margin: 0;
628
+ }
629
+
630
+ /* Key figures are short, so they fit two or three to a phone row. */
631
+ .grid:has(> .stat) {
632
+ grid-template-columns: repeat(auto-fit, minmax(min(8rem, 100%), 1fr));
633
+ }
634
+
635
+ /* A key figure: a small muted label, the value large beneath it. Flat, without a surface. */
636
+ .stat {
637
+ display: grid;
638
+ gap: 0.125rem;
639
+ }
640
+
641
+ .stat > * {
642
+ margin: 0;
643
+ }
644
+
645
+ .stat > :first-child {
646
+ color: var(--k2b-text-muted);
647
+ font-size: 0.8125rem;
648
+ }
649
+
650
+ .stat strong {
651
+ font-size: 1.5rem;
652
+ font-weight: 600;
653
+ line-height: 1.2;
654
+ font-variant-numeric: tabular-nums;
655
+ }
656
+
657
+ .muted {
658
+ color: var(--k2b-text-muted);
659
+ }
660
+
661
+ .num {
662
+ text-align: end;
663
+ font-variant-numeric: tabular-nums;
664
+ white-space: nowrap;
665
+ }
666
+
667
+ .sr-only {
668
+ position: absolute;
669
+ width: 1px;
670
+ height: 1px;
671
+ overflow: hidden;
672
+ clip-path: inset(50%);
673
+ white-space: nowrap;
674
+ }
675
+
676
+ /* Status tag: <span class="tag" data-tone="success">Paid</span>. */
677
+ .tag {
678
+ display: inline-flex;
679
+ align-items: center;
680
+ padding: 0.0625rem 0.5rem;
681
+ border-radius: 999px;
682
+ color: var(--k2b-text-secondary);
683
+ background: var(--k2b-surface-muted);
684
+ font-size: 0.75rem;
685
+ font-weight: 500;
686
+ white-space: nowrap;
687
+ }
688
+
689
+ /* Inline feedback. role="alert" is an error surface, hidden while empty.
690
+ role="status" is muted text without a surface, so a message next to a
691
+ button never looks like a second button. <output> holds a result the
692
+ reader needs, so it keeps the text color. */
693
+ [role="alert"] {
694
+ padding: 0.625rem 0.875rem;
695
+ border-radius: var(--k2b-radius-control);
696
+ color: var(--k2b-danger-text);
697
+ background: var(--k2b-danger-surface);
698
+ }
699
+
700
+ [role="alert"]:empty {
701
+ display: none;
702
+ }
703
+
704
+ [role="status"] {
705
+ color: var(--k2b-text-muted);
706
+ }
707
+
708
+ /* data-tone colors the surface of tags and alerts, and only the text of status messages and outputs. */
709
+ :is(.tag, [role="alert"])[data-tone="info"] {
710
+ color: var(--k2b-info-text);
711
+ background: var(--k2b-info-surface);
712
+ }
713
+
714
+ :is(.tag, [role="alert"])[data-tone="success"] {
715
+ color: var(--k2b-success-text);
716
+ background: var(--k2b-success-surface);
717
+ }
718
+
719
+ :is(.tag, [role="alert"])[data-tone="warning"] {
720
+ color: var(--k2b-warning-text);
721
+ background: var(--k2b-warning-surface);
722
+ }
723
+
724
+ :is(.tag, [role="alert"])[data-tone="danger"] {
725
+ color: var(--k2b-danger-text);
726
+ background: var(--k2b-danger-surface);
727
+ }
728
+
729
+ :is([role="status"], output)[data-tone="info"] {
730
+ color: var(--k2b-info-text);
731
+ }
732
+
733
+ :is([role="status"], output)[data-tone="success"] {
734
+ color: var(--k2b-success-text);
735
+ }
736
+
737
+ :is([role="status"], output)[data-tone="warning"] {
738
+ color: var(--k2b-warning-text);
739
+ }
740
+
741
+ :is([role="status"], output)[data-tone="danger"] {
742
+ color: var(--k2b-danger-text);
743
+ }
744
+
745
+ /* Tabs and filters: buttons or links in a nav; aria-current, aria-pressed or aria-selected marks the active one. */
746
+ nav,
747
+ nav > :is(ul, ol) {
748
+ display: flex;
749
+ flex-wrap: wrap;
750
+ gap: 0.25rem;
751
+ padding: 0;
752
+ list-style: none;
753
+ }
754
+
755
+ nav :is(a, button) {
756
+ display: inline-flex;
757
+ align-items: center;
758
+ min-height: 2.25rem;
759
+ padding: 0.375rem 0.75rem;
760
+ border-radius: var(--k2b-radius-control);
761
+ color: var(--k2b-text-secondary);
762
+ background: transparent;
763
+ font-size: 0.875rem;
764
+ font-weight: 500;
765
+ }
766
+
767
+ nav :is(a, button):hover {
768
+ color: var(--k2b-text);
769
+ background: var(--k2b-surface-muted);
770
+ text-decoration: none;
771
+ }
772
+
773
+ nav :is(a, button):is([aria-current]:not([aria-current="false"]), [aria-pressed="true"], [aria-selected="true"]) {
774
+ color: var(--k2b-text);
775
+ background: var(--k2b-selected);
776
+ }
777
+
778
+ /* ---------------------------------------------------------------- disclosure and dialog */
779
+
780
+ details > summary {
781
+ display: flex;
782
+ align-items: center;
783
+ gap: 0.5rem;
784
+ min-height: 2.25rem;
785
+ border-radius: var(--k2b-radius-control);
786
+ font-weight: 500;
787
+ list-style: none;
788
+ cursor: pointer;
789
+ }
790
+
791
+ details > summary::-webkit-details-marker {
792
+ display: none;
793
+ }
794
+
795
+ details > summary::before {
796
+ content: "";
797
+ width: 0.4rem;
798
+ height: 0.4rem;
799
+ border-right: 2px solid currentColor;
800
+ border-bottom: 2px solid currentColor;
801
+ rotate: -45deg;
802
+ transition: rotate 150ms ease;
803
+ }
804
+
805
+ details[open] > summary::before {
806
+ rotate: 45deg;
807
+ }
808
+
809
+ dialog {
810
+ width: min(32rem, calc(100vw - 2rem));
811
+ max-height: calc(100dvh - 2rem);
812
+ padding: 1.25rem;
813
+ border: 0;
814
+ border-radius: var(--k2b-radius-dialog);
815
+ color: var(--k2b-text);
816
+ background: var(--k2b-surface-elevated);
817
+ box-shadow: var(--k2b-shadow-float);
818
+ }
819
+
820
+ /* A modal dialog is centered in the top layer; no flow margin applies to it. */
821
+ dialog:modal {
822
+ margin: auto;
823
+ }
824
+
825
+ dialog::backdrop {
826
+ background: rgb(9 9 11 / 0.45);
827
+ }
828
+
829
+ dialog :is(footer, menu) {
830
+ display: flex;
831
+ flex-wrap: wrap;
832
+ justify-content: flex-end;
833
+ gap: 0.5rem;
834
+ padding: 0;
835
+ }
836
+
837
+ /* ---------------------------------------------------------------- feedback */
838
+
839
+ progress,
840
+ meter {
841
+ width: 100%;
842
+ height: 0.5rem;
843
+ overflow: hidden;
844
+ border: 0;
845
+ border-radius: 999px;
846
+ background: var(--k2b-surface-muted);
847
+ appearance: none;
848
+ }
849
+
850
+ progress::-webkit-progress-bar,
851
+ meter::-webkit-meter-inner-element,
852
+ meter::-webkit-meter-bar {
853
+ border: 0;
854
+ border-radius: 999px;
855
+ background: var(--k2b-surface-muted);
856
+ }
857
+
858
+ progress::-webkit-progress-value,
859
+ meter::-webkit-meter-optimum-value {
860
+ border-radius: 999px;
861
+ background: var(--k2b-action-solid);
862
+ }
863
+
864
+ progress::-moz-progress-bar,
865
+ meter::-moz-meter-bar {
866
+ border-radius: 999px;
867
+ background: var(--k2b-action-solid);
868
+ }
869
+
870
+ /* ---------------------------------------------------------------- media and charts */
871
+
872
+ img,
873
+ svg,
874
+ video,
875
+ canvas {
876
+ max-width: 100%;
877
+ height: auto;
878
+ }
879
+
880
+ figure {
881
+ margin-inline: 0;
882
+ }
883
+
884
+ /* Chart markup as the @k2b/ui Chart renders it: `.k2b-chart` wraps
885
+ `.k2b-chart__svg` with `--k2b-chart-width` and `--k2b-chart-height` and
886
+ the @k2b/stdlib SVG. The page removes the `<style>` element stdlib embeds
887
+ in the SVG: its rules sit outside this layer and would override the
888
+ tokens below. Cartesian geometry (`data-stretch`, with
889
+ `preserveAspectRatio="none"` on the SVG and a `transform-origin` at each
890
+ text's x and y) fills the width; text and markers keep their pixel size.
891
+ These rules mirror the `.k2b-chart` rules of styles.css. */
892
+ .k2b-chart {
893
+ position: relative;
894
+ display: block;
895
+ min-width: 0;
896
+ color: var(--k2b-text);
897
+ }
898
+
899
+ .k2b-chart__svg {
900
+ display: block;
901
+ width: 100%;
902
+ height: var(--k2b-chart-height);
903
+ max-height: 70vh;
904
+ }
905
+
906
+ .k2b-chart__svg > svg {
907
+ display: block;
908
+ width: 100%;
909
+ height: 100%;
910
+ overflow: visible;
911
+ }
912
+
913
+ .k2b-chart__svg[data-stretch] {
914
+ container-type: size;
915
+ --k2b-chart-inverse-x: tan(atan2(var(--k2b-chart-width), 100cqw));
916
+ --k2b-chart-inverse-y: tan(atan2(var(--k2b-chart-height), 100cqh));
917
+ }
918
+
919
+ .k2b-chart__svg[data-stretch] text {
920
+ scale: var(--k2b-chart-inverse-x) var(--k2b-chart-inverse-y);
921
+ }
922
+
923
+ .k2b-chart__svg[data-stretch] :is(circle, .stdlib-chart-point, .stdlib-chart-legend) {
924
+ transform-box: fill-box;
925
+ transform-origin: center;
926
+ scale: var(--k2b-chart-inverse-x) var(--k2b-chart-inverse-y);
927
+ }
928
+
929
+ /* The legend scales as one group, so its labels never overlap on a narrow screen. */
930
+ .k2b-chart__svg[data-stretch] .stdlib-chart-legend text {
931
+ scale: none;
932
+ }
933
+
934
+ .k2b-chart[data-chart-kind="scatter"] .k2b-chart__svg[data-stretch] .stdlib-chart-point {
935
+ scale: calc(var(--k2b-chart-inverse-x) * 1.75) calc(var(--k2b-chart-inverse-y) * 1.75);
936
+ }
937
+
938
+ .k2b-chart__svg[data-stretch] :is(path, line, rect, circle) {
939
+ vector-effect: non-scaling-stroke;
940
+ }
941
+
942
+ /* Room for the sparkline's end markers, so a short sparkline never clips them. */
943
+ .k2b-chart[data-chart-kind="sparkline"] .k2b-chart__svg {
944
+ padding: 4px;
945
+ }
946
+
947
+ .k2b-chart__svg:not([data-stretch]) {
948
+ height: auto;
949
+ max-width: calc(var(--k2b-chart-width) * 1.5);
950
+ margin-inline: auto;
951
+ }
952
+
953
+ .stdlib-chart {
954
+ color: inherit;
955
+ overflow: visible;
956
+ }
957
+
958
+ .stdlib-chart text {
959
+ font-family: var(--k2b-font-sans);
960
+ }
961
+
962
+ .stdlib-chart :is(.stdlib-chart-tick-label, .stdlib-chart-bar-value, .stdlib-chart-axis-label) {
963
+ font-family: var(--k2b-font-mono);
964
+ font-variant-numeric: tabular-nums;
965
+ }
966
+
967
+ .stdlib-chart-series-0 { fill: var(--stdlib-chart-c1); stroke: var(--stdlib-chart-c1); }
968
+ .stdlib-chart-series-1 { fill: var(--stdlib-chart-c2); stroke: var(--stdlib-chart-c2); }
969
+ .stdlib-chart-series-2 { fill: var(--stdlib-chart-c3); stroke: var(--stdlib-chart-c3); }
970
+ .stdlib-chart-series-3 { fill: var(--stdlib-chart-c4); stroke: var(--stdlib-chart-c4); }
971
+ .stdlib-chart-series-4 { fill: var(--stdlib-chart-c5); stroke: var(--stdlib-chart-c5); }
972
+ .stdlib-chart-series-5 { fill: var(--stdlib-chart-c6); stroke: var(--stdlib-chart-c6); }
973
+ .stdlib-chart-series-6 { fill: var(--stdlib-chart-c7); stroke: var(--stdlib-chart-c7); }
974
+ .stdlib-chart-series-7 { fill: var(--stdlib-chart-c8); stroke: var(--stdlib-chart-c8); }
975
+ .stdlib-chart-axis { stroke: currentColor; opacity: 0.25; fill: none; }
976
+ .stdlib-chart-tick-label { font-size: 11px; fill: currentColor; opacity: 0.65; }
977
+ .stdlib-chart-axis-label { font-size: 11px; fill: currentColor; opacity: 0.85; }
978
+ .stdlib-chart-grid { stroke: currentColor; opacity: 0.08; stroke-dasharray: 2 2; fill: none; }
979
+ .stdlib-chart-title { font-size: 14px; font-weight: 600; fill: currentColor; }
980
+ .stdlib-chart-subtitle { font-size: 11px; fill: currentColor; opacity: 0.7; }
981
+ .stdlib-chart-reference { stroke: currentColor; opacity: 0.5; stroke-dasharray: 4 4; fill: none; }
982
+ .stdlib-chart-reference-label { font-size: 10px; fill: currentColor; opacity: 0.7; }
983
+ .stdlib-chart-line { fill: none; stroke-width: 2; }
984
+ .stdlib-chart-area { fill-opacity: 0.18; stroke: none; }
985
+ .stdlib-chart-sparkline { fill: none; stroke-width: 1.5; stroke: currentColor; }
986
+ .stdlib-chart-sparkline-area { stroke: none; }
987
+ .stdlib-chart-sparkline-last { fill: currentColor; stroke: none; }
988
+ .stdlib-chart-sparkline-max { fill: var(--stdlib-chart-c2); stroke: none; }
989
+ .stdlib-chart-sparkline-min { fill: var(--k2b-danger-500); stroke: none; }
990
+ .stdlib-chart-point { stroke: var(--k2b-surface); stroke-width: 1.5; }
991
+ .stdlib-chart-slice { stroke: var(--k2b-surface); stroke-width: 2; stroke-linejoin: round; }
992
+ .stdlib-chart-label { font-size: 11px; fill: currentColor; }
993
+ .stdlib-chart-bar-value { font-size: 10px; fill: currentColor; opacity: 0.8; }
994
+ .stdlib-chart-errorbar { stroke: currentColor; opacity: 0.55; fill: none; stroke-width: 1; }
995
+ .stdlib-chart-error-band { fill-opacity: 0.15; stroke: none; }
996
+ .stdlib-chart-trendline { stroke: currentColor; opacity: 0.55; stroke-width: 1.5; fill: none; stroke-dasharray: 5 4; }
997
+ .stdlib-chart-minor-tick { stroke: currentColor; opacity: 0.25; }
998
+ .stdlib-chart-legend-swatch { stroke: none; }
999
+ .stdlib-chart-legend-label { font-size: 11px; font-weight: 400; fill: currentColor; stroke: none; }
1000
+ .stdlib-chart-empty-text { font-size: 11px; fill: currentColor; opacity: 0.5; }
1001
+ .stdlib-chart-gauge-track { stroke: currentColor; stroke-width: 10; opacity: 0.12; fill: none; stroke-linecap: round; }
1002
+ .stdlib-chart-gauge-fill { stroke-width: 10; fill: none; stroke-linecap: round; }
1003
+ .stdlib-chart-gauge-gradient-segment { stroke-width: 10; fill: none; stroke-linecap: butt; }
1004
+ .stdlib-chart-gauge-gradient-scale { opacity: 0.22; }
1005
+ .stdlib-chart-gauge-needle { stroke-width: 2; fill: none; stroke-linecap: round; }
1006
+ .stdlib-chart-gauge-hub { fill: currentColor; stroke: none; }
1007
+ .stdlib-chart-gauge-value { font-size: 28px; font-weight: 700; fill: currentColor; }
1008
+ .stdlib-chart-gauge-label { font-size: 12px; fill: currentColor; opacity: 0.72; }
1009
+ .stdlib-chart-gauge-unit { font-size: 11px; fill: currentColor; opacity: 0.6; }
1010
+
1011
+ /* ---------------------------------------------------------------- states */
1012
+
1013
+ :focus-visible {
1014
+ outline: var(--k2b-focus-width) solid var(--k2b-focus-ring);
1015
+ outline-offset: 2px;
1016
+ }
1017
+
1018
+ @media (pointer: coarse) {
1019
+ button,
1020
+ input:not([type="checkbox"], [type="radio"], [type="range"]),
1021
+ select,
1022
+ details > summary,
1023
+ nav :is(a, button) {
1024
+ min-height: 2.75rem;
1025
+ }
1026
+
1027
+ /* Icon-only buttons such as a row's ✕ get the full width too. */
1028
+ button,
1029
+ input:is([type="submit"], [type="button"], [type="reset"]),
1030
+ nav :is(a, button) {
1031
+ min-width: 2.75rem;
1032
+ }
1033
+
1034
+ /* The label is the tap target of a checklist row. */
1035
+ li:has(> input[type="checkbox"], > label > input[type="checkbox"]) > label {
1036
+ min-height: 2.75rem;
1037
+ }
1038
+
1039
+ input:is([type="checkbox"], [type="radio"]) {
1040
+ width: 1.375rem;
1041
+ height: 1.375rem;
1042
+ }
1043
+ }
1044
+
1045
+ [hidden] {
1046
+ display: none !important;
1047
+ }
1048
+
1049
+ @media (prefers-reduced-motion: reduce) {
1050
+ *,
1051
+ *::before,
1052
+ *::after {
1053
+ transition-duration: 0s !important;
1054
+ animation-duration: 0s !important;
1055
+ }
1056
+ }
1057
+
1058
+ @media print {
1059
+ /* Paper is white: a dark page prints with the light tokens. */
1060
+ :root,
1061
+ :root[data-theme="dark"] {
1062
+ color-scheme: light;
1063
+ }
1064
+
1065
+ html {
1066
+ background: #ffffff;
1067
+ color: #09090b;
1068
+ }
1069
+
1070
+ body {
1071
+ max-width: none;
1072
+ padding: 0;
1073
+ font-size: 10.5pt;
1074
+ }
1075
+
1076
+ button,
1077
+ input[type="file"],
1078
+ nav,
1079
+ dialog {
1080
+ display: none !important;
1081
+ }
1082
+
1083
+ tbody tr:hover {
1084
+ background: none;
1085
+ }
1086
+
1087
+ table,
1088
+ figure,
1089
+ .stat {
1090
+ break-inside: avoid;
1091
+ }
1092
+
1093
+ .k2b-chart__svg {
1094
+ max-height: none;
1095
+ }
1096
+ }
1097
+ }