actual-css 0.12.0 → 0.13.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 +49 -0
- package/README.md +7 -8
- package/components.json +8 -2
- package/dist/actual.full.css +505 -233
- package/dist/actual.full.js +1 -1
- package/dist/actual.full.min.css +1 -1
- package/docs/design-notes/architecture.md +67 -0
- package/docs/design-notes/steps.md +45 -28
- package/docs/pages/components/avatar.md +17 -11
- package/docs/pages/components/breadcrumb.md +16 -2
- package/docs/pages/components/list.md +9 -13
- package/docs/pages/components/navbar.md +42 -22
- package/docs/pages/components/pagination.md +19 -0
- package/docs/pages/components/steps.md +24 -20
- package/docs/pages/enhancements/scrollspy.md +6 -6
- package/docs/pages/enhancements/tabs.md +74 -3
- package/docs/pages/forms/floating-field.md +21 -7
- package/docs/pages/forms/form-actions.md +3 -6
- package/docs/pages/forms/states-and-sizes.md +15 -15
- package/docs/pages/foundations/philosophy.md +97 -1
- package/docs/pages/foundations/tokens.md +40 -4
- package/docs/pages/guides/design-tools.md +1 -1
- package/docs/pages/guides/integrating-actual-css.md +74 -8
- package/docs/pages/guides/introduction.md +3 -2
- package/docs/pages/guides/modular-import.md +1 -1
- package/docs/pages/guides/progressive-enhancement.md +25 -7
- package/docs/pages/guides/upgrading.md +86 -9
- package/docs/pages/layout/center.md +2 -1
- package/docs/pages/layout/cluster.md +11 -4
- package/docs/pages/layout/grid.md +2 -0
- package/docs/pages/layout/sidebar-layout.md +3 -0
- package/docs/pages/layout/switcher.md +2 -0
- package/docs/pages/layout/topbar.md +6 -4
- package/docs/pages/patterns/icon-slot.md +17 -1
- package/docs/pages/utilities/flex.md +1 -1
- package/docs/pages/utilities/sizing-wrapping.md +51 -6
- package/package.json +5 -5
- package/src/cli/actual-css.js +1 -1
- package/src/css/components/alert.css +34 -34
- package/src/css/components/avatar.css +2 -18
- package/src/css/components/badge.css +0 -1
- package/src/css/components/breadcrumb.css +74 -8
- package/src/css/components/drawer.css +23 -42
- package/src/css/components/flyout.css +0 -1
- package/src/css/components/join.css +2 -2
- package/src/css/components/list.css +10 -10
- package/src/css/components/modal.css +17 -10
- package/src/css/components/navbar.css +10 -8
- package/src/css/components/pagination.css +5 -0
- package/src/css/components/scrollspy.css +1 -1
- package/src/css/components/steps.css +66 -12
- package/src/css/components/tab.css +134 -4
- package/src/css/core/focus.css +8 -6
- package/src/css/core/theme.css +6 -1
- package/src/css/core/tokens.css +4 -2
- package/src/css/forms/choice-card.css +1 -1
- package/src/css/forms/choice.css +1 -1
- package/src/css/forms/control.css +1 -1
- package/src/css/forms/floating-field.css +20 -18
- package/src/css/forms/form-actions.css +5 -8
- package/src/css/forms/native.css +2 -2
- package/src/css/forms/otp.css +4 -5
- package/src/css/forms/switch.css +1 -1
- package/src/css/forms/validation.css +23 -30
- package/src/css/layout/center.css +2 -0
- package/src/css/layout/cluster.css +14 -0
- package/src/css/layout/grid.css +7 -0
- package/src/css/layout/scroller.css +19 -3
- package/src/css/layout/sidebar-layout.css +3 -0
- package/src/css/layout/switcher.css +3 -0
- package/src/css/utilities/extra.css +3 -3
- package/src/js/full.js +4 -0
- package/src/js/inline-visible.js +54 -0
- package/src/js/parsed.js +21 -0
- package/src/js/tab.js +33 -7
- package/src/tooling/design-export.js +1 -1
- package/src/tooling/design-tokens.js +44 -33
- package/src/tooling/theme-contrast.js +32 -24
package/dist/actual.full.css
CHANGED
|
@@ -134,9 +134,11 @@ select {
|
|
|
134
134
|
without a component ring (links, summary, plain items). Components draw
|
|
135
135
|
the themed ring instead: --focus-ring-width solid --focus. See focus.css
|
|
136
136
|
for the two contracts. Both share --focus-ring-width: they differ in
|
|
137
|
-
color only.
|
|
137
|
+
color only. No --focus-outline alias here: declared on :root it would be
|
|
138
|
+
inherited already computed, so a theme island or a region setting
|
|
139
|
+
--focus-ring-width would not reach it. focus.css composes the line on the
|
|
140
|
+
focused element instead. */
|
|
138
141
|
--focus-outline-color: currentColor;
|
|
139
|
-
--focus-outline: var(--focus-ring-width) solid var(--focus-outline-color);
|
|
140
142
|
--focus-outline-offset: 2px;
|
|
141
143
|
--focus-ring-width: 2px;
|
|
142
144
|
|
|
@@ -315,7 +317,9 @@ select {
|
|
|
315
317
|
* Derived: "Theme-derived aliases", Tone subsystem, Backdrop fill — reference
|
|
316
318
|
* other tokens; hard-coded overrides on a custom theme will break the intent.
|
|
317
319
|
* Exception: --surface-opaque and --surface-solid-fg are public; a theme with
|
|
318
|
-
* translucent surfaces overrides them with opaque values.
|
|
320
|
+
* translucent surfaces overrides them with opaque values. --border-control is
|
|
321
|
+
* public too: the resting edge of a neutral control, apart from structural
|
|
322
|
+
* separators.
|
|
319
323
|
* Internal: --hgroup-gap.
|
|
320
324
|
*/
|
|
321
325
|
:root {
|
|
@@ -417,6 +421,9 @@ select {
|
|
|
417
421
|
never a fill. --surface-solid-fg is the ink on --surface-solid. */
|
|
418
422
|
--surface-opaque: var(--surface);
|
|
419
423
|
--surface-solid-fg: var(--surface);
|
|
424
|
+
/* The edge that tells a control apart from the surface, where --border only
|
|
425
|
+
separates surfaces: fields, choices, switch, OTP cells, choice cards. */
|
|
426
|
+
--border-control: var(--border);
|
|
420
427
|
}
|
|
421
428
|
|
|
422
429
|
/* Soft foreground hooks — the soft ink is the intent rebated toward the local
|
|
@@ -876,16 +883,18 @@ small {
|
|
|
876
883
|
Documented exception: an invalid field focuses in --form-invalid-border
|
|
877
884
|
(danger), so the error stays visible at the point of correction. The border
|
|
878
885
|
carries the same hook; report:theme-contrast measures that line at 3:1
|
|
879
|
-
against the field's background alongside --focus.
|
|
886
|
+
against the field's background alongside --focus.
|
|
887
|
+
|
|
888
|
+
Public hooks:
|
|
889
|
+
--focus-outline (fallback-only: replaces the whole generic line)
|
|
890
|
+
*/
|
|
880
891
|
:focus {
|
|
881
|
-
outline: var(--focus-outline);
|
|
892
|
+
outline: var(--focus-outline, var(--focus-ring-width) solid var(--focus-outline-color));
|
|
882
893
|
outline-offset: var(--focus-outline-offset);
|
|
883
894
|
}
|
|
884
895
|
|
|
885
|
-
|
|
886
|
-
:
|
|
887
|
-
outline: none;
|
|
888
|
-
}
|
|
896
|
+
:focus:not(:focus-visible) {
|
|
897
|
+
outline: none;
|
|
889
898
|
}
|
|
890
899
|
|
|
891
900
|
@media print {
|
|
@@ -1349,7 +1358,21 @@ small {
|
|
|
1349
1358
|
*
|
|
1350
1359
|
* Child contract: direct-child margins are reset, spacing comes from --gap.
|
|
1351
1360
|
* A ul/ol/menu keeps its semantics but loses list chrome (see below).
|
|
1361
|
+
*
|
|
1362
|
+
* The hooks describe this row's own geometry, so they stop at each cluster:
|
|
1363
|
+
* set them on the cluster itself, or target nested rows with a selector. The
|
|
1364
|
+
* gap token is context rhythm and keeps inheriting.
|
|
1352
1365
|
*/
|
|
1366
|
+
|
|
1367
|
+
/* Instance boundary: an ancestor's value never reaches a nested cluster.
|
|
1368
|
+
Zero specificity, so any class or inline style on the cluster wins without
|
|
1369
|
+
depending on import order; `initial` makes the var() fallbacks apply. */
|
|
1370
|
+
:where(.cluster) {
|
|
1371
|
+
--cluster-align: initial;
|
|
1372
|
+
--cluster-justify: initial;
|
|
1373
|
+
--cluster-wrap: initial;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1353
1376
|
.cluster {
|
|
1354
1377
|
display: flex;
|
|
1355
1378
|
flex-direction: row;
|
|
@@ -1382,6 +1405,8 @@ small {
|
|
|
1382
1405
|
* --center-size
|
|
1383
1406
|
* --center-pad
|
|
1384
1407
|
*/
|
|
1408
|
+
/* The hooks inherit on purpose: a site's content width is page policy, so a
|
|
1409
|
+
theme sets them once on :root for every .center. */
|
|
1385
1410
|
.center {
|
|
1386
1411
|
box-sizing: content-box;
|
|
1387
1412
|
inline-size: calc(100% - var(--center-pad, 1rem) * 2);
|
|
@@ -1554,6 +1579,9 @@ small {
|
|
|
1554
1579
|
*
|
|
1555
1580
|
* Child contract: every direct child is a peer; the flex-basis applies to all.
|
|
1556
1581
|
*/
|
|
1582
|
+
/* No instance reset, unlike .cluster and .grid: the children read the hook,
|
|
1583
|
+
and a switcher nested as a direct child takes its own flex-basis from its
|
|
1584
|
+
parent's value, so the hook must reach it. */
|
|
1557
1585
|
.switcher {
|
|
1558
1586
|
display: flex;
|
|
1559
1587
|
flex-direction: row;
|
|
@@ -1586,6 +1614,9 @@ small {
|
|
|
1586
1614
|
* visual, reading and keyboard order all agree — the role rules swap so the
|
|
1587
1615
|
* aside still takes its preferred width and the main still reclaims the row.
|
|
1588
1616
|
*/
|
|
1617
|
+
/* No instance reset, unlike .cluster and .grid: the regions read the hooks,
|
|
1618
|
+
and a layout nested as a region takes its own size from its parent's
|
|
1619
|
+
values, so the hooks must reach it. */
|
|
1589
1620
|
.sidebar-layout {
|
|
1590
1621
|
display: flex;
|
|
1591
1622
|
flex-direction: row;
|
|
@@ -1647,6 +1678,13 @@ small {
|
|
|
1647
1678
|
* the thresholds were arrived at: docs/design-notes/grid.md.
|
|
1648
1679
|
*/
|
|
1649
1680
|
|
|
1681
|
+
/* Instance boundary: a nested grid never takes its parent's track template.
|
|
1682
|
+
Zero specificity, so a class or inline style on the grid itself wins. */
|
|
1683
|
+
:where(.grid) {
|
|
1684
|
+
--grid-columns: initial;
|
|
1685
|
+
--grid-min: initial;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1650
1688
|
/* --grid-columns replaces the intrinsic auto-fit template: once set, the grid
|
|
1651
1689
|
no longer collapses on its own and the author owns narrow-container
|
|
1652
1690
|
collapse. */
|
|
@@ -1832,7 +1870,16 @@ small {
|
|
|
1832
1870
|
* draw their native scrollbar — the intended fallback, not a broken state.
|
|
1833
1871
|
*
|
|
1834
1872
|
* Styles the scrollbar only; it does not create the overflow. Pair it with
|
|
1835
|
-
* .overflow-auto or a component that already scrolls
|
|
1873
|
+
* .overflow-auto or a component that already scrolls (horizontal steps, a
|
|
1874
|
+
* table wrap; tabs hide their bar and fade their own edges). No edge fade
|
|
1875
|
+
* here: a mask would dictate the strip's scroll-padding and focus geometry for
|
|
1876
|
+
* a decorative cue, and an item clipped at the edge already says the strip
|
|
1877
|
+
* continues.
|
|
1878
|
+
*
|
|
1879
|
+
* Not on touch: Chromium trades the overlay scrollbar of a touch screen,
|
|
1880
|
+
* hidden at rest, for a classic always-visible one as soon as scrollbar-color
|
|
1881
|
+
* or scrollbar-width is set. On a phone that painted a permanent bar under
|
|
1882
|
+
* every strip; the native overlay is the better cue there.
|
|
1836
1883
|
*
|
|
1837
1884
|
* Public hooks:
|
|
1838
1885
|
* --scroller-track
|
|
@@ -1841,9 +1888,16 @@ small {
|
|
|
1841
1888
|
.scroller {
|
|
1842
1889
|
--scroller-track: transparent;
|
|
1843
1890
|
--scroller-thumb: var(--border);
|
|
1891
|
+
}
|
|
1844
1892
|
|
|
1845
|
-
|
|
1846
|
-
|
|
1893
|
+
/* Any primary pointer but touch: a keyboard-only or pointerless device
|
|
1894
|
+
(pointer: none) draws classic scrollbars too. `all and` keeps the negation
|
|
1895
|
+
valid in Media Queries 3 parsers. */
|
|
1896
|
+
@media not all and (pointer: coarse) {
|
|
1897
|
+
.scroller {
|
|
1898
|
+
scrollbar-color: var(--scroller-thumb) var(--scroller-track);
|
|
1899
|
+
scrollbar-width: thin;
|
|
1900
|
+
}
|
|
1847
1901
|
}
|
|
1848
1902
|
|
|
1849
1903
|
/* Opt-in gutter: reserves space so the layout does not shift when
|
|
@@ -2107,7 +2161,7 @@ small {
|
|
|
2107
2161
|
.input,
|
|
2108
2162
|
.textarea,
|
|
2109
2163
|
.select {
|
|
2110
|
-
--control-border: var(--border);
|
|
2164
|
+
--control-border: var(--border-control);
|
|
2111
2165
|
--control-bg: var(--surface);
|
|
2112
2166
|
--control-fg: var(--text);
|
|
2113
2167
|
--control-cursor: auto;
|
|
@@ -2333,7 +2387,7 @@ small {
|
|
|
2333
2387
|
/* Control geometry. --choice-* are the per-state tokens. */
|
|
2334
2388
|
.check,
|
|
2335
2389
|
.radio {
|
|
2336
|
-
--choice-border: var(--border);
|
|
2390
|
+
--choice-border: var(--border-control);
|
|
2337
2391
|
--choice-bg: var(--surface);
|
|
2338
2392
|
--choice-mark: var(--primary-fg);
|
|
2339
2393
|
|
|
@@ -2521,7 +2575,7 @@ small {
|
|
|
2521
2575
|
calc((var(--control-size) - 1lh - 2 * var(--border-width)) / 2)
|
|
2522
2576
|
);
|
|
2523
2577
|
padding-inline: var(--space-20);
|
|
2524
|
-
border: var(--border-width) solid var(--border);
|
|
2578
|
+
border: var(--border-width) solid var(--border-control);
|
|
2525
2579
|
border-radius: var(--radius-sm);
|
|
2526
2580
|
background: var(--surface-subtle);
|
|
2527
2581
|
color: var(--text);
|
|
@@ -2555,7 +2609,7 @@ small {
|
|
|
2555
2609
|
inline-size: var(--control-size);
|
|
2556
2610
|
block-size: var(--control-size);
|
|
2557
2611
|
padding: var(--border-width);
|
|
2558
|
-
border: var(--border-width) solid var(--border);
|
|
2612
|
+
border: var(--border-width) solid var(--border-control);
|
|
2559
2613
|
border-radius: var(--radius);
|
|
2560
2614
|
background: var(--surface);
|
|
2561
2615
|
cursor: pointer;
|
|
@@ -2609,6 +2663,8 @@ small {
|
|
|
2609
2663
|
*
|
|
2610
2664
|
* Imported by default through forms/index.css. Custom builds may omit this
|
|
2611
2665
|
* file when an app wants to own validation styling completely.
|
|
2666
|
+
* Without :has(), automatic error display and choice-card borders drop;
|
|
2667
|
+
* native control states and the explicit .field.danger route stay available.
|
|
2612
2668
|
*
|
|
2613
2669
|
* State coverage (which states each component ships) is audited in
|
|
2614
2670
|
* tests/validation.test.js and summarized in docs/pages/forms/validation.md —
|
|
@@ -2630,11 +2686,14 @@ small {
|
|
|
2630
2686
|
display: block;
|
|
2631
2687
|
}
|
|
2632
2688
|
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2689
|
+
.field:has([aria-invalid="true"]) > .field-error,
|
|
2690
|
+
.needs-validation.was-validated .field:has(:invalid) > .field-error {
|
|
2691
|
+
display: block;
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
/* biome-ignore lint/style/noDescendingSpecificity: every invalid route reveals the error; keep :user-invalid separate so unsupported engines retain the other routes. */
|
|
2695
|
+
.needs-validation .field:has(:user-invalid) > .field-error {
|
|
2696
|
+
display: block;
|
|
2638
2697
|
}
|
|
2639
2698
|
|
|
2640
2699
|
:where(.input, .textarea, .select, .check, .radio, .switch)[aria-invalid="true"],
|
|
@@ -2658,41 +2717,29 @@ small {
|
|
|
2658
2717
|
--switch-bg: var(--danger);
|
|
2659
2718
|
}
|
|
2660
2719
|
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
}
|
|
2720
|
+
.needs-validation .check:is(:checked, :indeterminate):user-invalid,
|
|
2721
|
+
.needs-validation .radio:checked:user-invalid {
|
|
2722
|
+
--choice-bg: var(--danger);
|
|
2723
|
+
}
|
|
2666
2724
|
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
}
|
|
2725
|
+
.needs-validation .switch:checked:user-invalid {
|
|
2726
|
+
--switch-bg: var(--danger);
|
|
2670
2727
|
}
|
|
2671
2728
|
|
|
2672
2729
|
/* Choice cards hide their native input, so invalid state must surface on the
|
|
2673
2730
|
card itself. Sets the same --form-invalid-border hook the card's border
|
|
2674
2731
|
consumes. */
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
--form-invalid-border: var(--danger);
|
|
2679
|
-
}
|
|
2732
|
+
.choice-card:has([aria-invalid="true"]),
|
|
2733
|
+
.needs-validation.was-validated .choice-card:has(:invalid) {
|
|
2734
|
+
--form-invalid-border: var(--danger);
|
|
2680
2735
|
}
|
|
2681
2736
|
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
--form-invalid-border: var(--danger);
|
|
2685
|
-
}
|
|
2737
|
+
.needs-validation :is(.input, .textarea, .select, .check, .radio, .switch):user-invalid {
|
|
2738
|
+
--form-invalid-border: var(--danger);
|
|
2686
2739
|
}
|
|
2687
2740
|
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
display: block;
|
|
2691
|
-
}
|
|
2692
|
-
|
|
2693
|
-
.needs-validation .choice-card:has(:user-invalid) {
|
|
2694
|
-
--form-invalid-border: var(--danger);
|
|
2695
|
-
}
|
|
2741
|
+
.needs-validation .choice-card:has(:user-invalid) {
|
|
2742
|
+
--form-invalid-border: var(--danger);
|
|
2696
2743
|
}
|
|
2697
2744
|
|
|
2698
2745
|
/*
|
|
@@ -2718,15 +2765,12 @@ small {
|
|
|
2718
2765
|
* --form-actions-sticky-padding (owned by .form-actions.sticky)
|
|
2719
2766
|
*/
|
|
2720
2767
|
|
|
2721
|
-
/* The policy
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
forms load after layout in the official bundle (tests/css-audit.test.js
|
|
2725
|
-
guards the order); utilities load later and keep the author's last word. On
|
|
2726
|
-
this row --cluster-justify/--cluster-align have no effect. */
|
|
2768
|
+
/* The role relays its policy into the row's cluster hooks. Cluster hooks stop
|
|
2769
|
+
at every nested .cluster, so the relay cannot leak, and this class beats the
|
|
2770
|
+
cluster's zero-specificity reset in any import order. */
|
|
2727
2771
|
.form-actions {
|
|
2728
|
-
align
|
|
2729
|
-
justify
|
|
2772
|
+
--cluster-align: var(--form-actions-align, center);
|
|
2773
|
+
--cluster-justify: var(--form-actions-justify, flex-start);
|
|
2730
2774
|
}
|
|
2731
2775
|
|
|
2732
2776
|
/* Opt-in sticky variant. When the bar is not stuck (at rest) the translucent
|
|
@@ -2852,7 +2896,7 @@ small {
|
|
|
2852
2896
|
|
|
2853
2897
|
.switch {
|
|
2854
2898
|
--switch-bg: var(--border);
|
|
2855
|
-
--switch-border: var(--border);
|
|
2899
|
+
--switch-border: var(--border-control);
|
|
2856
2900
|
--switch-knob: var(--surface-raised);
|
|
2857
2901
|
--switch-block-size: calc(var(--choice-control-size) + 0.125em);
|
|
2858
2902
|
--switch-width: calc(var(--switch-block-size) * 1.8);
|
|
@@ -3189,7 +3233,7 @@ small {
|
|
|
3189
3233
|
--choice-card-pad: var(--density-space, var(--space-40));
|
|
3190
3234
|
--choice-card-gap: var(--space-30);
|
|
3191
3235
|
--choice-card-radius: var(--radius);
|
|
3192
|
-
--choice-card-border: var(--border);
|
|
3236
|
+
--choice-card-border: var(--border-control);
|
|
3193
3237
|
--choice-card-bg: var(--surface);
|
|
3194
3238
|
--choice-card-check-size: 1.25em;
|
|
3195
3239
|
|
|
@@ -3525,10 +3569,13 @@ small {
|
|
|
3525
3569
|
* messages stay in the surrounding .field, not inside the cell, so the
|
|
3526
3570
|
* resting label can center on the control alone.
|
|
3527
3571
|
*
|
|
3528
|
-
* The
|
|
3529
|
-
*
|
|
3530
|
-
*
|
|
3531
|
-
*
|
|
3572
|
+
* The cell reads its control's state through :has(), so the label may come
|
|
3573
|
+
* before or after the control (a form theme rendering label then widget needs
|
|
3574
|
+
* no reordering). Injected sibling nodes are tolerated (password managers,
|
|
3575
|
+
* extension wrappers) as long as the control stays a direct child; the cell
|
|
3576
|
+
* contract stays a single control plus one label, since a second control
|
|
3577
|
+
* would float the label on either one's state.
|
|
3578
|
+
* Every .input/.textarea needs
|
|
3532
3579
|
* placeholder=" " to expose a reliable empty state — without it the label
|
|
3533
3580
|
* only reacts to focus.
|
|
3534
3581
|
*
|
|
@@ -3578,23 +3625,22 @@ small {
|
|
|
3578
3625
|
}
|
|
3579
3626
|
|
|
3580
3627
|
/* Single-line controls rest in the middle until they receive a value. */
|
|
3581
|
-
.floating-field
|
|
3628
|
+
.floating-field:has(> :is(.input, .select)) > .field-label {
|
|
3582
3629
|
inset-block-start: 50%;
|
|
3583
3630
|
transform: translateY(-50%);
|
|
3584
3631
|
}
|
|
3585
3632
|
|
|
3586
3633
|
/* An empty textarea label follows the control's normal first-line position;
|
|
3587
3634
|
the control itself keeps the extra headroom reserved for its floated state. */
|
|
3588
|
-
.floating-field
|
|
3635
|
+
.floating-field:has(> .textarea) > .field-label {
|
|
3589
3636
|
inset-block-start: calc(1em + var(--border-width));
|
|
3590
3637
|
transform: none;
|
|
3591
3638
|
}
|
|
3592
3639
|
|
|
3593
3640
|
/* :autofill covers prefilled values before interaction, when the browser may
|
|
3594
3641
|
not have propagated the value to the placeholder state yet. */
|
|
3595
|
-
.floating-field
|
|
3596
|
-
|
|
3597
|
-
~ .field-label {
|
|
3642
|
+
.floating-field:has(> :is(.input, .textarea):is(:focus, :autofill, :not(:placeholder-shown)))
|
|
3643
|
+
> .field-label {
|
|
3598
3644
|
inset-block-start: var(--floating-label-top);
|
|
3599
3645
|
transform: none;
|
|
3600
3646
|
font-size: var(--font-size-xs);
|
|
@@ -3604,16 +3650,16 @@ small {
|
|
|
3604
3650
|
/* Controls without a dependable :placeholder-shown empty state stay floated:
|
|
3605
3651
|
select always, and date/time inputs whose placeholder rendering is
|
|
3606
3652
|
inconsistent across engines (Firefox shows none). */
|
|
3607
|
-
.floating-field
|
|
3653
|
+
.floating-field:has(
|
|
3608
3654
|
> :is(
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3655
|
+
.select,
|
|
3656
|
+
.input[type="date"],
|
|
3657
|
+
.input[type="time"],
|
|
3658
|
+
.input[type="datetime-local"],
|
|
3659
|
+
.input[type="month"],
|
|
3660
|
+
.input[type="week"]
|
|
3661
|
+
)
|
|
3662
|
+
) > .field-label {
|
|
3617
3663
|
inset-block-start: var(--floating-label-top);
|
|
3618
3664
|
transform: none;
|
|
3619
3665
|
font-size: var(--font-size-xs);
|
|
@@ -3649,7 +3695,7 @@ small {
|
|
|
3649
3695
|
--otp-cell-size: var(--control-size);
|
|
3650
3696
|
--otp-gap: var(--space-10);
|
|
3651
3697
|
--otp-font-size: 1.5em;
|
|
3652
|
-
--otp-cell-border: var(--border);
|
|
3698
|
+
--otp-cell-border: var(--border-control);
|
|
3653
3699
|
--otp-cell-bg: var(--surface);
|
|
3654
3700
|
--otp-caret-space: calc((var(--otp-cell-size) - 1ch) / 2 + var(--otp-gap) + 1px);
|
|
3655
3701
|
|
|
@@ -3735,10 +3781,9 @@ small {
|
|
|
3735
3781
|
--otp-cell-border: var(--danger);
|
|
3736
3782
|
}
|
|
3737
3783
|
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
}
|
|
3784
|
+
/* biome-ignore lint/style/noDescendingSpecificity: all invalid routes write the same border, so their order cannot change the state. Keep :user-invalid separate so unsupported engines retain the other routes. */
|
|
3785
|
+
.needs-validation .otp > input:user-invalid ~ span {
|
|
3786
|
+
--otp-cell-border: var(--danger);
|
|
3742
3787
|
}
|
|
3743
3788
|
|
|
3744
3789
|
.otp > input:disabled {
|
|
@@ -4264,7 +4309,6 @@ a.btn[aria-disabled="true"] {
|
|
|
4264
4309
|
--badge-size: var(--badge-dot-size);
|
|
4265
4310
|
--badge-bg: var(--intent, var(--neutral));
|
|
4266
4311
|
--badge-fg: var(--intent-fg, var(--neutral-fg));
|
|
4267
|
-
--badge-border: transparent;
|
|
4268
4312
|
padding: 0;
|
|
4269
4313
|
border: 0;
|
|
4270
4314
|
inline-size: var(--badge-size);
|
|
@@ -4464,10 +4508,11 @@ a.badge[aria-disabled="true"] {
|
|
|
4464
4508
|
}
|
|
4465
4509
|
}
|
|
4466
4510
|
|
|
4467
|
-
/* Decorative leading icon — use .alert-icon on the icon element.
|
|
4511
|
+
/* Decorative leading icon — use .alert-icon on the icon element. display and
|
|
4512
|
+
vertical-align are the no-:has() layout (Degraded tier: the icon sits inline
|
|
4513
|
+
before the text); the grid anatomy below replaces them. */
|
|
4468
4514
|
.alert > .alert-icon {
|
|
4469
4515
|
display: inline-block;
|
|
4470
|
-
flex: none;
|
|
4471
4516
|
inline-size: var(--alert-icon-size);
|
|
4472
4517
|
block-size: var(--alert-icon-size);
|
|
4473
4518
|
font-size: var(--alert-icon-size);
|
|
@@ -4499,45 +4544,44 @@ a.badge[aria-disabled="true"] {
|
|
|
4499
4544
|
|
|
4500
4545
|
/* Grid anatomy — activated by a leading icon, a trailing dismiss button, or
|
|
4501
4546
|
both. Icon and dismiss are pinned to the outer columns; anything else (the
|
|
4502
|
-
message content) flows into the remaining text column.
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
.alert:has(> .alert-icon) {
|
|
4512
|
-
grid-template-columns: auto minmax(0, 1fr);
|
|
4513
|
-
}
|
|
4547
|
+
message content) flows into the remaining text column. Without :has(), the
|
|
4548
|
+
grid rules drop together and the alert keeps ordinary block flow. */
|
|
4549
|
+
.alert:has(> .alert-icon, > .close) {
|
|
4550
|
+
display: grid;
|
|
4551
|
+
/* No grid-template-columns: the icon and close branches below always set
|
|
4552
|
+
it, since one of them is what matched. */
|
|
4553
|
+
align-items: var(--items-align, start);
|
|
4554
|
+
column-gap: var(--space-30);
|
|
4555
|
+
}
|
|
4514
4556
|
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
display: inline-grid;
|
|
4519
|
-
place-items: center;
|
|
4520
|
-
margin-block-start: 0.125em;
|
|
4521
|
-
}
|
|
4557
|
+
.alert:has(> .alert-icon) {
|
|
4558
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
4559
|
+
}
|
|
4522
4560
|
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4561
|
+
.alert:has(> .alert-icon) > .alert-icon {
|
|
4562
|
+
grid-column: 1;
|
|
4563
|
+
grid-row: 1 / -1;
|
|
4564
|
+
display: inline-grid;
|
|
4565
|
+
place-items: center;
|
|
4566
|
+
margin-block-start: 0.125em;
|
|
4567
|
+
}
|
|
4526
4568
|
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4569
|
+
.alert:has(> .alert-icon) > :not(.alert-icon, .close) {
|
|
4570
|
+
grid-column: 2;
|
|
4571
|
+
}
|
|
4530
4572
|
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4573
|
+
.alert:has(> .close) {
|
|
4574
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
4575
|
+
}
|
|
4534
4576
|
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
grid-row: 1;
|
|
4538
|
-
}
|
|
4577
|
+
.alert:has(> .alert-icon):has(> .close) {
|
|
4578
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
4539
4579
|
}
|
|
4540
4580
|
|
|
4581
|
+
.alert:has(> .close) > .close {
|
|
4582
|
+
grid-column: -2 / -1;
|
|
4583
|
+
grid-row: 1;
|
|
4584
|
+
}
|
|
4541
4585
|
/* Links */
|
|
4542
4586
|
.alert :where(a:not(.btn)) {
|
|
4543
4587
|
color: inherit;
|
|
@@ -4896,6 +4940,12 @@ a.badge[aria-disabled="true"] {
|
|
|
4896
4940
|
background: var(--surface-subtle);
|
|
4897
4941
|
}
|
|
4898
4942
|
|
|
4943
|
+
/* The chrome header is a centered row, so prose margins on a bare title only
|
|
4944
|
+
push the band taller and pull the title off the close axis. */
|
|
4945
|
+
:where(dialog).modal > header > :is(h1, h2, h3, h4, h5, h6) {
|
|
4946
|
+
margin-block: 0;
|
|
4947
|
+
}
|
|
4948
|
+
|
|
4899
4949
|
:where(dialog).modal > header > .media {
|
|
4900
4950
|
flex: 1 1 auto;
|
|
4901
4951
|
min-inline-size: 0;
|
|
@@ -4959,19 +5009,20 @@ a.badge[aria-disabled="true"] {
|
|
|
4959
5009
|
|
|
4960
5010
|
:where(dialog).modal.scrollable > form > :not(:is(header, footer)),
|
|
4961
5011
|
:where(dialog).modal.scrollable > .stack > :not(:is(header, footer)) {
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
);
|
|
5012
|
+
/* Declared here, not as a :root alias: an inherited alias arrives already
|
|
5013
|
+
computed, so a --focus-ring-width override on the dialog would not reach
|
|
5014
|
+
it. */
|
|
5015
|
+
--dialog-focus-bleed: calc(var(--focus-outline-offset) + var(--focus-ring-width));
|
|
4966
5016
|
|
|
4967
5017
|
flex: 1 1 auto;
|
|
4968
5018
|
min-block-size: 0;
|
|
4969
|
-
/* Keep focus paint inside the clip
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
5019
|
+
/* Keep focus paint inside the clip, on all four sides (overflow: auto clips
|
|
5020
|
+
both axes), without moving the content. Every outside indicator is
|
|
5021
|
+
--focus-ring-width wide (core/focus.css). On the block axis the reserve
|
|
5022
|
+
reaches into the slot gap, which is wider, so the scrollport never slides
|
|
5023
|
+
under the header or footer. */
|
|
5024
|
+
margin: calc(var(--dialog-focus-bleed) * -1);
|
|
5025
|
+
padding: var(--dialog-focus-bleed);
|
|
4975
5026
|
overflow: auto;
|
|
4976
5027
|
overscroll-behavior: contain;
|
|
4977
5028
|
}
|
|
@@ -5176,6 +5227,7 @@ html.has-modal-open.had-scrollbar {
|
|
|
5176
5227
|
* --drawer-close-reserve (header room for a corner close; zero without a close)
|
|
5177
5228
|
* --drawer-motion-duration (shared by panel and ::backdrop so exit duration
|
|
5178
5229
|
* changes in one place; entry state redefines it longer)
|
|
5230
|
+
* --drawer-motion-ease (panel easing owned by the open/closed state)
|
|
5179
5231
|
*/
|
|
5180
5232
|
:where(dialog).drawer {
|
|
5181
5233
|
--drawer-size: 20rem;
|
|
@@ -5190,6 +5242,7 @@ html.has-modal-open.had-scrollbar {
|
|
|
5190
5242
|
/* Exit is shorter than entry so the panel clears quickly; the scrim reads
|
|
5191
5243
|
the same variable through inheritance and never lags the panel. */
|
|
5192
5244
|
--drawer-motion-duration: var(--duration);
|
|
5245
|
+
--drawer-motion-ease: var(--ease-exit);
|
|
5193
5246
|
|
|
5194
5247
|
/* Keep the surface anchored to the viewport in the top layer. This also
|
|
5195
5248
|
provides the containing block for the absolutely positioned close button.
|
|
@@ -5221,21 +5274,21 @@ html.has-modal-open.had-scrollbar {
|
|
|
5221
5274
|
box-shadow: var(--shadow);
|
|
5222
5275
|
opacity: 0;
|
|
5223
5276
|
transform: translateX(var(--drawer-closed-translate));
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5277
|
+
/* Unsupported discrete behavior drops alone, preserving opacity/transform. */
|
|
5278
|
+
transition-property: opacity, transform, overlay, display;
|
|
5279
|
+
transition-duration: var(--drawer-motion-duration);
|
|
5280
|
+
transition-timing-function: var(--drawer-motion-ease);
|
|
5281
|
+
transition-behavior: allow-discrete;
|
|
5227
5282
|
}
|
|
5228
5283
|
|
|
5229
5284
|
:where(dialog).drawer[open] {
|
|
5230
5285
|
--drawer-motion-duration: var(--duration-slow);
|
|
5286
|
+
--drawer-motion-ease: var(--ease-enter);
|
|
5231
5287
|
display: grid;
|
|
5232
5288
|
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
5233
5289
|
gap: var(--space-40);
|
|
5234
5290
|
opacity: 1;
|
|
5235
5291
|
transform: translateX(0);
|
|
5236
|
-
transition:
|
|
5237
|
-
opacity var(--drawer-motion-duration) var(--ease-enter),
|
|
5238
|
-
transform var(--drawer-motion-duration) var(--ease-enter);
|
|
5239
5292
|
}
|
|
5240
5293
|
|
|
5241
5294
|
/* A form is the drawer's content wrapper when actions need method="dialog".
|
|
@@ -5255,7 +5308,10 @@ html.has-modal-open.had-scrollbar {
|
|
|
5255
5308
|
opacity: 0;
|
|
5256
5309
|
/* Scrim curve stays neutral `ease`; only the panel's character curves
|
|
5257
5310
|
apply. Duration follows --drawer-motion-duration through inheritance. */
|
|
5258
|
-
transition: opacity
|
|
5311
|
+
transition-property: opacity, overlay, display;
|
|
5312
|
+
transition-duration: var(--drawer-motion-duration);
|
|
5313
|
+
transition-timing-function: ease;
|
|
5314
|
+
transition-behavior: allow-discrete;
|
|
5259
5315
|
}
|
|
5260
5316
|
|
|
5261
5317
|
:where(dialog).drawer[data-side="end"] {
|
|
@@ -5289,47 +5345,23 @@ html.has-modal-open.had-scrollbar {
|
|
|
5289
5345
|
exclusion list is easier to trust than its `position: absolute`. */
|
|
5290
5346
|
:where(dialog).drawer > :not(:is(header, footer, form, .close)),
|
|
5291
5347
|
:where(dialog).drawer > form > :not(:is(header, footer, .close)) {
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
);
|
|
5348
|
+
/* Declared here, not as a :root alias: an inherited alias arrives already
|
|
5349
|
+
computed, so a --focus-ring-width override on the drawer would not reach
|
|
5350
|
+
it. */
|
|
5351
|
+
--drawer-focus-bleed: calc(var(--focus-outline-offset) + var(--focus-ring-width));
|
|
5296
5352
|
|
|
5297
5353
|
min-block-size: 0;
|
|
5298
|
-
/* Keep focus paint inside the clip
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5354
|
+
/* Keep focus paint inside the clip, on all four sides (overflow: auto clips
|
|
5355
|
+
both axes), without moving the content. Every outside indicator is
|
|
5356
|
+
--focus-ring-width wide (core/focus.css). On the block axis the reserve
|
|
5357
|
+
reaches into the slot gap, which is wider, so the scrollport never slides
|
|
5358
|
+
under the header or footer. */
|
|
5359
|
+
margin: calc(var(--drawer-focus-bleed) * -1);
|
|
5360
|
+
padding: var(--drawer-focus-bleed);
|
|
5304
5361
|
overflow: auto;
|
|
5305
5362
|
overscroll-behavior: contain;
|
|
5306
5363
|
}
|
|
5307
5364
|
|
|
5308
|
-
@supports (transition-behavior: allow-discrete) {
|
|
5309
|
-
:where(dialog).drawer {
|
|
5310
|
-
transition:
|
|
5311
|
-
opacity var(--drawer-motion-duration) var(--ease-exit),
|
|
5312
|
-
transform var(--drawer-motion-duration) var(--ease-exit),
|
|
5313
|
-
overlay var(--drawer-motion-duration) allow-discrete,
|
|
5314
|
-
display var(--drawer-motion-duration) allow-discrete;
|
|
5315
|
-
}
|
|
5316
|
-
|
|
5317
|
-
:where(dialog).drawer[open] {
|
|
5318
|
-
transition:
|
|
5319
|
-
opacity var(--drawer-motion-duration) var(--ease-enter),
|
|
5320
|
-
transform var(--drawer-motion-duration) var(--ease-enter),
|
|
5321
|
-
overlay var(--drawer-motion-duration) allow-discrete,
|
|
5322
|
-
display var(--drawer-motion-duration) allow-discrete;
|
|
5323
|
-
}
|
|
5324
|
-
|
|
5325
|
-
:where(dialog).drawer::backdrop {
|
|
5326
|
-
transition:
|
|
5327
|
-
opacity var(--drawer-motion-duration) ease,
|
|
5328
|
-
overlay var(--drawer-motion-duration) allow-discrete,
|
|
5329
|
-
display var(--drawer-motion-duration) allow-discrete;
|
|
5330
|
-
}
|
|
5331
|
-
}
|
|
5332
|
-
|
|
5333
5365
|
/* Safari before 16.4 does not support :dir(); keep the ancestor-attribute
|
|
5334
5366
|
path as the Degraded-tier fallback. The :dir() rules are dropped on their
|
|
5335
5367
|
own there, so they need no wrapper. :dir() follows inherited HTML
|
|
@@ -5561,7 +5593,6 @@ picture.bleed > img {
|
|
|
5561
5593
|
.flyout[popover] {
|
|
5562
5594
|
position: fixed;
|
|
5563
5595
|
inset: auto;
|
|
5564
|
-
margin: 0;
|
|
5565
5596
|
}
|
|
5566
5597
|
|
|
5567
5598
|
/* A presentation class may own its *visible* display type, but must not
|
|
@@ -6530,13 +6561,13 @@ picture.bleed > img {
|
|
|
6530
6561
|
* --avatar-size
|
|
6531
6562
|
* --avatar-radius
|
|
6532
6563
|
* --avatar-font-size
|
|
6564
|
+
* --avatar-bg
|
|
6533
6565
|
* --avatar-stack-size
|
|
6534
6566
|
* --avatar-stack-font-size
|
|
6535
6567
|
* --avatar-stack-overlap
|
|
6536
6568
|
* --avatar-stack-ring
|
|
6537
6569
|
*
|
|
6538
6570
|
* Internal:
|
|
6539
|
-
* --avatar-bg
|
|
6540
6571
|
* --avatar-fg
|
|
6541
6572
|
* --avatar-status-translate-x
|
|
6542
6573
|
*/
|
|
@@ -6614,16 +6645,13 @@ picture.bleed > img {
|
|
|
6614
6645
|
|
|
6615
6646
|
.avatar > img,
|
|
6616
6647
|
.avatar > picture > img {
|
|
6648
|
+
display: block;
|
|
6617
6649
|
inline-size: 100%;
|
|
6618
6650
|
block-size: 100%;
|
|
6619
6651
|
object-fit: cover;
|
|
6620
6652
|
border-radius: inherit;
|
|
6621
6653
|
}
|
|
6622
6654
|
|
|
6623
|
-
.avatar > picture > img {
|
|
6624
|
-
display: block;
|
|
6625
|
-
}
|
|
6626
|
-
|
|
6627
6655
|
button.avatar {
|
|
6628
6656
|
appearance: none;
|
|
6629
6657
|
cursor: pointer;
|
|
@@ -6717,19 +6745,6 @@ a.avatar[aria-disabled="true"] {
|
|
|
6717
6745
|
--avatar-stack-font-size: var(--font-size-lg);
|
|
6718
6746
|
}
|
|
6719
6747
|
|
|
6720
|
-
/* Infer the stack scale from its avatars. Progressive: without :has() the
|
|
6721
|
-
explicit .avatar-stack.sm/.lg classes and the default size still size the
|
|
6722
|
-
group, only the inference is lost. */
|
|
6723
|
-
.avatar-stack:has(> .avatar.sm) {
|
|
6724
|
-
--avatar-stack-size: 1.75rem;
|
|
6725
|
-
--avatar-stack-font-size: var(--font-size-xs);
|
|
6726
|
-
}
|
|
6727
|
-
|
|
6728
|
-
.avatar-stack:has(> .avatar.lg) {
|
|
6729
|
-
--avatar-stack-size: 3rem;
|
|
6730
|
-
--avatar-stack-font-size: var(--font-size-lg);
|
|
6731
|
-
}
|
|
6732
|
-
|
|
6733
6748
|
/* Overlapping avatars hide their neighbour: a translucent fill (--surface-subtle
|
|
6734
6749
|
on a glass palette) would show the next avatar's edge through it, most of all
|
|
6735
6750
|
when hover raises one above the other. The fill is layered over the opaque
|
|
@@ -7235,7 +7250,8 @@ a.avatar[aria-disabled="true"] {
|
|
|
7235
7250
|
* destinations. Anything else focusable in a step is out of contract.
|
|
7236
7251
|
*
|
|
7237
7252
|
* The framework never hides an authored label to solve a layout problem. A
|
|
7238
|
-
* labelled row
|
|
7253
|
+
* labelled row wraps its labels, then scrolls if its markers or authored item
|
|
7254
|
+
* minimum outgrow the available space. An `actual-container` size
|
|
7239
7255
|
* context buys it one thing: the wide representation, marker beside label.
|
|
7240
7256
|
*
|
|
7241
7257
|
* See docs/design-notes/steps.md for the threshold, the selector shapes, the
|
|
@@ -7371,10 +7387,11 @@ a.avatar[aria-disabled="true"] {
|
|
|
7371
7387
|
/* Horizontal ------------------------------------------------------------- */
|
|
7372
7388
|
|
|
7373
7389
|
.steps-horizontal {
|
|
7374
|
-
--step-min:
|
|
7390
|
+
--step-min: var(--step-size);
|
|
7375
7391
|
--step-gap: var(--space-20);
|
|
7376
7392
|
|
|
7377
7393
|
inline-size: 100%;
|
|
7394
|
+
column-gap: var(--step-gap);
|
|
7378
7395
|
overflow-x: auto;
|
|
7379
7396
|
/* Stated, never left to `overflow-x` alone: one scrolling axis computes the
|
|
7380
7397
|
other to `auto`, and a row hugs fractional content closely enough that a
|
|
@@ -7383,11 +7400,22 @@ a.avatar[aria-disabled="true"] {
|
|
|
7383
7400
|
overscroll-behavior-inline: contain;
|
|
7384
7401
|
}
|
|
7385
7402
|
|
|
7403
|
+
/* A row that loads mid-flow starts scrolled to its current step; engines
|
|
7404
|
+
without the property start at the first. */
|
|
7405
|
+
.steps-horizontal > [aria-current="step"] {
|
|
7406
|
+
/* biome-ignore lint/correctness/noUnknownProperty: CSS Scroll Snap 2 property that Biome's list predates; an engine without it drops the declaration. */
|
|
7407
|
+
scroll-initial-target: nearest;
|
|
7408
|
+
}
|
|
7409
|
+
|
|
7386
7410
|
/* A scroll container clips at the padding edge, so a navigable row reserves the
|
|
7387
|
-
focus ring's own width and offset
|
|
7388
|
-
link has a ring to
|
|
7411
|
+
focus ring's own width and offset, and focus scrolls a link in that far from
|
|
7412
|
+
the inline clip. Only this composition scrolls, and only a link has a ring to
|
|
7413
|
+
protect. */
|
|
7389
7414
|
.steps-horizontal:has(a[href]) {
|
|
7415
|
+
/* Wrapped names can fill their cell right up to either inline clip edge. */
|
|
7416
|
+
padding-inline: calc(var(--focus-outline-offset) + var(--focus-ring-width));
|
|
7390
7417
|
padding-block: calc(var(--focus-outline-offset) + var(--focus-ring-width));
|
|
7418
|
+
scroll-padding-inline: calc(var(--focus-outline-offset) + var(--focus-ring-width));
|
|
7391
7419
|
}
|
|
7392
7420
|
|
|
7393
7421
|
/* One explicit row, the marker's: a `.step-label` is auto-placed into an
|
|
@@ -7399,6 +7427,8 @@ a.avatar[aria-disabled="true"] {
|
|
|
7399
7427
|
.steps-horizontal > li {
|
|
7400
7428
|
display: grid;
|
|
7401
7429
|
flex: 1 0 var(--step-min);
|
|
7430
|
+
/* Bound each label to its share so it cannot change the marker pitch. */
|
|
7431
|
+
grid-template-columns: minmax(0, 1fr);
|
|
7402
7432
|
grid-template-rows: var(--step-size);
|
|
7403
7433
|
gap: var(--step-gap);
|
|
7404
7434
|
justify-items: center;
|
|
@@ -7406,9 +7436,42 @@ a.avatar[aria-disabled="true"] {
|
|
|
7406
7436
|
text-align: center;
|
|
7407
7437
|
}
|
|
7408
7438
|
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7439
|
+
.steps-horizontal .step-label {
|
|
7440
|
+
/* Even an unbroken name must fit its share instead of overlapping a peer.
|
|
7441
|
+
Hyphenation, when `lang` allows it, is tried before an arbitrary break. */
|
|
7442
|
+
overflow-wrap: anywhere;
|
|
7443
|
+
hyphens: auto;
|
|
7444
|
+
}
|
|
7445
|
+
|
|
7446
|
+
/* Half end cells put the outer markers on the row's edges without changing
|
|
7447
|
+
their pitch, so the track lines up with the text around it, as the compact
|
|
7448
|
+
and inline forms do. The marker's half-width keeps the first connector equal
|
|
7449
|
+
to its peers.
|
|
7450
|
+
|
|
7451
|
+
Source order is load-bearing: these tie the `:empty` rules below and the
|
|
7452
|
+
inline query on specificity, and both must win over them. */
|
|
7453
|
+
.steps-horizontal > li:is(:first-child, :last-child) {
|
|
7454
|
+
flex: 0.5 0 calc((var(--step-min) + var(--step-size)) / 2);
|
|
7455
|
+
min-inline-size: max(var(--step-size), calc((var(--step-min) + var(--step-size)) / 2));
|
|
7456
|
+
}
|
|
7457
|
+
|
|
7458
|
+
.steps-horizontal > li:first-child {
|
|
7459
|
+
justify-items: start;
|
|
7460
|
+
text-align: start;
|
|
7461
|
+
}
|
|
7462
|
+
|
|
7463
|
+
.steps-horizontal > li:first-child::after {
|
|
7464
|
+
inset-inline-start: var(--step-size);
|
|
7465
|
+
inline-size: calc(200% - var(--step-size) * 2 + var(--step-gap));
|
|
7466
|
+
}
|
|
7467
|
+
|
|
7468
|
+
.steps-horizontal > li:last-child {
|
|
7469
|
+
justify-items: end;
|
|
7470
|
+
text-align: end;
|
|
7471
|
+
}
|
|
7472
|
+
|
|
7473
|
+
/* An empty step ignores any authored reading-width floor and needs only its
|
|
7474
|
+
marker. That lets five of them fit a phone without a size context.
|
|
7412
7475
|
|
|
7413
7476
|
`justify-items` is not an override of the row's centring: the marker is
|
|
7414
7477
|
centred because a label sits under it and the two share a centre, and with no
|
|
@@ -7418,9 +7481,8 @@ a.avatar[aria-disabled="true"] {
|
|
|
7418
7481
|
the text around it, the way the inline representation does.
|
|
7419
7482
|
|
|
7420
7483
|
`:empty` says exactly what the markup says, the way `.badge:empty` does, and
|
|
7421
|
-
it reads the item's own content: bare text
|
|
7422
|
-
|
|
7423
|
-
floor and skew the row. `:empty` also keeps the compact form reachable on the
|
|
7484
|
+
it reads the item's own content: bare text keeps the same authored floor as
|
|
7485
|
+
its wrapped peers. `:empty` also keeps the compact form reachable on the
|
|
7424
7486
|
Degraded tier, where only the inline bonus needs `:has()`. */
|
|
7425
7487
|
.steps-horizontal > li:empty {
|
|
7426
7488
|
flex: 1 1 0;
|
|
@@ -7430,6 +7492,8 @@ a.avatar[aria-disabled="true"] {
|
|
|
7430
7492
|
|
|
7431
7493
|
.steps-horizontal > li:empty::after {
|
|
7432
7494
|
inset-inline-start: var(--step-size);
|
|
7495
|
+
/* Marker-only end cells keep full shares, unlike labelled end cells. */
|
|
7496
|
+
inline-size: calc(100% - var(--step-size) + var(--step-gap));
|
|
7433
7497
|
}
|
|
7434
7498
|
|
|
7435
7499
|
.steps-horizontal > li:empty:last-child {
|
|
@@ -7437,12 +7501,12 @@ a.avatar[aria-disabled="true"] {
|
|
|
7437
7501
|
}
|
|
7438
7502
|
|
|
7439
7503
|
/* The pseudo-element is marker-height so --step-connector may be any complete
|
|
7440
|
-
background value. */
|
|
7504
|
+
background value; its length also crosses the gap to the next equal track. */
|
|
7441
7505
|
.steps-horizontal > li::after {
|
|
7442
7506
|
position: absolute;
|
|
7443
7507
|
inset-block-start: 0;
|
|
7444
7508
|
inset-inline-start: calc(50% + var(--step-size) / 2);
|
|
7445
|
-
inline-size: calc(100% - var(--step-size));
|
|
7509
|
+
inline-size: calc(100% - var(--step-size) + var(--step-gap));
|
|
7446
7510
|
block-size: var(--step-size);
|
|
7447
7511
|
background: var(
|
|
7448
7512
|
--step-connector,
|
|
@@ -7461,6 +7525,11 @@ a.avatar[aria-disabled="true"] {
|
|
|
7461
7525
|
* written — a row out of contract is not owed a curated fallback.
|
|
7462
7526
|
*/
|
|
7463
7527
|
@container actual-container (inline-size >= 60rem) {
|
|
7528
|
+
.steps-horizontal:has(.step-label) {
|
|
7529
|
+
/* Inline connectors already own the spacing between marker-label groups. */
|
|
7530
|
+
column-gap: 0;
|
|
7531
|
+
}
|
|
7532
|
+
|
|
7464
7533
|
.steps-horizontal:has(.step-label) > li {
|
|
7465
7534
|
display: flex;
|
|
7466
7535
|
flex: 1 1 auto;
|
|
@@ -7727,8 +7796,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
7727
7796
|
|
|
7728
7797
|
/* nav-link serves two contracts: plain page navigation (aria-current="page")
|
|
7729
7798
|
and scrollspy, which toggles aria-current="location" on the active link.
|
|
7730
|
-
|
|
7731
|
-
removed everywhere else, never serialized as aria-current="false".
|
|
7799
|
+
Those two values are the contract; any other, "false" included, is inert.
|
|
7732
7800
|
Selection never changes text metrics: the shared medium weight is kept and
|
|
7733
7801
|
the current link is carried by the selected accent. */
|
|
7734
7802
|
.nav-link:hover {
|
|
@@ -7739,7 +7807,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
7739
7807
|
/* Current last, equal specificity to :hover (0,2,0): a hovered current link
|
|
7740
7808
|
keeps the selected accent instead of dropping to --text. The surface fill is
|
|
7741
7809
|
shared with the hover rule above. */
|
|
7742
|
-
.nav-link[aria-current] {
|
|
7810
|
+
.nav-link:is([aria-current="page"], [aria-current="location"]) {
|
|
7743
7811
|
background: var(--surface-subtle);
|
|
7744
7812
|
color: var(--state-selected);
|
|
7745
7813
|
}
|
|
@@ -7752,7 +7820,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
7752
7820
|
above keeps the light chip where color-mix is missing, with ink that is
|
|
7753
7821
|
legible on it. */
|
|
7754
7822
|
@supports (color: color-mix(in oklch, red, white)) {
|
|
7755
|
-
.nav-link:is(:hover, [aria-current]) {
|
|
7823
|
+
.nav-link:is(:hover, [aria-current="page"], [aria-current="location"]) {
|
|
7756
7824
|
background: color-mix(in oklch, currentColor 10%, transparent);
|
|
7757
7825
|
}
|
|
7758
7826
|
|
|
@@ -7761,7 +7829,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
7761
7829
|
}
|
|
7762
7830
|
|
|
7763
7831
|
/* After :hover so a hovered current link keeps the selected accent. */
|
|
7764
|
-
.nav-link[aria-current] {
|
|
7832
|
+
.nav-link:is([aria-current="page"], [aria-current="location"]) {
|
|
7765
7833
|
color: var(--ui-fg, var(--state-selected));
|
|
7766
7834
|
}
|
|
7767
7835
|
}
|
|
@@ -7795,13 +7863,16 @@ a.avatar[aria-disabled="true"] {
|
|
|
7795
7863
|
follows the forced palette without a forced-colors block and never touches
|
|
7796
7864
|
the row separators. Navbar horizontal links keep color + surface only. The
|
|
7797
7865
|
positioning context lives on every row link, so becoming current never moves
|
|
7798
|
-
an app-positioned decoration to a new containing block.
|
|
7799
|
-
|
|
7866
|
+
an app-positioned decoration to a new containing block.
|
|
7867
|
+
The trait sits centered in the link's inline padding with rounded ends: flush
|
|
7868
|
+
on the edge, a square line read as pasted onto the rounded fill. */
|
|
7869
|
+
.nav-list .nav-link:is([aria-current="page"], [aria-current="location"])::before {
|
|
7800
7870
|
content: "";
|
|
7801
7871
|
position: absolute;
|
|
7802
7872
|
inset-block: var(--space-20);
|
|
7803
|
-
inset-inline-start:
|
|
7873
|
+
inset-inline-start: calc((var(--space-20) - 2px) / 2);
|
|
7804
7874
|
border-inline-start: 2px solid currentColor;
|
|
7875
|
+
border-radius: var(--radius-full);
|
|
7805
7876
|
}
|
|
7806
7877
|
|
|
7807
7878
|
/*
|
|
@@ -7810,14 +7881,21 @@ a.avatar[aria-disabled="true"] {
|
|
|
7810
7881
|
* Docs: docs/pages/components/breadcrumb.md
|
|
7811
7882
|
*
|
|
7812
7883
|
* Inline list separated by "/". Mark the current page with `aria-current="page"`
|
|
7813
|
-
* either on the item or on the current link.
|
|
7884
|
+
* either on the item or on the current link. Ancestor labels sit in an <a> or a
|
|
7885
|
+
* <span>, the box that truncates.
|
|
7886
|
+
*
|
|
7887
|
+
* Breadcrumbs are a single navigation hierarchy. Wrapping makes hierarchy look
|
|
7888
|
+
* like separate rows and causes unstable header height, and scrolling hides the
|
|
7889
|
+
* root behind a scrollbar. A trail that does not fit truncates instead:
|
|
7890
|
+
* ancestors first, the current page last, each label behind an ellipsis.
|
|
7814
7891
|
*/
|
|
7892
|
+
|
|
7815
7893
|
.breadcrumb {
|
|
7816
7894
|
display: flex;
|
|
7817
|
-
flex-
|
|
7818
|
-
flex-wrap: wrap;
|
|
7895
|
+
flex-wrap: nowrap;
|
|
7819
7896
|
align-items: center;
|
|
7820
7897
|
gap: var(--space-10);
|
|
7898
|
+
min-inline-size: 0;
|
|
7821
7899
|
margin: 0;
|
|
7822
7900
|
padding: 0;
|
|
7823
7901
|
color: var(--text-subtle);
|
|
@@ -7825,15 +7903,74 @@ a.avatar[aria-disabled="true"] {
|
|
|
7825
7903
|
list-style: none;
|
|
7826
7904
|
}
|
|
7827
7905
|
|
|
7828
|
-
|
|
7829
|
-
|
|
7906
|
+
/* The landmark around the trail must shrink with it in a flex or grid row, or
|
|
7907
|
+
it keeps the trail's full width and the labels never get to truncate.
|
|
7908
|
+
Without :has() the landmark keeps its width; min-inline-size: 0 on the
|
|
7909
|
+
wrapper is the fallback. */
|
|
7910
|
+
:where(nav:has(> .breadcrumb)) {
|
|
7911
|
+
min-inline-size: 0;
|
|
7912
|
+
}
|
|
7913
|
+
|
|
7914
|
+
/* Grid, not flex: under a min-content constraint a grid track keeps its base
|
|
7915
|
+
size, which is the label's minimum below, so the item's own minimum is
|
|
7916
|
+
separator + label floor without anyone measuring a "/". A flex item would
|
|
7917
|
+
report the full label as its minimum and never shrink.
|
|
7918
|
+
|
|
7919
|
+
Ancestors take the shrink before the current page, so the location is the
|
|
7920
|
+
last thing to lose characters. The ratio is deliberately huge: at 1000 the
|
|
7921
|
+
current page still took a fraction of a pixel, enough to trigger its
|
|
7922
|
+
ellipsis while ancestors had room. Its own factor stays 1, because flex
|
|
7923
|
+
distributes only a share of the deficit when the unfrozen factors sum below
|
|
7924
|
+
one. Keyed on the last item, the current page by convention, so the order
|
|
7925
|
+
holds without :has(). */
|
|
7926
|
+
.breadcrumb > li {
|
|
7927
|
+
display: inline-grid;
|
|
7928
|
+
flex: 0 1000000 auto;
|
|
7929
|
+
grid-auto-flow: column;
|
|
7830
7930
|
align-items: center;
|
|
7831
|
-
|
|
7931
|
+
white-space: nowrap;
|
|
7832
7932
|
}
|
|
7833
7933
|
|
|
7834
|
-
.breadcrumb
|
|
7835
|
-
|
|
7934
|
+
.breadcrumb > li:last-child {
|
|
7935
|
+
flex-shrink: 1;
|
|
7936
|
+
}
|
|
7937
|
+
|
|
7938
|
+
/* A margin rather than a grid gap, so the separator keeps its space when the
|
|
7939
|
+
current page below lays it out inline. The weight stays normal there, where
|
|
7940
|
+
it would inherit the current page's. */
|
|
7941
|
+
.breadcrumb > li + li::before {
|
|
7942
|
+
margin-inline-end: var(--space-10);
|
|
7836
7943
|
color: var(--text-subtle);
|
|
7944
|
+
font-weight: var(--font-weight-normal);
|
|
7945
|
+
content: "/";
|
|
7946
|
+
}
|
|
7947
|
+
|
|
7948
|
+
/* The ellipsis lives on the label, not on the item: an element never clips
|
|
7949
|
+
its own outline, but an overflow-hidden item would clip its link's focus
|
|
7950
|
+
line, which sits outside the link box.
|
|
7951
|
+
|
|
7952
|
+
The floor keeps a truncated label readable as a step ("Po…") instead of
|
|
7953
|
+
collapsing to its separator. calc-size() lets a shorter label keep its own
|
|
7954
|
+
width; without it, the plain 2em floor leaves a label under 2em a little
|
|
7955
|
+
trailing room. */
|
|
7956
|
+
.breadcrumb > li > :is(a, span) {
|
|
7957
|
+
min-inline-size: 2em;
|
|
7958
|
+
min-inline-size: calc-size(max-content, min(size, 2em));
|
|
7959
|
+
overflow: hidden;
|
|
7960
|
+
text-overflow: ellipsis;
|
|
7961
|
+
}
|
|
7962
|
+
|
|
7963
|
+
/* The current page written as bare text has no label box, so its item
|
|
7964
|
+
truncates itself. Only with no element inside: an aria-current item that
|
|
7965
|
+
holds a link would clip that link's focus line, and keeps the label rule
|
|
7966
|
+
above instead. Its own rule, never merged with the one above: a browser
|
|
7967
|
+
without :has() drops it alone, and bare text then keeps its full width. */
|
|
7968
|
+
.breadcrumb > li[aria-current="page"]:not(:has(> *)) {
|
|
7969
|
+
display: block;
|
|
7970
|
+
min-inline-size: 2em;
|
|
7971
|
+
min-inline-size: calc-size(max-content, min(size, 2em));
|
|
7972
|
+
overflow: hidden;
|
|
7973
|
+
text-overflow: ellipsis;
|
|
7837
7974
|
}
|
|
7838
7975
|
|
|
7839
7976
|
.breadcrumb a {
|
|
@@ -7867,6 +8004,11 @@ a.avatar[aria-disabled="true"] {
|
|
|
7867
8004
|
* Truncated ranges are plain text (`.muted`, `aria-hidden="true"`), not .btn
|
|
7868
8005
|
* — they are decorative, not actionable. They still get the same box sizing
|
|
7869
8006
|
* as .btn below so the row stays visually aligned.
|
|
8007
|
+
*
|
|
8008
|
+
* The row wraps when it does not fit: every page stays visible and
|
|
8009
|
+
* clickable. A strip that scrolled hid pages behind a scrollbar that clicking
|
|
8010
|
+
* cannot move, since a click changes the page. Keeping the rendered range
|
|
8011
|
+
* compact is the application's job.
|
|
7870
8012
|
*/
|
|
7871
8013
|
.pagination {
|
|
7872
8014
|
display: flex;
|
|
@@ -7916,8 +8058,18 @@ a.avatar[aria-disabled="true"] {
|
|
|
7916
8058
|
* author class can then adjust a vertical strip without restating the
|
|
7917
8059
|
* attribute to reach its specificity.
|
|
7918
8060
|
*
|
|
8061
|
+
* A horizontal strip is one axis of navigation, so it never wraps: a second
|
|
8062
|
+
* line no longer matches the Left/Right model and reads as a nested level.
|
|
8063
|
+
* When the tabs do not fit, the strip scrolls without a scrollbar; focus
|
|
8064
|
+
* scrolls the active tab into view.
|
|
8065
|
+
*
|
|
7919
8066
|
* Public hooks:
|
|
8067
|
+
* --tab-flex
|
|
7920
8068
|
* --tab-gap
|
|
8069
|
+
*
|
|
8070
|
+
* Internal:
|
|
8071
|
+
* --tab-edge
|
|
8072
|
+
* --tab-fade-dir (read inside @keyframes, so it resolves per element)
|
|
7921
8073
|
*/
|
|
7922
8074
|
/* Intent boundary — inherited intents do not tint this component.
|
|
7923
8075
|
Local intent classes still win because :where() has zero specificity. */
|
|
@@ -7928,27 +8080,129 @@ a.avatar[aria-disabled="true"] {
|
|
|
7928
8080
|
.tabs {
|
|
7929
8081
|
display: flex;
|
|
7930
8082
|
flex-direction: row;
|
|
7931
|
-
flex-wrap:
|
|
8083
|
+
flex-wrap: nowrap;
|
|
7932
8084
|
gap: var(--space-10);
|
|
7933
8085
|
margin: 0;
|
|
7934
8086
|
padding: 0;
|
|
8087
|
+
/* Block overflow is hidden, not auto: overflow-x: auto alone computes the
|
|
8088
|
+
block axis to auto, so a sub-pixel line box would add a vertical
|
|
8089
|
+
scrollbar inside the strip. Nothing paints outside it: the focus outline
|
|
8090
|
+
is inset and the indicator sits inside the tab. */
|
|
8091
|
+
overflow: auto hidden;
|
|
8092
|
+
overscroll-behavior-inline: contain;
|
|
8093
|
+
/* why: a visible bar sits on the indicator and the hairline and reads as a
|
|
8094
|
+
second underline. The strip has its own scroll controls: selecting the
|
|
8095
|
+
clipped tab, or focusing it, brings it into view with the next one
|
|
8096
|
+
hinted by scroll-padding; arrow keys and swipes do the same. */
|
|
8097
|
+
scrollbar-width: none;
|
|
8098
|
+
/* Focus scrolls a tab in with room to spare, hinting at the next one; the
|
|
8099
|
+
edge fade below spans the same inset, so a revealed tab stays clear of it. */
|
|
8100
|
+
--tab-edge: var(--space-60);
|
|
8101
|
+
|
|
8102
|
+
scroll-padding-inline: var(--tab-edge);
|
|
7935
8103
|
border-block-end: var(--border-width) solid var(--border);
|
|
7936
8104
|
list-style: none;
|
|
7937
8105
|
}
|
|
7938
8106
|
|
|
8107
|
+
/* The landmark around link tabs is not a scroll container, so as a flex or
|
|
8108
|
+
grid item it would keep the strip's full width and push the page into
|
|
8109
|
+
scrolling. Letting it shrink hands the overflow back to the strip.
|
|
8110
|
+
Without :has() the landmark keeps its width; min-inline-size: 0 on the
|
|
8111
|
+
wrapper is the fallback. */
|
|
8112
|
+
:where(nav:has(> .tabs)) {
|
|
8113
|
+
min-inline-size: 0;
|
|
8114
|
+
}
|
|
8115
|
+
|
|
8116
|
+
/* Items keep their size and the strip scrolls instead. Covers both
|
|
8117
|
+
anatomies: .tab directly in a tablist, or <li> in a navigation list.
|
|
8118
|
+
--tab-flex: 1 1 0 shares the width equally; an item still never shrinks
|
|
8119
|
+
below its label (flex items keep min-content), so a strip that cannot fit
|
|
8120
|
+
scrolls as before. */
|
|
8121
|
+
.tabs > * {
|
|
8122
|
+
flex: var(--tab-flex, none);
|
|
8123
|
+
}
|
|
8124
|
+
|
|
7939
8125
|
.tabs:where([aria-orientation="vertical"]) {
|
|
7940
8126
|
display: grid;
|
|
8127
|
+
overflow: visible;
|
|
7941
8128
|
align-content: start;
|
|
7942
8129
|
justify-items: stretch;
|
|
7943
8130
|
border-block-end: 0;
|
|
7944
8131
|
border-inline-end: var(--border-width) solid var(--border);
|
|
7945
8132
|
}
|
|
7946
8133
|
|
|
8134
|
+
/* Edge fade: with no scrollbar, the strip fades the edges it can still scroll
|
|
8135
|
+
toward. It runs on the strip's own inline timeline, which is inactive on a
|
|
8136
|
+
strip that fits or a vertical rail, so those stay unmasked. Keep the
|
|
8137
|
+
@supports: without scroll timelines the animation would end at once and pin
|
|
8138
|
+
its last mask. It follows scroll, not time, so reduced motion keeps it. */
|
|
8139
|
+
@supports (animation-timeline: scroll()) {
|
|
8140
|
+
.tabs {
|
|
8141
|
+
--tab-fade-dir: to right;
|
|
8142
|
+
|
|
8143
|
+
animation: actual-tabs-fade linear both;
|
|
8144
|
+
animation-timeline: scroll(self inline);
|
|
8145
|
+
}
|
|
8146
|
+
|
|
8147
|
+
/* Gradient keywords are physical; :dir() follows the strip's own
|
|
8148
|
+
direction, so an LTR island in an RTL page keeps its fade on the right. */
|
|
8149
|
+
.tabs:dir(rtl) {
|
|
8150
|
+
--tab-fade-dir: to left;
|
|
8151
|
+
}
|
|
8152
|
+
}
|
|
8153
|
+
|
|
8154
|
+
/* A strip that loads on a current link tab starts scrolled to it. Horizontal
|
|
8155
|
+
only: the strip is then its nearest scroll container, while a vertical
|
|
8156
|
+
rail would hand the target to the page. Engines without the property start
|
|
8157
|
+
at the first tab, and the fade says there is more. */
|
|
8158
|
+
:where(.tabs:not([aria-orientation="vertical"]))
|
|
8159
|
+
.tab:is([aria-current="page"], [aria-current="true"]) {
|
|
8160
|
+
/* biome-ignore lint/correctness/noUnknownProperty: CSS Scroll Snap 2 property that Biome's list predates; an engine without it drops the declaration. */
|
|
8161
|
+
scroll-initial-target: nearest;
|
|
8162
|
+
}
|
|
8163
|
+
|
|
8164
|
+
/* Forced colors keep every label at full contrast. */
|
|
8165
|
+
@media (forced-colors: active) {
|
|
8166
|
+
.tabs {
|
|
8167
|
+
animation: none;
|
|
8168
|
+
}
|
|
8169
|
+
}
|
|
8170
|
+
|
|
8171
|
+
/* mask-image does not interpolate, so each mask holds until halfway to the
|
|
8172
|
+
next keyframe: the start fade appears within the first pixels of scroll and
|
|
8173
|
+
the end fade drops at the very end. */
|
|
8174
|
+
@keyframes actual-tabs-fade {
|
|
8175
|
+
0% {
|
|
8176
|
+
mask-image: linear-gradient(
|
|
8177
|
+
var(--tab-fade-dir),
|
|
8178
|
+
#000 calc(100% - var(--tab-edge)),
|
|
8179
|
+
transparent
|
|
8180
|
+
);
|
|
8181
|
+
}
|
|
8182
|
+
|
|
8183
|
+
1%,
|
|
8184
|
+
99% {
|
|
8185
|
+
mask-image: linear-gradient(
|
|
8186
|
+
var(--tab-fade-dir),
|
|
8187
|
+
transparent,
|
|
8188
|
+
#000 var(--tab-edge) calc(100% - var(--tab-edge)),
|
|
8189
|
+
transparent
|
|
8190
|
+
);
|
|
8191
|
+
}
|
|
8192
|
+
|
|
8193
|
+
100% {
|
|
8194
|
+
mask-image: linear-gradient(var(--tab-fade-dir), transparent, #000 var(--tab-edge));
|
|
8195
|
+
}
|
|
8196
|
+
}
|
|
8197
|
+
|
|
7947
8198
|
.tab {
|
|
7948
8199
|
/* Positioning context for the selected indicator pseudo. */
|
|
7949
8200
|
position: relative;
|
|
7950
8201
|
display: inline-flex;
|
|
7951
8202
|
align-items: center;
|
|
8203
|
+
/* A tab wider than its label (--tab-flex) centers it; at natural width this
|
|
8204
|
+
changes nothing, and a vertical rail resets it to the start below. */
|
|
8205
|
+
justify-content: center;
|
|
7952
8206
|
gap: var(--tab-gap, 0.375em);
|
|
7953
8207
|
min-block-size: var(--control-size);
|
|
7954
8208
|
padding-inline: var(--space-30);
|
|
@@ -7962,12 +8216,18 @@ a.avatar[aria-disabled="true"] {
|
|
|
7962
8216
|
the strip. The active state is carried by color and the indicator line. */
|
|
7963
8217
|
font-weight: var(--font-weight-semibold);
|
|
7964
8218
|
text-decoration: none;
|
|
8219
|
+
/* A label breaking inside a one-line strip is the same defect as the strip
|
|
8220
|
+
wrapping. */
|
|
8221
|
+
white-space: nowrap;
|
|
7965
8222
|
cursor: pointer;
|
|
7966
8223
|
}
|
|
7967
8224
|
|
|
8225
|
+
/* A vertical strip has no inline axis to protect: long labels wrap within the
|
|
8226
|
+
rail instead of overflowing it. */
|
|
7968
8227
|
.tabs:where([aria-orientation="vertical"]) .tab {
|
|
7969
8228
|
justify-content: flex-start;
|
|
7970
8229
|
padding-inline-end: var(--space-40);
|
|
8230
|
+
white-space: normal;
|
|
7971
8231
|
}
|
|
7972
8232
|
|
|
7973
8233
|
.tab:is(:disabled, [aria-disabled="true"]) {
|
|
@@ -7998,8 +8258,13 @@ a.avatar[aria-disabled="true"] {
|
|
|
7998
8258
|
border-end-end-radius: 0;
|
|
7999
8259
|
}
|
|
8000
8260
|
|
|
8261
|
+
/* A link tab is current on aria-current="page". "true" is for the one place
|
|
8262
|
+
where no page token fits: a navigation flyout trigger whose panel holds the
|
|
8263
|
+
current page. State selectors name their values, never the attribute's
|
|
8264
|
+
presence, so "false" (what React and Vue serialize for a false binding)
|
|
8265
|
+
stays inert. */
|
|
8001
8266
|
.tab[aria-selected="true"],
|
|
8002
|
-
.tab[aria-current="page"] {
|
|
8267
|
+
.tab:is([aria-current="page"], [aria-current="true"]) {
|
|
8003
8268
|
color: var(--intent, var(--state-selected));
|
|
8004
8269
|
}
|
|
8005
8270
|
|
|
@@ -8008,7 +8273,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
8008
8273
|
forced colors and erase the distinction. currentColor follows the palette the
|
|
8009
8274
|
UA forces, so no forced-colors block is needed. */
|
|
8010
8275
|
.tab[aria-selected="true"]::after,
|
|
8011
|
-
.tab[aria-current="page"]::after {
|
|
8276
|
+
.tab:is([aria-current="page"], [aria-current="true"])::after {
|
|
8012
8277
|
content: "";
|
|
8013
8278
|
position: absolute;
|
|
8014
8279
|
inset-inline: 0;
|
|
@@ -8017,13 +8282,20 @@ a.avatar[aria-disabled="true"] {
|
|
|
8017
8282
|
}
|
|
8018
8283
|
|
|
8019
8284
|
.tabs:where([aria-orientation="vertical"]) .tab[aria-selected="true"]::after,
|
|
8020
|
-
.tabs:where([aria-orientation="vertical"])
|
|
8285
|
+
.tabs:where([aria-orientation="vertical"])
|
|
8286
|
+
.tab:is([aria-current="page"], [aria-current="true"])::after {
|
|
8021
8287
|
inset-block: 0;
|
|
8022
8288
|
inset-inline: auto 0;
|
|
8023
8289
|
border-block-end: 0;
|
|
8024
8290
|
border-inline-end: 2px solid currentColor;
|
|
8025
8291
|
}
|
|
8026
8292
|
|
|
8293
|
+
/* A shared width (--tab-flex) lands on the <li> of a link tab; the tab fills
|
|
8294
|
+
it, so the indicator and the hit area span the whole share. */
|
|
8295
|
+
.tabs > li > .tab {
|
|
8296
|
+
inline-size: 100%;
|
|
8297
|
+
}
|
|
8298
|
+
|
|
8027
8299
|
/*
|
|
8028
8300
|
* Scrollspy — behavior hook for navigation that marks the active section
|
|
8029
8301
|
* while the page scrolls.
|
|
@@ -8034,7 +8306,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
8034
8306
|
* <nav class="scrollspy">
|
|
8035
8307
|
* containing <a href="#section"> links and toggles aria-current="location" on
|
|
8036
8308
|
* the link whose target is in view. Pair with .nav-list and .nav-link, or style
|
|
8037
|
-
* [aria-current] yourself.
|
|
8309
|
+
* [aria-current="location"] yourself.
|
|
8038
8310
|
*
|
|
8039
8311
|
* Progressive enhancement: on browsers that support :target-current,
|
|
8040
8312
|
* this file enables the native target group and marks the active link.
|
|
@@ -8149,9 +8421,10 @@ a.avatar[aria-disabled="true"] {
|
|
|
8149
8421
|
.list > :where(.list-item),
|
|
8150
8422
|
.list > li > .list-item {
|
|
8151
8423
|
display: flex;
|
|
8152
|
-
/*
|
|
8153
|
-
|
|
8154
|
-
|
|
8424
|
+
/* Centered fits the short rows lists mostly hold. Read through
|
|
8425
|
+
--items-align so .items-start on the row or the list wins without
|
|
8426
|
+
outranking this selector — the opt-out for long supporting text. */
|
|
8427
|
+
align-items: var(--items-align, center);
|
|
8155
8428
|
gap: var(--list-item-gap);
|
|
8156
8429
|
min-block-size: var(--list-item-min-size);
|
|
8157
8430
|
padding-block: var(--list-item-pad-block);
|
|
@@ -8173,21 +8446,20 @@ a.list-item:hover {
|
|
|
8173
8446
|
background: var(--surface-subtle);
|
|
8174
8447
|
}
|
|
8175
8448
|
|
|
8176
|
-
/* Current row in a navigational list
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
…) stays free for assistive technology.
|
|
8449
|
+
/* Current row in a navigational list: aria-current="page" when the row opens
|
|
8450
|
+
a page, "true" when it is the current item of a master/detail view (a
|
|
8451
|
+
conversation, a thread, a record) that is no page of its own. Any other
|
|
8452
|
+
value, "false" included, is inert.
|
|
8181
8453
|
Calm by design: the row keeps its normal text metrics and color on a subtle
|
|
8182
8454
|
surface, and the selected accent lives on an inset inline-start trait only.
|
|
8183
8455
|
The trait is a real border, so it survives forced colors without a repaint
|
|
8184
8456
|
block. The positioning context lives on every row link, so becoming current
|
|
8185
8457
|
never moves an app-positioned decoration to a new containing block. */
|
|
8186
|
-
a.list-item[aria-current] {
|
|
8458
|
+
a.list-item:is([aria-current="page"], [aria-current="true"]) {
|
|
8187
8459
|
background: var(--surface-subtle);
|
|
8188
8460
|
}
|
|
8189
8461
|
|
|
8190
|
-
a.list-item[aria-current]::before {
|
|
8462
|
+
a.list-item:is([aria-current="page"], [aria-current="true"])::before {
|
|
8191
8463
|
content: "";
|
|
8192
8464
|
position: absolute;
|
|
8193
8465
|
inset-block: var(--list-item-pad-block);
|
|
@@ -8456,7 +8728,7 @@ a.list-item[aria-current]::before {
|
|
|
8456
8728
|
}
|
|
8457
8729
|
|
|
8458
8730
|
.join-addon {
|
|
8459
|
-
--control-border: var(--border);
|
|
8731
|
+
--control-border: var(--border-control);
|
|
8460
8732
|
--control-bg: var(--surface-subtle);
|
|
8461
8733
|
--control-fg: var(--text-muted);
|
|
8462
8734
|
|
|
@@ -8561,7 +8833,7 @@ a.list-item[aria-current]::before {
|
|
|
8561
8833
|
The second :has() covers a control nested one wrapper level deep. */
|
|
8562
8834
|
.join:has(> :is(.input, .textarea, .select)) > .btn.outline,
|
|
8563
8835
|
.join:has(> * > :is(.input, .textarea, .select)) > .btn.outline {
|
|
8564
|
-
--btn-border-color: var(--intent, var(--border));
|
|
8836
|
+
--btn-border-color: var(--intent, var(--border-control));
|
|
8565
8837
|
}
|
|
8566
8838
|
|
|
8567
8839
|
/* Effects — purely visual enrichments. */
|
|
@@ -9095,9 +9367,9 @@ a.list-item[aria-current]::before {
|
|
|
9095
9367
|
flex-wrap: nowrap;
|
|
9096
9368
|
}
|
|
9097
9369
|
|
|
9098
|
-
/* The property is written directly,
|
|
9099
|
-
a
|
|
9100
|
-
|
|
9370
|
+
/* The property is written directly, so the utility works on any flex row, not
|
|
9371
|
+
only a .cluster. Utilities load last, so the direct declaration beats
|
|
9372
|
+
.cluster at equal specificity. */
|
|
9101
9373
|
.justify-content-start {
|
|
9102
9374
|
justify-content: flex-start;
|
|
9103
9375
|
}
|