actual-css 0.4.2 → 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 (63) hide show
  1. package/CHANGELOG.md +49 -2
  2. package/README.md +2 -2
  3. package/dist/actual.css +53 -14
  4. package/dist/actual.full.css +287 -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/data-list.css +27 -0
  43. package/src/css/components/drawer.css +30 -11
  44. package/src/css/components/flyout.css +17 -12
  45. package/src/css/components/index.css +1 -0
  46. package/src/css/components/meter.css +28 -1
  47. package/src/css/components/modal.css +28 -11
  48. package/src/css/components/progress.css +5 -0
  49. package/src/css/components/status-bar.css +31 -15
  50. package/src/css/components/steps.css +35 -9
  51. package/src/css/components/surface.css +5 -3
  52. package/src/css/components/tooltip.css +5 -8
  53. package/src/css/core/base.css +7 -1
  54. package/src/css/core/intents.css +6 -0
  55. package/src/css/core/tokens.css +23 -9
  56. package/src/css/core/variants.css +17 -4
  57. package/src/css/forms/form.css +4 -4
  58. package/src/css/typography/prose.css +2 -3
  59. package/src/css/utilities/base.css +16 -0
  60. package/src/js/selectors.js +1 -0
  61. package/src/js/status.js +88 -7
  62. package/src/js/surface.js +2 -11
  63. 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;
@@ -1822,6 +1860,10 @@ small {
1822
1860
  * Individual controls keep their own files; .choice label layout lives in
1823
1861
  * choice.css; .form-actions lives in form-actions.css.
1824
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
+ *
1825
1867
  * Public API: .field, .field-label, .field-help, .field-error, .field-group,
1826
1868
  * .required-mark.
1827
1869
  *
@@ -1842,10 +1884,6 @@ small {
1842
1884
  border-radius: var(--radius);
1843
1885
  }
1844
1886
 
1845
- .field-group + .field-group {
1846
- margin-block-start: var(--space-40);
1847
- }
1848
-
1849
1887
  .field-label,
1850
1888
  .field-group > legend {
1851
1889
  color: var(--text);
@@ -4161,7 +4199,9 @@ a.badge[aria-disabled="true"] {
4161
4199
  z-index: calc(var(--z-menu) - 1);
4162
4200
  background: var(--backdrop-fill);
4163
4201
  opacity: 1;
4164
- 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;
4165
4205
  }
4166
4206
 
4167
4207
  .surface-backdrop[hidden] {
@@ -4178,8 +4218,8 @@ a.badge[aria-disabled="true"] {
4178
4218
  @supports (transition-behavior: allow-discrete) {
4179
4219
  .surface-backdrop {
4180
4220
  transition:
4181
- opacity var(--duration) var(--ease),
4182
- display var(--duration) allow-discrete;
4221
+ opacity var(--duration-fast) ease,
4222
+ display var(--duration-fast) allow-discrete;
4183
4223
  }
4184
4224
  }
4185
4225
  @media print {
@@ -4229,12 +4269,14 @@ dialog.modal {
4229
4269
  * Keep the open dialog root untransformed. Fixed overlays mounted inside a
4230
4270
  * modal dialog use viewport coordinates; transform/translate/scale on this
4231
4271
  * element would make them position against the dialog instead. The closed
4232
- * 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.
4233
4275
  */
4234
- transform: translateY(0.35rem) scale(0.98);
4276
+ transform: translateY(0.35rem);
4235
4277
  transition:
4236
- opacity var(--duration),
4237
- transform var(--duration);
4278
+ opacity var(--duration) var(--ease-exit),
4279
+ transform var(--duration) var(--ease-exit);
4238
4280
  }
4239
4281
 
4240
4282
  dialog.modal > form,
@@ -4353,33 +4395,46 @@ dialog.modal.dialog-confirmation > form > footer {
4353
4395
  dialog.modal[open] {
4354
4396
  opacity: 1;
4355
4397
  transform: none;
4398
+ transition:
4399
+ opacity var(--duration) var(--ease-enter),
4400
+ transform var(--duration) var(--ease-enter);
4356
4401
  }
4357
4402
 
4358
4403
  @starting-style {
4359
4404
  dialog.modal[open] {
4360
4405
  opacity: 0;
4361
- transform: translateY(0.35rem) scale(0.98);
4406
+ transform: translateY(0.35rem);
4362
4407
  }
4363
4408
  }
4364
4409
 
4365
4410
  dialog.modal::backdrop {
4366
4411
  background: var(--backdrop-fill);
4367
4412
  opacity: 0;
4368
- transition: opacity var(--duration);
4413
+ /* Scrim curve stays neutral `ease`; only the panel carries the character
4414
+ curves. */
4415
+ transition: opacity var(--duration) ease;
4369
4416
  }
4370
4417
 
4371
4418
  @supports (transition-behavior: allow-discrete) {
4372
4419
  dialog.modal {
4373
4420
  transition:
4374
- opacity var(--duration),
4375
- 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),
4376
4431
  overlay var(--duration) allow-discrete,
4377
4432
  display var(--duration) allow-discrete;
4378
4433
  }
4379
4434
 
4380
4435
  dialog.modal::backdrop {
4381
4436
  transition:
4382
- opacity var(--duration),
4437
+ opacity var(--duration) ease,
4383
4438
  overlay var(--duration) allow-discrete,
4384
4439
  display var(--duration) allow-discrete;
4385
4440
  }
@@ -4404,13 +4459,15 @@ html.has-modal-open.had-scrollbar {
4404
4459
  }
4405
4460
 
4406
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. */
4407
4464
  dialog.modal.is-static {
4408
- animation: actual-dialog-static var(--duration-slow) var(--ease);
4465
+ animation: actual-dialog-static var(--duration-slow) ease;
4409
4466
  }
4410
4467
 
4411
4468
  ::view-transition-group(actual-dialog) {
4412
4469
  animation-duration: var(--duration);
4413
- animation-timing-function: var(--ease);
4470
+ animation-timing-function: ease;
4414
4471
  }
4415
4472
  }
4416
4473
 
@@ -4509,11 +4566,16 @@ html.has-modal-open.had-scrollbar {
4509
4566
  *
4510
4567
  * Internal:
4511
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)
4512
4571
  */
4513
4572
  dialog.drawer {
4514
4573
  --drawer-size: 20rem;
4515
4574
  --drawer-pad: var(--space-40);
4516
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);
4517
4579
 
4518
4580
  /* Keep the surface anchored to the viewport in the top layer. This also
4519
4581
  provides the containing block for the absolutely positioned close button.
@@ -4537,16 +4599,20 @@ dialog.drawer {
4537
4599
  opacity: 0;
4538
4600
  transform: translateX(var(--drawer-closed-translate));
4539
4601
  transition:
4540
- opacity var(--duration-slow),
4541
- transform var(--duration-slow);
4602
+ opacity var(--drawer-motion-duration) var(--ease-exit),
4603
+ transform var(--drawer-motion-duration) var(--ease-exit);
4542
4604
  }
4543
4605
 
4544
4606
  dialog.drawer[open] {
4607
+ --drawer-motion-duration: var(--duration-slow);
4545
4608
  display: grid;
4546
4609
  grid-template-rows: auto minmax(0, 1fr) auto;
4547
4610
  gap: var(--space-40);
4548
4611
  opacity: 1;
4549
4612
  transform: translateX(0);
4613
+ transition:
4614
+ opacity var(--drawer-motion-duration) var(--ease-enter),
4615
+ transform var(--drawer-motion-duration) var(--ease-enter);
4550
4616
  }
4551
4617
 
4552
4618
  /* Backdrop color sourced from components/surface.css so it matches .modal
@@ -4554,7 +4620,9 @@ dialog.drawer[open] {
4554
4620
  dialog.drawer::backdrop {
4555
4621
  background: var(--backdrop-fill);
4556
4622
  opacity: 0;
4557
- 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;
4558
4626
  }
4559
4627
 
4560
4628
  dialog.drawer[data-side="end"] {
@@ -4590,17 +4658,25 @@ dialog.drawer nav {
4590
4658
  @supports (transition-behavior: allow-discrete) {
4591
4659
  dialog.drawer {
4592
4660
  transition:
4593
- opacity var(--duration-slow),
4594
- transform var(--duration-slow),
4595
- overlay var(--duration-slow) allow-discrete,
4596
- 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;
4597
4673
  }
4598
4674
 
4599
4675
  dialog.drawer::backdrop {
4600
4676
  transition:
4601
- opacity var(--duration-slow),
4602
- overlay var(--duration-slow) allow-discrete,
4603
- 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;
4604
4680
  }
4605
4681
  }
4606
4682
 
@@ -4636,7 +4712,7 @@ dialog.drawer nav {
4636
4712
 
4637
4713
  @media (prefers-reduced-motion: no-preference) {
4638
4714
  dialog.drawer.is-static {
4639
- animation: actual-dialog-static var(--duration-slow) var(--ease);
4715
+ animation: actual-dialog-static var(--duration-slow) ease;
4640
4716
  }
4641
4717
  }
4642
4718
 
@@ -4730,11 +4806,10 @@ dialog.drawer > form > header {
4730
4806
  box-shadow: var(--shadow-popout);
4731
4807
  list-style: none;
4732
4808
  opacity: 1;
4733
- scale: 1;
4734
- transition:
4735
- opacity var(--duration-fast) var(--ease),
4736
- scale var(--duration-fast) var(--ease),
4737
- 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;
4738
4813
  }
4739
4814
 
4740
4815
  .flyout.is-open:not(.is-sheet) {
@@ -4744,7 +4819,6 @@ dialog.drawer > form > header {
4744
4819
  @starting-style {
4745
4820
  .flyout.is-open {
4746
4821
  opacity: 0;
4747
- scale: 0.98;
4748
4822
  }
4749
4823
  }
4750
4824
 
@@ -4766,22 +4840,24 @@ dialog.drawer > form > header {
4766
4840
  max(var(--space-30), env(safe-area-inset-bottom));
4767
4841
  border-end-start-radius: 0;
4768
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;
4769
4848
  }
4770
4849
 
4771
4850
  .flyout[hidden] {
4772
4851
  display: none;
4773
4852
  opacity: 0;
4774
- scale: 0.98;
4775
4853
  }
4776
4854
 
4777
4855
  .flyout.is-sheet[hidden] {
4778
- scale: 1;
4779
4856
  translate: 0 var(--space-30);
4780
4857
  }
4781
4858
 
4782
4859
  @starting-style {
4783
4860
  .flyout.is-sheet.is-open {
4784
- scale: 1;
4785
4861
  translate: 0 var(--space-30);
4786
4862
  }
4787
4863
  }
@@ -4789,9 +4865,14 @@ dialog.drawer > form > header {
4789
4865
  @supports (transition-behavior: allow-discrete) {
4790
4866
  .flyout {
4791
4867
  transition:
4792
- opacity var(--duration-fast) var(--ease),
4793
- scale var(--duration-fast) var(--ease),
4794
- 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,
4795
4876
  display var(--duration-fast) allow-discrete;
4796
4877
  }
4797
4878
  }
@@ -4996,31 +5077,28 @@ dialog.drawer > form > header {
4996
5077
  overflow-wrap: anywhere;
4997
5078
  pointer-events: auto;
4998
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. */
4999
5083
  opacity: 1;
5000
- scale: 1;
5001
- transition:
5002
- opacity var(--duration-fast) var(--ease),
5003
- scale var(--duration-fast) var(--ease);
5084
+ transition: opacity var(--duration-fast) ease;
5004
5085
  }
5005
5086
 
5006
5087
  .tooltip[hidden] {
5007
5088
  display: none;
5008
5089
  opacity: 0;
5009
- scale: 0.96;
5010
5090
  }
5011
5091
 
5012
5092
  @starting-style {
5013
5093
  .tooltip:not([hidden]) {
5014
5094
  opacity: 0;
5015
- scale: 0.96;
5016
5095
  }
5017
5096
  }
5018
5097
 
5019
5098
  @supports (transition-behavior: allow-discrete) {
5020
5099
  .tooltip {
5021
5100
  transition:
5022
- opacity var(--duration-fast) var(--ease),
5023
- scale var(--duration-fast) var(--ease),
5101
+ opacity var(--duration-fast) ease,
5024
5102
  display var(--duration-fast) allow-discrete;
5025
5103
  }
5026
5104
  }
@@ -5361,9 +5439,14 @@ dialog.drawer > form > header {
5361
5439
  * Consumes the shared --intent/--primary for the value bar and --bar-height
5362
5440
  * for thickness. Indeterminate state animates a shimmer using --duration-shimmer.
5363
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
+ *
5364
5446
  * Public hooks:
5365
5447
  * --progress-track
5366
5448
  * --progress-value
5449
+ * --bar-height
5367
5450
  */
5368
5451
  /* Intent boundary — inherited intents do not tint this component.
5369
5452
  Local intent classes still win because :where() has zero specificity. */
@@ -5467,9 +5550,12 @@ dialog.drawer > form > header {
5467
5550
  /*
5468
5551
  * Status bar — singleton floating status area.
5469
5552
  *
5470
- * One element in the HTML, empty by default, hidden when :empty but kept in the
5471
- * accessibility tree so it stays a reliable live region. JavaScript only sets
5472
- * 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.
5473
5559
  *
5474
5560
  * Consumes intent like badge or alert. No need for status-bar-specific modifier
5475
5561
  * classes — .danger / .success / .warning / .neutral from intents.css drive
@@ -5483,6 +5569,7 @@ dialog.drawer > form > header {
5483
5569
  * Internal:
5484
5570
  * --status-bg
5485
5571
  * --status-fg
5572
+ * --status-viewport-offset (written by status.js while the bar is open)
5486
5573
  */
5487
5574
 
5488
5575
  /* Intent boundary — inherited intents do not tint this component.
@@ -5497,7 +5584,9 @@ dialog.drawer > form > header {
5497
5584
  --status-fg: var(--intent-fg, var(--surface));
5498
5585
 
5499
5586
  position: fixed;
5500
- 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));
5501
5590
  inset-inline: var(--space-40);
5502
5591
  z-index: var(--z-status);
5503
5592
 
@@ -5519,22 +5608,32 @@ dialog.drawer > form > header {
5519
5608
  color: var(--status-fg);
5520
5609
  box-shadow: var(--shadow-popout);
5521
5610
 
5522
- opacity: 1;
5523
- scale: 1;
5524
- 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);
5525
5615
  pointer-events: none;
5526
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. */
5527
5622
  transition:
5528
- opacity var(--duration-slow) var(--ease),
5529
- scale var(--duration-slow) var(--ease),
5530
- 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;
5531
5626
  }
5532
5627
 
5533
- .status-bar:empty {
5534
- opacity: 0;
5535
- scale: 0.96;
5536
- translate: 0 var(--space-20);
5537
- 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;
5538
5637
  }
5539
5638
  @media print {
5540
5639
  .status-bar {
@@ -6232,31 +6331,57 @@ a.avatar[aria-disabled="true"] {
6232
6331
  content: none;
6233
6332
  }
6234
6333
 
6235
- .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 {
6236
6339
  --step-marker-bg: var(--state-selected);
6237
6340
  --step-marker-border: var(--state-selected);
6238
6341
  --step-marker-fg: var(--state-selected-fg);
6342
+ --step-line: var(--state-selected);
6239
6343
 
6240
6344
  color: var(--text);
6241
6345
  }
6242
6346
 
6243
- /* Thicker border is the selected affordance in every mode — structural, and it
6244
- never competes with the outline channel owned by focus. */
6245
- .steps > :is(.complete, [aria-current="step"])::before {
6246
- 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);
6247
6358
  }
6248
6359
 
6249
- .steps > .complete {
6250
- --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);
6251
6365
  }
6252
6366
 
6253
6367
  /* A finished step may replace its number with a mark (e.g. a check glyph):
6254
6368
  --step-complete-mark accepts any content value and falls back to the
6255
- counter, so existing numbered steppers are unchanged. */
6256
- .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 {
6257
6372
  content: var(--step-complete-mark, counter(actual-step));
6258
6373
  }
6259
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
+
6260
6385
 
6261
6386
  /* Navigation */
6262
6387
  /*
@@ -6658,6 +6783,34 @@ a.avatar[aria-disabled="true"] {
6658
6783
 
6659
6784
 
6660
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
+
6661
6814
  /*
6662
6815
  * Application list — repeated rows with optional leading and trailing regions.
6663
6816
  * Content determines the number of lines; the component only owns the row
@@ -6816,10 +6969,18 @@ a.list-item:hover {
6816
6969
  }
6817
6970
 
6818
6971
  /*
6819
- * Meter — native gauge for known-range values.
6972
+ * Meter — native and segmented gauges for known-range values.
6820
6973
  *
6821
6974
  * Maps the three standard meter zones (optimum / suboptimum / even-less-good)
6822
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
6823
6984
  */
6824
6985
 
6825
6986
  .meter {
@@ -6834,6 +6995,25 @@ a.list-item:hover {
6834
6995
  background: var(--surface-subtle);
6835
6996
  }
6836
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
+
6837
7017
  .meter::-webkit-meter-bar {
6838
7018
  -webkit-appearance: none;
6839
7019
  block-size: var(--bar-height);
@@ -7112,6 +7292,22 @@ a.list-item:hover {
7112
7292
  border: 0;
7113
7293
  }
7114
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
+
7115
7311
  .overflow-auto {
7116
7312
  overflow: auto;
7117
7313
  }