@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.
package/dist/tf.css CHANGED
@@ -278,7 +278,700 @@
278
278
  *
279
279
  * `scripts/copy-css.mjs` flattens these into one file at build time.
280
280
  */
281
- /* ---- ./base.css ---- */
281
+ /* ---- ./_tailwind.built.css ---- */
282
+ /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
283
+ @layer properties;
284
+ @layer theme, base, components, utilities;
285
+ @layer theme {
286
+ :root, :host {
287
+ --font-sans: var(--font-sans);
288
+ --font-mono: var(--font-mono);
289
+ --text-xs: var(--text-xs);
290
+ --text-sm: var(--text-sm);
291
+ --text-base: var(--text-base);
292
+ --text-lg: var(--text-lg);
293
+ --text-xl: var(--text-xl);
294
+ --font-weight-normal: var(--weight);
295
+ --tracking-normal: var(--tracking);
296
+ --radius-sm: var(--radius-sm);
297
+ --radius-md: var(--radius-md);
298
+ --radius-lg: var(--radius-lg);
299
+ --ease-out: cubic-bezier(0, 0, 0.2, 1);
300
+ --spacing-1: var(--space-1);
301
+ --spacing-2: var(--space-2);
302
+ --spacing-3: var(--space-3);
303
+ --spacing-4: var(--space-4);
304
+ --spacing-5: var(--space-5);
305
+ --spacing-6: var(--space-6);
306
+ --spacing-10: var(--space-10);
307
+ --text-2xs: var(--text-2xs);
308
+ --text-md: var(--text-md);
309
+ --font-weight-strong: var(--weight-strong);
310
+ --radius-pill: var(--radius-pill);
311
+ --color-text: var(--text);
312
+ --color-muted: var(--muted);
313
+ --color-border: var(--border);
314
+ --color-border-strong: var(--border-strong);
315
+ }
316
+ }
317
+ @layer utilities {
318
+ .collapse {
319
+ visibility: collapse;
320
+ }
321
+ .invisible {
322
+ visibility: hidden;
323
+ }
324
+ .visible {
325
+ visibility: visible;
326
+ }
327
+ .sr-only {
328
+ position: absolute;
329
+ width: 1px;
330
+ height: 1px;
331
+ padding: 0;
332
+ margin: -1px;
333
+ overflow: hidden;
334
+ clip-path: inset(50%);
335
+ white-space: nowrap;
336
+ border-width: 0;
337
+ }
338
+ .absolute {
339
+ position: absolute;
340
+ }
341
+ .fixed {
342
+ position: fixed;
343
+ }
344
+ .relative {
345
+ position: relative;
346
+ }
347
+ .sticky {
348
+ position: sticky;
349
+ }
350
+ .z-\[60\] {
351
+ z-index: 60;
352
+ }
353
+ .container {
354
+ width: 100%;
355
+ @media (width >= 40rem) {
356
+ max-width: 40rem;
357
+ }
358
+ @media (width >= 48rem) {
359
+ max-width: 48rem;
360
+ }
361
+ @media (width >= 64rem) {
362
+ max-width: 64rem;
363
+ }
364
+ @media (width >= 80rem) {
365
+ max-width: 80rem;
366
+ }
367
+ @media (width >= 96rem) {
368
+ max-width: 96rem;
369
+ }
370
+ }
371
+ .m-0 {
372
+ margin: 0px;
373
+ }
374
+ .m-auto {
375
+ margin: auto;
376
+ }
377
+ .mx-auto {
378
+ margin-inline: auto;
379
+ }
380
+ .mt-1 {
381
+ margin-top: var(--spacing-1);
382
+ }
383
+ .mb-3 {
384
+ margin-bottom: var(--spacing-3);
385
+ }
386
+ .ml-2 {
387
+ margin-left: var(--spacing-2);
388
+ }
389
+ .block {
390
+ display: block;
391
+ }
392
+ .contents {
393
+ display: contents;
394
+ }
395
+ .flex {
396
+ display: flex;
397
+ }
398
+ .grid {
399
+ display: grid;
400
+ }
401
+ .hidden {
402
+ display: none;
403
+ }
404
+ .inline {
405
+ display: inline;
406
+ }
407
+ .inline-flex {
408
+ display: inline-flex;
409
+ }
410
+ .table {
411
+ display: table;
412
+ }
413
+ .h-\[14px\] {
414
+ height: 14px;
415
+ }
416
+ .h-\[18px\] {
417
+ height: 18px;
418
+ }
419
+ .max-h-\[24rem\] {
420
+ max-height: 24rem;
421
+ }
422
+ .max-h-\[280px\] {
423
+ max-height: 280px;
424
+ }
425
+ .min-h-0 {
426
+ min-height: 0px;
427
+ }
428
+ .min-h-screen {
429
+ min-height: 100vh;
430
+ }
431
+ .w-\[18px\] {
432
+ width: 18px;
433
+ }
434
+ .w-full {
435
+ width: 100%;
436
+ }
437
+ .max-w-\[46ch\] {
438
+ max-width: 46ch;
439
+ }
440
+ .max-w-\[min\(20rem\,calc\(100vw-var\(--space-6\)\)\)\] {
441
+ max-width: min(20rem, calc(100vw - var(--space-6)));
442
+ }
443
+ .max-w-\[var\(--shell-measure\)\] {
444
+ max-width: var(--shell-measure);
445
+ }
446
+ .max-w-\[var\(--shell-measure-wide\)\] {
447
+ max-width: var(--shell-measure-wide);
448
+ }
449
+ .min-w-0 {
450
+ min-width: 0px;
451
+ }
452
+ .min-w-\[12rem\] {
453
+ min-width: 12rem;
454
+ }
455
+ .flex-1 {
456
+ flex: 1;
457
+ }
458
+ .flex-none {
459
+ flex: none;
460
+ }
461
+ .shrink {
462
+ flex-shrink: 1;
463
+ }
464
+ .grow {
465
+ flex-grow: 1;
466
+ }
467
+ .transform {
468
+ transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
469
+ }
470
+ .cursor-help {
471
+ cursor: help;
472
+ }
473
+ .cursor-pointer {
474
+ cursor: pointer;
475
+ }
476
+ .resize {
477
+ resize: both;
478
+ }
479
+ .list-none {
480
+ list-style-type: none;
481
+ }
482
+ .flex-col {
483
+ flex-direction: column;
484
+ }
485
+ .flex-wrap {
486
+ flex-wrap: wrap;
487
+ }
488
+ .items-baseline {
489
+ align-items: baseline;
490
+ }
491
+ .items-center {
492
+ align-items: center;
493
+ }
494
+ .items-start {
495
+ align-items: flex-start;
496
+ }
497
+ .justify-between {
498
+ justify-content: space-between;
499
+ }
500
+ .justify-items-center {
501
+ justify-items: center;
502
+ }
503
+ .gap-0 {
504
+ gap: 0px;
505
+ }
506
+ .gap-1 {
507
+ gap: var(--spacing-1);
508
+ }
509
+ .gap-2 {
510
+ gap: var(--spacing-2);
511
+ }
512
+ .gap-3 {
513
+ gap: var(--spacing-3);
514
+ }
515
+ .gap-4 {
516
+ gap: var(--spacing-4);
517
+ }
518
+ .gap-\[var\(--border-width\)\] {
519
+ gap: var(--border-width);
520
+ }
521
+ .self-center {
522
+ align-self: center;
523
+ }
524
+ .truncate {
525
+ overflow: hidden;
526
+ text-overflow: ellipsis;
527
+ white-space: nowrap;
528
+ }
529
+ .overflow-auto {
530
+ overflow: auto;
531
+ }
532
+ .overflow-hidden {
533
+ overflow: hidden;
534
+ }
535
+ .overflow-x-auto {
536
+ overflow-x: auto;
537
+ }
538
+ .overflow-y-auto {
539
+ overflow-y: auto;
540
+ }
541
+ .overscroll-contain {
542
+ overscroll-behavior: contain;
543
+ }
544
+ .rounded {
545
+ border-radius: 0.25rem;
546
+ }
547
+ .rounded-full {
548
+ border-radius: calc(infinity * 1px);
549
+ }
550
+ .rounded-md {
551
+ border-radius: var(--radius-md);
552
+ }
553
+ .rounded-sm {
554
+ border-radius: var(--radius-sm);
555
+ }
556
+ .border {
557
+ border-style: var(--tw-border-style);
558
+ border-width: var(--border-width);
559
+ }
560
+ .border-b {
561
+ border-bottom-style: var(--tw-border-style);
562
+ border-bottom-width: var(--border-width);
563
+ }
564
+ .border-dashed {
565
+ --tw-border-style: dashed;
566
+ border-style: dashed;
567
+ }
568
+ .border-border {
569
+ border-color: var(--color-border);
570
+ }
571
+ .border-border-strong {
572
+ border-color: var(--color-border-strong);
573
+ }
574
+ .p-0 {
575
+ padding: 0px;
576
+ }
577
+ .p-1 {
578
+ padding: var(--spacing-1);
579
+ }
580
+ .p-2 {
581
+ padding: var(--spacing-2);
582
+ }
583
+ .p-4 {
584
+ padding: var(--spacing-4);
585
+ }
586
+ .px-4 {
587
+ padding-inline: var(--spacing-4);
588
+ }
589
+ .py-2 {
590
+ padding-block: var(--spacing-2);
591
+ }
592
+ .py-3 {
593
+ padding-block: var(--spacing-3);
594
+ }
595
+ .py-5 {
596
+ padding-block: var(--spacing-5);
597
+ }
598
+ .py-6 {
599
+ padding-block: var(--spacing-6);
600
+ }
601
+ .py-10 {
602
+ padding-block: var(--spacing-10);
603
+ }
604
+ .pb-2 {
605
+ padding-bottom: var(--spacing-2);
606
+ }
607
+ .pb-3 {
608
+ padding-bottom: var(--spacing-3);
609
+ }
610
+ .text-center {
611
+ text-align: center;
612
+ }
613
+ .font-mono {
614
+ font-family: var(--font-mono);
615
+ }
616
+ .text-2xs {
617
+ font-size: var(--text-2xs);
618
+ }
619
+ .text-lg {
620
+ font-size: var(--text-lg);
621
+ }
622
+ .text-sm {
623
+ font-size: var(--text-sm);
624
+ }
625
+ .text-xs {
626
+ font-size: var(--text-xs);
627
+ }
628
+ .leading-\[0\] {
629
+ --tw-leading: 0;
630
+ line-height: 0;
631
+ }
632
+ .leading-\[1\.5\] {
633
+ --tw-leading: 1.5;
634
+ line-height: 1.5;
635
+ }
636
+ .leading-\[1\.25\] {
637
+ --tw-leading: 1.25;
638
+ line-height: 1.25;
639
+ }
640
+ .leading-\[16px\] {
641
+ --tw-leading: 16px;
642
+ line-height: 16px;
643
+ }
644
+ .font-normal {
645
+ --tw-font-weight: var(--font-weight-normal);
646
+ font-weight: var(--font-weight-normal);
647
+ }
648
+ .font-strong {
649
+ --tw-font-weight: var(--font-weight-strong);
650
+ font-weight: var(--font-weight-strong);
651
+ }
652
+ .tracking-normal {
653
+ --tw-tracking: var(--tracking-normal);
654
+ letter-spacing: var(--tracking-normal);
655
+ }
656
+ .text-ellipsis {
657
+ text-overflow: ellipsis;
658
+ }
659
+ .whitespace-nowrap {
660
+ white-space: nowrap;
661
+ }
662
+ .text-muted {
663
+ color: var(--color-muted);
664
+ }
665
+ .text-text {
666
+ color: var(--color-text);
667
+ }
668
+ .lowercase {
669
+ text-transform: lowercase;
670
+ }
671
+ .normal-case {
672
+ text-transform: none;
673
+ }
674
+ .uppercase {
675
+ text-transform: uppercase;
676
+ }
677
+ .tabular-nums {
678
+ --tw-numeric-spacing: tabular-nums;
679
+ font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
680
+ }
681
+ .opacity-60 {
682
+ opacity: 60%;
683
+ }
684
+ .opacity-\[0\.55\] {
685
+ opacity: 0.55;
686
+ }
687
+ .shadow {
688
+ --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));
689
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
690
+ }
691
+ .ring {
692
+ --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
693
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
694
+ }
695
+ .outline {
696
+ outline-style: var(--tw-outline-style);
697
+ outline-width: 1px;
698
+ }
699
+ .blur {
700
+ --tw-blur: blur(8px);
701
+ 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,);
702
+ }
703
+ .sepia {
704
+ --tw-sepia: sepia(100%);
705
+ 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,);
706
+ }
707
+ .filter {
708
+ 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,);
709
+ }
710
+ .outline-none {
711
+ --tw-outline-style: none;
712
+ outline-style: none;
713
+ }
714
+ .\[font\:inherit\] {
715
+ font: inherit;
716
+ }
717
+ .surface-bg {
718
+ background: var(--bg);
719
+ }
720
+ .surface-panel {
721
+ background: var(--panel);
722
+ }
723
+ .disabled\:cursor-default:disabled {
724
+ cursor: default;
725
+ }
726
+ .disabled\:opacity-50:disabled {
727
+ opacity: 50%;
728
+ }
729
+ .\[\&\>p\]\:m-0 > p {
730
+ margin: 0px;
731
+ }
732
+ }
733
+ @property --tw-rotate-x {
734
+ syntax: "*";
735
+ inherits: false;
736
+ }
737
+ @property --tw-rotate-y {
738
+ syntax: "*";
739
+ inherits: false;
740
+ }
741
+ @property --tw-rotate-z {
742
+ syntax: "*";
743
+ inherits: false;
744
+ }
745
+ @property --tw-skew-x {
746
+ syntax: "*";
747
+ inherits: false;
748
+ }
749
+ @property --tw-skew-y {
750
+ syntax: "*";
751
+ inherits: false;
752
+ }
753
+ @property --tw-border-style {
754
+ syntax: "*";
755
+ inherits: false;
756
+ initial-value: solid;
757
+ }
758
+ @property --tw-leading {
759
+ syntax: "*";
760
+ inherits: false;
761
+ }
762
+ @property --tw-font-weight {
763
+ syntax: "*";
764
+ inherits: false;
765
+ }
766
+ @property --tw-tracking {
767
+ syntax: "*";
768
+ inherits: false;
769
+ }
770
+ @property --tw-ordinal {
771
+ syntax: "*";
772
+ inherits: false;
773
+ }
774
+ @property --tw-slashed-zero {
775
+ syntax: "*";
776
+ inherits: false;
777
+ }
778
+ @property --tw-numeric-figure {
779
+ syntax: "*";
780
+ inherits: false;
781
+ }
782
+ @property --tw-numeric-spacing {
783
+ syntax: "*";
784
+ inherits: false;
785
+ }
786
+ @property --tw-numeric-fraction {
787
+ syntax: "*";
788
+ inherits: false;
789
+ }
790
+ @property --tw-shadow {
791
+ syntax: "*";
792
+ inherits: false;
793
+ initial-value: 0 0 #0000;
794
+ }
795
+ @property --tw-shadow-color {
796
+ syntax: "*";
797
+ inherits: false;
798
+ }
799
+ @property --tw-shadow-alpha {
800
+ syntax: "<percentage>";
801
+ inherits: false;
802
+ initial-value: 100%;
803
+ }
804
+ @property --tw-inset-shadow {
805
+ syntax: "*";
806
+ inherits: false;
807
+ initial-value: 0 0 #0000;
808
+ }
809
+ @property --tw-inset-shadow-color {
810
+ syntax: "*";
811
+ inherits: false;
812
+ }
813
+ @property --tw-inset-shadow-alpha {
814
+ syntax: "<percentage>";
815
+ inherits: false;
816
+ initial-value: 100%;
817
+ }
818
+ @property --tw-ring-color {
819
+ syntax: "*";
820
+ inherits: false;
821
+ }
822
+ @property --tw-ring-shadow {
823
+ syntax: "*";
824
+ inherits: false;
825
+ initial-value: 0 0 #0000;
826
+ }
827
+ @property --tw-inset-ring-color {
828
+ syntax: "*";
829
+ inherits: false;
830
+ }
831
+ @property --tw-inset-ring-shadow {
832
+ syntax: "*";
833
+ inherits: false;
834
+ initial-value: 0 0 #0000;
835
+ }
836
+ @property --tw-ring-inset {
837
+ syntax: "*";
838
+ inherits: false;
839
+ }
840
+ @property --tw-ring-offset-width {
841
+ syntax: "<length>";
842
+ inherits: false;
843
+ initial-value: 0px;
844
+ }
845
+ @property --tw-ring-offset-color {
846
+ syntax: "*";
847
+ inherits: false;
848
+ initial-value: #fff;
849
+ }
850
+ @property --tw-ring-offset-shadow {
851
+ syntax: "*";
852
+ inherits: false;
853
+ initial-value: 0 0 #0000;
854
+ }
855
+ @property --tw-outline-style {
856
+ syntax: "*";
857
+ inherits: false;
858
+ initial-value: solid;
859
+ }
860
+ @property --tw-blur {
861
+ syntax: "*";
862
+ inherits: false;
863
+ }
864
+ @property --tw-brightness {
865
+ syntax: "*";
866
+ inherits: false;
867
+ }
868
+ @property --tw-contrast {
869
+ syntax: "*";
870
+ inherits: false;
871
+ }
872
+ @property --tw-grayscale {
873
+ syntax: "*";
874
+ inherits: false;
875
+ }
876
+ @property --tw-hue-rotate {
877
+ syntax: "*";
878
+ inherits: false;
879
+ }
880
+ @property --tw-invert {
881
+ syntax: "*";
882
+ inherits: false;
883
+ }
884
+ @property --tw-opacity {
885
+ syntax: "*";
886
+ inherits: false;
887
+ }
888
+ @property --tw-saturate {
889
+ syntax: "*";
890
+ inherits: false;
891
+ }
892
+ @property --tw-sepia {
893
+ syntax: "*";
894
+ inherits: false;
895
+ }
896
+ @property --tw-drop-shadow {
897
+ syntax: "*";
898
+ inherits: false;
899
+ }
900
+ @property --tw-drop-shadow-color {
901
+ syntax: "*";
902
+ inherits: false;
903
+ }
904
+ @property --tw-drop-shadow-alpha {
905
+ syntax: "<percentage>";
906
+ inherits: false;
907
+ initial-value: 100%;
908
+ }
909
+ @property --tw-drop-shadow-size {
910
+ syntax: "*";
911
+ inherits: false;
912
+ }
913
+ @layer properties {
914
+ @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
915
+ *, ::before, ::after, ::backdrop {
916
+ --tw-rotate-x: initial;
917
+ --tw-rotate-y: initial;
918
+ --tw-rotate-z: initial;
919
+ --tw-skew-x: initial;
920
+ --tw-skew-y: initial;
921
+ --tw-border-style: solid;
922
+ --tw-leading: initial;
923
+ --tw-font-weight: initial;
924
+ --tw-tracking: initial;
925
+ --tw-ordinal: initial;
926
+ --tw-slashed-zero: initial;
927
+ --tw-numeric-figure: initial;
928
+ --tw-numeric-spacing: initial;
929
+ --tw-numeric-fraction: initial;
930
+ --tw-shadow: 0 0 #0000;
931
+ --tw-shadow-color: initial;
932
+ --tw-shadow-alpha: 100%;
933
+ --tw-inset-shadow: 0 0 #0000;
934
+ --tw-inset-shadow-color: initial;
935
+ --tw-inset-shadow-alpha: 100%;
936
+ --tw-ring-color: initial;
937
+ --tw-ring-shadow: 0 0 #0000;
938
+ --tw-inset-ring-color: initial;
939
+ --tw-inset-ring-shadow: 0 0 #0000;
940
+ --tw-ring-inset: initial;
941
+ --tw-ring-offset-width: 0px;
942
+ --tw-ring-offset-color: #fff;
943
+ --tw-ring-offset-shadow: 0 0 #0000;
944
+ --tw-outline-style: solid;
945
+ --tw-blur: initial;
946
+ --tw-brightness: initial;
947
+ --tw-contrast: initial;
948
+ --tw-grayscale: initial;
949
+ --tw-hue-rotate: initial;
950
+ --tw-invert: initial;
951
+ --tw-opacity: initial;
952
+ --tw-saturate: initial;
953
+ --tw-sepia: initial;
954
+ --tw-drop-shadow: initial;
955
+ --tw-drop-shadow-color: initial;
956
+ --tw-drop-shadow-alpha: 100%;
957
+ --tw-drop-shadow-size: initial;
958
+ }
959
+ }
960
+ }
961
+
962
+ /* ---- ./_system-light.css ---- */
963
+ /* The `system` theme's light half.
964
+ *
965
+ * Its own file, and outside the components layer, because it is a *theme*
966
+ * rather than a component rule. `tokens.css` states the dark base on `:root`
967
+ * and is unlayered; the moment this sat inside `@layer components` it lost to
968
+ * that, and every `system` specimen rendered dark on a light-mode machine.
969
+ * Unlayered beats layered whatever the selector says, which is exactly the
970
+ * trap this repository keeps finding in different costumes.
971
+ *
972
+ * The underscore marks it the same way `_tailwind.built.css` is marked:
973
+ * `copy-css.mjs` passes both through without wrapping them.
974
+ */
282
975
  /* Light, only under the `system` theme. This used to override `:root`
283
976
  unconditionally, so choosing a dark theme on a light-mode laptop got
284
977
  silently repainted. Every other theme states its own palette. */
@@ -307,6 +1000,8 @@
307
1000
  }
308
1001
  }
309
1002
 
1003
+ /* ---- ./base.css ---- */
1004
+ @layer components {
310
1005
  /* Derived from the two surfaces every theme already defines, so a theme gets a
311
1006
  control colour without knowing this exists. */
312
1007
  /* Pseudo-elements too. `*` does not match them, and `box-sizing` is not
@@ -371,23 +1066,31 @@ body::before {
371
1066
  `[aria-disabled='true']` twin rather than a rewrite, which leaves each
372
1067
  `button` selector at the specificity it had and every committed baseline
373
1068
  untouched. */
374
- a.btn {
1069
+ .btn {
1070
+ /* The three an anchor brings that a `<button>` does not. Harmless on the
1071
+ button form -- it has no underline and centres already -- and the reason
1072
+ `Button` puts one class on both elements rather than keeping a twin
1073
+ selector for each. */
375
1074
  text-decoration: none;
376
1075
  color: var(--text);
377
1076
  text-align: center;
378
1077
  }
379
1078
 
380
- a.btn[aria-disabled='true'] {
1079
+ .btn[aria-disabled='true'] {
381
1080
  /* A disabled anchor still takes the pointer, so say so. The click itself is
382
1081
  blocked in the capture phase by the component. */
383
1082
  cursor: not-allowed;
384
1083
  }
385
1084
 
386
- button,
387
- a.btn,
388
- input,
389
- textarea,
390
- select {
1085
+ /* `:where`, so this base carries no specificity of its own.
1086
+ *
1087
+ * It was `button, input, textarea, select` -- an element selector, which
1088
+ * `.size-sm` and every other class outranked without anyone thinking about
1089
+ * it. Moving it to `.btn` to stop a tab inheriting a control's look raised it
1090
+ * to a class, so it tied with `.size-sm` and won on source order: every
1091
+ * button came out `8px 12px` whatever size it asked for. `:where` puts it
1092
+ * back to zero, which is what the element selector was silently providing. */
1093
+ :where(.btn, input, textarea, select) {
391
1094
  font: inherit;
392
1095
  color: var(--text);
393
1096
  background: var(--control);
@@ -408,8 +1111,7 @@ select {
408
1111
  the icon; the button simply had no opinion about how to arrange two things.
409
1112
  `inline-flex` and a gap gives it one, and the gap replaces the space
410
1113
  character people were putting between an icon and its label. */
411
- button,
412
- a.btn {
1114
+ .btn {
413
1115
  display: inline-flex;
414
1116
  align-items: center;
415
1117
  justify-content: center;
@@ -425,18 +1127,41 @@ a.btn {
425
1127
 
426
1128
  Several places already drew this by hand as `.chat-bar-btn.on`. This is the
427
1129
  same look, said once, on a plain button. */
428
- button[aria-pressed='true'] {
1130
+ .btn[aria-pressed='true'] {
429
1131
  border-color: var(--accent);
430
1132
  color: var(--accent);
431
1133
  background: color-mix(in srgb, var(--accent) 10%, var(--panel));
432
1134
  }
433
1135
 
434
- button[aria-pressed='true']:hover:not(:disabled) { border-color: var(--accent); }
1136
+ .btn[aria-pressed='true']:hover:not(:disabled) { border-color: var(--accent); }
435
1137
 
436
1138
  /* The glyph never shrinks: in a narrow row flexbox would take it from the icon
437
1139
  before the text, and half an icon is not a smaller icon. */
438
- button > svg,
439
- a.btn > svg { flex: none; }
1140
+ .btn > svg { flex: none; }
1141
+
1142
+ /* `Select`'s control wears the control styling, and says so.
1143
+ *
1144
+ * It renders React Aria's `Button` -- a bare `<button>` -- and used to take
1145
+ * `font`, colour, the surface and the border from the
1146
+ * `button, input, textarea, select` reset without asking. When that reset
1147
+ * moved to `.btn`, so a tab and a row would stop inheriting a control's look
1148
+ * only to undo it, this lost all of it at once.
1149
+ *
1150
+ * Here rather than in `select.css`, and that placement is the rule, not
1151
+ * tidiness: `font` is a shorthand and it resets `font-size`, so declared
1152
+ * after `.size-sm` below it silently makes every small control full size.
1153
+ * `index.css` says the import order is behaviour; this is what that means.
1154
+ *
1155
+ * `:where` for the same reason the reset above uses it: this is a base a
1156
+ * `size` class has to be able to beat, and a bare class ties with one. */
1157
+ :where(.sel-control) {
1158
+ font: inherit;
1159
+ color: var(--text);
1160
+ background: var(--control);
1161
+ border: var(--border-width) solid var(--border-strong);
1162
+ border-radius: var(--radius);
1163
+ cursor: pointer;
1164
+ }
440
1165
 
441
1166
  .size-sm { font-size: var(--text-xs); padding: var(--space-1) var(--space-2); }
442
1167
 
@@ -461,8 +1186,7 @@ input, textarea, select { width: 100%; }
461
1186
  same disagreement written twice. */
462
1187
  textarea { resize: none; }
463
1188
 
464
- button:hover:not(:disabled),
465
- a.btn:hover:not([aria-disabled='true']),
1189
+ .btn:hover:not(:disabled),
466
1190
  input:hover:not(:disabled),
467
1191
  textarea:hover:not(:disabled),
468
1192
  select:hover:not(:disabled) { border-color: var(--accent); }
@@ -470,8 +1194,7 @@ select:hover:not(:disabled) { border-color: var(--accent); }
470
1194
  /* `:focus-visible` on the button so a click does not leave a ring behind it,
471
1195
  `:focus` on the fields because a focused field is a place you are typing and
472
1196
  should say so however you got there. */
473
- button:focus-visible,
474
- a.btn:focus-visible,
1197
+ .btn:focus-visible,
475
1198
  input:focus,
476
1199
  textarea:focus,
477
1200
  select:focus { outline: none; border-color: var(--accent); }
@@ -564,14 +1287,13 @@ input[type='radio']:focus-visible { outline: none; box-shadow: var(--focus-ring)
564
1287
  input[type='checkbox']:disabled,
565
1288
  input[type='radio']:disabled { cursor: not-allowed; background: var(--panel-2); }
566
1289
 
567
- button:disabled,
568
- a.btn[aria-disabled='true'],
1290
+ .btn:disabled,
1291
+ .btn[aria-disabled='true'],
569
1292
  input:disabled,
570
1293
  textarea:disabled,
571
1294
  select:disabled { opacity: 0.45; cursor: not-allowed; }
572
1295
 
573
- button.primary,
574
- a.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
1296
+ .btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
575
1297
 
576
1298
  /* A disabled primary is not a faded primary.
577
1299
  `opacity: .45` was the whole of it, and a coloured fill at 45% is still a
@@ -579,8 +1301,7 @@ a.btn.primary { background: var(--accent); border-color: var(--accent); color: v
579
1301
  thing on the panel. That got worse when onboarding started holding people at
580
1302
  a disabled Next. So it gives up the fill entirely and becomes the shape of a
581
1303
  disabled control -- the opacity then has nothing left to do. */
582
- button.primary:disabled,
583
- a.btn.primary[aria-disabled='true'] {
1304
+ .btn.primary:disabled {
584
1305
  background: var(--panel-2);
585
1306
  border-color: var(--border);
586
1307
  color: var(--muted);
@@ -594,7 +1315,7 @@ a.btn.primary[aria-disabled='true'] {
594
1315
  line under it that travels.
595
1316
  `aria-busy` rather than a class: the attribute is what a screen reader reads,
596
1317
  and styling the thing that carries the meaning is how they stay in step. */
597
- button[aria-busy='true'] {
1318
+ .btn[aria-busy='true'] {
598
1319
  position: relative;
599
1320
  overflow: hidden;
600
1321
  cursor: progress;
@@ -607,18 +1328,18 @@ button[aria-busy='true'] {
607
1328
  something that is working, that reads as a control that failed and went
608
1329
  grey. So a busy button keeps every colour it had and takes only the loss of
609
1330
  the cursor and the press; the travelling line is what says why. */
610
- button[aria-busy='true']:disabled {
1331
+ .btn[aria-busy='true']:disabled {
611
1332
  opacity: 1;
612
1333
  cursor: progress;
613
1334
  }
614
1335
 
615
- button.primary[aria-busy='true']:disabled {
1336
+ .btn.primary[aria-busy='true']:disabled {
616
1337
  background: var(--accent);
617
1338
  border-color: var(--accent);
618
1339
  color: var(--on-accent);
619
1340
  }
620
1341
 
621
- button[aria-busy='true']::after {
1342
+ .btn[aria-busy='true']::after {
622
1343
  content: '';
623
1344
  position: absolute;
624
1345
  left: 0;
@@ -650,15 +1371,13 @@ button[aria-busy='true']::after {
650
1371
 
651
1372
  /* Quiet: no border, no fill. For the action beside the action -- Cancel next to
652
1373
  Save -- where a bordered box competes with the thing it is next to. */
653
- button.ghost,
654
- a.btn.ghost {
1374
+ .btn.ghost {
655
1375
  background: transparent;
656
1376
  border-color: transparent;
657
1377
  color: var(--muted);
658
1378
  }
659
1379
 
660
- button.ghost:hover:not(:disabled),
661
- a.btn.ghost:hover:not([aria-disabled='true']) {
1380
+ .btn.ghost:hover:not(:disabled) {
662
1381
  background: var(--panel-2);
663
1382
  border-color: transparent;
664
1383
  color: var(--text);
@@ -740,7 +1459,7 @@ a.btn.ghost:hover:not([aria-disabled='true']) {
740
1459
  header.) */
741
1460
  .row { display: flex; align-items: center; gap: var(--space-3); }
742
1461
 
743
- .grow { flex: 1; min-width: 0; }
1462
+ .ctl-grow { flex: 1; min-width: 0; }
744
1463
 
745
1464
  .mono { font-family: var(--font-mono); font-size: var(--text-sm); }
746
1465
 
@@ -804,10 +1523,17 @@ a.btn.ghost:hover:not([aria-disabled='true']) {
804
1523
  /* A pill following a name needs the gap outside its own border. Leading
805
1524
  whitespace inside the span puts it under the pill's background instead,
806
1525
  which reads as the badge touching the word it qualifies. */
807
- .row-pill { margin-left: var(--space-2); vertical-align: baseline; }
1526
+ /* A pill beside a name, and the one thing that keeps the pair working:
1527
+ `flex: none`, so the pill never shrinks and the name ellipsises instead.
1528
+ That was `rows.css`'s `.named > .pill`, which meant anybody laying out a
1529
+ name and its pills had to know a package class by name to get it. The
1530
+ variant already says "I am in a row"; it may as well say what that costs. */
1531
+ .row-pill { margin-left: var(--space-2); vertical-align: baseline; flex: none; }
808
1532
 
809
1533
  /* The tile's only control. Dim until hovered: it is chrome on someone's app. */
810
1534
  .icon-btn {
1535
+ /* 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`. */
1536
+ font: inherit;
811
1537
  display: inline-flex; align-items: center; padding: var(--space-1);
812
1538
  background: none; border: 0; color: var(--muted); opacity: 0.6; cursor: pointer;
813
1539
  }
@@ -873,19 +1599,17 @@ a.btn.ghost:hover:not([aria-disabled='true']) {
873
1599
  transition: none;
874
1600
  }
875
1601
 
876
- button.primary:active:not(:disabled),
877
- a.btn.primary:active:not([aria-disabled='true']) {
1602
+ .btn.primary:active:not(:disabled) {
878
1603
  background: color-mix(in srgb, var(--text) var(--press-ink), var(--accent));
879
1604
  border-color: color-mix(in srgb, var(--text) var(--press-ink), var(--accent));
880
1605
  }
881
1606
 
882
- button.ghost:active:not(:disabled),
883
- a.btn.ghost:active:not([aria-disabled='true']) {
1607
+ .btn.ghost:active:not(:disabled) {
884
1608
  background: color-mix(in srgb, var(--text) var(--press-ink), var(--panel-2));
885
1609
  border-color: transparent;
886
1610
  }
887
1611
 
888
- button[aria-pressed='true']:active:not(:disabled) {
1612
+ .btn[aria-pressed='true']:active:not(:disabled) {
889
1613
  background: color-mix(in srgb, var(--accent) 18%, var(--panel));
890
1614
  }
891
1615
 
@@ -1056,7 +1780,7 @@ input:active, textarea:active, select:active { transform: none; }
1056
1780
 
1057
1781
  /* The top bar carries the title, the views and the way into settings. Nothing
1058
1782
  that belongs to a conversation lives here any more, so it can breathe. */
1059
- .topbar .grow { flex: 1; min-width: 0; }
1783
+ .topbar .ctl-grow { flex: 1; min-width: 0; }
1060
1784
 
1061
1785
  /* Back on the left, Next on the right, with the gap between them doing the
1062
1786
  separating -- so the button that goes forward is in the same place on both
@@ -1088,7 +1812,60 @@ input:active, textarea:active, select:active { transform: none; }
1088
1812
  to { opacity: 1; }
1089
1813
  }
1090
1814
 
1815
+ /* The states the `button` element rule used to hand every bare `<button>` in
1816
+ the package, now asked for by name.
1817
+ *
1818
+ * This is the half of the decoupling that a screenshot cannot show: `:focus-visible`
1819
+ * is not photographed, so nine components would have lost their focus ring in
1820
+ * silence. `keyboard.spec.ts` is what actually holds this.
1821
+ *
1822
+ * `.btn` has its own, in the rules above. These are the ones that are a button
1823
+ * for their behaviour and not for their appearance -- a tab, a row, a card, a
1824
+ * pager step, a grid cell, a remove cross. */
1825
+ .tab:focus-visible,
1826
+ .row-hit:focus-visible,
1827
+ .card:focus-visible,
1828
+ .ident-remove:focus-visible,
1829
+ .size-cell:focus-visible,
1830
+ .pager-step:focus-visible,
1831
+ .pager-page:focus-visible,
1832
+ .sel-control:focus-visible {
1833
+ outline: 2px solid var(--accent);
1834
+ outline-offset: 2px;
1835
+ }
1836
+
1837
+ /* `:disabled` came from the same rule. A control that cannot be pressed has to
1838
+ say so whatever element it is. */
1839
+ .tab:disabled,
1840
+ .row-hit:disabled,
1841
+ .card:disabled,
1842
+ .ident-remove:disabled,
1843
+ .size-cell:disabled,
1844
+ .pager-step:disabled,
1845
+ .pager-page:disabled,
1846
+ .sel-control:disabled {
1847
+ opacity: 0.45;
1848
+ cursor: not-allowed;
1849
+ }
1850
+
1851
+ /* The anchor form's disabled states, alongside the button's.
1852
+ *
1853
+ * `.btn` is one class on two elements, and an `<a>` has no `:disabled` -- it
1854
+ * carries `aria-disabled` and a capture-phase click block instead. Every
1855
+ * rule above that reads `:disabled` needs the attribute twin, or a disabled
1856
+ * link is a link that looks pressable and refuses silently. Folding the old
1857
+ * `a.btn` twins into `.btn` dropped these, and the baselines showed a
1858
+ * disabled link at full strength. */
1859
+ .btn.primary[aria-disabled='true'] {
1860
+ background: var(--panel-2);
1861
+ border-color: var(--border);
1862
+ color: var(--muted);
1863
+ opacity: 1;
1864
+ }
1865
+ }
1866
+
1091
1867
  /* ---- ./select.css ---- */
1868
+ @layer components {
1092
1869
  /* How big a control is, asked for rather than inherited.
1093
1870
  Four sizes existed and every one of them came from a descendant selector:
1094
1871
  `.set-row button` was 11px in 3px of padding, `.approval-actions button` was
@@ -1103,8 +1880,7 @@ input:active, textarea:active, select:active { transform: none; }
1103
1880
  /* Full width, for the one action a card or a step is about. Not a size: a
1104
1881
  `block` button is the same height as any other, it just stops being a thing
1105
1882
  you have to aim at. */
1106
- button.block,
1107
- a.btn.block { display: flex; width: 100%; }
1883
+ .btn.ctl-block { display: flex; width: 100%; }
1108
1884
 
1109
1885
  /* The default is `size-md`, spelled once for every control.
1110
1886
  The paragraph above promised one scale for buttons, inputs and selects, and
@@ -1112,13 +1888,24 @@ a.btn.block { display: flex; width: 100%; }
1112
1888
  input was eight pixels taller than the button beside it and no row of the two
1113
1889
  could line up. The download modal's search box against its Search button was
1114
1890
  the version you could see from across the room. */
1115
- button,
1116
- a.btn,
1117
- input,
1118
- textarea,
1119
- select,
1891
+ /* `:where`, so this base carries no specificity of its own.
1892
+ *
1893
+ * It was `button, input, textarea, select` -- an element selector, which
1894
+ * `.size-sm` and every other class outranked without anyone thinking about
1895
+ * it. Moving it to `.btn` to stop a tab inheriting a control's look raised it
1896
+ * to a class, so it tied with `.size-sm` and won on source order: every
1897
+ * button came out `8px 12px` whatever size it asked for. `:where` puts it
1898
+ * back to zero, which is what the element selector was silently providing. */
1899
+ :where(.btn, input, textarea, select) { padding: var(--space-2) var(--space-3); }
1900
+
1901
+ /* The select's control keeps its padding at full class weight, and after
1902
+ `.size-sm` in the cascade, because that is where it has always been: this
1903
+ file is imported after `base.css`, so a `size`d select took the size's type
1904
+ and this padding. Folding it into the `:where` above handed the padding
1905
+ back to `.size-sm` and shrank every small select by seven pixels. */
1120
1906
  .sel-control { padding: var(--space-2) var(--space-3); }
1121
1907
 
1908
+
1122
1909
  /* Centred on the whole block -- the caption and the sentence together -- rather
1123
1910
  than aligned to either end of it. `.field-row` pins to the bottom, which is
1124
1911
  right for a field beside a button and wrong beside two lines of prose. */
@@ -1142,16 +1929,16 @@ select,
1142
1929
  .sel { position: relative; display: inline-block; }
1143
1930
 
1144
1931
  /* The subject of its pane rather than a field in it: full width and a size up.
1145
- That was `.select-field.block` on the native element, and it carried the
1932
+ That was `.select-field.ctl-block` on the native element, and it carried the
1146
1933
  reason with it -- model ids are long (`qwen2.5-coder:32b-instruct-q4_K_M`) and
1147
1934
  at the narrow size they truncated mid-name. */
1148
- .sel.block { display: block; width: 100%; }
1935
+ .sel.ctl-block { display: block; width: 100%; }
1149
1936
 
1150
- .sel.block .sel-control { padding: var(--space-2) var(--space-3); font-weight: 500; }
1937
+ .sel.ctl-block .sel-control { padding: var(--space-2) var(--space-3); font-weight: 500; }
1151
1938
 
1152
1939
  /* Not full width otherwise: a select as wide as its row, holding one short
1153
1940
  word, reads as an input somebody forgot to fill in. */
1154
- .sel:not(.block) .sel-control { width: auto; max-width: 320px; }
1941
+ .sel:not(.ctl-block) .sel-control { width: auto; max-width: 320px; }
1155
1942
 
1156
1943
  .sel-control {
1157
1944
  display: flex;
@@ -1164,9 +1951,7 @@ select,
1164
1951
  background: var(--control);
1165
1952
  }
1166
1953
 
1167
- .sel-value { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1168
-
1169
- .sel-caret { flex: none; opacity: 0.6; transition: transform var(--dur-fast) var(--ease); }
1954
+ .sel-caret { transition: transform var(--dur-fast) var(--ease); }
1170
1955
 
1171
1956
  .sel[data-open] .sel-caret { transform: rotate(180deg); }
1172
1957
 
@@ -1175,45 +1960,22 @@ select,
1175
1960
  /* State is read off React Aria's attributes: `data-open` on the root,
1176
1961
  `data-focused` / `data-selected` / `data-disabled` on an item, where
1177
1962
  `.open`, `[data-active]`, `.on` and `.off` used to be. */
1963
+ /* The list keeps two things the utilities on `Select` cannot say.
1964
+ *
1965
+ * `scroll-padding-block`: the same room while the list is scrolled as it has
1966
+ * at rest. Padding at the top of a scroll container scrolls away with the
1967
+ * content, so the row `scrollIntoView({block: 'nearest'})` brings up ended
1968
+ * flush against the border and read as clipped rather than as one of a set --
1969
+ * and this list opens scrolled to whatever is chosen, so that was the
1970
+ * ordinary state.
1971
+ *
1972
+ * `box-shadow`: `--shadow-2` is a token, and a utility would have to inline
1973
+ * the value, which is the one thing the vocabulary exists to stop. */
1178
1974
  .sel-list {
1179
- /* `fixed`, and in `document.body`: an ancestor that scrolls or hides its
1180
- overflow would otherwise clip it, which in this app means every settings
1181
- pane. Position comes from the button's measured rectangle. */
1182
- /* Positioned by React Aria -- inline `top`/`left` on a portalled popover
1183
- that flips when the edge is near. `position: fixed` and the measured
1184
- rectangle it was placed by are gone with the hand-rolled version. */
1185
- z-index: 60;
1186
- max-height: 280px;
1187
- overflow-y: auto;
1188
- margin: 0;
1189
- padding: var(--space-1);
1190
- /* The same room while the list is scrolled as it has at rest. Padding at
1191
- the top of a scroll container scrolls away with the content, so the row
1192
- `scrollIntoView({block: 'nearest'})` brings up ended flush against the
1193
- border and read as clipped rather than as one of a set -- and this list
1194
- opens scrolled to whatever is chosen, so that was the ordinary state.
1195
- `scroll-padding` insets what counts as "in view", so it stops short by
1196
- the same four the sides have. */
1197
1975
  scroll-padding-block: var(--space-1);
1198
- list-style: none;
1199
- border: var(--border-width) solid var(--border-strong);
1200
- border-radius: var(--radius-md);
1201
- background: var(--panel);
1202
1976
  box-shadow: var(--shadow-2);
1203
1977
  }
1204
1978
 
1205
- .sel-item {
1206
- display: flex;
1207
- align-items: center;
1208
- gap: var(--space-2);
1209
- padding: var(--space-2) var(--space-2);
1210
- border-radius: var(--radius-sm);
1211
- cursor: pointer;
1212
- white-space: nowrap;
1213
- }
1214
-
1215
- .sel-item-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
1216
-
1217
1979
  /* Hover and keyboard land on the same row, because they are the same idea --
1218
1980
  two highlights for one cursor is how a list starts lying about where Enter
1219
1981
  will go. */
@@ -1225,7 +1987,18 @@ select,
1225
1987
 
1226
1988
  /* The tick marks the chosen row. Colour alone would not: a list where the only
1227
1989
  difference is a hue is a list you cannot read past. */
1228
- .sel-tick { flex: none; opacity: 0; }
1990
+ /* The tick's resting state lives here, beside the state that overrides it.
1991
+ *
1992
+ * It was `opacity-0` on the element and `opacity: 1` here, which worked only
1993
+ * while this sheet was unlayered. A utility is in `@layer utilities` and
1994
+ * beats `@layer components` whatever the selector, so the moment the sheets
1995
+ * moved into a layer the tick stopped appearing on the selected row -- the
1996
+ * resting state won permanently.
1997
+ *
1998
+ * The rule generalises: a utility for a resting state whose active state is a
1999
+ * relationship between elements will always beat that active state. The pair
2000
+ * has to sit in the same layer. */
2001
+ .sel-tick { opacity: 0; }
1229
2002
 
1230
2003
  .sel-item[data-selected] .sel-tick { opacity: 1; }
1231
2004
 
@@ -1261,24 +2034,23 @@ select,
1261
2034
  .field > input,
1262
2035
  .field > textarea,
1263
2036
  .field > .sel { width: 100%; }
2037
+ }
1264
2038
 
1265
2039
  /* ---- ./dangerzone.css ---- */
2040
+ @layer components {
1266
2041
  /* Destructive, and legible as such standing still. */
1267
- button.danger,
1268
- a.btn.danger {
2042
+ .btn.danger {
1269
2043
  color: var(--bad);
1270
2044
  border-color: color-mix(in srgb, var(--bad) 45%, var(--border-strong));
1271
2045
  }
1272
2046
 
1273
- button.danger:hover:not(:disabled),
1274
- a.btn.danger:hover:not([aria-disabled='true']) {
2047
+ .btn.danger:hover:not(:disabled) {
1275
2048
  background: color-mix(in srgb, var(--bad) 12%, var(--panel));
1276
2049
  border-color: var(--bad);
1277
2050
  color: var(--bad);
1278
2051
  }
1279
2052
 
1280
- button.danger:active:not(:disabled),
1281
- a.btn.danger:active:not([aria-disabled='true']) {
2053
+ .btn.danger:active:not(:disabled) {
1282
2054
  background: color-mix(in srgb, var(--bad) 20%, var(--panel));
1283
2055
  border-color: var(--bad);
1284
2056
  }
@@ -1377,8 +2149,10 @@ a.btn.danger:active:not([aria-disabled='true']) {
1377
2149
  controls in the same column at the same indent that do not line up read as
1378
2150
  an accident rather than as a pair. */
1379
2151
  .field-row > button { min-width: 9ch; }
2152
+ }
1380
2153
 
1381
2154
  /* ---- ./callout.css ---- */
2155
+ @layer components {
1382
2156
  /* One shape for everything that tells you something.
1383
2157
  `.banner` and `.callout` were the same box drawn twice: a bordered, rounded,
1384
2158
  12px panel with a tint. They differed in padding by a pixel in each direction
@@ -1517,35 +2291,50 @@ a.btn.danger:active:not([aria-disabled='true']) {
1517
2291
  }
1518
2292
 
1519
2293
  .callout button { flex: none; }
2294
+ }
1520
2295
 
1521
2296
  /* ---- ./card.css ---- */
2297
+ @layer components {
2298
+ /* `.card` is public API, not this component's private styling.
2299
+ *
2300
+ * Consumers apply it to their own markup -- the gallery's skeleton specimen
2301
+ * renders `<div className="card">` around three rows, and app-template uses
2302
+ * `card-pick`. Moving the box into `Card`'s own class list took that away
2303
+ * with no type error and no failing test in this package: it broke only
2304
+ * where somebody else had used the class, which is the worst way for an API
2305
+ * to go.
2306
+ *
2307
+ * So the rule stays and `Card` wears it. The lesson generalises to every
2308
+ * class in this package that a consumer can reach: a component may draw
2309
+ * itself with utilities only where its class is genuinely private. */
1522
2310
  .card {
2311
+ display: block;
2312
+ width: 100%;
2313
+ box-sizing: border-box;
2314
+ font: inherit;
2315
+ color: var(--text);
2316
+ text-align: left;
1523
2317
  background: var(--panel);
1524
2318
  border: var(--border-width) solid var(--border);
1525
2319
  border-radius: var(--radius-md);
1526
- padding: var(--space-3) var(--space-3);
2320
+ padding: var(--space-3);
1527
2321
  margin-bottom: var(--space-3);
1528
2322
  }
1529
2323
 
2324
+ /* What is left once `Card` draws its own box: the tones, the pick and
2325
+ selected states, and the rules that reach from the card into what it
2326
+ holds. `:has(.card-icon) .card-inline` is a relationship, and a tone is a
2327
+ `color-mix`; neither is a property on one element. */
2328
+
1530
2329
  /* Header, when there is one. The title and the line under it were retyped at
1531
2330
  every call site and no two agreed on the gap between them. */
1532
- .card-head { display: flex; align-items: flex-start; gap: var(--space-3); }
1533
-
1534
- .card-head:not(:last-child) { margin-bottom: var(--space-3); }
1535
-
1536
- .card-headings { min-width: 0; flex: 1; }
1537
-
1538
2331
  /* The card owns the space between the things it holds. Without this a callout
1539
2332
  and the button under it sit edge to edge, and every caller has to remember a
1540
2333
  margin on whichever child happens to be second. */
1541
- .card-body { display: grid; gap: var(--space-3); }
1542
-
1543
2334
  /* The card's own control, on the card's padding rather than indented into its
1544
2335
  body. Centred against the whole heading block, not against the title -- a
1545
2336
  switch aligned to the first line drifts upward the moment the description
1546
2337
  runs to two. */
1547
- .card-action { flex: none; align-self: center; display: flex; align-items: center; }
1548
-
1549
2338
  /* A row inside a card that lines up with the header rather than indenting past
1550
2339
  it: same left edge as the title, same right edge as the action. Its label is
1551
2340
  a quiet caption, not a second heading -- the card already has one, and two
@@ -1592,17 +2381,6 @@ a.btn.danger:active:not([aria-disabled='true']) {
1592
2381
  color: var(--muted);
1593
2382
  }
1594
2383
 
1595
- /* A card that is itself the choice. Reset first, because it is a `<button>`
1596
- now and inherits a control's padding and centring. */
1597
- button.card {
1598
- display: block;
1599
- width: 100%;
1600
- text-align: left;
1601
- font: inherit;
1602
- color: inherit;
1603
- padding: var(--space-3);
1604
- }
1605
-
1606
2384
  /* Variants, because a card is doing four jobs and looked like one.
1607
2385
  A search result you can act on, a thing already installed, something that
1608
2386
  went wrong, and a heading with rows under it are not the same object, and
@@ -1645,7 +2423,23 @@ button.card {
1645
2423
 
1646
2424
  .modal-body .card { background: var(--panel-2); }
1647
2425
 
2426
+ /* Selected, which used to arrive from `button[aria-pressed='true']`.
2427
+ *
2428
+ * A card marks its choice with `aria-pressed`, and the tint came from the
2429
+ * element rule every bare `<button>` in the package was picking up. That rule
2430
+ * belongs to `Button` now, so the card says what being chosen looks like --
2431
+ * which is the right place for it anyway: a chosen card is a card's idea, not
2432
+ * a button's. */
2433
+ .card[aria-pressed='true'] {
2434
+ border-color: var(--accent);
2435
+ background: color-mix(in srgb, var(--accent) 10%, var(--panel));
2436
+ }
2437
+
2438
+ .card[aria-pressed='true']:hover { border-color: var(--accent); }
2439
+ }
2440
+
1648
2441
  /* ---- ./toggle.css ---- */
2442
+ @layer components {
1649
2443
  .card-action .switch-row { padding: 0; gap: var(--space-2); }
1650
2444
 
1651
2445
  .card-action .switch-row .switch-body { flex: none; }
@@ -1876,8 +2670,10 @@ input[type='radio']:active:not(:disabled) {
1876
2670
  @media (prefers-reduced-motion: reduce) {
1877
2671
  .toggle-sweep::before { animation: none; background: color-mix(in oklab, var(--accent) 45%, transparent); }
1878
2672
  }
2673
+ }
1879
2674
 
1880
2675
  /* ---- ./skeleton.css ---- */
2676
+ @layer components {
1881
2677
  .muted { color: var(--muted); }
1882
2678
 
1883
2679
  .pulse-block {
@@ -1932,8 +2728,10 @@ input[type='radio']:active:not(:disabled) {
1932
2728
  /* Still distinguishable from a real block of colour, and still still. */
1933
2729
  .skel-pulse { animation: none; opacity: 0.8; }
1934
2730
  }
2731
+ }
1935
2732
 
1936
2733
  /* ---- ./rows.css ---- */
2734
+ @layer components {
1937
2735
  .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1938
2736
 
1939
2737
  /* A name and the pills that qualify it, on one line, where the name is the only
@@ -2194,72 +2992,24 @@ input[type='radio']:active:not(:disabled) {
2194
2992
  .row-hit:hover .truncate { color: var(--accent); }
2195
2993
 
2196
2994
  .row-hit:focus-visible { outline: none; box-shadow: var(--focus-ring); }
2197
-
2198
- /* ---- ./empty.css ---- */
2199
- /* `Empty`: a list with nothing in it.
2200
-
2201
- Quieter than `.empty`, which is a whole-screen state at `--space-10`. These
2202
- sit inside a card that already has a heading and a description, so the same
2203
- padding would leave a card that is mostly air about nothing.
2204
-
2205
- Dashed rather than solid: a solid border reads as a thing, and the point of
2206
- this box is that there is no thing. It marks out where the list will be. */
2207
- .nothing {
2208
- display: grid;
2209
- justify-items: center;
2210
- gap: var(--space-2);
2211
- padding: var(--space-5) var(--space-4);
2212
- border: var(--border-width) dashed var(--border-strong);
2213
- border-radius: var(--radius-md);
2214
- text-align: center;
2215
2995
  }
2216
2996
 
2217
- .nothing-mark { color: var(--muted); line-height: 0; }
2218
-
2219
- /* The same box with a figure in it, for a whole surface rather than a slot in
2220
- a card. Two things change and both follow from the size: the dashed outline
2221
- goes, because an outline marks out where a list will be and a window-sized
2222
- dashed rectangle reads as a layout that failed rather than a view with
2223
- nothing on it; and the box centres itself in whatever it was dropped into,
2224
- which is what makes it land in the middle of an empty dashboard rather than
2225
- under the header. */
2226
- .nothing-surface {
2227
- border: none;
2228
- padding: var(--space-10) var(--space-4);
2229
- gap: var(--space-3);
2230
- margin: auto;
2231
- }
2232
-
2233
- /* `currentColor` reaches the figure through the two rules the illustrations
2234
- ship with, so the colour is set here and the drawing follows it -- the same
2235
- grey as the sentence under it, in every theme.
2236
- Two classes, not one: `.illo` sets `color: inherit` further down this file,
2237
- which beats a single-class rule written earlier and drew the figure in full
2238
- text black beside a muted sentence. */
2239
- .nothing-surface .nothing-figure { color: var(--muted); }
2240
-
2241
- /* `--text-sm`, not the `--text-xs` these lines used to be: it is a sentence
2242
- somebody is meant to read, not a caption qualifying something else. */
2243
- .nothing-said { margin: 0; color: var(--muted); font-size: var(--text-sm); line-height: 1.5; max-width: 46ch; }
2244
-
2245
- .nothing-act { margin-top: var(--space-1); }
2246
-
2247
2997
  /* ---- ./progress.css ---- */
2998
+ @layer components {
2248
2999
  /* progress ----------------------------------------------------------- */
2249
3000
 
2250
- /* The bar sat 4px under its own label and flush against whatever was above
3001
+ /* The bar itself: a track with a child it fills, an animation and a
3002
+ reduced-motion answer. A utility is a property on one element, so none of
3003
+ that is one -- `Progress` draws its own wrapper and head with utilities and
3004
+ this is what is left.
3005
+
3006
+ The bar sat 4px under its own label and flush against whatever was above
2251
3007
  the whole block, so a download row read as four lines of text with a rule
2252
3008
  drawn through the last one. The label is a caption for the bar and stays
2253
3009
  close to it; the block as a whole is a separate statement from the row it
2254
3010
  follows. */
2255
- .progress { display: grid; gap: var(--space-1); }
2256
-
2257
3011
  .card > .progress { margin-top: var(--space-3); }
2258
3012
 
2259
- .progress-head { display: flex; align-items: baseline; gap: var(--space-2); font-size: var(--text-sm); }
2260
-
2261
- .progress-detail { color: var(--muted); font-size: var(--text-xs); }
2262
-
2263
3013
  .bar { height: 6px; background: var(--panel-2); border-radius: var(--radius-pill); overflow: hidden; }
2264
3014
 
2265
3015
  .bar > i {
@@ -2287,12 +3037,22 @@ input[type='radio']:active:not(:disabled) {
2287
3037
  /* Still distinguishable from empty, and from full. */
2288
3038
  .bar-indeterminate > i { animation: none; width: 100%; opacity: 0.35; }
2289
3039
  }
3040
+ }
2290
3041
 
2291
3042
  /* ---- ./tabs.css ---- */
3043
+ @layer components {
2292
3044
  /* ---------------------------------------------------------------- tabs */
2293
3045
  .tabs { position: relative; display: flex; gap: var(--space-1); }
2294
3046
 
2295
3047
  .tab {
3048
+ /* 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. */
3049
+ display: inline-flex;
3050
+ align-items: center;
3051
+ justify-content: center;
3052
+ gap: var(--space-2);
3053
+ /* 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`. */
3054
+ font: inherit;
3055
+ cursor: pointer;
2296
3056
  position: relative;
2297
3057
  z-index: 1;
2298
3058
  background: transparent;
@@ -2381,8 +3141,10 @@ input[type='radio']:active:not(:disabled) {
2381
3141
  }
2382
3142
 
2383
3143
  .tabs-vertical .tab:active:not(:disabled) { border-color: transparent; }
3144
+ }
2384
3145
 
2385
3146
  /* ---- ./toast.css ---- */
3147
+ @layer components {
2386
3148
  /* ---------------------------------------------------------------- toasts */
2387
3149
  .toasts {
2388
3150
  position: fixed;
@@ -2449,7 +3211,41 @@ input[type='radio']:active:not(:disabled) {
2449
3211
  .toast[data-exiting] { animation: none; }
2450
3212
  }
2451
3213
 
3214
+ /* The dismiss control, in its own words.
3215
+ *
3216
+ * It was `className="ghost size-sm"` -- `Button`'s vocabulary, borrowed from
3217
+ * a component that does not render a `Button`. That only ever worked because
3218
+ * `.ghost` and `.size-sm` were element-and-class rules any `<button>` could
3219
+ * pick up, which is the coupling this pass is removing. Nothing photographed
3220
+ * it, either: the toast is transient and the close button is not in any
3221
+ * baseline, so this would have gone quiet. */
3222
+ .toast-close {
3223
+ display: inline-flex;
3224
+ align-items: center;
3225
+ justify-content: center;
3226
+ font: inherit;
3227
+ font-size: var(--text-xs);
3228
+ padding: var(--space-1) var(--space-2);
3229
+ background: transparent;
3230
+ border: var(--border-width) solid transparent;
3231
+ border-radius: var(--radius);
3232
+ color: var(--muted);
3233
+ cursor: pointer;
3234
+ }
3235
+
3236
+ .toast-close:hover:not(:disabled) {
3237
+ background: var(--panel-2);
3238
+ color: var(--text);
3239
+ }
3240
+
3241
+ .toast-close:focus-visible {
3242
+ outline: 2px solid var(--accent);
3243
+ outline-offset: 2px;
3244
+ }
3245
+ }
3246
+
2452
3247
  /* ---- ./modal.css ---- */
3248
+ @layer components {
2453
3249
  /* modal --------------------------------------------------------------- */
2454
3250
 
2455
3251
  /* `hidden` needs saying out loud here. The browser's own `[hidden]` rule is a
@@ -2489,13 +3285,6 @@ input[type='radio']:active:not(:disabled) {
2489
3285
  Not `display: contents`, which was the first attempt. An element with no box
2490
3286
  cannot take focus, so React Aria's initial focus fell through to the first
2491
3287
  input and Escape -- handled on the section -- stopped closing the window. */
2492
- .modal-dialog {
2493
- display: flex;
2494
- flex-direction: column;
2495
- min-height: 0;
2496
- flex: 1;
2497
- }
2498
-
2499
3288
  .modal {
2500
3289
  background: var(--panel);
2501
3290
  border: 1px solid var(--border);
@@ -2546,18 +3335,9 @@ input[type='radio']:active:not(:disabled) {
2546
3335
  laid out for the bare `<strong>` it used to hold -- 86px tall with them,
2547
3336
  41px without, on a head whose padding is 12 over 8. `font: inherit` so the
2548
3337
  `<strong>` rule above is the one that sets the size. */
2549
- .modal-title { margin: 0; font: inherit; }
2550
-
2551
3338
  /* Under the title, on the header's padding rather than the body's, so it reads
2552
3339
  as part of the heading rather than as the first line of content. */
2553
- .modal-desc {
2554
- margin: 0;
2555
- padding: 0 var(--space-4) var(--space-3);
2556
- color: var(--muted);
2557
- font-size: var(--text-sm);
2558
- line-height: 1.5;
2559
- max-width: 62ch;
2560
- }
3340
+ .modal-desc { line-height: 1.5; }
2561
3341
 
2562
3342
  .modal-body { padding: var(--space-2) var(--space-4) var(--space-3); overflow-y: auto; }
2563
3343
 
@@ -2696,8 +3476,10 @@ input[type='radio']:active:not(:disabled) {
2696
3476
  .modal.sheet[data-entering],
2697
3477
  .modal.sheet[data-exiting] { animation: none; }
2698
3478
  }
3479
+ }
2699
3480
 
2700
3481
  /* ---- ./slider.css ---- */
3482
+ @layer components {
2701
3483
  /* slider ----------------------------------------------------------------- */
2702
3484
  /* A number from a range -- `Slider.tsx` says when one is the right control.
2703
3485
  The row is laid out like a switch row: words on the left, the control on the
@@ -2917,9 +3699,12 @@ input[type='range'].slider:disabled::-moz-range-thumb { opacity: 0; }
2917
3699
  buttons only, so a button in a row stood 25px beside a 35px select and no
2918
3700
  size class could ask for the select's height. Type from the row, box from
2919
3701
  the scale, and the two line up; an explicit `size-*` still wins. */
2920
- :where(.set-row) button, :where(.set-row) .slider-box { font-size: var(--text-xs); }
3702
+ :where(.set-row) button,
3703
+ :where(.set-row) .slider-box { font-size: var(--text-xs); }
3704
+ }
2921
3705
 
2922
3706
  /* ---- ./checkbox.css ---- */
3707
+ @layer components {
2923
3708
  /* A row you choose. There is no checkbox in it.
2924
3709
  Every one of the fourteen in this app sits beside a name and a line of
2925
3710
  explanation, so the row was always the thing being designed and the 17px
@@ -3089,36 +3874,36 @@ input[type='range'].slider:disabled::-moz-range-thumb { opacity: 0; }
3089
3874
  .choice[data-disabled] .choice-why { opacity: 0.5; }
3090
3875
 
3091
3876
  .choice[data-disabled]::after { opacity: 0.4; }
3877
+ }
3092
3878
 
3093
3879
  /* ---- ./table.css ---- */
3880
+ @layer components {
3094
3881
  /* A second line under a row's name, inside the `<th scope="row">` that makes it
3095
3882
  one. The cell is bold because it is a heading; what hangs under it is not,
3096
3883
  and inherited the weight -- so a pinned version read as loud as the thing it
3097
3884
  versions. */
3098
- .table th .set-hint { font-weight: 400; }
3885
+ .data-table th .set-hint { font-weight: 400; }
3099
3886
 
3100
- .table-scroll { overflow-x: auto; }
3101
-
3102
- .table {
3887
+ .data-table {
3103
3888
  width: 100%;
3104
3889
  border-collapse: collapse;
3105
3890
  font-size: var(--text-sm);
3106
3891
  }
3107
3892
 
3108
- .table th,
3109
- .table td {
3893
+ .data-table th,
3894
+ .data-table td {
3110
3895
  text-align: left;
3111
3896
  padding: var(--space-2) var(--space-3);
3112
3897
  border-bottom: var(--border-width) solid var(--border);
3113
3898
  }
3114
3899
 
3115
- .table th:first-child,
3116
- .table td:first-child { padding-left: 0; }
3900
+ .data-table th:first-child,
3901
+ .data-table td:first-child { padding-left: 0; }
3117
3902
 
3118
- .table th:last-child,
3119
- .table td:last-child { padding-right: 0; }
3903
+ .data-table th:last-child,
3904
+ .data-table td:last-child { padding-right: 0; }
3120
3905
 
3121
- .table thead th {
3906
+ .data-table thead th {
3122
3907
  font-size: var(--text-xs);
3123
3908
  font-weight: var(--weight-strong);
3124
3909
  text-transform: uppercase;
@@ -3129,78 +3914,69 @@ input[type='range'].slider:disabled::-moz-range-thumb { opacity: 0; }
3129
3914
 
3130
3915
  /* The row's own name: a `<th scope="row">`, so it carries weight rather than
3131
3916
  reading as one more cell. */
3132
- .table tbody th { font-weight: var(--weight-strong); }
3917
+ .data-table tbody th { font-weight: var(--weight-strong); }
3133
3918
 
3134
- .table tbody tr:last-child th,
3135
- .table tbody tr:last-child td { border-bottom: none; }
3919
+ .data-table tbody tr:last-child th,
3920
+ .data-table tbody tr:last-child td { border-bottom: none; }
3136
3921
 
3137
3922
  /* Numbers compare by column, so they align right and hold their tracks --
3138
3923
  proportional digits shift the alignment by a pixel per glyph, which is
3139
3924
  exactly the comparison the column was for. */
3140
- .table .num { text-align: right; font-variant-numeric: tabular-nums; font-family: var(--font-mono); }
3925
+ .data-table .num { text-align: right; font-variant-numeric: tabular-nums; font-family: var(--font-mono); }
3141
3926
 
3142
- .table thead th[style*='right'] { text-align: right; }
3927
+ .data-table thead th[style*='right'] { text-align: right; }
3928
+ }
3143
3929
 
3144
3930
  /* ---- ./sizegrid.css ---- */
3145
- /* Tile size, picked as a rectangle rather than read as a notation. */
3146
- .size-grid-wrap { display: flex; align-items: center; gap: var(--space-2); }
3147
-
3148
- .size-grid { display: grid; gap: var(--space-1); }
3149
-
3931
+ @layer components {
3932
+ /* The cell's colour, all of it, in one place.
3933
+ *
3934
+ * `SizeGrid` draws the cell's geometry with utilities and leaves every colour
3935
+ * here, because `on` and `chosen` override the resting state -- and a utility
3936
+ * is in `@layer utilities`, which beats `@layer components` whatever the
3937
+ * selector. Stated as `surface-panel-2` and `border-border` on the element,
3938
+ * the resting state won permanently and no cell ever lit up.
3939
+ *
3940
+ * That is the third time this pattern has bitten: `Select`'s tick and
3941
+ * `Empty`'s figure were the other two. The rule: a resting state whose
3942
+ * active state is a CSS rule has to be a CSS rule too. */
3150
3943
  .size-cell {
3151
- width: 18px; height: 14px; padding: 0;
3152
- border: 1px solid var(--border); border-radius: var(--radius-sm);
3153
- background: var(--panel-2); cursor: pointer;
3944
+ border: var(--border-width) solid var(--border);
3945
+ background: var(--panel-2);
3154
3946
  }
3155
3947
 
3156
3948
  /* `on` follows the pointer; `chosen` is what is actually saved, shown only
3157
3949
  when nothing is hovered so the two can never contradict each other. */
3158
- .size-cell.on { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 45%, transparent); }
3950
+ .size-cell.on {
3951
+ border-color: var(--accent);
3952
+ background: color-mix(in srgb, var(--accent) 45%, transparent);
3953
+ }
3159
3954
 
3160
3955
  .size-cell.chosen { border-color: var(--accent); }
3161
-
3162
- .size-cell:disabled { cursor: default; opacity: 0.5; }
3163
-
3164
- .size-ceiling { opacity: 0.55; }
3956
+ }
3165
3957
 
3166
3958
  /* ---- ./tooltip.css ---- */
3959
+ @layer components {
3167
3960
  /* A callout: something worth knowing, with nothing to answer. Distinct from a
3168
3961
  `?`, which is an offer to read more, and from a banner, which is a problem.
3169
3962
  The mark is a round `i` rather than an emoji so it takes the theme's colours
3170
3963
  and stays the same size as the text beside it. */
3171
- /* An info-toned `.callout`, spelled the old way. Identical to it now rather than
3172
- nearly identical to it, which is the whole point of the consolidation: the
3173
-
3174
- /* The `?` beside a setting, and the box it shows on hover. Built like the
3964
+ /* An info-toned `.callout`, spelled the old way. Identical to it now rather
3965
+ than nearly identical to it, which is the whole point of the consolidation.
3966
+ (This comment has been unterminated since before the Tailwind branch -- it
3967
+ stops mid-sentence and never closes, so the CSS parser merged it with the
3968
+ one below and recovered. Nothing was lost from the stylesheet; the rest of
3969
+ the sentence was. Closed here rather than guessed at.) */
3970
+
3971
+ /* What is left once `Tooltip` draws its own mark: the hover and focus that
3972
+ reach the mark from its parent, and the tip -- whose closed and open
3973
+ states are three rules the cascade resolves in order, with
3974
+ `data-entering` and `data-exiting` around the transition. None of that is
3975
+ a property on one element.
3976
+
3977
+ The `?` beside a setting, and the box it shows on hover. Built like the
3175
3978
  status dot's tooltip and for the same reasons: `title` waits a second and a
3176
3979
  half, renders newlines however it likes, and cannot be styled. */
3177
- .explain {
3178
- position: relative;
3179
- flex: none;
3180
- display: inline-flex;
3181
- align-items: center;
3182
- /* The heading it follows is tracked out, which leaves the last letter's
3183
- spacing sitting between them and reads as none at all. */
3184
- margin-left: var(--space-2);
3185
- cursor: help;
3186
- /* Headings here are uppercase and tracked out, and one of these sits inside
3187
- a heading. */
3188
- text-transform: none;
3189
- letter-spacing: normal;
3190
- font-weight: 400;
3191
- }
3192
-
3193
- .explain-mark {
3194
- width: 18px;
3195
- height: 18px;
3196
- border-radius: 50%;
3197
- border: 1px solid var(--border);
3198
- color: var(--muted);
3199
- font-size: var(--text-xs);
3200
- line-height: 16px;
3201
- text-align: center;
3202
- }
3203
-
3204
3980
  .explain:hover .explain-mark,
3205
3981
  .explain:focus-visible .explain-mark { color: var(--text); border-color: var(--accent); }
3206
3982
 
@@ -3273,8 +4049,10 @@ input[type='range'].slider:disabled::-moz-range-thumb { opacity: 0; }
3273
4049
  }
3274
4050
 
3275
4051
  .explain-tip strong { color: var(--text); font-weight: 600; }
4052
+ }
3276
4053
 
3277
4054
  /* ---- ./illustration.css ---- */
4055
+ @layer components {
3278
4056
  /* illustrations -------------------------------------------------------- */
3279
4057
 
3280
4058
  /* Open Peeps, two colours, wearing whatever the theme is.
@@ -3301,7 +4079,21 @@ input[type='range'].slider:disabled::-moz-range-thumb { opacity: 0; }
3301
4079
  a shirt that lets the arm behind it show through reads as a hole. */
3302
4080
  .illo .paper { fill: var(--illo-paper); }
3303
4081
 
4082
+ /* The figure in a whole-surface `Empty`, drawn in the muted grey of the
4083
+ sentence under it.
4084
+ *
4085
+ * A utility cannot do this, and the reason is worth keeping: `.illo` sets
4086
+ * `color: inherit` and is unlayered, while every Tailwind utility lives in
4087
+ * `@layer utilities` -- and an unlayered declaration beats a layered one
4088
+ * whatever the specificity. So `text-muted` on the figure lost, silently,
4089
+ * and the shirt came out full text black beside a muted sentence. The
4090
+ * original rule already carried a note about needing two classes to beat
4091
+ * `.illo`; layers make that impossible rather than merely fiddly. */
4092
+ .nothing-figure { color: var(--muted); }
4093
+ }
4094
+
3304
4095
  /* ---- ./field.css ---- */
4096
+ @layer components {
3305
4097
  /* fields --------------------------------------------------------------- */
3306
4098
 
3307
4099
  /* Label, description, input, error -- in the order they are wanted. The
@@ -3399,7 +4191,7 @@ input[type='range'].slider:disabled::-moz-range-thumb { opacity: 0; }
3399
4191
  .adorned > .adorned-mark,
3400
4192
  .adorned > .adorned-end { grid-area: 1 / 1; }
3401
4193
 
3402
- .adorned.block { width: 100%; }
4194
+ .adorned.ctl-block { width: 100%; }
3403
4195
 
3404
4196
  .adorned > .adorned-mark {
3405
4197
  justify-self: start;
@@ -3452,7 +4244,34 @@ input[type='range'].slider:disabled::-moz-range-thumb { opacity: 0; }
3452
4244
 
3453
4245
  .adorned-lg .adorned-clear { padding: var(--space-2); }
3454
4246
 
4247
+ /* The eye and the clear button stop borrowing `ghost`.
4248
+ *
4249
+ * Both were `className="icon-btn ghost …"` -- reaching into `Button`'s class
4250
+ * vocabulary from another component, which worked only because `.ghost` was
4251
+ * written as `button.ghost` and they happen to be `<button>`s. It is `.btn.ghost`
4252
+ * now, so the borrowing stopped matching and `.icon-btn`'s panel background
4253
+ * came through: the baselines showed a filled tile where there had been a bare
4254
+ * glyph.
4255
+ *
4256
+ * Stated here instead. What they actually wanted from `ghost` is two lines,
4257
+ * and saying them is cheaper than either component knowing what a button's
4258
+ * classes are called. */
4259
+ .secret-eye,
4260
+ .adorned-clear {
4261
+ background: transparent;
4262
+ border-color: transparent;
4263
+ color: var(--muted);
4264
+ }
4265
+
4266
+ .secret-eye:hover:not(:disabled),
4267
+ .adorned-clear:hover:not(:disabled) {
4268
+ background: var(--panel-2);
4269
+ color: var(--text);
4270
+ }
4271
+ }
4272
+
3455
4273
  /* ---- ./tour.css ---- */
4274
+ @layer components {
3456
4275
  /* tour ----------------------------------------------------------------- */
3457
4276
 
3458
4277
  /* The layer sits over everything and catches nothing: the scrim is painted by
@@ -3514,141 +4333,41 @@ input[type='range'].slider:disabled::-moz-range-thumb { opacity: 0; }
3514
4333
  .tour-actions { display: flex; align-items: center; gap: var(--space-2); margin-top: var(--space-2); }
3515
4334
 
3516
4335
  .tour-count { color: var(--muted); font-size: var(--text-xs); }
4336
+ }
3517
4337
 
3518
4338
  /* The mail and forum shell, added in 0.4.0. After the rest, because the
3519
4339
  anchored surfaces (`.pop`, `.menu-sheet`, `.cmd`) sit over everything and a
3520
4340
  rule of equal specificity that came earlier would lose to whatever the
3521
4341
  sheets they cover happen to set. */
3522
- /* ---- ./kbd.css ---- */
3523
- /* One rule where there were two identical ones, in `menu.css` and
3524
- `command.css`. Both keep their class as well, so `Menu` and `Command` are
3525
- `.kbd .menu-key` and `.kbd .cmd-key` now: the declarations are the same, so
3526
- nothing moves, and the context classes stay available for a rule that ever
3527
- needs to tell the two apart. */
3528
- .kbd {
3529
- flex: none;
3530
- font-family: var(--font-mono);
3531
- font-size: var(--text-2xs);
3532
- color: var(--muted);
3533
- letter-spacing: var(--tracking);
3534
- }
3535
-
3536
4342
  /* ---- ./stat.css ---- */
3537
- /* A figure with furniture. `--space-1` between the three lines because they
3538
- are one object read top to bottom, not three paragraphs. */
3539
- .stat {
3540
- display: grid;
3541
- gap: var(--space-1);
3542
- padding: var(--space-4);
3543
- border: var(--border-width) solid var(--border);
3544
- border-radius: var(--radius-md);
3545
- background: var(--panel);
3546
- }
3547
-
3548
- /* Inside something already bordered. A bordered tile in a bordered row is two
3549
- boxes saying one thing. */
3550
- .stat-bare {
3551
- padding: 0;
3552
- border: 0;
3553
- border-radius: 0;
3554
- background: none;
3555
- gap: 0;
3556
- line-height: 1.25;
3557
- }
3558
-
3559
- .stat > p { margin: 0; }
3560
-
3561
- .stat-label {
3562
- color: var(--muted);
3563
- font-size: var(--text-xs);
3564
- }
3565
-
3566
- /* Tabular figures are the point: a column of numbers is compared by its
3567
- digits' positions, and proportional ones move them per row. */
3568
- .stat-value {
3569
- font-size: var(--text-lg);
3570
- font-weight: var(--weight-strong);
3571
- font-variant-numeric: tabular-nums;
3572
- }
3573
-
3574
- .stat-bare .stat-value { font-size: var(--text-sm); }
3575
-
3576
- .stat-note {
3577
- color: var(--muted);
3578
- font-size: var(--text-xs);
3579
- }
4343
+ @layer components {
4344
+ /* What the utilities on `Stat` cannot say: a tone on the tile colouring the
4345
+ figure inside it. An attribute on the parent selecting a child is a
4346
+ relationship; a utility is a property on one element.
3580
4347
 
3581
- /* Only the figure takes the colour. Colouring the label too makes the tile a
3582
- status box, and a tone here is a remark about the number. */
4348
+ Only the figure takes the colour. Colouring the label too makes the tile a
4349
+ status box, where a tone here is a remark about the number. */
3583
4350
  .stat[data-tone='info'] .stat-value { color: var(--info); }
3584
4351
  .stat[data-tone='good'] .stat-value { color: var(--good); }
3585
4352
  .stat[data-tone='warn'] .stat-value { color: var(--warn); }
3586
4353
  .stat[data-tone='bad'] .stat-value { color: var(--bad); }
3587
-
3588
- /* ---- ./shell.css ---- */
3589
- /* The frame. Layout only: every colour and every step is a token, and the
3590
- page inside it owns its own spacing. */
3591
- .shell {
3592
- display: flex;
3593
- flex-direction: column;
3594
- min-height: 100vh;
3595
- background: var(--bg);
3596
- color: var(--text);
3597
4354
  }
3598
4355
 
3599
- .shell-head {
3600
- border-bottom: var(--border-width) solid var(--border);
3601
- background: var(--panel);
3602
- /* `flex: none` so a long page does not squeeze the header. */
3603
- flex: none;
3604
- }
3605
-
3606
- /* The measure, said once. `--shell-measure` rather than a literal so an app
3607
- with a different one sets a variable instead of overriding three rules and
3608
- missing the fourth -- which is how manage ended up with `head-inner-wide`
3609
- and `main-wide` as separate classes. */
4356
+ /* ---- ./shell.css ---- */
4357
+ @layer components {
4358
+ /* The measure, and only the measure.
4359
+ *
4360
+ * `Shell` draws itself with utilities -- it is layout and nothing else. What
4361
+ * stays here is the one value the header band, the nav band and the main band
4362
+ * all have to agree on: a custom property, so an app changes it in one place
4363
+ * rather than overriding three rules and missing the fourth. That is how
4364
+ * manage ended up carrying `head-inner-wide` and `main-wide` as separate
4365
+ * classes it had to remember together. */
3610
4366
  .shell {
3611
4367
  --shell-measure: 64rem;
3612
4368
  --shell-measure-wide: 90rem;
3613
4369
  }
3614
4370
 
3615
- .shell-head-inner,
3616
- .shell-nav,
3617
- .shell-main {
3618
- width: 100%;
3619
- max-width: var(--shell-measure);
3620
- margin-inline: auto;
3621
- padding-inline: var(--space-4);
3622
- }
3623
-
3624
- .shell-wide { max-width: var(--shell-measure-wide); }
3625
-
3626
- .shell-head-inner {
3627
- display: flex;
3628
- align-items: center;
3629
- justify-content: space-between;
3630
- gap: var(--space-4);
3631
- padding-block: var(--space-3);
3632
- min-width: 0;
3633
- }
3634
-
3635
- .shell-who {
3636
- display: flex;
3637
- align-items: center;
3638
- gap: var(--space-3);
3639
- min-width: 0;
3640
- }
3641
-
3642
- .shell-nav { padding-bottom: var(--space-2); }
3643
-
3644
- .shell-main {
3645
- flex: 1;
3646
- padding-block: var(--space-6);
3647
- /* `min-width: 0` because a flex child will not shrink below its content,
3648
- and one wide table inside then pushes the whole page sideways. */
3649
- min-width: 0;
3650
- }
3651
-
3652
4371
  /* The label beside the theme control, when it is drawn. */
3653
4372
  .theme-switch {
3654
4373
  display: flex;
@@ -3662,8 +4381,10 @@ input[type='range'].slider:disabled::-moz-range-thumb { opacity: 0; }
3662
4381
  font-size: var(--text-sm);
3663
4382
  white-space: nowrap;
3664
4383
  }
4384
+ }
3665
4385
 
3666
4386
  /* ---- ./scrollarea.css ---- */
4387
+ @layer components {
3667
4388
  /* A box that scrolls, with its edges drawn.
3668
4389
 
3669
4390
  The fades are `::before`/`::after` on the scroller itself, pinned with
@@ -3756,8 +4477,10 @@ input[type='range'].slider:disabled::-moz-range-thumb { opacity: 0; }
3756
4477
  .scroller.faded::before,
3757
4478
  .scroller.faded::after { transition: none; }
3758
4479
  }
4480
+ }
3759
4481
 
3760
4482
  /* ---- ./splitpane.css ---- */
4483
+ @layer components {
3761
4484
  /* Two panes and the handle between them. */
3762
4485
  .split {
3763
4486
  display: flex;
@@ -3857,8 +4580,10 @@ body.splitting * { cursor: inherit !important; }
3857
4580
  @media (prefers-reduced-motion: reduce) {
3858
4581
  .split-grip { transition: none; }
3859
4582
  }
4583
+ }
3860
4584
 
3861
4585
  /* ---- ./popover.css ---- */
4586
+ @layer components {
3862
4587
  /* A small surface anchored to what opened it. Same sheet as the select's list,
3863
4588
  because they are the same object -- something that appeared over the page,
3864
4589
  attached to a control. */
@@ -3912,29 +4637,17 @@ body.splitting * { cursor: inherit !important; }
3912
4637
  .pop[data-entering],
3913
4638
  .pop[data-exiting] { animation: none; }
3914
4639
  }
3915
-
3916
- /* ---- ./menu.css ---- */
3917
- /* A list of verbs, on the same sheet as every other anchored surface. */
3918
- .menu-sheet {
3919
- background: var(--panel);
3920
- border: var(--border-width) solid var(--border);
3921
- border-radius: var(--radius-md);
3922
- box-shadow: var(--shadow-2);
3923
- min-width: 12rem;
3924
- max-width: min(20rem, calc(100vw - var(--space-6)));
3925
- max-height: 24rem;
3926
- overflow: auto;
3927
- overscroll-behavior: contain;
3928
- z-index: 60;
3929
4640
  }
3930
4641
 
3931
- .menu-list {
3932
- padding: var(--space-1);
3933
- outline: none;
3934
- display: grid;
3935
- gap: var(--border-width);
3936
- }
4642
+ /* ---- ./menu.css ---- */
4643
+ @layer components {
4644
+ /* What is left once `Menu` draws its sheet and its list: the rows, whose
4645
+ `data-focused`, `data-pressed` and danger states React Aria stamps on
4646
+ them, and the rules that reach from a row into what it holds. The
4647
+ `box-shadow` stays on the sheet too -- `--shadow-2` is a token, and a
4648
+ utility would have to inline it. */
3937
4649
 
4650
+ /* A list of verbs, on the same sheet as every other anchored surface. */
3938
4651
  .menu-item {
3939
4652
  display: flex;
3940
4653
  align-items: center;
@@ -3973,14 +4686,6 @@ body.splitting * { cursor: inherit !important; }
3973
4686
  /* The binding, set in the mono face at the size of the small print. Right of
3974
4687
  the row and never the reason the row is wide: `flex: none` with the label
3975
4688
  taking the slack. */
3976
- .menu-key {
3977
- flex: none;
3978
- font-family: var(--font-mono);
3979
- font-size: var(--text-2xs);
3980
- color: var(--muted);
3981
- letter-spacing: var(--tracking);
3982
- }
3983
-
3984
4689
  .menu-more { flex: none; color: var(--muted); }
3985
4690
 
3986
4691
  .menu-rule {
@@ -4016,7 +4721,11 @@ body.splitting * { cursor: inherit !important; }
4016
4721
  .menu-sheet[data-exiting] { animation: none; }
4017
4722
  }
4018
4723
 
4724
+ .menu-sheet { box-shadow: var(--shadow-2); }
4725
+ }
4726
+
4019
4727
  /* ---- ./command.css ---- */
4728
+ @layer components {
4020
4729
  /* The command palette: a field over the app and a list under it. */
4021
4730
  .cmd-scrim {
4022
4731
  position: fixed;
@@ -4133,14 +4842,6 @@ body.splitting * { cursor: inherit !important; }
4133
4842
  white-space: nowrap;
4134
4843
  }
4135
4844
 
4136
- .cmd-key {
4137
- flex: none;
4138
- font-family: var(--font-mono);
4139
- font-size: var(--text-2xs);
4140
- color: var(--muted);
4141
- letter-spacing: var(--tracking);
4142
- }
4143
-
4144
4845
  .cmd-empty {
4145
4846
  margin: 0;
4146
4847
  padding: var(--space-6) var(--space-4);
@@ -4166,25 +4867,18 @@ body.splitting * { cursor: inherit !important; }
4166
4867
  .cmd-scrim[data-exiting],
4167
4868
  .cmd[data-entering] { animation: none; }
4168
4869
  }
4870
+ }
4169
4871
 
4170
4872
  /* ---- ./pagination.css ---- */
4171
- /* Moving through a list that does not fit, and saying how far in you are. */
4172
- .pager {
4173
- display: flex;
4174
- align-items: center;
4175
- justify-content: space-between;
4176
- gap: var(--space-4);
4177
- flex-wrap: wrap;
4178
- padding: var(--space-2) 0;
4179
- font-size: var(--text-xs);
4180
- color: var(--muted);
4181
- }
4873
+ @layer components {
4874
+ /* What is left once `Pagination` draws its own frame: the step and page
4875
+ buttons, whose hover, focus-visible and current states are pseudo-classes
4876
+ on two selectors at once, and `.pager-count strong`, which reaches into
4877
+ text the caller passed. */
4182
4878
 
4183
- .pager-count { margin: 0; }
4879
+ /* Moving through a list that does not fit, and saying how far in you are. */
4184
4880
  .pager-count strong { color: var(--text); font-weight: var(--weight-strong); }
4185
4881
 
4186
- .pager-controls { display: flex; align-items: center; gap: var(--space-1); }
4187
-
4188
4882
  .pager-step,
4189
4883
  .pager-page {
4190
4884
  display: inline-flex;
@@ -4295,8 +4989,10 @@ body.splitting * { cursor: inherit !important; }
4295
4989
  .pager-page,
4296
4990
  .pager-jump { transition: none; }
4297
4991
  }
4992
+ }
4298
4993
 
4299
4994
  /* ---- ./identity.css ---- */
4995
+ @layer components {
4300
4996
  /* A person, said in one line: initials, name, sometimes the address. */
4301
4997
  .ident {
4302
4998
  display: inline-flex;
@@ -4376,6 +5072,8 @@ body.splitting * { cursor: inherit !important; }
4376
5072
  }
4377
5073
 
4378
5074
  .ident-remove {
5075
+ /* 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`. */
5076
+ font: inherit;
4379
5077
  flex: none;
4380
5078
  display: inline-flex;
4381
5079
  align-items: center;
@@ -4395,6 +5093,7 @@ body.splitting * { cursor: inherit !important; }
4395
5093
 
4396
5094
  .ident-remove:hover { background: var(--panel); color: var(--text); }
4397
5095
  .ident-remove:focus-visible { outline: none; box-shadow: var(--focus-ring); }
5096
+ }
4398
5097
  /* ---- tf: the themes ---- */
4399
5098
  :root[data-theme='night']{--bg:#0f1115;--panel:#161a21;--panel-2:#1c222b;--border:#262d38;--text:#e6e9ef;--muted:#8b94a4;--accent:#5b9dff;--accent-dim:#2a4877;--app-bg:#0f1115;color-scheme:dark}
4400
5099
  :root[data-theme='paper']{--bg:#f6f7f9;--panel:#ffffff;--panel-2:#f0f2f5;--border:#e4e8ec;--border-strong:#8792a1;--text:#191d23;--muted:#5d6773;--accent:#0e7872;--accent-dim:#7fbdb8;--on-accent:#ffffff;--good:#1c7a4a;--warn:#8a6216;--bad:#b3312c;--info:#216bc9;--shadow-1:0 4px 14px rgba(16, 24, 40, 0.08);--shadow-2:0 8px 24px rgba(16, 24, 40, 0.10);--shadow-3:0 12px 32px rgba(16, 24, 40, 0.12);--scrim:rgba(16, 24, 40, 0.32);color-scheme:light}