actual-css 0.4.1 → 0.5.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.
Files changed (74) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +2 -2
  3. package/dist/actual.css +53 -14
  4. package/dist/actual.full.css +300 -91
  5. package/dist/actual.full.js +1 -1
  6. package/dist/actual.full.min.css +1 -1
  7. package/dist/actual.min.css +1 -1
  8. package/docs/design-notes/spacing-units.md +286 -0
  9. package/docs/navigation.json +1 -0
  10. package/docs/pages/components/alert.md +1 -1
  11. package/docs/pages/components/app-navigation.md +2 -0
  12. package/docs/pages/components/badge.md +17 -3
  13. package/docs/pages/components/button.md +9 -2
  14. package/docs/pages/components/card.md +13 -0
  15. package/docs/pages/components/data-list.md +72 -0
  16. package/docs/pages/components/dialog.md +2 -0
  17. package/docs/pages/components/drawer.md +2 -0
  18. package/docs/pages/components/fab.md +2 -0
  19. package/docs/pages/components/meter.md +56 -1
  20. package/docs/pages/components/progress.md +10 -0
  21. package/docs/pages/components/skeleton.md +2 -0
  22. package/docs/pages/components/steps.md +10 -2
  23. package/docs/pages/components/table.md +1 -0
  24. package/docs/pages/enhancements/flyout.md +2 -0
  25. package/docs/pages/enhancements/status-bar.md +10 -1
  26. package/docs/pages/examples/overview.md +2 -1
  27. package/docs/pages/forms/joined-controls.md +2 -0
  28. package/docs/pages/forms/overview.md +1 -1
  29. package/docs/pages/forms/range.md +2 -0
  30. package/docs/pages/forms/states-and-sizes.md +1 -1
  31. package/docs/pages/forms/validation.md +2 -0
  32. package/docs/pages/foundations/philosophy.md +11 -1
  33. package/docs/pages/foundations/tokens.md +7 -3
  34. package/docs/pages/foundations/typography.md +10 -2
  35. package/docs/pages/guides/introduction.md +1 -1
  36. package/docs/pages/layout/scroll-snap.md +2 -0
  37. package/docs/pages/layout/sidebar-layout.md +2 -0
  38. package/docs/pages/layout/topbar.md +2 -0
  39. package/docs/pages/utilities/accessibility.md +18 -0
  40. package/package.json +1 -1
  41. package/scripts/reserved-classes.json +4 -0
  42. package/src/css/components/actions.css +1 -0
  43. package/src/css/components/breadcrumb.css +1 -0
  44. package/src/css/components/data-list.css +27 -0
  45. package/src/css/components/drawer.css +30 -11
  46. package/src/css/components/flyout.css +17 -12
  47. package/src/css/components/index.css +1 -0
  48. package/src/css/components/meter.css +28 -1
  49. package/src/css/components/modal.css +28 -11
  50. package/src/css/components/navbar.css +2 -0
  51. package/src/css/components/pagination.css +1 -0
  52. package/src/css/components/progress.css +5 -0
  53. package/src/css/components/status-bar.css +31 -15
  54. package/src/css/components/steps.css +36 -9
  55. package/src/css/components/surface.css +5 -3
  56. package/src/css/components/tab.css +1 -0
  57. package/src/css/components/tooltip.css +5 -8
  58. package/src/css/core/base.css +7 -1
  59. package/src/css/core/intents.css +6 -0
  60. package/src/css/core/tokens.css +23 -9
  61. package/src/css/core/variants.css +17 -4
  62. package/src/css/forms/form-actions.css +1 -0
  63. package/src/css/forms/form.css +4 -4
  64. package/src/css/layout/cluster.css +1 -0
  65. package/src/css/layout/scroll-snap.css +1 -0
  66. package/src/css/layout/sidebar-layout.css +1 -0
  67. package/src/css/layout/switcher.css +1 -0
  68. package/src/css/layout/topbar.css +1 -0
  69. package/src/css/typography/prose.css +2 -3
  70. package/src/css/utilities/base.css +16 -0
  71. package/src/js/selectors.js +1 -0
  72. package/src/js/status.js +88 -7
  73. package/src/js/surface.js +2 -11
  74. package/src/js/transition.js +20 -0
@@ -60,8 +60,8 @@ select {
60
60
  *
61
61
  * Derived: "Theme-derived aliases" and "State tokens" reference intent and
62
62
  * surface tokens and follow the theme; do not hard-code values into them.
63
- * Internal: indicator offsets, --bar-height, --disabled-opacity and the
64
- * like are consumed by components, not authored.
63
+ * Internal: indicator offsets, --disabled-opacity and the like are
64
+ * consumed by components, not authored.
65
65
  *
66
66
  * Component-local hooks and internal state relays live with their
67
67
  * components. See docs/pages/foundations/tokens.md for the public/internal
@@ -85,9 +85,10 @@ select {
85
85
  --font-size-md: 1rem;
86
86
  --font-size-lg: 1.125rem;
87
87
 
88
- /* Spacing — official steps are named in tens so the scale stays open for
89
- application themes to intercalate intermediate values (--space-15,
90
- --space-45…) without colliding with future framework steps. */
88
+ /* Spacing — official steps are named in tens so the scale stays open.
89
+ Actual CSS will never define a --space-* that is not a multiple of 10:
90
+ the intermediate names (--space-15, --space-45…) are reserved for
91
+ applications, and no future framework step can collide with them. */
91
92
  --gap: 0.75rem;
92
93
  --density-space: var(--space-40);
93
94
  --density-compact-size: 1.5rem;
@@ -105,14 +106,25 @@ select {
105
106
  --shadow-popout: 0 0.25rem 0.75rem rgb(0 0 0 / 0.15);
106
107
 
107
108
  /* Motion — see reset.css for the prefers-reduced-motion hard reset.
108
- --duration drives component transitions (hover/focus); the dedicated
109
- animation rates below tune one-shot open/close and decorative loops. */
109
+ --duration scales component transitions (hover/focus); the dedicated
110
+ animation rates below tune one-shot open/close and decorative loops.
111
+ Generic state/interaction motion uses the CSS default `ease` directly
112
+ (registered nowhere). --ease-enter/--ease-exit express the character of
113
+ one-shot presence changes, owned by the open/closed state rules that
114
+ separate them (status-bar, drawer, modal). The pair follows the standard
115
+ M3 rhythm; the more aggressive emphasized-decelerate entrance is
116
+ deliberately not offered because it only pays off on 250–400ms motions,
117
+ beyond this vocabulary. Toggles that share one transition list at
118
+ --duration-fast (flyout, tooltip) and pure-opacity backdrops stay on
119
+ `ease`: at 100ms the curve has almost no resolving time, and a scrim is
120
+ not traveling anywhere. */
110
121
  --duration: 150ms;
111
122
  --duration-fast: 100ms;
112
123
  --duration-slow: 200ms;
113
124
  --duration-spin: 0.75s;
114
125
  --duration-shimmer: 1.2s;
115
- --ease: ease;
126
+ --ease-enter: cubic-bezier(0.2, 0, 0, 1);
127
+ --ease-exit: cubic-bezier(0.4, 0, 1, 1);
116
128
 
117
129
  /* Focus ring — shared by every interactive component's :focus-visible */
118
130
  --focus-outline-color: currentColor;
@@ -130,7 +142,9 @@ select {
130
142
  --backdrop-opacity: 0.45;
131
143
  --backdrop-fill: rgb(0 0 0 / var(--backdrop-opacity));
132
144
 
133
- /* Progress / meter track height */
145
+ /* Bar thickness shared by .progress and .meter — two conceptual variants of
146
+ the same geometry. Public: set it on the element or on an application
147
+ class rather than reaching for a size variant. */
134
148
  --bar-height: 0.65rem;
135
149
 
136
150
  /* Controls — inputs and buttons share --control-size so heights always match.
@@ -453,6 +467,9 @@ select {
453
467
  *
454
468
  * Sits between reset (browser defaults) and prose (rich content scope).
455
469
  * These styles apply everywhere — cards, dialogs, heroes — not only in prose.
470
+ *
471
+ * Public hooks:
472
+ * --link
456
473
  */
457
474
 
458
475
  body {
@@ -462,8 +479,11 @@ body {
462
479
  line-height: var(--line-height);
463
480
  }
464
481
 
482
+ /* Bare links inherit their ink by default, so nothing changes unless a theme
483
+ opts in by setting --link. Because this is an element selector, any component
484
+ rule that colors its own anchors still wins. */
465
485
  a {
466
- color: inherit;
486
+ color: var(--link, inherit);
467
487
  text-underline-offset: 0.18em;
468
488
  }
469
489
 
@@ -547,11 +567,17 @@ small {
547
567
  --intent-fg: var(--neutral-fg);
548
568
  }
549
569
 
570
+ /* Paint an inherited or local intent on a simple foreground element without
571
+ turning the intent classes into general-purpose color utilities. */
572
+ .intent-color {
573
+ color: var(--intent);
574
+ }
575
+
550
576
  /*
551
577
  * Cross-cutting variant token mapping.
552
578
  *
553
- * Variant scope: .solid/.soft/.outline set --ui-* for components that opt into
554
- * shared variants. .inverted is a surface modifier, not a variant: it paints
579
+ * Variant scope: .solid/.soft/.outline/.surface set --ui-* for components that
580
+ * opt into shared variants. .inverted is a surface modifier, not a variant: it paints
555
581
  * the element directly (background/color/border-color) so it works on any bare
556
582
  * block, and remaps the shared --ui-* tokens so participating components
557
583
  * (.card, .navbar, .alert, .badge) invert through their own rules. Loaded
@@ -570,8 +596,10 @@ small {
570
596
  * components may consume --density-* when the density context should affect
571
597
  * their local rhythm. Compact components may consume --density-compact-size.
572
598
  * Components with bespoke sizing (spinner, avatar) define local .sm/.lg;
573
- * applying .sm/.lg to card, dialog, accordion, navbar, table is
574
- * a no-op.
599
+ * applying .sm/.lg to card, dialog, accordion, navbar, table, progress, meter
600
+ * is a no-op. Progress and meter are deliberate: a bar keeps its inline size
601
+ * and only changes thickness, which --bar-height expresses better than a
602
+ * three-step scale.
575
603
  *
576
604
  * Intent list source of truth: intents.css. Components consume --intent* tokens
577
605
  * instead of re-listing intent classes.
@@ -594,6 +622,17 @@ small {
594
622
  --ui-border: var(--intent, currentColor);
595
623
  }
596
624
 
625
+ /* Neutral chrome, intent only in the ink: the page surface with the theme
626
+ border. .soft is the intent-tinted treatment (its baseline fallback uses
627
+ --surface-subtle) and .outline has no fill at all, so neither can express
628
+ it. Deliberately literal — no color-mix enhancement — so this stays the one
629
+ variant that reads exactly as the theme's own surface. */
630
+ .surface {
631
+ --ui-bg: var(--surface);
632
+ --ui-fg: var(--intent, var(--text));
633
+ --ui-border: var(--border);
634
+ }
635
+
597
636
  /* Inverse surface treatment. Paints the element directly (so it works on any
598
637
  bare block) and remaps the shared --ui-* tokens so opt-in components
599
638
  (.card, .navbar) invert through their own rules. Because this loads before
@@ -776,9 +815,8 @@ small {
776
815
  font-size: var(--prose-h4);
777
816
  }
778
817
 
779
- /* Link ink defaults to --primary. --link is an optional escape hatch: a
780
- theme whose primary is too light or too vivid to read as inline text sets
781
- it without having to restate the rule. */
818
+ /* --link is a theme-level token (core/base.css also reads it for bare links);
819
+ prose falls back to --primary so a theme that never sets it is unaffected. */
782
820
  .prose :where(a) {
783
821
  color: var(--link, var(--primary));
784
822
  text-decoration-line: underline;
@@ -1096,6 +1134,7 @@ small {
1096
1134
  */
1097
1135
  .cluster {
1098
1136
  display: flex;
1137
+ flex-direction: row;
1099
1138
  flex-wrap: var(--cluster-wrap, wrap);
1100
1139
  align-items: var(--cluster-align, center);
1101
1140
  justify-content: var(--cluster-justify, flex-start);
@@ -1276,6 +1315,7 @@ small {
1276
1315
  */
1277
1316
  .switcher {
1278
1317
  display: flex;
1318
+ flex-direction: row;
1279
1319
  flex-wrap: wrap;
1280
1320
  gap: var(--gap);
1281
1321
  }
@@ -1305,6 +1345,7 @@ small {
1305
1345
  */
1306
1346
  .sidebar-layout {
1307
1347
  display: flex;
1348
+ flex-direction: row;
1308
1349
  flex-wrap: wrap;
1309
1350
  gap: var(--gap);
1310
1351
  align-items: flex-start;
@@ -1702,6 +1743,7 @@ small {
1702
1743
  --scroll-snap-item-size: auto;
1703
1744
 
1704
1745
  display: flex;
1746
+ flex-direction: row;
1705
1747
  gap: var(--scroll-snap-gap);
1706
1748
  overflow-x: auto;
1707
1749
  }
@@ -1773,6 +1815,7 @@ small {
1773
1815
  z-index: var(--z-sticky);
1774
1816
  isolation: isolate;
1775
1817
  display: flex;
1818
+ flex-direction: row;
1776
1819
  align-items: center;
1777
1820
  gap: var(--gap);
1778
1821
  padding-block: var(--space-30);
@@ -1817,6 +1860,10 @@ small {
1817
1860
  * Individual controls keep their own files; .choice label layout lives in
1818
1861
  * choice.css; .form-actions lives in form-actions.css.
1819
1862
  *
1863
+ * .field-group owns its own box only. Distance between sibling groups is a
1864
+ * composition concern (.stack on the form), never a sibling margin here: the
1865
+ * component cannot know whether its siblings are stacked or gridded.
1866
+ *
1820
1867
  * Public API: .field, .field-label, .field-help, .field-error, .field-group,
1821
1868
  * .required-mark.
1822
1869
  *
@@ -1837,10 +1884,6 @@ small {
1837
1884
  border-radius: var(--radius);
1838
1885
  }
1839
1886
 
1840
- .field-group + .field-group {
1841
- margin-block-start: var(--space-40);
1842
- }
1843
-
1844
1887
  .field-label,
1845
1888
  .field-group > legend {
1846
1889
  color: var(--text);
@@ -2420,6 +2463,7 @@ small {
2420
2463
  */
2421
2464
  .form-actions {
2422
2465
  display: flex;
2466
+ flex-direction: row;
2423
2467
  flex-wrap: wrap;
2424
2468
  gap: var(--gap);
2425
2469
  align-items: var(--form-actions-align, center);
@@ -3965,6 +4009,7 @@ a.badge[aria-disabled="true"] {
3965
4009
  --gap: var(--space-20);
3966
4010
 
3967
4011
  display: flex;
4012
+ flex-direction: row;
3968
4013
  flex-wrap: wrap;
3969
4014
  align-items: center;
3970
4015
  gap: var(--gap);
@@ -4154,7 +4199,9 @@ a.badge[aria-disabled="true"] {
4154
4199
  z-index: calc(var(--z-menu) - 1);
4155
4200
  background: var(--backdrop-fill);
4156
4201
  opacity: 1;
4157
- transition: opacity var(--duration) var(--ease);
4202
+ /* A scrim never travels; it fades on the neutral `ease`. Duration matches
4203
+ the flyout sheet it accompanies so it never lags the panel. */
4204
+ transition: opacity var(--duration-fast) ease;
4158
4205
  }
4159
4206
 
4160
4207
  .surface-backdrop[hidden] {
@@ -4171,8 +4218,8 @@ a.badge[aria-disabled="true"] {
4171
4218
  @supports (transition-behavior: allow-discrete) {
4172
4219
  .surface-backdrop {
4173
4220
  transition:
4174
- opacity var(--duration) var(--ease),
4175
- display var(--duration) allow-discrete;
4221
+ opacity var(--duration-fast) ease,
4222
+ display var(--duration-fast) allow-discrete;
4176
4223
  }
4177
4224
  }
4178
4225
  @media print {
@@ -4222,12 +4269,14 @@ dialog.modal {
4222
4269
  * Keep the open dialog root untransformed. Fixed overlays mounted inside a
4223
4270
  * modal dialog use viewport coordinates; transform/translate/scale on this
4224
4271
  * element would make them position against the dialog instead. The closed
4225
- * state can still carry a small transform for enter/exit motion.
4272
+ * state can still carry a small transform for enter/exit motion. The lift
4273
+ * is a pure vertical offset: scaling a wide panel around its center drags
4274
+ * both edges, which reads as horizontal movement.
4226
4275
  */
4227
- transform: translateY(0.35rem) scale(0.98);
4276
+ transform: translateY(0.35rem);
4228
4277
  transition:
4229
- opacity var(--duration),
4230
- transform var(--duration);
4278
+ opacity var(--duration) var(--ease-exit),
4279
+ transform var(--duration) var(--ease-exit);
4231
4280
  }
4232
4281
 
4233
4282
  dialog.modal > form,
@@ -4346,33 +4395,46 @@ dialog.modal.dialog-confirmation > form > footer {
4346
4395
  dialog.modal[open] {
4347
4396
  opacity: 1;
4348
4397
  transform: none;
4398
+ transition:
4399
+ opacity var(--duration) var(--ease-enter),
4400
+ transform var(--duration) var(--ease-enter);
4349
4401
  }
4350
4402
 
4351
4403
  @starting-style {
4352
4404
  dialog.modal[open] {
4353
4405
  opacity: 0;
4354
- transform: translateY(0.35rem) scale(0.98);
4406
+ transform: translateY(0.35rem);
4355
4407
  }
4356
4408
  }
4357
4409
 
4358
4410
  dialog.modal::backdrop {
4359
4411
  background: var(--backdrop-fill);
4360
4412
  opacity: 0;
4361
- transition: opacity var(--duration);
4413
+ /* Scrim curve stays neutral `ease`; only the panel carries the character
4414
+ curves. */
4415
+ transition: opacity var(--duration) ease;
4362
4416
  }
4363
4417
 
4364
4418
  @supports (transition-behavior: allow-discrete) {
4365
4419
  dialog.modal {
4366
4420
  transition:
4367
- opacity var(--duration),
4368
- transform var(--duration),
4421
+ opacity var(--duration) var(--ease-exit),
4422
+ transform var(--duration) var(--ease-exit),
4423
+ overlay var(--duration) allow-discrete,
4424
+ display var(--duration) allow-discrete;
4425
+ }
4426
+
4427
+ dialog.modal[open] {
4428
+ transition:
4429
+ opacity var(--duration) var(--ease-enter),
4430
+ transform var(--duration) var(--ease-enter),
4369
4431
  overlay var(--duration) allow-discrete,
4370
4432
  display var(--duration) allow-discrete;
4371
4433
  }
4372
4434
 
4373
4435
  dialog.modal::backdrop {
4374
4436
  transition:
4375
- opacity var(--duration),
4437
+ opacity var(--duration) ease,
4376
4438
  overlay var(--duration) allow-discrete,
4377
4439
  display var(--duration) allow-discrete;
4378
4440
  }
@@ -4397,13 +4459,15 @@ html.has-modal-open.had-scrollbar {
4397
4459
  }
4398
4460
 
4399
4461
  @media (prefers-reduced-motion: no-preference) {
4462
+ /* Wrong-close feedback and view-transition transport are motion quirks, not
4463
+ presence enter/exit, so they stay on the plain CSS default easing. */
4400
4464
  dialog.modal.is-static {
4401
- animation: actual-dialog-static var(--duration-slow) var(--ease);
4465
+ animation: actual-dialog-static var(--duration-slow) ease;
4402
4466
  }
4403
4467
 
4404
4468
  ::view-transition-group(actual-dialog) {
4405
4469
  animation-duration: var(--duration);
4406
- animation-timing-function: var(--ease);
4470
+ animation-timing-function: ease;
4407
4471
  }
4408
4472
  }
4409
4473
 
@@ -4502,11 +4566,16 @@ html.has-modal-open.had-scrollbar {
4502
4566
  *
4503
4567
  * Internal:
4504
4568
  * --drawer-closed-translate (side/RTL positioning state)
4569
+ * --drawer-motion-duration (shared by panel and ::backdrop so exit duration
4570
+ * changes in one place; entry state redefines it longer)
4505
4571
  */
4506
4572
  dialog.drawer {
4507
4573
  --drawer-size: 20rem;
4508
4574
  --drawer-pad: var(--space-40);
4509
4575
  --drawer-closed-translate: -100%;
4576
+ /* Exit is shorter than entry so the panel clears quickly; the scrim reads
4577
+ the same variable through inheritance and never lags the panel. */
4578
+ --drawer-motion-duration: var(--duration);
4510
4579
 
4511
4580
  /* Keep the surface anchored to the viewport in the top layer. This also
4512
4581
  provides the containing block for the absolutely positioned close button.
@@ -4530,16 +4599,20 @@ dialog.drawer {
4530
4599
  opacity: 0;
4531
4600
  transform: translateX(var(--drawer-closed-translate));
4532
4601
  transition:
4533
- opacity var(--duration-slow),
4534
- transform var(--duration-slow);
4602
+ opacity var(--drawer-motion-duration) var(--ease-exit),
4603
+ transform var(--drawer-motion-duration) var(--ease-exit);
4535
4604
  }
4536
4605
 
4537
4606
  dialog.drawer[open] {
4607
+ --drawer-motion-duration: var(--duration-slow);
4538
4608
  display: grid;
4539
4609
  grid-template-rows: auto minmax(0, 1fr) auto;
4540
4610
  gap: var(--space-40);
4541
4611
  opacity: 1;
4542
4612
  transform: translateX(0);
4613
+ transition:
4614
+ opacity var(--drawer-motion-duration) var(--ease-enter),
4615
+ transform var(--drawer-motion-duration) var(--ease-enter);
4543
4616
  }
4544
4617
 
4545
4618
  /* Backdrop color sourced from components/surface.css so it matches .modal
@@ -4547,7 +4620,9 @@ dialog.drawer[open] {
4547
4620
  dialog.drawer::backdrop {
4548
4621
  background: var(--backdrop-fill);
4549
4622
  opacity: 0;
4550
- transition: opacity var(--duration-slow);
4623
+ /* Scrim curve stays neutral `ease`; only the panel's character curves
4624
+ apply. Duration follows --drawer-motion-duration through inheritance. */
4625
+ transition: opacity var(--drawer-motion-duration) ease;
4551
4626
  }
4552
4627
 
4553
4628
  dialog.drawer[data-side="end"] {
@@ -4583,17 +4658,25 @@ dialog.drawer nav {
4583
4658
  @supports (transition-behavior: allow-discrete) {
4584
4659
  dialog.drawer {
4585
4660
  transition:
4586
- opacity var(--duration-slow),
4587
- transform var(--duration-slow),
4588
- overlay var(--duration-slow) allow-discrete,
4589
- display var(--duration-slow) allow-discrete;
4661
+ opacity var(--drawer-motion-duration) var(--ease-exit),
4662
+ transform var(--drawer-motion-duration) var(--ease-exit),
4663
+ overlay var(--drawer-motion-duration) allow-discrete,
4664
+ display var(--drawer-motion-duration) allow-discrete;
4665
+ }
4666
+
4667
+ dialog.drawer[open] {
4668
+ transition:
4669
+ opacity var(--drawer-motion-duration) var(--ease-enter),
4670
+ transform var(--drawer-motion-duration) var(--ease-enter),
4671
+ overlay var(--drawer-motion-duration) allow-discrete,
4672
+ display var(--drawer-motion-duration) allow-discrete;
4590
4673
  }
4591
4674
 
4592
4675
  dialog.drawer::backdrop {
4593
4676
  transition:
4594
- opacity var(--duration-slow),
4595
- overlay var(--duration-slow) allow-discrete,
4596
- display var(--duration-slow) allow-discrete;
4677
+ opacity var(--drawer-motion-duration) ease,
4678
+ overlay var(--drawer-motion-duration) allow-discrete,
4679
+ display var(--drawer-motion-duration) allow-discrete;
4597
4680
  }
4598
4681
  }
4599
4682
 
@@ -4629,7 +4712,7 @@ dialog.drawer nav {
4629
4712
 
4630
4713
  @media (prefers-reduced-motion: no-preference) {
4631
4714
  dialog.drawer.is-static {
4632
- animation: actual-dialog-static var(--duration-slow) var(--ease);
4715
+ animation: actual-dialog-static var(--duration-slow) ease;
4633
4716
  }
4634
4717
  }
4635
4718
 
@@ -4723,11 +4806,10 @@ dialog.drawer > form > header {
4723
4806
  box-shadow: var(--shadow-popout);
4724
4807
  list-style: none;
4725
4808
  opacity: 1;
4726
- scale: 1;
4727
- transition:
4728
- opacity var(--duration-fast) var(--ease),
4729
- scale var(--duration-fast) var(--ease),
4730
- translate var(--duration-fast) var(--ease);
4809
+ /* Anchored flyouts fade in and out; scaling around the center would detach
4810
+ the anchored edge from its trigger on wide panels. The sheet presentation
4811
+ below adds its own translate motion. */
4812
+ transition: opacity var(--duration-fast) ease;
4731
4813
  }
4732
4814
 
4733
4815
  .flyout.is-open:not(.is-sheet) {
@@ -4737,7 +4819,6 @@ dialog.drawer > form > header {
4737
4819
  @starting-style {
4738
4820
  .flyout.is-open {
4739
4821
  opacity: 0;
4740
- scale: 0.98;
4741
4822
  }
4742
4823
  }
4743
4824
 
@@ -4759,22 +4840,24 @@ dialog.drawer > form > header {
4759
4840
  max(var(--space-30), env(safe-area-inset-bottom));
4760
4841
  border-end-start-radius: 0;
4761
4842
  border-end-end-radius: 0;
4843
+ /* Sheet motion is a directional slide from the viewport edge; it replaces
4844
+ the base fade-only recipe. */
4845
+ transition:
4846
+ opacity var(--duration-fast) ease,
4847
+ translate var(--duration-fast) ease;
4762
4848
  }
4763
4849
 
4764
4850
  .flyout[hidden] {
4765
4851
  display: none;
4766
4852
  opacity: 0;
4767
- scale: 0.98;
4768
4853
  }
4769
4854
 
4770
4855
  .flyout.is-sheet[hidden] {
4771
- scale: 1;
4772
4856
  translate: 0 var(--space-30);
4773
4857
  }
4774
4858
 
4775
4859
  @starting-style {
4776
4860
  .flyout.is-sheet.is-open {
4777
- scale: 1;
4778
4861
  translate: 0 var(--space-30);
4779
4862
  }
4780
4863
  }
@@ -4782,9 +4865,14 @@ dialog.drawer > form > header {
4782
4865
  @supports (transition-behavior: allow-discrete) {
4783
4866
  .flyout {
4784
4867
  transition:
4785
- opacity var(--duration-fast) var(--ease),
4786
- scale var(--duration-fast) var(--ease),
4787
- translate var(--duration-fast) var(--ease),
4868
+ opacity var(--duration-fast) ease,
4869
+ display var(--duration-fast) allow-discrete;
4870
+ }
4871
+
4872
+ .flyout.is-sheet {
4873
+ transition:
4874
+ opacity var(--duration-fast) ease,
4875
+ translate var(--duration-fast) ease,
4788
4876
  display var(--duration-fast) allow-discrete;
4789
4877
  }
4790
4878
  }
@@ -4989,31 +5077,28 @@ dialog.drawer > form > header {
4989
5077
  overflow-wrap: anywhere;
4990
5078
  pointer-events: auto;
4991
5079
  user-select: none;
5080
+ /* Tooltips fade only: the anchor edge, marked by the arrow, must stay put
5081
+ during motion, and a 4% scale was perceptible on small text. Scaling moves
5082
+ the arrow tip away from its trigger on every placement. */
4992
5083
  opacity: 1;
4993
- scale: 1;
4994
- transition:
4995
- opacity var(--duration-fast) var(--ease),
4996
- scale var(--duration-fast) var(--ease);
5084
+ transition: opacity var(--duration-fast) ease;
4997
5085
  }
4998
5086
 
4999
5087
  .tooltip[hidden] {
5000
5088
  display: none;
5001
5089
  opacity: 0;
5002
- scale: 0.96;
5003
5090
  }
5004
5091
 
5005
5092
  @starting-style {
5006
5093
  .tooltip:not([hidden]) {
5007
5094
  opacity: 0;
5008
- scale: 0.96;
5009
5095
  }
5010
5096
  }
5011
5097
 
5012
5098
  @supports (transition-behavior: allow-discrete) {
5013
5099
  .tooltip {
5014
5100
  transition:
5015
- opacity var(--duration-fast) var(--ease),
5016
- scale var(--duration-fast) var(--ease),
5101
+ opacity var(--duration-fast) ease,
5017
5102
  display var(--duration-fast) allow-discrete;
5018
5103
  }
5019
5104
  }
@@ -5354,9 +5439,14 @@ dialog.drawer > form > header {
5354
5439
  * Consumes the shared --intent/--primary for the value bar and --bar-height
5355
5440
  * for thickness. Indeterminate state animates a shimmer using --duration-shimmer.
5356
5441
  *
5442
+ * Thickness is a dimension, not a density step: the bar always spans its
5443
+ * container and only gets thinner or thicker, so --bar-height is the API and
5444
+ * .sm/.lg are deliberately not supported.
5445
+ *
5357
5446
  * Public hooks:
5358
5447
  * --progress-track
5359
5448
  * --progress-value
5449
+ * --bar-height
5360
5450
  */
5361
5451
  /* Intent boundary — inherited intents do not tint this component.
5362
5452
  Local intent classes still win because :where() has zero specificity. */
@@ -5460,9 +5550,12 @@ dialog.drawer > form > header {
5460
5550
  /*
5461
5551
  * Status bar — singleton floating status area.
5462
5552
  *
5463
- * One element in the HTML, empty by default, hidden when :empty but kept in the
5464
- * accessibility tree so it stays a reliable live region. JavaScript only sets
5465
- * text content; the visual shows and hides with the content.
5553
+ * One element in the HTML, empty by default. .is-open is the visual state, set
5554
+ * by status.js like any other surface: it lands after the message is written
5555
+ * and lifts before the bar is cleared, so the bar leaves in the exact state it
5556
+ * was shown in — same text, same width, same intent color — instead of snapping
5557
+ * to a neutral empty pill mid-exit. Emptying the text is the *end* of the exit,
5558
+ * not its trigger.
5466
5559
  *
5467
5560
  * Consumes intent like badge or alert. No need for status-bar-specific modifier
5468
5561
  * classes — .danger / .success / .warning / .neutral from intents.css drive
@@ -5476,6 +5569,7 @@ dialog.drawer > form > header {
5476
5569
  * Internal:
5477
5570
  * --status-bg
5478
5571
  * --status-fg
5572
+ * --status-viewport-offset (written by status.js while the bar is open)
5479
5573
  */
5480
5574
 
5481
5575
  /* Intent boundary — inherited intents do not tint this component.
@@ -5490,7 +5584,9 @@ dialog.drawer > form > header {
5490
5584
  --status-fg: var(--intent-fg, var(--surface));
5491
5585
 
5492
5586
  position: fixed;
5493
- inset-block-end: var(--space-40);
5587
+ /* Fixed positioning tracks the layout viewport, which the mobile software
5588
+ keyboard covers. status.js writes the occluded band into the offset. */
5589
+ inset-block-end: calc(var(--space-40) + var(--status-viewport-offset, 0px));
5494
5590
  inset-inline: var(--space-40);
5495
5591
  z-index: var(--z-status);
5496
5592
 
@@ -5512,22 +5608,32 @@ dialog.drawer > form > header {
5512
5608
  color: var(--status-fg);
5513
5609
  box-shadow: var(--shadow-popout);
5514
5610
 
5515
- opacity: 1;
5516
- scale: 1;
5517
- translate: 0 0;
5611
+ /* Closed by default. Opacity, not visibility or display: the bar has to stay
5612
+ in the accessibility tree to work as a live region. */
5613
+ opacity: 0;
5614
+ translate: 0 var(--space-30);
5518
5615
  pointer-events: none;
5519
5616
 
5617
+ /* Exit is shorter than entry so dismissed feedback gets out of the way.
5618
+ background stays a shorthand so --status-bg still accepts gradients;
5619
+ solid colors expose an interpolable computed background-color. The
5620
+ intent color follows the state/interaction `ease`, not the presence
5621
+ curves. */
5520
5622
  transition:
5521
- opacity var(--duration-slow) var(--ease),
5522
- scale var(--duration-slow) var(--ease),
5523
- translate var(--duration-slow) var(--ease);
5623
+ opacity var(--duration-fast) var(--ease-exit),
5624
+ translate var(--duration) var(--ease-exit),
5625
+ background-color var(--duration) ease;
5524
5626
  }
5525
5627
 
5526
- .status-bar:empty {
5527
- opacity: 0;
5528
- scale: 0.96;
5529
- translate: 0 var(--space-20);
5530
- pointer-events: none;
5628
+ .status-bar.is-open {
5629
+ opacity: 1;
5630
+ translate: 0 0;
5631
+ /* Entry takes a little longer and decelerates, making the message legible
5632
+ before its motion settles. */
5633
+ transition:
5634
+ opacity var(--duration) var(--ease-enter),
5635
+ translate var(--duration-slow) var(--ease-enter),
5636
+ background-color var(--duration) ease;
5531
5637
  }
5532
5638
  @media print {
5533
5639
  .status-bar {
@@ -6163,6 +6269,7 @@ a.avatar[aria-disabled="true"] {
6163
6269
 
6164
6270
  counter-reset: actual-step;
6165
6271
  display: flex;
6272
+ flex-direction: row;
6166
6273
  inline-size: 100%;
6167
6274
  margin: 0;
6168
6275
  padding: 0;
@@ -6224,31 +6331,57 @@ a.avatar[aria-disabled="true"] {
6224
6331
  content: none;
6225
6332
  }
6226
6333
 
6227
- .steps > :is(.complete, [aria-current="step"]) {
6334
+ /* Three states must be readable without a check glyph: future stays neutral,
6335
+ complete is a filled marker, current is an outlined one. Filled reads as
6336
+ "done", outlined as "you are here" — the distinction survives a numbered
6337
+ stepper that never sets --step-complete-mark. */
6338
+ .steps > .complete {
6228
6339
  --step-marker-bg: var(--state-selected);
6229
6340
  --step-marker-border: var(--state-selected);
6230
6341
  --step-marker-fg: var(--state-selected-fg);
6342
+ --step-line: var(--state-selected);
6231
6343
 
6232
6344
  color: var(--text);
6233
6345
  }
6234
6346
 
6235
- /* Thicker border is the selected affordance in every mode — structural, and it
6236
- never competes with the outline channel owned by focus. */
6237
- .steps > :is(.complete, [aria-current="step"])::before {
6238
- border-width: calc(var(--border-width) * 2);
6347
+ /* Declared after .complete at equal specificity, and it wins on every channel
6348
+ including the connector: the accented line means "already walked", so the
6349
+ segment leaving the current step must stay neutral. The line reaching the
6350
+ current step is owned by the previous step, which is complete. */
6351
+ .steps > [aria-current="step"] {
6352
+ --step-marker-bg: var(--surface);
6353
+ --step-marker-border: var(--state-selected);
6354
+ --step-marker-fg: var(--state-selected);
6355
+ --step-line: var(--border);
6356
+
6357
+ color: var(--text);
6239
6358
  }
6240
6359
 
6241
- .steps > .complete {
6242
- --step-line: var(--state-selected);
6360
+ /* Thicker border is the current affordance in every mode — structural, so it
6361
+ survives forced colors, and it never competes with the outline channel owned
6362
+ by focus. */
6363
+ .steps > [aria-current="step"]::before {
6364
+ border-width: calc(var(--border-width) * 2);
6243
6365
  }
6244
6366
 
6245
6367
  /* A finished step may replace its number with a mark (e.g. a check glyph):
6246
6368
  --step-complete-mark accepts any content value and falls back to the
6247
- counter, so existing numbered steppers are unchanged. */
6248
- .steps > .complete::before {
6369
+ counter, so existing numbered steppers are unchanged. Excluded on the
6370
+ current step so "current wins" holds on the glyph too, not only on color. */
6371
+ .steps > .complete:not([aria-current="step"])::before {
6249
6372
  content: var(--step-complete-mark, counter(actual-step));
6250
6373
  }
6251
6374
 
6375
+ /* Forced colors drop the accent fill, which would leave complete and future
6376
+ markers identical — current keeps its thicker ring, complete needs a system
6377
+ color to stay filled. Same exclusion, for the same reason. */
6378
+ @media (forced-colors: active) {
6379
+ .steps > .complete:not([aria-current="step"])::before {
6380
+ background: Highlight;
6381
+ color: HighlightText;
6382
+ }
6383
+ }
6384
+
6252
6385
 
6253
6386
  /* Navigation */
6254
6387
  /*
@@ -6348,6 +6481,7 @@ a.avatar[aria-disabled="true"] {
6348
6481
 
6349
6482
  .navbar {
6350
6483
  display: flex;
6484
+ flex-direction: row;
6351
6485
  flex-wrap: wrap;
6352
6486
  align-items: center;
6353
6487
  justify-content: space-between;
@@ -6375,6 +6509,7 @@ a.avatar[aria-disabled="true"] {
6375
6509
 
6376
6510
  .navbar-nav {
6377
6511
  display: flex;
6512
+ flex-direction: row;
6378
6513
  flex-wrap: wrap;
6379
6514
  align-items: center;
6380
6515
  gap: var(--space-10);
@@ -6433,6 +6568,7 @@ a.avatar[aria-disabled="true"] {
6433
6568
  */
6434
6569
  .breadcrumb {
6435
6570
  display: flex;
6571
+ flex-direction: row;
6436
6572
  flex-wrap: wrap;
6437
6573
  align-items: center;
6438
6574
  gap: var(--space-10);
@@ -6486,6 +6622,7 @@ a.avatar[aria-disabled="true"] {
6486
6622
  */
6487
6623
  .pagination {
6488
6624
  display: flex;
6625
+ flex-direction: row;
6489
6626
  flex-wrap: wrap;
6490
6627
  align-items: center;
6491
6628
  gap: var(--space-10);
@@ -6534,6 +6671,7 @@ a.avatar[aria-disabled="true"] {
6534
6671
 
6535
6672
  .tabs {
6536
6673
  display: flex;
6674
+ flex-direction: row;
6537
6675
  flex-wrap: wrap;
6538
6676
  gap: var(--space-10);
6539
6677
  margin: 0;
@@ -6645,6 +6783,34 @@ a.avatar[aria-disabled="true"] {
6645
6783
 
6646
6784
 
6647
6785
  /* Data */
6786
+ /*
6787
+ * Data list — term/value pairs for the properties of a single object.
6788
+ *
6789
+ * Expects alternating <dt>/<dd> children: one term, one value per property.
6790
+ * A plain <dl> handles richer relationships (several terms or descriptions)
6791
+ * and stays untouched by this component.
6792
+ *
6793
+ * Why margin resets: neutralizes .prose list margins if .data-list appears
6794
+ * inside an authored rich-text scope; the grid owns row/column rhythm.
6795
+ */
6796
+ .data-list {
6797
+ display: grid;
6798
+ grid-template-columns: max-content minmax(0, 1fr);
6799
+ align-items: baseline;
6800
+ gap: var(--space-20) var(--space-40);
6801
+ }
6802
+
6803
+ .data-list > dt {
6804
+ min-inline-size: 0;
6805
+ margin-block-start: 0;
6806
+ font-weight: var(--font-weight-strong);
6807
+ }
6808
+
6809
+ .data-list > dd {
6810
+ min-inline-size: 0;
6811
+ margin: 0;
6812
+ }
6813
+
6648
6814
  /*
6649
6815
  * Application list — repeated rows with optional leading and trailing regions.
6650
6816
  * Content determines the number of lines; the component only owns the row
@@ -6803,10 +6969,18 @@ a.list-item:hover {
6803
6969
  }
6804
6970
 
6805
6971
  /*
6806
- * Meter — native gauge for known-range values.
6972
+ * Meter — native and segmented gauges for known-range values.
6807
6973
  *
6808
6974
  * Maps the three standard meter zones (optimum / suboptimum / even-less-good)
6809
6975
  * to the shared success / warning / danger intent colors.
6976
+ *
6977
+ * Thickness is a dimension, not a density step: the bar always spans its
6978
+ * container and only gets thinner or thicker, so --bar-height is the API and
6979
+ * .sm/.lg are deliberately not supported.
6980
+ *
6981
+ * Public hooks:
6982
+ * --meter-value
6983
+ * --bar-height
6810
6984
  */
6811
6985
 
6812
6986
  .meter {
@@ -6821,6 +6995,25 @@ a.list-item:hover {
6821
6995
  background: var(--surface-subtle);
6822
6996
  }
6823
6997
 
6998
+ /* A non-native meter is a weighted flex container. The element type keeps the
6999
+ native <meter> API and the composed segmented API distinct without another
7000
+ public variant class. */
7001
+ .meter:not(meter) {
7002
+ --intent: initial;
7003
+
7004
+ display: flex;
7005
+ }
7006
+
7007
+ .meter:not(meter) > * {
7008
+ flex: var(--meter-value, 1) 1 0;
7009
+ min-inline-size: 0;
7010
+ background: var(--intent);
7011
+ }
7012
+
7013
+ .meter:not(meter) > * + * {
7014
+ border-inline-start: var(--border-width) solid var(--surface);
7015
+ }
7016
+
6824
7017
  .meter::-webkit-meter-bar {
6825
7018
  -webkit-appearance: none;
6826
7019
  block-size: var(--bar-height);
@@ -7099,6 +7292,22 @@ a.list-item:hover {
7099
7292
  border: 0;
7100
7293
  }
7101
7294
 
7295
+ /* Enlarge a small control's touch target on touch-primary devices without
7296
+ growing the visual. Opt-in — apply to genuinely small targets only. The
7297
+ ::after exists only under (pointer: coarse), so on fine pointers the class
7298
+ is fully neutral. */
7299
+ @media (pointer: coarse) {
7300
+ .touch-target {
7301
+ position: relative;
7302
+ }
7303
+
7304
+ .touch-target::after {
7305
+ content: "";
7306
+ position: absolute;
7307
+ inset: calc(var(--space-20) * -1);
7308
+ }
7309
+ }
7310
+
7102
7311
  .overflow-auto {
7103
7312
  overflow: auto;
7104
7313
  }