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.
- package/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/actual.css +4929 -0
- package/dist/actual.js +3 -0
- package/dist/actual.js.map +31 -0
- package/dist/actual.min.css +1 -0
- package/docs/README.md +26 -0
- package/docs/components.md +860 -0
- package/docs/design-notes/accessibility-decisions.md +32 -0
- package/docs/design-notes/browser-support.md +93 -0
- package/docs/design-notes/cascade-layer.md +52 -0
- package/docs/design-notes/css-anchor.md +33 -0
- package/docs/design-notes/naming.md +37 -0
- package/docs/design-notes/previous-work.md +5 -0
- package/docs/design-notes/references.md +324 -0
- package/docs/forms.md +1265 -0
- package/docs/javascript.md +426 -0
- package/docs/layout.md +480 -0
- package/docs/mental-model.md +73 -0
- package/docs/patterns.md +415 -0
- package/docs/reserved.md +7 -0
- package/docs/tokens.md +455 -0
- package/docs/typography.md +159 -0
- package/docs/ui.md +937 -0
- package/docs/utilities.md +384 -0
- package/llms.txt +17 -0
- package/package.json +101 -0
- package/src/css/actual.css +17 -0
- package/src/css/actual.full.css +2 -0
- package/src/css/actual.layer.css +3 -0
- package/src/css/base.css +70 -0
- package/src/css/components/accordion.css +97 -0
- package/src/css/components/actions.css +21 -0
- package/src/css/components/alert.css +137 -0
- package/src/css/components/avatar.css +199 -0
- package/src/css/components/badge.css +143 -0
- package/src/css/components/breadcrumb.css +56 -0
- package/src/css/components/busy.css +44 -0
- package/src/css/components/button.css +216 -0
- package/src/css/components/card.css +140 -0
- package/src/css/components/drawer.css +122 -0
- package/src/css/components/flyout.css +243 -0
- package/src/css/components/index.css +41 -0
- package/src/css/components/join.css +106 -0
- package/src/css/components/key.css +25 -0
- package/src/css/components/meter.css +81 -0
- package/src/css/components/modal.css +162 -0
- package/src/css/components/navbar.css +98 -0
- package/src/css/components/overline.css +49 -0
- package/src/css/components/pagination.css +30 -0
- package/src/css/components/progress.css +126 -0
- package/src/css/components/scrollspy.css +27 -0
- package/src/css/components/skeleton.css +67 -0
- package/src/css/components/spinner.css +60 -0
- package/src/css/components/status-bar.css +82 -0
- package/src/css/components/surface.css +48 -0
- package/src/css/components/tab.css +128 -0
- package/src/css/components/table.css +64 -0
- package/src/css/components/tooltip.css +107 -0
- package/src/css/dialog-fallback.css +56 -0
- package/src/css/focus.css +13 -0
- package/src/css/forms/choice-card.css +158 -0
- package/src/css/forms/choice.css +196 -0
- package/src/css/forms/control.css +106 -0
- package/src/css/forms/custom-select.css +93 -0
- package/src/css/forms/form-actions.css +43 -0
- package/src/css/forms/form.css +57 -0
- package/src/css/forms/index.css +15 -0
- package/src/css/forms/input-icon.css +36 -0
- package/src/css/forms/native.css +68 -0
- package/src/css/forms/select.css +36 -0
- package/src/css/forms/switch.css +127 -0
- package/src/css/forms/validation.css +63 -0
- package/src/css/grid.css +95 -0
- package/src/css/intents.css +33 -0
- package/src/css/layout.css +80 -0
- package/src/css/optional/index.css +5 -0
- package/src/css/optional/layout-extra.css +55 -0
- package/src/css/optional/scroll-snap.css +51 -0
- package/src/css/optional/scroller.css +51 -0
- package/src/css/optional/typography-fluid.css +48 -0
- package/src/css/optional/utilities-extra.css +289 -0
- package/src/css/print.css +47 -0
- package/src/css/prose.css +187 -0
- package/src/css/reset.css +56 -0
- package/src/css/theme.css +186 -0
- package/src/css/themes/corporate.css +60 -0
- package/src/css/themes/cyberpunk.css +60 -0
- package/src/css/themes/dark.css +32 -0
- package/src/css/themes/dim.css +33 -0
- package/src/css/themes/edge.css +143 -0
- package/src/css/themes/forest.css +65 -0
- package/src/css/themes/gradient.css +97 -0
- package/src/css/themes/index.css +43 -0
- package/src/css/themes/lavender.css +66 -0
- package/src/css/themes/mono.css +60 -0
- package/src/css/themes/neon.css +66 -0
- package/src/css/themes/ocean.css +60 -0
- package/src/css/themes/petrol.css +64 -0
- package/src/css/themes/spruce.css +64 -0
- package/src/css/themes/square.css +68 -0
- package/src/css/themes/sunset.css +66 -0
- package/src/css/tokens.css +122 -0
- package/src/css/utilities.css +162 -0
- package/src/css/variants.css +118 -0
- package/src/js/command.js +165 -0
- package/src/js/context-menu.js +291 -0
- package/src/js/dialog-fallback.js +163 -0
- package/src/js/dialog.js +503 -0
- package/src/js/enhance.js +208 -0
- package/src/js/events.js +22 -0
- package/src/js/filter.js +122 -0
- package/src/js/floating.js +462 -0
- package/src/js/flyout.js +187 -0
- package/src/js/focus.js +35 -0
- package/src/js/index.js +26 -0
- package/src/js/input.js +47 -0
- package/src/js/keys.js +30 -0
- package/src/js/mask.js +184 -0
- package/src/js/menu.js +88 -0
- package/src/js/password.js +98 -0
- package/src/js/scrollspy.js +121 -0
- package/src/js/selectors.js +59 -0
- package/src/js/status.js +158 -0
- package/src/js/surface.js +358 -0
- package/src/js/tab.js +173 -0
- package/src/js/tooltip.js +374 -0
- package/src/js/validation.js +419 -0
package/dist/actual.css
ADDED
|
@@ -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
|
+
}
|