actual-css 0.9.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.
Files changed (84) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/components.json +2 -0
  3. package/dist/actual.css +10 -3
  4. package/dist/actual.full.css +238 -71
  5. package/dist/actual.full.js +1 -1
  6. package/dist/actual.full.min.css +1 -1
  7. package/dist/actual.min.css +1 -1
  8. package/docs/pages/foundations/tokens.md +1 -1
  9. package/docs/pages/guides/progressive-enhancement.md +12 -8
  10. package/package.json +159 -159
  11. package/src/css/components/accordion.css +2 -0
  12. package/src/css/components/actions.css +2 -0
  13. package/src/css/components/alert.css +2 -0
  14. package/src/css/components/app-nav.css +2 -0
  15. package/src/css/components/avatar.css +2 -0
  16. package/src/css/components/badge.css +2 -0
  17. package/src/css/components/breadcrumb.css +2 -0
  18. package/src/css/components/busy.css +2 -0
  19. package/src/css/components/button.css +2 -0
  20. package/src/css/components/card.css +2 -0
  21. package/src/css/components/chat.css +2 -0
  22. package/src/css/components/data-list.css +2 -0
  23. package/src/css/components/drawer.css +35 -22
  24. package/src/css/components/fab.css +2 -0
  25. package/src/css/components/flyout.css +2 -0
  26. package/src/css/components/indicator.css +2 -0
  27. package/src/css/components/join.css +2 -0
  28. package/src/css/components/key.css +2 -0
  29. package/src/css/components/list.css +2 -0
  30. package/src/css/components/meter.css +2 -0
  31. package/src/css/components/modal.css +47 -40
  32. package/src/css/components/navbar.css +2 -0
  33. package/src/css/components/pagination.css +2 -0
  34. package/src/css/components/progress.css +2 -0
  35. package/src/css/components/rating.css +2 -0
  36. package/src/css/components/scrollspy.css +2 -0
  37. package/src/css/components/skeleton.css +2 -0
  38. package/src/css/components/spinner.css +2 -0
  39. package/src/css/components/status-bar.css +2 -0
  40. package/src/css/components/steps.css +2 -0
  41. package/src/css/components/tab.css +2 -0
  42. package/src/css/components/table.css +2 -0
  43. package/src/css/components/tooltip.css +2 -0
  44. package/src/css/core/print.css +3 -0
  45. package/src/css/core/variants.css +7 -3
  46. package/src/css/effects/aura.css +2 -0
  47. package/src/css/forms/choice-card.css +4 -0
  48. package/src/css/forms/control.css +2 -0
  49. package/src/css/forms/floating-field.css +5 -1
  50. package/src/css/forms/form-actions.css +2 -0
  51. package/src/css/forms/form.css +2 -0
  52. package/src/css/forms/input-icon.css +2 -0
  53. package/src/css/forms/native.css +2 -0
  54. package/src/css/forms/otp.css +10 -0
  55. package/src/css/forms/range.css +2 -0
  56. package/src/css/forms/select.css +2 -0
  57. package/src/css/forms/switch.css +2 -0
  58. package/src/css/forms/validation.css +2 -0
  59. package/src/css/layout/app-layout.css +2 -0
  60. package/src/css/layout/app-shell.css +5 -1
  61. package/src/css/layout/center.css +2 -0
  62. package/src/css/layout/cluster.css +2 -0
  63. package/src/css/layout/column-layout.css +2 -0
  64. package/src/css/layout/frame.css +2 -0
  65. package/src/css/layout/grid.css +2 -0
  66. package/src/css/layout/media.css +2 -1
  67. package/src/css/layout/scroll-snap.css +2 -0
  68. package/src/css/layout/scroll-target.css +2 -0
  69. package/src/css/layout/sidebar-layout.css +2 -0
  70. package/src/css/layout/stack.css +2 -1
  71. package/src/css/layout/switcher.css +2 -0
  72. package/src/css/layout/topbar.css +2 -0
  73. package/src/css/typography/fluid.css +2 -0
  74. package/src/css/typography/lead.css +2 -1
  75. package/src/css/typography/overline.css +2 -0
  76. package/src/css/typography/prose.css +2 -0
  77. package/src/css/utilities/base.css +6 -1
  78. package/src/js/context-menu.js +56 -15
  79. package/src/js/enhancement-loader.js +5 -1
  80. package/src/js/escape.js +11 -1
  81. package/src/js/flyout.js +6 -1
  82. package/src/js/focus.js +8 -3
  83. package/src/js/surface.js +28 -0
  84. package/src/js/tooltip.js +10 -4
@@ -748,9 +748,9 @@ small {
748
748
  }
749
749
 
750
750
  /* 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. */
751
+ component (field, choice-card, floating-field, input-icon, join, pagination,
752
+ tabs, flyout). A local size declaration beats inherited density because it
753
+ is declared on the sized component itself. */
754
754
  :where(
755
755
  .btn,
756
756
  .input,
@@ -760,7 +760,9 @@ small {
760
760
  .file,
761
761
  .field,
762
762
  .choice,
763
+ .choice-card,
763
764
  .input-icon,
765
+ .floating-field,
764
766
  .otp,
765
767
  .join,
766
768
  .pagination,
@@ -782,7 +784,9 @@ small {
782
784
  .file,
783
785
  .field,
784
786
  .choice,
787
+ .choice-card,
785
788
  .input-icon,
789
+ .floating-field,
786
790
  .otp,
787
791
  .join,
788
792
  .pagination,
@@ -842,6 +846,9 @@ small {
842
846
 
843
847
  @media print {
844
848
  :root,
849
+ /* The doubled attribute intentionally outranks single-attribute preset
850
+ islands (data-theme="dark", paired themes) loaded after the core. */
851
+ [data-theme][data-theme],
845
852
  body {
846
853
  /* Save some ink in dark mode */
847
854
  color-scheme: light;
@@ -863,6 +870,8 @@ small {
863
870
  /*
864
871
  * Prose is the opt-in rich text scope for authored content.
865
872
  *
873
+ * Docs: docs/pages/foundations/typography.md
874
+ *
866
875
  * Global element styles stay mild so app UI is not shaped accidentally. Page
867
876
  * layout, forms, cards, navigation, and app tables remain outside this scope.
868
877
  *
@@ -1079,7 +1088,8 @@ small {
1079
1088
 
1080
1089
  /* Lead paragraph: a slightly larger, more relaxed reading-introduction text.
1081
1090
  Non-fluid and always available — use .text-lead from the optional fluid
1082
- module for marketing/display surfaces. */
1091
+ module for marketing/display surfaces.
1092
+ Docs: docs/pages/foundations/typography.md */
1083
1093
  .lead {
1084
1094
  font-size: 1.125em;
1085
1095
  line-height: var(--line-height-relaxed);
@@ -1088,6 +1098,8 @@ small {
1088
1098
  /*
1089
1099
  * Overline — editorial small-cap label for section openers.
1090
1100
  *
1101
+ * Docs: docs/pages/foundations/typography.md
1102
+ *
1091
1103
  * Base form is unstyled chrome that takes its surrounding color. Add .pill for
1092
1104
  * the bordered rounded shape, and combine with an intent class (.primary,
1093
1105
  * .success, etc.) to tint the text, border, and background.
@@ -1137,6 +1149,8 @@ small {
1137
1149
  /*
1138
1150
  * Optional fluid typography module.
1139
1151
  *
1152
+ * Docs: docs/pages/foundations/typography.md
1153
+ *
1140
1154
  * Part of the typography family (typography/index.css) and the full bundle; it
1141
1155
  * is not in the minimal core, so a core-only import adds it explicitly when
1142
1156
  * display, title, and lead sizes must scale with the viewport. Typical use
@@ -1267,7 +1281,8 @@ small {
1267
1281
  region re-spaces every nested stack, cluster and grid at any depth. Setting
1268
1282
  the token stays correct on the element that consumes it, and
1269
1283
  .compact/.spacious set it deliberately so a density context reaches nested layouts. See
1270
- docs/pages/layout/stack.md. */
1284
+ docs/pages/layout/stack.md.
1285
+ Docs: docs/pages/layout/stack.md */
1271
1286
  .stack {
1272
1287
  display: flex;
1273
1288
  flex-direction: column;
@@ -1281,6 +1296,8 @@ small {
1281
1296
  /*
1282
1297
  * Inline group that wraps when space runs out.
1283
1298
  *
1299
+ * Docs: docs/pages/layout/cluster.md
1300
+ *
1284
1301
  * Public hooks:
1285
1302
  * --cluster-align
1286
1303
  * --cluster-justify
@@ -1304,6 +1321,8 @@ small {
1304
1321
  /*
1305
1322
  * Centered content column with a maximum measure.
1306
1323
  *
1324
+ * Docs: docs/pages/layout/center.md
1325
+ *
1307
1326
  * Public hooks:
1308
1327
  * --center-size
1309
1328
  * --center-pad
@@ -1319,6 +1338,8 @@ small {
1319
1338
  /*
1320
1339
  * Media frame with a fixed ratio.
1321
1340
  *
1341
+ * Docs: docs/pages/layout/frame.md
1342
+ *
1322
1343
  * Public hooks:
1323
1344
  * --frame-ratio
1324
1345
  *
@@ -1336,7 +1357,11 @@ small {
1336
1357
  object-fit: cover;
1337
1358
  }
1338
1359
 
1339
- /* Full-height single-column document shell. */
1360
+ /*
1361
+ * Full-height single-column document shell.
1362
+ *
1363
+ * Docs: docs/pages/layout/app-shell.md
1364
+ */
1340
1365
  .app-shell {
1341
1366
  min-block-size: var(--viewport-block);
1342
1367
  display: flex;
@@ -1350,6 +1375,8 @@ small {
1350
1375
  * chrome available without fixed offsets. This is not a generic page wrapper
1351
1376
  * or a public-site layout; all three direct regions are part of the contract.
1352
1377
  *
1378
+ * Docs: docs/pages/layout/app-layout.md
1379
+ *
1353
1380
  * Public hook:
1354
1381
  * --app-nav-side-size
1355
1382
  */
@@ -1449,7 +1476,8 @@ small {
1449
1476
  /* Media object: fixed-width leading element (avatar, icon, image) + flexible
1450
1477
  trailing content. Replaces the repeated "auto minmax(0, 1fr)" pattern in
1451
1478
  author cards, comments, meta rows, and similar compositions.
1452
- Child contract: exactly two direct children (leading media + content). */
1479
+ Child contract: exactly two direct children (leading media + content).
1480
+ Docs: docs/pages/layout/media.md */
1453
1481
  .media {
1454
1482
  display: grid;
1455
1483
  grid-template-columns: auto minmax(0, 1fr);
@@ -1460,6 +1488,8 @@ small {
1460
1488
  /*
1461
1489
  * Peer regions that switch as a group.
1462
1490
  *
1491
+ * Docs: docs/pages/layout/switcher.md
1492
+ *
1463
1493
  * All children sit side by side when they all fit; otherwise every child
1464
1494
  * stacks. Unlike .grid, this recipe cannot create a partial row such as two
1465
1495
  * items plus one orphan.
@@ -1485,6 +1515,8 @@ small {
1485
1515
  /*
1486
1516
  * Main content + secondary region.
1487
1517
  *
1518
+ * Docs: docs/pages/layout/sidebar-layout.md
1519
+ *
1488
1520
  * The children wrap according to the space allocated to this layout, without
1489
1521
  * an ancestor opt-in or viewport threshold. The large growth ratio lets the
1490
1522
  * main region reclaim the full row when the aside wraps instead of remaining
@@ -1539,6 +1571,8 @@ small {
1539
1571
  /*
1540
1572
  * Responsive equal-width grids.
1541
1573
  *
1574
+ * Docs: docs/pages/layout/grid.md
1575
+ *
1542
1576
  * .grid is space-driven: it fits as many tracks as --grid-min allows.
1543
1577
  * .grid-N is count-driven: it declares a structural density of N and collapses
1544
1578
  * through natural divisors of N.
@@ -1669,6 +1703,8 @@ small {
1669
1703
  /*
1670
1704
  * Explicit 12-column composition canvas.
1671
1705
  *
1706
+ * Docs: docs/pages/layout/column-layout.md
1707
+ *
1672
1708
  * Neither space-driven like .grid nor count-driven like .grid-N: this is a
1673
1709
  * coordinate system. The author places regions with .column-span-N /
1674
1710
  * .column-start-N and owns narrow-container recomposition. There is
@@ -1856,6 +1892,8 @@ small {
1856
1892
  * Scroll snap — optional horizontal rails for niche card strips and simple
1857
1893
  * touch-friendly overflow areas.
1858
1894
  *
1895
+ * Docs: docs/pages/layout/scroll-snap.md
1896
+ *
1859
1897
  * This is not a full carousel. For previous/next controls, pagination,
1860
1898
  * active state, drag behavior, looping, or robust desktop mouse interaction,
1861
1899
  * use a dedicated carousel library.
@@ -1920,6 +1958,8 @@ small {
1920
1958
  * header (.topbar) or underlaps it; scroll-margin reserves space on the
1921
1959
  * target itself so the browser stops early.
1922
1960
  *
1961
+ * Docs: docs/pages/layout/scroll-target.md
1962
+ *
1923
1963
  * Apply to the elements that are link targets: <section id="...">, headings,
1924
1964
  * etc. The scrollspy enhancer only reads ids — this offset is what actually
1925
1965
  * shapes where the page lands, and the two are independent.
@@ -1937,6 +1977,8 @@ small {
1937
1977
  * background only — row content (search, breadcrumb, actions) stays local
1938
1978
  * to the page.
1939
1979
  *
1980
+ * Docs: docs/pages/layout/topbar.md
1981
+ *
1940
1982
  * Not for marketing/editorial site headers — those are one-off visual
1941
1983
  * identity per page; see docs/pages/patterns/overview.md's Header Navigation
1942
1984
  * section. This is for the structural app-shell topbar, which recurs unchanged
@@ -1990,6 +2032,8 @@ small {
1990
2032
  /*
1991
2033
  * Form layout and supporting text.
1992
2034
  *
2035
+ * Docs: docs/pages/forms/overview.md
2036
+ *
1993
2037
  * Individual controls keep their own files; .choice label layout lives in
1994
2038
  * choice.css; .form-actions lives in form-actions.css.
1995
2039
  *
@@ -2052,6 +2096,8 @@ small {
2052
2096
  /*
2053
2097
  * Text controls — input, textarea, select.
2054
2098
  *
2099
+ * Docs: docs/pages/forms/overview.md
2100
+ *
2055
2101
  * Neutral geometry and interaction states shared across all text-like
2056
2102
  * controls. select shares this base recipe; its chevron/appearance layer
2057
2103
  * lives in select.css. checkbox, radio, range, switch, and file have their
@@ -2405,6 +2451,8 @@ small {
2405
2451
  * Select — chevron and appearance layer on top of the shared text-control
2406
2452
  * recipe.
2407
2453
  *
2454
+ * Docs: docs/pages/forms/overview.md
2455
+ *
2408
2456
  * Base geometry and interaction states come from control.css (select shares
2409
2457
  * the --control-* tokens). This file only removes the native arrow and draws
2410
2458
  * the CSS chevron, so the picker (appearance: base-select) enhancement in
@@ -2463,6 +2511,8 @@ small {
2463
2511
  * custom styling, so the framework stays out of the way and just keeps them
2464
2512
  * readable and theme-aware.
2465
2513
  *
2514
+ * Docs: docs/pages/forms/controls.md
2515
+ *
2466
2516
  * Range has its own custom-styled file (range.css) so the focus ring can
2467
2517
  * target the thumb pseudo-element rather than the rectangular CSS box.
2468
2518
  */
@@ -2563,6 +2613,8 @@ small {
2563
2613
  /*
2564
2614
  * Form validation states.
2565
2615
  *
2616
+ * Docs: docs/pages/forms/validation.md
2617
+ *
2566
2618
  * Imported by default through forms/index.css. Custom builds may omit this
2567
2619
  * file when an app wants to own validation styling completely.
2568
2620
  *
@@ -2654,6 +2706,8 @@ small {
2654
2706
  /*
2655
2707
  * Form actions — bottom form bar with sticky variant.
2656
2708
  *
2709
+ * Docs: docs/pages/forms/form-actions.md
2710
+ *
2657
2711
  * A flex cluster of buttons/actions that closes a form. The sticky variant
2658
2712
  * docks to the viewport bottom with a translucent blur where supported; at
2659
2713
  * rest it stays visually neutral so it does not double as a separator.
@@ -2710,6 +2764,8 @@ small {
2710
2764
  /*
2711
2765
  * Input Icon — decorative icon inside a relative wrapper, leading or trailing.
2712
2766
  *
2767
+ * Docs: docs/pages/patterns/icon-input.md
2768
+ *
2713
2769
  * Icon-library agnostic: detects position by DOM order. First child that is
2714
2770
  * not .input = leading icon; icon after .input = trailing. The icon follows
2715
2771
  * local typography; density contexts shrink the control geometry around it
@@ -2771,6 +2827,8 @@ small {
2771
2827
  /*
2772
2828
  * Switch controls for checkbox inputs with switch semantics.
2773
2829
  *
2830
+ * Docs: docs/pages/forms/switch.md
2831
+ *
2774
2832
  * The switch remains an input control, not a decorative toggle. Checked, hover,
2775
2833
  * focus-visible, and disabled states stay clear.
2776
2834
  *
@@ -2923,6 +2981,8 @@ small {
2923
2981
  * Range slider — custom-styled so the focus ring always targets the
2924
2982
  * user-manipulated surface (the thumb) rather than the CSS box.
2925
2983
  *
2984
+ * Docs: docs/pages/forms/range.md
2985
+ *
2926
2986
  * appearance: none gives us full control over track and thumb; without it,
2927
2987
  * pseudo-element rules for ::-webkit-slider-thumb and ::-moz-range-thumb
2928
2988
  * are not reliably applied across browsers.
@@ -3038,6 +3098,8 @@ small {
3038
3098
  /*
3039
3099
  * Choice cards — card-style radio and checkbox labels.
3040
3100
  *
3101
+ * Docs: docs/pages/forms/choice-cards.md
3102
+ *
3041
3103
  * Each .choice-card is a <label> wrapping a native radio or checkbox. When
3042
3104
  * :has() is supported the input is visually hidden and the card mirrors its
3043
3105
  * state: checked border and tint, checkmark badge (round for radio, rounded
@@ -3074,6 +3136,8 @@ small {
3074
3136
  border-radius: var(--choice-card-radius);
3075
3137
  background: var(--choice-card-bg);
3076
3138
  color: var(--text);
3139
+ /* Consumed so .sm/.lg reach the card; the badge's em sizing follows. */
3140
+ font-size: var(--control-font-size);
3077
3141
  cursor: pointer;
3078
3142
  transition:
3079
3143
  border-color var(--duration),
@@ -3282,6 +3346,8 @@ small {
3282
3346
  * Floating labels — optional composition over the text controls
3283
3347
  * (.input, .textarea, .select) and .field-label.
3284
3348
  *
3349
+ * Docs: docs/pages/forms/floating-field.md
3350
+ *
3285
3351
  * The cell owns a control plus its label; the label overlays the control and
3286
3352
  * floats to the top when the control is focused or non-empty. Help and error
3287
3353
  * messages stay in the surrounding .field, not inside the cell, so the
@@ -3323,7 +3389,9 @@ small {
3323
3389
 
3324
3390
  .floating-field > .field-label {
3325
3391
  position: absolute;
3326
- inset-inline-start: calc(var(--space-30) + var(--border-width));
3392
+ /* Same start edge as the control's text: --control-pad-x owns that shared
3393
+ geometry, so .sm/.lg keep label and value aligned. */
3394
+ inset-inline-start: calc(var(--control-pad-x) + var(--border-width));
3327
3395
  color: var(--text-muted);
3328
3396
  font-size: var(--control-font-size);
3329
3397
  font-weight: var(--font-weight-normal);
@@ -3389,6 +3457,8 @@ small {
3389
3457
  /*
3390
3458
  * One-time code input with visual cells and one native input.
3391
3459
  *
3460
+ * Docs: docs/pages/forms/otp.md
3461
+ *
3392
3462
  * Add one direct span per accepted character. The cells determine the
3393
3463
  * component width; the overlaid input remains the only interactive control.
3394
3464
  *
@@ -3517,12 +3587,22 @@ small {
3517
3587
  .needs-validation.was-validated .otp > input:invalid ~ span {
3518
3588
  --otp-cell-border: Mark;
3519
3589
  }
3590
+
3591
+ /* biome-ignore lint/style/noDescendingSpecificity: the :user-invalid route
3592
+ mirrors the non-forced-colors block above and must come last — the three
3593
+ invalid markers write the same value, so source order is inert. An engine
3594
+ without the pseudo drops this rule alone. */
3595
+ .needs-validation .otp > input:user-invalid ~ span {
3596
+ --otp-cell-border: Mark;
3597
+ }
3520
3598
  }
3521
3599
 
3522
3600
  /* Controls */
3523
3601
  /*
3524
3602
  * Buttons for actions and button-like links.
3525
3603
  *
3604
+ * Docs: docs/pages/components/button.md
3605
+ *
3526
3606
  * .btn is primarily for actionable elements: <button>, <a href>,
3527
3607
  * input[type="button"|"submit"|"reset"].
3528
3608
  *
@@ -3769,6 +3849,8 @@ a.btn[aria-disabled="true"] {
3769
3849
  /*
3770
3850
  * Badges for compact labels, counts, and state hints.
3771
3851
  *
3852
+ * Docs: docs/pages/components/badge.md
3853
+ *
3772
3854
  * Soft by default. .solid for counters and notification badges. The dot
3773
3855
  * (.badge:empty) stays solid regardless — a soft dot is pointless.
3774
3856
  * Shared .outline variant stays available.
@@ -4029,6 +4111,8 @@ a.badge[aria-disabled="true"] {
4029
4111
  /*
4030
4112
  * Alerts for inline feedback and status messages.
4031
4113
  *
4114
+ * Docs: docs/pages/components/alert.md
4115
+ *
4032
4116
  * Keeps alert defaults local while consuming the shared --ui-* variant
4033
4117
  * plumbing for emphasis.
4034
4118
  *
@@ -4315,6 +4399,8 @@ a.badge[aria-disabled="true"] {
4315
4399
  /*
4316
4400
  * Actions — a horizontal action-button cluster.
4317
4401
  *
4402
+ * Docs: docs/pages/patterns/actions.md
4403
+ *
4318
4404
  * Composes with .btn (or any inline element) and feeds layout via the shared
4319
4405
  * --gap token.
4320
4406
  *
@@ -4343,6 +4429,8 @@ a.badge[aria-disabled="true"] {
4343
4429
  /*
4344
4430
  * Cards provide a neutral raised surface for grouped content.
4345
4431
  *
4432
+ * Docs: docs/pages/components/card.md
4433
+ *
4346
4434
  * Surface, border, radius, padding, density, and elevation stay local here.
4347
4435
  * Direct header/footer children are structural slots; a direct footer is
4348
4436
  * anchored to the card's bottom edge when extra block space is available (so
@@ -4504,6 +4592,8 @@ a.badge[aria-disabled="true"] {
4504
4592
  /*
4505
4593
  * Modal shell styling for native dialog elements.
4506
4594
  *
4595
+ * Docs: docs/pages/components/dialog.md
4596
+ *
4507
4597
  * Spacing, radius, transitions, and focus treatment come from shared tokens.
4508
4598
  * Opening behavior and intent-specific decisions stay outside this component.
4509
4599
  *
@@ -4515,16 +4605,15 @@ a.badge[aria-disabled="true"] {
4515
4605
  *
4516
4606
  * Internal:
4517
4607
  * --dialog-close-icon-size
4608
+ * --dialog-focus-bleed
4518
4609
  * --dialog-viewport-block
4519
4610
  * --dialog-viewport-gap-default
4520
4611
  * --dialog-viewport-gap-2x
4521
4612
  */
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.
4613
+ :where(dialog).modal {
4614
+ /* --dialog-viewport-gap stays fallback-only so the responsive default can
4615
+ live in an internal token: a media query moves it without closing the
4616
+ public hook.
4528
4617
 
4529
4618
  calc() multiplication is not in the supported browser range, so the
4530
4619
  doubled gap is spelled out with an additive calc. */
@@ -4538,10 +4627,8 @@ dialog.modal {
4538
4627
  provides the containing block for the absolutely positioned close button. */
4539
4628
  position: fixed;
4540
4629
 
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. */
4630
+ /* The root is :where(dialog).modal (0-1-0), so a single author class can
4631
+ outrank these defaults — no fallback-only indirection needed here. */
4545
4632
  max-inline-size: min(calc(100% - var(--dialog-viewport-gap-2x)), var(--modal-size, 32rem));
4546
4633
 
4547
4634
  /* Stable viewport cap for the panel: dvh tracks the dynamic browser toolbar
@@ -4571,8 +4658,8 @@ dialog.modal {
4571
4658
  transform var(--duration) var(--ease-exit);
4572
4659
  }
4573
4660
 
4574
- dialog.modal > form,
4575
- dialog.modal > .stack {
4661
+ :where(dialog).modal > form,
4662
+ :where(dialog).modal > .stack {
4576
4663
  display: grid;
4577
4664
  gap: var(--space-40);
4578
4665
  max-block-size: calc(var(--dialog-viewport-block) - var(--dialog-viewport-gap-2x));
@@ -4582,7 +4669,7 @@ dialog.modal > .stack {
4582
4669
  }
4583
4670
 
4584
4671
  @supports (block-size: 100svh) {
4585
- dialog.modal {
4672
+ :where(dialog).modal {
4586
4673
  --dialog-viewport-block: 100svh;
4587
4674
  }
4588
4675
  }
@@ -4590,42 +4677,50 @@ dialog.modal > .stack {
4590
4677
  /* Keep the dialog off the viewport edges on small screens. Moves the internal
4591
4678
  default, not the public hook, so an author override still wins here too. */
4592
4679
  @media (max-width: 30rem) {
4593
- dialog.modal {
4680
+ :where(dialog).modal {
4594
4681
  --dialog-viewport-gap-default: var(--space-30);
4595
4682
  }
4596
4683
  }
4597
4684
 
4598
- dialog.modal.scrollable > form,
4599
- dialog.modal.scrollable > .stack {
4685
+ :where(dialog).modal.scrollable > form,
4686
+ :where(dialog).modal.scrollable > .stack {
4600
4687
  grid-template-rows: auto minmax(0, 1fr) auto;
4601
4688
  overflow: hidden;
4602
4689
  }
4603
4690
 
4604
- dialog.modal.scrollable > form > :not(:is(header, footer)),
4605
- dialog.modal.scrollable > .stack > :not(:is(header, footer)) {
4691
+ :where(dialog).modal.scrollable > form > :not(:is(header, footer)),
4692
+ :where(dialog).modal.scrollable > .stack > :not(:is(header, footer)) {
4693
+ --dialog-focus-bleed: calc(var(--focus-outline-offset) + var(--border-width) * 2);
4694
+
4606
4695
  min-block-size: 0;
4696
+ /* Keep focus paint inside the clip without moving the body off the fixed
4697
+ header and footer edges. */
4698
+ margin-inline: calc(var(--dialog-focus-bleed) * -1);
4699
+ padding-inline: var(--dialog-focus-bleed);
4607
4700
  overflow: auto;
4608
4701
  overscroll-behavior: contain;
4609
4702
  }
4610
4703
 
4611
- dialog.modal header > :first-child,
4612
- dialog.modal footer > :first-child {
4704
+ /* Anatomy is bounded to the structural slots (direct children of the
4705
+ form/.stack body): a header or footer deeper in the content keeps its own
4706
+ rhythm and is not restyled as the modal's chrome. */
4707
+ :where(dialog).modal > :where(form, .stack) > :is(header, footer) > :first-child {
4613
4708
  margin-block-start: 0;
4614
4709
  }
4615
4710
 
4616
- dialog.modal header > :last-child,
4617
- dialog.modal footer > :last-child {
4711
+ :where(dialog).modal > :where(form, .stack) > :is(header, footer) > :last-child {
4618
4712
  margin-block-end: 0;
4619
4713
  }
4620
4714
 
4621
4715
  /* 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 {
4716
+ content flow; headers reserve inline room so titles never run under it.
4717
+ The form/.stack anatomy is structural, so it stays in :where() and never
4718
+ lends .stack's specificity to a bare <form>. */
4719
+ :where(dialog).modal > :where(form, .stack) > header {
4625
4720
  padding-inline-end: calc(var(--control-size) + var(--space-30));
4626
4721
  }
4627
4722
 
4628
- dialog.modal footer {
4723
+ :where(dialog).modal > :where(form, .stack) > footer {
4629
4724
  display: flex;
4630
4725
  flex-wrap: wrap;
4631
4726
  justify-content: flex-end;
@@ -4635,17 +4730,17 @@ dialog.modal footer {
4635
4730
  /* Confirmation layout — a focused message followed by a visually distinct
4636
4731
  action band. The media primitive owns icon/content alignment; this variant
4637
4732
  only removes the standard form inset so both regions can own their padding. */
4638
- dialog.modal.dialog-confirmation > form {
4733
+ :where(dialog).modal.dialog-confirmation > form {
4639
4734
  gap: 0;
4640
4735
  padding: 0;
4641
4736
  }
4642
4737
 
4643
- dialog.modal.dialog-confirmation > form > .media {
4738
+ :where(dialog).modal.dialog-confirmation > form > .media {
4644
4739
  gap: var(--space-40);
4645
4740
  padding: var(--space-50);
4646
4741
  }
4647
4742
 
4648
- dialog.modal.dialog-confirmation > form > footer {
4743
+ :where(dialog).modal.dialog-confirmation > form > footer {
4649
4744
  padding: var(--space-40) var(--space-50);
4650
4745
  border-end-start-radius: calc(var(--radius-lg) - var(--border-width));
4651
4746
  border-end-end-radius: calc(var(--radius-lg) - var(--border-width));
@@ -4686,13 +4781,13 @@ dialog.modal.dialog-confirmation > form > footer {
4686
4781
  }
4687
4782
 
4688
4783
  @media (max-width: 30rem) {
4689
- dialog.modal.dialog-confirmation > form > .media,
4690
- dialog.modal.dialog-confirmation > form > footer {
4784
+ :where(dialog).modal.dialog-confirmation > form > .media,
4785
+ :where(dialog).modal.dialog-confirmation > form > footer {
4691
4786
  padding: var(--space-40);
4692
4787
  }
4693
4788
  }
4694
4789
 
4695
- dialog.modal[open] {
4790
+ :where(dialog).modal[open] {
4696
4791
  opacity: 1;
4697
4792
  transform: none;
4698
4793
  transition:
@@ -4701,13 +4796,13 @@ dialog.modal[open] {
4701
4796
  }
4702
4797
 
4703
4798
  @starting-style {
4704
- dialog.modal[open] {
4799
+ :where(dialog).modal[open] {
4705
4800
  opacity: 0;
4706
4801
  transform: translateY(0.35rem);
4707
4802
  }
4708
4803
  }
4709
4804
 
4710
- dialog.modal::backdrop {
4805
+ :where(dialog).modal::backdrop {
4711
4806
  background: var(--backdrop-fill);
4712
4807
  opacity: 0;
4713
4808
  /* Scrim curve stays neutral `ease`; only the panel carries the character
@@ -4716,7 +4811,7 @@ dialog.modal::backdrop {
4716
4811
  }
4717
4812
 
4718
4813
  @supports (transition-behavior: allow-discrete) {
4719
- dialog.modal {
4814
+ :where(dialog).modal {
4720
4815
  transition:
4721
4816
  opacity var(--duration) var(--ease-exit),
4722
4817
  transform var(--duration) var(--ease-exit),
@@ -4724,7 +4819,7 @@ dialog.modal::backdrop {
4724
4819
  display var(--duration) allow-discrete;
4725
4820
  }
4726
4821
 
4727
- dialog.modal[open] {
4822
+ :where(dialog).modal[open] {
4728
4823
  transition:
4729
4824
  opacity var(--duration) var(--ease-enter),
4730
4825
  transform var(--duration) var(--ease-enter),
@@ -4732,7 +4827,7 @@ dialog.modal::backdrop {
4732
4827
  display var(--duration) allow-discrete;
4733
4828
  }
4734
4829
 
4735
- dialog.modal::backdrop {
4830
+ :where(dialog).modal::backdrop {
4736
4831
  transition:
4737
4832
  opacity var(--duration) ease,
4738
4833
  overlay var(--duration) allow-discrete,
@@ -4740,7 +4835,7 @@ dialog.modal::backdrop {
4740
4835
  }
4741
4836
  }
4742
4837
 
4743
- dialog.modal[open]::backdrop {
4838
+ :where(dialog).modal[open]::backdrop {
4744
4839
  opacity: 1;
4745
4840
  }
4746
4841
 
@@ -4753,7 +4848,7 @@ html.has-modal-open.had-scrollbar {
4753
4848
  }
4754
4849
 
4755
4850
  @starting-style {
4756
- dialog.modal[open]::backdrop {
4851
+ :where(dialog).modal[open]::backdrop {
4757
4852
  opacity: 0;
4758
4853
  }
4759
4854
  }
@@ -4761,7 +4856,7 @@ html.has-modal-open.had-scrollbar {
4761
4856
  @media (prefers-reduced-motion: no-preference) {
4762
4857
  /* Wrong-close feedback and view-transition transport are motion quirks, not
4763
4858
  presence enter/exit, so they stay on the plain CSS default easing. */
4764
- dialog.modal.is-static {
4859
+ :where(dialog).modal.is-static {
4765
4860
  animation: actual-dialog-static var(--duration-slow) ease;
4766
4861
  }
4767
4862
 
@@ -4842,6 +4937,12 @@ html.has-modal-open.had-scrollbar {
4842
4937
  /*
4843
4938
  * Drawer — modal side-sheet built on the native dialog element.
4844
4939
  *
4940
+ * Docs: docs/pages/components/drawer.md
4941
+ *
4942
+ * Open with a native command button:
4943
+ * command="show-modal" commandfor="<drawer-id>".
4944
+ * For stacked navigation, compose with .nav-list from navbar.css.
4945
+ *
4845
4946
  * Top-layer rendering, ::backdrop, Escape, and modal focus come from the
4846
4947
  * native dialog. CSS owns progressive enter/exit motion where browsers support
4847
4948
  * discrete display/overlay transitions. The drawer is a layout shell only —
@@ -4854,10 +4955,11 @@ html.has-modal-open.had-scrollbar {
4854
4955
  *
4855
4956
  * Internal:
4856
4957
  * --drawer-closed-translate (side/RTL positioning state)
4958
+ * --drawer-focus-bleed
4857
4959
  * --drawer-motion-duration (shared by panel and ::backdrop so exit duration
4858
4960
  * changes in one place; entry state redefines it longer)
4859
4961
  */
4860
- dialog.drawer {
4962
+ :where(dialog).drawer {
4861
4963
  --drawer-size: 20rem;
4862
4964
  --drawer-pad: var(--space-40);
4863
4965
  --drawer-closed-translate: -100%;
@@ -4900,7 +5002,7 @@ dialog.drawer {
4900
5002
  transform var(--drawer-motion-duration) var(--ease-exit);
4901
5003
  }
4902
5004
 
4903
- dialog.drawer[open] {
5005
+ :where(dialog).drawer[open] {
4904
5006
  --drawer-motion-duration: var(--duration-slow);
4905
5007
  display: grid;
4906
5008
  grid-template-rows: auto minmax(0, 1fr) auto;
@@ -4915,7 +5017,7 @@ dialog.drawer[open] {
4915
5017
  /* A form is the drawer's content wrapper when actions need method="dialog".
4916
5018
  Span the drawer tracks so the form can keep the header at the top, let the
4917
5019
  body absorb the available height, and keep the footer at the bottom. */
4918
- dialog.drawer > form {
5020
+ :where(dialog).drawer > form {
4919
5021
  display: grid;
4920
5022
  grid-row: 1 / -1;
4921
5023
  grid-template-rows: auto minmax(0, 1fr) auto;
@@ -4924,7 +5026,7 @@ dialog.drawer > form {
4924
5026
  }
4925
5027
 
4926
5028
  /* The shared backdrop token keeps drawer and modal scrims consistent. */
4927
- dialog.drawer::backdrop {
5029
+ :where(dialog).drawer::backdrop {
4928
5030
  background: var(--backdrop-fill);
4929
5031
  opacity: 0;
4930
5032
  /* Scrim curve stays neutral `ease`; only the panel's character curves
@@ -4932,7 +5034,7 @@ dialog.drawer::backdrop {
4932
5034
  transition: opacity var(--drawer-motion-duration) ease;
4933
5035
  }
4934
5036
 
4935
- dialog.drawer[data-side="end"] {
5037
+ :where(dialog).drawer[data-side="end"] {
4936
5038
  --drawer-closed-translate: 100%;
4937
5039
 
4938
5040
  margin-inline-start: auto;
@@ -4941,17 +5043,17 @@ dialog.drawer[data-side="end"] {
4941
5043
  border-inline-start: var(--border-width) solid var(--border);
4942
5044
  }
4943
5045
 
4944
- dialog.drawer[open]::backdrop {
5046
+ :where(dialog).drawer[open]::backdrop {
4945
5047
  opacity: 1;
4946
5048
  }
4947
5049
 
4948
5050
  @starting-style {
4949
- dialog.drawer[open] {
5051
+ :where(dialog).drawer[open] {
4950
5052
  opacity: 0;
4951
5053
  transform: translateX(var(--drawer-closed-translate));
4952
5054
  }
4953
5055
 
4954
- dialog.drawer[open]::backdrop {
5056
+ :where(dialog).drawer[open]::backdrop {
4955
5057
  opacity: 0;
4956
5058
  }
4957
5059
  }
@@ -4961,15 +5063,21 @@ dialog.drawer[open]::backdrop {
4961
5063
  modal, which sizes to its content and opts in with .scrollable. Header and
4962
5064
  footer keep their own tracks; .drawer-close is named because a generic
4963
5065
  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)) {
5066
+ :where(dialog).drawer > :not(:is(header, footer, form, .drawer-close)),
5067
+ :where(dialog).drawer > form > :not(:is(header, footer, .drawer-close)) {
5068
+ --drawer-focus-bleed: calc(var(--focus-outline-offset) + var(--border-width) * 2);
5069
+
4966
5070
  min-block-size: 0;
5071
+ /* Keep focus paint inside the clip without moving the body off the fixed
5072
+ header and footer edges. */
5073
+ margin-inline: calc(var(--drawer-focus-bleed) * -1);
5074
+ padding-inline: var(--drawer-focus-bleed);
4967
5075
  overflow: auto;
4968
5076
  overscroll-behavior: contain;
4969
5077
  }
4970
5078
 
4971
5079
  @supports (transition-behavior: allow-discrete) {
4972
- dialog.drawer {
5080
+ :where(dialog).drawer {
4973
5081
  transition:
4974
5082
  opacity var(--drawer-motion-duration) var(--ease-exit),
4975
5083
  transform var(--drawer-motion-duration) var(--ease-exit),
@@ -4977,7 +5085,7 @@ dialog.drawer > form > :not(:is(header, footer, .drawer-close)) {
4977
5085
  display var(--drawer-motion-duration) allow-discrete;
4978
5086
  }
4979
5087
 
4980
- dialog.drawer[open] {
5088
+ :where(dialog).drawer[open] {
4981
5089
  transition:
4982
5090
  opacity var(--drawer-motion-duration) var(--ease-enter),
4983
5091
  transform var(--drawer-motion-duration) var(--ease-enter),
@@ -4985,7 +5093,7 @@ dialog.drawer > form > :not(:is(header, footer, .drawer-close)) {
4985
5093
  display var(--drawer-motion-duration) allow-discrete;
4986
5094
  }
4987
5095
 
4988
- dialog.drawer::backdrop {
5096
+ :where(dialog).drawer::backdrop {
4989
5097
  transition:
4990
5098
  opacity var(--drawer-motion-duration) ease,
4991
5099
  overlay var(--drawer-motion-duration) allow-discrete,
@@ -4999,40 +5107,40 @@ dialog.drawer > form > :not(:is(header, footer, .drawer-close)) {
4999
5107
  directionality and handles nested direction changes; the explicit ltr rules
5000
5108
  neutralize an outer RTL fallback selector. Each specificity group keeps the
5001
5109
  fallback before the :dir() rules. */
5002
- [dir="rtl"] dialog.drawer {
5110
+ [dir="rtl"] :where(dialog).drawer {
5003
5111
  --drawer-closed-translate: 100%;
5004
5112
  }
5005
5113
 
5006
- dialog.drawer:dir(ltr) {
5114
+ :where(dialog).drawer:dir(ltr) {
5007
5115
  --drawer-closed-translate: -100%;
5008
5116
  }
5009
5117
 
5010
- dialog.drawer:dir(rtl) {
5118
+ :where(dialog).drawer:dir(rtl) {
5011
5119
  --drawer-closed-translate: 100%;
5012
5120
  }
5013
5121
 
5014
- [dir="rtl"] dialog.drawer[data-side="end"] {
5122
+ [dir="rtl"] :where(dialog).drawer[data-side="end"] {
5015
5123
  --drawer-closed-translate: -100%;
5016
5124
  }
5017
5125
 
5018
- dialog.drawer[data-side="end"]:dir(ltr) {
5126
+ :where(dialog).drawer[data-side="end"]:dir(ltr) {
5019
5127
  --drawer-closed-translate: 100%;
5020
5128
  }
5021
5129
 
5022
- dialog.drawer[data-side="end"]:dir(rtl) {
5130
+ :where(dialog).drawer[data-side="end"]:dir(rtl) {
5023
5131
  --drawer-closed-translate: -100%;
5024
5132
  }
5025
5133
 
5026
5134
  @media (prefers-reduced-motion: no-preference) {
5027
- dialog.drawer.is-static {
5135
+ :where(dialog).drawer.is-static {
5028
5136
  animation: actual-dialog-static var(--duration-slow) ease;
5029
5137
  }
5030
5138
  }
5031
5139
 
5032
5140
  /* The header reserves inline room so the title never runs under the close
5033
5141
  control. */
5034
- dialog.drawer > header,
5035
- dialog.drawer > form > header {
5142
+ :where(dialog).drawer > header,
5143
+ :where(dialog).drawer > form > header {
5036
5144
  padding-inline-end: calc(var(--control-size) + var(--space-30));
5037
5145
  }
5038
5146
 
@@ -5091,6 +5199,8 @@ dialog.drawer > form > header {
5091
5199
  /*
5092
5200
  * Flyout — floating panel for actions and links.
5093
5201
  *
5202
+ * Docs: docs/pages/enhancements/flyout.md
5203
+ *
5094
5204
  * JavaScript promotes the panel to the top layer and chooses viewport or
5095
5205
  * document coordinates from its anchor. It also owns state (.is-open), focus,
5096
5206
  * and keyboard behavior. CSS owns the surfaced panel appearance, item styles,
@@ -5393,6 +5503,8 @@ dialog.drawer > form > header {
5393
5503
  /*
5394
5504
  * Tooltip — supplemental label overlaid near a trigger.
5395
5505
  *
5506
+ * Docs: docs/pages/enhancements/tooltip.md
5507
+ *
5396
5508
  * Position, placement, and arrow position are managed by JavaScript via left,
5397
5509
  * top, data-placement, and --arrow-x. The runtime also owns `position`: it
5398
5510
  * writes `absolute` for a tip whose trigger scrolls with the page, so the
@@ -5535,6 +5647,8 @@ dialog.drawer > form > header {
5535
5647
  /*
5536
5648
  * Accordion disclosures built on native details and summary.
5537
5649
  *
5650
+ * Docs: docs/pages/components/accordion.md
5651
+ *
5538
5652
  * Keeps the clickable summary affordance aligned and usable without JavaScript
5539
5653
  * or demo styles. Decorative palettes and custom markers belong outside the
5540
5654
  * base component.
@@ -5662,6 +5776,8 @@ dialog.drawer > form > header {
5662
5776
  /*
5663
5777
  * Spinner — compact loading feedback.
5664
5778
  *
5779
+ * Docs: docs/pages/components/spinner.md
5780
+ *
5665
5781
  * Uses currentColor so it adapts to buttons, alerts, badges, and text. The
5666
5782
  * [aria-busy] overlay composition lives in components/busy.css.
5667
5783
  *
@@ -5727,6 +5843,8 @@ dialog.drawer > form > header {
5727
5843
  /*
5728
5844
  * Skeleton placeholders for loading states.
5729
5845
  *
5846
+ * Docs: docs/pages/components/skeleton.md
5847
+ *
5730
5848
  * Shows approximate loading structure without implying real content hierarchy.
5731
5849
  * Geometry, animation, and color stay theme-aware.
5732
5850
  *
@@ -5813,6 +5931,8 @@ dialog.drawer > form > header {
5813
5931
  /*
5814
5932
  * Busy overlay — non-button loading state.
5815
5933
  *
5934
+ * Docs: docs/pages/components/spinner.md
5935
+ *
5816
5936
  * A non-button element with aria-busy="true" and a direct last-child .spinner
5817
5937
  * shows that spinner as a centered overlay over a faded surface. Buttons keep
5818
5938
  * their spinners inline (e.g. .btn[aria-busy] > .spinner stays inline-flex).
@@ -5865,6 +5985,8 @@ dialog.drawer > form > header {
5865
5985
  /*
5866
5986
  * Progress — native progress bar with theme-aware track and value.
5867
5987
  *
5988
+ * Docs: docs/pages/components/progress.md
5989
+ *
5868
5990
  * Consumes the shared --intent/--primary for the value bar and --bar-height
5869
5991
  * for thickness. Indeterminate state animates a shimmer using --duration-shimmer.
5870
5992
  *
@@ -5979,6 +6101,8 @@ dialog.drawer > form > header {
5979
6101
  /*
5980
6102
  * Status bar — singleton floating status area.
5981
6103
  *
6104
+ * Docs: docs/pages/enhancements/status-bar.md
6105
+ *
5982
6106
  * One element in the HTML, empty by default. .is-open is the visual state, set
5983
6107
  * by status.js like any other surface: it lands after the message is written
5984
6108
  * and lifts before the bar is cleared, so the bar leaves in the exact state it
@@ -6070,6 +6194,8 @@ dialog.drawer > form > header {
6070
6194
  /*
6071
6195
  * Avatars for initials, images, links, and buttons.
6072
6196
  *
6197
+ * Docs: docs/pages/components/avatar.md
6198
+ *
6073
6199
  * The visual box stays the same across semantic elements. Grouping and status
6074
6200
  * dots (an empty .badge child) compose around the avatar without changing its
6075
6201
  * meaning.
@@ -6292,6 +6418,8 @@ a.avatar[aria-disabled="true"] {
6292
6418
  /*
6293
6419
  * Indicator — generic corner positioning for overlaid component content.
6294
6420
  *
6421
+ * Docs: docs/pages/components/indicator.md
6422
+ *
6295
6423
  * The container only establishes the containing block. The item owns no
6296
6424
  * paint, size, or ring so badges and other components keep their own visual
6297
6425
  * contract.
@@ -6361,6 +6489,8 @@ a.avatar[aria-disabled="true"] {
6361
6489
  /*
6362
6490
  * Key — keyboard keys and shortcut tokens for UI chrome.
6363
6491
  *
6492
+ * Docs: docs/pages/components/key.md
6493
+ *
6364
6494
  * Use .key outside prose. Plain kbd inside .prose gets the same visual
6365
6495
  * treatment from prose.css so authored content stays convenient.
6366
6496
  *
@@ -6401,6 +6531,8 @@ a.avatar[aria-disabled="true"] {
6401
6531
  /*
6402
6532
  * Message rows composed from an optional avatar, metadata, and bubble.
6403
6533
  *
6534
+ * Docs: docs/pages/components/chat.md
6535
+ *
6404
6536
  * Public hooks:
6405
6537
  * --chat-gap
6406
6538
  * --chat-max-size
@@ -6504,6 +6636,8 @@ a.avatar[aria-disabled="true"] {
6504
6636
  * Button owns the trigger's size, shape, and intent; FAB owns placement and
6505
6637
  * the speed dial's structural open/close markers (--icon-plus / --icon-close).
6506
6638
  *
6639
+ * Docs: docs/pages/components/fab.md
6640
+ *
6507
6641
  * Public hooks:
6508
6642
  * --fab-offset
6509
6643
  * --fab-gap
@@ -6631,6 +6765,8 @@ a.avatar[aria-disabled="true"] {
6631
6765
  /*
6632
6766
  * Rating — integer star rating drawn on native radio inputs.
6633
6767
  *
6768
+ * Docs: docs/pages/components/rating.md
6769
+ *
6634
6770
  * The checked radio remains the functional baseline. :has() progressively
6635
6771
  * fills preceding stars without reversing DOM or keyboard order. Forced
6636
6772
  * colors restores the platform radio presentation.
@@ -6733,6 +6869,8 @@ a.avatar[aria-disabled="true"] {
6733
6869
  /*
6734
6870
  * Steps — semantic ordered-list progression for a multi-step process.
6735
6871
  *
6872
+ * Docs: docs/pages/components/steps.md
6873
+ *
6736
6874
  * One primitive, two explicit compositions. Both classes are required:
6737
6875
  *
6738
6876
  * <ol class="steps steps-horizontal"> horizontal, 2 to 5 stages
@@ -7081,6 +7219,8 @@ a.avatar[aria-disabled="true"] {
7081
7219
  * and item states; app-layout owns whether that surface is a bottom bar or
7082
7220
  * labelled side navigation.
7083
7221
  *
7222
+ * Docs: docs/pages/components/app-navigation.md
7223
+ *
7084
7224
  * Public hooks:
7085
7225
  * --app-nav-size
7086
7226
  * --app-nav-gap
@@ -7166,6 +7306,8 @@ a.avatar[aria-disabled="true"] {
7166
7306
  /*
7167
7307
  * Navbar — top-level navigation bar.
7168
7308
  *
7309
+ * Docs: docs/pages/components/navbar.md
7310
+ *
7169
7311
  * .navbar is a horizontal bar; .navbar-brand is the brand link; .navbar-nav is
7170
7312
  * the inline link list using .nav-link items. .nav-list is the vertical nav
7171
7313
  * list shared with .drawer, fed by the shared --gap token.
@@ -7298,6 +7440,8 @@ a.avatar[aria-disabled="true"] {
7298
7440
  /*
7299
7441
  * Breadcrumb — current-location trail from the site root.
7300
7442
  *
7443
+ * Docs: docs/pages/components/breadcrumb.md
7444
+ *
7301
7445
  * Inline list separated by "/". Mark the current page with `aria-current="page"`
7302
7446
  * either on the item or on the current link.
7303
7447
  */
@@ -7347,6 +7491,8 @@ a.avatar[aria-disabled="true"] {
7347
7491
  /*
7348
7492
  * Pagination — page navigation for paginated lists.
7349
7493
  *
7494
+ * Docs: docs/pages/components/pagination.md
7495
+ *
7350
7496
  * Render .btn elements inside a .pagination list. The active page is marked
7351
7497
  * with aria-current="page"; .btn owns the active state styling.
7352
7498
  *
@@ -7391,6 +7537,8 @@ a.avatar[aria-disabled="true"] {
7391
7537
  /*
7392
7538
  * Tabs — in-place panel switcher with real tab semantics.
7393
7539
  *
7540
+ * Docs: docs/pages/enhancements/tabs.md
7541
+ *
7394
7542
  * JavaScript owns roving tabindex, aria-selected, panel visibility,
7395
7543
  * and arrow-key navigation. CSS owns the tab strip appearance and
7396
7544
  * active indicator.
@@ -7512,6 +7660,8 @@ a.avatar[aria-disabled="true"] {
7512
7660
  * Scrollspy — behavior hook for navigation that marks the active section
7513
7661
  * while the page scrolls.
7514
7662
  *
7663
+ * Docs: docs/pages/enhancements/scrollspy.md
7664
+ *
7515
7665
  * Does not add visual styles by itself. scrollspy.js watches
7516
7666
  * <nav class="scrollspy">
7517
7667
  * containing <a href="#section"> links and toggles aria-current="location" on
@@ -7541,6 +7691,8 @@ a.avatar[aria-disabled="true"] {
7541
7691
  /*
7542
7692
  * Data list — term/value pairs for the properties of a single object.
7543
7693
  *
7694
+ * Docs: docs/pages/components/data-list.md
7695
+ *
7544
7696
  * Expects alternating <dt>/<dd> children: one term, one value per property.
7545
7697
  * A plain <dl> handles richer relationships (several terms or descriptions)
7546
7698
  * and stays untouched by this component.
@@ -7571,6 +7723,8 @@ a.avatar[aria-disabled="true"] {
7571
7723
  * Content determines the number of lines; the component only owns the row
7572
7724
  * geometry, dividers, and supporting-text treatment.
7573
7725
  *
7726
+ * Docs: docs/pages/components/list.md
7727
+ *
7574
7728
  * Public hooks:
7575
7729
  * --list-item-min-size
7576
7730
  * --list-item-pad-block
@@ -7691,6 +7845,8 @@ a.list-item[aria-current]::before {
7691
7845
  /*
7692
7846
  * Table — structured tabular data.
7693
7847
  *
7848
+ * Docs: docs/pages/components/table.md
7849
+ *
7694
7850
  * .table-wrap provides the scroll container and outer chrome; .table owns row
7695
7851
  * and cell rules. Tokens are sourced from the shared scale.
7696
7852
  *
@@ -7772,6 +7928,8 @@ a.list-item[aria-current]::before {
7772
7928
  /*
7773
7929
  * Meter — native and segmented gauges for known-range values.
7774
7930
  *
7931
+ * Docs: docs/pages/components/meter.md
7932
+ *
7775
7933
  * Maps the three standard meter zones (optimum / suboptimum / even-less-good)
7776
7934
  * to the shared success / warning / danger intent colors.
7777
7935
  *
@@ -7864,6 +8022,8 @@ a.list-item[aria-current]::before {
7864
8022
  /*
7865
8023
  * Join — visually groups adjacent controls into a single unit.
7866
8024
  *
8025
+ * Docs: docs/pages/forms/joined-controls.md
8026
+ *
7867
8027
  * This is a cross-cutting control primitive, not a forms-only component:
7868
8028
  * it applies equally to inputs, selects, buttons, and button-like links.
7869
8029
  *
@@ -7997,6 +8157,8 @@ a.list-item[aria-current]::before {
7997
8157
  /*
7998
8158
  * Decorative intent-colored frame for one direct child.
7999
8159
  *
8160
+ * Docs: docs/pages/components/aura.md
8161
+ *
8000
8162
  * The effect is built from three layers with distinct roles: a frame carrying
8001
8163
  * a short accent of light, a small halo that belongs to the frame and keeps
8002
8164
  * that light attached to the edge, and a wide static halo that belongs to the
@@ -8315,7 +8477,12 @@ a.list-item[aria-current]::before {
8315
8477
  text-decoration: none;
8316
8478
  }
8317
8479
 
8318
- .link-muted:hover,
8480
+ /* :focus-visible stays in its own rule: in a mixed selector list an engine
8481
+ without it would drop the whole rule, taking the :hover styling with it. */
8482
+ .link-muted:hover {
8483
+ color: var(--text);
8484
+ }
8485
+
8319
8486
  .link-muted:focus-visible {
8320
8487
  color: var(--text);
8321
8488
  }