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