actual-css 0.1.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 (129) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -0
  4. package/dist/actual.css +4929 -0
  5. package/dist/actual.js +3 -0
  6. package/dist/actual.js.map +31 -0
  7. package/dist/actual.min.css +1 -0
  8. package/docs/README.md +26 -0
  9. package/docs/components.md +860 -0
  10. package/docs/design-notes/accessibility-decisions.md +32 -0
  11. package/docs/design-notes/browser-support.md +93 -0
  12. package/docs/design-notes/cascade-layer.md +52 -0
  13. package/docs/design-notes/css-anchor.md +33 -0
  14. package/docs/design-notes/naming.md +37 -0
  15. package/docs/design-notes/previous-work.md +5 -0
  16. package/docs/design-notes/references.md +324 -0
  17. package/docs/forms.md +1265 -0
  18. package/docs/javascript.md +426 -0
  19. package/docs/layout.md +480 -0
  20. package/docs/mental-model.md +73 -0
  21. package/docs/patterns.md +415 -0
  22. package/docs/reserved.md +7 -0
  23. package/docs/tokens.md +455 -0
  24. package/docs/typography.md +159 -0
  25. package/docs/ui.md +937 -0
  26. package/docs/utilities.md +384 -0
  27. package/llms.txt +17 -0
  28. package/package.json +101 -0
  29. package/src/css/actual.css +17 -0
  30. package/src/css/actual.full.css +2 -0
  31. package/src/css/actual.layer.css +3 -0
  32. package/src/css/base.css +70 -0
  33. package/src/css/components/accordion.css +97 -0
  34. package/src/css/components/actions.css +21 -0
  35. package/src/css/components/alert.css +137 -0
  36. package/src/css/components/avatar.css +199 -0
  37. package/src/css/components/badge.css +143 -0
  38. package/src/css/components/breadcrumb.css +56 -0
  39. package/src/css/components/busy.css +44 -0
  40. package/src/css/components/button.css +216 -0
  41. package/src/css/components/card.css +140 -0
  42. package/src/css/components/drawer.css +122 -0
  43. package/src/css/components/flyout.css +243 -0
  44. package/src/css/components/index.css +41 -0
  45. package/src/css/components/join.css +106 -0
  46. package/src/css/components/key.css +25 -0
  47. package/src/css/components/meter.css +81 -0
  48. package/src/css/components/modal.css +162 -0
  49. package/src/css/components/navbar.css +98 -0
  50. package/src/css/components/overline.css +49 -0
  51. package/src/css/components/pagination.css +30 -0
  52. package/src/css/components/progress.css +126 -0
  53. package/src/css/components/scrollspy.css +27 -0
  54. package/src/css/components/skeleton.css +67 -0
  55. package/src/css/components/spinner.css +60 -0
  56. package/src/css/components/status-bar.css +82 -0
  57. package/src/css/components/surface.css +48 -0
  58. package/src/css/components/tab.css +128 -0
  59. package/src/css/components/table.css +64 -0
  60. package/src/css/components/tooltip.css +107 -0
  61. package/src/css/dialog-fallback.css +56 -0
  62. package/src/css/focus.css +13 -0
  63. package/src/css/forms/choice-card.css +158 -0
  64. package/src/css/forms/choice.css +196 -0
  65. package/src/css/forms/control.css +106 -0
  66. package/src/css/forms/custom-select.css +93 -0
  67. package/src/css/forms/form-actions.css +43 -0
  68. package/src/css/forms/form.css +57 -0
  69. package/src/css/forms/index.css +15 -0
  70. package/src/css/forms/input-icon.css +36 -0
  71. package/src/css/forms/native.css +68 -0
  72. package/src/css/forms/select.css +36 -0
  73. package/src/css/forms/switch.css +127 -0
  74. package/src/css/forms/validation.css +63 -0
  75. package/src/css/grid.css +95 -0
  76. package/src/css/intents.css +33 -0
  77. package/src/css/layout.css +80 -0
  78. package/src/css/optional/index.css +5 -0
  79. package/src/css/optional/layout-extra.css +55 -0
  80. package/src/css/optional/scroll-snap.css +51 -0
  81. package/src/css/optional/scroller.css +51 -0
  82. package/src/css/optional/typography-fluid.css +48 -0
  83. package/src/css/optional/utilities-extra.css +289 -0
  84. package/src/css/print.css +47 -0
  85. package/src/css/prose.css +187 -0
  86. package/src/css/reset.css +56 -0
  87. package/src/css/theme.css +186 -0
  88. package/src/css/themes/corporate.css +60 -0
  89. package/src/css/themes/cyberpunk.css +60 -0
  90. package/src/css/themes/dark.css +32 -0
  91. package/src/css/themes/dim.css +33 -0
  92. package/src/css/themes/edge.css +143 -0
  93. package/src/css/themes/forest.css +65 -0
  94. package/src/css/themes/gradient.css +97 -0
  95. package/src/css/themes/index.css +43 -0
  96. package/src/css/themes/lavender.css +66 -0
  97. package/src/css/themes/mono.css +60 -0
  98. package/src/css/themes/neon.css +66 -0
  99. package/src/css/themes/ocean.css +60 -0
  100. package/src/css/themes/petrol.css +64 -0
  101. package/src/css/themes/spruce.css +64 -0
  102. package/src/css/themes/square.css +68 -0
  103. package/src/css/themes/sunset.css +66 -0
  104. package/src/css/tokens.css +122 -0
  105. package/src/css/utilities.css +162 -0
  106. package/src/css/variants.css +118 -0
  107. package/src/js/command.js +165 -0
  108. package/src/js/context-menu.js +291 -0
  109. package/src/js/dialog-fallback.js +163 -0
  110. package/src/js/dialog.js +503 -0
  111. package/src/js/enhance.js +208 -0
  112. package/src/js/events.js +22 -0
  113. package/src/js/filter.js +122 -0
  114. package/src/js/floating.js +462 -0
  115. package/src/js/flyout.js +187 -0
  116. package/src/js/focus.js +35 -0
  117. package/src/js/index.js +26 -0
  118. package/src/js/input.js +47 -0
  119. package/src/js/keys.js +30 -0
  120. package/src/js/mask.js +184 -0
  121. package/src/js/menu.js +88 -0
  122. package/src/js/password.js +98 -0
  123. package/src/js/scrollspy.js +121 -0
  124. package/src/js/selectors.js +59 -0
  125. package/src/js/status.js +158 -0
  126. package/src/js/surface.js +358 -0
  127. package/src/js/tab.js +173 -0
  128. package/src/js/tooltip.js +374 -0
  129. package/src/js/validation.js +419 -0
@@ -0,0 +1,4929 @@
1
+ /* Mild reset: useful defaults without classless magic. */
2
+ *,
3
+ *::before,
4
+ *::after {
5
+ box-sizing: border-box;
6
+ }
7
+
8
+ html {
9
+ -webkit-text-size-adjust: 100%;
10
+ }
11
+
12
+ body {
13
+ margin: 0;
14
+ }
15
+
16
+ button,
17
+ input,
18
+ textarea,
19
+ select {
20
+ font: inherit;
21
+ }
22
+
23
+ [hidden]:not([hidden="until-found"]) {
24
+ display: none !important;
25
+ }
26
+
27
+ /* Legacy dialog fallback only. Modern dialogs keep the browser's native closed
28
+ state so display/overlay transitions are not flattened by the reset.
29
+ The presentation for the dialog-fallback.js shim lives in the separate
30
+ dialog-fallback.css, so it can be dropped from custom bundles. */
31
+ @supports not (selector(dialog:modal)) {
32
+ dialog:not([open]) {
33
+ display: none;
34
+ }
35
+ }
36
+
37
+ :where(img, svg, video) {
38
+ max-inline-size: 100%;
39
+ block-size: auto;
40
+ }
41
+
42
+ @media (prefers-reduced-motion: reduce) {
43
+ html {
44
+ scroll-behavior: auto !important;
45
+ }
46
+
47
+ *,
48
+ *::before,
49
+ *::after {
50
+ animation-duration: 0.01ms !important;
51
+ animation-delay: 0s !important;
52
+ animation-iteration-count: 1 !important;
53
+ transition-duration: 0.01ms !important;
54
+ transition-delay: 0s !important;
55
+ }
56
+ }
57
+
58
+ /*
59
+ * Legacy dialog fallback — presentation for the dialog-fallback.js shim
60
+ * (browsers without native <dialog>: Safari <= 15.3, Firefox <= 97).
61
+ *
62
+ * Isolated in its own file so a custom bundle can opt out by dropping this
63
+ * import; the JS side is disabled with data-dialog-fallback="false" on the
64
+ * root element or on a dialog. Must load before component styles so ties
65
+ * such as dialog.modal's padding resolve in the component's favor.
66
+ *
67
+ * The whole file is inert on modern browsers: rules key on the
68
+ * .dialog-fallback marker written by the shim, so browsers that support
69
+ * <dialog> but not :modal are never affected either.
70
+ */
71
+ @supports not (selector(dialog:modal)) {
72
+ /* Browsers without native <dialog> ship no UA dialog styles at all;
73
+ approximate them. :where() keeps specificity at dialog[open] level so
74
+ component styles such as dialog.modal still override the emulation. */
75
+ dialog[open]:where(.dialog-fallback) {
76
+ position: absolute;
77
+ inset-inline: 0;
78
+ width: -moz-fit-content;
79
+ width: fit-content;
80
+ max-inline-size: calc(100% - 2rem);
81
+ margin: auto;
82
+ padding: var(--space-4, 1em);
83
+ border: var(--border-width, 1px) solid var(--border, currentColor);
84
+ background: var(--surface-raised, #fff);
85
+ color: var(--text, inherit);
86
+ }
87
+
88
+ /* Open modally: fixed near the top (no fit-content in the block axis on
89
+ these browsers, so no vertical centering) and above menus, below
90
+ tooltips. Legacy-only layer, hence no z-index token. The spread shadow is
91
+ the visible backdrop: the dialog is its own stacking context, where a
92
+ negative z-index child paints above the element's background, so the
93
+ dimming cannot live on the ::before. */
94
+ dialog[open].is-fallback-modal {
95
+ position: fixed;
96
+ inset-block: 8vh auto;
97
+ max-block-size: 84vh;
98
+ overflow: auto;
99
+ z-index: 40;
100
+ box-shadow: 0 0 0 100vmax var(--backdrop-fill, rgb(0 0 0 / 35%));
101
+ }
102
+
103
+ /* Transparent stand-in for ::backdrop, hit-testing only. Clicks on it hit
104
+ the dialog element outside its own box, which dialog.js already treats as
105
+ a backdrop click, and it blocks pointer interaction with the page behind
106
+ the modal. */
107
+ dialog[open].is-fallback-modal::before {
108
+ content: "";
109
+ position: fixed;
110
+ inset: 0;
111
+ z-index: -1;
112
+ }
113
+ }
114
+
115
+ :root {
116
+ /* Typography */
117
+ --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
118
+ --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
119
+ --line-height: 1.5;
120
+ --line-height-tight: 1.25;
121
+ --line-height-relaxed: 1.75;
122
+ --font-weight: 400;
123
+ --font-weight-medium: 500;
124
+ --font-weight-strong: 650;
125
+ --font-weight-bold: 700;
126
+
127
+ /* Typography scale */
128
+ --font-size-xs: 0.8125rem;
129
+ --font-size-sm: 0.875rem;
130
+ --font-size-md: 1rem;
131
+ --font-size-lg: 1.125rem;
132
+
133
+ /* Spacing */
134
+ --gap: 0.75rem;
135
+ --variant-space: var(--space-4);
136
+ --space-1: 0.25rem;
137
+ --space-2: 0.5rem;
138
+ --space-3: 0.75rem;
139
+ --space-4: 1rem;
140
+ --space-5: 1.5rem;
141
+ --space-6: 2rem;
142
+
143
+ /* Shape */
144
+ --border-width: 1px;
145
+ --radius-full: 9999px;
146
+ --shadow: 0 0.5rem 1.5rem rgb(0 0 0 / 0.08);
147
+ --shadow-popout: 0 0.25rem 0.75rem rgb(0 0 0 / 0.15);
148
+
149
+ /* Motion — see reset.css for the prefers-reduced-motion hard reset.
150
+ --duration drives component transitions (hover/focus); the dedicated
151
+ animation rates below tune one-shot open/close and decorative loops. */
152
+ --duration: 150ms;
153
+ --duration-fast: 100ms;
154
+ --duration-slow: 200ms;
155
+ --duration-spin: 0.75s;
156
+ --duration-shimmer: 1.2s;
157
+ --ease: ease;
158
+
159
+ /* Focus ring — shared by every interactive component's :focus-visible */
160
+ --focus-outline: calc(var(--border-width) * 2) solid var(--focus);
161
+ --focus-outline-offset: 2px;
162
+
163
+ /* Status dot positioning — used by .avatar > .badge:empty. The offset is
164
+ the circle-aware inset (1 - 1/√2) / 2 ≈ 14.65%; the ring surrounds the
165
+ dot with the parent surface color. */
166
+ --indicator-offset: 14.65%;
167
+ --indicator-ring: var(--surface);
168
+
169
+ /* Shared backdrop — consumed by modal, drawer, and mobile flyout sheets. */
170
+ --backdrop-color: rgb(0 0 0);
171
+ --backdrop-opacity: 0.45;
172
+ --backdrop-fill: rgb(0 0 0 / var(--backdrop-opacity));
173
+
174
+ /* Progress / meter track height */
175
+ --bar-height: 0.65rem;
176
+
177
+ /* Controls — inputs and buttons share --control-size so heights always match.
178
+ .sm/.lg (variants.css) switch --control-size and --control-font-size. */
179
+ --control-size-sm: 2rem;
180
+ --control-size-md: 2.375rem;
181
+ --control-size-lg: 2.75rem;
182
+ --control-size: var(--control-size-md);
183
+ --control-pad-x: 1em;
184
+ --control-font-size: var(--font-size-md);
185
+ --disabled-opacity: 0.65;
186
+
187
+ /* Variant tuning — themes may adjust generated soft surfaces without
188
+ replacing the component rules. */
189
+ --soft-bg-mix: 88%;
190
+ --soft-border-mix: 65%;
191
+ --soft-hover-alpha: 12%;
192
+
193
+ /* Shared icons */
194
+ --icon-chevron-mask: 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='6 9 12 15 18 9'/%3E%3C/svg%3E");
195
+ --icon-check-mask: 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");
196
+ /* Native select uses background-image, which cannot inherit currentColor.
197
+ The stroke matches the default theme's text-subtle (hue 258 neutral). */
198
+ --icon-chevron-image: 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");
199
+
200
+ /* Viewport size tokens — fallback vh/vw with dvh/vi enhancement.
201
+ Consumed by full-viewport components (app-shell, drawer, modal, flyout).
202
+ Custom properties keep the fallback explicit while allowing modern units
203
+ to enhance the same public tokens. */
204
+ --viewport-block: 100vh;
205
+ --viewport-inline: 100vw;
206
+
207
+ /* Layering */
208
+ --z-sticky: 10;
209
+ --z-menu: 20;
210
+ --z-tooltip: 50;
211
+ --z-status: 60;
212
+ }
213
+
214
+ @supports (block-size: 100dvh) {
215
+ :root {
216
+ --viewport-block: 100dvh;
217
+ }
218
+ }
219
+
220
+ @supports (inline-size: 100vi) {
221
+ :root {
222
+ --viewport-inline: 100vi;
223
+ }
224
+ }
225
+
226
+ @supports (color: color-mix(in oklch, red, white)) {
227
+ :root {
228
+ --shadow: 0 0.5rem 1.5rem color-mix(in oklch, var(--shadow-color) 8%, transparent);
229
+ --shadow-popout: 0 0.25rem 0.75rem color-mix(in oklch, var(--shadow-color) 15%, transparent);
230
+ --backdrop-fill: color-mix(
231
+ in oklch,
232
+ var(--backdrop-color) calc(var(--backdrop-opacity) * 100%),
233
+ transparent
234
+ );
235
+ }
236
+ }
237
+
238
+ :root {
239
+ color-scheme: light dark;
240
+ }
241
+
242
+ [data-theme="light"] {
243
+ color-scheme: light;
244
+ }
245
+
246
+ [data-theme="dark"] {
247
+ color-scheme: dark;
248
+ }
249
+
250
+ /* Dark mode is progressive enhancement via light-dark(). Older browsers get
251
+ the light fallback rather than a partially dark native color scheme over
252
+ light tokens. */
253
+ @supports not (color: light-dark(red, blue)) {
254
+ :root,
255
+ [data-theme="light"],
256
+ [data-theme="dark"] {
257
+ color-scheme: light;
258
+ }
259
+ }
260
+
261
+ /* Default palette — "ink & terra": aubergine primary, terracotta secondary,
262
+ pigment-toned statuses (sage, ochre, brick) tuned to equal perceived weight.
263
+ The neutral ramp (text, borders, subtle surfaces) is tinted toward the
264
+ primary hue (~260) so the whole page carries the identity, quietly. */
265
+
266
+ /* Light fallback — also the default on browsers without light-dark() support. */
267
+ :root,
268
+ [data-theme="light"] {
269
+ /* Intent colors */
270
+ --primary: hsl(268 32% 33%);
271
+ --primary-fg: hsl(0 0% 98%);
272
+
273
+ --secondary: hsl(16 38% 38%);
274
+ --secondary-fg: hsl(0 0% 98%);
275
+
276
+ --success: hsl(150 40% 31%);
277
+ --success-fg: hsl(0 0% 98%);
278
+
279
+ --warning: hsl(36 68% 34%);
280
+ --warning-fg: hsl(0 0% 98%);
281
+
282
+ --danger: hsl(5 58% 43%);
283
+ --danger-fg: hsl(0 0% 98%);
284
+
285
+ --neutral: hsl(258 7% 43%);
286
+ --neutral-fg: hsl(0 0% 98%);
287
+
288
+ /* Surfaces */
289
+ --surface: hsl(0 0% 100%);
290
+ --surface-raised: hsl(0 0% 100%);
291
+ --surface-subtle: hsl(262 24% 96%);
292
+ --surface-solid: hsl(261 16% 13%);
293
+
294
+ /* Text */
295
+ --text: hsl(261 20% 13%);
296
+ --text-muted: hsl(259 9% 42%);
297
+ --text-subtle: hsl(258 7% 57%);
298
+
299
+ /* Lines and focus */
300
+ --border: hsl(260 15% 88%);
301
+ --focus: var(--primary);
302
+ /* Pre-color-mix fallback only — superseded below by a derivation from
303
+ --focus so overriding --primary/--focus never leaves a stale ring. */
304
+ --focus-ring: hsl(268 32% 33% / 0.22);
305
+ --focus-ring-shadow: 0 0 0 3px var(--focus-ring);
306
+ --hover-overlay: hsl(260 20% 10% / 0.04);
307
+
308
+ /* Radius scale — themes may override the whole scale (e.g. [data-theme="square"]) */
309
+ --radius: 0.5rem;
310
+ --radius-sm: 0.25rem;
311
+ --radius-lg: 0.75rem;
312
+
313
+ /* Shadow color — consumed by --shadow / --shadow-popout in tokens.css */
314
+ --shadow-color: hsl(260 25% 10%);
315
+
316
+ /* Heading and selection — derived from theme color tokens */
317
+ --heading: var(--text);
318
+ --selection-bg: var(--primary);
319
+ --selection-fg: var(--primary-fg);
320
+
321
+ /* hgroup internal gap */
322
+ --hgroup-gap: 0.25em;
323
+ }
324
+
325
+ /* Tone subsystem — fixed-backdrop foreground for components placed over media
326
+ or other surfaces outside the theme token stack. Use data-theme islands for
327
+ full dark/light subtree inversion. */
328
+ [data-tone="dark"] {
329
+ --tone-fg: white;
330
+ }
331
+
332
+ [data-tone="light"] {
333
+ --tone-fg: var(--text);
334
+ }
335
+
336
+ /* Focus ring derivation — color-mix tier. Runs whether or not light-dark()
337
+ is also supported, so it covers the color-mix-without-light-dark browser
338
+ tier on its own; the light-dark block below refines it further with the
339
+ light/dark alpha split once both features are available. Deriving from
340
+ --focus (not a literal) means overriding --primary/--focus, in the
341
+ default theme or any custom one, keeps the ring in sync automatically. */
342
+ @supports (color: color-mix(in oklch, red, white)) {
343
+ :root,
344
+ [data-theme="light"],
345
+ [data-theme="dark"] {
346
+ --focus-ring: color-mix(in oklch, var(--focus) 22%, transparent);
347
+ }
348
+ }
349
+
350
+ /* Auto dark mode via light-dark(). Resolves based on the element's color-scheme:
351
+ :root (light dark) follows the OS, [data-theme="light"] forces light,
352
+ [data-theme="dark"] forces dark by overriding color-scheme.
353
+ Dark pastels sit a little lower and more chromatic (L 70-76, sat 38-45)
354
+ than usual so the palette stays warm rather than washed gray. */
355
+ @supports (color: light-dark(red, blue)) {
356
+ :root,
357
+ [data-theme="light"] {
358
+ --primary: light-dark(hsl(268 32% 33%), hsl(268 42% 76%));
359
+ --primary-fg: light-dark(hsl(0 0% 98%), hsl(268 30% 12%));
360
+
361
+ --secondary: light-dark(hsl(16 38% 38%), hsl(18 45% 72%));
362
+ --secondary-fg: light-dark(hsl(0 0% 98%), hsl(18 32% 11%));
363
+
364
+ --success: light-dark(hsl(150 40% 31%), hsl(150 38% 62%));
365
+ --success-fg: light-dark(hsl(0 0% 98%), hsl(150 35% 8%));
366
+
367
+ --warning: light-dark(hsl(36 68% 34%), hsl(40 62% 64%));
368
+ --warning-fg: light-dark(hsl(0 0% 98%), hsl(38 35% 10%));
369
+
370
+ --danger: light-dark(hsl(5 58% 43%), hsl(4 60% 70%));
371
+ --danger-fg: light-dark(hsl(0 0% 98%), hsl(4 35% 10%));
372
+
373
+ --neutral: light-dark(hsl(258 7% 43%), hsl(258 10% 72%));
374
+ --neutral-fg: light-dark(hsl(0 0% 98%), hsl(260 18% 10%));
375
+
376
+ --surface: light-dark(hsl(0 0% 100%), hsl(263 17% 9%));
377
+ --surface-raised: light-dark(hsl(0 0% 100%), hsl(263 15% 12%));
378
+ --surface-subtle: light-dark(hsl(262 24% 96%), hsl(263 13% 16%));
379
+ --surface-solid: light-dark(hsl(261 16% 13%), hsl(0 0% 96%));
380
+
381
+ --text: light-dark(hsl(261 20% 13%), hsl(260 12% 95%));
382
+ --text-muted: light-dark(hsl(259 9% 42%), hsl(259 9% 67%));
383
+ --text-subtle: light-dark(hsl(258 7% 57%), hsl(258 7% 54%));
384
+
385
+ --border: light-dark(hsl(260 15% 88%), hsl(262 12% 24%));
386
+ --focus-ring: light-dark(hsl(268 32% 33% / 0.22), hsl(268 42% 76% / 0.28));
387
+ --hover-overlay: light-dark(hsl(260 20% 10% / 0.04), hsl(0 0% 100% / 0.06));
388
+ --shadow-color: light-dark(hsl(260 25% 10%), hsl(262 40% 2%));
389
+ }
390
+ }
391
+
392
+ @supports (color: color-mix(in oklch, red, white)) and (color: light-dark(red, blue)) {
393
+ :root,
394
+ [data-theme="light"] {
395
+ --focus-ring: light-dark(
396
+ color-mix(in oklch, var(--focus) 22%, transparent),
397
+ color-mix(in oklch, var(--focus) 28%, transparent)
398
+ );
399
+ }
400
+ }
401
+
402
+ @media (forced-colors: active) {
403
+ :root,
404
+ [data-theme="light"],
405
+ [data-theme="dark"] {
406
+ --focus-outline: calc(var(--border-width) * 2) solid Highlight;
407
+ --focus-ring-shadow: none;
408
+
409
+ --backdrop-fill: Canvas;
410
+ }
411
+ }
412
+
413
+ @media (prefers-contrast: more) {
414
+ :root,
415
+ [data-theme="light"],
416
+ [data-theme="dark"] {
417
+ --border-width: 2px;
418
+ --focus-outline-offset: 3px;
419
+ --focus-ring-shadow: 0 0 0 4px var(--focus-ring);
420
+ --text-muted: var(--text);
421
+ --text-subtle: var(--text);
422
+ }
423
+ }
424
+
425
+ /*
426
+ * Base element styles for native HTML semantics.
427
+ *
428
+ * Sits between reset (browser defaults) and prose (rich content scope).
429
+ * These styles apply everywhere — cards, dialogs, heroes — not only in prose.
430
+ */
431
+
432
+ body {
433
+ background: var(--surface);
434
+ color: var(--text);
435
+ font-family: var(--font-sans);
436
+ line-height: var(--line-height);
437
+ }
438
+
439
+ a {
440
+ color: inherit;
441
+ text-underline-offset: 0.18em;
442
+ }
443
+
444
+ a:focus-visible {
445
+ box-shadow: var(--focus-ring-shadow);
446
+ }
447
+
448
+ @media (hover: hover) {
449
+ a:hover {
450
+ text-decoration-thickness: 0.12em;
451
+ }
452
+ }
453
+
454
+ /* Global heading chrome: color (--heading or inherit), a tight line-height,
455
+ and progressive text-wrap. No font-size or font-weight is set globally —
456
+ components and .prose own the per-context scale. */
457
+ :where(h1, h2, h3, h4, h5, h6) {
458
+ color: var(--heading, inherit);
459
+ line-height: var(--line-height-tight);
460
+ text-wrap: balance;
461
+ }
462
+
463
+ /* Compact heading groups. Structural only: display, gap, internal margin reset.
464
+ No font-size, color, or weight is applied here. */
465
+ hgroup {
466
+ display: grid;
467
+ gap: var(--hgroup-gap);
468
+ }
469
+
470
+ hgroup > * {
471
+ margin-block: 0;
472
+ }
473
+
474
+ hr {
475
+ border: 0;
476
+ border-block-start: var(--border-width) solid var(--border);
477
+ margin-block: var(--space-6);
478
+ }
479
+
480
+ code,
481
+ kbd,
482
+ samp,
483
+ pre {
484
+ font-family: var(--font-mono);
485
+ }
486
+
487
+ small {
488
+ font-size: 0.875em;
489
+ }
490
+
491
+ ::selection {
492
+ background: var(--selection-bg, var(--primary));
493
+ color: var(--selection-fg, var(--primary-fg));
494
+ }
495
+
496
+ /*
497
+ * Intent classes do not style elements directly.
498
+ * They only expose --intent and --intent-fg.
499
+ * Components and variants decide how to consume them. Each component that
500
+ * consumes --intent or --ui-* must open a local intent boundary in a :where()
501
+ * block that resets --intent / --intent-fg / --ui-* to initial. Parent intents
502
+ * do not leak into nested components. Local intent classes (.primary, .danger,
503
+ * etc.) still apply because they have higher specificity than :where().
504
+ */
505
+ .primary {
506
+ --intent: var(--primary);
507
+ --intent-fg: var(--primary-fg);
508
+ }
509
+ .secondary {
510
+ --intent: var(--secondary);
511
+ --intent-fg: var(--secondary-fg);
512
+ }
513
+ .success {
514
+ --intent: var(--success);
515
+ --intent-fg: var(--success-fg);
516
+ }
517
+ .warning {
518
+ --intent: var(--warning);
519
+ --intent-fg: var(--warning-fg);
520
+ }
521
+ .danger {
522
+ --intent: var(--danger);
523
+ --intent-fg: var(--danger-fg);
524
+ }
525
+ .neutral {
526
+ --intent: var(--neutral);
527
+ --intent-fg: var(--neutral-fg);
528
+ }
529
+
530
+ .container-query {
531
+ container-type: inline-size;
532
+ }
533
+
534
+ .stack {
535
+ display: flex;
536
+ flex-direction: column;
537
+ gap: var(--gap);
538
+ }
539
+
540
+ .stack > * {
541
+ margin: 0;
542
+ }
543
+
544
+ .cluster {
545
+ display: flex;
546
+ flex-wrap: wrap;
547
+ align-items: var(--cluster-align, center);
548
+ justify-content: var(--cluster-justify, flex-start);
549
+ gap: var(--gap);
550
+ }
551
+
552
+ .center {
553
+ box-sizing: content-box;
554
+ max-inline-size: var(--center-size, 72rem);
555
+ margin-inline: auto;
556
+ padding-inline: var(--center-pad, 1rem);
557
+ }
558
+
559
+ .frame {
560
+ overflow: hidden;
561
+ aspect-ratio: var(--frame-ratio, 16 / 9);
562
+ }
563
+
564
+ .frame > :where(img, video, iframe, canvas, svg) {
565
+ inline-size: 100%;
566
+ block-size: 100%;
567
+ object-fit: cover;
568
+ }
569
+
570
+ .app-shell {
571
+ min-block-size: var(--viewport-block);
572
+ display: flex;
573
+ flex-direction: column;
574
+ }
575
+
576
+ /* Media object: fixed-width leading element (avatar, icon, image) + flexible
577
+ trailing content. Replaces the repeated "auto minmax(0, 1fr)" pattern in
578
+ author cards, comments, meta rows, and similar compositions. */
579
+ .media {
580
+ display: grid;
581
+ grid-template-columns: auto minmax(0, 1fr);
582
+ gap: var(--gap);
583
+ align-items: start;
584
+ }
585
+
586
+ /* Sidebar layout: content + aside using grid. Use this when the aside is
587
+ sticky or the content is multi-section. The aside width is tunable with
588
+ --sidebar-layout-size. */
589
+ .sidebar-layout {
590
+ display: grid;
591
+ gap: var(--gap);
592
+ grid-template-columns: minmax(0, 1fr);
593
+ }
594
+
595
+ @container (width >= 64rem) {
596
+ .container-query .sidebar-layout {
597
+ grid-template-columns: minmax(0, 1fr) var(--sidebar-layout-size, 18rem);
598
+ align-items: start;
599
+ }
600
+ }
601
+
602
+ @supports not (container-type: inline-size) {
603
+ @media (min-width: 64rem) {
604
+ .container-query .sidebar-layout {
605
+ grid-template-columns: minmax(0, 1fr) var(--sidebar-layout-size, 18rem);
606
+ align-items: start;
607
+ }
608
+ }
609
+ }
610
+
611
+ .grid {
612
+ display: grid;
613
+ gap: var(--gap);
614
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--grid-min, 16rem)), 1fr));
615
+ }
616
+
617
+ /* Query-container wrapper that opts .grid-2/3/4/6 into responsive
618
+ * collapse. Wrap a .grid-2/3/4/6 inside .container-query to make the
619
+ * column count collapse on narrow wrappers. Without this wrapper,
620
+ * .grid-2/3/4/6 stay at their full column count. */
621
+ /* Fixed-column grids at their full count by default.
622
+ * .grid-2: 2 cols
623
+ * .grid-3: 3 cols
624
+ * .grid-4: 4 cols
625
+ * .grid-6: 6 cols
626
+ * When placed inside .container-query, the column count collapses
627
+ * on narrow wrappers. The intermediate 2-col step applies to
628
+ * .grid-3/.grid-4; .grid-6 starts at 2 columns because it is meant
629
+ * for compact items that do not need a full single-column stack.
630
+ * .grid-2: 1 col -> 2 cols
631
+ * .grid-3: 1 col -> 2 cols -> 3 cols
632
+ * .grid-4: 1 col -> 2 cols -> 4 cols
633
+ * .grid-6: 2 cols -> 3 cols -> 6 cols */
634
+ .grid-2,
635
+ .grid-3,
636
+ .grid-4,
637
+ .grid-6 {
638
+ display: grid;
639
+ gap: var(--gap);
640
+ }
641
+
642
+ .grid-2 {
643
+ grid-template-columns: repeat(2, minmax(0, 1fr));
644
+ }
645
+ .grid-3 {
646
+ grid-template-columns: repeat(3, minmax(0, 1fr));
647
+ }
648
+ .grid-4 {
649
+ grid-template-columns: repeat(4, minmax(0, 1fr));
650
+ }
651
+ .grid-6 {
652
+ grid-template-columns: repeat(6, minmax(0, 1fr));
653
+ }
654
+
655
+ @container (width < 28rem) {
656
+ .container-query .grid-2,
657
+ .container-query .grid-3,
658
+ .container-query .grid-4 {
659
+ grid-template-columns: 1fr;
660
+ }
661
+
662
+ .container-query .grid-6 {
663
+ grid-template-columns: repeat(2, minmax(0, 1fr));
664
+ }
665
+ }
666
+
667
+ @container (28rem <= width < 48rem) {
668
+ .container-query .grid-3,
669
+ .container-query .grid-4 {
670
+ grid-template-columns: repeat(2, minmax(0, 1fr));
671
+ }
672
+ }
673
+
674
+ @container (28rem <= width < 64rem) {
675
+ .container-query .grid-6 {
676
+ grid-template-columns: repeat(3, minmax(0, 1fr));
677
+ }
678
+ }
679
+
680
+ @supports not (container-type: inline-size) {
681
+ @media (max-width: 63.999rem) {
682
+ .container-query .grid-6 {
683
+ grid-template-columns: repeat(3, minmax(0, 1fr));
684
+ }
685
+ }
686
+
687
+ @media (max-width: 47.999rem) {
688
+ .container-query .grid-3,
689
+ .container-query .grid-4 {
690
+ grid-template-columns: repeat(2, minmax(0, 1fr));
691
+ }
692
+ }
693
+
694
+ @media (max-width: 27.999rem) {
695
+ .container-query .grid-2,
696
+ .container-query .grid-3,
697
+ .container-query .grid-4 {
698
+ grid-template-columns: 1fr;
699
+ }
700
+
701
+ .container-query .grid-6 {
702
+ grid-template-columns: repeat(2, minmax(0, 1fr));
703
+ }
704
+ }
705
+ }
706
+
707
+ /*
708
+ * Cross-cutting variant token mapping.
709
+ *
710
+ * Variant scope: .solid/.soft/.outline set --ui-* for components that opt into
711
+ * shared variants. Component-specific defaults (.btn solid-by-default, .badge
712
+ * solid-by-default, .alert soft-by-default) live with their components:
713
+ * components/button.css — .btn default solid + .btn.ghost + .btn.link
714
+ * components/badge.css — .badge default solid
715
+ * components/alert.css — .alert default + .alert.solid/.alert.outline
716
+ *
717
+ * .sm/.lg set shared size tokens. Controls consume --control-*; non-control
718
+ * components may consume --variant-* when the variant should affect their
719
+ * local density. Compact components may consume --variant-compact-*.
720
+ * Components with bespoke sizing (spinner, avatar) define local .sm/.lg;
721
+ * applying .sm/.lg to card, dialog, accordion, navbar, table is
722
+ * a no-op.
723
+ *
724
+ * Intent list source of truth: intents.css. Components consume --intent* tokens
725
+ * instead of re-listing intent classes.
726
+ */
727
+ .solid {
728
+ --ui-bg: var(--intent, var(--neutral));
729
+ --ui-fg: var(--intent-fg, var(--neutral-fg));
730
+ --ui-border: transparent;
731
+ }
732
+
733
+ .soft {
734
+ --ui-bg: var(--surface-subtle);
735
+ --ui-border: var(--border);
736
+ --ui-fg: var(--intent, var(--text));
737
+ }
738
+
739
+ .outline {
740
+ --ui-bg: transparent;
741
+ --ui-fg: var(--intent, var(--text));
742
+ --ui-border: var(--intent, var(--border));
743
+ }
744
+
745
+ .sm {
746
+ --gap: var(--space-2);
747
+ --variant-space: var(--space-2);
748
+ --control-size: var(--control-size-sm);
749
+ --control-font-size: var(--font-size-sm);
750
+ --variant-font-size: var(--font-size-sm);
751
+ --variant-icon-size: 1rem;
752
+ --variant-pad-block: var(--space-3);
753
+ --variant-compact-size: 1.25rem;
754
+ --variant-compact-font-size: 0.75rem;
755
+ }
756
+ .lg {
757
+ --gap: var(--space-5);
758
+ --variant-space: var(--space-5);
759
+ --control-size: var(--control-size-lg);
760
+ --control-font-size: var(--font-size-lg);
761
+ --variant-font-size: var(--font-size-lg);
762
+ --variant-icon-size: 1.5rem;
763
+ --variant-pad-block: var(--space-5);
764
+ --variant-compact-size: 1.875rem;
765
+ --variant-compact-font-size: var(--font-size-sm);
766
+ }
767
+
768
+ /* Modern color enhancement. Baseline variant values above remain usable
769
+ everywhere. Interactive states live in the components that consume them. */
770
+ @supports (color: color-mix(in oklch, red, white)) {
771
+ .soft {
772
+ /* @sync soft-recipe */
773
+ --ui-bg: color-mix(in oklch, var(--surface) var(--soft-bg-mix), var(--intent, var(--neutral)));
774
+ --ui-border: color-mix(
775
+ in oklch,
776
+ var(--surface) var(--soft-border-mix),
777
+ var(--intent, var(--neutral))
778
+ );
779
+ /* @sync soft-recipe */
780
+ }
781
+ }
782
+
783
+ /* Interactive hover recipe shared by .btn and .badge. Declared outside :hover
784
+ on purpose: --ui-hover-bg is only consumed inside their @media (hover: hover)
785
+ rules, so these values are inert until hovered. :is() carries real
786
+ specificity so the components' :where() intent boundaries
787
+ (--ui-hover-bg: initial) cannot override the recipe. The solid/soft overlay
788
+ darkening lives in the consuming components. */
789
+ :is(.btn, a.badge, button.badge):is(.outline, .ghost) {
790
+ --ui-hover-bg: var(--surface-subtle);
791
+ }
792
+
793
+ @supports (color: color-mix(in oklch, red, white)) {
794
+ /* Solid has no literal class requirement: bare .btn/.badge default to solid,
795
+ so this must match anything that isn't one of the other variants rather
796
+ than an explicit .solid class. Stacks with the box-shadow overlay the
797
+ consuming components apply on top for extra depth. */
798
+ :is(.btn, a.badge, button.badge):not(:where(.soft, .outline, .ghost, .link)) {
799
+ --ui-hover-bg: color-mix(in oklch, var(--intent, var(--neutral)) 88%, black);
800
+ }
801
+
802
+ /* Same recipe as the resting .soft bg with the surface share reduced by
803
+ --soft-hover-alpha: stays opaque, so it reads on any backdrop instead of
804
+ letting the container show through on hover. */
805
+ :is(.btn, a.badge, button.badge):is(.soft) {
806
+ --ui-hover-bg: color-mix(
807
+ in oklch,
808
+ var(--surface) calc(var(--soft-bg-mix) - var(--soft-hover-alpha)),
809
+ var(--intent, var(--neutral))
810
+ );
811
+ }
812
+
813
+ /* Mixed against the opaque --surface-subtle rather than transparent: a
814
+ transparent-based tint reads clean on light surfaces but goes muddy and
815
+ low-contrast on dark ones, since the same low alpha lands very
816
+ differently over a light vs. a near-black backdrop. */
817
+ :is(.btn, a.badge, button.badge):is(.outline) {
818
+ --ui-hover-bg: color-mix(in oklch, var(--surface-subtle) 92%, var(--intent, var(--neutral)));
819
+ }
820
+
821
+ :is(.btn, a.badge, button.badge):is(.ghost) {
822
+ --ui-hover-bg: color-mix(in oklch, var(--intent, var(--text)) 8%, transparent);
823
+ }
824
+ }
825
+
826
+ /*
827
+ * Prose is the opt-in rich text scope for authored content.
828
+ *
829
+ * Global element styles stay mild so app UI is not shaped accidentally. Page
830
+ * layout, forms, cards, navigation, and app tables remain outside this scope.
831
+ */
832
+
833
+ .prose {
834
+ /* Prose scale */
835
+ --prose-size: 1rem;
836
+ --prose-line-height: 1.7;
837
+ --prose-measure: 70ch;
838
+ --prose-flow: 1em;
839
+ --prose-heading-line-height: 1.2;
840
+ --prose-h1: 2rem;
841
+ --prose-h2: 1.5rem;
842
+ --prose-h3: 1.25rem;
843
+ --prose-h4: 1.125rem;
844
+
845
+ max-inline-size: var(--prose-measure);
846
+ color: var(--text);
847
+ font-size: var(--prose-size);
848
+ line-height: var(--prose-line-height);
849
+ overflow-wrap: break-word;
850
+ }
851
+
852
+ .prose :where(p, ul, ol, dl, blockquote, pre, table, figure) {
853
+ margin-block: var(--prose-flow) 0;
854
+ }
855
+
856
+ .prose :where(h1, h2, h3, h4, h5, h6) {
857
+ margin-block: 1.5em 0;
858
+ color: var(--text);
859
+ font-weight: var(--font-weight-strong);
860
+ line-height: var(--prose-heading-line-height);
861
+ }
862
+
863
+ .prose :where(p, li, blockquote) {
864
+ text-wrap: pretty;
865
+ }
866
+
867
+ .prose > :first-child {
868
+ margin-block-start: 0;
869
+ }
870
+
871
+ .prose h1 {
872
+ font-size: var(--prose-h1);
873
+ }
874
+
875
+ .prose h2 {
876
+ font-size: var(--prose-h2);
877
+ }
878
+
879
+ .prose h3 {
880
+ font-size: var(--prose-h3);
881
+ }
882
+
883
+ .prose h4 {
884
+ font-size: var(--prose-h4);
885
+ }
886
+
887
+ .prose a {
888
+ color: var(--primary);
889
+ text-decoration-line: underline;
890
+ text-decoration-thickness: 0.08em;
891
+ text-underline-offset: 0.18em;
892
+ }
893
+
894
+ @media (hover: hover) {
895
+ .prose a:hover {
896
+ text-decoration-thickness: 0.12em;
897
+ }
898
+
899
+ @supports (color: color-mix(in oklch, red, white)) {
900
+ .prose a:hover {
901
+ color: color-mix(in oklch, var(--primary), var(--text) 15%);
902
+ }
903
+ }
904
+ }
905
+
906
+ .prose :where(ul, ol) {
907
+ padding-inline-start: 1.5em;
908
+ }
909
+
910
+ .prose li + li {
911
+ margin-block-start: 0.35em;
912
+ }
913
+
914
+ .prose code {
915
+ padding: 0.15em 0.3em;
916
+ border-radius: var(--radius-sm);
917
+ background: var(--surface-subtle);
918
+ color: var(--text);
919
+ font-size: 0.875em;
920
+ }
921
+
922
+ .prose kbd {
923
+ display: inline-flex;
924
+ align-items: center;
925
+ justify-content: center;
926
+ min-inline-size: 1.75em;
927
+ min-block-size: 1.6em;
928
+ padding-inline: 0.45em;
929
+ border: var(--border-width) solid var(--border);
930
+ border-block-end-width: calc(var(--border-width) * 2);
931
+ border-radius: var(--radius-sm);
932
+ background: var(--surface-raised);
933
+ color: var(--text);
934
+ font-family: var(--font-mono);
935
+ font-size: 0.875em;
936
+ font-weight: var(--font-weight-medium);
937
+ line-height: 1;
938
+ white-space: nowrap;
939
+ vertical-align: 0.08em;
940
+ }
941
+
942
+ .prose pre {
943
+ overflow-x: auto;
944
+ padding: 1em;
945
+ border: var(--border-width) solid var(--border);
946
+ border-radius: var(--radius);
947
+ background: var(--surface-subtle);
948
+ }
949
+
950
+ .prose pre code {
951
+ padding: 0;
952
+ background: transparent;
953
+ }
954
+
955
+ .prose blockquote {
956
+ padding-inline-start: 1em;
957
+ border-inline-start: var(--border-width) solid var(--border);
958
+ color: var(--text-muted);
959
+ }
960
+
961
+ .prose figure {
962
+ margin-inline: 0;
963
+ }
964
+
965
+ .prose figcaption {
966
+ margin-block-start: 0.5em;
967
+ color: var(--text-muted);
968
+ font-size: 0.875em;
969
+ }
970
+
971
+ .prose table {
972
+ inline-size: 100%;
973
+ border-collapse: collapse;
974
+ }
975
+
976
+ .prose th,
977
+ .prose td {
978
+ padding: 0.5em;
979
+ border-block-end: var(--border-width) solid var(--border);
980
+ text-align: start;
981
+ }
982
+
983
+ /* Lead paragraph: a slightly larger, more relaxed reading-introduction text.
984
+ Non-fluid and always available — use .text-lead from the optional fluid
985
+ module for marketing/display surfaces. */
986
+ .lead {
987
+ font-size: 1.125em;
988
+ line-height: var(--line-height-relaxed);
989
+ }
990
+
991
+ /* Readable content measure. Composes with .center for centering or with
992
+ margin-inline: auto for a one-off centered block. */
993
+ .measure {
994
+ max-inline-size: var(--prose-measure, 70ch);
995
+ }
996
+
997
+ .prose.sm {
998
+ --prose-size: 0.9375rem;
999
+ --prose-h1: 1.75rem;
1000
+ --prose-h2: 1.375rem;
1001
+ --prose-h3: 1.125rem;
1002
+ --prose-h4: 1rem;
1003
+ }
1004
+
1005
+ .prose.lg {
1006
+ --prose-size: 1.125rem;
1007
+ --prose-measure: 72ch;
1008
+ --prose-h1: 2.25rem;
1009
+ --prose-h2: 1.625rem;
1010
+ --prose-h3: 1.375rem;
1011
+ --prose-h4: 1.25rem;
1012
+ }
1013
+
1014
+ /* Controls */
1015
+ /*
1016
+ * Buttons for actions and button-like links.
1017
+ *
1018
+ * .btn is primarily for actionable elements: <button>, <a href>,
1019
+ * input[type="button"|"submit"|"reset"].
1020
+ *
1021
+ * Keeps shared intent, variant, and size behavior with a solid default.
1022
+ *
1023
+ * Disabled state uses the native disabled attribute for buttons and inputs.
1024
+ * Anchors have no native disabled state, so a.btn uses aria-disabled="true"
1025
+ * for disabled styling and activation guards.
1026
+ */
1027
+
1028
+ /* Intent boundary — inherited intents do not tint this component.
1029
+ Local intent classes still win because :where() has zero specificity. */
1030
+ :where(.btn) {
1031
+ /* @sync intent-boundary */
1032
+ --intent: initial;
1033
+ --intent-fg: initial;
1034
+ --ui-bg: initial;
1035
+ --ui-fg: initial;
1036
+ --ui-border: initial;
1037
+ --ui-hover-bg: initial;
1038
+ /* @sync intent-boundary */
1039
+ }
1040
+
1041
+ .btn {
1042
+ --btn-font-size: var(--control-font-size);
1043
+ --btn-min-size: var(--control-size);
1044
+ --btn-pad-x: var(--control-pad-x, 1em);
1045
+ --btn-bg: var(--ui-bg, var(--btn-default-bg, var(--intent, var(--neutral))));
1046
+ --btn-fg: var(--ui-fg, var(--btn-default-fg, var(--intent-fg, var(--neutral-fg))));
1047
+ --btn-border: var(--ui-border, var(--btn-default-border, transparent));
1048
+ --btn-radius: var(--radius);
1049
+ --btn-focus-color: var(--intent, var(--focus));
1050
+ --btn-focus-outline-width: calc(var(--border-width) * 2);
1051
+ --btn-hover-bg: var(--ui-hover-bg, var(--btn-bg));
1052
+ --btn-hover-overlay: var(--hover-overlay, rgb(0 0 0 / 0.08));
1053
+
1054
+ position: relative;
1055
+ display: inline-flex;
1056
+ align-items: center;
1057
+ justify-content: center;
1058
+ gap: 0.5em;
1059
+ min-block-size: var(--btn-min-size);
1060
+ padding-block: var(--space-2);
1061
+ padding-block: max(var(--space-1), calc((var(--btn-min-size) - 1lh) / 2));
1062
+ padding-inline: var(--btn-pad-x);
1063
+ border: var(--border-width) solid var(--btn-border-color, var(--btn-border));
1064
+ border-block-end-color: var(
1065
+ --btn-border-block-end-color,
1066
+ var(--btn-border-color, var(--btn-border))
1067
+ );
1068
+ border-block-end-width: var(--btn-border-block-end-width, var(--border-width));
1069
+ border-radius: var(--btn-radius);
1070
+ background: var(--btn-bg);
1071
+ color: var(--btn-fg);
1072
+ font: inherit;
1073
+ font-size: var(--btn-font-size);
1074
+ font-weight: var(--font-weight-strong);
1075
+ line-height: 1;
1076
+ text-decoration: none;
1077
+ cursor: pointer;
1078
+ transition:
1079
+ background-color var(--duration),
1080
+ border-color var(--duration),
1081
+ color var(--duration),
1082
+ box-shadow var(--duration),
1083
+ transform var(--duration);
1084
+ }
1085
+
1086
+ @supports (color: color-mix(in oklch, red, white)) {
1087
+ .btn {
1088
+ --btn-focus-ring-color: color-mix(in oklch, var(--btn-focus-color) 28%, transparent);
1089
+ }
1090
+ }
1091
+
1092
+ /* .solid/.soft/.outline are shared via variants.css. The baseline --btn-*
1093
+ fallbacks make .btn solid by default; .ghost/.link are .btn-only variants. */
1094
+ .btn.ghost {
1095
+ --ui-bg: transparent;
1096
+ --ui-fg: var(--intent, var(--text));
1097
+ --ui-border: transparent;
1098
+ }
1099
+
1100
+ /* Solid/soft get the overlay darkening on hover; outline/ghost/link stay flat.
1101
+ Routed through a variable instead of a :not(.outline, .ghost, .link)
1102
+ exclusion so the hover rule below only has to state the interactive
1103
+ element list once. */
1104
+ .btn.outline,
1105
+ .btn.ghost,
1106
+ .btn.link {
1107
+ --btn-hover-overlay: transparent;
1108
+ }
1109
+
1110
+ /* The .soft/.outline/.ghost hover recipe (--ui-hover-bg) is shared with
1111
+ .badge in variants.css; the overlay darkening for solid/soft is .btn-specific. */
1112
+ @media (hover: hover) {
1113
+ .btn:not(:where(:disabled, [aria-disabled="true"])):hover {
1114
+ background: var(--btn-hover-bg);
1115
+ box-shadow: inset 0 0 0 999px var(--btn-hover-overlay);
1116
+ }
1117
+ }
1118
+
1119
+ .btn:not(:where(:disabled, [aria-disabled="true"])):active {
1120
+ transform: translateY(1px);
1121
+ }
1122
+
1123
+ .btn.link {
1124
+ --ui-bg: transparent;
1125
+ --ui-fg: var(--intent, var(--text));
1126
+ --ui-border: transparent;
1127
+ --btn-radius: var(--radius-sm);
1128
+ --btn-border-block-end-color: transparent;
1129
+ --btn-border-block-end-width: var(--border-width);
1130
+
1131
+ min-block-size: auto;
1132
+ padding-inline: 0;
1133
+ border-color: transparent;
1134
+ text-decoration: underline;
1135
+ text-underline-offset: 0.18em;
1136
+ }
1137
+
1138
+ /* .btn.circle needs no local rule: the .circle utility (utilities.css) loads
1139
+ after components and overrides border-radius at equal specificity. */
1140
+
1141
+ @media (hover: hover) {
1142
+ .btn.link:not(:where(:disabled, [aria-disabled="true"])):hover {
1143
+ background: transparent;
1144
+ color: var(--intent, var(--text));
1145
+ text-decoration-thickness: 0.12em;
1146
+ }
1147
+ }
1148
+
1149
+ .btn:focus-visible {
1150
+ outline: var(--btn-focus-outline-width) solid var(--btn-focus-color);
1151
+ outline-offset: var(--focus-outline-offset);
1152
+ box-shadow: 0 0 0 3px var(--btn-focus-ring-color, var(--focus-ring));
1153
+ }
1154
+
1155
+ .btn.link:focus-visible {
1156
+ outline-color: currentColor;
1157
+ outline-offset: 0.2em;
1158
+ }
1159
+
1160
+ .btn:disabled {
1161
+ cursor: not-allowed;
1162
+ opacity: var(--disabled-opacity);
1163
+ }
1164
+
1165
+ /* Anchors have no native disabled state.
1166
+ a.btn[aria-disabled="true"] only provides disabled styling and blocks pointer
1167
+ activation. For a truly disabled link, remove href or prevent activation in
1168
+ app code. */
1169
+ a.btn[aria-disabled="true"] {
1170
+ cursor: not-allowed;
1171
+ opacity: var(--disabled-opacity);
1172
+ pointer-events: none;
1173
+ }
1174
+
1175
+ /* Busy */
1176
+ .btn[aria-busy="true"]:not(:disabled) {
1177
+ cursor: progress;
1178
+ }
1179
+
1180
+ .btn[aria-busy="true"] > .spinner {
1181
+ --spinner-size: 1em;
1182
+ --spinner-width: 0.15em;
1183
+ }
1184
+
1185
+ /* Toggle */
1186
+ /* State must outrank the shared .outline/.ghost hover recipes: a pressed
1187
+ button keeps its intent background and foreground while hovered. */
1188
+ .btn:is([aria-pressed="true"], [aria-current]:not([aria-current="false"])):not(.link) {
1189
+ --btn-bg: var(--intent, var(--primary));
1190
+ --btn-fg: var(--intent-fg, var(--primary-fg));
1191
+ --btn-border: var(--intent, var(--primary));
1192
+ --ui-hover-bg: var(--btn-bg);
1193
+ }
1194
+
1195
+ @media (forced-colors: active) {
1196
+ .btn {
1197
+ border-color: ButtonText;
1198
+ background: ButtonFace;
1199
+ color: ButtonText;
1200
+ box-shadow: none;
1201
+ }
1202
+
1203
+ .btn.link {
1204
+ border-color: transparent;
1205
+ background: Canvas;
1206
+ color: LinkText;
1207
+ }
1208
+
1209
+ .btn:is([aria-pressed="true"], [aria-current]:not([aria-current="false"])):not(.link) {
1210
+ border-color: Highlight;
1211
+ background: Highlight;
1212
+ color: HighlightText;
1213
+ forced-color-adjust: none;
1214
+ }
1215
+
1216
+ @media (hover: hover) {
1217
+ .btn:hover:not(:where(:disabled, [aria-disabled="true"], .link)) {
1218
+ outline: var(--border-width) solid ButtonText;
1219
+ outline-offset: 2px;
1220
+ }
1221
+ }
1222
+
1223
+ .btn:disabled,
1224
+ a.btn[aria-disabled="true"] {
1225
+ border-color: GrayText;
1226
+ background: Canvas;
1227
+ color: GrayText;
1228
+ forced-color-adjust: auto;
1229
+ }
1230
+ }
1231
+
1232
+ /*
1233
+ * Badges for compact labels, counts, and state hints.
1234
+ *
1235
+ * Solid by default. Shared .soft/.outline variants stay available for quieter
1236
+ * tags without adding a badge-specific color API.
1237
+ */
1238
+ /* Intent boundary — inherited intents do not tint this component.
1239
+ Local intent classes still win because :where() has zero specificity. */
1240
+ :where(.badge) {
1241
+ /* @sync intent-boundary */
1242
+ --intent: initial;
1243
+ --intent-fg: initial;
1244
+ --ui-bg: initial;
1245
+ --ui-fg: initial;
1246
+ --ui-border: initial;
1247
+ --ui-hover-bg: initial;
1248
+ /* @sync intent-boundary */
1249
+ }
1250
+
1251
+ .badge {
1252
+ --badge-size: var(--variant-compact-size, 1.5rem);
1253
+ --badge-pad-x: 0.55em;
1254
+ --badge-font-size: var(--variant-compact-font-size, 0.8125rem);
1255
+ --badge-dot-size: 0.5rem;
1256
+ --badge-bg: var(--ui-bg, var(--intent, var(--neutral)));
1257
+ --badge-fg: var(--ui-fg, var(--intent-fg, var(--neutral-fg)));
1258
+ --badge-border: var(--ui-border, transparent);
1259
+ --badge-radius: 999px;
1260
+
1261
+ display: inline-flex;
1262
+ align-items: center;
1263
+ justify-content: center;
1264
+ gap: 0.35em;
1265
+ min-block-size: var(--badge-size);
1266
+ padding-inline: var(--badge-pad-x);
1267
+ border: var(--border-width) solid var(--badge-border);
1268
+ border-radius: var(--badge-radius);
1269
+ background: var(--badge-bg);
1270
+ color: var(--badge-fg);
1271
+ font-size: var(--badge-font-size);
1272
+ font-weight: var(--font-weight-strong);
1273
+ line-height: 1;
1274
+ text-decoration: none;
1275
+ white-space: nowrap;
1276
+ transition:
1277
+ background-color var(--duration),
1278
+ border-color var(--duration),
1279
+ color var(--duration),
1280
+ box-shadow var(--duration);
1281
+ }
1282
+
1283
+ .badge:empty {
1284
+ --badge-size: var(--badge-dot-size);
1285
+ padding: 0;
1286
+ border: 0;
1287
+ inline-size: var(--badge-size);
1288
+ block-size: var(--badge-size);
1289
+ }
1290
+
1291
+ .badge > button {
1292
+ display: inline-grid;
1293
+ place-items: center;
1294
+ flex: 0 0 auto;
1295
+ min-inline-size: 1.5rem;
1296
+ min-block-size: 1.5rem;
1297
+ margin-block: -0.25rem;
1298
+ margin-inline-start: -0.15em;
1299
+ margin-inline-end: -0.45em;
1300
+ padding: 0.25rem;
1301
+ border: 0;
1302
+ border-radius: var(--radius-full);
1303
+ background: transparent;
1304
+ color: inherit;
1305
+ font: inherit;
1306
+ line-height: 1;
1307
+ cursor: pointer;
1308
+ }
1309
+
1310
+ @media (hover: hover) {
1311
+ .badge > button:not(:disabled):hover {
1312
+ background: var(--hover-overlay);
1313
+ }
1314
+ }
1315
+
1316
+ /* .solid/.soft/.outline are shared via variants.css, including the
1317
+ .soft/.outline hover recipe (--ui-hover-bg). The --badge-* fallbacks make
1318
+ .badge solid by default. */
1319
+ @media (hover: hover) {
1320
+ :where(a.badge, button.badge):not(:where(:disabled, [aria-disabled="true"])):hover {
1321
+ background: var(--ui-hover-bg, var(--badge-bg));
1322
+ }
1323
+
1324
+ :where(a.badge, button.badge):not(:where(:disabled, [aria-disabled="true"], .outline)):hover {
1325
+ box-shadow: inset 0 0 0 999px var(--hover-overlay);
1326
+ }
1327
+ }
1328
+
1329
+ button.badge {
1330
+ cursor: pointer;
1331
+ font: inherit;
1332
+ }
1333
+
1334
+ button.badge:disabled,
1335
+ .badge[aria-disabled="true"] {
1336
+ opacity: var(--disabled-opacity);
1337
+ cursor: not-allowed;
1338
+ }
1339
+
1340
+ a.badge[aria-disabled="true"] {
1341
+ pointer-events: none;
1342
+ }
1343
+
1344
+ @media (forced-colors: active) {
1345
+ .badge {
1346
+ border-color: ButtonText;
1347
+ background: Canvas;
1348
+ color: CanvasText;
1349
+ box-shadow: none;
1350
+ }
1351
+
1352
+ .badge:empty {
1353
+ background: ButtonText;
1354
+ }
1355
+
1356
+ @media (hover: hover) {
1357
+ :where(a.badge, button.badge):hover:not(:where(:disabled, [aria-disabled="true"])) {
1358
+ outline: var(--border-width) solid ButtonText;
1359
+ outline-offset: 2px;
1360
+ }
1361
+
1362
+ .badge > button:not(:disabled):hover {
1363
+ outline: var(--border-width) solid currentColor;
1364
+ outline-offset: 1px;
1365
+ }
1366
+ }
1367
+
1368
+ button.badge:disabled,
1369
+ .badge[aria-disabled="true"] {
1370
+ border-color: GrayText;
1371
+ background: Canvas;
1372
+ color: GrayText;
1373
+ }
1374
+ }
1375
+
1376
+ /*
1377
+ * Alerts for inline feedback and status messages.
1378
+ *
1379
+ * Keeps alert defaults local while consuming the shared --ui-* variant
1380
+ * plumbing for emphasis.
1381
+ *
1382
+ * --alert-border-inline-start-color/-width are unset extension points, mirroring
1383
+ * --btn-border-block-end-* (button.css), for themes that give alerts a
1384
+ * callout-style colored flag on the leading edge instead of a uniform border.
1385
+ */
1386
+ /* Intent boundary — inherited intents do not tint this component.
1387
+ Local intent classes still win because :where() has zero specificity. */
1388
+ :where(.alert) {
1389
+ /* @sync intent-boundary */
1390
+ --intent: initial;
1391
+ --intent-fg: initial;
1392
+ --ui-bg: initial;
1393
+ --ui-fg: initial;
1394
+ --ui-border: initial;
1395
+ --ui-hover-bg: initial;
1396
+ /* @sync intent-boundary */
1397
+ }
1398
+
1399
+ .alert {
1400
+ --alert-pad-block: var(--variant-pad-block, var(--space-4));
1401
+ --alert-pad-inline: var(--space-4);
1402
+ --alert-font-size: var(--variant-font-size, var(--font-size-md));
1403
+ --alert-icon-size: var(--variant-icon-size, 1.25rem);
1404
+ --alert-bg: var(--surface-subtle);
1405
+ --alert-border: var(--border);
1406
+ --ui-bg: var(--alert-bg);
1407
+ --ui-border: var(--alert-border);
1408
+ --ui-fg: var(--intent, var(--text));
1409
+
1410
+ padding-block: var(--alert-pad-block);
1411
+ padding-inline: var(--alert-pad-inline);
1412
+ background: var(--ui-bg, transparent);
1413
+ color: var(--ui-fg, var(--text));
1414
+ border: var(--border-width) solid var(--ui-border, var(--border));
1415
+ border-inline-start-color: var(
1416
+ --alert-border-inline-start-color,
1417
+ var(--ui-border, var(--border))
1418
+ );
1419
+ border-inline-start-width: var(--alert-border-inline-start-width, var(--border-width));
1420
+ border-radius: var(--radius-lg);
1421
+ font-size: var(--alert-font-size);
1422
+ }
1423
+
1424
+ .alert > * {
1425
+ min-inline-size: 0;
1426
+ }
1427
+
1428
+ .alert > :first-child {
1429
+ margin-block-start: 0;
1430
+ }
1431
+
1432
+ .alert > :last-child {
1433
+ margin-block-end: 0;
1434
+ }
1435
+
1436
+ /* Alerts are soft by default. Use .solid or .outline for stronger emphasis. */
1437
+ .alert.solid {
1438
+ --ui-bg: var(--intent, var(--neutral));
1439
+ --ui-fg: var(--intent-fg, var(--neutral-fg));
1440
+ --ui-border: transparent;
1441
+ }
1442
+
1443
+ .alert.outline {
1444
+ --ui-bg: transparent;
1445
+ --ui-fg: var(--intent, var(--text));
1446
+ --ui-border: var(--intent, var(--border));
1447
+ }
1448
+
1449
+ @supports (color: color-mix(in oklch, red, white)) {
1450
+ .alert:where(:not(.solid, .outline)) {
1451
+ /* @sync soft-recipe */
1452
+ --ui-bg: color-mix(in oklch, var(--surface) var(--soft-bg-mix), var(--intent, var(--neutral)));
1453
+ --ui-border: color-mix(
1454
+ in oklch,
1455
+ var(--surface) var(--soft-border-mix),
1456
+ var(--intent, var(--neutral))
1457
+ );
1458
+ /* @sync soft-recipe */
1459
+ }
1460
+ }
1461
+
1462
+ /* Decorative leading icon */
1463
+ .alert > :first-child[aria-hidden="true"] {
1464
+ display: inline-block;
1465
+ flex: none;
1466
+ inline-size: var(--alert-icon-size);
1467
+ block-size: var(--alert-icon-size);
1468
+ font-size: var(--alert-icon-size);
1469
+ line-height: 1;
1470
+ vertical-align: -0.125em;
1471
+ }
1472
+
1473
+ @supports selector(.alert:has(> *)) {
1474
+ .alert:has(> :first-child[aria-hidden="true"]) {
1475
+ display: grid;
1476
+ grid-template-columns: auto minmax(0, 1fr);
1477
+ align-items: var(--items-align, start);
1478
+ column-gap: var(--space-3);
1479
+ }
1480
+
1481
+ .alert:has(> :first-child[aria-hidden="true"]) > :first-child {
1482
+ grid-column: 1;
1483
+ grid-row: 1 / -1;
1484
+ display: inline-grid;
1485
+ place-items: center;
1486
+ inline-size: var(--alert-icon-size);
1487
+ block-size: var(--alert-icon-size);
1488
+ margin-block-start: 0.125em;
1489
+ font-size: var(--alert-icon-size);
1490
+ line-height: 1;
1491
+ }
1492
+
1493
+ .alert:has(> :first-child[aria-hidden="true"]) > :first-child:is(svg, i) {
1494
+ inline-size: var(--alert-icon-size);
1495
+ block-size: var(--alert-icon-size);
1496
+ }
1497
+
1498
+ .alert:has(> :first-child[aria-hidden="true"]) > :not(:first-child) {
1499
+ grid-column: 2;
1500
+ }
1501
+ }
1502
+
1503
+ /* Links */
1504
+ .alert :where(a:not(.btn)) {
1505
+ color: inherit;
1506
+ font-weight: var(--font-weight-strong);
1507
+ }
1508
+
1509
+ /* Forced colors: no override needed. The always-on border gets a forced
1510
+ readable color, and text on a Highlight surface would trigger Chromium's
1511
+ Canvas backplate (unreadable) — so .solid intentionally falls back to the
1512
+ default look. */
1513
+
1514
+ /*
1515
+ * Actions — a horizontal action-button cluster.
1516
+ *
1517
+ * Composes with .btn (or any inline element) and feeds layout via the shared
1518
+ * --gap token.
1519
+ */
1520
+ .actions {
1521
+ --gap: var(--space-2);
1522
+
1523
+ display: flex;
1524
+ flex-wrap: wrap;
1525
+ align-items: center;
1526
+ gap: var(--gap);
1527
+ margin: 0;
1528
+ padding: 0;
1529
+ list-style: none;
1530
+ }
1531
+
1532
+ .actions > li {
1533
+ margin: 0;
1534
+ }
1535
+
1536
+
1537
+ /* Surfaces */
1538
+ /*
1539
+ * Cards provide a neutral raised surface for grouped content.
1540
+ *
1541
+ * Surface, border, radius, padding, density, and elevation stay local here.
1542
+ * Direct header/footer children are structural slots. Page layout remains the
1543
+ * job of markup and layout primitives outside the card.
1544
+ */
1545
+ /* Intent boundary — inherited intents do not tint this component.
1546
+ Local intent classes still win because :where() has zero specificity. */
1547
+ :where(.card) {
1548
+ /* @sync intent-boundary */
1549
+ --intent: initial;
1550
+ --intent-fg: initial;
1551
+ --ui-bg: initial;
1552
+ --ui-fg: initial;
1553
+ --ui-border: initial;
1554
+ --ui-hover-bg: initial;
1555
+ /* @sync intent-boundary */
1556
+ }
1557
+
1558
+ .card {
1559
+ --card-pad: var(--space-4);
1560
+ --card-radius: var(--radius-lg);
1561
+ --card-max-inline-size: none;
1562
+
1563
+ max-inline-size: var(--card-max-inline-size);
1564
+ padding: var(--card-pad);
1565
+ background: var(--ui-bg, var(--surface));
1566
+ color: var(--ui-fg, var(--text));
1567
+ border: var(--border-width) solid var(--ui-border, var(--border));
1568
+ border-radius: var(--card-radius);
1569
+ }
1570
+
1571
+ .card > * {
1572
+ min-inline-size: 0;
1573
+ }
1574
+
1575
+ .card > :where(header, footer) {
1576
+ display: flex;
1577
+ flex-wrap: wrap;
1578
+ align-items: var(--items-align, center);
1579
+ justify-content: space-between;
1580
+ gap: var(--space-3);
1581
+ min-inline-size: 0;
1582
+ }
1583
+
1584
+ .card > :where(header, footer) > * {
1585
+ min-inline-size: 0;
1586
+ margin-block: 0;
1587
+ }
1588
+
1589
+ .card > header {
1590
+ align-items: var(--items-align, flex-start);
1591
+ }
1592
+
1593
+ .card > :first-child {
1594
+ margin-block-start: 0;
1595
+ }
1596
+
1597
+ .card > :last-child {
1598
+ margin-block-end: 0;
1599
+ }
1600
+
1601
+ .card.raised {
1602
+ box-shadow: var(--shadow);
1603
+ }
1604
+ .card.subtle {
1605
+ background: var(--surface-subtle);
1606
+ }
1607
+ .card.inverted {
1608
+ background: var(--surface-solid);
1609
+ color: var(--surface);
1610
+ border-color: transparent;
1611
+ --heading: var(--surface);
1612
+ --busy-overlay-bg: var(--surface-solid);
1613
+ }
1614
+
1615
+ .card.compact {
1616
+ --card-pad: var(--space-2);
1617
+ }
1618
+
1619
+ /* Full-bleed children */
1620
+ .card > .bleed {
1621
+ margin-inline: calc(var(--card-pad) * -1);
1622
+ max-inline-size: none;
1623
+ }
1624
+
1625
+ .card > .bleed:first-child {
1626
+ margin-block-start: calc(var(--card-pad) * -1);
1627
+ border-start-start-radius: inherit;
1628
+ border-start-end-radius: inherit;
1629
+ }
1630
+
1631
+ .card > .bleed:last-child {
1632
+ margin-block-end: calc(var(--card-pad) * -1);
1633
+ border-end-start-radius: inherit;
1634
+ border-end-end-radius: inherit;
1635
+ }
1636
+
1637
+ /* Media bleed */
1638
+ .card > :where(img, picture, video, canvas, svg).bleed {
1639
+ display: block;
1640
+ inline-size: calc(100% + var(--card-pad) * 2);
1641
+ max-inline-size: none;
1642
+ }
1643
+
1644
+ .card > picture.bleed {
1645
+ overflow: hidden;
1646
+ }
1647
+
1648
+ .card > picture.bleed > img {
1649
+ display: block;
1650
+ inline-size: 100%;
1651
+ block-size: auto;
1652
+ }
1653
+
1654
+ /* Padded structural bleed */
1655
+ .card > :where(header, footer, figure).bleed {
1656
+ padding: var(--card-pad);
1657
+ }
1658
+
1659
+ @media (forced-colors: active) {
1660
+ .card {
1661
+ border-color: ButtonText;
1662
+ background: Canvas;
1663
+ color: CanvasText;
1664
+ }
1665
+
1666
+ .card.raised {
1667
+ box-shadow: none;
1668
+ }
1669
+
1670
+ .card.inverted {
1671
+ --heading: CanvasText;
1672
+
1673
+ border-color: ButtonText;
1674
+ background: Canvas;
1675
+ color: CanvasText;
1676
+ }
1677
+ }
1678
+
1679
+ /*
1680
+ * Surface — shared modal scrim and backdrop for flyout and drawer.
1681
+ *
1682
+ * Drawn by .drawer::backdrop (native dialog top-layer) and .surface-backdrop
1683
+ * (custom fixed div for flyout sheets on coarse pointers). The native
1684
+ * .modal::backdrop uses the same fill and animates opacity from 0 to 1
1685
+ * (modal.css), so the scrim strength stays consistent across the dialog
1686
+ * family.
1687
+ *
1688
+ * .surface-backdrop, .is-sheet, and .is-open are runtime markers set by
1689
+ * surface.js. Authors opt in via [data-flyout-mobile="sheet|auto|none|anchored"]
1690
+ * — the JS resolves "auto" into .is-sheet at run time.
1691
+ */
1692
+ /* Custom backdrop for overlays that do not use the native dialog top-layer.
1693
+ * Its z-index sits just below the flyout it accompanies. */
1694
+ .surface-backdrop {
1695
+ position: fixed;
1696
+ inset: 0;
1697
+ z-index: calc(var(--z-menu) - 1);
1698
+ background: var(--backdrop-fill);
1699
+ opacity: 1;
1700
+ transition: opacity var(--duration) var(--ease, ease);
1701
+ }
1702
+
1703
+ .surface-backdrop[hidden] {
1704
+ display: none;
1705
+ opacity: 0;
1706
+ }
1707
+
1708
+ @starting-style {
1709
+ .surface-backdrop:not([hidden]) {
1710
+ opacity: 0;
1711
+ }
1712
+ }
1713
+
1714
+ @supports (transition-behavior: allow-discrete) {
1715
+ .surface-backdrop {
1716
+ transition:
1717
+ opacity var(--duration) var(--ease, ease),
1718
+ display var(--duration) allow-discrete;
1719
+ }
1720
+ }
1721
+
1722
+ @media (forced-colors: active) {
1723
+ .surface-backdrop {
1724
+ background: Canvas;
1725
+ }
1726
+ }
1727
+
1728
+ /*
1729
+ * Modal shell styling for native dialog elements.
1730
+ *
1731
+ * Spacing, radius, transitions, and focus treatment come from shared tokens.
1732
+ * Opening behavior and intent-specific decisions stay outside this component.
1733
+ */
1734
+ dialog.modal {
1735
+ --modal-size: 32rem;
1736
+
1737
+ max-inline-size: min(100% - 2rem, var(--modal-size));
1738
+ max-block-size: calc(var(--viewport-block) - 2rem);
1739
+ border: var(--border-width) solid var(--border);
1740
+ border-radius: var(--radius-lg);
1741
+ background: var(--surface-raised);
1742
+ color: var(--text);
1743
+ box-shadow: var(--shadow);
1744
+ padding: 0;
1745
+ overflow: visible;
1746
+ opacity: 0;
1747
+ /*
1748
+ * Keep the open dialog root untransformed. Fixed overlays mounted inside a
1749
+ * modal dialog use viewport coordinates; transform/translate/scale on this
1750
+ * element would make them position against the dialog instead. The closed
1751
+ * state can still carry a small transform for enter/exit motion.
1752
+ */
1753
+ transform: translateY(0.35rem) scale(0.98);
1754
+ transition:
1755
+ opacity var(--duration),
1756
+ transform var(--duration);
1757
+ }
1758
+
1759
+ dialog.modal > form,
1760
+ dialog.modal > .stack {
1761
+ display: grid;
1762
+ gap: var(--space-4);
1763
+ max-block-size: calc(var(--viewport-block) - 2rem);
1764
+ overflow: auto;
1765
+ overscroll-behavior: contain;
1766
+ padding: var(--space-4);
1767
+ }
1768
+
1769
+ dialog.modal.scrollable > form,
1770
+ dialog.modal.scrollable > .stack {
1771
+ grid-template-rows: auto minmax(0, 1fr) auto;
1772
+ overflow: hidden;
1773
+ }
1774
+
1775
+ dialog.modal.scrollable > form > :not(header, footer),
1776
+ dialog.modal.scrollable > .stack > :not(header, footer) {
1777
+ min-block-size: 0;
1778
+ overflow: auto;
1779
+ overscroll-behavior: contain;
1780
+ }
1781
+
1782
+ dialog.modal header > :first-child,
1783
+ dialog.modal footer > :first-child {
1784
+ margin-block-start: 0;
1785
+ }
1786
+
1787
+ dialog.modal header > :last-child,
1788
+ dialog.modal footer > :last-child {
1789
+ margin-block-end: 0;
1790
+ }
1791
+
1792
+ dialog.modal footer {
1793
+ display: flex;
1794
+ flex-wrap: wrap;
1795
+ justify-content: flex-end;
1796
+ gap: var(--gap);
1797
+ }
1798
+
1799
+ dialog.modal[open] {
1800
+ opacity: 1;
1801
+ transform: none;
1802
+ }
1803
+
1804
+ @starting-style {
1805
+ dialog.modal[open] {
1806
+ opacity: 0;
1807
+ transform: translateY(0.35rem) scale(0.98);
1808
+ }
1809
+ }
1810
+
1811
+ dialog.modal::backdrop {
1812
+ background: var(--backdrop-fill);
1813
+ opacity: 0;
1814
+ transition: opacity var(--duration);
1815
+ }
1816
+
1817
+ @supports (transition-behavior: allow-discrete) {
1818
+ dialog.modal {
1819
+ transition:
1820
+ opacity var(--duration),
1821
+ transform var(--duration),
1822
+ overlay var(--duration) allow-discrete,
1823
+ display var(--duration) allow-discrete;
1824
+ }
1825
+
1826
+ dialog.modal::backdrop {
1827
+ transition:
1828
+ opacity var(--duration),
1829
+ overlay var(--duration) allow-discrete,
1830
+ display var(--duration) allow-discrete;
1831
+ }
1832
+ }
1833
+
1834
+ dialog.modal[open]::backdrop {
1835
+ opacity: 1;
1836
+ }
1837
+
1838
+ html.has-modal-open {
1839
+ overflow: hidden;
1840
+ }
1841
+
1842
+ html.has-modal-open.had-scrollbar {
1843
+ scrollbar-gutter: stable;
1844
+ }
1845
+
1846
+ @starting-style {
1847
+ dialog.modal[open]::backdrop {
1848
+ opacity: 0;
1849
+ }
1850
+ }
1851
+
1852
+ @media (prefers-reduced-motion: no-preference) {
1853
+ dialog.modal.is-static {
1854
+ animation: actual-modal-static 180ms var(--ease);
1855
+ }
1856
+
1857
+ ::view-transition-group(actual-dialog) {
1858
+ animation-duration: var(--duration);
1859
+ animation-timing-function: var(--ease);
1860
+ }
1861
+ }
1862
+
1863
+ @keyframes actual-modal-static {
1864
+ 0%,
1865
+ 100% {
1866
+ transform: none;
1867
+ }
1868
+
1869
+ 35% {
1870
+ transform: translateX(-0.35rem);
1871
+ }
1872
+
1873
+ 70% {
1874
+ transform: translateX(0.35rem);
1875
+ }
1876
+ }
1877
+
1878
+ @media (forced-colors: active) {
1879
+ dialog.modal {
1880
+ border-color: ButtonText;
1881
+ background: Canvas;
1882
+ color: CanvasText;
1883
+ box-shadow: none;
1884
+ }
1885
+
1886
+ dialog.modal::backdrop {
1887
+ background: Canvas;
1888
+ }
1889
+ }
1890
+
1891
+ /*
1892
+ * Drawer — modal side-sheet built on the native dialog element.
1893
+ *
1894
+ * Top-layer rendering, ::backdrop, Escape, and modal focus come from the
1895
+ * native dialog. CSS owns progressive enter/exit motion where browsers support
1896
+ * discrete display/overlay transitions. The drawer is a layout shell only —
1897
+ * navigation content inside uses .nav-link and .accordion, which carry their
1898
+ * own link/summary styling.
1899
+ */
1900
+ dialog.drawer {
1901
+ --drawer-size: 20rem;
1902
+ --drawer-pad: var(--space-4);
1903
+ --drawer-closed-translate: -100%;
1904
+
1905
+ inline-size: min(calc(var(--viewport-inline) * 0.9), var(--drawer-size));
1906
+ block-size: var(--viewport-block);
1907
+ max-inline-size: none;
1908
+ max-block-size: var(--viewport-block);
1909
+ margin: 0;
1910
+ padding: var(--drawer-pad);
1911
+ border: 0;
1912
+ border-inline-end: var(--border-width) solid var(--border);
1913
+ border-radius: 0;
1914
+ background: var(--surface-raised);
1915
+ color: var(--text);
1916
+ box-shadow: var(--shadow);
1917
+ opacity: 0;
1918
+ transform: translateX(var(--drawer-closed-translate));
1919
+ transition:
1920
+ opacity var(--duration-slow),
1921
+ transform var(--duration-slow);
1922
+ }
1923
+
1924
+ dialog.drawer[open] {
1925
+ display: grid;
1926
+ grid-template-rows: auto minmax(0, 1fr) auto;
1927
+ gap: var(--space-4);
1928
+ opacity: 1;
1929
+ transform: translateX(0);
1930
+ }
1931
+
1932
+ /* Backdrop color sourced from components/surface.css so it matches .modal
1933
+ and .surface-backdrop across the dialog family. */
1934
+ dialog.drawer::backdrop {
1935
+ background: var(--backdrop-fill);
1936
+ opacity: 0;
1937
+ transition: opacity var(--duration-slow);
1938
+ }
1939
+
1940
+ dialog.drawer[data-side="end"] {
1941
+ --drawer-closed-translate: 100%;
1942
+
1943
+ margin-inline-start: auto;
1944
+ margin-inline-end: 0;
1945
+ border-inline-end: 0;
1946
+ border-inline-start: var(--border-width) solid var(--border);
1947
+ }
1948
+
1949
+ dialog.drawer[open]::backdrop {
1950
+ opacity: 1;
1951
+ }
1952
+
1953
+ @starting-style {
1954
+ dialog.drawer[open] {
1955
+ opacity: 0;
1956
+ transform: translateX(var(--drawer-closed-translate));
1957
+ }
1958
+
1959
+ dialog.drawer[open]::backdrop {
1960
+ opacity: 0;
1961
+ }
1962
+ }
1963
+
1964
+ /* Scrollable navigation region inside the drawer. */
1965
+ dialog.drawer nav {
1966
+ min-block-size: 0;
1967
+ overflow-y: auto;
1968
+ }
1969
+
1970
+ @supports (transition-behavior: allow-discrete) {
1971
+ dialog.drawer {
1972
+ transition:
1973
+ opacity var(--duration-slow),
1974
+ transform var(--duration-slow),
1975
+ overlay var(--duration-slow) allow-discrete,
1976
+ display var(--duration-slow) allow-discrete;
1977
+ }
1978
+
1979
+ dialog.drawer::backdrop {
1980
+ transition:
1981
+ opacity var(--duration-slow),
1982
+ overlay var(--duration-slow) allow-discrete,
1983
+ display var(--duration-slow) allow-discrete;
1984
+ }
1985
+ }
1986
+
1987
+ /* RTL: swap closed transform directions so the drawer slides from the correct
1988
+ inline side. */
1989
+ [dir="rtl"] dialog.drawer {
1990
+ --drawer-closed-translate: 100%;
1991
+ }
1992
+
1993
+ [dir="rtl"] dialog.drawer[data-side="end"] {
1994
+ --drawer-closed-translate: -100%;
1995
+ }
1996
+
1997
+ @media (forced-colors: active) {
1998
+ dialog.drawer {
1999
+ border-inline-end-color: ButtonText;
2000
+ background: Canvas;
2001
+ color: CanvasText;
2002
+ box-shadow: none;
2003
+ }
2004
+
2005
+ dialog.drawer[data-side="end"] {
2006
+ border-inline-start-color: ButtonText;
2007
+ }
2008
+
2009
+ dialog.drawer::backdrop {
2010
+ background: Canvas;
2011
+ }
2012
+ }
2013
+
2014
+
2015
+ /* Overlays */
2016
+ /*
2017
+ * Flyout — floating panel for actions and links.
2018
+ *
2019
+ * JavaScript owns positioning (position: fixed + left/top), state
2020
+ * (hidden + .is-open), focus, and keyboard behavior. CSS owns the surfaced
2021
+ * panel appearance, item styles, and animation.
2022
+ *
2023
+ * Runtime markers (managed by surface.js): .is-open marks an open flyout,
2024
+ * .is-sheet marks "auto" resolved to bottom-sheet presentation on coarse
2025
+ * pointers. Authors opt into mobile behavior with
2026
+ * [data-flyout-mobile="sheet|auto|none|anchored"].
2027
+ * --flyout-trigger-width is set by surface.js while an open flyout is positioned.
2028
+ */
2029
+
2030
+ .flyout-trigger {
2031
+ position: relative;
2032
+ display: inline-block;
2033
+ }
2034
+
2035
+ /* Stretch: for a flyout trigger that must span its container — e.g. the
2036
+ last row of a full-width sidebar nav list. Stretches both the
2037
+ positioning wrapper and its trigger control. Named for the effect
2038
+ (stretch to fill), not the display value, to avoid reading like a
2039
+ generic display-block utility. */
2040
+ .flyout-trigger.stretch {
2041
+ display: block;
2042
+ inline-size: 100%;
2043
+ }
2044
+
2045
+ .flyout-trigger.stretch > [aria-haspopup="menu"] {
2046
+ inline-size: 100%;
2047
+ }
2048
+
2049
+ .flyout {
2050
+ z-index: var(--z-menu);
2051
+ /* No-JS fallback: below the trigger inside the relative .flyout-trigger.
2052
+ surface.js switches an open flyout to position: fixed with measured
2053
+ coordinates; this only covers a flyout revealed without the runtime. */
2054
+ position: absolute;
2055
+ inset-block-start: calc(100% + var(--space-1));
2056
+ inset-inline-start: 0;
2057
+ display: grid;
2058
+ gap: var(--space-1);
2059
+ inline-size: var(--flyout-inline-size, max-content);
2060
+ min-inline-size: min(
2061
+ var(--flyout-trigger-width, 0px),
2062
+ min(var(--flyout-max-inline-size, 20rem), calc(var(--viewport-inline) - var(--space-6)))
2063
+ );
2064
+ max-inline-size: min(
2065
+ var(--flyout-max-inline-size, 20rem),
2066
+ calc(var(--viewport-inline) - var(--space-6))
2067
+ );
2068
+ max-block-size: min(24rem, var(--available-height, calc(var(--viewport-block) - var(--space-6))));
2069
+ overflow: auto;
2070
+ margin: 0;
2071
+ padding: var(--space-1);
2072
+ border: var(--border-width) solid var(--border);
2073
+ border-radius: var(--radius);
2074
+ background: var(--surface-raised);
2075
+ box-shadow: var(--shadow-popout);
2076
+ list-style: none;
2077
+ opacity: 1;
2078
+ scale: 1;
2079
+ transition:
2080
+ opacity var(--duration-fast, var(--duration)) var(--ease, ease),
2081
+ scale var(--duration-fast, var(--duration)) var(--ease, ease),
2082
+ translate var(--duration-fast, var(--duration)) var(--ease, ease);
2083
+ }
2084
+
2085
+ .flyout.is-open:not(.is-sheet) {
2086
+ inset: auto;
2087
+ }
2088
+
2089
+ @starting-style {
2090
+ .flyout.is-open {
2091
+ opacity: 0;
2092
+ scale: 0.98;
2093
+ }
2094
+ }
2095
+
2096
+ /* Bottom-sheet presentation. surface.js toggles .is-sheet when the flyout is
2097
+ opened on coarse pointer / narrow viewports. */
2098
+ .flyout.is-sheet {
2099
+ position: fixed;
2100
+ inset-inline: 0;
2101
+ inset-block-start: auto;
2102
+ inset-block-end: 0;
2103
+ inline-size: auto;
2104
+ min-inline-size: 0;
2105
+ max-inline-size: none;
2106
+ max-block-size: min(calc(var(--viewport-block) * 0.75), 32rem);
2107
+ overflow: auto;
2108
+ grid-template-columns: 1fr;
2109
+ padding: max(var(--space-3), env(safe-area-inset-top)) var(--space-3)
2110
+ max(var(--space-3), env(safe-area-inset-bottom));
2111
+ border-end-start-radius: 0;
2112
+ border-end-end-radius: 0;
2113
+ }
2114
+
2115
+ .flyout[hidden] {
2116
+ display: none;
2117
+ opacity: 0;
2118
+ scale: 0.98;
2119
+ }
2120
+
2121
+ .flyout.is-sheet[hidden] {
2122
+ scale: 1;
2123
+ translate: 0 var(--space-3);
2124
+ }
2125
+
2126
+ @starting-style {
2127
+ .flyout.is-sheet.is-open {
2128
+ scale: 1;
2129
+ translate: 0 var(--space-3);
2130
+ }
2131
+ }
2132
+
2133
+ @supports (transition-behavior: allow-discrete) {
2134
+ .flyout {
2135
+ transition:
2136
+ opacity var(--duration-fast, var(--duration)) var(--ease, ease),
2137
+ scale var(--duration-fast, var(--duration)) var(--ease, ease),
2138
+ translate var(--duration-fast, var(--duration)) var(--ease, ease),
2139
+ display var(--duration-fast, var(--duration)) allow-discrete;
2140
+ }
2141
+ }
2142
+
2143
+ .flyout > li {
2144
+ margin: 0;
2145
+ }
2146
+
2147
+ .flyout > :where(section, footer) {
2148
+ min-inline-size: 0;
2149
+ padding: var(--space-2);
2150
+ }
2151
+
2152
+ .flyout > section > :where(h1, h2, h3, h4, h5, h6) {
2153
+ margin: 0;
2154
+ color: var(--text-muted);
2155
+ font-size: var(--font-size-xs);
2156
+ font-weight: var(--font-weight-strong);
2157
+ }
2158
+
2159
+ .flyout > section > ul {
2160
+ display: grid;
2161
+ gap: var(--space-1);
2162
+ margin: var(--space-1) 0 0;
2163
+ padding: 0;
2164
+ list-style: none;
2165
+ }
2166
+
2167
+ .flyout > section > ul > li {
2168
+ margin: 0;
2169
+ }
2170
+
2171
+ /* Item contract: a button, link, or menuitem placed directly in the flyout
2172
+ or directly in any of its <li> (bare list, or grouped section > ul > li).
2173
+ menu.js mirrors this contract in MENU_ITEM_SELECTOR — keep them in sync.
2174
+ :where(li) keeps specificity at a single class (0,1,0) so consumer
2175
+ overrides behave the same as a plain .flyout > rule. */
2176
+ :is(.flyout, .flyout :where(li)) > :where(button, a, [role="menuitem"]) {
2177
+ display: flex;
2178
+ align-items: center;
2179
+ justify-content: flex-start;
2180
+ gap: var(--space-2);
2181
+ inline-size: 100%;
2182
+ min-block-size: var(--control-size);
2183
+ padding-inline: var(--space-2);
2184
+ border: 0;
2185
+ border-radius: var(--radius-sm);
2186
+ background: none;
2187
+ color: var(--text);
2188
+ font: inherit;
2189
+ font-weight: var(--font-weight);
2190
+ text-decoration: none;
2191
+ text-align: start;
2192
+ white-space: nowrap;
2193
+ cursor: pointer;
2194
+ }
2195
+
2196
+ :is(.flyout.is-sheet, .flyout.is-sheet :where(li)) > :where(button, a, [role="menuitem"]) {
2197
+ min-block-size: var(--control-size-lg);
2198
+ }
2199
+
2200
+ :is(.flyout, .flyout :where(li)) > :where(button, a, [role="menuitem"]):focus-visible {
2201
+ background: var(--surface-subtle);
2202
+ outline: var(--focus-outline);
2203
+ outline-offset: var(--focus-outline-offset);
2204
+ box-shadow: var(--focus-ring-shadow);
2205
+ }
2206
+
2207
+ :is(.flyout, .flyout :where(li))
2208
+ > :where(button, a, [role="menuitem"]):is(:disabled, [aria-disabled="true"]) {
2209
+ cursor: not-allowed;
2210
+ opacity: var(--disabled-opacity);
2211
+ }
2212
+
2213
+ :is(.flyout, .flyout :where(li)) > :where(a, [role="menuitem"])[aria-disabled="true"] {
2214
+ pointer-events: none;
2215
+ }
2216
+
2217
+ @media (hover: hover) {
2218
+ :is(.flyout, .flyout :where(li))
2219
+ > :where(button, a, [role="menuitem"]):hover:not(
2220
+ :where(:focus-visible, :disabled, [aria-disabled="true"])
2221
+ ) {
2222
+ background: var(--surface-subtle);
2223
+ outline: none;
2224
+ }
2225
+ }
2226
+
2227
+ .flyout > hr,
2228
+ .flyout > li > hr {
2229
+ inline-size: 100%;
2230
+ margin: 0;
2231
+ margin-block: var(--space-1);
2232
+ border: 0;
2233
+ border-block-start: var(--border-width) solid var(--border);
2234
+ }
2235
+
2236
+ @media (forced-colors: active) {
2237
+ .flyout {
2238
+ border-color: ButtonText;
2239
+ background: Canvas;
2240
+ color: CanvasText;
2241
+ box-shadow: none;
2242
+ }
2243
+
2244
+ :is(.flyout, .flyout :where(li)) > :where(button, a, [role="menuitem"]):focus-visible {
2245
+ background: Canvas;
2246
+ color: CanvasText;
2247
+ }
2248
+
2249
+ @media (hover: hover) {
2250
+ :is(.flyout, .flyout :where(li))
2251
+ > :where(button, a, [role="menuitem"]):hover:not(:where(:disabled, [aria-disabled="true"])) {
2252
+ outline: var(--border-width) solid ButtonText;
2253
+ outline-offset: -1px;
2254
+ background: Canvas;
2255
+ color: CanvasText;
2256
+ }
2257
+ }
2258
+ }
2259
+
2260
+ /*
2261
+ * Tooltip — supplemental label overlaid near a trigger.
2262
+ *
2263
+ * Position, placement, and arrow position are managed by JavaScript
2264
+ * via position: fixed, left, top, data-placement, and --arrow-x.
2265
+ * Visibility is managed with hidden; CSS owns entry and exit animation.
2266
+ */
2267
+ .tooltip {
2268
+ --tooltip-bg: var(--surface-solid);
2269
+ --tooltip-fg: var(--surface);
2270
+ --tooltip-arrow-size: 5px;
2271
+
2272
+ position: fixed;
2273
+ z-index: var(--z-tooltip);
2274
+ max-inline-size: min(24rem, calc(var(--viewport-inline) - 2rem));
2275
+ padding: 0.25em 0.6em;
2276
+ border-radius: var(--radius-sm);
2277
+ background: var(--tooltip-bg);
2278
+ color: var(--tooltip-fg);
2279
+ font-size: var(--font-size-xs);
2280
+ line-height: var(--line-height-tight);
2281
+ overflow-wrap: anywhere;
2282
+ pointer-events: auto;
2283
+ user-select: none;
2284
+ opacity: 1;
2285
+ scale: 1;
2286
+ transition:
2287
+ opacity var(--duration-fast, var(--duration)) var(--ease, ease),
2288
+ scale var(--duration-fast, var(--duration)) var(--ease, ease);
2289
+ }
2290
+
2291
+ .tooltip[hidden] {
2292
+ display: none;
2293
+ opacity: 0;
2294
+ scale: 0.96;
2295
+ }
2296
+
2297
+ @starting-style {
2298
+ .tooltip:not([hidden]) {
2299
+ opacity: 0;
2300
+ scale: 0.96;
2301
+ }
2302
+ }
2303
+
2304
+ @supports (transition-behavior: allow-discrete) {
2305
+ .tooltip {
2306
+ transition:
2307
+ opacity var(--duration-fast, var(--duration)) var(--ease, ease),
2308
+ scale var(--duration-fast, var(--duration)) var(--ease, ease),
2309
+ display var(--duration-fast, var(--duration)) allow-discrete;
2310
+ }
2311
+ }
2312
+
2313
+ /* ── Arrow — clipped triangle ──────────────────────── */
2314
+
2315
+ .tooltip::before {
2316
+ content: "";
2317
+ position: absolute;
2318
+ inline-size: calc(var(--tooltip-arrow-size) * 2);
2319
+ block-size: var(--tooltip-arrow-size);
2320
+ background: var(--tooltip-bg);
2321
+ }
2322
+
2323
+ .tooltip[data-placement^="top"]::before {
2324
+ bottom: calc(-0.98 * var(--tooltip-arrow-size));
2325
+ left: var(--arrow-x, 50%);
2326
+ translate: -50% 0;
2327
+ clip-path: polygon(50% 100%, 0 0, 100% 0);
2328
+ }
2329
+
2330
+ .tooltip[data-placement^="bottom"]::before {
2331
+ top: calc(-0.98 * var(--tooltip-arrow-size));
2332
+ left: var(--arrow-x, 50%);
2333
+ translate: -50% 0;
2334
+ clip-path: polygon(50% 0, 0 100%, 100% 100%);
2335
+ }
2336
+
2337
+ .tooltip[data-placement^="left"]::before {
2338
+ right: calc(-0.98 * var(--tooltip-arrow-size));
2339
+ top: var(--arrow-y, 50%);
2340
+ inline-size: var(--tooltip-arrow-size);
2341
+ block-size: calc(var(--tooltip-arrow-size) * 2);
2342
+ translate: 0 -50%;
2343
+ clip-path: polygon(100% 50%, 0 0, 0 100%);
2344
+ }
2345
+
2346
+ .tooltip[data-placement^="right"]::before {
2347
+ left: calc(-0.98 * var(--tooltip-arrow-size));
2348
+ top: var(--arrow-y, 50%);
2349
+ inline-size: var(--tooltip-arrow-size);
2350
+ block-size: calc(var(--tooltip-arrow-size) * 2);
2351
+ translate: 0 -50%;
2352
+ clip-path: polygon(0 50%, 100% 0, 100% 100%);
2353
+ }
2354
+
2355
+ @media (forced-colors: active) {
2356
+ .tooltip {
2357
+ --tooltip-bg: Canvas;
2358
+ --tooltip-fg: CanvasText;
2359
+
2360
+ border: var(--border-width) solid ButtonText;
2361
+ }
2362
+
2363
+ .tooltip::before {
2364
+ display: none;
2365
+ }
2366
+ }
2367
+
2368
+
2369
+ /* Disclosure */
2370
+ /*
2371
+ * Accordion disclosures built on native details and summary.
2372
+ *
2373
+ * Keeps the clickable summary affordance aligned and usable without JavaScript
2374
+ * or demo styles. Decorative palettes and custom markers belong outside the
2375
+ * base component.
2376
+ */
2377
+ .accordion {
2378
+ --accordion-radius: var(--radius);
2379
+
2380
+ border: var(--border-width) solid var(--border);
2381
+ border-radius: var(--accordion-radius);
2382
+ background: var(--surface-raised);
2383
+ }
2384
+
2385
+ .accordion > details + details {
2386
+ border-block-start: var(--border-width) solid var(--border);
2387
+ }
2388
+
2389
+ .accordion summary {
2390
+ display: flex;
2391
+ align-items: center;
2392
+ gap: var(--space-3);
2393
+ cursor: pointer;
2394
+ list-style: none;
2395
+ padding: var(--space-4);
2396
+ color: var(--text);
2397
+ font-weight: var(--font-weight-bold);
2398
+ }
2399
+
2400
+ .accordion summary::-webkit-details-marker {
2401
+ display: none;
2402
+ }
2403
+
2404
+ .accordion summary::after {
2405
+ content: "";
2406
+ flex: 0 0 auto;
2407
+ margin-inline-start: auto;
2408
+ inline-size: 1rem;
2409
+ block-size: 1rem;
2410
+ background-color: var(--text-muted);
2411
+ -webkit-mask-image: var(--icon-chevron-mask);
2412
+ mask-image: var(--icon-chevron-mask);
2413
+ -webkit-mask-repeat: no-repeat;
2414
+ mask-repeat: no-repeat;
2415
+ -webkit-mask-size: contain;
2416
+ mask-size: contain;
2417
+ -webkit-mask-position: center;
2418
+ mask-position: center;
2419
+ transition: transform var(--duration);
2420
+ }
2421
+
2422
+ .accordion details[open] summary::after {
2423
+ transform: rotate(180deg);
2424
+ }
2425
+
2426
+ .accordion details > summary + * {
2427
+ padding-block: 0 var(--space-4);
2428
+ padding-inline: var(--space-4);
2429
+ color: var(--text-muted);
2430
+ margin-block: 0;
2431
+ }
2432
+
2433
+ .accordion details[open] > summary + * {
2434
+ padding-block-start: var(--space-1);
2435
+ }
2436
+
2437
+ .accordion details > summary + * > :first-child {
2438
+ margin-block-start: 0;
2439
+ }
2440
+
2441
+ .accordion details > summary + ul,
2442
+ .accordion details > summary + ol {
2443
+ margin: 0;
2444
+ padding-inline-start: 2rem;
2445
+ }
2446
+
2447
+ @media (forced-colors: active) {
2448
+ .accordion {
2449
+ border-color: ButtonText;
2450
+ background: Canvas;
2451
+ color: CanvasText;
2452
+ }
2453
+
2454
+ .accordion > details + details {
2455
+ border-block-start-color: ButtonText;
2456
+ }
2457
+
2458
+ .accordion summary {
2459
+ color: CanvasText;
2460
+ }
2461
+
2462
+ .accordion summary::after {
2463
+ background-color: ButtonText;
2464
+ forced-color-adjust: none;
2465
+ }
2466
+ }
2467
+
2468
+
2469
+ /* Feedback */
2470
+ /*
2471
+ * Spinner — compact loading feedback.
2472
+ *
2473
+ * Uses currentColor so it adapts to buttons, alerts, badges, and text. The
2474
+ * [aria-busy] overlay composition lives in components/busy.css.
2475
+ */
2476
+ /* Intent boundary — inherited intents do not tint this component.
2477
+ Local intent classes still win because :where() has zero specificity. */
2478
+ :where(.spinner) {
2479
+ /* @sync intent-boundary */
2480
+ --intent: initial;
2481
+ --intent-fg: initial;
2482
+ --ui-bg: initial;
2483
+ --ui-fg: initial;
2484
+ --ui-border: initial;
2485
+ --ui-hover-bg: initial;
2486
+ /* @sync intent-boundary */
2487
+ }
2488
+
2489
+ .spinner {
2490
+ --spinner-size: 1.5rem;
2491
+ --spinner-width: 2px;
2492
+ --spinner-track: currentColor;
2493
+ --spinner-value: transparent;
2494
+
2495
+ color: var(--intent, currentColor);
2496
+ display: inline-block;
2497
+ flex-shrink: 0;
2498
+ inline-size: var(--spinner-size);
2499
+ block-size: var(--spinner-size);
2500
+ border: var(--spinner-width) solid var(--spinner-track);
2501
+ border-inline-end-color: var(--spinner-value);
2502
+ border-radius: var(--radius-full);
2503
+ opacity: 0.85;
2504
+ animation: actual-spin var(--duration-spin) linear infinite;
2505
+ vertical-align: text-bottom;
2506
+ }
2507
+
2508
+ @supports (color: color-mix(in oklch, red, white)) {
2509
+ .spinner {
2510
+ --spinner-track: color-mix(in oklch, currentColor, transparent 70%);
2511
+ --spinner-value: currentColor;
2512
+ opacity: 1;
2513
+ }
2514
+ }
2515
+
2516
+ .spinner.sm {
2517
+ --spinner-size: 1rem;
2518
+ }
2519
+
2520
+ .spinner.lg {
2521
+ --spinner-size: 3rem;
2522
+ --spinner-width: 3px;
2523
+ }
2524
+
2525
+ @keyframes actual-spin {
2526
+ to {
2527
+ transform: rotate(360deg);
2528
+ }
2529
+ }
2530
+
2531
+ /*
2532
+ * Skeleton placeholders for loading states.
2533
+ *
2534
+ * Shows approximate loading structure without implying real content hierarchy.
2535
+ * Geometry, animation, and color stay theme-aware.
2536
+ */
2537
+ .skeleton {
2538
+ --skeleton-size: 1rem;
2539
+ --skeleton-width: 100%;
2540
+ --skeleton-radius: var(--radius);
2541
+ --skeleton-track: var(--surface-subtle);
2542
+ --skeleton-highlight: var(--surface-raised);
2543
+
2544
+ display: block;
2545
+ inline-size: var(--skeleton-width);
2546
+ max-inline-size: 100%;
2547
+ block-size: var(--skeleton-size);
2548
+ border-radius: var(--skeleton-radius);
2549
+ background:
2550
+ linear-gradient(90deg, var(--skeleton-track), var(--skeleton-highlight), var(--skeleton-track))
2551
+ 0 0 / 200% 100%;
2552
+ }
2553
+
2554
+ @media (prefers-reduced-motion: no-preference) {
2555
+ .skeleton {
2556
+ animation: actual-skeleton var(--duration-shimmer) ease-in-out infinite;
2557
+ }
2558
+ }
2559
+
2560
+ .skeleton[data-shape="text"] {
2561
+ --skeleton-size: 0.85rem;
2562
+ --skeleton-width: 32rem;
2563
+ }
2564
+
2565
+ .skeleton[data-shape="title"] {
2566
+ --skeleton-size: 1.4rem;
2567
+ --skeleton-width: 18rem;
2568
+ }
2569
+
2570
+ .skeleton[data-shape="avatar"] {
2571
+ --skeleton-size: var(--avatar-size, 2.25rem);
2572
+ --skeleton-width: var(--skeleton-size);
2573
+ --skeleton-radius: var(--radius-full);
2574
+
2575
+ flex: 0 0 auto;
2576
+ }
2577
+
2578
+ .skeleton[data-shape="box"] {
2579
+ --skeleton-size: 8rem;
2580
+ }
2581
+
2582
+ @media (forced-colors: active) {
2583
+ .skeleton {
2584
+ border: var(--border-width) solid ButtonText;
2585
+ background: GrayText;
2586
+ }
2587
+ }
2588
+
2589
+ @keyframes actual-skeleton {
2590
+ from {
2591
+ background-position: 200% 0;
2592
+ }
2593
+
2594
+ to {
2595
+ background-position: -200% 0;
2596
+ }
2597
+ }
2598
+
2599
+ /*
2600
+ * Busy overlay — non-button loading state.
2601
+ *
2602
+ * A non-button element with aria-busy="true" and a direct last-child .spinner
2603
+ * shows that spinner as a centered overlay over a faded surface. Buttons keep
2604
+ * their spinners inline (e.g. .btn[aria-busy] > .spinner stays inline-flex).
2605
+ */
2606
+ [aria-busy] > .spinner:last-child {
2607
+ display: none;
2608
+ }
2609
+
2610
+ [aria-busy="true"] > .spinner:last-child {
2611
+ display: inline-block;
2612
+ }
2613
+
2614
+ @supports selector([aria-busy="true"]:has(> .spinner:last-child)) {
2615
+ [aria-busy="true"]:not(.btn):has(> .spinner:last-child) {
2616
+ position: relative;
2617
+ }
2618
+
2619
+ [aria-busy="true"]:not(.btn):has(> .spinner:last-child)::before {
2620
+ content: "";
2621
+ position: absolute;
2622
+ inset: 0;
2623
+ z-index: 1;
2624
+ border-radius: inherit;
2625
+ background: var(--busy-overlay-bg, var(--surface));
2626
+ }
2627
+
2628
+ [aria-busy="true"]:not(.btn) > .spinner:last-child {
2629
+ position: absolute;
2630
+ inset: 0;
2631
+ z-index: 2;
2632
+ margin: auto;
2633
+ display: block;
2634
+ }
2635
+ }
2636
+
2637
+ @supports (selector([aria-busy="true"]:has(> .spinner:last-child))) and
2638
+ (color: color-mix(in oklch, red, white)) {
2639
+ [aria-busy="true"]:not(.btn):has(> .spinner:last-child)::before {
2640
+ background: color-mix(in oklch, var(--busy-overlay-bg, var(--surface)), transparent 20%);
2641
+ }
2642
+ }
2643
+
2644
+ /*
2645
+ * Progress — native progress bar with theme-aware track and value.
2646
+ *
2647
+ * Consumes the shared --intent/--primary for the value bar and --bar-height
2648
+ * for thickness. Indeterminate state animates a shimmer using --duration-shimmer.
2649
+ */
2650
+ /* Intent boundary — inherited intents do not tint this component.
2651
+ Local intent classes still win because :where() has zero specificity. */
2652
+ :where(.progress) {
2653
+ /* @sync intent-boundary */
2654
+ --intent: initial;
2655
+ --intent-fg: initial;
2656
+ --ui-bg: initial;
2657
+ --ui-fg: initial;
2658
+ --ui-border: initial;
2659
+ --ui-hover-bg: initial;
2660
+ /* @sync intent-boundary */
2661
+ }
2662
+
2663
+ .progress {
2664
+ --progress-track: var(--surface-subtle);
2665
+ --progress-value: var(--intent, var(--primary));
2666
+ }
2667
+
2668
+ .progress {
2669
+ appearance: none;
2670
+ -webkit-appearance: none;
2671
+ display: block;
2672
+ inline-size: 100%;
2673
+ block-size: var(--bar-height);
2674
+ overflow: hidden;
2675
+ border: 0;
2676
+ border-radius: var(--radius-full);
2677
+ background: var(--progress-track);
2678
+ }
2679
+
2680
+ .progress::-webkit-progress-bar {
2681
+ block-size: var(--bar-height);
2682
+ border-radius: var(--radius-full);
2683
+ background: var(--progress-track);
2684
+ }
2685
+
2686
+ .progress::-webkit-progress-value {
2687
+ block-size: var(--bar-height);
2688
+ border-radius: var(--radius-full);
2689
+ background: var(--progress-value);
2690
+ }
2691
+
2692
+ .progress::-moz-progress-bar {
2693
+ block-size: var(--bar-height);
2694
+ border-radius: var(--radius-full);
2695
+ background: var(--progress-value);
2696
+ }
2697
+
2698
+ /* Indeterminate progress */
2699
+ .progress:indeterminate {
2700
+ background:
2701
+ linear-gradient(to right, var(--progress-value) 30%, var(--progress-track) 30%) top left / 150%
2702
+ 150% no-repeat,
2703
+ var(--progress-track);
2704
+ }
2705
+
2706
+ .progress:indeterminate::-webkit-progress-bar {
2707
+ background: transparent;
2708
+ }
2709
+
2710
+ .progress:indeterminate::-webkit-progress-value {
2711
+ background: transparent;
2712
+ }
2713
+
2714
+ .progress:indeterminate::-moz-progress-bar {
2715
+ background: transparent;
2716
+ }
2717
+
2718
+ @media (prefers-reduced-motion: no-preference) {
2719
+ .progress:indeterminate {
2720
+ animation: actual-progress-indeterminate var(--duration-shimmer) ease-in-out infinite;
2721
+ }
2722
+
2723
+ :where([dir="rtl"] .progress:indeterminate) {
2724
+ animation-direction: reverse;
2725
+ }
2726
+ }
2727
+
2728
+ @keyframes actual-progress-indeterminate {
2729
+ 0% {
2730
+ background-position: 200% 0;
2731
+ }
2732
+
2733
+ 20% {
2734
+ background-position: 100% 0;
2735
+ }
2736
+
2737
+ 80% {
2738
+ background-position: -100% 0;
2739
+ }
2740
+
2741
+ 100% {
2742
+ background-position: -200% 0;
2743
+ }
2744
+ }
2745
+
2746
+ @media (forced-colors: active) {
2747
+ .progress {
2748
+ border: var(--border-width) solid ButtonText;
2749
+ background: Canvas;
2750
+ }
2751
+
2752
+ .progress::-webkit-progress-bar {
2753
+ background: Canvas;
2754
+ }
2755
+
2756
+ .progress::-webkit-progress-value {
2757
+ background: Highlight;
2758
+ forced-color-adjust: none;
2759
+ }
2760
+
2761
+ .progress::-moz-progress-bar {
2762
+ background: Highlight;
2763
+ forced-color-adjust: none;
2764
+ }
2765
+
2766
+ .progress:indeterminate {
2767
+ background: Canvas;
2768
+ }
2769
+ }
2770
+
2771
+ /*
2772
+ * Status bar — singleton floating status area.
2773
+ *
2774
+ * One element in the HTML, empty by default, hidden when :empty but kept in the
2775
+ * accessibility tree so it stays a reliable live region. JavaScript only sets
2776
+ * text content; the visual shows and hides with the content.
2777
+ *
2778
+ * Consumes intent like badge or alert. No need for status-bar-specific modifier
2779
+ * classes — .danger / .success / .warning / .neutral from intents.css drive
2780
+ * the coloring via --intent / --intent-fg. status.js applies these as plain
2781
+ * classes on the target; it does not know their names or that they exist.
2782
+ *
2783
+ * Public API: .status-bar + [data-status], both required — status.js reads
2784
+ * the class name from js/selectors.js rather than hardcoding it, but still
2785
+ * needs it to scope its document-wide lookup away from unrelated elements
2786
+ * that carry their own generic `data-status`.
2787
+ */
2788
+
2789
+ /* Intent boundary — inherited intents do not tint this component.
2790
+ Local intent classes still win because :where() has zero specificity. */
2791
+ :where(.status-bar) {
2792
+ /* @sync intent-boundary */
2793
+ --intent: initial;
2794
+ --intent-fg: initial;
2795
+ --ui-bg: initial;
2796
+ --ui-fg: initial;
2797
+ --ui-border: initial;
2798
+ --ui-hover-bg: initial;
2799
+ /* @sync intent-boundary */
2800
+ }
2801
+
2802
+ .status-bar {
2803
+ --status-bg: var(--intent, var(--text));
2804
+ --status-fg: var(--intent-fg, var(--surface));
2805
+
2806
+ position: fixed;
2807
+ inset-block-end: var(--space-4);
2808
+ inset-inline: var(--space-4);
2809
+ z-index: var(--z-status, 60);
2810
+
2811
+ display: inline-flex;
2812
+ align-items: center;
2813
+ justify-content: center;
2814
+ gap: var(--space-2);
2815
+
2816
+ inline-size: fit-content;
2817
+ max-inline-size: min(calc(100vw - var(--space-6)), 32rem);
2818
+ margin-inline: auto;
2819
+ overflow-wrap: anywhere;
2820
+
2821
+ padding-block: var(--space-2);
2822
+ padding-inline: var(--space-3);
2823
+
2824
+ border-radius: var(--radius-full);
2825
+ background: var(--status-bg);
2826
+ color: var(--status-fg);
2827
+ box-shadow: var(--shadow-popout);
2828
+
2829
+ opacity: 1;
2830
+ scale: 1;
2831
+ translate: 0 0;
2832
+ pointer-events: none;
2833
+
2834
+ transition:
2835
+ opacity var(--duration-slow) var(--ease, ease),
2836
+ scale var(--duration-slow) var(--ease, ease),
2837
+ translate var(--duration-slow) var(--ease, ease);
2838
+ }
2839
+
2840
+ .status-bar:empty {
2841
+ opacity: 0;
2842
+ scale: 0.96;
2843
+ translate: 0 var(--space-2);
2844
+ pointer-events: none;
2845
+ }
2846
+
2847
+ @media (forced-colors: active) {
2848
+ .status-bar {
2849
+ border: var(--border-width) solid ButtonText;
2850
+ box-shadow: none;
2851
+ }
2852
+ }
2853
+
2854
+
2855
+ /* Display */
2856
+ /*
2857
+ * Avatars for initials, images, links, and buttons.
2858
+ *
2859
+ * The visual box stays the same across semantic elements. Grouping and status
2860
+ * dots (an empty .badge child) compose around the avatar without changing its
2861
+ * meaning.
2862
+ */
2863
+ /* Intent boundary — inherited intents do not tint this component.
2864
+ Local intent classes still win because :where() has zero specificity. */
2865
+ :where(.avatar) {
2866
+ /* @sync intent-boundary */
2867
+ --intent: initial;
2868
+ --intent-fg: initial;
2869
+ --ui-bg: initial;
2870
+ --ui-fg: initial;
2871
+ --ui-border: initial;
2872
+ --ui-hover-bg: initial;
2873
+ /* @sync intent-boundary */
2874
+ }
2875
+
2876
+ .avatar {
2877
+ --avatar-size: var(--avatar-stack-size, 2.25rem);
2878
+ --avatar-radius: var(--radius-full);
2879
+ --avatar-font-size: var(--avatar-stack-font-size, var(--font-size-sm));
2880
+ --avatar-bg: var(--intent, var(--surface-subtle));
2881
+ --avatar-fg: var(--intent-fg, var(--tone-fg, var(--text-muted)));
2882
+
2883
+ position: relative;
2884
+ display: inline-grid;
2885
+ place-items: center;
2886
+ inline-size: var(--avatar-size);
2887
+ block-size: var(--avatar-size);
2888
+ overflow: visible;
2889
+ padding: 0;
2890
+ border: 0;
2891
+ border-radius: var(--avatar-radius);
2892
+ background: var(--avatar-bg);
2893
+ color: var(--avatar-fg);
2894
+ font: inherit;
2895
+ font-size: var(--avatar-font-size);
2896
+ font-weight: var(--font-weight-strong);
2897
+ line-height: 1;
2898
+ text-decoration: none;
2899
+ vertical-align: middle;
2900
+ user-select: none;
2901
+ transition:
2902
+ box-shadow var(--duration),
2903
+ opacity var(--duration),
2904
+ transform var(--duration);
2905
+ }
2906
+
2907
+ /* Forced colors wipe the background and the stack ring (box-shadow), so the
2908
+ shape needs a real border to stay visible. */
2909
+ @media (forced-colors: active) {
2910
+ .avatar {
2911
+ border: var(--border-width) solid CanvasText;
2912
+ }
2913
+
2914
+ :where(a.avatar, button.avatar)::after {
2915
+ display: none;
2916
+ }
2917
+ }
2918
+
2919
+ .avatar > :first-child:not(:where(img, picture, .badge)) {
2920
+ display: inline-grid;
2921
+ place-items: center;
2922
+ inline-size: 100%;
2923
+ block-size: 100%;
2924
+ overflow: hidden;
2925
+ border-radius: inherit;
2926
+ }
2927
+
2928
+ .avatar > img,
2929
+ .avatar > picture {
2930
+ display: block;
2931
+ inline-size: 100%;
2932
+ block-size: 100%;
2933
+ overflow: hidden;
2934
+ border-radius: inherit;
2935
+ }
2936
+
2937
+ .avatar > img,
2938
+ .avatar > picture > img {
2939
+ inline-size: 100%;
2940
+ block-size: 100%;
2941
+ object-fit: cover;
2942
+ border-radius: inherit;
2943
+ }
2944
+
2945
+ .avatar > picture > img {
2946
+ display: block;
2947
+ }
2948
+
2949
+ button.avatar {
2950
+ appearance: none;
2951
+ cursor: pointer;
2952
+ }
2953
+
2954
+ :where(a.avatar, button.avatar)::after {
2955
+ content: "";
2956
+ position: absolute;
2957
+ inset: 0;
2958
+ border-radius: inherit;
2959
+ background: var(--hover-overlay);
2960
+ opacity: 0;
2961
+ pointer-events: none;
2962
+ transition: opacity var(--duration);
2963
+ }
2964
+
2965
+ @media (hover: hover) {
2966
+ :where(a.avatar, button.avatar):not(:where(:disabled, [aria-disabled="true"])):hover::after {
2967
+ opacity: 1;
2968
+ }
2969
+ }
2970
+
2971
+ :where(a.avatar, button.avatar):not(:where(:disabled, [aria-disabled="true"])):active {
2972
+ transform: translateY(1px);
2973
+ }
2974
+
2975
+ button.avatar:disabled,
2976
+ a.avatar[aria-disabled="true"] {
2977
+ cursor: not-allowed;
2978
+ opacity: var(--disabled-opacity);
2979
+ }
2980
+
2981
+ a.avatar[aria-disabled="true"] {
2982
+ pointer-events: none;
2983
+ }
2984
+
2985
+ .avatar.sm {
2986
+ --avatar-stack-size: 1.75rem;
2987
+ --avatar-stack-font-size: var(--font-size-xs);
2988
+ }
2989
+
2990
+ .avatar.lg {
2991
+ --avatar-stack-size: 3rem;
2992
+ --avatar-stack-font-size: var(--font-size-lg);
2993
+ }
2994
+
2995
+ /* Status dot: an empty .badge positioned at the avatar's bottom-end corner.
2996
+ The dot conveys no text — always pair it with an aria-label on the badge. */
2997
+ .avatar > .badge:empty {
2998
+ --avatar-status-translate-x: 50%;
2999
+
3000
+ position: absolute;
3001
+ inset-block-end: var(--indicator-offset);
3002
+ inset-inline-end: var(--indicator-offset);
3003
+ z-index: 1;
3004
+ box-shadow: 0 0 0 2px var(--indicator-ring);
3005
+ transform: translate(var(--avatar-status-translate-x), 50%);
3006
+ }
3007
+
3008
+ .avatar:dir(rtl) > .badge:empty {
3009
+ --avatar-status-translate-x: -50%;
3010
+ }
3011
+
3012
+ /* Avatar stack — explicit class for overlapping avatar groups.
3013
+ Does not rely on role="group" auto-detection. */
3014
+ .avatar-stack {
3015
+ --avatar-stack-overlap: calc(var(--avatar-stack-size, 2.25rem) / -4);
3016
+ --avatar-stack-ring: 2px;
3017
+
3018
+ display: inline-flex;
3019
+ align-items: center;
3020
+ }
3021
+
3022
+ .avatar-stack.sm {
3023
+ --avatar-stack-size: 1.75rem;
3024
+ --avatar-stack-font-size: var(--font-size-xs);
3025
+ }
3026
+
3027
+ .avatar-stack.lg {
3028
+ --avatar-stack-size: 3rem;
3029
+ --avatar-stack-font-size: var(--font-size-lg);
3030
+ }
3031
+
3032
+ @supports selector(:has(> .avatar)) {
3033
+ .avatar-stack:has(> .avatar.sm) {
3034
+ --avatar-stack-size: 1.75rem;
3035
+ --avatar-stack-font-size: var(--font-size-xs);
3036
+ }
3037
+
3038
+ .avatar-stack:has(> .avatar.lg) {
3039
+ --avatar-stack-size: 3rem;
3040
+ --avatar-stack-font-size: var(--font-size-lg);
3041
+ }
3042
+ }
3043
+
3044
+ .avatar-stack > .avatar {
3045
+ box-shadow: 0 0 0 var(--avatar-stack-ring) var(--surface);
3046
+ }
3047
+
3048
+ .avatar-stack > .avatar + .avatar {
3049
+ margin-inline-start: var(--avatar-stack-overlap);
3050
+ }
3051
+
3052
+ .avatar-stack > .avatar:is(:hover, :focus-visible) {
3053
+ z-index: 1;
3054
+ }
3055
+
3056
+ /*
3057
+ * Key — keyboard keys and shortcut tokens for UI chrome.
3058
+ *
3059
+ * Use .key outside prose. Plain kbd inside .prose gets the same visual
3060
+ * treatment from prose.css so authored content stays convenient.
3061
+ */
3062
+ .key {
3063
+ display: inline-flex;
3064
+ align-items: center;
3065
+ justify-content: center;
3066
+ min-inline-size: 1.75em;
3067
+ min-block-size: 1.6em;
3068
+ padding-inline: 0.45em;
3069
+ border: var(--border-width) solid var(--border);
3070
+ border-block-end-width: calc(var(--border-width) * 2);
3071
+ border-radius: var(--radius-sm);
3072
+ background: var(--surface-raised);
3073
+ color: var(--text);
3074
+ font-family: var(--font-mono);
3075
+ font-size: 0.875em;
3076
+ font-weight: var(--font-weight-medium);
3077
+ line-height: 1;
3078
+ white-space: nowrap;
3079
+ vertical-align: 0.08em;
3080
+ }
3081
+
3082
+ /*
3083
+ * Overline — editorial small-cap label for section openers.
3084
+ *
3085
+ * Base form is unstyled chrome that takes its surrounding color. Add .pill for
3086
+ * the bordered rounded shape, and combine with an intent class (.primary,
3087
+ * .success, etc.) to tint the text, border, and background.
3088
+ */
3089
+ /* Intent boundary — inherited intents do not tint this component.
3090
+ Local intent classes still win because :where() has zero specificity. */
3091
+ :where(.overline) {
3092
+ /* @sync intent-boundary */
3093
+ --intent: initial;
3094
+ --intent-fg: initial;
3095
+ --ui-bg: initial;
3096
+ --ui-fg: initial;
3097
+ --ui-border: initial;
3098
+ --ui-hover-bg: initial;
3099
+ /* @sync intent-boundary */
3100
+ }
3101
+
3102
+ .overline {
3103
+ align-self: flex-start;
3104
+ color: var(--text-muted);
3105
+ font-size: var(--font-size-sm);
3106
+ font-weight: var(--font-weight-medium);
3107
+ letter-spacing: 0.04em;
3108
+ text-transform: uppercase;
3109
+ }
3110
+
3111
+ .overline.pill {
3112
+ display: inline-flex;
3113
+ align-items: center;
3114
+ gap: 0.5em;
3115
+ padding-inline: 0.6em;
3116
+ padding-block: 0.25em;
3117
+ color: var(--intent, var(--text-muted));
3118
+ border: var(--border-width) solid var(--border);
3119
+ border-radius: var(--radius-full);
3120
+ background: var(--surface);
3121
+ }
3122
+
3123
+ /* Tinted pill surface where color-mix is available; the flat fallback above
3124
+ keeps a readable bordered pill everywhere else. */
3125
+ @supports (color: color-mix(in oklch, red, white)) {
3126
+ .overline.pill {
3127
+ border-color: color-mix(in oklch, var(--intent, var(--border)) 35%, var(--border));
3128
+ background: color-mix(in oklch, var(--intent, transparent) 6%, var(--surface));
3129
+ }
3130
+ }
3131
+
3132
+
3133
+ /* Navigation */
3134
+ /*
3135
+ * Navbar — top-level navigation bar.
3136
+ *
3137
+ * .navbar is a horizontal bar; .navbar-brand is the brand link; .navbar-nav is
3138
+ * the inline link list using .nav-link items. .nav-list is the vertical nav
3139
+ * list shared with .drawer, fed by the shared --gap token.
3140
+ */
3141
+ .navbar {
3142
+ display: flex;
3143
+ flex-wrap: wrap;
3144
+ align-items: center;
3145
+ justify-content: space-between;
3146
+ gap: var(--gap);
3147
+ padding-block: var(--space-3);
3148
+ padding-inline: var(--space-4);
3149
+ border: var(--border-width) solid var(--border);
3150
+ border-radius: var(--radius-lg);
3151
+ background: var(--surface-raised);
3152
+ color: var(--text);
3153
+ }
3154
+
3155
+ .navbar-brand {
3156
+ display: inline-flex;
3157
+ align-items: center;
3158
+ gap: var(--space-2);
3159
+ color: var(--text);
3160
+ font-weight: var(--font-weight-bold);
3161
+ text-decoration: none;
3162
+ }
3163
+
3164
+ .navbar-brand:focus-visible {
3165
+ border-radius: var(--radius-sm);
3166
+ }
3167
+
3168
+ .navbar-nav {
3169
+ display: flex;
3170
+ flex-wrap: wrap;
3171
+ align-items: center;
3172
+ gap: var(--space-1);
3173
+ margin: 0;
3174
+ padding: 0;
3175
+ list-style: none;
3176
+ }
3177
+
3178
+ .nav-link {
3179
+ display: inline-flex;
3180
+ align-items: center;
3181
+ min-block-size: var(--control-size-sm);
3182
+ padding-inline: var(--space-2);
3183
+ border-radius: var(--radius);
3184
+ color: var(--text-muted);
3185
+ font-weight: var(--font-weight-medium);
3186
+ text-decoration: none;
3187
+ }
3188
+
3189
+ .nav-link:where([aria-current]:not([aria-current="false"])) {
3190
+ background: var(--surface-subtle);
3191
+ color: var(--text);
3192
+ }
3193
+
3194
+ @media (hover: hover) {
3195
+ .nav-link:hover {
3196
+ background: var(--surface-subtle);
3197
+ color: var(--text);
3198
+ }
3199
+ }
3200
+
3201
+ /* Nav list — a vertical navigation list. Used by .drawer for stacked links. */
3202
+ .nav-list {
3203
+ display: grid;
3204
+ gap: var(--gap);
3205
+ margin: 0;
3206
+ padding: 0;
3207
+ list-style: none;
3208
+ --gap: var(--space-2);
3209
+ }
3210
+
3211
+ .nav-list > li {
3212
+ margin: 0;
3213
+ }
3214
+
3215
+ @media (forced-colors: active) {
3216
+ .nav-link:where([aria-current]:not([aria-current="false"])) {
3217
+ outline: var(--border-width) solid Highlight;
3218
+ background: Highlight;
3219
+ color: HighlightText;
3220
+ forced-color-adjust: none;
3221
+ }
3222
+
3223
+ @media (hover: hover) {
3224
+ .nav-link:hover:not(:where([aria-current]:not([aria-current="false"]))) {
3225
+ outline: var(--border-width) solid ButtonText;
3226
+ outline-offset: 2px;
3227
+ background: Canvas;
3228
+ color: CanvasText;
3229
+ }
3230
+ }
3231
+ }
3232
+
3233
+ /*
3234
+ * Breadcrumb — current-location trail from the site root.
3235
+ *
3236
+ * Inline list separated by "/". Mark the current page with `aria-current="page"`
3237
+ * either on the item or on the current link.
3238
+ */
3239
+ .breadcrumb {
3240
+ display: flex;
3241
+ flex-wrap: wrap;
3242
+ align-items: center;
3243
+ gap: var(--space-1);
3244
+ margin: 0;
3245
+ padding: 0;
3246
+ color: var(--text-subtle);
3247
+ font-size: var(--font-size-sm);
3248
+ list-style: none;
3249
+ }
3250
+
3251
+ .breadcrumb li {
3252
+ display: inline-flex;
3253
+ align-items: center;
3254
+ gap: var(--space-1);
3255
+ }
3256
+
3257
+ .breadcrumb li + li::before {
3258
+ content: "/";
3259
+ color: var(--text-subtle);
3260
+ }
3261
+
3262
+ .breadcrumb a {
3263
+ color: var(--text-muted);
3264
+ text-decoration: none;
3265
+ }
3266
+
3267
+ @media (hover: hover) {
3268
+ .breadcrumb
3269
+ li:not(:where([aria-current]:not([aria-current="false"])))
3270
+ > a:hover:not(:where([aria-current]:not([aria-current="false"]))) {
3271
+ color: var(--text);
3272
+ text-decoration: underline;
3273
+ }
3274
+ }
3275
+
3276
+ .breadcrumb a:focus-visible {
3277
+ border-radius: var(--radius-sm);
3278
+ }
3279
+
3280
+ .breadcrumb a:where([aria-current]:not([aria-current="false"])) {
3281
+ color: var(--text);
3282
+ font-weight: var(--font-weight-medium);
3283
+ }
3284
+
3285
+ .breadcrumb li:where([aria-current]:not([aria-current="false"])) > :where(a, span) {
3286
+ color: var(--text);
3287
+ font-weight: var(--font-weight-medium);
3288
+ }
3289
+
3290
+ /*
3291
+ * Pagination — page navigation for paginated lists.
3292
+ *
3293
+ * Render .btn elements inside a .pagination list. The active page is marked
3294
+ * with aria-current="page"; .btn owns the active state styling.
3295
+ *
3296
+ * Truncated ranges are plain text (`.muted`, `aria-hidden="true"`), not .btn
3297
+ * — they are decorative, not actionable. They still get the same box sizing
3298
+ * as .btn below so the row stays visually aligned.
3299
+ */
3300
+ .pagination {
3301
+ display: flex;
3302
+ flex-wrap: wrap;
3303
+ align-items: center;
3304
+ gap: var(--space-1);
3305
+ margin: 0;
3306
+ padding: 0;
3307
+ list-style: none;
3308
+ }
3309
+
3310
+ .pagination .btn {
3311
+ min-inline-size: var(--control-size);
3312
+ }
3313
+
3314
+ .pagination > li > [aria-hidden="true"] {
3315
+ display: inline-flex;
3316
+ align-items: center;
3317
+ justify-content: center;
3318
+ min-inline-size: var(--control-size);
3319
+ }
3320
+
3321
+ /*
3322
+ * Tabs — in-place panel switcher with real tab semantics.
3323
+ *
3324
+ * JavaScript owns roving tabindex, aria-selected, panel visibility,
3325
+ * and arrow-key navigation. CSS owns the tab strip appearance and
3326
+ * active indicator.
3327
+ */
3328
+ /* Intent boundary — inherited intents do not tint this component.
3329
+ Local intent classes still win because :where() has zero specificity. */
3330
+ :where(.tab) {
3331
+ /* @sync intent-boundary */
3332
+ --intent: initial;
3333
+ --intent-fg: initial;
3334
+ --ui-bg: initial;
3335
+ --ui-fg: initial;
3336
+ --ui-border: initial;
3337
+ --ui-hover-bg: initial;
3338
+ /* @sync intent-boundary */
3339
+ }
3340
+
3341
+ .tabs {
3342
+ display: flex;
3343
+ flex-wrap: wrap;
3344
+ gap: var(--space-1);
3345
+ margin: 0;
3346
+ padding: 0;
3347
+ border-block-end: var(--border-width) solid var(--border);
3348
+ list-style: none;
3349
+ }
3350
+
3351
+ .tabs[aria-orientation="vertical"] {
3352
+ display: grid;
3353
+ align-content: start;
3354
+ justify-items: stretch;
3355
+ border-block-end: 0;
3356
+ border-inline-end: var(--border-width) solid var(--border);
3357
+ }
3358
+
3359
+ .tab {
3360
+ display: inline-flex;
3361
+ align-items: center;
3362
+ min-block-size: var(--control-size);
3363
+ padding-inline: var(--space-3);
3364
+ border: 0;
3365
+ border-block-end: 2px solid transparent;
3366
+ border-radius: 0;
3367
+ background: transparent;
3368
+ color: var(--text-muted);
3369
+ font: inherit;
3370
+ font-weight: var(--font-weight-strong);
3371
+ text-decoration: none;
3372
+ cursor: pointer;
3373
+ }
3374
+
3375
+ .tabs[aria-orientation="vertical"] .tab {
3376
+ justify-content: flex-start;
3377
+ border-block-end: 0;
3378
+ border-inline-end: 2px solid transparent;
3379
+ padding-inline-end: var(--space-4);
3380
+ }
3381
+
3382
+ .tab:is(:disabled, [aria-disabled="true"]) {
3383
+ cursor: not-allowed;
3384
+ opacity: var(--disabled-opacity);
3385
+ }
3386
+
3387
+ @media (hover: hover) {
3388
+ .tab:hover:not(:where(:disabled, [aria-disabled="true"])) {
3389
+ color: var(--text);
3390
+ }
3391
+ }
3392
+
3393
+ .tab:focus-visible {
3394
+ outline: var(--focus-outline);
3395
+ outline-offset: -2px;
3396
+ border-radius: var(--radius-sm) var(--radius-sm) 0 0;
3397
+ }
3398
+
3399
+ .tab[aria-selected="true"],
3400
+ .tab[aria-current]:not([aria-current="false"]),
3401
+ .tab[aria-pressed="true"] {
3402
+ border-block-end-color: var(--intent, var(--primary));
3403
+ color: var(--intent, var(--primary));
3404
+ }
3405
+
3406
+ .tabs[aria-orientation="vertical"] .tab[aria-selected="true"],
3407
+ .tabs[aria-orientation="vertical"] .tab[aria-current]:not([aria-current="false"]),
3408
+ .tabs[aria-orientation="vertical"] .tab[aria-pressed="true"] {
3409
+ border-block-end-color: transparent;
3410
+ border-inline-end-color: var(--intent, var(--primary));
3411
+ }
3412
+
3413
+ @media (forced-colors: active) {
3414
+ .tabs {
3415
+ border-block-end-color: ButtonText;
3416
+ }
3417
+
3418
+ .tabs[aria-orientation="vertical"] {
3419
+ border-block-end-color: transparent;
3420
+ border-inline-end-color: ButtonText;
3421
+ }
3422
+
3423
+ .tab {
3424
+ color: ButtonText;
3425
+ }
3426
+
3427
+ @media (hover: hover) {
3428
+ .tab:hover:not(:where(:disabled, [aria-disabled="true"])) {
3429
+ color: ButtonText;
3430
+ text-decoration: underline;
3431
+ text-underline-offset: 0.18em;
3432
+ }
3433
+ }
3434
+
3435
+ .tab[aria-selected="true"],
3436
+ .tab[aria-current]:not([aria-current="false"]),
3437
+ .tab[aria-pressed="true"] {
3438
+ border-block-end-color: Highlight;
3439
+ color: Highlight;
3440
+ }
3441
+
3442
+ .tabs[aria-orientation="vertical"] .tab[aria-selected="true"],
3443
+ .tabs[aria-orientation="vertical"] .tab[aria-current]:not([aria-current="false"]),
3444
+ .tabs[aria-orientation="vertical"] .tab[aria-pressed="true"] {
3445
+ border-block-end-color: transparent;
3446
+ border-inline-end-color: Highlight;
3447
+ }
3448
+ }
3449
+
3450
+ /*
3451
+ * Scrollspy — behavior hook for navigation that marks the active section
3452
+ * while the page scrolls.
3453
+ *
3454
+ * Does not add visual styles by itself. scrollspy.js watches
3455
+ * <nav class="scrollspy">
3456
+ * containing <a href="#section"> links and toggles aria-current="location" on
3457
+ * the link whose target is in view. Pair with .nav-list and .nav-link, or style
3458
+ * [aria-current] yourself.
3459
+ *
3460
+ * Progressive enhancement: on browsers that support :target-current,
3461
+ * this file enables the native target group and marks the active link.
3462
+ *
3463
+ * Falls back gracefully: links still work when IntersectionObserver or
3464
+ * :target-current is unavailable.
3465
+ */
3466
+
3467
+ @supports selector(.scrollspy a:target-current) {
3468
+ .scrollspy {
3469
+ scroll-target-group: auto;
3470
+ }
3471
+
3472
+ .scrollspy a:target-current {
3473
+ color: var(--text);
3474
+ font-weight: var(--font-weight-medium);
3475
+ }
3476
+ }
3477
+
3478
+
3479
+ /* Data */
3480
+ /*
3481
+ * Table — structured tabular data.
3482
+ *
3483
+ * .table-wrap provides the scroll container and outer chrome; .table owns row
3484
+ * and cell rules. Tokens are sourced from the shared scale.
3485
+ */
3486
+ .table-wrap {
3487
+ inline-size: 100%;
3488
+ max-inline-size: 100%;
3489
+ min-inline-size: 0;
3490
+ overflow-x: auto;
3491
+ border: var(--border-width) solid var(--border);
3492
+ border-radius: var(--radius-lg);
3493
+ background: var(--surface-raised);
3494
+ }
3495
+
3496
+ .table {
3497
+ --table-min: 100%;
3498
+ --table-cell-pad: var(--space-3);
3499
+
3500
+ inline-size: max(var(--table-min), 100%);
3501
+ border-collapse: collapse;
3502
+ background: transparent;
3503
+ color: var(--text);
3504
+ font-size: var(--font-size-md);
3505
+ }
3506
+
3507
+ .table caption {
3508
+ padding: var(--table-cell-pad);
3509
+ color: var(--text-muted);
3510
+ text-align: var(--text-align, start);
3511
+ }
3512
+
3513
+ .table th,
3514
+ .table td {
3515
+ padding: var(--table-cell-pad);
3516
+ border-block-end: var(--border-width) solid var(--border);
3517
+ text-align: var(--text-align, start);
3518
+ vertical-align: baseline;
3519
+ }
3520
+
3521
+ .table thead th {
3522
+ vertical-align: bottom;
3523
+ }
3524
+
3525
+ .table th {
3526
+ color: var(--text-muted);
3527
+ font-weight: var(--font-weight-strong);
3528
+ }
3529
+
3530
+ .table thead {
3531
+ background: var(--surface-subtle);
3532
+ }
3533
+
3534
+ @media (hover: hover) {
3535
+ .table tbody tr:hover {
3536
+ background-color: var(--hover-overlay);
3537
+ }
3538
+ }
3539
+
3540
+ .table > tr:last-child > *,
3541
+ .table > :last-child > tr:last-child > * {
3542
+ border-block-end: 0;
3543
+ }
3544
+
3545
+ /*
3546
+ * Meter — native gauge for known-range values.
3547
+ *
3548
+ * Maps the three standard meter zones (optimum / suboptimum / even-less-good)
3549
+ * to the shared success / warning / danger intent colors.
3550
+ */
3551
+ /* Intent boundary — inherited intents do not tint this component.
3552
+ Local intent classes still win because :where() has zero specificity. */
3553
+ :where(.meter) {
3554
+ /* @sync intent-boundary */
3555
+ --intent: initial;
3556
+ --intent-fg: initial;
3557
+ --ui-bg: initial;
3558
+ --ui-fg: initial;
3559
+ --ui-border: initial;
3560
+ --ui-hover-bg: initial;
3561
+ /* @sync intent-boundary */
3562
+ }
3563
+
3564
+ .meter {
3565
+ appearance: none;
3566
+ -webkit-appearance: none;
3567
+ display: block;
3568
+ inline-size: 100%;
3569
+ block-size: var(--bar-height);
3570
+ overflow: hidden;
3571
+ border: 0;
3572
+ border-radius: var(--radius-full);
3573
+ background: var(--surface-subtle);
3574
+ }
3575
+
3576
+ .meter::-webkit-meter-bar {
3577
+ -webkit-appearance: none;
3578
+ block-size: var(--bar-height);
3579
+ border: 0;
3580
+ border-radius: var(--radius-full);
3581
+ background: var(--surface-subtle);
3582
+ }
3583
+
3584
+ .meter::-webkit-meter-optimum-value {
3585
+ block-size: var(--bar-height);
3586
+ border-radius: var(--radius-full);
3587
+ background: var(--success);
3588
+ }
3589
+
3590
+ .meter::-webkit-meter-suboptimum-value {
3591
+ block-size: var(--bar-height);
3592
+ border-radius: var(--radius-full);
3593
+ background: var(--warning);
3594
+ }
3595
+
3596
+ .meter::-webkit-meter-even-less-good-value {
3597
+ block-size: var(--bar-height);
3598
+ border-radius: var(--radius-full);
3599
+ background: var(--danger);
3600
+ }
3601
+
3602
+ .meter::-moz-meter-bar {
3603
+ block-size: var(--bar-height);
3604
+ border-radius: var(--radius-full);
3605
+ }
3606
+
3607
+ @media (forced-colors: active) {
3608
+ .meter {
3609
+ border: var(--border-width) solid ButtonText;
3610
+ background: Canvas;
3611
+ }
3612
+
3613
+ .meter::-webkit-meter-bar {
3614
+ border: 0;
3615
+ background: Canvas;
3616
+ }
3617
+
3618
+ .meter::-webkit-meter-optimum-value,
3619
+ .meter::-webkit-meter-suboptimum-value,
3620
+ .meter::-webkit-meter-even-less-good-value,
3621
+ .meter::-moz-meter-bar {
3622
+ background: Highlight;
3623
+ forced-color-adjust: none;
3624
+ }
3625
+ }
3626
+
3627
+ /* Forms — import order matters: switch.css must follow choice.css so its
3628
+ :not([role="switch"]) exclusion in choice.css still applies.
3629
+ validation.css is part of the default forms bundle, but custom builds may
3630
+ omit it when validation styling is app-owned. */
3631
+ /*
3632
+ * Form layout and supporting text.
3633
+ *
3634
+ * Individual controls keep their own files; .choice label layout lives in
3635
+ * choice.css; .form-actions lives in form-actions.css.
3636
+ *
3637
+ * Public API: .field, .field-label, .field-help, .field-error, .field-group,
3638
+ * .required-mark.
3639
+ */
3640
+ .field {
3641
+ display: grid;
3642
+ gap: var(--space-1);
3643
+ color: var(--text);
3644
+ }
3645
+
3646
+ .field-group {
3647
+ min-inline-size: 0;
3648
+ margin: 0;
3649
+ padding: var(--space-3);
3650
+ border: var(--border-width) solid var(--border);
3651
+ border-radius: var(--radius);
3652
+ }
3653
+
3654
+ .field-group + .field-group {
3655
+ margin-block-start: var(--space-4);
3656
+ }
3657
+
3658
+ .field-label,
3659
+ .field-group > legend {
3660
+ color: var(--text);
3661
+ font-weight: var(--font-weight-strong);
3662
+ }
3663
+
3664
+ .field-group > legend {
3665
+ padding-block: 0;
3666
+ padding-inline: var(--fieldset-legend-padding-inline, var(--space-1));
3667
+ font-size: var(--font-size-xs);
3668
+ }
3669
+
3670
+ .field-help {
3671
+ color: var(--text-muted);
3672
+ font-size: var(--font-size-sm);
3673
+ }
3674
+
3675
+ .field-error {
3676
+ display: none;
3677
+ color: var(--danger);
3678
+ font-size: var(--font-size-sm);
3679
+ }
3680
+
3681
+ .required-mark {
3682
+ color: var(--danger);
3683
+ }
3684
+
3685
+ .field.danger .field-help {
3686
+ color: var(--danger);
3687
+ }
3688
+
3689
+ /*
3690
+ * Text controls — input, textarea, select.
3691
+ *
3692
+ * Neutral geometry and interaction states shared across all text-like
3693
+ * controls. select shares this base recipe; its chevron/appearance layer
3694
+ * lives in select.css. checkbox, radio, range, switch, and file have their
3695
+ * own files.
3696
+ *
3697
+ * --control-* are the per-state control tokens, reserved for control geometry.
3698
+ * State selectors only flip tokens; non-text controls do not consume them, so
3699
+ * matching those selectors is a no-op. The .field wrapper uses --field-*
3700
+ * (form.css).
3701
+ *
3702
+ * --control-border-block-end-color/-width are unset extension points, mirroring
3703
+ * --btn-border-block-end-* (button.css), for themes that give text controls an
3704
+ * underline-style lower edge instead of a uniform border.
3705
+ */
3706
+ .input,
3707
+ .textarea,
3708
+ .select {
3709
+ --control-border: var(--border);
3710
+ --control-bg: var(--surface);
3711
+ --control-fg: var(--text);
3712
+ --control-ring: none;
3713
+ --control-cursor: auto;
3714
+ --control-opacity: 1;
3715
+
3716
+ appearance: none;
3717
+ inline-size: 100%;
3718
+ min-block-size: var(--control-size);
3719
+ padding-inline: var(--space-3);
3720
+ border: var(--border-width) solid var(--form-invalid-border, var(--control-border));
3721
+ border-block-end-color: var(
3722
+ --form-invalid-border,
3723
+ var(--control-border-block-end-color, var(--control-border))
3724
+ );
3725
+ border-block-end-width: var(--control-border-block-end-width, var(--border-width));
3726
+ border-radius: var(--radius);
3727
+ background: var(--control-bg);
3728
+ color: var(--control-fg);
3729
+ font-size: var(--control-font-size);
3730
+ box-shadow: var(--control-ring);
3731
+ cursor: var(--control-cursor);
3732
+ opacity: var(--control-opacity);
3733
+ outline: none;
3734
+ transition:
3735
+ border-color var(--duration),
3736
+ box-shadow var(--duration),
3737
+ background-color var(--duration),
3738
+ color var(--duration);
3739
+ }
3740
+
3741
+ .textarea {
3742
+ min-block-size: 6rem;
3743
+ padding-block: var(--space-2);
3744
+ resize: vertical;
3745
+ }
3746
+
3747
+ @media (hover: hover) {
3748
+ .input:hover:not(:disabled),
3749
+ .textarea:hover:not(:disabled) {
3750
+ --control-border: var(--text-subtle);
3751
+ }
3752
+
3753
+ .select:hover:not(:disabled) {
3754
+ --control-bg: var(--surface-subtle);
3755
+ }
3756
+ }
3757
+
3758
+ .input:focus-visible,
3759
+ .textarea:focus-visible,
3760
+ .select:focus-visible {
3761
+ --control-border: var(--focus);
3762
+ outline: var(--focus-outline);
3763
+ outline-offset: var(--focus-outline-offset);
3764
+ box-shadow: var(--focus-ring-shadow);
3765
+ }
3766
+
3767
+ .input[readonly],
3768
+ .textarea[readonly] {
3769
+ --control-bg: var(--surface-subtle);
3770
+ --control-border: var(--border);
3771
+ }
3772
+
3773
+ .input:disabled,
3774
+ .textarea:disabled,
3775
+ .select:disabled {
3776
+ --control-bg: var(--surface-subtle);
3777
+ --control-fg: var(--text-subtle);
3778
+ --control-border: var(--border);
3779
+ --control-cursor: not-allowed;
3780
+ --control-opacity: 1;
3781
+ }
3782
+
3783
+ .input::placeholder,
3784
+ .textarea::placeholder {
3785
+ color: var(--text-muted);
3786
+ }
3787
+
3788
+ @media (forced-colors: active) {
3789
+ .input:focus-visible,
3790
+ .textarea:focus-visible,
3791
+ .select:focus-visible {
3792
+ box-shadow: none;
3793
+ }
3794
+ }
3795
+
3796
+ /*
3797
+ * Input Icon — leading decorative icon inside a relative wrapper.
3798
+ *
3799
+ * Icon-library agnostic: selects the wrapper's first child rather than a
3800
+ * specific tag, so it works with an <i> webfont icon, an inline <svg>, or a
3801
+ * <span>. Sizes with --variant-icon-size (variants.css) so .sm/.lg pick up
3802
+ * the same icon scale used elsewhere (alert.css); the input's start padding
3803
+ * grows to match so text never runs under the icon.
3804
+ */
3805
+ .input-icon {
3806
+ --input-icon-size: var(--variant-icon-size, 1.25rem);
3807
+
3808
+ display: block;
3809
+ position: relative;
3810
+ }
3811
+
3812
+ .input-icon > :first-child {
3813
+ position: absolute;
3814
+ inset-block-start: 50%;
3815
+ inset-inline-start: var(--space-3);
3816
+ transform: translateY(-50%);
3817
+ font-size: var(--input-icon-size);
3818
+ color: var(--text-muted);
3819
+ pointer-events: none;
3820
+ }
3821
+
3822
+ /* font-size only sizes text-metric icons (webfont <i>, <span>). An inline
3823
+ <svg> with just a viewBox ignores it, so give it explicit dimensions. */
3824
+ .input-icon > svg:first-child {
3825
+ inline-size: var(--input-icon-size);
3826
+ block-size: var(--input-icon-size);
3827
+ }
3828
+
3829
+ .input-icon > .input {
3830
+ padding-inline-start: calc(var(--space-3) * 2 + var(--input-icon-size));
3831
+ }
3832
+
3833
+ /*
3834
+ * Checkbox and radio controls.
3835
+ *
3836
+ * Native inputs keep their behavior while sharing custom geometry, checked,
3837
+ * focus, and disabled states across semantic forms and classed aliases. This
3838
+ * file also owns the .choice label layout (input + label text) so the whole
3839
+ * concept lives in one file.
3840
+ *
3841
+ * Switches (input[role="switch"]) are excluded so switch.css owns their
3842
+ * geometry and states. switch.css is imported after this file.
3843
+ */
3844
+ /* Intent boundary — inherited intents do not tint this component.
3845
+ Local intent classes still win because :where() has zero specificity. */
3846
+ :where(.choice, .check, .radio) {
3847
+ /* @sync intent-boundary */
3848
+ --intent: initial;
3849
+ --intent-fg: initial;
3850
+ --ui-bg: initial;
3851
+ --ui-fg: initial;
3852
+ --ui-border: initial;
3853
+ --ui-hover-bg: initial;
3854
+ /* @sync intent-boundary */
3855
+ }
3856
+
3857
+ /* A classed .choice is the component surface. Let its nested input consume a
3858
+ local intent without reopening inherited page-level intents. */
3859
+ :where(.choice) :where(input[type="checkbox"]:not([role="switch"]), input[type="radio"]) {
3860
+ --intent: inherit;
3861
+ --intent-fg: inherit;
3862
+ }
3863
+
3864
+ .choice {
3865
+ --choice-control-offset: 0.25em;
3866
+
3867
+ display: inline-flex;
3868
+ align-items: flex-start;
3869
+ gap: var(--space-2);
3870
+ color: var(--text);
3871
+ font-size: var(--control-font-size);
3872
+ cursor: pointer;
3873
+ }
3874
+
3875
+ /* Optical alignment: nudge the control down so the checkbox/radio sits
3876
+ visually centered with the first line of label text, not flush with the
3877
+ top of the line-box. Default 0.25em (~4px at 16px) places the control just
3878
+ below the cap-height, matching what Bootstrap, Material, and most polished
3879
+ form libraries ship. Multi-line labels still keep the control at the top
3880
+ thanks to align-items: flex-start. Override per-instance via
3881
+ --choice-control-offset for themes with a different line-height or control
3882
+ size. */
3883
+ .choice > :is(input[type="checkbox"]:not([role="switch"]), input[type="radio"]) {
3884
+ flex: 0 0 auto;
3885
+ margin-block-start: var(--choice-control-offset);
3886
+ }
3887
+
3888
+ .choice > span > .field-help {
3889
+ display: block;
3890
+ }
3891
+
3892
+ .choice:has(:disabled) {
3893
+ color: var(--text-subtle);
3894
+ cursor: not-allowed;
3895
+ }
3896
+
3897
+ /* Control geometry. --choice-* are the per-state tokens. */
3898
+ .check,
3899
+ .radio {
3900
+ --choice-border: var(--border);
3901
+ --choice-bg: var(--surface);
3902
+ --choice-mark: var(--primary-fg);
3903
+
3904
+ position: relative;
3905
+ vertical-align: middle;
3906
+ display: inline-grid;
3907
+ place-content: center;
3908
+ inline-size: 1.125em;
3909
+ block-size: 1.125em;
3910
+ margin-inline: 0;
3911
+ margin-block-end: 0;
3912
+ border: var(--border-width) solid var(--form-invalid-border, var(--choice-border));
3913
+ background: var(--choice-bg);
3914
+ color: var(--choice-mark);
3915
+ appearance: none;
3916
+ cursor: pointer;
3917
+ }
3918
+
3919
+ .check {
3920
+ border-radius: var(--radius-sm);
3921
+ }
3922
+
3923
+ .radio {
3924
+ border-radius: var(--radius-full);
3925
+ }
3926
+
3927
+ .check::before,
3928
+ .radio::before {
3929
+ content: "";
3930
+ position: absolute;
3931
+ inset: 0;
3932
+ margin: auto;
3933
+ transform: scale(0);
3934
+ transition: transform var(--duration);
3935
+ }
3936
+
3937
+ .check::before {
3938
+ inline-size: 0.58em;
3939
+ block-size: 0.34em;
3940
+ border-inline-start: 2px solid currentColor;
3941
+ border-block-end: 2px solid currentColor;
3942
+ transform: translateY(-0.06em) rotate(-45deg) scale(0);
3943
+ }
3944
+
3945
+ .radio::before {
3946
+ inline-size: 0.5em;
3947
+ block-size: 0.5em;
3948
+ border-radius: var(--radius-full);
3949
+ background: currentColor;
3950
+ }
3951
+
3952
+ .check:checked,
3953
+ .radio:checked {
3954
+ --choice-border: var(--intent, var(--primary));
3955
+ --choice-bg: var(--intent, var(--primary));
3956
+ --choice-mark: var(--intent-fg, var(--primary-fg));
3957
+ }
3958
+
3959
+ .check:checked::before {
3960
+ transform: translateY(-0.06em) rotate(-45deg) scale(1);
3961
+ }
3962
+
3963
+ .check:indeterminate {
3964
+ --choice-border: var(--intent, var(--primary));
3965
+ --choice-bg: var(--intent, var(--primary));
3966
+ --choice-mark: var(--intent-fg, var(--primary-fg));
3967
+ }
3968
+
3969
+ .check:indeterminate::before {
3970
+ border-inline-start: 0;
3971
+ inline-size: 0.6em;
3972
+ block-size: 0;
3973
+ border-block-end: 2px solid currentColor;
3974
+ transform: scale(1);
3975
+ }
3976
+
3977
+ .radio:checked::before {
3978
+ transform: scale(1);
3979
+ }
3980
+
3981
+ @media (hover: hover) {
3982
+ .check:hover:not(:checked, :indeterminate, :disabled),
3983
+ .radio:hover:not(:checked, :indeterminate, :disabled) {
3984
+ --choice-border: var(--text-subtle);
3985
+ }
3986
+ }
3987
+
3988
+ .check:focus-visible,
3989
+ .radio:focus-visible {
3990
+ --choice-border: var(--focus);
3991
+ outline: var(--focus-outline);
3992
+ outline-offset: var(--focus-outline-offset);
3993
+ box-shadow: var(--focus-ring-shadow);
3994
+ }
3995
+
3996
+ .check:disabled,
3997
+ .radio:disabled {
3998
+ --choice-bg: var(--surface-subtle);
3999
+ --choice-border: var(--border);
4000
+ --choice-mark: var(--text-subtle);
4001
+ cursor: not-allowed;
4002
+ opacity: 1;
4003
+ }
4004
+
4005
+ @media (forced-colors: active) {
4006
+ .check,
4007
+ .radio {
4008
+ --choice-border: ButtonText;
4009
+ --choice-bg: Canvas;
4010
+ --choice-mark: ButtonText;
4011
+ }
4012
+
4013
+ .check:checked,
4014
+ .radio:checked,
4015
+ .check:indeterminate {
4016
+ --choice-border: Highlight;
4017
+ --choice-bg: Highlight;
4018
+ --choice-mark: HighlightText;
4019
+ forced-color-adjust: none;
4020
+ }
4021
+
4022
+ .check:disabled,
4023
+ .radio:disabled {
4024
+ --choice-border: GrayText;
4025
+ --choice-bg: Canvas;
4026
+ --choice-mark: GrayText;
4027
+ }
4028
+ }
4029
+
4030
+ /*
4031
+ * Choice cards — card-style radio and checkbox labels.
4032
+ *
4033
+ * Each .choice-card is a <label> with a hidden native input. Both radio and
4034
+ * checkbox show a checkmark badge on selection: round for radio, rounded
4035
+ * square for checkbox. Pair with grid or flex utilities for group layout.
4036
+ */
4037
+ :where(.choice-card) {
4038
+ /* @sync intent-boundary */
4039
+ --intent: initial;
4040
+ --intent-fg: initial;
4041
+ --ui-bg: initial;
4042
+ --ui-fg: initial;
4043
+ --ui-border: initial;
4044
+ --ui-hover-bg: initial;
4045
+ /* @sync intent-boundary */
4046
+ }
4047
+
4048
+ .choice-card {
4049
+ --choice-card-pad: var(--variant-space, var(--space-4));
4050
+ --choice-card-radius: var(--radius);
4051
+ --choice-card-border: var(--border);
4052
+ --choice-card-bg: var(--surface);
4053
+ --choice-card-check-size: 1.5em;
4054
+
4055
+ position: relative;
4056
+ display: block;
4057
+ padding: var(--choice-card-pad);
4058
+ border: var(--border-width) solid var(--choice-card-border);
4059
+ border-radius: var(--choice-card-radius);
4060
+ background: var(--choice-card-bg);
4061
+ color: var(--text);
4062
+ cursor: pointer;
4063
+ transition:
4064
+ border-color var(--duration),
4065
+ background-color var(--duration),
4066
+ box-shadow var(--duration);
4067
+ }
4068
+
4069
+ /* The native input is visually hidden but stays focusable and participates in
4070
+ form submission and keyboard navigation (sr-only pattern). */
4071
+ .choice-card > input {
4072
+ position: absolute;
4073
+ inline-size: 1px;
4074
+ block-size: 1px;
4075
+ padding: 0;
4076
+ margin: -1px;
4077
+ overflow: hidden;
4078
+ clip: rect(0, 0, 0, 0);
4079
+ white-space: nowrap;
4080
+ border: 0;
4081
+ }
4082
+
4083
+ /* Focus ring on the label follows the hidden input's focus. */
4084
+ .choice-card:has(:focus-visible) {
4085
+ outline: var(--focus-outline);
4086
+ outline-offset: var(--focus-outline-offset);
4087
+ box-shadow: var(--focus-ring-shadow);
4088
+ }
4089
+
4090
+ .choice-card:has(:disabled) {
4091
+ opacity: var(--disabled-opacity);
4092
+ cursor: not-allowed;
4093
+ }
4094
+
4095
+ @media (hover: hover) {
4096
+ .choice-card:hover:not(:has(:disabled)) {
4097
+ --choice-card-border: var(--text-subtle);
4098
+ }
4099
+ }
4100
+
4101
+ /* Checked border — shared by radio and checkbox */
4102
+ .choice-card:has(:checked) {
4103
+ --choice-card-border: var(--intent, var(--primary));
4104
+ }
4105
+
4106
+ /* Subtle background tint on checked — modern browsers only */
4107
+ @supports (color: color-mix(in oklch, red, white)) {
4108
+ .choice-card:has(:checked) {
4109
+ --choice-card-bg: color-mix(in oklch, var(--surface) 96%, var(--intent, var(--primary)));
4110
+ }
4111
+ }
4112
+
4113
+ /* Checkmark badge — circle for radio, rounded square for checkbox */
4114
+ .choice-card:has(:checked)::before {
4115
+ content: "";
4116
+ position: absolute;
4117
+ inset-inline-end: -0.375em;
4118
+ inset-block-start: -0.375em;
4119
+ inline-size: var(--choice-card-check-size);
4120
+ block-size: var(--choice-card-check-size);
4121
+ background: var(--intent, var(--primary));
4122
+ z-index: 1;
4123
+ box-shadow: 0 0 0 2px var(--surface);
4124
+ }
4125
+
4126
+ .choice-card:has(input[type="radio"]:checked)::before {
4127
+ border-radius: var(--radius-full);
4128
+ }
4129
+
4130
+ .choice-card:has(input[type="checkbox"]:checked)::before {
4131
+ border-radius: var(--radius-sm);
4132
+ }
4133
+
4134
+ .choice-card:has(:checked)::after {
4135
+ content: "";
4136
+ position: absolute;
4137
+ inset-inline-end: -0.375em;
4138
+ inset-block-start: -0.375em;
4139
+ inline-size: var(--choice-card-check-size);
4140
+ block-size: var(--choice-card-check-size);
4141
+ background: var(--intent-fg, var(--primary-fg));
4142
+ mask: var(--icon-check-mask) center / 60% no-repeat;
4143
+ -webkit-mask: var(--icon-check-mask) center / 60% no-repeat;
4144
+ z-index: 2;
4145
+ }
4146
+
4147
+ @media (forced-colors: active) {
4148
+ .choice-card {
4149
+ --choice-card-border: ButtonText;
4150
+ --choice-card-bg: Canvas;
4151
+ }
4152
+
4153
+ .choice-card:has(:checked) {
4154
+ --choice-card-border: Highlight;
4155
+ forced-color-adjust: none;
4156
+ }
4157
+
4158
+ .choice-card:has(:checked)::before {
4159
+ background: Highlight;
4160
+ box-shadow: 0 0 0 2px Canvas;
4161
+ }
4162
+
4163
+ .choice-card:has(input[type="radio"]:checked)::before {
4164
+ border-radius: var(--radius-full);
4165
+ }
4166
+
4167
+ .choice-card:has(input[type="checkbox"]:checked)::before {
4168
+ border-radius: var(--radius-sm);
4169
+ }
4170
+
4171
+ .choice-card:has(:checked)::after {
4172
+ background: HighlightText;
4173
+ }
4174
+ }
4175
+
4176
+ /*
4177
+ * Select — chevron and appearance layer on top of the shared text-control
4178
+ * recipe.
4179
+ *
4180
+ * Base geometry and interaction states come from control.css (select shares
4181
+ * the --control-* tokens). This file only removes the native arrow and draws
4182
+ * the CSS chevron, so the picker (appearance: base-select) enhancement in
4183
+ * custom-select.css can be opted out independently. Add .native to a specific
4184
+ * select to opt out of the enhancement.
4185
+ */
4186
+ .select:is(:not([multiple], [size]), :not([multiple])[size="1"]) {
4187
+ appearance: none;
4188
+ padding-inline-end: calc(var(--space-3) + var(--space-6));
4189
+ background-image: var(--icon-chevron-image);
4190
+ background-position: center right var(--space-3);
4191
+ background-size: 1rem auto;
4192
+ background-repeat: no-repeat;
4193
+ }
4194
+
4195
+ /* The chevron sits on the inline-end side; flip it in RTL so it follows
4196
+ the padding-inline-end space reserved for it. */
4197
+ .select:is(:not([multiple], [size]), :not([multiple])[size="1"]):dir(rtl) {
4198
+ background-position: center left var(--space-3);
4199
+ }
4200
+
4201
+ /* Both forced-colors and prefers-contrast hand the arrow back to the
4202
+ browser instead of guessing a higher-contrast recolor of our own icon —
4203
+ simpler than maintaining contrast variants, and native rendering already
4204
+ matches whatever contrast mode the user picked. */
4205
+ @media (forced-colors: active), (prefers-contrast: more) {
4206
+ .select:is(:not([multiple], [size]), :not([multiple])[size="1"]) {
4207
+ appearance: auto;
4208
+ background-image: none;
4209
+ padding-inline-end: var(--space-3);
4210
+ }
4211
+ }
4212
+
4213
+ /*
4214
+ * Customizable select — appearance: base-select picker enhancement.
4215
+ *
4216
+ * Progressive enhancement on top of the native .select baseline in select.css.
4217
+ * Wrapped in @supports and gated on fine pointer devices so it never blocks a
4218
+ * layout. Add .native to a specific select to opt out.
4219
+ *
4220
+ * No layout or interaction contract may depend on appearance: base-select.
4221
+ * Arbitrary menu and popover behavior belongs elsewhere.
4222
+ */
4223
+ @media (forced-colors: none) and (prefers-contrast: no-preference) and (hover: hover) and (
4224
+ pointer: fine
4225
+ ) {
4226
+ @supports (appearance: base-select) {
4227
+ .select:not([multiple], .native, [size]:not([size="1"])) {
4228
+ appearance: base-select;
4229
+ align-items: center;
4230
+
4231
+ &::picker(select) {
4232
+ appearance: base-select;
4233
+ max-height: min(24rem, calc(var(--viewport-block) * 0.7));
4234
+ margin-block: var(--space-2);
4235
+ padding: 0;
4236
+ border: var(--border-width) solid var(--border);
4237
+ border-radius: var(--radius);
4238
+ background-color: var(--surface-raised);
4239
+ color: inherit;
4240
+ box-shadow: var(--shadow-popout);
4241
+ }
4242
+
4243
+ &:open {
4244
+ --control-border: var(--border);
4245
+ --control-bg: var(--surface-raised);
4246
+ outline: none;
4247
+ box-shadow: var(--focus-ring-shadow);
4248
+ }
4249
+
4250
+ &::picker-icon {
4251
+ display: none;
4252
+ }
4253
+
4254
+ & option {
4255
+ position: relative;
4256
+ padding-block: var(--space-2);
4257
+ padding-inline: var(--space-3);
4258
+ white-space: normal;
4259
+ transition-property: color, background-color;
4260
+ transition-duration: var(--duration);
4261
+
4262
+ &:not(:disabled):hover {
4263
+ background-color: var(--surface-subtle);
4264
+ color: var(--text);
4265
+ cursor: pointer;
4266
+ }
4267
+
4268
+ &:not(:disabled):focus-visible {
4269
+ background-color: var(--primary);
4270
+ color: var(--primary-fg);
4271
+ outline: none;
4272
+ }
4273
+
4274
+ &:active {
4275
+ background-color: var(--surface-subtle);
4276
+ color: var(--text);
4277
+ }
4278
+
4279
+ &::checkmark {
4280
+ content: "";
4281
+ display: block;
4282
+ inline-size: 1rem;
4283
+ block-size: 1rem;
4284
+ background-color: currentColor;
4285
+ -webkit-mask-image: var(--icon-check-mask);
4286
+ mask-image: var(--icon-check-mask);
4287
+ -webkit-mask-repeat: no-repeat;
4288
+ mask-repeat: no-repeat;
4289
+ -webkit-mask-size: contain;
4290
+ mask-size: contain;
4291
+ -webkit-mask-position: center;
4292
+ mask-position: center;
4293
+ }
4294
+ }
4295
+
4296
+ & optgroup {
4297
+ padding-block-start: 0.5em;
4298
+
4299
+ & option:first-child {
4300
+ margin-block-start: 0.5em;
4301
+ }
4302
+ }
4303
+ }
4304
+ }
4305
+ }
4306
+
4307
+ /*
4308
+ * Native-only control polish: file and range inputs that defeat most custom
4309
+ * styling, so the framework stays out of their way and just keeps them readable
4310
+ * and theme-aware.
4311
+ */
4312
+ /* Intent boundary — inherited intents do not tint this component.
4313
+ Local intent classes still win because :where() has zero specificity. */
4314
+ :where(.range) {
4315
+ /* @sync intent-boundary */
4316
+ --intent: initial;
4317
+ --intent-fg: initial;
4318
+ --ui-bg: initial;
4319
+ --ui-fg: initial;
4320
+ --ui-border: initial;
4321
+ --ui-hover-bg: initial;
4322
+ /* @sync intent-boundary */
4323
+ }
4324
+
4325
+ .file {
4326
+ color: var(--text-muted);
4327
+ }
4328
+
4329
+ .file::file-selector-button {
4330
+ margin-inline-end: var(--space-3);
4331
+ padding-block: var(--space-1);
4332
+ padding-inline: var(--space-2);
4333
+ border: var(--border-width) solid var(--border);
4334
+ border-radius: var(--radius-sm);
4335
+ background: var(--surface-subtle);
4336
+ color: var(--text);
4337
+ font: inherit;
4338
+ font-size: var(--font-size-sm);
4339
+ font-weight: var(--font-weight-medium);
4340
+ cursor: pointer;
4341
+ transition:
4342
+ background-color var(--duration),
4343
+ border-color var(--duration),
4344
+ color var(--duration);
4345
+ }
4346
+
4347
+ .file:disabled::file-selector-button {
4348
+ cursor: not-allowed;
4349
+ }
4350
+
4351
+ @media (hover: hover) {
4352
+ .file:not(:disabled)::file-selector-button:hover {
4353
+ background: var(--border);
4354
+ }
4355
+ }
4356
+
4357
+ .range {
4358
+ inline-size: 100%;
4359
+ accent-color: var(--intent, var(--primary));
4360
+ }
4361
+
4362
+ .file:focus-visible,
4363
+ .range:focus-visible {
4364
+ outline: var(--focus-outline);
4365
+ outline-offset: var(--focus-outline-offset);
4366
+ box-shadow: var(--focus-ring-shadow);
4367
+ }
4368
+
4369
+ @media (forced-colors: active) {
4370
+ .file:focus-visible,
4371
+ .range:focus-visible {
4372
+ box-shadow: none;
4373
+ }
4374
+ }
4375
+
4376
+ /*
4377
+ * Switch controls for checkbox inputs with switch semantics.
4378
+ *
4379
+ * The switch remains an input control, not a decorative toggle. Checked, hover,
4380
+ * focus-visible, and disabled states stay clear.
4381
+ */
4382
+ /* Intent boundary — inherited intents do not tint this component.
4383
+ Local intent classes still win because :where() has zero specificity. */
4384
+ :where(.switch) {
4385
+ /* @sync intent-boundary */
4386
+ --intent: initial;
4387
+ --intent-fg: initial;
4388
+ --ui-bg: initial;
4389
+ --ui-fg: initial;
4390
+ --ui-border: initial;
4391
+ --ui-hover-bg: initial;
4392
+ /* @sync intent-boundary */
4393
+ }
4394
+
4395
+ .switch {
4396
+ --switch-bg: var(--border);
4397
+ --switch-border: var(--border);
4398
+ --switch-knob: var(--surface-raised);
4399
+ --switch-width: calc(var(--control-size) * 1.16);
4400
+ --switch-block-size: calc(var(--control-size) * 0.63);
4401
+ --switch-knob-margin: calc(var(--switch-block-size) * 0.12);
4402
+ --switch-knob-size: calc(var(--switch-block-size) - 2 * var(--switch-knob-margin));
4403
+ --switch-travel: calc(
4404
+ var(--switch-width) -
4405
+ var(--switch-knob-size) -
4406
+ 2 *
4407
+ var(--border-width) -
4408
+ 2 *
4409
+ var(--switch-knob-margin)
4410
+ );
4411
+
4412
+ position: relative;
4413
+ display: inline-grid;
4414
+ align-items: center;
4415
+ justify-content: start;
4416
+ inline-size: var(--switch-width);
4417
+ block-size: var(--switch-block-size);
4418
+ margin: 0;
4419
+ border: var(--border-width) solid var(--form-invalid-border, var(--switch-border));
4420
+ border-radius: var(--radius-full);
4421
+ background: var(--switch-bg);
4422
+ appearance: none;
4423
+ cursor: pointer;
4424
+ }
4425
+
4426
+ .switch::before {
4427
+ content: "";
4428
+ inline-size: var(--switch-knob-size);
4429
+ block-size: var(--switch-knob-size);
4430
+ margin-inline: var(--switch-knob-margin);
4431
+ border-radius: var(--radius-full);
4432
+ background: var(--switch-knob);
4433
+ box-shadow: var(--shadow);
4434
+ transition:
4435
+ transform var(--duration),
4436
+ background-color var(--duration),
4437
+ box-shadow var(--duration);
4438
+ }
4439
+
4440
+ @media (hover: hover) {
4441
+ .switch:hover:not(:disabled) {
4442
+ --switch-border: var(--intent, var(--primary));
4443
+ }
4444
+ }
4445
+
4446
+ .switch:checked {
4447
+ --switch-bg: var(--intent, var(--primary));
4448
+ --switch-border: var(--intent, var(--primary));
4449
+ --switch-knob: var(--intent-fg, var(--primary-fg));
4450
+ }
4451
+
4452
+ .switch:checked::before {
4453
+ transform: translateX(var(--switch-travel));
4454
+ }
4455
+
4456
+ /* The knob travels toward inline-end; in RTL that is leftward, so the
4457
+ translateX sign flips. translateX is physical, not logical. */
4458
+ .switch:checked:dir(rtl)::before {
4459
+ transform: translateX(calc(var(--switch-travel) * -1));
4460
+ }
4461
+
4462
+ .switch:focus-visible {
4463
+ --switch-border: var(--focus);
4464
+ outline: var(--focus-outline);
4465
+ outline-offset: var(--focus-outline-offset);
4466
+ box-shadow: var(--focus-ring-shadow);
4467
+ }
4468
+
4469
+ .switch:disabled {
4470
+ --switch-bg: var(--surface-subtle);
4471
+ --switch-border: var(--border);
4472
+ --switch-knob: var(--text-subtle);
4473
+ opacity: 1;
4474
+ cursor: not-allowed;
4475
+ }
4476
+
4477
+ @media (forced-colors: active) {
4478
+ /* --switch-knob must be set on the element, not on ::before: a custom
4479
+ property declared on the pseudo would shadow the :checked override. */
4480
+ .switch {
4481
+ --switch-border: ButtonText;
4482
+ --switch-knob: ButtonText;
4483
+ }
4484
+
4485
+ .switch::before {
4486
+ box-shadow: none;
4487
+ }
4488
+
4489
+ .switch:checked {
4490
+ --switch-bg: Highlight;
4491
+ --switch-border: Highlight;
4492
+ --switch-knob: HighlightText;
4493
+ forced-color-adjust: none;
4494
+ }
4495
+
4496
+ .switch:disabled {
4497
+ --switch-bg: Canvas;
4498
+ --switch-border: GrayText;
4499
+ --switch-knob: GrayText;
4500
+ forced-color-adjust: auto;
4501
+ }
4502
+ }
4503
+
4504
+ /*
4505
+ * Form validation states.
4506
+ *
4507
+ * Imported by default through forms/index.css. Custom builds may omit this
4508
+ * file when an app wants to own validation styling completely.
4509
+ */
4510
+
4511
+ .field.danger {
4512
+ --form-invalid-border: var(--danger);
4513
+ }
4514
+
4515
+ .field.danger > .field-error {
4516
+ display: block;
4517
+ }
4518
+
4519
+ @supports selector(:has(*)) {
4520
+ .field:has([aria-invalid="true"]) > .field-error,
4521
+ .needs-validation.was-validated .field:has(:invalid) > .field-error {
4522
+ display: block;
4523
+ }
4524
+ }
4525
+
4526
+ :where(.input, .textarea, .select, .check, .radio, .switch)[aria-invalid="true"],
4527
+ .needs-validation.was-validated :is(.input, .textarea, .select, .check, .radio, .switch):invalid {
4528
+ --form-invalid-border: var(--danger);
4529
+ }
4530
+
4531
+ /* Override checked/indeterminate background so both border and fill signal
4532
+ danger instead of showing a red border around a primary-colored control. */
4533
+ .check[aria-invalid="true"]:is(:checked, :indeterminate),
4534
+ .radio[aria-invalid="true"]:checked {
4535
+ --choice-bg: var(--danger);
4536
+ }
4537
+
4538
+ .switch[aria-invalid="true"]:checked {
4539
+ --switch-bg: var(--danger);
4540
+ }
4541
+
4542
+ @supports selector(:user-invalid) {
4543
+ .needs-validation :is(.input, .textarea, .select, .check, .radio, .switch):user-invalid {
4544
+ --form-invalid-border: var(--danger);
4545
+ }
4546
+ }
4547
+
4548
+ @supports selector(:has(:user-invalid)) {
4549
+ .needs-validation .field:has(:user-invalid) > .field-error {
4550
+ display: block;
4551
+ }
4552
+ }
4553
+
4554
+ /*
4555
+ * Form actions — bottom form bar with sticky variant.
4556
+ *
4557
+ * A flex cluster of buttons/actions that closes a form. The sticky variant
4558
+ * docks to the viewport bottom with a translucent blur where supported; at
4559
+ * rest it stays visually neutral so it does not double as a separator.
4560
+ */
4561
+ .form-actions {
4562
+ display: flex;
4563
+ flex-wrap: wrap;
4564
+ gap: var(--gap);
4565
+ align-items: center;
4566
+ margin-block-start: var(--form-actions-margin-block-start, var(--space-5));
4567
+ }
4568
+
4569
+ /* Opt-in sticky variant. When the bar is not stuck (at rest) the translucent
4570
+ surface and backdrop-blur are visually neutral. Content scrolling behind
4571
+ the bar reveals the blur as a natural separator instead of relying on a
4572
+ shadow. */
4573
+ .form-actions.sticky {
4574
+ position: sticky;
4575
+ inset-block-end: var(--form-actions-sticky-block-offset, 0px);
4576
+ z-index: var(--z-sticky);
4577
+ isolation: isolate;
4578
+
4579
+ margin-inline: calc(var(--form-actions-sticky-inline-offset, 0px) * -1);
4580
+ padding: var(--form-actions-sticky-padding, var(--space-3));
4581
+
4582
+ border-block-start: var(--border-width) solid var(--border);
4583
+ background: var(--surface);
4584
+ -webkit-backdrop-filter: blur(1rem);
4585
+ backdrop-filter: blur(1rem);
4586
+ }
4587
+
4588
+ /* The translucent surface needs @supports, not a double declaration: the
4589
+ value contains var(), so a browser without color-mix() cannot reject it at
4590
+ parse time — it wins the cascade, then fails at computed-value time and
4591
+ the background disappears instead of falling back to the opaque line. */
4592
+ @supports (background: color-mix(in oklch, red 50%, transparent)) {
4593
+ .form-actions.sticky {
4594
+ background: color-mix(in oklch, var(--surface) 88%, transparent);
4595
+ }
4596
+ }
4597
+
4598
+ /*
4599
+ * Small, single-purpose utilities for common escape hatches.
4600
+ *
4601
+ * These support components and layout primitives without becoming a
4602
+ * utility-first API.
4603
+ */
4604
+ .sr-only {
4605
+ position: absolute;
4606
+ inline-size: 1px;
4607
+ block-size: 1px;
4608
+ padding: 0;
4609
+ margin: -1px;
4610
+ overflow: hidden;
4611
+ clip: rect(0, 0, 0, 0);
4612
+ white-space: nowrap;
4613
+ border: 0;
4614
+ }
4615
+
4616
+ .overflow-auto {
4617
+ overflow: auto;
4618
+ }
4619
+
4620
+ .fit {
4621
+ inline-size: fit-content;
4622
+ max-inline-size: 100%;
4623
+ }
4624
+
4625
+ .grow {
4626
+ flex: 1 1 auto;
4627
+ min-inline-size: 0;
4628
+ }
4629
+
4630
+ .nowrap {
4631
+ white-space: nowrap;
4632
+ }
4633
+
4634
+ .truncate {
4635
+ min-inline-size: 0;
4636
+ overflow: hidden;
4637
+ text-overflow: ellipsis;
4638
+ white-space: nowrap;
4639
+ }
4640
+
4641
+ .items-start {
4642
+ --items-align: start;
4643
+ align-items: start;
4644
+ }
4645
+
4646
+ .items-center {
4647
+ --items-align: center;
4648
+ align-items: center;
4649
+ }
4650
+
4651
+ .items-end {
4652
+ --items-align: end;
4653
+ align-items: end;
4654
+ }
4655
+
4656
+ .muted {
4657
+ color: var(--text-muted);
4658
+ }
4659
+
4660
+ /* Circle: forces a fully round shape without mutating global radius tokens.
4661
+ Works on any component because utilities load after components. .join is
4662
+ the one exception: it writes corner longhands on its children, so
4663
+ .join.circle keeps its own --join-radius hook (join.css). */
4664
+ .circle {
4665
+ border-radius: var(--radius-full);
4666
+ }
4667
+
4668
+ /* Text alignment — uses logical properties so it follows writing direction. */
4669
+ .text-start {
4670
+ --text-align: start;
4671
+ text-align: start;
4672
+ }
4673
+
4674
+ .text-center {
4675
+ --text-align: center;
4676
+ text-align: center;
4677
+ }
4678
+
4679
+ .text-end {
4680
+ --text-align: end;
4681
+ text-align: end;
4682
+ }
4683
+
4684
+ /* Text wrapping utilities. Unsupported values are ignored by the browser. */
4685
+ .text-balance {
4686
+ text-wrap: balance;
4687
+ }
4688
+
4689
+ .text-pretty {
4690
+ text-wrap: pretty;
4691
+ }
4692
+
4693
+ /* Generic list chrome reset. Use for any list (tags, footer columns, related
4694
+ items). For navigation lists, prefer .nav-list for semantic intent.
4695
+ components/{actions,navbar}.css apply their own resets where needed. */
4696
+ .list-reset {
4697
+ margin: 0;
4698
+ padding: 0;
4699
+ list-style: none;
4700
+ }
4701
+
4702
+ /* Separator dot for inline metadata rows (byline, timestamp, read time,
4703
+ breadcrumb-like facts). Pair with a flex/cluster container; flex: none
4704
+ keeps a flex parent from shrinking it to nothing. */
4705
+ .dot {
4706
+ flex: none;
4707
+ inline-size: 0.25rem;
4708
+ block-size: 0.25rem;
4709
+ border-radius: var(--radius-full);
4710
+ background: var(--text-subtle);
4711
+ }
4712
+
4713
+ /* Link variants for chrome that should not look like prose links. Use in
4714
+ navigation, tags, footer, tables of contents, and similar surfaces. */
4715
+ .link-plain {
4716
+ color: inherit;
4717
+ text-decoration: none;
4718
+ }
4719
+
4720
+ .link-muted {
4721
+ color: var(--text-muted);
4722
+ text-decoration: none;
4723
+ }
4724
+
4725
+ @media (hover: hover) {
4726
+ .link-muted:hover {
4727
+ color: var(--text);
4728
+ }
4729
+ }
4730
+
4731
+ /* Spacing step helpers — semantic scale (sm / default / lg).
4732
+ Names map to logical properties for writing-direction safety. The default
4733
+ step uses --space-4, which matches the framework's built-in padding on
4734
+ cards, dialogs, and form-actions. */
4735
+
4736
+ /* Gap */
4737
+ .gap-none {
4738
+ gap: 0;
4739
+ }
4740
+
4741
+ /* Padding — block (vertical) */
4742
+ .py {
4743
+ padding-block: var(--variant-space);
4744
+ }
4745
+
4746
+ /* Padding — inline (horizontal) */
4747
+ .px {
4748
+ padding-inline: var(--variant-space);
4749
+ }
4750
+
4751
+ /* Margin — block-start */
4752
+ .mbs {
4753
+ margin-block-start: var(--variant-space);
4754
+ }
4755
+
4756
+ /* Margin — block-end */
4757
+ .mbe {
4758
+ margin-block-end: var(--variant-space);
4759
+ }
4760
+
4761
+ /* Joined controls are a component, but must load after the controls it groups. */
4762
+ /*
4763
+ * Join — visually groups adjacent controls into a single unit.
4764
+ *
4765
+ * This is a cross-cutting control primitive, not a forms-only component:
4766
+ * it applies equally to inputs, selects, buttons, and button-like links.
4767
+ *
4768
+ * Cascade note: .join writes border-radius longhands on its direct children.
4769
+ * Keep this file loaded after control/component styles so those corners win at
4770
+ * equal specificity.
4771
+ */
4772
+ .join {
4773
+ --join-radius: var(--radius);
4774
+ --join-ss: 0;
4775
+ --join-se: 0;
4776
+ --join-es: 0;
4777
+ --join-ee: 0;
4778
+
4779
+ align-items: stretch;
4780
+ display: inline-flex;
4781
+ }
4782
+
4783
+ /* The .circle utility cannot round a join: corners are written as longhands
4784
+ on the children below, so the group radius must flow through the token. */
4785
+ .join.circle {
4786
+ --join-radius: var(--radius-full);
4787
+ }
4788
+
4789
+ .join-addon {
4790
+ --control-border: var(--border);
4791
+ --control-bg: var(--surface-subtle);
4792
+ --control-fg: var(--text-muted);
4793
+
4794
+ align-items: center;
4795
+ display: inline-flex;
4796
+ justify-content: center;
4797
+ min-block-size: var(--control-size);
4798
+ padding-inline: var(--space-3);
4799
+ border: var(--border-width) solid var(--control-border);
4800
+ border-radius: var(--radius);
4801
+ background: var(--control-bg);
4802
+ color: var(--control-fg);
4803
+ font-size: var(--control-font-size);
4804
+ line-height: 1;
4805
+ white-space: nowrap;
4806
+ }
4807
+
4808
+ .join > * {
4809
+ border-start-start-radius: var(--join-ss);
4810
+ border-start-end-radius: var(--join-se);
4811
+ border-end-end-radius: var(--join-ee);
4812
+ border-end-start-radius: var(--join-es);
4813
+ }
4814
+
4815
+ .join > :first-child {
4816
+ --join-ss: var(--join-radius);
4817
+ --join-es: var(--join-radius);
4818
+ }
4819
+
4820
+ .join > :last-child {
4821
+ --join-se: var(--join-radius);
4822
+ --join-ee: var(--join-radius);
4823
+ }
4824
+
4825
+ .join > :only-child {
4826
+ --join-ss: var(--join-radius);
4827
+ --join-se: var(--join-radius);
4828
+ --join-es: var(--join-radius);
4829
+ --join-ee: var(--join-radius);
4830
+ }
4831
+
4832
+ .join > * + * {
4833
+ margin-inline-start: calc(var(--border-width) * -1);
4834
+ }
4835
+
4836
+ .join > :is(:hover, :focus, :focus-within) {
4837
+ z-index: 1;
4838
+ }
4839
+
4840
+ /* Keep an active focus ring above an adjacent hovered control. This matters
4841
+ on platforms where clicking a button does not move focus from the input. */
4842
+ .join > :is(:focus, :focus-within) {
4843
+ z-index: 2;
4844
+ }
4845
+
4846
+ /* An input's border must read as the "primary" edge against an attached
4847
+ action (e.g. a password-reveal button) at all times, not only while
4848
+ hovered or focused — otherwise the shared edge flips ownership as the
4849
+ user's pointer moves, which reads as a rendering glitch. */
4850
+ .join > :is(.input, .textarea, .select) {
4851
+ z-index: 3;
4852
+ }
4853
+
4854
+ /* An input/select with an attached action reads as one composite field. Move
4855
+ its outer focus treatment to the join; button-only groups keep per-button
4856
+ focus, and unsupported :has() browsers retain the control's normal ring. */
4857
+ .join:has(> :is(.input, .textarea, .select):focus-visible) {
4858
+ border-radius: var(--join-radius);
4859
+ outline: var(--focus-outline);
4860
+ outline-offset: var(--focus-outline-offset);
4861
+ box-shadow: var(--focus-ring-shadow);
4862
+ }
4863
+
4864
+ .join:has(> :is(.input, .textarea, .select)) > :is(.input, .textarea, .select):focus-visible {
4865
+ outline: none;
4866
+ box-shadow: none;
4867
+ }
4868
+
4869
+ /* Focus baseline — loaded after components so its legacy fallback also covers
4870
+ controls that reset their native outline. Modern browsers hide it for focus
4871
+ that is not keyboard-relevant; components layer local state on top. */
4872
+ :focus {
4873
+ outline: var(--focus-outline);
4874
+ outline-offset: var(--focus-outline-offset);
4875
+ }
4876
+
4877
+ @supports selector(:focus-visible) {
4878
+ :focus:not(:focus-visible) {
4879
+ outline: none;
4880
+ }
4881
+ }
4882
+
4883
+ @media print {
4884
+ :root,
4885
+ body {
4886
+ /* Save some ink in dark mode */
4887
+ color-scheme: light;
4888
+ background: #fff;
4889
+ color: #000;
4890
+ }
4891
+
4892
+ .flyout,
4893
+ .tooltip,
4894
+ .surface-backdrop,
4895
+ .status-bar,
4896
+ .spinner,
4897
+ dialog.modal[open],
4898
+ dialog.drawer[open] {
4899
+ display: none !important;
4900
+ }
4901
+
4902
+ button.btn,
4903
+ input[type="button"].btn,
4904
+ input[type="submit"].btn,
4905
+ input[type="reset"].btn {
4906
+ display: none !important;
4907
+ }
4908
+
4909
+ a.btn[href^="http"]::after {
4910
+ content: " (" attr(href) ")";
4911
+ font-size: 0.875em;
4912
+ }
4913
+
4914
+ .card,
4915
+ .alert,
4916
+ .badge {
4917
+ border: var(--border-width) solid CanvasText;
4918
+ break-inside: avoid;
4919
+ }
4920
+
4921
+ h1,
4922
+ h2,
4923
+ h3,
4924
+ h4,
4925
+ h5,
4926
+ h6 {
4927
+ break-after: avoid;
4928
+ }
4929
+ }