actual-css 0.9.1 → 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 (107) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/README.md +67 -45
  3. package/components.json +33 -13
  4. package/dist/actual.full.css +607 -502
  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/components/table.md +3 -2
  26. package/docs/pages/enhancements/overview.md +5 -0
  27. package/docs/pages/enhancements/tooltip.md +3 -1
  28. package/docs/pages/examples/overview.md +3 -1
  29. package/docs/pages/forms/choice-cards.md +39 -0
  30. package/docs/pages/forms/combobox.md +23 -11
  31. package/docs/pages/forms/input-policy.md +59 -0
  32. package/docs/pages/forms/joined-controls.md +2 -5
  33. package/docs/pages/forms/overview.md +6 -0
  34. package/docs/pages/forms/validation.md +37 -1
  35. package/docs/pages/foundations/theming.md +64 -18
  36. package/docs/pages/foundations/tokens.md +50 -13
  37. package/docs/pages/foundations/typography.md +10 -0
  38. package/docs/pages/guides/introduction.md +17 -24
  39. package/docs/pages/guides/modular-import.md +53 -6
  40. package/docs/pages/guides/progressive-enhancement.md +9 -5
  41. package/docs/pages/guides/tailwind.md +17 -5
  42. package/docs/pages/layout/column-layout.md +92 -54
  43. package/docs/pages/layout/media.md +1 -1
  44. package/docs/pages/layout/overview.md +1 -1
  45. package/docs/pages/utilities/overview.md +1 -1
  46. package/docs/pages/utilities/sizing-wrapping.md +5 -3
  47. package/llms.txt +24 -4
  48. package/package.json +7 -6
  49. package/reserved-classes.json +5 -30
  50. package/skills/actual-css/PROJECT.md.example +65 -0
  51. package/skills/actual-css/README.md +64 -0
  52. package/skills/actual-css/SKILL.md +234 -0
  53. package/skills/actual-css/references/accessibility.md +44 -0
  54. package/skills/actual-css/references/api-and-vocabulary.md +94 -0
  55. package/skills/actual-css/references/enhancements.md +59 -0
  56. package/skills/actual-css/references/forms.md +49 -0
  57. package/skills/actual-css/references/layout-and-theming.md +82 -0
  58. package/skills/actual-css/references/review-checklist.md +51 -0
  59. package/skills/actual-css/scripts/inspect-actual-css.mjs +171 -0
  60. package/src/css/components/accordion.css +44 -7
  61. package/src/css/components/alert.css +23 -43
  62. package/src/css/components/app-nav.css +9 -0
  63. package/src/css/components/badge.css +11 -39
  64. package/src/css/components/bleed.css +79 -0
  65. package/src/css/components/button.css +6 -17
  66. package/src/css/components/card.css +7 -40
  67. package/src/css/components/close.css +91 -0
  68. package/src/css/components/data-list.css +18 -0
  69. package/src/css/components/drawer.css +15 -56
  70. package/src/css/components/fab.css +10 -0
  71. package/src/css/components/flyout.css +1 -0
  72. package/src/css/components/index.css +2 -0
  73. package/src/css/components/join.css +36 -20
  74. package/src/css/components/menu.css +12 -0
  75. package/src/css/components/modal.css +10 -57
  76. package/src/css/components/navbar.css +27 -0
  77. package/src/css/core/base.css +2 -0
  78. package/src/css/core/focus.css +9 -3
  79. package/src/css/core/theme.css +25 -43
  80. package/src/css/core/tokens.css +3 -1
  81. package/src/css/core/variants.css +15 -36
  82. package/src/css/forms/choice-card.css +74 -4
  83. package/src/css/forms/choice.css +2 -3
  84. package/src/css/forms/control.css +15 -8
  85. package/src/css/forms/custom-select.css +4 -4
  86. package/src/css/forms/form.css +6 -0
  87. package/src/css/forms/native.css +4 -4
  88. package/src/css/forms/range.css +23 -8
  89. package/src/css/forms/switch.css +2 -3
  90. package/src/css/layout/app-layout.css +1 -1
  91. package/src/css/layout/column-layout.css +26 -111
  92. package/src/css/utilities/extra.css +7 -6
  93. package/src/js/dialog.js +95 -24
  94. package/src/js/enhance.js +7 -3
  95. package/src/js/escape.js +1 -0
  96. package/src/js/flyout.js +10 -1
  97. package/src/js/focus-group.js +2 -1
  98. package/src/js/keys.js +14 -0
  99. package/src/js/menu.js +2 -1
  100. package/src/js/status.js +19 -0
  101. package/src/js/surface.js +12 -8
  102. package/src/js/tab.js +2 -1
  103. package/src/js/tooltip.js +18 -1
  104. package/src/js/validation.js +36 -6
  105. package/dist/actual.css +0 -863
  106. package/dist/actual.js +0 -1
  107. package/dist/actual.min.css +0 -1
@@ -72,10 +72,10 @@
72
72
 
73
73
  /* Lines and focus */
74
74
  --border: hsl(260 15% 88%);
75
- --focus: var(--neutral);
76
- /* Pre-color-mix fallback only — superseded below by a derivation from
77
- --focus so overriding --neutral/--focus never leaves a stale ring. */
78
- --focus-ring: hsl(258 7% 43% / 0.3);
75
+ /* A primary-hued mid-tone on purpose, one value for both schemes: focus
76
+ lines must hold 3:1 against --surface and --surface-solid alike (page,
77
+ fields, a solid band), which rules out the primary itself. */
78
+ --focus: hsl(268 30% 55%);
79
79
  --hover-overlay: hsl(260 20% 10% / 0.04);
80
80
 
81
81
  /* Radius scale — themes may override the whole scale (e.g. [data-theme="square"]) */
@@ -98,7 +98,6 @@
98
98
  --heading: var(--text);
99
99
  --selection-bg: var(--primary);
100
100
  --selection-fg: var(--primary-fg);
101
- --focus-ring-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring);
102
101
  }
103
102
 
104
103
  /* Soft foreground hooks — the soft ink is the intent rebated toward the local
@@ -107,18 +106,17 @@
107
106
  default theme: a custom property bakes its var() references where it is
108
107
  declared, so these resolve against root/light/dark tokens on every boundary.
109
108
  Presets and custom islands keep their --soft-fg-mix / raw-intent ink unless
110
- they declare hooks themselves — :where() keeps this reset from outranking a
111
- theme's own declaration (e.g. brutalist's 45% soft-fg-mix). */
112
- :where([data-theme]:not([data-theme="light"]):not([data-theme="dark"])) {
113
- --primary-soft-fg: initial;
114
- --secondary-soft-fg: initial;
115
- --success-soft-fg: initial;
116
- --warning-soft-fg: initial;
117
- --danger-soft-fg: initial;
118
- --neutral-soft-fg: initial;
119
- }
120
-
121
- :root,
109
+ they declare hooks themselves.
110
+
111
+ Why :where(:root) and why the reset comes second: both rules match when a
112
+ custom theme sits on <html> itself. A bare :root is (0,1,0) and would
113
+ outrank the zero-specificity reset, so the same theme resolved a calibrated
114
+ Ink & Terra ink on <html data-theme="brand"> and the raw --soft-fg-mix ink
115
+ on <div data-theme="brand"> — one attribute, two soft palettes. Levelling
116
+ both rules to zero specificity makes source order the tiebreaker, so the
117
+ reset must stay below. Zero specificity is also what lets a theme declare
118
+ its own hooks (e.g. brutalist's 45% soft-fg-mix) and win at (0,1,0). */
119
+ :where(:root),
122
120
  [data-theme="light"],
123
121
  [data-theme="dark"] {
124
122
  --secondary-soft-fg: color-mix(in oklab, var(--text) 10%, var(--secondary) 90%);
@@ -128,6 +126,15 @@
128
126
  --neutral-soft-fg: color-mix(in oklab, var(--text) 20%, var(--neutral) 80%);
129
127
  }
130
128
 
129
+ :where([data-theme]:not([data-theme="light"]):not([data-theme="dark"])) {
130
+ --primary-soft-fg: initial;
131
+ --secondary-soft-fg: initial;
132
+ --success-soft-fg: initial;
133
+ --warning-soft-fg: initial;
134
+ --danger-soft-fg: initial;
135
+ --neutral-soft-fg: initial;
136
+ }
137
+
131
138
  /* Native platform controls (checkbox, radio, range, progress) that are not
132
139
  restyled by a component class still follow the theme accent. Declared on
133
140
  every island: accent-color inherits as a computed value, so islands must
@@ -149,20 +156,6 @@
149
156
  --tone-fg: var(--text);
150
157
  }
151
158
 
152
- /* Focus ring derivation — color-mix tier. Runs whether or not light-dark()
153
- is also supported, so it covers the color-mix-without-light-dark browser
154
- tier on its own; the light-dark block below refines it further with the
155
- light/dark alpha split once both features are available. Declared on every
156
- [data-theme] island (not just the built-in light/dark pair) so custom
157
- islands recolor the ring for free; a preset that re-declares --focus-ring
158
- later in source order intentionally pins its own ring instead. */
159
- @supports (color: color-mix(in oklch, red, white)) {
160
- :root,
161
- [data-theme] {
162
- --focus-ring: color-mix(in oklch, var(--focus) 30%, transparent);
163
- }
164
- }
165
-
166
159
  /* Auto dark mode via light-dark(). Resolves based on the element's color-scheme:
167
160
  :root (light dark) follows the OS, [data-theme="light"] forces light,
168
161
  [data-theme="dark"] forces dark by overriding color-scheme.
@@ -199,22 +192,11 @@
199
192
  --text-subtle: light-dark(hsl(258 7% 57%), hsl(258 7% 54%));
200
193
 
201
194
  --border: light-dark(hsl(260 15% 88%), hsl(262 12% 24%));
202
- --focus-ring: light-dark(hsl(258 7% 43% / 0.3), hsl(258 10% 72% / 0.28));
203
195
  --hover-overlay: light-dark(hsl(260 20% 10% / 0.04), hsl(0 0% 100% / 0.06));
204
196
  --shadow-color: light-dark(hsl(260 25% 10%), hsl(262 40% 2%));
205
197
  }
206
198
  }
207
199
 
208
- @supports (color: color-mix(in oklch, red, white)) and (color: light-dark(red, blue)) {
209
- :root,
210
- [data-theme] {
211
- --focus-ring: light-dark(
212
- color-mix(in oklch, var(--focus) 30%, transparent),
213
- color-mix(in oklch, var(--focus) 28%, transparent)
214
- );
215
- }
216
- }
217
-
218
200
  @media (prefers-contrast: more) {
219
201
  :root,
220
202
  /* The repeated attribute intentionally outranks single-attribute preset
@@ -222,7 +204,7 @@
222
204
  [data-theme][data-theme] {
223
205
  --border-width: 2px;
224
206
  --focus-outline-offset: 3px;
225
- --focus-ring-width: 4px;
207
+ --focus-ring-width: 3px;
226
208
  --text-muted: var(--text);
227
209
  --text-subtle: var(--text);
228
210
  }
@@ -19,6 +19,8 @@
19
19
  /* Typography */
20
20
  --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
21
21
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
22
+ --font-width: 100%;
23
+ --font-width-dense: 95%;
22
24
  --line-height: 1.5;
23
25
  --line-height-tight: 1.25;
24
26
  --line-height-relaxed: 1.75;
@@ -80,7 +82,7 @@
80
82
  --focus-outline-color: currentColor;
81
83
  --focus-outline: calc(var(--border-width) * 2) solid var(--focus-outline-color);
82
84
  --focus-outline-offset: 2px;
83
- --focus-ring-width: 3px;
85
+ --focus-ring-width: 2px;
84
86
 
85
87
  /* Status dot positioning — used by .avatar > .badge:empty. The offset is
86
88
  the circle-aware inset (1 - 1/√2) / 2 ≈ 14.65%; the ring surrounds the
@@ -2,13 +2,9 @@
2
2
  * Cross-cutting variant token mapping.
3
3
  *
4
4
  * Variant scope: .solid/.soft/.outline/.surface set --ui-* for components that
5
- * opt into shared variants. .inverted is a surface modifier, not a variant: it paints
6
- * the element directly (background/color/border-color) so it works on any bare
7
- * block, and remaps the shared --ui-* tokens so participating components
8
- * (.card, .navbar, .alert, .badge) invert through their own rules. Loaded
9
- * before components, so a component with its own explicit surface (e.g.
10
- * .accordion) naturally keeps that surface — .inverted never forces a
11
- * non-participant.
5
+ * opt into shared variants. A subtree that needs another palette or scheme is
6
+ * a data-theme island, never a variant: only an island recomputes every token
7
+ * (fields, states, intents), where a remap of a few --ui-* tokens cannot.
12
8
  * Component-specific defaults (.btn solid-by-default, .badge
13
9
  * soft-by-default, .alert soft-by-default) live with their components:
14
10
  * components/button.css — .btn default solid + .btn.ghost + .btn.link
@@ -16,9 +12,16 @@
16
12
  * components/alert.css — .alert default soft behind explicit --ui-*
17
13
  *
18
14
  * .compact/.spacious set inherited density tokens: spacing rhythm and geometry
19
- * only. Density never changes typography or icon size. Controls consume
20
- * --control-size; non-control components may consume --density-* when the
21
- * context should affect their local rhythm.
15
+ * only, plus a slight typographic width bonus. Density never changes font size
16
+ * or icon size. Controls consume --control-size; non-control components may
17
+ * consume --density-* when the context should affect their local rhythm.
18
+ * .compact applies --font-width-dense through font-stretch (a variable wdth
19
+ * axis when available, otherwise normal width-face matching); layout must
20
+ * never depend on the narrowing being available. .spacious restores
21
+ * --font-width, so a spacious island inside a compact context returns to the
22
+ * normal width exactly as it restores its own gap, spacing, and control size.
23
+ * Themes whose typeface has no useful width axis opt out with
24
+ * --font-width-dense: 100%.
22
25
  *
23
26
  * .sm/.lg are local size roles, not shared measurements. Shared control
24
27
  * participants below map both typography and geometry; components with
@@ -57,38 +60,18 @@
57
60
  --ui-border: var(--border);
58
61
  }
59
62
 
60
- /* Inverse surface treatment. Paints the element directly (so it works on any
61
- bare block) and remaps the shared --ui-* tokens so opt-in components
62
- (.card, .navbar) invert through their own rules. Because this loads before
63
- component base surfaces, components with their own explicit surface
64
- (e.g. .accordion) keep it — participation is opt-in, never forced. --heading
65
- is set so global headings (var(--heading, inherit)) do not inherit an outside
66
- heading color onto the dark surface; surface-owning descendants derive
67
- their contextual aliases from their own local surface. */
68
- .inverted {
69
- --ui-bg: var(--surface-solid);
70
- --ui-fg: var(--surface);
71
- --ui-border: transparent;
72
- --ui-hover-bg: var(--surface-solid);
73
-
74
- --heading: var(--surface);
75
- --busy-overlay-bg: var(--surface-solid);
76
-
77
- background: var(--ui-bg);
78
- color: var(--ui-fg);
79
- border-color: var(--ui-border);
80
- }
81
-
82
63
  .compact {
83
64
  --gap: var(--space-20);
84
65
  --density-space: var(--space-20);
85
66
  --control-size: var(--control-size-sm);
67
+ font-stretch: var(--font-width-dense);
86
68
  }
87
69
 
88
70
  .spacious {
89
71
  --gap: var(--space-50);
90
72
  --density-space: var(--space-50);
91
73
  --control-size: var(--control-size-lg);
74
+ font-stretch: var(--font-width);
92
75
  }
93
76
 
94
77
  /* Shared control scale. Wrappers are listed when their descendants form one
@@ -160,10 +143,6 @@
160
143
  );
161
144
  /* @sync soft-recipe */
162
145
  }
163
-
164
- .inverted {
165
- --ui-hover-bg: color-mix(in oklch, var(--surface-solid) 88%, black);
166
- }
167
146
  }
168
147
 
169
148
  /* Interactive hover recipes live in the components that consume them:
@@ -13,12 +13,18 @@
13
13
  * overflow: hidden will clip it. Pair with grid or flex utilities for group
14
14
  * layout.
15
15
  *
16
+ * .chip is the compact pill form (filter chips, selectable tags): control
17
+ * height, and a leading dot that turns into the check in a fixed slot, so
18
+ * checking never moves the label.
19
+ *
16
20
  * Public hooks:
17
21
  * --choice-card-pad
18
22
  * --choice-card-radius
19
23
  * --choice-card-border
20
24
  * --choice-card-bg
21
25
  * --choice-card-check-size
26
+ * --choice-chip-dot (read-only, on .chip ::before: the check-slot centre as a
27
+ * circle() position, for a theme that grows the dot into a fill)
22
28
  */
23
29
  :where(.choice-card) {
24
30
  --intent: initial;
@@ -68,8 +74,8 @@
68
74
 
69
75
  /* Focus ring on the label follows the hidden input's focus. */
70
76
  .choice-card:has(:focus-visible) {
71
- outline: 2px solid transparent;
72
- box-shadow: var(--focus-ring-shadow);
77
+ outline: var(--focus-ring-width) solid var(--focus);
78
+ outline-offset: var(--focus-outline-offset);
73
79
  }
74
80
 
75
81
  .choice-card:has(:disabled) {
@@ -114,11 +120,13 @@
114
120
  z-index: 1;
115
121
  }
116
122
 
117
- .choice-card:has(input[type="radio"])::before {
123
+ /* :where() keeps the shape at single-class weight, so .chip can reshape the
124
+ layer without out-arguing the input-type test. */
125
+ .choice-card:where(:has(input[type="radio"]))::before {
118
126
  border-radius: var(--radius-full);
119
127
  }
120
128
 
121
- .choice-card:has(input[type="checkbox"])::before {
129
+ .choice-card:where(:has(input[type="checkbox"]))::before {
122
130
  border-radius: var(--radius-sm);
123
131
  }
124
132
 
@@ -135,6 +143,54 @@
135
143
  transform: scale(1);
136
144
  }
137
145
 
146
+ /* Chip — the rules above stay in force; this section only relocates the
147
+ indicator. Must follow them: several ties are won by source order. */
148
+ .choice-card.chip {
149
+ --choice-card-pad: 0 var(--control-pad-x);
150
+ --choice-card-radius: var(--radius-full);
151
+ --choice-card-check-size: 1em;
152
+
153
+ display: inline-flex;
154
+ align-items: center;
155
+ min-block-size: var(--control-size);
156
+ line-height: 1.2;
157
+ /* The dot layer sits under the label at z-index -1. */
158
+ isolation: isolate;
159
+ }
160
+
161
+ /* ::before is a full-bleed layer clipped to a dot centred on the check slot,
162
+ so a theme can grow the same clip into a selected fill. The chip itself
163
+ must not clip (its focus ring is an outline), hence the inherited radius. */
164
+ .choice-card.chip::before {
165
+ --choice-chip-dot: calc(var(--control-pad-x) + var(--choice-card-check-size) / 2) 50%;
166
+
167
+ inset: 0;
168
+ inline-size: auto;
169
+ block-size: auto;
170
+ border-radius: inherit;
171
+ box-shadow: none;
172
+ background: var(--text-subtle);
173
+ transform: none;
174
+ clip-path: circle(0.1875em at var(--choice-chip-dot));
175
+ z-index: -1;
176
+ transition: clip-path var(--duration);
177
+ }
178
+
179
+ .choice-card.chip:has(:checked)::before {
180
+ clip-path: circle(0 at var(--choice-chip-dot));
181
+ }
182
+
183
+ /* The check keeps the card's scale-in, in flow ahead of the label. */
184
+ .choice-card.chip::after {
185
+ position: static;
186
+ order: -1;
187
+ flex: none;
188
+ margin-inline-end: 0.375em;
189
+ background: var(--intent, var(--state-selected));
190
+ -webkit-mask-size: contain;
191
+ mask-size: contain;
192
+ }
193
+
138
194
  /* The native input is hidden and the checked marker is custom-drawn: without
139
195
  this repair, forced colors pushes disc and check to the same system color
140
196
  and the state disappears. Highlight/Canvas/HighlightText are preserved as-is,
@@ -149,4 +205,18 @@
149
205
  .choice-card:has(:checked)::after {
150
206
  background: HighlightText;
151
207
  }
208
+
209
+ /* A chip has no disc behind its check: the check carries the state alone,
210
+ and dropping the checked layer also drops any theme fill. */
211
+ .choice-card.chip::before {
212
+ background: CanvasText;
213
+ }
214
+
215
+ .choice-card.chip:has(:checked)::before {
216
+ content: none;
217
+ }
218
+
219
+ .choice-card.chip:has(:checked)::after {
220
+ background: CanvasText;
221
+ }
152
222
  }
@@ -228,9 +228,8 @@
228
228
 
229
229
  .check:focus-visible,
230
230
  .radio:focus-visible {
231
- --choice-border: var(--focus);
232
- outline: 2px solid transparent;
233
- box-shadow: var(--focus-ring-shadow);
231
+ outline: var(--focus-ring-width) solid var(--focus);
232
+ outline-offset: var(--focus-outline-offset);
234
233
  }
235
234
 
236
235
  .check:disabled,
@@ -24,6 +24,8 @@
24
24
  * --control-cursor
25
25
  * --control-border-block-end-color
26
26
  * --control-border-block-end-width
27
+ * --control-hover-border (input and textarea)
28
+ * --control-hover-bg (select)
27
29
  */
28
30
  .input,
29
31
  .textarea,
@@ -68,19 +70,24 @@
68
70
 
69
71
  .input:hover:not(:disabled),
70
72
  .textarea:hover:not(:disabled) {
71
- --control-border: var(--text-subtle);
73
+ --control-border: var(--control-hover-border, var(--text-subtle));
72
74
  }
73
75
 
74
76
  .select:hover:not(:disabled) {
75
- --control-bg: var(--surface-subtle);
77
+ --control-bg: var(--control-hover-bg, var(--surface-subtle));
76
78
  }
77
79
 
78
- .input:focus-visible,
79
- .textarea:focus-visible,
80
- .select:focus-visible {
81
- --control-border: var(--focus);
82
- outline: 2px solid transparent;
83
- box-shadow: var(--focus-ring-shadow);
80
+ /* Field focus is an inset outline: offset = -width keeps every pixel inside
81
+ the control's border box (any smaller offset bleeds outward), so it covers
82
+ the border, never touches a .join neighbour or a scrollport edge, and needs
83
+ no group treatment. Width follows --focus-ring-width, including its
84
+ prefers-contrast step. Border = shape and validation, outline = focus. */
85
+ .input:focus-visible:not(:disabled),
86
+ .textarea:focus-visible:not(:disabled),
87
+ .select:focus-visible:not(:disabled) {
88
+ outline: var(--focus-ring-width) solid var(--form-invalid-border, var(--focus));
89
+ outline-offset: calc(var(--focus-ring-width) * -1);
90
+ box-shadow: none;
84
91
  }
85
92
 
86
93
  .input[readonly],
@@ -9,7 +9,6 @@
9
9
  * Arbitrary menu and popover behavior belongs elsewhere.
10
10
  *
11
11
  * Internal:
12
- * --control-border
13
12
  * --control-bg
14
13
  */
15
14
  /* This enhancement is excluded from forced-colors, where the native/shared
@@ -34,11 +33,12 @@
34
33
  box-shadow: var(--shadow-popout);
35
34
  }
36
35
 
36
+ /* Same inset outline as a focused field (control.css). */
37
37
  &:open {
38
- --control-border: var(--border);
39
38
  --control-bg: var(--surface-raised);
40
- outline: none;
41
- box-shadow: var(--focus-ring-shadow);
39
+ outline: var(--focus-ring-width) solid var(--form-invalid-border, var(--focus));
40
+ outline-offset: calc(var(--focus-ring-width) * -1);
41
+ box-shadow: none;
42
42
  }
43
43
 
44
44
  &::picker-icon {
@@ -18,6 +18,12 @@
18
18
  */
19
19
  .field {
20
20
  display: grid;
21
+
22
+ /* why: a field stretched by a taller row sibling — the classic case is a
23
+ neighbour revealing its .field-error — would otherwise hand the extra
24
+ block size to its auto rows and inflate the control. Rows keep their
25
+ natural height; the slack stays at the end. */
26
+ align-content: start;
21
27
  gap: var(--space-10);
22
28
  color: var(--text);
23
29
  font-size: var(--control-font-size);
@@ -44,8 +44,8 @@
44
44
  }
45
45
 
46
46
  .file:focus-visible {
47
- outline: 2px solid transparent;
48
- box-shadow: var(--focus-ring-shadow);
47
+ outline: var(--focus-ring-width) solid var(--focus);
48
+ outline-offset: var(--focus-outline-offset);
49
49
  }
50
50
 
51
51
  /* Native color picker — keep the browser's dialog and selected-color UI. The
@@ -81,8 +81,8 @@
81
81
  }
82
82
 
83
83
  .color:focus-visible {
84
- outline: 2px solid transparent;
85
- box-shadow: var(--focus-ring-shadow);
84
+ outline: var(--focus-ring-width) solid var(--focus);
85
+ outline-offset: var(--focus-outline-offset);
86
86
  }
87
87
 
88
88
  @media (forced-colors: active) {
@@ -75,20 +75,22 @@
75
75
  }
76
76
 
77
77
  /*
78
- * The 1px surface-coloured separator keeps the ring visually distinct from
79
- * the small, coloured thumb — unlike a full-size input where the same gap
80
- * would read as an unwanted void.
78
+ * A thumb pseudo-element takes no reliable outline, so its ring is a solid
79
+ * --focus shadow (the transparent outline above covers forced colors). The
80
+ * 1px surface-coloured separator keeps the ring visually distinct from the
81
+ * small, coloured thumb — unlike a full-size input where the same gap would
82
+ * read as an unwanted void.
81
83
  */
82
84
  .range:focus-visible::-webkit-slider-thumb {
83
85
  box-shadow:
84
86
  0 0 0 1px var(--surface),
85
- 0 0 0 calc(1px + var(--focus-ring-width)) var(--focus-ring);
87
+ 0 0 0 calc(1px + var(--focus-ring-width)) var(--focus);
86
88
  }
87
89
 
88
90
  .range:focus-visible::-moz-range-thumb {
89
91
  box-shadow:
90
92
  0 0 0 1px var(--surface),
91
- 0 0 0 calc(1px + var(--focus-ring-width)) var(--focus-ring);
93
+ 0 0 0 calc(1px + var(--focus-ring-width)) var(--focus);
92
94
  }
93
95
 
94
96
  .range:disabled {
@@ -98,20 +100,33 @@
98
100
  }
99
101
 
100
102
  /* Forced colors: the track must stay a visible line against the page (Canvas
101
- would merge with it) and the thumb needs a real boundary to remain
102
- distinguishable from the track. Disabled keeps GrayText via :disabled.
103
- Focus stays on the transparent outline of :focus-visible. */
103
+ would merge with it) and the thumb needs a real boundary to remain
104
+ distinguishable from the track. Disabled keeps GrayText via :disabled.
105
+ Focus stays on the transparent outline of :focus-visible. Track and thumb
106
+ fills are written on the properties, not only the hooks: an application
107
+ hook override at the same specificity loads after this file and would win
108
+ the variable, leaving a Canvas-remapped fill. */
104
109
  @media (forced-colors: active) {
105
110
  .range {
106
111
  --range-track-bg: CanvasText;
107
112
  --range-thumb-bg: ButtonFace;
108
113
  }
109
114
 
115
+ .range::-webkit-slider-runnable-track {
116
+ background: CanvasText;
117
+ }
118
+
119
+ .range::-moz-range-track {
120
+ background: CanvasText;
121
+ }
122
+
110
123
  .range::-webkit-slider-thumb {
124
+ background: ButtonFace;
111
125
  border: var(--border-width) solid ButtonText;
112
126
  }
113
127
 
114
128
  .range::-moz-range-thumb {
129
+ background: ButtonFace;
115
130
  border: var(--border-width) solid ButtonText;
116
131
  }
117
132
  }
@@ -128,9 +128,8 @@
128
128
  }
129
129
 
130
130
  .switch:focus-visible {
131
- --switch-border: var(--focus);
132
- outline: 2px solid transparent;
133
- box-shadow: var(--focus-ring-shadow);
131
+ outline: var(--focus-ring-width) solid var(--focus);
132
+ outline-offset: var(--focus-outline-offset);
134
133
  }
135
134
 
136
135
  .switch:disabled {
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Docs: docs/pages/layout/app-layout.md
9
9
  *
10
- * Public hook:
10
+ * Public hooks:
11
11
  * --app-nav-side-size
12
12
  */
13
13
  .app-layout {