actual-css 0.2.0 → 0.3.0

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 (214) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/README.md +24 -9
  3. package/dist/actual-themes.min.css +1 -0
  4. package/dist/actual.css +1244 -915
  5. package/dist/actual.js +1 -3
  6. package/dist/actual.min.css +1 -1
  7. package/dist/optional.css +1048 -0
  8. package/dist/optional.min.css +1 -0
  9. package/docs/design-notes/browser-support.md +94 -14
  10. package/docs/design-notes/cascade-layer.md +1 -1
  11. package/docs/design-notes/enhancement-contract.md +11 -14
  12. package/docs/design-notes/forced-colors.md +105 -0
  13. package/docs/design-notes/naming.md +2 -1
  14. package/docs/design-notes/platform-alignment.md +492 -0
  15. package/docs/design-notes/references.md +8 -0
  16. package/docs/design-notes/widget-primitives.md +37 -1
  17. package/docs/navigation.json +135 -0
  18. package/docs/pages/components/accordion.md +80 -0
  19. package/docs/pages/components/alert.md +203 -0
  20. package/docs/pages/components/aura.md +35 -0
  21. package/docs/pages/components/avatar.md +95 -0
  22. package/docs/pages/components/badge.md +102 -0
  23. package/docs/pages/components/breadcrumb.md +19 -0
  24. package/docs/pages/components/button.md +205 -0
  25. package/docs/pages/components/card.md +183 -0
  26. package/docs/pages/components/chat.md +50 -0
  27. package/docs/pages/components/dialog.md +367 -0
  28. package/docs/pages/components/drawer.md +113 -0
  29. package/docs/pages/components/fab.md +118 -0
  30. package/docs/pages/components/key.md +33 -0
  31. package/docs/pages/components/meter.md +23 -0
  32. package/docs/pages/components/navbar.md +92 -0
  33. package/docs/pages/components/pagination.md +55 -0
  34. package/docs/pages/components/progress.md +29 -0
  35. package/docs/pages/components/skeleton.md +38 -0
  36. package/docs/pages/components/spinner.md +93 -0
  37. package/docs/pages/components/table.md +102 -0
  38. package/docs/pages/enhancements/flyout.md +391 -0
  39. package/docs/pages/enhancements/overview.md +36 -0
  40. package/docs/pages/enhancements/scrollspy.md +71 -0
  41. package/docs/pages/enhancements/status-bar.md +72 -0
  42. package/docs/pages/enhancements/tabs.md +80 -0
  43. package/docs/pages/enhancements/tooltip.md +130 -0
  44. package/docs/pages/examples/overview.md +41 -0
  45. package/docs/pages/forms/choice-cards.md +102 -0
  46. package/docs/pages/forms/controls.md +32 -0
  47. package/docs/pages/forms/detached-actions.md +59 -0
  48. package/docs/pages/forms/floating-field.md +88 -0
  49. package/docs/pages/forms/form-actions.md +56 -0
  50. package/docs/pages/forms/input-policy.md +91 -0
  51. package/docs/pages/forms/joined-controls.md +37 -0
  52. package/docs/pages/forms/labels.md +42 -0
  53. package/docs/pages/forms/otp.md +50 -0
  54. package/docs/pages/forms/overview.md +188 -0
  55. package/docs/pages/forms/password.md +40 -0
  56. package/docs/pages/forms/range.md +17 -0
  57. package/docs/pages/forms/states-and-sizes.md +321 -0
  58. package/docs/pages/forms/sticky-actions.md +187 -0
  59. package/docs/pages/forms/switch.md +31 -0
  60. package/docs/pages/forms/validation.md +241 -0
  61. package/docs/{mental-model.md → pages/foundations/philosophy.md} +33 -18
  62. package/docs/pages/foundations/theming.md +92 -0
  63. package/docs/pages/foundations/tokens.md +421 -0
  64. package/docs/{typography.md → pages/foundations/typography.md} +27 -9
  65. package/docs/pages/guides/bootstrap.md +438 -0
  66. package/docs/pages/guides/introduction.md +157 -0
  67. package/docs/pages/guides/optional-components.md +33 -0
  68. package/docs/pages/guides/progressive-enhancement.md +686 -0
  69. package/docs/pages/guides/tailwind.md +455 -0
  70. package/docs/pages/layout/app-shell.md +25 -0
  71. package/docs/pages/layout/center.md +29 -0
  72. package/docs/pages/layout/cluster.md +57 -0
  73. package/docs/pages/layout/frame.md +30 -0
  74. package/docs/pages/layout/grid.md +142 -0
  75. package/docs/pages/layout/list.md +41 -0
  76. package/docs/pages/layout/media.md +21 -0
  77. package/docs/pages/layout/overview.md +37 -0
  78. package/docs/pages/layout/scroll-snap.md +54 -0
  79. package/docs/pages/layout/sidebar-layout.md +27 -0
  80. package/docs/pages/layout/stack.md +18 -0
  81. package/docs/pages/layout/switcher.md +36 -0
  82. package/docs/pages/layout/topbar.md +23 -0
  83. package/docs/pages/patterns/actions.md +52 -0
  84. package/docs/pages/patterns/code-block.md +36 -0
  85. package/docs/pages/patterns/exclusive-accordion.md +16 -0
  86. package/docs/pages/patterns/hierarchical-nav.md +26 -0
  87. package/docs/pages/patterns/icon-input.md +25 -0
  88. package/docs/pages/patterns/intent-nesting.md +3 -0
  89. package/docs/pages/patterns/navigation-list.md +235 -0
  90. package/docs/pages/patterns/overview.md +58 -0
  91. package/docs/pages/patterns/structured-lists.md +47 -0
  92. package/docs/pages/utilities/accessibility.md +34 -0
  93. package/docs/pages/utilities/flex.md +105 -0
  94. package/docs/pages/utilities/links.md +18 -0
  95. package/docs/pages/utilities/list.md +31 -0
  96. package/docs/pages/utilities/overflow.md +43 -0
  97. package/docs/pages/utilities/overview.md +18 -0
  98. package/docs/pages/utilities/shape.md +25 -0
  99. package/docs/pages/utilities/sizing-wrapping.md +115 -0
  100. package/docs/pages/utilities/spacing.md +79 -0
  101. package/docs/pages/utilities/text.md +43 -0
  102. package/llms.txt +4 -4
  103. package/package.json +20 -12
  104. package/src/css/actual.css +12 -1
  105. package/src/css/base.css +3 -5
  106. package/src/css/components/accordion.css +18 -30
  107. package/src/css/components/actions.css +4 -1
  108. package/src/css/components/alert.css +126 -99
  109. package/src/css/components/avatar.css +16 -10
  110. package/src/css/components/badge.css +55 -35
  111. package/src/css/components/breadcrumb.css +7 -15
  112. package/src/css/components/busy.css +6 -0
  113. package/src/css/components/button.css +78 -32
  114. package/src/css/components/card.css +17 -37
  115. package/src/css/components/drawer.css +30 -13
  116. package/src/css/components/flyout.css +39 -109
  117. package/src/css/components/index.css +1 -0
  118. package/src/css/components/join.css +14 -2
  119. package/src/css/components/key.css +2 -0
  120. package/src/css/components/menu.css +125 -0
  121. package/src/css/components/meter.css +0 -23
  122. package/src/css/components/modal.css +162 -7
  123. package/src/css/components/navbar.css +17 -18
  124. package/src/css/components/overline.css +0 -7
  125. package/src/css/components/pagination.css +1 -1
  126. package/src/css/components/progress.css +8 -26
  127. package/src/css/components/skeleton.css +7 -0
  128. package/src/css/components/spinner.css +6 -7
  129. package/src/css/components/status-bar.css +15 -24
  130. package/src/css/components/surface.css +2 -8
  131. package/src/css/components/tab.css +19 -48
  132. package/src/css/components/table.css +7 -5
  133. package/src/css/components/tooltip.css +14 -7
  134. package/src/css/focus.css +14 -38
  135. package/src/css/forms/choice-card.css +13 -14
  136. package/src/css/forms/choice.css +21 -27
  137. package/src/css/forms/control.css +16 -17
  138. package/src/css/forms/custom-select.css +9 -11
  139. package/src/css/forms/form-actions.css +14 -3
  140. package/src/css/forms/form.css +7 -4
  141. package/src/css/forms/index.css +4 -4
  142. package/src/css/forms/input-icon.css +30 -12
  143. package/src/css/forms/native.css +50 -11
  144. package/src/css/forms/range.css +17 -9
  145. package/src/css/forms/select.css +8 -8
  146. package/src/css/forms/switch.css +14 -12
  147. package/src/css/forms/validation.css +32 -5
  148. package/src/css/grid.css +19 -18
  149. package/src/css/intents.css +2 -2
  150. package/src/css/layout.css +29 -1
  151. package/src/css/optional/aura.css +93 -0
  152. package/src/css/optional/chat.css +105 -0
  153. package/src/css/optional/fab.css +104 -0
  154. package/src/css/optional/floating-field.css +87 -0
  155. package/src/css/optional/index.css +5 -0
  156. package/src/css/optional/layout-extra.css +7 -4
  157. package/src/css/optional/otp.css +115 -0
  158. package/src/css/optional/scroll-snap.css +6 -0
  159. package/src/css/optional/scroller.css +13 -7
  160. package/src/css/optional/typography-fluid.css +5 -0
  161. package/src/css/optional/utilities-extra.css +17 -2
  162. package/src/css/prose.css +18 -8
  163. package/src/css/reset.css +0 -10
  164. package/src/css/theme.css +21 -5
  165. package/src/css/themes/cyberpunk.css +1 -1
  166. package/src/css/themes/dim.css +3 -3
  167. package/src/css/themes/edge.css +8 -10
  168. package/src/css/themes/index.css +3 -3
  169. package/src/css/themes/{dark.css → indigo.css} +5 -4
  170. package/src/css/themes/neon.css +5 -5
  171. package/src/css/themes/square.css +0 -3
  172. package/src/css/tokens.css +74 -20
  173. package/src/css/utilities.css +8 -10
  174. package/src/css/variants.css +37 -65
  175. package/src/js/command.js +1 -3
  176. package/src/js/context-menu.js +67 -35
  177. package/src/js/dialog.js +68 -79
  178. package/src/js/dismiss.js +15 -0
  179. package/src/js/enhance.js +54 -23
  180. package/src/js/enhancement-loader.js +301 -0
  181. package/src/js/events.js +3 -0
  182. package/src/js/filter.js +3 -3
  183. package/src/js/floating.js +101 -131
  184. package/src/js/flyout.js +104 -87
  185. package/src/js/focus-group.js +116 -0
  186. package/src/js/focus.js +6 -1
  187. package/src/js/index.js +4 -2
  188. package/src/js/input.js +1 -1
  189. package/src/js/keys.js +27 -0
  190. package/src/js/mask.js +7 -6
  191. package/src/js/menu.js +87 -34
  192. package/src/js/parse-config.js +47 -0
  193. package/src/js/password.js +9 -1
  194. package/src/js/scrollspy.js +12 -4
  195. package/src/js/selectors.js +0 -7
  196. package/src/js/status.js +3 -1
  197. package/src/js/surface.js +185 -136
  198. package/src/js/tab.js +36 -53
  199. package/src/js/tooltip.js +53 -31
  200. package/src/js/validation.js +34 -8
  201. package/dist/actual.js.map +0 -31
  202. package/docs/README.md +0 -28
  203. package/docs/components.md +0 -860
  204. package/docs/design-notes/previous-work.md +0 -5
  205. package/docs/forms.md +0 -1265
  206. package/docs/javascript.md +0 -496
  207. package/docs/layout.md +0 -480
  208. package/docs/patterns.md +0 -504
  209. package/docs/reserved.md +0 -7
  210. package/docs/tokens.md +0 -455
  211. package/docs/ui.md +0 -929
  212. package/docs/utilities.md +0 -384
  213. package/src/css/dialog-fallback.css +0 -56
  214. package/src/js/dialog-fallback.js +0 -163
package/dist/actual.css CHANGED
@@ -24,16 +24,6 @@ select {
24
24
  display: none !important;
25
25
  }
26
26
 
27
- /* Legacy dialog fallback only. Modern dialogs keep the browser's native closed
28
- state so display/overlay transitions are not flattened by the reset.
29
- The presentation for the dialog-fallback.js shim lives in the separate
30
- dialog-fallback.css, so it can be dropped from custom bundles. */
31
- @supports not (selector(dialog:modal)) {
32
- dialog:not([open]) {
33
- display: none;
34
- }
35
- }
36
-
37
27
  :where(img, svg, video) {
38
28
  max-inline-size: 100%;
39
29
  block-size: auto;
@@ -55,64 +45,24 @@ select {
55
45
  }
56
46
  }
57
47
 
58
- /*
59
- * Legacy dialog fallback — presentation for the dialog-fallback.js shim
60
- * (browsers without native <dialog>: Safari <= 15.3, Firefox <= 97).
61
- *
62
- * Isolated in its own file so a custom bundle can opt out by dropping this
63
- * import; the JS side is disabled with data-dialog-fallback="false" on the
64
- * root element or on a dialog. Must load before component styles so ties
65
- * such as dialog.modal's padding resolve in the component's favor.
66
- *
67
- * The whole file is inert on modern browsers: rules key on the
68
- * .dialog-fallback marker written by the shim, so browsers that support
69
- * <dialog> but not :modal are never affected either.
70
- */
71
- @supports not (selector(dialog:modal)) {
72
- /* Browsers without native <dialog> ship no UA dialog styles at all;
73
- approximate them. :where() keeps specificity at dialog[open] level so
74
- component styles such as dialog.modal still override the emulation. */
75
- dialog[open]:where(.dialog-fallback) {
76
- position: absolute;
77
- inset-inline: 0;
78
- width: -moz-fit-content;
79
- width: fit-content;
80
- max-inline-size: calc(100% - 2rem);
81
- margin: auto;
82
- padding: var(--space-4, 1em);
83
- border: var(--border-width, 1px) solid var(--border, currentColor);
84
- background: var(--surface-raised, #fff);
85
- color: var(--text, inherit);
86
- }
87
-
88
- /* Open modally: fixed near the top (no fit-content in the block axis on
89
- these browsers, so no vertical centering) and above menus, below
90
- tooltips. Legacy-only layer, hence no z-index token. The spread shadow is
91
- the visible backdrop: the dialog is its own stacking context, where a
92
- negative z-index child paints above the element's background, so the
93
- dimming cannot live on the ::before. */
94
- dialog[open].is-fallback-modal {
95
- position: fixed;
96
- inset-block: 8vh auto;
97
- max-block-size: 84vh;
98
- overflow: auto;
99
- z-index: 40;
100
- box-shadow: 0 0 0 100vmax var(--backdrop-fill, rgb(0 0 0 / 35%));
101
- }
102
-
103
- /* Transparent stand-in for ::backdrop, hit-testing only. Clicks on it hit
104
- the dialog element outside its own box, which dialog.js already treats as
105
- a backdrop click, and it blocks pointer interaction with the page behind
106
- the modal. */
107
- dialog[open].is-fallback-modal::before {
108
- content: "";
109
- position: fixed;
110
- inset: 0;
111
- z-index: -1;
112
- }
113
- }
114
-
115
48
  :root {
49
+ /*
50
+ * Global framework tokens.
51
+ *
52
+ * Public: properties in the named groups below are public API (typography,
53
+ * spacing, shape, motion, focus, controls, viewport, z-index, icons) unless
54
+ * a group is explicitly marked Internal or Derived.
55
+ *
56
+ * Derived: "Theme-derived aliases" and "State tokens" reference intent and
57
+ * surface tokens and follow the theme; do not hard-code values into them.
58
+ * Internal: indicator offsets, --bar-height, --disabled-opacity and the
59
+ * like are consumed by components, not authored.
60
+ *
61
+ * Component-local hooks and internal state relays live with their
62
+ * components. See docs/pages/foundations/tokens.md for the public/internal
63
+ * token contract.
64
+ */
65
+
116
66
  /* Typography */
117
67
  --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
118
68
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
@@ -130,20 +80,22 @@ select {
130
80
  --font-size-md: 1rem;
131
81
  --font-size-lg: 1.125rem;
132
82
 
133
- /* Spacing */
83
+ /* Spacing — official steps are named in tens so the scale stays open for
84
+ application themes to intercalate intermediate values (--space-15,
85
+ --space-45…) without colliding with future framework steps. */
134
86
  --gap: 0.75rem;
135
- --variant-space: var(--space-4);
136
- --space-1: 0.25rem;
137
- --space-2: 0.5rem;
138
- --space-3: 0.75rem;
139
- --space-4: 1rem;
140
- --space-5: 1.5rem;
141
- --space-6: 2rem;
87
+ --density-space: var(--space-40);
88
+ --density-compact-size: 1.5rem;
89
+ --space-10: 0.25rem;
90
+ --space-20: 0.5rem;
91
+ --space-30: 0.75rem;
92
+ --space-40: 1rem;
93
+ --space-50: 1.5rem;
94
+ --space-60: 2rem;
142
95
 
143
96
  /* Shape */
144
97
  --border-width: 1px;
145
98
  --radius-full: 9999px;
146
- --shadow-xs: 0 1px 2px rgb(0 0 0 / 0.04);
147
99
  --shadow: 0 1px 3px rgb(0 0 0 / 0.05), 0 4px 12px rgb(0 0 0 / 0.04);
148
100
  --shadow-popout: 0 0.25rem 0.75rem rgb(0 0 0 / 0.15);
149
101
 
@@ -167,7 +119,6 @@ select {
167
119
  the circle-aware inset (1 - 1/√2) / 2 ≈ 14.65%; the ring surrounds the
168
120
  dot with the parent surface color. */
169
121
  --indicator-offset: 14.65%;
170
- --indicator-ring: var(--surface);
171
122
 
172
123
  /* Shared backdrop — consumed by modal, drawer, and mobile flyout sheets. */
173
124
  --backdrop-color: rgb(0 0 0);
@@ -178,7 +129,8 @@ select {
178
129
  --bar-height: 0.65rem;
179
130
 
180
131
  /* Controls — inputs and buttons share --control-size so heights always match.
181
- .sm/.lg (variants.css) switch --control-size and --control-font-size. */
132
+ .sm/.lg (variants.css) switch --control-size; --control-font-size stays
133
+ at its baseline because density does not change typography. */
182
134
  --control-size-sm: 2rem;
183
135
  --control-size-md: 2.375rem;
184
136
  --control-size-lg: 2.75rem;
@@ -186,19 +138,38 @@ select {
186
138
  --control-pad-x: 1em;
187
139
  --control-font-size: var(--font-size-md);
188
140
  --disabled-opacity: 0.65;
141
+ }
189
142
 
143
+ /* Theme-derived aliases. These reference intent/surface tokens, so they must
144
+ be re-declared on every theme boundary: a custom property resolves its
145
+ var() references at computed-value time on the element that declares it,
146
+ so an alias declared only on :root is inherited as an already-resolved
147
+ value and would not follow a [data-theme] island's overridden tokens.
148
+ Themes that want a distinct alias override it explicitly afterwards. */
149
+ :root,
150
+ [data-theme] {
151
+ /* State tokens — semantic colors for selected/pressed/current and disabled.
152
+ Components consume these instead of hard-wiring --primary/--text-subtle
153
+ so forced-colors can override them in one place. */
154
+ --state-selected: var(--primary);
155
+ --state-selected-fg: var(--primary-fg);
156
+ --state-disabled: var(--text-subtle);
157
+
158
+ /* Status dot ring — follows the island's surface. */
159
+ --indicator-ring: var(--surface);
160
+ }
161
+
162
+ :root {
190
163
  /* Variant tuning — themes may adjust generated soft surfaces without
191
164
  replacing the component rules. */
192
165
  --soft-bg-mix: 88%;
193
166
  --soft-border-mix: 65%;
194
167
  --soft-hover-alpha: 12%;
195
168
 
196
- /* Shared icons */
197
- --icon-chevron-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
198
- --icon-check-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E");
199
- /* Native select uses background-image, which cannot inherit currentColor.
200
- The stroke matches the default theme's text-subtle (hue 258 neutral). */
201
- --icon-chevron-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%238a8594' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
169
+ /* Shared icon assets. Override these URLs when a strict CSP disallows data:. */
170
+ --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%238a8594' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
171
+ --icon-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E");
172
+ --icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='6' y1='6' x2='18' y2='18'/%3E%3Cline x1='18' y1='6' x2='6' y2='18'/%3E%3C/svg%3E");
202
173
 
203
174
  /* Viewport size tokens — fallback vh/vw with dvh/vi enhancement.
204
175
  Consumed by full-viewport components (app-shell, drawer, modal, flyout).
@@ -207,13 +178,24 @@ select {
207
178
  --viewport-block: 100vh;
208
179
  --viewport-inline: 100vw;
209
180
 
210
- /* Layering */
181
+ /* Z-index scale — used ONLY by non-dialog overlays (see the layering
182
+ note below for the full model). Actual CSS reserves multiples of 10 for
183
+ framework layers; applications may use intermediate values for local
184
+ layering. */
211
185
  --z-sticky: 10;
212
186
  --z-menu: 20;
213
187
  --z-tooltip: 50;
214
188
  --z-status: 60;
215
189
  }
216
190
 
191
+ @media (forced-colors: active) {
192
+ :root {
193
+ --state-selected: Highlight;
194
+ --state-selected-fg: HighlightText;
195
+ --state-disabled: GrayText;
196
+ }
197
+ }
198
+
217
199
  @supports (block-size: 100dvh) {
218
200
  :root {
219
201
  --viewport-block: 100dvh;
@@ -227,12 +209,17 @@ select {
227
209
  }
228
210
 
229
211
  @supports (color: color-mix(in oklch, red, white)) {
230
- :root {
231
- --shadow-xs: 0 1px 2px color-mix(in oklch, var(--shadow-color) 4%, transparent);
212
+ :root,
213
+ [data-theme] {
214
+ /* Shadows derive from --shadow-color (theme.css) — re-declared on every
215
+ theme boundary so islands follow their own shadow-color. */
232
216
  --shadow:
233
217
  0 1px 3px color-mix(in oklch, var(--shadow-color) 5%, transparent),
234
218
  0 4px 12px color-mix(in oklch, var(--shadow-color) 4%, transparent);
235
219
  --shadow-popout: 0 0.25rem 0.75rem color-mix(in oklch, var(--shadow-color) 15%, transparent);
220
+ }
221
+
222
+ :root {
236
223
  --backdrop-fill: color-mix(
237
224
  in oklch,
238
225
  var(--backdrop-color) calc(var(--backdrop-opacity) * 100%),
@@ -241,6 +228,17 @@ select {
241
228
  }
242
229
  }
243
230
 
231
+ /*
232
+ * Theme tokens — the palette, surfaces, text, borders, radius and shadow
233
+ * color. Named themes (themes/*.css) override this file; authors can set the
234
+ * same tokens on :root or on [data-theme="…"] islands.
235
+ *
236
+ * Public: Intent colors, Surfaces, Text, Lines and focus, Radius scale,
237
+ * Shadow color, Focus ring derivation.
238
+ * Derived: "Theme-derived aliases", Tone subsystem, Backdrop fill — reference
239
+ * other tokens; hard-coded overrides on a custom theme will break the intent.
240
+ * Internal: --hgroup-gap.
241
+ */
244
242
  :root {
245
243
  color-scheme: light dark;
246
244
  }
@@ -308,7 +306,6 @@ select {
308
306
  /* Pre-color-mix fallback only — superseded below by a derivation from
309
307
  --focus so overriding --neutral/--focus never leaves a stale ring. */
310
308
  --focus-ring: hsl(258 7% 43% / 0.3);
311
- --focus-ring-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring);
312
309
  --hover-overlay: hsl(260 20% 10% / 0.04);
313
310
 
314
311
  /* Radius scale — themes may override the whole scale (e.g. [data-theme="square"]) */
@@ -319,13 +316,19 @@ select {
319
316
  /* Shadow color — consumed by --shadow / --shadow-popout in tokens.css */
320
317
  --shadow-color: hsl(260 25% 10%);
321
318
 
322
- /* Heading and selection — derived from theme color tokens */
319
+ /* hgroup internal gap */
320
+ --hgroup-gap: 0.25em;
321
+ }
322
+
323
+ /* Theme-derived aliases — re-declared on every theme boundary (see tokens.css
324
+ for the custom-property resolution rationale): islands recompute these from
325
+ their own tokens instead of inheriting :root's resolved values. */
326
+ :root,
327
+ [data-theme] {
323
328
  --heading: var(--text);
324
329
  --selection-bg: var(--primary);
325
330
  --selection-fg: var(--primary-fg);
326
-
327
- /* hgroup internal gap */
328
- --hgroup-gap: 0.25em;
331
+ --focus-ring-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring);
329
332
  }
330
333
 
331
334
  /* Tone subsystem — fixed-backdrop foreground for components placed over media
@@ -447,10 +450,8 @@ a {
447
450
  text-underline-offset: 0.18em;
448
451
  }
449
452
 
450
- @media (hover: hover) {
451
- a:hover {
452
- text-decoration-thickness: 0.12em;
453
- }
453
+ a:hover {
454
+ text-decoration-thickness: 0.12em;
454
455
  }
455
456
 
456
457
  /* Global heading chrome: color (--heading or inherit), a tight line-height,
@@ -476,7 +477,7 @@ hgroup > * {
476
477
  hr {
477
478
  border: 0;
478
479
  border-block-start: var(--border-width) solid var(--border);
479
- margin-block: var(--space-6);
480
+ margin-block: var(--space-60);
480
481
  }
481
482
 
482
483
  code,
@@ -499,8 +500,8 @@ small {
499
500
  * Intent classes do not style elements directly.
500
501
  * They only expose --intent and --intent-fg.
501
502
  * Components and variants decide how to consume them. Each component that
502
- * consumes --intent or --ui-* must open a local intent boundary in a :where()
503
- * block that resets --intent / --intent-fg / --ui-* to initial. Parent intents
503
+ * consumes --intent or --ui-* opens a local intent boundary in a :where()
504
+ * block that resets exactly the tokens it consumes to initial. Parent intents
504
505
  * do not leak into nested components. Local intent classes (.primary, .danger,
505
506
  * etc.) still apply because they have higher specificity than :where().
506
507
  */
@@ -529,6 +530,28 @@ small {
529
530
  --intent-fg: var(--neutral-fg);
530
531
  }
531
532
 
533
+ /*
534
+ * Core layout primitives: stack, cluster, center, frame, app-shell, media,
535
+ * sidebar-layout. Each primitive owns a single, readable behavior; they are
536
+ * meant to compose in markup.
537
+ *
538
+ * Public hooks:
539
+ * --cluster-align
540
+ * --cluster-justify
541
+ * --cluster-wrap
542
+ * --center-size
543
+ * --center-pad
544
+ * --frame-ratio
545
+ * --sidebar-layout-size
546
+ *
547
+ * Child contracts:
548
+ * .stack owns vertical rhythm: direct-child margins are reset, spacing comes
549
+ * from --gap.
550
+ * .cluster owns inline rhythm: direct-child margins are reset, spacing comes
551
+ * from --gap.
552
+ * .frame sizes direct media children (img/video/iframe/canvas/svg) to fill the
553
+ * frame.
554
+ */
532
555
  .container-query {
533
556
  container-type: inline-size;
534
557
  }
@@ -545,12 +568,16 @@ small {
545
568
 
546
569
  .cluster {
547
570
  display: flex;
548
- flex-wrap: wrap;
571
+ flex-wrap: var(--cluster-wrap, wrap);
549
572
  align-items: var(--cluster-align, center);
550
573
  justify-content: var(--cluster-justify, flex-start);
551
574
  gap: var(--gap);
552
575
  }
553
576
 
577
+ .cluster > * {
578
+ margin: 0;
579
+ }
580
+
554
581
  .center {
555
582
  box-sizing: content-box;
556
583
  max-inline-size: var(--center-size, 72rem);
@@ -594,6 +621,8 @@ small {
594
621
  grid-template-columns: minmax(0, 1fr);
595
622
  }
596
623
 
624
+ /* Container queries are the preferred path; without them the @supports not
625
+ block below restores the same columns at the same viewport breakpoint. */
597
626
  @container (width >= 64rem) {
598
627
  .container-query .sidebar-layout {
599
628
  grid-template-columns: minmax(0, 1fr) var(--sidebar-layout-size, 18rem);
@@ -610,29 +639,27 @@ small {
610
639
  }
611
640
  }
612
641
 
642
+ /*
643
+ * Responsive equal-width grids, with fixed-column presets below.
644
+ *
645
+ * Public hooks:
646
+ * --grid-min
647
+ * --grid-columns
648
+ */
613
649
  .grid {
614
650
  display: grid;
615
651
  gap: var(--gap);
616
- grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--grid-min, 16rem)), 1fr));
617
- }
618
-
619
- /* Query-container wrapper that opts .grid-2/3/4/6 into responsive
620
- * collapse. Wrap a .grid-2/3/4/6 inside .container-query to make the
621
- * column count collapse on narrow wrappers. Without this wrapper,
622
- * .grid-2/3/4/6 stay at their full column count. */
623
- /* Fixed-column grids at their full count by default.
624
- * .grid-2: 2 cols
625
- * .grid-3: 3 cols
626
- * .grid-4: 4 cols
627
- * .grid-6: 6 cols
628
- * When placed inside .container-query, the column count collapses
629
- * on narrow wrappers. The intermediate 2-col step applies to
630
- * .grid-3/.grid-4; .grid-6 starts at 2 columns because it is meant
631
- * for compact items that do not need a full single-column stack.
632
- * .grid-2: 1 col -> 2 cols
633
- * .grid-3: 1 col -> 2 cols -> 3 cols
634
- * .grid-4: 1 col -> 2 cols -> 4 cols
635
- * .grid-6: 2 cols -> 3 cols -> 6 cols */
652
+ grid-template-columns: var(
653
+ --grid-columns,
654
+ repeat(auto-fit, minmax(min(100%, var(--grid-min, 16rem)), 1fr))
655
+ );
656
+ }
657
+
658
+ /* Fixed-column grids at their full count by default: .grid-2/3/4/6 stay at
659
+ 2/3/4/6 columns unless wrapped in .container-query, which collapses the
660
+ column count on narrow wrappers (a 2-col intermediate step for
661
+ .grid-3/.grid-4; .grid-6 starts at 2 columns because it is meant for
662
+ compact items that do not need a full single-column stack). */
636
663
  .grid-2,
637
664
  .grid-3,
638
665
  .grid-4,
@@ -654,6 +681,9 @@ small {
654
681
  grid-template-columns: repeat(6, minmax(0, 1fr));
655
682
  }
656
683
 
684
+ /* Container queries are the preferred responsive path; browsers without them
685
+ ignore these rules and get the equivalent @media rules from the
686
+ @supports not (container-type) block below — same columns, same breakpoints. */
657
687
  @container (width < 28rem) {
658
688
  .container-query .grid-2,
659
689
  .container-query .grid-3,
@@ -710,15 +740,20 @@ small {
710
740
  * Cross-cutting variant token mapping.
711
741
  *
712
742
  * Variant scope: .solid/.soft/.outline set --ui-* for components that opt into
713
- * shared variants. Component-specific defaults (.btn solid-by-default, .badge
714
- * solid-by-default, .alert soft-by-default) live with their components:
743
+ * shared variants. .inverted is a surface modifier, not a variant: it also
744
+ * paints the element directly (background/color/border-color) so it works on
745
+ * any block, competing with component surface treatments (.card.subtle).
746
+ * Component-specific defaults (.btn solid-by-default, .badge
747
+ * soft-by-default, .alert soft-by-default) live with their components:
715
748
  * components/button.css — .btn default solid + .btn.ghost + .btn.link
716
- * components/badge.css — .badge default solid
717
- * components/alert.css — .alert default + .alert.solid/.alert.outline
749
+ * components/badge.css — .badge default soft
750
+ * components/alert.css — .alert default soft (consumes --ui-* directly)
718
751
  *
719
- * .sm/.lg set shared size tokens. Controls consume --control-*; non-control
720
- * components may consume --variant-* when the variant should affect their
721
- * local density. Compact components may consume --variant-compact-*.
752
+ * .sm/.lg set shared density tokens: spacing rhythm and geometry only.
753
+ * Density never changes typography — controls keep their font-size and
754
+ * content components keep theirs. Controls consume --control-size; non-control
755
+ * components may consume --density-* when the density context should affect
756
+ * their local rhythm. Compact components may consume --density-compact-size.
722
757
  * Components with bespoke sizing (spinner, avatar) define local .sm/.lg;
723
758
  * applying .sm/.lg to card, dialog, accordion, navbar, table is
724
759
  * a no-op.
@@ -744,27 +779,34 @@ small {
744
779
  --ui-border: var(--intent, var(--border));
745
780
  }
746
781
 
782
+ /* Inverse surface treatment. Components (e.g. .card) compose through --ui-*;
783
+ the direct background/color/border-color also make .inverted useful on a
784
+ generic element. --heading is set so global headings (var(--heading, inherit))
785
+ do not inherit an outside heading color onto the dark surface. */
786
+ .inverted {
787
+ --ui-bg: var(--surface-solid);
788
+ --ui-fg: var(--surface);
789
+ --ui-border: transparent;
790
+
791
+ --heading: var(--surface);
792
+ --busy-overlay-bg: var(--surface-solid);
793
+
794
+ background: var(--ui-bg);
795
+ color: var(--ui-fg);
796
+ border-color: var(--ui-border);
797
+ }
798
+
747
799
  .sm {
748
- --gap: var(--space-2);
749
- --variant-space: var(--space-2);
800
+ --gap: var(--space-20);
801
+ --density-space: var(--space-20);
750
802
  --control-size: var(--control-size-sm);
751
- --control-font-size: var(--font-size-sm);
752
- --variant-font-size: var(--font-size-sm);
753
- --variant-icon-size: 1rem;
754
- --variant-pad-block: var(--space-3);
755
- --variant-compact-size: 1.25rem;
756
- --variant-compact-font-size: 0.75rem;
803
+ --density-compact-size: 1.25rem;
757
804
  }
758
805
  .lg {
759
- --gap: var(--space-5);
760
- --variant-space: var(--space-5);
806
+ --gap: var(--space-50);
807
+ --density-space: var(--space-50);
761
808
  --control-size: var(--control-size-lg);
762
- --control-font-size: var(--font-size-lg);
763
- --variant-font-size: var(--font-size-lg);
764
- --variant-icon-size: 1.5rem;
765
- --variant-pad-block: var(--space-5);
766
- --variant-compact-size: 1.875rem;
767
- --variant-compact-font-size: var(--font-size-sm);
809
+ --density-compact-size: 1.875rem;
768
810
  }
769
811
 
770
812
  /* Modern color enhancement. Baseline variant values above remain usable
@@ -782,54 +824,25 @@ small {
782
824
  }
783
825
  }
784
826
 
785
- /* Interactive hover recipe shared by .btn and .badge. Declared outside :hover
786
- on purpose: --ui-hover-bg is only consumed inside their @media (hover: hover)
787
- rules, so these values are inert until hovered. :is() carries real
788
- specificity so the components' :where() intent boundaries
789
- (--ui-hover-bg: initial) cannot override the recipe. The solid/soft overlay
790
- darkening lives in the consuming components. */
791
- :is(.btn, a.badge, button.badge):is(.outline, .ghost) {
792
- --ui-hover-bg: var(--surface-subtle);
793
- }
794
-
795
- @supports (color: color-mix(in oklch, red, white)) {
796
- /* Solid has no literal class requirement: bare .btn/.badge default to solid,
797
- so this must match anything that isn't one of the other variants rather
798
- than an explicit .solid class. Stacks with the box-shadow overlay the
799
- consuming components apply on top for extra depth. */
800
- :is(.btn, a.badge, button.badge):not(:where(.soft, .outline, .ghost, .link)) {
801
- --ui-hover-bg: color-mix(in oklch, var(--intent, var(--neutral)) 88%, black);
802
- }
803
-
804
- /* Same recipe as the resting .soft bg with the surface share reduced by
805
- --soft-hover-alpha: stays opaque, so it reads on any backdrop instead of
806
- letting the container show through on hover. */
807
- :is(.btn, a.badge, button.badge):is(.soft) {
808
- --ui-hover-bg: color-mix(
809
- in oklch,
810
- var(--surface) calc(var(--soft-bg-mix) - var(--soft-hover-alpha)),
811
- var(--intent, var(--neutral))
812
- );
813
- }
814
-
815
- /* Mixed against the opaque --surface-subtle rather than transparent: a
816
- transparent-based tint reads clean on light surfaces but goes muddy and
817
- low-contrast on dark ones, since the same low alpha lands very
818
- differently over a light vs. a near-black backdrop. */
819
- :is(.btn, a.badge, button.badge):is(.outline) {
820
- --ui-hover-bg: color-mix(in oklch, var(--surface-subtle) 92%, var(--intent, var(--neutral)));
821
- }
822
-
823
- :is(.btn, a.badge, button.badge):is(.ghost) {
824
- --ui-hover-bg: color-mix(in oklch, var(--intent, var(--text)) 8%, transparent);
825
- }
826
- }
827
+ /* Interactive hover recipes live in the components that consume them:
828
+ components/button.css and components/badge.css. */
827
829
 
828
830
  /*
829
831
  * Prose is the opt-in rich text scope for authored content.
830
832
  *
831
833
  * Global element styles stay mild so app UI is not shaped accidentally. Page
832
834
  * layout, forms, cards, navigation, and app tables remain outside this scope.
835
+ *
836
+ * Public hooks:
837
+ * --prose-size
838
+ * --prose-line-height
839
+ * --prose-measure
840
+ * --prose-flow
841
+ * --prose-heading-line-height
842
+ * --prose-h1
843
+ * --prose-h2
844
+ * --prose-h3
845
+ * --prose-h4
833
846
  */
834
847
 
835
848
  .prose {
@@ -893,15 +906,13 @@ small {
893
906
  text-underline-offset: 0.18em;
894
907
  }
895
908
 
896
- @media (hover: hover) {
897
- .prose a:hover {
898
- text-decoration-thickness: 0.12em;
899
- }
909
+ .prose a:hover {
910
+ text-decoration-thickness: 0.12em;
911
+ }
900
912
 
901
- @supports (color: color-mix(in oklch, red, white)) {
902
- .prose a:hover {
903
- color: color-mix(in oklch, var(--primary), var(--text) 15%);
904
- }
913
+ @supports (color: color-mix(in oklch, red, white)) {
914
+ .prose a:hover {
915
+ color: color-mix(in oklch, var(--primary), var(--text) 15%);
905
916
  }
906
917
  }
907
918
 
@@ -921,6 +932,7 @@ small {
921
932
  font-size: 0.875em;
922
933
  }
923
934
 
935
+ /* Visual twin of .key (components/key.css) — keep the two recipes in sync. */
924
936
  .prose kbd {
925
937
  display: inline-flex;
926
938
  align-items: center;
@@ -1025,19 +1037,35 @@ small {
1025
1037
  * Disabled state uses the native disabled attribute for buttons and inputs.
1026
1038
  * Anchors have no native disabled state, so a.btn uses aria-disabled="true"
1027
1039
  * for disabled styling and activation guards.
1040
+ *
1041
+ * Public hooks:
1042
+ * --btn-font-size
1043
+ * --btn-min-size
1044
+ * --btn-pad-x
1045
+ * --btn-radius
1046
+ * --btn-focus-color
1047
+ * --btn-gap
1048
+ * --btn-hover-overlay
1049
+ *
1050
+ * Internal:
1051
+ * --btn-bg
1052
+ * --btn-fg
1053
+ * --btn-border
1054
+ * --btn-focus-ring-color
1055
+ * --btn-border-block-end-color
1056
+ * --btn-border-block-end-width
1057
+ * --spinner-width
1028
1058
  */
1029
1059
 
1030
1060
  /* Intent boundary — inherited intents do not tint this component.
1031
1061
  Local intent classes still win because :where() has zero specificity. */
1032
1062
  :where(.btn) {
1033
- /* @sync intent-boundary */
1034
1063
  --intent: initial;
1035
1064
  --intent-fg: initial;
1036
1065
  --ui-bg: initial;
1037
1066
  --ui-fg: initial;
1038
1067
  --ui-border: initial;
1039
1068
  --ui-hover-bg: initial;
1040
- /* @sync intent-boundary */
1041
1069
  }
1042
1070
 
1043
1071
  .btn {
@@ -1049,17 +1077,17 @@ small {
1049
1077
  --btn-border: var(--ui-border, var(--btn-default-border, transparent));
1050
1078
  --btn-radius: var(--radius);
1051
1079
  --btn-focus-color: var(--intent, var(--focus));
1052
- --btn-hover-bg: var(--ui-hover-bg, var(--btn-bg));
1053
1080
  --btn-hover-overlay: var(--hover-overlay, rgb(0 0 0 / 0.08));
1054
1081
 
1082
+ /* Positioning context for indicators, overlays, and grouped states. */
1055
1083
  position: relative;
1056
1084
  display: inline-flex;
1057
1085
  align-items: center;
1058
1086
  justify-content: center;
1059
- gap: 0.375em;
1087
+ gap: var(--btn-gap, 0.375em);
1060
1088
  min-block-size: var(--btn-min-size);
1061
- padding-block: var(--space-2);
1062
- padding-block: max(var(--space-1), calc((var(--btn-min-size) - 1lh) / 2));
1089
+ padding-block: var(--space-20);
1090
+ padding-block: max(var(--space-10), calc((var(--btn-min-size) - 1lh) / 2));
1063
1091
  padding-inline: var(--btn-pad-x);
1064
1092
  border: var(--border-width) solid var(--btn-border-color, var(--btn-border));
1065
1093
  border-block-end-color: var(
@@ -1084,6 +1112,11 @@ small {
1084
1112
  transform var(--duration);
1085
1113
  }
1086
1114
 
1115
+ .btn.icon-only {
1116
+ inline-size: var(--btn-min-size);
1117
+ padding-inline: 0;
1118
+ }
1119
+
1087
1120
  @supports (color: color-mix(in oklch, red, white)) {
1088
1121
  .btn {
1089
1122
  --btn-focus-ring-color: color-mix(in oklch, var(--btn-focus-color) 28%, transparent);
@@ -1108,13 +1141,40 @@ small {
1108
1141
  --btn-hover-overlay: transparent;
1109
1142
  }
1110
1143
 
1111
- /* The .soft/.outline/.ghost hover recipe (--ui-hover-bg) is shared with
1112
- .badge in variants.css; the overlay darkening for solid/soft is .btn-specific. */
1113
- @media (hover: hover) {
1114
- .btn:not(:where(:disabled, [aria-disabled="true"])):hover {
1115
- background: var(--btn-hover-bg);
1116
- box-shadow: inset 0 0 0 999px var(--btn-hover-overlay);
1144
+ /* Shared hover recipe — sets --ui-hover-bg consumed by the hover rule below.
1145
+ Declared outside :hover: the variables resolve only when the hover rule
1146
+ reads them, so the values are inert until interaction. */
1147
+ .btn:is(.outline, .ghost) {
1148
+ --ui-hover-bg: var(--surface-subtle);
1149
+ }
1150
+
1151
+ @supports (color: color-mix(in oklch, red, white)) {
1152
+ .btn:not(:where(.soft, .outline, .ghost, .link)) {
1153
+ --ui-hover-bg: color-mix(in oklch, var(--intent, var(--neutral)) 88%, black);
1154
+ }
1155
+
1156
+ .btn.soft {
1157
+ --ui-hover-bg: color-mix(
1158
+ in oklch,
1159
+ var(--surface) calc(var(--soft-bg-mix) - var(--soft-hover-alpha)),
1160
+ var(--intent, var(--neutral))
1161
+ );
1162
+ }
1163
+
1164
+ .btn.outline {
1165
+ --ui-hover-bg: color-mix(in oklch, var(--surface-subtle) 92%, var(--intent, var(--neutral)));
1117
1166
  }
1167
+
1168
+ .btn.ghost {
1169
+ --ui-hover-bg: color-mix(in oklch, var(--intent, var(--text)) 8%, transparent);
1170
+ }
1171
+ }
1172
+
1173
+ /* The .soft/.outline/.ghost hover recipe (--ui-hover-bg) is set locally above;
1174
+ the overlay darkening for solid/soft is .btn-specific. */
1175
+ .btn:not(:where(:disabled, [aria-disabled="true"])):hover {
1176
+ background: var(--ui-hover-bg, var(--btn-bg));
1177
+ box-shadow: inset 0 0 0 999px var(--btn-hover-overlay);
1118
1178
  }
1119
1179
 
1120
1180
  .btn:not(:where(:disabled, [aria-disabled="true"])):active {
@@ -1139,16 +1199,14 @@ small {
1139
1199
  /* .btn.circle needs no local rule: the .circle utility (utilities.css) loads
1140
1200
  after components and overrides border-radius at equal specificity. */
1141
1201
 
1142
- @media (hover: hover) {
1143
- .btn.link:not(:where(:disabled, [aria-disabled="true"])):hover {
1144
- background: transparent;
1145
- color: var(--intent, var(--text));
1146
- text-decoration-thickness: 0.12em;
1147
- }
1202
+ .btn.link:not(:where(:disabled, [aria-disabled="true"])):hover {
1203
+ background: transparent;
1204
+ color: var(--intent, var(--text));
1205
+ text-decoration-thickness: 0.12em;
1148
1206
  }
1149
1207
 
1150
1208
  .btn:focus-visible {
1151
- outline: none;
1209
+ outline: 2px solid transparent;
1152
1210
  box-shadow: 0 0 0 var(--focus-ring-width) var(--btn-focus-ring-color, var(--focus-ring));
1153
1211
  }
1154
1212
 
@@ -1182,13 +1240,15 @@ a.btn[aria-disabled="true"] {
1182
1240
  --spinner-width: 0.15em;
1183
1241
  }
1184
1242
 
1185
- /* Toggle */
1186
- /* State must outrank the shared .outline/.ghost hover recipes: a pressed
1187
- button keeps its intent background and foreground while hovered. */
1188
- .btn:is([aria-pressed="true"], [aria-current]:not([aria-current="false"])):not(.link) {
1189
- --btn-bg: var(--intent, var(--primary));
1190
- --btn-fg: var(--intent-fg, var(--primary-fg));
1191
- --btn-border: var(--intent, var(--primary));
1243
+ /* Toggle and current state.
1244
+ State must outrank the shared .outline/.ghost hover recipes: a pressed or
1245
+ current button keeps its intent background and foreground while hovered.
1246
+ aria-current="page" is the pagination current-page marker; aria-pressed
1247
+ covers toggle buttons. */
1248
+ .btn:is([aria-pressed="true"], [aria-current="page"]):not(.link) {
1249
+ --btn-bg: var(--intent, var(--state-selected));
1250
+ --btn-fg: var(--intent-fg, var(--state-selected-fg));
1251
+ --btn-border: var(--intent, var(--state-selected));
1192
1252
  --ui-hover-bg: var(--btn-bg);
1193
1253
  }
1194
1254
 
@@ -1206,18 +1266,16 @@ a.btn[aria-disabled="true"] {
1206
1266
  color: LinkText;
1207
1267
  }
1208
1268
 
1209
- .btn:is([aria-pressed="true"], [aria-current]:not([aria-current="false"])):not(.link) {
1269
+ .btn:is([aria-pressed="true"], [aria-current="page"]):not(.link) {
1210
1270
  border-color: Highlight;
1211
1271
  background: Highlight;
1212
1272
  color: HighlightText;
1213
1273
  forced-color-adjust: none;
1214
1274
  }
1215
1275
 
1216
- @media (hover: hover) {
1217
- .btn:hover:not(:where(:disabled, [aria-disabled="true"], .link)) {
1218
- outline: var(--border-width) solid ButtonText;
1219
- outline-offset: 2px;
1220
- }
1276
+ .btn:hover:not(:where(:disabled, [aria-disabled="true"], .link)) {
1277
+ outline: var(--border-width) solid ButtonText;
1278
+ outline-offset: 2px;
1221
1279
  }
1222
1280
 
1223
1281
  .btn:disabled,
@@ -1235,34 +1293,46 @@ a.btn[aria-disabled="true"] {
1235
1293
  * Soft by default. .solid for counters and notification badges. The dot
1236
1294
  * (.badge:empty) stays solid regardless — a soft dot is pointless.
1237
1295
  * Shared .outline variant stays available.
1296
+ *
1297
+ * Public hooks:
1298
+ * --badge-size
1299
+ * --badge-pad-x
1300
+ * --badge-font-size
1301
+ * --badge-dot-size
1302
+ * --badge-radius
1303
+ *
1304
+ * Internal:
1305
+ * --badge-bg
1306
+ * --badge-fg
1307
+ * --badge-border
1238
1308
  */
1239
1309
  /* Intent boundary — inherited intents do not tint this component.
1240
1310
  Local intent classes still win because :where() has zero specificity. */
1241
1311
  :where(.badge) {
1242
- /* @sync intent-boundary */
1243
1312
  --intent: initial;
1244
1313
  --intent-fg: initial;
1245
1314
  --ui-bg: initial;
1246
1315
  --ui-fg: initial;
1247
1316
  --ui-border: initial;
1248
1317
  --ui-hover-bg: initial;
1249
- /* @sync intent-boundary */
1250
1318
  }
1251
1319
 
1252
1320
  .badge {
1253
- --badge-size: var(--variant-compact-size, 1.5rem);
1321
+ --badge-size: var(--density-compact-size);
1254
1322
  --badge-pad-x: 0.55em;
1255
- --badge-font-size: var(--variant-compact-font-size, 0.8125rem);
1323
+ --badge-font-size: 0.8125rem;
1256
1324
  --badge-dot-size: 0.5rem;
1257
- --badge-bg: var(--ui-bg, transparent);
1325
+ --badge-bg: var(--ui-bg, var(--surface-subtle));
1258
1326
  --badge-fg: var(--ui-fg, var(--intent, var(--text)));
1259
- --badge-border: var(--ui-border, var(--intent, var(--border)));
1260
- --badge-radius: 999px;
1327
+ --badge-border: var(--ui-border, var(--border));
1328
+ --badge-radius: var(--radius-full);
1261
1329
 
1262
1330
  display: inline-flex;
1263
1331
  align-items: center;
1264
1332
  justify-content: center;
1265
1333
  gap: 0.35em;
1334
+ inline-size: fit-content;
1335
+ max-inline-size: 100%;
1266
1336
  min-block-size: var(--badge-size);
1267
1337
  padding-inline: var(--badge-pad-x);
1268
1338
  border: var(--border-width) solid var(--badge-border);
@@ -1331,27 +1401,44 @@ a.btn[aria-disabled="true"] {
1331
1401
  cursor: pointer;
1332
1402
  }
1333
1403
 
1334
- @media (hover: hover) {
1335
- .badge > button:not(:disabled):hover {
1336
- background: var(--hover-overlay);
1337
- }
1404
+ .badge > button:not(:disabled):hover {
1405
+ background: var(--hover-overlay);
1338
1406
  }
1339
1407
 
1340
- /* .solid/.soft/.outline are shared via variants.css, including the
1341
- .soft/.outline hover recipe (--ui-hover-bg). */
1342
- @media (hover: hover) {
1343
- :where(a.badge, button.badge):not(:where(:disabled, [aria-disabled="true"])):hover {
1344
- background: var(--ui-hover-bg, var(--badge-bg));
1408
+ /* Hover recipes — sets --ui-hover-bg consumed by the hover rule below.
1409
+ Declared outside :hover like button.css for the same reason. */
1410
+ :is(a.badge, button.badge).outline {
1411
+ --ui-hover-bg: var(--surface-subtle);
1412
+ }
1413
+
1414
+ @supports (color: color-mix(in oklch, red, white)) {
1415
+ :is(a.badge, button.badge):is(.solid, :empty) {
1416
+ --ui-hover-bg: color-mix(in oklch, var(--intent, var(--neutral)) 88%, black);
1345
1417
  }
1346
1418
 
1347
- :where(a.badge, button.badge):not(:where(:disabled, [aria-disabled="true"], .outline)):hover {
1348
- box-shadow: inset 0 0 0 999px var(--hover-overlay);
1419
+ :is(a.badge, button.badge):where(:not(.solid, .outline, :empty)) {
1420
+ --ui-hover-bg: color-mix(
1421
+ in oklch,
1422
+ var(--surface) calc(var(--soft-bg-mix) - var(--soft-hover-alpha)),
1423
+ var(--intent, var(--neutral))
1424
+ );
1425
+ }
1426
+
1427
+ :is(a.badge, button.badge).outline {
1428
+ --ui-hover-bg: color-mix(in oklch, var(--surface-subtle) 92%, var(--intent, var(--neutral)));
1349
1429
  }
1350
1430
  }
1351
1431
 
1432
+ :where(a.badge, button.badge):not(:where(:disabled, [aria-disabled="true"])):hover {
1433
+ background: var(--ui-hover-bg, var(--badge-bg));
1434
+ }
1435
+
1436
+ :where(a.badge, button.badge):not(:where(:disabled, [aria-disabled="true"], .outline)):hover {
1437
+ box-shadow: inset 0 0 0 999px var(--hover-overlay);
1438
+ }
1439
+
1352
1440
  button.badge {
1353
1441
  cursor: pointer;
1354
- font: inherit;
1355
1442
  }
1356
1443
 
1357
1444
  button.badge:disabled,
@@ -1365,27 +1452,18 @@ a.badge[aria-disabled="true"] {
1365
1452
  }
1366
1453
 
1367
1454
  @media (forced-colors: active) {
1368
- .badge {
1369
- border-color: ButtonText;
1370
- background: Canvas;
1371
- color: CanvasText;
1372
- box-shadow: none;
1373
- }
1374
-
1375
1455
  .badge:empty {
1376
1456
  background: ButtonText;
1377
1457
  }
1378
1458
 
1379
- @media (hover: hover) {
1380
- :where(a.badge, button.badge):hover:not(:where(:disabled, [aria-disabled="true"])) {
1381
- outline: var(--border-width) solid ButtonText;
1382
- outline-offset: 2px;
1383
- }
1459
+ :where(a.badge, button.badge):hover:not(:where(:disabled, [aria-disabled="true"])) {
1460
+ outline: var(--border-width) solid ButtonText;
1461
+ outline-offset: 2px;
1462
+ }
1384
1463
 
1385
- .badge > button:not(:disabled):hover {
1386
- outline: var(--border-width) solid currentColor;
1387
- outline-offset: 1px;
1388
- }
1464
+ .badge > button:not(:disabled):hover {
1465
+ outline: var(--border-width) solid currentColor;
1466
+ outline-offset: 1px;
1389
1467
  }
1390
1468
 
1391
1469
  button.badge:disabled,
@@ -1405,42 +1483,48 @@ a.badge[aria-disabled="true"] {
1405
1483
  * --alert-border-inline-start-color/-width are unset extension points, mirroring
1406
1484
  * --btn-border-block-end-* (button.css), for themes that give alerts a
1407
1485
  * callout-style colored flag on the leading edge instead of a uniform border.
1486
+ *
1487
+ * Public hooks:
1488
+ * --alert-pad-block
1489
+ * --alert-pad-inline
1490
+ * --alert-font-size
1491
+ * --alert-radius
1492
+ * --alert-icon-size
1493
+ * --alert-dismiss-size
1494
+ *
1495
+ * Internal:
1496
+ * --alert-bg
1497
+ * --alert-fg
1498
+ * --alert-border
1499
+ * --alert-dismiss-icon-size
1408
1500
  */
1409
1501
  /* Intent boundary — inherited intents do not tint this component.
1410
1502
  Local intent classes still win because :where() has zero specificity. */
1411
1503
  :where(.alert) {
1412
- /* @sync intent-boundary */
1413
1504
  --intent: initial;
1414
1505
  --intent-fg: initial;
1415
1506
  --ui-bg: initial;
1416
1507
  --ui-fg: initial;
1417
1508
  --ui-border: initial;
1418
- --ui-hover-bg: initial;
1419
- /* @sync intent-boundary */
1420
1509
  }
1421
1510
 
1422
1511
  .alert {
1423
- --alert-pad-block: var(--variant-pad-block, var(--space-3));
1424
- --alert-pad-inline: var(--space-4);
1425
- --alert-font-size: var(--variant-font-size, var(--font-size-md));
1426
- --alert-icon-size: var(--variant-icon-size, 1.25rem);
1427
- --alert-bg: var(--surface-subtle);
1428
- --alert-border: var(--border);
1429
- --ui-bg: var(--alert-bg);
1430
- --ui-border: var(--alert-border);
1431
- --ui-fg: var(--intent, var(--text));
1512
+ --alert-pad-block: var(--space-30);
1513
+ --alert-pad-inline: var(--space-40);
1514
+ --alert-font-size: var(--font-size-md);
1515
+ --alert-icon-size: 1.25rem;
1516
+ --alert-bg: var(--ui-bg, var(--surface-subtle));
1517
+ --alert-fg: var(--ui-fg, var(--intent, var(--text)));
1518
+ --alert-border: var(--ui-border, var(--border));
1432
1519
 
1433
1520
  padding-block: var(--alert-pad-block);
1434
1521
  padding-inline: var(--alert-pad-inline);
1435
- background: var(--ui-bg, transparent);
1436
- color: var(--ui-fg, var(--text));
1437
- border: var(--border-width) solid var(--ui-border, var(--border));
1438
- border-inline-start-color: var(
1439
- --alert-border-inline-start-color,
1440
- var(--ui-border, var(--border))
1441
- );
1522
+ background: var(--alert-bg);
1523
+ color: var(--alert-fg);
1524
+ border: var(--border-width) solid var(--alert-border);
1525
+ border-inline-start-color: var(--alert-border-inline-start-color, var(--alert-border));
1442
1526
  border-inline-start-width: var(--alert-border-inline-start-width, var(--border-width));
1443
- border-radius: var(--radius-lg);
1527
+ border-radius: var(--alert-radius, var(--radius-lg));
1444
1528
  font-size: var(--alert-font-size);
1445
1529
  }
1446
1530
 
@@ -1456,19 +1540,8 @@ a.badge[aria-disabled="true"] {
1456
1540
  margin-block-end: 0;
1457
1541
  }
1458
1542
 
1459
- /* Alerts are soft by default. Use .solid or .outline for stronger emphasis. */
1460
- .alert.solid {
1461
- --ui-bg: var(--intent, var(--neutral));
1462
- --ui-fg: var(--intent-fg, var(--neutral-fg));
1463
- --ui-border: transparent;
1464
- }
1465
-
1466
- .alert.outline {
1467
- --ui-bg: transparent;
1468
- --ui-fg: var(--intent, var(--text));
1469
- --ui-border: var(--intent, var(--border));
1470
- }
1471
-
1543
+ /* Alerts are soft by default. .solid and .outline need no local rules — the
1544
+ shared variant plumbing in variants.css sets --ui-* that alert consumes. */
1472
1545
  @supports (color: color-mix(in oklch, red, white)) {
1473
1546
  .alert:where(:not(.solid, .outline, .callout)) {
1474
1547
  /* @sync soft-recipe */
@@ -1482,8 +1555,8 @@ a.badge[aria-disabled="true"] {
1482
1555
  }
1483
1556
  }
1484
1557
 
1485
- /* Decorative leading icon */
1486
- .alert > :first-child[aria-hidden="true"] {
1558
+ /* Decorative leading icon — use .alert-icon on the icon element. */
1559
+ .alert > .alert-icon {
1487
1560
  display: inline-block;
1488
1561
  flex: none;
1489
1562
  inline-size: var(--alert-icon-size);
@@ -1493,33 +1566,84 @@ a.badge[aria-disabled="true"] {
1493
1566
  vertical-align: -0.125em;
1494
1567
  }
1495
1568
 
1569
+ /* Dismiss — compact close button for inline alerts. Transparent at rest with
1570
+ a reduced opacity glyph so it stays secondary inside an already-colored
1571
+ alert; hover reinforces it via the shared overlay. Uses the shared
1572
+ --icon-close mask, so no icon font or text glyph is required in the markup.
1573
+ Standard alerts place it as a direct trailing child; admonitions place it
1574
+ inside .alert-title (see below). */
1575
+ .alert-dismiss {
1576
+ --alert-dismiss-size: 1.75rem;
1577
+ --alert-dismiss-icon-size: 0.875rem;
1578
+
1579
+ display: inline-grid;
1580
+ place-items: center;
1581
+ inline-size: var(--alert-dismiss-size);
1582
+ block-size: var(--alert-dismiss-size);
1583
+ flex: none;
1584
+ padding: 0;
1585
+ border: 0;
1586
+ border-radius: var(--radius);
1587
+ background: transparent;
1588
+ color: currentColor;
1589
+ opacity: 0.65;
1590
+ cursor: pointer;
1591
+ }
1592
+
1593
+ .alert-dismiss::before {
1594
+ content: "";
1595
+ inline-size: var(--alert-dismiss-icon-size);
1596
+ block-size: var(--alert-dismiss-icon-size);
1597
+ background: currentColor;
1598
+ -webkit-mask: var(--icon-close) center / contain no-repeat;
1599
+ mask: var(--icon-close) center / contain no-repeat;
1600
+ }
1601
+
1602
+ @media (hover: hover) {
1603
+ .alert-dismiss:hover {
1604
+ background: var(--hover-overlay);
1605
+ opacity: 1;
1606
+ }
1607
+ }
1608
+
1609
+ /* Grid anatomy — activated by a leading icon, a trailing dismiss button, or
1610
+ both. Icon and dismiss are pinned to the outer columns; anything else (the
1611
+ message content) flows into the remaining text column. */
1496
1612
  @supports selector(.alert:has(> *)) {
1497
- .alert:has(> :first-child[aria-hidden="true"]) {
1613
+ .alert:has(> .alert-icon, > .alert-dismiss) {
1498
1614
  display: grid;
1499
- grid-template-columns: auto minmax(0, 1fr);
1615
+ grid-template-columns: minmax(0, 1fr);
1500
1616
  align-items: var(--items-align, start);
1501
- column-gap: var(--space-3);
1617
+ column-gap: var(--space-30);
1618
+ }
1619
+
1620
+ .alert:has(> .alert-icon) {
1621
+ grid-template-columns: auto minmax(0, 1fr);
1502
1622
  }
1503
1623
 
1504
- .alert:has(> :first-child[aria-hidden="true"]) > :first-child {
1624
+ .alert:has(> .alert-icon) > .alert-icon {
1505
1625
  grid-column: 1;
1506
1626
  grid-row: 1 / -1;
1507
1627
  display: inline-grid;
1508
1628
  place-items: center;
1509
- inline-size: var(--alert-icon-size);
1510
- block-size: var(--alert-icon-size);
1511
1629
  margin-block-start: 0.125em;
1512
- font-size: var(--alert-icon-size);
1513
- line-height: 1;
1514
1630
  }
1515
1631
 
1516
- .alert:has(> :first-child[aria-hidden="true"]) > :first-child:is(svg, i) {
1517
- inline-size: var(--alert-icon-size);
1518
- block-size: var(--alert-icon-size);
1632
+ .alert:has(> .alert-icon) > :not(.alert-icon, .alert-dismiss) {
1633
+ grid-column: 2;
1519
1634
  }
1520
1635
 
1521
- .alert:has(> :first-child[aria-hidden="true"]) > :not(:first-child) {
1522
- grid-column: 2;
1636
+ .alert:has(> .alert-dismiss) {
1637
+ grid-template-columns: minmax(0, 1fr) auto;
1638
+ }
1639
+
1640
+ .alert:has(> .alert-icon):has(> .alert-dismiss) {
1641
+ grid-template-columns: auto minmax(0, 1fr) auto;
1642
+ }
1643
+
1644
+ .alert:has(> .alert-dismiss) > .alert-dismiss {
1645
+ grid-column: -2 / -1;
1646
+ grid-row: 1;
1523
1647
  }
1524
1648
  }
1525
1649
 
@@ -1534,100 +1658,84 @@ a.badge[aria-disabled="true"] {
1534
1658
  Canvas backplate (unreadable) — so .solid intentionally falls back to the
1535
1659
  default look. */
1536
1660
 
1537
- /* Callout — neutral surface with an inset accent band on the leading edge.
1538
- The ::before pseudo is absolutely positioned and inset to respect the
1539
- outer border-radius, so it never needs its own radius. */
1661
+ /* Callout — neutral surface with a thick accent border on the leading edge.
1662
+ The thickness follows the shared --alert-border-inline-start-width hook. */
1540
1663
  .alert.callout {
1541
1664
  --alert-bg: var(--surface-subtle);
1542
- --ui-bg: var(--alert-bg);
1543
- --ui-fg: var(--text);
1544
- --ui-border: var(--border);
1545
- position: relative;
1546
- padding-inline-start: calc(var(--alert-pad-inline) + var(--space-3));
1547
- }
1548
-
1549
- .alert.callout::before {
1550
- content: "";
1551
- position: absolute;
1552
- inset-block: var(--space-2);
1553
- inset-inline-start: var(--space-2);
1554
- inline-size: 0.25rem;
1555
- border-radius: var(--radius-full);
1556
- background: var(--intent, var(--neutral));
1665
+ --alert-fg: var(--text);
1666
+ border: 0;
1667
+ border-inline-start: var(--alert-border-inline-start-width, 4px) solid
1668
+ var(--alert-border-inline-start-color, var(--intent, var(--neutral)));
1557
1669
  }
1558
1670
 
1559
- /* Admonition — structured title/body anatomy with a tinted title band.
1560
- The root is layout-neutralized: no padding, no gap, no icon grid. */
1671
+ /* Admonition — structured static box with a tinted title bar and a body
1672
+ that rests on the page surface. Not collapsible — use .accordion inside
1673
+ the body if the content needs to be expandable. */
1561
1674
  .alert.admonition {
1562
1675
  padding: 0;
1563
- gap: 0;
1564
1676
  display: block;
1565
1677
  overflow: clip;
1678
+ border-color: var(--intent, var(--border));
1566
1679
  }
1567
1680
 
1568
- .alert-title {
1681
+ .alert.admonition > .alert-title {
1682
+ display: flex;
1683
+ align-items: center;
1684
+ gap: var(--space-20);
1569
1685
  padding-block: var(--alert-pad-block);
1570
1686
  padding-inline: var(--alert-pad-inline);
1687
+ border-start-start-radius: calc(var(--alert-radius, var(--radius-lg)) - var(--border-width));
1688
+ border-start-end-radius: calc(var(--alert-radius, var(--radius-lg)) - var(--border-width));
1689
+ }
1571
1690
 
1572
- @supports (color: color-mix(in oklch, red, white)) {
1691
+ /* Dismiss inside an admonition title bar rides the inline end. */
1692
+ .alert.admonition > .alert-title > .alert-dismiss {
1693
+ margin-inline-start: auto;
1694
+ }
1695
+
1696
+ @supports (color: color-mix(in oklch, red, white)) {
1697
+ .alert.admonition > .alert-title {
1573
1698
  background: color-mix(
1574
1699
  in oklch,
1575
1700
  var(--surface) var(--soft-bg-mix),
1576
1701
  var(--intent, var(--neutral))
1577
1702
  );
1578
1703
  }
1704
+ }
1579
1705
 
1580
- @supports not (color: color-mix(in oklch, red, white)) {
1706
+ @supports not (color: color-mix(in oklch, red, white)) {
1707
+ .alert.admonition > .alert-title {
1581
1708
  background: var(--intent, var(--neutral));
1709
+ color: var(--intent-fg, var(--neutral-fg));
1582
1710
  }
1583
-
1584
- border-start-start-radius: calc(var(--radius-lg) - var(--border-width));
1585
- border-start-end-radius: calc(var(--radius-lg) - var(--border-width));
1586
1711
  }
1587
1712
 
1588
- .alert-title:where(summary) {
1589
- cursor: pointer;
1590
- user-select: none;
1591
- list-style: none;
1592
- }
1593
-
1594
- .alert-title:where(summary)::-webkit-details-marker {
1595
- display: none;
1596
- }
1597
-
1598
- .alert-title[aria-hidden] {
1599
- display: inline-flex;
1600
- gap: var(--space-2);
1601
- align-items: center;
1602
- }
1603
-
1604
- .alert-body {
1713
+ .alert.admonition > .alert-body {
1605
1714
  padding-block: var(--alert-pad-block);
1606
1715
  padding-inline: var(--alert-pad-inline);
1716
+ background: var(--surface);
1607
1717
  color: var(--text);
1608
1718
  }
1609
1719
 
1610
- .alert-body > :first-child {
1720
+ .alert.admonition > .alert-body > :first-child {
1611
1721
  margin-block-start: 0;
1612
1722
  }
1613
1723
 
1614
- .alert-body > :last-child {
1724
+ .alert.admonition > .alert-body > :last-child {
1615
1725
  margin-block-end: 0;
1616
1726
  }
1617
1727
 
1618
- details.alert.admonition[open] .alert-title {
1619
- border-end-start-radius: 0;
1620
- border-end-end-radius: 0;
1621
- }
1622
-
1623
1728
  /*
1624
1729
  * Actions — a horizontal action-button cluster.
1625
1730
  *
1626
1731
  * Composes with .btn (or any inline element) and feeds layout via the shared
1627
1732
  * --gap token.
1733
+ *
1734
+ * Internal:
1735
+ * --gap (local value for the shared gap token)
1628
1736
  */
1629
1737
  .actions {
1630
- --gap: var(--space-2);
1738
+ --gap: var(--space-20);
1631
1739
 
1632
1740
  display: flex;
1633
1741
  flex-wrap: wrap;
@@ -1650,28 +1758,36 @@ details.alert.admonition[open] .alert-title {
1650
1758
  * Surface, border, radius, padding, density, and elevation stay local here.
1651
1759
  * Direct header/footer children are structural slots. Page layout remains the
1652
1760
  * job of markup and layout primitives outside the card.
1761
+ *
1762
+ * Public hooks:
1763
+ * --card-pad
1764
+ * --card-radius
1765
+ * --card-max-inline-size
1766
+ *
1767
+ * Child contract:
1768
+ * Direct .bleed children escape --card-pad (negative inset, wider media);
1769
+ * edge children inherit the card corners. .bleed must be a direct child.
1770
+ *
1771
+ * Internal:
1772
+ * --heading
1773
+ * --busy-overlay-bg
1653
1774
  */
1654
1775
  /* Intent boundary — inherited intents do not tint this component.
1655
1776
  Local intent classes still win because :where() has zero specificity. */
1656
1777
  :where(.card) {
1657
- /* @sync intent-boundary */
1658
- --intent: initial;
1659
- --intent-fg: initial;
1660
1778
  --ui-bg: initial;
1661
1779
  --ui-fg: initial;
1662
1780
  --ui-border: initial;
1663
- --ui-hover-bg: initial;
1664
- /* @sync intent-boundary */
1665
1781
  }
1666
1782
 
1667
1783
  .card {
1668
- --card-pad: var(--space-4);
1784
+ --card-pad: var(--space-40);
1669
1785
  --card-radius: var(--radius-lg);
1670
1786
  --card-max-inline-size: none;
1671
1787
 
1672
1788
  max-inline-size: var(--card-max-inline-size);
1673
1789
  padding: var(--card-pad);
1674
- background: var(--ui-bg, var(--surface));
1790
+ background: var(--ui-bg, var(--surface-raised));
1675
1791
  color: var(--ui-fg, var(--text));
1676
1792
  border: var(--border-width) solid var(--ui-border, var(--border));
1677
1793
  border-radius: var(--card-radius);
@@ -1686,7 +1802,7 @@ details.alert.admonition[open] .alert-title {
1686
1802
  flex-wrap: wrap;
1687
1803
  align-items: var(--items-align, center);
1688
1804
  justify-content: space-between;
1689
- gap: var(--space-3);
1805
+ gap: var(--space-30);
1690
1806
  min-inline-size: 0;
1691
1807
  }
1692
1808
 
@@ -1713,16 +1829,8 @@ details.alert.admonition[open] .alert-title {
1713
1829
  .card.subtle {
1714
1830
  background: var(--surface-subtle);
1715
1831
  }
1716
- .card.inverted {
1717
- background: var(--surface-solid);
1718
- color: var(--surface);
1719
- border-color: transparent;
1720
- --heading: var(--surface);
1721
- --busy-overlay-bg: var(--surface-solid);
1722
- }
1723
-
1724
1832
  .card.compact {
1725
- --card-pad: var(--space-2);
1833
+ --card-pad: var(--space-20);
1726
1834
  }
1727
1835
 
1728
1836
  /* Full-bleed children */
@@ -1765,26 +1873,6 @@ details.alert.admonition[open] .alert-title {
1765
1873
  padding: var(--card-pad);
1766
1874
  }
1767
1875
 
1768
- @media (forced-colors: active) {
1769
- .card {
1770
- border-color: ButtonText;
1771
- background: Canvas;
1772
- color: CanvasText;
1773
- }
1774
-
1775
- .card.raised {
1776
- box-shadow: none;
1777
- }
1778
-
1779
- .card.inverted {
1780
- --heading: CanvasText;
1781
-
1782
- border-color: ButtonText;
1783
- background: Canvas;
1784
- color: CanvasText;
1785
- }
1786
- }
1787
-
1788
1876
  /*
1789
1877
  * Surface — shared modal scrim and backdrop for flyout and drawer.
1790
1878
  *
@@ -1806,7 +1894,7 @@ details.alert.admonition[open] .alert-title {
1806
1894
  z-index: calc(var(--z-menu) - 1);
1807
1895
  background: var(--backdrop-fill);
1808
1896
  opacity: 1;
1809
- transition: opacity var(--duration) var(--ease, ease);
1897
+ transition: opacity var(--duration) var(--ease);
1810
1898
  }
1811
1899
 
1812
1900
  .surface-backdrop[hidden] {
@@ -1823,28 +1911,40 @@ details.alert.admonition[open] .alert-title {
1823
1911
  @supports (transition-behavior: allow-discrete) {
1824
1912
  .surface-backdrop {
1825
1913
  transition:
1826
- opacity var(--duration) var(--ease, ease),
1914
+ opacity var(--duration) var(--ease),
1827
1915
  display var(--duration) allow-discrete;
1828
1916
  }
1829
1917
  }
1830
1918
 
1831
- @media (forced-colors: active) {
1832
- .surface-backdrop {
1833
- background: Canvas;
1834
- }
1835
- }
1836
-
1837
1919
  /*
1838
1920
  * Modal shell styling for native dialog elements.
1839
1921
  *
1840
1922
  * Spacing, radius, transitions, and focus treatment come from shared tokens.
1841
1923
  * Opening behavior and intent-specific decisions stay outside this component.
1924
+ *
1925
+ * Public hooks:
1926
+ * --modal-size
1927
+ * --dialog-viewport-gap
1928
+ * --dialog-icon-size
1929
+ * --dialog-icon-glyph-size
1930
+ *
1931
+ * Internal:
1932
+ * --dialog-close-icon-size
1933
+ * --dialog-viewport-gap-2x
1842
1934
  */
1843
1935
  dialog.modal {
1844
1936
  --modal-size: 32rem;
1937
+ --dialog-viewport-gap: var(--space-40);
1938
+ /* calc() multiplication is not in the supported browser range, so the
1939
+ doubled gap is spelled out with an additive calc. */
1940
+ --dialog-viewport-gap-2x: calc(var(--dialog-viewport-gap) + var(--dialog-viewport-gap));
1845
1941
 
1846
- max-inline-size: min(100% - 2rem, var(--modal-size));
1847
- max-block-size: calc(var(--viewport-block) - 2rem);
1942
+ /* Keep the surface anchored to the viewport in the top layer. This also
1943
+ provides the containing block for the absolutely positioned close button. */
1944
+ position: fixed;
1945
+
1946
+ max-inline-size: min(calc(100% - var(--dialog-viewport-gap-2x)), var(--modal-size));
1947
+ max-block-size: calc(var(--viewport-block) - var(--dialog-viewport-gap-2x));
1848
1948
  border: var(--border-width) solid var(--border);
1849
1949
  border-radius: var(--radius-lg);
1850
1950
  background: var(--surface-raised);
@@ -1868,11 +1968,18 @@ dialog.modal {
1868
1968
  dialog.modal > form,
1869
1969
  dialog.modal > .stack {
1870
1970
  display: grid;
1871
- gap: var(--space-4);
1872
- max-block-size: calc(var(--viewport-block) - 2rem);
1971
+ gap: var(--space-40);
1972
+ max-block-size: calc(var(--viewport-block) - var(--dialog-viewport-gap-2x));
1873
1973
  overflow: auto;
1874
1974
  overscroll-behavior: contain;
1875
- padding: var(--space-4);
1975
+ padding: var(--space-40);
1976
+ }
1977
+
1978
+ /* Keep the dialog off the viewport edges on small screens. */
1979
+ @media (max-width: 30rem) {
1980
+ dialog.modal {
1981
+ --dialog-viewport-gap: var(--space-30);
1982
+ }
1876
1983
  }
1877
1984
 
1878
1985
  dialog.modal.scrollable > form,
@@ -1898,6 +2005,13 @@ dialog.modal footer > :last-child {
1898
2005
  margin-block-end: 0;
1899
2006
  }
1900
2007
 
2008
+ /* The close button is anchored to the dialog at top inline-end, out of the
2009
+ content flow; headers reserve inline room so titles never run under it. */
2010
+ dialog.modal > form > header,
2011
+ dialog.modal > .stack > header {
2012
+ padding-inline-end: calc(var(--control-size) + var(--space-30));
2013
+ }
2014
+
1901
2015
  dialog.modal footer {
1902
2016
  display: flex;
1903
2017
  flex-wrap: wrap;
@@ -1905,6 +2019,65 @@ dialog.modal footer {
1905
2019
  gap: var(--gap);
1906
2020
  }
1907
2021
 
2022
+ /* Confirmation layout — a focused message followed by a visually distinct
2023
+ action band. The media primitive owns icon/content alignment; this variant
2024
+ only removes the standard form inset so both regions can own their padding. */
2025
+ dialog.modal.dialog-confirmation > form {
2026
+ gap: 0;
2027
+ padding: 0;
2028
+ }
2029
+
2030
+ dialog.modal.dialog-confirmation > form > .media {
2031
+ gap: var(--space-40);
2032
+ padding: var(--space-50);
2033
+ }
2034
+
2035
+ dialog.modal.dialog-confirmation > form > footer {
2036
+ padding: var(--space-40) var(--space-50);
2037
+ border-end-start-radius: calc(var(--radius-lg) - var(--border-width));
2038
+ border-end-end-radius: calc(var(--radius-lg) - var(--border-width));
2039
+ background: var(--surface-subtle);
2040
+ }
2041
+
2042
+ /* Intent boundary — inherited intents do not tint the icon well. A local
2043
+ intent plus .soft opts into the shared soft-variant recipe. */
2044
+ :where(.dialog-icon) {
2045
+ --intent: initial;
2046
+ --intent-fg: initial;
2047
+ --ui-bg: initial;
2048
+ --ui-fg: initial;
2049
+ }
2050
+
2051
+ .dialog-icon {
2052
+ --dialog-icon-size: 3rem;
2053
+ --dialog-icon-glyph-size: 1.5rem;
2054
+
2055
+ display: inline-grid;
2056
+ place-items: center;
2057
+ inline-size: var(--dialog-icon-size);
2058
+ block-size: var(--dialog-icon-size);
2059
+ flex: none;
2060
+ border-radius: var(--radius-full);
2061
+ background: var(--ui-bg, var(--surface-subtle));
2062
+ color: var(--ui-fg, var(--intent, var(--text-muted)));
2063
+ }
2064
+
2065
+ .dialog-icon > * {
2066
+ display: inline-grid;
2067
+ place-items: center;
2068
+ inline-size: var(--dialog-icon-glyph-size);
2069
+ block-size: var(--dialog-icon-glyph-size);
2070
+ font-size: var(--dialog-icon-glyph-size);
2071
+ line-height: 1;
2072
+ }
2073
+
2074
+ @media (max-width: 30rem) {
2075
+ dialog.modal.dialog-confirmation > form > .media,
2076
+ dialog.modal.dialog-confirmation > form > footer {
2077
+ padding: var(--space-40);
2078
+ }
2079
+ }
2080
+
1908
2081
  dialog.modal[open] {
1909
2082
  opacity: 1;
1910
2083
  transform: none;
@@ -1960,7 +2133,7 @@ html.has-modal-open.had-scrollbar {
1960
2133
 
1961
2134
  @media (prefers-reduced-motion: no-preference) {
1962
2135
  dialog.modal.is-static {
1963
- animation: actual-modal-static 180ms var(--ease);
2136
+ animation: actual-dialog-static var(--duration-slow) var(--ease);
1964
2137
  }
1965
2138
 
1966
2139
  ::view-transition-group(actual-dialog) {
@@ -1969,7 +2142,7 @@ html.has-modal-open.had-scrollbar {
1969
2142
  }
1970
2143
  }
1971
2144
 
1972
- @keyframes actual-modal-static {
2145
+ @keyframes actual-dialog-static {
1973
2146
  0%,
1974
2147
  100% {
1975
2148
  transform: none;
@@ -1995,6 +2168,70 @@ html.has-modal-open.had-scrollbar {
1995
2168
  dialog.modal::backdrop {
1996
2169
  background: Canvas;
1997
2170
  }
2171
+
2172
+ .dialog-icon {
2173
+ border: var(--border-width) solid CanvasText;
2174
+ }
2175
+ }
2176
+
2177
+ /* Close controls. A close button is an action attached to a surface, so it
2178
+ never participates in the content flow. The modal and the drawer share the
2179
+ same simple placement — absolute at the panel's top inline-end, inside the
2180
+ surface — so both close buttons look and behave identically. The glyph comes
2181
+ from --icon-close, masked to currentColor, so no icon font or text glyph is
2182
+ required in the markup. */
2183
+ .dialog-close,
2184
+ .drawer-close {
2185
+ --dialog-close-icon-size: 1rem;
2186
+
2187
+ display: inline-grid;
2188
+ place-items: center;
2189
+ padding: 0;
2190
+ border: 0;
2191
+ border-radius: var(--radius);
2192
+ color: var(--text-muted);
2193
+ cursor: pointer;
2194
+
2195
+ /* Out of flow, anchored to the positioned dialog surface, not the header or
2196
+ any scroll container inside it. */
2197
+ position: absolute;
2198
+ z-index: 1;
2199
+ inset-block-start: var(--space-20);
2200
+ inset-inline-end: var(--space-20);
2201
+
2202
+ inline-size: var(--control-size);
2203
+ block-size: var(--control-size);
2204
+
2205
+ background: var(--surface-subtle);
2206
+ }
2207
+
2208
+ .dialog-close::before,
2209
+ .drawer-close::before {
2210
+ content: "";
2211
+ inline-size: var(--dialog-close-icon-size);
2212
+ block-size: var(--dialog-close-icon-size);
2213
+ background: currentColor;
2214
+ -webkit-mask: var(--icon-close) center / contain no-repeat;
2215
+ mask: var(--icon-close) center / contain no-repeat;
2216
+ }
2217
+
2218
+ @media (hover: hover) {
2219
+ .dialog-close:hover,
2220
+ .drawer-close:hover {
2221
+ box-shadow: inset 0 0 0 999px var(--hover-overlay);
2222
+ color: var(--text);
2223
+ }
2224
+ }
2225
+
2226
+ .dialog-close:active,
2227
+ .drawer-close:active {
2228
+ box-shadow: inset 0 0 0 999px var(--hover-overlay);
2229
+ }
2230
+
2231
+ .dialog-close:focus-visible,
2232
+ .drawer-close:focus-visible {
2233
+ outline: 2px solid transparent;
2234
+ box-shadow: var(--focus-ring-shadow);
1998
2235
  }
1999
2236
 
2000
2237
  /*
@@ -2005,16 +2242,30 @@ html.has-modal-open.had-scrollbar {
2005
2242
  * discrete display/overlay transitions. The drawer is a layout shell only —
2006
2243
  * navigation content inside uses .nav-link and .accordion, which carry their
2007
2244
  * own link/summary styling.
2245
+ *
2246
+ * Public hooks:
2247
+ * --drawer-size
2248
+ * --drawer-pad
2249
+ *
2250
+ * Internal:
2251
+ * --drawer-closed-translate (side/RTL positioning state)
2008
2252
  */
2009
2253
  dialog.drawer {
2010
2254
  --drawer-size: 20rem;
2011
- --drawer-pad: var(--space-4);
2255
+ --drawer-pad: var(--space-40);
2012
2256
  --drawer-closed-translate: -100%;
2013
2257
 
2258
+ /* Keep the surface anchored to the viewport in the top layer. This also
2259
+ provides the containing block for the absolutely positioned close button.
2260
+ A percentage height resolves against the fixed containing viewport, while
2261
+ dynamic viewport units can be stale in embedded previews. */
2262
+ position: fixed;
2263
+ inset-block-start: 0;
2264
+
2014
2265
  inline-size: min(calc(var(--viewport-inline) * 0.9), var(--drawer-size));
2015
- block-size: var(--viewport-block);
2266
+ block-size: 100%;
2016
2267
  max-inline-size: none;
2017
- max-block-size: var(--viewport-block);
2268
+ max-block-size: 100%;
2018
2269
  margin: 0;
2019
2270
  padding: var(--drawer-pad);
2020
2271
  border: 0;
@@ -2033,7 +2284,7 @@ dialog.drawer {
2033
2284
  dialog.drawer[open] {
2034
2285
  display: grid;
2035
2286
  grid-template-rows: auto minmax(0, 1fr) auto;
2036
- gap: var(--space-4);
2287
+ gap: var(--space-40);
2037
2288
  opacity: 1;
2038
2289
  transform: translateX(0);
2039
2290
  }
@@ -2104,20 +2355,23 @@ dialog.drawer nav {
2104
2355
  }
2105
2356
 
2106
2357
  @media (forced-colors: active) {
2107
- dialog.drawer {
2108
- border-inline-end-color: ButtonText;
2358
+ dialog.drawer::backdrop {
2109
2359
  background: Canvas;
2110
- color: CanvasText;
2111
- box-shadow: none;
2112
2360
  }
2361
+ }
2113
2362
 
2114
- dialog.drawer[data-side="end"] {
2115
- border-inline-start-color: ButtonText;
2363
+ @media (prefers-reduced-motion: no-preference) {
2364
+ dialog.drawer.is-static {
2365
+ animation: actual-dialog-static var(--duration-slow) var(--ease);
2116
2366
  }
2367
+ }
2117
2368
 
2118
- dialog.drawer::backdrop {
2119
- background: Canvas;
2120
- }
2369
+ /* The close button shares the modal's interior placement (modal.css): absolute
2370
+ at the panel's top inline-end, out of the content flow. The header reserves
2371
+ inline room so the title never runs under it. */
2372
+ dialog.drawer > header,
2373
+ dialog.drawer > form > header {
2374
+ padding-inline-end: calc(var(--control-size) + var(--space-30));
2121
2375
  }
2122
2376
 
2123
2377
 
@@ -2133,7 +2387,16 @@ dialog.drawer nav {
2133
2387
  * .is-sheet marks "auto" resolved to bottom-sheet presentation on coarse
2134
2388
  * pointers. Authors opt into mobile behavior with
2135
2389
  * [data-flyout-mobile="sheet|auto|none|anchored"].
2136
- * --flyout-trigger-width is set by surface.js while an open flyout is positioned.
2390
+ * --surface-anchor-width is set by surface.js while an open flyout is positioned.
2391
+ *
2392
+ * Public hooks:
2393
+ * --flyout-inline-size
2394
+ * --flyout-max-inline-size
2395
+ *
2396
+ * Internal:
2397
+ * --menu-item-size (derived from --control-size for menu rows)
2398
+ * --available-height (measured by floating.js)
2399
+ * --surface-anchor-width (measured by surface.js)
2137
2400
  */
2138
2401
 
2139
2402
  .flyout-trigger {
@@ -2156,28 +2419,32 @@ dialog.drawer nav {
2156
2419
  }
2157
2420
 
2158
2421
  .flyout {
2422
+ --menu-item-size: var(--control-size);
2159
2423
  z-index: var(--z-menu);
2160
2424
  /* No-JS fallback: below the trigger inside the relative .flyout-trigger.
2161
2425
  surface.js switches an open flyout to position: fixed with measured
2162
2426
  coordinates; this only covers a flyout revealed without the runtime. */
2163
2427
  position: absolute;
2164
- inset-block-start: calc(100% + var(--space-1));
2428
+ inset-block-start: calc(100% + var(--space-10));
2165
2429
  inset-inline-start: 0;
2166
2430
  display: grid;
2167
- gap: var(--space-1);
2431
+ gap: var(--space-10);
2168
2432
  inline-size: var(--flyout-inline-size, max-content);
2169
2433
  min-inline-size: min(
2170
- var(--flyout-trigger-width, 0px),
2171
- min(var(--flyout-max-inline-size, 20rem), calc(var(--viewport-inline) - var(--space-6)))
2434
+ var(--surface-anchor-width, 0px),
2435
+ min(var(--flyout-max-inline-size, 20rem), calc(var(--viewport-inline) - var(--space-60)))
2172
2436
  );
2173
2437
  max-inline-size: min(
2174
2438
  var(--flyout-max-inline-size, 20rem),
2175
- calc(var(--viewport-inline) - var(--space-6))
2439
+ calc(var(--viewport-inline) - var(--space-60))
2440
+ );
2441
+ max-block-size: min(
2442
+ 24rem,
2443
+ var(--available-height, calc(var(--viewport-block) - var(--space-60)))
2176
2444
  );
2177
- max-block-size: min(24rem, var(--available-height, calc(var(--viewport-block) - var(--space-6))));
2178
2445
  overflow: auto;
2179
2446
  margin: 0;
2180
- padding: var(--space-1);
2447
+ padding: var(--space-10);
2181
2448
  border: var(--border-width) solid var(--border);
2182
2449
  border-radius: var(--radius);
2183
2450
  background: var(--surface-raised);
@@ -2186,9 +2453,9 @@ dialog.drawer nav {
2186
2453
  opacity: 1;
2187
2454
  scale: 1;
2188
2455
  transition:
2189
- opacity var(--duration-fast, var(--duration)) var(--ease, ease),
2190
- scale var(--duration-fast, var(--duration)) var(--ease, ease),
2191
- translate var(--duration-fast, var(--duration)) var(--ease, ease);
2456
+ opacity var(--duration-fast) var(--ease),
2457
+ scale var(--duration-fast) var(--ease),
2458
+ translate var(--duration-fast) var(--ease);
2192
2459
  }
2193
2460
 
2194
2461
  .flyout.is-open:not(.is-sheet) {
@@ -2205,6 +2472,7 @@ dialog.drawer nav {
2205
2472
  /* Bottom-sheet presentation. surface.js toggles .is-sheet when the flyout is
2206
2473
  opened on coarse pointer / narrow viewports. */
2207
2474
  .flyout.is-sheet {
2475
+ --menu-item-size: var(--control-size-lg);
2208
2476
  position: fixed;
2209
2477
  inset-inline: 0;
2210
2478
  inset-block-start: auto;
@@ -2215,8 +2483,8 @@ dialog.drawer nav {
2215
2483
  max-block-size: min(calc(var(--viewport-block) * 0.75), 32rem);
2216
2484
  overflow: auto;
2217
2485
  grid-template-columns: 1fr;
2218
- padding: max(var(--space-3), env(safe-area-inset-top)) var(--space-3)
2219
- max(var(--space-3), env(safe-area-inset-bottom));
2486
+ padding: max(var(--space-30), env(safe-area-inset-top)) var(--space-30)
2487
+ max(var(--space-30), env(safe-area-inset-bottom));
2220
2488
  border-end-start-radius: 0;
2221
2489
  border-end-end-radius: 0;
2222
2490
  }
@@ -2229,23 +2497,23 @@ dialog.drawer nav {
2229
2497
 
2230
2498
  .flyout.is-sheet[hidden] {
2231
2499
  scale: 1;
2232
- translate: 0 var(--space-3);
2500
+ translate: 0 var(--space-30);
2233
2501
  }
2234
2502
 
2235
2503
  @starting-style {
2236
2504
  .flyout.is-sheet.is-open {
2237
2505
  scale: 1;
2238
- translate: 0 var(--space-3);
2506
+ translate: 0 var(--space-30);
2239
2507
  }
2240
2508
  }
2241
2509
 
2242
2510
  @supports (transition-behavior: allow-discrete) {
2243
2511
  .flyout {
2244
2512
  transition:
2245
- opacity var(--duration-fast, var(--duration)) var(--ease, ease),
2246
- scale var(--duration-fast, var(--duration)) var(--ease, ease),
2247
- translate var(--duration-fast, var(--duration)) var(--ease, ease),
2248
- display var(--duration-fast, var(--duration)) allow-discrete;
2513
+ opacity var(--duration-fast) var(--ease),
2514
+ scale var(--duration-fast) var(--ease),
2515
+ translate var(--duration-fast) var(--ease),
2516
+ display var(--duration-fast) allow-discrete;
2249
2517
  }
2250
2518
  }
2251
2519
 
@@ -2255,7 +2523,7 @@ dialog.drawer nav {
2255
2523
 
2256
2524
  .flyout > :where(section, footer) {
2257
2525
  min-inline-size: 0;
2258
- padding: var(--space-2);
2526
+ padding: var(--space-20);
2259
2527
  }
2260
2528
 
2261
2529
  .flyout > section > :where(h1, h2, h3, h4, h5, h6) {
@@ -2267,8 +2535,8 @@ dialog.drawer nav {
2267
2535
 
2268
2536
  .flyout > section > ul {
2269
2537
  display: grid;
2270
- gap: var(--space-1);
2271
- margin: var(--space-1) 0 0;
2538
+ gap: var(--space-10);
2539
+ margin: var(--space-10) 0 0;
2272
2540
  padding: 0;
2273
2541
  list-style: none;
2274
2542
  }
@@ -2277,91 +2545,133 @@ dialog.drawer nav {
2277
2545
  margin: 0;
2278
2546
  }
2279
2547
 
2280
- /* Item contract: a button, link, or menuitem placed directly in the flyout
2281
- or directly in any of its <li> (bare list, or grouped section > ul > li).
2282
- menu.js mirrors this contract in MENU_ITEM_SELECTOR — keep them in sync.
2283
- :where(li) keeps specificity at a single class (0,1,0) so consumer
2284
- overrides behave the same as a plain .flyout > rule. */
2285
- :is(.flyout, .flyout :where(li)) > :where(button, a, [role="menuitem"]) {
2548
+ /* Vertical dividers — .menu-separator (menu.css) is the canonical way to
2549
+ separate menu items. Flyout panels that are not menus use auto grid gap
2550
+ declared on .flyout itself. */
2551
+
2552
+ /*
2553
+ * Menu — action list primitive consumed by flyout and context-menu.
2554
+ *
2555
+ * Strict anatomy: .menu > li > .menu-item. Separators and section labels
2556
+ * sit directly under .menu as siblings of <li>.
2557
+ *
2558
+ * Public hooks:
2559
+ * --menu-item-icon-size
2560
+ */
2561
+
2562
+ .menu {
2563
+ display: grid;
2564
+ gap: var(--space-10);
2565
+ margin: 0;
2566
+ padding: var(--space-10);
2567
+ list-style: none;
2568
+ }
2569
+
2570
+ .menu > li {
2571
+ margin: 0;
2572
+ }
2573
+
2574
+ .menu-item {
2575
+ --menu-item-icon-size: 1.25em;
2576
+
2286
2577
  display: flex;
2287
2578
  align-items: center;
2288
- justify-content: flex-start;
2289
- gap: var(--space-2);
2579
+ gap: var(--space-20);
2290
2580
  inline-size: 100%;
2291
- min-block-size: var(--control-size);
2292
- padding-inline: var(--space-2);
2581
+ min-block-size: var(--menu-item-size, var(--control-size));
2582
+ padding-inline: var(--space-20);
2293
2583
  border: 0;
2294
2584
  border-radius: var(--radius-sm);
2295
- background: none;
2585
+ background: transparent;
2296
2586
  color: var(--text);
2297
2587
  font: inherit;
2298
- font-weight: var(--font-weight);
2299
- text-decoration: none;
2300
2588
  text-align: start;
2589
+ text-decoration: none;
2301
2590
  white-space: nowrap;
2302
2591
  cursor: pointer;
2303
2592
  }
2304
2593
 
2305
- :is(.flyout.is-sheet, .flyout.is-sheet :where(li)) > :where(button, a, [role="menuitem"]) {
2306
- min-block-size: var(--control-size-lg);
2594
+ .menu-item-icon {
2595
+ display: grid;
2596
+ flex: 0 0 var(--menu-item-icon-size);
2597
+ inline-size: var(--menu-item-icon-size);
2598
+ place-items: center;
2599
+ }
2600
+
2601
+ .menu-item-text {
2602
+ flex: 1;
2603
+ min-inline-size: 0;
2604
+ overflow: hidden;
2605
+ text-overflow: ellipsis;
2307
2606
  }
2308
2607
 
2309
- :is(.flyout, .flyout :where(li)) > :where(button, a, [role="menuitem"]):focus-visible {
2608
+ .menu-item-end {
2609
+ display: inline-flex;
2610
+ align-items: center;
2611
+ gap: var(--space-10);
2612
+ margin-inline-start: auto;
2613
+ color: var(--text-muted);
2614
+ }
2615
+
2616
+ /* Checkable menu items are one command with one accessible state. Their
2617
+ indicator is generated so authors do not add a second interactive input. */
2618
+ .menu-item:is([role="menuitemcheckbox"], [role="menuitemradio"])::before {
2619
+ display: grid;
2620
+ flex: 0 0 var(--menu-item-icon-size);
2621
+ inline-size: var(--menu-item-icon-size);
2622
+ place-items: center;
2623
+ }
2624
+
2625
+ .menu-item[role="menuitemcheckbox"]::before {
2626
+ content: "";
2627
+ }
2628
+
2629
+ .menu-item[role="menuitemcheckbox"][aria-checked="true"]::before {
2630
+ content: "✓";
2631
+ }
2632
+
2633
+ .menu-item[role="menuitemcheckbox"][aria-checked="mixed"]::before {
2634
+ content: "−";
2635
+ }
2636
+
2637
+ .menu-item[role="menuitemradio"]::before {
2638
+ content: "○";
2639
+ }
2640
+
2641
+ .menu-item[role="menuitemradio"][aria-checked="true"]::before {
2642
+ content: "●";
2643
+ }
2644
+
2645
+ .menu-item:focus-visible {
2310
2646
  background: var(--surface-subtle);
2311
- outline: var(--focus-outline);
2312
- outline-offset: var(--focus-outline-offset);
2313
2647
  }
2314
2648
 
2315
- :is(.flyout, .flyout :where(li))
2316
- > :where(button, a, [role="menuitem"]):is(:disabled, [aria-disabled="true"]) {
2317
- cursor: not-allowed;
2318
- opacity: var(--disabled-opacity);
2649
+ .menu-item:hover:not(:where(:disabled, [aria-disabled="true"])) {
2650
+ background: var(--surface-subtle);
2319
2651
  }
2320
2652
 
2321
- :is(.flyout, .flyout :where(li)) > :where(a, [role="menuitem"])[aria-disabled="true"] {
2322
- pointer-events: none;
2653
+ .menu-item:is(:disabled, [aria-disabled="true"]) {
2654
+ cursor: not-allowed;
2655
+ opacity: var(--disabled-opacity);
2323
2656
  }
2324
2657
 
2325
- @media (hover: hover) {
2326
- :is(.flyout, .flyout :where(li))
2327
- > :where(button, a, [role="menuitem"]):hover:not(
2328
- :where(:focus-visible, :disabled, [aria-disabled="true"])
2329
- ) {
2330
- background: var(--surface-subtle);
2331
- outline: none;
2332
- }
2658
+ .menu-label {
2659
+ padding: var(--space-20);
2660
+ color: var(--text-muted);
2661
+ font-size: var(--font-size-xs);
2662
+ font-weight: var(--font-weight-strong);
2333
2663
  }
2334
2664
 
2335
- .flyout > hr,
2336
- .flyout > li > hr {
2337
- inline-size: 100%;
2665
+ .menu-separator {
2338
2666
  margin: 0;
2339
- margin-block: var(--space-1);
2340
2667
  border: 0;
2341
2668
  border-block-start: var(--border-width) solid var(--border);
2342
2669
  }
2343
2670
 
2344
2671
  @media (forced-colors: active) {
2345
- .flyout {
2346
- border-color: ButtonText;
2347
- background: Canvas;
2348
- color: CanvasText;
2349
- box-shadow: none;
2350
- }
2351
-
2352
- :is(.flyout, .flyout :where(li)) > :where(button, a, [role="menuitem"]):focus-visible {
2353
- background: Canvas;
2354
- color: CanvasText;
2355
- }
2356
-
2357
- @media (hover: hover) {
2358
- :is(.flyout, .flyout :where(li))
2359
- > :where(button, a, [role="menuitem"]):hover:not(:where(:disabled, [aria-disabled="true"])) {
2360
- outline: var(--border-width) solid ButtonText;
2361
- outline-offset: -1px;
2362
- background: Canvas;
2363
- color: CanvasText;
2364
- }
2672
+ .menu-item:focus-visible {
2673
+ outline: var(--border-width) solid ButtonText;
2674
+ outline-offset: -1px;
2365
2675
  }
2366
2676
  }
2367
2677
 
@@ -2371,6 +2681,15 @@ dialog.drawer nav {
2371
2681
  * Position, placement, and arrow position are managed by JavaScript
2372
2682
  * via position: fixed, left, top, data-placement, and --arrow-x.
2373
2683
  * Visibility is managed with hidden; CSS owns entry and exit animation.
2684
+ *
2685
+ * Public hooks:
2686
+ * --tooltip-bg
2687
+ * --tooltip-fg
2688
+ * --tooltip-arrow-size
2689
+ *
2690
+ * Internal:
2691
+ * --arrow-x (arrow position, written by floating.js)
2692
+ * --arrow-y (arrow position, written by floating.js)
2374
2693
  */
2375
2694
  .tooltip {
2376
2695
  --tooltip-bg: var(--surface-solid);
@@ -2392,8 +2711,8 @@ dialog.drawer nav {
2392
2711
  opacity: 1;
2393
2712
  scale: 1;
2394
2713
  transition:
2395
- opacity var(--duration-fast, var(--duration)) var(--ease, ease),
2396
- scale var(--duration-fast, var(--duration)) var(--ease, ease);
2714
+ opacity var(--duration-fast) var(--ease),
2715
+ scale var(--duration-fast) var(--ease);
2397
2716
  }
2398
2717
 
2399
2718
  .tooltip[hidden] {
@@ -2412,9 +2731,9 @@ dialog.drawer nav {
2412
2731
  @supports (transition-behavior: allow-discrete) {
2413
2732
  .tooltip {
2414
2733
  transition:
2415
- opacity var(--duration-fast, var(--duration)) var(--ease, ease),
2416
- scale var(--duration-fast, var(--duration)) var(--ease, ease),
2417
- display var(--duration-fast, var(--duration)) allow-discrete;
2734
+ opacity var(--duration-fast) var(--ease),
2735
+ scale var(--duration-fast) var(--ease),
2736
+ display var(--duration-fast) allow-discrete;
2418
2737
  }
2419
2738
  }
2420
2739
 
@@ -2464,8 +2783,6 @@ dialog.drawer nav {
2464
2783
  .tooltip {
2465
2784
  --tooltip-bg: Canvas;
2466
2785
  --tooltip-fg: CanvasText;
2467
-
2468
- border: var(--border-width) solid ButtonText;
2469
2786
  }
2470
2787
 
2471
2788
  .tooltip::before {
@@ -2481,9 +2798,14 @@ dialog.drawer nav {
2481
2798
  * Keeps the clickable summary affordance aligned and usable without JavaScript
2482
2799
  * or demo styles. Decorative palettes and custom markers belong outside the
2483
2800
  * base component.
2801
+ *
2802
+ * Public hooks:
2803
+ * --accordion-radius
2804
+ * --accordion-marker-color
2484
2805
  */
2485
2806
  .accordion {
2486
2807
  --accordion-radius: var(--radius);
2808
+ --accordion-marker-color: var(--text-muted);
2487
2809
 
2488
2810
  border: var(--border-width) solid var(--border);
2489
2811
  border-radius: var(--accordion-radius);
@@ -2497,14 +2819,18 @@ dialog.drawer nav {
2497
2819
  .accordion summary {
2498
2820
  display: flex;
2499
2821
  align-items: center;
2500
- gap: var(--space-3);
2822
+ gap: var(--space-30);
2501
2823
  cursor: pointer;
2502
2824
  list-style: none;
2503
- padding: var(--space-4);
2825
+ padding: var(--space-40);
2504
2826
  color: var(--text);
2505
2827
  font-weight: var(--font-weight-bold);
2506
2828
  }
2507
2829
 
2830
+ .accordion summary:hover {
2831
+ color: var(--primary);
2832
+ }
2833
+
2508
2834
  .accordion summary::-webkit-details-marker {
2509
2835
  display: none;
2510
2836
  }
@@ -2515,15 +2841,9 @@ dialog.drawer nav {
2515
2841
  margin-inline-start: auto;
2516
2842
  inline-size: 1rem;
2517
2843
  block-size: 1rem;
2518
- background-color: var(--text-muted);
2519
- -webkit-mask-image: var(--icon-chevron-mask);
2520
- mask-image: var(--icon-chevron-mask);
2521
- -webkit-mask-repeat: no-repeat;
2522
- mask-repeat: no-repeat;
2523
- -webkit-mask-size: contain;
2524
- mask-size: contain;
2525
- -webkit-mask-position: center;
2526
- mask-position: center;
2844
+ background-color: var(--accordion-marker-color);
2845
+ -webkit-mask: var(--icon-chevron) center / contain no-repeat;
2846
+ mask: var(--icon-chevron) center / contain no-repeat;
2527
2847
  transition: transform var(--duration);
2528
2848
  }
2529
2849
 
@@ -2532,14 +2852,14 @@ dialog.drawer nav {
2532
2852
  }
2533
2853
 
2534
2854
  .accordion details > summary + * {
2535
- padding-block: 0 var(--space-4);
2536
- padding-inline: var(--space-4);
2855
+ padding-block: 0 var(--space-40);
2856
+ padding-inline: var(--space-40);
2537
2857
  color: var(--text-muted);
2538
2858
  margin-block: 0;
2539
2859
  }
2540
2860
 
2541
2861
  .accordion details[open] > summary + * {
2542
- padding-block-start: var(--space-1);
2862
+ padding-block-start: var(--space-10);
2543
2863
  }
2544
2864
 
2545
2865
  .accordion details > summary + * > :first-child {
@@ -2549,27 +2869,12 @@ dialog.drawer nav {
2549
2869
  .accordion details > summary + ul,
2550
2870
  .accordion details > summary + ol {
2551
2871
  margin: 0;
2552
- padding-inline-start: 2rem;
2553
- }
2554
-
2555
- @media (forced-colors: active) {
2556
- .accordion {
2557
- border-color: ButtonText;
2558
- background: Canvas;
2559
- color: CanvasText;
2560
- }
2561
-
2562
- .accordion > details + details {
2563
- border-block-start-color: ButtonText;
2564
- }
2565
-
2566
- .accordion summary {
2567
- color: CanvasText;
2568
- }
2872
+ padding-inline-start: 2rem;
2873
+ }
2569
2874
 
2570
- .accordion summary::after {
2571
- background-color: ButtonText;
2572
- forced-color-adjust: none;
2875
+ @media (forced-colors: active) {
2876
+ .accordion {
2877
+ --accordion-marker-color: CanvasText;
2573
2878
  }
2574
2879
  }
2575
2880
 
@@ -2580,18 +2885,17 @@ dialog.drawer nav {
2580
2885
  *
2581
2886
  * Uses currentColor so it adapts to buttons, alerts, badges, and text. The
2582
2887
  * [aria-busy] overlay composition lives in components/busy.css.
2888
+ *
2889
+ * Public hooks:
2890
+ * --spinner-size
2891
+ * --spinner-width
2892
+ * --spinner-track
2893
+ * --spinner-value
2583
2894
  */
2584
2895
  /* Intent boundary — inherited intents do not tint this component.
2585
2896
  Local intent classes still win because :where() has zero specificity. */
2586
2897
  :where(.spinner) {
2587
- /* @sync intent-boundary */
2588
2898
  --intent: initial;
2589
- --intent-fg: initial;
2590
- --ui-bg: initial;
2591
- --ui-fg: initial;
2592
- --ui-border: initial;
2593
- --ui-hover-bg: initial;
2594
- /* @sync intent-boundary */
2595
2899
  }
2596
2900
 
2597
2901
  .spinner {
@@ -2641,6 +2945,13 @@ dialog.drawer nav {
2641
2945
  *
2642
2946
  * Shows approximate loading structure without implying real content hierarchy.
2643
2947
  * Geometry, animation, and color stay theme-aware.
2948
+ *
2949
+ * Public hooks:
2950
+ * --skeleton-size
2951
+ * --skeleton-width
2952
+ * --skeleton-radius
2953
+ * --skeleton-track
2954
+ * --skeleton-highlight
2644
2955
  */
2645
2956
  .skeleton {
2646
2957
  --skeleton-size: 1rem;
@@ -2710,6 +3021,12 @@ dialog.drawer nav {
2710
3021
  * A non-button element with aria-busy="true" and a direct last-child .spinner
2711
3022
  * shows that spinner as a centered overlay over a faded surface. Buttons keep
2712
3023
  * their spinners inline (e.g. .btn[aria-busy] > .spinner stays inline-flex).
3024
+ *
3025
+ * Public hooks:
3026
+ * --busy-overlay-bg
3027
+ *
3028
+ * Internal:
3029
+ * --spinner-size (overlay copy of the shared spinner hook)
2713
3030
  */
2714
3031
  [aria-busy] > .spinner:last-child {
2715
3032
  display: none;
@@ -2755,26 +3072,21 @@ dialog.drawer nav {
2755
3072
  *
2756
3073
  * Consumes the shared --intent/--primary for the value bar and --bar-height
2757
3074
  * for thickness. Indeterminate state animates a shimmer using --duration-shimmer.
3075
+ *
3076
+ * Public hooks:
3077
+ * --progress-track
3078
+ * --progress-value
2758
3079
  */
2759
3080
  /* Intent boundary — inherited intents do not tint this component.
2760
3081
  Local intent classes still win because :where() has zero specificity. */
2761
3082
  :where(.progress) {
2762
- /* @sync intent-boundary */
2763
3083
  --intent: initial;
2764
- --intent-fg: initial;
2765
- --ui-bg: initial;
2766
- --ui-fg: initial;
2767
- --ui-border: initial;
2768
- --ui-hover-bg: initial;
2769
- /* @sync intent-boundary */
2770
3084
  }
2771
3085
 
2772
3086
  .progress {
2773
3087
  --progress-track: var(--surface-subtle);
2774
3088
  --progress-value: var(--intent, var(--primary));
2775
- }
2776
3089
 
2777
- .progress {
2778
3090
  appearance: none;
2779
3091
  -webkit-appearance: none;
2780
3092
  display: block;
@@ -2853,27 +3165,14 @@ dialog.drawer nav {
2853
3165
  }
2854
3166
 
2855
3167
  @media (forced-colors: active) {
2856
- .progress {
2857
- border: var(--border-width) solid ButtonText;
2858
- background: Canvas;
2859
- }
2860
-
2861
- .progress::-webkit-progress-bar {
2862
- background: Canvas;
2863
- }
2864
-
2865
- .progress::-webkit-progress-value {
3168
+ .progress::-webkit-progress-value,
3169
+ .progress:indeterminate::-webkit-progress-value {
2866
3170
  background: Highlight;
2867
- forced-color-adjust: none;
2868
3171
  }
2869
3172
 
2870
- .progress::-moz-progress-bar {
3173
+ .progress::-moz-progress-bar,
3174
+ .progress:indeterminate::-moz-progress-bar {
2871
3175
  background: Highlight;
2872
- forced-color-adjust: none;
2873
- }
2874
-
2875
- .progress:indeterminate {
2876
- background: Canvas;
2877
3176
  }
2878
3177
  }
2879
3178
 
@@ -2892,19 +3191,17 @@ dialog.drawer nav {
2892
3191
  * CSS API: .status-bar
2893
3192
  * JS target contract: [data-status][role="status"]
2894
3193
  * Normally combined, but they describe separate concerns.
3194
+ *
3195
+ * Internal:
3196
+ * --status-bg
3197
+ * --status-fg
2895
3198
  */
2896
3199
 
2897
3200
  /* Intent boundary — inherited intents do not tint this component.
2898
3201
  Local intent classes still win because :where() has zero specificity. */
2899
3202
  :where(.status-bar) {
2900
- /* @sync intent-boundary */
2901
3203
  --intent: initial;
2902
3204
  --intent-fg: initial;
2903
- --ui-bg: initial;
2904
- --ui-fg: initial;
2905
- --ui-border: initial;
2906
- --ui-hover-bg: initial;
2907
- /* @sync intent-boundary */
2908
3205
  }
2909
3206
 
2910
3207
  .status-bar {
@@ -2912,22 +3209,22 @@ dialog.drawer nav {
2912
3209
  --status-fg: var(--intent-fg, var(--surface));
2913
3210
 
2914
3211
  position: fixed;
2915
- inset-block-end: var(--space-4);
2916
- inset-inline: var(--space-4);
2917
- z-index: var(--z-status, 60);
3212
+ inset-block-end: var(--space-40);
3213
+ inset-inline: var(--space-40);
3214
+ z-index: var(--z-status);
2918
3215
 
2919
3216
  display: inline-flex;
2920
3217
  align-items: center;
2921
3218
  justify-content: center;
2922
- gap: var(--space-2);
3219
+ gap: var(--space-20);
2923
3220
 
2924
3221
  inline-size: fit-content;
2925
- max-inline-size: min(calc(100vw - var(--space-6)), 32rem);
3222
+ max-inline-size: min(calc(var(--viewport-inline) - var(--space-60)), 32rem);
2926
3223
  margin-inline: auto;
2927
3224
  overflow-wrap: anywhere;
2928
3225
 
2929
- padding-block: var(--space-2);
2930
- padding-inline: var(--space-3);
3226
+ padding-block: var(--space-20);
3227
+ padding-inline: var(--space-30);
2931
3228
 
2932
3229
  border-radius: var(--radius-full);
2933
3230
  background: var(--status-bg);
@@ -2940,25 +3237,18 @@ dialog.drawer nav {
2940
3237
  pointer-events: none;
2941
3238
 
2942
3239
  transition:
2943
- opacity var(--duration-slow) var(--ease, ease),
2944
- scale var(--duration-slow) var(--ease, ease),
2945
- translate var(--duration-slow) var(--ease, ease);
3240
+ opacity var(--duration-slow) var(--ease),
3241
+ scale var(--duration-slow) var(--ease),
3242
+ translate var(--duration-slow) var(--ease);
2946
3243
  }
2947
3244
 
2948
3245
  .status-bar:empty {
2949
3246
  opacity: 0;
2950
3247
  scale: 0.96;
2951
- translate: 0 var(--space-2);
3248
+ translate: 0 var(--space-20);
2952
3249
  pointer-events: none;
2953
3250
  }
2954
3251
 
2955
- @media (forced-colors: active) {
2956
- .status-bar {
2957
- border: var(--border-width) solid ButtonText;
2958
- box-shadow: none;
2959
- }
2960
- }
2961
-
2962
3252
 
2963
3253
  /* Display */
2964
3254
  /*
@@ -2967,18 +3257,26 @@ dialog.drawer nav {
2967
3257
  * The visual box stays the same across semantic elements. Grouping and status
2968
3258
  * dots (an empty .badge child) compose around the avatar without changing its
2969
3259
  * meaning.
3260
+ *
3261
+ * Public hooks:
3262
+ * --avatar-size
3263
+ * --avatar-radius
3264
+ * --avatar-font-size
3265
+ * --avatar-stack-size
3266
+ * --avatar-stack-font-size
3267
+ * --avatar-stack-overlap
3268
+ * --avatar-stack-ring
3269
+ *
3270
+ * Internal:
3271
+ * --avatar-bg
3272
+ * --avatar-fg
3273
+ * --avatar-status-translate-x
2970
3274
  */
2971
3275
  /* Intent boundary — inherited intents do not tint this component.
2972
3276
  Local intent classes still win because :where() has zero specificity. */
2973
3277
  :where(.avatar) {
2974
- /* @sync intent-boundary */
2975
3278
  --intent: initial;
2976
3279
  --intent-fg: initial;
2977
- --ui-bg: initial;
2978
- --ui-fg: initial;
2979
- --ui-border: initial;
2980
- --ui-hover-bg: initial;
2981
- /* @sync intent-boundary */
2982
3280
  }
2983
3281
 
2984
3282
  .avatar {
@@ -3070,10 +3368,8 @@ button.avatar {
3070
3368
  transition: opacity var(--duration);
3071
3369
  }
3072
3370
 
3073
- @media (hover: hover) {
3074
- :where(a.avatar, button.avatar):not(:where(:disabled, [aria-disabled="true"])):hover::after {
3075
- opacity: 1;
3076
- }
3371
+ :where(a.avatar, button.avatar):not(:where(:disabled, [aria-disabled="true"])):hover::after {
3372
+ opacity: 1;
3077
3373
  }
3078
3374
 
3079
3375
  :where(a.avatar, button.avatar):not(:where(:disabled, [aria-disabled="true"])):active {
@@ -3166,6 +3462,8 @@ a.avatar[aria-disabled="true"] {
3166
3462
  *
3167
3463
  * Use .key outside prose. Plain kbd inside .prose gets the same visual
3168
3464
  * treatment from prose.css so authored content stays convenient.
3465
+ *
3466
+ * Visual twin of .prose kbd (prose.css) — keep the two recipes in sync.
3169
3467
  */
3170
3468
  .key {
3171
3469
  display: inline-flex;
@@ -3197,14 +3495,7 @@ a.avatar[aria-disabled="true"] {
3197
3495
  /* Intent boundary — inherited intents do not tint this component.
3198
3496
  Local intent classes still win because :where() has zero specificity. */
3199
3497
  :where(.overline) {
3200
- /* @sync intent-boundary */
3201
3498
  --intent: initial;
3202
- --intent-fg: initial;
3203
- --ui-bg: initial;
3204
- --ui-fg: initial;
3205
- --ui-border: initial;
3206
- --ui-hover-bg: initial;
3207
- /* @sync intent-boundary */
3208
3499
  }
3209
3500
 
3210
3501
  .overline {
@@ -3245,6 +3536,9 @@ a.avatar[aria-disabled="true"] {
3245
3536
  * .navbar is a horizontal bar; .navbar-brand is the brand link; .navbar-nav is
3246
3537
  * the inline link list using .nav-link items. .nav-list is the vertical nav
3247
3538
  * list shared with .drawer, fed by the shared --gap token.
3539
+ *
3540
+ * Internal:
3541
+ * --gap (local value for the shared gap token)
3248
3542
  */
3249
3543
  .navbar {
3250
3544
  display: flex;
@@ -3252,8 +3546,8 @@ a.avatar[aria-disabled="true"] {
3252
3546
  align-items: center;
3253
3547
  justify-content: space-between;
3254
3548
  gap: var(--gap);
3255
- padding-block: var(--space-3);
3256
- padding-inline: var(--space-4);
3549
+ padding-block: var(--space-30);
3550
+ padding-inline: var(--space-40);
3257
3551
  border: var(--border-width) solid var(--border);
3258
3552
  border-radius: var(--radius-lg);
3259
3553
  background: var(--surface-raised);
@@ -3263,7 +3557,7 @@ a.avatar[aria-disabled="true"] {
3263
3557
  .navbar-brand {
3264
3558
  display: inline-flex;
3265
3559
  align-items: center;
3266
- gap: var(--space-2);
3560
+ gap: var(--space-20);
3267
3561
  color: var(--text);
3268
3562
  font-weight: var(--font-weight-bold);
3269
3563
  text-decoration: none;
@@ -3277,7 +3571,7 @@ a.avatar[aria-disabled="true"] {
3277
3571
  display: flex;
3278
3572
  flex-wrap: wrap;
3279
3573
  align-items: center;
3280
- gap: var(--space-1);
3574
+ gap: var(--space-10);
3281
3575
  margin: 0;
3282
3576
  padding: 0;
3283
3577
  list-style: none;
@@ -3287,7 +3581,7 @@ a.avatar[aria-disabled="true"] {
3287
3581
  display: inline-flex;
3288
3582
  align-items: center;
3289
3583
  min-block-size: var(--control-size-sm);
3290
- padding-inline: var(--space-2);
3584
+ padding-inline: var(--space-20);
3291
3585
  border-radius: var(--radius);
3292
3586
  color: var(--text-muted);
3293
3587
  font-weight: var(--font-weight-medium);
@@ -3299,21 +3593,19 @@ a.avatar[aria-disabled="true"] {
3299
3593
  color: var(--text);
3300
3594
  }
3301
3595
 
3302
- @media (hover: hover) {
3303
- .nav-link:hover {
3304
- background: var(--surface-subtle);
3305
- color: var(--text);
3306
- }
3596
+ .nav-link:hover {
3597
+ background: var(--surface-subtle);
3598
+ color: var(--text);
3307
3599
  }
3308
3600
 
3309
3601
  /* Nav list — a vertical navigation list. Used by .drawer for stacked links. */
3310
3602
  .nav-list {
3311
3603
  display: grid;
3604
+ --gap: var(--space-20);
3312
3605
  gap: var(--gap);
3313
3606
  margin: 0;
3314
3607
  padding: 0;
3315
3608
  list-style: none;
3316
- --gap: var(--space-2);
3317
3609
  }
3318
3610
 
3319
3611
  .nav-list > li {
@@ -3328,13 +3620,11 @@ a.avatar[aria-disabled="true"] {
3328
3620
  forced-color-adjust: none;
3329
3621
  }
3330
3622
 
3331
- @media (hover: hover) {
3332
- .nav-link:hover:not(:where([aria-current]:not([aria-current="false"]))) {
3333
- outline: var(--border-width) solid ButtonText;
3334
- outline-offset: 2px;
3335
- background: Canvas;
3336
- color: CanvasText;
3337
- }
3623
+ .nav-link:hover:not(:where([aria-current]:not([aria-current="false"]))) {
3624
+ outline: var(--border-width) solid ButtonText;
3625
+ outline-offset: 2px;
3626
+ background: Canvas;
3627
+ color: CanvasText;
3338
3628
  }
3339
3629
  }
3340
3630
 
@@ -3348,7 +3638,7 @@ a.avatar[aria-disabled="true"] {
3348
3638
  display: flex;
3349
3639
  flex-wrap: wrap;
3350
3640
  align-items: center;
3351
- gap: var(--space-1);
3641
+ gap: var(--space-10);
3352
3642
  margin: 0;
3353
3643
  padding: 0;
3354
3644
  color: var(--text-subtle);
@@ -3359,7 +3649,7 @@ a.avatar[aria-disabled="true"] {
3359
3649
  .breadcrumb li {
3360
3650
  display: inline-flex;
3361
3651
  align-items: center;
3362
- gap: var(--space-1);
3652
+ gap: var(--space-10);
3363
3653
  }
3364
3654
 
3365
3655
  .breadcrumb li + li::before {
@@ -3372,25 +3662,17 @@ a.avatar[aria-disabled="true"] {
3372
3662
  text-decoration: none;
3373
3663
  }
3374
3664
 
3375
- @media (hover: hover) {
3376
- .breadcrumb
3377
- li:not(:where([aria-current]:not([aria-current="false"])))
3378
- > a:hover:not(:where([aria-current]:not([aria-current="false"]))) {
3379
- color: var(--text);
3380
- text-decoration: underline;
3381
- }
3665
+ .breadcrumb a:not([aria-current]):hover,
3666
+ .breadcrumb a[aria-current="false"]:hover {
3667
+ color: var(--text);
3668
+ text-decoration: underline;
3382
3669
  }
3383
3670
 
3384
3671
  .breadcrumb a:focus-visible {
3385
3672
  border-radius: var(--radius-sm);
3386
3673
  }
3387
3674
 
3388
- .breadcrumb a:where([aria-current]:not([aria-current="false"])) {
3389
- color: var(--text);
3390
- font-weight: var(--font-weight-medium);
3391
- }
3392
-
3393
- .breadcrumb li:where([aria-current]:not([aria-current="false"])) > :where(a, span) {
3675
+ .breadcrumb :where(a, span):where([aria-current]:not([aria-current="false"])) {
3394
3676
  color: var(--text);
3395
3677
  font-weight: var(--font-weight-medium);
3396
3678
  }
@@ -3409,7 +3691,7 @@ a.avatar[aria-disabled="true"] {
3409
3691
  display: flex;
3410
3692
  flex-wrap: wrap;
3411
3693
  align-items: center;
3412
- gap: var(--space-1);
3694
+ gap: var(--space-10);
3413
3695
  margin: 0;
3414
3696
  padding: 0;
3415
3697
  list-style: none;
@@ -3432,24 +3714,20 @@ a.avatar[aria-disabled="true"] {
3432
3714
  * JavaScript owns roving tabindex, aria-selected, panel visibility,
3433
3715
  * and arrow-key navigation. CSS owns the tab strip appearance and
3434
3716
  * active indicator.
3717
+ *
3718
+ * Public hooks:
3719
+ * --tab-gap
3435
3720
  */
3436
3721
  /* Intent boundary — inherited intents do not tint this component.
3437
3722
  Local intent classes still win because :where() has zero specificity. */
3438
3723
  :where(.tab) {
3439
- /* @sync intent-boundary */
3440
3724
  --intent: initial;
3441
- --intent-fg: initial;
3442
- --ui-bg: initial;
3443
- --ui-fg: initial;
3444
- --ui-border: initial;
3445
- --ui-hover-bg: initial;
3446
- /* @sync intent-boundary */
3447
3725
  }
3448
3726
 
3449
3727
  .tabs {
3450
3728
  display: flex;
3451
3729
  flex-wrap: wrap;
3452
- gap: var(--space-1);
3730
+ gap: var(--space-10);
3453
3731
  margin: 0;
3454
3732
  padding: 0;
3455
3733
  border-block-end: var(--border-width) solid var(--border);
@@ -3467,8 +3745,9 @@ a.avatar[aria-disabled="true"] {
3467
3745
  .tab {
3468
3746
  display: inline-flex;
3469
3747
  align-items: center;
3748
+ gap: var(--tab-gap, 0.375em);
3470
3749
  min-block-size: var(--control-size);
3471
- padding-inline: var(--space-3);
3750
+ padding-inline: var(--space-30);
3472
3751
  border: 0;
3473
3752
  border-block-end: 2px solid transparent;
3474
3753
  border-radius: 0;
@@ -3484,7 +3763,7 @@ a.avatar[aria-disabled="true"] {
3484
3763
  justify-content: flex-start;
3485
3764
  border-block-end: 0;
3486
3765
  border-inline-end: 2px solid transparent;
3487
- padding-inline-end: var(--space-4);
3766
+ padding-inline-end: var(--space-40);
3488
3767
  }
3489
3768
 
3490
3769
  .tab:is(:disabled, [aria-disabled="true"]) {
@@ -3492,10 +3771,8 @@ a.avatar[aria-disabled="true"] {
3492
3771
  opacity: var(--disabled-opacity);
3493
3772
  }
3494
3773
 
3495
- @media (hover: hover) {
3496
- .tab:hover:not(:where(:disabled, [aria-disabled="true"])) {
3497
- color: var(--text);
3498
- }
3774
+ .tab:hover:not(:where(:disabled, [aria-disabled="true"])) {
3775
+ color: var(--text);
3499
3776
  }
3500
3777
 
3501
3778
  .tab:focus-visible {
@@ -3505,52 +3782,28 @@ a.avatar[aria-disabled="true"] {
3505
3782
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
3506
3783
  }
3507
3784
 
3508
- .tab[aria-selected="true"],
3509
- .tab[aria-current]:not([aria-current="false"]),
3510
- .tab[aria-pressed="true"] {
3511
- border-block-end-color: var(--intent, var(--primary));
3512
- color: var(--intent, var(--primary));
3785
+ .tab[aria-selected="true"] {
3786
+ border-block-end-color: var(--intent, var(--state-selected));
3787
+ color: var(--intent, var(--state-selected));
3513
3788
  }
3514
3789
 
3515
- .tabs[aria-orientation="vertical"] .tab[aria-selected="true"],
3516
- .tabs[aria-orientation="vertical"] .tab[aria-current]:not([aria-current="false"]),
3517
- .tabs[aria-orientation="vertical"] .tab[aria-pressed="true"] {
3790
+ .tabs[aria-orientation="vertical"] .tab[aria-selected="true"] {
3518
3791
  border-block-end-color: transparent;
3519
- border-inline-end-color: var(--intent, var(--primary));
3792
+ border-inline-end-color: var(--intent, var(--state-selected));
3520
3793
  }
3521
3794
 
3522
3795
  @media (forced-colors: active) {
3523
- .tabs {
3524
- border-block-end-color: ButtonText;
3525
- }
3526
-
3527
- .tabs[aria-orientation="vertical"] {
3528
- border-block-end-color: transparent;
3529
- border-inline-end-color: ButtonText;
3530
- }
3531
-
3532
- .tab {
3533
- color: ButtonText;
3534
- }
3535
-
3536
- @media (hover: hover) {
3537
- .tab:hover:not(:where(:disabled, [aria-disabled="true"])) {
3538
- color: ButtonText;
3539
- text-decoration: underline;
3540
- text-underline-offset: 0.18em;
3541
- }
3796
+ .tab:hover:not(:where(:disabled, [aria-disabled="true"])) {
3797
+ text-decoration: underline;
3798
+ text-underline-offset: 0.18em;
3542
3799
  }
3543
3800
 
3544
- .tab[aria-selected="true"],
3545
- .tab[aria-current]:not([aria-current="false"]),
3546
- .tab[aria-pressed="true"] {
3801
+ .tab[aria-selected="true"] {
3547
3802
  border-block-end-color: Highlight;
3548
3803
  color: Highlight;
3549
3804
  }
3550
3805
 
3551
- .tabs[aria-orientation="vertical"] .tab[aria-selected="true"],
3552
- .tabs[aria-orientation="vertical"] .tab[aria-current]:not([aria-current="false"]),
3553
- .tabs[aria-orientation="vertical"] .tab[aria-pressed="true"] {
3806
+ .tabs[aria-orientation="vertical"] .tab[aria-selected="true"] {
3554
3807
  border-block-end-color: transparent;
3555
3808
  border-inline-end-color: Highlight;
3556
3809
  }
@@ -3591,6 +3844,10 @@ a.avatar[aria-disabled="true"] {
3591
3844
  *
3592
3845
  * .table-wrap provides the scroll container and outer chrome; .table owns row
3593
3846
  * and cell rules. Tokens are sourced from the shared scale.
3847
+ *
3848
+ * Public hooks:
3849
+ * --table-min
3850
+ * --table-cell-pad
3594
3851
  */
3595
3852
  .table-wrap {
3596
3853
  inline-size: 100%;
@@ -3604,7 +3861,7 @@ a.avatar[aria-disabled="true"] {
3604
3861
 
3605
3862
  .table {
3606
3863
  --table-min: 100%;
3607
- --table-cell-pad: var(--space-3);
3864
+ --table-cell-pad: var(--space-30);
3608
3865
 
3609
3866
  inline-size: max(var(--table-min), 100%);
3610
3867
  border-collapse: collapse;
@@ -3640,10 +3897,8 @@ a.avatar[aria-disabled="true"] {
3640
3897
  background: var(--surface-subtle);
3641
3898
  }
3642
3899
 
3643
- @media (hover: hover) {
3644
- .table tbody tr:hover {
3645
- background-color: var(--hover-overlay);
3646
- }
3900
+ .table tbody tr:hover {
3901
+ background-color: var(--hover-overlay);
3647
3902
  }
3648
3903
 
3649
3904
  .table > tr:last-child > *,
@@ -3657,18 +3912,6 @@ a.avatar[aria-disabled="true"] {
3657
3912
  * Maps the three standard meter zones (optimum / suboptimum / even-less-good)
3658
3913
  * to the shared success / warning / danger intent colors.
3659
3914
  */
3660
- /* Intent boundary — inherited intents do not tint this component.
3661
- Local intent classes still win because :where() has zero specificity. */
3662
- :where(.meter) {
3663
- /* @sync intent-boundary */
3664
- --intent: initial;
3665
- --intent-fg: initial;
3666
- --ui-bg: initial;
3667
- --ui-fg: initial;
3668
- --ui-border: initial;
3669
- --ui-hover-bg: initial;
3670
- /* @sync intent-boundary */
3671
- }
3672
3915
 
3673
3916
  .meter {
3674
3917
  appearance: none;
@@ -3714,29 +3957,18 @@ a.avatar[aria-disabled="true"] {
3714
3957
  }
3715
3958
 
3716
3959
  @media (forced-colors: active) {
3717
- .meter {
3718
- border: var(--border-width) solid ButtonText;
3719
- background: Canvas;
3720
- }
3721
-
3722
- .meter::-webkit-meter-bar {
3723
- border: 0;
3724
- background: Canvas;
3725
- }
3726
-
3727
3960
  .meter::-webkit-meter-optimum-value,
3728
3961
  .meter::-webkit-meter-suboptimum-value,
3729
3962
  .meter::-webkit-meter-even-less-good-value,
3730
3963
  .meter::-moz-meter-bar {
3731
3964
  background: Highlight;
3732
- forced-color-adjust: none;
3733
3965
  }
3734
3966
  }
3735
3967
 
3736
- /* Forms — import order matters: switch.css must follow choice.css so its
3737
- :not([role="switch"]) exclusion in choice.css still applies.
3738
- validation.css is part of the default forms bundle, but custom builds may
3739
- omit it when validation styling is app-owned. */
3968
+ /* Forms — import order matters: switch.css follows choice.css so its rules
3969
+ land after the shared choice recipe. validation.css is part of the default
3970
+ forms bundle, but custom builds may omit it when validation styling is
3971
+ app-owned. */
3740
3972
  /*
3741
3973
  * Form layout and supporting text.
3742
3974
  *
@@ -3745,23 +3977,26 @@ a.avatar[aria-disabled="true"] {
3745
3977
  *
3746
3978
  * Public API: .field, .field-label, .field-help, .field-error, .field-group,
3747
3979
  * .required-mark.
3980
+ *
3981
+ * Public hooks:
3982
+ * --fieldset-legend-padding-inline
3748
3983
  */
3749
3984
  .field {
3750
3985
  display: grid;
3751
- gap: var(--space-1);
3986
+ gap: var(--space-10);
3752
3987
  color: var(--text);
3753
3988
  }
3754
3989
 
3755
3990
  .field-group {
3756
3991
  min-inline-size: 0;
3757
3992
  margin: 0;
3758
- padding: var(--space-3);
3993
+ padding: var(--space-30);
3759
3994
  border: var(--border-width) solid var(--border);
3760
3995
  border-radius: var(--radius);
3761
3996
  }
3762
3997
 
3763
3998
  .field-group + .field-group {
3764
- margin-block-start: var(--space-4);
3999
+ margin-block-start: var(--space-40);
3765
4000
  }
3766
4001
 
3767
4002
  .field-label,
@@ -3772,7 +4007,7 @@ a.avatar[aria-disabled="true"] {
3772
4007
 
3773
4008
  .field-group > legend {
3774
4009
  padding-block: 0;
3775
- padding-inline: var(--fieldset-legend-padding-inline, var(--space-1));
4010
+ padding-inline: var(--fieldset-legend-padding-inline, var(--space-10));
3776
4011
  font-size: var(--font-size-xs);
3777
4012
  }
3778
4013
 
@@ -3811,6 +4046,12 @@ a.avatar[aria-disabled="true"] {
3811
4046
  * --control-border-block-end-color/-width are unset extension points, mirroring
3812
4047
  * --btn-border-block-end-* (button.css), for themes that give text controls an
3813
4048
  * underline-style lower edge instead of a uniform border.
4049
+ *
4050
+ * Public hooks:
4051
+ * --control-border
4052
+ * --control-bg
4053
+ * --control-fg
4054
+ * --control-cursor
3814
4055
  */
3815
4056
  .input,
3816
4057
  .textarea,
@@ -3818,14 +4059,12 @@ a.avatar[aria-disabled="true"] {
3818
4059
  --control-border: var(--border);
3819
4060
  --control-bg: var(--surface);
3820
4061
  --control-fg: var(--text);
3821
- --control-ring: none;
3822
4062
  --control-cursor: auto;
3823
- --control-opacity: 1;
3824
4063
 
3825
4064
  appearance: none;
3826
4065
  inline-size: 100%;
3827
4066
  min-block-size: var(--control-size);
3828
- padding-inline: var(--space-3);
4067
+ padding-inline: var(--space-30);
3829
4068
  border: var(--border-width) solid var(--form-invalid-border, var(--control-border));
3830
4069
  border-block-end-color: var(
3831
4070
  --form-invalid-border,
@@ -3836,9 +4075,7 @@ a.avatar[aria-disabled="true"] {
3836
4075
  background: var(--control-bg);
3837
4076
  color: var(--control-fg);
3838
4077
  font-size: var(--control-font-size);
3839
- box-shadow: var(--control-ring);
3840
4078
  cursor: var(--control-cursor);
3841
- opacity: var(--control-opacity);
3842
4079
  outline: none;
3843
4080
  transition:
3844
4081
  border-color var(--duration),
@@ -3849,26 +4086,24 @@ a.avatar[aria-disabled="true"] {
3849
4086
 
3850
4087
  .textarea {
3851
4088
  min-block-size: 6rem;
3852
- padding-block: var(--space-2);
4089
+ padding-block: var(--space-20);
3853
4090
  resize: vertical;
3854
4091
  }
3855
4092
 
3856
- @media (hover: hover) {
3857
- .input:hover:not(:disabled),
3858
- .textarea:hover:not(:disabled) {
3859
- --control-border: var(--text-subtle);
3860
- }
4093
+ .input:hover:not(:disabled),
4094
+ .textarea:hover:not(:disabled) {
4095
+ --control-border: var(--text-subtle);
4096
+ }
3861
4097
 
3862
- .select:hover:not(:disabled) {
3863
- --control-bg: var(--surface-subtle);
3864
- }
4098
+ .select:hover:not(:disabled) {
4099
+ --control-bg: var(--surface-subtle);
3865
4100
  }
3866
4101
 
3867
4102
  .input:focus-visible,
3868
4103
  .textarea:focus-visible,
3869
4104
  .select:focus-visible {
3870
4105
  --control-border: var(--focus);
3871
- outline: none;
4106
+ outline: 2px solid transparent;
3872
4107
  box-shadow: var(--focus-ring-shadow);
3873
4108
  }
3874
4109
 
@@ -3882,10 +4117,9 @@ a.avatar[aria-disabled="true"] {
3882
4117
  .textarea:disabled,
3883
4118
  .select:disabled {
3884
4119
  --control-bg: var(--surface-subtle);
3885
- --control-fg: var(--text-subtle);
4120
+ --control-fg: var(--state-disabled);
3886
4121
  --control-border: var(--border);
3887
4122
  --control-cursor: not-allowed;
3888
- --control-opacity: 1;
3889
4123
  }
3890
4124
 
3891
4125
  .input::placeholder,
@@ -3894,40 +4128,58 @@ a.avatar[aria-disabled="true"] {
3894
4128
  }
3895
4129
 
3896
4130
  /*
3897
- * Input Icon — leading decorative icon inside a relative wrapper.
4131
+ * Input Icon — decorative icon inside a relative wrapper, leading or trailing.
3898
4132
  *
3899
- * Icon-library agnostic: selects the wrapper's first child rather than a
3900
- * specific tag, so it works with an <i> webfont icon, an inline <svg>, or a
3901
- * <span>. Sizes with --variant-icon-size (variants.css) so .sm/.lg pick up
3902
- * the same icon scale used elsewhere (alert.css); the input's start padding
3903
- * grows to match so text never runs under the icon.
4133
+ * Icon-library agnostic: detects position by DOM order. First child that is
4134
+ * not .input = leading icon; icon after .input = trailing. The icon keeps a
4135
+ * fixed size; density contexts shrink the control geometry around it, not
4136
+ * the pictogram.
4137
+ *
4138
+ * Public hooks:
4139
+ * --input-icon-size
3904
4140
  */
3905
4141
  .input-icon {
3906
- --input-icon-size: var(--variant-icon-size, 1.25rem);
4142
+ --input-icon-size: 1.25rem;
3907
4143
 
3908
4144
  display: block;
3909
4145
  position: relative;
3910
4146
  }
3911
4147
 
3912
- .input-icon > :first-child {
4148
+ /* Icon — shared by leading and trailing */
4149
+ .input-icon > :where(:not(.input)) {
3913
4150
  position: absolute;
3914
4151
  inset-block-start: 50%;
3915
- inset-inline-start: var(--space-3);
3916
4152
  transform: translateY(-50%);
3917
4153
  font-size: var(--input-icon-size);
3918
4154
  color: var(--text-muted);
3919
4155
  pointer-events: none;
3920
4156
  }
3921
4157
 
4158
+ /* Leading: icon is the first child */
4159
+ .input-icon > :first-child:where(:not(.input)) {
4160
+ inset-inline-start: var(--space-30);
4161
+ }
4162
+
4163
+ /* Trailing: input is the first child, icon follows */
4164
+ .input-icon > .input:first-child ~ :where(:not(.input)) {
4165
+ inset-inline-end: var(--space-30);
4166
+ }
4167
+
3922
4168
  /* font-size only sizes text-metric icons (webfont <i>, <span>). An inline
3923
4169
  <svg> with just a viewBox ignores it, so give it explicit dimensions. */
3924
- .input-icon > svg:first-child {
4170
+ .input-icon > svg:where(:not(.input)) {
3925
4171
  inline-size: var(--input-icon-size);
3926
4172
  block-size: var(--input-icon-size);
3927
4173
  }
3928
4174
 
3929
- .input-icon > .input {
3930
- padding-inline-start: calc(var(--space-3) * 2 + var(--input-icon-size));
4175
+ /* Leading: input gets start padding to clear the icon */
4176
+ .input-icon > :first-child:where(:not(.input)) ~ .input {
4177
+ padding-inline-start: calc(var(--space-30) * 2 + var(--input-icon-size));
4178
+ }
4179
+
4180
+ /* Trailing: input is first child and has a sibling → end padding */
4181
+ .input-icon > .input:first-child:where(:not(:last-child)) {
4182
+ padding-inline-end: calc(var(--space-30) * 2 + var(--input-icon-size));
3931
4183
  }
3932
4184
 
3933
4185
  /*
@@ -3940,23 +4192,23 @@ a.avatar[aria-disabled="true"] {
3940
4192
  *
3941
4193
  * Switches (input[role="switch"]) are excluded so switch.css owns their
3942
4194
  * geometry and states. switch.css is imported after this file.
4195
+ *
4196
+ * Public hooks:
4197
+ * --choice-control-offset
4198
+ * --choice-border
4199
+ * --choice-bg
4200
+ * --choice-mark
3943
4201
  */
3944
4202
  /* Intent boundary — inherited intents do not tint this component.
3945
4203
  Local intent classes still win because :where() has zero specificity. */
3946
4204
  :where(.choice, .check, .radio) {
3947
- /* @sync intent-boundary */
3948
4205
  --intent: initial;
3949
4206
  --intent-fg: initial;
3950
- --ui-bg: initial;
3951
- --ui-fg: initial;
3952
- --ui-border: initial;
3953
- --ui-hover-bg: initial;
3954
- /* @sync intent-boundary */
3955
4207
  }
3956
4208
 
3957
- /* A classed .choice is the component surface. Let its nested input consume a
4209
+ /* A classed .choice is the component surface. Let its direct input consume a
3958
4210
  local intent without reopening inherited page-level intents. */
3959
- :where(.choice) :where(input[type="checkbox"]:not([role="switch"]), input[type="radio"]) {
4211
+ :where(.choice) > :where(input[type="checkbox"]:not([role="switch"]), input[type="radio"]) {
3960
4212
  --intent: inherit;
3961
4213
  --intent-fg: inherit;
3962
4214
  }
@@ -3966,7 +4218,7 @@ a.avatar[aria-disabled="true"] {
3966
4218
 
3967
4219
  display: inline-flex;
3968
4220
  align-items: flex-start;
3969
- gap: var(--space-2);
4221
+ gap: var(--space-20);
3970
4222
  color: var(--text);
3971
4223
  font-size: var(--control-font-size);
3972
4224
  cursor: pointer;
@@ -3989,8 +4241,11 @@ a.avatar[aria-disabled="true"] {
3989
4241
  display: block;
3990
4242
  }
3991
4243
 
3992
- .choice:has(:disabled) {
3993
- color: var(--text-subtle);
4244
+ /* Progressive enhancement: dim the whole row when one of its controls is
4245
+ disabled. Without :has() the native disabled control keeps its own state
4246
+ styling — the row-level dim is lost but the field stays clearly disabled. */
4247
+ .choice:has(> :disabled) {
4248
+ color: var(--state-disabled);
3994
4249
  cursor: not-allowed;
3995
4250
  }
3996
4251
 
@@ -4007,8 +4262,7 @@ a.avatar[aria-disabled="true"] {
4007
4262
  place-content: center;
4008
4263
  inline-size: 1.125em;
4009
4264
  block-size: 1.125em;
4010
- margin-inline: 0;
4011
- margin-block-end: 0;
4265
+ margin: 0;
4012
4266
  border: var(--border-width) solid var(--form-invalid-border, var(--choice-border));
4013
4267
  background: var(--choice-bg);
4014
4268
  color: var(--choice-mark);
@@ -4049,7 +4303,7 @@ a.avatar[aria-disabled="true"] {
4049
4303
  background: currentColor;
4050
4304
  }
4051
4305
 
4052
- .check:checked,
4306
+ .check:is(:checked, :indeterminate),
4053
4307
  .radio:checked {
4054
4308
  --choice-border: var(--intent, var(--primary));
4055
4309
  --choice-bg: var(--intent, var(--primary));
@@ -4060,12 +4314,6 @@ a.avatar[aria-disabled="true"] {
4060
4314
  transform: translateY(-0.06em) rotate(-45deg) scale(1);
4061
4315
  }
4062
4316
 
4063
- .check:indeterminate {
4064
- --choice-border: var(--intent, var(--primary));
4065
- --choice-bg: var(--intent, var(--primary));
4066
- --choice-mark: var(--intent-fg, var(--primary-fg));
4067
- }
4068
-
4069
4317
  .check:indeterminate::before {
4070
4318
  border-inline-start: 0;
4071
4319
  inline-size: 0.6em;
@@ -4078,17 +4326,15 @@ a.avatar[aria-disabled="true"] {
4078
4326
  transform: scale(1);
4079
4327
  }
4080
4328
 
4081
- @media (hover: hover) {
4082
- .check:hover:not(:checked, :indeterminate, :disabled),
4083
- .radio:hover:not(:checked, :indeterminate, :disabled) {
4084
- --choice-border: var(--text-subtle);
4085
- }
4329
+ .check:hover:not(:checked, :indeterminate, :disabled),
4330
+ .radio:hover:not(:checked, :indeterminate, :disabled) {
4331
+ --choice-border: var(--text-subtle);
4086
4332
  }
4087
4333
 
4088
4334
  .check:focus-visible,
4089
4335
  .radio:focus-visible {
4090
4336
  --choice-border: var(--focus);
4091
- outline: none;
4337
+ outline: 2px solid transparent;
4092
4338
  box-shadow: var(--focus-ring-shadow);
4093
4339
  }
4094
4340
 
@@ -4096,7 +4342,7 @@ a.avatar[aria-disabled="true"] {
4096
4342
  .radio:disabled {
4097
4343
  --choice-bg: var(--surface-subtle);
4098
4344
  --choice-border: var(--border);
4099
- --choice-mark: var(--text-subtle);
4345
+ --choice-mark: var(--state-disabled);
4100
4346
  cursor: not-allowed;
4101
4347
  opacity: 1;
4102
4348
  }
@@ -4138,20 +4384,21 @@ a.avatar[aria-disabled="true"] {
4138
4384
  * The badge overhangs the top-right corner by 0.375em; ancestors with
4139
4385
  * overflow: hidden will clip it. Pair with grid or flex utilities for group
4140
4386
  * layout.
4387
+ *
4388
+ * Public hooks:
4389
+ * --choice-card-pad
4390
+ * --choice-card-radius
4391
+ * --choice-card-border
4392
+ * --choice-card-bg
4393
+ * --choice-card-check-size
4141
4394
  */
4142
4395
  :where(.choice-card) {
4143
- /* @sync intent-boundary */
4144
4396
  --intent: initial;
4145
4397
  --intent-fg: initial;
4146
- --ui-bg: initial;
4147
- --ui-fg: initial;
4148
- --ui-border: initial;
4149
- --ui-hover-bg: initial;
4150
- /* @sync intent-boundary */
4151
4398
  }
4152
4399
 
4153
4400
  .choice-card {
4154
- --choice-card-pad: var(--variant-space, var(--space-4));
4401
+ --choice-card-pad: var(--density-space, var(--space-40));
4155
4402
  --choice-card-radius: var(--radius);
4156
4403
  --choice-card-border: var(--border);
4157
4404
  --choice-card-bg: var(--surface);
@@ -4191,7 +4438,7 @@ a.avatar[aria-disabled="true"] {
4191
4438
 
4192
4439
  /* Focus ring on the label follows the hidden input's focus. */
4193
4440
  .choice-card:has(:focus-visible) {
4194
- outline: none;
4441
+ outline: 2px solid transparent;
4195
4442
  box-shadow: var(--focus-ring-shadow);
4196
4443
  }
4197
4444
 
@@ -4200,10 +4447,8 @@ a.avatar[aria-disabled="true"] {
4200
4447
  cursor: not-allowed;
4201
4448
  }
4202
4449
 
4203
- @media (hover: hover) {
4204
- .choice-card:hover:not(:has(:disabled)) {
4205
- --choice-card-border: var(--text-subtle);
4206
- }
4450
+ .choice-card:hover:not(:has(:disabled)) {
4451
+ --choice-card-border: var(--text-subtle);
4207
4452
  }
4208
4453
 
4209
4454
  /* Checked border — shared by radio and checkbox */
@@ -4250,8 +4495,8 @@ a.avatar[aria-disabled="true"] {
4250
4495
  /* Check mark */
4251
4496
  .choice-card::after {
4252
4497
  background: var(--intent-fg, var(--primary-fg));
4253
- -webkit-mask: var(--icon-check-mask) center / 60% no-repeat;
4254
- mask: var(--icon-check-mask) center / 60% no-repeat;
4498
+ -webkit-mask: var(--icon-check) center / 60% no-repeat;
4499
+ mask: var(--icon-check) center / 60% no-repeat;
4255
4500
  z-index: 2;
4256
4501
  }
4257
4502
 
@@ -4294,19 +4539,19 @@ a.avatar[aria-disabled="true"] {
4294
4539
  * custom-select.css can be opted out independently. Add .native to a specific
4295
4540
  * select to opt out of the enhancement.
4296
4541
  */
4297
- .select:is(:not([multiple], [size]), :not([multiple])[size="1"]) {
4542
+ .select:not([multiple], [size]:not([size="1"])) {
4298
4543
  appearance: none;
4299
- padding-inline-end: calc(var(--space-3) + var(--space-6));
4300
- background-image: var(--icon-chevron-image);
4301
- background-position: center right var(--space-3);
4544
+ padding-inline-end: calc(var(--space-30) + var(--space-60));
4545
+ background-image: var(--icon-chevron);
4546
+ background-position: center right var(--space-30);
4302
4547
  background-size: 1rem auto;
4303
4548
  background-repeat: no-repeat;
4304
4549
  }
4305
4550
 
4306
4551
  /* The chevron sits on the inline-end side; flip it in RTL so it follows
4307
4552
  the padding-inline-end space reserved for it. */
4308
- .select:is(:not([multiple], [size]), :not([multiple])[size="1"]):dir(rtl) {
4309
- background-position: center left var(--space-3);
4553
+ .select:not([multiple], [size]:not([size="1"])):dir(rtl) {
4554
+ background-position: center left var(--space-30);
4310
4555
  }
4311
4556
 
4312
4557
  /* Both forced-colors and prefers-contrast hand the arrow back to the
@@ -4314,10 +4559,10 @@ a.avatar[aria-disabled="true"] {
4314
4559
  simpler than maintaining contrast variants, and native rendering already
4315
4560
  matches whatever contrast mode the user picked. */
4316
4561
  @media (forced-colors: active), (prefers-contrast: more) {
4317
- .select:is(:not([multiple], [size]), :not([multiple])[size="1"]) {
4562
+ .select:not([multiple], [size]:not([size="1"])) {
4318
4563
  appearance: auto;
4319
4564
  background-image: none;
4320
- padding-inline-end: var(--space-3);
4565
+ padding-inline-end: var(--space-30);
4321
4566
  }
4322
4567
  }
4323
4568
 
@@ -4330,6 +4575,10 @@ a.avatar[aria-disabled="true"] {
4330
4575
  *
4331
4576
  * No layout or interaction contract may depend on appearance: base-select.
4332
4577
  * Arbitrary menu and popover behavior belongs elsewhere.
4578
+ *
4579
+ * Internal:
4580
+ * --control-border
4581
+ * --control-bg
4333
4582
  */
4334
4583
  @media (forced-colors: none) and (prefers-contrast: no-preference) and (hover: hover) and (
4335
4584
  pointer: fine
@@ -4342,7 +4591,7 @@ a.avatar[aria-disabled="true"] {
4342
4591
  &::picker(select) {
4343
4592
  appearance: base-select;
4344
4593
  max-height: min(24rem, calc(var(--viewport-block) * 0.7));
4345
- margin-block: var(--space-2);
4594
+ margin-block: var(--space-20);
4346
4595
  padding: 0;
4347
4596
  border: var(--border-width) solid var(--border);
4348
4597
  border-radius: var(--radius);
@@ -4364,8 +4613,8 @@ a.avatar[aria-disabled="true"] {
4364
4613
 
4365
4614
  & option {
4366
4615
  position: relative;
4367
- padding-block: var(--space-2);
4368
- padding-inline: var(--space-3);
4616
+ padding-block: var(--space-20);
4617
+ padding-inline: var(--space-30);
4369
4618
  white-space: normal;
4370
4619
  transition-property: color, background-color;
4371
4620
  transition-duration: var(--duration);
@@ -4393,14 +4642,8 @@ a.avatar[aria-disabled="true"] {
4393
4642
  inline-size: 1rem;
4394
4643
  block-size: 1rem;
4395
4644
  background-color: currentColor;
4396
- -webkit-mask-image: var(--icon-check-mask);
4397
- mask-image: var(--icon-check-mask);
4398
- -webkit-mask-repeat: no-repeat;
4399
- mask-repeat: no-repeat;
4400
- -webkit-mask-size: contain;
4401
- mask-size: contain;
4402
- -webkit-mask-position: center;
4403
- mask-position: center;
4645
+ -webkit-mask: var(--icon-check) center / contain no-repeat;
4646
+ mask: var(--icon-check) center / contain no-repeat;
4404
4647
  }
4405
4648
  }
4406
4649
 
@@ -4416,9 +4659,9 @@ a.avatar[aria-disabled="true"] {
4416
4659
  }
4417
4660
 
4418
4661
  /*
4419
- * Native-only control polish: file input that mostly defeats custom styling,
4420
- * so the framework stays out of the way and just keeps it readable and
4421
- * theme-aware.
4662
+ * Native-only control polish: the file input and color picker mostly defeat
4663
+ * custom styling, so the framework stays out of the way and just keeps them
4664
+ * readable and theme-aware.
4422
4665
  *
4423
4666
  * Range has its own custom-styled file (range.css) so the focus ring can
4424
4667
  * target the thumb pseudo-element rather than the rectangular CSS box.
@@ -4428,9 +4671,9 @@ a.avatar[aria-disabled="true"] {
4428
4671
  }
4429
4672
 
4430
4673
  .file::file-selector-button {
4431
- margin-inline-end: var(--space-3);
4432
- padding-block: var(--space-1);
4433
- padding-inline: var(--space-2);
4674
+ margin-inline-end: var(--space-30);
4675
+ padding-block: var(--space-10);
4676
+ padding-inline: var(--space-20);
4434
4677
  border: var(--border-width) solid var(--border);
4435
4678
  border-radius: var(--radius-sm);
4436
4679
  background: var(--surface-subtle);
@@ -4449,17 +4692,56 @@ a.avatar[aria-disabled="true"] {
4449
4692
  cursor: not-allowed;
4450
4693
  }
4451
4694
 
4452
- @media (hover: hover) {
4453
- .file:not(:disabled)::file-selector-button:hover {
4454
- background: var(--border);
4455
- }
4695
+ .file:not(:disabled)::file-selector-button:hover {
4696
+ background: var(--border);
4456
4697
  }
4457
4698
 
4458
4699
  .file:focus-visible {
4459
- outline: none;
4700
+ outline: 2px solid transparent;
4701
+ box-shadow: var(--focus-ring-shadow);
4702
+ }
4703
+
4704
+ /* Native color picker — keep the browser's dialog and selected-color UI. */
4705
+ .color {
4706
+ inline-size: var(--control-size);
4707
+ block-size: var(--control-size);
4708
+ padding: var(--space-10);
4709
+ border: var(--border-width) solid var(--border);
4710
+ border-radius: var(--radius);
4711
+ background: var(--surface);
4712
+ cursor: pointer;
4713
+ }
4714
+
4715
+ .color::-webkit-color-swatch-wrapper {
4716
+ padding: 0;
4717
+ }
4718
+
4719
+ .color::-webkit-color-swatch {
4720
+ border: 0;
4721
+ }
4722
+
4723
+ .color::-moz-color-swatch {
4724
+ border: 0;
4725
+ }
4726
+
4727
+ .color:disabled {
4728
+ cursor: not-allowed;
4729
+ opacity: var(--disabled-opacity);
4730
+ }
4731
+
4732
+ .color:focus-visible {
4733
+ outline: 2px solid transparent;
4460
4734
  box-shadow: var(--focus-ring-shadow);
4461
4735
  }
4462
4736
 
4737
+ @media (forced-colors: active) {
4738
+ .color {
4739
+ border-color: ButtonText;
4740
+ background: ButtonFace;
4741
+ forced-color-adjust: none;
4742
+ }
4743
+ }
4744
+
4463
4745
  /*
4464
4746
  * Range slider — custom-styled so the focus ring always targets the
4465
4747
  * user-manipulated surface (the thumb) rather than the CSS box.
@@ -4467,19 +4749,18 @@ a.avatar[aria-disabled="true"] {
4467
4749
  * appearance: none gives us full control over track and thumb; without it,
4468
4750
  * pseudo-element rules for ::-webkit-slider-thumb and ::-moz-range-thumb
4469
4751
  * are not reliably applied across browsers.
4752
+ *
4753
+ * Public hooks:
4754
+ * --range-thumb-size
4755
+ * --range-track-height
4756
+ * --range-thumb-bg
4757
+ * --range-track-bg
4470
4758
  */
4471
4759
 
4472
4760
  /* Intent boundary — inherited intents do not tint this component.
4473
4761
  Local intent classes still win because :where() has zero specificity. */
4474
4762
  :where(.range) {
4475
- /* @sync intent-boundary */
4476
4763
  --intent: initial;
4477
- --intent-fg: initial;
4478
- --ui-bg: initial;
4479
- --ui-fg: initial;
4480
- --ui-border: initial;
4481
- --ui-hover-bg: initial;
4482
- /* @sync intent-boundary */
4483
4764
  }
4484
4765
 
4485
4766
  .range {
@@ -4530,7 +4811,7 @@ a.avatar[aria-disabled="true"] {
4530
4811
  }
4531
4812
 
4532
4813
  .range:focus-visible {
4533
- outline: none;
4814
+ outline: 2px solid transparent;
4534
4815
  }
4535
4816
 
4536
4817
  /*
@@ -4551,28 +4832,41 @@ a.avatar[aria-disabled="true"] {
4551
4832
  }
4552
4833
 
4553
4834
  .range:disabled {
4554
- --range-thumb-bg: var(--text-subtle);
4835
+ --range-thumb-bg: var(--state-disabled);
4555
4836
  cursor: not-allowed;
4556
4837
  opacity: var(--disabled-opacity);
4557
4838
  }
4558
4839
 
4840
+ /* Forced colors: map the custom thumb/track to system colors. The focus
4841
+ indicator stays visible through the transparent outline on :focus-visible. */
4842
+ @media (forced-colors: active) {
4843
+ .range {
4844
+ --range-thumb-bg: ButtonText;
4845
+ --range-track-bg: Canvas;
4846
+ }
4847
+ }
4848
+
4559
4849
  /*
4560
4850
  * Switch controls for checkbox inputs with switch semantics.
4561
4851
  *
4562
4852
  * The switch remains an input control, not a decorative toggle. Checked, hover,
4563
4853
  * focus-visible, and disabled states stay clear.
4854
+ *
4855
+ * Public hooks:
4856
+ * --switch-bg
4857
+ * --switch-border
4858
+ * --switch-knob
4859
+ * --switch-width
4860
+ * --switch-block-size
4861
+ * --switch-knob-margin
4862
+ * --switch-knob-size
4863
+ * --switch-travel
4564
4864
  */
4565
4865
  /* Intent boundary — inherited intents do not tint this component.
4566
4866
  Local intent classes still win because :where() has zero specificity. */
4567
4867
  :where(.switch) {
4568
- /* @sync intent-boundary */
4569
4868
  --intent: initial;
4570
4869
  --intent-fg: initial;
4571
- --ui-bg: initial;
4572
- --ui-fg: initial;
4573
- --ui-border: initial;
4574
- --ui-hover-bg: initial;
4575
- /* @sync intent-boundary */
4576
4870
  }
4577
4871
 
4578
4872
  .switch {
@@ -4620,10 +4914,8 @@ a.avatar[aria-disabled="true"] {
4620
4914
  box-shadow var(--duration);
4621
4915
  }
4622
4916
 
4623
- @media (hover: hover) {
4624
- .switch:hover:not(:disabled) {
4625
- --switch-border: var(--intent, var(--primary));
4626
- }
4917
+ .switch:hover:not(:disabled) {
4918
+ --switch-border: var(--intent, var(--primary));
4627
4919
  }
4628
4920
 
4629
4921
  .switch:checked {
@@ -4644,14 +4936,14 @@ a.avatar[aria-disabled="true"] {
4644
4936
 
4645
4937
  .switch:focus-visible {
4646
4938
  --switch-border: var(--focus);
4647
- outline: none;
4939
+ outline: 2px solid transparent;
4648
4940
  box-shadow: var(--focus-ring-shadow);
4649
4941
  }
4650
4942
 
4651
4943
  .switch:disabled {
4652
4944
  --switch-bg: var(--surface-subtle);
4653
4945
  --switch-border: var(--border);
4654
- --switch-knob: var(--text-subtle);
4946
+ --switch-knob: var(--state-disabled);
4655
4947
  opacity: 1;
4656
4948
  cursor: not-allowed;
4657
4949
  }
@@ -4688,6 +4980,17 @@ a.avatar[aria-disabled="true"] {
4688
4980
  *
4689
4981
  * Imported by default through forms/index.css. Custom builds may omit this
4690
4982
  * file when an app wants to own validation styling completely.
4983
+ *
4984
+ * State coverage (which states each component ships) is audited in
4985
+ * tests/validation.test.js and summarized in docs/pages/forms/validation.md —
4986
+ * not kept as a source comment that can drift from the implementation.
4987
+ *
4988
+ * Public hooks:
4989
+ * --form-invalid-border
4990
+ *
4991
+ * Internal:
4992
+ * --choice-bg
4993
+ * --switch-bg
4691
4994
  */
4692
4995
 
4693
4996
  .field.danger {
@@ -4710,17 +5013,33 @@ a.avatar[aria-disabled="true"] {
4710
5013
  --form-invalid-border: var(--danger);
4711
5014
  }
4712
5015
 
4713
- /* Override checked/indeterminate background so both border and fill signal
4714
- danger instead of showing a red border around a primary-colored control. */
4715
- .check[aria-invalid="true"]:is(:checked, :indeterminate),
4716
- .radio[aria-invalid="true"]:checked {
5016
+ /* Override checked/indeterminate fill so both border and fill signal danger
5017
+ instead of a red border around a primary-colored control. All three
5018
+ invalid routes must converge: aria-invalid, was-validated :invalid, and
5019
+ :user-invalid. */
5020
+ .check:is(:checked, :indeterminate)[aria-invalid="true"],
5021
+ .needs-validation.was-validated .check:is(:checked, :indeterminate):invalid,
5022
+ .radio:checked[aria-invalid="true"],
5023
+ .needs-validation.was-validated .radio:checked:invalid {
4717
5024
  --choice-bg: var(--danger);
4718
5025
  }
4719
5026
 
4720
- .switch[aria-invalid="true"]:checked {
5027
+ .switch:checked[aria-invalid="true"],
5028
+ .needs-validation.was-validated .switch:checked:invalid {
4721
5029
  --switch-bg: var(--danger);
4722
5030
  }
4723
5031
 
5032
+ @supports selector(:user-invalid) {
5033
+ .needs-validation .check:is(:checked, :indeterminate):user-invalid,
5034
+ .needs-validation .radio:checked:user-invalid {
5035
+ --choice-bg: var(--danger);
5036
+ }
5037
+
5038
+ .needs-validation .switch:checked:user-invalid {
5039
+ --switch-bg: var(--danger);
5040
+ }
5041
+ }
5042
+
4724
5043
  /* Choice cards hide their native input, so invalid state must surface on the
4725
5044
  card itself. Sets the same --form-invalid-border hook the card's border
4726
5045
  consumes. */
@@ -4753,13 +5072,24 @@ a.avatar[aria-disabled="true"] {
4753
5072
  * A flex cluster of buttons/actions that closes a form. The sticky variant
4754
5073
  * docks to the viewport bottom with a translucent blur where supported; at
4755
5074
  * rest it stays visually neutral so it does not double as a separator.
5075
+ *
5076
+ * Public hooks:
5077
+ * --form-actions-align
5078
+ * --form-actions-justify
5079
+ * --form-actions-margin-block-start
5080
+ *
5081
+ * Internal:
5082
+ * --form-actions-sticky-block-offset (owned by .form-actions.sticky)
5083
+ * --form-actions-sticky-inline-offset (owned by .form-actions.sticky)
5084
+ * --form-actions-sticky-padding (owned by .form-actions.sticky)
4756
5085
  */
4757
5086
  .form-actions {
4758
5087
  display: flex;
4759
5088
  flex-wrap: wrap;
4760
5089
  gap: var(--gap);
4761
- align-items: center;
4762
- margin-block-start: var(--form-actions-margin-block-start, var(--space-5));
5090
+ align-items: var(--form-actions-align, center);
5091
+ justify-content: var(--form-actions-justify, flex-start);
5092
+ margin-block-start: var(--form-actions-margin-block-start, var(--space-50));
4763
5093
  }
4764
5094
 
4765
5095
  /* Opt-in sticky variant. When the bar is not stuck (at rest) the translucent
@@ -4773,7 +5103,7 @@ a.avatar[aria-disabled="true"] {
4773
5103
  isolation: isolate;
4774
5104
 
4775
5105
  margin-inline: calc(var(--form-actions-sticky-inline-offset, 0px) * -1);
4776
- padding: var(--form-actions-sticky-padding, var(--space-3));
5106
+ padding: var(--form-actions-sticky-padding, var(--space-30));
4777
5107
 
4778
5108
  border-block-start: var(--border-width) solid var(--border);
4779
5109
  background: var(--surface);
@@ -4823,7 +5153,7 @@ a.avatar[aria-disabled="true"] {
4823
5153
  min-inline-size: 0;
4824
5154
  }
4825
5155
 
4826
- .nowrap {
5156
+ .text-nowrap {
4827
5157
  white-space: nowrap;
4828
5158
  }
4829
5159
 
@@ -4918,15 +5248,13 @@ a.avatar[aria-disabled="true"] {
4918
5248
  text-decoration: none;
4919
5249
  }
4920
5250
 
4921
- @media (hover: hover) {
4922
- .link-muted:hover {
4923
- color: var(--text);
4924
- }
5251
+ .link-muted:hover {
5252
+ color: var(--text);
4925
5253
  }
4926
5254
 
4927
5255
  /* Spacing step helpers — semantic scale (sm / default / lg).
4928
5256
  Names map to logical properties for writing-direction safety. The default
4929
- step uses --space-4, which matches the framework's built-in padding on
5257
+ step uses --space-40, which matches the framework's built-in padding on
4930
5258
  cards, dialogs, and form-actions. */
4931
5259
 
4932
5260
  /* Gap */
@@ -4936,22 +5264,22 @@ a.avatar[aria-disabled="true"] {
4936
5264
 
4937
5265
  /* Padding — block (vertical) */
4938
5266
  .py {
4939
- padding-block: var(--variant-space);
5267
+ padding-block: var(--density-space);
4940
5268
  }
4941
5269
 
4942
5270
  /* Padding — inline (horizontal) */
4943
5271
  .px {
4944
- padding-inline: var(--variant-space);
5272
+ padding-inline: var(--density-space);
4945
5273
  }
4946
5274
 
4947
5275
  /* Margin — block-start */
4948
5276
  .mbs {
4949
- margin-block-start: var(--variant-space);
5277
+ margin-block-start: var(--density-space);
4950
5278
  }
4951
5279
 
4952
5280
  /* Margin — block-end */
4953
5281
  .mbe {
4954
- margin-block-end: var(--variant-space);
5282
+ margin-block-end: var(--density-space);
4955
5283
  }
4956
5284
 
4957
5285
  /* Joined controls are a component, but must load after the controls it groups. */
@@ -4964,6 +5292,18 @@ a.avatar[aria-disabled="true"] {
4964
5292
  * Cascade note: .join writes border-radius longhands on its direct children.
4965
5293
  * Keep this file loaded after control/component styles so those corners win at
4966
5294
  * equal specificity.
5295
+ *
5296
+ * Public hooks:
5297
+ * --join-radius
5298
+ *
5299
+ * Internal:
5300
+ * --join-ss
5301
+ * --join-se
5302
+ * --join-es
5303
+ * --join-ee
5304
+ * --control-border
5305
+ * --control-bg
5306
+ * --control-fg
4967
5307
  */
4968
5308
  .join {
4969
5309
  --join-radius: var(--radius);
@@ -4991,7 +5331,7 @@ a.avatar[aria-disabled="true"] {
4991
5331
  display: inline-flex;
4992
5332
  justify-content: center;
4993
5333
  min-block-size: var(--control-size);
4994
- padding-inline: var(--space-3);
5334
+ padding-inline: var(--space-30);
4995
5335
  border: var(--border-width) solid var(--control-border);
4996
5336
  border-radius: var(--radius);
4997
5337
  background: var(--control-bg);
@@ -5052,7 +5392,7 @@ a.avatar[aria-disabled="true"] {
5052
5392
  focus, and unsupported :has() browsers retain the control's normal ring. */
5053
5393
  .join:has(> :is(.input, .textarea, .select):focus-visible) {
5054
5394
  border-radius: var(--join-radius);
5055
- outline: none;
5395
+ outline: 2px solid transparent;
5056
5396
  box-shadow: var(--focus-ring-shadow);
5057
5397
  }
5058
5398
 
@@ -5061,9 +5401,14 @@ a.avatar[aria-disabled="true"] {
5061
5401
  box-shadow: none;
5062
5402
  }
5063
5403
 
5064
- /* Focus baseline — loaded after components so its legacy fallback also covers
5065
- controls that reset their native outline. Modern browsers hide it for focus
5066
- that is not keyboard-relevant; components layer local state on top. */
5404
+ /* Focus baseline — a visible outline is the guaranteed focus affordance. The
5405
+ :focus outline covers legacy browsers; modern browsers hide it for
5406
+ pointer/mouse focus and interactive components layer their ring on top.
5407
+
5408
+ Invariant: an interactive component must never rely on box-shadow alone
5409
+ for its keyboard focus indicator. Components that remove the native
5410
+ outline on :focus-visible replace it with a transparent outline — invisible
5411
+ normally, forced to the system focus color under forced-colors. */
5067
5412
  :focus {
5068
5413
  outline: var(--focus-outline);
5069
5414
  outline-offset: var(--focus-outline-offset);
@@ -5073,42 +5418,13 @@ a.avatar[aria-disabled="true"] {
5073
5418
  :focus:not(:focus-visible) {
5074
5419
  outline: none;
5075
5420
  }
5076
- }
5077
-
5078
- /*
5079
- * Forced-colors focus — centralised so individual components do not need
5080
- * their own fallback. The double :focus keeps specificity high enough
5081
- * (0,0,2,0) to beat a single :focus-visible; against a class selector like
5082
- * .input:focus-visible it ties, but this file loads last so it wins on
5083
- * source order.
5084
- *
5085
- * .choice-card and .join receive focus indirectly (:has on a wrapper that
5086
- * never matches :focus), so they are listed explicitly.
5087
- *
5088
- * .range is excluded from the rectangular fallback: its focus is drawn on
5089
- * the thumb pseudo-element, which is the manipulated surface.
5090
- *
5091
- * box-shadow: none is redundant when forced-color-adjust is auto, but safe
5092
- * when a component uses forced-color-adjust: none (e.g. checked switches /
5093
- * checkboxes).
5094
- */
5095
- @media (forced-colors: active) {
5096
- :focus:focus-visible:not(.range),
5097
- .choice-card:has(:focus-visible),
5098
- .join:has(> :is(.input, .textarea, .select):focus-visible) {
5099
- outline: var(--focus-outline);
5100
- outline-offset: var(--focus-outline-offset);
5101
- box-shadow: none;
5102
- }
5103
-
5104
- .range:focus-visible::-webkit-slider-thumb {
5105
- outline: var(--focus-outline);
5106
- outline-offset: var(--focus-outline-offset);
5107
- }
5108
5421
 
5109
- .range:focus-visible::-moz-range-thumb {
5110
- outline: var(--focus-outline);
5111
- outline-offset: var(--focus-outline-offset);
5422
+ /* Pointer focus must still signal in high-contrast mode, where the shared
5423
+ box-shadow rings are disabled (--focus-ring-shadow: none). */
5424
+ @media (forced-colors: active) {
5425
+ :focus:not(:focus-visible) {
5426
+ outline: var(--focus-outline);
5427
+ }
5112
5428
  }
5113
5429
  }
5114
5430
 
@@ -5159,3 +5475,16 @@ a.avatar[aria-disabled="true"] {
5159
5475
  break-after: avoid;
5160
5476
  }
5161
5477
  }
5478
+
5479
+
5480
+ /*
5481
+ * NOTE: The 'optional/' directory contains layout and utility extensions
5482
+ * (such as optional grids, split, visual effects, and extra utility classes
5483
+ * like .gap-sm, .flex-nowrap, .shadow-sm, etc.). These are intentionally not
5484
+ * bundled here. Two entry points ship them:
5485
+ *
5486
+ * actual-css/css/optional/index optional layer only
5487
+ * actual-css/css/actual.full core + optional
5488
+ *
5489
+ * Import one of these paths in your project when you need them.
5490
+ */