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
package/dist/actual.css DELETED
@@ -1,876 +0,0 @@
1
- /* Core — the minimal Actual language. Every module depends on this order. */
2
- /* Mild reset: useful defaults without classless magic. */
3
- *,
4
- *::before,
5
- *::after {
6
- box-sizing: border-box;
7
- }
8
-
9
- html {
10
- -webkit-text-size-adjust: 100%;
11
- }
12
-
13
- body {
14
- margin: 0;
15
- }
16
-
17
- button,
18
- input,
19
- textarea,
20
- select {
21
- font: inherit;
22
- }
23
-
24
- [hidden]:not([hidden="until-found"]) {
25
- display: none !important;
26
- }
27
-
28
- :where(img, svg, video) {
29
- max-inline-size: 100%;
30
- block-size: auto;
31
- }
32
-
33
- @media (prefers-reduced-motion: reduce) {
34
- html {
35
- scroll-behavior: auto !important;
36
- }
37
-
38
- *,
39
- *::before,
40
- *::after {
41
- animation-duration: 0.01ms !important;
42
- animation-delay: 0s !important;
43
- animation-iteration-count: 1 !important;
44
- transition-duration: 0.01ms !important;
45
- transition-delay: 0s !important;
46
- }
47
- }
48
-
49
- :root {
50
- /*
51
- * Global framework tokens.
52
- *
53
- * Public: properties in the named groups below are public API (typography,
54
- * spacing, shape, motion, focus, controls, viewport, z-index, icons) unless
55
- * a group is explicitly marked Internal or Derived.
56
- *
57
- * Derived: "Theme-derived aliases" and "State tokens" reference intent and
58
- * surface tokens and follow the theme; do not hard-code values into them.
59
- * Internal: indicator offsets, --disabled-opacity and the like are
60
- * consumed by components, not authored.
61
- *
62
- * Component-local hooks and internal state relays live with their
63
- * components. See docs/pages/foundations/tokens.md for the public/internal
64
- * token contract.
65
- */
66
-
67
- /* Typography */
68
- --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
69
- --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
70
- --font-width: 100%;
71
- --font-width-dense: 95%;
72
- --line-height: 1.5;
73
- --line-height-tight: 1.25;
74
- --line-height-relaxed: 1.75;
75
- --font-weight-normal: 400;
76
- --font-weight-medium: 500;
77
- --font-weight-semibold: 600;
78
- --font-weight-bold: 700;
79
-
80
- /* Typography scale */
81
- --font-size-xs: 0.8125rem;
82
- --font-size-sm: 0.875rem;
83
- --font-size-md: 1rem;
84
- --font-size-lg: 1.125rem;
85
-
86
- /* Spacing — official steps are named in tens so the scale stays open.
87
- Actual CSS will never define a --space-* that is not a multiple of 10:
88
- the intermediate names (--space-15, --space-45…) are reserved for
89
- applications, and no future framework step can collide with them. */
90
- --gap: 0.75rem;
91
- --density-space: var(--space-40);
92
- --space-10: 0.25rem;
93
- --space-20: 0.5rem;
94
- --space-30: 0.75rem;
95
- --space-40: 1rem;
96
- --space-50: 1.5rem;
97
- --space-60: 2rem;
98
-
99
- /* Shape */
100
- --border-width: 1px;
101
- --radius-full: 9999px;
102
- --shadow: 0 1px 3px rgb(0 0 0 / 0.05), 0 4px 12px rgb(0 0 0 / 0.04);
103
- --shadow-popout: 0 0.25rem 0.75rem rgb(0 0 0 / 0.15);
104
-
105
- /* Motion — see reset.css for the prefers-reduced-motion hard reset.
106
- --duration scales component transitions (hover/focus); the dedicated
107
- animation rates below tune one-shot open/close and decorative loops.
108
- Generic state/interaction motion uses the CSS default `ease` directly
109
- (registered nowhere). --ease-enter/--ease-exit express the character of
110
- one-shot presence changes, owned by the open/closed state rules that
111
- separate them (status-bar, drawer, modal). The pair follows the standard
112
- M3 rhythm; the more aggressive emphasized-decelerate entrance is
113
- deliberately not offered because it only pays off on 250–400ms motions,
114
- beyond this vocabulary. Toggles that share one transition list at
115
- --duration-fast (flyout, tooltip) and pure-opacity backdrops stay on
116
- `ease`: at 100ms the curve has almost no resolving time, and a scrim is
117
- not traveling anywhere. Animation policy: reset.css is the global safety
118
- net; every meaningful @keyframes is explicitly gated behind
119
- prefers-reduced-motion: no-preference; ordinary transitions rely on the
120
- reset alone. */
121
- --duration: 150ms;
122
- --duration-fast: 100ms;
123
- --duration-slow: 200ms;
124
- --duration-spin: 0.75s;
125
- --duration-shimmer: 1.2s;
126
- --ease-enter: cubic-bezier(0.2, 0, 0, 1);
127
- --ease-exit: cubic-bezier(0.4, 0, 1, 1);
128
-
129
- /* Focus ring — shared by every interactive component's :focus-visible */
130
- --focus-outline-color: currentColor;
131
- --focus-outline: calc(var(--border-width) * 2) solid var(--focus-outline-color);
132
- --focus-outline-offset: 2px;
133
- --focus-ring-width: 3px;
134
-
135
- /* Status dot positioning — used by .avatar > .badge:empty. The offset is
136
- the circle-aware inset (1 - 1/√2) / 2 ≈ 14.65%; the ring surrounds the
137
- dot with the parent surface color. */
138
- --indicator-offset: 14.65%;
139
-
140
- /* Shared backdrop — consumed by modal, drawer, and mobile flyout sheets. */
141
- --backdrop-color: rgb(0 0 0);
142
- --backdrop-opacity: 0.45;
143
- --backdrop-fill: rgb(0 0 0 / var(--backdrop-opacity));
144
-
145
- /* Bar thickness shared by .progress and .meter — two conceptual variants of
146
- the same geometry. Public: set it on the element or on an application
147
- class rather than reaching for a size variant. */
148
- --bar-height: 0.65rem;
149
-
150
- /* Controls — inputs and buttons share --control-size so heights always match.
151
- .compact/.spacious (variants.css) switch geometry without changing
152
- --control-font-size. Local .sm/.lg component sizes switch all three.
153
-
154
- Inline choices (checkbox, radio, switch) are typographic, not field-sized:
155
- they sit on a line of text, so they follow --control-font-size through
156
- --choice-control-size instead of --control-size. Being an em value in
157
- :root is deliberate — an unregistered custom property resolves its units
158
- at the element that consumes it, so one declaration tracks whatever font
159
- size the choice actually renders at.
160
-
161
- --control-pad-x is the opposite kind of token: a resolved geometry length,
162
- not an em ratio. Every part of one control must see the same horizontal
163
- distance, so it is stated in the typographic tokens rather than an em
164
- whose value would change on a descendant with its own font size (the
165
- input-icon glyph, made larger by --input-icon-size). It mirrors
166
- --control-font-size and is rebound beside it at the .sm/.lg boundaries
167
- (variants.css); declaring it only in :root would freeze the md value,
168
- because a var() inside a custom property resolves where it is declared. */
169
- --control-size-sm: 2rem;
170
- --control-size-md: 2.375rem;
171
- --control-size-lg: 2.75rem;
172
- --control-size: var(--control-size-md);
173
- --control-pad-x: var(--font-size-md);
174
- --control-font-size: var(--font-size-md);
175
- --choice-control-size: 1.125em;
176
- --disabled-opacity: 0.65;
177
- }
178
-
179
- /* Theme-derived aliases. These reference intent/surface tokens, so they must
180
- be re-declared on every theme boundary: a custom property resolves its
181
- var() references at computed-value time on the element that declares it,
182
- so an alias declared only on :root is inherited as an already-resolved
183
- value and would not follow a [data-theme] island's overridden tokens.
184
- Themes that want a distinct alias override it explicitly afterwards. */
185
- :root,
186
- [data-theme] {
187
- /* State tokens — semantic colors for selected/pressed/current and disabled.
188
- Components consume these instead of hard-wiring --primary/--text-subtle
189
- so forced-colors can override them in one place. */
190
- --state-selected: var(--primary);
191
- --state-selected-fg: var(--primary-fg);
192
- --state-disabled: var(--text-subtle);
193
-
194
- /* Status dot ring — follows the island's surface. */
195
- --indicator-ring: var(--surface);
196
- }
197
-
198
- :root {
199
- /* Variant tuning — themes may adjust generated soft surfaces without
200
- replacing the component rules. */
201
- --soft-bg-mix: 88%;
202
- --soft-border-mix: 65%;
203
- --soft-hover-alpha: 12%;
204
- /* Soft foreground: the share of raw intent in soft text. 100% keeps the
205
- pure intent color; lower values rebate it toward --text, which vivid or
206
- light intent palettes need to stay legible on their own soft surface.
207
- The default palette ships per-role --*-soft-fg hooks calibrated against
208
- the hovered soft fill instead (theme.css); this mix is the fallback a
209
- theme without hooks falls back to. */
210
- --soft-fg-mix: 100%;
211
-
212
- /* Shared icon assets. Override these URLs when a strict CSP disallows data:. */
213
- --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%238a8594' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
214
- --icon-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E");
215
- --icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='6' y1='6' x2='18' y2='18'/%3E%3Cline x1='18' y1='6' x2='6' y2='18'/%3E%3C/svg%3E");
216
- --icon-plus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='12' y1='5' x2='12' y2='19'/%3E%3Cline x1='5' y1='12' x2='19' y2='12'/%3E%3C/svg%3E");
217
-
218
- /* Viewport size tokens — fallback vh/vw with dvh/vi enhancement.
219
- Consumed by full-viewport components (app-shell, drawer, modal, flyout).
220
- Custom properties keep the fallback explicit while allowing modern units
221
- to enhance the same public tokens. */
222
- --viewport-block: 100vh;
223
- --viewport-inline: 100vw;
224
-
225
- /* Z-index scale — orders document-layer components only. Native top-layer
226
- elements, dialogs and popovers alike, do not participate in this ordering:
227
- they always paint above the document, and their order among themselves is
228
- their top-layer promotion order. No z-index value here can reach them.
229
- Actual CSS reserves multiples of 10 for framework layers; applications may
230
- use intermediate values for local layering. */
231
- --z-sticky: 10;
232
- --z-menu: 20;
233
- --z-status: 60;
234
- }
235
-
236
- @media (forced-colors: active) {
237
- :root,
238
- [data-theme] {
239
- --state-disabled: GrayText;
240
- --disabled-opacity: 1;
241
- }
242
- }
243
-
244
- @supports (block-size: 100dvh) {
245
- :root {
246
- --viewport-block: 100dvh;
247
- }
248
- }
249
-
250
- @supports (inline-size: 100vi) {
251
- :root {
252
- --viewport-inline: 100vi;
253
- }
254
- }
255
-
256
- @supports (color: color-mix(in oklch, red, white)) {
257
- :root,
258
- [data-theme] {
259
- /* Shadows derive from --shadow-color (theme.css) — re-declared on every
260
- theme boundary so islands follow their own shadow-color. */
261
- --shadow:
262
- 0 1px 3px color-mix(in oklch, var(--shadow-color) 5%, transparent),
263
- 0 4px 12px color-mix(in oklch, var(--shadow-color) 4%, transparent);
264
- --shadow-popout: 0 0.25rem 0.75rem color-mix(in oklch, var(--shadow-color) 15%, transparent);
265
- }
266
-
267
- /* Theme-derived like the shadows above, so it is declared on the same
268
- boundaries: an alias resolved only on :root is inherited already composed,
269
- and a [data-theme] island overriding --backdrop-color or
270
- --backdrop-opacity would not change the scrim of a dialog inside it. */
271
- :root,
272
- [data-theme] {
273
- --backdrop-fill: color-mix(
274
- in oklch,
275
- var(--backdrop-color) calc(var(--backdrop-opacity) * 100%),
276
- transparent
277
- );
278
- }
279
- }
280
-
281
- /*
282
- * Theme tokens — the palette, surfaces, text, borders, radius and shadow
283
- * color. Named themes (themes/*.css) override this file; authors can set the
284
- * same tokens on :root or on [data-theme="…"] islands.
285
- *
286
- * Public: Intent colors, Surfaces, Text, Lines and focus, Radius scale,
287
- * Shadow color, Focus ring derivation.
288
- * Derived: "Theme-derived aliases", Tone subsystem, Backdrop fill — reference
289
- * other tokens; hard-coded overrides on a custom theme will break the intent.
290
- * Internal: --hgroup-gap.
291
- */
292
- :root {
293
- color-scheme: light dark;
294
- }
295
-
296
- [data-theme="light"] {
297
- color-scheme: light;
298
- }
299
-
300
- [data-theme="dark"] {
301
- color-scheme: dark;
302
- }
303
-
304
- /* Dark mode is progressive enhancement via light-dark(). Older browsers get
305
- the light fallback rather than a partially dark native color scheme over
306
- light tokens. */
307
- @supports not (color: light-dark(red, blue)) {
308
- :root,
309
- [data-theme="light"],
310
- [data-theme="dark"] {
311
- color-scheme: light;
312
- }
313
- }
314
-
315
- /* Default palette — "ink & terra": aubergine primary, terracotta secondary,
316
- pigment-toned statuses (sage, ochre, brick) tuned to equal perceived weight.
317
- The neutral ramp (text, borders, subtle surfaces) is tinted toward the
318
- primary hue (~260) so the whole page carries the identity, quietly. */
319
-
320
- /* Light fallback — also the default on browsers without light-dark() support. */
321
- :root,
322
- [data-theme="light"] {
323
- /* Intent colors */
324
- --primary: hsl(268 32% 33%);
325
- --primary-fg: hsl(0 0% 98%);
326
-
327
- --secondary: hsl(16 38% 38%);
328
- --secondary-fg: hsl(0 0% 98%);
329
-
330
- --success: hsl(150 40% 31%);
331
- --success-fg: hsl(0 0% 98%);
332
-
333
- --warning: hsl(36 68% 34%);
334
- --warning-fg: hsl(0 0% 98%);
335
-
336
- --danger: hsl(5 58% 43%);
337
- --danger-fg: hsl(0 0% 98%);
338
-
339
- --neutral: hsl(258 7% 43%);
340
- --neutral-fg: hsl(0 0% 98%);
341
-
342
- /* Surfaces */
343
- --surface: hsl(0 0% 100%);
344
- --surface-raised: hsl(0 0% 100%);
345
- --surface-subtle: hsl(262 24% 96%);
346
- --surface-solid: hsl(261 16% 13%);
347
-
348
- /* Text */
349
- --text: hsl(261 20% 13%);
350
- --text-muted: hsl(259 9% 42%);
351
- --text-subtle: hsl(258 7% 57%);
352
-
353
- /* Lines and focus */
354
- --border: hsl(260 15% 88%);
355
- --focus: var(--neutral);
356
- /* Pre-color-mix fallback only — superseded below by a derivation from
357
- --focus so overriding --neutral/--focus never leaves a stale ring. */
358
- --focus-ring: hsl(258 7% 43% / 0.3);
359
- --hover-overlay: hsl(260 20% 10% / 0.04);
360
-
361
- /* Radius scale — themes may override the whole scale (e.g. [data-theme="square"]) */
362
- --radius: 0.5rem;
363
- --radius-sm: 0.25rem;
364
- --radius-lg: 0.75rem;
365
-
366
- /* Shadow color — consumed by --shadow / --shadow-popout in tokens.css */
367
- --shadow-color: hsl(260 25% 10%);
368
-
369
- /* hgroup internal gap */
370
- --hgroup-gap: 0.25em;
371
- }
372
-
373
- /* Theme-derived aliases — re-declared on every theme boundary (see tokens.css
374
- for the custom-property resolution rationale): islands recompute these from
375
- their own tokens instead of inheriting :root's resolved values. */
376
- :root,
377
- [data-theme] {
378
- --heading: var(--text);
379
- --selection-bg: var(--primary);
380
- --selection-fg: var(--primary-fg);
381
- --focus-ring-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring);
382
- }
383
-
384
- /* Soft foreground hooks — the soft ink is the intent rebated toward the local
385
- text so it keeps 4.5:1 against the soft hover fill (the resting fill carries
386
- the extra margin; variants.css soft recipe). Deliberately scoped to the
387
- default theme: a custom property bakes its var() references where it is
388
- declared, so these resolve against root/light/dark tokens on every boundary.
389
- Presets and custom islands keep their --soft-fg-mix / raw-intent ink unless
390
- they declare hooks themselves — :where() keeps this reset from outranking a
391
- theme's own declaration (e.g. brutalist's 45% soft-fg-mix). */
392
- :where([data-theme]:not([data-theme="light"]):not([data-theme="dark"])) {
393
- --primary-soft-fg: initial;
394
- --secondary-soft-fg: initial;
395
- --success-soft-fg: initial;
396
- --warning-soft-fg: initial;
397
- --danger-soft-fg: initial;
398
- --neutral-soft-fg: initial;
399
- }
400
-
401
- :root,
402
- [data-theme="light"],
403
- [data-theme="dark"] {
404
- --secondary-soft-fg: color-mix(in oklab, var(--text) 10%, var(--secondary) 90%);
405
- --success-soft-fg: color-mix(in oklab, var(--text) 15%, var(--success) 85%);
406
- --warning-soft-fg: color-mix(in oklab, var(--text) 25%, var(--warning) 75%);
407
- --danger-soft-fg: color-mix(in oklab, var(--text) 15%, var(--danger) 85%);
408
- --neutral-soft-fg: color-mix(in oklab, var(--text) 20%, var(--neutral) 80%);
409
- }
410
-
411
- /* Native platform controls (checkbox, radio, range, progress) that are not
412
- restyled by a component class still follow the theme accent. Declared on
413
- every island: accent-color inherits as a computed value, so islands must
414
- recompute it from their own --primary (same resolution rule as above).
415
- Unsupported browsers ignore it; forced-colors keeps browser control. */
416
- :root,
417
- [data-theme] {
418
- accent-color: var(--primary);
419
- }
420
-
421
- /* Tone subsystem — fixed-backdrop foreground for components placed over media
422
- or other surfaces outside the theme token stack. Use data-theme islands for
423
- full dark/light subtree inversion. */
424
- [data-tone="dark"] {
425
- --tone-fg: white;
426
- }
427
-
428
- [data-tone="light"] {
429
- --tone-fg: var(--text);
430
- }
431
-
432
- /* Focus ring derivation — color-mix tier. Runs whether or not light-dark()
433
- is also supported, so it covers the color-mix-without-light-dark browser
434
- tier on its own; the light-dark block below refines it further with the
435
- light/dark alpha split once both features are available. Declared on every
436
- [data-theme] island (not just the built-in light/dark pair) so custom
437
- islands recolor the ring for free; a preset that re-declares --focus-ring
438
- later in source order intentionally pins its own ring instead. */
439
- @supports (color: color-mix(in oklch, red, white)) {
440
- :root,
441
- [data-theme] {
442
- --focus-ring: color-mix(in oklch, var(--focus) 30%, transparent);
443
- }
444
- }
445
-
446
- /* Auto dark mode via light-dark(). Resolves based on the element's color-scheme:
447
- :root (light dark) follows the OS, [data-theme="light"] forces light,
448
- [data-theme="dark"] forces dark by overriding color-scheme.
449
- Dark pastels sit a little lower and more chromatic (L 70-76, sat 38-45)
450
- than usual so the palette stays warm rather than washed gray. */
451
- @supports (color: light-dark(red, blue)) {
452
- :root,
453
- [data-theme="light"] {
454
- --primary: light-dark(hsl(268 32% 33%), hsl(268 42% 76%));
455
- --primary-fg: light-dark(hsl(0 0% 98%), hsl(268 30% 12%));
456
-
457
- --secondary: light-dark(hsl(16 38% 38%), hsl(18 45% 72%));
458
- --secondary-fg: light-dark(hsl(0 0% 98%), hsl(18 32% 11%));
459
-
460
- --success: light-dark(hsl(150 40% 31%), hsl(150 38% 62%));
461
- --success-fg: light-dark(hsl(0 0% 98%), hsl(150 35% 8%));
462
-
463
- --warning: light-dark(hsl(36 68% 34%), hsl(40 62% 64%));
464
- --warning-fg: light-dark(hsl(0 0% 98%), hsl(38 35% 10%));
465
-
466
- --danger: light-dark(hsl(5 58% 43%), hsl(4 60% 70%));
467
- --danger-fg: light-dark(hsl(0 0% 98%), hsl(4 35% 10%));
468
-
469
- --neutral: light-dark(hsl(258 7% 43%), hsl(258 10% 72%));
470
- --neutral-fg: light-dark(hsl(0 0% 98%), hsl(260 18% 10%));
471
-
472
- --surface: light-dark(hsl(0 0% 100%), hsl(263 17% 9%));
473
- --surface-raised: light-dark(hsl(0 0% 100%), hsl(263 15% 12%));
474
- --surface-subtle: light-dark(hsl(262 24% 96%), hsl(263 13% 16%));
475
- --surface-solid: light-dark(hsl(261 16% 13%), hsl(0 0% 96%));
476
-
477
- --text: light-dark(hsl(261 20% 13%), hsl(260 12% 95%));
478
- --text-muted: light-dark(hsl(259 9% 42%), hsl(259 9% 67%));
479
- --text-subtle: light-dark(hsl(258 7% 57%), hsl(258 7% 54%));
480
-
481
- --border: light-dark(hsl(260 15% 88%), hsl(262 12% 24%));
482
- --focus-ring: light-dark(hsl(258 7% 43% / 0.3), hsl(258 10% 72% / 0.28));
483
- --hover-overlay: light-dark(hsl(260 20% 10% / 0.04), hsl(0 0% 100% / 0.06));
484
- --shadow-color: light-dark(hsl(260 25% 10%), hsl(262 40% 2%));
485
- }
486
- }
487
-
488
- @supports (color: color-mix(in oklch, red, white)) and (color: light-dark(red, blue)) {
489
- :root,
490
- [data-theme] {
491
- --focus-ring: light-dark(
492
- color-mix(in oklch, var(--focus) 30%, transparent),
493
- color-mix(in oklch, var(--focus) 28%, transparent)
494
- );
495
- }
496
- }
497
-
498
- @media (prefers-contrast: more) {
499
- :root,
500
- /* The repeated attribute intentionally outranks single-attribute preset
501
- palettes, including a preset stylesheet loaded after the core. */
502
- [data-theme][data-theme] {
503
- --border-width: 2px;
504
- --focus-outline-offset: 3px;
505
- --focus-ring-width: 4px;
506
- --text-muted: var(--text);
507
- --text-subtle: var(--text);
508
- }
509
- }
510
-
511
- /*
512
- * Base element styles for native HTML semantics.
513
- *
514
- * Sits between reset (browser defaults) and prose (rich content scope).
515
- * These styles apply everywhere — cards, dialogs, heroes — not only in prose.
516
- *
517
- * Public hooks:
518
- * --link
519
- * --hr-space
520
- * --font-width / --font-width-dense (see variants.css for the dense application)
521
- */
522
-
523
- body {
524
- background: var(--surface);
525
- color: var(--text);
526
- font-family: var(--font-sans);
527
- font-stretch: var(--font-width);
528
- line-height: var(--line-height);
529
- }
530
-
531
- /* Bare links inherit their ink by default, so nothing changes unless a theme
532
- opts in by setting --link. Because this is an element selector, any component
533
- rule that colors its own anchors still wins. */
534
- a {
535
- color: var(--link, inherit);
536
- text-underline-offset: 0.18em;
537
- }
538
-
539
- a:hover {
540
- text-decoration-thickness: 0.12em;
541
- }
542
-
543
- /* Global heading chrome: color (--heading or inherit), a tight line-height,
544
- and progressive text-wrap. No font-size or font-weight is set globally —
545
- components and .prose own the per-context scale. */
546
- :where(h1, h2, h3, h4, h5, h6) {
547
- color: var(--heading, inherit);
548
- line-height: var(--line-height-tight);
549
- text-wrap: balance;
550
- }
551
-
552
- /* Compact heading groups. Structural only: display, gap, internal margin reset.
553
- No font-size, color, or weight is applied here. */
554
- hgroup {
555
- display: grid;
556
- gap: var(--hgroup-gap);
557
- }
558
-
559
- hgroup > * {
560
- margin-block: 0;
561
- }
562
-
563
- /* Section break.
564
-
565
- margin-inline: 0 overrides the UA's `margin: 0.5em auto`. In block flow the
566
- auto margins resolve to zero and nothing changes, but as a flex or grid item
567
- an inline auto margin replaces the stretch — an <hr> in a .stack computed to
568
- width 0 and vanished. .stack deliberately leaves the inline axis free, so
569
- the reset belongs here.
570
-
571
- --hr-space tunes the block rhythm on the instance: a rule closing a page
572
- section and a rule separating two rows in a panel are the same element at
573
- very different scales. It applies to an hr in document flow; inside .stack
574
- or .prose the parent owns the spacing between siblings and wins. */
575
- hr {
576
- border: 0;
577
- border-block-start: var(--border-width) solid var(--border);
578
- margin-block: var(--hr-space, var(--space-60));
579
- margin-inline: 0;
580
- }
581
-
582
- code,
583
- kbd,
584
- samp,
585
- pre {
586
- font-family: var(--font-mono);
587
- }
588
-
589
- /* Overflow containment only. One unbreakable line — a URL, a minified token, a
590
- wide ASCII table — otherwise widens the whole page, and an author cannot see
591
- it coming from the markup. Background, padding and radius stay out: those
592
- belong to .prose or to an application code-block component. */
593
- pre {
594
- overflow-x: auto;
595
- }
596
-
597
- small {
598
- font-size: 0.875em;
599
- }
600
-
601
- ::selection {
602
- background: var(--selection-bg, var(--primary));
603
- color: var(--selection-fg, var(--primary-fg));
604
- }
605
-
606
- /*
607
- * Intent classes do not style elements directly.
608
- * They only expose --intent, --intent-fg, and the optional per-intent soft
609
- * foreground --intent-soft-fg, relayed from the per-role --*-soft-fg hooks.
610
- * The default theme declares those hooks for its soft ink (see theme.css);
611
- * a theme without them lets the relay resolve to an invalid value, so the soft
612
- * recipe falls back to its --soft-fg-mix derivation and a theme opts in one
613
- * role at a time (docs/pages/foundations/tokens.md). Components and variants
614
- * decide how to consume them. Each component that consumes --intent,
615
- * --intent-soft-fg or --ui-* opens a local intent boundary in a :where() block
616
- * that resets exactly the tokens it consumes to initial. Parent intents do not
617
- * leak into nested components. Local intent classes (.primary, .danger, etc.)
618
- * still apply because they have higher specificity than :where().
619
- */
620
- .primary {
621
- --intent: var(--primary);
622
- --intent-fg: var(--primary-fg);
623
- --intent-soft-fg: var(--primary-soft-fg);
624
- }
625
- .secondary {
626
- --intent: var(--secondary);
627
- --intent-fg: var(--secondary-fg);
628
- --intent-soft-fg: var(--secondary-soft-fg);
629
- }
630
- .success {
631
- --intent: var(--success);
632
- --intent-fg: var(--success-fg);
633
- --intent-soft-fg: var(--success-soft-fg);
634
- }
635
- .warning {
636
- --intent: var(--warning);
637
- --intent-fg: var(--warning-fg);
638
- --intent-soft-fg: var(--warning-soft-fg);
639
- }
640
- .danger {
641
- --intent: var(--danger);
642
- --intent-fg: var(--danger-fg);
643
- --intent-soft-fg: var(--danger-soft-fg);
644
- }
645
- .neutral {
646
- --intent: var(--neutral);
647
- --intent-fg: var(--neutral-fg);
648
- --intent-soft-fg: var(--neutral-soft-fg);
649
- }
650
-
651
- /* Paint an inherited or local intent on a simple foreground element without
652
- turning the intent classes into general-purpose color utilities. */
653
- .intent-color {
654
- color: var(--intent);
655
- }
656
-
657
- /*
658
- * Cross-cutting variant token mapping.
659
- *
660
- * Variant scope: .solid/.soft/.outline/.surface set --ui-* for components that
661
- * opt into shared variants. .inverted is a surface modifier, not a variant: it paints
662
- * the element directly (background/color/border-color) so it works on any bare
663
- * block, and remaps the shared --ui-* tokens so participating components
664
- * (.card, .navbar, .alert, .badge) invert through their own rules. Loaded
665
- * before components, so a component with its own explicit surface (e.g.
666
- * .accordion) naturally keeps that surface — .inverted never forces a
667
- * non-participant.
668
- * Component-specific defaults (.btn solid-by-default, .badge
669
- * soft-by-default, .alert soft-by-default) live with their components:
670
- * components/button.css — .btn default solid + .btn.ghost + .btn.link
671
- * components/badge.css — .badge default soft behind explicit --ui-*
672
- * components/alert.css — .alert default soft behind explicit --ui-*
673
- *
674
- * .compact/.spacious set inherited density tokens: spacing rhythm and geometry
675
- * only, plus a slight typographic width bonus. Density never changes font size
676
- * or icon size. Controls consume --control-size; non-control components may
677
- * consume --density-* when the context should affect their local rhythm.
678
- * .compact applies --font-width-dense through font-stretch (a variable wdth
679
- * axis when available, otherwise normal width-face matching); layout must
680
- * never depend on the narrowing being available. .spacious restores
681
- * --font-width, so a spacious island inside a compact context returns to the
682
- * normal width exactly as it restores its own gap, spacing, and control size.
683
- * Themes whose typeface has no useful width axis opt out with
684
- * --font-width-dense: 100%.
685
- *
686
- * .sm/.lg are local size roles, not shared measurements. Shared control
687
- * participants below map both typography and geometry; components with
688
- * bespoke geometry (badge, spinner, avatar, rating, key, prose) own those
689
- * mappings locally. Components without an explicit mapping do not participate.
690
- *
691
- * Intent list source of truth: intents.css. Components consume --intent* tokens
692
- * instead of re-listing intent classes.
693
- */
694
- .solid {
695
- --ui-bg: var(--intent, var(--neutral));
696
- --ui-fg: var(--intent-fg, var(--neutral-fg));
697
- --ui-border: transparent;
698
- }
699
-
700
- .soft {
701
- --ui-bg: var(--surface-subtle);
702
- --ui-border: var(--border);
703
- --ui-fg: var(--intent, var(--text));
704
- }
705
-
706
- .outline {
707
- --ui-bg: transparent;
708
- --ui-fg: var(--intent, currentColor);
709
- --ui-border: var(--intent, currentColor);
710
- }
711
-
712
- /* Neutral chrome, intent only in the ink: the page surface with the theme
713
- border. .soft is the intent-tinted treatment (its baseline fallback uses
714
- --surface-subtle) and .outline has no fill at all, so neither can express
715
- it. Deliberately literal — no color-mix enhancement — so this stays the one
716
- variant that reads exactly as the theme's own surface. */
717
- .surface {
718
- --ui-bg: var(--surface);
719
- --ui-fg: var(--intent, var(--text));
720
- --ui-border: var(--border);
721
- }
722
-
723
- /* Inverse surface treatment. Paints the element directly (so it works on any
724
- bare block) and remaps the shared --ui-* tokens so opt-in components
725
- (.card, .navbar) invert through their own rules. Because this loads before
726
- component base surfaces, components with their own explicit surface
727
- (e.g. .accordion) keep it — participation is opt-in, never forced. --heading
728
- is set so global headings (var(--heading, inherit)) do not inherit an outside
729
- heading color onto the dark surface; surface-owning descendants derive
730
- their contextual aliases from their own local surface. */
731
- .inverted {
732
- --ui-bg: var(--surface-solid);
733
- --ui-fg: var(--surface);
734
- --ui-border: transparent;
735
- --ui-hover-bg: var(--surface-solid);
736
-
737
- --heading: var(--surface);
738
- --busy-overlay-bg: var(--surface-solid);
739
-
740
- background: var(--ui-bg);
741
- color: var(--ui-fg);
742
- border-color: var(--ui-border);
743
- }
744
-
745
- .compact {
746
- --gap: var(--space-20);
747
- --density-space: var(--space-20);
748
- --control-size: var(--control-size-sm);
749
- font-stretch: var(--font-width-dense);
750
- }
751
-
752
- .spacious {
753
- --gap: var(--space-50);
754
- --density-space: var(--space-50);
755
- --control-size: var(--control-size-lg);
756
- font-stretch: var(--font-width);
757
- }
758
-
759
- /* Shared control scale. Wrappers are listed when their descendants form one
760
- component (field, choice-card, floating-field, input-icon, join, pagination,
761
- tabs, flyout). A local size declaration beats inherited density because it
762
- is declared on the sized component itself. */
763
- :where(
764
- .btn,
765
- .input,
766
- .textarea,
767
- .select,
768
- .color,
769
- .file,
770
- .field,
771
- .choice,
772
- .choice-card,
773
- .input-icon,
774
- .floating-field,
775
- .otp,
776
- .join,
777
- .pagination,
778
- .tabs,
779
- .tab,
780
- .flyout
781
- ).sm {
782
- --control-size: var(--control-size-sm);
783
- --control-font-size: var(--font-size-sm);
784
- --control-pad-x: var(--font-size-sm);
785
- }
786
-
787
- :where(
788
- .btn,
789
- .input,
790
- .textarea,
791
- .select,
792
- .color,
793
- .file,
794
- .field,
795
- .choice,
796
- .choice-card,
797
- .input-icon,
798
- .floating-field,
799
- .otp,
800
- .join,
801
- .pagination,
802
- .tabs,
803
- .tab,
804
- .flyout
805
- ).lg {
806
- --control-size: var(--control-size-lg);
807
- --control-font-size: var(--font-size-lg);
808
- --control-pad-x: var(--font-size-lg);
809
- }
810
-
811
- /* Modern color enhancement. Baseline variant values above remain usable
812
- everywhere. Interactive states live in the components that consume them. */
813
- @supports (color: color-mix(in oklch, red, white)) {
814
- .soft {
815
- /* @sync soft-recipe */
816
- --ui-bg: color-mix(in oklab, var(--surface) var(--soft-bg-mix), var(--intent, var(--neutral)));
817
- --ui-border: color-mix(
818
- in oklab,
819
- var(--surface) var(--soft-border-mix),
820
- var(--intent, var(--neutral))
821
- );
822
- --ui-fg: var(
823
- --intent-soft-fg,
824
- color-mix(in oklab, var(--text), var(--intent, var(--text)) var(--soft-fg-mix))
825
- );
826
- /* @sync soft-recipe */
827
- }
828
-
829
- .inverted {
830
- --ui-hover-bg: color-mix(in oklch, var(--surface-solid) 88%, black);
831
- }
832
- }
833
-
834
- /* Interactive hover recipes live in the components that consume them:
835
- components/button.css and components/badge.css. */
836
-
837
- /* Focus baseline — a visible outline is the guaranteed focus affordance. The
838
- :focus outline covers legacy browsers; modern browsers hide it for
839
- pointer/mouse focus and interactive components layer their ring on top.
840
-
841
- Invariant: an interactive component must never rely on box-shadow alone
842
- for its keyboard focus indicator. Components that remove the native
843
- outline on :focus-visible replace it with a transparent outline — invisible
844
- normally, forced to the system focus color under forced-colors. */
845
- :focus {
846
- outline: var(--focus-outline);
847
- outline-offset: var(--focus-outline-offset);
848
- }
849
-
850
- @supports selector(:focus-visible) {
851
- :focus:not(:focus-visible) {
852
- outline: none;
853
- }
854
- }
855
-
856
- @media print {
857
- :root,
858
- /* The doubled attribute intentionally outranks single-attribute preset
859
- islands (data-theme="dark", paired themes) loaded after the core. */
860
- [data-theme][data-theme],
861
- body {
862
- /* Save some ink in dark mode */
863
- color-scheme: light;
864
- background: #fff;
865
- color: #000;
866
- }
867
-
868
- h1,
869
- h2,
870
- h3,
871
- h4,
872
- h5,
873
- h6 {
874
- break-after: avoid;
875
- }
876
- }