actual-css 0.9.0 → 0.9.2

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.
Files changed (94) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +2 -1
  3. package/components.json +2 -0
  4. package/dist/actual.css +26 -6
  5. package/dist/actual.full.css +260 -74
  6. package/dist/actual.full.js +1 -1
  7. package/dist/actual.full.min.css +1 -1
  8. package/dist/actual.min.css +1 -1
  9. package/docs/pages/components/table.md +3 -2
  10. package/docs/pages/examples/overview.md +1 -0
  11. package/docs/pages/forms/input-policy.md +59 -0
  12. package/docs/pages/forms/validation.md +37 -1
  13. package/docs/pages/foundations/tokens.md +23 -4
  14. package/docs/pages/foundations/typography.md +10 -0
  15. package/docs/pages/guides/introduction.md +2 -1
  16. package/docs/pages/guides/progressive-enhancement.md +15 -10
  17. package/package.json +160 -159
  18. package/src/css/components/accordion.css +2 -0
  19. package/src/css/components/actions.css +2 -0
  20. package/src/css/components/alert.css +2 -0
  21. package/src/css/components/app-nav.css +2 -0
  22. package/src/css/components/avatar.css +2 -0
  23. package/src/css/components/badge.css +2 -0
  24. package/src/css/components/breadcrumb.css +2 -0
  25. package/src/css/components/busy.css +2 -0
  26. package/src/css/components/button.css +2 -0
  27. package/src/css/components/card.css +2 -0
  28. package/src/css/components/chat.css +2 -0
  29. package/src/css/components/data-list.css +2 -0
  30. package/src/css/components/drawer.css +35 -22
  31. package/src/css/components/fab.css +2 -0
  32. package/src/css/components/flyout.css +2 -0
  33. package/src/css/components/indicator.css +2 -0
  34. package/src/css/components/join.css +2 -0
  35. package/src/css/components/key.css +2 -0
  36. package/src/css/components/list.css +2 -0
  37. package/src/css/components/meter.css +2 -0
  38. package/src/css/components/modal.css +47 -40
  39. package/src/css/components/navbar.css +2 -0
  40. package/src/css/components/pagination.css +2 -0
  41. package/src/css/components/progress.css +2 -0
  42. package/src/css/components/rating.css +2 -0
  43. package/src/css/components/scrollspy.css +2 -0
  44. package/src/css/components/skeleton.css +2 -0
  45. package/src/css/components/spinner.css +2 -0
  46. package/src/css/components/status-bar.css +2 -0
  47. package/src/css/components/steps.css +2 -0
  48. package/src/css/components/tab.css +2 -0
  49. package/src/css/components/table.css +2 -0
  50. package/src/css/components/tooltip.css +2 -0
  51. package/src/css/core/base.css +2 -0
  52. package/src/css/core/print.css +3 -0
  53. package/src/css/core/tokens.css +2 -0
  54. package/src/css/core/variants.css +19 -6
  55. package/src/css/effects/aura.css +2 -0
  56. package/src/css/forms/choice-card.css +4 -0
  57. package/src/css/forms/control.css +2 -0
  58. package/src/css/forms/floating-field.css +5 -1
  59. package/src/css/forms/form-actions.css +2 -0
  60. package/src/css/forms/form.css +8 -0
  61. package/src/css/forms/input-icon.css +2 -0
  62. package/src/css/forms/native.css +2 -0
  63. package/src/css/forms/otp.css +10 -0
  64. package/src/css/forms/range.css +2 -0
  65. package/src/css/forms/select.css +2 -0
  66. package/src/css/forms/switch.css +2 -0
  67. package/src/css/forms/validation.css +2 -0
  68. package/src/css/layout/app-layout.css +2 -0
  69. package/src/css/layout/app-shell.css +5 -1
  70. package/src/css/layout/center.css +2 -0
  71. package/src/css/layout/cluster.css +2 -0
  72. package/src/css/layout/column-layout.css +2 -0
  73. package/src/css/layout/frame.css +2 -0
  74. package/src/css/layout/grid.css +2 -0
  75. package/src/css/layout/media.css +2 -1
  76. package/src/css/layout/scroll-snap.css +2 -0
  77. package/src/css/layout/scroll-target.css +2 -0
  78. package/src/css/layout/sidebar-layout.css +2 -0
  79. package/src/css/layout/stack.css +2 -1
  80. package/src/css/layout/switcher.css +2 -0
  81. package/src/css/layout/topbar.css +2 -0
  82. package/src/css/typography/fluid.css +2 -0
  83. package/src/css/typography/lead.css +2 -1
  84. package/src/css/typography/overline.css +2 -0
  85. package/src/css/typography/prose.css +2 -0
  86. package/src/css/utilities/base.css +6 -1
  87. package/src/js/context-menu.js +56 -15
  88. package/src/js/enhancement-loader.js +5 -1
  89. package/src/js/escape.js +11 -1
  90. package/src/js/flyout.js +6 -1
  91. package/src/js/focus.js +8 -3
  92. package/src/js/surface.js +28 -0
  93. package/src/js/tooltip.js +10 -4
  94. package/src/js/validation.js +31 -0
@@ -71,6 +71,8 @@ select {
71
71
  /* Typography */
72
72
  --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
73
73
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
74
+ --font-width: 100%;
75
+ --font-width-dense: 95%;
74
76
  --line-height: 1.5;
75
77
  --line-height-tight: 1.25;
76
78
  --line-height-relaxed: 1.75;
@@ -519,12 +521,14 @@ select {
519
521
  * Public hooks:
520
522
  * --link
521
523
  * --hr-space
524
+ * --font-width / --font-width-dense (see variants.css for the dense application)
522
525
  */
523
526
 
524
527
  body {
525
528
  background: var(--surface);
526
529
  color: var(--text);
527
530
  font-family: var(--font-sans);
531
+ font-stretch: var(--font-width);
528
532
  line-height: var(--line-height);
529
533
  }
530
534
 
@@ -672,9 +676,16 @@ small {
672
676
  * components/alert.css — .alert default soft behind explicit --ui-*
673
677
  *
674
678
  * .compact/.spacious set inherited density tokens: spacing rhythm and geometry
675
- * only. Density never changes typography or icon size. Controls consume
676
- * --control-size; non-control components may consume --density-* when the
677
- * context should affect their local rhythm.
679
+ * only, plus a slight typographic width bonus. Density never changes font size
680
+ * or icon size. Controls consume --control-size; non-control components may
681
+ * consume --density-* when the context should affect their local rhythm.
682
+ * .compact applies --font-width-dense through font-stretch (a variable wdth
683
+ * axis when available, otherwise normal width-face matching); layout must
684
+ * never depend on the narrowing being available. .spacious restores
685
+ * --font-width, so a spacious island inside a compact context returns to the
686
+ * normal width exactly as it restores its own gap, spacing, and control size.
687
+ * Themes whose typeface has no useful width axis opt out with
688
+ * --font-width-dense: 100%.
678
689
  *
679
690
  * .sm/.lg are local size roles, not shared measurements. Shared control
680
691
  * participants below map both typography and geometry; components with
@@ -739,18 +750,20 @@ small {
739
750
  --gap: var(--space-20);
740
751
  --density-space: var(--space-20);
741
752
  --control-size: var(--control-size-sm);
753
+ font-stretch: var(--font-width-dense);
742
754
  }
743
755
 
744
756
  .spacious {
745
757
  --gap: var(--space-50);
746
758
  --density-space: var(--space-50);
747
759
  --control-size: var(--control-size-lg);
760
+ font-stretch: var(--font-width);
748
761
  }
749
762
 
750
763
  /* Shared control scale. Wrappers are listed when their descendants form one
751
- component (field, input-icon, join, pagination, tabs, flyout). A local size
752
- declaration beats inherited density because it is declared on the sized
753
- component itself. */
764
+ component (field, choice-card, floating-field, input-icon, join, pagination,
765
+ tabs, flyout). A local size declaration beats inherited density because it
766
+ is declared on the sized component itself. */
754
767
  :where(
755
768
  .btn,
756
769
  .input,
@@ -760,7 +773,9 @@ small {
760
773
  .file,
761
774
  .field,
762
775
  .choice,
776
+ .choice-card,
763
777
  .input-icon,
778
+ .floating-field,
764
779
  .otp,
765
780
  .join,
766
781
  .pagination,
@@ -782,7 +797,9 @@ small {
782
797
  .file,
783
798
  .field,
784
799
  .choice,
800
+ .choice-card,
785
801
  .input-icon,
802
+ .floating-field,
786
803
  .otp,
787
804
  .join,
788
805
  .pagination,
@@ -842,6 +859,9 @@ small {
842
859
 
843
860
  @media print {
844
861
  :root,
862
+ /* The doubled attribute intentionally outranks single-attribute preset
863
+ islands (data-theme="dark", paired themes) loaded after the core. */
864
+ [data-theme][data-theme],
845
865
  body {
846
866
  /* Save some ink in dark mode */
847
867
  color-scheme: light;
@@ -863,6 +883,8 @@ small {
863
883
  /*
864
884
  * Prose is the opt-in rich text scope for authored content.
865
885
  *
886
+ * Docs: docs/pages/foundations/typography.md
887
+ *
866
888
  * Global element styles stay mild so app UI is not shaped accidentally. Page
867
889
  * layout, forms, cards, navigation, and app tables remain outside this scope.
868
890
  *
@@ -1079,7 +1101,8 @@ small {
1079
1101
 
1080
1102
  /* Lead paragraph: a slightly larger, more relaxed reading-introduction text.
1081
1103
  Non-fluid and always available — use .text-lead from the optional fluid
1082
- module for marketing/display surfaces. */
1104
+ module for marketing/display surfaces.
1105
+ Docs: docs/pages/foundations/typography.md */
1083
1106
  .lead {
1084
1107
  font-size: 1.125em;
1085
1108
  line-height: var(--line-height-relaxed);
@@ -1088,6 +1111,8 @@ small {
1088
1111
  /*
1089
1112
  * Overline — editorial small-cap label for section openers.
1090
1113
  *
1114
+ * Docs: docs/pages/foundations/typography.md
1115
+ *
1091
1116
  * Base form is unstyled chrome that takes its surrounding color. Add .pill for
1092
1117
  * the bordered rounded shape, and combine with an intent class (.primary,
1093
1118
  * .success, etc.) to tint the text, border, and background.
@@ -1137,6 +1162,8 @@ small {
1137
1162
  /*
1138
1163
  * Optional fluid typography module.
1139
1164
  *
1165
+ * Docs: docs/pages/foundations/typography.md
1166
+ *
1140
1167
  * Part of the typography family (typography/index.css) and the full bundle; it
1141
1168
  * is not in the minimal core, so a core-only import adds it explicitly when
1142
1169
  * display, title, and lead sizes must scale with the viewport. Typical use
@@ -1267,7 +1294,8 @@ small {
1267
1294
  region re-spaces every nested stack, cluster and grid at any depth. Setting
1268
1295
  the token stays correct on the element that consumes it, and
1269
1296
  .compact/.spacious set it deliberately so a density context reaches nested layouts. See
1270
- docs/pages/layout/stack.md. */
1297
+ docs/pages/layout/stack.md.
1298
+ Docs: docs/pages/layout/stack.md */
1271
1299
  .stack {
1272
1300
  display: flex;
1273
1301
  flex-direction: column;
@@ -1281,6 +1309,8 @@ small {
1281
1309
  /*
1282
1310
  * Inline group that wraps when space runs out.
1283
1311
  *
1312
+ * Docs: docs/pages/layout/cluster.md
1313
+ *
1284
1314
  * Public hooks:
1285
1315
  * --cluster-align
1286
1316
  * --cluster-justify
@@ -1304,6 +1334,8 @@ small {
1304
1334
  /*
1305
1335
  * Centered content column with a maximum measure.
1306
1336
  *
1337
+ * Docs: docs/pages/layout/center.md
1338
+ *
1307
1339
  * Public hooks:
1308
1340
  * --center-size
1309
1341
  * --center-pad
@@ -1319,6 +1351,8 @@ small {
1319
1351
  /*
1320
1352
  * Media frame with a fixed ratio.
1321
1353
  *
1354
+ * Docs: docs/pages/layout/frame.md
1355
+ *
1322
1356
  * Public hooks:
1323
1357
  * --frame-ratio
1324
1358
  *
@@ -1336,7 +1370,11 @@ small {
1336
1370
  object-fit: cover;
1337
1371
  }
1338
1372
 
1339
- /* Full-height single-column document shell. */
1373
+ /*
1374
+ * Full-height single-column document shell.
1375
+ *
1376
+ * Docs: docs/pages/layout/app-shell.md
1377
+ */
1340
1378
  .app-shell {
1341
1379
  min-block-size: var(--viewport-block);
1342
1380
  display: flex;
@@ -1350,6 +1388,8 @@ small {
1350
1388
  * chrome available without fixed offsets. This is not a generic page wrapper
1351
1389
  * or a public-site layout; all three direct regions are part of the contract.
1352
1390
  *
1391
+ * Docs: docs/pages/layout/app-layout.md
1392
+ *
1353
1393
  * Public hook:
1354
1394
  * --app-nav-side-size
1355
1395
  */
@@ -1449,7 +1489,8 @@ small {
1449
1489
  /* Media object: fixed-width leading element (avatar, icon, image) + flexible
1450
1490
  trailing content. Replaces the repeated "auto minmax(0, 1fr)" pattern in
1451
1491
  author cards, comments, meta rows, and similar compositions.
1452
- Child contract: exactly two direct children (leading media + content). */
1492
+ Child contract: exactly two direct children (leading media + content).
1493
+ Docs: docs/pages/layout/media.md */
1453
1494
  .media {
1454
1495
  display: grid;
1455
1496
  grid-template-columns: auto minmax(0, 1fr);
@@ -1460,6 +1501,8 @@ small {
1460
1501
  /*
1461
1502
  * Peer regions that switch as a group.
1462
1503
  *
1504
+ * Docs: docs/pages/layout/switcher.md
1505
+ *
1463
1506
  * All children sit side by side when they all fit; otherwise every child
1464
1507
  * stacks. Unlike .grid, this recipe cannot create a partial row such as two
1465
1508
  * items plus one orphan.
@@ -1485,6 +1528,8 @@ small {
1485
1528
  /*
1486
1529
  * Main content + secondary region.
1487
1530
  *
1531
+ * Docs: docs/pages/layout/sidebar-layout.md
1532
+ *
1488
1533
  * The children wrap according to the space allocated to this layout, without
1489
1534
  * an ancestor opt-in or viewport threshold. The large growth ratio lets the
1490
1535
  * main region reclaim the full row when the aside wraps instead of remaining
@@ -1539,6 +1584,8 @@ small {
1539
1584
  /*
1540
1585
  * Responsive equal-width grids.
1541
1586
  *
1587
+ * Docs: docs/pages/layout/grid.md
1588
+ *
1542
1589
  * .grid is space-driven: it fits as many tracks as --grid-min allows.
1543
1590
  * .grid-N is count-driven: it declares a structural density of N and collapses
1544
1591
  * through natural divisors of N.
@@ -1669,6 +1716,8 @@ small {
1669
1716
  /*
1670
1717
  * Explicit 12-column composition canvas.
1671
1718
  *
1719
+ * Docs: docs/pages/layout/column-layout.md
1720
+ *
1672
1721
  * Neither space-driven like .grid nor count-driven like .grid-N: this is a
1673
1722
  * coordinate system. The author places regions with .column-span-N /
1674
1723
  * .column-start-N and owns narrow-container recomposition. There is
@@ -1856,6 +1905,8 @@ small {
1856
1905
  * Scroll snap — optional horizontal rails for niche card strips and simple
1857
1906
  * touch-friendly overflow areas.
1858
1907
  *
1908
+ * Docs: docs/pages/layout/scroll-snap.md
1909
+ *
1859
1910
  * This is not a full carousel. For previous/next controls, pagination,
1860
1911
  * active state, drag behavior, looping, or robust desktop mouse interaction,
1861
1912
  * use a dedicated carousel library.
@@ -1920,6 +1971,8 @@ small {
1920
1971
  * header (.topbar) or underlaps it; scroll-margin reserves space on the
1921
1972
  * target itself so the browser stops early.
1922
1973
  *
1974
+ * Docs: docs/pages/layout/scroll-target.md
1975
+ *
1923
1976
  * Apply to the elements that are link targets: <section id="...">, headings,
1924
1977
  * etc. The scrollspy enhancer only reads ids — this offset is what actually
1925
1978
  * shapes where the page lands, and the two are independent.
@@ -1937,6 +1990,8 @@ small {
1937
1990
  * background only — row content (search, breadcrumb, actions) stays local
1938
1991
  * to the page.
1939
1992
  *
1993
+ * Docs: docs/pages/layout/topbar.md
1994
+ *
1940
1995
  * Not for marketing/editorial site headers — those are one-off visual
1941
1996
  * identity per page; see docs/pages/patterns/overview.md's Header Navigation
1942
1997
  * section. This is for the structural app-shell topbar, which recurs unchanged
@@ -1990,6 +2045,8 @@ small {
1990
2045
  /*
1991
2046
  * Form layout and supporting text.
1992
2047
  *
2048
+ * Docs: docs/pages/forms/overview.md
2049
+ *
1993
2050
  * Individual controls keep their own files; .choice label layout lives in
1994
2051
  * choice.css; .form-actions lives in form-actions.css.
1995
2052
  *
@@ -2005,6 +2062,12 @@ small {
2005
2062
  */
2006
2063
  .field {
2007
2064
  display: grid;
2065
+
2066
+ /* why: a field stretched by a taller row sibling — the classic case is a
2067
+ neighbour revealing its .field-error — would otherwise hand the extra
2068
+ block size to its auto rows and inflate the control. Rows keep their
2069
+ natural height; the slack stays at the end. */
2070
+ align-content: start;
2008
2071
  gap: var(--space-10);
2009
2072
  color: var(--text);
2010
2073
  font-size: var(--control-font-size);
@@ -2052,6 +2115,8 @@ small {
2052
2115
  /*
2053
2116
  * Text controls — input, textarea, select.
2054
2117
  *
2118
+ * Docs: docs/pages/forms/overview.md
2119
+ *
2055
2120
  * Neutral geometry and interaction states shared across all text-like
2056
2121
  * controls. select shares this base recipe; its chevron/appearance layer
2057
2122
  * lives in select.css. checkbox, radio, range, switch, and file have their
@@ -2405,6 +2470,8 @@ small {
2405
2470
  * Select — chevron and appearance layer on top of the shared text-control
2406
2471
  * recipe.
2407
2472
  *
2473
+ * Docs: docs/pages/forms/overview.md
2474
+ *
2408
2475
  * Base geometry and interaction states come from control.css (select shares
2409
2476
  * the --control-* tokens). This file only removes the native arrow and draws
2410
2477
  * the CSS chevron, so the picker (appearance: base-select) enhancement in
@@ -2463,6 +2530,8 @@ small {
2463
2530
  * custom styling, so the framework stays out of the way and just keeps them
2464
2531
  * readable and theme-aware.
2465
2532
  *
2533
+ * Docs: docs/pages/forms/controls.md
2534
+ *
2466
2535
  * Range has its own custom-styled file (range.css) so the focus ring can
2467
2536
  * target the thumb pseudo-element rather than the rectangular CSS box.
2468
2537
  */
@@ -2563,6 +2632,8 @@ small {
2563
2632
  /*
2564
2633
  * Form validation states.
2565
2634
  *
2635
+ * Docs: docs/pages/forms/validation.md
2636
+ *
2566
2637
  * Imported by default through forms/index.css. Custom builds may omit this
2567
2638
  * file when an app wants to own validation styling completely.
2568
2639
  *
@@ -2654,6 +2725,8 @@ small {
2654
2725
  /*
2655
2726
  * Form actions — bottom form bar with sticky variant.
2656
2727
  *
2728
+ * Docs: docs/pages/forms/form-actions.md
2729
+ *
2657
2730
  * A flex cluster of buttons/actions that closes a form. The sticky variant
2658
2731
  * docks to the viewport bottom with a translucent blur where supported; at
2659
2732
  * rest it stays visually neutral so it does not double as a separator.
@@ -2710,6 +2783,8 @@ small {
2710
2783
  /*
2711
2784
  * Input Icon — decorative icon inside a relative wrapper, leading or trailing.
2712
2785
  *
2786
+ * Docs: docs/pages/patterns/icon-input.md
2787
+ *
2713
2788
  * Icon-library agnostic: detects position by DOM order. First child that is
2714
2789
  * not .input = leading icon; icon after .input = trailing. The icon follows
2715
2790
  * local typography; density contexts shrink the control geometry around it
@@ -2771,6 +2846,8 @@ small {
2771
2846
  /*
2772
2847
  * Switch controls for checkbox inputs with switch semantics.
2773
2848
  *
2849
+ * Docs: docs/pages/forms/switch.md
2850
+ *
2774
2851
  * The switch remains an input control, not a decorative toggle. Checked, hover,
2775
2852
  * focus-visible, and disabled states stay clear.
2776
2853
  *
@@ -2923,6 +3000,8 @@ small {
2923
3000
  * Range slider — custom-styled so the focus ring always targets the
2924
3001
  * user-manipulated surface (the thumb) rather than the CSS box.
2925
3002
  *
3003
+ * Docs: docs/pages/forms/range.md
3004
+ *
2926
3005
  * appearance: none gives us full control over track and thumb; without it,
2927
3006
  * pseudo-element rules for ::-webkit-slider-thumb and ::-moz-range-thumb
2928
3007
  * are not reliably applied across browsers.
@@ -3038,6 +3117,8 @@ small {
3038
3117
  /*
3039
3118
  * Choice cards — card-style radio and checkbox labels.
3040
3119
  *
3120
+ * Docs: docs/pages/forms/choice-cards.md
3121
+ *
3041
3122
  * Each .choice-card is a <label> wrapping a native radio or checkbox. When
3042
3123
  * :has() is supported the input is visually hidden and the card mirrors its
3043
3124
  * state: checked border and tint, checkmark badge (round for radio, rounded
@@ -3074,6 +3155,8 @@ small {
3074
3155
  border-radius: var(--choice-card-radius);
3075
3156
  background: var(--choice-card-bg);
3076
3157
  color: var(--text);
3158
+ /* Consumed so .sm/.lg reach the card; the badge's em sizing follows. */
3159
+ font-size: var(--control-font-size);
3077
3160
  cursor: pointer;
3078
3161
  transition:
3079
3162
  border-color var(--duration),
@@ -3282,6 +3365,8 @@ small {
3282
3365
  * Floating labels — optional composition over the text controls
3283
3366
  * (.input, .textarea, .select) and .field-label.
3284
3367
  *
3368
+ * Docs: docs/pages/forms/floating-field.md
3369
+ *
3285
3370
  * The cell owns a control plus its label; the label overlays the control and
3286
3371
  * floats to the top when the control is focused or non-empty. Help and error
3287
3372
  * messages stay in the surrounding .field, not inside the cell, so the
@@ -3323,7 +3408,9 @@ small {
3323
3408
 
3324
3409
  .floating-field > .field-label {
3325
3410
  position: absolute;
3326
- inset-inline-start: calc(var(--space-30) + var(--border-width));
3411
+ /* Same start edge as the control's text: --control-pad-x owns that shared
3412
+ geometry, so .sm/.lg keep label and value aligned. */
3413
+ inset-inline-start: calc(var(--control-pad-x) + var(--border-width));
3327
3414
  color: var(--text-muted);
3328
3415
  font-size: var(--control-font-size);
3329
3416
  font-weight: var(--font-weight-normal);
@@ -3389,6 +3476,8 @@ small {
3389
3476
  /*
3390
3477
  * One-time code input with visual cells and one native input.
3391
3478
  *
3479
+ * Docs: docs/pages/forms/otp.md
3480
+ *
3392
3481
  * Add one direct span per accepted character. The cells determine the
3393
3482
  * component width; the overlaid input remains the only interactive control.
3394
3483
  *
@@ -3517,12 +3606,22 @@ small {
3517
3606
  .needs-validation.was-validated .otp > input:invalid ~ span {
3518
3607
  --otp-cell-border: Mark;
3519
3608
  }
3609
+
3610
+ /* biome-ignore lint/style/noDescendingSpecificity: the :user-invalid route
3611
+ mirrors the non-forced-colors block above and must come last — the three
3612
+ invalid markers write the same value, so source order is inert. An engine
3613
+ without the pseudo drops this rule alone. */
3614
+ .needs-validation .otp > input:user-invalid ~ span {
3615
+ --otp-cell-border: Mark;
3616
+ }
3520
3617
  }
3521
3618
 
3522
3619
  /* Controls */
3523
3620
  /*
3524
3621
  * Buttons for actions and button-like links.
3525
3622
  *
3623
+ * Docs: docs/pages/components/button.md
3624
+ *
3526
3625
  * .btn is primarily for actionable elements: <button>, <a href>,
3527
3626
  * input[type="button"|"submit"|"reset"].
3528
3627
  *
@@ -3769,6 +3868,8 @@ a.btn[aria-disabled="true"] {
3769
3868
  /*
3770
3869
  * Badges for compact labels, counts, and state hints.
3771
3870
  *
3871
+ * Docs: docs/pages/components/badge.md
3872
+ *
3772
3873
  * Soft by default. .solid for counters and notification badges. The dot
3773
3874
  * (.badge:empty) stays solid regardless — a soft dot is pointless.
3774
3875
  * Shared .outline variant stays available.
@@ -4029,6 +4130,8 @@ a.badge[aria-disabled="true"] {
4029
4130
  /*
4030
4131
  * Alerts for inline feedback and status messages.
4031
4132
  *
4133
+ * Docs: docs/pages/components/alert.md
4134
+ *
4032
4135
  * Keeps alert defaults local while consuming the shared --ui-* variant
4033
4136
  * plumbing for emphasis.
4034
4137
  *
@@ -4315,6 +4418,8 @@ a.badge[aria-disabled="true"] {
4315
4418
  /*
4316
4419
  * Actions — a horizontal action-button cluster.
4317
4420
  *
4421
+ * Docs: docs/pages/patterns/actions.md
4422
+ *
4318
4423
  * Composes with .btn (or any inline element) and feeds layout via the shared
4319
4424
  * --gap token.
4320
4425
  *
@@ -4343,6 +4448,8 @@ a.badge[aria-disabled="true"] {
4343
4448
  /*
4344
4449
  * Cards provide a neutral raised surface for grouped content.
4345
4450
  *
4451
+ * Docs: docs/pages/components/card.md
4452
+ *
4346
4453
  * Surface, border, radius, padding, density, and elevation stay local here.
4347
4454
  * Direct header/footer children are structural slots; a direct footer is
4348
4455
  * anchored to the card's bottom edge when extra block space is available (so
@@ -4504,6 +4611,8 @@ a.badge[aria-disabled="true"] {
4504
4611
  /*
4505
4612
  * Modal shell styling for native dialog elements.
4506
4613
  *
4614
+ * Docs: docs/pages/components/dialog.md
4615
+ *
4507
4616
  * Spacing, radius, transitions, and focus treatment come from shared tokens.
4508
4617
  * Opening behavior and intent-specific decisions stay outside this component.
4509
4618
  *
@@ -4515,16 +4624,15 @@ a.badge[aria-disabled="true"] {
4515
4624
  *
4516
4625
  * Internal:
4517
4626
  * --dialog-close-icon-size
4627
+ * --dialog-focus-bleed
4518
4628
  * --dialog-viewport-block
4519
4629
  * --dialog-viewport-gap-default
4520
4630
  * --dialog-viewport-gap-2x
4521
4631
  */
4522
- dialog.modal {
4523
- /* --dialog-viewport-gap stays fallback-only for the same reason as
4524
- --modal-size below: declaring the public hook here would give it
4525
- type+class specificity that a single author class cannot outrank. The
4526
- responsive default lives in an internal token instead, so a media query
4527
- can still move it without closing the hook.
4632
+ :where(dialog).modal {
4633
+ /* --dialog-viewport-gap stays fallback-only so the responsive default can
4634
+ live in an internal token: a media query moves it without closing the
4635
+ public hook.
4528
4636
 
4529
4637
  calc() multiplication is not in the supported browser range, so the
4530
4638
  doubled gap is spelled out with an additive calc. */
@@ -4538,10 +4646,8 @@ dialog.modal {
4538
4646
  provides the containing block for the absolutely positioned close button. */
4539
4647
  position: fixed;
4540
4648
 
4541
- /* --modal-size stays fallback-only: declaring it on dialog.modal gives it
4542
- type+class specificity, which a single author class cannot outrank, so the
4543
- public hook would silently keep this default. Same reason choice.css never
4544
- declares --choice-control-nudge. */
4649
+ /* The root is :where(dialog).modal (0-1-0), so a single author class can
4650
+ outrank these defaults — no fallback-only indirection needed here. */
4545
4651
  max-inline-size: min(calc(100% - var(--dialog-viewport-gap-2x)), var(--modal-size, 32rem));
4546
4652
 
4547
4653
  /* Stable viewport cap for the panel: dvh tracks the dynamic browser toolbar
@@ -4571,8 +4677,8 @@ dialog.modal {
4571
4677
  transform var(--duration) var(--ease-exit);
4572
4678
  }
4573
4679
 
4574
- dialog.modal > form,
4575
- dialog.modal > .stack {
4680
+ :where(dialog).modal > form,
4681
+ :where(dialog).modal > .stack {
4576
4682
  display: grid;
4577
4683
  gap: var(--space-40);
4578
4684
  max-block-size: calc(var(--dialog-viewport-block) - var(--dialog-viewport-gap-2x));
@@ -4582,7 +4688,7 @@ dialog.modal > .stack {
4582
4688
  }
4583
4689
 
4584
4690
  @supports (block-size: 100svh) {
4585
- dialog.modal {
4691
+ :where(dialog).modal {
4586
4692
  --dialog-viewport-block: 100svh;
4587
4693
  }
4588
4694
  }
@@ -4590,42 +4696,50 @@ dialog.modal > .stack {
4590
4696
  /* Keep the dialog off the viewport edges on small screens. Moves the internal
4591
4697
  default, not the public hook, so an author override still wins here too. */
4592
4698
  @media (max-width: 30rem) {
4593
- dialog.modal {
4699
+ :where(dialog).modal {
4594
4700
  --dialog-viewport-gap-default: var(--space-30);
4595
4701
  }
4596
4702
  }
4597
4703
 
4598
- dialog.modal.scrollable > form,
4599
- dialog.modal.scrollable > .stack {
4704
+ :where(dialog).modal.scrollable > form,
4705
+ :where(dialog).modal.scrollable > .stack {
4600
4706
  grid-template-rows: auto minmax(0, 1fr) auto;
4601
4707
  overflow: hidden;
4602
4708
  }
4603
4709
 
4604
- dialog.modal.scrollable > form > :not(:is(header, footer)),
4605
- dialog.modal.scrollable > .stack > :not(:is(header, footer)) {
4710
+ :where(dialog).modal.scrollable > form > :not(:is(header, footer)),
4711
+ :where(dialog).modal.scrollable > .stack > :not(:is(header, footer)) {
4712
+ --dialog-focus-bleed: calc(var(--focus-outline-offset) + var(--border-width) * 2);
4713
+
4606
4714
  min-block-size: 0;
4715
+ /* Keep focus paint inside the clip without moving the body off the fixed
4716
+ header and footer edges. */
4717
+ margin-inline: calc(var(--dialog-focus-bleed) * -1);
4718
+ padding-inline: var(--dialog-focus-bleed);
4607
4719
  overflow: auto;
4608
4720
  overscroll-behavior: contain;
4609
4721
  }
4610
4722
 
4611
- dialog.modal header > :first-child,
4612
- dialog.modal footer > :first-child {
4723
+ /* Anatomy is bounded to the structural slots (direct children of the
4724
+ form/.stack body): a header or footer deeper in the content keeps its own
4725
+ rhythm and is not restyled as the modal's chrome. */
4726
+ :where(dialog).modal > :where(form, .stack) > :is(header, footer) > :first-child {
4613
4727
  margin-block-start: 0;
4614
4728
  }
4615
4729
 
4616
- dialog.modal header > :last-child,
4617
- dialog.modal footer > :last-child {
4730
+ :where(dialog).modal > :where(form, .stack) > :is(header, footer) > :last-child {
4618
4731
  margin-block-end: 0;
4619
4732
  }
4620
4733
 
4621
4734
  /* The close button is anchored to the dialog at top inline-end, out of the
4622
- content flow; headers reserve inline room so titles never run under it. */
4623
- dialog.modal > form > header,
4624
- dialog.modal > .stack > header {
4735
+ content flow; headers reserve inline room so titles never run under it.
4736
+ The form/.stack anatomy is structural, so it stays in :where() and never
4737
+ lends .stack's specificity to a bare <form>. */
4738
+ :where(dialog).modal > :where(form, .stack) > header {
4625
4739
  padding-inline-end: calc(var(--control-size) + var(--space-30));
4626
4740
  }
4627
4741
 
4628
- dialog.modal footer {
4742
+ :where(dialog).modal > :where(form, .stack) > footer {
4629
4743
  display: flex;
4630
4744
  flex-wrap: wrap;
4631
4745
  justify-content: flex-end;
@@ -4635,17 +4749,17 @@ dialog.modal footer {
4635
4749
  /* Confirmation layout — a focused message followed by a visually distinct
4636
4750
  action band. The media primitive owns icon/content alignment; this variant
4637
4751
  only removes the standard form inset so both regions can own their padding. */
4638
- dialog.modal.dialog-confirmation > form {
4752
+ :where(dialog).modal.dialog-confirmation > form {
4639
4753
  gap: 0;
4640
4754
  padding: 0;
4641
4755
  }
4642
4756
 
4643
- dialog.modal.dialog-confirmation > form > .media {
4757
+ :where(dialog).modal.dialog-confirmation > form > .media {
4644
4758
  gap: var(--space-40);
4645
4759
  padding: var(--space-50);
4646
4760
  }
4647
4761
 
4648
- dialog.modal.dialog-confirmation > form > footer {
4762
+ :where(dialog).modal.dialog-confirmation > form > footer {
4649
4763
  padding: var(--space-40) var(--space-50);
4650
4764
  border-end-start-radius: calc(var(--radius-lg) - var(--border-width));
4651
4765
  border-end-end-radius: calc(var(--radius-lg) - var(--border-width));
@@ -4686,13 +4800,13 @@ dialog.modal.dialog-confirmation > form > footer {
4686
4800
  }
4687
4801
 
4688
4802
  @media (max-width: 30rem) {
4689
- dialog.modal.dialog-confirmation > form > .media,
4690
- dialog.modal.dialog-confirmation > form > footer {
4803
+ :where(dialog).modal.dialog-confirmation > form > .media,
4804
+ :where(dialog).modal.dialog-confirmation > form > footer {
4691
4805
  padding: var(--space-40);
4692
4806
  }
4693
4807
  }
4694
4808
 
4695
- dialog.modal[open] {
4809
+ :where(dialog).modal[open] {
4696
4810
  opacity: 1;
4697
4811
  transform: none;
4698
4812
  transition:
@@ -4701,13 +4815,13 @@ dialog.modal[open] {
4701
4815
  }
4702
4816
 
4703
4817
  @starting-style {
4704
- dialog.modal[open] {
4818
+ :where(dialog).modal[open] {
4705
4819
  opacity: 0;
4706
4820
  transform: translateY(0.35rem);
4707
4821
  }
4708
4822
  }
4709
4823
 
4710
- dialog.modal::backdrop {
4824
+ :where(dialog).modal::backdrop {
4711
4825
  background: var(--backdrop-fill);
4712
4826
  opacity: 0;
4713
4827
  /* Scrim curve stays neutral `ease`; only the panel carries the character
@@ -4716,7 +4830,7 @@ dialog.modal::backdrop {
4716
4830
  }
4717
4831
 
4718
4832
  @supports (transition-behavior: allow-discrete) {
4719
- dialog.modal {
4833
+ :where(dialog).modal {
4720
4834
  transition:
4721
4835
  opacity var(--duration) var(--ease-exit),
4722
4836
  transform var(--duration) var(--ease-exit),
@@ -4724,7 +4838,7 @@ dialog.modal::backdrop {
4724
4838
  display var(--duration) allow-discrete;
4725
4839
  }
4726
4840
 
4727
- dialog.modal[open] {
4841
+ :where(dialog).modal[open] {
4728
4842
  transition:
4729
4843
  opacity var(--duration) var(--ease-enter),
4730
4844
  transform var(--duration) var(--ease-enter),
@@ -4732,7 +4846,7 @@ dialog.modal::backdrop {
4732
4846
  display var(--duration) allow-discrete;
4733
4847
  }
4734
4848
 
4735
- dialog.modal::backdrop {
4849
+ :where(dialog).modal::backdrop {
4736
4850
  transition:
4737
4851
  opacity var(--duration) ease,
4738
4852
  overlay var(--duration) allow-discrete,
@@ -4740,7 +4854,7 @@ dialog.modal::backdrop {
4740
4854
  }
4741
4855
  }
4742
4856
 
4743
- dialog.modal[open]::backdrop {
4857
+ :where(dialog).modal[open]::backdrop {
4744
4858
  opacity: 1;
4745
4859
  }
4746
4860
 
@@ -4753,7 +4867,7 @@ html.has-modal-open.had-scrollbar {
4753
4867
  }
4754
4868
 
4755
4869
  @starting-style {
4756
- dialog.modal[open]::backdrop {
4870
+ :where(dialog).modal[open]::backdrop {
4757
4871
  opacity: 0;
4758
4872
  }
4759
4873
  }
@@ -4761,7 +4875,7 @@ html.has-modal-open.had-scrollbar {
4761
4875
  @media (prefers-reduced-motion: no-preference) {
4762
4876
  /* Wrong-close feedback and view-transition transport are motion quirks, not
4763
4877
  presence enter/exit, so they stay on the plain CSS default easing. */
4764
- dialog.modal.is-static {
4878
+ :where(dialog).modal.is-static {
4765
4879
  animation: actual-dialog-static var(--duration-slow) ease;
4766
4880
  }
4767
4881
 
@@ -4842,6 +4956,12 @@ html.has-modal-open.had-scrollbar {
4842
4956
  /*
4843
4957
  * Drawer — modal side-sheet built on the native dialog element.
4844
4958
  *
4959
+ * Docs: docs/pages/components/drawer.md
4960
+ *
4961
+ * Open with a native command button:
4962
+ * command="show-modal" commandfor="<drawer-id>".
4963
+ * For stacked navigation, compose with .nav-list from navbar.css.
4964
+ *
4845
4965
  * Top-layer rendering, ::backdrop, Escape, and modal focus come from the
4846
4966
  * native dialog. CSS owns progressive enter/exit motion where browsers support
4847
4967
  * discrete display/overlay transitions. The drawer is a layout shell only —
@@ -4854,10 +4974,11 @@ html.has-modal-open.had-scrollbar {
4854
4974
  *
4855
4975
  * Internal:
4856
4976
  * --drawer-closed-translate (side/RTL positioning state)
4977
+ * --drawer-focus-bleed
4857
4978
  * --drawer-motion-duration (shared by panel and ::backdrop so exit duration
4858
4979
  * changes in one place; entry state redefines it longer)
4859
4980
  */
4860
- dialog.drawer {
4981
+ :where(dialog).drawer {
4861
4982
  --drawer-size: 20rem;
4862
4983
  --drawer-pad: var(--space-40);
4863
4984
  --drawer-closed-translate: -100%;
@@ -4900,7 +5021,7 @@ dialog.drawer {
4900
5021
  transform var(--drawer-motion-duration) var(--ease-exit);
4901
5022
  }
4902
5023
 
4903
- dialog.drawer[open] {
5024
+ :where(dialog).drawer[open] {
4904
5025
  --drawer-motion-duration: var(--duration-slow);
4905
5026
  display: grid;
4906
5027
  grid-template-rows: auto minmax(0, 1fr) auto;
@@ -4915,7 +5036,7 @@ dialog.drawer[open] {
4915
5036
  /* A form is the drawer's content wrapper when actions need method="dialog".
4916
5037
  Span the drawer tracks so the form can keep the header at the top, let the
4917
5038
  body absorb the available height, and keep the footer at the bottom. */
4918
- dialog.drawer > form {
5039
+ :where(dialog).drawer > form {
4919
5040
  display: grid;
4920
5041
  grid-row: 1 / -1;
4921
5042
  grid-template-rows: auto minmax(0, 1fr) auto;
@@ -4924,7 +5045,7 @@ dialog.drawer > form {
4924
5045
  }
4925
5046
 
4926
5047
  /* The shared backdrop token keeps drawer and modal scrims consistent. */
4927
- dialog.drawer::backdrop {
5048
+ :where(dialog).drawer::backdrop {
4928
5049
  background: var(--backdrop-fill);
4929
5050
  opacity: 0;
4930
5051
  /* Scrim curve stays neutral `ease`; only the panel's character curves
@@ -4932,7 +5053,7 @@ dialog.drawer::backdrop {
4932
5053
  transition: opacity var(--drawer-motion-duration) ease;
4933
5054
  }
4934
5055
 
4935
- dialog.drawer[data-side="end"] {
5056
+ :where(dialog).drawer[data-side="end"] {
4936
5057
  --drawer-closed-translate: 100%;
4937
5058
 
4938
5059
  margin-inline-start: auto;
@@ -4941,17 +5062,17 @@ dialog.drawer[data-side="end"] {
4941
5062
  border-inline-start: var(--border-width) solid var(--border);
4942
5063
  }
4943
5064
 
4944
- dialog.drawer[open]::backdrop {
5065
+ :where(dialog).drawer[open]::backdrop {
4945
5066
  opacity: 1;
4946
5067
  }
4947
5068
 
4948
5069
  @starting-style {
4949
- dialog.drawer[open] {
5070
+ :where(dialog).drawer[open] {
4950
5071
  opacity: 0;
4951
5072
  transform: translateX(var(--drawer-closed-translate));
4952
5073
  }
4953
5074
 
4954
- dialog.drawer[open]::backdrop {
5075
+ :where(dialog).drawer[open]::backdrop {
4955
5076
  opacity: 0;
4956
5077
  }
4957
5078
  }
@@ -4961,15 +5082,21 @@ dialog.drawer[open]::backdrop {
4961
5082
  modal, which sizes to its content and opts in with .scrollable. Header and
4962
5083
  footer keep their own tracks; .drawer-close is named because a generic
4963
5084
  exclusion list is easier to trust than its `position: absolute`. */
4964
- dialog.drawer > :not(:is(header, footer, form, .drawer-close)),
4965
- dialog.drawer > form > :not(:is(header, footer, .drawer-close)) {
5085
+ :where(dialog).drawer > :not(:is(header, footer, form, .drawer-close)),
5086
+ :where(dialog).drawer > form > :not(:is(header, footer, .drawer-close)) {
5087
+ --drawer-focus-bleed: calc(var(--focus-outline-offset) + var(--border-width) * 2);
5088
+
4966
5089
  min-block-size: 0;
5090
+ /* Keep focus paint inside the clip without moving the body off the fixed
5091
+ header and footer edges. */
5092
+ margin-inline: calc(var(--drawer-focus-bleed) * -1);
5093
+ padding-inline: var(--drawer-focus-bleed);
4967
5094
  overflow: auto;
4968
5095
  overscroll-behavior: contain;
4969
5096
  }
4970
5097
 
4971
5098
  @supports (transition-behavior: allow-discrete) {
4972
- dialog.drawer {
5099
+ :where(dialog).drawer {
4973
5100
  transition:
4974
5101
  opacity var(--drawer-motion-duration) var(--ease-exit),
4975
5102
  transform var(--drawer-motion-duration) var(--ease-exit),
@@ -4977,7 +5104,7 @@ dialog.drawer > form > :not(:is(header, footer, .drawer-close)) {
4977
5104
  display var(--drawer-motion-duration) allow-discrete;
4978
5105
  }
4979
5106
 
4980
- dialog.drawer[open] {
5107
+ :where(dialog).drawer[open] {
4981
5108
  transition:
4982
5109
  opacity var(--drawer-motion-duration) var(--ease-enter),
4983
5110
  transform var(--drawer-motion-duration) var(--ease-enter),
@@ -4985,7 +5112,7 @@ dialog.drawer > form > :not(:is(header, footer, .drawer-close)) {
4985
5112
  display var(--drawer-motion-duration) allow-discrete;
4986
5113
  }
4987
5114
 
4988
- dialog.drawer::backdrop {
5115
+ :where(dialog).drawer::backdrop {
4989
5116
  transition:
4990
5117
  opacity var(--drawer-motion-duration) ease,
4991
5118
  overlay var(--drawer-motion-duration) allow-discrete,
@@ -4999,40 +5126,40 @@ dialog.drawer > form > :not(:is(header, footer, .drawer-close)) {
4999
5126
  directionality and handles nested direction changes; the explicit ltr rules
5000
5127
  neutralize an outer RTL fallback selector. Each specificity group keeps the
5001
5128
  fallback before the :dir() rules. */
5002
- [dir="rtl"] dialog.drawer {
5129
+ [dir="rtl"] :where(dialog).drawer {
5003
5130
  --drawer-closed-translate: 100%;
5004
5131
  }
5005
5132
 
5006
- dialog.drawer:dir(ltr) {
5133
+ :where(dialog).drawer:dir(ltr) {
5007
5134
  --drawer-closed-translate: -100%;
5008
5135
  }
5009
5136
 
5010
- dialog.drawer:dir(rtl) {
5137
+ :where(dialog).drawer:dir(rtl) {
5011
5138
  --drawer-closed-translate: 100%;
5012
5139
  }
5013
5140
 
5014
- [dir="rtl"] dialog.drawer[data-side="end"] {
5141
+ [dir="rtl"] :where(dialog).drawer[data-side="end"] {
5015
5142
  --drawer-closed-translate: -100%;
5016
5143
  }
5017
5144
 
5018
- dialog.drawer[data-side="end"]:dir(ltr) {
5145
+ :where(dialog).drawer[data-side="end"]:dir(ltr) {
5019
5146
  --drawer-closed-translate: 100%;
5020
5147
  }
5021
5148
 
5022
- dialog.drawer[data-side="end"]:dir(rtl) {
5149
+ :where(dialog).drawer[data-side="end"]:dir(rtl) {
5023
5150
  --drawer-closed-translate: -100%;
5024
5151
  }
5025
5152
 
5026
5153
  @media (prefers-reduced-motion: no-preference) {
5027
- dialog.drawer.is-static {
5154
+ :where(dialog).drawer.is-static {
5028
5155
  animation: actual-dialog-static var(--duration-slow) ease;
5029
5156
  }
5030
5157
  }
5031
5158
 
5032
5159
  /* The header reserves inline room so the title never runs under the close
5033
5160
  control. */
5034
- dialog.drawer > header,
5035
- dialog.drawer > form > header {
5161
+ :where(dialog).drawer > header,
5162
+ :where(dialog).drawer > form > header {
5036
5163
  padding-inline-end: calc(var(--control-size) + var(--space-30));
5037
5164
  }
5038
5165
 
@@ -5091,6 +5218,8 @@ dialog.drawer > form > header {
5091
5218
  /*
5092
5219
  * Flyout — floating panel for actions and links.
5093
5220
  *
5221
+ * Docs: docs/pages/enhancements/flyout.md
5222
+ *
5094
5223
  * JavaScript promotes the panel to the top layer and chooses viewport or
5095
5224
  * document coordinates from its anchor. It also owns state (.is-open), focus,
5096
5225
  * and keyboard behavior. CSS owns the surfaced panel appearance, item styles,
@@ -5393,6 +5522,8 @@ dialog.drawer > form > header {
5393
5522
  /*
5394
5523
  * Tooltip — supplemental label overlaid near a trigger.
5395
5524
  *
5525
+ * Docs: docs/pages/enhancements/tooltip.md
5526
+ *
5396
5527
  * Position, placement, and arrow position are managed by JavaScript via left,
5397
5528
  * top, data-placement, and --arrow-x. The runtime also owns `position`: it
5398
5529
  * writes `absolute` for a tip whose trigger scrolls with the page, so the
@@ -5535,6 +5666,8 @@ dialog.drawer > form > header {
5535
5666
  /*
5536
5667
  * Accordion disclosures built on native details and summary.
5537
5668
  *
5669
+ * Docs: docs/pages/components/accordion.md
5670
+ *
5538
5671
  * Keeps the clickable summary affordance aligned and usable without JavaScript
5539
5672
  * or demo styles. Decorative palettes and custom markers belong outside the
5540
5673
  * base component.
@@ -5662,6 +5795,8 @@ dialog.drawer > form > header {
5662
5795
  /*
5663
5796
  * Spinner — compact loading feedback.
5664
5797
  *
5798
+ * Docs: docs/pages/components/spinner.md
5799
+ *
5665
5800
  * Uses currentColor so it adapts to buttons, alerts, badges, and text. The
5666
5801
  * [aria-busy] overlay composition lives in components/busy.css.
5667
5802
  *
@@ -5727,6 +5862,8 @@ dialog.drawer > form > header {
5727
5862
  /*
5728
5863
  * Skeleton placeholders for loading states.
5729
5864
  *
5865
+ * Docs: docs/pages/components/skeleton.md
5866
+ *
5730
5867
  * Shows approximate loading structure without implying real content hierarchy.
5731
5868
  * Geometry, animation, and color stay theme-aware.
5732
5869
  *
@@ -5813,6 +5950,8 @@ dialog.drawer > form > header {
5813
5950
  /*
5814
5951
  * Busy overlay — non-button loading state.
5815
5952
  *
5953
+ * Docs: docs/pages/components/spinner.md
5954
+ *
5816
5955
  * A non-button element with aria-busy="true" and a direct last-child .spinner
5817
5956
  * shows that spinner as a centered overlay over a faded surface. Buttons keep
5818
5957
  * their spinners inline (e.g. .btn[aria-busy] > .spinner stays inline-flex).
@@ -5865,6 +6004,8 @@ dialog.drawer > form > header {
5865
6004
  /*
5866
6005
  * Progress — native progress bar with theme-aware track and value.
5867
6006
  *
6007
+ * Docs: docs/pages/components/progress.md
6008
+ *
5868
6009
  * Consumes the shared --intent/--primary for the value bar and --bar-height
5869
6010
  * for thickness. Indeterminate state animates a shimmer using --duration-shimmer.
5870
6011
  *
@@ -5979,6 +6120,8 @@ dialog.drawer > form > header {
5979
6120
  /*
5980
6121
  * Status bar — singleton floating status area.
5981
6122
  *
6123
+ * Docs: docs/pages/enhancements/status-bar.md
6124
+ *
5982
6125
  * One element in the HTML, empty by default. .is-open is the visual state, set
5983
6126
  * by status.js like any other surface: it lands after the message is written
5984
6127
  * and lifts before the bar is cleared, so the bar leaves in the exact state it
@@ -6070,6 +6213,8 @@ dialog.drawer > form > header {
6070
6213
  /*
6071
6214
  * Avatars for initials, images, links, and buttons.
6072
6215
  *
6216
+ * Docs: docs/pages/components/avatar.md
6217
+ *
6073
6218
  * The visual box stays the same across semantic elements. Grouping and status
6074
6219
  * dots (an empty .badge child) compose around the avatar without changing its
6075
6220
  * meaning.
@@ -6292,6 +6437,8 @@ a.avatar[aria-disabled="true"] {
6292
6437
  /*
6293
6438
  * Indicator — generic corner positioning for overlaid component content.
6294
6439
  *
6440
+ * Docs: docs/pages/components/indicator.md
6441
+ *
6295
6442
  * The container only establishes the containing block. The item owns no
6296
6443
  * paint, size, or ring so badges and other components keep their own visual
6297
6444
  * contract.
@@ -6361,6 +6508,8 @@ a.avatar[aria-disabled="true"] {
6361
6508
  /*
6362
6509
  * Key — keyboard keys and shortcut tokens for UI chrome.
6363
6510
  *
6511
+ * Docs: docs/pages/components/key.md
6512
+ *
6364
6513
  * Use .key outside prose. Plain kbd inside .prose gets the same visual
6365
6514
  * treatment from prose.css so authored content stays convenient.
6366
6515
  *
@@ -6401,6 +6550,8 @@ a.avatar[aria-disabled="true"] {
6401
6550
  /*
6402
6551
  * Message rows composed from an optional avatar, metadata, and bubble.
6403
6552
  *
6553
+ * Docs: docs/pages/components/chat.md
6554
+ *
6404
6555
  * Public hooks:
6405
6556
  * --chat-gap
6406
6557
  * --chat-max-size
@@ -6504,6 +6655,8 @@ a.avatar[aria-disabled="true"] {
6504
6655
  * Button owns the trigger's size, shape, and intent; FAB owns placement and
6505
6656
  * the speed dial's structural open/close markers (--icon-plus / --icon-close).
6506
6657
  *
6658
+ * Docs: docs/pages/components/fab.md
6659
+ *
6507
6660
  * Public hooks:
6508
6661
  * --fab-offset
6509
6662
  * --fab-gap
@@ -6631,6 +6784,8 @@ a.avatar[aria-disabled="true"] {
6631
6784
  /*
6632
6785
  * Rating — integer star rating drawn on native radio inputs.
6633
6786
  *
6787
+ * Docs: docs/pages/components/rating.md
6788
+ *
6634
6789
  * The checked radio remains the functional baseline. :has() progressively
6635
6790
  * fills preceding stars without reversing DOM or keyboard order. Forced
6636
6791
  * colors restores the platform radio presentation.
@@ -6733,6 +6888,8 @@ a.avatar[aria-disabled="true"] {
6733
6888
  /*
6734
6889
  * Steps — semantic ordered-list progression for a multi-step process.
6735
6890
  *
6891
+ * Docs: docs/pages/components/steps.md
6892
+ *
6736
6893
  * One primitive, two explicit compositions. Both classes are required:
6737
6894
  *
6738
6895
  * <ol class="steps steps-horizontal"> horizontal, 2 to 5 stages
@@ -7081,6 +7238,8 @@ a.avatar[aria-disabled="true"] {
7081
7238
  * and item states; app-layout owns whether that surface is a bottom bar or
7082
7239
  * labelled side navigation.
7083
7240
  *
7241
+ * Docs: docs/pages/components/app-navigation.md
7242
+ *
7084
7243
  * Public hooks:
7085
7244
  * --app-nav-size
7086
7245
  * --app-nav-gap
@@ -7166,6 +7325,8 @@ a.avatar[aria-disabled="true"] {
7166
7325
  /*
7167
7326
  * Navbar — top-level navigation bar.
7168
7327
  *
7328
+ * Docs: docs/pages/components/navbar.md
7329
+ *
7169
7330
  * .navbar is a horizontal bar; .navbar-brand is the brand link; .navbar-nav is
7170
7331
  * the inline link list using .nav-link items. .nav-list is the vertical nav
7171
7332
  * list shared with .drawer, fed by the shared --gap token.
@@ -7298,6 +7459,8 @@ a.avatar[aria-disabled="true"] {
7298
7459
  /*
7299
7460
  * Breadcrumb — current-location trail from the site root.
7300
7461
  *
7462
+ * Docs: docs/pages/components/breadcrumb.md
7463
+ *
7301
7464
  * Inline list separated by "/". Mark the current page with `aria-current="page"`
7302
7465
  * either on the item or on the current link.
7303
7466
  */
@@ -7347,6 +7510,8 @@ a.avatar[aria-disabled="true"] {
7347
7510
  /*
7348
7511
  * Pagination — page navigation for paginated lists.
7349
7512
  *
7513
+ * Docs: docs/pages/components/pagination.md
7514
+ *
7350
7515
  * Render .btn elements inside a .pagination list. The active page is marked
7351
7516
  * with aria-current="page"; .btn owns the active state styling.
7352
7517
  *
@@ -7391,6 +7556,8 @@ a.avatar[aria-disabled="true"] {
7391
7556
  /*
7392
7557
  * Tabs — in-place panel switcher with real tab semantics.
7393
7558
  *
7559
+ * Docs: docs/pages/enhancements/tabs.md
7560
+ *
7394
7561
  * JavaScript owns roving tabindex, aria-selected, panel visibility,
7395
7562
  * and arrow-key navigation. CSS owns the tab strip appearance and
7396
7563
  * active indicator.
@@ -7512,6 +7679,8 @@ a.avatar[aria-disabled="true"] {
7512
7679
  * Scrollspy — behavior hook for navigation that marks the active section
7513
7680
  * while the page scrolls.
7514
7681
  *
7682
+ * Docs: docs/pages/enhancements/scrollspy.md
7683
+ *
7515
7684
  * Does not add visual styles by itself. scrollspy.js watches
7516
7685
  * <nav class="scrollspy">
7517
7686
  * containing <a href="#section"> links and toggles aria-current="location" on
@@ -7541,6 +7710,8 @@ a.avatar[aria-disabled="true"] {
7541
7710
  /*
7542
7711
  * Data list — term/value pairs for the properties of a single object.
7543
7712
  *
7713
+ * Docs: docs/pages/components/data-list.md
7714
+ *
7544
7715
  * Expects alternating <dt>/<dd> children: one term, one value per property.
7545
7716
  * A plain <dl> handles richer relationships (several terms or descriptions)
7546
7717
  * and stays untouched by this component.
@@ -7571,6 +7742,8 @@ a.avatar[aria-disabled="true"] {
7571
7742
  * Content determines the number of lines; the component only owns the row
7572
7743
  * geometry, dividers, and supporting-text treatment.
7573
7744
  *
7745
+ * Docs: docs/pages/components/list.md
7746
+ *
7574
7747
  * Public hooks:
7575
7748
  * --list-item-min-size
7576
7749
  * --list-item-pad-block
@@ -7691,6 +7864,8 @@ a.list-item[aria-current]::before {
7691
7864
  /*
7692
7865
  * Table — structured tabular data.
7693
7866
  *
7867
+ * Docs: docs/pages/components/table.md
7868
+ *
7694
7869
  * .table-wrap provides the scroll container and outer chrome; .table owns row
7695
7870
  * and cell rules. Tokens are sourced from the shared scale.
7696
7871
  *
@@ -7772,6 +7947,8 @@ a.list-item[aria-current]::before {
7772
7947
  /*
7773
7948
  * Meter — native and segmented gauges for known-range values.
7774
7949
  *
7950
+ * Docs: docs/pages/components/meter.md
7951
+ *
7775
7952
  * Maps the three standard meter zones (optimum / suboptimum / even-less-good)
7776
7953
  * to the shared success / warning / danger intent colors.
7777
7954
  *
@@ -7864,6 +8041,8 @@ a.list-item[aria-current]::before {
7864
8041
  /*
7865
8042
  * Join — visually groups adjacent controls into a single unit.
7866
8043
  *
8044
+ * Docs: docs/pages/forms/joined-controls.md
8045
+ *
7867
8046
  * This is a cross-cutting control primitive, not a forms-only component:
7868
8047
  * it applies equally to inputs, selects, buttons, and button-like links.
7869
8048
  *
@@ -7997,6 +8176,8 @@ a.list-item[aria-current]::before {
7997
8176
  /*
7998
8177
  * Decorative intent-colored frame for one direct child.
7999
8178
  *
8179
+ * Docs: docs/pages/components/aura.md
8180
+ *
8000
8181
  * The effect is built from three layers with distinct roles: a frame carrying
8001
8182
  * a short accent of light, a small halo that belongs to the frame and keeps
8002
8183
  * that light attached to the edge, and a wide static halo that belongs to the
@@ -8315,7 +8496,12 @@ a.list-item[aria-current]::before {
8315
8496
  text-decoration: none;
8316
8497
  }
8317
8498
 
8318
- .link-muted:hover,
8499
+ /* :focus-visible stays in its own rule: in a mixed selector list an engine
8500
+ without it would drop the whole rule, taking the :hover styling with it. */
8501
+ .link-muted:hover {
8502
+ color: var(--text);
8503
+ }
8504
+
8319
8505
  .link-muted:focus-visible {
8320
8506
  color: var(--text);
8321
8507
  }