actual-css 0.12.0 → 0.14.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 (108) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +7 -8
  3. package/components.json +124 -12
  4. package/dist/actual.full.css +1152 -309
  5. package/dist/actual.full.js +1 -1
  6. package/dist/actual.full.min.css +1 -1
  7. package/docs/design-notes/architecture.md +67 -0
  8. package/docs/design-notes/steps.md +45 -28
  9. package/docs/navigation.json +2 -0
  10. package/docs/pages/components/alert.md +12 -2
  11. package/docs/pages/components/avatar.md +17 -11
  12. package/docs/pages/components/breadcrumb.md +16 -2
  13. package/docs/pages/components/button.md +3 -2
  14. package/docs/pages/components/list.md +15 -16
  15. package/docs/pages/components/navbar.md +42 -22
  16. package/docs/pages/components/pagination.md +19 -0
  17. package/docs/pages/components/segmented.md +83 -0
  18. package/docs/pages/components/steps.md +28 -22
  19. package/docs/pages/enhancements/flyout.md +5 -0
  20. package/docs/pages/enhancements/scrollspy.md +6 -6
  21. package/docs/pages/enhancements/tabs.md +79 -3
  22. package/docs/pages/examples/overview.md +3 -1
  23. package/docs/pages/forms/color-swatches.md +131 -0
  24. package/docs/pages/forms/combobox.md +2 -2
  25. package/docs/pages/forms/controls.md +6 -3
  26. package/docs/pages/forms/floating-field.md +21 -7
  27. package/docs/pages/forms/form-actions.md +3 -6
  28. package/docs/pages/forms/joined-controls.md +6 -2
  29. package/docs/pages/forms/overview.md +1 -0
  30. package/docs/pages/forms/states-and-sizes.md +15 -15
  31. package/docs/pages/forms/switch.md +4 -2
  32. package/docs/pages/foundations/philosophy.md +97 -1
  33. package/docs/pages/foundations/theming.md +1 -1
  34. package/docs/pages/foundations/tokens.md +94 -27
  35. package/docs/pages/guides/design-tools.md +14 -8
  36. package/docs/pages/guides/integrating-actual-css.md +74 -8
  37. package/docs/pages/guides/introduction.md +3 -2
  38. package/docs/pages/guides/modular-import.md +1 -1
  39. package/docs/pages/guides/progressive-enhancement.md +25 -7
  40. package/docs/pages/guides/upgrading.md +86 -9
  41. package/docs/pages/layout/center.md +2 -1
  42. package/docs/pages/layout/cluster.md +11 -4
  43. package/docs/pages/layout/grid.md +2 -0
  44. package/docs/pages/layout/scroll-snap.md +4 -2
  45. package/docs/pages/layout/sidebar-layout.md +3 -0
  46. package/docs/pages/layout/switcher.md +2 -0
  47. package/docs/pages/layout/topbar.md +6 -4
  48. package/docs/pages/patterns/icon-slot.md +17 -1
  49. package/docs/pages/utilities/flex.md +1 -1
  50. package/docs/pages/utilities/sizing-wrapping.md +51 -6
  51. package/package.json +6 -6
  52. package/reserved-classes.json +3 -0
  53. package/src/cli/actual-css.js +4 -4
  54. package/src/css/components/alert.css +99 -48
  55. package/src/css/components/app-nav.css +17 -9
  56. package/src/css/components/avatar.css +2 -18
  57. package/src/css/components/badge.css +23 -10
  58. package/src/css/components/breadcrumb.css +74 -8
  59. package/src/css/components/button.css +17 -2
  60. package/src/css/components/close.css +6 -3
  61. package/src/css/components/drawer.css +23 -42
  62. package/src/css/components/flyout.css +0 -1
  63. package/src/css/components/index.css +1 -0
  64. package/src/css/components/join.css +2 -2
  65. package/src/css/components/list.css +28 -13
  66. package/src/css/components/modal.css +17 -10
  67. package/src/css/components/navbar.css +15 -10
  68. package/src/css/components/pagination.css +5 -0
  69. package/src/css/components/scrollspy.css +1 -1
  70. package/src/css/components/segmented.css +186 -0
  71. package/src/css/components/steps.css +72 -15
  72. package/src/css/components/tab.css +135 -5
  73. package/src/css/core/focus.css +11 -6
  74. package/src/css/core/theme.css +6 -1
  75. package/src/css/core/tokens.css +24 -6
  76. package/src/css/core/variants.css +16 -5
  77. package/src/css/forms/choice-card.css +1 -1
  78. package/src/css/forms/choice.css +1 -1
  79. package/src/css/forms/color-swatch.css +159 -0
  80. package/src/css/forms/control.css +1 -1
  81. package/src/css/forms/floating-field.css +20 -18
  82. package/src/css/forms/form-actions.css +14 -8
  83. package/src/css/forms/index.css +1 -0
  84. package/src/css/forms/native.css +51 -21
  85. package/src/css/forms/otp.css +4 -5
  86. package/src/css/forms/switch.css +6 -3
  87. package/src/css/forms/validation.css +23 -30
  88. package/src/css/layout/center.css +2 -0
  89. package/src/css/layout/cluster.css +14 -0
  90. package/src/css/layout/grid.css +7 -0
  91. package/src/css/layout/scroll-snap.css +30 -0
  92. package/src/css/layout/scroller.css +19 -3
  93. package/src/css/layout/sidebar-layout.css +3 -0
  94. package/src/css/layout/switcher.css +3 -0
  95. package/src/css/layout/topbar.css +10 -0
  96. package/src/css/utilities/extra.css +3 -3
  97. package/src/js/focus.js +16 -1
  98. package/src/js/full.js +4 -0
  99. package/src/js/inline-visible.js +54 -0
  100. package/src/js/parsed.js +21 -0
  101. package/src/js/surface.js +10 -0
  102. package/src/js/tab.js +33 -7
  103. package/src/tooling/color.js +14 -1
  104. package/src/tooling/css-bundle.js +0 -5
  105. package/src/tooling/design-components.js +9 -5
  106. package/src/tooling/design-export.js +9 -1
  107. package/src/tooling/design-tokens.js +207 -76
  108. package/src/tooling/theme-contrast.js +182 -45
@@ -61,6 +61,16 @@
61
61
  font-size: var(--alert-font-size);
62
62
  }
63
63
 
64
+ /* Density steps the block padding only: the inline padding is the text
65
+ column's edge and stays stable. Local to the alert, like .card.compact. */
66
+ .alert.compact {
67
+ --alert-pad-block: var(--space-20);
68
+ }
69
+
70
+ .alert.spacious {
71
+ --alert-pad-block: var(--space-40);
72
+ }
73
+
64
74
  .alert > * {
65
75
  min-inline-size: 0;
66
76
  }
@@ -85,32 +95,45 @@
85
95
  }
86
96
 
87
97
  /* Alerts are soft by default. The recipe writes local defaults rather than
88
- --ui-* so an explicit shared treatment always wins without exclusions. */
98
+ --ui-* so an explicit shared treatment always wins without exclusions. The
99
+ fill is the translucent soft tint (see badge.css), with no rim unless a
100
+ theme sets --soft-border-mix. */
89
101
  @supports (color: color-mix(in oklch, red, white)) {
90
102
  .alert {
91
- /* @sync soft-recipe */
103
+ /* @sync soft-tint */
92
104
  --alert-default-bg: color-mix(
93
105
  in oklab,
94
- var(--surface) var(--soft-bg-mix),
95
- var(--intent, var(--neutral))
106
+ var(--intent, var(--neutral)) calc(100% - var(--soft-bg-mix)),
107
+ transparent
96
108
  );
97
109
  --alert-default-border: color-mix(
98
110
  in oklab,
99
- var(--surface) var(--soft-border-mix),
100
- var(--intent, var(--neutral))
111
+ var(--intent, var(--neutral)) calc(100% - var(--soft-border-mix, 100%)),
112
+ transparent
101
113
  );
114
+ /* @sync soft-tint */
115
+ /* @sync soft-ink */
102
116
  --alert-default-fg: var(
103
117
  --intent-soft-fg,
104
118
  color-mix(in oklab, var(--text), var(--intent, var(--text)) var(--soft-fg-mix))
105
119
  );
106
- /* @sync soft-recipe */
120
+ /* @sync soft-ink */
107
121
  }
108
122
  }
109
123
 
110
- /* Decorative leading icon — use .alert-icon on the icon element. */
124
+ /* Soft is the alert's own default: an explicit .soft must not swap the tint
125
+ for the shared opaque .soft recipe, so it defers to the default. */
126
+ .alert.soft {
127
+ --ui-bg: initial;
128
+ --ui-fg: initial;
129
+ --ui-border: initial;
130
+ }
131
+
132
+ /* Decorative leading icon — use .alert-icon on the icon element. display and
133
+ vertical-align are the no-:has() layout (Degraded tier: the icon sits inline
134
+ before the text); the grid anatomy below replaces them. */
111
135
  .alert > .alert-icon {
112
136
  display: inline-block;
113
- flex: none;
114
137
  inline-size: var(--alert-icon-size);
115
138
  block-size: var(--alert-icon-size);
116
139
  font-size: var(--alert-icon-size);
@@ -142,45 +165,44 @@
142
165
 
143
166
  /* Grid anatomy — activated by a leading icon, a trailing dismiss button, or
144
167
  both. Icon and dismiss are pinned to the outer columns; anything else (the
145
- message content) flows into the remaining text column. */
146
- @supports selector(.alert:has(> *)) {
147
- .alert:has(> .alert-icon, > .close) {
148
- display: grid;
149
- grid-template-columns: minmax(0, 1fr);
150
- align-items: var(--items-align, start);
151
- column-gap: var(--space-30);
152
- }
153
-
154
- .alert:has(> .alert-icon) {
155
- grid-template-columns: auto minmax(0, 1fr);
156
- }
168
+ message content) flows into the remaining text column. Without :has(), the
169
+ grid rules drop together and the alert keeps ordinary block flow. */
170
+ .alert:has(> .alert-icon, > .close) {
171
+ display: grid;
172
+ /* No grid-template-columns: the icon and close branches below always set
173
+ it, since one of them is what matched. */
174
+ align-items: var(--items-align, start);
175
+ column-gap: var(--space-30);
176
+ }
157
177
 
158
- .alert:has(> .alert-icon) > .alert-icon {
159
- grid-column: 1;
160
- grid-row: 1 / -1;
161
- display: inline-grid;
162
- place-items: center;
163
- margin-block-start: 0.125em;
164
- }
178
+ .alert:has(> .alert-icon) {
179
+ grid-template-columns: auto minmax(0, 1fr);
180
+ }
165
181
 
166
- .alert:has(> .alert-icon) > :not(.alert-icon, .close) {
167
- grid-column: 2;
168
- }
182
+ .alert:has(> .alert-icon) > .alert-icon {
183
+ grid-column: 1;
184
+ grid-row: 1 / -1;
185
+ display: inline-grid;
186
+ place-items: center;
187
+ margin-block-start: 0.125em;
188
+ }
169
189
 
170
- .alert:has(> .close) {
171
- grid-template-columns: minmax(0, 1fr) auto;
172
- }
190
+ .alert:has(> .alert-icon) > :not(.alert-icon, .close) {
191
+ grid-column: 2;
192
+ }
173
193
 
174
- .alert:has(> .alert-icon):has(> .close) {
175
- grid-template-columns: auto minmax(0, 1fr) auto;
176
- }
194
+ .alert:has(> .close) {
195
+ grid-template-columns: minmax(0, 1fr) auto;
196
+ }
177
197
 
178
- .alert:has(> .close) > .close {
179
- grid-column: -2 / -1;
180
- grid-row: 1;
181
- }
198
+ .alert:has(> .alert-icon):has(> .close) {
199
+ grid-template-columns: auto minmax(0, 1fr) auto;
182
200
  }
183
201
 
202
+ .alert:has(> .close) > .close {
203
+ grid-column: -2 / -1;
204
+ grid-row: 1;
205
+ }
184
206
  /* Links */
185
207
  .alert :where(a:not(.btn)) {
186
208
  color: inherit;
@@ -205,7 +227,10 @@
205
227
 
206
228
  /* Admonition — structured static box with a tinted title bar and a body
207
229
  that rests on the page surface. Not collapsible — use .accordion inside
208
- the body if the content needs to be expandable. */
230
+ the body if the content needs to be expandable. The title bar is what a
231
+ shared treatment paints (.solid fills it, .surface/.outline clear it): the
232
+ root's fill never shows behind the opaque title and body, and the title ink
233
+ already follows --ui-fg, so a bar left on the soft tint is unreadable. */
209
234
  .alert.admonition {
210
235
  padding: 0;
211
236
  display: block;
@@ -219,6 +244,7 @@
219
244
  gap: var(--space-20);
220
245
  padding-block: var(--alert-pad-block);
221
246
  padding-inline: var(--alert-pad-inline);
247
+ font-weight: var(--font-weight-semibold);
222
248
  border-start-start-radius: calc(var(--alert-radius, var(--radius-lg)) - var(--border-width));
223
249
  border-start-end-radius: calc(var(--alert-radius, var(--radius-lg)) - var(--border-width));
224
250
  }
@@ -230,18 +256,17 @@
230
256
 
231
257
  @supports (color: color-mix(in oklch, red, white)) {
232
258
  .alert.admonition > .alert-title {
233
- background: color-mix(
234
- in oklab,
235
- var(--surface) var(--soft-bg-mix),
236
- var(--intent, var(--neutral))
259
+ background: var(
260
+ --ui-bg,
261
+ color-mix(in oklab, var(--surface) var(--soft-bg-mix), var(--intent, var(--neutral)))
237
262
  );
238
263
  }
239
264
  }
240
265
 
241
266
  @supports not (color: color-mix(in oklch, red, white)) {
242
267
  .alert.admonition > .alert-title {
243
- background: var(--intent, var(--neutral));
244
- color: var(--intent-fg, var(--neutral-fg));
268
+ background: var(--ui-bg, var(--intent, var(--neutral)));
269
+ color: var(--ui-fg, var(--intent-fg, var(--neutral-fg)));
245
270
  }
246
271
  }
247
272
 
@@ -252,6 +277,32 @@
252
277
  color: var(--text);
253
278
  }
254
279
 
280
+ /* Solid: the filled title bar already carries the intent, so the frame moves
281
+ to the body and stays neutral. The root border goes because a transparent
282
+ one would show the root's intent fill around the body. */
283
+ .alert.admonition.solid {
284
+ border: 0;
285
+ }
286
+
287
+ .alert.admonition.solid > .alert-body {
288
+ border: var(--border-width) solid var(--border);
289
+ border-block-start: 0;
290
+ border-end-start-radius: var(--alert-radius, var(--radius-lg));
291
+ border-end-end-radius: var(--alert-radius, var(--radius-lg));
292
+ }
293
+
294
+ /* Forced colors drop the title fill, so the bar would lose its edge: the
295
+ frame returns to the root as one uniform system-colored border. */
296
+ @media (forced-colors: active) {
297
+ .alert.admonition.solid {
298
+ border: var(--border-width) solid CanvasText;
299
+ }
300
+
301
+ .alert.admonition.solid > .alert-body {
302
+ border: 0;
303
+ }
304
+ }
305
+
255
306
  .alert.admonition > .alert-body > :first-child {
256
307
  margin-block-start: 0;
257
308
  }
@@ -40,6 +40,7 @@
40
40
  }
41
41
 
42
42
  .app-nav > a {
43
+ position: relative;
43
44
  display: grid;
44
45
  grid-template-rows: auto auto;
45
46
  place-content: center;
@@ -80,18 +81,25 @@
80
81
  }
81
82
 
82
83
  /* Current tile is already a full selected surface. Selection never changes
83
- text metrics: the shared medium weight is kept, no permanent indicator is
84
- added here. */
84
+ text metrics: the shared medium weight is kept. The forced-colors cue is a
85
+ transparent border on a ::before overlay, not on the tile: an element border
86
+ shrinks the content box, and the side layout starts its content at the inline
87
+ edge, so the icon and label shifted by --border-width when a tile became
88
+ current. The overlay costs no layout, yet forced colors still turn its
89
+ transparent border into a system-color frame once --state-selected flattens
90
+ to the bar's own Canvas. No forced-colors repaint: a Highlight fill put
91
+ HighlightText on Chromium's Canvas backplate, which hid the label. The
92
+ positioning context is on every link so it does not move when one becomes
93
+ current or an application places its own decoration. */
85
94
  .app-nav > a[aria-current="page"] {
86
95
  background: var(--state-selected);
87
96
  color: var(--state-selected-fg);
88
97
  }
89
98
 
90
- @media (forced-colors: active) {
91
- /* --state-selected flattens to a UA color that no longer distinguishes the
92
- current tile from the rest of the bar, so paint the system Highlight pair. */
93
- .app-nav > a[aria-current="page"] {
94
- background: Highlight;
95
- color: HighlightText;
96
- }
99
+ .app-nav > a[aria-current="page"]::before {
100
+ content: "";
101
+ position: absolute;
102
+ inset: 0;
103
+ border: var(--border-width) solid transparent;
104
+ border-radius: inherit;
97
105
  }
@@ -11,13 +11,13 @@
11
11
  * --avatar-size
12
12
  * --avatar-radius
13
13
  * --avatar-font-size
14
+ * --avatar-bg
14
15
  * --avatar-stack-size
15
16
  * --avatar-stack-font-size
16
17
  * --avatar-stack-overlap
17
18
  * --avatar-stack-ring
18
19
  *
19
20
  * Internal:
20
- * --avatar-bg
21
21
  * --avatar-fg
22
22
  * --avatar-status-translate-x
23
23
  */
@@ -95,16 +95,13 @@
95
95
 
96
96
  .avatar > img,
97
97
  .avatar > picture > img {
98
+ display: block;
98
99
  inline-size: 100%;
99
100
  block-size: 100%;
100
101
  object-fit: cover;
101
102
  border-radius: inherit;
102
103
  }
103
104
 
104
- .avatar > picture > img {
105
- display: block;
106
- }
107
-
108
105
  button.avatar {
109
106
  appearance: none;
110
107
  cursor: pointer;
@@ -198,19 +195,6 @@ a.avatar[aria-disabled="true"] {
198
195
  --avatar-stack-font-size: var(--font-size-lg);
199
196
  }
200
197
 
201
- /* Infer the stack scale from its avatars. Progressive: without :has() the
202
- explicit .avatar-stack.sm/.lg classes and the default size still size the
203
- group, only the inference is lost. */
204
- .avatar-stack:has(> .avatar.sm) {
205
- --avatar-stack-size: 1.75rem;
206
- --avatar-stack-font-size: var(--font-size-xs);
207
- }
208
-
209
- .avatar-stack:has(> .avatar.lg) {
210
- --avatar-stack-size: 3rem;
211
- --avatar-stack-font-size: var(--font-size-lg);
212
- }
213
-
214
198
  /* Overlapping avatars hide their neighbour: a translucent fill (--surface-subtle
215
199
  on a glass palette) would show the next avatar's edge through it, most of all
216
200
  when hover raises one above the other. The fill is layered over the opaque
@@ -111,33 +111,46 @@
111
111
  }
112
112
 
113
113
  /* Soft by default. Local defaults remain behind --ui-* in the resolution
114
- chain, so explicit shared treatments compose without selector exclusions. */
114
+ chain, so explicit shared treatments compose without selector exclusions.
115
+ The fill is a translucent intent tint, so it shows the surface it sits on
116
+ instead of a fixed mix with --surface: a mix stops separating on a subtle
117
+ or raised surface of its own lightness. No rim unless a theme sets
118
+ --soft-border-mix (tokens.css). */
115
119
  @supports (color: color-mix(in oklch, red, white)) {
116
120
  .badge {
117
- /* @sync soft-recipe */
121
+ /* @sync soft-tint */
118
122
  --badge-default-bg: color-mix(
119
123
  in oklab,
120
- var(--surface) var(--soft-bg-mix),
121
- var(--intent, var(--neutral))
124
+ var(--intent, var(--neutral)) calc(100% - var(--soft-bg-mix)),
125
+ transparent
122
126
  );
123
127
  --badge-default-border: color-mix(
124
128
  in oklab,
125
- var(--surface) var(--soft-border-mix),
126
- var(--intent, var(--neutral))
129
+ var(--intent, var(--neutral)) calc(100% - var(--soft-border-mix, 100%)),
130
+ transparent
127
131
  );
132
+ /* @sync soft-tint */
133
+ /* @sync soft-ink */
128
134
  --badge-default-fg: var(
129
135
  --intent-soft-fg,
130
136
  color-mix(in oklab, var(--text), var(--intent, var(--text)) var(--soft-fg-mix))
131
137
  );
132
- /* @sync soft-recipe */
138
+ /* @sync soft-ink */
133
139
  }
134
140
  }
135
141
 
142
+ /* Soft is the badge's own default: an explicit .soft must not swap the tint
143
+ for the shared opaque .soft recipe, so it defers to the default. */
144
+ .badge.soft {
145
+ --ui-bg: initial;
146
+ --ui-fg: initial;
147
+ --ui-border: initial;
148
+ }
149
+
136
150
  .badge:empty {
137
151
  --badge-size: var(--badge-dot-size);
138
152
  --badge-bg: var(--intent, var(--neutral));
139
153
  --badge-fg: var(--intent-fg, var(--neutral-fg));
140
- --badge-border: transparent;
141
154
  padding: 0;
142
155
  border: 0;
143
156
  inline-size: var(--badge-size);
@@ -172,8 +185,8 @@
172
185
  :is(a.badge, button.badge):where(:not(.solid, .outline, :empty)) {
173
186
  --badge-default-hover-bg: color-mix(
174
187
  in oklab,
175
- var(--surface) calc(var(--soft-bg-mix) - var(--soft-hover-alpha)),
176
- var(--intent, var(--neutral))
188
+ var(--intent, var(--neutral)) calc(100% - var(--soft-bg-mix) + var(--soft-hover-alpha)),
189
+ transparent
177
190
  );
178
191
  }
179
192
 
@@ -4,14 +4,21 @@
4
4
  * Docs: docs/pages/components/breadcrumb.md
5
5
  *
6
6
  * Inline list separated by "/". Mark the current page with `aria-current="page"`
7
- * either on the item or on the current link.
7
+ * either on the item or on the current link. Ancestor labels sit in an <a> or a
8
+ * <span>, the box that truncates.
9
+ *
10
+ * Breadcrumbs are a single navigation hierarchy. Wrapping makes hierarchy look
11
+ * like separate rows and causes unstable header height, and scrolling hides the
12
+ * root behind a scrollbar. A trail that does not fit truncates instead:
13
+ * ancestors first, the current page last, each label behind an ellipsis.
8
14
  */
15
+
9
16
  .breadcrumb {
10
17
  display: flex;
11
- flex-direction: row;
12
- flex-wrap: wrap;
18
+ flex-wrap: nowrap;
13
19
  align-items: center;
14
20
  gap: var(--space-10);
21
+ min-inline-size: 0;
15
22
  margin: 0;
16
23
  padding: 0;
17
24
  color: var(--text-subtle);
@@ -19,15 +26,74 @@
19
26
  list-style: none;
20
27
  }
21
28
 
22
- .breadcrumb li {
23
- display: inline-flex;
29
+ /* The landmark around the trail must shrink with it in a flex or grid row, or
30
+ it keeps the trail's full width and the labels never get to truncate.
31
+ Without :has() the landmark keeps its width; min-inline-size: 0 on the
32
+ wrapper is the fallback. */
33
+ :where(nav:has(> .breadcrumb)) {
34
+ min-inline-size: 0;
35
+ }
36
+
37
+ /* Grid, not flex: under a min-content constraint a grid track keeps its base
38
+ size, which is the label's minimum below, so the item's own minimum is
39
+ separator + label floor without anyone measuring a "/". A flex item would
40
+ report the full label as its minimum and never shrink.
41
+
42
+ Ancestors take the shrink before the current page, so the location is the
43
+ last thing to lose characters. The ratio is deliberately huge: at 1000 the
44
+ current page still took a fraction of a pixel, enough to trigger its
45
+ ellipsis while ancestors had room. Its own factor stays 1, because flex
46
+ distributes only a share of the deficit when the unfrozen factors sum below
47
+ one. Keyed on the last item, the current page by convention, so the order
48
+ holds without :has(). */
49
+ .breadcrumb > li {
50
+ display: inline-grid;
51
+ flex: 0 1000000 auto;
52
+ grid-auto-flow: column;
24
53
  align-items: center;
25
- gap: var(--space-10);
54
+ white-space: nowrap;
26
55
  }
27
56
 
28
- .breadcrumb li + li::before {
29
- content: "/";
57
+ .breadcrumb > li:last-child {
58
+ flex-shrink: 1;
59
+ }
60
+
61
+ /* A margin rather than a grid gap, so the separator keeps its space when the
62
+ current page below lays it out inline. The weight stays normal there, where
63
+ it would inherit the current page's. */
64
+ .breadcrumb > li + li::before {
65
+ margin-inline-end: var(--space-10);
30
66
  color: var(--text-subtle);
67
+ font-weight: var(--font-weight-normal);
68
+ content: "/";
69
+ }
70
+
71
+ /* The ellipsis lives on the label, not on the item: an element never clips
72
+ its own outline, but an overflow-hidden item would clip its link's focus
73
+ line, which sits outside the link box.
74
+
75
+ The floor keeps a truncated label readable as a step ("Po…") instead of
76
+ collapsing to its separator. calc-size() lets a shorter label keep its own
77
+ width; without it, the plain 2em floor leaves a label under 2em a little
78
+ trailing room. */
79
+ .breadcrumb > li > :is(a, span) {
80
+ min-inline-size: 2em;
81
+ min-inline-size: calc-size(max-content, min(size, 2em));
82
+ overflow: hidden;
83
+ text-overflow: ellipsis;
84
+ }
85
+
86
+ /* The current page written as bare text has no label box, so its item
87
+ truncates itself. Only with no element inside: an aria-current item that
88
+ holds a link would clip that link's focus line, and keeps the label rule
89
+ above instead. Its own rule, never merged with the one above: a browser
90
+ without :has() drops it alone, and bare text then keeps its full width. */
91
+ .breadcrumb > li[aria-current="page"]:not(:has(> *)) {
92
+ display: block;
93
+ min-inline-size: 2em;
94
+ min-inline-size: calc-size(max-content, min(size, 2em));
95
+ overflow: hidden;
96
+ text-overflow: ellipsis;
31
97
  }
32
98
 
33
99
  .breadcrumb a {
@@ -133,12 +133,27 @@
133
133
  --ui-hover-bg: transparent;
134
134
  }
135
135
 
136
+ /* A soft button takes the badge and alert tint, not the shared opaque .soft:
137
+ it carries a short label like them, and the translucent fill keeps
138
+ separating on a subtle or raised surface. Ink stays the shared .soft one. */
136
139
  @supports (color: color-mix(in oklch, red, white)) {
137
140
  .btn.soft {
141
+ /* @sync soft-tint */
142
+ --ui-bg: color-mix(
143
+ in oklab,
144
+ var(--intent, var(--neutral)) calc(100% - var(--soft-bg-mix)),
145
+ transparent
146
+ );
147
+ --ui-border: color-mix(
148
+ in oklab,
149
+ var(--intent, var(--neutral)) calc(100% - var(--soft-border-mix, 100%)),
150
+ transparent
151
+ );
152
+ /* @sync soft-tint */
138
153
  --ui-hover-bg: color-mix(
139
154
  in oklab,
140
- var(--surface) calc(var(--soft-bg-mix) - var(--soft-hover-alpha)),
141
- var(--intent, var(--neutral))
155
+ var(--intent, var(--neutral)) calc(100% - var(--soft-bg-mix) + var(--soft-hover-alpha)),
156
+ transparent
142
157
  );
143
158
  }
144
159
 
@@ -93,10 +93,13 @@
93
93
  }
94
94
 
95
95
  @media (forced-colors: active) {
96
- /* The X is a currentColor mask fill: without this the system remaps the
97
- background to Canvas and the glyph disappears. Kept on the pseudo so the
98
- host keeps its native color and focus adaptation. */
96
+ /* The X is a mask fill: without this the system remaps the background to
97
+ Canvas and the glyph disappears. Kept on the pseudo so the host keeps its
98
+ native color and focus adaptation. The fill is the system color, not
99
+ currentColor: an unadjusted pseudo inherits the authored ink, so a white
100
+ .solid ink painted a white X on Canvas. */
99
101
  .close::before {
100
102
  forced-color-adjust: none;
103
+ background: ButtonText;
101
104
  }
102
105
  }
@@ -25,6 +25,7 @@
25
25
  * --drawer-close-reserve (header room for a corner close; zero without a close)
26
26
  * --drawer-motion-duration (shared by panel and ::backdrop so exit duration
27
27
  * changes in one place; entry state redefines it longer)
28
+ * --drawer-motion-ease (panel easing owned by the open/closed state)
28
29
  */
29
30
  :where(dialog).drawer {
30
31
  --drawer-size: 20rem;
@@ -39,6 +40,7 @@
39
40
  /* Exit is shorter than entry so the panel clears quickly; the scrim reads
40
41
  the same variable through inheritance and never lags the panel. */
41
42
  --drawer-motion-duration: var(--duration);
43
+ --drawer-motion-ease: var(--ease-exit);
42
44
 
43
45
  /* Keep the surface anchored to the viewport in the top layer. This also
44
46
  provides the containing block for the absolutely positioned close button.
@@ -70,21 +72,21 @@
70
72
  box-shadow: var(--shadow);
71
73
  opacity: 0;
72
74
  transform: translateX(var(--drawer-closed-translate));
73
- transition:
74
- opacity var(--drawer-motion-duration) var(--ease-exit),
75
- transform var(--drawer-motion-duration) var(--ease-exit);
75
+ /* Unsupported discrete behavior drops alone, preserving opacity/transform. */
76
+ transition-property: opacity, transform, overlay, display;
77
+ transition-duration: var(--drawer-motion-duration);
78
+ transition-timing-function: var(--drawer-motion-ease);
79
+ transition-behavior: allow-discrete;
76
80
  }
77
81
 
78
82
  :where(dialog).drawer[open] {
79
83
  --drawer-motion-duration: var(--duration-slow);
84
+ --drawer-motion-ease: var(--ease-enter);
80
85
  display: grid;
81
86
  grid-template-rows: auto minmax(0, 1fr) auto;
82
87
  gap: var(--space-40);
83
88
  opacity: 1;
84
89
  transform: translateX(0);
85
- transition:
86
- opacity var(--drawer-motion-duration) var(--ease-enter),
87
- transform var(--drawer-motion-duration) var(--ease-enter);
88
90
  }
89
91
 
90
92
  /* A form is the drawer's content wrapper when actions need method="dialog".
@@ -104,7 +106,10 @@
104
106
  opacity: 0;
105
107
  /* Scrim curve stays neutral `ease`; only the panel's character curves
106
108
  apply. Duration follows --drawer-motion-duration through inheritance. */
107
- transition: opacity var(--drawer-motion-duration) ease;
109
+ transition-property: opacity, overlay, display;
110
+ transition-duration: var(--drawer-motion-duration);
111
+ transition-timing-function: ease;
112
+ transition-behavior: allow-discrete;
108
113
  }
109
114
 
110
115
  :where(dialog).drawer[data-side="end"] {
@@ -138,47 +143,23 @@
138
143
  exclusion list is easier to trust than its `position: absolute`. */
139
144
  :where(dialog).drawer > :not(:is(header, footer, form, .close)),
140
145
  :where(dialog).drawer > form > :not(:is(header, footer, .close)) {
141
- --drawer-focus-bleed: calc(
142
- var(--focus-outline-offset) +
143
- max(var(--focus-ring-width), var(--border-width) * 2)
144
- );
146
+ /* Declared here, not as a :root alias: an inherited alias arrives already
147
+ computed, so a --focus-ring-width override on the drawer would not reach
148
+ it. */
149
+ --drawer-focus-bleed: calc(var(--focus-outline-offset) + var(--focus-ring-width));
145
150
 
146
151
  min-block-size: 0;
147
- /* Keep focus paint inside the clip without moving the body off the fixed
148
- header and footer edges. The reserve covers the thicker of the two
149
- outside indicators: the :focus baseline (2 × border) and the component
150
- line (--focus-ring-width, 3px in bootstrap-v6). */
151
- margin-inline: calc(var(--drawer-focus-bleed) * -1);
152
- padding-inline: var(--drawer-focus-bleed);
152
+ /* Keep focus paint inside the clip, on all four sides (overflow: auto clips
153
+ both axes), without moving the content. Every outside indicator is
154
+ --focus-ring-width wide (core/focus.css). On the block axis the reserve
155
+ reaches into the slot gap, which is wider, so the scrollport never slides
156
+ under the header or footer. */
157
+ margin: calc(var(--drawer-focus-bleed) * -1);
158
+ padding: var(--drawer-focus-bleed);
153
159
  overflow: auto;
154
160
  overscroll-behavior: contain;
155
161
  }
156
162
 
157
- @supports (transition-behavior: allow-discrete) {
158
- :where(dialog).drawer {
159
- transition:
160
- opacity var(--drawer-motion-duration) var(--ease-exit),
161
- transform var(--drawer-motion-duration) var(--ease-exit),
162
- overlay var(--drawer-motion-duration) allow-discrete,
163
- display var(--drawer-motion-duration) allow-discrete;
164
- }
165
-
166
- :where(dialog).drawer[open] {
167
- transition:
168
- opacity var(--drawer-motion-duration) var(--ease-enter),
169
- transform var(--drawer-motion-duration) var(--ease-enter),
170
- overlay var(--drawer-motion-duration) allow-discrete,
171
- display var(--drawer-motion-duration) allow-discrete;
172
- }
173
-
174
- :where(dialog).drawer::backdrop {
175
- transition:
176
- opacity var(--drawer-motion-duration) ease,
177
- overlay var(--drawer-motion-duration) allow-discrete,
178
- display var(--drawer-motion-duration) allow-discrete;
179
- }
180
- }
181
-
182
163
  /* Safari before 16.4 does not support :dir(); keep the ancestor-attribute
183
164
  path as the Degraded-tier fallback. The :dir() rules are dropped on their
184
165
  own there, so they need no wrapper. :dir() follows inherited HTML
@@ -93,7 +93,6 @@
93
93
  .flyout[popover] {
94
94
  position: fixed;
95
95
  inset: auto;
96
- margin: 0;
97
96
  }
98
97
 
99
98
  /* A presentation class may own its *visible* display type, but must not
@@ -40,6 +40,7 @@
40
40
  @import "./breadcrumb.css";
41
41
  @import "./pagination.css";
42
42
  @import "./tab.css";
43
+ @import "./segmented.css";
43
44
  @import "./scrollspy.css";
44
45
 
45
46
  /* Data */