@slotted/styles 0.1.0-beta.5 → 0.1.0-beta.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slotted/styles",
3
- "version": "0.1.0-beta.5",
3
+ "version": "0.1.0-beta.7",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -82,6 +82,16 @@
82
82
  /** The box a row's mark occupies: a control's own size, so the two agree. */
83
83
  --_row-mark: var(--slotted-app-shell-row-mark-size, var(--slotted-control-height-md, 34px));
84
84
  --_row-gap: var(--slotted-control-gap-md, 8px);
85
+ /**
86
+ * How far a focus ring drawn outside a control reaches past its box: the
87
+ * ring's width and its offset, 4px with the default theme. The three parts
88
+ * of the navigation column pad their inline sides by it, because the body
89
+ * scrolls and clips at its padding box, and a full-width control's ring
90
+ * stood outside that box on both sides.
91
+ */
92
+ --_row-ring-inset: calc(
93
+ var(--slotted-focus-ring-width, 2px) + var(--slotted-focus-ring-offset, 2px)
94
+ );
85
95
 
86
96
  /**
87
97
  * A row's type. A list of destinations is text and not a control, so it
@@ -495,7 +505,19 @@
495
505
  gap: var(--_row-gap);
496
506
  min-block-size: var(--_rail-size);
497
507
  min-inline-size: 0;
508
+ /**
509
+ * The gutter the body reserves, reserved here too. `scrollbar-gutter`
510
+ * applies to a scroll container only, and `hidden` is the overflow that
511
+ * makes one without a bar of its own: with classic scrollbars the body's
512
+ * content box ended one bar width (10px in Chrome) before the head's and
513
+ * the foot's. Nothing is clipped that was drawn before: the ring inset
514
+ * keeps a control's focus ring inside the padding box. The bar's width is
515
+ * the body's, or the gutter is the platform's full bar (15px in Chrome).
516
+ */
517
+ overflow: hidden;
498
518
  padding-block: var(--_row-inset);
519
+ scrollbar-gutter: stable;
520
+ scrollbar-width: thin;
499
521
  }
500
522
  /**
501
523
  * The inset is the rows', not the region's: on the parts, a seam drawn by
@@ -505,7 +527,13 @@
505
527
  [data-ui='app-shell']
506
528
  > [data-part='nav']
507
529
  > :is([data-part='nav-header'], [data-part='nav-body'], [data-part='nav-footer']) {
508
- padding-inline: var(--_nav-inset);
530
+ /**
531
+ * Published where it pads, beside the row geometry the root publishes:
532
+ * `side-navigation` reads it to keep its rows' box on the edge it had,
533
+ * and a row it reaches outside these parts reads nothing.
534
+ */
535
+ --slotted-app-shell-row-ring-inset: var(--_row-ring-inset);
536
+ padding-inline: calc(var(--_nav-inset) + var(--_row-ring-inset));
509
537
  }
510
538
  /**
511
539
  * The body scrolls the way `scroll-area` scrolls, and for the reasons written
@@ -1221,6 +1249,19 @@
1221
1249
  border-start-end-radius: 0;
1222
1250
  margin-inline-end: 0;
1223
1251
  }
1252
+
1253
+ /**
1254
+ * The seam re-read in each region's scope. `--_seam` declared on the root
1255
+ * holds the root's `--slotted-app-shell-border-color`, so the property written
1256
+ * on a region recoloured nothing. Declared again on every region, the chain
1257
+ * resolves where the region stands: a colour written there recolours the
1258
+ * seams the region and its parts draw, and `transparent` removes them. The
1259
+ * tone rules below state the same two declarations at a higher specificity.
1260
+ */
1261
+ [data-ui='app-shell'] > [data-part] {
1262
+ --_rule: var(--slotted-app-shell-border-color, var(--slotted-border-subtle, ButtonBorder));
1263
+ --_seam: var(--_rule-width) solid var(--_rule);
1264
+ }
1224
1265
  /* ── Separation by tone ──────────────────────────────────────────────── */
1225
1266
 
1226
1267
  /**
@@ -1238,7 +1279,8 @@
1238
1279
  * where it is declared, and a region carrying a scheme of its own must mix
1239
1280
  * its own ink.
1240
1281
  */
1241
- [data-ui='app-shell'][data-separation='tone'] {
1282
+ [data-ui='app-shell'][data-separation='tone'],
1283
+ [data-ui='app-shell'][data-separation='tone'] > [data-part] {
1242
1284
  --_seam: none;
1243
1285
  --_seam-width: 0px;
1244
1286
  }
@@ -1365,7 +1407,7 @@
1365
1407
  [data-ui='app-shell']
1366
1408
  > [data-part='nav']:has(> [data-part='nav-rail'])
1367
1409
  > :is([data-part='nav-header'], [data-part='nav-body'], [data-part='nav-footer']) {
1368
- padding-inline-start: var(--_row-inset);
1410
+ padding-inline-start: calc(var(--_row-inset) + var(--_row-ring-inset));
1369
1411
  transition: visibility var(--_duration) step-start;
1370
1412
  }
1371
1413
  [data-ui='app-shell'][data-nav-mode='inline'][data-open]
@@ -1400,7 +1442,8 @@
1400
1442
  * a forced-colours rule of its own for.
1401
1443
  */
1402
1444
  @media (forced-colors: active) {
1403
- [data-ui='app-shell'][data-separation='tone'] {
1445
+ [data-ui='app-shell'][data-separation='tone'],
1446
+ [data-ui='app-shell'][data-separation='tone'] > [data-part] {
1404
1447
  --_seam: var(--_rule-width) solid CanvasText;
1405
1448
  --_seam-width: var(--_rule-width);
1406
1449
  }
@@ -1501,8 +1544,12 @@
1501
1544
  * mode at any width. The container query further down is the fallback for
1502
1545
  * the frame before the binding has measured, and nothing else.
1503
1546
  */
1547
+ /* The rail holds the parts' ring inset on each side as well, so a control
1548
+ at the mark's size keeps its focus ring inside the body beside the gutter. */
1504
1549
  [data-ui='app-shell'][data-nav-mode='inline'] > [data-part='nav'] {
1505
- inline-size: calc(var(--_rail-size) + 2 * var(--_nav-inset) + var(--_nav-edges));
1550
+ inline-size: calc(
1551
+ var(--_rail-size) + 2 * (var(--_nav-inset) + var(--_row-ring-inset)) + var(--_nav-edges)
1552
+ );
1506
1553
  }
1507
1554
  [data-ui='app-shell'][data-nav-mode='inline'][data-open] > [data-part='nav'] {
1508
1555
  inline-size: var(--_nav-size);
@@ -1779,7 +1826,9 @@
1779
1826
  */
1780
1827
  @container slotted-app-shell (inline-size >= 600px) {
1781
1828
  [data-ui='app-shell']:not([data-nav-mode]) > [data-part='nav'] {
1782
- inline-size: calc(var(--_rail-size) + 2 * var(--_nav-inset) + var(--_nav-edges));
1829
+ inline-size: calc(
1830
+ var(--_rail-size) + 2 * (var(--_nav-inset) + var(--_row-ring-inset)) + var(--_nav-edges)
1831
+ );
1783
1832
  }
1784
1833
  [data-ui='app-shell']:not([data-nav-mode])[data-open] > [data-part='nav'] {
1785
1834
  inline-size: var(--_nav-size);
@@ -625,14 +625,29 @@
625
625
  position: relative;
626
626
  }
627
627
 
628
- [data-ui='button'] [data-part='start'] > *:not([data-part='indicator']),
629
- [data-ui='button'] [data-part='end'] > *:not([data-part='indicator']),
628
+ [data-ui='button'] [data-part='start'] > *:not([data-part='indicator'], [data-ui='kbd']),
629
+ [data-ui='button'] [data-part='end'] > *:not([data-part='indicator'], [data-ui='kbd']),
630
630
  [data-ui='button'] [data-part='icon'] > *:not([data-part='indicator']) {
631
631
  block-size: 100%;
632
632
  display: block;
633
633
  inline-size: 100%;
634
634
  }
635
635
 
636
+ /**
637
+ * A key in a slot is sized by its own family. The slot above is a glyph's
638
+ * box: it stated `display: block` on the key, which put the legend on a line
639
+ * box's baseline instead of the key's centre, sixteen pixels square, and the
640
+ * icon's font size, which `size="inherit"` read as the text's. Measured, a
641
+ * `Kbd` legend stood 1.5px above the label's centre and a `Shortcut` 1.5px
642
+ * below it. Here the slot takes the key's measure and the label's type, and
643
+ * the flex container centres the key on the label.
644
+ */
645
+ [data-ui='button'] :is([data-part='start'], [data-part='end']):has(> [data-ui='kbd']) {
646
+ block-size: auto;
647
+ font-size: inherit;
648
+ inline-size: auto;
649
+ }
650
+
636
651
  /**
637
652
  * The label carries a wrapping line height of its own: the control's line
638
653
  * height is 1, which centres a single line inside the box and crushes the
package/src/kbd/kbd.css CHANGED
@@ -237,6 +237,10 @@
237
237
  align-items: center;
238
238
  direction: ltr;
239
239
  display: inline-flex;
240
+ /* The root is a `kbd`: without these it keeps the user agent's monospace
241
+ face and its line box, which a key in the combination does not share. */
242
+ font-family: var(--slotted-kbd-font-family, var(--slotted-control-font-family, sans-serif));
243
+ line-height: 1;
240
244
  /**
241
245
  * A shortcut wider than its column wraps between caps and never inside
242
246
  * one. The row gets taller; no instruction gets shorter.
package/src/menu/menu.css CHANGED
@@ -290,6 +290,28 @@
290
290
  visibility: hidden;
291
291
  }
292
292
 
293
+ /**
294
+ * The surface as wide as its trigger: an account switcher at the foot of a
295
+ * sidebar, opened from a row the column's full width. The content takes the
296
+ * anchor's width as its size and its floor, as `popover`'s `width="anchor"`
297
+ * does, and the room the positioner reports caps both. The layer drops the
298
+ * family's floor and cap, which would hold it narrower or wider than the
299
+ * row. Opened above or below, the layer has no inline padding, so the
300
+ * content and the trigger share both edges.
301
+ */
302
+ [data-ui='menu'][data-width='anchor'] {
303
+ max-inline-size: var(--slotted-positioner-available-width, 100vw);
304
+ min-inline-size: 0;
305
+ }
306
+ [data-ui='menu'][data-width='anchor'] > [data-part='content'] {
307
+ inline-size: var(--slotted-positioner-anchor-width, auto);
308
+ max-inline-size: var(--slotted-positioner-available-width, 100vw);
309
+ min-inline-size: min(
310
+ var(--slotted-positioner-anchor-width, 0px),
311
+ var(--slotted-positioner-available-width, 100vw)
312
+ );
313
+ }
314
+
293
315
  [data-ui='menu'][data-size='sm'] {
294
316
  --_item-padding: var(--slotted-control-padding-inline-sm, 0.625rem);
295
317
  --_item-height: var(--slotted-control-height-sm, 30px);
@@ -59,6 +59,8 @@
59
59
  --_inset: var(--slotted-segmented-control-inset, var(--slotted-space-1, 0.125rem));
60
60
  /** What is left for a segment once the track has taken its edge and ring. */
61
61
  --_inner: calc(var(--_height) - (var(--_inset) + var(--_border-width)) * 2);
62
+ /** The space between two segments: the ring's own step. */
63
+ --_spacing: var(--slotted-segmented-control-gap, var(--_inset));
62
64
  /** The thumb's corner, kept concentric with the track's own. */
63
65
  --_thumb-radius: max(calc(var(--_radius) - var(--_inset)), var(--slotted-radius-xs, 2px));
64
66
  /**
@@ -69,15 +71,22 @@
69
71
  */
70
72
  --_travel: var(--slotted-duration-medium);
71
73
 
72
- /* A control in a row of controls: the fill, edge and lift a field has. */
73
- background: var(--slotted-segmented-control-background, var(--slotted-control-surface, Field));
74
- border: var(--_border-width) solid
75
- var(
76
- --slotted-segmented-control-border-color,
77
- var(--slotted-control-border-color, ButtonBorder)
78
- );
74
+ /**
75
+ * A neutral track: the page's own ink at a low alpha rather than an opaque
76
+ * fill, so it darkens whatever it stands on — a surface, a sunken panel, a
77
+ * card header — by the same step, and reads as a recess in each. The edge
78
+ * keeps its width and is transparent by default, so the arithmetic below is
79
+ * the same with a theme's edge and without one. An accent track with an
80
+ * accent thumb edge was the first appearance: a coloured outline on a row
81
+ * of neutral controls read as an alert before it read as a choice.
82
+ */
83
+ background: var(
84
+ --slotted-segmented-control-background,
85
+ color-mix(in oklab, var(--slotted-text, CanvasText) 6%, transparent)
86
+ );
87
+ border: var(--_border-width) solid var(--slotted-segmented-control-border-color, transparent);
79
88
  border-radius: var(--_radius);
80
- box-shadow: var(--slotted-segmented-control-shadow, var(--slotted-control-shadow, none));
89
+ box-shadow: var(--slotted-segmented-control-shadow, none);
81
90
  box-sizing: border-box;
82
91
  display: inline-grid;
83
92
  font-family: var(--slotted-control-font-family, system-ui, sans-serif);
@@ -87,8 +96,15 @@
87
96
  * sized grid resolves every track to the largest content contribution, so
88
97
  * the rule Carbon states — base the width on the longest label, and give
89
98
  * every segment that width — falls out of the layout.
99
+ *
100
+ * The column carries the segment's floor, and the segment none of its own:
101
+ * a segment with a minimum inside a column that could shrink below it ran
102
+ * past its column in a narrow container, while the thumb, a column wide,
103
+ * did not — the thumb and its segment no longer matched, and the label
104
+ * overflowed the track.
90
105
  */
91
- grid-auto-columns: minmax(0, 1fr);
106
+ gap: var(--_spacing);
107
+ grid-auto-columns: minmax(max(var(--_inner), var(--slotted-target-min, 24px)), 1fr);
92
108
  grid-auto-flow: column;
93
109
  isolation: isolate;
94
110
  min-inline-size: 0;
@@ -172,7 +188,10 @@
172
188
  color: var(--slotted-segmented-control-color, var(--slotted-text, ButtonText));
173
189
  cursor: pointer;
174
190
  display: flex;
175
- font-weight: var(--slotted-control-font-weight, 600);
191
+ font-weight: var(
192
+ --slotted-segmented-control-font-weight,
193
+ var(--slotted-font-weight-medium, 500)
194
+ );
176
195
  gap: var(--_gap);
177
196
  justify-content: center;
178
197
  letter-spacing: var(--slotted-control-letter-spacing, 0);
@@ -180,18 +199,34 @@
180
199
  /**
181
200
  * The thumb is shorter than the control it lives in, so at the smallest
182
201
  * step and the tightest density it is eighteen pixels tall. The WCAG
183
- * minimum is the floor across the segment, and the radio below reaches the
184
- * track's own edge to meet it in the other axis.
202
+ * minimum is the column's floor across the segment, and the radio below
203
+ * reaches the track's own edge to meet it in the other axis.
185
204
  */
186
- min-inline-size: max(var(--_inner), var(--slotted-target-min, 24px));
205
+ min-inline-size: 0;
187
206
  padding-inline: var(--_padding-inline);
207
+ /**
208
+ * Positioned without a `z-index`: the segment paints after the thumb, which
209
+ * comes first in the DOM, so its words lie over the thumb, and its divider
210
+ * and its hover ground — pseudo-elements at `z-index: -1` — lie under it.
211
+ * A segment at `z-index: 1` lifted both over the thumb as well, and a thumb
212
+ * crossing a hovered segment or a divider lost part of its edge for the
213
+ * length of the slide.
214
+ */
188
215
  position: relative;
189
- transition:
190
- background-color var(--slotted-control-transition-duration)
191
- var(--slotted-control-transition-easing),
192
- color var(--slotted-control-transition-duration) var(--slotted-control-transition-easing);
193
- /* Above the thumb, which is painted behind every segment. */
194
- z-index: 1;
216
+ transition: color var(--slotted-control-transition-duration)
217
+ var(--slotted-control-transition-easing);
218
+ }
219
+
220
+ /** The hover and press ground, under the thumb; see the segment above. */
221
+ [data-ui='segmented-control'] [data-part='item']::after {
222
+ background: transparent;
223
+ border-radius: inherit;
224
+ content: '';
225
+ inset: 0;
226
+ position: absolute;
227
+ transition: background-color var(--slotted-control-transition-duration)
228
+ var(--slotted-control-transition-easing);
229
+ z-index: -1;
195
230
  }
196
231
 
197
232
  /* An empty label is no label: a binding that always renders the span must
@@ -205,27 +240,36 @@
205
240
  }
206
241
 
207
242
  /**
208
- * ── The rule between two segments ─────────────────────────────────────
243
+ * ── Between two segments ──────────────────────────────────────────────
244
+ *
245
+ * Segments stand apart by the same step as the ring of track around them,
246
+ * so each one — its hover ground, the thumb on it — is a shape of its own
247
+ * on the track, and a rule in the middle of that room says the control is
248
+ * divided: a track of words with nothing between them reads as a sentence.
209
249
  *
210
- * A track of words with nothing between them reads as a sentence. The rule
211
- * is what says the control is divided, and it is what Mantine ships on by
212
- * default, Apple draws, and Carbon draws. It is shorter than the segment, so
213
- * it reads as a divider rather than as a second frame, and it gives way
214
- * around the thumb — a grey line touching the accent edge is the "border
215
- * inside a border" this family had to correct elsewhere.
250
+ * The rule is centred in the gap rather than on a segment's edge, half the
251
+ * segment's height, rounded at its ends, in a light tint of the ink rather
252
+ * than an opaque border colour. It gives way wherever it would touch a
253
+ * shape: beside the thumb, and beside the segment under the pointer, whose
254
+ * ground would otherwise meet it. It lies under the thumb, so a thumb that
255
+ * crosses it while sliding passes over it.
216
256
  */
217
257
  [data-ui='segmented-control'] [data-part='item'] + [data-part='item']::before {
218
258
  /* No system colour behind it: an unthemed decoration disappears rather
219
259
  than shouting, which is what `fallbacks.test.mjs` holds a pseudo-element
220
- to. The rule is a decoration — what divides the segments in forced
221
- colours is the thumb's own border. */
222
- background: var(--slotted-segmented-control-divider-color, var(--slotted-border-subtle));
260
+ to. In forced colours the thumb's own edge is what divides. */
261
+ background: var(
262
+ --slotted-segmented-control-divider-color,
263
+ color-mix(in oklab, var(--slotted-text) 14%, transparent)
264
+ );
265
+ border-radius: var(--slotted-radius-full, 9999px);
223
266
  content: '';
224
267
  inline-size: var(--_border-width);
225
- inset-block: 22%;
226
- inset-inline-start: 0;
268
+ inset-block: 25%;
269
+ inset-inline-start: calc((var(--_spacing) + var(--_border-width)) / -2);
227
270
  position: absolute;
228
271
  transition: opacity var(--_travel) var(--slotted-easing-standard);
272
+ z-index: -1;
229
273
  }
230
274
 
231
275
  [data-ui='segmented-control'][data-part='root'][data-orientation='vertical']
@@ -233,13 +277,17 @@
233
277
  + [data-part='item']::before {
234
278
  block-size: var(--_border-width);
235
279
  inline-size: auto;
236
- inset-block: auto;
237
- inset-block-start: 0;
238
- inset-inline: 22%;
280
+ inset-block: calc((var(--_spacing) + var(--_border-width)) / -2) auto;
281
+ inset-inline: 25%;
239
282
  }
240
283
 
241
284
  [data-ui='segmented-control'] [data-part='item'][data-checked]::before,
242
- [data-ui='segmented-control'] [data-part='item'][data-checked] + [data-part='item']::before {
285
+ [data-ui='segmented-control'] [data-part='item'][data-checked] + [data-part='item']::before,
286
+ [data-ui='segmented-control'][data-part='root']:not([data-readonly])
287
+ [data-part='item']:hover:has([data-part='control']:enabled)::before,
288
+ [data-ui='segmented-control'][data-part='root']:not([data-readonly])
289
+ [data-part='item']:hover:has([data-part='control']:enabled)
290
+ + [data-part='item']::before {
243
291
  opacity: 0;
244
292
  }
245
293
 
@@ -257,7 +305,9 @@
257
305
  border-radius: inherit;
258
306
  cursor: inherit;
259
307
  inset-block: calc(-1 * (var(--_inset) + var(--_border-width)));
260
- inset-inline: 0;
308
+ /* Half the space on each side, so the room between two segments is not a
309
+ place a press lands on nothing. */
310
+ inset-inline: calc(var(--_spacing) / -2);
261
311
  margin: 0;
262
312
  opacity: 0;
263
313
  padding: 0;
@@ -277,7 +327,12 @@
277
327
  * string stays whole in the DOM: truncation never removes content.
278
328
  */
279
329
  [data-ui='segmented-control'] [data-part='label'] {
280
- overflow: hidden;
330
+ /* Clipped along the line only: `hidden` on a one-line box cut the
331
+ descenders and the accents above the cap height. A clipped box is not a
332
+ scroll container, so the flex minimum is given back by hand, or the words
333
+ would hold their full width and run into the next segment. */
334
+ min-inline-size: 0;
335
+ overflow-x: clip;
281
336
  text-overflow: ellipsis;
282
337
  white-space: nowrap;
283
338
  }
@@ -307,20 +362,33 @@
307
362
  * never seen — which is what Mantine does.
308
363
  */
309
364
  [data-ui='segmented-control'] [data-part='indicator'] {
365
+ /* A raised surface with a hairline and the lightest elevation: the chosen
366
+ segment sits on the track rather than being painted into it. In a dark
367
+ scheme the raised surface is darker than the tinted track, and a thumb
368
+ darker than its track reads as a hole, so it takes a step of the ink. */
310
369
  background: var(
311
370
  --slotted-segmented-control-indicator-background,
312
- var(--slotted-tone-accent-subtle-active, Highlight)
371
+ light-dark(
372
+ var(--slotted-surface-raised, Canvas),
373
+ color-mix(
374
+ in oklab,
375
+ var(--slotted-text, CanvasText) 14%,
376
+ var(--slotted-surface-raised, Canvas)
377
+ )
378
+ )
313
379
  );
314
380
  block-size: calc(100% - var(--_inset) * 2);
315
381
  border: var(--_border-width) solid
316
382
  var(
317
383
  --slotted-segmented-control-indicator-border-color,
318
- var(--slotted-tone-accent-border, Highlight)
384
+ color-mix(in oklab, var(--slotted-text, CanvasText) 10%, transparent)
319
385
  );
320
386
  border-radius: var(--_thumb-radius);
321
- box-shadow: var(--slotted-control-shadow, none);
387
+ box-shadow: var(--slotted-segmented-control-indicator-shadow, var(--slotted-shadow-xs, none));
322
388
  box-sizing: border-box;
323
- inline-size: calc((100% - var(--_inset) * 2) / var(--_count));
389
+ inline-size: calc(
390
+ (100% - var(--_inset) * 2 - var(--_spacing) * (var(--_count) - 1)) / var(--_count)
391
+ );
324
392
  inset-block-start: var(--_inset);
325
393
  inset-inline-start: var(--_inset);
326
394
  pointer-events: none;
@@ -328,7 +396,7 @@
328
396
  transition:
329
397
  opacity var(--slotted-control-transition-duration) var(--slotted-easing-standard),
330
398
  translate var(--_travel) var(--slotted-easing-standard);
331
- translate: calc(var(--_index) * 100%);
399
+ translate: calc(var(--_index) * (100% + var(--_spacing)));
332
400
  z-index: 0;
333
401
 
334
402
  @starting-style {
@@ -337,14 +405,16 @@
337
405
  }
338
406
 
339
407
  [data-ui='segmented-control'] [data-part='indicator']:dir(rtl) {
340
- translate: calc(var(--_index) * -100%);
408
+ translate: calc(var(--_index) * (-100% - var(--_spacing)));
341
409
  }
342
410
 
343
411
  [data-ui='segmented-control'][data-part='root'][data-orientation='vertical']
344
412
  [data-part='indicator'] {
345
- block-size: calc((100% - var(--_inset) * 2) / var(--_count));
413
+ block-size: calc(
414
+ (100% - var(--_inset) * 2 - var(--_spacing) * (var(--_count) - 1)) / var(--_count)
415
+ );
346
416
  inline-size: calc(100% - var(--_inset) * 2);
347
- translate: 0 calc(var(--_index) * 100%);
417
+ translate: 0 calc(var(--_index) * (100% + var(--_spacing)));
348
418
  }
349
419
 
350
420
  /**
@@ -356,10 +426,7 @@
356
426
  * beside it.
357
427
  */
358
428
  [data-ui='segmented-control'] [data-part='item'][data-checked] {
359
- color: var(
360
- --slotted-segmented-control-checked-color,
361
- var(--slotted-tone-accent-text, HighlightText)
362
- );
429
+ color: var(--slotted-segmented-control-checked-color, var(--slotted-text, CanvasText));
363
430
  }
364
431
 
365
432
  /* ── Hover and press, held back for every stronger state ──────────────── */
@@ -376,10 +443,10 @@
376
443
  [data-ui='segmented-control'][data-part='root']:not([data-readonly])
377
444
  [data-part='item']:hover:not([data-disabled]):not([data-checked]):has(
378
445
  [data-part='control']:enabled
379
- ) {
446
+ )::after {
380
447
  background: var(
381
448
  --slotted-segmented-control-hover-background,
382
- var(--slotted-tone-secondary-subtle-hover, ButtonFace)
449
+ color-mix(in oklab, var(--slotted-text) 5%, transparent)
383
450
  );
384
451
  }
385
452
 
@@ -391,10 +458,10 @@
391
458
  [data-ui='segmented-control'][data-part='root']:not([data-readonly])
392
459
  [data-part='item']:active:not([data-disabled]):not([data-checked]):has(
393
460
  [data-part='control']:enabled
394
- ) {
461
+ )::after {
395
462
  background: var(
396
463
  --slotted-segmented-control-active-background,
397
- var(--slotted-tone-secondary-subtle-active, ButtonFace)
464
+ color-mix(in oklab, var(--slotted-text) 9%, transparent)
398
465
  );
399
466
  transition-duration: var(--slotted-duration-instant);
400
467
  }
@@ -544,7 +611,9 @@
544
611
  [data-ui='segmented-control'][data-part='root'][data-variant='underline'] [data-part='indicator'],
545
612
  [data-ui='segmented-control'][data-part='root'][data-variant='underline']
546
613
  [data-part='item']
547
- + [data-part='item']::before {
614
+ + [data-part='item']::before,
615
+ [data-ui='segmented-control'][data-part='root'][data-variant='underline']
616
+ [data-part='item']::after {
548
617
  display: none;
549
618
  }
550
619
 
@@ -636,6 +705,16 @@
636
705
  * ring belongs to a segment and leaves it alone.
637
706
  */
638
707
  @media (forced-colors: active) {
708
+ /* The neutral track is a tint, and a tint flattens to Canvas: the edge
709
+ says where the control is, and the thumb's edge says which is chosen. */
710
+ [data-ui='segmented-control'][data-part='root']:not([data-variant='underline']) {
711
+ border-color: ButtonBorder;
712
+ }
713
+
714
+ [data-ui='segmented-control'] [data-part='indicator'] {
715
+ border-color: Highlight;
716
+ }
717
+
639
718
  [data-ui='segmented-control'][data-part='root'][data-invalid] {
640
719
  outline: var(--_invalid-edge-width) solid CanvasText;
641
720
  outline-offset: calc(-1 * (var(--_border-width) + var(--_invalid-edge-width)));
@@ -1,13 +1,10 @@
1
1
  [
2
- "--slotted-border-subtle",
3
2
  "--slotted-border-width-thick",
4
- "--slotted-control-border-color",
5
3
  "--slotted-control-border-width",
6
4
  "--slotted-control-font-family",
7
5
  "--slotted-control-font-size-lg",
8
6
  "--slotted-control-font-size-md",
9
7
  "--slotted-control-font-size-sm",
10
- "--slotted-control-font-weight",
11
8
  "--slotted-control-gap-lg",
12
9
  "--slotted-control-gap-md",
13
10
  "--slotted-control-gap-sm",
@@ -24,8 +21,6 @@
24
21
  "--slotted-control-padding-inline-md",
25
22
  "--slotted-control-padding-inline-sm",
26
23
  "--slotted-control-radius",
27
- "--slotted-control-shadow",
28
- "--slotted-control-surface",
29
24
  "--slotted-control-transition-duration",
30
25
  "--slotted-control-transition-easing",
31
26
  "--slotted-disabled-background",
@@ -39,6 +34,7 @@
39
34
  "--slotted-focus-ring-offset",
40
35
  "--slotted-focus-ring-width",
41
36
  "--slotted-font-weight-medium",
37
+ "--slotted-radius-full",
42
38
  "--slotted-radius-xs",
43
39
  "--slotted-segmented-control-active-background",
44
40
  "--slotted-segmented-control-background",
@@ -49,9 +45,12 @@
49
45
  "--slotted-segmented-control-disabled-color",
50
46
  "--slotted-segmented-control-disabled-indicator-background",
51
47
  "--slotted-segmented-control-divider-color",
48
+ "--slotted-segmented-control-font-weight",
49
+ "--slotted-segmented-control-gap",
52
50
  "--slotted-segmented-control-hover-background",
53
51
  "--slotted-segmented-control-indicator-background",
54
52
  "--slotted-segmented-control-indicator-border-color",
53
+ "--slotted-segmented-control-indicator-shadow",
55
54
  "--slotted-segmented-control-inset",
56
55
  "--slotted-segmented-control-invalid-border-color",
57
56
  "--slotted-segmented-control-invalid-edge-width",
@@ -61,15 +60,12 @@
61
60
  "--slotted-segmented-control-underline-checked-color",
62
61
  "--slotted-segmented-control-underline-color",
63
62
  "--slotted-segmented-control-underline-gap",
63
+ "--slotted-shadow-xs",
64
64
  "--slotted-space-1",
65
65
  "--slotted-space-6",
66
+ "--slotted-surface-raised",
66
67
  "--slotted-target-min",
67
68
  "--slotted-text",
68
69
  "--slotted-text-muted",
69
- "--slotted-tone-accent-border",
70
- "--slotted-tone-accent-subtle-active",
71
- "--slotted-tone-accent-text",
72
- "--slotted-tone-danger-border",
73
- "--slotted-tone-secondary-subtle-active",
74
- "--slotted-tone-secondary-subtle-hover"
70
+ "--slotted-tone-danger-border"
75
71
  ]
@@ -48,6 +48,13 @@
48
48
  * there is to give.
49
49
  */
50
50
  --_standoff: min(var(--slotted-space-2, 0.25rem), var(--_inset));
51
+ /**
52
+ * The inline padding `app-shell`'s navigation parts add for an outward
53
+ * focus ring, read so a row's box keeps the edge it had there: the row
54
+ * reaches back into that padding, and its own ring is drawn inside. Nought
55
+ * outside those parts.
56
+ */
57
+ --_ring-inset: var(--slotted-app-shell-row-ring-inset, 0px);
51
58
  --_gap: var(--slotted-app-shell-row-gap, var(--slotted-control-gap-md, 8px));
52
59
  --_font-size: var(--slotted-app-shell-row-font-size, var(--slotted-font-size-sm, 0.8125rem));
53
60
  --_line-height: var(--slotted-app-shell-row-line-height, var(--slotted-line-height-sm, 1.45));
@@ -354,11 +361,11 @@
354
361
  grid-template-columns: auto minmax(0, 1fr) auto auto;
355
362
  grid-template-rows: auto auto;
356
363
  margin-block: 0;
357
- margin-inline: var(--_standoff);
364
+ margin-inline: calc(var(--_standoff) - var(--_ring-inset));
358
365
  min-block-size: var(--_row-height);
359
366
  min-inline-size: 0;
360
367
  padding-block: 0;
361
- padding-inline: calc(var(--_inset) - var(--_standoff));
368
+ padding-inline: calc(var(--_inset) - var(--_standoff) + var(--_ring-inset));
362
369
  position: relative;
363
370
  text-align: start;
364
371
  text-decoration: none;
@@ -5,6 +5,7 @@
5
5
  "--slotted-app-shell-row-inset",
6
6
  "--slotted-app-shell-row-line-height",
7
7
  "--slotted-app-shell-row-mark",
8
+ "--slotted-app-shell-row-ring-inset",
8
9
  "--slotted-border-subtle",
9
10
  "--slotted-border-width-thin",
10
11
  "--slotted-control-font-family",
@@ -218,7 +218,7 @@
218
218
  * number is broken only when it is wider than the card itself. A floor on
219
219
  * the trend was tried and pushed the trend through the card's edge.
220
220
  */
221
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
221
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
222
222
  :scope [data-part='body'] {
223
223
  column-gap: var(--_trend-gap);
224
224
  display: grid;
@@ -234,7 +234,7 @@
234
234
  }
235
235
  }
236
236
 
237
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
237
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
238
238
  :scope[data-layout='stacked'] [data-part='body'] {
239
239
  grid-template-areas:
240
240
  'header'
@@ -254,14 +254,14 @@
254
254
  * inline axis gives these regions no intrinsic width: the figure alone
255
255
  * decides the column, and the text is cut or wrapped by the card's width.
256
256
  */
257
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
257
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
258
258
  :scope [data-part='header'] {
259
259
  contain: inline-size;
260
260
  grid-area: header;
261
261
  }
262
262
  }
263
263
 
264
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
264
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
265
265
  :scope [data-part='figure'] {
266
266
  align-self: start;
267
267
  grid-area: figure;
@@ -276,7 +276,7 @@
276
276
  * width is the space a wide value left, which a query on the card cannot
277
277
  * see.
278
278
  */
279
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
279
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
280
280
  :scope [data-part='trend'] {
281
281
  /* The marker's ring is the card's ground, not the page's. */
282
282
  --slotted-sparkline-ring-color: var(--_surface);
@@ -291,21 +291,21 @@
291
291
  }
292
292
 
293
293
  @container stat-card-trend (max-width: 39px) {
294
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
294
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
295
295
  :scope [data-part='trend'] > * {
296
296
  visibility: hidden;
297
297
  }
298
298
  }
299
299
  }
300
300
 
301
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
301
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
302
302
  :scope[data-layout='stacked'] [data-part='trend'] {
303
303
  align-self: end;
304
304
  margin-block-start: var(--slotted-space-2, 0.25rem);
305
305
  }
306
306
  }
307
307
 
308
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
308
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
309
309
  :scope [data-part='message'] {
310
310
  contain: inline-size;
311
311
  grid-area: message;
@@ -319,7 +319,7 @@
319
319
  * (`docs/token-foundation.md`, rule 4).
320
320
  */
321
321
  @container stat-card (max-width: 219px) {
322
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
322
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
323
323
  :scope [data-part='body'] {
324
324
  grid-template-areas:
325
325
  'header'
@@ -331,14 +331,14 @@
331
331
  }
332
332
  }
333
333
 
334
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
334
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
335
335
  :scope [data-part='trend'] {
336
336
  align-self: end;
337
337
  margin-block-start: var(--slotted-space-2, 0.25rem);
338
338
  }
339
339
  }
340
340
 
341
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
341
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
342
342
  :scope [data-part='value'] {
343
343
  font-size: var(--_value-size-narrow);
344
344
  line-height: var(--_value-line-narrow);
@@ -346,7 +346,7 @@
346
346
  }
347
347
 
348
348
  /* The value steps down here, and so does the line its placeholder keeps. */
349
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
349
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
350
350
  :scope[data-loading] [data-part='value'] {
351
351
  min-block-size: calc(var(--_value-size-narrow) * var(--_value-line-narrow));
352
352
  }
@@ -361,7 +361,7 @@
361
361
  * them as part of the name, instead of floating them at the middle of the
362
362
  * block.
363
363
  */
364
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
364
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
365
365
  :scope [data-part='header'] {
366
366
  align-items: baseline;
367
367
  column-gap: var(--slotted-space-3, 0.375rem);
@@ -381,7 +381,7 @@
381
381
  * proportion of the type rather than a scale step: it is a width of
382
382
  * words, and it scales with them.
383
383
  */
384
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
384
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
385
385
  :scope [data-part='label'] {
386
386
  color: var(--_ink-muted);
387
387
  flex: 1 1 0%;
@@ -394,7 +394,7 @@
394
394
  }
395
395
  }
396
396
 
397
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
397
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
398
398
  :scope [data-part='status'] {
399
399
  display: inline-flex;
400
400
  flex: none;
@@ -404,7 +404,7 @@
404
404
  /* The action takes the end of the header's first line and stays there
405
405
  when the label wraps: it is the one thing on the card the reader presses,
406
406
  and a corner is where a control is looked for. */
407
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
407
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
408
408
  :scope [data-part='action'] {
409
409
  flex: none;
410
410
  /* The label's line, so a link here is as tall in the fallback face as in
@@ -420,13 +420,13 @@
420
420
  part itself when `render` made it one — reaches
421
421
  the comfortable size in both axes, and the header keeps its height. */
422
422
  @media (pointer: coarse) {
423
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
423
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
424
424
  :scope :is([data-part='action']:is(a, button), [data-part='action'] > :is(a, button)) {
425
425
  position: relative;
426
426
  }
427
427
  }
428
428
 
429
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
429
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
430
430
  :scope
431
431
  :is([data-part='action']:is(a, button), [data-part='action'] > :is(a, button))::before {
432
432
  content: '';
@@ -444,7 +444,7 @@
444
444
  * so a delta that does not fit beside a wide number drops under it rather
445
445
  * than pushing the number.
446
446
  */
447
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
447
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
448
448
  :scope [data-part='figure'] {
449
449
  align-items: baseline;
450
450
  column-gap: var(--slotted-space-3, 0.375rem);
@@ -454,7 +454,7 @@
454
454
  }
455
455
  }
456
456
 
457
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
457
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
458
458
  :scope [data-part='comparison'] {
459
459
  contain: inline-size;
460
460
  flex: 1 1 100%;
@@ -471,7 +471,7 @@
471
471
  * to pull a large numeral's counters back together, and it is the same
472
472
  * fraction at every size.
473
473
  */
474
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
474
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
475
475
  :scope [data-part='value'] {
476
476
  /* The figure and its unit are two flex items on one baseline — the text
477
477
  node is an item of its own — so the gap spaces the unit from the digit
@@ -502,7 +502,7 @@
502
502
 
503
503
  /* The unit is in the value's own line, a step down in size and weight, so
504
504
  `$` and `%` read as annotation of the number rather than as digits. */
505
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
505
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
506
506
  :scope [data-part='unit'] {
507
507
  color: var(--_ink-muted);
508
508
  font-size: var(--_delta-size);
@@ -517,7 +517,7 @@
517
517
  * metrics, and the tone's text role already clears the reading ratio on
518
518
  * the surface. It sits on the value's baseline.
519
519
  */
520
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
520
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
521
521
  :scope [data-part='delta'] {
522
522
  align-items: center;
523
523
  color: var(--_tone-text);
@@ -535,37 +535,37 @@
535
535
  }
536
536
  }
537
537
 
538
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
538
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
539
539
  :scope [data-part='delta'][data-tone='neutral'] {
540
540
  --_tone-text: var(--_ink-muted);
541
541
  }
542
542
  }
543
543
 
544
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
544
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
545
545
  :scope [data-part='delta'][data-tone='accent'] {
546
546
  --_tone-text: var(--slotted-tone-accent-text);
547
547
  }
548
548
  }
549
549
 
550
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
550
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
551
551
  :scope [data-part='delta'][data-tone='secondary'] {
552
552
  --_tone-text: var(--slotted-tone-secondary-text);
553
553
  }
554
554
  }
555
555
 
556
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
556
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
557
557
  :scope [data-part='delta'][data-tone='success'] {
558
558
  --_tone-text: var(--slotted-tone-success-text);
559
559
  }
560
560
  }
561
561
 
562
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
562
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
563
563
  :scope [data-part='delta'][data-tone='warning'] {
564
564
  --_tone-text: var(--slotted-tone-warning-text);
565
565
  }
566
566
  }
567
567
 
568
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
568
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
569
569
  :scope [data-part='delta'][data-tone='danger'] {
570
570
  --_tone-text: var(--slotted-tone-danger-text);
571
571
  }
@@ -579,7 +579,7 @@
579
579
  * change draws `minus`. The binding renders the glyph inside the icon box;
580
580
  * the part hands the box its size, and the box inherits the delta's ink.
581
581
  */
582
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
582
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
583
583
  :scope [data-part='indicator'] {
584
584
  --slotted-icon-size: var(--_indicator-size);
585
585
 
@@ -592,7 +592,7 @@
592
592
  a rise becomes a fall. Time runs from the right in a right-to-left
593
593
  document, so the rise points the other way there, as the sparkline beside
594
594
  it does — the `directional` icon box the binding draws does that. */
595
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
595
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
596
596
  :scope [data-part='delta'][data-trend='down'] [data-part='indicator'] > [data-ui='icon'] {
597
597
  rotate: x 180deg;
598
598
  }
@@ -600,7 +600,7 @@
600
600
 
601
601
  /* ── Comparison: the prose under the figure ───────────────────────────── */
602
602
 
603
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
603
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
604
604
  :scope [data-part='comparison'] {
605
605
  color: var(--_ink-muted);
606
606
  font-size: var(--_prose-size);
@@ -612,7 +612,7 @@
612
612
  }
613
613
  }
614
614
 
615
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
615
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
616
616
  :scope [data-part='period'] {
617
617
  color: var(--_ink-muted);
618
618
  }
@@ -628,44 +628,44 @@
628
628
  * — a card with no delta, or a metric with no valence, draws its trend in
629
629
  * the theme's blue and not in grey.
630
630
  */
631
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
631
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
632
632
  :scope [data-part='trend'] {
633
633
  align-self: center;
634
634
  display: flex;
635
635
  }
636
636
  }
637
637
 
638
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
638
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
639
639
  :scope[data-tone='success'] [data-part='trend'] {
640
640
  --slotted-sparkline-color: var(--slotted-tone-success-text);
641
641
  }
642
642
  }
643
643
 
644
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
644
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
645
645
  :scope[data-tone='danger'] [data-part='trend'] {
646
646
  --slotted-sparkline-color: var(--slotted-tone-danger-text);
647
647
  }
648
648
  }
649
649
 
650
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
650
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
651
651
  :scope[data-tone='warning'] [data-part='trend'] {
652
652
  --slotted-sparkline-color: var(--slotted-tone-warning-text);
653
653
  }
654
654
  }
655
655
 
656
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
656
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
657
657
  :scope[data-tone='accent'] [data-part='trend'] {
658
658
  --slotted-sparkline-color: var(--slotted-tone-accent-text);
659
659
  }
660
660
  }
661
661
 
662
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
662
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
663
663
  :scope[data-tone='secondary'] [data-part='trend'] {
664
664
  --slotted-sparkline-color: var(--slotted-tone-secondary-text);
665
665
  }
666
666
  }
667
667
 
668
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
668
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
669
669
  :scope[data-fill='plain'] [data-part='trend'] {
670
670
  --slotted-sparkline-ring-color: var(--slotted-surface-raised, Canvas);
671
671
  }
@@ -673,7 +673,7 @@
673
673
 
674
674
  /* ── Message: what stands in for a value that is missing or failed ────── */
675
675
 
676
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
676
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
677
677
  :scope [data-part='message'] {
678
678
  color: var(--_ink-muted);
679
679
  font-size: var(--_prose-size);
@@ -684,7 +684,7 @@
684
684
  }
685
685
  }
686
686
 
687
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
687
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
688
688
  :scope[data-status='error'] [data-part='message'] {
689
689
  color: var(--slotted-tone-danger-text);
690
690
  }
@@ -692,7 +692,7 @@
692
692
 
693
693
  /* A value that is not there is a dash in the muted step, at the value's
694
694
  own size, so the row keeps its height and its rhythm. */
695
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
695
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
696
696
  :scope[data-status='empty'] [data-part='value'],
697
697
  :scope[data-status='error'] [data-part='value'] {
698
698
  color: var(--_ink-muted);
@@ -716,13 +716,13 @@
716
716
  * arrived. The floor is one line of the value's own type, so the two states
717
717
  * occupy the same box.
718
718
  */
719
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
719
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
720
720
  :scope[data-loading] [data-part='value'] {
721
721
  min-block-size: calc(var(--_value-size) * var(--_value-line));
722
722
  }
723
723
  }
724
724
 
725
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
725
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
726
726
  :scope[data-loading] [data-part='trend'] {
727
727
  --slotted-skeleton-block-size: var(
728
728
  --slotted-sparkline-block-size,
@@ -731,7 +731,7 @@
731
731
  }
732
732
  }
733
733
 
734
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
734
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
735
735
  :scope[data-loading][data-size='sm'] [data-part='trend'] {
736
736
  --slotted-skeleton-block-size: var(
737
737
  --slotted-sparkline-block-size-sm,
@@ -740,7 +740,7 @@
740
740
  }
741
741
  }
742
742
 
743
- @scope ([data-ui='stat-card']) to ([data-ui]:not(:scope) > *) {
743
+ @scope ([data-ui='stat-card']) to (:scope [data-ui] > *) {
744
744
  :scope[data-loading][data-size='lg'] [data-part='trend'] {
745
745
  --slotted-skeleton-block-size: var(
746
746
  --slotted-sparkline-block-size-lg,
package/src/tabs/tabs.css CHANGED
@@ -50,7 +50,7 @@
50
50
  /**
51
51
  * The marker: one bar the list draws under the selected tab, and the hover
52
52
  * bar under the tab the pointer is on, in the same geometry — the tab's
53
- * content box, a pill on the list's edge. It is never thinner than the
53
+ * content box, a pill just above the list's edge. It is never thinner than the
54
54
  * thick border width, the floor the high-contrast scheme keeps for a mark
55
55
  * that carries state, and its thickness, its rounding and its inset are
56
56
  * tokens of their own: tied to the thick border width, a theme could not
@@ -62,6 +62,13 @@
62
62
  );
63
63
  --_marker-radius: var(--slotted-tabs-marker-radius, var(--slotted-radius-full, 9999px));
64
64
  --_marker-inset: var(--slotted-tabs-marker-inset, var(--_padding-inline));
65
+ /**
66
+ * The gap between the bars and the list's edge: the selected marker and the
67
+ * hover bar stand this far above the track, so the line reads as a line and
68
+ * the bar as a mark on it. An inset rather than a transform, because the
69
+ * marker's `translate` is its slide between tabs.
70
+ */
71
+ --_marker-offset: var(--slotted-tabs-marker-offset, 1px);
65
72
  --_track-color: var(--slotted-tabs-track-color, var(--slotted-border-subtle, ButtonBorder));
66
73
  /**
67
74
  * The length of the fade a scrolling list takes at an edge that has more
@@ -460,8 +467,8 @@
460
467
  * `--slotted-tabs-marker-inset`, which defaults to the tab's own inline
461
468
  * padding, and never the closing control: a tab followed by one gives up its
462
469
  * end padding, so the end inset is nought there. Below the width of a short
463
- * label it keeps a minimum length, centred on the content. It stands on the
464
- * list's edge, over the track, and the axes are physical in this one rule
470
+ * label it keeps a minimum length, centred on the content. It stands
471
+ * `--slotted-tabs-marker-offset` above the list's edge, over the track, and the axes are physical in this one rule
465
472
  * because the measurement is: the translate is from the list's left edge.
466
473
  */
467
474
  [data-ui='tabs'] > :where([data-part='list']) > [data-part='marker'] {
@@ -475,7 +482,7 @@
475
482
  block-size: var(--_marker-size);
476
483
  border-radius: var(--_marker-radius);
477
484
  inline-size: var(--_length);
478
- inset-block-end: var(--_marker-lift, 0px);
485
+ inset-block-end: calc(var(--_marker-lift, 0px) + var(--_marker-offset));
479
486
  left: 0;
480
487
  pointer-events: none;
481
488
  position: absolute;
@@ -500,20 +507,26 @@
500
507
  }
501
508
 
502
509
  /**
503
- * Placed once without motion, then it slides: the first position arrives
504
- * before `data-positioned`, so the bar does not travel in from the list's
505
- * corner. Under reduced motion it jumps.
510
+ * It slides only when the selection moves: `data-moving` arrives with a
511
+ * position whose tab changed, and leaves with a re-measurement of the same
512
+ * tab, so the first position, a font arriving and a resized list place the
513
+ * bar without motion. The slide takes the medium duration on a curve that
514
+ * starts and ends slowly, `--slotted-tabs-marker-easing`: the control
515
+ * duration on the theme's decelerating curve covered a third of the
516
+ * distance in its first frame and read as a jump. Under reduced motion it
517
+ * jumps.
506
518
  */
507
- [data-ui='tabs'] > :where([data-part='list']) > [data-part='marker'][data-positioned] {
519
+ [data-ui='tabs'] > :where([data-part='list']) > [data-part='marker'][data-moving] {
520
+ --_marker-easing: var(--slotted-tabs-marker-easing, cubic-bezier(0.4, 0, 0.2, 1));
521
+
508
522
  transition:
509
- translate var(--slotted-control-transition-duration) var(--slotted-control-transition-easing),
510
- inline-size var(--slotted-control-transition-duration)
511
- var(--slotted-control-transition-easing),
512
- block-size var(--slotted-control-transition-duration) var(--slotted-control-transition-easing);
523
+ translate var(--slotted-duration-medium) var(--_marker-easing),
524
+ inline-size var(--slotted-duration-medium) var(--_marker-easing),
525
+ block-size var(--slotted-duration-medium) var(--_marker-easing);
513
526
  }
514
527
 
515
528
  @media (prefers-reduced-motion: reduce) {
516
- [data-ui='tabs'] > :where([data-part='list']) > [data-part='marker'][data-positioned] {
529
+ [data-ui='tabs'] > :where([data-part='list']) > [data-part='marker'][data-moving] {
517
530
  transition: none;
518
531
  }
519
532
  }
@@ -532,7 +545,7 @@
532
545
  block-size: var(--_marker-size);
533
546
  border-radius: var(--_marker-radius);
534
547
  content: '';
535
- inset-block-end: calc(-1 * var(--_track-reach, 0px));
548
+ inset-block-end: calc(var(--_marker-offset) - var(--_track-reach, 0px));
536
549
  inset-inline: var(--_marker-inset);
537
550
  min-inline-size: var(--slotted-space-6, 1rem);
538
551
  opacity: 0;
@@ -578,7 +591,7 @@
578
591
  block-size: var(--_length);
579
592
  inline-size: var(--_marker-size);
580
593
  inset-block: 0 auto;
581
- inset-inline: auto var(--_marker-lift, 0px);
594
+ inset-inline: auto calc(var(--_marker-lift, 0px) + var(--_marker-offset));
582
595
  translate: 0
583
596
  calc(var(--_indicator-start, 0px) + var(--_start-inset) + (var(--_span) - var(--_length)) / 2);
584
597
  }
@@ -589,7 +602,7 @@
589
602
  block-size: auto;
590
603
  inline-size: var(--_marker-size);
591
604
  inset-block: var(--_marker-inset);
592
- inset-inline: auto calc(-1 * var(--_track-reach, 0px));
605
+ inset-inline: auto calc(var(--_marker-offset) - var(--_track-reach, 0px));
593
606
  min-block-size: var(--slotted-space-6, 1rem);
594
607
  min-inline-size: 0;
595
608
  }
@@ -31,6 +31,7 @@
31
31
  "--slotted-disabled-background",
32
32
  "--slotted-disabled-border",
33
33
  "--slotted-disabled-foreground",
34
+ "--slotted-duration-medium",
34
35
  "--slotted-duration-short",
35
36
  "--slotted-easing-enter",
36
37
  "--slotted-focus-ring-color",
@@ -65,7 +66,9 @@
65
66
  "--slotted-tabs-hover-marker-opacity",
66
67
  "--slotted-tabs-list-gap",
67
68
  "--slotted-tabs-marker-color",
69
+ "--slotted-tabs-marker-easing",
68
70
  "--slotted-tabs-marker-inset",
71
+ "--slotted-tabs-marker-offset",
69
72
  "--slotted-tabs-marker-radius",
70
73
  "--slotted-tabs-marker-size",
71
74
  "--slotted-tabs-scroll-color",