@wtfalch/design 0.8.0 → 0.9.1

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.
@@ -8,7 +8,700 @@
8
8
  *
9
9
  * `scripts/copy-css.mjs` flattens these into one file at build time.
10
10
  */
11
- /* ---- ./base.css ---- */
11
+ /* ---- ./_tailwind.built.css ---- */
12
+ /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
13
+ @layer properties;
14
+ @layer theme, base, components, utilities;
15
+ @layer theme {
16
+ :root, :host {
17
+ --font-sans: var(--font-sans);
18
+ --font-mono: var(--font-mono);
19
+ --text-xs: var(--text-xs);
20
+ --text-sm: var(--text-sm);
21
+ --text-base: var(--text-base);
22
+ --text-lg: var(--text-lg);
23
+ --text-xl: var(--text-xl);
24
+ --font-weight-normal: var(--weight);
25
+ --tracking-normal: var(--tracking);
26
+ --radius-sm: var(--radius-sm);
27
+ --radius-md: var(--radius-md);
28
+ --radius-lg: var(--radius-lg);
29
+ --ease-out: cubic-bezier(0, 0, 0.2, 1);
30
+ --spacing-1: var(--space-1);
31
+ --spacing-2: var(--space-2);
32
+ --spacing-3: var(--space-3);
33
+ --spacing-4: var(--space-4);
34
+ --spacing-5: var(--space-5);
35
+ --spacing-6: var(--space-6);
36
+ --spacing-10: var(--space-10);
37
+ --text-2xs: var(--text-2xs);
38
+ --text-md: var(--text-md);
39
+ --font-weight-strong: var(--weight-strong);
40
+ --radius-pill: var(--radius-pill);
41
+ --color-text: var(--text);
42
+ --color-muted: var(--muted);
43
+ --color-border: var(--border);
44
+ --color-border-strong: var(--border-strong);
45
+ }
46
+ }
47
+ @layer utilities {
48
+ .collapse {
49
+ visibility: collapse;
50
+ }
51
+ .invisible {
52
+ visibility: hidden;
53
+ }
54
+ .visible {
55
+ visibility: visible;
56
+ }
57
+ .sr-only {
58
+ position: absolute;
59
+ width: 1px;
60
+ height: 1px;
61
+ padding: 0;
62
+ margin: -1px;
63
+ overflow: hidden;
64
+ clip-path: inset(50%);
65
+ white-space: nowrap;
66
+ border-width: 0;
67
+ }
68
+ .absolute {
69
+ position: absolute;
70
+ }
71
+ .fixed {
72
+ position: fixed;
73
+ }
74
+ .relative {
75
+ position: relative;
76
+ }
77
+ .sticky {
78
+ position: sticky;
79
+ }
80
+ .z-\[60\] {
81
+ z-index: 60;
82
+ }
83
+ .container {
84
+ width: 100%;
85
+ @media (width >= 40rem) {
86
+ max-width: 40rem;
87
+ }
88
+ @media (width >= 48rem) {
89
+ max-width: 48rem;
90
+ }
91
+ @media (width >= 64rem) {
92
+ max-width: 64rem;
93
+ }
94
+ @media (width >= 80rem) {
95
+ max-width: 80rem;
96
+ }
97
+ @media (width >= 96rem) {
98
+ max-width: 96rem;
99
+ }
100
+ }
101
+ .m-0 {
102
+ margin: 0px;
103
+ }
104
+ .m-auto {
105
+ margin: auto;
106
+ }
107
+ .mx-auto {
108
+ margin-inline: auto;
109
+ }
110
+ .mt-1 {
111
+ margin-top: var(--spacing-1);
112
+ }
113
+ .mb-3 {
114
+ margin-bottom: var(--spacing-3);
115
+ }
116
+ .ml-2 {
117
+ margin-left: var(--spacing-2);
118
+ }
119
+ .block {
120
+ display: block;
121
+ }
122
+ .contents {
123
+ display: contents;
124
+ }
125
+ .flex {
126
+ display: flex;
127
+ }
128
+ .grid {
129
+ display: grid;
130
+ }
131
+ .hidden {
132
+ display: none;
133
+ }
134
+ .inline {
135
+ display: inline;
136
+ }
137
+ .inline-flex {
138
+ display: inline-flex;
139
+ }
140
+ .table {
141
+ display: table;
142
+ }
143
+ .h-\[14px\] {
144
+ height: 14px;
145
+ }
146
+ .h-\[18px\] {
147
+ height: 18px;
148
+ }
149
+ .max-h-\[24rem\] {
150
+ max-height: 24rem;
151
+ }
152
+ .max-h-\[280px\] {
153
+ max-height: 280px;
154
+ }
155
+ .min-h-0 {
156
+ min-height: 0px;
157
+ }
158
+ .min-h-screen {
159
+ min-height: 100vh;
160
+ }
161
+ .w-\[18px\] {
162
+ width: 18px;
163
+ }
164
+ .w-full {
165
+ width: 100%;
166
+ }
167
+ .max-w-\[46ch\] {
168
+ max-width: 46ch;
169
+ }
170
+ .max-w-\[min\(20rem\,calc\(100vw-var\(--space-6\)\)\)\] {
171
+ max-width: min(20rem, calc(100vw - var(--space-6)));
172
+ }
173
+ .max-w-\[var\(--shell-measure\)\] {
174
+ max-width: var(--shell-measure);
175
+ }
176
+ .max-w-\[var\(--shell-measure-wide\)\] {
177
+ max-width: var(--shell-measure-wide);
178
+ }
179
+ .min-w-0 {
180
+ min-width: 0px;
181
+ }
182
+ .min-w-\[12rem\] {
183
+ min-width: 12rem;
184
+ }
185
+ .flex-1 {
186
+ flex: 1;
187
+ }
188
+ .flex-none {
189
+ flex: none;
190
+ }
191
+ .shrink {
192
+ flex-shrink: 1;
193
+ }
194
+ .grow {
195
+ flex-grow: 1;
196
+ }
197
+ .transform {
198
+ transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
199
+ }
200
+ .cursor-help {
201
+ cursor: help;
202
+ }
203
+ .cursor-pointer {
204
+ cursor: pointer;
205
+ }
206
+ .resize {
207
+ resize: both;
208
+ }
209
+ .list-none {
210
+ list-style-type: none;
211
+ }
212
+ .flex-col {
213
+ flex-direction: column;
214
+ }
215
+ .flex-wrap {
216
+ flex-wrap: wrap;
217
+ }
218
+ .items-baseline {
219
+ align-items: baseline;
220
+ }
221
+ .items-center {
222
+ align-items: center;
223
+ }
224
+ .items-start {
225
+ align-items: flex-start;
226
+ }
227
+ .justify-between {
228
+ justify-content: space-between;
229
+ }
230
+ .justify-items-center {
231
+ justify-items: center;
232
+ }
233
+ .gap-0 {
234
+ gap: 0px;
235
+ }
236
+ .gap-1 {
237
+ gap: var(--spacing-1);
238
+ }
239
+ .gap-2 {
240
+ gap: var(--spacing-2);
241
+ }
242
+ .gap-3 {
243
+ gap: var(--spacing-3);
244
+ }
245
+ .gap-4 {
246
+ gap: var(--spacing-4);
247
+ }
248
+ .gap-\[var\(--border-width\)\] {
249
+ gap: var(--border-width);
250
+ }
251
+ .self-center {
252
+ align-self: center;
253
+ }
254
+ .truncate {
255
+ overflow: hidden;
256
+ text-overflow: ellipsis;
257
+ white-space: nowrap;
258
+ }
259
+ .overflow-auto {
260
+ overflow: auto;
261
+ }
262
+ .overflow-hidden {
263
+ overflow: hidden;
264
+ }
265
+ .overflow-x-auto {
266
+ overflow-x: auto;
267
+ }
268
+ .overflow-y-auto {
269
+ overflow-y: auto;
270
+ }
271
+ .overscroll-contain {
272
+ overscroll-behavior: contain;
273
+ }
274
+ .rounded {
275
+ border-radius: 0.25rem;
276
+ }
277
+ .rounded-full {
278
+ border-radius: calc(infinity * 1px);
279
+ }
280
+ .rounded-md {
281
+ border-radius: var(--radius-md);
282
+ }
283
+ .rounded-sm {
284
+ border-radius: var(--radius-sm);
285
+ }
286
+ .border {
287
+ border-style: var(--tw-border-style);
288
+ border-width: var(--border-width);
289
+ }
290
+ .border-b {
291
+ border-bottom-style: var(--tw-border-style);
292
+ border-bottom-width: var(--border-width);
293
+ }
294
+ .border-dashed {
295
+ --tw-border-style: dashed;
296
+ border-style: dashed;
297
+ }
298
+ .border-border {
299
+ border-color: var(--color-border);
300
+ }
301
+ .border-border-strong {
302
+ border-color: var(--color-border-strong);
303
+ }
304
+ .p-0 {
305
+ padding: 0px;
306
+ }
307
+ .p-1 {
308
+ padding: var(--spacing-1);
309
+ }
310
+ .p-2 {
311
+ padding: var(--spacing-2);
312
+ }
313
+ .p-4 {
314
+ padding: var(--spacing-4);
315
+ }
316
+ .px-4 {
317
+ padding-inline: var(--spacing-4);
318
+ }
319
+ .py-2 {
320
+ padding-block: var(--spacing-2);
321
+ }
322
+ .py-3 {
323
+ padding-block: var(--spacing-3);
324
+ }
325
+ .py-5 {
326
+ padding-block: var(--spacing-5);
327
+ }
328
+ .py-6 {
329
+ padding-block: var(--spacing-6);
330
+ }
331
+ .py-10 {
332
+ padding-block: var(--spacing-10);
333
+ }
334
+ .pb-2 {
335
+ padding-bottom: var(--spacing-2);
336
+ }
337
+ .pb-3 {
338
+ padding-bottom: var(--spacing-3);
339
+ }
340
+ .text-center {
341
+ text-align: center;
342
+ }
343
+ .font-mono {
344
+ font-family: var(--font-mono);
345
+ }
346
+ .text-2xs {
347
+ font-size: var(--text-2xs);
348
+ }
349
+ .text-lg {
350
+ font-size: var(--text-lg);
351
+ }
352
+ .text-sm {
353
+ font-size: var(--text-sm);
354
+ }
355
+ .text-xs {
356
+ font-size: var(--text-xs);
357
+ }
358
+ .leading-\[0\] {
359
+ --tw-leading: 0;
360
+ line-height: 0;
361
+ }
362
+ .leading-\[1\.5\] {
363
+ --tw-leading: 1.5;
364
+ line-height: 1.5;
365
+ }
366
+ .leading-\[1\.25\] {
367
+ --tw-leading: 1.25;
368
+ line-height: 1.25;
369
+ }
370
+ .leading-\[16px\] {
371
+ --tw-leading: 16px;
372
+ line-height: 16px;
373
+ }
374
+ .font-normal {
375
+ --tw-font-weight: var(--font-weight-normal);
376
+ font-weight: var(--font-weight-normal);
377
+ }
378
+ .font-strong {
379
+ --tw-font-weight: var(--font-weight-strong);
380
+ font-weight: var(--font-weight-strong);
381
+ }
382
+ .tracking-normal {
383
+ --tw-tracking: var(--tracking-normal);
384
+ letter-spacing: var(--tracking-normal);
385
+ }
386
+ .text-ellipsis {
387
+ text-overflow: ellipsis;
388
+ }
389
+ .whitespace-nowrap {
390
+ white-space: nowrap;
391
+ }
392
+ .text-muted {
393
+ color: var(--color-muted);
394
+ }
395
+ .text-text {
396
+ color: var(--color-text);
397
+ }
398
+ .lowercase {
399
+ text-transform: lowercase;
400
+ }
401
+ .normal-case {
402
+ text-transform: none;
403
+ }
404
+ .uppercase {
405
+ text-transform: uppercase;
406
+ }
407
+ .tabular-nums {
408
+ --tw-numeric-spacing: tabular-nums;
409
+ font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
410
+ }
411
+ .opacity-60 {
412
+ opacity: 60%;
413
+ }
414
+ .opacity-\[0\.55\] {
415
+ opacity: 0.55;
416
+ }
417
+ .shadow {
418
+ --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
419
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
420
+ }
421
+ .ring {
422
+ --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
423
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
424
+ }
425
+ .outline {
426
+ outline-style: var(--tw-outline-style);
427
+ outline-width: 1px;
428
+ }
429
+ .blur {
430
+ --tw-blur: blur(8px);
431
+ filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
432
+ }
433
+ .sepia {
434
+ --tw-sepia: sepia(100%);
435
+ filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
436
+ }
437
+ .filter {
438
+ filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
439
+ }
440
+ .outline-none {
441
+ --tw-outline-style: none;
442
+ outline-style: none;
443
+ }
444
+ .\[font\:inherit\] {
445
+ font: inherit;
446
+ }
447
+ .surface-bg {
448
+ background: var(--bg);
449
+ }
450
+ .surface-panel {
451
+ background: var(--panel);
452
+ }
453
+ .disabled\:cursor-default:disabled {
454
+ cursor: default;
455
+ }
456
+ .disabled\:opacity-50:disabled {
457
+ opacity: 50%;
458
+ }
459
+ .\[\&\>p\]\:m-0 > p {
460
+ margin: 0px;
461
+ }
462
+ }
463
+ @property --tw-rotate-x {
464
+ syntax: "*";
465
+ inherits: false;
466
+ }
467
+ @property --tw-rotate-y {
468
+ syntax: "*";
469
+ inherits: false;
470
+ }
471
+ @property --tw-rotate-z {
472
+ syntax: "*";
473
+ inherits: false;
474
+ }
475
+ @property --tw-skew-x {
476
+ syntax: "*";
477
+ inherits: false;
478
+ }
479
+ @property --tw-skew-y {
480
+ syntax: "*";
481
+ inherits: false;
482
+ }
483
+ @property --tw-border-style {
484
+ syntax: "*";
485
+ inherits: false;
486
+ initial-value: solid;
487
+ }
488
+ @property --tw-leading {
489
+ syntax: "*";
490
+ inherits: false;
491
+ }
492
+ @property --tw-font-weight {
493
+ syntax: "*";
494
+ inherits: false;
495
+ }
496
+ @property --tw-tracking {
497
+ syntax: "*";
498
+ inherits: false;
499
+ }
500
+ @property --tw-ordinal {
501
+ syntax: "*";
502
+ inherits: false;
503
+ }
504
+ @property --tw-slashed-zero {
505
+ syntax: "*";
506
+ inherits: false;
507
+ }
508
+ @property --tw-numeric-figure {
509
+ syntax: "*";
510
+ inherits: false;
511
+ }
512
+ @property --tw-numeric-spacing {
513
+ syntax: "*";
514
+ inherits: false;
515
+ }
516
+ @property --tw-numeric-fraction {
517
+ syntax: "*";
518
+ inherits: false;
519
+ }
520
+ @property --tw-shadow {
521
+ syntax: "*";
522
+ inherits: false;
523
+ initial-value: 0 0 #0000;
524
+ }
525
+ @property --tw-shadow-color {
526
+ syntax: "*";
527
+ inherits: false;
528
+ }
529
+ @property --tw-shadow-alpha {
530
+ syntax: "<percentage>";
531
+ inherits: false;
532
+ initial-value: 100%;
533
+ }
534
+ @property --tw-inset-shadow {
535
+ syntax: "*";
536
+ inherits: false;
537
+ initial-value: 0 0 #0000;
538
+ }
539
+ @property --tw-inset-shadow-color {
540
+ syntax: "*";
541
+ inherits: false;
542
+ }
543
+ @property --tw-inset-shadow-alpha {
544
+ syntax: "<percentage>";
545
+ inherits: false;
546
+ initial-value: 100%;
547
+ }
548
+ @property --tw-ring-color {
549
+ syntax: "*";
550
+ inherits: false;
551
+ }
552
+ @property --tw-ring-shadow {
553
+ syntax: "*";
554
+ inherits: false;
555
+ initial-value: 0 0 #0000;
556
+ }
557
+ @property --tw-inset-ring-color {
558
+ syntax: "*";
559
+ inherits: false;
560
+ }
561
+ @property --tw-inset-ring-shadow {
562
+ syntax: "*";
563
+ inherits: false;
564
+ initial-value: 0 0 #0000;
565
+ }
566
+ @property --tw-ring-inset {
567
+ syntax: "*";
568
+ inherits: false;
569
+ }
570
+ @property --tw-ring-offset-width {
571
+ syntax: "<length>";
572
+ inherits: false;
573
+ initial-value: 0px;
574
+ }
575
+ @property --tw-ring-offset-color {
576
+ syntax: "*";
577
+ inherits: false;
578
+ initial-value: #fff;
579
+ }
580
+ @property --tw-ring-offset-shadow {
581
+ syntax: "*";
582
+ inherits: false;
583
+ initial-value: 0 0 #0000;
584
+ }
585
+ @property --tw-outline-style {
586
+ syntax: "*";
587
+ inherits: false;
588
+ initial-value: solid;
589
+ }
590
+ @property --tw-blur {
591
+ syntax: "*";
592
+ inherits: false;
593
+ }
594
+ @property --tw-brightness {
595
+ syntax: "*";
596
+ inherits: false;
597
+ }
598
+ @property --tw-contrast {
599
+ syntax: "*";
600
+ inherits: false;
601
+ }
602
+ @property --tw-grayscale {
603
+ syntax: "*";
604
+ inherits: false;
605
+ }
606
+ @property --tw-hue-rotate {
607
+ syntax: "*";
608
+ inherits: false;
609
+ }
610
+ @property --tw-invert {
611
+ syntax: "*";
612
+ inherits: false;
613
+ }
614
+ @property --tw-opacity {
615
+ syntax: "*";
616
+ inherits: false;
617
+ }
618
+ @property --tw-saturate {
619
+ syntax: "*";
620
+ inherits: false;
621
+ }
622
+ @property --tw-sepia {
623
+ syntax: "*";
624
+ inherits: false;
625
+ }
626
+ @property --tw-drop-shadow {
627
+ syntax: "*";
628
+ inherits: false;
629
+ }
630
+ @property --tw-drop-shadow-color {
631
+ syntax: "*";
632
+ inherits: false;
633
+ }
634
+ @property --tw-drop-shadow-alpha {
635
+ syntax: "<percentage>";
636
+ inherits: false;
637
+ initial-value: 100%;
638
+ }
639
+ @property --tw-drop-shadow-size {
640
+ syntax: "*";
641
+ inherits: false;
642
+ }
643
+ @layer properties {
644
+ @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
645
+ *, ::before, ::after, ::backdrop {
646
+ --tw-rotate-x: initial;
647
+ --tw-rotate-y: initial;
648
+ --tw-rotate-z: initial;
649
+ --tw-skew-x: initial;
650
+ --tw-skew-y: initial;
651
+ --tw-border-style: solid;
652
+ --tw-leading: initial;
653
+ --tw-font-weight: initial;
654
+ --tw-tracking: initial;
655
+ --tw-ordinal: initial;
656
+ --tw-slashed-zero: initial;
657
+ --tw-numeric-figure: initial;
658
+ --tw-numeric-spacing: initial;
659
+ --tw-numeric-fraction: initial;
660
+ --tw-shadow: 0 0 #0000;
661
+ --tw-shadow-color: initial;
662
+ --tw-shadow-alpha: 100%;
663
+ --tw-inset-shadow: 0 0 #0000;
664
+ --tw-inset-shadow-color: initial;
665
+ --tw-inset-shadow-alpha: 100%;
666
+ --tw-ring-color: initial;
667
+ --tw-ring-shadow: 0 0 #0000;
668
+ --tw-inset-ring-color: initial;
669
+ --tw-inset-ring-shadow: 0 0 #0000;
670
+ --tw-ring-inset: initial;
671
+ --tw-ring-offset-width: 0px;
672
+ --tw-ring-offset-color: #fff;
673
+ --tw-ring-offset-shadow: 0 0 #0000;
674
+ --tw-outline-style: solid;
675
+ --tw-blur: initial;
676
+ --tw-brightness: initial;
677
+ --tw-contrast: initial;
678
+ --tw-grayscale: initial;
679
+ --tw-hue-rotate: initial;
680
+ --tw-invert: initial;
681
+ --tw-opacity: initial;
682
+ --tw-saturate: initial;
683
+ --tw-sepia: initial;
684
+ --tw-drop-shadow: initial;
685
+ --tw-drop-shadow-color: initial;
686
+ --tw-drop-shadow-alpha: 100%;
687
+ --tw-drop-shadow-size: initial;
688
+ }
689
+ }
690
+ }
691
+
692
+ /* ---- ./_system-light.css ---- */
693
+ /* The `system` theme's light half.
694
+ *
695
+ * Its own file, and outside the components layer, because it is a *theme*
696
+ * rather than a component rule. `tokens.css` states the dark base on `:root`
697
+ * and is unlayered; the moment this sat inside `@layer components` it lost to
698
+ * that, and every `system` specimen rendered dark on a light-mode machine.
699
+ * Unlayered beats layered whatever the selector says, which is exactly the
700
+ * trap this repository keeps finding in different costumes.
701
+ *
702
+ * The underscore marks it the same way `_tailwind.built.css` is marked:
703
+ * `copy-css.mjs` passes both through without wrapping them.
704
+ */
12
705
  /* Light, only under the `system` theme. This used to override `:root`
13
706
  unconditionally, so choosing a dark theme on a light-mode laptop got
14
707
  silently repainted. Every other theme states its own palette. */
@@ -37,6 +730,8 @@
37
730
  }
38
731
  }
39
732
 
733
+ /* ---- ./base.css ---- */
734
+ @layer components {
40
735
  /* Derived from the two surfaces every theme already defines, so a theme gets a
41
736
  control colour without knowing this exists. */
42
737
  /* Pseudo-elements too. `*` does not match them, and `box-sizing` is not
@@ -101,23 +796,31 @@ body::before {
101
796
  `[aria-disabled='true']` twin rather than a rewrite, which leaves each
102
797
  `button` selector at the specificity it had and every committed baseline
103
798
  untouched. */
104
- a.btn {
799
+ .btn {
800
+ /* The three an anchor brings that a `<button>` does not. Harmless on the
801
+ button form -- it has no underline and centres already -- and the reason
802
+ `Button` puts one class on both elements rather than keeping a twin
803
+ selector for each. */
105
804
  text-decoration: none;
106
805
  color: var(--text);
107
806
  text-align: center;
108
807
  }
109
808
 
110
- a.btn[aria-disabled='true'] {
809
+ .btn[aria-disabled='true'] {
111
810
  /* A disabled anchor still takes the pointer, so say so. The click itself is
112
811
  blocked in the capture phase by the component. */
113
812
  cursor: not-allowed;
114
813
  }
115
814
 
116
- button,
117
- a.btn,
118
- input,
119
- textarea,
120
- select {
815
+ /* `:where`, so this base carries no specificity of its own.
816
+ *
817
+ * It was `button, input, textarea, select` -- an element selector, which
818
+ * `.size-sm` and every other class outranked without anyone thinking about
819
+ * it. Moving it to `.btn` to stop a tab inheriting a control's look raised it
820
+ * to a class, so it tied with `.size-sm` and won on source order: every
821
+ * button came out `8px 12px` whatever size it asked for. `:where` puts it
822
+ * back to zero, which is what the element selector was silently providing. */
823
+ :where(.btn, input, textarea, select) {
121
824
  font: inherit;
122
825
  color: var(--text);
123
826
  background: var(--control);
@@ -138,8 +841,7 @@ select {
138
841
  the icon; the button simply had no opinion about how to arrange two things.
139
842
  `inline-flex` and a gap gives it one, and the gap replaces the space
140
843
  character people were putting between an icon and its label. */
141
- button,
142
- a.btn {
844
+ .btn {
143
845
  display: inline-flex;
144
846
  align-items: center;
145
847
  justify-content: center;
@@ -155,18 +857,41 @@ a.btn {
155
857
 
156
858
  Several places already drew this by hand as `.chat-bar-btn.on`. This is the
157
859
  same look, said once, on a plain button. */
158
- button[aria-pressed='true'] {
860
+ .btn[aria-pressed='true'] {
159
861
  border-color: var(--accent);
160
862
  color: var(--accent);
161
863
  background: color-mix(in srgb, var(--accent) 10%, var(--panel));
162
864
  }
163
865
 
164
- button[aria-pressed='true']:hover:not(:disabled) { border-color: var(--accent); }
866
+ .btn[aria-pressed='true']:hover:not(:disabled) { border-color: var(--accent); }
165
867
 
166
868
  /* The glyph never shrinks: in a narrow row flexbox would take it from the icon
167
869
  before the text, and half an icon is not a smaller icon. */
168
- button > svg,
169
- a.btn > svg { flex: none; }
870
+ .btn > svg { flex: none; }
871
+
872
+ /* `Select`'s control wears the control styling, and says so.
873
+ *
874
+ * It renders React Aria's `Button` -- a bare `<button>` -- and used to take
875
+ * `font`, colour, the surface and the border from the
876
+ * `button, input, textarea, select` reset without asking. When that reset
877
+ * moved to `.btn`, so a tab and a row would stop inheriting a control's look
878
+ * only to undo it, this lost all of it at once.
879
+ *
880
+ * Here rather than in `select.css`, and that placement is the rule, not
881
+ * tidiness: `font` is a shorthand and it resets `font-size`, so declared
882
+ * after `.size-sm` below it silently makes every small control full size.
883
+ * `index.css` says the import order is behaviour; this is what that means.
884
+ *
885
+ * `:where` for the same reason the reset above uses it: this is a base a
886
+ * `size` class has to be able to beat, and a bare class ties with one. */
887
+ :where(.sel-control) {
888
+ font: inherit;
889
+ color: var(--text);
890
+ background: var(--control);
891
+ border: var(--border-width) solid var(--border-strong);
892
+ border-radius: var(--radius);
893
+ cursor: pointer;
894
+ }
170
895
 
171
896
  .size-sm { font-size: var(--text-xs); padding: var(--space-1) var(--space-2); }
172
897
 
@@ -191,8 +916,7 @@ input, textarea, select { width: 100%; }
191
916
  same disagreement written twice. */
192
917
  textarea { resize: none; }
193
918
 
194
- button:hover:not(:disabled),
195
- a.btn:hover:not([aria-disabled='true']),
919
+ .btn:hover:not(:disabled),
196
920
  input:hover:not(:disabled),
197
921
  textarea:hover:not(:disabled),
198
922
  select:hover:not(:disabled) { border-color: var(--accent); }
@@ -200,8 +924,7 @@ select:hover:not(:disabled) { border-color: var(--accent); }
200
924
  /* `:focus-visible` on the button so a click does not leave a ring behind it,
201
925
  `:focus` on the fields because a focused field is a place you are typing and
202
926
  should say so however you got there. */
203
- button:focus-visible,
204
- a.btn:focus-visible,
927
+ .btn:focus-visible,
205
928
  input:focus,
206
929
  textarea:focus,
207
930
  select:focus { outline: none; border-color: var(--accent); }
@@ -294,14 +1017,13 @@ input[type='radio']:focus-visible { outline: none; box-shadow: var(--focus-ring)
294
1017
  input[type='checkbox']:disabled,
295
1018
  input[type='radio']:disabled { cursor: not-allowed; background: var(--panel-2); }
296
1019
 
297
- button:disabled,
298
- a.btn[aria-disabled='true'],
1020
+ .btn:disabled,
1021
+ .btn[aria-disabled='true'],
299
1022
  input:disabled,
300
1023
  textarea:disabled,
301
1024
  select:disabled { opacity: 0.45; cursor: not-allowed; }
302
1025
 
303
- button.primary,
304
- a.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
1026
+ .btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
305
1027
 
306
1028
  /* A disabled primary is not a faded primary.
307
1029
  `opacity: .45` was the whole of it, and a coloured fill at 45% is still a
@@ -309,8 +1031,7 @@ a.btn.primary { background: var(--accent); border-color: var(--accent); color: v
309
1031
  thing on the panel. That got worse when onboarding started holding people at
310
1032
  a disabled Next. So it gives up the fill entirely and becomes the shape of a
311
1033
  disabled control -- the opacity then has nothing left to do. */
312
- button.primary:disabled,
313
- a.btn.primary[aria-disabled='true'] {
1034
+ .btn.primary:disabled {
314
1035
  background: var(--panel-2);
315
1036
  border-color: var(--border);
316
1037
  color: var(--muted);
@@ -324,7 +1045,7 @@ a.btn.primary[aria-disabled='true'] {
324
1045
  line under it that travels.
325
1046
  `aria-busy` rather than a class: the attribute is what a screen reader reads,
326
1047
  and styling the thing that carries the meaning is how they stay in step. */
327
- button[aria-busy='true'] {
1048
+ .btn[aria-busy='true'] {
328
1049
  position: relative;
329
1050
  overflow: hidden;
330
1051
  cursor: progress;
@@ -337,18 +1058,18 @@ button[aria-busy='true'] {
337
1058
  something that is working, that reads as a control that failed and went
338
1059
  grey. So a busy button keeps every colour it had and takes only the loss of
339
1060
  the cursor and the press; the travelling line is what says why. */
340
- button[aria-busy='true']:disabled {
1061
+ .btn[aria-busy='true']:disabled {
341
1062
  opacity: 1;
342
1063
  cursor: progress;
343
1064
  }
344
1065
 
345
- button.primary[aria-busy='true']:disabled {
1066
+ .btn.primary[aria-busy='true']:disabled {
346
1067
  background: var(--accent);
347
1068
  border-color: var(--accent);
348
1069
  color: var(--on-accent);
349
1070
  }
350
1071
 
351
- button[aria-busy='true']::after {
1072
+ .btn[aria-busy='true']::after {
352
1073
  content: '';
353
1074
  position: absolute;
354
1075
  left: 0;
@@ -380,15 +1101,13 @@ button[aria-busy='true']::after {
380
1101
 
381
1102
  /* Quiet: no border, no fill. For the action beside the action -- Cancel next to
382
1103
  Save -- where a bordered box competes with the thing it is next to. */
383
- button.ghost,
384
- a.btn.ghost {
1104
+ .btn.ghost {
385
1105
  background: transparent;
386
1106
  border-color: transparent;
387
1107
  color: var(--muted);
388
1108
  }
389
1109
 
390
- button.ghost:hover:not(:disabled),
391
- a.btn.ghost:hover:not([aria-disabled='true']) {
1110
+ .btn.ghost:hover:not(:disabled) {
392
1111
  background: var(--panel-2);
393
1112
  border-color: transparent;
394
1113
  color: var(--text);
@@ -470,7 +1189,7 @@ a.btn.ghost:hover:not([aria-disabled='true']) {
470
1189
  header.) */
471
1190
  .row { display: flex; align-items: center; gap: var(--space-3); }
472
1191
 
473
- .grow { flex: 1; min-width: 0; }
1192
+ .ctl-grow { flex: 1; min-width: 0; }
474
1193
 
475
1194
  .mono { font-family: var(--font-mono); font-size: var(--text-sm); }
476
1195
 
@@ -534,10 +1253,17 @@ a.btn.ghost:hover:not([aria-disabled='true']) {
534
1253
  /* A pill following a name needs the gap outside its own border. Leading
535
1254
  whitespace inside the span puts it under the pill's background instead,
536
1255
  which reads as the badge touching the word it qualifies. */
537
- .row-pill { margin-left: var(--space-2); vertical-align: baseline; }
1256
+ /* A pill beside a name, and the one thing that keeps the pair working:
1257
+ `flex: none`, so the pill never shrinks and the name ellipsises instead.
1258
+ That was `rows.css`'s `.named > .pill`, which meant anybody laying out a
1259
+ name and its pills had to know a package class by name to get it. The
1260
+ variant already says "I am in a row"; it may as well say what that costs. */
1261
+ .row-pill { margin-left: var(--space-2); vertical-align: baseline; flex: none; }
538
1262
 
539
1263
  /* The tile's only control. Dim until hovered: it is chrome on someone's app. */
540
1264
  .icon-btn {
1265
+ /* Stated rather than inherited. These came from the `button, input, textarea, select` reset, which applied control styling to everything that happened to be a `<button>` -- so this rule spent its opening lines undoing it, and would break silently if that rule ever moved. It did move, to `.btn`. */
1266
+ font: inherit;
541
1267
  display: inline-flex; align-items: center; padding: var(--space-1);
542
1268
  background: none; border: 0; color: var(--muted); opacity: 0.6; cursor: pointer;
543
1269
  }
@@ -603,19 +1329,17 @@ a.btn.ghost:hover:not([aria-disabled='true']) {
603
1329
  transition: none;
604
1330
  }
605
1331
 
606
- button.primary:active:not(:disabled),
607
- a.btn.primary:active:not([aria-disabled='true']) {
1332
+ .btn.primary:active:not(:disabled) {
608
1333
  background: color-mix(in srgb, var(--text) var(--press-ink), var(--accent));
609
1334
  border-color: color-mix(in srgb, var(--text) var(--press-ink), var(--accent));
610
1335
  }
611
1336
 
612
- button.ghost:active:not(:disabled),
613
- a.btn.ghost:active:not([aria-disabled='true']) {
1337
+ .btn.ghost:active:not(:disabled) {
614
1338
  background: color-mix(in srgb, var(--text) var(--press-ink), var(--panel-2));
615
1339
  border-color: transparent;
616
1340
  }
617
1341
 
618
- button[aria-pressed='true']:active:not(:disabled) {
1342
+ .btn[aria-pressed='true']:active:not(:disabled) {
619
1343
  background: color-mix(in srgb, var(--accent) 18%, var(--panel));
620
1344
  }
621
1345
 
@@ -786,7 +1510,7 @@ input:active, textarea:active, select:active { transform: none; }
786
1510
 
787
1511
  /* The top bar carries the title, the views and the way into settings. Nothing
788
1512
  that belongs to a conversation lives here any more, so it can breathe. */
789
- .topbar .grow { flex: 1; min-width: 0; }
1513
+ .topbar .ctl-grow { flex: 1; min-width: 0; }
790
1514
 
791
1515
  /* Back on the left, Next on the right, with the gap between them doing the
792
1516
  separating -- so the button that goes forward is in the same place on both
@@ -818,7 +1542,60 @@ input:active, textarea:active, select:active { transform: none; }
818
1542
  to { opacity: 1; }
819
1543
  }
820
1544
 
1545
+ /* The states the `button` element rule used to hand every bare `<button>` in
1546
+ the package, now asked for by name.
1547
+ *
1548
+ * This is the half of the decoupling that a screenshot cannot show: `:focus-visible`
1549
+ * is not photographed, so nine components would have lost their focus ring in
1550
+ * silence. `keyboard.spec.ts` is what actually holds this.
1551
+ *
1552
+ * `.btn` has its own, in the rules above. These are the ones that are a button
1553
+ * for their behaviour and not for their appearance -- a tab, a row, a card, a
1554
+ * pager step, a grid cell, a remove cross. */
1555
+ .tab:focus-visible,
1556
+ .row-hit:focus-visible,
1557
+ .card:focus-visible,
1558
+ .ident-remove:focus-visible,
1559
+ .size-cell:focus-visible,
1560
+ .pager-step:focus-visible,
1561
+ .pager-page:focus-visible,
1562
+ .sel-control:focus-visible {
1563
+ outline: 2px solid var(--accent);
1564
+ outline-offset: 2px;
1565
+ }
1566
+
1567
+ /* `:disabled` came from the same rule. A control that cannot be pressed has to
1568
+ say so whatever element it is. */
1569
+ .tab:disabled,
1570
+ .row-hit:disabled,
1571
+ .card:disabled,
1572
+ .ident-remove:disabled,
1573
+ .size-cell:disabled,
1574
+ .pager-step:disabled,
1575
+ .pager-page:disabled,
1576
+ .sel-control:disabled {
1577
+ opacity: 0.45;
1578
+ cursor: not-allowed;
1579
+ }
1580
+
1581
+ /* The anchor form's disabled states, alongside the button's.
1582
+ *
1583
+ * `.btn` is one class on two elements, and an `<a>` has no `:disabled` -- it
1584
+ * carries `aria-disabled` and a capture-phase click block instead. Every
1585
+ * rule above that reads `:disabled` needs the attribute twin, or a disabled
1586
+ * link is a link that looks pressable and refuses silently. Folding the old
1587
+ * `a.btn` twins into `.btn` dropped these, and the baselines showed a
1588
+ * disabled link at full strength. */
1589
+ .btn.primary[aria-disabled='true'] {
1590
+ background: var(--panel-2);
1591
+ border-color: var(--border);
1592
+ color: var(--muted);
1593
+ opacity: 1;
1594
+ }
1595
+ }
1596
+
821
1597
  /* ---- ./select.css ---- */
1598
+ @layer components {
822
1599
  /* How big a control is, asked for rather than inherited.
823
1600
  Four sizes existed and every one of them came from a descendant selector:
824
1601
  `.set-row button` was 11px in 3px of padding, `.approval-actions button` was
@@ -833,8 +1610,7 @@ input:active, textarea:active, select:active { transform: none; }
833
1610
  /* Full width, for the one action a card or a step is about. Not a size: a
834
1611
  `block` button is the same height as any other, it just stops being a thing
835
1612
  you have to aim at. */
836
- button.block,
837
- a.btn.block { display: flex; width: 100%; }
1613
+ .btn.ctl-block { display: flex; width: 100%; }
838
1614
 
839
1615
  /* The default is `size-md`, spelled once for every control.
840
1616
  The paragraph above promised one scale for buttons, inputs and selects, and
@@ -842,13 +1618,24 @@ a.btn.block { display: flex; width: 100%; }
842
1618
  input was eight pixels taller than the button beside it and no row of the two
843
1619
  could line up. The download modal's search box against its Search button was
844
1620
  the version you could see from across the room. */
845
- button,
846
- a.btn,
847
- input,
848
- textarea,
849
- select,
1621
+ /* `:where`, so this base carries no specificity of its own.
1622
+ *
1623
+ * It was `button, input, textarea, select` -- an element selector, which
1624
+ * `.size-sm` and every other class outranked without anyone thinking about
1625
+ * it. Moving it to `.btn` to stop a tab inheriting a control's look raised it
1626
+ * to a class, so it tied with `.size-sm` and won on source order: every
1627
+ * button came out `8px 12px` whatever size it asked for. `:where` puts it
1628
+ * back to zero, which is what the element selector was silently providing. */
1629
+ :where(.btn, input, textarea, select) { padding: var(--space-2) var(--space-3); }
1630
+
1631
+ /* The select's control keeps its padding at full class weight, and after
1632
+ `.size-sm` in the cascade, because that is where it has always been: this
1633
+ file is imported after `base.css`, so a `size`d select took the size's type
1634
+ and this padding. Folding it into the `:where` above handed the padding
1635
+ back to `.size-sm` and shrank every small select by seven pixels. */
850
1636
  .sel-control { padding: var(--space-2) var(--space-3); }
851
1637
 
1638
+
852
1639
  /* Centred on the whole block -- the caption and the sentence together -- rather
853
1640
  than aligned to either end of it. `.field-row` pins to the bottom, which is
854
1641
  right for a field beside a button and wrong beside two lines of prose. */
@@ -872,16 +1659,16 @@ select,
872
1659
  .sel { position: relative; display: inline-block; }
873
1660
 
874
1661
  /* The subject of its pane rather than a field in it: full width and a size up.
875
- That was `.select-field.block` on the native element, and it carried the
1662
+ That was `.select-field.ctl-block` on the native element, and it carried the
876
1663
  reason with it -- model ids are long (`qwen2.5-coder:32b-instruct-q4_K_M`) and
877
1664
  at the narrow size they truncated mid-name. */
878
- .sel.block { display: block; width: 100%; }
1665
+ .sel.ctl-block { display: block; width: 100%; }
879
1666
 
880
- .sel.block .sel-control { padding: var(--space-2) var(--space-3); font-weight: 500; }
1667
+ .sel.ctl-block .sel-control { padding: var(--space-2) var(--space-3); font-weight: 500; }
881
1668
 
882
1669
  /* Not full width otherwise: a select as wide as its row, holding one short
883
1670
  word, reads as an input somebody forgot to fill in. */
884
- .sel:not(.block) .sel-control { width: auto; max-width: 320px; }
1671
+ .sel:not(.ctl-block) .sel-control { width: auto; max-width: 320px; }
885
1672
 
886
1673
  .sel-control {
887
1674
  display: flex;
@@ -894,9 +1681,7 @@ select,
894
1681
  background: var(--control);
895
1682
  }
896
1683
 
897
- .sel-value { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
898
-
899
- .sel-caret { flex: none; opacity: 0.6; transition: transform var(--dur-fast) var(--ease); }
1684
+ .sel-caret { transition: transform var(--dur-fast) var(--ease); }
900
1685
 
901
1686
  .sel[data-open] .sel-caret { transform: rotate(180deg); }
902
1687
 
@@ -905,45 +1690,22 @@ select,
905
1690
  /* State is read off React Aria's attributes: `data-open` on the root,
906
1691
  `data-focused` / `data-selected` / `data-disabled` on an item, where
907
1692
  `.open`, `[data-active]`, `.on` and `.off` used to be. */
1693
+ /* The list keeps two things the utilities on `Select` cannot say.
1694
+ *
1695
+ * `scroll-padding-block`: the same room while the list is scrolled as it has
1696
+ * at rest. Padding at the top of a scroll container scrolls away with the
1697
+ * content, so the row `scrollIntoView({block: 'nearest'})` brings up ended
1698
+ * flush against the border and read as clipped rather than as one of a set --
1699
+ * and this list opens scrolled to whatever is chosen, so that was the
1700
+ * ordinary state.
1701
+ *
1702
+ * `box-shadow`: `--shadow-2` is a token, and a utility would have to inline
1703
+ * the value, which is the one thing the vocabulary exists to stop. */
908
1704
  .sel-list {
909
- /* `fixed`, and in `document.body`: an ancestor that scrolls or hides its
910
- overflow would otherwise clip it, which in this app means every settings
911
- pane. Position comes from the button's measured rectangle. */
912
- /* Positioned by React Aria -- inline `top`/`left` on a portalled popover
913
- that flips when the edge is near. `position: fixed` and the measured
914
- rectangle it was placed by are gone with the hand-rolled version. */
915
- z-index: 60;
916
- max-height: 280px;
917
- overflow-y: auto;
918
- margin: 0;
919
- padding: var(--space-1);
920
- /* The same room while the list is scrolled as it has at rest. Padding at
921
- the top of a scroll container scrolls away with the content, so the row
922
- `scrollIntoView({block: 'nearest'})` brings up ended flush against the
923
- border and read as clipped rather than as one of a set -- and this list
924
- opens scrolled to whatever is chosen, so that was the ordinary state.
925
- `scroll-padding` insets what counts as "in view", so it stops short by
926
- the same four the sides have. */
927
1705
  scroll-padding-block: var(--space-1);
928
- list-style: none;
929
- border: var(--border-width) solid var(--border-strong);
930
- border-radius: var(--radius-md);
931
- background: var(--panel);
932
1706
  box-shadow: var(--shadow-2);
933
1707
  }
934
1708
 
935
- .sel-item {
936
- display: flex;
937
- align-items: center;
938
- gap: var(--space-2);
939
- padding: var(--space-2) var(--space-2);
940
- border-radius: var(--radius-sm);
941
- cursor: pointer;
942
- white-space: nowrap;
943
- }
944
-
945
- .sel-item-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
946
-
947
1709
  /* Hover and keyboard land on the same row, because they are the same idea --
948
1710
  two highlights for one cursor is how a list starts lying about where Enter
949
1711
  will go. */
@@ -955,7 +1717,18 @@ select,
955
1717
 
956
1718
  /* The tick marks the chosen row. Colour alone would not: a list where the only
957
1719
  difference is a hue is a list you cannot read past. */
958
- .sel-tick { flex: none; opacity: 0; }
1720
+ /* The tick's resting state lives here, beside the state that overrides it.
1721
+ *
1722
+ * It was `opacity-0` on the element and `opacity: 1` here, which worked only
1723
+ * while this sheet was unlayered. A utility is in `@layer utilities` and
1724
+ * beats `@layer components` whatever the selector, so the moment the sheets
1725
+ * moved into a layer the tick stopped appearing on the selected row -- the
1726
+ * resting state won permanently.
1727
+ *
1728
+ * The rule generalises: a utility for a resting state whose active state is a
1729
+ * relationship between elements will always beat that active state. The pair
1730
+ * has to sit in the same layer. */
1731
+ .sel-tick { opacity: 0; }
959
1732
 
960
1733
  .sel-item[data-selected] .sel-tick { opacity: 1; }
961
1734
 
@@ -991,24 +1764,23 @@ select,
991
1764
  .field > input,
992
1765
  .field > textarea,
993
1766
  .field > .sel { width: 100%; }
1767
+ }
994
1768
 
995
1769
  /* ---- ./dangerzone.css ---- */
1770
+ @layer components {
996
1771
  /* Destructive, and legible as such standing still. */
997
- button.danger,
998
- a.btn.danger {
1772
+ .btn.danger {
999
1773
  color: var(--bad);
1000
1774
  border-color: color-mix(in srgb, var(--bad) 45%, var(--border-strong));
1001
1775
  }
1002
1776
 
1003
- button.danger:hover:not(:disabled),
1004
- a.btn.danger:hover:not([aria-disabled='true']) {
1777
+ .btn.danger:hover:not(:disabled) {
1005
1778
  background: color-mix(in srgb, var(--bad) 12%, var(--panel));
1006
1779
  border-color: var(--bad);
1007
1780
  color: var(--bad);
1008
1781
  }
1009
1782
 
1010
- button.danger:active:not(:disabled),
1011
- a.btn.danger:active:not([aria-disabled='true']) {
1783
+ .btn.danger:active:not(:disabled) {
1012
1784
  background: color-mix(in srgb, var(--bad) 20%, var(--panel));
1013
1785
  border-color: var(--bad);
1014
1786
  }
@@ -1107,8 +1879,10 @@ a.btn.danger:active:not([aria-disabled='true']) {
1107
1879
  controls in the same column at the same indent that do not line up read as
1108
1880
  an accident rather than as a pair. */
1109
1881
  .field-row > button { min-width: 9ch; }
1882
+ }
1110
1883
 
1111
1884
  /* ---- ./callout.css ---- */
1885
+ @layer components {
1112
1886
  /* One shape for everything that tells you something.
1113
1887
  `.banner` and `.callout` were the same box drawn twice: a bordered, rounded,
1114
1888
  12px panel with a tint. They differed in padding by a pixel in each direction
@@ -1247,35 +2021,50 @@ a.btn.danger:active:not([aria-disabled='true']) {
1247
2021
  }
1248
2022
 
1249
2023
  .callout button { flex: none; }
2024
+ }
1250
2025
 
1251
2026
  /* ---- ./card.css ---- */
2027
+ @layer components {
2028
+ /* `.card` is public API, not this component's private styling.
2029
+ *
2030
+ * Consumers apply it to their own markup -- the gallery's skeleton specimen
2031
+ * renders `<div className="card">` around three rows, and app-template uses
2032
+ * `card-pick`. Moving the box into `Card`'s own class list took that away
2033
+ * with no type error and no failing test in this package: it broke only
2034
+ * where somebody else had used the class, which is the worst way for an API
2035
+ * to go.
2036
+ *
2037
+ * So the rule stays and `Card` wears it. The lesson generalises to every
2038
+ * class in this package that a consumer can reach: a component may draw
2039
+ * itself with utilities only where its class is genuinely private. */
1252
2040
  .card {
2041
+ display: block;
2042
+ width: 100%;
2043
+ box-sizing: border-box;
2044
+ font: inherit;
2045
+ color: var(--text);
2046
+ text-align: left;
1253
2047
  background: var(--panel);
1254
2048
  border: var(--border-width) solid var(--border);
1255
2049
  border-radius: var(--radius-md);
1256
- padding: var(--space-3) var(--space-3);
2050
+ padding: var(--space-3);
1257
2051
  margin-bottom: var(--space-3);
1258
2052
  }
1259
2053
 
2054
+ /* What is left once `Card` draws its own box: the tones, the pick and
2055
+ selected states, and the rules that reach from the card into what it
2056
+ holds. `:has(.card-icon) .card-inline` is a relationship, and a tone is a
2057
+ `color-mix`; neither is a property on one element. */
2058
+
1260
2059
  /* Header, when there is one. The title and the line under it were retyped at
1261
2060
  every call site and no two agreed on the gap between them. */
1262
- .card-head { display: flex; align-items: flex-start; gap: var(--space-3); }
1263
-
1264
- .card-head:not(:last-child) { margin-bottom: var(--space-3); }
1265
-
1266
- .card-headings { min-width: 0; flex: 1; }
1267
-
1268
2061
  /* The card owns the space between the things it holds. Without this a callout
1269
2062
  and the button under it sit edge to edge, and every caller has to remember a
1270
2063
  margin on whichever child happens to be second. */
1271
- .card-body { display: grid; gap: var(--space-3); }
1272
-
1273
2064
  /* The card's own control, on the card's padding rather than indented into its
1274
2065
  body. Centred against the whole heading block, not against the title -- a
1275
2066
  switch aligned to the first line drifts upward the moment the description
1276
2067
  runs to two. */
1277
- .card-action { flex: none; align-self: center; display: flex; align-items: center; }
1278
-
1279
2068
  /* A row inside a card that lines up with the header rather than indenting past
1280
2069
  it: same left edge as the title, same right edge as the action. Its label is
1281
2070
  a quiet caption, not a second heading -- the card already has one, and two
@@ -1322,17 +2111,6 @@ a.btn.danger:active:not([aria-disabled='true']) {
1322
2111
  color: var(--muted);
1323
2112
  }
1324
2113
 
1325
- /* A card that is itself the choice. Reset first, because it is a `<button>`
1326
- now and inherits a control's padding and centring. */
1327
- button.card {
1328
- display: block;
1329
- width: 100%;
1330
- text-align: left;
1331
- font: inherit;
1332
- color: inherit;
1333
- padding: var(--space-3);
1334
- }
1335
-
1336
2114
  /* Variants, because a card is doing four jobs and looked like one.
1337
2115
  A search result you can act on, a thing already installed, something that
1338
2116
  went wrong, and a heading with rows under it are not the same object, and
@@ -1375,7 +2153,23 @@ button.card {
1375
2153
 
1376
2154
  .modal-body .card { background: var(--panel-2); }
1377
2155
 
2156
+ /* Selected, which used to arrive from `button[aria-pressed='true']`.
2157
+ *
2158
+ * A card marks its choice with `aria-pressed`, and the tint came from the
2159
+ * element rule every bare `<button>` in the package was picking up. That rule
2160
+ * belongs to `Button` now, so the card says what being chosen looks like --
2161
+ * which is the right place for it anyway: a chosen card is a card's idea, not
2162
+ * a button's. */
2163
+ .card[aria-pressed='true'] {
2164
+ border-color: var(--accent);
2165
+ background: color-mix(in srgb, var(--accent) 10%, var(--panel));
2166
+ }
2167
+
2168
+ .card[aria-pressed='true']:hover { border-color: var(--accent); }
2169
+ }
2170
+
1378
2171
  /* ---- ./toggle.css ---- */
2172
+ @layer components {
1379
2173
  .card-action .switch-row { padding: 0; gap: var(--space-2); }
1380
2174
 
1381
2175
  .card-action .switch-row .switch-body { flex: none; }
@@ -1606,8 +2400,10 @@ input[type='radio']:active:not(:disabled) {
1606
2400
  @media (prefers-reduced-motion: reduce) {
1607
2401
  .toggle-sweep::before { animation: none; background: color-mix(in oklab, var(--accent) 45%, transparent); }
1608
2402
  }
2403
+ }
1609
2404
 
1610
2405
  /* ---- ./skeleton.css ---- */
2406
+ @layer components {
1611
2407
  .muted { color: var(--muted); }
1612
2408
 
1613
2409
  .pulse-block {
@@ -1662,8 +2458,10 @@ input[type='radio']:active:not(:disabled) {
1662
2458
  /* Still distinguishable from a real block of colour, and still still. */
1663
2459
  .skel-pulse { animation: none; opacity: 0.8; }
1664
2460
  }
2461
+ }
1665
2462
 
1666
2463
  /* ---- ./rows.css ---- */
2464
+ @layer components {
1667
2465
  .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1668
2466
 
1669
2467
  /* A name and the pills that qualify it, on one line, where the name is the only
@@ -1924,72 +2722,24 @@ input[type='radio']:active:not(:disabled) {
1924
2722
  .row-hit:hover .truncate { color: var(--accent); }
1925
2723
 
1926
2724
  .row-hit:focus-visible { outline: none; box-shadow: var(--focus-ring); }
1927
-
1928
- /* ---- ./empty.css ---- */
1929
- /* `Empty`: a list with nothing in it.
1930
-
1931
- Quieter than `.empty`, which is a whole-screen state at `--space-10`. These
1932
- sit inside a card that already has a heading and a description, so the same
1933
- padding would leave a card that is mostly air about nothing.
1934
-
1935
- Dashed rather than solid: a solid border reads as a thing, and the point of
1936
- this box is that there is no thing. It marks out where the list will be. */
1937
- .nothing {
1938
- display: grid;
1939
- justify-items: center;
1940
- gap: var(--space-2);
1941
- padding: var(--space-5) var(--space-4);
1942
- border: var(--border-width) dashed var(--border-strong);
1943
- border-radius: var(--radius-md);
1944
- text-align: center;
1945
- }
1946
-
1947
- .nothing-mark { color: var(--muted); line-height: 0; }
1948
-
1949
- /* The same box with a figure in it, for a whole surface rather than a slot in
1950
- a card. Two things change and both follow from the size: the dashed outline
1951
- goes, because an outline marks out where a list will be and a window-sized
1952
- dashed rectangle reads as a layout that failed rather than a view with
1953
- nothing on it; and the box centres itself in whatever it was dropped into,
1954
- which is what makes it land in the middle of an empty dashboard rather than
1955
- under the header. */
1956
- .nothing-surface {
1957
- border: none;
1958
- padding: var(--space-10) var(--space-4);
1959
- gap: var(--space-3);
1960
- margin: auto;
1961
2725
  }
1962
2726
 
1963
- /* `currentColor` reaches the figure through the two rules the illustrations
1964
- ship with, so the colour is set here and the drawing follows it -- the same
1965
- grey as the sentence under it, in every theme.
1966
- Two classes, not one: `.illo` sets `color: inherit` further down this file,
1967
- which beats a single-class rule written earlier and drew the figure in full
1968
- text black beside a muted sentence. */
1969
- .nothing-surface .nothing-figure { color: var(--muted); }
1970
-
1971
- /* `--text-sm`, not the `--text-xs` these lines used to be: it is a sentence
1972
- somebody is meant to read, not a caption qualifying something else. */
1973
- .nothing-said { margin: 0; color: var(--muted); font-size: var(--text-sm); line-height: 1.5; max-width: 46ch; }
1974
-
1975
- .nothing-act { margin-top: var(--space-1); }
1976
-
1977
2727
  /* ---- ./progress.css ---- */
2728
+ @layer components {
1978
2729
  /* progress ----------------------------------------------------------- */
1979
2730
 
1980
- /* The bar sat 4px under its own label and flush against whatever was above
2731
+ /* The bar itself: a track with a child it fills, an animation and a
2732
+ reduced-motion answer. A utility is a property on one element, so none of
2733
+ that is one -- `Progress` draws its own wrapper and head with utilities and
2734
+ this is what is left.
2735
+
2736
+ The bar sat 4px under its own label and flush against whatever was above
1981
2737
  the whole block, so a download row read as four lines of text with a rule
1982
2738
  drawn through the last one. The label is a caption for the bar and stays
1983
2739
  close to it; the block as a whole is a separate statement from the row it
1984
2740
  follows. */
1985
- .progress { display: grid; gap: var(--space-1); }
1986
-
1987
2741
  .card > .progress { margin-top: var(--space-3); }
1988
2742
 
1989
- .progress-head { display: flex; align-items: baseline; gap: var(--space-2); font-size: var(--text-sm); }
1990
-
1991
- .progress-detail { color: var(--muted); font-size: var(--text-xs); }
1992
-
1993
2743
  .bar { height: 6px; background: var(--panel-2); border-radius: var(--radius-pill); overflow: hidden; }
1994
2744
 
1995
2745
  .bar > i {
@@ -2017,12 +2767,22 @@ input[type='radio']:active:not(:disabled) {
2017
2767
  /* Still distinguishable from empty, and from full. */
2018
2768
  .bar-indeterminate > i { animation: none; width: 100%; opacity: 0.35; }
2019
2769
  }
2770
+ }
2020
2771
 
2021
2772
  /* ---- ./tabs.css ---- */
2773
+ @layer components {
2022
2774
  /* ---------------------------------------------------------------- tabs */
2023
2775
  .tabs { position: relative; display: flex; gap: var(--space-1); }
2024
2776
 
2025
2777
  .tab {
2778
+ /* Layout the `button` element rule used to supply. A tab is a button for its behaviour, not its look, so it now says what it wants instead of taking a control's arrangement and undoing the rest. */
2779
+ display: inline-flex;
2780
+ align-items: center;
2781
+ justify-content: center;
2782
+ gap: var(--space-2);
2783
+ /* Stated rather than inherited. These came from the `button, input, textarea, select` reset, which applied control styling to everything that happened to be a `<button>` -- so this rule spent its opening lines undoing it, and would break silently if that rule ever moved. It did move, to `.btn`. */
2784
+ font: inherit;
2785
+ cursor: pointer;
2026
2786
  position: relative;
2027
2787
  z-index: 1;
2028
2788
  background: transparent;
@@ -2111,8 +2871,10 @@ input[type='radio']:active:not(:disabled) {
2111
2871
  }
2112
2872
 
2113
2873
  .tabs-vertical .tab:active:not(:disabled) { border-color: transparent; }
2874
+ }
2114
2875
 
2115
2876
  /* ---- ./toast.css ---- */
2877
+ @layer components {
2116
2878
  /* ---------------------------------------------------------------- toasts */
2117
2879
  .toasts {
2118
2880
  position: fixed;
@@ -2179,7 +2941,41 @@ input[type='radio']:active:not(:disabled) {
2179
2941
  .toast[data-exiting] { animation: none; }
2180
2942
  }
2181
2943
 
2944
+ /* The dismiss control, in its own words.
2945
+ *
2946
+ * It was `className="ghost size-sm"` -- `Button`'s vocabulary, borrowed from
2947
+ * a component that does not render a `Button`. That only ever worked because
2948
+ * `.ghost` and `.size-sm` were element-and-class rules any `<button>` could
2949
+ * pick up, which is the coupling this pass is removing. Nothing photographed
2950
+ * it, either: the toast is transient and the close button is not in any
2951
+ * baseline, so this would have gone quiet. */
2952
+ .toast-close {
2953
+ display: inline-flex;
2954
+ align-items: center;
2955
+ justify-content: center;
2956
+ font: inherit;
2957
+ font-size: var(--text-xs);
2958
+ padding: var(--space-1) var(--space-2);
2959
+ background: transparent;
2960
+ border: var(--border-width) solid transparent;
2961
+ border-radius: var(--radius);
2962
+ color: var(--muted);
2963
+ cursor: pointer;
2964
+ }
2965
+
2966
+ .toast-close:hover:not(:disabled) {
2967
+ background: var(--panel-2);
2968
+ color: var(--text);
2969
+ }
2970
+
2971
+ .toast-close:focus-visible {
2972
+ outline: 2px solid var(--accent);
2973
+ outline-offset: 2px;
2974
+ }
2975
+ }
2976
+
2182
2977
  /* ---- ./modal.css ---- */
2978
+ @layer components {
2183
2979
  /* modal --------------------------------------------------------------- */
2184
2980
 
2185
2981
  /* `hidden` needs saying out loud here. The browser's own `[hidden]` rule is a
@@ -2219,13 +3015,6 @@ input[type='radio']:active:not(:disabled) {
2219
3015
  Not `display: contents`, which was the first attempt. An element with no box
2220
3016
  cannot take focus, so React Aria's initial focus fell through to the first
2221
3017
  input and Escape -- handled on the section -- stopped closing the window. */
2222
- .modal-dialog {
2223
- display: flex;
2224
- flex-direction: column;
2225
- min-height: 0;
2226
- flex: 1;
2227
- }
2228
-
2229
3018
  .modal {
2230
3019
  background: var(--panel);
2231
3020
  border: 1px solid var(--border);
@@ -2276,18 +3065,9 @@ input[type='radio']:active:not(:disabled) {
2276
3065
  laid out for the bare `<strong>` it used to hold -- 86px tall with them,
2277
3066
  41px without, on a head whose padding is 12 over 8. `font: inherit` so the
2278
3067
  `<strong>` rule above is the one that sets the size. */
2279
- .modal-title { margin: 0; font: inherit; }
2280
-
2281
3068
  /* Under the title, on the header's padding rather than the body's, so it reads
2282
3069
  as part of the heading rather than as the first line of content. */
2283
- .modal-desc {
2284
- margin: 0;
2285
- padding: 0 var(--space-4) var(--space-3);
2286
- color: var(--muted);
2287
- font-size: var(--text-sm);
2288
- line-height: 1.5;
2289
- max-width: 62ch;
2290
- }
3070
+ .modal-desc { line-height: 1.5; }
2291
3071
 
2292
3072
  .modal-body { padding: var(--space-2) var(--space-4) var(--space-3); overflow-y: auto; }
2293
3073
 
@@ -2426,8 +3206,10 @@ input[type='radio']:active:not(:disabled) {
2426
3206
  .modal.sheet[data-entering],
2427
3207
  .modal.sheet[data-exiting] { animation: none; }
2428
3208
  }
3209
+ }
2429
3210
 
2430
3211
  /* ---- ./slider.css ---- */
3212
+ @layer components {
2431
3213
  /* slider ----------------------------------------------------------------- */
2432
3214
  /* A number from a range -- `Slider.tsx` says when one is the right control.
2433
3215
  The row is laid out like a switch row: words on the left, the control on the
@@ -2647,9 +3429,12 @@ input[type='range'].slider:disabled::-moz-range-thumb { opacity: 0; }
2647
3429
  buttons only, so a button in a row stood 25px beside a 35px select and no
2648
3430
  size class could ask for the select's height. Type from the row, box from
2649
3431
  the scale, and the two line up; an explicit `size-*` still wins. */
2650
- :where(.set-row) button, :where(.set-row) .slider-box { font-size: var(--text-xs); }
3432
+ :where(.set-row) button,
3433
+ :where(.set-row) .slider-box { font-size: var(--text-xs); }
3434
+ }
2651
3435
 
2652
3436
  /* ---- ./checkbox.css ---- */
3437
+ @layer components {
2653
3438
  /* A row you choose. There is no checkbox in it.
2654
3439
  Every one of the fourteen in this app sits beside a name and a line of
2655
3440
  explanation, so the row was always the thing being designed and the 17px
@@ -2819,36 +3604,36 @@ input[type='range'].slider:disabled::-moz-range-thumb { opacity: 0; }
2819
3604
  .choice[data-disabled] .choice-why { opacity: 0.5; }
2820
3605
 
2821
3606
  .choice[data-disabled]::after { opacity: 0.4; }
3607
+ }
2822
3608
 
2823
3609
  /* ---- ./table.css ---- */
3610
+ @layer components {
2824
3611
  /* A second line under a row's name, inside the `<th scope="row">` that makes it
2825
3612
  one. The cell is bold because it is a heading; what hangs under it is not,
2826
3613
  and inherited the weight -- so a pinned version read as loud as the thing it
2827
3614
  versions. */
2828
- .table th .set-hint { font-weight: 400; }
2829
-
2830
- .table-scroll { overflow-x: auto; }
3615
+ .data-table th .set-hint { font-weight: 400; }
2831
3616
 
2832
- .table {
3617
+ .data-table {
2833
3618
  width: 100%;
2834
3619
  border-collapse: collapse;
2835
3620
  font-size: var(--text-sm);
2836
3621
  }
2837
3622
 
2838
- .table th,
2839
- .table td {
3623
+ .data-table th,
3624
+ .data-table td {
2840
3625
  text-align: left;
2841
3626
  padding: var(--space-2) var(--space-3);
2842
3627
  border-bottom: var(--border-width) solid var(--border);
2843
3628
  }
2844
3629
 
2845
- .table th:first-child,
2846
- .table td:first-child { padding-left: 0; }
3630
+ .data-table th:first-child,
3631
+ .data-table td:first-child { padding-left: 0; }
2847
3632
 
2848
- .table th:last-child,
2849
- .table td:last-child { padding-right: 0; }
3633
+ .data-table th:last-child,
3634
+ .data-table td:last-child { padding-right: 0; }
2850
3635
 
2851
- .table thead th {
3636
+ .data-table thead th {
2852
3637
  font-size: var(--text-xs);
2853
3638
  font-weight: var(--weight-strong);
2854
3639
  text-transform: uppercase;
@@ -2859,78 +3644,69 @@ input[type='range'].slider:disabled::-moz-range-thumb { opacity: 0; }
2859
3644
 
2860
3645
  /* The row's own name: a `<th scope="row">`, so it carries weight rather than
2861
3646
  reading as one more cell. */
2862
- .table tbody th { font-weight: var(--weight-strong); }
3647
+ .data-table tbody th { font-weight: var(--weight-strong); }
2863
3648
 
2864
- .table tbody tr:last-child th,
2865
- .table tbody tr:last-child td { border-bottom: none; }
3649
+ .data-table tbody tr:last-child th,
3650
+ .data-table tbody tr:last-child td { border-bottom: none; }
2866
3651
 
2867
3652
  /* Numbers compare by column, so they align right and hold their tracks --
2868
3653
  proportional digits shift the alignment by a pixel per glyph, which is
2869
3654
  exactly the comparison the column was for. */
2870
- .table .num { text-align: right; font-variant-numeric: tabular-nums; font-family: var(--font-mono); }
3655
+ .data-table .num { text-align: right; font-variant-numeric: tabular-nums; font-family: var(--font-mono); }
2871
3656
 
2872
- .table thead th[style*='right'] { text-align: right; }
3657
+ .data-table thead th[style*='right'] { text-align: right; }
3658
+ }
2873
3659
 
2874
3660
  /* ---- ./sizegrid.css ---- */
2875
- /* Tile size, picked as a rectangle rather than read as a notation. */
2876
- .size-grid-wrap { display: flex; align-items: center; gap: var(--space-2); }
2877
-
2878
- .size-grid { display: grid; gap: var(--space-1); }
2879
-
3661
+ @layer components {
3662
+ /* The cell's colour, all of it, in one place.
3663
+ *
3664
+ * `SizeGrid` draws the cell's geometry with utilities and leaves every colour
3665
+ * here, because `on` and `chosen` override the resting state -- and a utility
3666
+ * is in `@layer utilities`, which beats `@layer components` whatever the
3667
+ * selector. Stated as `surface-panel-2` and `border-border` on the element,
3668
+ * the resting state won permanently and no cell ever lit up.
3669
+ *
3670
+ * That is the third time this pattern has bitten: `Select`'s tick and
3671
+ * `Empty`'s figure were the other two. The rule: a resting state whose
3672
+ * active state is a CSS rule has to be a CSS rule too. */
2880
3673
  .size-cell {
2881
- width: 18px; height: 14px; padding: 0;
2882
- border: 1px solid var(--border); border-radius: var(--radius-sm);
2883
- background: var(--panel-2); cursor: pointer;
3674
+ border: var(--border-width) solid var(--border);
3675
+ background: var(--panel-2);
2884
3676
  }
2885
3677
 
2886
3678
  /* `on` follows the pointer; `chosen` is what is actually saved, shown only
2887
3679
  when nothing is hovered so the two can never contradict each other. */
2888
- .size-cell.on { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 45%, transparent); }
3680
+ .size-cell.on {
3681
+ border-color: var(--accent);
3682
+ background: color-mix(in srgb, var(--accent) 45%, transparent);
3683
+ }
2889
3684
 
2890
3685
  .size-cell.chosen { border-color: var(--accent); }
2891
-
2892
- .size-cell:disabled { cursor: default; opacity: 0.5; }
2893
-
2894
- .size-ceiling { opacity: 0.55; }
3686
+ }
2895
3687
 
2896
3688
  /* ---- ./tooltip.css ---- */
3689
+ @layer components {
2897
3690
  /* A callout: something worth knowing, with nothing to answer. Distinct from a
2898
3691
  `?`, which is an offer to read more, and from a banner, which is a problem.
2899
3692
  The mark is a round `i` rather than an emoji so it takes the theme's colours
2900
3693
  and stays the same size as the text beside it. */
2901
- /* An info-toned `.callout`, spelled the old way. Identical to it now rather than
2902
- nearly identical to it, which is the whole point of the consolidation: the
2903
-
2904
- /* The `?` beside a setting, and the box it shows on hover. Built like the
3694
+ /* An info-toned `.callout`, spelled the old way. Identical to it now rather
3695
+ than nearly identical to it, which is the whole point of the consolidation.
3696
+ (This comment has been unterminated since before the Tailwind branch -- it
3697
+ stops mid-sentence and never closes, so the CSS parser merged it with the
3698
+ one below and recovered. Nothing was lost from the stylesheet; the rest of
3699
+ the sentence was. Closed here rather than guessed at.) */
3700
+
3701
+ /* What is left once `Tooltip` draws its own mark: the hover and focus that
3702
+ reach the mark from its parent, and the tip -- whose closed and open
3703
+ states are three rules the cascade resolves in order, with
3704
+ `data-entering` and `data-exiting` around the transition. None of that is
3705
+ a property on one element.
3706
+
3707
+ The `?` beside a setting, and the box it shows on hover. Built like the
2905
3708
  status dot's tooltip and for the same reasons: `title` waits a second and a
2906
3709
  half, renders newlines however it likes, and cannot be styled. */
2907
- .explain {
2908
- position: relative;
2909
- flex: none;
2910
- display: inline-flex;
2911
- align-items: center;
2912
- /* The heading it follows is tracked out, which leaves the last letter's
2913
- spacing sitting between them and reads as none at all. */
2914
- margin-left: var(--space-2);
2915
- cursor: help;
2916
- /* Headings here are uppercase and tracked out, and one of these sits inside
2917
- a heading. */
2918
- text-transform: none;
2919
- letter-spacing: normal;
2920
- font-weight: 400;
2921
- }
2922
-
2923
- .explain-mark {
2924
- width: 18px;
2925
- height: 18px;
2926
- border-radius: 50%;
2927
- border: 1px solid var(--border);
2928
- color: var(--muted);
2929
- font-size: var(--text-xs);
2930
- line-height: 16px;
2931
- text-align: center;
2932
- }
2933
-
2934
3710
  .explain:hover .explain-mark,
2935
3711
  .explain:focus-visible .explain-mark { color: var(--text); border-color: var(--accent); }
2936
3712
 
@@ -3003,8 +3779,10 @@ input[type='range'].slider:disabled::-moz-range-thumb { opacity: 0; }
3003
3779
  }
3004
3780
 
3005
3781
  .explain-tip strong { color: var(--text); font-weight: 600; }
3782
+ }
3006
3783
 
3007
3784
  /* ---- ./illustration.css ---- */
3785
+ @layer components {
3008
3786
  /* illustrations -------------------------------------------------------- */
3009
3787
 
3010
3788
  /* Open Peeps, two colours, wearing whatever the theme is.
@@ -3031,7 +3809,21 @@ input[type='range'].slider:disabled::-moz-range-thumb { opacity: 0; }
3031
3809
  a shirt that lets the arm behind it show through reads as a hole. */
3032
3810
  .illo .paper { fill: var(--illo-paper); }
3033
3811
 
3812
+ /* The figure in a whole-surface `Empty`, drawn in the muted grey of the
3813
+ sentence under it.
3814
+ *
3815
+ * A utility cannot do this, and the reason is worth keeping: `.illo` sets
3816
+ * `color: inherit` and is unlayered, while every Tailwind utility lives in
3817
+ * `@layer utilities` -- and an unlayered declaration beats a layered one
3818
+ * whatever the specificity. So `text-muted` on the figure lost, silently,
3819
+ * and the shirt came out full text black beside a muted sentence. The
3820
+ * original rule already carried a note about needing two classes to beat
3821
+ * `.illo`; layers make that impossible rather than merely fiddly. */
3822
+ .nothing-figure { color: var(--muted); }
3823
+ }
3824
+
3034
3825
  /* ---- ./field.css ---- */
3826
+ @layer components {
3035
3827
  /* fields --------------------------------------------------------------- */
3036
3828
 
3037
3829
  /* Label, description, input, error -- in the order they are wanted. The
@@ -3129,7 +3921,7 @@ input[type='range'].slider:disabled::-moz-range-thumb { opacity: 0; }
3129
3921
  .adorned > .adorned-mark,
3130
3922
  .adorned > .adorned-end { grid-area: 1 / 1; }
3131
3923
 
3132
- .adorned.block { width: 100%; }
3924
+ .adorned.ctl-block { width: 100%; }
3133
3925
 
3134
3926
  .adorned > .adorned-mark {
3135
3927
  justify-self: start;
@@ -3182,7 +3974,34 @@ input[type='range'].slider:disabled::-moz-range-thumb { opacity: 0; }
3182
3974
 
3183
3975
  .adorned-lg .adorned-clear { padding: var(--space-2); }
3184
3976
 
3977
+ /* The eye and the clear button stop borrowing `ghost`.
3978
+ *
3979
+ * Both were `className="icon-btn ghost …"` -- reaching into `Button`'s class
3980
+ * vocabulary from another component, which worked only because `.ghost` was
3981
+ * written as `button.ghost` and they happen to be `<button>`s. It is `.btn.ghost`
3982
+ * now, so the borrowing stopped matching and `.icon-btn`'s panel background
3983
+ * came through: the baselines showed a filled tile where there had been a bare
3984
+ * glyph.
3985
+ *
3986
+ * Stated here instead. What they actually wanted from `ghost` is two lines,
3987
+ * and saying them is cheaper than either component knowing what a button's
3988
+ * classes are called. */
3989
+ .secret-eye,
3990
+ .adorned-clear {
3991
+ background: transparent;
3992
+ border-color: transparent;
3993
+ color: var(--muted);
3994
+ }
3995
+
3996
+ .secret-eye:hover:not(:disabled),
3997
+ .adorned-clear:hover:not(:disabled) {
3998
+ background: var(--panel-2);
3999
+ color: var(--text);
4000
+ }
4001
+ }
4002
+
3185
4003
  /* ---- ./tour.css ---- */
4004
+ @layer components {
3186
4005
  /* tour ----------------------------------------------------------------- */
3187
4006
 
3188
4007
  /* The layer sits over everything and catches nothing: the scrim is painted by
@@ -3244,141 +4063,41 @@ input[type='range'].slider:disabled::-moz-range-thumb { opacity: 0; }
3244
4063
  .tour-actions { display: flex; align-items: center; gap: var(--space-2); margin-top: var(--space-2); }
3245
4064
 
3246
4065
  .tour-count { color: var(--muted); font-size: var(--text-xs); }
4066
+ }
3247
4067
 
3248
4068
  /* The mail and forum shell, added in 0.4.0. After the rest, because the
3249
4069
  anchored surfaces (`.pop`, `.menu-sheet`, `.cmd`) sit over everything and a
3250
4070
  rule of equal specificity that came earlier would lose to whatever the
3251
4071
  sheets they cover happen to set. */
3252
- /* ---- ./kbd.css ---- */
3253
- /* One rule where there were two identical ones, in `menu.css` and
3254
- `command.css`. Both keep their class as well, so `Menu` and `Command` are
3255
- `.kbd .menu-key` and `.kbd .cmd-key` now: the declarations are the same, so
3256
- nothing moves, and the context classes stay available for a rule that ever
3257
- needs to tell the two apart. */
3258
- .kbd {
3259
- flex: none;
3260
- font-family: var(--font-mono);
3261
- font-size: var(--text-2xs);
3262
- color: var(--muted);
3263
- letter-spacing: var(--tracking);
3264
- }
3265
-
3266
4072
  /* ---- ./stat.css ---- */
3267
- /* A figure with furniture. `--space-1` between the three lines because they
3268
- are one object read top to bottom, not three paragraphs. */
3269
- .stat {
3270
- display: grid;
3271
- gap: var(--space-1);
3272
- padding: var(--space-4);
3273
- border: var(--border-width) solid var(--border);
3274
- border-radius: var(--radius-md);
3275
- background: var(--panel);
3276
- }
4073
+ @layer components {
4074
+ /* What the utilities on `Stat` cannot say: a tone on the tile colouring the
4075
+ figure inside it. An attribute on the parent selecting a child is a
4076
+ relationship; a utility is a property on one element.
3277
4077
 
3278
- /* Inside something already bordered. A bordered tile in a bordered row is two
3279
- boxes saying one thing. */
3280
- .stat-bare {
3281
- padding: 0;
3282
- border: 0;
3283
- border-radius: 0;
3284
- background: none;
3285
- gap: 0;
3286
- line-height: 1.25;
3287
- }
3288
-
3289
- .stat > p { margin: 0; }
3290
-
3291
- .stat-label {
3292
- color: var(--muted);
3293
- font-size: var(--text-xs);
3294
- }
3295
-
3296
- /* Tabular figures are the point: a column of numbers is compared by its
3297
- digits' positions, and proportional ones move them per row. */
3298
- .stat-value {
3299
- font-size: var(--text-lg);
3300
- font-weight: var(--weight-strong);
3301
- font-variant-numeric: tabular-nums;
3302
- }
3303
-
3304
- .stat-bare .stat-value { font-size: var(--text-sm); }
3305
-
3306
- .stat-note {
3307
- color: var(--muted);
3308
- font-size: var(--text-xs);
3309
- }
3310
-
3311
- /* Only the figure takes the colour. Colouring the label too makes the tile a
3312
- status box, and a tone here is a remark about the number. */
4078
+ Only the figure takes the colour. Colouring the label too makes the tile a
4079
+ status box, where a tone here is a remark about the number. */
3313
4080
  .stat[data-tone='info'] .stat-value { color: var(--info); }
3314
4081
  .stat[data-tone='good'] .stat-value { color: var(--good); }
3315
4082
  .stat[data-tone='warn'] .stat-value { color: var(--warn); }
3316
4083
  .stat[data-tone='bad'] .stat-value { color: var(--bad); }
3317
-
3318
- /* ---- ./shell.css ---- */
3319
- /* The frame. Layout only: every colour and every step is a token, and the
3320
- page inside it owns its own spacing. */
3321
- .shell {
3322
- display: flex;
3323
- flex-direction: column;
3324
- min-height: 100vh;
3325
- background: var(--bg);
3326
- color: var(--text);
3327
4084
  }
3328
4085
 
3329
- .shell-head {
3330
- border-bottom: var(--border-width) solid var(--border);
3331
- background: var(--panel);
3332
- /* `flex: none` so a long page does not squeeze the header. */
3333
- flex: none;
3334
- }
3335
-
3336
- /* The measure, said once. `--shell-measure` rather than a literal so an app
3337
- with a different one sets a variable instead of overriding three rules and
3338
- missing the fourth -- which is how manage ended up with `head-inner-wide`
3339
- and `main-wide` as separate classes. */
4086
+ /* ---- ./shell.css ---- */
4087
+ @layer components {
4088
+ /* The measure, and only the measure.
4089
+ *
4090
+ * `Shell` draws itself with utilities -- it is layout and nothing else. What
4091
+ * stays here is the one value the header band, the nav band and the main band
4092
+ * all have to agree on: a custom property, so an app changes it in one place
4093
+ * rather than overriding three rules and missing the fourth. That is how
4094
+ * manage ended up carrying `head-inner-wide` and `main-wide` as separate
4095
+ * classes it had to remember together. */
3340
4096
  .shell {
3341
4097
  --shell-measure: 64rem;
3342
4098
  --shell-measure-wide: 90rem;
3343
4099
  }
3344
4100
 
3345
- .shell-head-inner,
3346
- .shell-nav,
3347
- .shell-main {
3348
- width: 100%;
3349
- max-width: var(--shell-measure);
3350
- margin-inline: auto;
3351
- padding-inline: var(--space-4);
3352
- }
3353
-
3354
- .shell-wide { max-width: var(--shell-measure-wide); }
3355
-
3356
- .shell-head-inner {
3357
- display: flex;
3358
- align-items: center;
3359
- justify-content: space-between;
3360
- gap: var(--space-4);
3361
- padding-block: var(--space-3);
3362
- min-width: 0;
3363
- }
3364
-
3365
- .shell-who {
3366
- display: flex;
3367
- align-items: center;
3368
- gap: var(--space-3);
3369
- min-width: 0;
3370
- }
3371
-
3372
- .shell-nav { padding-bottom: var(--space-2); }
3373
-
3374
- .shell-main {
3375
- flex: 1;
3376
- padding-block: var(--space-6);
3377
- /* `min-width: 0` because a flex child will not shrink below its content,
3378
- and one wide table inside then pushes the whole page sideways. */
3379
- min-width: 0;
3380
- }
3381
-
3382
4101
  /* The label beside the theme control, when it is drawn. */
3383
4102
  .theme-switch {
3384
4103
  display: flex;
@@ -3392,8 +4111,10 @@ input[type='range'].slider:disabled::-moz-range-thumb { opacity: 0; }
3392
4111
  font-size: var(--text-sm);
3393
4112
  white-space: nowrap;
3394
4113
  }
4114
+ }
3395
4115
 
3396
4116
  /* ---- ./scrollarea.css ---- */
4117
+ @layer components {
3397
4118
  /* A box that scrolls, with its edges drawn.
3398
4119
 
3399
4120
  The fades are `::before`/`::after` on the scroller itself, pinned with
@@ -3486,8 +4207,10 @@ input[type='range'].slider:disabled::-moz-range-thumb { opacity: 0; }
3486
4207
  .scroller.faded::before,
3487
4208
  .scroller.faded::after { transition: none; }
3488
4209
  }
4210
+ }
3489
4211
 
3490
4212
  /* ---- ./splitpane.css ---- */
4213
+ @layer components {
3491
4214
  /* Two panes and the handle between them. */
3492
4215
  .split {
3493
4216
  display: flex;
@@ -3587,8 +4310,10 @@ body.splitting * { cursor: inherit !important; }
3587
4310
  @media (prefers-reduced-motion: reduce) {
3588
4311
  .split-grip { transition: none; }
3589
4312
  }
4313
+ }
3590
4314
 
3591
4315
  /* ---- ./popover.css ---- */
4316
+ @layer components {
3592
4317
  /* A small surface anchored to what opened it. Same sheet as the select's list,
3593
4318
  because they are the same object -- something that appeared over the page,
3594
4319
  attached to a control. */
@@ -3642,29 +4367,17 @@ body.splitting * { cursor: inherit !important; }
3642
4367
  .pop[data-entering],
3643
4368
  .pop[data-exiting] { animation: none; }
3644
4369
  }
3645
-
3646
- /* ---- ./menu.css ---- */
3647
- /* A list of verbs, on the same sheet as every other anchored surface. */
3648
- .menu-sheet {
3649
- background: var(--panel);
3650
- border: var(--border-width) solid var(--border);
3651
- border-radius: var(--radius-md);
3652
- box-shadow: var(--shadow-2);
3653
- min-width: 12rem;
3654
- max-width: min(20rem, calc(100vw - var(--space-6)));
3655
- max-height: 24rem;
3656
- overflow: auto;
3657
- overscroll-behavior: contain;
3658
- z-index: 60;
3659
4370
  }
3660
4371
 
3661
- .menu-list {
3662
- padding: var(--space-1);
3663
- outline: none;
3664
- display: grid;
3665
- gap: var(--border-width);
3666
- }
4372
+ /* ---- ./menu.css ---- */
4373
+ @layer components {
4374
+ /* What is left once `Menu` draws its sheet and its list: the rows, whose
4375
+ `data-focused`, `data-pressed` and danger states React Aria stamps on
4376
+ them, and the rules that reach from a row into what it holds. The
4377
+ `box-shadow` stays on the sheet too -- `--shadow-2` is a token, and a
4378
+ utility would have to inline it. */
3667
4379
 
4380
+ /* A list of verbs, on the same sheet as every other anchored surface. */
3668
4381
  .menu-item {
3669
4382
  display: flex;
3670
4383
  align-items: center;
@@ -3703,14 +4416,6 @@ body.splitting * { cursor: inherit !important; }
3703
4416
  /* The binding, set in the mono face at the size of the small print. Right of
3704
4417
  the row and never the reason the row is wide: `flex: none` with the label
3705
4418
  taking the slack. */
3706
- .menu-key {
3707
- flex: none;
3708
- font-family: var(--font-mono);
3709
- font-size: var(--text-2xs);
3710
- color: var(--muted);
3711
- letter-spacing: var(--tracking);
3712
- }
3713
-
3714
4419
  .menu-more { flex: none; color: var(--muted); }
3715
4420
 
3716
4421
  .menu-rule {
@@ -3746,7 +4451,11 @@ body.splitting * { cursor: inherit !important; }
3746
4451
  .menu-sheet[data-exiting] { animation: none; }
3747
4452
  }
3748
4453
 
4454
+ .menu-sheet { box-shadow: var(--shadow-2); }
4455
+ }
4456
+
3749
4457
  /* ---- ./command.css ---- */
4458
+ @layer components {
3750
4459
  /* The command palette: a field over the app and a list under it. */
3751
4460
  .cmd-scrim {
3752
4461
  position: fixed;
@@ -3863,14 +4572,6 @@ body.splitting * { cursor: inherit !important; }
3863
4572
  white-space: nowrap;
3864
4573
  }
3865
4574
 
3866
- .cmd-key {
3867
- flex: none;
3868
- font-family: var(--font-mono);
3869
- font-size: var(--text-2xs);
3870
- color: var(--muted);
3871
- letter-spacing: var(--tracking);
3872
- }
3873
-
3874
4575
  .cmd-empty {
3875
4576
  margin: 0;
3876
4577
  padding: var(--space-6) var(--space-4);
@@ -3896,25 +4597,18 @@ body.splitting * { cursor: inherit !important; }
3896
4597
  .cmd-scrim[data-exiting],
3897
4598
  .cmd[data-entering] { animation: none; }
3898
4599
  }
4600
+ }
3899
4601
 
3900
4602
  /* ---- ./pagination.css ---- */
3901
- /* Moving through a list that does not fit, and saying how far in you are. */
3902
- .pager {
3903
- display: flex;
3904
- align-items: center;
3905
- justify-content: space-between;
3906
- gap: var(--space-4);
3907
- flex-wrap: wrap;
3908
- padding: var(--space-2) 0;
3909
- font-size: var(--text-xs);
3910
- color: var(--muted);
3911
- }
4603
+ @layer components {
4604
+ /* What is left once `Pagination` draws its own frame: the step and page
4605
+ buttons, whose hover, focus-visible and current states are pseudo-classes
4606
+ on two selectors at once, and `.pager-count strong`, which reaches into
4607
+ text the caller passed. */
3912
4608
 
3913
- .pager-count { margin: 0; }
4609
+ /* Moving through a list that does not fit, and saying how far in you are. */
3914
4610
  .pager-count strong { color: var(--text); font-weight: var(--weight-strong); }
3915
4611
 
3916
- .pager-controls { display: flex; align-items: center; gap: var(--space-1); }
3917
-
3918
4612
  .pager-step,
3919
4613
  .pager-page {
3920
4614
  display: inline-flex;
@@ -4025,8 +4719,10 @@ body.splitting * { cursor: inherit !important; }
4025
4719
  .pager-page,
4026
4720
  .pager-jump { transition: none; }
4027
4721
  }
4722
+ }
4028
4723
 
4029
4724
  /* ---- ./identity.css ---- */
4725
+ @layer components {
4030
4726
  /* A person, said in one line: initials, name, sometimes the address. */
4031
4727
  .ident {
4032
4728
  display: inline-flex;
@@ -4106,6 +4802,8 @@ body.splitting * { cursor: inherit !important; }
4106
4802
  }
4107
4803
 
4108
4804
  .ident-remove {
4805
+ /* Stated rather than inherited. These came from the `button, input, textarea, select` reset, which applied control styling to everything that happened to be a `<button>` -- so this rule spent its opening lines undoing it, and would break silently if that rule ever moved. It did move, to `.btn`. */
4806
+ font: inherit;
4109
4807
  flex: none;
4110
4808
  display: inline-flex;
4111
4809
  align-items: center;
@@ -4125,4 +4823,5 @@ body.splitting * { cursor: inherit !important; }
4125
4823
 
4126
4824
  .ident-remove:hover { background: var(--panel); color: var(--text); }
4127
4825
  .ident-remove:focus-visible { outline: none; box-shadow: var(--focus-ring); }
4826
+ }
4128
4827