actual-css 0.11.1 → 0.13.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 (123) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +25 -6
  3. package/components.json +12 -18
  4. package/dist/actual.full.css +780 -479
  5. package/dist/actual.full.js +1 -1
  6. package/dist/actual.full.min.css +1 -1
  7. package/docs/brand/favicon.svg +5 -0
  8. package/docs/brand/lockup.svg +12 -0
  9. package/docs/brand/logo-dark.svg +9 -0
  10. package/docs/brand/logo-light.svg +9 -0
  11. package/docs/brand/logo.svg +11 -0
  12. package/docs/brand/mark.svg +11 -0
  13. package/docs/design-notes/architecture.md +67 -0
  14. package/docs/design-notes/steps.md +45 -28
  15. package/docs/navigation.json +3 -2
  16. package/docs/pages/components/alert.md +2 -10
  17. package/docs/pages/components/avatar.md +17 -11
  18. package/docs/pages/components/badge.md +3 -9
  19. package/docs/pages/components/breadcrumb.md +16 -2
  20. package/docs/pages/components/card.md +31 -2
  21. package/docs/pages/components/dialog.md +121 -33
  22. package/docs/pages/components/drawer.md +2 -2
  23. package/docs/pages/components/list.md +9 -13
  24. package/docs/pages/components/navbar.md +49 -30
  25. package/docs/pages/components/pagination.md +19 -0
  26. package/docs/pages/components/steps.md +24 -20
  27. package/docs/pages/components/table.md +3 -3
  28. package/docs/pages/enhancements/flyout.md +2 -2
  29. package/docs/pages/enhancements/scrollspy.md +7 -7
  30. package/docs/pages/enhancements/status-bar.md +2 -2
  31. package/docs/pages/enhancements/tabs.md +74 -3
  32. package/docs/pages/examples/overview.md +2 -1
  33. package/docs/pages/forms/date-picker.md +90 -0
  34. package/docs/pages/forms/detached-actions.md +37 -30
  35. package/docs/pages/forms/floating-field.md +21 -7
  36. package/docs/pages/forms/form-actions.md +58 -28
  37. package/docs/pages/forms/input-policy.md +1 -1
  38. package/docs/pages/forms/overview.md +6 -5
  39. package/docs/pages/forms/states-and-sizes.md +15 -15
  40. package/docs/pages/forms/sticky-actions.md +85 -83
  41. package/docs/pages/forms/validation.md +5 -5
  42. package/docs/pages/foundations/philosophy.md +102 -4
  43. package/docs/pages/foundations/theming.md +15 -4
  44. package/docs/pages/foundations/tokens.md +89 -36
  45. package/docs/pages/foundations/typography.md +4 -7
  46. package/docs/pages/guides/bootstrap.md +2 -2
  47. package/docs/pages/guides/design-tools.md +2 -2
  48. package/docs/pages/guides/integrating-actual-css.md +79 -9
  49. package/docs/pages/guides/introduction.md +5 -5
  50. package/docs/pages/guides/modular-import.md +3 -0
  51. package/docs/pages/guides/progressive-enhancement.md +27 -9
  52. package/docs/pages/guides/tailwind.md +70 -0
  53. package/docs/pages/guides/upgrading.md +151 -0
  54. package/docs/pages/layout/center.md +2 -1
  55. package/docs/pages/layout/cluster.md +37 -4
  56. package/docs/pages/layout/grid.md +2 -0
  57. package/docs/pages/layout/overview.md +1 -1
  58. package/docs/pages/layout/sidebar-layout.md +3 -0
  59. package/docs/pages/layout/stack.md +21 -16
  60. package/docs/pages/layout/switcher.md +2 -0
  61. package/docs/pages/layout/topbar.md +7 -5
  62. package/docs/pages/patterns/icon-slot.md +17 -1
  63. package/docs/pages/patterns/navigation-list.md +16 -46
  64. package/docs/pages/patterns/overview.md +3 -4
  65. package/docs/pages/utilities/flex.md +30 -20
  66. package/docs/pages/utilities/sizing-wrapping.md +52 -7
  67. package/docs/pages/utilities/spacing.md +4 -3
  68. package/package.json +6 -6
  69. package/reserved-classes.json +3 -3
  70. package/skills/actual-css/references/enhancements.md +10 -7
  71. package/src/cli/actual-css.js +39 -13
  72. package/src/css/components/alert.css +34 -34
  73. package/src/css/components/avatar.css +8 -17
  74. package/src/css/components/badge.css +1 -6
  75. package/src/css/components/breadcrumb.css +74 -8
  76. package/src/css/components/button.css +3 -9
  77. package/src/css/components/card.css +18 -10
  78. package/src/css/components/chat.css +4 -5
  79. package/src/css/components/drawer.css +42 -45
  80. package/src/css/components/flyout.css +3 -1
  81. package/src/css/components/index.css +0 -1
  82. package/src/css/components/join.css +2 -2
  83. package/src/css/components/list.css +10 -10
  84. package/src/css/components/modal.css +113 -134
  85. package/src/css/components/navbar.css +22 -31
  86. package/src/css/components/pagination.css +5 -0
  87. package/src/css/components/scrollspy.css +1 -1
  88. package/src/css/components/steps.css +66 -12
  89. package/src/css/components/tab.css +134 -4
  90. package/src/css/core/base.css +1 -1
  91. package/src/css/core/focus.css +8 -6
  92. package/src/css/core/reset.css +2 -1
  93. package/src/css/core/theme.css +10 -1
  94. package/src/css/core/tokens.css +32 -12
  95. package/src/css/core/variants.css +41 -14
  96. package/src/css/forms/choice-card.css +1 -1
  97. package/src/css/forms/choice.css +1 -1
  98. package/src/css/forms/control.css +1 -1
  99. package/src/css/forms/floating-field.css +20 -18
  100. package/src/css/forms/form-actions.css +14 -12
  101. package/src/css/forms/native.css +2 -2
  102. package/src/css/forms/otp.css +4 -5
  103. package/src/css/forms/switch.css +1 -1
  104. package/src/css/forms/validation.css +23 -30
  105. package/src/css/layout/center.css +2 -0
  106. package/src/css/layout/cluster.css +25 -0
  107. package/src/css/layout/grid.css +7 -0
  108. package/src/css/layout/scroller.css +19 -3
  109. package/src/css/layout/sidebar-layout.css +3 -0
  110. package/src/css/layout/switcher.css +3 -0
  111. package/src/css/layout/topbar.css +12 -6
  112. package/src/css/utilities/base.css +5 -0
  113. package/src/css/utilities/extra.css +3 -7
  114. package/src/js/full.js +4 -0
  115. package/src/js/inline-visible.js +54 -0
  116. package/src/js/parsed.js +21 -0
  117. package/src/js/tab.js +33 -7
  118. package/src/tooling/color.js +65 -0
  119. package/src/tooling/design-export.js +1 -1
  120. package/src/tooling/design-tokens.js +53 -34
  121. package/src/tooling/theme-contrast.js +217 -0
  122. package/docs/pages/patterns/actions.md +0 -52
  123. package/src/css/components/actions.css +0 -27
@@ -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);
@@ -90,7 +93,6 @@
90
93
  .flyout[popover] {
91
94
  position: fixed;
92
95
  inset: auto;
93
- margin: 0;
94
96
  }
95
97
 
96
98
  /* A presentation class may own its *visible* display type, but must not
@@ -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";
@@ -40,7 +40,7 @@
40
40
  }
41
41
 
42
42
  .join-addon {
43
- --control-border: var(--border);
43
+ --control-border: var(--border-control);
44
44
  --control-bg: var(--surface-subtle);
45
45
  --control-fg: var(--text-muted);
46
46
 
@@ -145,5 +145,5 @@
145
145
  The second :has() covers a control nested one wrapper level deep. */
146
146
  .join:has(> :is(.input, .textarea, .select)) > .btn.outline,
147
147
  .join:has(> * > :is(.input, .textarea, .select)) > .btn.outline {
148
- --btn-border-color: var(--intent, var(--border));
148
+ --btn-border-color: var(--intent, var(--border-control));
149
149
  }
@@ -43,9 +43,10 @@
43
43
  .list > :where(.list-item),
44
44
  .list > li > .list-item {
45
45
  display: flex;
46
- /* Regions align to the first line, so leading, title, and trailing controls
47
- share one horizontal line while supporting text wraps beneath. */
48
- align-items: start;
46
+ /* Centered fits the short rows lists mostly hold. Read through
47
+ --items-align so .items-start on the row or the list wins without
48
+ outranking this selector — the opt-out for long supporting text. */
49
+ align-items: var(--items-align, center);
49
50
  gap: var(--list-item-gap);
50
51
  min-block-size: var(--list-item-min-size);
51
52
  padding-block: var(--list-item-pad-block);
@@ -67,21 +68,20 @@ a.list-item:hover {
67
68
  background: var(--surface-subtle);
68
69
  }
69
70
 
70
- /* Current row in a navigational list (master/detail). Generic by design: the
71
- visual treatment only cares that the row is current, so presence is the
72
- contract — the attribute is set on the current row and removed everywhere
73
- else, never serialized as aria-current="false". The value (page, location,
74
- …) stays free for assistive technology.
71
+ /* Current row in a navigational list: aria-current="page" when the row opens
72
+ a page, "true" when it is the current item of a master/detail view (a
73
+ conversation, a thread, a record) that is no page of its own. Any other
74
+ value, "false" included, is inert.
75
75
  Calm by design: the row keeps its normal text metrics and color on a subtle
76
76
  surface, and the selected accent lives on an inset inline-start trait only.
77
77
  The trait is a real border, so it survives forced colors without a repaint
78
78
  block. The positioning context lives on every row link, so becoming current
79
79
  never moves an app-positioned decoration to a new containing block. */
80
- a.list-item[aria-current] {
80
+ a.list-item:is([aria-current="page"], [aria-current="true"]) {
81
81
  background: var(--surface-subtle);
82
82
  }
83
83
 
84
- a.list-item[aria-current]::before {
84
+ a.list-item:is([aria-current="page"], [aria-current="true"])::before {
85
85
  content: "";
86
86
  position: absolute;
87
87
  inset-block: var(--list-item-pad-block);
@@ -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,20 @@
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);
132
+ }
133
+
134
+ /* The chrome header is a centered row, so prose margins on a bare title only
135
+ push the band taller and pull the title off the close axis. */
136
+ :where(dialog).modal > header > :is(h1, h2, h3, h4, h5, h6) {
137
+ margin-block: 0;
116
138
  }
117
139
 
118
140
  :where(dialog).modal > header > .media {
@@ -133,18 +155,16 @@
133
155
  flex: none;
134
156
  }
135
157
 
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
- }
158
+ /* The header stays fixed while the body wrapper owns overflow. Without :has()
159
+ these drop individually and compact dialogs keep ordinary block flow. */
160
+ :where(dialog).modal[open]:has(> header) {
161
+ display: grid;
162
+ grid-template-rows: auto minmax(0, 1fr);
163
+ }
143
164
 
144
- :where(dialog).modal:has(> header) > :is(form, .stack) {
145
- min-block-size: 0;
146
- max-block-size: none;
147
- }
165
+ :where(dialog).modal:has(> header) > :is(form, .stack) {
166
+ min-block-size: 0;
167
+ max-block-size: none;
148
168
  }
149
169
 
150
170
  @supports (block-size: 100svh) {
@@ -163,24 +183,37 @@
163
183
 
164
184
  :where(dialog).modal.scrollable > form,
165
185
  :where(dialog).modal.scrollable > .stack {
166
- grid-template-rows: auto minmax(0, 1fr) auto;
186
+ display: flex;
187
+ flex-direction: column;
167
188
  overflow: hidden;
168
189
  }
169
190
 
191
+ /* Regions carry their own behaviour: a structural header or footer keeps its
192
+ size, every other child absorbs the free space and scrolls. Nothing here has
193
+ to detect which regions exist — an external header leaves the wrapper with
194
+ content + footer and this still holds. Each non-chrome child is its own
195
+ scrollport; wrap the body in one region for a single one. */
196
+ :where(dialog).modal.scrollable > form > :is(header, footer),
197
+ :where(dialog).modal.scrollable > .stack > :is(header, footer) {
198
+ flex: none;
199
+ }
200
+
170
201
  :where(dialog).modal.scrollable > form > :not(:is(header, footer)),
171
202
  :where(dialog).modal.scrollable > .stack > :not(:is(header, footer)) {
172
- --dialog-focus-bleed: calc(
173
- var(--focus-outline-offset) +
174
- max(var(--focus-ring-width), var(--border-width) * 2)
175
- );
203
+ /* Declared here, not as a :root alias: an inherited alias arrives already
204
+ computed, so a --focus-ring-width override on the dialog would not reach
205
+ it. */
206
+ --dialog-focus-bleed: calc(var(--focus-outline-offset) + var(--focus-ring-width));
176
207
 
208
+ flex: 1 1 auto;
177
209
  min-block-size: 0;
178
- /* Keep focus paint inside the clip without moving the body off the fixed
179
- header and footer edges. The reserve covers the thicker of the two
180
- outside indicators: the :focus baseline (2 × border) and the component
181
- line (--focus-ring-width, 3px in bootstrap-v6). */
182
- margin-inline: calc(var(--dialog-focus-bleed) * -1);
183
- padding-inline: var(--dialog-focus-bleed);
210
+ /* Keep focus paint inside the clip, on all four sides (overflow: auto clips
211
+ both axes), without moving the content. Every outside indicator is
212
+ --focus-ring-width wide (core/focus.css). On the block axis the reserve
213
+ reaches into the slot gap, which is wider, so the scrollport never slides
214
+ under the header or footer. */
215
+ margin: calc(var(--dialog-focus-bleed) * -1);
216
+ padding: var(--dialog-focus-bleed);
184
217
  overflow: auto;
185
218
  overscroll-behavior: contain;
186
219
  }
@@ -202,59 +235,30 @@
202
235
  margin-block: 0;
203
236
  }
204
237
 
238
+ /* No close anywhere: reclaim the reserve. Deliberately coarse — a close in
239
+ the body keeps the reserve, which costs only room, never overlap. */
240
+ :where(dialog).modal:not(:has(.close)) {
241
+ --dialog-close-reserve: 0px;
242
+ }
243
+
205
244
  /* 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>. */
245
+ content flow; headers reserve inline room so titles never run under it. A
246
+ .bleed header keeps at least its band inset. The form/.stack anatomy is
247
+ structural, so it stays in :where() and never lends .stack's specificity to
248
+ a bare <form>. */
209
249
  :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
- );
250
+ padding-inline-end: var(--dialog-close-reserve);
251
+ }
252
+
253
+ :where(dialog).modal > :where(form, .stack) > header:where(.bleed) {
254
+ padding-inline-end: max(var(--bleed-pad, 0px), var(--dialog-close-reserve));
215
255
  }
216
256
 
217
257
  /* A corner .close that is a direct child of the chrome header needs the same
218
258
  reserve, but never below the header's own pad. Kept as its own rule so an
219
259
  unsupported :has() drops only this geometry, not the wrapper reserve. */
220
260
  :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);
261
+ padding-inline-end: max(var(--modal-pad, var(--space-40)), var(--dialog-close-reserve));
258
262
  }
259
263
 
260
264
  /* Intent boundary — inherited intents do not tint the icon well. A local
@@ -267,14 +271,17 @@
267
271
  --ui-fg: initial;
268
272
  }
269
273
 
274
+ /* The size hooks are read here, never declared: an author sets them on the
275
+ icon's media row (or the dialog) so both the circle and the title that
276
+ centers on it inherit one value. A local declaration on .dialog-icon would
277
+ shadow that value for the circle while the title kept the inherited one —
278
+ the two centers would drift. An override on the icon alone is deliberately
279
+ local and moves only the circle. */
270
280
  .dialog-icon {
271
- --dialog-icon-size: 3rem;
272
- --dialog-icon-glyph-size: 1.5rem;
273
-
274
281
  display: inline-grid;
275
282
  place-items: center;
276
- inline-size: var(--dialog-icon-size);
277
- block-size: var(--dialog-icon-size);
283
+ inline-size: var(--dialog-icon-size, 3rem);
284
+ block-size: var(--dialog-icon-size, 3rem);
278
285
  flex: none;
279
286
  border-radius: var(--radius-full);
280
287
  background: var(--ui-bg, var(--surface-subtle));
@@ -284,46 +291,27 @@
284
291
  .dialog-icon > * {
285
292
  display: inline-grid;
286
293
  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);
294
+ inline-size: var(--dialog-icon-glyph-size, 1.5rem);
295
+ block-size: var(--dialog-icon-glyph-size, 1.5rem);
296
+ font-size: var(--dialog-icon-glyph-size, 1.5rem);
290
297
  line-height: 1;
291
298
  }
292
299
 
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 {
300
+ /* A leading icon well centers the first heading of the text column beside it:
301
+ the heading occupies the icon row, so its text centers without a
302
+ font-relative nudge, and following copy stays in the same column. Keyed on
303
+ the media anatomy, not on the dialog, so a card or page row composes it too. */
304
+ .media > .dialog-icon + * > :is(h1, h2, h3, h4, h5, h6):first-child {
309
305
  display: flex;
310
306
  align-items: center;
311
307
  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
- }
308
+ margin-block: 0;
319
309
  }
320
310
 
321
311
  :where(dialog).modal[open] {
322
312
  opacity: 1;
323
313
  transform: none;
324
- transition:
325
- opacity var(--duration) var(--ease-enter),
326
- transform var(--duration) var(--ease-enter);
314
+ transition-timing-function: var(--ease-enter);
327
315
  }
328
316
 
329
317
  @starting-style {
@@ -338,31 +326,22 @@
338
326
  opacity: 0;
339
327
  /* Scrim curve stays neutral `ease`; only the panel carries the character
340
328
  curves. */
341
- transition: opacity var(--duration) ease;
329
+ transition-property: opacity;
330
+ transition-duration: var(--duration);
331
+ transition-timing-function: ease;
342
332
  }
343
333
 
344
334
  @supports (transition-behavior: allow-discrete) {
335
+ /* Add the discrete top-layer pair to the property list. Duration and timing
336
+ stay on the base rules, so [open] still only swaps the easing. */
345
337
  :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;
338
+ transition-property: opacity, transform, overlay, display;
339
+ transition-behavior: allow-discrete;
359
340
  }
360
341
 
361
342
  :where(dialog).modal::backdrop {
362
- transition:
363
- opacity var(--duration) ease,
364
- overlay var(--duration) allow-discrete,
365
- display var(--duration) allow-discrete;
343
+ transition-property: opacity, overlay, display;
344
+ transition-behavior: allow-discrete;
366
345
  }
367
346
  }
368
347
 
@@ -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;
@@ -82,23 +71,19 @@
82
71
 
83
72
  /* nav-link serves two contracts: plain page navigation (aria-current="page")
84
73
  and scrollspy, which toggles aria-current="location" on the active link.
85
- Presence is the contract: the attribute is set on the current link and
86
- removed everywhere else, never serialized as aria-current="false".
74
+ Those two values are the contract; any other, "false" included, is inert.
87
75
  Selection never changes text metrics: the shared medium weight is kept and
88
76
  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
77
  .nav-link:hover {
95
78
  background: var(--surface-subtle);
96
79
  color: var(--text);
97
80
  }
98
81
 
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 {
82
+ /* Current last, equal specificity to :hover (0,2,0): a hovered current link
83
+ keeps the selected accent instead of dropping to --text. The surface fill is
84
+ shared with the hover rule above. */
85
+ .nav-link:is([aria-current="page"], [aria-current="location"]) {
86
+ background: var(--surface-subtle);
102
87
  color: var(--state-selected);
103
88
  }
104
89
 
@@ -110,7 +95,7 @@
110
95
  above keeps the light chip where color-mix is missing, with ink that is
111
96
  legible on it. */
112
97
  @supports (color: color-mix(in oklch, red, white)) {
113
- .nav-link:is(:hover, [aria-current]) {
98
+ .nav-link:is(:hover, [aria-current="page"], [aria-current="location"]) {
114
99
  background: color-mix(in oklch, currentColor 10%, transparent);
115
100
  }
116
101
 
@@ -118,19 +103,22 @@
118
103
  color: var(--ui-fg, var(--text));
119
104
  }
120
105
 
121
- .nav-link[aria-current],
122
- .nav-link[aria-current]:hover {
106
+ /* After :hover so a hovered current link keeps the selected accent. */
107
+ .nav-link:is([aria-current="page"], [aria-current="location"]) {
123
108
  color: var(--ui-fg, var(--state-selected));
124
109
  }
125
110
  }
126
111
 
127
112
  /* Nav list — a vertical navigation list. Used by .drawer for stacked links.
128
113
  Links fill each row so the whole line is an interactive target; the
129
- horizontal .navbar-nav links keep their intrinsic size. */
114
+ horizontal .cluster links keep their intrinsic size. */
115
+ /* A vertical nav reads as one list, not a stack of separate buttons: each row
116
+ is already a control-height target, so the rows sit a hair apart. The grid
117
+ is the component's own layout; for a horizontal row use .cluster instead. */
130
118
  .nav-list {
131
119
  display: grid;
132
120
  grid-template-columns: minmax(0, 1fr);
133
- --gap: var(--space-20);
121
+ --gap: var(--space-10);
134
122
  gap: var(--gap);
135
123
  margin: 0;
136
124
  padding: 0;
@@ -150,11 +138,14 @@
150
138
  follows the forced palette without a forced-colors block and never touches
151
139
  the row separators. Navbar horizontal links keep color + surface only. The
152
140
  positioning context lives on every row link, so becoming current never moves
153
- an app-positioned decoration to a new containing block. */
154
- .nav-list .nav-link[aria-current]::before {
141
+ an app-positioned decoration to a new containing block.
142
+ The trait sits centered in the link's inline padding with rounded ends: flush
143
+ on the edge, a square line read as pasted onto the rounded fill. */
144
+ .nav-list .nav-link:is([aria-current="page"], [aria-current="location"])::before {
155
145
  content: "";
156
146
  position: absolute;
157
147
  inset-block: var(--space-20);
158
- inset-inline-start: 0;
148
+ inset-inline-start: calc((var(--space-20) - 2px) / 2);
159
149
  border-inline-start: 2px solid currentColor;
150
+ border-radius: var(--radius-full);
160
151
  }
@@ -10,6 +10,11 @@
10
10
  * Truncated ranges are plain text (`.muted`, `aria-hidden="true"`), not .btn
11
11
  * — they are decorative, not actionable. They still get the same box sizing
12
12
  * as .btn below so the row stays visually aligned.
13
+ *
14
+ * The row wraps when it does not fit: every page stays visible and
15
+ * clickable. A strip that scrolled hid pages behind a scrollbar that clicking
16
+ * cannot move, since a click changes the page. Keeping the rendered range
17
+ * compact is the application's job.
13
18
  */
14
19
  .pagination {
15
20
  display: flex;
@@ -8,7 +8,7 @@
8
8
  * <nav class="scrollspy">
9
9
  * containing <a href="#section"> links and toggles aria-current="location" on
10
10
  * the link whose target is in view. Pair with .nav-list and .nav-link, or style
11
- * [aria-current] yourself.
11
+ * [aria-current="location"] yourself.
12
12
  *
13
13
  * Progressive enhancement: on browsers that support :target-current,
14
14
  * this file enables the native target group and marks the active link.