staffa 0.18.1 → 0.18.2

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.
@@ -416,10 +416,11 @@ function matchRoute(r: { segs: Seg[] }, segments: string[]): Record<string, any>
416
416
 
417
417
  /**
418
418
  * The one duration every bit of shell motion shares: the enter/exit fades, the
419
- * sideways `left` moves, the narrow-screen nav slide. Published below as the
420
- * `--s-panel-ms` custom property, so CSS and JS can't drift apart.
419
+ * slides, the narrow-screen nav slide. Interpolated into every transition as a
420
+ * literal the shared constant is what keeps CSS and JS in step — and also
421
+ * published as the `--s-panel-ms` custom property for app CSS.
421
422
  */
422
- const PAGE_MS = 250;
423
+ export const PAGE_MS = 250;
423
424
  /** How long a freshly pushed `loading` panel holds its fade-in. */
424
425
  const LOADING_HOLD_MS = 300;
425
426
  /**
@@ -447,16 +448,23 @@ A.insertGlobalCss({
447
448
  PANEL_SHEEN,
448
449
  ".s-panel": {
449
450
  // The shell knows three motions, and this vocabulary is all of them:
450
- // a panel that MOVES animates its `left` — every mover in a pass shares
451
- // this one duration and ease-out, so panels travelling the same distance
452
- // travel as one — a CREATED panel joins the strip beside the old position
453
- // of the panels beneath it and rides their slide while fading in, and a
454
- // CLOSED one fades out where it stood. Nothing else ever animates.
455
- // A plain `left`, never a transform: a transformed element is
456
- // composited, costing it subpixel text antialiasing. No `width`
457
- // transition either animating one reflows the column every frame. And
458
- // the fade is `linear` while the moves ease out: an eased opacity spends
459
- // its last stretch near zero, reading as a vanish.
451
+ // a panel that MOVES slides — every mover in a pass shares this one
452
+ // duration and ease-out, so panels travelling the same distance travel
453
+ // as one — a CREATED panel joins the strip beside the old position of
454
+ // the panels beneath it and rides their slide while fading in, and a
455
+ // CLOSED one keeps its seat beside the surviving panel beneath it and
456
+ // rides that panel's slide while fading out its creation played
457
+ // backwards. Nothing else ever animates.
458
+ // A slide is a `transform` playing out to none (see `layout`), never an
459
+ // animated `left`: left is a layout property, so animating it would
460
+ // relayout and repaint every travelling column on every frame, where a
461
+ // transform moves composited layers. `left` always holds the resting
462
+ // position — it is not in the transition list — and the transform
463
+ // exists only while a panel travels, so text gets its subpixel
464
+ // antialiasing back the moment it settles. No `width` transition
465
+ // either — animating one reflows the column every frame. And the fade
466
+ // is `linear` while the moves ease out: an eased opacity spends its
467
+ // last stretch near zero, reading as a vanish.
460
468
  // Layering is fixed per state, not per stack depth: live panels never
461
469
  // overlap each other (the strip keeps them adjacent, see `layout()`), so
462
470
  // only the fading ones need an order — below, in both directions, per
@@ -466,7 +474,7 @@ A.insertGlobalCss({
466
474
  "&":
467
475
  "position:absolute top:0 bottom:0 left:0 display:flex flex-direction:column " +
468
476
  PANEL_SHEEN + " " +
469
- "z-index:2 transition: left var(--s-panel-ms) ease-out, opacity var(--s-panel-ms) linear;",
477
+ `z-index:2 transition: transform ${PAGE_MS}ms ease-out, opacity ${PAGE_MS}ms linear;`,
470
478
  // The hairline between two columns, fading at both ends (like the sidebar's
471
479
  // `.s-nav-sep`). Columns tile with no gutter — each brings its own `$3` of
472
480
  // padding — so this sits exactly on the boundary.
@@ -477,9 +485,10 @@ A.insertGlobalCss({
477
485
  // whatever slides across its spot passes over it. Dropped once the fade
478
486
  // is over (see `releaseEnter`).
479
487
  "&.s-panel-new": "z-index:1",
480
- // On its way out: it fades where it stood, beneath every live panel
481
- // (declared after `.s-panel-new`, so closing mid-enter drops a panel to
482
- // the bottom), and leaves the DOM when the fade ends (see `playExit`).
488
+ // On its way out: it fades, still riding the strip (see `layout`),
489
+ // beneath every live panel (declared after `.s-panel-new`, so closing
490
+ // mid-enter drops a panel to the bottom), and leaves the DOM when the
491
+ // fade ends (see `playExit`).
483
492
  "&.s-panel-closing": "z-index:0 opacity:0 pointer-events:none",
484
493
  // The fade-in's start state: a newcomer wears it from creation until its
485
494
  // content is ready — usually the very pass that placed it, later for a
@@ -490,12 +499,13 @@ A.insertGlobalCss({
490
499
  // edges, so these rest at their true positions, clipped — being crowded
491
500
  // out or revealed is an ordinary move, not a fade. Both keep their DOM —
492
501
  // and so their scroll position and half-typed forms — hence
493
- // `visibility`, not `display:none`: it holds through the move out
494
- // (flipping only at its end) and lifts instantly on the move back in
495
- // (the base transition above doesn't list it).
496
- "&.s-panel-hidden, &.s-panel-parked":
497
- "visibility:hidden " +
498
- "transition: left var(--s-panel-ms) ease-out, opacity var(--s-panel-ms) linear, visibility var(--s-panel-ms);",
502
+ // `visibility`, not `display:none`. The flip is timed by `layout()`,
503
+ // never transitioned: a panel sliding offstage gets the class once its
504
+ // slide ends and drops it the moment it is revealed. `visibility` is
505
+ // not compositable, and Chrome composites an element's transitions as
506
+ // a group, so listing it here would drag the slide itself onto the
507
+ // main thread — relaid out and repainted every frame.
508
+ "&.s-panel-hidden, &.s-panel-parked": "visibility:hidden",
499
509
  },
500
510
  // The scroll container, with the column's own padding. Its scrollbar sits
501
511
  // flush against the column edge (unlike content mode's inset one), so it meets
@@ -601,12 +611,24 @@ interface PanelEntry {
601
611
  hash?: string;
602
612
  /** Set once the panel is on its way out, playing its exit animation. */
603
613
  closing?: boolean;
614
+ /**
615
+ * The nearest surviving panel beneath this one when it closed. The exit
616
+ * rides that panel's motion (see `layout`), the mirror of how a newcomer
617
+ * rides the panels it opens over, so a close plays the open backwards.
618
+ */
619
+ anchor?: string;
604
620
  /** Set while the fade-in is still owed (a `loading` hold can owe it past placement). */
605
621
  enter?: boolean;
606
622
  /** Whether the panel has been through a full layout pass (and so may animate). */
607
623
  placed?: boolean;
608
624
  /** Whether its `loading` hold has already expired, so it can't hold again. */
609
625
  holdDone?: boolean;
626
+ /**
627
+ * Whether the last layout pass put this panel off screen. The deferred
628
+ * visibility flip (see `layout`) consults it when it fires, so a panel
629
+ * revealed again before its slide out ended is never hidden by mistake.
630
+ */
631
+ offstage?: boolean;
610
632
  /** What the panel asks for, kept in step with its `$panel.maxWidth`. */
611
633
  maxWidth: PanelSize;
612
634
  /**
@@ -813,6 +835,8 @@ export class PanelStackController implements PanelStack {
813
835
  private lastGeom?: Geometry;
814
836
  private layoutQueued = false;
815
837
  private timers = new Set<ReturnType<typeof setTimeout>>();
838
+ /** Elements playing their exit fade, each riding its anchor's motion (see `layout`). */
839
+ private exiting = new Set<{ el: HTMLElement; anchor?: string; ride: number }>();
816
840
  /** The arrangement the navigation in flight is heading for; see {@link intended}. */
817
841
  private intent: Arrangement | null = null;
818
842
  /** The navigation the router hasn't settled yet, if any. */
@@ -1049,7 +1073,13 @@ export class PanelStackController implements PanelStack {
1049
1073
  next.push(entry);
1050
1074
  this.$open[path] = entry;
1051
1075
  }
1052
- for (const entry of existing.values()) this.beginClose(entry);
1076
+ // What remains in `existing` closes, each remembering the nearest
1077
+ // surviving panel beneath it — the anchor its exit fade rides.
1078
+ let anchor: string | undefined;
1079
+ for (const entry of this.$state.live) {
1080
+ if (existing.has(entry.path)) this.beginClose(entry, anchor);
1081
+ else anchor = entry.path;
1082
+ }
1053
1083
  this.$state.live = next;
1054
1084
  this.$state.focus = Math.min(target.focus, next.length - 1);
1055
1085
  this.scheduleLayout();
@@ -1090,18 +1120,17 @@ export class PanelStackController implements PanelStack {
1090
1120
  * at the end of the animation. Only the element lingers to play that animation,
1091
1121
  * which is what `drawPanel`'s `destroy=` hook hands to {@link playExit}.
1092
1122
  */
1093
- private beginClose(entry: PanelEntry): void {
1123
+ private beginClose(entry: PanelEntry, anchor?: string): void {
1094
1124
  entry.closing = true;
1125
+ entry.anchor = anchor;
1095
1126
  entry.$panel.visible = false;
1096
1127
  delete this.$open[entry.path];
1097
1128
  }
1098
1129
 
1099
1130
  /**
1100
- * A closed panel's send-off, run by Aberdeen once its scope is gone: it fades
1101
- * where it stands, inert, and leaves the DOM on `transitionend`. A fixed timer
1102
- * would race the transition and pull the element a frame early, making the
1103
- * panel appear to fade half-way and vanish; the timeout is only a fallback for
1104
- * when no `transitionend` is coming (transitions off, element never placed).
1131
+ * A closed panel's send-off, run by Aberdeen once its scope is gone: it fades,
1132
+ * inert, riding its anchor's slide (see `layout`), and leaves the DOM once
1133
+ * the fade is over (see {@link afterFade}).
1105
1134
  */
1106
1135
  private playExit(entry: PanelEntry, el: HTMLElement): void {
1107
1136
  // A panel being *redrawn* replaces its element through here too; that one
@@ -1109,16 +1138,36 @@ export class PanelStackController implements PanelStack {
1109
1138
  if (!entry.closing) { el.remove(); return; }
1110
1139
  el.classList.add("s-panel-closing");
1111
1140
  el.setAttribute("inert", "");
1112
- const drop = () => {
1113
- clearTimeout(timer);
1114
- this.timers.delete(timer);
1141
+ // Only a placed panel has a seat on the strip to ride out from.
1142
+ const exit = entry.placed ? { el, anchor: entry.anchor, ride: 0 } : null;
1143
+ if (exit) this.exiting.add(exit);
1144
+ this.afterTransition(el, "opacity", () => {
1145
+ if (exit) this.exiting.delete(exit);
1115
1146
  el.remove();
1116
- };
1117
- el.addEventListener("transitionend", (e: TransitionEvent) => {
1118
- if (e.target === el && e.propertyName === "opacity") drop();
1119
1147
  });
1120
- const timer = setTimeout(drop, PAGE_MS + 80);
1148
+ }
1149
+
1150
+ /**
1151
+ * Run `done` once `el`'s transition of `prop` is over. The real signal is
1152
+ * `transitionend` — or `transitioncancel`, for one a resize snaps short —
1153
+ * so the transition's actual length rules, however long: DevTools' slowed
1154
+ * animations stretch it tenfold without touching any timer. The timer only
1155
+ * stands in for a transition that never starts at all (transitions off,
1156
+ * element never placed, nothing to travel), which is why one proving real
1157
+ * (`transitionrun`) disarms it.
1158
+ */
1159
+ private afterTransition(el: HTMLElement, prop: string, done: () => void): void {
1160
+ let called = false;
1161
+ const timer = setTimeout(() => finish(), PAGE_MS + 80);
1121
1162
  this.timers.add(timer);
1163
+ const disarm = () => { clearTimeout(timer); this.timers.delete(timer); };
1164
+ const finish = () => { disarm(); if (!called) { called = true; done(); } };
1165
+ const forProp = (fn: () => void) => (e: TransitionEvent) => {
1166
+ if (e.target === el && e.propertyName === prop) fn();
1167
+ };
1168
+ el.addEventListener("transitionrun", forProp(disarm));
1169
+ el.addEventListener("transitionend", forProp(finish));
1170
+ el.addEventListener("transitioncancel", forProp(finish));
1122
1171
  }
1123
1172
 
1124
1173
  // ── Navigation ─────────────────────────────────────────────────────────
@@ -1821,14 +1870,23 @@ export class PanelStackController implements PanelStack {
1821
1870
  // Phase 1 — lay the strip out for this frame: each panel flush against
1822
1871
  // its neighbours, the visible run [first..cur] in the viewport, earlier
1823
1872
  // panels continuing off its left edge and parked ones held past its
1824
- // right. Placed panels get their new positionstheir standing
1825
- // transitions carry them there. Newcomers, transitions still off, get
1826
- // their *start* state instead: their strip position anchored to the OLD
1827
- // position of the nearest placed panel beneath them (`delta`), so the
1828
- // slide in is one motion with the panels making room. With nothing
1829
- // beneath them to come from or nothing moving that start is where
1830
- // they already stand, and they simply fade in.
1831
- const fresh: { entry: PanelEntry; x: number }[] = [];
1873
+ // right. `left` gets each new resting position outright it never
1874
+ // animates and the slide to it is a FLIP: a mover is held at its
1875
+ // current visual spot by a transform (its mid-slide offset plus the
1876
+ // distance), a newcomer at its start beside the OLD position of the
1877
+ // nearest placed panel beneath it (`delta`, so the slide in is one
1878
+ // motion with the panels making room; with nothing beneath it to come
1879
+ // from, it simply fades in place). Phase 3 plays every held transform
1880
+ // out to none. A snap pass holds nothing: geometry must land, not
1881
+ // travel.
1882
+ const fresh: PanelEntry[] = [];
1883
+ const movers: HTMLElement[] = [];
1884
+ const deltas = new Map<string, number>();
1885
+ // All reads before all writes: a mover may still be mid-slide, and its
1886
+ // current offset must come out of the computed style before this pass
1887
+ // dirties it (one style recalc, then cached).
1888
+ const txs = new Map<PanelEntry, number>();
1889
+ if (!snap) for (const entry of live) { if (entry.placed) txs.set(entry, transformX(entry.el!)); }
1832
1890
  let x = left;
1833
1891
  let delta = 0;
1834
1892
  for (let i = 0; i < first; i++) x -= live[i].width;
@@ -1846,14 +1904,26 @@ export class PanelStackController implements PanelStack {
1846
1904
  }
1847
1905
  if (entry.placed) {
1848
1906
  delta = parseFloat(el.style.left) - x;
1907
+ deltas.set(entry.path, delta);
1908
+ if (delta && !snap) {
1909
+ // Hold the visual spot while `left` jumps beneath it. The
1910
+ // inline transition keeps the transform still — and only it:
1911
+ // running fades carry on, and one starting this pass (a
1912
+ // release below) still gets its linear curve.
1913
+ el.style.transition = `opacity ${PAGE_MS}ms linear`;
1914
+ el.style.transform = `translateX(${(txs.get(entry) ?? 0) + delta}px)`;
1915
+ movers.push(el);
1916
+ }
1849
1917
  el.style.left = `${x}px`;
1850
1918
  // A fade held back for content starts the moment its hold lifts.
1851
1919
  if (entry.enter && !entry.$ui.holding) this.releaseEnter(entry);
1852
1920
  } else {
1853
- fresh.push({ entry, x });
1854
- const entering = entry.enter && shown;
1855
- el.style.left = `${entering ? x + delta : x}px`;
1856
- if (entering) el.classList.add("s-panel-enter", "s-panel-new");
1921
+ fresh.push(entry);
1922
+ el.style.left = `${x}px`;
1923
+ if (entry.enter && shown) {
1924
+ el.style.transform = `translateX(${delta}px)`;
1925
+ el.classList.add("s-panel-enter", "s-panel-new");
1926
+ }
1857
1927
  }
1858
1928
  el.style.width = `${entry.width}px`;
1859
1929
  x += entry.width;
@@ -1862,23 +1932,58 @@ export class PanelStackController implements PanelStack {
1862
1932
  if (entry.$panel.visible !== shown) entry.$panel.visible = shown;
1863
1933
  if (entry.$panel.width !== entry.width) entry.$panel.width = entry.width;
1864
1934
  el.classList.toggle("s-panel-sep", shown && i > first);
1865
- // Off-screen panels stop rendering, keeping their DOM.
1866
- el.classList.toggle("s-panel-hidden", i < first);
1867
- el.classList.toggle("s-panel-parked", i > cur);
1935
+ // Off-screen panels stop rendering, keeping their DOM. A revealed
1936
+ // panel is visible at once; one sliding offstage stays visible for
1937
+ // the whole slide, its visibility class deferred to the slide's
1938
+ // end — but applied outright when it is already invisible, about
1939
+ // to be (a newcomer offstage from birth), or snapping.
1940
+ const hidden = i < first;
1941
+ const wasOff = el.classList.contains("s-panel-hidden") || el.classList.contains("s-panel-parked");
1942
+ if (shown) {
1943
+ el.classList.remove("s-panel-hidden", "s-panel-parked");
1944
+ } else if (wasOff || !entry.placed || snap) {
1945
+ el.classList.toggle("s-panel-hidden", hidden);
1946
+ el.classList.toggle("s-panel-parked", !hidden);
1947
+ } else {
1948
+ this.afterTransition(el, "transform", () => {
1949
+ if (entry.el !== el || !entry.offstage) return;
1950
+ el.classList.toggle("s-panel-hidden", hidden);
1951
+ el.classList.toggle("s-panel-parked", !hidden);
1952
+ });
1953
+ }
1954
+ entry.offstage = !shown;
1868
1955
  el.toggleAttribute("inert", !shown);
1869
1956
  }
1870
1957
 
1958
+ // Closing panels keep their seat on the strip while they fade: each
1959
+ // travels exactly as far as its anchor — the surviving panel it stood
1960
+ // on — so a close slides back out the way the open slid in, and one
1961
+ // whose anchor stays put is the plain crossfade it should be. A plain
1962
+ // retarget, no holding: the transition picks the slide up from wherever
1963
+ // the element is now.
1964
+ for (const exit of this.exiting) {
1965
+ const d = exit.anchor == null ? undefined : deltas.get(exit.anchor);
1966
+ if (d) {
1967
+ exit.ride -= d;
1968
+ exit.el.style.transform = `translateX(${exit.ride}px)`;
1969
+ }
1970
+ }
1971
+
1871
1972
  // Phase 2 — reading a layout property forces the browser to adopt those
1872
- // start states (and a snap pass's transition-free geometry) to animate from.
1873
- if (fresh.length || snap) void container.offsetWidth;
1973
+ // held spots (and a snap pass's transition-free geometry) to slide from.
1974
+ if (fresh.length || movers.length || snap) void container.offsetWidth;
1874
1975
  if (snap) shell.classList.remove("s-shell-snap");
1875
- // Phase 3 — newcomers get their transitions and their resting place: the
1876
- // slide starts now, and the fade with it — unless the panel is holding
1976
+ // Phase 3 — every held transform plays out to none: the slides start
1977
+ // now, and the newcomers' fades with them — unless a panel is holding
1877
1978
  // for its content, which keeps the fade's start state on until then.
1878
- for (const { entry, x } of fresh) {
1979
+ for (const el of movers) {
1980
+ el.style.transition = "";
1981
+ el.style.transform = "";
1982
+ }
1983
+ for (const entry of fresh) {
1879
1984
  const el = entry.el!;
1880
1985
  el.style.transition = "";
1881
- el.style.left = `${x}px`;
1986
+ el.style.transform = "";
1882
1987
  entry.placed = true;
1883
1988
  if (!entry.$ui.holding) this.releaseEnter(entry);
1884
1989
  }
@@ -1890,14 +1995,8 @@ export class PanelStackController implements PanelStack {
1890
1995
  const el = entry.el;
1891
1996
  if (!el || !el.classList.contains("s-panel-enter")) return;
1892
1997
  el.classList.remove("s-panel-enter");
1893
- // Keep it beneath its elders until the fade is over — by timer, a hair
1894
- // past it, since a resize can snap the fade short without ever firing a
1895
- // `transitionend`.
1896
- const timer = setTimeout(() => {
1897
- this.timers.delete(timer);
1898
- el.classList.remove("s-panel-new");
1899
- }, PAGE_MS + 80);
1900
- this.timers.add(timer);
1998
+ // Keep it beneath its elders until the fade is over.
1999
+ this.afterTransition(el, "opacity", () => el.classList.remove("s-panel-new"));
1901
2000
  }
1902
2001
 
1903
2002
  /** Let a `loading` panel's fade-in wait — but not indefinitely. */
@@ -1920,6 +2019,12 @@ function sameStack(a: string[], b: string[]): boolean {
1920
2019
  return a.length === b.length && a.every((v, i) => v === b[i]);
1921
2020
  }
1922
2021
 
2022
+ /** The X offset of `el`'s computed transform — where a slide has it right now. */
2023
+ function transformX(el: HTMLElement): number {
2024
+ const t = getComputedStyle(el).transform;
2025
+ return t && t !== "none" ? new DOMMatrixReadOnly(t).m41 : 0;
2026
+ }
2027
+
1923
2028
  /**
1924
2029
  * The first non-empty text inside `el`, trimmed and capped at a name-like
1925
2030
  * length — the stand-in title for a panel that never set one.
package/src/theme.ts CHANGED
@@ -32,9 +32,11 @@ import A from "aberdeen";
32
32
  * declaration, at the given angle. A shared constant rather than a `--s-sheen`
33
33
  * custom property: `var()`s inside a custom property resolve where it is
34
34
  * *defined*, so every surface would get the page's wash instead of its own.
35
+ * The shorthand names `$s-bg` as well as the gradient: a `background:` that
36
+ * gave only an image would reset the colour under it to transparent.
35
37
  */
36
38
  const sheen = (angle: string) =>
37
- `background: linear-gradient(${angle}, color-mix(in oklab, $s-bg, white 9%), color-mix(in oklab, $s-bg, black 9%));`;
39
+ `background: $s-bg linear-gradient(${angle}, color-mix(in oklab, $s-bg, white 9%), color-mix(in oklab, $s-bg, black 9%));`;
38
40
 
39
41
  /** The surface sheen: the wash every surface (and the page) is painted with. */
40
42
  export const SURFACE_SHEEN = sheen("170deg");
@@ -120,7 +122,14 @@ A.insertGlobalCss({
120
122
  // A lightweight reset: bare semantic HTML, with less ugly defaults.
121
123
  "*, *::before, *::after": "box-sizing:border-box",
122
124
  html: "text-size-adjust:100%",
123
- body: "m:0 p:$3 line-height:1.5 font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; -webkit-font-smoothing:antialiased background-color:$s-bg text:$s-text",
125
+ // The page wears the same wash as a surface, on a page at least as tall as
126
+ // what that wash is painted on: <body>'s background is propagated to the
127
+ // canvas, which covers the whole viewport, while the gradient is still
128
+ // *sized* from the root box. So a page shorter than the viewport — a bare
129
+ // spinner while an app boots — would tile its wash down the rest of the
130
+ // canvas in bands. `dvh`, not `vh`: a mobile viewport that grows as the URL
131
+ // bar retracts would open a band again.
132
+ body: "m:0 p:$3 min-height:100dvh line-height:1.5 font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; -webkit-font-smoothing:antialiased text:$s-text " + SURFACE_SHEEN,
124
133
  // The contextual link colour: `--s-link` on neutral surfaces, the ink on accent ones.
125
134
  a: "color: $s-link-fg; text-decoration:underline text-underline-offset:2px; transition: color 0.12s, filter 0.12s;",
126
135
  "a:hover": "filter: brightness(1.15)",
@@ -196,9 +205,9 @@ A.insertGlobalCss({
196
205
  "--s-muted: color-mix(in oklab, $s-text, $s-bg 42%); " +
197
206
  "--s-faint: color-mix(in oklab, $s-text, $s-bg 80%); " +
198
207
  "color:$s-text accent-color:$s-accent scrollbar-width:thin scrollbar-color: $s-faint transparent;",
199
- // Subtle single-colour gradient sheen, painted on every surface (and the page).
200
- ".s-s, body": SURFACE_SHEEN,
201
- ".s-s": "r:$s-radius",
208
+ // Subtle single-colour gradient sheen, painted on every surface (the page
209
+ // gets its own, up in the reset).
210
+ ".s-s": SURFACE_SHEEN + " r:$s-radius",
202
211
  // A neutral surface owns a hairline border, so a card reads as a card without
203
212
  // any component help. `:where()` keeps it zero-specificity, so a bar that wants
204
213
  // only a divider overrides it with a single plain rule.