actual-css 0.4.1 → 0.5.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 (74) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +2 -2
  3. package/dist/actual.css +53 -14
  4. package/dist/actual.full.css +300 -91
  5. package/dist/actual.full.js +1 -1
  6. package/dist/actual.full.min.css +1 -1
  7. package/dist/actual.min.css +1 -1
  8. package/docs/design-notes/spacing-units.md +286 -0
  9. package/docs/navigation.json +1 -0
  10. package/docs/pages/components/alert.md +1 -1
  11. package/docs/pages/components/app-navigation.md +2 -0
  12. package/docs/pages/components/badge.md +17 -3
  13. package/docs/pages/components/button.md +9 -2
  14. package/docs/pages/components/card.md +13 -0
  15. package/docs/pages/components/data-list.md +72 -0
  16. package/docs/pages/components/dialog.md +2 -0
  17. package/docs/pages/components/drawer.md +2 -0
  18. package/docs/pages/components/fab.md +2 -0
  19. package/docs/pages/components/meter.md +56 -1
  20. package/docs/pages/components/progress.md +10 -0
  21. package/docs/pages/components/skeleton.md +2 -0
  22. package/docs/pages/components/steps.md +10 -2
  23. package/docs/pages/components/table.md +1 -0
  24. package/docs/pages/enhancements/flyout.md +2 -0
  25. package/docs/pages/enhancements/status-bar.md +10 -1
  26. package/docs/pages/examples/overview.md +2 -1
  27. package/docs/pages/forms/joined-controls.md +2 -0
  28. package/docs/pages/forms/overview.md +1 -1
  29. package/docs/pages/forms/range.md +2 -0
  30. package/docs/pages/forms/states-and-sizes.md +1 -1
  31. package/docs/pages/forms/validation.md +2 -0
  32. package/docs/pages/foundations/philosophy.md +11 -1
  33. package/docs/pages/foundations/tokens.md +7 -3
  34. package/docs/pages/foundations/typography.md +10 -2
  35. package/docs/pages/guides/introduction.md +1 -1
  36. package/docs/pages/layout/scroll-snap.md +2 -0
  37. package/docs/pages/layout/sidebar-layout.md +2 -0
  38. package/docs/pages/layout/topbar.md +2 -0
  39. package/docs/pages/utilities/accessibility.md +18 -0
  40. package/package.json +1 -1
  41. package/scripts/reserved-classes.json +4 -0
  42. package/src/css/components/actions.css +1 -0
  43. package/src/css/components/breadcrumb.css +1 -0
  44. package/src/css/components/data-list.css +27 -0
  45. package/src/css/components/drawer.css +30 -11
  46. package/src/css/components/flyout.css +17 -12
  47. package/src/css/components/index.css +1 -0
  48. package/src/css/components/meter.css +28 -1
  49. package/src/css/components/modal.css +28 -11
  50. package/src/css/components/navbar.css +2 -0
  51. package/src/css/components/pagination.css +1 -0
  52. package/src/css/components/progress.css +5 -0
  53. package/src/css/components/status-bar.css +31 -15
  54. package/src/css/components/steps.css +36 -9
  55. package/src/css/components/surface.css +5 -3
  56. package/src/css/components/tab.css +1 -0
  57. package/src/css/components/tooltip.css +5 -8
  58. package/src/css/core/base.css +7 -1
  59. package/src/css/core/intents.css +6 -0
  60. package/src/css/core/tokens.css +23 -9
  61. package/src/css/core/variants.css +17 -4
  62. package/src/css/forms/form-actions.css +1 -0
  63. package/src/css/forms/form.css +4 -4
  64. package/src/css/layout/cluster.css +1 -0
  65. package/src/css/layout/scroll-snap.css +1 -0
  66. package/src/css/layout/sidebar-layout.css +1 -0
  67. package/src/css/layout/switcher.css +1 -0
  68. package/src/css/layout/topbar.css +1 -0
  69. package/src/css/typography/prose.css +2 -3
  70. package/src/css/utilities/base.css +16 -0
  71. package/src/js/selectors.js +1 -0
  72. package/src/js/status.js +88 -7
  73. package/src/js/surface.js +2 -11
  74. package/src/js/transition.js +20 -0
@@ -39,12 +39,14 @@ dialog.modal {
39
39
  * Keep the open dialog root untransformed. Fixed overlays mounted inside a
40
40
  * modal dialog use viewport coordinates; transform/translate/scale on this
41
41
  * element would make them position against the dialog instead. The closed
42
- * state can still carry a small transform for enter/exit motion.
42
+ * state can still carry a small transform for enter/exit motion. The lift
43
+ * is a pure vertical offset: scaling a wide panel around its center drags
44
+ * both edges, which reads as horizontal movement.
43
45
  */
44
- transform: translateY(0.35rem) scale(0.98);
46
+ transform: translateY(0.35rem);
45
47
  transition:
46
- opacity var(--duration),
47
- transform var(--duration);
48
+ opacity var(--duration) var(--ease-exit),
49
+ transform var(--duration) var(--ease-exit);
48
50
  }
49
51
 
50
52
  dialog.modal > form,
@@ -163,33 +165,46 @@ dialog.modal.dialog-confirmation > form > footer {
163
165
  dialog.modal[open] {
164
166
  opacity: 1;
165
167
  transform: none;
168
+ transition:
169
+ opacity var(--duration) var(--ease-enter),
170
+ transform var(--duration) var(--ease-enter);
166
171
  }
167
172
 
168
173
  @starting-style {
169
174
  dialog.modal[open] {
170
175
  opacity: 0;
171
- transform: translateY(0.35rem) scale(0.98);
176
+ transform: translateY(0.35rem);
172
177
  }
173
178
  }
174
179
 
175
180
  dialog.modal::backdrop {
176
181
  background: var(--backdrop-fill);
177
182
  opacity: 0;
178
- transition: opacity var(--duration);
183
+ /* Scrim curve stays neutral `ease`; only the panel carries the character
184
+ curves. */
185
+ transition: opacity var(--duration) ease;
179
186
  }
180
187
 
181
188
  @supports (transition-behavior: allow-discrete) {
182
189
  dialog.modal {
183
190
  transition:
184
- opacity var(--duration),
185
- transform var(--duration),
191
+ opacity var(--duration) var(--ease-exit),
192
+ transform var(--duration) var(--ease-exit),
193
+ overlay var(--duration) allow-discrete,
194
+ display var(--duration) allow-discrete;
195
+ }
196
+
197
+ dialog.modal[open] {
198
+ transition:
199
+ opacity var(--duration) var(--ease-enter),
200
+ transform var(--duration) var(--ease-enter),
186
201
  overlay var(--duration) allow-discrete,
187
202
  display var(--duration) allow-discrete;
188
203
  }
189
204
 
190
205
  dialog.modal::backdrop {
191
206
  transition:
192
- opacity var(--duration),
207
+ opacity var(--duration) ease,
193
208
  overlay var(--duration) allow-discrete,
194
209
  display var(--duration) allow-discrete;
195
210
  }
@@ -214,13 +229,15 @@ html.has-modal-open.had-scrollbar {
214
229
  }
215
230
 
216
231
  @media (prefers-reduced-motion: no-preference) {
232
+ /* Wrong-close feedback and view-transition transport are motion quirks, not
233
+ presence enter/exit, so they stay on the plain CSS default easing. */
217
234
  dialog.modal.is-static {
218
- animation: actual-dialog-static var(--duration-slow) var(--ease);
235
+ animation: actual-dialog-static var(--duration-slow) ease;
219
236
  }
220
237
 
221
238
  ::view-transition-group(actual-dialog) {
222
239
  animation-duration: var(--duration);
223
- animation-timing-function: var(--ease);
240
+ animation-timing-function: ease;
224
241
  }
225
242
  }
226
243
 
@@ -18,6 +18,7 @@
18
18
 
19
19
  .navbar {
20
20
  display: flex;
21
+ flex-direction: row;
21
22
  flex-wrap: wrap;
22
23
  align-items: center;
23
24
  justify-content: space-between;
@@ -45,6 +46,7 @@
45
46
 
46
47
  .navbar-nav {
47
48
  display: flex;
49
+ flex-direction: row;
48
50
  flex-wrap: wrap;
49
51
  align-items: center;
50
52
  gap: var(--space-10);
@@ -10,6 +10,7 @@
10
10
  */
11
11
  .pagination {
12
12
  display: flex;
13
+ flex-direction: row;
13
14
  flex-wrap: wrap;
14
15
  align-items: center;
15
16
  gap: var(--space-10);
@@ -4,9 +4,14 @@
4
4
  * Consumes the shared --intent/--primary for the value bar and --bar-height
5
5
  * for thickness. Indeterminate state animates a shimmer using --duration-shimmer.
6
6
  *
7
+ * Thickness is a dimension, not a density step: the bar always spans its
8
+ * container and only gets thinner or thicker, so --bar-height is the API and
9
+ * .sm/.lg are deliberately not supported.
10
+ *
7
11
  * Public hooks:
8
12
  * --progress-track
9
13
  * --progress-value
14
+ * --bar-height
10
15
  */
11
16
  /* Intent boundary — inherited intents do not tint this component.
12
17
  Local intent classes still win because :where() has zero specificity. */
@@ -1,9 +1,12 @@
1
1
  /*
2
2
  * Status bar — singleton floating status area.
3
3
  *
4
- * One element in the HTML, empty by default, hidden when :empty but kept in the
5
- * accessibility tree so it stays a reliable live region. JavaScript only sets
6
- * text content; the visual shows and hides with the content.
4
+ * One element in the HTML, empty by default. .is-open is the visual state, set
5
+ * by status.js like any other surface: it lands after the message is written
6
+ * and lifts before the bar is cleared, so the bar leaves in the exact state it
7
+ * was shown in — same text, same width, same intent color — instead of snapping
8
+ * to a neutral empty pill mid-exit. Emptying the text is the *end* of the exit,
9
+ * not its trigger.
7
10
  *
8
11
  * Consumes intent like badge or alert. No need for status-bar-specific modifier
9
12
  * classes — .danger / .success / .warning / .neutral from intents.css drive
@@ -17,6 +20,7 @@
17
20
  * Internal:
18
21
  * --status-bg
19
22
  * --status-fg
23
+ * --status-viewport-offset (written by status.js while the bar is open)
20
24
  */
21
25
 
22
26
  /* Intent boundary — inherited intents do not tint this component.
@@ -31,7 +35,9 @@
31
35
  --status-fg: var(--intent-fg, var(--surface));
32
36
 
33
37
  position: fixed;
34
- inset-block-end: var(--space-40);
38
+ /* Fixed positioning tracks the layout viewport, which the mobile software
39
+ keyboard covers. status.js writes the occluded band into the offset. */
40
+ inset-block-end: calc(var(--space-40) + var(--status-viewport-offset, 0px));
35
41
  inset-inline: var(--space-40);
36
42
  z-index: var(--z-status);
37
43
 
@@ -53,22 +59,32 @@
53
59
  color: var(--status-fg);
54
60
  box-shadow: var(--shadow-popout);
55
61
 
56
- opacity: 1;
57
- scale: 1;
58
- translate: 0 0;
62
+ /* Closed by default. Opacity, not visibility or display: the bar has to stay
63
+ in the accessibility tree to work as a live region. */
64
+ opacity: 0;
65
+ translate: 0 var(--space-30);
59
66
  pointer-events: none;
60
67
 
68
+ /* Exit is shorter than entry so dismissed feedback gets out of the way.
69
+ background stays a shorthand so --status-bg still accepts gradients;
70
+ solid colors expose an interpolable computed background-color. The
71
+ intent color follows the state/interaction `ease`, not the presence
72
+ curves. */
61
73
  transition:
62
- opacity var(--duration-slow) var(--ease),
63
- scale var(--duration-slow) var(--ease),
64
- translate var(--duration-slow) var(--ease);
74
+ opacity var(--duration-fast) var(--ease-exit),
75
+ translate var(--duration) var(--ease-exit),
76
+ background-color var(--duration) ease;
65
77
  }
66
78
 
67
- .status-bar:empty {
68
- opacity: 0;
69
- scale: 0.96;
70
- translate: 0 var(--space-20);
71
- pointer-events: none;
79
+ .status-bar.is-open {
80
+ opacity: 1;
81
+ translate: 0 0;
82
+ /* Entry takes a little longer and decelerates, making the message legible
83
+ before its motion settles. */
84
+ transition:
85
+ opacity var(--duration) var(--ease-enter),
86
+ translate var(--duration-slow) var(--ease-enter),
87
+ background-color var(--duration) ease;
72
88
  }
73
89
  @media print {
74
90
  .status-bar {
@@ -35,6 +35,7 @@
35
35
 
36
36
  counter-reset: actual-step;
37
37
  display: flex;
38
+ flex-direction: row;
38
39
  inline-size: 100%;
39
40
  margin: 0;
40
41
  padding: 0;
@@ -96,27 +97,53 @@
96
97
  content: none;
97
98
  }
98
99
 
99
- .steps > :is(.complete, [aria-current="step"]) {
100
+ /* Three states must be readable without a check glyph: future stays neutral,
101
+ complete is a filled marker, current is an outlined one. Filled reads as
102
+ "done", outlined as "you are here" — the distinction survives a numbered
103
+ stepper that never sets --step-complete-mark. */
104
+ .steps > .complete {
100
105
  --step-marker-bg: var(--state-selected);
101
106
  --step-marker-border: var(--state-selected);
102
107
  --step-marker-fg: var(--state-selected-fg);
108
+ --step-line: var(--state-selected);
103
109
 
104
110
  color: var(--text);
105
111
  }
106
112
 
107
- /* Thicker border is the selected affordance in every mode — structural, and it
108
- never competes with the outline channel owned by focus. */
109
- .steps > :is(.complete, [aria-current="step"])::before {
110
- border-width: calc(var(--border-width) * 2);
113
+ /* Declared after .complete at equal specificity, and it wins on every channel
114
+ including the connector: the accented line means "already walked", so the
115
+ segment leaving the current step must stay neutral. The line reaching the
116
+ current step is owned by the previous step, which is complete. */
117
+ .steps > [aria-current="step"] {
118
+ --step-marker-bg: var(--surface);
119
+ --step-marker-border: var(--state-selected);
120
+ --step-marker-fg: var(--state-selected);
121
+ --step-line: var(--border);
122
+
123
+ color: var(--text);
111
124
  }
112
125
 
113
- .steps > .complete {
114
- --step-line: var(--state-selected);
126
+ /* Thicker border is the current affordance in every mode — structural, so it
127
+ survives forced colors, and it never competes with the outline channel owned
128
+ by focus. */
129
+ .steps > [aria-current="step"]::before {
130
+ border-width: calc(var(--border-width) * 2);
115
131
  }
116
132
 
117
133
  /* A finished step may replace its number with a mark (e.g. a check glyph):
118
134
  --step-complete-mark accepts any content value and falls back to the
119
- counter, so existing numbered steppers are unchanged. */
120
- .steps > .complete::before {
135
+ counter, so existing numbered steppers are unchanged. Excluded on the
136
+ current step so "current wins" holds on the glyph too, not only on color. */
137
+ .steps > .complete:not([aria-current="step"])::before {
121
138
  content: var(--step-complete-mark, counter(actual-step));
122
139
  }
140
+
141
+ /* Forced colors drop the accent fill, which would leave complete and future
142
+ markers identical — current keeps its thicker ring, complete needs a system
143
+ color to stay filled. Same exclusion, for the same reason. */
144
+ @media (forced-colors: active) {
145
+ .steps > .complete:not([aria-current="step"])::before {
146
+ background: Highlight;
147
+ color: HighlightText;
148
+ }
149
+ }
@@ -19,7 +19,9 @@
19
19
  z-index: calc(var(--z-menu) - 1);
20
20
  background: var(--backdrop-fill);
21
21
  opacity: 1;
22
- transition: opacity var(--duration) var(--ease);
22
+ /* A scrim never travels; it fades on the neutral `ease`. Duration matches
23
+ the flyout sheet it accompanies so it never lags the panel. */
24
+ transition: opacity var(--duration-fast) ease;
23
25
  }
24
26
 
25
27
  .surface-backdrop[hidden] {
@@ -36,8 +38,8 @@
36
38
  @supports (transition-behavior: allow-discrete) {
37
39
  .surface-backdrop {
38
40
  transition:
39
- opacity var(--duration) var(--ease),
40
- display var(--duration) allow-discrete;
41
+ opacity var(--duration-fast) ease,
42
+ display var(--duration-fast) allow-discrete;
41
43
  }
42
44
  }
43
45
  @media print {
@@ -16,6 +16,7 @@
16
16
 
17
17
  .tabs {
18
18
  display: flex;
19
+ flex-direction: row;
19
20
  flex-wrap: wrap;
20
21
  gap: var(--space-10);
21
22
  margin: 0;
@@ -32,31 +32,28 @@
32
32
  overflow-wrap: anywhere;
33
33
  pointer-events: auto;
34
34
  user-select: none;
35
+ /* Tooltips fade only: the anchor edge, marked by the arrow, must stay put
36
+ during motion, and a 4% scale was perceptible on small text. Scaling moves
37
+ the arrow tip away from its trigger on every placement. */
35
38
  opacity: 1;
36
- scale: 1;
37
- transition:
38
- opacity var(--duration-fast) var(--ease),
39
- scale var(--duration-fast) var(--ease);
39
+ transition: opacity var(--duration-fast) ease;
40
40
  }
41
41
 
42
42
  .tooltip[hidden] {
43
43
  display: none;
44
44
  opacity: 0;
45
- scale: 0.96;
46
45
  }
47
46
 
48
47
  @starting-style {
49
48
  .tooltip:not([hidden]) {
50
49
  opacity: 0;
51
- scale: 0.96;
52
50
  }
53
51
  }
54
52
 
55
53
  @supports (transition-behavior: allow-discrete) {
56
54
  .tooltip {
57
55
  transition:
58
- opacity var(--duration-fast) var(--ease),
59
- scale var(--duration-fast) var(--ease),
56
+ opacity var(--duration-fast) ease,
60
57
  display var(--duration-fast) allow-discrete;
61
58
  }
62
59
  }
@@ -3,6 +3,9 @@
3
3
  *
4
4
  * Sits between reset (browser defaults) and prose (rich content scope).
5
5
  * These styles apply everywhere — cards, dialogs, heroes — not only in prose.
6
+ *
7
+ * Public hooks:
8
+ * --link
6
9
  */
7
10
 
8
11
  body {
@@ -12,8 +15,11 @@ body {
12
15
  line-height: var(--line-height);
13
16
  }
14
17
 
18
+ /* Bare links inherit their ink by default, so nothing changes unless a theme
19
+ opts in by setting --link. Because this is an element selector, any component
20
+ rule that colors its own anchors still wins. */
15
21
  a {
16
- color: inherit;
22
+ color: var(--link, inherit);
17
23
  text-underline-offset: 0.18em;
18
24
  }
19
25
 
@@ -31,3 +31,9 @@
31
31
  --intent: var(--neutral);
32
32
  --intent-fg: var(--neutral-fg);
33
33
  }
34
+
35
+ /* Paint an inherited or local intent on a simple foreground element without
36
+ turning the intent classes into general-purpose color utilities. */
37
+ .intent-color {
38
+ color: var(--intent);
39
+ }
@@ -8,8 +8,8 @@
8
8
  *
9
9
  * Derived: "Theme-derived aliases" and "State tokens" reference intent and
10
10
  * surface tokens and follow the theme; do not hard-code values into them.
11
- * Internal: indicator offsets, --bar-height, --disabled-opacity and the
12
- * like are consumed by components, not authored.
11
+ * Internal: indicator offsets, --disabled-opacity and the like are
12
+ * consumed by components, not authored.
13
13
  *
14
14
  * Component-local hooks and internal state relays live with their
15
15
  * components. See docs/pages/foundations/tokens.md for the public/internal
@@ -33,9 +33,10 @@
33
33
  --font-size-md: 1rem;
34
34
  --font-size-lg: 1.125rem;
35
35
 
36
- /* Spacing — official steps are named in tens so the scale stays open for
37
- application themes to intercalate intermediate values (--space-15,
38
- --space-45…) without colliding with future framework steps. */
36
+ /* Spacing — official steps are named in tens so the scale stays open.
37
+ Actual CSS will never define a --space-* that is not a multiple of 10:
38
+ the intermediate names (--space-15, --space-45…) are reserved for
39
+ applications, and no future framework step can collide with them. */
39
40
  --gap: 0.75rem;
40
41
  --density-space: var(--space-40);
41
42
  --density-compact-size: 1.5rem;
@@ -53,14 +54,25 @@
53
54
  --shadow-popout: 0 0.25rem 0.75rem rgb(0 0 0 / 0.15);
54
55
 
55
56
  /* Motion — see reset.css for the prefers-reduced-motion hard reset.
56
- --duration drives component transitions (hover/focus); the dedicated
57
- animation rates below tune one-shot open/close and decorative loops. */
57
+ --duration scales component transitions (hover/focus); the dedicated
58
+ animation rates below tune one-shot open/close and decorative loops.
59
+ Generic state/interaction motion uses the CSS default `ease` directly
60
+ (registered nowhere). --ease-enter/--ease-exit express the character of
61
+ one-shot presence changes, owned by the open/closed state rules that
62
+ separate them (status-bar, drawer, modal). The pair follows the standard
63
+ M3 rhythm; the more aggressive emphasized-decelerate entrance is
64
+ deliberately not offered because it only pays off on 250–400ms motions,
65
+ beyond this vocabulary. Toggles that share one transition list at
66
+ --duration-fast (flyout, tooltip) and pure-opacity backdrops stay on
67
+ `ease`: at 100ms the curve has almost no resolving time, and a scrim is
68
+ not traveling anywhere. */
58
69
  --duration: 150ms;
59
70
  --duration-fast: 100ms;
60
71
  --duration-slow: 200ms;
61
72
  --duration-spin: 0.75s;
62
73
  --duration-shimmer: 1.2s;
63
- --ease: ease;
74
+ --ease-enter: cubic-bezier(0.2, 0, 0, 1);
75
+ --ease-exit: cubic-bezier(0.4, 0, 1, 1);
64
76
 
65
77
  /* Focus ring — shared by every interactive component's :focus-visible */
66
78
  --focus-outline-color: currentColor;
@@ -78,7 +90,9 @@
78
90
  --backdrop-opacity: 0.45;
79
91
  --backdrop-fill: rgb(0 0 0 / var(--backdrop-opacity));
80
92
 
81
- /* Progress / meter track height */
93
+ /* Bar thickness shared by .progress and .meter — two conceptual variants of
94
+ the same geometry. Public: set it on the element or on an application
95
+ class rather than reaching for a size variant. */
82
96
  --bar-height: 0.65rem;
83
97
 
84
98
  /* Controls — inputs and buttons share --control-size so heights always match.
@@ -1,8 +1,8 @@
1
1
  /*
2
2
  * Cross-cutting variant token mapping.
3
3
  *
4
- * Variant scope: .solid/.soft/.outline set --ui-* for components that opt into
5
- * shared variants. .inverted is a surface modifier, not a variant: it paints
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
6
  * the element directly (background/color/border-color) so it works on any bare
7
7
  * block, and remaps the shared --ui-* tokens so participating components
8
8
  * (.card, .navbar, .alert, .badge) invert through their own rules. Loaded
@@ -21,8 +21,10 @@
21
21
  * components may consume --density-* when the density context should affect
22
22
  * their local rhythm. Compact components may consume --density-compact-size.
23
23
  * Components with bespoke sizing (spinner, avatar) define local .sm/.lg;
24
- * applying .sm/.lg to card, dialog, accordion, navbar, table is
25
- * a no-op.
24
+ * applying .sm/.lg to card, dialog, accordion, navbar, table, progress, meter
25
+ * is a no-op. Progress and meter are deliberate: a bar keeps its inline size
26
+ * and only changes thickness, which --bar-height expresses better than a
27
+ * three-step scale.
26
28
  *
27
29
  * Intent list source of truth: intents.css. Components consume --intent* tokens
28
30
  * instead of re-listing intent classes.
@@ -45,6 +47,17 @@
45
47
  --ui-border: var(--intent, currentColor);
46
48
  }
47
49
 
50
+ /* Neutral chrome, intent only in the ink: the page surface with the theme
51
+ border. .soft is the intent-tinted treatment (its baseline fallback uses
52
+ --surface-subtle) and .outline has no fill at all, so neither can express
53
+ it. Deliberately literal — no color-mix enhancement — so this stays the one
54
+ variant that reads exactly as the theme's own surface. */
55
+ .surface {
56
+ --ui-bg: var(--surface);
57
+ --ui-fg: var(--intent, var(--text));
58
+ --ui-border: var(--border);
59
+ }
60
+
48
61
  /* Inverse surface treatment. Paints the element directly (so it works on any
49
62
  bare block) and remaps the shared --ui-* tokens so opt-in components
50
63
  (.card, .navbar) invert through their own rules. Because this loads before
@@ -17,6 +17,7 @@
17
17
  */
18
18
  .form-actions {
19
19
  display: flex;
20
+ flex-direction: row;
20
21
  flex-wrap: wrap;
21
22
  gap: var(--gap);
22
23
  align-items: var(--form-actions-align, center);
@@ -4,6 +4,10 @@
4
4
  * Individual controls keep their own files; .choice label layout lives in
5
5
  * choice.css; .form-actions lives in form-actions.css.
6
6
  *
7
+ * .field-group owns its own box only. Distance between sibling groups is a
8
+ * composition concern (.stack on the form), never a sibling margin here: the
9
+ * component cannot know whether its siblings are stacked or gridded.
10
+ *
7
11
  * Public API: .field, .field-label, .field-help, .field-error, .field-group,
8
12
  * .required-mark.
9
13
  *
@@ -24,10 +28,6 @@
24
28
  border-radius: var(--radius);
25
29
  }
26
30
 
27
- .field-group + .field-group {
28
- margin-block-start: var(--space-40);
29
- }
30
-
31
31
  .field-label,
32
32
  .field-group > legend {
33
33
  color: var(--text);
@@ -10,6 +10,7 @@
10
10
  */
11
11
  .cluster {
12
12
  display: flex;
13
+ flex-direction: row;
13
14
  flex-wrap: var(--cluster-wrap, wrap);
14
15
  align-items: var(--cluster-align, center);
15
16
  justify-content: var(--cluster-justify, flex-start);
@@ -18,6 +18,7 @@
18
18
  --scroll-snap-item-size: auto;
19
19
 
20
20
  display: flex;
21
+ flex-direction: row;
21
22
  gap: var(--scroll-snap-gap);
22
23
  overflow-x: auto;
23
24
  }
@@ -17,6 +17,7 @@
17
17
  */
18
18
  .sidebar-layout {
19
19
  display: flex;
20
+ flex-direction: row;
20
21
  flex-wrap: wrap;
21
22
  gap: var(--gap);
22
23
  align-items: flex-start;
@@ -12,6 +12,7 @@
12
12
  */
13
13
  .switcher {
14
14
  display: flex;
15
+ flex-direction: row;
15
16
  flex-wrap: wrap;
16
17
  gap: var(--gap);
17
18
  }
@@ -15,6 +15,7 @@
15
15
  z-index: var(--z-sticky);
16
16
  isolation: isolate;
17
17
  display: flex;
18
+ flex-direction: row;
18
19
  align-items: center;
19
20
  gap: var(--gap);
20
21
  padding-block: var(--space-30);
@@ -83,9 +83,8 @@
83
83
  font-size: var(--prose-h4);
84
84
  }
85
85
 
86
- /* Link ink defaults to --primary. --link is an optional escape hatch: a
87
- theme whose primary is too light or too vivid to read as inline text sets
88
- it without having to restate the rule. */
86
+ /* --link is a theme-level token (core/base.css also reads it for bare links);
87
+ prose falls back to --primary so a theme that never sets it is unaffected. */
89
88
  .prose :where(a) {
90
89
  color: var(--link, var(--primary));
91
90
  text-decoration-line: underline;
@@ -18,6 +18,22 @@
18
18
  border: 0;
19
19
  }
20
20
 
21
+ /* Enlarge a small control's touch target on touch-primary devices without
22
+ growing the visual. Opt-in — apply to genuinely small targets only. The
23
+ ::after exists only under (pointer: coarse), so on fine pointers the class
24
+ is fully neutral. */
25
+ @media (pointer: coarse) {
26
+ .touch-target {
27
+ position: relative;
28
+ }
29
+
30
+ .touch-target::after {
31
+ content: "";
32
+ position: absolute;
33
+ inset: calc(var(--space-20) * -1);
34
+ }
35
+ }
36
+
21
37
  .overflow-auto {
22
38
  overflow: auto;
23
39
  }
@@ -25,6 +25,7 @@ export const CLASSES = {
25
25
  // for aria-invalid/danger state. Optional — degrades to no-op.
26
26
  field: "field",
27
27
  // surface.js — state class on the open flyout/menu/dialog surface.
28
+ // status.js writes the same class on the status bar while a message shows.
28
29
  open: "is-open",
29
30
  // surface.js — state class while the surface renders as a mobile sheet.
30
31
  sheet: "is-sheet",