actual-css 0.7.0 → 0.9.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 +217 -0
- package/README.md +13 -11
- package/components.json +668 -0
- package/dist/actual.css +166 -37
- package/dist/actual.full.css +1087 -643
- package/dist/actual.full.js +1 -1
- package/dist/actual.full.min.css +1 -1
- package/dist/actual.js +1 -1
- package/dist/actual.min.css +1 -1
- package/docs/design-notes/accessibility-decisions.md +33 -0
- package/docs/design-notes/browser-support.md +62 -30
- package/docs/design-notes/column-layout.md +3 -3
- package/docs/design-notes/css-anchor.md +75 -21
- package/docs/design-notes/enhancement-contract.md +14 -6
- package/docs/design-notes/grid.md +4 -4
- package/docs/design-notes/naming.md +6 -1
- package/docs/design-notes/platform-alignment.md +53 -7
- package/docs/design-notes/popover-manual-poc.md +105 -25
- package/docs/design-notes/surface-reparenting.md +56 -47
- package/docs/design-notes/tooltip-coordinate-space.md +132 -0
- package/docs/design-notes/widget-primitives.md +7 -4
- package/docs/pages/components/accordion.md +41 -2
- package/docs/pages/components/alert.md +85 -30
- package/docs/pages/components/app-navigation.md +6 -0
- package/docs/pages/components/avatar.md +6 -6
- package/docs/pages/components/badge.md +101 -16
- package/docs/pages/components/button.md +16 -8
- package/docs/pages/components/card.md +10 -10
- package/docs/pages/components/dialog.md +21 -6
- package/docs/pages/components/drawer.md +13 -0
- package/docs/pages/components/fab.md +4 -3
- package/docs/pages/components/list.md +42 -4
- package/docs/pages/components/navbar.md +17 -1
- package/docs/pages/components/pagination.md +2 -2
- package/docs/pages/components/skeleton.md +3 -1
- package/docs/pages/components/steps.md +19 -0
- package/docs/pages/components/table.md +3 -2
- package/docs/pages/enhancements/flyout.md +82 -38
- package/docs/pages/enhancements/overview.md +25 -0
- package/docs/pages/enhancements/status-bar.md +1 -1
- package/docs/pages/enhancements/tabs.md +45 -37
- package/docs/pages/enhancements/tooltip.md +36 -10
- package/docs/pages/examples/overview.md +38 -5
- package/docs/pages/forms/choice-cards.md +11 -6
- package/docs/pages/forms/combobox.md +1 -1
- package/docs/pages/forms/floating-field.md +4 -3
- package/docs/pages/forms/joined-controls.md +22 -0
- package/docs/pages/forms/otp.md +2 -1
- package/docs/pages/forms/overview.md +1 -1
- package/docs/pages/forms/states-and-sizes.md +16 -18
- package/docs/pages/forms/sticky-actions.md +2 -2
- package/docs/pages/forms/switch.md +1 -1
- package/docs/pages/foundations/philosophy.md +57 -26
- package/docs/pages/foundations/theming.md +2 -2
- package/docs/pages/foundations/tokens.md +118 -21
- package/docs/pages/foundations/typography.md +35 -14
- package/docs/pages/guides/introduction.md +11 -9
- package/docs/pages/guides/progressive-enhancement.md +115 -30
- package/docs/pages/guides/tailwind.md +3 -2
- package/docs/pages/layout/media.md +1 -1
- package/docs/pages/layout/overview.md +11 -1
- package/docs/pages/layout/scroll-snap.md +3 -3
- package/docs/pages/layout/stack.md +17 -9
- package/docs/pages/patterns/icon-input.md +4 -2
- package/docs/pages/patterns/icon-slot.md +2 -0
- package/docs/pages/patterns/intent-nesting.md +1 -1
- package/docs/pages/utilities/overview.md +23 -2
- package/docs/pages/utilities/sizing-wrapping.md +1 -1
- package/docs/pages/utilities/spacing.md +10 -8
- package/llms.txt +2 -1
- package/package.json +159 -149
- package/{scripts/reserved-classes.json → reserved-classes.json} +4 -6
- package/src/css/components/accordion.css +42 -3
- package/src/css/components/alert.css +25 -13
- package/src/css/components/app-nav.css +18 -8
- package/src/css/components/avatar.css +13 -1
- package/src/css/components/badge.css +78 -12
- package/src/css/components/breadcrumb.css +2 -3
- package/src/css/components/button.css +9 -16
- package/src/css/components/card.css +9 -2
- package/src/css/components/chat.css +1 -0
- package/src/css/components/data-list.css +1 -1
- package/src/css/components/drawer.css +101 -32
- package/src/css/components/fab.css +0 -6
- package/src/css/components/flyout.css +35 -98
- package/src/css/components/index.css +0 -1
- package/src/css/components/indicator.css +21 -0
- package/src/css/components/key.css +10 -0
- package/src/css/components/list.css +40 -4
- package/src/css/components/menu.css +2 -1
- package/src/css/components/modal.css +48 -33
- package/src/css/components/navbar.css +37 -4
- package/src/css/components/pagination.css +1 -0
- package/src/css/components/skeleton.css +15 -4
- package/src/css/components/spinner.css +6 -6
- package/src/css/components/status-bar.css +0 -5
- package/src/css/components/steps.css +45 -59
- package/src/css/components/tab.css +29 -9
- package/src/css/components/table.css +13 -5
- package/src/css/components/tooltip.css +36 -40
- package/src/css/core/base.css +23 -1
- package/src/css/core/intents.css +17 -6
- package/src/css/core/theme.css +27 -0
- package/src/css/core/tokens.css +35 -15
- package/src/css/core/variants.css +64 -15
- package/src/css/forms/choice-card.css +4 -4
- package/src/css/forms/choice.css +76 -29
- package/src/css/forms/control.css +2 -1
- package/src/css/forms/floating-field.css +3 -3
- package/src/css/forms/form.css +4 -3
- package/src/css/forms/input-icon.css +20 -14
- package/src/css/forms/native.css +6 -1
- package/src/css/forms/otp.css +9 -1
- package/src/css/forms/range.css +2 -0
- package/src/css/forms/select.css +29 -9
- package/src/css/forms/switch.css +28 -8
- package/src/css/layout/app-layout.css +1 -1
- package/src/css/layout/grid.css +50 -76
- package/src/css/layout/scroll-snap.css +11 -7
- package/src/css/layout/scroller.css +3 -4
- package/src/css/layout/stack.css +2 -2
- package/src/css/typography/fluid.css +4 -3
- package/src/css/typography/prose.css +6 -5
- package/src/css/utilities/base.css +2 -1
- package/src/css/utilities/extra.css +20 -17
- package/src/js/context-menu.js +27 -26
- package/src/js/dialog.js +2 -1
- package/src/js/dismiss.js +5 -0
- package/src/js/enhance.js +34 -2
- package/src/js/enhancement-loader.js +36 -43
- package/src/js/events.js +0 -3
- package/src/js/filter.js +0 -1
- package/src/js/floating-space.js +42 -0
- package/src/js/flyout.js +9 -44
- package/src/js/focus-group.js +7 -0
- package/src/js/focus.js +6 -0
- package/src/js/menu.js +10 -1
- package/src/js/selectors.js +5 -9
- package/src/js/status.js +6 -5
- package/src/js/surface.js +97 -139
- package/src/js/tab.js +6 -1
- package/src/js/tooltip.js +150 -51
- package/src/js/validation.js +1 -1
- package/src/css/components/surface.css +0 -49
package/dist/actual.full.css
CHANGED
|
@@ -74,9 +74,9 @@ select {
|
|
|
74
74
|
--line-height: 1.5;
|
|
75
75
|
--line-height-tight: 1.25;
|
|
76
76
|
--line-height-relaxed: 1.75;
|
|
77
|
-
--font-weight: 400;
|
|
77
|
+
--font-weight-normal: 400;
|
|
78
78
|
--font-weight-medium: 500;
|
|
79
|
-
--font-weight-
|
|
79
|
+
--font-weight-semibold: 600;
|
|
80
80
|
--font-weight-bold: 700;
|
|
81
81
|
|
|
82
82
|
/* Typography scale */
|
|
@@ -91,7 +91,6 @@ select {
|
|
|
91
91
|
applications, and no future framework step can collide with them. */
|
|
92
92
|
--gap: 0.75rem;
|
|
93
93
|
--density-space: var(--space-40);
|
|
94
|
-
--density-compact-size: 1.5rem;
|
|
95
94
|
--space-10: 0.25rem;
|
|
96
95
|
--space-20: 0.5rem;
|
|
97
96
|
--space-30: 0.75rem;
|
|
@@ -117,7 +116,10 @@ select {
|
|
|
117
116
|
beyond this vocabulary. Toggles that share one transition list at
|
|
118
117
|
--duration-fast (flyout, tooltip) and pure-opacity backdrops stay on
|
|
119
118
|
`ease`: at 100ms the curve has almost no resolving time, and a scrim is
|
|
120
|
-
not traveling anywhere.
|
|
119
|
+
not traveling anywhere. Animation policy: reset.css is the global safety
|
|
120
|
+
net; every meaningful @keyframes is explicitly gated behind
|
|
121
|
+
prefers-reduced-motion: no-preference; ordinary transitions rely on the
|
|
122
|
+
reset alone. */
|
|
121
123
|
--duration: 150ms;
|
|
122
124
|
--duration-fast: 100ms;
|
|
123
125
|
--duration-slow: 200ms;
|
|
@@ -148,20 +150,29 @@ select {
|
|
|
148
150
|
--bar-height: 0.65rem;
|
|
149
151
|
|
|
150
152
|
/* Controls — inputs and buttons share --control-size so heights always match.
|
|
151
|
-
.
|
|
152
|
-
|
|
153
|
+
.compact/.spacious (variants.css) switch geometry without changing
|
|
154
|
+
--control-font-size. Local .sm/.lg component sizes switch all three.
|
|
153
155
|
|
|
154
156
|
Inline choices (checkbox, radio, switch) are typographic, not field-sized:
|
|
155
157
|
they sit on a line of text, so they follow --control-font-size through
|
|
156
158
|
--choice-control-size instead of --control-size. Being an em value in
|
|
157
159
|
:root is deliberate — an unregistered custom property resolves its units
|
|
158
160
|
at the element that consumes it, so one declaration tracks whatever font
|
|
159
|
-
size the choice actually renders at.
|
|
161
|
+
size the choice actually renders at.
|
|
162
|
+
|
|
163
|
+
--control-pad-x is the opposite kind of token: a resolved geometry length,
|
|
164
|
+
not an em ratio. Every part of one control must see the same horizontal
|
|
165
|
+
distance, so it is stated in the typographic tokens rather than an em
|
|
166
|
+
whose value would change on a descendant with its own font size (the
|
|
167
|
+
input-icon glyph, made larger by --input-icon-size). It mirrors
|
|
168
|
+
--control-font-size and is rebound beside it at the .sm/.lg boundaries
|
|
169
|
+
(variants.css); declaring it only in :root would freeze the md value,
|
|
170
|
+
because a var() inside a custom property resolves where it is declared. */
|
|
160
171
|
--control-size-sm: 2rem;
|
|
161
172
|
--control-size-md: 2.375rem;
|
|
162
173
|
--control-size-lg: 2.75rem;
|
|
163
174
|
--control-size: var(--control-size-md);
|
|
164
|
-
--control-pad-x:
|
|
175
|
+
--control-pad-x: var(--font-size-md);
|
|
165
176
|
--control-font-size: var(--font-size-md);
|
|
166
177
|
--choice-control-size: 1.125em;
|
|
167
178
|
--disabled-opacity: 0.65;
|
|
@@ -194,7 +205,10 @@ select {
|
|
|
194
205
|
--soft-hover-alpha: 12%;
|
|
195
206
|
/* Soft foreground: the share of raw intent in soft text. 100% keeps the
|
|
196
207
|
pure intent color; lower values rebate it toward --text, which vivid or
|
|
197
|
-
light intent palettes need to stay legible on their own soft surface.
|
|
208
|
+
light intent palettes need to stay legible on their own soft surface.
|
|
209
|
+
The default palette ships per-role --*-soft-fg hooks calibrated against
|
|
210
|
+
the hovered soft fill instead (theme.css); this mix is the fallback a
|
|
211
|
+
theme without hooks falls back to. */
|
|
198
212
|
--soft-fg-mix: 100%;
|
|
199
213
|
|
|
200
214
|
/* Shared icon assets. Override these URLs when a strict CSP disallows data:. */
|
|
@@ -210,13 +224,14 @@ select {
|
|
|
210
224
|
--viewport-block: 100vh;
|
|
211
225
|
--viewport-inline: 100vw;
|
|
212
226
|
|
|
213
|
-
/* Z-index scale —
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
227
|
+
/* Z-index scale — orders document-layer components only. Native top-layer
|
|
228
|
+
elements, dialogs and popovers alike, do not participate in this ordering:
|
|
229
|
+
they always paint above the document, and their order among themselves is
|
|
230
|
+
their top-layer promotion order. No z-index value here can reach them.
|
|
231
|
+
Actual CSS reserves multiples of 10 for framework layers; applications may
|
|
232
|
+
use intermediate values for local layering. */
|
|
217
233
|
--z-sticky: 10;
|
|
218
234
|
--z-menu: 20;
|
|
219
|
-
--z-tooltip: 50;
|
|
220
235
|
--z-status: 60;
|
|
221
236
|
}
|
|
222
237
|
|
|
@@ -251,7 +266,12 @@ select {
|
|
|
251
266
|
--shadow-popout: 0 0.25rem 0.75rem color-mix(in oklch, var(--shadow-color) 15%, transparent);
|
|
252
267
|
}
|
|
253
268
|
|
|
254
|
-
|
|
269
|
+
/* Theme-derived like the shadows above, so it is declared on the same
|
|
270
|
+
boundaries: an alias resolved only on :root is inherited already composed,
|
|
271
|
+
and a [data-theme] island overriding --backdrop-color or
|
|
272
|
+
--backdrop-opacity would not change the scrim of a dialog inside it. */
|
|
273
|
+
:root,
|
|
274
|
+
[data-theme] {
|
|
255
275
|
--backdrop-fill: color-mix(
|
|
256
276
|
in oklch,
|
|
257
277
|
var(--backdrop-color) calc(var(--backdrop-opacity) * 100%),
|
|
@@ -363,6 +383,33 @@ select {
|
|
|
363
383
|
--focus-ring-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring);
|
|
364
384
|
}
|
|
365
385
|
|
|
386
|
+
/* Soft foreground hooks — the soft ink is the intent rebated toward the local
|
|
387
|
+
text so it keeps 4.5:1 against the soft hover fill (the resting fill carries
|
|
388
|
+
the extra margin; variants.css soft recipe). Deliberately scoped to the
|
|
389
|
+
default theme: a custom property bakes its var() references where it is
|
|
390
|
+
declared, so these resolve against root/light/dark tokens on every boundary.
|
|
391
|
+
Presets and custom islands keep their --soft-fg-mix / raw-intent ink unless
|
|
392
|
+
they declare hooks themselves — :where() keeps this reset from outranking a
|
|
393
|
+
theme's own declaration (e.g. brutalist's 45% soft-fg-mix). */
|
|
394
|
+
:where([data-theme]:not([data-theme="light"]):not([data-theme="dark"])) {
|
|
395
|
+
--primary-soft-fg: initial;
|
|
396
|
+
--secondary-soft-fg: initial;
|
|
397
|
+
--success-soft-fg: initial;
|
|
398
|
+
--warning-soft-fg: initial;
|
|
399
|
+
--danger-soft-fg: initial;
|
|
400
|
+
--neutral-soft-fg: initial;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
:root,
|
|
404
|
+
[data-theme="light"],
|
|
405
|
+
[data-theme="dark"] {
|
|
406
|
+
--secondary-soft-fg: color-mix(in oklab, var(--text) 10%, var(--secondary) 90%);
|
|
407
|
+
--success-soft-fg: color-mix(in oklab, var(--text) 15%, var(--success) 85%);
|
|
408
|
+
--warning-soft-fg: color-mix(in oklab, var(--text) 25%, var(--warning) 75%);
|
|
409
|
+
--danger-soft-fg: color-mix(in oklab, var(--text) 15%, var(--danger) 85%);
|
|
410
|
+
--neutral-soft-fg: color-mix(in oklab, var(--text) 20%, var(--neutral) 80%);
|
|
411
|
+
}
|
|
412
|
+
|
|
366
413
|
/* Native platform controls (checkbox, radio, range, progress) that are not
|
|
367
414
|
restyled by a component class still follow the theme accent. Declared on
|
|
368
415
|
every island: accent-color inherits as a computed value, so islands must
|
|
@@ -471,6 +518,7 @@ select {
|
|
|
471
518
|
*
|
|
472
519
|
* Public hooks:
|
|
473
520
|
* --link
|
|
521
|
+
* --hr-space
|
|
474
522
|
*/
|
|
475
523
|
|
|
476
524
|
body {
|
|
@@ -512,10 +560,23 @@ hgroup > * {
|
|
|
512
560
|
margin-block: 0;
|
|
513
561
|
}
|
|
514
562
|
|
|
563
|
+
/* Section break.
|
|
564
|
+
|
|
565
|
+
margin-inline: 0 overrides the UA's `margin: 0.5em auto`. In block flow the
|
|
566
|
+
auto margins resolve to zero and nothing changes, but as a flex or grid item
|
|
567
|
+
an inline auto margin replaces the stretch — an <hr> in a .stack computed to
|
|
568
|
+
width 0 and vanished. .stack deliberately leaves the inline axis free, so
|
|
569
|
+
the reset belongs here.
|
|
570
|
+
|
|
571
|
+
--hr-space tunes the block rhythm on the instance: a rule closing a page
|
|
572
|
+
section and a rule separating two rows in a panel are the same element at
|
|
573
|
+
very different scales. It applies to an hr in document flow; inside .stack
|
|
574
|
+
or .prose the parent owns the spacing between siblings and wins. */
|
|
515
575
|
hr {
|
|
516
576
|
border: 0;
|
|
517
577
|
border-block-start: var(--border-width) solid var(--border);
|
|
518
|
-
margin-block: var(--space-60);
|
|
578
|
+
margin-block: var(--hr-space, var(--space-60));
|
|
579
|
+
margin-inline: 0;
|
|
519
580
|
}
|
|
520
581
|
|
|
521
582
|
code,
|
|
@@ -525,6 +586,14 @@ pre {
|
|
|
525
586
|
font-family: var(--font-mono);
|
|
526
587
|
}
|
|
527
588
|
|
|
589
|
+
/* Overflow containment only. One unbreakable line — a URL, a minified token, a
|
|
590
|
+
wide ASCII table — otherwise widens the whole page, and an author cannot see
|
|
591
|
+
it coming from the markup. Background, padding and radius stay out: those
|
|
592
|
+
belong to .prose or to an application code-block component. */
|
|
593
|
+
pre {
|
|
594
|
+
overflow-x: auto;
|
|
595
|
+
}
|
|
596
|
+
|
|
528
597
|
small {
|
|
529
598
|
font-size: 0.875em;
|
|
530
599
|
}
|
|
@@ -536,36 +605,47 @@ small {
|
|
|
536
605
|
|
|
537
606
|
/*
|
|
538
607
|
* Intent classes do not style elements directly.
|
|
539
|
-
* They only expose --intent
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
608
|
+
* They only expose --intent, --intent-fg, and the optional per-intent soft
|
|
609
|
+
* foreground --intent-soft-fg, relayed from the per-role --*-soft-fg hooks.
|
|
610
|
+
* The default theme declares those hooks for its soft ink (see theme.css);
|
|
611
|
+
* a theme without them lets the relay resolve to an invalid value, so the soft
|
|
612
|
+
* recipe falls back to its --soft-fg-mix derivation and a theme opts in one
|
|
613
|
+
* role at a time (docs/pages/foundations/tokens.md). Components and variants
|
|
614
|
+
* decide how to consume them. Each component that consumes --intent,
|
|
615
|
+
* --intent-soft-fg or --ui-* opens a local intent boundary in a :where() block
|
|
616
|
+
* that resets exactly the tokens it consumes to initial. Parent intents do not
|
|
617
|
+
* leak into nested components. Local intent classes (.primary, .danger, etc.)
|
|
618
|
+
* still apply because they have higher specificity than :where().
|
|
545
619
|
*/
|
|
546
620
|
.primary {
|
|
547
621
|
--intent: var(--primary);
|
|
548
622
|
--intent-fg: var(--primary-fg);
|
|
623
|
+
--intent-soft-fg: var(--primary-soft-fg);
|
|
549
624
|
}
|
|
550
625
|
.secondary {
|
|
551
626
|
--intent: var(--secondary);
|
|
552
627
|
--intent-fg: var(--secondary-fg);
|
|
628
|
+
--intent-soft-fg: var(--secondary-soft-fg);
|
|
553
629
|
}
|
|
554
630
|
.success {
|
|
555
631
|
--intent: var(--success);
|
|
556
632
|
--intent-fg: var(--success-fg);
|
|
633
|
+
--intent-soft-fg: var(--success-soft-fg);
|
|
557
634
|
}
|
|
558
635
|
.warning {
|
|
559
636
|
--intent: var(--warning);
|
|
560
637
|
--intent-fg: var(--warning-fg);
|
|
638
|
+
--intent-soft-fg: var(--warning-soft-fg);
|
|
561
639
|
}
|
|
562
640
|
.danger {
|
|
563
641
|
--intent: var(--danger);
|
|
564
642
|
--intent-fg: var(--danger-fg);
|
|
643
|
+
--intent-soft-fg: var(--danger-soft-fg);
|
|
565
644
|
}
|
|
566
645
|
.neutral {
|
|
567
646
|
--intent: var(--neutral);
|
|
568
647
|
--intent-fg: var(--neutral-fg);
|
|
648
|
+
--intent-soft-fg: var(--neutral-soft-fg);
|
|
569
649
|
}
|
|
570
650
|
|
|
571
651
|
/* Paint an inherited or local intent on a simple foreground element without
|
|
@@ -591,16 +671,15 @@ small {
|
|
|
591
671
|
* components/badge.css — .badge default soft behind explicit --ui-*
|
|
592
672
|
* components/alert.css — .alert default soft behind explicit --ui-*
|
|
593
673
|
*
|
|
594
|
-
* .
|
|
595
|
-
* Density never changes typography
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
* three-step scale.
|
|
674
|
+
* .compact/.spacious set inherited density tokens: spacing rhythm and geometry
|
|
675
|
+
* only. Density never changes typography or icon size. Controls consume
|
|
676
|
+
* --control-size; non-control components may consume --density-* when the
|
|
677
|
+
* context should affect their local rhythm.
|
|
678
|
+
*
|
|
679
|
+
* .sm/.lg are local size roles, not shared measurements. Shared control
|
|
680
|
+
* participants below map both typography and geometry; components with
|
|
681
|
+
* bespoke geometry (badge, spinner, avatar, rating, key, prose) own those
|
|
682
|
+
* mappings locally. Components without an explicit mapping do not participate.
|
|
604
683
|
*
|
|
605
684
|
* Intent list source of truth: intents.css. Components consume --intent* tokens
|
|
606
685
|
* instead of re-listing intent classes.
|
|
@@ -656,17 +735,64 @@ small {
|
|
|
656
735
|
border-color: var(--ui-border);
|
|
657
736
|
}
|
|
658
737
|
|
|
659
|
-
.
|
|
738
|
+
.compact {
|
|
660
739
|
--gap: var(--space-20);
|
|
661
740
|
--density-space: var(--space-20);
|
|
662
741
|
--control-size: var(--control-size-sm);
|
|
663
|
-
--density-compact-size: 1.25rem;
|
|
664
742
|
}
|
|
665
|
-
|
|
743
|
+
|
|
744
|
+
.spacious {
|
|
666
745
|
--gap: var(--space-50);
|
|
667
746
|
--density-space: var(--space-50);
|
|
668
747
|
--control-size: var(--control-size-lg);
|
|
669
|
-
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/* Shared control scale. Wrappers are listed when their descendants form one
|
|
751
|
+
component (field, input-icon, join, pagination, tabs, flyout). A local size
|
|
752
|
+
declaration beats inherited density because it is declared on the sized
|
|
753
|
+
component itself. */
|
|
754
|
+
:where(
|
|
755
|
+
.btn,
|
|
756
|
+
.input,
|
|
757
|
+
.textarea,
|
|
758
|
+
.select,
|
|
759
|
+
.color,
|
|
760
|
+
.file,
|
|
761
|
+
.field,
|
|
762
|
+
.choice,
|
|
763
|
+
.input-icon,
|
|
764
|
+
.otp,
|
|
765
|
+
.join,
|
|
766
|
+
.pagination,
|
|
767
|
+
.tabs,
|
|
768
|
+
.tab,
|
|
769
|
+
.flyout
|
|
770
|
+
).sm {
|
|
771
|
+
--control-size: var(--control-size-sm);
|
|
772
|
+
--control-font-size: var(--font-size-sm);
|
|
773
|
+
--control-pad-x: var(--font-size-sm);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
:where(
|
|
777
|
+
.btn,
|
|
778
|
+
.input,
|
|
779
|
+
.textarea,
|
|
780
|
+
.select,
|
|
781
|
+
.color,
|
|
782
|
+
.file,
|
|
783
|
+
.field,
|
|
784
|
+
.choice,
|
|
785
|
+
.input-icon,
|
|
786
|
+
.otp,
|
|
787
|
+
.join,
|
|
788
|
+
.pagination,
|
|
789
|
+
.tabs,
|
|
790
|
+
.tab,
|
|
791
|
+
.flyout
|
|
792
|
+
).lg {
|
|
793
|
+
--control-size: var(--control-size-lg);
|
|
794
|
+
--control-font-size: var(--font-size-lg);
|
|
795
|
+
--control-pad-x: var(--font-size-lg);
|
|
670
796
|
}
|
|
671
797
|
|
|
672
798
|
/* Modern color enhancement. Baseline variant values above remain usable
|
|
@@ -680,7 +806,10 @@ small {
|
|
|
680
806
|
var(--surface) var(--soft-border-mix),
|
|
681
807
|
var(--intent, var(--neutral))
|
|
682
808
|
);
|
|
683
|
-
--ui-fg:
|
|
809
|
+
--ui-fg: var(
|
|
810
|
+
--intent-soft-fg,
|
|
811
|
+
color-mix(in oklab, var(--text), var(--intent, var(--text)) var(--soft-fg-mix))
|
|
812
|
+
);
|
|
684
813
|
/* @sync soft-recipe */
|
|
685
814
|
}
|
|
686
815
|
|
|
@@ -784,7 +913,7 @@ small {
|
|
|
784
913
|
.prose :where(h1, h2, h3, h4, h5, h6) {
|
|
785
914
|
margin-block: 1.5em 0;
|
|
786
915
|
color: var(--heading, var(--text));
|
|
787
|
-
font-weight: var(--font-weight-
|
|
916
|
+
font-weight: var(--font-weight-semibold);
|
|
788
917
|
line-height: var(--prose-heading-line-height);
|
|
789
918
|
}
|
|
790
919
|
|
|
@@ -825,12 +954,12 @@ small {
|
|
|
825
954
|
text-underline-offset: 0.18em;
|
|
826
955
|
}
|
|
827
956
|
|
|
828
|
-
.prose :where(a:hover) {
|
|
957
|
+
.prose :where(a:hover, a:focus-visible) {
|
|
829
958
|
text-decoration-thickness: 0.12em;
|
|
830
959
|
}
|
|
831
960
|
|
|
832
961
|
@supports (color: color-mix(in oklch, red, white)) {
|
|
833
|
-
.prose :where(a:hover) {
|
|
962
|
+
.prose :where(a:hover, a:focus-visible) {
|
|
834
963
|
color: color-mix(in oklab, var(--link, var(--primary)), var(--text) 15%);
|
|
835
964
|
}
|
|
836
965
|
}
|
|
@@ -848,7 +977,7 @@ small {
|
|
|
848
977
|
/* Description lists keep document flow: a valid <dl> may pair several <dt>
|
|
849
978
|
with one <dd> or the reverse, so no grid layout is imposed here. */
|
|
850
979
|
.prose :where(dt) {
|
|
851
|
-
font-weight: var(--font-weight-
|
|
980
|
+
font-weight: var(--font-weight-semibold);
|
|
852
981
|
}
|
|
853
982
|
|
|
854
983
|
.prose :where(dd) {
|
|
@@ -890,8 +1019,8 @@ small {
|
|
|
890
1019
|
}
|
|
891
1020
|
/* @sync key-recipe */
|
|
892
1021
|
|
|
1022
|
+
/* Overflow containment is global on `pre` (base.css); only the box is prose's. */
|
|
893
1023
|
.prose :where(pre) {
|
|
894
|
-
overflow-x: auto;
|
|
895
1024
|
padding: 1em;
|
|
896
1025
|
border: var(--border-width) solid var(--border);
|
|
897
1026
|
border-radius: var(--radius);
|
|
@@ -904,6 +1033,7 @@ small {
|
|
|
904
1033
|
}
|
|
905
1034
|
|
|
906
1035
|
.prose :where(blockquote) {
|
|
1036
|
+
margin-inline: 0;
|
|
907
1037
|
padding-inline-start: 1em;
|
|
908
1038
|
border-inline-start: var(--border-width) solid var(--border);
|
|
909
1039
|
color: var(--text-muted);
|
|
@@ -1007,9 +1137,10 @@ small {
|
|
|
1007
1137
|
/*
|
|
1008
1138
|
* Optional fluid typography module.
|
|
1009
1139
|
*
|
|
1010
|
-
*
|
|
1011
|
-
*
|
|
1012
|
-
*
|
|
1140
|
+
* Part of the typography family (typography/index.css) and the full bundle; it
|
|
1141
|
+
* is not in the minimal core, so a core-only import adds it explicitly when
|
|
1142
|
+
* display, title, and lead sizes must scale with the viewport. Typical use
|
|
1143
|
+
* cases: marketing pages, landing pages, hero sections, documentation
|
|
1013
1144
|
* homepages.
|
|
1014
1145
|
*
|
|
1015
1146
|
* Do not use this for body text, app UI, or the default prose scale. Fluid
|
|
@@ -1134,8 +1265,8 @@ small {
|
|
|
1134
1265
|
--gap is inherited, so a container that composes other primitives sets its
|
|
1135
1266
|
own rhythm on the `gap` property, not on the token: setting --gap on a
|
|
1136
1267
|
region re-spaces every nested stack, cluster and grid at any depth. Setting
|
|
1137
|
-
the token stays correct on the element that consumes it, and
|
|
1138
|
-
deliberately so a density context reaches nested layouts. See
|
|
1268
|
+
the token stays correct on the element that consumes it, and
|
|
1269
|
+
.compact/.spacious set it deliberately so a density context reaches nested layouts. See
|
|
1139
1270
|
docs/pages/layout/stack.md. */
|
|
1140
1271
|
.stack {
|
|
1141
1272
|
display: flex;
|
|
@@ -1297,7 +1428,7 @@ small {
|
|
|
1297
1428
|
text-align: start;
|
|
1298
1429
|
}
|
|
1299
1430
|
|
|
1300
|
-
/*
|
|
1431
|
+
/* In the side layout the FAB owns the viewport's bottom safe-area inset. */
|
|
1301
1432
|
.app-layout > .fab {
|
|
1302
1433
|
margin-block-end: max(var(--fab-offset), env(safe-area-inset-bottom));
|
|
1303
1434
|
}
|
|
@@ -1410,7 +1541,7 @@ small {
|
|
|
1410
1541
|
*
|
|
1411
1542
|
* .grid is space-driven: it fits as many tracks as --grid-min allows.
|
|
1412
1543
|
* .grid-N is count-driven: it declares a structural density of N and collapses
|
|
1413
|
-
* through natural divisors of N
|
|
1544
|
+
* through natural divisors of N.
|
|
1414
1545
|
*
|
|
1415
1546
|
* Public hooks:
|
|
1416
1547
|
* --grid-min
|
|
@@ -1421,22 +1552,15 @@ small {
|
|
|
1421
1552
|
*
|
|
1422
1553
|
* Child contract: each direct child is a grid item placed in the tracks the
|
|
1423
1554
|
* recipe generates.
|
|
1555
|
+
*
|
|
1556
|
+
* Choosing between these and the other layout primitives:
|
|
1557
|
+
* docs/pages/layout/grid.md. Why divisors, why a *named* container, and how
|
|
1558
|
+
* the thresholds were arrived at: docs/design-notes/grid.md.
|
|
1424
1559
|
*/
|
|
1425
1560
|
|
|
1426
|
-
/* --grid-columns replaces the intrinsic auto-fit template: once set, the
|
|
1427
|
-
|
|
1428
|
-
collapse.
|
|
1429
|
-
--grid-columns for exact custom track templates. */
|
|
1430
|
-
|
|
1431
|
-
/* Recipe:
|
|
1432
|
-
- collection whose item width is the contract -> .grid, tune --grid-min
|
|
1433
|
-
- collection whose column density is the contract -> .grid-2/3/4/6
|
|
1434
|
-
- balanced subdivision of that density -> place it inside any size query
|
|
1435
|
-
container (.container-query is the convenience helper for that context)
|
|
1436
|
-
- peer regions that switch as a group -> .switcher
|
|
1437
|
-
- content + aside -> .sidebar-layout
|
|
1438
|
-
- exact custom track templates -> --grid-columns with an explicit collapse
|
|
1439
|
-
rule of your own */
|
|
1561
|
+
/* --grid-columns replaces the intrinsic auto-fit template: once set, the grid
|
|
1562
|
+
no longer collapses on its own and the author owns narrow-container
|
|
1563
|
+
collapse. */
|
|
1440
1564
|
.grid {
|
|
1441
1565
|
display: grid;
|
|
1442
1566
|
gap: var(--gap);
|
|
@@ -1446,16 +1570,14 @@ small {
|
|
|
1446
1570
|
);
|
|
1447
1571
|
}
|
|
1448
1572
|
|
|
1449
|
-
/*
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
partial final page holds fewer items than the grid's capacity.
|
|
1573
|
+
/* Baseline: auto-fill bounded at N, so a preset is responsive and
|
|
1574
|
+
overflow-safe with no wrapper at all. auto-fill rather than auto-fit keeps
|
|
1575
|
+
item width stable when a partial final page holds fewer items than the
|
|
1576
|
+
grid's capacity.
|
|
1454
1577
|
|
|
1455
|
-
--grid-min is deliberately NOT read here
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
added. Item width is .grid's contract; column density is .grid-N's. */
|
|
1578
|
+
--grid-min is deliberately NOT read here: @container thresholds cannot
|
|
1579
|
+
resolve a custom property, so a hook honored in the baseline would fall
|
|
1580
|
+
silent the moment a query container is added. */
|
|
1459
1581
|
:where(.grid-2, .grid-3, .grid-4, .grid-6) {
|
|
1460
1582
|
display: grid;
|
|
1461
1583
|
gap: var(--gap);
|
|
@@ -1484,80 +1606,63 @@ small {
|
|
|
1484
1606
|
--grid-count: 6;
|
|
1485
1607
|
}
|
|
1486
1608
|
|
|
1487
|
-
/* Grid items keep an automatic
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
need .min-inline-size-0 outside primitives that do not already remove this
|
|
1491
|
-
floor. */
|
|
1609
|
+
/* Grid items keep an automatic min-content floor that can overflow a
|
|
1610
|
+
minmax(0, 1fr) track. Layout primitives remove it so their overflow-safe
|
|
1611
|
+
track sizing holds; .min-inline-size-0 is for the ones that do not. */
|
|
1492
1612
|
:where(.grid, .grid-2, .grid-3, .grid-4, .grid-6) > * {
|
|
1493
1613
|
min-inline-size: 0;
|
|
1494
1614
|
}
|
|
1495
1615
|
|
|
1496
|
-
/* Balanced subdivision,
|
|
1497
|
-
*
|
|
1498
|
-
*
|
|
1499
|
-
* or a region that sets container-type: inline-size for an unrelated reason
|
|
1500
|
-
* would silently change the grid. The author opts the grid in by naming the
|
|
1501
|
-
* context — .container-query is the official helper, or declare
|
|
1502
|
-
* container: actual-container / inline-size on your own region.
|
|
1616
|
+
/* Balanced subdivision, keyed on an ancestor container *named*
|
|
1617
|
+
* actual-container — never an anonymous one, so a region that sets
|
|
1618
|
+
* container-type: inline-size for an unrelated reason cannot change a grid.
|
|
1503
1619
|
*
|
|
1504
1620
|
* .grid-2 2 -> 1
|
|
1505
1621
|
* .grid-3 3 -> 1
|
|
1506
1622
|
* .grid-4 4 -> 2 -> 1
|
|
1507
1623
|
* .grid-6 6 -> 3 -> 2 -> 1
|
|
1508
1624
|
*
|
|
1509
|
-
* Every state is a divisor of N
|
|
1510
|
-
*
|
|
1511
|
-
*
|
|
1512
|
-
*
|
|
1513
|
-
*
|
|
1514
|
-
*
|
|
1515
|
-
*
|
|
1516
|
-
*
|
|
1517
|
-
*
|
|
1518
|
-
*
|
|
1519
|
-
*
|
|
1520
|
-
*
|
|
1521
|
-
*
|
|
1522
|
-
* threshold fires (a bare grid would otherwise show a 2 + 1 orphan at 28-48rem).
|
|
1523
|
-
*
|
|
1524
|
-
* Three shared thresholds, not one calibrated per preset: 28rem opens the
|
|
1525
|
-
* first horizontal density, 48rem opens three columns, 64rem opens the dense
|
|
1526
|
-
* 4 and 6. N already carries the author's statement about item size, so
|
|
1527
|
-
* tuning every step to land the same item width would erase that signal —
|
|
1528
|
-
* .grid-6 at 64rem gives ~161px per tile, which is the point of choosing it.
|
|
1529
|
-
* Probed with the content each preset targets: large cards for .grid-2 down
|
|
1530
|
-
* to stats, avatars, and thumbnails for .grid-6.
|
|
1625
|
+
* Every state is a divisor of N. Do not complete these chains with the
|
|
1626
|
+
* missing integers and do not replace them with an unbounded auto-fit: both
|
|
1627
|
+
* bring back uneven splits such as 5 + 1.
|
|
1628
|
+
*
|
|
1629
|
+
* The container must be an ANCESTOR. An element cannot query its own size
|
|
1630
|
+
* without inline-size containment, which destroys its intrinsic inline
|
|
1631
|
+
* contribution and collapses it inside .cluster, .topbar, floats and any
|
|
1632
|
+
* shrink-to-fit box.
|
|
1633
|
+
*
|
|
1634
|
+
* With no such ancestor none of these resolve and the baseline above stays.
|
|
1635
|
+
* min-width: 0 is the lowest step: it pins the grid to one column before any
|
|
1636
|
+
* larger threshold fires, which a bare grid would otherwise show as a 2 + 1
|
|
1637
|
+
* orphan between 28 and 48rem.
|
|
1531
1638
|
*
|
|
1532
1639
|
* Ascending min-width only: the last matching step wins by cascade order.
|
|
1533
1640
|
*/
|
|
1534
|
-
@
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
grid-template-columns: 1fr;
|
|
1538
|
-
}
|
|
1641
|
+
@container actual-container (min-width: 0px) {
|
|
1642
|
+
:where(.grid-2, .grid-3, .grid-4, .grid-6) {
|
|
1643
|
+
grid-template-columns: 1fr;
|
|
1539
1644
|
}
|
|
1645
|
+
}
|
|
1540
1646
|
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
}
|
|
1647
|
+
@container actual-container (min-width: 28rem) {
|
|
1648
|
+
:where(.grid-2, .grid-4, .grid-6) {
|
|
1649
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1545
1650
|
}
|
|
1651
|
+
}
|
|
1546
1652
|
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
}
|
|
1653
|
+
@container actual-container (min-width: 48rem) {
|
|
1654
|
+
:where(.grid-3, .grid-6) {
|
|
1655
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
1551
1656
|
}
|
|
1657
|
+
}
|
|
1552
1658
|
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1659
|
+
@container actual-container (min-width: 64rem) {
|
|
1660
|
+
:where(.grid-4) {
|
|
1661
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
1662
|
+
}
|
|
1557
1663
|
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
}
|
|
1664
|
+
:where(.grid-6) {
|
|
1665
|
+
grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
1561
1666
|
}
|
|
1562
1667
|
}
|
|
1563
1668
|
|
|
@@ -1716,10 +1821,9 @@ small {
|
|
|
1716
1821
|
* the light/dark adaptation that color-scheme already gives for free, and the
|
|
1717
1822
|
* two never match anyway.
|
|
1718
1823
|
*
|
|
1719
|
-
* Above
|
|
1720
|
-
*
|
|
1721
|
-
*
|
|
1722
|
-
* not a broken state.
|
|
1824
|
+
* Above every tier: scrollbar-color lands in Chromium 121 and Safari 18.2, so
|
|
1825
|
+
* even a Recommended engine may lack it. Engines that ignore both declarations
|
|
1826
|
+
* draw their native scrollbar — the intended fallback, not a broken state.
|
|
1723
1827
|
*
|
|
1724
1828
|
* Styles the scrollbar only; it does not create the overflow. Pair it with
|
|
1725
1829
|
* .overflow-auto or a component that already scrolls.
|
|
@@ -1781,24 +1885,28 @@ small {
|
|
|
1781
1885
|
.scroll-snap {
|
|
1782
1886
|
overscroll-behavior-inline: contain;
|
|
1783
1887
|
scroll-padding-inline: var(--scroll-snap-padding, var(--scroll-snap-gap));
|
|
1888
|
+
/* x then inline: an engine that lacks the logical keyword keeps x.
|
|
1889
|
+
proximity is the default, so a short rail cannot trap the pointer. */
|
|
1890
|
+
scroll-snap-type: x proximity;
|
|
1891
|
+
scroll-snap-type: inline proximity;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
/* Strict snapping is opt-in; proximity keeps a rail from fighting the
|
|
1895
|
+
pointer on a short strip. */
|
|
1896
|
+
.scroll-snap[data-snap="mandatory"] {
|
|
1784
1897
|
scroll-snap-type: x mandatory;
|
|
1898
|
+
scroll-snap-type: inline mandatory;
|
|
1785
1899
|
}
|
|
1786
1900
|
|
|
1787
1901
|
.scroll-snap > * {
|
|
1788
1902
|
scroll-snap-align: var(--scroll-snap-align);
|
|
1789
1903
|
}
|
|
1790
1904
|
|
|
1791
|
-
.scroll-snap[data-snap="center"] {
|
|
1905
|
+
.scroll-snap[data-snap-align="center"] {
|
|
1792
1906
|
--scroll-snap-align: center;
|
|
1793
1907
|
}
|
|
1794
1908
|
}
|
|
1795
1909
|
|
|
1796
|
-
@supports (scroll-snap-type: inline mandatory) {
|
|
1797
|
-
.scroll-snap {
|
|
1798
|
-
scroll-snap-type: inline mandatory;
|
|
1799
|
-
}
|
|
1800
|
-
}
|
|
1801
|
-
|
|
1802
1910
|
.scroll-snap[data-scrollbar="hidden"] {
|
|
1803
1911
|
scrollbar-width: none;
|
|
1804
1912
|
}
|
|
@@ -1899,6 +2007,7 @@ small {
|
|
|
1899
2007
|
display: grid;
|
|
1900
2008
|
gap: var(--space-10);
|
|
1901
2009
|
color: var(--text);
|
|
2010
|
+
font-size: var(--control-font-size);
|
|
1902
2011
|
}
|
|
1903
2012
|
|
|
1904
2013
|
.field-group {
|
|
@@ -1912,7 +2021,7 @@ small {
|
|
|
1912
2021
|
.field-label,
|
|
1913
2022
|
.field-group > legend {
|
|
1914
2023
|
color: var(--text);
|
|
1915
|
-
font-weight: var(--font-weight-
|
|
2024
|
+
font-weight: var(--font-weight-semibold);
|
|
1916
2025
|
}
|
|
1917
2026
|
|
|
1918
2027
|
.field-group > legend {
|
|
@@ -1923,13 +2032,13 @@ small {
|
|
|
1923
2032
|
|
|
1924
2033
|
.field-help {
|
|
1925
2034
|
color: var(--text-muted);
|
|
1926
|
-
font-size:
|
|
2035
|
+
font-size: 0.875em;
|
|
1927
2036
|
}
|
|
1928
2037
|
|
|
1929
2038
|
.field-error {
|
|
1930
2039
|
display: none;
|
|
1931
2040
|
color: var(--danger);
|
|
1932
|
-
font-size:
|
|
2041
|
+
font-size: 0.875em;
|
|
1933
2042
|
}
|
|
1934
2043
|
|
|
1935
2044
|
.required-mark {
|
|
@@ -1974,9 +2083,10 @@ small {
|
|
|
1974
2083
|
--control-cursor: auto;
|
|
1975
2084
|
|
|
1976
2085
|
appearance: none;
|
|
2086
|
+
-webkit-appearance: none;
|
|
1977
2087
|
inline-size: 100%;
|
|
1978
2088
|
min-block-size: var(--control-size);
|
|
1979
|
-
padding-inline: var(--
|
|
2089
|
+
padding-inline: var(--control-pad-x);
|
|
1980
2090
|
border: var(--border-width) solid var(--form-invalid-border, var(--control-border));
|
|
1981
2091
|
border-block-end-color: var(
|
|
1982
2092
|
--form-invalid-border,
|
|
@@ -2054,7 +2164,7 @@ small {
|
|
|
2054
2164
|
* geometry and states. switch.css is imported after this file.
|
|
2055
2165
|
*
|
|
2056
2166
|
* Public hooks:
|
|
2057
|
-
* --choice-control-
|
|
2167
|
+
* --choice-control-nudge
|
|
2058
2168
|
* --choice-border
|
|
2059
2169
|
* --choice-bg
|
|
2060
2170
|
* --choice-mark
|
|
@@ -2086,45 +2196,89 @@ small {
|
|
|
2086
2196
|
cursor: pointer;
|
|
2087
2197
|
}
|
|
2088
2198
|
|
|
2089
|
-
/* Optical alignment:
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2199
|
+
/* Optical alignment: the control reads level with the first line's cap height,
|
|
2200
|
+
not with the top of its line box. Every inline choice runs the same formula
|
|
2201
|
+
over its own block size, so a checkbox and a taller switch stay level with
|
|
2202
|
+
each other under any line-height or --choice-control-size — something a
|
|
2203
|
+
per-control constant cannot do.
|
|
2204
|
+
|
|
2205
|
+
Three tiers, each the best the engine can express:
|
|
2206
|
+
|
|
2207
|
+
- `cap` unit: exact, and the only font-independent answer. Where the cap
|
|
2208
|
+
center sits inside a line box depends on the font's ascent/descent/cap
|
|
2209
|
+
ratios, so no constant can serve both Segoe UI and Roboto — the same rule
|
|
2210
|
+
was a pixel high on Windows and two pixels low on Android. Baseline
|
|
2211
|
+
alignment puts the control's border-box bottom on the text baseline, and
|
|
2212
|
+
one inset drops it by half the difference between its own height and the
|
|
2213
|
+
cap height. Both classed controls synthesize that baseline from their
|
|
2214
|
+
border box because their mark and knob are out of flow.
|
|
2215
|
+
- `lh` only: center the control on the line box, which is within a pixel of
|
|
2216
|
+
the cap center for the usual UI faces.
|
|
2217
|
+
- neither: the constants that formula yields at the default 1.5
|
|
2218
|
+
line-height, 0.1875em for a checkbox and 0.125em for the switch track.
|
|
2219
|
+
|
|
2220
|
+
--choice-control-nudge adds to whichever tier applies, rather than replacing
|
|
2221
|
+
it: the derived value is exact now, so a design has a correction to make,
|
|
2222
|
+
not a formula to restate — and one number cannot stand in for three
|
|
2223
|
+
different references anyway. It stays fallback-only rather than declared
|
|
2224
|
+
here, so an override placed anywhere up the tree — :root included — reaches
|
|
2225
|
+
the control instead of losing to a component-level declaration. */
|
|
2106
2226
|
.choice > :is(input[type="checkbox"]:not([role="switch"]), input[type="radio"]) {
|
|
2107
2227
|
flex: 0 0 auto;
|
|
2108
|
-
margin-block-start: var(--choice-control-
|
|
2228
|
+
margin-block-start: calc(0.1875em + var(--choice-control-nudge, 0px));
|
|
2109
2229
|
}
|
|
2110
2230
|
|
|
2111
2231
|
.choice > .switch {
|
|
2112
2232
|
flex: 0 0 auto;
|
|
2113
|
-
margin-block-start: var(--choice-control-
|
|
2233
|
+
margin-block-start: calc(0.125em + var(--choice-control-nudge, 0px));
|
|
2114
2234
|
}
|
|
2115
2235
|
|
|
2116
2236
|
@supports (margin-block-start: 1lh) {
|
|
2117
2237
|
.choice > :is(input[type="checkbox"]:not([role="switch"]), input[type="radio"]) {
|
|
2118
|
-
margin-block-start:
|
|
2119
|
-
--choice-control-
|
|
2120
|
-
|
|
2238
|
+
margin-block-start: calc(
|
|
2239
|
+
(1lh - var(--choice-control-size)) /
|
|
2240
|
+
2 +
|
|
2241
|
+
var(--choice-control-nudge, 0px)
|
|
2121
2242
|
);
|
|
2122
2243
|
}
|
|
2123
2244
|
|
|
2124
2245
|
.choice > .switch {
|
|
2125
|
-
margin-block-start:
|
|
2126
|
-
--
|
|
2127
|
-
|
|
2246
|
+
margin-block-start: calc(
|
|
2247
|
+
(1lh - var(--switch-block-size)) /
|
|
2248
|
+
2 +
|
|
2249
|
+
var(--choice-control-nudge, 0px)
|
|
2250
|
+
);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
@supports (margin-block-start: 1cap) {
|
|
2255
|
+
.choice {
|
|
2256
|
+
align-items: baseline;
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
/* Baseline alignment owns the block position now; the tier above would only
|
|
2260
|
+
grow the row. An unclassed native input keeps the UA's own baseline, which
|
|
2261
|
+
is already tuned for text — the formula below describes the framework's
|
|
2262
|
+
geometry and would not fit it. */
|
|
2263
|
+
.choice > :is(input[type="checkbox"], input[type="radio"]) {
|
|
2264
|
+
margin-block-start: 0;
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
/* inset-block-start rides the `position: relative` these controls already
|
|
2268
|
+
carry for their mark and knob. */
|
|
2269
|
+
.choice > :is(.check, .radio) {
|
|
2270
|
+
inset-block-start: calc(
|
|
2271
|
+
(var(--choice-control-size) - 1cap) /
|
|
2272
|
+
2 +
|
|
2273
|
+
var(--choice-control-nudge, 0px)
|
|
2274
|
+
);
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
.choice > .switch {
|
|
2278
|
+
inset-block-start: calc(
|
|
2279
|
+
(var(--switch-block-size) - 1cap) /
|
|
2280
|
+
2 +
|
|
2281
|
+
var(--choice-control-nudge, 0px)
|
|
2128
2282
|
);
|
|
2129
2283
|
}
|
|
2130
2284
|
}
|
|
@@ -2159,6 +2313,7 @@ small {
|
|
|
2159
2313
|
background: var(--choice-bg);
|
|
2160
2314
|
color: var(--choice-mark);
|
|
2161
2315
|
appearance: none;
|
|
2316
|
+
-webkit-appearance: none;
|
|
2162
2317
|
cursor: pointer;
|
|
2163
2318
|
}
|
|
2164
2319
|
|
|
@@ -2197,9 +2352,9 @@ small {
|
|
|
2197
2352
|
|
|
2198
2353
|
.check:is(:checked, :indeterminate),
|
|
2199
2354
|
.radio:checked {
|
|
2200
|
-
--choice-border: var(--intent, var(--
|
|
2201
|
-
--choice-bg: var(--intent, var(--
|
|
2202
|
-
--choice-mark: var(--intent-fg, var(--
|
|
2355
|
+
--choice-border: var(--intent, var(--state-selected));
|
|
2356
|
+
--choice-bg: var(--intent, var(--state-selected));
|
|
2357
|
+
--choice-mark: var(--intent-fg, var(--state-selected-fg));
|
|
2203
2358
|
}
|
|
2204
2359
|
|
|
2205
2360
|
.check:checked::before {
|
|
@@ -2218,6 +2373,8 @@ small {
|
|
|
2218
2373
|
transform: scale(1);
|
|
2219
2374
|
}
|
|
2220
2375
|
|
|
2376
|
+
/* Progressive: without :not() selector lists, the normal unchecked border
|
|
2377
|
+
remains visible and the control stays usable. */
|
|
2221
2378
|
.check:hover:not(:checked, :indeterminate, :disabled),
|
|
2222
2379
|
.radio:hover:not(:checked, :indeterminate, :disabled) {
|
|
2223
2380
|
--choice-border: var(--text-subtle);
|
|
@@ -2253,20 +2410,40 @@ small {
|
|
|
2253
2410
|
* the CSS chevron, so the picker (appearance: base-select) enhancement in
|
|
2254
2411
|
* custom-select.css can be opted out independently. Add .native to a specific
|
|
2255
2412
|
* select to opt out of the enhancement.
|
|
2413
|
+
*
|
|
2414
|
+
* Public hooks:
|
|
2415
|
+
* --select-icon-size
|
|
2256
2416
|
*/
|
|
2257
|
-
|
|
2417
|
+
/* --select-icon-size is typographic (em), so the reserved slot and the
|
|
2418
|
+
pictogram scale with the control's font size, not a fixed rem sliver. */
|
|
2419
|
+
.select:not(:is([multiple], [size]:not([size="1"]))) {
|
|
2420
|
+
--select-icon-size: 1em;
|
|
2421
|
+
|
|
2258
2422
|
appearance: none;
|
|
2259
|
-
|
|
2423
|
+
-webkit-appearance: none;
|
|
2424
|
+
padding-inline-end: calc(var(--control-pad-x) * 2 + var(--select-icon-size));
|
|
2260
2425
|
background-image: var(--icon-chevron);
|
|
2261
|
-
background-position: center right var(--
|
|
2262
|
-
background-size:
|
|
2426
|
+
background-position: center right var(--control-pad-x);
|
|
2427
|
+
background-size: var(--select-icon-size) auto;
|
|
2263
2428
|
background-repeat: no-repeat;
|
|
2264
2429
|
}
|
|
2265
2430
|
|
|
2266
2431
|
/* The chevron sits on the inline-end side; flip it in RTL so it follows
|
|
2267
|
-
the padding-inline-end space reserved for it.
|
|
2268
|
-
|
|
2269
|
-
|
|
2432
|
+
the padding-inline-end space reserved for it. Safari before 16.4 has no
|
|
2433
|
+
:dir(); the ancestor-attribute path is the Degraded-tier fallback and the
|
|
2434
|
+
:dir() rules are dropped on their own there. :dir() follows inherited
|
|
2435
|
+
directionality, so the explicit ltr rule neutralizes an outer RTL fallback
|
|
2436
|
+
for a nested direction change. */
|
|
2437
|
+
[dir="rtl"] .select:not(:is([multiple], [size]:not([size="1"]))) {
|
|
2438
|
+
background-position: center left var(--control-pad-x);
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
.select:not(:is([multiple], [size]:not([size="1"]))):dir(ltr) {
|
|
2442
|
+
background-position: center right var(--control-pad-x);
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
.select:not(:is([multiple], [size]:not([size="1"]))):dir(rtl) {
|
|
2446
|
+
background-position: center left var(--control-pad-x);
|
|
2270
2447
|
}
|
|
2271
2448
|
|
|
2272
2449
|
/* Both forced-colors and prefers-contrast hand the arrow back to the
|
|
@@ -2274,10 +2451,10 @@ small {
|
|
|
2274
2451
|
simpler than maintaining contrast variants, and native rendering already
|
|
2275
2452
|
matches whatever contrast mode the user picked. */
|
|
2276
2453
|
@media (forced-colors: active), (prefers-contrast: more) {
|
|
2277
|
-
.select:not([multiple], [size]:not([size="1"])) {
|
|
2454
|
+
.select:not(:is([multiple], [size]:not([size="1"]))) {
|
|
2278
2455
|
appearance: auto;
|
|
2279
2456
|
background-image: none;
|
|
2280
|
-
padding-inline-end: var(--
|
|
2457
|
+
padding-inline-end: var(--control-pad-x);
|
|
2281
2458
|
}
|
|
2282
2459
|
}
|
|
2283
2460
|
|
|
@@ -2291,18 +2468,23 @@ small {
|
|
|
2291
2468
|
*/
|
|
2292
2469
|
.file {
|
|
2293
2470
|
color: var(--text-muted);
|
|
2471
|
+
font-size: var(--control-font-size);
|
|
2294
2472
|
}
|
|
2295
2473
|
|
|
2296
2474
|
.file::file-selector-button {
|
|
2297
2475
|
margin-inline-end: var(--space-30);
|
|
2476
|
+
/* Consume the shared control scale like .btn, so .sm/.lg and density resize
|
|
2477
|
+
the whole control, not just the filename text. */
|
|
2478
|
+
min-block-size: var(--control-size);
|
|
2298
2479
|
padding-block: var(--space-10);
|
|
2480
|
+
padding-block: max(var(--space-10), calc((var(--control-size) - 1lh) / 2));
|
|
2299
2481
|
padding-inline: var(--space-20);
|
|
2300
2482
|
border: var(--border-width) solid var(--border);
|
|
2301
2483
|
border-radius: var(--radius-sm);
|
|
2302
2484
|
background: var(--surface-subtle);
|
|
2303
2485
|
color: var(--text);
|
|
2304
2486
|
font: inherit;
|
|
2305
|
-
font-size: var(--font-size
|
|
2487
|
+
font-size: var(--control-font-size);
|
|
2306
2488
|
font-weight: var(--font-weight-medium);
|
|
2307
2489
|
cursor: pointer;
|
|
2308
2490
|
transition:
|
|
@@ -2529,18 +2711,19 @@ small {
|
|
|
2529
2711
|
* Input Icon — decorative icon inside a relative wrapper, leading or trailing.
|
|
2530
2712
|
*
|
|
2531
2713
|
* Icon-library agnostic: detects position by DOM order. First child that is
|
|
2532
|
-
* not .input = leading icon; icon after .input = trailing. The icon
|
|
2533
|
-
*
|
|
2534
|
-
* the pictogram.
|
|
2714
|
+
* not .input = leading icon; icon after .input = trailing. The icon follows
|
|
2715
|
+
* local typography; density contexts shrink the control geometry around it
|
|
2716
|
+
* without changing the pictogram.
|
|
2535
2717
|
*
|
|
2536
2718
|
* Public hooks:
|
|
2537
2719
|
* --input-icon-size
|
|
2538
2720
|
*/
|
|
2539
2721
|
.input-icon {
|
|
2540
|
-
--input-icon-size: 1.
|
|
2722
|
+
--input-icon-size: 1.25em;
|
|
2541
2723
|
|
|
2542
2724
|
display: block;
|
|
2543
2725
|
position: relative;
|
|
2726
|
+
font-size: var(--control-font-size);
|
|
2544
2727
|
}
|
|
2545
2728
|
|
|
2546
2729
|
/* Icon — shared by leading and trailing */
|
|
@@ -2555,29 +2738,34 @@ small {
|
|
|
2555
2738
|
|
|
2556
2739
|
/* Leading: icon is the first child */
|
|
2557
2740
|
.input-icon > :first-child:where(:not(.input)) {
|
|
2558
|
-
inset-inline-start: var(--
|
|
2741
|
+
inset-inline-start: var(--control-pad-x);
|
|
2559
2742
|
}
|
|
2560
2743
|
|
|
2561
|
-
/* Trailing:
|
|
2562
|
-
.input-icon > .input
|
|
2563
|
-
inset-inline-end: var(--
|
|
2744
|
+
/* Trailing: an icon follows the input, with or without a leading icon. */
|
|
2745
|
+
.input-icon > .input ~ :where(:not(.input)) {
|
|
2746
|
+
inset-inline-end: var(--control-pad-x);
|
|
2564
2747
|
}
|
|
2565
2748
|
|
|
2566
2749
|
/* font-size only sizes text-metric icons (webfont <i>, <span>). An inline
|
|
2567
2750
|
<svg> with just a viewBox ignores it, so give it explicit dimensions. */
|
|
2568
2751
|
.input-icon > svg:where(:not(.input)) {
|
|
2569
|
-
|
|
2570
|
-
|
|
2752
|
+
/* The shared rule already maps --input-icon-size onto font-size. One em is
|
|
2753
|
+
therefore the requested optical size; reusing the em-valued hook here
|
|
2754
|
+
would apply its factor a second time. */
|
|
2755
|
+
inline-size: 1em;
|
|
2756
|
+
block-size: 1em;
|
|
2571
2757
|
}
|
|
2572
2758
|
|
|
2573
2759
|
/* Leading: input gets start padding to clear the icon */
|
|
2574
2760
|
.input-icon > :first-child:where(:not(.input)) ~ .input {
|
|
2575
|
-
padding-inline-start: calc(var(--
|
|
2761
|
+
padding-inline-start: calc(var(--control-pad-x) * 2 + var(--input-icon-size));
|
|
2576
2762
|
}
|
|
2577
2763
|
|
|
2578
|
-
/* Trailing: input
|
|
2579
|
-
|
|
2580
|
-
|
|
2764
|
+
/* Trailing: the input has a following sibling → end padding. Not tied to the
|
|
2765
|
+
input being first child, so a leading icon does not disable the trailing
|
|
2766
|
+
icon's reserved space. */
|
|
2767
|
+
.input-icon > .input:where(:not(:last-child)) {
|
|
2768
|
+
padding-inline-end: calc(var(--control-pad-x) * 2 + var(--input-icon-size));
|
|
2581
2769
|
}
|
|
2582
2770
|
|
|
2583
2771
|
/*
|
|
@@ -2638,8 +2826,6 @@ small {
|
|
|
2638
2826
|
|
|
2639
2827
|
position: relative;
|
|
2640
2828
|
display: inline-grid;
|
|
2641
|
-
align-items: center;
|
|
2642
|
-
justify-content: start;
|
|
2643
2829
|
inline-size: var(--switch-width);
|
|
2644
2830
|
block-size: var(--switch-block-size);
|
|
2645
2831
|
margin: 0;
|
|
@@ -2647,6 +2833,7 @@ small {
|
|
|
2647
2833
|
border-radius: var(--radius-full);
|
|
2648
2834
|
background: var(--switch-bg);
|
|
2649
2835
|
appearance: none;
|
|
2836
|
+
-webkit-appearance: none;
|
|
2650
2837
|
cursor: pointer;
|
|
2651
2838
|
}
|
|
2652
2839
|
|
|
@@ -2657,9 +2844,17 @@ small {
|
|
|
2657
2844
|
against the track is what makes the knob legible. */
|
|
2658
2845
|
.switch::before {
|
|
2659
2846
|
content: "";
|
|
2847
|
+
/* Out of flow like the checkbox mark, so the track carries no in-flow item
|
|
2848
|
+
and the switch synthesizes its baseline from its own border box. A knob
|
|
2849
|
+
laid out as a grid item makes the baseline the knob's bottom edge instead,
|
|
2850
|
+
which is what .choice measures its cap-height offset from. The inset is
|
|
2851
|
+
the same --switch-knob-margin on both axes: it resolves against the
|
|
2852
|
+
padding box, so the border is already out of the way. */
|
|
2853
|
+
position: absolute;
|
|
2854
|
+
inset-block-start: var(--switch-knob-margin);
|
|
2855
|
+
inset-inline-start: var(--switch-knob-margin);
|
|
2660
2856
|
inline-size: var(--switch-knob-size);
|
|
2661
2857
|
block-size: var(--switch-knob-size);
|
|
2662
|
-
margin-inline: var(--switch-knob-margin);
|
|
2663
2858
|
border-radius: var(--radius-full);
|
|
2664
2859
|
background: var(--switch-knob);
|
|
2665
2860
|
transition:
|
|
@@ -2667,14 +2862,17 @@ small {
|
|
|
2667
2862
|
background-color var(--duration);
|
|
2668
2863
|
}
|
|
2669
2864
|
|
|
2670
|
-
|
|
2865
|
+
/* Hover is not selected: an unchecked switch keeps the brand fallback on hover,
|
|
2866
|
+
so a theme that separates --primary from --state-selected never tints an
|
|
2867
|
+
OFF control with the selection accent before it is checked. */
|
|
2868
|
+
.switch:not(:checked):hover:not(:disabled) {
|
|
2671
2869
|
--switch-border: var(--intent, var(--primary));
|
|
2672
2870
|
}
|
|
2673
2871
|
|
|
2674
2872
|
.switch:checked {
|
|
2675
|
-
--switch-bg: var(--intent, var(--
|
|
2676
|
-
--switch-border: var(--intent, var(--
|
|
2677
|
-
--switch-knob: var(--intent-fg, var(--
|
|
2873
|
+
--switch-bg: var(--intent, var(--state-selected));
|
|
2874
|
+
--switch-border: var(--intent, var(--state-selected));
|
|
2875
|
+
--switch-knob: var(--intent-fg, var(--state-selected-fg));
|
|
2678
2876
|
}
|
|
2679
2877
|
|
|
2680
2878
|
.switch:checked::before {
|
|
@@ -2682,7 +2880,17 @@ small {
|
|
|
2682
2880
|
}
|
|
2683
2881
|
|
|
2684
2882
|
/* The knob travels toward inline-end; in RTL that is leftward, so the
|
|
2685
|
-
translateX sign flips. translateX is physical, not logical.
|
|
2883
|
+
translateX sign flips. translateX is physical, not logical. Safari before
|
|
2884
|
+
16.4 has no :dir(); the ancestor-attribute path is the Degraded-tier
|
|
2885
|
+
fallback and the :dir() rules are dropped on their own there. */
|
|
2886
|
+
[dir="rtl"] .switch:checked::before {
|
|
2887
|
+
transform: translateX(calc(var(--switch-travel) * -1));
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
.switch:checked:dir(ltr)::before {
|
|
2891
|
+
transform: translateX(var(--switch-travel));
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2686
2894
|
.switch:checked:dir(rtl)::before {
|
|
2687
2895
|
transform: translateX(calc(var(--switch-travel) * -1));
|
|
2688
2896
|
}
|
|
@@ -2739,6 +2947,7 @@ small {
|
|
|
2739
2947
|
--range-track-bg: var(--border);
|
|
2740
2948
|
|
|
2741
2949
|
appearance: none;
|
|
2950
|
+
-webkit-appearance: none;
|
|
2742
2951
|
inline-size: 100%;
|
|
2743
2952
|
block-size: var(--range-thumb-size);
|
|
2744
2953
|
background: transparent;
|
|
@@ -2759,6 +2968,7 @@ small {
|
|
|
2759
2968
|
|
|
2760
2969
|
.range::-webkit-slider-thumb {
|
|
2761
2970
|
appearance: none;
|
|
2971
|
+
-webkit-appearance: none;
|
|
2762
2972
|
inline-size: var(--range-thumb-size);
|
|
2763
2973
|
block-size: var(--range-thumb-size);
|
|
2764
2974
|
border-radius: var(--radius-full);
|
|
@@ -2906,13 +3116,13 @@ small {
|
|
|
2906
3116
|
|
|
2907
3117
|
/* Checked border — shared by radio and checkbox */
|
|
2908
3118
|
.choice-card:has(:checked) {
|
|
2909
|
-
--choice-card-border: var(--intent, var(--
|
|
3119
|
+
--choice-card-border: var(--intent, var(--state-selected));
|
|
2910
3120
|
}
|
|
2911
3121
|
|
|
2912
3122
|
/* Subtle background tint on checked — modern browsers only */
|
|
2913
3123
|
@supports (color: color-mix(in oklch, red, white)) {
|
|
2914
3124
|
.choice-card:has(:checked) {
|
|
2915
|
-
--choice-card-bg: color-mix(in oklab, var(--surface) 96%, var(--intent, var(--
|
|
3125
|
+
--choice-card-bg: color-mix(in oklab, var(--surface) 96%, var(--intent, var(--state-selected)));
|
|
2916
3126
|
}
|
|
2917
3127
|
}
|
|
2918
3128
|
|
|
@@ -2932,7 +3142,7 @@ small {
|
|
|
2932
3142
|
|
|
2933
3143
|
/* Badge disc — circle for radio, rounded square for checkbox */
|
|
2934
3144
|
.choice-card::before {
|
|
2935
|
-
background: var(--intent, var(--
|
|
3145
|
+
background: var(--intent, var(--state-selected));
|
|
2936
3146
|
box-shadow: 0 0 0 2px var(--surface);
|
|
2937
3147
|
z-index: 1;
|
|
2938
3148
|
}
|
|
@@ -2947,7 +3157,7 @@ small {
|
|
|
2947
3157
|
|
|
2948
3158
|
/* Check mark */
|
|
2949
3159
|
.choice-card::after {
|
|
2950
|
-
background: var(--intent-fg, var(--
|
|
3160
|
+
background: var(--intent-fg, var(--state-selected-fg));
|
|
2951
3161
|
-webkit-mask: var(--icon-check) center / 60% no-repeat;
|
|
2952
3162
|
mask: var(--icon-check) center / 60% no-repeat;
|
|
2953
3163
|
z-index: 2;
|
|
@@ -3084,8 +3294,8 @@ small {
|
|
|
3084
3294
|
* placeholder=" " to expose a reliable empty state — without it the label
|
|
3085
3295
|
* only reacts to focus.
|
|
3086
3296
|
*
|
|
3087
|
-
* Density flows through --control-size: .
|
|
3088
|
-
* and the reserved headroom with it, leaving typography untouched.
|
|
3297
|
+
* Density flows through --control-size: .compact/.spacious change the control
|
|
3298
|
+
* geometry and the reserved headroom with it, leaving typography untouched.
|
|
3089
3299
|
*
|
|
3090
3300
|
* Public hooks:
|
|
3091
3301
|
* --floating-label-top
|
|
@@ -3116,7 +3326,7 @@ small {
|
|
|
3116
3326
|
inset-inline-start: calc(var(--space-30) + var(--border-width));
|
|
3117
3327
|
color: var(--text-muted);
|
|
3118
3328
|
font-size: var(--control-font-size);
|
|
3119
|
-
font-weight: var(--font-weight);
|
|
3329
|
+
font-weight: var(--font-weight-normal);
|
|
3120
3330
|
opacity: var(--floating-label-opacity);
|
|
3121
3331
|
pointer-events: none;
|
|
3122
3332
|
transition:
|
|
@@ -3196,7 +3406,7 @@ small {
|
|
|
3196
3406
|
.otp {
|
|
3197
3407
|
--otp-cell-size: var(--control-size);
|
|
3198
3408
|
--otp-gap: var(--space-10);
|
|
3199
|
-
--otp-font-size: 1.
|
|
3409
|
+
--otp-font-size: 1.5em;
|
|
3200
3410
|
--otp-cell-border: var(--border);
|
|
3201
3411
|
--otp-cell-bg: var(--surface);
|
|
3202
3412
|
--otp-caret-space: calc((var(--otp-cell-size) - 1ch) / 2 + var(--otp-gap) + 1px);
|
|
@@ -3209,6 +3419,7 @@ small {
|
|
|
3209
3419
|
max-inline-size: 100%;
|
|
3210
3420
|
gap: var(--otp-gap);
|
|
3211
3421
|
border-radius: var(--radius);
|
|
3422
|
+
font-size: var(--control-font-size);
|
|
3212
3423
|
/* Unlike hidden, clip cannot become a scroll container when the caret moves
|
|
3213
3424
|
into the input extension. */
|
|
3214
3425
|
overflow: clip;
|
|
@@ -3250,6 +3461,7 @@ small {
|
|
|
3250
3461
|
letter-spacing: calc(var(--otp-cell-size) + var(--otp-gap) - 1ch);
|
|
3251
3462
|
text-align: start;
|
|
3252
3463
|
appearance: none;
|
|
3464
|
+
-webkit-appearance: none;
|
|
3253
3465
|
}
|
|
3254
3466
|
|
|
3255
3467
|
/* Deliberately no group ring: a box around all the cells reads as a second
|
|
@@ -3284,6 +3496,12 @@ small {
|
|
|
3284
3496
|
cursor: not-allowed;
|
|
3285
3497
|
}
|
|
3286
3498
|
|
|
3499
|
+
/* biome-ignore lint/style/noDescendingSpecificity: disabled must come last of
|
|
3500
|
+
the cell states, and the reorder the rule asks for would invert that. A
|
|
3501
|
+
disabled input is barred from constraint validation, so `:invalid` and
|
|
3502
|
+
`:focus` cannot reach it at all; the only overlap left is an authored
|
|
3503
|
+
aria-invalid, where equal specificity and source order are what make
|
|
3504
|
+
disabled — not danger — the colour a dead field reads as. */
|
|
3287
3505
|
.otp > input:disabled ~ span {
|
|
3288
3506
|
--otp-cell-bg: var(--surface-subtle);
|
|
3289
3507
|
--otp-cell-border: var(--state-disabled);
|
|
@@ -3343,6 +3561,7 @@ small {
|
|
|
3343
3561
|
:where(.btn) {
|
|
3344
3562
|
--intent: initial;
|
|
3345
3563
|
--intent-fg: initial;
|
|
3564
|
+
--intent-soft-fg: initial;
|
|
3346
3565
|
--ui-bg: initial;
|
|
3347
3566
|
--ui-fg: initial;
|
|
3348
3567
|
--ui-border: initial;
|
|
@@ -3414,13 +3633,16 @@ small {
|
|
|
3414
3633
|
--ui-border: transparent;
|
|
3415
3634
|
}
|
|
3416
3635
|
|
|
3417
|
-
/* Solid
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3636
|
+
/* Solid keeps the overlay darkening on hover; soft/outline/ghost/link stay
|
|
3637
|
+
flat. The soft hover is its own fill change (--soft-hover-alpha) — a second
|
|
3638
|
+
overlay on top would double the fill step and drag its ink below the resting
|
|
3639
|
+
contrast on every intent. Routed through a variable instead of a
|
|
3640
|
+
:not(.outline, .ghost, .link) exclusion so the hover rule below only has to
|
|
3641
|
+
state the interactive element list once. */
|
|
3421
3642
|
.btn.outline,
|
|
3422
3643
|
.btn.ghost,
|
|
3423
|
-
.btn.link
|
|
3644
|
+
.btn.link,
|
|
3645
|
+
.btn.soft {
|
|
3424
3646
|
--btn-hover-overlay: transparent;
|
|
3425
3647
|
}
|
|
3426
3648
|
|
|
@@ -3544,17 +3766,6 @@ a.btn[aria-disabled="true"] {
|
|
|
3544
3766
|
}
|
|
3545
3767
|
}
|
|
3546
3768
|
|
|
3547
|
-
@media print {
|
|
3548
|
-
:is(button, input[type="button"], input[type="submit"], input[type="reset"]).btn {
|
|
3549
|
-
display: none !important;
|
|
3550
|
-
}
|
|
3551
|
-
|
|
3552
|
-
a.btn[href^="http"]::after {
|
|
3553
|
-
content: " (" attr(href) ")";
|
|
3554
|
-
font-size: 0.875em;
|
|
3555
|
-
}
|
|
3556
|
-
}
|
|
3557
|
-
|
|
3558
3769
|
/*
|
|
3559
3770
|
* Badges for compact labels, counts, and state hints.
|
|
3560
3771
|
*
|
|
@@ -3566,8 +3777,10 @@ a.btn[aria-disabled="true"] {
|
|
|
3566
3777
|
* --badge-size
|
|
3567
3778
|
* --badge-pad-x
|
|
3568
3779
|
* --badge-font-size
|
|
3780
|
+
* --badge-icon-size
|
|
3569
3781
|
* --badge-dot-size
|
|
3570
3782
|
* --badge-radius
|
|
3783
|
+
* --badge-dismiss-icon-size
|
|
3571
3784
|
*
|
|
3572
3785
|
* Internal:
|
|
3573
3786
|
* --badge-default-bg
|
|
@@ -3583,6 +3796,7 @@ a.btn[aria-disabled="true"] {
|
|
|
3583
3796
|
:where(.badge) {
|
|
3584
3797
|
--intent: initial;
|
|
3585
3798
|
--intent-fg: initial;
|
|
3799
|
+
--intent-soft-fg: initial;
|
|
3586
3800
|
--ui-bg: initial;
|
|
3587
3801
|
--ui-fg: initial;
|
|
3588
3802
|
--ui-border: initial;
|
|
@@ -3590,10 +3804,14 @@ a.btn[aria-disabled="true"] {
|
|
|
3590
3804
|
}
|
|
3591
3805
|
|
|
3592
3806
|
.badge {
|
|
3593
|
-
--badge-size:
|
|
3594
|
-
|
|
3595
|
-
|
|
3807
|
+
--badge-size: 1.5rem;
|
|
3808
|
+
/* A third of the height keeps the pill proportional across local sizes
|
|
3809
|
+
without coupling it to the label typography. */
|
|
3810
|
+
--badge-pad-x: calc(var(--badge-size) / 3);
|
|
3811
|
+
--badge-font-size: var(--font-size-sm);
|
|
3812
|
+
--badge-icon-size: 1em;
|
|
3596
3813
|
--badge-dot-size: 0.5rem;
|
|
3814
|
+
--badge-dismiss-icon-size: 0.875em;
|
|
3597
3815
|
--badge-default-bg: var(--surface-subtle);
|
|
3598
3816
|
--badge-default-fg: var(--intent, var(--text));
|
|
3599
3817
|
--badge-default-border: var(--border);
|
|
@@ -3606,7 +3824,11 @@ a.btn[aria-disabled="true"] {
|
|
|
3606
3824
|
display: inline-flex;
|
|
3607
3825
|
align-items: center;
|
|
3608
3826
|
justify-content: center;
|
|
3827
|
+
vertical-align: middle;
|
|
3609
3828
|
gap: 0.35em;
|
|
3829
|
+
/* Content-sized: a flex parent must not squeeze the pill and push its
|
|
3830
|
+
nowrap label outside the background. */
|
|
3831
|
+
flex: none;
|
|
3610
3832
|
inline-size: fit-content;
|
|
3611
3833
|
max-inline-size: 100%;
|
|
3612
3834
|
min-block-size: var(--badge-size);
|
|
@@ -3616,7 +3838,7 @@ a.btn[aria-disabled="true"] {
|
|
|
3616
3838
|
background: var(--badge-bg);
|
|
3617
3839
|
color: var(--badge-fg);
|
|
3618
3840
|
font-size: var(--badge-font-size);
|
|
3619
|
-
font-weight: var(--font-weight-
|
|
3841
|
+
font-weight: var(--font-weight-medium);
|
|
3620
3842
|
line-height: 1;
|
|
3621
3843
|
text-decoration: none;
|
|
3622
3844
|
white-space: nowrap;
|
|
@@ -3627,6 +3849,31 @@ a.btn[aria-disabled="true"] {
|
|
|
3627
3849
|
box-shadow var(--duration);
|
|
3628
3850
|
}
|
|
3629
3851
|
|
|
3852
|
+
.badge.sm {
|
|
3853
|
+
--badge-size: 1.25rem;
|
|
3854
|
+
--badge-font-size: var(--font-size-xs);
|
|
3855
|
+
--badge-dot-size: 0.375rem;
|
|
3856
|
+
}
|
|
3857
|
+
|
|
3858
|
+
.badge.lg {
|
|
3859
|
+
--badge-size: 1.875rem;
|
|
3860
|
+
--badge-font-size: var(--font-size-md);
|
|
3861
|
+
--badge-dot-size: 0.625rem;
|
|
3862
|
+
}
|
|
3863
|
+
|
|
3864
|
+
/* Decorative children follow the badge's optical size. Text and interactive
|
|
3865
|
+
dismiss buttons retain their own boxes. */
|
|
3866
|
+
.badge > :where(svg, img, [aria-hidden="true"]:not(.dot)) {
|
|
3867
|
+
flex: none;
|
|
3868
|
+
/* The shared rule maps --badge-icon-size onto font-size. One em is therefore
|
|
3869
|
+
the requested optical size; reusing the em-valued hook here would apply its
|
|
3870
|
+
factor a second time. */
|
|
3871
|
+
inline-size: 1em;
|
|
3872
|
+
block-size: 1em;
|
|
3873
|
+
font-size: var(--badge-icon-size);
|
|
3874
|
+
line-height: 1;
|
|
3875
|
+
}
|
|
3876
|
+
|
|
3630
3877
|
/* Soft by default. Local defaults remain behind --ui-* in the resolution
|
|
3631
3878
|
chain, so explicit shared treatments compose without selector exclusions. */
|
|
3632
3879
|
@supports (color: color-mix(in oklch, red, white)) {
|
|
@@ -3642,10 +3889,9 @@ a.btn[aria-disabled="true"] {
|
|
|
3642
3889
|
var(--surface) var(--soft-border-mix),
|
|
3643
3890
|
var(--intent, var(--neutral))
|
|
3644
3891
|
);
|
|
3645
|
-
--badge-default-fg:
|
|
3646
|
-
|
|
3647
|
-
var(--text),
|
|
3648
|
-
var(--intent, var(--text)) var(--soft-fg-mix)
|
|
3892
|
+
--badge-default-fg: var(
|
|
3893
|
+
--intent-soft-fg,
|
|
3894
|
+
color-mix(in oklab, var(--text), var(--intent, var(--text)) var(--soft-fg-mix))
|
|
3649
3895
|
);
|
|
3650
3896
|
/* @sync soft-recipe */
|
|
3651
3897
|
}
|
|
@@ -3662,12 +3908,22 @@ a.btn[aria-disabled="true"] {
|
|
|
3662
3908
|
block-size: var(--badge-size);
|
|
3663
3909
|
}
|
|
3664
3910
|
|
|
3911
|
+
/* The shared .dot separator doubles as a status dot inside a badge.
|
|
3912
|
+
currentColor follows the badge's resolved ink, so the dot matches a soft
|
|
3913
|
+
intent without repeating the intent class on it. */
|
|
3914
|
+
.badge > .dot {
|
|
3915
|
+
background-color: currentColor;
|
|
3916
|
+
}
|
|
3917
|
+
|
|
3665
3918
|
.badge > button {
|
|
3666
3919
|
display: inline-grid;
|
|
3667
3920
|
place-items: center;
|
|
3668
3921
|
flex: 0 0 auto;
|
|
3669
|
-
|
|
3670
|
-
|
|
3922
|
+
/* WCAG 2.5.8 floors pointer targets at 24 CSS px. Density and local size may
|
|
3923
|
+
keep the badge visually compact through the negative margins, but never
|
|
3924
|
+
shrink its action target below that literal floor. */
|
|
3925
|
+
min-inline-size: max(var(--badge-size), 24px);
|
|
3926
|
+
min-block-size: max(var(--badge-size), 24px);
|
|
3671
3927
|
margin-block: -0.25rem;
|
|
3672
3928
|
margin-inline-start: -0.15em;
|
|
3673
3929
|
margin-inline-end: -0.45em;
|
|
@@ -3681,6 +3937,21 @@ a.btn[aria-disabled="true"] {
|
|
|
3681
3937
|
cursor: pointer;
|
|
3682
3938
|
}
|
|
3683
3939
|
|
|
3940
|
+
/* Dismiss mark — an empty dismiss button paints its own X from the shared
|
|
3941
|
+
--icon-close mask, so no icon font or text glyph is required in the markup
|
|
3942
|
+
(same contract as .alert-dismiss and .dialog-close). A mask box is a block
|
|
3943
|
+
box, so place-items centers the ink itself; a text glyph would only center
|
|
3944
|
+
its line box and land the ink low, because a lowercase x has no descender
|
|
3945
|
+
and only x-height of ascent. A button with its own content keeps it. */
|
|
3946
|
+
.badge > button:empty::before {
|
|
3947
|
+
content: "";
|
|
3948
|
+
inline-size: var(--badge-dismiss-icon-size);
|
|
3949
|
+
block-size: var(--badge-dismiss-icon-size);
|
|
3950
|
+
background: currentColor;
|
|
3951
|
+
-webkit-mask: var(--icon-close) center / contain no-repeat;
|
|
3952
|
+
mask: var(--icon-close) center / contain no-repeat;
|
|
3953
|
+
}
|
|
3954
|
+
|
|
3684
3955
|
.badge > button:not(:disabled):hover {
|
|
3685
3956
|
background: var(--hover-overlay);
|
|
3686
3957
|
}
|
|
@@ -3713,7 +3984,14 @@ a.btn[aria-disabled="true"] {
|
|
|
3713
3984
|
background: var(--ui-hover-bg, var(--badge-default-hover-bg));
|
|
3714
3985
|
}
|
|
3715
3986
|
|
|
3716
|
-
|
|
3987
|
+
/* Solid and empty badges keep the overlay darkening on hover; soft badges do
|
|
3988
|
+
not — their hover fill (--soft-hover-alpha) is already the state change, and
|
|
3989
|
+
stacking a second darkening drags the soft ink below its resting contrast.
|
|
3990
|
+
Soft here means :not(.solid, .outline, :empty), mirroring the soft hover-bg
|
|
3991
|
+
recipe above. */
|
|
3992
|
+
:where(a.badge, button.badge):is(.solid, :empty):not(
|
|
3993
|
+
:where(:disabled, [aria-disabled="true"])
|
|
3994
|
+
):hover {
|
|
3717
3995
|
/* Huge inset spread paints the overlay over the pill radius (see button.css). */
|
|
3718
3996
|
box-shadow: inset 0 0 0 999px var(--hover-overlay);
|
|
3719
3997
|
}
|
|
@@ -3745,7 +4023,6 @@ a.badge[aria-disabled="true"] {
|
|
|
3745
4023
|
@media print {
|
|
3746
4024
|
.badge {
|
|
3747
4025
|
border: var(--border-width) solid CanvasText;
|
|
3748
|
-
break-inside: avoid;
|
|
3749
4026
|
}
|
|
3750
4027
|
}
|
|
3751
4028
|
|
|
@@ -3783,6 +4060,7 @@ a.badge[aria-disabled="true"] {
|
|
|
3783
4060
|
:where(.alert) {
|
|
3784
4061
|
--intent: initial;
|
|
3785
4062
|
--intent-fg: initial;
|
|
4063
|
+
--intent-soft-fg: initial;
|
|
3786
4064
|
--ui-bg: initial;
|
|
3787
4065
|
--ui-fg: initial;
|
|
3788
4066
|
--ui-border: initial;
|
|
@@ -3815,12 +4093,23 @@ a.badge[aria-disabled="true"] {
|
|
|
3815
4093
|
min-inline-size: 0;
|
|
3816
4094
|
}
|
|
3817
4095
|
|
|
3818
|
-
|
|
3819
|
-
|
|
4096
|
+
/* The alert owns the block spacing between its direct content blocks, the way
|
|
4097
|
+
.stack and .prose own theirs, so a title and a description are plain
|
|
4098
|
+
siblings instead of a wrapper carrying an inline --gap. What made that pair
|
|
4099
|
+
read as detached is the UA paragraph margin: trimming only the outer edges
|
|
4100
|
+
left it in place between them. Content keeps its own internal rhythm — a
|
|
4101
|
+
nested list keeps its bullets and padding, a .prose child keeps its flow.
|
|
4102
|
+
The slots are excluded because they are anatomy, not content: an
|
|
4103
|
+
admonition's title bar has to stay flush against its body. The list follows
|
|
4104
|
+
the anatomy, so it grows with it. */
|
|
4105
|
+
.alert > :not(:where(.alert-icon, .alert-dismiss, .alert-title, .alert-body)) {
|
|
4106
|
+
margin-block: 0;
|
|
3820
4107
|
}
|
|
3821
4108
|
|
|
3822
|
-
.alert
|
|
3823
|
-
|
|
4109
|
+
.alert
|
|
4110
|
+
> :not(:where(.alert-icon, .alert-dismiss, .alert-title, .alert-body))
|
|
4111
|
+
+ :not(:where(.alert-icon, .alert-dismiss, .alert-title, .alert-body)) {
|
|
4112
|
+
margin-block-start: var(--space-10);
|
|
3824
4113
|
}
|
|
3825
4114
|
|
|
3826
4115
|
/* Alerts are soft by default. The recipe writes local defaults rather than
|
|
@@ -3838,10 +4127,9 @@ a.badge[aria-disabled="true"] {
|
|
|
3838
4127
|
var(--surface) var(--soft-border-mix),
|
|
3839
4128
|
var(--intent, var(--neutral))
|
|
3840
4129
|
);
|
|
3841
|
-
--alert-default-fg:
|
|
3842
|
-
|
|
3843
|
-
var(--text),
|
|
3844
|
-
var(--intent, var(--text)) var(--soft-fg-mix)
|
|
4130
|
+
--alert-default-fg: var(
|
|
4131
|
+
--intent-soft-fg,
|
|
4132
|
+
color-mix(in oklab, var(--text), var(--intent, var(--text)) var(--soft-fg-mix))
|
|
3845
4133
|
);
|
|
3846
4134
|
/* @sync soft-recipe */
|
|
3847
4135
|
}
|
|
@@ -3942,7 +4230,7 @@ a.badge[aria-disabled="true"] {
|
|
|
3942
4230
|
/* Links */
|
|
3943
4231
|
.alert :where(a:not(.btn)) {
|
|
3944
4232
|
color: inherit;
|
|
3945
|
-
font-weight: var(--font-weight-
|
|
4233
|
+
font-weight: var(--font-weight-semibold);
|
|
3946
4234
|
}
|
|
3947
4235
|
|
|
3948
4236
|
/* Forced colors: no override needed. The always-on border gets a forced
|
|
@@ -3950,11 +4238,12 @@ a.badge[aria-disabled="true"] {
|
|
|
3950
4238
|
Canvas backplate (unreadable) — so .solid intentionally falls back to the
|
|
3951
4239
|
default look. */
|
|
3952
4240
|
|
|
3953
|
-
/* Callout —
|
|
3954
|
-
|
|
4241
|
+
/* Callout — leading-edge flag treatment. It owns the border geometry only, so
|
|
4242
|
+
the surface and the ink stay with the alert's own intent and shared
|
|
4243
|
+
treatment: bare is the soft panel, .surface the page surface, .solid a
|
|
4244
|
+
filled one, and .outline has no fill and no other side. Thickness and color
|
|
4245
|
+
follow the --alert-border-inline-start-* hooks. */
|
|
3955
4246
|
.alert.callout {
|
|
3956
|
-
--alert-bg: var(--surface-subtle);
|
|
3957
|
-
--alert-fg: var(--text);
|
|
3958
4247
|
border: 0;
|
|
3959
4248
|
border-inline-start: var(--alert-border-inline-start-width, 4px) solid
|
|
3960
4249
|
var(--alert-border-inline-start-color, var(--intent, var(--neutral)));
|
|
@@ -4079,8 +4368,15 @@ a.badge[aria-disabled="true"] {
|
|
|
4079
4368
|
* --busy-overlay-bg
|
|
4080
4369
|
*/
|
|
4081
4370
|
/* Intent boundary — inherited intents do not tint this component.
|
|
4082
|
-
Local intent classes still win because :where() has zero specificity.
|
|
4371
|
+
Local intent classes still win because :where() has zero specificity.
|
|
4372
|
+
The intent tokens are reset alongside --ui-*: the shared variants
|
|
4373
|
+
(.soft/.solid/.outline/.surface) read --intent / --intent-fg / --intent-soft-fg
|
|
4374
|
+
when they paint this surface, so an ancestor intent would otherwise leak
|
|
4375
|
+
through a variant that has no local intent class. */
|
|
4083
4376
|
:where(.card) {
|
|
4377
|
+
--intent: initial;
|
|
4378
|
+
--intent-fg: initial;
|
|
4379
|
+
--intent-soft-fg: initial;
|
|
4084
4380
|
--ui-bg: initial;
|
|
4085
4381
|
--ui-fg: initial;
|
|
4086
4382
|
--ui-border: initial;
|
|
@@ -4153,7 +4449,7 @@ a.badge[aria-disabled="true"] {
|
|
|
4153
4449
|
box-shadow: var(--shadow);
|
|
4154
4450
|
}
|
|
4155
4451
|
.card.subtle {
|
|
4156
|
-
|
|
4452
|
+
--card-bg: var(--surface-subtle);
|
|
4157
4453
|
}
|
|
4158
4454
|
.card.compact {
|
|
4159
4455
|
--card-pad: var(--space-20);
|
|
@@ -4205,56 +4501,6 @@ a.badge[aria-disabled="true"] {
|
|
|
4205
4501
|
}
|
|
4206
4502
|
}
|
|
4207
4503
|
|
|
4208
|
-
/*
|
|
4209
|
-
* Surface — shared modal scrim and backdrop for flyout and drawer.
|
|
4210
|
-
*
|
|
4211
|
-
* Drawn by .drawer::backdrop (native dialog top-layer) and .surface-backdrop
|
|
4212
|
-
* (custom fixed div for flyout sheets on coarse pointers). The native
|
|
4213
|
-
* .modal::backdrop uses the same fill and animates opacity from 0 to 1
|
|
4214
|
-
* (modal.css), so the scrim strength stays consistent across the dialog
|
|
4215
|
-
* family.
|
|
4216
|
-
*
|
|
4217
|
-
* .surface-backdrop, .is-sheet, and .is-open are runtime markers set by
|
|
4218
|
-
* surface.js. Authors opt in via [data-flyout-mobile="sheet|auto|none|anchored"]
|
|
4219
|
-
* — the JS resolves "auto" into .is-sheet at run time.
|
|
4220
|
-
*/
|
|
4221
|
-
/* Custom backdrop for overlays that do not use the native dialog top-layer.
|
|
4222
|
-
* Its z-index sits just below the flyout it accompanies. */
|
|
4223
|
-
.surface-backdrop {
|
|
4224
|
-
position: fixed;
|
|
4225
|
-
inset: 0;
|
|
4226
|
-
z-index: calc(var(--z-menu) - 1);
|
|
4227
|
-
background: var(--backdrop-fill);
|
|
4228
|
-
opacity: 1;
|
|
4229
|
-
/* A scrim never travels; it fades on the neutral `ease`. Duration matches
|
|
4230
|
-
the flyout sheet it accompanies so it never lags the panel. */
|
|
4231
|
-
transition: opacity var(--duration-fast) ease;
|
|
4232
|
-
}
|
|
4233
|
-
|
|
4234
|
-
.surface-backdrop[hidden] {
|
|
4235
|
-
display: none;
|
|
4236
|
-
opacity: 0;
|
|
4237
|
-
}
|
|
4238
|
-
|
|
4239
|
-
@starting-style {
|
|
4240
|
-
.surface-backdrop:not([hidden]) {
|
|
4241
|
-
opacity: 0;
|
|
4242
|
-
}
|
|
4243
|
-
}
|
|
4244
|
-
|
|
4245
|
-
@supports (transition-behavior: allow-discrete) {
|
|
4246
|
-
.surface-backdrop {
|
|
4247
|
-
transition:
|
|
4248
|
-
opacity var(--duration-fast) ease,
|
|
4249
|
-
display var(--duration-fast) allow-discrete;
|
|
4250
|
-
}
|
|
4251
|
-
}
|
|
4252
|
-
@media print {
|
|
4253
|
-
.surface-backdrop {
|
|
4254
|
-
display: none !important;
|
|
4255
|
-
}
|
|
4256
|
-
}
|
|
4257
|
-
|
|
4258
4504
|
/*
|
|
4259
4505
|
* Modal shell styling for native dialog elements.
|
|
4260
4506
|
*
|
|
@@ -4269,21 +4515,40 @@ a.badge[aria-disabled="true"] {
|
|
|
4269
4515
|
*
|
|
4270
4516
|
* Internal:
|
|
4271
4517
|
* --dialog-close-icon-size
|
|
4518
|
+
* --dialog-viewport-block
|
|
4519
|
+
* --dialog-viewport-gap-default
|
|
4272
4520
|
* --dialog-viewport-gap-2x
|
|
4273
4521
|
*/
|
|
4274
4522
|
dialog.modal {
|
|
4275
|
-
--
|
|
4276
|
-
|
|
4277
|
-
|
|
4523
|
+
/* --dialog-viewport-gap stays fallback-only for the same reason as
|
|
4524
|
+
--modal-size below: declaring the public hook here would give it
|
|
4525
|
+
type+class specificity that a single author class cannot outrank. The
|
|
4526
|
+
responsive default lives in an internal token instead, so a media query
|
|
4527
|
+
can still move it without closing the hook.
|
|
4528
|
+
|
|
4529
|
+
calc() multiplication is not in the supported browser range, so the
|
|
4278
4530
|
doubled gap is spelled out with an additive calc. */
|
|
4279
|
-
--dialog-viewport-gap-
|
|
4531
|
+
--dialog-viewport-gap-default: var(--space-40);
|
|
4532
|
+
--dialog-viewport-gap-2x: calc(
|
|
4533
|
+
var(--dialog-viewport-gap, var(--dialog-viewport-gap-default)) +
|
|
4534
|
+
var(--dialog-viewport-gap, var(--dialog-viewport-gap-default))
|
|
4535
|
+
);
|
|
4280
4536
|
|
|
4281
4537
|
/* Keep the surface anchored to the viewport in the top layer. This also
|
|
4282
4538
|
provides the containing block for the absolutely positioned close button. */
|
|
4283
4539
|
position: fixed;
|
|
4284
4540
|
|
|
4285
|
-
|
|
4286
|
-
|
|
4541
|
+
/* --modal-size stays fallback-only: declaring it on dialog.modal gives it
|
|
4542
|
+
type+class specificity, which a single author class cannot outrank, so the
|
|
4543
|
+
public hook would silently keep this default. Same reason choice.css never
|
|
4544
|
+
declares --choice-control-nudge. */
|
|
4545
|
+
max-inline-size: min(calc(100% - var(--dialog-viewport-gap-2x)), var(--modal-size, 32rem));
|
|
4546
|
+
|
|
4547
|
+
/* Stable viewport cap for the panel: dvh tracks the dynamic browser toolbar
|
|
4548
|
+
and makes the dialog reflow on mobile, svh does not. App shells keep the
|
|
4549
|
+
dynamic token; only the modal pins the small viewport. */
|
|
4550
|
+
--dialog-viewport-block: var(--viewport-block);
|
|
4551
|
+
max-block-size: calc(var(--dialog-viewport-block) - var(--dialog-viewport-gap-2x));
|
|
4287
4552
|
border: var(--border-width) solid var(--border);
|
|
4288
4553
|
border-radius: var(--radius-lg);
|
|
4289
4554
|
background: var(--surface-raised);
|
|
@@ -4293,7 +4558,7 @@ dialog.modal {
|
|
|
4293
4558
|
overflow: visible;
|
|
4294
4559
|
opacity: 0;
|
|
4295
4560
|
/*
|
|
4296
|
-
* Keep the open dialog root untransformed. Fixed overlays
|
|
4561
|
+
* Keep the open dialog root untransformed. Fixed overlays authored inside a
|
|
4297
4562
|
* modal dialog use viewport coordinates; transform/translate/scale on this
|
|
4298
4563
|
* element would make them position against the dialog instead. The closed
|
|
4299
4564
|
* state can still carry a small transform for enter/exit motion. The lift
|
|
@@ -4310,16 +4575,23 @@ dialog.modal > form,
|
|
|
4310
4575
|
dialog.modal > .stack {
|
|
4311
4576
|
display: grid;
|
|
4312
4577
|
gap: var(--space-40);
|
|
4313
|
-
max-block-size: calc(var(--viewport-block) - var(--dialog-viewport-gap-2x));
|
|
4578
|
+
max-block-size: calc(var(--dialog-viewport-block) - var(--dialog-viewport-gap-2x));
|
|
4314
4579
|
overflow: auto;
|
|
4315
4580
|
overscroll-behavior: contain;
|
|
4316
4581
|
padding: var(--space-40);
|
|
4317
4582
|
}
|
|
4318
4583
|
|
|
4319
|
-
|
|
4584
|
+
@supports (block-size: 100svh) {
|
|
4585
|
+
dialog.modal {
|
|
4586
|
+
--dialog-viewport-block: 100svh;
|
|
4587
|
+
}
|
|
4588
|
+
}
|
|
4589
|
+
|
|
4590
|
+
/* Keep the dialog off the viewport edges on small screens. Moves the internal
|
|
4591
|
+
default, not the public hook, so an author override still wins here too. */
|
|
4320
4592
|
@media (max-width: 30rem) {
|
|
4321
4593
|
dialog.modal {
|
|
4322
|
-
--dialog-viewport-gap: var(--space-30);
|
|
4594
|
+
--dialog-viewport-gap-default: var(--space-30);
|
|
4323
4595
|
}
|
|
4324
4596
|
}
|
|
4325
4597
|
|
|
@@ -4329,8 +4601,8 @@ dialog.modal.scrollable > .stack {
|
|
|
4329
4601
|
overflow: hidden;
|
|
4330
4602
|
}
|
|
4331
4603
|
|
|
4332
|
-
dialog.modal.scrollable > form > :not(header, footer),
|
|
4333
|
-
dialog.modal.scrollable > .stack > :not(header, footer) {
|
|
4604
|
+
dialog.modal.scrollable > form > :not(:is(header, footer)),
|
|
4605
|
+
dialog.modal.scrollable > .stack > :not(:is(header, footer)) {
|
|
4334
4606
|
min-block-size: 0;
|
|
4335
4607
|
overflow: auto;
|
|
4336
4608
|
overscroll-behavior: contain;
|
|
@@ -4385,6 +4657,7 @@ dialog.modal.dialog-confirmation > form > footer {
|
|
|
4385
4657
|
:where(.dialog-icon) {
|
|
4386
4658
|
--intent: initial;
|
|
4387
4659
|
--intent-fg: initial;
|
|
4660
|
+
--intent-soft-fg: initial;
|
|
4388
4661
|
--ui-bg: initial;
|
|
4389
4662
|
--ui-fg: initial;
|
|
4390
4663
|
}
|
|
@@ -4513,14 +4786,11 @@ html.has-modal-open.had-scrollbar {
|
|
|
4513
4786
|
}
|
|
4514
4787
|
}
|
|
4515
4788
|
|
|
4516
|
-
/* Close
|
|
4517
|
-
never participates in the content flow
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
required in the markup. */
|
|
4522
|
-
.dialog-close,
|
|
4523
|
-
.drawer-close {
|
|
4789
|
+
/* Close control. A close button is an action attached to a surface, so it
|
|
4790
|
+
never participates in the content flow — absolute at the panel's top
|
|
4791
|
+
inline-end, inside the surface. The glyph comes from --icon-close, masked to
|
|
4792
|
+
currentColor, so no icon font or text glyph is required in the markup. */
|
|
4793
|
+
.dialog-close {
|
|
4524
4794
|
--dialog-close-icon-size: 1rem;
|
|
4525
4795
|
|
|
4526
4796
|
display: inline-grid;
|
|
@@ -4544,8 +4814,7 @@ html.has-modal-open.had-scrollbar {
|
|
|
4544
4814
|
background: var(--surface-subtle);
|
|
4545
4815
|
}
|
|
4546
4816
|
|
|
4547
|
-
.dialog-close::before
|
|
4548
|
-
.drawer-close::before {
|
|
4817
|
+
.dialog-close::before {
|
|
4549
4818
|
content: "";
|
|
4550
4819
|
inline-size: var(--dialog-close-icon-size);
|
|
4551
4820
|
block-size: var(--dialog-close-icon-size);
|
|
@@ -4555,28 +4824,20 @@ html.has-modal-open.had-scrollbar {
|
|
|
4555
4824
|
}
|
|
4556
4825
|
|
|
4557
4826
|
@media (hover: hover) {
|
|
4558
|
-
.dialog-close:hover
|
|
4559
|
-
.drawer-close:hover {
|
|
4827
|
+
.dialog-close:hover {
|
|
4560
4828
|
box-shadow: inset 0 0 0 999px var(--hover-overlay);
|
|
4561
4829
|
color: var(--text);
|
|
4562
4830
|
}
|
|
4563
4831
|
}
|
|
4564
4832
|
|
|
4565
|
-
.dialog-close:active
|
|
4566
|
-
.drawer-close:active {
|
|
4833
|
+
.dialog-close:active {
|
|
4567
4834
|
box-shadow: inset 0 0 0 999px var(--hover-overlay);
|
|
4568
4835
|
}
|
|
4569
4836
|
|
|
4570
|
-
.dialog-close:focus-visible
|
|
4571
|
-
.drawer-close:focus-visible {
|
|
4837
|
+
.dialog-close:focus-visible {
|
|
4572
4838
|
outline: 2px solid transparent;
|
|
4573
4839
|
box-shadow: var(--focus-ring-shadow);
|
|
4574
4840
|
}
|
|
4575
|
-
@media print {
|
|
4576
|
-
dialog.modal[open] {
|
|
4577
|
-
display: none !important;
|
|
4578
|
-
}
|
|
4579
|
-
}
|
|
4580
4841
|
|
|
4581
4842
|
/*
|
|
4582
4843
|
* Drawer — modal side-sheet built on the native dialog element.
|
|
@@ -4606,15 +4867,24 @@ dialog.drawer {
|
|
|
4606
4867
|
|
|
4607
4868
|
/* Keep the surface anchored to the viewport in the top layer. This also
|
|
4608
4869
|
provides the containing block for the absolutely positioned close button.
|
|
4609
|
-
|
|
4610
|
-
|
|
4870
|
+
|
|
4871
|
+
The panel reaches both viewport edges, so it has to follow the space that
|
|
4872
|
+
is actually on screen: --viewport-block resolves to 100dvh and tracks a
|
|
4873
|
+
mobile browser's collapsing URL bar. A percentage resolves against the
|
|
4874
|
+
layout viewport instead, which those browsers size as if the bar were
|
|
4875
|
+
hidden — that put the footer below the fold for as long as it showed. The
|
|
4876
|
+
modal pins the small viewport for the opposite reason: it floats, so a
|
|
4877
|
+
stable height beats a panel that breathes while the bar moves.
|
|
4878
|
+
|
|
4879
|
+
An embedded preview that fakes a viewport overrides --viewport-block on
|
|
4880
|
+
its own shell; the component does not degrade to a percentage for it. */
|
|
4611
4881
|
position: fixed;
|
|
4612
4882
|
inset-block-start: 0;
|
|
4613
4883
|
|
|
4614
4884
|
inline-size: min(calc(var(--viewport-inline) * 0.9), var(--drawer-size));
|
|
4615
|
-
block-size:
|
|
4885
|
+
block-size: var(--viewport-block);
|
|
4616
4886
|
max-inline-size: none;
|
|
4617
|
-
max-block-size:
|
|
4887
|
+
max-block-size: var(--viewport-block);
|
|
4618
4888
|
margin: 0;
|
|
4619
4889
|
padding: var(--drawer-pad);
|
|
4620
4890
|
border: 0;
|
|
@@ -4642,8 +4912,18 @@ dialog.drawer[open] {
|
|
|
4642
4912
|
transform var(--drawer-motion-duration) var(--ease-enter);
|
|
4643
4913
|
}
|
|
4644
4914
|
|
|
4645
|
-
/*
|
|
4646
|
-
|
|
4915
|
+
/* A form is the drawer's content wrapper when actions need method="dialog".
|
|
4916
|
+
Span the drawer tracks so the form can keep the header at the top, let the
|
|
4917
|
+
body absorb the available height, and keep the footer at the bottom. */
|
|
4918
|
+
dialog.drawer > form {
|
|
4919
|
+
display: grid;
|
|
4920
|
+
grid-row: 1 / -1;
|
|
4921
|
+
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
4922
|
+
gap: var(--space-40);
|
|
4923
|
+
min-block-size: 100%;
|
|
4924
|
+
}
|
|
4925
|
+
|
|
4926
|
+
/* The shared backdrop token keeps drawer and modal scrims consistent. */
|
|
4647
4927
|
dialog.drawer::backdrop {
|
|
4648
4928
|
background: var(--backdrop-fill);
|
|
4649
4929
|
opacity: 0;
|
|
@@ -4676,10 +4956,16 @@ dialog.drawer[open]::backdrop {
|
|
|
4676
4956
|
}
|
|
4677
4957
|
}
|
|
4678
4958
|
|
|
4679
|
-
/*
|
|
4680
|
-
|
|
4959
|
+
/* The body region absorbs the panel's height and scrolls. A drawer is a
|
|
4960
|
+
full-height shell by definition, so it always scrolls its body — unlike the
|
|
4961
|
+
modal, which sizes to its content and opts in with .scrollable. Header and
|
|
4962
|
+
footer keep their own tracks; .drawer-close is named because a generic
|
|
4963
|
+
exclusion list is easier to trust than its `position: absolute`. */
|
|
4964
|
+
dialog.drawer > :not(:is(header, footer, form, .drawer-close)),
|
|
4965
|
+
dialog.drawer > form > :not(:is(header, footer, .drawer-close)) {
|
|
4681
4966
|
min-block-size: 0;
|
|
4682
|
-
overflow
|
|
4967
|
+
overflow: auto;
|
|
4968
|
+
overscroll-behavior: contain;
|
|
4683
4969
|
}
|
|
4684
4970
|
|
|
4685
4971
|
@supports (transition-behavior: allow-discrete) {
|
|
@@ -4708,33 +4994,33 @@ dialog.drawer nav {
|
|
|
4708
4994
|
}
|
|
4709
4995
|
|
|
4710
4996
|
/* Safari before 16.4 does not support :dir(); keep the ancestor-attribute
|
|
4711
|
-
path as the
|
|
4997
|
+
path as the Degraded-tier fallback. The :dir() rules are dropped on their
|
|
4998
|
+
own there, so they need no wrapper. :dir() follows inherited HTML
|
|
4999
|
+
directionality and handles nested direction changes; the explicit ltr rules
|
|
5000
|
+
neutralize an outer RTL fallback selector. Each specificity group keeps the
|
|
5001
|
+
fallback before the :dir() rules. */
|
|
4712
5002
|
[dir="rtl"] dialog.drawer {
|
|
4713
5003
|
--drawer-closed-translate: 100%;
|
|
4714
5004
|
}
|
|
4715
5005
|
|
|
4716
|
-
|
|
5006
|
+
dialog.drawer:dir(ltr) {
|
|
4717
5007
|
--drawer-closed-translate: -100%;
|
|
4718
5008
|
}
|
|
4719
5009
|
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
dialog.drawer:dir(ltr) {
|
|
4724
|
-
--drawer-closed-translate: -100%;
|
|
4725
|
-
}
|
|
5010
|
+
dialog.drawer:dir(rtl) {
|
|
5011
|
+
--drawer-closed-translate: 100%;
|
|
5012
|
+
}
|
|
4726
5013
|
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
5014
|
+
[dir="rtl"] dialog.drawer[data-side="end"] {
|
|
5015
|
+
--drawer-closed-translate: -100%;
|
|
5016
|
+
}
|
|
4730
5017
|
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
5018
|
+
dialog.drawer[data-side="end"]:dir(ltr) {
|
|
5019
|
+
--drawer-closed-translate: 100%;
|
|
5020
|
+
}
|
|
4734
5021
|
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
}
|
|
5022
|
+
dialog.drawer[data-side="end"]:dir(rtl) {
|
|
5023
|
+
--drawer-closed-translate: -100%;
|
|
4738
5024
|
}
|
|
4739
5025
|
|
|
4740
5026
|
@media (prefers-reduced-motion: no-preference) {
|
|
@@ -4743,32 +5029,74 @@ dialog.drawer nav {
|
|
|
4743
5029
|
}
|
|
4744
5030
|
}
|
|
4745
5031
|
|
|
4746
|
-
/* The
|
|
4747
|
-
|
|
4748
|
-
inline room so the title never runs under it. */
|
|
5032
|
+
/* The header reserves inline room so the title never runs under the close
|
|
5033
|
+
control. */
|
|
4749
5034
|
dialog.drawer > header,
|
|
4750
5035
|
dialog.drawer > form > header {
|
|
4751
5036
|
padding-inline-end: calc(var(--control-size) + var(--space-30));
|
|
4752
5037
|
}
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
5038
|
+
|
|
5039
|
+
/* Close control, owned here so a drawer-only build does not depend on
|
|
5040
|
+
modal.css. Mirrors the modal's .dialog-close placement: absolute at the
|
|
5041
|
+
panel's top inline-end, out of the content flow. */
|
|
5042
|
+
.drawer-close {
|
|
5043
|
+
--drawer-close-icon-size: 1rem;
|
|
5044
|
+
|
|
5045
|
+
display: inline-grid;
|
|
5046
|
+
place-items: center;
|
|
5047
|
+
padding: 0;
|
|
5048
|
+
border: 0;
|
|
5049
|
+
border-radius: var(--radius);
|
|
5050
|
+
color: var(--text-muted);
|
|
5051
|
+
cursor: pointer;
|
|
5052
|
+
|
|
5053
|
+
position: absolute;
|
|
5054
|
+
z-index: 1;
|
|
5055
|
+
inset-block-start: var(--space-20);
|
|
5056
|
+
inset-inline-end: var(--space-20);
|
|
5057
|
+
|
|
5058
|
+
inline-size: var(--control-size);
|
|
5059
|
+
block-size: var(--control-size);
|
|
5060
|
+
|
|
5061
|
+
background: var(--surface-subtle);
|
|
5062
|
+
}
|
|
5063
|
+
|
|
5064
|
+
.drawer-close::before {
|
|
5065
|
+
content: "";
|
|
5066
|
+
inline-size: var(--drawer-close-icon-size);
|
|
5067
|
+
block-size: var(--drawer-close-icon-size);
|
|
5068
|
+
background: currentColor;
|
|
5069
|
+
-webkit-mask: var(--icon-close) center / contain no-repeat;
|
|
5070
|
+
mask: var(--icon-close) center / contain no-repeat;
|
|
5071
|
+
}
|
|
5072
|
+
|
|
5073
|
+
@media (hover: hover) {
|
|
5074
|
+
.drawer-close:hover {
|
|
5075
|
+
box-shadow: inset 0 0 0 999px var(--hover-overlay);
|
|
5076
|
+
color: var(--text);
|
|
4756
5077
|
}
|
|
4757
5078
|
}
|
|
4758
5079
|
|
|
5080
|
+
.drawer-close:active {
|
|
5081
|
+
box-shadow: inset 0 0 0 999px var(--hover-overlay);
|
|
5082
|
+
}
|
|
5083
|
+
|
|
5084
|
+
.drawer-close:focus-visible {
|
|
5085
|
+
outline: 2px solid transparent;
|
|
5086
|
+
box-shadow: var(--focus-ring-shadow);
|
|
5087
|
+
}
|
|
5088
|
+
|
|
4759
5089
|
|
|
4760
5090
|
/* Overlays */
|
|
4761
5091
|
/*
|
|
4762
5092
|
* Flyout — floating panel for actions and links.
|
|
4763
5093
|
*
|
|
4764
|
-
* JavaScript
|
|
4765
|
-
*
|
|
4766
|
-
* panel appearance, item styles,
|
|
5094
|
+
* JavaScript promotes the panel to the top layer and chooses viewport or
|
|
5095
|
+
* document coordinates from its anchor. It also owns state (.is-open), focus,
|
|
5096
|
+
* and keyboard behavior. CSS owns the surfaced panel appearance, item styles,
|
|
5097
|
+
* and animation.
|
|
4767
5098
|
*
|
|
4768
|
-
* Runtime
|
|
4769
|
-
* .is-sheet marks "auto" resolved to bottom-sheet presentation on coarse
|
|
4770
|
-
* pointers. Authors opt into mobile behavior with
|
|
4771
|
-
* [data-flyout-mobile="sheet|auto|none|anchored"].
|
|
5099
|
+
* Runtime marker (managed by surface.js): .is-open marks an open flyout.
|
|
4772
5100
|
* --surface-anchor-width is set by surface.js while an open flyout is positioned.
|
|
4773
5101
|
*
|
|
4774
5102
|
* Public hooks:
|
|
@@ -4777,7 +5105,7 @@ dialog.drawer > form > header {
|
|
|
4777
5105
|
*
|
|
4778
5106
|
* Internal:
|
|
4779
5107
|
* --menu-item-size (derived from --control-size for menu rows)
|
|
4780
|
-
* --available-height (measured by
|
|
5108
|
+
* --available-height (measured by the positioner)
|
|
4781
5109
|
* --surface-anchor-width (measured by surface.js)
|
|
4782
5110
|
*/
|
|
4783
5111
|
|
|
@@ -4804,8 +5132,8 @@ dialog.drawer > form > header {
|
|
|
4804
5132
|
--menu-item-size: var(--control-size);
|
|
4805
5133
|
z-index: var(--z-menu);
|
|
4806
5134
|
/* No-JS fallback: below the trigger inside the relative .flyout-trigger.
|
|
4807
|
-
surface.js
|
|
4808
|
-
|
|
5135
|
+
surface.js replaces this with the anchor's coupled position/coordinate
|
|
5136
|
+
mode; this only covers a flyout revealed without the runtime. */
|
|
4809
5137
|
position: absolute;
|
|
4810
5138
|
inset-block-start: calc(100% + var(--space-10));
|
|
4811
5139
|
inset-inline-start: 0;
|
|
@@ -4833,101 +5161,53 @@ dialog.drawer > form > header {
|
|
|
4833
5161
|
box-shadow: var(--shadow-popout);
|
|
4834
5162
|
list-style: none;
|
|
4835
5163
|
opacity: 1;
|
|
4836
|
-
/*
|
|
4837
|
-
|
|
4838
|
-
below adds its own translate motion. */
|
|
5164
|
+
/* Flyouts fade in and out; scaling around the center would detach the
|
|
5165
|
+
anchored edge from its trigger on wide panels. */
|
|
4839
5166
|
transition: opacity var(--duration-fast) ease;
|
|
4840
5167
|
}
|
|
4841
5168
|
|
|
4842
|
-
/*
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
reset would strand a permanently visible panel at inset: auto. Popover is
|
|
4847
|
-
not part of Actual's Minimal contract — engines inside the Minimal range
|
|
4848
|
-
may lack it — so the gate is about the capability, not the floor. Kept
|
|
4849
|
-
scoped to Actual surfaces so bare [popover] elements retain their platform
|
|
5169
|
+
/* A popover enters the top layer with UA geometry that conflicts with anchored
|
|
5170
|
+
positioning. surface.js sets popover="manual" on every surface it manages,
|
|
5171
|
+
so this is the transport's own geometry rather than an opt-in. Kept scoped to
|
|
5172
|
+
Actual surfaces so bare [popover] elements retain their platform
|
|
4850
5173
|
presentation. */
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
margin: 0;
|
|
4856
|
-
}
|
|
4857
|
-
|
|
4858
|
-
/* A presentation class may own its *visible* display type, but must not
|
|
4859
|
-
override the platform's hidden state. The UA closes a popover with
|
|
4860
|
-
display: none, and an author declaration beats the UA origin whatever the
|
|
4861
|
-
specificity — so .flyout's own display: grid would leave a closed popover
|
|
4862
|
-
painted at its static position. Restoring both properties here also gives
|
|
4863
|
-
the native path the exit target the runtime path already has in
|
|
4864
|
-
.flyout[hidden]: without an opacity to animate, the overlay/display
|
|
4865
|
-
allow-discrete transition below would hold a fully opaque panel in the top
|
|
4866
|
-
layer and then snap it away. */
|
|
4867
|
-
.flyout[popover]:not(:popover-open) {
|
|
4868
|
-
display: none;
|
|
4869
|
-
opacity: 0;
|
|
4870
|
-
}
|
|
5174
|
+
.flyout[popover] {
|
|
5175
|
+
position: fixed;
|
|
5176
|
+
inset: auto;
|
|
5177
|
+
margin: 0;
|
|
4871
5178
|
}
|
|
4872
5179
|
|
|
4873
|
-
/*
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
5180
|
+
/* A presentation class may own its *visible* display type, but must not
|
|
5181
|
+
override the platform's hidden state. The UA closes a popover with
|
|
5182
|
+
display: none, and an author declaration beats the UA origin whatever the
|
|
5183
|
+
specificity — so .flyout's own display: grid would leave a closed popover
|
|
5184
|
+
painted at its static position. The opacity is the exit target: with nothing
|
|
5185
|
+
to animate, the overlay/display allow-discrete transition below would hold a
|
|
5186
|
+
fully opaque panel in the top layer and then snap it away. */
|
|
5187
|
+
.flyout[popover]:not(:popover-open) {
|
|
5188
|
+
display: none;
|
|
5189
|
+
opacity: 0;
|
|
5190
|
+
}
|
|
5191
|
+
|
|
5192
|
+
/* Both lifecycles in one rule: .is-open for a surface.js panel, :popover-open
|
|
5193
|
+
for one an author drives natively with popovertarget. */
|
|
5194
|
+
.flyout:is(.is-open, :popover-open) {
|
|
4879
5195
|
inset: auto;
|
|
4880
5196
|
}
|
|
4881
5197
|
|
|
4882
|
-
/*
|
|
4883
|
-
|
|
5198
|
+
/* @starting-style is above the floor (Safari 17.5), so where it is missing the
|
|
5199
|
+
entry simply has no animated origin and the panel appears at full opacity. */
|
|
4884
5200
|
@starting-style {
|
|
4885
5201
|
.flyout:is(.is-open, :popover-open) {
|
|
4886
5202
|
opacity: 0;
|
|
4887
5203
|
}
|
|
4888
5204
|
}
|
|
4889
5205
|
|
|
4890
|
-
/* Bottom-sheet presentation. surface.js toggles .is-sheet when the flyout is
|
|
4891
|
-
opened on coarse pointer / narrow viewports. */
|
|
4892
|
-
.flyout.is-sheet {
|
|
4893
|
-
--menu-item-size: var(--control-size-lg);
|
|
4894
|
-
position: fixed;
|
|
4895
|
-
inset-inline: 0;
|
|
4896
|
-
inset-block-start: auto;
|
|
4897
|
-
inset-block-end: 0;
|
|
4898
|
-
inline-size: auto;
|
|
4899
|
-
min-inline-size: 0;
|
|
4900
|
-
max-inline-size: none;
|
|
4901
|
-
max-block-size: min(calc(var(--viewport-block) * 0.75), 32rem);
|
|
4902
|
-
overflow: auto;
|
|
4903
|
-
grid-template-columns: 1fr;
|
|
4904
|
-
padding: max(var(--space-30), env(safe-area-inset-top)) var(--space-30)
|
|
4905
|
-
max(var(--space-30), env(safe-area-inset-bottom));
|
|
4906
|
-
border-end-start-radius: 0;
|
|
4907
|
-
border-end-end-radius: 0;
|
|
4908
|
-
/* Sheet motion is a directional slide from the viewport edge; it replaces
|
|
4909
|
-
the base fade-only recipe. */
|
|
4910
|
-
transition:
|
|
4911
|
-
opacity var(--duration-fast) ease,
|
|
4912
|
-
translate var(--duration-fast) ease;
|
|
4913
|
-
}
|
|
4914
|
-
|
|
4915
5206
|
.flyout[hidden] {
|
|
4916
5207
|
display: none;
|
|
4917
5208
|
opacity: 0;
|
|
4918
5209
|
}
|
|
4919
5210
|
|
|
4920
|
-
.flyout.is-sheet[hidden] {
|
|
4921
|
-
translate: 0 var(--space-30);
|
|
4922
|
-
}
|
|
4923
|
-
|
|
4924
|
-
/* compat-ok: forgiving :is() inside @starting-style, as above. */
|
|
4925
|
-
@starting-style {
|
|
4926
|
-
.flyout.is-sheet:is(.is-open, :popover-open) {
|
|
4927
|
-
translate: 0 var(--space-30);
|
|
4928
|
-
}
|
|
4929
|
-
}
|
|
4930
|
-
|
|
4931
5211
|
/* display keeps the exit transition alive for the [hidden] path; overlay does
|
|
4932
5212
|
the same for the native popover path, where hidePopover() would otherwise
|
|
4933
5213
|
drop the panel out of the top layer on the first frame and let a parent
|
|
@@ -4940,14 +5220,6 @@ dialog.drawer > form > header {
|
|
|
4940
5220
|
overlay var(--duration-fast) allow-discrete,
|
|
4941
5221
|
display var(--duration-fast) allow-discrete;
|
|
4942
5222
|
}
|
|
4943
|
-
|
|
4944
|
-
.flyout.is-sheet {
|
|
4945
|
-
transition:
|
|
4946
|
-
opacity var(--duration-fast) ease,
|
|
4947
|
-
translate var(--duration-fast) ease,
|
|
4948
|
-
overlay var(--duration-fast) allow-discrete,
|
|
4949
|
-
display var(--duration-fast) allow-discrete;
|
|
4950
|
-
}
|
|
4951
5223
|
}
|
|
4952
5224
|
|
|
4953
5225
|
.flyout > li {
|
|
@@ -4963,7 +5235,7 @@ dialog.drawer > form > header {
|
|
|
4963
5235
|
margin: 0;
|
|
4964
5236
|
color: var(--text-muted);
|
|
4965
5237
|
font-size: var(--font-size-xs);
|
|
4966
|
-
font-weight: var(--font-weight-
|
|
5238
|
+
font-weight: var(--font-weight-semibold);
|
|
4967
5239
|
}
|
|
4968
5240
|
|
|
4969
5241
|
.flyout > section > ul {
|
|
@@ -4981,11 +5253,6 @@ dialog.drawer > form > header {
|
|
|
4981
5253
|
/* Vertical dividers — .menu-separator (menu.css) is the canonical way to
|
|
4982
5254
|
separate menu items. Flyout panels that are not menus use auto grid gap
|
|
4983
5255
|
declared on .flyout itself. */
|
|
4984
|
-
@media print {
|
|
4985
|
-
.flyout {
|
|
4986
|
-
display: none !important;
|
|
4987
|
-
}
|
|
4988
|
-
}
|
|
4989
5256
|
|
|
4990
5257
|
/*
|
|
4991
5258
|
* Menu — action list primitive consumed by flyout and context-menu.
|
|
@@ -5033,6 +5300,7 @@ dialog.drawer > form > header {
|
|
|
5033
5300
|
background: transparent;
|
|
5034
5301
|
color: var(--intent, var(--text));
|
|
5035
5302
|
font: inherit;
|
|
5303
|
+
font-size: var(--control-font-size);
|
|
5036
5304
|
text-align: start;
|
|
5037
5305
|
text-decoration: none;
|
|
5038
5306
|
white-space: nowrap;
|
|
@@ -5113,7 +5381,7 @@ dialog.drawer > form > header {
|
|
|
5113
5381
|
padding: var(--space-20);
|
|
5114
5382
|
color: var(--text-muted);
|
|
5115
5383
|
font-size: var(--font-size-xs);
|
|
5116
|
-
font-weight: var(--font-weight-
|
|
5384
|
+
font-weight: var(--font-weight-semibold);
|
|
5117
5385
|
}
|
|
5118
5386
|
|
|
5119
5387
|
.menu-separator {
|
|
@@ -5125,9 +5393,13 @@ dialog.drawer > form > header {
|
|
|
5125
5393
|
/*
|
|
5126
5394
|
* Tooltip — supplemental label overlaid near a trigger.
|
|
5127
5395
|
*
|
|
5128
|
-
* Position, placement, and arrow position are managed by JavaScript
|
|
5129
|
-
*
|
|
5130
|
-
*
|
|
5396
|
+
* Position, placement, and arrow position are managed by JavaScript via left,
|
|
5397
|
+
* top, data-placement, and --arrow-x. The runtime also owns `position`: it
|
|
5398
|
+
* writes `absolute` for a tip whose trigger scrolls with the page, so the
|
|
5399
|
+
* browser carries the tip along instead of the positioner correcting it a
|
|
5400
|
+
* frame later, and `fixed` — the declaration below — for every other trigger.
|
|
5401
|
+
* Visibility is the Popover lifecycle: tooltip.js promotes the tip to the top
|
|
5402
|
+
* layer with popover="manual". CSS owns entry and exit animation.
|
|
5131
5403
|
*
|
|
5132
5404
|
* Public hooks:
|
|
5133
5405
|
* --tooltip-bg
|
|
@@ -5135,8 +5407,8 @@ dialog.drawer > form > header {
|
|
|
5135
5407
|
* --tooltip-arrow-size
|
|
5136
5408
|
*
|
|
5137
5409
|
* Internal:
|
|
5138
|
-
* --arrow-x (arrow position, written by
|
|
5139
|
-
* --arrow-y (arrow position, written by
|
|
5410
|
+
* --arrow-x (arrow position, written by the positioner)
|
|
5411
|
+
* --arrow-y (arrow position, written by the positioner)
|
|
5140
5412
|
*/
|
|
5141
5413
|
.tooltip {
|
|
5142
5414
|
--tooltip-bg: var(--surface-solid);
|
|
@@ -5144,7 +5416,6 @@ dialog.drawer > form > header {
|
|
|
5144
5416
|
--tooltip-arrow-size: 5px;
|
|
5145
5417
|
|
|
5146
5418
|
position: fixed;
|
|
5147
|
-
z-index: var(--z-tooltip);
|
|
5148
5419
|
max-inline-size: min(24rem, calc(var(--viewport-inline) - 2rem));
|
|
5149
5420
|
padding: 0.25em 0.6em;
|
|
5150
5421
|
border-radius: var(--radius-sm);
|
|
@@ -5165,43 +5436,42 @@ dialog.drawer > form > header {
|
|
|
5165
5436
|
|
|
5166
5437
|
/* Neutralize only the native geometry that conflicts with a positioned
|
|
5167
5438
|
tooltip. In particular, the UA overflow would turn the arrow into scroll
|
|
5168
|
-
overflow instead of letting it project beyond the bubble.
|
|
5169
|
-
Popover support: the attribute selector matches whether or not the engine
|
|
5170
|
-
implements Popover, and without it nothing hides the element, so an ungated
|
|
5171
|
-
reset would leave a stray bubble parked at inset: auto. Popover sits outside
|
|
5172
|
-
Actual's Minimal contract, so the gate tracks the capability rather than the
|
|
5173
|
-
floor. */
|
|
5174
|
-
@supports selector(:popover-open) {
|
|
5175
|
-
.tooltip[popover] {
|
|
5176
|
-
inset: auto;
|
|
5177
|
-
margin: 0;
|
|
5178
|
-
overflow: visible;
|
|
5179
|
-
}
|
|
5439
|
+
overflow instead of letting it project beyond the bubble.
|
|
5180
5440
|
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5441
|
+
One rule for both lifecycles: the runtime writes popover="manual" on the
|
|
5442
|
+
tips it manages, and an application driving a .tooltip[popover] itself lands
|
|
5443
|
+
on the same declarations. Who owns which is in
|
|
5444
|
+
docs/pages/enhancements/overview.md. */
|
|
5445
|
+
.tooltip[popover] {
|
|
5446
|
+
inset: auto;
|
|
5447
|
+
margin: 0;
|
|
5448
|
+
overflow: visible;
|
|
5449
|
+
}
|
|
5450
|
+
|
|
5451
|
+
/* The closed state, for either lifecycle. .tooltip declares no display of its
|
|
5452
|
+
own so the UA would already hide it, but the opacity target is what makes
|
|
5453
|
+
the exit fade at all — see the overlay note below. */
|
|
5454
|
+
.tooltip[popover]:not(:popover-open) {
|
|
5455
|
+
display: none;
|
|
5456
|
+
opacity: 0;
|
|
5190
5457
|
}
|
|
5191
5458
|
|
|
5459
|
+
/* The state before enhancement, and the author's to write — not the runtime's.
|
|
5460
|
+
Wiring is lazy: tooltip.js adopts an explicit tip on its trigger's first
|
|
5461
|
+
hover, so a tooltip nobody hovers keeps [hidden] for the whole life of the
|
|
5462
|
+
page. Without this rule it would sit on screen at full opacity until then.
|
|
5463
|
+
The runtime removes the attribute at the moment it takes the lifecycle over,
|
|
5464
|
+
because an author declaration outranks the platform's own hidden state. */
|
|
5192
5465
|
.tooltip[hidden] {
|
|
5193
5466
|
display: none;
|
|
5194
5467
|
opacity: 0;
|
|
5195
5468
|
}
|
|
5196
5469
|
|
|
5197
|
-
/* Entry opacity for
|
|
5198
|
-
is
|
|
5199
|
-
|
|
5200
|
-
evaluates correctly whether or not it implements Popover, :popover-open
|
|
5201
|
-
rides in a forgiving :is(), and @starting-style is itself unsupported across
|
|
5202
|
-
part of the Minimal range — so the rule does not exist there at all. */
|
|
5470
|
+
/* Entry opacity, for either lifecycle — both are :popover-open.
|
|
5471
|
+
@starting-style is above the floor (Safari 17.5); where it is missing the
|
|
5472
|
+
tip simply appears at full opacity. */
|
|
5203
5473
|
@starting-style {
|
|
5204
|
-
.tooltip:
|
|
5474
|
+
.tooltip:popover-open {
|
|
5205
5475
|
opacity: 0;
|
|
5206
5476
|
}
|
|
5207
5477
|
}
|
|
@@ -5260,12 +5530,6 @@ dialog.drawer > form > header {
|
|
|
5260
5530
|
clip-path: polygon(0 50%, 100% 0, 100% 100%);
|
|
5261
5531
|
}
|
|
5262
5532
|
|
|
5263
|
-
@media print {
|
|
5264
|
-
.tooltip {
|
|
5265
|
-
display: none !important;
|
|
5266
|
-
}
|
|
5267
|
-
}
|
|
5268
|
-
|
|
5269
5533
|
|
|
5270
5534
|
/* Disclosure */
|
|
5271
5535
|
/*
|
|
@@ -5275,6 +5539,12 @@ dialog.drawer > form > header {
|
|
|
5275
5539
|
* or demo styles. Decorative palettes and custom markers belong outside the
|
|
5276
5540
|
* base component.
|
|
5277
5541
|
*
|
|
5542
|
+
* Container treatments (structural chrome, not skins):
|
|
5543
|
+
* - default — the common raised shell;
|
|
5544
|
+
* - .flush — the shell removed, only the item separators remain, so the group
|
|
5545
|
+
* can sit directly inside an existing surface (card, drawer, page section);
|
|
5546
|
+
* - .separated — each item becomes its own card.
|
|
5547
|
+
*
|
|
5278
5548
|
* Public hooks:
|
|
5279
5549
|
* --accordion-radius
|
|
5280
5550
|
* --accordion-marker-color
|
|
@@ -5300,11 +5570,14 @@ dialog.drawer > form > header {
|
|
|
5300
5570
|
list-style: none;
|
|
5301
5571
|
padding: var(--space-40);
|
|
5302
5572
|
color: var(--text);
|
|
5303
|
-
font-weight: var(--font-weight-
|
|
5573
|
+
font-weight: var(--font-weight-medium);
|
|
5304
5574
|
}
|
|
5305
5575
|
|
|
5306
|
-
.accordion summary:hover {
|
|
5307
|
-
|
|
5576
|
+
.accordion summary:hover::after {
|
|
5577
|
+
/* The whole row is one focus target; reinforcing only the end marker keeps
|
|
5578
|
+
the affordance on the disclosure control instead of making the whole
|
|
5579
|
+
label read as a link. */
|
|
5580
|
+
--accordion-marker-color: currentColor;
|
|
5308
5581
|
}
|
|
5309
5582
|
|
|
5310
5583
|
.accordion summary::-webkit-details-marker {
|
|
@@ -5348,6 +5621,36 @@ dialog.drawer > form > header {
|
|
|
5348
5621
|
padding-inline-start: 2rem;
|
|
5349
5622
|
}
|
|
5350
5623
|
|
|
5624
|
+
/* Flush — the shell is dropped and only the shared item separators remain.
|
|
5625
|
+
Zero inline padding puts the group edge to edge inside the surface that
|
|
5626
|
+
already frames it, so no override is repeated per embedding. */
|
|
5627
|
+
.accordion.flush {
|
|
5628
|
+
border: 0;
|
|
5629
|
+
border-radius: 0;
|
|
5630
|
+
background: transparent;
|
|
5631
|
+
}
|
|
5632
|
+
|
|
5633
|
+
.accordion.flush summary,
|
|
5634
|
+
.accordion.flush details > summary + * {
|
|
5635
|
+
padding-inline: 0;
|
|
5636
|
+
}
|
|
5637
|
+
|
|
5638
|
+
/* Separated — each item owns a full card. The item's own border beats the
|
|
5639
|
+
shared details + details separator on specificity alone, so nothing needs
|
|
5640
|
+
undoing for the second item onwards. */
|
|
5641
|
+
.accordion.separated {
|
|
5642
|
+
display: grid;
|
|
5643
|
+
gap: var(--space-20);
|
|
5644
|
+
border: 0;
|
|
5645
|
+
background: transparent;
|
|
5646
|
+
}
|
|
5647
|
+
|
|
5648
|
+
.accordion.separated > details {
|
|
5649
|
+
border: var(--border-width) solid var(--border);
|
|
5650
|
+
border-radius: var(--accordion-radius);
|
|
5651
|
+
background: var(--surface-raised);
|
|
5652
|
+
}
|
|
5653
|
+
|
|
5351
5654
|
@media (forced-colors: active) {
|
|
5352
5655
|
.accordion {
|
|
5353
5656
|
--accordion-marker-color: CanvasText;
|
|
@@ -5389,7 +5692,6 @@ dialog.drawer > form > header {
|
|
|
5389
5692
|
border-inline-end-color: var(--spinner-value);
|
|
5390
5693
|
border-radius: var(--radius-full);
|
|
5391
5694
|
opacity: 0.85;
|
|
5392
|
-
animation: actual-spin var(--duration-spin) linear infinite;
|
|
5393
5695
|
vertical-align: text-bottom;
|
|
5394
5696
|
}
|
|
5395
5697
|
|
|
@@ -5410,16 +5712,17 @@ dialog.drawer > form > header {
|
|
|
5410
5712
|
--spinner-width: 3px;
|
|
5411
5713
|
}
|
|
5412
5714
|
|
|
5715
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
5716
|
+
.spinner {
|
|
5717
|
+
animation: actual-spin var(--duration-spin) linear infinite;
|
|
5718
|
+
}
|
|
5719
|
+
}
|
|
5720
|
+
|
|
5413
5721
|
@keyframes actual-spin {
|
|
5414
5722
|
to {
|
|
5415
5723
|
transform: rotate(360deg);
|
|
5416
5724
|
}
|
|
5417
5725
|
}
|
|
5418
|
-
@media print {
|
|
5419
|
-
.spinner {
|
|
5420
|
-
display: none !important;
|
|
5421
|
-
}
|
|
5422
|
-
}
|
|
5423
5726
|
|
|
5424
5727
|
/*
|
|
5425
5728
|
* Skeleton placeholders for loading states.
|
|
@@ -5434,16 +5737,24 @@ dialog.drawer > form > header {
|
|
|
5434
5737
|
* --skeleton-track
|
|
5435
5738
|
* --skeleton-highlight
|
|
5436
5739
|
*/
|
|
5740
|
+
/* A placeholder fills its container up to --skeleton-width; the preset widths
|
|
5741
|
+
are a cap, never an intrinsic size. Stated the other way round — a rem
|
|
5742
|
+
`inline-size` capped by a percentage `max-inline-size` — the cap is ignored
|
|
5743
|
+
during intrinsic sizing, so every shrink-to-fit ancestor inherits the
|
|
5744
|
+
preset as a min-content floor: a card holding a title placeholder could not
|
|
5745
|
+
go below 18rem and pushed a phone's layout sideways. A percentage
|
|
5746
|
+
`inline-size` contributes nothing intrinsically and a rem cap only lowers
|
|
5747
|
+
the contribution, so the same geometry costs no floor. */
|
|
5437
5748
|
.skeleton {
|
|
5438
5749
|
--skeleton-size: 1rem;
|
|
5439
|
-
--skeleton-width:
|
|
5750
|
+
--skeleton-width: none;
|
|
5440
5751
|
--skeleton-radius: var(--radius);
|
|
5441
5752
|
--skeleton-track: var(--surface-subtle);
|
|
5442
5753
|
--skeleton-highlight: var(--surface-raised);
|
|
5443
5754
|
|
|
5444
5755
|
display: block;
|
|
5445
|
-
inline-size:
|
|
5446
|
-
max-inline-size:
|
|
5756
|
+
inline-size: 100%;
|
|
5757
|
+
max-inline-size: var(--skeleton-width);
|
|
5447
5758
|
block-size: var(--skeleton-size);
|
|
5448
5759
|
border-radius: var(--skeleton-radius);
|
|
5449
5760
|
background:
|
|
@@ -5467,11 +5778,14 @@ dialog.drawer > form > header {
|
|
|
5467
5778
|
--skeleton-width: 18rem;
|
|
5468
5779
|
}
|
|
5469
5780
|
|
|
5781
|
+
/* The one preset with a real intrinsic width: a disc is square, so it sizes
|
|
5782
|
+
rather than caps. The floor it does carry is the avatar size, which is small
|
|
5783
|
+
enough never to widen a layout. */
|
|
5470
5784
|
.skeleton[data-shape="avatar"] {
|
|
5471
5785
|
--skeleton-size: var(--avatar-size, 2.25rem);
|
|
5472
|
-
--skeleton-width: var(--skeleton-size);
|
|
5473
5786
|
--skeleton-radius: var(--radius-full);
|
|
5474
5787
|
|
|
5788
|
+
inline-size: var(--skeleton-size);
|
|
5475
5789
|
flex: 0 0 auto;
|
|
5476
5790
|
}
|
|
5477
5791
|
|
|
@@ -5750,11 +6064,6 @@ dialog.drawer > form > header {
|
|
|
5750
6064
|
translate var(--duration-slow) var(--ease-enter),
|
|
5751
6065
|
background-color var(--duration) ease;
|
|
5752
6066
|
}
|
|
5753
|
-
@media print {
|
|
5754
|
-
.status-bar {
|
|
5755
|
-
display: none !important;
|
|
5756
|
-
}
|
|
5757
|
-
}
|
|
5758
6067
|
|
|
5759
6068
|
|
|
5760
6069
|
/* Display */
|
|
@@ -5806,7 +6115,7 @@ dialog.drawer > form > header {
|
|
|
5806
6115
|
color: var(--avatar-fg);
|
|
5807
6116
|
font: inherit;
|
|
5808
6117
|
font-size: var(--avatar-font-size);
|
|
5809
|
-
font-weight: var(--font-weight-
|
|
6118
|
+
font-weight: var(--font-weight-semibold);
|
|
5810
6119
|
line-height: 1;
|
|
5811
6120
|
text-decoration: none;
|
|
5812
6121
|
vertical-align: middle;
|
|
@@ -5920,6 +6229,18 @@ a.avatar[aria-disabled="true"] {
|
|
|
5920
6229
|
transform: translate(var(--avatar-status-translate-x), 50%);
|
|
5921
6230
|
}
|
|
5922
6231
|
|
|
6232
|
+
/* Safari before 16.4 has no :dir(); the ancestor-attribute path is the
|
|
6233
|
+
Degraded-tier fallback and the :dir() rules are dropped on their own there.
|
|
6234
|
+
:dir() follows inherited directionality, so the explicit ltr rule
|
|
6235
|
+
neutralizes an outer RTL fallback for a nested direction change. */
|
|
6236
|
+
[dir="rtl"] .avatar > .badge:empty {
|
|
6237
|
+
--avatar-status-translate-x: -50%;
|
|
6238
|
+
}
|
|
6239
|
+
|
|
6240
|
+
.avatar:dir(ltr) > .badge:empty {
|
|
6241
|
+
--avatar-status-translate-x: 50%;
|
|
6242
|
+
}
|
|
6243
|
+
|
|
5923
6244
|
.avatar:dir(rtl) > .badge:empty {
|
|
5924
6245
|
--avatar-status-translate-x: -50%;
|
|
5925
6246
|
}
|
|
@@ -6008,10 +6329,31 @@ a.avatar[aria-disabled="true"] {
|
|
|
6008
6329
|
inset-block-end: 0;
|
|
6009
6330
|
}
|
|
6010
6331
|
|
|
6332
|
+
/* Safari before 16.4 has no :dir(); the ancestor-attribute path is the
|
|
6333
|
+
Degraded-tier fallback and the :dir() rules are dropped on their own there.
|
|
6334
|
+
:dir() follows inherited directionality, so the explicit ltr rules
|
|
6335
|
+
neutralize an outer RTL fallback for a nested direction change. The rules
|
|
6336
|
+
stay grouped by specificity so the fallback never follows the :dir() rules. */
|
|
6337
|
+
[dir="rtl"] .indicator-item {
|
|
6338
|
+
--indicator-translate-x: -50%;
|
|
6339
|
+
}
|
|
6340
|
+
|
|
6341
|
+
.indicator-item:dir(ltr) {
|
|
6342
|
+
--indicator-translate-x: 50%;
|
|
6343
|
+
}
|
|
6344
|
+
|
|
6011
6345
|
.indicator-item:dir(rtl) {
|
|
6012
6346
|
--indicator-translate-x: -50%;
|
|
6013
6347
|
}
|
|
6014
6348
|
|
|
6349
|
+
[dir="rtl"] .indicator-item.start {
|
|
6350
|
+
--indicator-translate-x: 50%;
|
|
6351
|
+
}
|
|
6352
|
+
|
|
6353
|
+
.indicator-item.start:dir(ltr) {
|
|
6354
|
+
--indicator-translate-x: -50%;
|
|
6355
|
+
}
|
|
6356
|
+
|
|
6015
6357
|
.indicator-item.start:dir(rtl) {
|
|
6016
6358
|
--indicator-translate-x: 50%;
|
|
6017
6359
|
}
|
|
@@ -6046,6 +6388,16 @@ a.avatar[aria-disabled="true"] {
|
|
|
6046
6388
|
}
|
|
6047
6389
|
/* @sync key-recipe */
|
|
6048
6390
|
|
|
6391
|
+
/* .key has a compact typographic default of its own, so its local size roles
|
|
6392
|
+
step around that default instead of inheriting the generic mapping. */
|
|
6393
|
+
.key.sm {
|
|
6394
|
+
font-size: var(--font-size-xs);
|
|
6395
|
+
}
|
|
6396
|
+
|
|
6397
|
+
.key.lg {
|
|
6398
|
+
font-size: var(--font-size-md);
|
|
6399
|
+
}
|
|
6400
|
+
|
|
6049
6401
|
/*
|
|
6050
6402
|
* Message rows composed from an optional avatar, metadata, and bubble.
|
|
6051
6403
|
*
|
|
@@ -6110,6 +6462,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
6110
6462
|
:where(.chat-bubble) {
|
|
6111
6463
|
--intent: initial;
|
|
6112
6464
|
--intent-fg: initial;
|
|
6465
|
+
--intent-soft-fg: initial;
|
|
6113
6466
|
--ui-bg: initial;
|
|
6114
6467
|
--ui-fg: initial;
|
|
6115
6468
|
--ui-border: initial;
|
|
@@ -6275,12 +6628,6 @@ a.avatar[aria-disabled="true"] {
|
|
|
6275
6628
|
transform: none;
|
|
6276
6629
|
}
|
|
6277
6630
|
|
|
6278
|
-
@media print {
|
|
6279
|
-
.fab {
|
|
6280
|
-
display: none;
|
|
6281
|
-
}
|
|
6282
|
-
}
|
|
6283
|
-
|
|
6284
6631
|
/*
|
|
6285
6632
|
* Rating — integer star rating drawn on native radio inputs.
|
|
6286
6633
|
*
|
|
@@ -6393,23 +6740,16 @@ a.avatar[aria-disabled="true"] {
|
|
|
6393
6740
|
*
|
|
6394
6741
|
* `.steps` is what a step *is* — the counter, the marker, `.step-label`,
|
|
6395
6742
|
* `.step-complete`, `aria-current="step"`. It carries no orientation, so it is
|
|
6396
|
-
* never written alone.
|
|
6397
|
-
*
|
|
6398
|
-
* has to exclude, so no rule says `:not(...)` and no rule undoes another's
|
|
6399
|
-
* layout.
|
|
6743
|
+
* never written alone. The two orientations are peers: no rule says
|
|
6744
|
+
* `:not(...)` and no rule undoes another's layout.
|
|
6400
6745
|
*
|
|
6401
|
-
* Orientation and labels are independent
|
|
6402
|
-
*
|
|
6403
|
-
* one:
|
|
6746
|
+
* Orientation and labels are independent. `.step-label` is optional in either;
|
|
6747
|
+
* when a sequence carries names, every step carries one:
|
|
6404
6748
|
*
|
|
6405
6749
|
* empty steps .step-label on every step
|
|
6406
6750
|
* horizontal compact progression named sequence, stacked or inline
|
|
6407
6751
|
* vertical compact rail named sequence, labels beside markers
|
|
6408
6752
|
*
|
|
6409
|
-
* An empty step is a marker and nothing else, so it is compact by structure —
|
|
6410
|
-
* `> li:empty` in both orientations, no variant class, and nothing authored
|
|
6411
|
-
* only to be hidden again. The page around it owns the step name.
|
|
6412
|
-
*
|
|
6413
6753
|
* A label may be an `a[href]`, which is the whole interactive surface: the
|
|
6414
6754
|
* marker is never navigable, so navigating a flow means naming its
|
|
6415
6755
|
* destinations. Anything else focusable in a step is out of contract.
|
|
@@ -6480,7 +6820,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
6480
6820
|
background: var(--step-marker-bg);
|
|
6481
6821
|
color: var(--step-marker-fg);
|
|
6482
6822
|
font-size: var(--font-size-sm);
|
|
6483
|
-
font-weight: var(--font-weight-
|
|
6823
|
+
font-weight: var(--font-weight-semibold);
|
|
6484
6824
|
line-height: 1;
|
|
6485
6825
|
}
|
|
6486
6826
|
|
|
@@ -6540,11 +6880,9 @@ a.avatar[aria-disabled="true"] {
|
|
|
6540
6880
|
border-width: calc(var(--border-width) * 2);
|
|
6541
6881
|
}
|
|
6542
6882
|
|
|
6543
|
-
/*
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
font-weight: var(--font-weight-strong);
|
|
6547
|
-
}
|
|
6883
|
+
/* No label weight change for current: the accented marker with its doubled
|
|
6884
|
+
border already carries the state, and selection never changes text metrics.
|
|
6885
|
+
Complete needs none either: its filled disc already reads as strongly. */
|
|
6548
6886
|
|
|
6549
6887
|
.steps > .step-complete:not([aria-current="step"])::before {
|
|
6550
6888
|
content: var(--step-complete-mark, counter(actual-step));
|
|
@@ -6602,8 +6940,8 @@ a.avatar[aria-disabled="true"] {
|
|
|
6602
6940
|
`:empty` says exactly what the markup says, the way `.badge:empty` does, and
|
|
6603
6941
|
it reads the item's own content: bare text in an `<li>` keeps a reading width
|
|
6604
6942
|
like its wrapped peers, where `> li:has(.step-label)` would give it a marker
|
|
6605
|
-
floor and skew the row.
|
|
6606
|
-
|
|
6943
|
+
floor and skew the row. `:empty` also keeps the compact form reachable on the
|
|
6944
|
+
Degraded tier, where only the inline bonus needs `:has()`. */
|
|
6607
6945
|
.steps-horizontal > li:empty {
|
|
6608
6946
|
flex: 1 1 0;
|
|
6609
6947
|
justify-items: start;
|
|
@@ -6634,48 +6972,43 @@ a.avatar[aria-disabled="true"] {
|
|
|
6634
6972
|
}
|
|
6635
6973
|
|
|
6636
6974
|
/*
|
|
6637
|
-
* The one horizontal container query
|
|
6638
|
-
*
|
|
6639
|
-
*
|
|
6640
|
-
*
|
|
6641
|
-
*
|
|
6642
|
-
*
|
|
6643
|
-
*
|
|
6644
|
-
* Horizontal is a 2-5 step component. Outside that domain the rule simply
|
|
6645
|
-
* applies as written, because a row that is out of contract is not owed a
|
|
6646
|
-
* curated fallback.
|
|
6975
|
+
* The one horizontal container query: past 60rem a labelled row can put marker
|
|
6976
|
+
* and label inline and still leave the connector a real region between groups.
|
|
6977
|
+
* It counts nothing, so it serves every count; below it, stacked. A marker row
|
|
6978
|
+
* is already the shape this query produces and stays out of it.
|
|
6979
|
+
*
|
|
6980
|
+
* Horizontal is a 2-5 step component. Outside that domain the rule applies as
|
|
6981
|
+
* written — a row out of contract is not owed a curated fallback.
|
|
6647
6982
|
*/
|
|
6648
|
-
@
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
}
|
|
6983
|
+
@container actual-container (inline-size >= 60rem) {
|
|
6984
|
+
.steps-horizontal:has(.step-label) > li {
|
|
6985
|
+
display: flex;
|
|
6986
|
+
flex: 1 1 auto;
|
|
6987
|
+
align-items: center;
|
|
6988
|
+
gap: var(--step-inline-gap);
|
|
6989
|
+
min-inline-size: 0;
|
|
6990
|
+
text-align: start;
|
|
6991
|
+
white-space: nowrap;
|
|
6992
|
+
}
|
|
6659
6993
|
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6994
|
+
.steps-horizontal:has(.step-label) > li::after {
|
|
6995
|
+
position: static;
|
|
6996
|
+
flex: 1 1 2rem;
|
|
6997
|
+
inline-size: auto;
|
|
6998
|
+
block-size: var(--step-size);
|
|
6999
|
+
margin-inline-end: var(--step-inline-gap);
|
|
7000
|
+
background: var(
|
|
7001
|
+
--step-inline-connector,
|
|
7002
|
+
var(
|
|
7003
|
+
--step-connector,
|
|
7004
|
+
linear-gradient(var(--step-line), var(--step-line)) center /
|
|
7005
|
+
100% var(--step-line-size) no-repeat
|
|
7006
|
+
)
|
|
7007
|
+
);
|
|
7008
|
+
}
|
|
6675
7009
|
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
}
|
|
7010
|
+
.steps-horizontal:has(.step-label) > li:last-child {
|
|
7011
|
+
flex: 0 0 auto;
|
|
6679
7012
|
}
|
|
6680
7013
|
}
|
|
6681
7014
|
|
|
@@ -6751,8 +7084,12 @@ a.avatar[aria-disabled="true"] {
|
|
|
6751
7084
|
* Public hooks:
|
|
6752
7085
|
* --app-nav-size
|
|
6753
7086
|
* --app-nav-gap
|
|
7087
|
+
* --app-nav-icon-size
|
|
6754
7088
|
*/
|
|
6755
7089
|
:where(.app-nav) {
|
|
7090
|
+
--intent: initial;
|
|
7091
|
+
--intent-fg: initial;
|
|
7092
|
+
--intent-soft-fg: initial;
|
|
6756
7093
|
--ui-bg: initial;
|
|
6757
7094
|
--ui-fg: initial;
|
|
6758
7095
|
--ui-border: initial;
|
|
@@ -6761,6 +7098,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
6761
7098
|
.app-nav {
|
|
6762
7099
|
--app-nav-size: 4rem;
|
|
6763
7100
|
--app-nav-gap: var(--space-10);
|
|
7101
|
+
--app-nav-icon-size: 1.5rem;
|
|
6764
7102
|
|
|
6765
7103
|
position: sticky;
|
|
6766
7104
|
inset-block-end: 0;
|
|
@@ -6797,9 +7135,9 @@ a.avatar[aria-disabled="true"] {
|
|
|
6797
7135
|
}
|
|
6798
7136
|
|
|
6799
7137
|
.app-nav > a > :where(svg, img, [aria-hidden="true"]) {
|
|
6800
|
-
inline-size:
|
|
6801
|
-
block-size:
|
|
6802
|
-
font-size:
|
|
7138
|
+
inline-size: var(--app-nav-icon-size);
|
|
7139
|
+
block-size: var(--app-nav-icon-size);
|
|
7140
|
+
font-size: var(--app-nav-icon-size);
|
|
6803
7141
|
line-height: 1;
|
|
6804
7142
|
}
|
|
6805
7143
|
|
|
@@ -6808,15 +7146,20 @@ a.avatar[aria-disabled="true"] {
|
|
|
6808
7146
|
color: var(--text);
|
|
6809
7147
|
}
|
|
6810
7148
|
|
|
6811
|
-
|
|
7149
|
+
/* Current tile is already a full selected surface. Selection never changes
|
|
7150
|
+
text metrics: the shared medium weight is kept, no permanent indicator is
|
|
7151
|
+
added here. */
|
|
7152
|
+
.app-nav > a[aria-current="page"] {
|
|
6812
7153
|
background: var(--state-selected);
|
|
6813
7154
|
color: var(--state-selected-fg);
|
|
6814
|
-
font-weight: var(--font-weight-bold);
|
|
6815
7155
|
}
|
|
6816
7156
|
|
|
6817
|
-
@media
|
|
6818
|
-
|
|
6819
|
-
|
|
7157
|
+
@media (forced-colors: active) {
|
|
7158
|
+
/* --state-selected flattens to a UA color that no longer distinguishes the
|
|
7159
|
+
current tile from the rest of the bar, so paint the system Highlight pair. */
|
|
7160
|
+
.app-nav > a[aria-current="page"] {
|
|
7161
|
+
background: Highlight;
|
|
7162
|
+
color: HighlightText;
|
|
6820
7163
|
}
|
|
6821
7164
|
}
|
|
6822
7165
|
|
|
@@ -6831,8 +7174,15 @@ a.avatar[aria-disabled="true"] {
|
|
|
6831
7174
|
* --gap (local value for the shared gap token)
|
|
6832
7175
|
*/
|
|
6833
7176
|
/* Intent boundary — inherited intents do not tint this component.
|
|
6834
|
-
Local intent classes still win because :where() has zero specificity.
|
|
7177
|
+
Local intent classes still win because :where() has zero specificity.
|
|
7178
|
+
The intent tokens are reset alongside --ui-*: the shared variants
|
|
7179
|
+
(.soft/.solid/.outline/.surface) read --intent / --intent-fg / --intent-soft-fg
|
|
7180
|
+
when they paint this surface, so an ancestor intent would otherwise leak
|
|
7181
|
+
through a variant that has no local intent class. */
|
|
6835
7182
|
:where(.navbar) {
|
|
7183
|
+
--intent: initial;
|
|
7184
|
+
--intent-fg: initial;
|
|
7185
|
+
--intent-soft-fg: initial;
|
|
6836
7186
|
--ui-bg: initial;
|
|
6837
7187
|
--ui-fg: initial;
|
|
6838
7188
|
--ui-border: initial;
|
|
@@ -6888,10 +7238,15 @@ a.avatar[aria-disabled="true"] {
|
|
|
6888
7238
|
text-decoration: none;
|
|
6889
7239
|
}
|
|
6890
7240
|
|
|
6891
|
-
|
|
7241
|
+
/* nav-link serves two contracts: plain page navigation (aria-current="page")
|
|
7242
|
+
and scrollspy, which toggles aria-current="location" on the active link.
|
|
7243
|
+
Presence is the contract: the attribute is set on the current link and
|
|
7244
|
+
removed everywhere else, never serialized as aria-current="false".
|
|
7245
|
+
Selection never changes text metrics: the shared medium weight is kept and
|
|
7246
|
+
the current link is carried by the selected accent. */
|
|
7247
|
+
.nav-link[aria-current] {
|
|
6892
7248
|
background: var(--surface-subtle);
|
|
6893
|
-
color: var(--
|
|
6894
|
-
font-weight: var(--font-weight-bold);
|
|
7249
|
+
color: var(--state-selected);
|
|
6895
7250
|
}
|
|
6896
7251
|
|
|
6897
7252
|
.nav-link:hover {
|
|
@@ -6899,11 +7254,18 @@ a.avatar[aria-disabled="true"] {
|
|
|
6899
7254
|
color: var(--text);
|
|
6900
7255
|
}
|
|
6901
7256
|
|
|
7257
|
+
/* A current link keeps its accent on hover: without this, the hover rule above
|
|
7258
|
+
would drop it back to --text. The surface hover still applies. */
|
|
7259
|
+
.nav-link[aria-current]:hover {
|
|
7260
|
+
color: var(--state-selected);
|
|
7261
|
+
}
|
|
7262
|
+
|
|
6902
7263
|
/* Nav list — a vertical navigation list. Used by .drawer for stacked links.
|
|
6903
7264
|
Links fill each row so the whole line is an interactive target; the
|
|
6904
7265
|
horizontal .navbar-nav links keep their intrinsic size. */
|
|
6905
7266
|
.nav-list {
|
|
6906
7267
|
display: grid;
|
|
7268
|
+
grid-template-columns: minmax(0, 1fr);
|
|
6907
7269
|
--gap: var(--space-20);
|
|
6908
7270
|
gap: var(--gap);
|
|
6909
7271
|
margin: 0;
|
|
@@ -6916,9 +7278,23 @@ a.avatar[aria-disabled="true"] {
|
|
|
6916
7278
|
}
|
|
6917
7279
|
|
|
6918
7280
|
.nav-list .nav-link {
|
|
7281
|
+
position: relative;
|
|
6919
7282
|
inline-size: 100%;
|
|
6920
7283
|
}
|
|
6921
7284
|
|
|
7285
|
+
/* Vertical current indicator: inset inline-start trait in currentColor, so it
|
|
7286
|
+
follows the forced palette without a forced-colors block and never touches
|
|
7287
|
+
the row separators. Navbar horizontal links keep color + surface only. The
|
|
7288
|
+
positioning context lives on every row link, so becoming current never moves
|
|
7289
|
+
an app-positioned decoration to a new containing block. */
|
|
7290
|
+
.nav-list .nav-link[aria-current]::before {
|
|
7291
|
+
content: "";
|
|
7292
|
+
position: absolute;
|
|
7293
|
+
inset-block: var(--space-20);
|
|
7294
|
+
inset-inline-start: 0;
|
|
7295
|
+
border-inline-start: 2px solid currentColor;
|
|
7296
|
+
}
|
|
7297
|
+
|
|
6922
7298
|
/*
|
|
6923
7299
|
* Breadcrumb — current-location trail from the site root.
|
|
6924
7300
|
*
|
|
@@ -6954,8 +7330,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
6954
7330
|
text-decoration: none;
|
|
6955
7331
|
}
|
|
6956
7332
|
|
|
6957
|
-
.breadcrumb a:not([aria-current]):hover
|
|
6958
|
-
.breadcrumb a[aria-current="false"]:hover {
|
|
7333
|
+
.breadcrumb a:not([aria-current="page"]):hover {
|
|
6959
7334
|
color: var(--text);
|
|
6960
7335
|
text-decoration: underline;
|
|
6961
7336
|
}
|
|
@@ -6964,7 +7339,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
6964
7339
|
border-radius: var(--radius-sm);
|
|
6965
7340
|
}
|
|
6966
7341
|
|
|
6967
|
-
.breadcrumb :where(li, a, span)
|
|
7342
|
+
.breadcrumb :where(li, a, span)[aria-current="page"] {
|
|
6968
7343
|
color: var(--text);
|
|
6969
7344
|
font-weight: var(--font-weight-medium);
|
|
6970
7345
|
}
|
|
@@ -6988,6 +7363,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
6988
7363
|
margin: 0;
|
|
6989
7364
|
padding: 0;
|
|
6990
7365
|
list-style: none;
|
|
7366
|
+
font-size: var(--control-font-size);
|
|
6991
7367
|
|
|
6992
7368
|
/* Arrows, page numbers, and truncation ellipses share one geometric
|
|
6993
7369
|
system: equal digit widths keep the row from re-flowing as numbers grow
|
|
@@ -7019,6 +7395,11 @@ a.avatar[aria-disabled="true"] {
|
|
|
7019
7395
|
* and arrow-key navigation. CSS owns the tab strip appearance and
|
|
7020
7396
|
* active indicator.
|
|
7021
7397
|
*
|
|
7398
|
+
* aria-orientation is matched inside :where() so it keeps describing which
|
|
7399
|
+
* arrow keys move between tabs without also carrying cascade weight: an
|
|
7400
|
+
* author class can then adjust a vertical strip without restating the
|
|
7401
|
+
* attribute to reach its specificity.
|
|
7402
|
+
*
|
|
7022
7403
|
* Public hooks:
|
|
7023
7404
|
* --tab-gap
|
|
7024
7405
|
*/
|
|
@@ -7039,7 +7420,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
7039
7420
|
list-style: none;
|
|
7040
7421
|
}
|
|
7041
7422
|
|
|
7042
|
-
.tabs[aria-orientation="vertical"] {
|
|
7423
|
+
.tabs:where([aria-orientation="vertical"]) {
|
|
7043
7424
|
display: grid;
|
|
7044
7425
|
align-content: start;
|
|
7045
7426
|
justify-items: stretch;
|
|
@@ -7060,12 +7441,15 @@ a.avatar[aria-disabled="true"] {
|
|
|
7060
7441
|
background: transparent;
|
|
7061
7442
|
color: var(--text-muted);
|
|
7062
7443
|
font: inherit;
|
|
7063
|
-
font-
|
|
7444
|
+
font-size: var(--control-font-size);
|
|
7445
|
+
/* Selection must not change text metrics: a heavier active label would shift
|
|
7446
|
+
the strip. The active state is carried by color and the indicator line. */
|
|
7447
|
+
font-weight: var(--font-weight-semibold);
|
|
7064
7448
|
text-decoration: none;
|
|
7065
7449
|
cursor: pointer;
|
|
7066
7450
|
}
|
|
7067
7451
|
|
|
7068
|
-
.tabs[aria-orientation="vertical"] .tab {
|
|
7452
|
+
.tabs:where([aria-orientation="vertical"]) .tab {
|
|
7069
7453
|
justify-content: flex-start;
|
|
7070
7454
|
padding-inline-end: var(--space-40);
|
|
7071
7455
|
}
|
|
@@ -7079,6 +7463,8 @@ a.avatar[aria-disabled="true"] {
|
|
|
7079
7463
|
color: var(--text);
|
|
7080
7464
|
}
|
|
7081
7465
|
|
|
7466
|
+
/* The ring is square on the edge the tab is attached to, so it reads as part
|
|
7467
|
+
of the strip rather than a floating chip. */
|
|
7082
7468
|
.tab:focus-visible {
|
|
7083
7469
|
outline: var(--focus-outline);
|
|
7084
7470
|
outline-color: var(--focus);
|
|
@@ -7086,18 +7472,27 @@ a.avatar[aria-disabled="true"] {
|
|
|
7086
7472
|
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
|
|
7087
7473
|
}
|
|
7088
7474
|
|
|
7089
|
-
|
|
7475
|
+
/* A vertical tab is attached along its inline end, not its block end, so the
|
|
7476
|
+
horizontal geometry above would round the wrong pair of corners. Logical
|
|
7477
|
+
corners keep it correct in RTL. */
|
|
7478
|
+
.tabs:where([aria-orientation="vertical"]) .tab:focus-visible {
|
|
7479
|
+
border-start-start-radius: var(--radius-sm);
|
|
7480
|
+
border-start-end-radius: 0;
|
|
7481
|
+
border-end-start-radius: var(--radius-sm);
|
|
7482
|
+
border-end-end-radius: 0;
|
|
7483
|
+
}
|
|
7484
|
+
|
|
7485
|
+
.tab[aria-selected="true"],
|
|
7486
|
+
.tab[aria-current="page"] {
|
|
7090
7487
|
color: var(--intent, var(--state-selected));
|
|
7091
|
-
/* Second, layout-free selected cue — the underline alone is color-only in
|
|
7092
|
-
forced colors. */
|
|
7093
|
-
font-weight: var(--font-weight-bold);
|
|
7094
7488
|
}
|
|
7095
7489
|
|
|
7096
7490
|
/* The selected indicator lives on a positioned pseudo so that only the active
|
|
7097
7491
|
tab owns an underline: a transparent border on every tab would surface in
|
|
7098
7492
|
forced colors and erase the distinction. currentColor follows the palette the
|
|
7099
7493
|
UA forces, so no forced-colors block is needed. */
|
|
7100
|
-
.tab[aria-selected="true"]::after
|
|
7494
|
+
.tab[aria-selected="true"]::after,
|
|
7495
|
+
.tab[aria-current="page"]::after {
|
|
7101
7496
|
content: "";
|
|
7102
7497
|
position: absolute;
|
|
7103
7498
|
inset-inline: 0;
|
|
@@ -7105,7 +7500,8 @@ a.avatar[aria-disabled="true"] {
|
|
|
7105
7500
|
border-block-end: 2px solid currentColor;
|
|
7106
7501
|
}
|
|
7107
7502
|
|
|
7108
|
-
.tabs[aria-orientation="vertical"] .tab[aria-selected="true"]::after
|
|
7503
|
+
.tabs:where([aria-orientation="vertical"]) .tab[aria-selected="true"]::after,
|
|
7504
|
+
.tabs:where([aria-orientation="vertical"]) .tab[aria-current="page"]::after {
|
|
7109
7505
|
inset-block: 0;
|
|
7110
7506
|
inset-inline: auto 0;
|
|
7111
7507
|
border-block-end: 0;
|
|
@@ -7162,7 +7558,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
7162
7558
|
.data-list > dt {
|
|
7163
7559
|
min-inline-size: 0;
|
|
7164
7560
|
margin-block-start: 0;
|
|
7165
|
-
font-weight: var(--font-weight-
|
|
7561
|
+
font-weight: var(--font-weight-semibold);
|
|
7166
7562
|
}
|
|
7167
7563
|
|
|
7168
7564
|
.data-list > dd {
|
|
@@ -7190,6 +7586,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
7190
7586
|
--list-divider: var(--border-width) solid var(--border);
|
|
7191
7587
|
|
|
7192
7588
|
display: grid;
|
|
7589
|
+
grid-template-columns: minmax(0, 1fr);
|
|
7193
7590
|
gap: 0;
|
|
7194
7591
|
margin: 0;
|
|
7195
7592
|
padding: 0;
|
|
@@ -7201,10 +7598,17 @@ a.avatar[aria-disabled="true"] {
|
|
|
7201
7598
|
margin: 0;
|
|
7202
7599
|
}
|
|
7203
7600
|
|
|
7601
|
+
/*
|
|
7602
|
+
* Flex, not a three-track grid: the leading and trailing regions are optional,
|
|
7603
|
+
* and a fixed `auto minmax(0, 1fr) auto` template gives an absent region a
|
|
7604
|
+
* track anyway — the content of a row with no leading region lands in the
|
|
7605
|
+
* leading track and the trailing control inherits the flexible one, which is
|
|
7606
|
+
* the opposite of the intent. Flex drops both the track and its gap when a
|
|
7607
|
+
* region is not there, so all four shapes work off one rule.
|
|
7608
|
+
*/
|
|
7204
7609
|
.list > :where(.list-item),
|
|
7205
7610
|
.list > li > .list-item {
|
|
7206
|
-
display:
|
|
7207
|
-
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
7611
|
+
display: flex;
|
|
7208
7612
|
/* Regions align to the first line, so leading, title, and trailing controls
|
|
7209
7613
|
share one horizontal line while supporting text wraps beneath. */
|
|
7210
7614
|
align-items: start;
|
|
@@ -7220,6 +7624,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
7220
7624
|
}
|
|
7221
7625
|
|
|
7222
7626
|
a.list-item {
|
|
7627
|
+
position: relative;
|
|
7223
7628
|
color: inherit;
|
|
7224
7629
|
text-decoration: none;
|
|
7225
7630
|
}
|
|
@@ -7228,7 +7633,31 @@ a.list-item:hover {
|
|
|
7228
7633
|
background: var(--surface-subtle);
|
|
7229
7634
|
}
|
|
7230
7635
|
|
|
7636
|
+
/* Current row in a navigational list (master/detail). Generic by design: the
|
|
7637
|
+
visual treatment only cares that the row is current, so presence is the
|
|
7638
|
+
contract — the attribute is set on the current row and removed everywhere
|
|
7639
|
+
else, never serialized as aria-current="false". The value (page, location,
|
|
7640
|
+
…) stays free for assistive technology.
|
|
7641
|
+
Calm by design: the row keeps its normal text metrics and color on a subtle
|
|
7642
|
+
surface, and the selected accent lives on an inset inline-start trait only.
|
|
7643
|
+
The trait is a real border, so it survives forced colors without a repaint
|
|
7644
|
+
block. The positioning context lives on every row link, so becoming current
|
|
7645
|
+
never moves an app-positioned decoration to a new containing block. */
|
|
7646
|
+
a.list-item[aria-current] {
|
|
7647
|
+
background: var(--surface-subtle);
|
|
7648
|
+
}
|
|
7649
|
+
|
|
7650
|
+
a.list-item[aria-current]::before {
|
|
7651
|
+
content: "";
|
|
7652
|
+
position: absolute;
|
|
7653
|
+
inset-block: var(--list-item-pad-block);
|
|
7654
|
+
inset-inline-start: 0;
|
|
7655
|
+
border-inline-start: 2px solid var(--state-selected);
|
|
7656
|
+
}
|
|
7657
|
+
|
|
7658
|
+
/* The content region is the one that flexes, whichever position it holds. */
|
|
7231
7659
|
.list-item-content {
|
|
7660
|
+
flex: 1 1 0;
|
|
7232
7661
|
min-inline-size: 0;
|
|
7233
7662
|
}
|
|
7234
7663
|
|
|
@@ -7236,16 +7665,19 @@ a.list-item:hover {
|
|
|
7236
7665
|
.list-item-text {
|
|
7237
7666
|
display: block;
|
|
7238
7667
|
margin: 0;
|
|
7668
|
+
overflow-wrap: anywhere;
|
|
7239
7669
|
}
|
|
7240
7670
|
|
|
7241
7671
|
.list-item-text {
|
|
7242
7672
|
color: var(--text-muted);
|
|
7243
7673
|
font-size: var(--font-size-sm);
|
|
7244
|
-
overflow-wrap: anywhere;
|
|
7245
7674
|
}
|
|
7246
7675
|
|
|
7676
|
+
/* The auto margin keeps the trailing region at the row's end even in a row
|
|
7677
|
+
with no content region to absorb the free space. */
|
|
7247
7678
|
.list-item-trailing {
|
|
7248
|
-
|
|
7679
|
+
flex: none;
|
|
7680
|
+
margin-inline-start: auto;
|
|
7249
7681
|
min-inline-size: 0;
|
|
7250
7682
|
}
|
|
7251
7683
|
|
|
@@ -7267,6 +7699,14 @@ a.list-item:hover {
|
|
|
7267
7699
|
* --table-cell-pad
|
|
7268
7700
|
*/
|
|
7269
7701
|
.table-wrap {
|
|
7702
|
+
/* Containing block for anything absolutely positioned inside the table —
|
|
7703
|
+
`.sr-only` on a column header above all. Without it that box resolves
|
|
7704
|
+
against the initial containing block, so it keeps the static position of a
|
|
7705
|
+
cell that may be a thousand pixels along the scrolled row, escapes this
|
|
7706
|
+
scroller, and extends the *document's* scrollable width. The page then
|
|
7707
|
+
scrolls sideways on a phone with nothing painted out there. */
|
|
7708
|
+
position: relative;
|
|
7709
|
+
|
|
7270
7710
|
inline-size: 100%;
|
|
7271
7711
|
max-inline-size: 100%;
|
|
7272
7712
|
min-inline-size: 0;
|
|
@@ -7301,13 +7741,13 @@ a.list-item:hover {
|
|
|
7301
7741
|
vertical-align: baseline;
|
|
7302
7742
|
}
|
|
7303
7743
|
|
|
7304
|
-
.table thead th {
|
|
7305
|
-
vertical-align: bottom;
|
|
7306
|
-
}
|
|
7307
|
-
|
|
7308
7744
|
.table th {
|
|
7309
7745
|
color: var(--text-muted);
|
|
7310
|
-
font-weight: var(--font-weight-
|
|
7746
|
+
font-weight: var(--font-weight-semibold);
|
|
7747
|
+
}
|
|
7748
|
+
|
|
7749
|
+
.table thead th {
|
|
7750
|
+
vertical-align: bottom;
|
|
7311
7751
|
}
|
|
7312
7752
|
|
|
7313
7753
|
.table thead {
|
|
@@ -7875,7 +8315,8 @@ a.list-item:hover {
|
|
|
7875
8315
|
text-decoration: none;
|
|
7876
8316
|
}
|
|
7877
8317
|
|
|
7878
|
-
.link-muted:hover
|
|
8318
|
+
.link-muted:hover,
|
|
8319
|
+
.link-muted:focus-visible {
|
|
7879
8320
|
color: var(--text);
|
|
7880
8321
|
}
|
|
7881
8322
|
|
|
@@ -7916,7 +8357,8 @@ a.list-item:hover {
|
|
|
7916
8357
|
* They are escape hatches for layout nudges, not a full atomic scale.
|
|
7917
8358
|
*
|
|
7918
8359
|
* Core principle:
|
|
7919
|
-
* - .
|
|
8360
|
+
* - .compact / .spacious provide inherited rhythm through --gap and
|
|
8361
|
+
* --density-space.
|
|
7920
8362
|
* - utilities consume the current context instead of defining their own -sm/-lg scale.
|
|
7921
8363
|
* - optional utilities extend core utilities, they do not re-express them.
|
|
7922
8364
|
* - follow verbose .{property}-{value} naming.
|
|
@@ -7997,18 +8439,6 @@ a.list-item:hover {
|
|
|
7997
8439
|
flex-wrap: nowrap;
|
|
7998
8440
|
}
|
|
7999
8441
|
|
|
8000
|
-
.gap-sm {
|
|
8001
|
-
gap: var(--space-20);
|
|
8002
|
-
}
|
|
8003
|
-
|
|
8004
|
-
.gap-md {
|
|
8005
|
-
gap: var(--space-40);
|
|
8006
|
-
}
|
|
8007
|
-
|
|
8008
|
-
.gap-lg {
|
|
8009
|
-
gap: var(--space-50);
|
|
8010
|
-
}
|
|
8011
|
-
|
|
8012
8442
|
.justify-content-start {
|
|
8013
8443
|
--cluster-justify: flex-start;
|
|
8014
8444
|
justify-content: flex-start;
|
|
@@ -8066,6 +8496,10 @@ a.list-item:hover {
|
|
|
8066
8496
|
margin-block-end: auto;
|
|
8067
8497
|
}
|
|
8068
8498
|
|
|
8499
|
+
/* Consume the inherited --gap rhythm on an arbitrary layout container.
|
|
8500
|
+
Setting --gap alone only defines the custom property; it has no visual
|
|
8501
|
+
effect unless the element already uses gap: var(--gap) (as .stack,
|
|
8502
|
+
.cluster, .grid, etc. do). */
|
|
8069
8503
|
.gap-context {
|
|
8070
8504
|
gap: var(--gap);
|
|
8071
8505
|
}
|
|
@@ -8117,15 +8551,15 @@ a.list-item:hover {
|
|
|
8117
8551
|
}
|
|
8118
8552
|
|
|
8119
8553
|
.font-weight-normal {
|
|
8120
|
-
font-weight: var(--font-weight);
|
|
8554
|
+
font-weight: var(--font-weight-normal);
|
|
8121
8555
|
}
|
|
8122
8556
|
|
|
8123
8557
|
.font-weight-medium {
|
|
8124
8558
|
font-weight: var(--font-weight-medium);
|
|
8125
8559
|
}
|
|
8126
8560
|
|
|
8127
|
-
.font-weight-
|
|
8128
|
-
font-weight: var(--font-weight-
|
|
8561
|
+
.font-weight-semibold {
|
|
8562
|
+
font-weight: var(--font-weight-semibold);
|
|
8129
8563
|
}
|
|
8130
8564
|
|
|
8131
8565
|
.font-weight-bold {
|
|
@@ -8166,18 +8600,28 @@ a.list-item:hover {
|
|
|
8166
8600
|
border-inline-end: var(--border-width) solid var(--border);
|
|
8167
8601
|
}
|
|
8168
8602
|
|
|
8169
|
-
/* Surface
|
|
8603
|
+
/* Surface. These are semantic surfaces, not arbitrary paint: each owns its
|
|
8604
|
+
matching ink and rebinds --heading to that ink, so it stays readable when it
|
|
8605
|
+
recreates a light surface inside a context that installed a dark one
|
|
8606
|
+
(.inverted). Intent ink is not inherited through a surface utility; use
|
|
8607
|
+
.intent-color for that. */
|
|
8170
8608
|
.background-surface {
|
|
8609
|
+
--heading: currentColor;
|
|
8610
|
+
|
|
8171
8611
|
background: var(--surface);
|
|
8172
8612
|
color: var(--text);
|
|
8173
8613
|
}
|
|
8174
8614
|
|
|
8175
8615
|
.background-raised {
|
|
8616
|
+
--heading: currentColor;
|
|
8617
|
+
|
|
8176
8618
|
background: var(--surface-raised);
|
|
8177
8619
|
color: var(--text);
|
|
8178
8620
|
}
|
|
8179
8621
|
|
|
8180
8622
|
.background-subtle {
|
|
8623
|
+
--heading: currentColor;
|
|
8624
|
+
|
|
8181
8625
|
background: var(--surface-subtle);
|
|
8182
8626
|
color: var(--text);
|
|
8183
8627
|
}
|