actual-css 0.9.2 → 0.10.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 (101) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +65 -44
  3. package/components.json +33 -13
  4. package/dist/actual.full.css +585 -499
  5. package/dist/actual.full.js +1 -1
  6. package/dist/actual.full.min.css +1 -1
  7. package/docs/design-notes/app-ui.md +1 -1
  8. package/docs/design-notes/column-layout.md +103 -520
  9. package/docs/design-notes/forced-colors.md +12 -1
  10. package/docs/design-notes/naming.md +1 -1
  11. package/docs/design-notes/surface-reparenting.md +8 -9
  12. package/docs/navigation.json +2 -0
  13. package/docs/pages/components/accordion.md +55 -1
  14. package/docs/pages/components/alert.md +9 -15
  15. package/docs/pages/components/badge.md +6 -7
  16. package/docs/pages/components/bleed.md +42 -0
  17. package/docs/pages/components/button.md +30 -4
  18. package/docs/pages/components/card.md +21 -86
  19. package/docs/pages/components/close.md +48 -0
  20. package/docs/pages/components/data-list.md +26 -1
  21. package/docs/pages/components/dialog.md +16 -16
  22. package/docs/pages/components/drawer.md +9 -9
  23. package/docs/pages/components/navbar.md +5 -5
  24. package/docs/pages/components/spinner.md +3 -3
  25. package/docs/pages/enhancements/overview.md +5 -0
  26. package/docs/pages/enhancements/tooltip.md +3 -1
  27. package/docs/pages/examples/overview.md +2 -1
  28. package/docs/pages/forms/choice-cards.md +39 -0
  29. package/docs/pages/forms/combobox.md +23 -11
  30. package/docs/pages/forms/joined-controls.md +2 -5
  31. package/docs/pages/forms/overview.md +6 -0
  32. package/docs/pages/foundations/theming.md +64 -18
  33. package/docs/pages/foundations/tokens.md +28 -10
  34. package/docs/pages/guides/introduction.md +15 -23
  35. package/docs/pages/guides/modular-import.md +53 -6
  36. package/docs/pages/guides/progressive-enhancement.md +6 -3
  37. package/docs/pages/guides/tailwind.md +17 -5
  38. package/docs/pages/layout/column-layout.md +92 -54
  39. package/docs/pages/layout/media.md +1 -1
  40. package/docs/pages/layout/overview.md +1 -1
  41. package/docs/pages/utilities/overview.md +1 -1
  42. package/docs/pages/utilities/sizing-wrapping.md +5 -3
  43. package/llms.txt +24 -4
  44. package/package.json +6 -6
  45. package/reserved-classes.json +5 -30
  46. package/skills/actual-css/PROJECT.md.example +65 -0
  47. package/skills/actual-css/README.md +64 -0
  48. package/skills/actual-css/SKILL.md +234 -0
  49. package/skills/actual-css/references/accessibility.md +44 -0
  50. package/skills/actual-css/references/api-and-vocabulary.md +94 -0
  51. package/skills/actual-css/references/enhancements.md +59 -0
  52. package/skills/actual-css/references/forms.md +49 -0
  53. package/skills/actual-css/references/layout-and-theming.md +82 -0
  54. package/skills/actual-css/references/review-checklist.md +51 -0
  55. package/skills/actual-css/scripts/inspect-actual-css.mjs +171 -0
  56. package/src/css/components/accordion.css +44 -7
  57. package/src/css/components/alert.css +23 -43
  58. package/src/css/components/app-nav.css +9 -0
  59. package/src/css/components/badge.css +11 -39
  60. package/src/css/components/bleed.css +79 -0
  61. package/src/css/components/button.css +6 -17
  62. package/src/css/components/card.css +7 -40
  63. package/src/css/components/close.css +91 -0
  64. package/src/css/components/data-list.css +18 -0
  65. package/src/css/components/drawer.css +15 -56
  66. package/src/css/components/fab.css +10 -0
  67. package/src/css/components/flyout.css +1 -0
  68. package/src/css/components/index.css +2 -0
  69. package/src/css/components/join.css +36 -20
  70. package/src/css/components/menu.css +12 -0
  71. package/src/css/components/modal.css +10 -57
  72. package/src/css/components/navbar.css +27 -0
  73. package/src/css/core/focus.css +9 -3
  74. package/src/css/core/theme.css +25 -43
  75. package/src/css/core/tokens.css +1 -1
  76. package/src/css/core/variants.css +3 -33
  77. package/src/css/forms/choice-card.css +74 -4
  78. package/src/css/forms/choice.css +2 -3
  79. package/src/css/forms/control.css +15 -8
  80. package/src/css/forms/custom-select.css +4 -4
  81. package/src/css/forms/native.css +4 -4
  82. package/src/css/forms/range.css +23 -8
  83. package/src/css/forms/switch.css +2 -3
  84. package/src/css/layout/app-layout.css +1 -1
  85. package/src/css/layout/column-layout.css +26 -111
  86. package/src/css/utilities/extra.css +7 -6
  87. package/src/js/dialog.js +95 -24
  88. package/src/js/enhance.js +7 -3
  89. package/src/js/escape.js +1 -0
  90. package/src/js/flyout.js +10 -1
  91. package/src/js/focus-group.js +2 -1
  92. package/src/js/keys.js +14 -0
  93. package/src/js/menu.js +2 -1
  94. package/src/js/status.js +19 -0
  95. package/src/js/surface.js +12 -8
  96. package/src/js/tab.js +2 -1
  97. package/src/js/tooltip.js +18 -1
  98. package/src/js/validation.js +5 -6
  99. package/dist/actual.css +0 -876
  100. package/dist/actual.js +0 -1
  101. package/dist/actual.min.css +0 -1
@@ -17,7 +17,6 @@
17
17
  * --btn-min-size
18
18
  * --btn-pad-x
19
19
  * --btn-radius
20
- * --btn-focus-color
21
20
  * --btn-gap
22
21
  * --btn-hover-overlay
23
22
  * --btn-default-bg
@@ -30,7 +29,6 @@
30
29
  * --btn-fg
31
30
  * --btn-border
32
31
  * --btn-default-hover-bg
33
- * --btn-focus-ring-color
34
32
  * --btn-border-block-end-color
35
33
  * --btn-border-block-end-width
36
34
  * --spinner-width
@@ -57,7 +55,6 @@
57
55
  --btn-border: var(--ui-border, var(--btn-default-border, transparent));
58
56
  --btn-default-hover-bg: var(--btn-bg);
59
57
  --btn-radius: var(--radius);
60
- --btn-focus-color: var(--intent, var(--focus));
61
58
  --btn-hover-overlay: var(--hover-overlay, rgb(0 0 0 / 0.08));
62
59
 
63
60
  /* Positioning context for indicators, overlays, and grouped states. */
@@ -99,12 +96,6 @@
99
96
  padding: 0;
100
97
  }
101
98
 
102
- @supports (color: color-mix(in oklch, red, white)) {
103
- .btn {
104
- --btn-focus-ring-color: color-mix(in oklch, var(--btn-focus-color) 28%, transparent);
105
- }
106
- }
107
-
108
99
  /* .solid/.soft/.outline are shared via variants.css. The baseline --btn-*
109
100
  fallbacks make .btn solid by default; .ghost/.link are .btn-only variants. */
110
101
  .btn.ghost {
@@ -192,15 +183,13 @@
192
183
  text-decoration-thickness: 0.12em;
193
184
  }
194
185
 
186
+ /* why: the ring must contrast with the surface around the button, not with
187
+ the button, so it is --focus for every intent and variant — the theme
188
+ guarantees --focus against --surface and --surface-solid, which covers
189
+ the page, a solid band and nested surfaces without tracking any context. */
195
190
  .btn:focus-visible {
196
- outline: 2px solid transparent;
197
- box-shadow: 0 0 0 var(--focus-ring-width) var(--btn-focus-ring-color, var(--focus-ring));
198
- }
199
-
200
- .btn.link:focus-visible {
201
- outline: var(--focus-outline);
202
- outline-offset: 0.2em;
203
- box-shadow: none;
191
+ outline: var(--focus-ring-width) solid var(--focus);
192
+ outline-offset: var(--focus-outline-offset);
204
193
  }
205
194
 
206
195
  .btn:disabled {
@@ -19,7 +19,9 @@
19
19
  *
20
20
  * Child contract:
21
21
  * Direct .bleed children escape --card-pad (negative inset, wider media);
22
- * edge children inherit the card corners. .bleed must be a direct child.
22
+ * edge children inherit the card corners. .bleed must be a direct child, and
23
+ * its rules live in components/bleed.css — the card relays --card-pad to the
24
+ * shared --surface-pad that they read.
23
25
  *
24
26
  * Internal:
25
27
  * --card-bg
@@ -50,6 +52,10 @@
50
52
  --card-bg: var(--ui-bg, var(--surface-raised));
51
53
  --card-fg: var(--ui-fg, var(--text));
52
54
 
55
+ /* The inset a direct .bleed child cancels (components/bleed.css). Relayed
56
+ from --card-pad so .compact and an author override both reach it. */
57
+ --surface-pad: var(--card-pad);
58
+
53
59
  /* Keep contextual descendants tied to the surface this card actually owns. */
54
60
  --heading: var(--card-fg);
55
61
  --busy-overlay-bg: var(--card-bg);
@@ -115,45 +121,6 @@
115
121
  --card-pad: var(--space-20);
116
122
  }
117
123
 
118
- /* Full-bleed children */
119
- .card > .bleed {
120
- margin-inline: calc(var(--card-pad) * -1);
121
- max-inline-size: none;
122
- }
123
-
124
- .card > .bleed:first-child {
125
- margin-block-start: calc(var(--card-pad) * -1);
126
- border-start-start-radius: inherit;
127
- border-start-end-radius: inherit;
128
- }
129
-
130
- .card > .bleed:last-child {
131
- margin-block-end: calc(var(--card-pad) * -1);
132
- border-end-start-radius: inherit;
133
- border-end-end-radius: inherit;
134
- }
135
-
136
- /* Media bleed */
137
- .card > :where(img, picture, video, canvas, svg).bleed {
138
- display: block;
139
- inline-size: calc(100% + var(--card-pad) * 2);
140
- max-inline-size: none;
141
- }
142
-
143
- .card > picture.bleed {
144
- overflow: hidden;
145
- }
146
-
147
- .card > picture.bleed > img {
148
- display: block;
149
- inline-size: 100%;
150
- block-size: auto;
151
- }
152
-
153
- /* Padded structural bleed */
154
- .card > :where(header, footer, figure).bleed {
155
- padding: var(--card-pad);
156
- }
157
124
  @media print {
158
125
  .card {
159
126
  border: var(--border-width) solid CanvasText;
@@ -0,0 +1,91 @@
1
+ /*
2
+ * Close button — the shared X that removes or closes the thing it sits in.
3
+ *
4
+ * Docs: docs/pages/components/close.md
5
+ *
6
+ * .close is always the X, painted from the --icon-close mask: an empty
7
+ * <button class="close" aria-label="…"> needs no icon font and no text glyph.
8
+ * The mechanism stays with the container (command="request-close",
9
+ * method="dialog", command="--dismiss", application logic); the class only
10
+ * names the visual action.
11
+ *
12
+ * The containers that host one (alert, badge) tune it from their own files by
13
+ * declaring the size hooks and their geometry; this file owns the control
14
+ * and the dialog-surface corner, shared by .modal and .drawer.
15
+ *
16
+ * Public hooks:
17
+ * --close-size
18
+ * --close-icon-size
19
+ */
20
+ .close {
21
+ display: inline-grid;
22
+ place-items: center;
23
+ flex: none;
24
+ inline-size: var(--close-size, var(--control-size));
25
+ block-size: var(--close-size, var(--control-size));
26
+ padding: 0;
27
+ border: 0;
28
+ border-radius: var(--radius);
29
+ background: transparent;
30
+ color: inherit;
31
+ cursor: pointer;
32
+ }
33
+
34
+ .close::before {
35
+ content: "";
36
+ inline-size: var(--close-icon-size, 1rem);
37
+ block-size: var(--close-icon-size, 1rem);
38
+ background: currentColor;
39
+ -webkit-mask: var(--icon-close) center / contain no-repeat;
40
+ mask: var(--icon-close) center / contain no-repeat;
41
+ }
42
+
43
+ /* An inset overlay, not a background: it tints whatever fill the context gave
44
+ the control (the dialog corner's --surface-subtle) instead of replacing it. */
45
+ @media (hover: hover) {
46
+ .close:hover {
47
+ box-shadow: inset 0 0 0 999px var(--hover-overlay);
48
+ }
49
+ }
50
+
51
+ .close:active {
52
+ box-shadow: inset 0 0 0 999px var(--hover-overlay);
53
+ }
54
+
55
+ .close:focus-visible {
56
+ outline: var(--focus-ring-width) solid var(--focus);
57
+ outline-offset: var(--focus-outline-offset);
58
+ }
59
+
60
+ /* Dialog corner — out of the content flow, anchored to the positioned dialog
61
+ surface rather than to the header or a scroll container inside it. Matched
62
+ on the documented positions (the dialog, its form, its header, or a form
63
+ inside that header) so a .close inside content — an alert in the body —
64
+ keeps its own place. */
65
+ :where(dialog):is(.modal, .drawer) > .close,
66
+ :where(dialog):is(.modal, .drawer) > :is(form, header) > .close,
67
+ :where(dialog):is(.modal, .drawer) > :is(form, header) > :is(form, header) > .close {
68
+ position: absolute;
69
+ z-index: 1;
70
+ inset-block-start: var(--space-20);
71
+ inset-inline-end: var(--space-20);
72
+ background: var(--surface-subtle);
73
+ color: var(--text-muted);
74
+ }
75
+
76
+ @media (hover: hover) {
77
+ :where(dialog):is(.modal, .drawer) > .close:hover,
78
+ :where(dialog):is(.modal, .drawer) > :is(form, header) > .close:hover,
79
+ :where(dialog):is(.modal, .drawer) > :is(form, header) > :is(form, header) > .close:hover {
80
+ color: var(--text);
81
+ }
82
+ }
83
+
84
+ @media (forced-colors: active) {
85
+ /* The X is a currentColor mask fill: without this the system remaps the
86
+ background to Canvas and the glyph disappears. Kept on the pseudo so the
87
+ host keeps its native color and focus adaptation. */
88
+ .close::before {
89
+ forced-color-adjust: none;
90
+ }
91
+ }
@@ -7,6 +7,9 @@
7
7
  * A plain <dl> handles richer relationships (several terms or descriptions)
8
8
  * and stays untouched by this component.
9
9
  *
10
+ * Variants:
11
+ * .stacked — value under its term rather than beside it.
12
+ *
10
13
  * Why margin resets: neutralizes .prose list margins if .data-list appears
11
14
  * inside an authored rich-text scope; the grid owns row/column rhythm.
12
15
  */
@@ -27,3 +30,18 @@
27
30
  min-inline-size: 0;
28
31
  margin: 0;
29
32
  }
33
+
34
+ /* Stacked — the value sits under its term instead of beside it, for narrow
35
+ columns and for values too long to sit in a second column.
36
+ Why the pair separation rides on the term's margin: the children alternate
37
+ dt/dd with no element around a pair, so the grid has no row boundary to give
38
+ a larger gap to. The tight gap is the row gap; the term reopens the space. */
39
+ .data-list.stacked {
40
+ grid-template-columns: minmax(0, 1fr);
41
+ gap: var(--space-10);
42
+ }
43
+
44
+ .data-list.stacked > dt + dt,
45
+ .data-list.stacked > dd + dt {
46
+ margin-block-start: var(--space-30);
47
+ }
@@ -17,6 +17,8 @@
17
17
  * --drawer-size
18
18
  * --drawer-pad
19
19
  *
20
+ * The corner close control is .close (components/close.css).
21
+ *
20
22
  * Internal:
21
23
  * --drawer-closed-translate (side/RTL positioning state)
22
24
  * --drawer-focus-bleed
@@ -26,6 +28,8 @@
26
28
  :where(dialog).drawer {
27
29
  --drawer-size: 20rem;
28
30
  --drawer-pad: var(--space-40);
31
+ /* The inset a direct .bleed child cancels (components/bleed.css). */
32
+ --surface-pad: var(--drawer-pad);
29
33
  --drawer-closed-translate: -100%;
30
34
  /* Exit is shorter than entry so the panel clears quickly; the scrim reads
31
35
  the same variable through inheritance and never lags the panel. */
@@ -125,15 +129,20 @@
125
129
  /* The body region absorbs the panel's height and scrolls. A drawer is a
126
130
  full-height shell by definition, so it always scrolls its body — unlike the
127
131
  modal, which sizes to its content and opts in with .scrollable. Header and
128
- footer keep their own tracks; .drawer-close is named because a generic
132
+ footer keep their own tracks; .close is named because a generic
129
133
  exclusion list is easier to trust than its `position: absolute`. */
130
- :where(dialog).drawer > :not(:is(header, footer, form, .drawer-close)),
131
- :where(dialog).drawer > form > :not(:is(header, footer, .drawer-close)) {
132
- --drawer-focus-bleed: calc(var(--focus-outline-offset) + var(--border-width) * 2);
134
+ :where(dialog).drawer > :not(:is(header, footer, form, .close)),
135
+ :where(dialog).drawer > form > :not(:is(header, footer, .close)) {
136
+ --drawer-focus-bleed: calc(
137
+ var(--focus-outline-offset) +
138
+ max(var(--focus-ring-width), var(--border-width) * 2)
139
+ );
133
140
 
134
141
  min-block-size: 0;
135
142
  /* Keep focus paint inside the clip without moving the body off the fixed
136
- header and footer edges. */
143
+ header and footer edges. The reserve covers the thicker of the two
144
+ outside indicators: the :focus baseline (2 × border) and the component
145
+ line (--focus-ring-width, 3px in bootstrap-v6). */
137
146
  margin-inline: calc(var(--drawer-focus-bleed) * -1);
138
147
  padding-inline: var(--drawer-focus-bleed);
139
148
  overflow: auto;
@@ -205,55 +214,5 @@
205
214
  control. */
206
215
  :where(dialog).drawer > header,
207
216
  :where(dialog).drawer > form > header {
208
- padding-inline-end: calc(var(--control-size) + var(--space-30));
209
- }
210
-
211
- /* Close control, owned here so a drawer-only build does not depend on
212
- modal.css. Mirrors the modal's .dialog-close placement: absolute at the
213
- panel's top inline-end, out of the content flow. */
214
- .drawer-close {
215
- --drawer-close-icon-size: 1rem;
216
-
217
- display: inline-grid;
218
- place-items: center;
219
- padding: 0;
220
- border: 0;
221
- border-radius: var(--radius);
222
- color: var(--text-muted);
223
- cursor: pointer;
224
-
225
- position: absolute;
226
- z-index: 1;
227
- inset-block-start: var(--space-20);
228
- inset-inline-end: var(--space-20);
229
-
230
- inline-size: var(--control-size);
231
- block-size: var(--control-size);
232
-
233
- background: var(--surface-subtle);
234
- }
235
-
236
- .drawer-close::before {
237
- content: "";
238
- inline-size: var(--drawer-close-icon-size);
239
- block-size: var(--drawer-close-icon-size);
240
- background: currentColor;
241
- -webkit-mask: var(--icon-close) center / contain no-repeat;
242
- mask: var(--icon-close) center / contain no-repeat;
243
- }
244
-
245
- @media (hover: hover) {
246
- .drawer-close:hover {
247
- box-shadow: inset 0 0 0 999px var(--hover-overlay);
248
- color: var(--text);
249
- }
250
- }
251
-
252
- .drawer-close:active {
253
- box-shadow: inset 0 0 0 999px var(--hover-overlay);
254
- }
255
-
256
- .drawer-close:focus-visible {
257
- outline: 2px solid transparent;
258
- box-shadow: var(--focus-ring-shadow);
217
+ padding-inline-end: calc(var(--close-size, var(--control-size)) + var(--space-30));
259
218
  }
@@ -128,3 +128,13 @@
128
128
  opacity: 1;
129
129
  transform: none;
130
130
  }
131
+
132
+ @media (forced-colors: active) {
133
+ /* Both toggle markers are currentColor mask fills: without this the system
134
+ remaps the backgrounds to Canvas and the glyphs disappear. Kept on the
135
+ pseudos so the host keeps its native color and focus adaptation. */
136
+ .fab > summary::before,
137
+ .fab > summary::after {
138
+ forced-color-adjust: none;
139
+ }
140
+ }
@@ -70,6 +70,7 @@
70
70
  border: var(--border-width) solid var(--border);
71
71
  border-radius: var(--radius);
72
72
  background: var(--surface-raised);
73
+ color: var(--text);
73
74
  box-shadow: var(--shadow-popout);
74
75
  list-style: none;
75
76
  opacity: 1;
@@ -1,5 +1,6 @@
1
1
  /* Controls */
2
2
  @import "./button.css";
3
+ @import "./close.css";
3
4
  @import "./badge.css";
4
5
  @import "./alert.css";
5
6
  @import "./actions.css";
@@ -8,6 +9,7 @@
8
9
  @import "./card.css";
9
10
  @import "./modal.css";
10
11
  @import "./drawer.css";
12
+ @import "./bleed.css";
11
13
 
12
14
  /* Overlays */
13
15
  @import "./flyout.css";
@@ -50,6 +50,11 @@
50
50
  min-block-size: var(--control-size);
51
51
  padding-inline: var(--space-30);
52
52
  border: var(--border-width) solid var(--control-border);
53
+ /* An addon is a field segment: it honours the same lower-edge extension
54
+ points as text controls (control.css), so a theme's underline or accent
55
+ edge reaches it instead of stopping at the field. */
56
+ border-block-end-color: var(--control-border-block-end-color, var(--control-border));
57
+ border-block-end-width: var(--control-border-block-end-width, var(--border-width));
53
58
  border-radius: var(--radius);
54
59
  background: var(--control-bg);
55
60
  color: var(--control-fg);
@@ -97,37 +102,48 @@
97
102
  z-index: 1;
98
103
  }
99
104
 
100
- /* Keep an active focus ring above an adjacent hovered control. This matters
101
- on platforms where clicking a button does not move focus from the input. */
102
- .join > :is(:focus, :focus-within) {
103
- z-index: 4;
104
- }
105
-
106
105
  /* An input's border must read as the "primary" edge against an attached
107
106
  action (e.g. a password-reveal button) at all times, not only while
108
107
  hovered or focused — otherwise the shared edge flips ownership as the
109
- user's pointer moves, which reads as a rendering glitch. */
110
- .join > :is(.input, .textarea, .select) {
108
+ user's pointer moves, which reads as a rendering glitch. The :has()
109
+ variant covers a control nested one wrapper level deep (e.g. a combobox
110
+ element): the wrapper is the flex item, so it carries the z-index — a
111
+ z-index on the inner control would be a no-op on a static grandchild. */
112
+ .join > :is(.input, .textarea, .select),
113
+ .join > :has(> :is(.input, .textarea, .select)) {
111
114
  z-index: 3;
112
115
  }
113
116
 
114
- /* An input/select with an attached action reads as one composite field. Move
115
- its outer focus treatment to the join; button-only groups keep per-button
116
- focus, and unsupported :has() browsers retain the control's normal ring. */
117
- .join:has(> :is(.input, .textarea, .select):focus-visible) {
118
- border-radius: var(--join-radius);
119
- outline: 2px solid transparent;
120
- box-shadow: var(--focus-ring-shadow);
117
+ /* Keep an active focus line above every neighbour, including an adjacent
118
+ field and a hovered control (some platforms keep focus on the input while
119
+ a button is clicked). It must follow the primary-edge rule above: both are
120
+ (0,2,0), and when it came first a focused field stayed at 3 and the next
121
+ field, painted later, covered the 1px shared edge of its line. */
122
+ .join > :is(:focus, :focus-within) {
123
+ z-index: 4;
121
124
  }
122
125
 
123
- .join:has(> :is(.input, .textarea, .select)) > :is(.input, .textarea, .select):focus-visible {
124
- outline: none;
125
- box-shadow: none;
126
+ /* One wrapper level (e.g. a combobox element around the native control) is
127
+ supported generically — no widget is named here. The wrapper is the flex
128
+ item that receives the corner longhands above; the inner control inherits
129
+ them. The wrapper is also what align-items stretches: block-size: 100%
130
+ resolves against that stretched size, so the field fills it. :hover and
131
+ :focus-within already propagate to the wrapper, so the z-index rules above
132
+ need no duplication. */
133
+ .join > * > :is(.input, .textarea, .select) {
134
+ block-size: 100%;
135
+ border-radius: inherit;
126
136
  }
127
137
 
138
+ /* Text controls paint their own inset focus outline (control.css), so a
139
+ .join never moves the ring to the group: button-only groups and wrapped
140
+ inputs alike keep per-control focus, with no :has() group treatment. */
141
+
128
142
  /* A default outline action belongs to the composite field border. An explicit
129
143
  intent still provides its own outline color — --intent is guaranteed-invalid
130
- for a neutral button (the :where(.btn) boundary), so the fallback applies. */
131
- .join:has(> :is(.input, .textarea, .select)) > .btn.outline {
144
+ for a neutral button (the :where(.btn) boundary), so the fallback applies.
145
+ The second :has() covers a control nested one wrapper level deep. */
146
+ .join:has(> :is(.input, .textarea, .select)) > .btn.outline,
147
+ .join:has(> * > :is(.input, .textarea, .select)) > .btn.outline {
132
148
  --btn-border-color: var(--intent, var(--border));
133
149
  }
@@ -133,3 +133,15 @@
133
133
  border: 0;
134
134
  border-block-start: var(--border-width) solid var(--border);
135
135
  }
136
+
137
+ @media (forced-colors: active) {
138
+ /* The check mark is a currentColor mask fill and the mixed/radio marks are
139
+ currentColor gradients: without this the system remaps them to Canvas or
140
+ drops the image and the state disappears. Kept on the pseudo so the host
141
+ keeps its native color and focus adaptation. */
142
+ .menu-item[role="menuitemcheckbox"][aria-checked="true"]::before,
143
+ .menu-item[role="menuitemcheckbox"][aria-checked="mixed"]::before,
144
+ .menu-item[role="menuitemradio"]::before {
145
+ forced-color-adjust: none;
146
+ }
147
+ }
@@ -12,8 +12,9 @@
12
12
  * --dialog-icon-size
13
13
  * --dialog-icon-glyph-size
14
14
  *
15
+ * The corner close control is .close (components/close.css).
16
+ *
15
17
  * Internal:
16
- * --dialog-close-icon-size
17
18
  * --dialog-focus-bleed
18
19
  * --dialog-viewport-block
19
20
  * --dialog-viewport-gap-default
@@ -99,11 +100,16 @@
99
100
 
100
101
  :where(dialog).modal.scrollable > form > :not(:is(header, footer)),
101
102
  :where(dialog).modal.scrollable > .stack > :not(:is(header, footer)) {
102
- --dialog-focus-bleed: calc(var(--focus-outline-offset) + var(--border-width) * 2);
103
+ --dialog-focus-bleed: calc(
104
+ var(--focus-outline-offset) +
105
+ max(var(--focus-ring-width), var(--border-width) * 2)
106
+ );
103
107
 
104
108
  min-block-size: 0;
105
109
  /* Keep focus paint inside the clip without moving the body off the fixed
106
- header and footer edges. */
110
+ header and footer edges. The reserve covers the thicker of the two
111
+ outside indicators: the :focus baseline (2 × border) and the component
112
+ line (--focus-ring-width, 3px in bootstrap-v6). */
107
113
  margin-inline: calc(var(--dialog-focus-bleed) * -1);
108
114
  padding-inline: var(--dialog-focus-bleed);
109
115
  overflow: auto;
@@ -126,7 +132,7 @@
126
132
  The form/.stack anatomy is structural, so it stays in :where() and never
127
133
  lends .stack's specificity to a bare <form>. */
128
134
  :where(dialog).modal > :where(form, .stack) > header {
129
- padding-inline-end: calc(var(--control-size) + var(--space-30));
135
+ padding-inline-end: calc(var(--close-size, var(--control-size)) + var(--space-30));
130
136
  }
131
137
 
132
138
  :where(dialog).modal > :where(form, .stack) > footer {
@@ -289,56 +295,3 @@ html.has-modal-open.had-scrollbar {
289
295
  transform: translateX(0.35rem);
290
296
  }
291
297
  }
292
-
293
- /* Close control. A close button is an action attached to a surface, so it
294
- never participates in the content flow — absolute at the panel's top
295
- inline-end, inside the surface. The glyph comes from --icon-close, masked to
296
- currentColor, so no icon font or text glyph is required in the markup. */
297
- .dialog-close {
298
- --dialog-close-icon-size: 1rem;
299
-
300
- display: inline-grid;
301
- place-items: center;
302
- padding: 0;
303
- border: 0;
304
- border-radius: var(--radius);
305
- color: var(--text-muted);
306
- cursor: pointer;
307
-
308
- /* Out of flow, anchored to the positioned dialog surface, not the header or
309
- any scroll container inside it. */
310
- position: absolute;
311
- z-index: 1;
312
- inset-block-start: var(--space-20);
313
- inset-inline-end: var(--space-20);
314
-
315
- inline-size: var(--control-size);
316
- block-size: var(--control-size);
317
-
318
- background: var(--surface-subtle);
319
- }
320
-
321
- .dialog-close::before {
322
- content: "";
323
- inline-size: var(--dialog-close-icon-size);
324
- block-size: var(--dialog-close-icon-size);
325
- background: currentColor;
326
- -webkit-mask: var(--icon-close) center / contain no-repeat;
327
- mask: var(--icon-close) center / contain no-repeat;
328
- }
329
-
330
- @media (hover: hover) {
331
- .dialog-close:hover {
332
- box-shadow: inset 0 0 0 999px var(--hover-overlay);
333
- color: var(--text);
334
- }
335
- }
336
-
337
- .dialog-close:active {
338
- box-shadow: inset 0 0 0 999px var(--hover-overlay);
339
- }
340
-
341
- .dialog-close:focus-visible {
342
- outline: 2px solid transparent;
343
- box-shadow: var(--focus-ring-shadow);
344
- }
@@ -68,11 +68,16 @@
68
68
  display: inline-flex;
69
69
  align-items: center;
70
70
  min-block-size: var(--control-size-sm);
71
+ padding-block: 0;
71
72
  padding-inline: var(--space-20);
73
+ border: 0;
72
74
  border-radius: var(--radius);
75
+ background: transparent;
73
76
  color: var(--ui-fg, var(--text-muted));
77
+ font: inherit;
74
78
  font-weight: var(--font-weight-medium);
75
79
  text-decoration: none;
80
+ cursor: pointer;
76
81
  }
77
82
 
78
83
  /* nav-link serves two contracts: plain page navigation (aria-current="page")
@@ -97,6 +102,28 @@
97
102
  color: var(--state-selected);
98
103
  }
99
104
 
105
+ /* On a bar with its own --ui-bg/--ui-fg the fill is a tint of the link's own
106
+ ink, the .btn.ghost hover recipe, so it reads on light and dark bars alike
107
+ instead of snapping back to --surface-subtle. Not --ui-hover-bg: that is the
108
+ bar's own hover and can equal its fill. The ink follows --ui-fg too — the
109
+ selected accent has no contrast pair with an arbitrary bar. The baseline
110
+ above keeps the light chip where color-mix is missing, with ink that is
111
+ legible on it. */
112
+ @supports (color: color-mix(in oklch, red, white)) {
113
+ .nav-link:is(:hover, [aria-current]) {
114
+ background: color-mix(in oklch, currentColor 10%, transparent);
115
+ }
116
+
117
+ .nav-link:hover {
118
+ color: var(--ui-fg, var(--text));
119
+ }
120
+
121
+ .nav-link[aria-current],
122
+ .nav-link[aria-current]:hover {
123
+ color: var(--ui-fg, var(--state-selected));
124
+ }
125
+ }
126
+
100
127
  /* Nav list — a vertical navigation list. Used by .drawer for stacked links.
101
128
  Links fill each row so the whole line is an interactive target; the
102
129
  horizontal .navbar-nav links keep their intrinsic size. */
@@ -3,9 +3,15 @@
3
3
  pointer/mouse focus and interactive components layer their ring on top.
4
4
 
5
5
  Invariant: an interactive component must never rely on box-shadow alone
6
- for its keyboard focus indicator. Components that remove the native
7
- outline on :focus-visible replace it with a transparent outline — invisible
8
- normally, forced to the system focus color under forced-colors. */
6
+ for its keyboard focus indicator; forced colors keep outlines and drop
7
+ shadows.
8
+
9
+ Component indicators are a solid --focus-ring-width line in --focus: inset
10
+ on fields (inside their border box), offset by --focus-outline-offset on
11
+ discrete actions. The color never derives from an intent or a local
12
+ surface — the theme guarantees --focus at 3:1 against both --surface and
13
+ --surface-solid, so one token reads on the page, on a solid band and on
14
+ any nested surface. */
9
15
  :focus {
10
16
  outline: var(--focus-outline);
11
17
  outline-offset: var(--focus-outline-offset);