actual-css 0.11.1 → 0.12.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 (80) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +26 -6
  3. package/components.json +4 -16
  4. package/dist/actual.full.css +296 -267
  5. package/dist/actual.full.min.css +1 -1
  6. package/docs/brand/favicon.svg +5 -0
  7. package/docs/brand/lockup.svg +12 -0
  8. package/docs/brand/logo-dark.svg +9 -0
  9. package/docs/brand/logo-light.svg +9 -0
  10. package/docs/brand/logo.svg +11 -0
  11. package/docs/brand/mark.svg +11 -0
  12. package/docs/navigation.json +3 -2
  13. package/docs/pages/components/alert.md +2 -10
  14. package/docs/pages/components/badge.md +3 -9
  15. package/docs/pages/components/card.md +31 -2
  16. package/docs/pages/components/dialog.md +121 -33
  17. package/docs/pages/components/drawer.md +2 -2
  18. package/docs/pages/components/navbar.md +8 -9
  19. package/docs/pages/components/table.md +3 -3
  20. package/docs/pages/enhancements/flyout.md +2 -2
  21. package/docs/pages/enhancements/scrollspy.md +1 -1
  22. package/docs/pages/enhancements/status-bar.md +2 -2
  23. package/docs/pages/examples/overview.md +2 -1
  24. package/docs/pages/forms/date-picker.md +90 -0
  25. package/docs/pages/forms/detached-actions.md +37 -30
  26. package/docs/pages/forms/form-actions.md +61 -28
  27. package/docs/pages/forms/input-policy.md +1 -1
  28. package/docs/pages/forms/overview.md +6 -5
  29. package/docs/pages/forms/sticky-actions.md +85 -83
  30. package/docs/pages/forms/validation.md +5 -5
  31. package/docs/pages/foundations/philosophy.md +5 -3
  32. package/docs/pages/foundations/theming.md +15 -4
  33. package/docs/pages/foundations/tokens.md +50 -33
  34. package/docs/pages/foundations/typography.md +4 -7
  35. package/docs/pages/guides/bootstrap.md +2 -2
  36. package/docs/pages/guides/design-tools.md +2 -2
  37. package/docs/pages/guides/integrating-actual-css.md +5 -1
  38. package/docs/pages/guides/introduction.md +3 -4
  39. package/docs/pages/guides/modular-import.md +3 -0
  40. package/docs/pages/guides/progressive-enhancement.md +2 -2
  41. package/docs/pages/guides/tailwind.md +70 -0
  42. package/docs/pages/guides/upgrading.md +74 -0
  43. package/docs/pages/layout/cluster.md +28 -2
  44. package/docs/pages/layout/overview.md +1 -1
  45. package/docs/pages/layout/stack.md +21 -16
  46. package/docs/pages/layout/topbar.md +1 -1
  47. package/docs/pages/patterns/navigation-list.md +16 -46
  48. package/docs/pages/patterns/overview.md +3 -4
  49. package/docs/pages/utilities/flex.md +30 -20
  50. package/docs/pages/utilities/sizing-wrapping.md +1 -1
  51. package/docs/pages/utilities/spacing.md +4 -3
  52. package/package.json +2 -2
  53. package/reserved-classes.json +3 -3
  54. package/skills/actual-css/references/enhancements.md +10 -7
  55. package/src/cli/actual-css.js +39 -13
  56. package/src/css/components/avatar.css +16 -9
  57. package/src/css/components/badge.css +1 -5
  58. package/src/css/components/button.css +3 -9
  59. package/src/css/components/card.css +18 -10
  60. package/src/css/components/chat.css +4 -5
  61. package/src/css/components/drawer.css +19 -3
  62. package/src/css/components/flyout.css +3 -0
  63. package/src/css/components/index.css +0 -1
  64. package/src/css/components/modal.css +96 -124
  65. package/src/css/components/navbar.css +14 -25
  66. package/src/css/core/base.css +1 -1
  67. package/src/css/core/reset.css +2 -1
  68. package/src/css/core/theme.css +4 -0
  69. package/src/css/core/tokens.css +28 -10
  70. package/src/css/core/variants.css +41 -14
  71. package/src/css/forms/form-actions.css +15 -10
  72. package/src/css/layout/cluster.css +11 -0
  73. package/src/css/layout/topbar.css +12 -6
  74. package/src/css/utilities/base.css +5 -0
  75. package/src/css/utilities/extra.css +3 -7
  76. package/src/tooling/color.js +65 -0
  77. package/src/tooling/design-tokens.js +9 -1
  78. package/src/tooling/theme-contrast.js +209 -0
  79. package/docs/pages/patterns/actions.md +0 -52
  80. package/src/css/components/actions.css +0 -27
@@ -22,6 +22,7 @@
22
22
  * Internal:
23
23
  * --drawer-closed-translate (side/RTL positioning state)
24
24
  * --drawer-focus-bleed
25
+ * --drawer-close-reserve (header room for a corner close; zero without a close)
25
26
  * --drawer-motion-duration (shared by panel and ::backdrop so exit duration
26
27
  * changes in one place; entry state redefines it longer)
27
28
  */
@@ -31,6 +32,10 @@
31
32
  /* The inset a direct .bleed child cancels (components/bleed.css). */
32
33
  --surface-pad: var(--drawer-pad);
33
34
  --drawer-closed-translate: -100%;
35
+ /* Header room for a corner close. Unconditional baseline: without :has()
36
+ (Degraded tier) a header keeps too much room rather than a title running
37
+ under the close. Zeroed when the drawer has no close. */
38
+ --drawer-close-reserve: calc(var(--close-size, var(--control-size)) + var(--space-30));
34
39
  /* Exit is shorter than entry so the panel clears quickly; the scrim reads
35
40
  the same variable through inheritance and never lags the panel. */
36
41
  --drawer-motion-duration: var(--duration);
@@ -210,9 +215,20 @@
210
215
  }
211
216
  }
212
217
 
218
+ /* No close anywhere: reclaim the reserve. Deliberately coarse — a close in
219
+ the body keeps the reserve, which costs only room, never overlap. */
220
+ :where(dialog).drawer:not(:has(.close)) {
221
+ --drawer-close-reserve: 0px;
222
+ }
223
+
213
224
  /* The header reserves inline room so the title never runs under the close
214
- control. */
225
+ control. A .bleed header keeps at least its band inset. */
215
226
  :where(dialog).drawer > header,
216
- :where(dialog).drawer > form > header {
217
- padding-inline-end: calc(var(--close-size, var(--control-size)) + var(--space-30));
227
+ :where(dialog).drawer > :where(form) > header {
228
+ padding-inline-end: var(--drawer-close-reserve);
229
+ }
230
+
231
+ :where(dialog).drawer > header:where(.bleed),
232
+ :where(dialog).drawer > :where(form) > header:where(.bleed) {
233
+ padding-inline-end: max(var(--bleed-pad, 0px), var(--drawer-close-reserve));
218
234
  }
@@ -50,6 +50,9 @@
50
50
  position: absolute;
51
51
  inset-block-start: calc(100% + var(--space-10));
52
52
  inset-inline-start: 0;
53
+ /* Anatomy, not a default to replace: `flyout grid-3` (mega menu) takes its
54
+ columns from the grid preset and keeps this tight gap, because each
55
+ section pads itself — the visible gutter is section padding + gap. */
53
56
  display: grid;
54
57
  gap: var(--space-10);
55
58
  inline-size: var(--flyout-inline-size, max-content);
@@ -3,7 +3,6 @@
3
3
  @import "./close.css";
4
4
  @import "./badge.css";
5
5
  @import "./alert.css";
6
- @import "./actions.css";
7
6
 
8
7
  /* Surfaces */
9
8
  @import "./card.css";
@@ -9,15 +9,19 @@
9
9
  * Public hooks:
10
10
  * --modal-size
11
11
  * --modal-pad
12
- * --modal-header-bg
13
12
  * --dialog-viewport-gap
14
13
  * --dialog-icon-size
15
14
  * --dialog-icon-glyph-size
16
15
  *
17
16
  * The corner close control is .close (components/close.css).
18
17
  *
18
+ * The body footer is a structural slot with no layout of its own: its action
19
+ * row is composed in markup (`<footer class="cluster justify-content-end">`),
20
+ * as in the drawer. A default row here would fight every composed one.
21
+ *
19
22
  * Internal:
20
23
  * --dialog-inner-radius
24
+ * --dialog-close-reserve (header room for a corner close; zero without a close)
21
25
  * --dialog-focus-bleed
22
26
  * --dialog-viewport-block
23
27
  * --dialog-viewport-gap-default
@@ -26,10 +30,8 @@
26
30
  :where(dialog).modal {
27
31
  /* --dialog-viewport-gap stays fallback-only so the responsive default can
28
32
  live in an internal token: a media query moves it without closing the
29
- public hook.
30
-
31
- calc() multiplication is not in the supported browser range, so the
32
- doubled gap is spelled out with an additive calc. */
33
+ public hook. `-2x` is the gap on the two viewport edges, shared by the
34
+ width and height caps. */
33
35
  --dialog-viewport-gap-default: var(--space-40);
34
36
  --dialog-viewport-gap-2x: calc(
35
37
  var(--dialog-viewport-gap, var(--dialog-viewport-gap-default)) +
@@ -37,10 +39,19 @@
37
39
  );
38
40
 
39
41
  /* The panel is borderless, so its inner and outer radius are the same. Kept
40
- as one name for the padded wrapper and any edge band (header,
41
- confirmation footer) so none of them drifts from the corner. */
42
+ as one name for the padded wrapper and the header band; a .bleed band
43
+ inherits it from the wrapper, so none of them drifts from the corner. */
42
44
  --dialog-inner-radius: var(--radius-lg);
43
45
 
46
+ /* Header room for a corner close. Unconditional baseline: without :has()
47
+ (Degraded tier) a header keeps too much room rather than a title running
48
+ under the close. Zeroed when the dialog has no close. */
49
+ --dialog-close-reserve: calc(
50
+ var(--close-size, var(--control-size)) +
51
+ var(--space-20) +
52
+ var(--space-30)
53
+ );
54
+
44
55
  /* Keep the surface anchored to the viewport in the top layer. This also
45
56
  provides the containing block for the absolutely positioned close button. */
46
57
  position: fixed;
@@ -73,9 +84,9 @@
73
84
  * both edges, which reads as horizontal movement.
74
85
  */
75
86
  transform: translateY(0.35rem);
76
- transition:
77
- opacity var(--duration) var(--ease-exit),
78
- transform var(--duration) var(--ease-exit);
87
+ transition-property: opacity, transform;
88
+ transition-duration: var(--duration);
89
+ transition-timing-function: var(--ease-exit);
79
90
  }
80
91
 
81
92
  @media (forced-colors: active) {
@@ -99,10 +110,8 @@
99
110
  padding: var(--surface-pad);
100
111
  }
101
112
 
102
- /* Optional dialog chrome lives beside the padded body wrapper. Its leading
103
- region and trailing controls share one center axis; wrapping the close in
104
- the trailing cluster keeps it in flow instead of invoking the absolute
105
- corner treatment from close.css. */
113
+ /* Dialog chrome beside the padded body wrapper. Wrapping the close in the
114
+ trailing cluster keeps it in flow, not the absolute corner overlay. */
106
115
  :where(dialog).modal > header {
107
116
  display: flex;
108
117
  align-items: center;
@@ -112,7 +121,14 @@
112
121
  padding: var(--modal-pad, var(--space-40));
113
122
  border-start-start-radius: var(--dialog-inner-radius);
114
123
  border-start-end-radius: var(--dialog-inner-radius);
115
- background: var(--modal-header-bg, var(--surface-subtle));
124
+ }
125
+
126
+ /* Paint is not anatomy: the default band fill has zero specificity, so a
127
+ .background-* utility on the header replaces it. The layout above stays at
128
+ the component's weight — a composed .cluster must not reset the title/close
129
+ alignment. */
130
+ :where(dialog.modal > header) {
131
+ background: var(--surface-subtle);
116
132
  }
117
133
 
118
134
  :where(dialog).modal > header > .media {
@@ -133,18 +149,16 @@
133
149
  flex: none;
134
150
  }
135
151
 
136
- @supports selector(dialog:has(> header)) {
137
- /* The header stays fixed while the body wrapper owns overflow. Without
138
- :has(), compact dialogs keep ordinary block flow as the fallback. */
139
- :where(dialog).modal[open]:has(> header) {
140
- display: grid;
141
- grid-template-rows: auto minmax(0, 1fr);
142
- }
152
+ /* The header stays fixed while the body wrapper owns overflow. Without :has()
153
+ these drop individually and compact dialogs keep ordinary block flow. */
154
+ :where(dialog).modal[open]:has(> header) {
155
+ display: grid;
156
+ grid-template-rows: auto minmax(0, 1fr);
157
+ }
143
158
 
144
- :where(dialog).modal:has(> header) > :is(form, .stack) {
145
- min-block-size: 0;
146
- max-block-size: none;
147
- }
159
+ :where(dialog).modal:has(> header) > :is(form, .stack) {
160
+ min-block-size: 0;
161
+ max-block-size: none;
148
162
  }
149
163
 
150
164
  @supports (block-size: 100svh) {
@@ -163,10 +177,21 @@
163
177
 
164
178
  :where(dialog).modal.scrollable > form,
165
179
  :where(dialog).modal.scrollable > .stack {
166
- grid-template-rows: auto minmax(0, 1fr) auto;
180
+ display: flex;
181
+ flex-direction: column;
167
182
  overflow: hidden;
168
183
  }
169
184
 
185
+ /* Regions carry their own behaviour: a structural header or footer keeps its
186
+ size, every other child absorbs the free space and scrolls. Nothing here has
187
+ to detect which regions exist — an external header leaves the wrapper with
188
+ content + footer and this still holds. Each non-chrome child is its own
189
+ scrollport; wrap the body in one region for a single one. */
190
+ :where(dialog).modal.scrollable > form > :is(header, footer),
191
+ :where(dialog).modal.scrollable > .stack > :is(header, footer) {
192
+ flex: none;
193
+ }
194
+
170
195
  :where(dialog).modal.scrollable > form > :not(:is(header, footer)),
171
196
  :where(dialog).modal.scrollable > .stack > :not(:is(header, footer)) {
172
197
  --dialog-focus-bleed: calc(
@@ -174,6 +199,7 @@
174
199
  max(var(--focus-ring-width), var(--border-width) * 2)
175
200
  );
176
201
 
202
+ flex: 1 1 auto;
177
203
  min-block-size: 0;
178
204
  /* Keep focus paint inside the clip without moving the body off the fixed
179
205
  header and footer edges. The reserve covers the thicker of the two
@@ -202,59 +228,30 @@
202
228
  margin-block: 0;
203
229
  }
204
230
 
231
+ /* No close anywhere: reclaim the reserve. Deliberately coarse — a close in
232
+ the body keeps the reserve, which costs only room, never overlap. */
233
+ :where(dialog).modal:not(:has(.close)) {
234
+ --dialog-close-reserve: 0px;
235
+ }
236
+
205
237
  /* The close button is anchored to the dialog at top inline-end, out of the
206
- content flow; headers reserve inline room so titles never run under it.
207
- The form/.stack anatomy is structural, so it stays in :where() and never
208
- lends .stack's specificity to a bare <form>. */
238
+ content flow; headers reserve inline room so titles never run under it. A
239
+ .bleed header keeps at least its band inset. The form/.stack anatomy is
240
+ structural, so it stays in :where() and never lends .stack's specificity to
241
+ a bare <form>. */
209
242
  :where(dialog).modal > :where(form, .stack) > header {
210
- padding-inline-end: calc(
211
- var(--close-size, var(--control-size)) +
212
- var(--space-20) +
213
- var(--space-30)
214
- );
243
+ padding-inline-end: var(--dialog-close-reserve);
244
+ }
245
+
246
+ :where(dialog).modal > :where(form, .stack) > header:where(.bleed) {
247
+ padding-inline-end: max(var(--bleed-pad, 0px), var(--dialog-close-reserve));
215
248
  }
216
249
 
217
250
  /* A corner .close that is a direct child of the chrome header needs the same
218
251
  reserve, but never below the header's own pad. Kept as its own rule so an
219
252
  unsupported :has() drops only this geometry, not the wrapper reserve. */
220
253
  :where(dialog).modal > header:has(> .close) {
221
- padding-inline-end: max(
222
- var(--modal-pad, var(--space-40)),
223
- calc(var(--close-size, var(--control-size)) + var(--space-20) + var(--space-30))
224
- );
225
- }
226
-
227
- :where(dialog).modal > :where(form, .stack) > footer {
228
- display: flex;
229
- flex-wrap: wrap;
230
- justify-content: flex-end;
231
- gap: var(--gap);
232
- }
233
-
234
- /* Confirmation layout — a focused message followed by a visually distinct
235
- action band. The media primitive owns icon/content alignment; this variant
236
- only removes the standard form inset so both regions can own their padding. */
237
- :where(dialog).modal.dialog-confirmation > form {
238
- --surface-pad: 0px;
239
-
240
- gap: 0;
241
- padding: 0;
242
- }
243
-
244
- :where(dialog).modal.dialog-confirmation > form > :is(.media, header) {
245
- gap: var(--space-40);
246
- padding: var(--space-50);
247
- }
248
-
249
- /* The action band splits a decision pair: the cancel-style action holds the
250
- leading edge and the committing action the trailing one, so the destructive
251
- choice is never adjacent to the dismiss. */
252
- :where(dialog).modal.dialog-confirmation > form > footer {
253
- justify-content: space-between;
254
- padding: var(--space-40) var(--space-50);
255
- border-end-start-radius: var(--dialog-inner-radius);
256
- border-end-end-radius: var(--dialog-inner-radius);
257
- background: var(--surface-subtle);
254
+ padding-inline-end: max(var(--modal-pad, var(--space-40)), var(--dialog-close-reserve));
258
255
  }
259
256
 
260
257
  /* Intent boundary — inherited intents do not tint the icon well. A local
@@ -267,14 +264,17 @@
267
264
  --ui-fg: initial;
268
265
  }
269
266
 
267
+ /* The size hooks are read here, never declared: an author sets them on the
268
+ icon's media row (or the dialog) so both the circle and the title that
269
+ centers on it inherit one value. A local declaration on .dialog-icon would
270
+ shadow that value for the circle while the title kept the inherited one —
271
+ the two centers would drift. An override on the icon alone is deliberately
272
+ local and moves only the circle. */
270
273
  .dialog-icon {
271
- --dialog-icon-size: 3rem;
272
- --dialog-icon-glyph-size: 1.5rem;
273
-
274
274
  display: inline-grid;
275
275
  place-items: center;
276
- inline-size: var(--dialog-icon-size);
277
- block-size: var(--dialog-icon-size);
276
+ inline-size: var(--dialog-icon-size, 3rem);
277
+ block-size: var(--dialog-icon-size, 3rem);
278
278
  flex: none;
279
279
  border-radius: var(--radius-full);
280
280
  background: var(--ui-bg, var(--surface-subtle));
@@ -284,46 +284,27 @@
284
284
  .dialog-icon > * {
285
285
  display: inline-grid;
286
286
  place-items: center;
287
- inline-size: var(--dialog-icon-glyph-size);
288
- block-size: var(--dialog-icon-glyph-size);
289
- font-size: var(--dialog-icon-glyph-size);
287
+ inline-size: var(--dialog-icon-glyph-size, 1.5rem);
288
+ block-size: var(--dialog-icon-glyph-size, 1.5rem);
289
+ font-size: var(--dialog-icon-glyph-size, 1.5rem);
290
290
  line-height: 1;
291
291
  }
292
292
 
293
- :where(dialog).modal.dialog-confirmation > form > .media > header {
294
- display: grid;
295
- gap: var(--space-20);
296
- }
297
-
298
- :where(dialog).modal.dialog-confirmation > form > .media > header > * {
299
- margin-block: 0;
300
- }
301
-
302
- /* The first heading occupies the icon row, so its text centers on the icon
303
- without a font-relative nudge. Following copy stays in the same column. */
304
- :where(dialog).modal.dialog-confirmation
305
- > form
306
- > .media
307
- > header
308
- > :is(h1, h2, h3, h4, h5, h6):first-child {
293
+ /* A leading icon well centers the first heading of the text column beside it:
294
+ the heading occupies the icon row, so its text centers without a
295
+ font-relative nudge, and following copy stays in the same column. Keyed on
296
+ the media anatomy, not on the dialog, so a card or page row composes it too. */
297
+ .media > .dialog-icon + * > :is(h1, h2, h3, h4, h5, h6):first-child {
309
298
  display: flex;
310
299
  align-items: center;
311
300
  min-block-size: var(--dialog-icon-size, 3rem);
312
- }
313
-
314
- @media (max-width: 30rem) {
315
- :where(dialog).modal.dialog-confirmation > form > :is(.media, header),
316
- :where(dialog).modal.dialog-confirmation > form > footer {
317
- padding: var(--space-40);
318
- }
301
+ margin-block: 0;
319
302
  }
320
303
 
321
304
  :where(dialog).modal[open] {
322
305
  opacity: 1;
323
306
  transform: none;
324
- transition:
325
- opacity var(--duration) var(--ease-enter),
326
- transform var(--duration) var(--ease-enter);
307
+ transition-timing-function: var(--ease-enter);
327
308
  }
328
309
 
329
310
  @starting-style {
@@ -338,31 +319,22 @@
338
319
  opacity: 0;
339
320
  /* Scrim curve stays neutral `ease`; only the panel carries the character
340
321
  curves. */
341
- transition: opacity var(--duration) ease;
322
+ transition-property: opacity;
323
+ transition-duration: var(--duration);
324
+ transition-timing-function: ease;
342
325
  }
343
326
 
344
327
  @supports (transition-behavior: allow-discrete) {
328
+ /* Add the discrete top-layer pair to the property list. Duration and timing
329
+ stay on the base rules, so [open] still only swaps the easing. */
345
330
  :where(dialog).modal {
346
- transition:
347
- opacity var(--duration) var(--ease-exit),
348
- transform var(--duration) var(--ease-exit),
349
- overlay var(--duration) allow-discrete,
350
- display var(--duration) allow-discrete;
351
- }
352
-
353
- :where(dialog).modal[open] {
354
- transition:
355
- opacity var(--duration) var(--ease-enter),
356
- transform var(--duration) var(--ease-enter),
357
- overlay var(--duration) allow-discrete,
358
- display var(--duration) allow-discrete;
331
+ transition-property: opacity, transform, overlay, display;
332
+ transition-behavior: allow-discrete;
359
333
  }
360
334
 
361
335
  :where(dialog).modal::backdrop {
362
- transition:
363
- opacity var(--duration) ease,
364
- overlay var(--duration) allow-discrete,
365
- display var(--duration) allow-discrete;
336
+ transition-property: opacity, overlay, display;
337
+ transition-behavior: allow-discrete;
366
338
  }
367
339
  }
368
340
 
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Docs: docs/pages/components/navbar.md
5
5
  *
6
- * .navbar is a horizontal bar; .navbar-brand is the brand link; .navbar-nav is
7
- * the inline link list using .nav-link items. .nav-list is the vertical nav
6
+ * .navbar is a horizontal bar; .navbar-brand is the brand link; the inline
7
+ * links are a .cluster using .nav-link items. .nav-list is the vertical nav
8
8
  * list shared with .drawer, fed by the shared --gap token.
9
9
  *
10
10
  * Internal:
@@ -53,17 +53,6 @@
53
53
  border-radius: var(--radius-sm);
54
54
  }
55
55
 
56
- .navbar-nav {
57
- display: flex;
58
- flex-direction: row;
59
- flex-wrap: wrap;
60
- align-items: center;
61
- gap: var(--space-10);
62
- margin: 0;
63
- padding: 0;
64
- list-style: none;
65
- }
66
-
67
56
  .nav-link {
68
57
  display: inline-flex;
69
58
  align-items: center;
@@ -86,19 +75,16 @@
86
75
  removed everywhere else, never serialized as aria-current="false".
87
76
  Selection never changes text metrics: the shared medium weight is kept and
88
77
  the current link is carried by the selected accent. */
89
- .nav-link[aria-current] {
90
- background: var(--surface-subtle);
91
- color: var(--state-selected);
92
- }
93
-
94
78
  .nav-link:hover {
95
79
  background: var(--surface-subtle);
96
80
  color: var(--text);
97
81
  }
98
82
 
99
- /* A current link keeps its accent on hover: without this, the hover rule above
100
- would drop it back to --text. The surface hover still applies. */
101
- .nav-link[aria-current]:hover {
83
+ /* Current last, equal specificity to :hover (0,2,0): a hovered current link
84
+ keeps the selected accent instead of dropping to --text. The surface fill is
85
+ shared with the hover rule above. */
86
+ .nav-link[aria-current] {
87
+ background: var(--surface-subtle);
102
88
  color: var(--state-selected);
103
89
  }
104
90
 
@@ -118,19 +104,22 @@
118
104
  color: var(--ui-fg, var(--text));
119
105
  }
120
106
 
121
- .nav-link[aria-current],
122
- .nav-link[aria-current]:hover {
107
+ /* After :hover so a hovered current link keeps the selected accent. */
108
+ .nav-link[aria-current] {
123
109
  color: var(--ui-fg, var(--state-selected));
124
110
  }
125
111
  }
126
112
 
127
113
  /* Nav list — a vertical navigation list. Used by .drawer for stacked links.
128
114
  Links fill each row so the whole line is an interactive target; the
129
- horizontal .navbar-nav links keep their intrinsic size. */
115
+ horizontal .cluster links keep their intrinsic size. */
116
+ /* A vertical nav reads as one list, not a stack of separate buttons: each row
117
+ is already a control-height target, so the rows sit a hair apart. The grid
118
+ is the component's own layout; for a horizontal row use .cluster instead. */
130
119
  .nav-list {
131
120
  display: grid;
132
121
  grid-template-columns: minmax(0, 1fr);
133
- --gap: var(--space-20);
122
+ --gap: var(--space-10);
134
123
  gap: var(--gap);
135
124
  margin: 0;
136
125
  padding: 0;
@@ -7,7 +7,7 @@
7
7
  * Public hooks:
8
8
  * --link
9
9
  * --hr-space
10
- * --font-width / --font-width-dense (see variants.css for the dense application)
10
+ * --font-width (the document's font-stretch; a theme typography choice)
11
11
  */
12
12
 
13
13
  body {
@@ -36,7 +36,8 @@ select {
36
36
 
37
37
  *,
38
38
  *::before,
39
- *::after {
39
+ *::after,
40
+ ::backdrop {
40
41
  animation-duration: 0.01ms !important;
41
42
  animation-delay: 0s !important;
42
43
  animation-iteration-count: 1 !important;
@@ -79,6 +79,10 @@
79
79
  fields, a solid band), which rules out the primary itself. */
80
80
  --focus: hsl(268 30% 55%);
81
81
  --hover-overlay: hsl(260 20% 10% / 0.04);
82
+ /* Solid fills take a stronger wash than the subtle shared overlay. Black in
83
+ both schemes on purpose: a solid button darkens on hover even in dark mode
84
+ (see themes/gradient.css), unlike the neutral washes. */
85
+ --hover-overlay-solid: rgb(0 0 0 / 0.14);
82
86
 
83
87
  /* Radius scale — themes may override the whole scale (e.g. [data-theme="square"]) */
84
88
  --radius: 0.5rem;
@@ -20,7 +20,6 @@
20
20
  --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
21
21
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
22
22
  --font-width: 100%;
23
- --font-width-dense: 95%;
24
23
  --line-height: 1.5;
25
24
  --line-height-tight: 1.25;
26
25
  --line-height-relaxed: 1.75;
@@ -104,8 +103,9 @@
104
103
  --bar-height: 0.65rem;
105
104
 
106
105
  /* Controls — inputs and buttons share --control-size so heights always match.
107
- .compact/.spacious (variants.css) switch geometry without changing
108
- --control-font-size. Local .sm/.lg component sizes switch all three.
106
+ .compact/.spacious (variants.css) switch geometry (--control-size,
107
+ --control-pad-x) without changing --control-font-size. Local .sm/.lg
108
+ component sizes switch all three.
109
109
 
110
110
  Inline choices (checkbox, radio, switch) are typographic, not field-sized:
111
111
  they sit on a line of text, so they follow --control-font-size through
@@ -116,17 +116,35 @@
116
116
 
117
117
  --control-pad-x is the opposite kind of token: a resolved geometry length,
118
118
  not an em ratio. Every part of one control must see the same horizontal
119
- distance, so it is stated in the typographic tokens rather than an em
120
- whose value would change on a descendant with its own font size (the
121
- input-icon glyph, made larger by --input-icon-size). It mirrors
122
- --control-font-size and is rebound beside it at the .sm/.lg boundaries
123
- (variants.css); declaring it only in :root would freeze the md value,
124
- because a var() inside a custom property resolves where it is declared. */
119
+ distance, so it is stated in lengths rather than an em whose value would
120
+ change on a descendant with its own font size (the input-icon glyph, made
121
+ larger by --input-icon-size).
122
+
123
+ Like --control-size, it is a scale plus a selection: themes set the
124
+ -sm/-md/-lg steps, and :root, .sm/.lg and .compact/.spacious (variants.css)
125
+ pick one. A theme that wrote --control-pad-x itself would be overwritten
126
+ by every size and density boundary — bootstrap-v6's 12px became a wider
127
+ 13px inside .compact. The selection is rebound at each boundary because a
128
+ var() inside a custom property resolves where it is declared.
129
+
130
+ The boundaries also write --control-step-sm/-md/-lg: which step is
131
+ selected, as 0/1 weights that mean the same under every theme. A theme
132
+ that sets its own steps rebuilds the selection from them in its own block
133
+ (bootstrap-v6.css), so a nested island of it reads its steps without
134
+ resetting the density it sits in. Never rebind on every [data-theme]: a
135
+ palette-only island then became a geometry boundary that an inherited
136
+ --control-size or --control-pad-x no longer crossed. */
125
137
  --control-size-sm: 2rem;
126
138
  --control-size-md: 2.375rem;
127
139
  --control-size-lg: 2.75rem;
128
140
  --control-size: var(--control-size-md);
129
- --control-pad-x: var(--font-size-md);
141
+ --control-pad-x-sm: var(--font-size-xs);
142
+ --control-pad-x-md: var(--font-size-md);
143
+ --control-pad-x-lg: var(--font-size-lg);
144
+ --control-pad-x: var(--control-pad-x-md);
145
+ --control-step-sm: 0;
146
+ --control-step-md: 1;
147
+ --control-step-lg: 0;
130
148
  --control-font-size: var(--font-size-md);
131
149
  --choice-control-size: 1.125em;
132
150
  --disabled-opacity: 0.65;