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