actual-css 0.5.0 → 0.6.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 (86) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/dist/actual.css +1 -0
  3. package/dist/actual.full.css +480 -117
  4. package/dist/actual.full.js +1 -1
  5. package/dist/actual.full.min.css +1 -1
  6. package/dist/actual.min.css +1 -1
  7. package/docs/design-notes/browser-support.md +14 -14
  8. package/docs/design-notes/enhancement-contract.md +30 -13
  9. package/docs/design-notes/forced-colors.md +11 -7
  10. package/docs/design-notes/grid.md +4 -4
  11. package/docs/design-notes/naming.md +15 -9
  12. package/docs/design-notes/platform-alignment.md +9 -0
  13. package/docs/design-notes/references.md +4 -0
  14. package/docs/design-notes/steps.md +748 -0
  15. package/docs/design-notes/widget-primitives.md +32 -15
  16. package/docs/navigation.json +1 -0
  17. package/docs/pages/components/alert.md +13 -8
  18. package/docs/pages/components/aura.md +102 -6
  19. package/docs/pages/components/avatar.md +9 -6
  20. package/docs/pages/components/badge.md +11 -7
  21. package/docs/pages/components/button.md +17 -17
  22. package/docs/pages/components/card.md +15 -8
  23. package/docs/pages/components/chat.md +1 -1
  24. package/docs/pages/components/dialog.md +10 -7
  25. package/docs/pages/components/drawer.md +1 -1
  26. package/docs/pages/components/fab.md +12 -10
  27. package/docs/pages/components/indicator.md +6 -6
  28. package/docs/pages/components/list.md +11 -2
  29. package/docs/pages/components/navbar.md +1 -1
  30. package/docs/pages/components/pagination.md +1 -1
  31. package/docs/pages/components/rating.md +4 -4
  32. package/docs/pages/components/skeleton.md +1 -1
  33. package/docs/pages/components/spinner.md +1 -1
  34. package/docs/pages/components/steps.md +376 -52
  35. package/docs/pages/components/table.md +5 -5
  36. package/docs/pages/enhancements/flyout.md +12 -10
  37. package/docs/pages/enhancements/overview.md +2 -2
  38. package/docs/pages/enhancements/scrollspy.md +1 -1
  39. package/docs/pages/enhancements/tabs.md +1 -1
  40. package/docs/pages/examples/overview.md +2 -0
  41. package/docs/pages/forms/choice-cards.md +4 -4
  42. package/docs/pages/forms/combobox.md +87 -0
  43. package/docs/pages/forms/floating-field.md +5 -3
  44. package/docs/pages/forms/joined-controls.md +4 -4
  45. package/docs/pages/forms/labels.md +8 -5
  46. package/docs/pages/forms/overview.md +25 -18
  47. package/docs/pages/forms/password.md +3 -2
  48. package/docs/pages/forms/range.md +13 -2
  49. package/docs/pages/forms/states-and-sizes.md +6 -4
  50. package/docs/pages/forms/validation.md +13 -9
  51. package/docs/pages/foundations/tokens.md +9 -6
  52. package/docs/pages/foundations/typography.md +1 -1
  53. package/docs/pages/guides/bootstrap.md +3 -3
  54. package/docs/pages/guides/introduction.md +1 -1
  55. package/docs/pages/guides/modular-import.md +21 -10
  56. package/docs/pages/guides/progressive-enhancement.md +75 -54
  57. package/docs/pages/guides/tailwind.md +2 -2
  58. package/docs/pages/layout/app-shell.md +4 -4
  59. package/docs/pages/layout/choosing.md +10 -6
  60. package/docs/pages/layout/grid.md +11 -10
  61. package/docs/pages/layout/stack.md +10 -6
  62. package/docs/pages/patterns/structured-lists.md +7 -7
  63. package/docs/pages/utilities/accessibility.md +1 -1
  64. package/docs/pages/utilities/flex.md +20 -13
  65. package/docs/pages/utilities/links.md +1 -1
  66. package/docs/pages/utilities/list.md +1 -1
  67. package/docs/pages/utilities/overflow.md +14 -7
  68. package/docs/pages/utilities/overview.md +22 -0
  69. package/docs/pages/utilities/shape.md +1 -1
  70. package/docs/pages/utilities/sizing-wrapping.md +10 -8
  71. package/docs/pages/utilities/spacing.md +26 -19
  72. package/docs/pages/utilities/text.md +1 -1
  73. package/llms.txt +27 -4
  74. package/package.json +3 -2
  75. package/scripts/reserved-classes.json +4 -1
  76. package/src/css/components/fab.css +44 -8
  77. package/src/css/components/list.css +3 -1
  78. package/src/css/components/steps.css +277 -69
  79. package/src/css/core/tokens.css +1 -0
  80. package/src/css/effects/aura.css +101 -11
  81. package/src/css/forms/custom-select.css +1 -1
  82. package/src/css/forms/floating-field.css +8 -6
  83. package/src/css/layout/container-query.css +37 -13
  84. package/src/css/layout/grid.css +7 -7
  85. package/src/css/layout/index.css +1 -1
  86. package/src/js/password.js +1 -1
@@ -201,6 +201,7 @@ select {
201
201
  --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%238a8594' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
202
202
  --icon-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E");
203
203
  --icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='6' y1='6' x2='18' y2='18'/%3E%3Cline x1='18' y1='6' x2='6' y2='18'/%3E%3C/svg%3E");
204
+ --icon-plus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='12' y1='5' x2='12' y2='19'/%3E%3Cline x1='5' y1='12' x2='19' y2='12'/%3E%3C/svg%3E");
204
205
 
205
206
  /* Viewport size tokens — fallback vh/vw with dvh/vi enhancement.
206
207
  Consumed by full-viewport components (app-shell, drawer, modal, flyout).
@@ -1073,7 +1074,7 @@ small {
1073
1074
  * .sidebar-layout main content + secondary aside
1074
1075
  * .grid responsive collection; item width is the contract
1075
1076
  * .grid-N structural density of N; balanced inside a container named
1076
- * actual-grid (in grid.css)
1077
+ * actual-container (in grid.css)
1077
1078
  * .column-layout explicit 12-column region composition; the author owns
1078
1079
  * narrow-container recomposition
1079
1080
  * .scroller theme-aware scrollbar density and colour
@@ -1084,20 +1085,44 @@ small {
1084
1085
  *
1085
1086
  * Prefer these compositions over viewport breakpoint utilities.
1086
1087
  */
1087
- /* Convenience helper: establishes a query container named actual-grid for
1088
- descendants. Use it for a component that changes representation according
1089
- to its allocated inline size, and to give .grid-N the named context it needs
1090
- for balanced subdivision (see grid.css). The name matters: grids only respond
1091
- to an explicitly actual-grid-named container, never to an incidental
1092
- query container. Layout primitives are responsive without it; this adds
1093
- precision, it is never a prerequisite.
1094
-
1095
- The element takes inline-size containment, so put it on a normal
1096
- block-level wrapper, never on a shrink-to-fit item (a .cluster child, a
1097
- float, an inline-block): its contents would stop contributing to its own
1098
- inline size and it would collapse. */
1088
+ /* Convenience helper for size-aware components: establishes an inline-size
1089
+ query container named actual-container. The name is the contract, not this
1090
+ class — an author can declare `container: actual-container / inline-size` on
1091
+ their own region and get the same result.
1092
+
1093
+ actual-container is shared. Granting it to a region may enable size-aware
1094
+ enhancements for any Actual component inside that region, not only the one
1095
+ that motivated the container: .grid-N reads it for balanced subdivision,
1096
+ .steps for its label collapse, and future components will read the same
1097
+ context. That is the scope of the API, not a caveat.
1098
+
1099
+ An anonymous query container is not part of it. A region that declares
1100
+ `container-type: inline-size` for the application's own purposes carries no
1101
+ actual-container name, so no framework component changes behaviour there.
1102
+ Opting into the protocol is always explicit. Nothing here is a prerequisite
1103
+ either — every primitive is responsive without it; this adds precision.
1104
+
1105
+ Place it where the width you want measured lives: usually a wrapper for a
1106
+ grid, whose queried children sit inside it, and the component itself for
1107
+ .steps, whose own row is the width in question.
1108
+
1109
+ The rule for where it is safe is the containment rule, not a list of
1110
+ primitives. Query containment removes the element's intrinsic inline-size
1111
+ contribution, so:
1112
+
1113
+ safe the element's width is imposed by its context
1114
+ (.steps sets inline-size: 100%, so it survives even as a
1115
+ .cluster child — it wraps and fills)
1116
+
1117
+ unsafe the element's width depends on its contents' intrinsic
1118
+ contribution (a bare .grid child of .cluster measures 0 once
1119
+ contained, as does a float, inline-block, table, grid auto track,
1120
+ max-content parent, or an abspos box sized by its contents)
1121
+
1122
+ So .cluster is neither safe nor unsafe; what matters is where the child's
1123
+ width comes from. */
1099
1124
  .container-query {
1100
- container: actual-grid / inline-size;
1125
+ container: actual-container / inline-size;
1101
1126
  }
1102
1127
 
1103
1128
  /* Vertical flow. .stack owns the block axis only: block margins reset, inline
@@ -1468,13 +1493,13 @@ small {
1468
1493
  min-inline-size: 0;
1469
1494
  }
1470
1495
 
1471
- /* Balanced subdivision, activated by an ancestor container named actual-grid.
1496
+ /* Balanced subdivision, activated by an ancestor container named actual-container.
1472
1497
  * A .grid-N inside such a container enters only divisor states of N. The name
1473
1498
  * is deliberate: a grid must not key off the nearest anonymous query container,
1474
1499
  * or a region that sets container-type: inline-size for an unrelated reason
1475
1500
  * would silently change the grid. The author opts the grid in by naming the
1476
1501
  * context — .container-query is the official helper, or declare
1477
- * container: actual-grid / inline-size on your own region.
1502
+ * container: actual-container / inline-size on your own region.
1478
1503
  *
1479
1504
  * .grid-2 2 -> 1
1480
1505
  * .grid-3 3 -> 1
@@ -1491,7 +1516,7 @@ small {
1491
1516
  * inline contribution and collapses it inside .cluster, .topbar, floats, and
1492
1517
  * any shrink-to-fit box. See docs/design-notes/grid.md.
1493
1518
  *
1494
- * Without an actual-grid ancestor none of these @container rules resolve, so
1519
+ * Without an actual-container ancestor none of these @container rules resolve, so
1495
1520
  * the auto-fill baseline above stays. The min-width: 0 rule is the lowest
1496
1521
  * step: it pins the grid to one column inside a container before any larger
1497
1522
  * threshold fires (a bare grid would otherwise show a 2 + 1 orphan at 28-48rem).
@@ -1507,25 +1532,25 @@ small {
1507
1532
  * Ascending min-width only: the last matching step wins by cascade order.
1508
1533
  */
1509
1534
  @supports (container-type: inline-size) {
1510
- @container actual-grid (min-width: 0px) {
1535
+ @container actual-container (min-width: 0px) {
1511
1536
  :where(.grid-2, .grid-3, .grid-4, .grid-6) {
1512
1537
  grid-template-columns: 1fr;
1513
1538
  }
1514
1539
  }
1515
1540
 
1516
- @container actual-grid (min-width: 28rem) {
1541
+ @container actual-container (min-width: 28rem) {
1517
1542
  :where(.grid-2, .grid-4, .grid-6) {
1518
1543
  grid-template-columns: repeat(2, minmax(0, 1fr));
1519
1544
  }
1520
1545
  }
1521
1546
 
1522
- @container actual-grid (min-width: 48rem) {
1547
+ @container actual-container (min-width: 48rem) {
1523
1548
  :where(.grid-3, .grid-6) {
1524
1549
  grid-template-columns: repeat(3, minmax(0, 1fr));
1525
1550
  }
1526
1551
  }
1527
1552
 
1528
- @container actual-grid (min-width: 64rem) {
1553
+ @container actual-container (min-width: 64rem) {
1529
1554
  :where(.grid-4) {
1530
1555
  grid-template-columns: repeat(4, minmax(0, 1fr));
1531
1556
  }
@@ -2975,7 +3000,7 @@ small {
2975
3000
 
2976
3001
  &::picker(select) {
2977
3002
  appearance: base-select;
2978
- max-height: min(24rem, calc(var(--viewport-block) * 0.7));
3003
+ max-block-size: min(24rem, calc(var(--viewport-block) * 0.7));
2979
3004
  margin-block: var(--space-20);
2980
3005
  padding: 0;
2981
3006
  border: var(--border-width) solid var(--border);
@@ -3052,8 +3077,10 @@ small {
3052
3077
  * messages stay in the surrounding .field, not inside the cell, so the
3053
3078
  * resting label can center on the control alone.
3054
3079
  *
3055
- * The control must precede the label: the float is driven by the sibling
3056
- * combinator and :placeholder-shown. Every .input/.textarea needs
3080
+ * The control must precede the label: the float is driven by the general
3081
+ * sibling combinator and :placeholder-shown. Injected nodes may appear between
3082
+ * the control and the label (password managers, extension wrappers); the cell
3083
+ * contract stays a single control plus one label. Every .input/.textarea needs
3057
3084
  * placeholder=" " to expose a reliable empty state — without it the label
3058
3085
  * only reacts to focus.
3059
3086
  *
@@ -3101,14 +3128,14 @@ small {
3101
3128
  }
3102
3129
 
3103
3130
  /* Single-line controls rest in the middle until they receive a value. */
3104
- .floating-field > :is(.input, .select) + .field-label {
3131
+ .floating-field > :is(.input, .select) ~ .field-label {
3105
3132
  inset-block-start: 50%;
3106
3133
  transform: translateY(-50%);
3107
3134
  }
3108
3135
 
3109
3136
  /* An empty textarea label follows the control's normal first-line position;
3110
3137
  the control itself keeps the extra headroom reserved for its floated state. */
3111
- .floating-field > .textarea + .field-label {
3138
+ .floating-field > .textarea ~ .field-label {
3112
3139
  inset-block-start: calc(1em + var(--border-width));
3113
3140
  transform: none;
3114
3141
  }
@@ -3117,7 +3144,7 @@ small {
3117
3144
  not have propagated the value to the placeholder state yet. */
3118
3145
  .floating-field
3119
3146
  > :is(.input, .textarea):is(:focus, :autofill, :not(:placeholder-shown))
3120
- + .field-label {
3147
+ ~ .field-label {
3121
3148
  inset-block-start: var(--floating-label-top);
3122
3149
  transform: none;
3123
3150
  font-size: var(--font-size-xs);
@@ -3136,7 +3163,7 @@ small {
3136
3163
  .input[type="month"],
3137
3164
  .input[type="week"]
3138
3165
  )
3139
- + .field-label {
3166
+ ~ .field-label {
3140
3167
  inset-block-start: var(--floating-label-top);
3141
3168
  transform: none;
3142
3169
  font-size: var(--font-size-xs);
@@ -6033,15 +6060,18 @@ a.avatar[aria-disabled="true"] {
6033
6060
 
6034
6061
  /*
6035
6062
  * Fixed floating action placement and a native-details speed dial.
6036
- * Button owns the action's size, shape, and intent; FAB only owns placement.
6063
+ * Button owns the trigger's size, shape, and intent; FAB owns placement and
6064
+ * the speed dial's structural open/close markers (--icon-plus / --icon-close).
6037
6065
  *
6038
6066
  * Public hooks:
6039
6067
  * --fab-offset
6040
6068
  * --fab-gap
6069
+ * --fab-icon-size
6041
6070
  */
6042
6071
  .fab {
6043
6072
  --fab-offset: var(--space-40);
6044
6073
  --fab-gap: var(--space-20);
6074
+ --fab-icon-size: 1.5rem;
6045
6075
 
6046
6076
  position: fixed;
6047
6077
  z-index: var(--z-menu);
@@ -6050,6 +6080,9 @@ a.avatar[aria-disabled="true"] {
6050
6080
  }
6051
6081
 
6052
6082
  .fab > summary {
6083
+ /* Centers the toggle markers: the summary is the markers' positioned
6084
+ context, not just the app-button class. */
6085
+ position: relative;
6053
6086
  list-style: none;
6054
6087
  cursor: pointer;
6055
6088
  }
@@ -6058,11 +6091,35 @@ a.avatar[aria-disabled="true"] {
6058
6091
  display: none;
6059
6092
  }
6060
6093
 
6061
- /* A lone decorative plus can communicate the open/close state without
6062
- requiring a second icon or JavaScript-managed content. */
6063
- .fab > summary > [aria-hidden="true"]:only-child {
6064
- display: inline-block;
6065
- transition: transform var(--duration);
6094
+ /* The speed dial trigger is an icon-only toggle. FAB owns both structural
6095
+ markers: masked pseudo-elements swap the shared plus (closed) and close
6096
+ (open) glyphs at the same size, so the two states never drift apart. */
6097
+ .fab > summary::before,
6098
+ .fab > summary::after {
6099
+ content: "";
6100
+ position: absolute;
6101
+ inset: 0;
6102
+ margin: auto;
6103
+ inline-size: var(--fab-icon-size);
6104
+ block-size: var(--fab-icon-size);
6105
+ background: currentColor;
6106
+ opacity: 0;
6107
+ transform: scale(0.8);
6108
+ transition:
6109
+ opacity var(--duration),
6110
+ transform var(--duration);
6111
+ }
6112
+
6113
+ .fab > summary::before {
6114
+ -webkit-mask: var(--icon-plus) center / contain no-repeat;
6115
+ mask: var(--icon-plus) center / contain no-repeat;
6116
+ opacity: 1;
6117
+ transform: none;
6118
+ }
6119
+
6120
+ .fab > summary::after {
6121
+ -webkit-mask: var(--icon-close) center / contain no-repeat;
6122
+ mask: var(--icon-close) center / contain no-repeat;
6066
6123
  }
6067
6124
 
6068
6125
  .fab-actions {
@@ -6120,8 +6177,14 @@ a.avatar[aria-disabled="true"] {
6120
6177
  transition-delay: 0s;
6121
6178
  }
6122
6179
 
6123
- .fab[open] > summary > [aria-hidden="true"]:only-child {
6124
- transform: rotate(45deg);
6180
+ .fab[open] > summary::before {
6181
+ opacity: 0;
6182
+ transform: scale(0.8);
6183
+ }
6184
+
6185
+ .fab[open] > summary::after {
6186
+ opacity: 1;
6187
+ transform: none;
6125
6188
  }
6126
6189
 
6127
6190
  @media print {
@@ -6233,22 +6296,56 @@ a.avatar[aria-disabled="true"] {
6233
6296
  }
6234
6297
 
6235
6298
  /*
6236
- * Steps — semantic ordered-list progression. One intrinsic layout: items fill
6237
- * the row when they fit and hold a minimum width otherwise, so the sequence
6238
- * scrolls horizontally only when it genuinely cannot fit. No breakpoint decides
6239
- * this — the item minimum tracks the real content, not an arbitrary width.
6299
+ * Steps — semantic ordered-list progression for a multi-step process.
6300
+ *
6301
+ * One primitive, two explicit compositions. Both classes are required:
6302
+ *
6303
+ * <ol class="steps steps-horizontal"> horizontal, 2 to 5 stages
6304
+ * <ol class="steps steps-vertical"> vertical, any number of stages
6305
+ *
6306
+ * `.steps` is what a step *is* — the counter, the marker, `.step-label`,
6307
+ * `.step-complete`, `aria-current="step"`. It carries no orientation, so it is
6308
+ * never written alone. `.steps-horizontal` and `.steps-vertical` are what a
6309
+ * sequence *looks like*, and they are peers: neither is the default the other
6310
+ * has to exclude, so no rule says `:not(...)` and no rule undoes another's
6311
+ * layout.
6312
+ *
6313
+ * Orientation and labels are independent dimensions. `.step-label` is optional
6314
+ * in either orientation; when a sequence carries names, every step carries
6315
+ * one:
6240
6316
  *
6241
- * Markers come from the list counter; state stays in .complete and
6242
- * aria-current="step", so the component needs no runtime.
6317
+ * empty steps .step-label on every step
6318
+ * horizontal compact progression named sequence, stacked or inline
6319
+ * vertical compact rail named sequence, labels beside markers
6320
+ *
6321
+ * An empty step is a marker and nothing else, so it is compact by structure —
6322
+ * `> li:empty` in both orientations, no variant class, and nothing authored
6323
+ * only to be hidden again. The page around it owns the step name.
6324
+ *
6325
+ * A label may be an `a[href]`, which is the whole interactive surface: the
6326
+ * marker is never navigable, so navigating a flow means naming its
6327
+ * destinations. Anything else focusable in a step is out of contract.
6328
+ *
6329
+ * The framework never hides an authored label to solve a layout problem. A
6330
+ * labelled row that outgrows its space scrolls. An `actual-container` size
6331
+ * context buys it one thing: the wide representation, marker beside label.
6332
+ *
6333
+ * See docs/design-notes/steps.md for the threshold, the selector shapes, the
6334
+ * rejected alternatives, and why there is no forced-colors branch.
6335
+ *
6336
+ * Each public hook is followed by the class that owns it.
6243
6337
  *
6244
6338
  * Public hooks:
6245
- * --step-size
6246
- * --step-min
6247
- * --step-gap
6248
- * --step-line-size
6249
- * --step-marker-radius
6250
- * --step-complete-mark
6251
- * --step-connector
6339
+ * --step-size .steps
6340
+ * --step-inline-gap .steps
6341
+ * --step-line-size .steps
6342
+ * --step-marker-radius .steps
6343
+ * --step-complete-mark .steps
6344
+ * --step-min .steps-horizontal
6345
+ * --step-gap .steps-horizontal
6346
+ * --step-connector .steps-horizontal
6347
+ * --step-inline-connector .steps-horizontal
6348
+ * --step-vertical-gap .steps-vertical
6252
6349
  *
6253
6350
  * Internal:
6254
6351
  * --step-marker-bg
@@ -6256,10 +6353,12 @@ a.avatar[aria-disabled="true"] {
6256
6353
  * --step-marker-fg
6257
6354
  * --step-line
6258
6355
  */
6356
+
6357
+ /* Step primitive --------------------------------------------------------- */
6358
+
6259
6359
  .steps {
6260
6360
  --step-size: 1.75rem;
6261
- --step-min: 7rem;
6262
- --step-gap: var(--space-20);
6361
+ --step-inline-gap: var(--space-20);
6263
6362
  --step-line-size: var(--border-width);
6264
6363
  --step-marker-bg: var(--surface);
6265
6364
  --step-marker-border: var(--border);
@@ -6269,29 +6368,14 @@ a.avatar[aria-disabled="true"] {
6269
6368
 
6270
6369
  counter-reset: actual-step;
6271
6370
  display: flex;
6272
- flex-direction: row;
6273
- inline-size: 100%;
6274
6371
  margin: 0;
6275
6372
  padding: 0;
6276
6373
  list-style: none;
6277
- /* Fill when it fits, scroll when it cannot: items never shrink below
6278
- --step-min, so labels keep a readable width and the sequence scrolls
6279
- horizontally instead of squishing. overscroll-behavior keeps the scroll
6280
- from chaining to the page. */
6281
- overflow-x: auto;
6282
- overscroll-behavior-inline: contain;
6283
6374
  }
6284
6375
 
6285
6376
  .steps > li {
6286
6377
  position: relative;
6287
- display: grid;
6288
- flex: 1 0 var(--step-min);
6289
- grid-template-rows: var(--step-size) auto;
6290
- gap: var(--step-gap);
6291
- justify-items: center;
6292
- min-inline-size: max(var(--step-size), var(--step-min));
6293
6378
  color: var(--text-muted);
6294
- text-align: center;
6295
6379
  counter-increment: actual-step;
6296
6380
  }
6297
6381
 
@@ -6312,30 +6396,38 @@ a.avatar[aria-disabled="true"] {
6312
6396
  line-height: 1;
6313
6397
  }
6314
6398
 
6315
- .steps > li::after {
6316
- content: "";
6317
- position: absolute;
6318
- inset-block-start: calc((var(--step-size) - var(--step-line-size)) / 2);
6319
- inset-inline-start: calc(50% + var(--step-size) / 2);
6320
- inline-size: calc(100% - var(--step-size));
6321
- block-size: var(--step-line-size);
6322
- /* A custom property resolves its var() before inheritance, so a default
6323
- --step-connector: var(--step-line) on .steps would freeze the parent line
6324
- color and a .complete child's connector would never follow its own line.
6325
- Falling back to the local --step-line here keeps each connector in sync;
6326
- an explicit --step-connector still wins when the author sets one. */
6327
- background: var(--step-connector, var(--step-line));
6399
+ /* The marker carries the state, so the label reads a notch below it — set on
6400
+ the component, never per representation: a size that changed with the layout
6401
+ would resize the text under a reader dragging a window. `color: inherit` is
6402
+ here rather than in a rule of its own for `a.step-label`, because a span
6403
+ inherits its ink anyway; only a link has anything to take back, and taking it
6404
+ back for every label costs nothing and one rule less. A navigable label keeps
6405
+ its underline, hover and focus ring — just not a link color, which would read
6406
+ ahead of the marker states.
6407
+
6408
+ Scoped to `.steps`, unlike `.menu-item-text` and the framework's other
6409
+ sub-part classes: a bare `.step-label` weighs (0,1,0), exactly tying
6410
+ `.prose :where(a)`, so a stepper in prose would be decided by bundle order. */
6411
+ .steps .step-label {
6412
+ font-size: var(--font-size-sm);
6413
+ line-height: var(--line-height-tight);
6414
+ color: inherit;
6328
6415
  }
6329
6416
 
6330
- .steps > li:last-child::after {
6331
- content: none;
6417
+ /* Each composition gives the connector its own geometry and background, so the
6418
+ only shared fact is which steps get one — every step but the last. Stated as
6419
+ the condition rather than as a `content: none` rule undoing a `content: ""`
6420
+ rule: a pseudo-element with no content generates no box, so a composition can
6421
+ address `> li::after` freely and still not trail a line past the last step. */
6422
+ .steps > li:not(:last-child)::after {
6423
+ content: "";
6332
6424
  }
6333
6425
 
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 {
6426
+ /* Prefixed like `.step-label`, unlike the framework's bare `.primary` /
6427
+ `.outline` modifiers: those qualify a component on its own element, this
6428
+ marks one item inside one. A bare `.complete` would also claim a very
6429
+ ordinary English word globally, for a single component's single state. */
6430
+ .steps > .step-complete {
6339
6431
  --step-marker-bg: var(--state-selected);
6340
6432
  --step-marker-border: var(--state-selected);
6341
6433
  --step-marker-fg: var(--state-selected-fg);
@@ -6344,10 +6436,9 @@ a.avatar[aria-disabled="true"] {
6344
6436
  color: var(--text);
6345
6437
  }
6346
6438
 
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. */
6439
+ /* Complete owns the accented connector that reaches the current step, so the
6440
+ connector leaving current goes back to neutral. Declared after
6441
+ .step-complete at equal specificity: current wins on every channel. */
6351
6442
  .steps > [aria-current="step"] {
6352
6443
  --step-marker-bg: var(--surface);
6353
6444
  --step-marker-border: var(--state-selected);
@@ -6357,31 +6448,211 @@ a.avatar[aria-disabled="true"] {
6357
6448
  color: var(--text);
6358
6449
  }
6359
6450
 
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
6451
  .steps > [aria-current="step"]::before {
6364
6452
  border-width: calc(var(--border-width) * 2);
6365
6453
  }
6366
6454
 
6367
- /* A finished step may replace its number with a mark (e.g. a check glyph):
6368
- --step-complete-mark accepts any content value and falls back to the
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 {
6455
+ /* Weight marks "where I am now". Complete needs none: its filled disc already
6456
+ reads as strongly. */
6457
+ .steps > [aria-current="step"] .step-label {
6458
+ font-weight: var(--font-weight-strong);
6459
+ }
6460
+
6461
+ .steps > .step-complete:not([aria-current="step"])::before {
6372
6462
  content: var(--step-complete-mark, counter(actual-step));
6373
6463
  }
6374
6464
 
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;
6465
+ /* Horizontal ------------------------------------------------------------- */
6466
+
6467
+ .steps-horizontal {
6468
+ --step-min: 7rem;
6469
+ --step-gap: var(--space-20);
6470
+
6471
+ inline-size: 100%;
6472
+ overflow-x: auto;
6473
+ /* Stated, never left to `overflow-x` alone: one scrolling axis computes the
6474
+ other to `auto`, and a row hugs fractional content closely enough that a
6475
+ rounding difference then raises a phantom vertical scrollbar. */
6476
+ overflow-y: hidden;
6477
+ overscroll-behavior-inline: contain;
6478
+ }
6479
+
6480
+ /* A scroll container clips at the padding edge, so a navigable row reserves the
6481
+ focus ring's own width and offset. Only this composition scrolls, and only a
6482
+ link has a ring to protect. */
6483
+ .steps-horizontal:has(a[href]) {
6484
+ padding-block: calc(var(--focus-outline-offset) + var(--border-width) * 2);
6485
+ }
6486
+
6487
+ /* One explicit row, the marker's: a `.step-label` is auto-placed into an
6488
+ implicit second row, and two tracks are also the only thing `gap` has to sit
6489
+ between. So an empty step measures exactly `--step-size`, and the rule below
6490
+ takes back nothing. `var(--step-size) auto` would build that second row
6491
+ whether or not a label ever arrives, and the compact form would then have to
6492
+ remove both the track and the gap. */
6493
+ .steps-horizontal > li {
6494
+ display: grid;
6495
+ flex: 1 0 var(--step-min);
6496
+ grid-template-rows: var(--step-size);
6497
+ gap: var(--step-gap);
6498
+ justify-items: center;
6499
+ min-inline-size: max(var(--step-size), var(--step-min));
6500
+ text-align: center;
6501
+ }
6502
+
6503
+ /* An empty step is a marker and nothing else, so it needs a marker's width
6504
+ rather than a reading width. That is what lets five of them fit a phone with
6505
+ no size context and no variant class.
6506
+
6507
+ `justify-items` is not an override of the row's centring: the marker is
6508
+ centred because a label sits under it and the two share a centre, and with no
6509
+ label there is nothing to centre against. The marker takes the start of its
6510
+ own share instead, the connector below fills the rest of it, and the last
6511
+ step has no share to fill — so the track runs edge to edge and lines up with
6512
+ the text around it, the way the inline representation does.
6513
+
6514
+ `:empty` says exactly what the markup says, the way `.badge:empty` does, and
6515
+ it reads the item's own content: bare text in an `<li>` keeps a reading width
6516
+ like its wrapped peers, where `> li:has(.step-label)` would give it a marker
6517
+ floor and skew the row. It also predates `:has()` by a decade, so the compact
6518
+ form reaches the Degraded tier and only the inline bonus needs `:has()`. */
6519
+ .steps-horizontal > li:empty {
6520
+ flex: 1 1 0;
6521
+ justify-items: start;
6522
+ min-inline-size: var(--step-size);
6523
+ }
6524
+
6525
+ .steps-horizontal > li:empty::after {
6526
+ inset-inline-start: var(--step-size);
6527
+ }
6528
+
6529
+ .steps-horizontal > li:empty:last-child {
6530
+ flex: 0 0 auto;
6531
+ }
6532
+
6533
+ /* The pseudo-element is marker-height so --step-connector may be any complete
6534
+ background value. */
6535
+ .steps-horizontal > li::after {
6536
+ position: absolute;
6537
+ inset-block-start: 0;
6538
+ inset-inline-start: calc(50% + var(--step-size) / 2);
6539
+ inline-size: calc(100% - var(--step-size));
6540
+ block-size: var(--step-size);
6541
+ background: var(
6542
+ --step-connector,
6543
+ linear-gradient(var(--step-line), var(--step-line)) center /
6544
+ 100% var(--step-line-size) no-repeat
6545
+ );
6546
+ }
6547
+
6548
+ /*
6549
+ * The one horizontal container query. Past 60rem a labelled row has the room
6550
+ * to put marker and label inline and still leave the connector a real region
6551
+ * between groups, rather than a stub. Calibrated on five steps — the widest
6552
+ * supported row, and therefore the binding case — and it counts nothing, so it
6553
+ * serves every count. Below it, stacked; a marker row is already the shape
6554
+ * this query produces and stays out of it.
6555
+ *
6556
+ * Horizontal is a 2-5 step component. Outside that domain the rule simply
6557
+ * applies as written, because a row that is out of contract is not owed a
6558
+ * curated fallback.
6559
+ */
6560
+ @supports (container-type: inline-size) {
6561
+ @container actual-container (inline-size >= 60rem) {
6562
+ .steps-horizontal:has(.step-label) > li {
6563
+ display: flex;
6564
+ flex: 1 1 auto;
6565
+ align-items: center;
6566
+ gap: var(--step-inline-gap);
6567
+ min-inline-size: 0;
6568
+ text-align: start;
6569
+ white-space: nowrap;
6570
+ }
6571
+
6572
+ .steps-horizontal:has(.step-label) > li::after {
6573
+ position: static;
6574
+ flex: 1 1 2rem;
6575
+ inline-size: auto;
6576
+ block-size: var(--step-size);
6577
+ margin-inline-end: var(--step-inline-gap);
6578
+ background: var(
6579
+ --step-inline-connector,
6580
+ var(
6581
+ --step-connector,
6582
+ linear-gradient(var(--step-line), var(--step-line)) center /
6583
+ 100% var(--step-line-size) no-repeat
6584
+ )
6585
+ );
6586
+ }
6587
+
6588
+ .steps-horizontal:has(.step-label) > li:last-child {
6589
+ flex: 0 0 auto;
6590
+ }
6382
6591
  }
6383
6592
  }
6384
6593
 
6594
+ /* Vertical --------------------------------------------------------------- */
6595
+
6596
+ .steps-vertical {
6597
+ --step-vertical-gap: var(--space-40);
6598
+
6599
+ flex-direction: column;
6600
+ gap: var(--step-vertical-gap);
6601
+ }
6602
+
6603
+ /* `justify-items` looks like a reset and is not one — nothing here resets
6604
+ anything, since the horizontal rules never applied. Grid items blockify, so
6605
+ without it a label stretches the whole 1fr track: 264px against 75px for an
6606
+ `a.step-label`, a full-width focus ring the row does not give the same link. */
6607
+ .steps-vertical > li {
6608
+ display: grid;
6609
+ grid-template-columns: var(--step-size) minmax(0, 1fr);
6610
+ align-items: start;
6611
+ justify-items: start;
6612
+ gap: var(--step-inline-gap);
6613
+ min-block-size: var(--step-size);
6614
+ }
6615
+
6616
+ /* An empty step is a marker rail. The column cannot use the row's implicit
6617
+ second track, because `grid-auto-flow` is `row`: an auto-placed label would
6618
+ land *under* its marker. Its second track is therefore explicit, and this
6619
+ takes it back.
6620
+
6621
+ Silent in a stretched column, where the item fills its line whatever its
6622
+ tracks say. It bites in a shrink-to-fit box — which is what a sticky
6623
+ progress rail usually is — where the label track and the gap to it make the
6624
+ rail `--step-inline-gap` wider than the markers it draws: measured 36px
6625
+ against 28px. No `gap: 0` needed; one track has nothing to sit beside. */
6626
+ .steps-vertical > li:empty {
6627
+ grid-template-columns: var(--step-size);
6628
+ }
6629
+
6630
+ /* The row starts at the top so a wrapped label begins level with its marker.
6631
+ align-self only bites on a single line, which would otherwise float above
6632
+ the marker's centre.
6633
+
6634
+ The one `.step-label` rule that keeps a child combinator: `align-self` is a
6635
+ grid-item property, so this declaration is meaningless on anything but a
6636
+ direct child of the row. Elsewhere the label is addressed as a descendant —
6637
+ the framework owns the structure, and repeating it buys nothing. */
6638
+ .steps-vertical > li > .step-label {
6639
+ align-self: center;
6640
+ }
6641
+
6642
+ /* The vertical track publishes no hook of its own: a horizontal connector value
6643
+ cannot transpose onto this geometry, and this selector is already a reachable
6644
+ override for the design that finally asks. */
6645
+ .steps-vertical > li::after {
6646
+ position: absolute;
6647
+ inset-block-start: var(--step-size);
6648
+ inset-inline-start: calc((var(--step-size) - var(--step-line-size)) / 2);
6649
+ inline-size: var(--step-line-size);
6650
+ block-size: calc(100% - var(--step-size) + var(--step-vertical-gap));
6651
+ background:
6652
+ linear-gradient(var(--step-line), var(--step-line)) center / var(--step-line-size) 100%
6653
+ no-repeat;
6654
+ }
6655
+
6385
6656
 
6386
6657
  /* Navigation */
6387
6658
  /*
@@ -6846,7 +7117,9 @@ a.avatar[aria-disabled="true"] {
6846
7117
  .list > li > .list-item {
6847
7118
  display: grid;
6848
7119
  grid-template-columns: auto minmax(0, 1fr) auto;
6849
- align-items: center;
7120
+ /* Regions align to the first line, so leading, title, and trailing controls
7121
+ share one horizontal line while supporting text wraps beneath. */
7122
+ align-items: start;
6850
7123
  gap: var(--list-item-gap);
6851
7124
  min-block-size: var(--list-item-min-size);
6852
7125
  padding-block: var(--list-item-pad-block);
@@ -7189,12 +7462,23 @@ a.list-item:hover {
7189
7462
  /*
7190
7463
  * Decorative intent-colored frame for one direct child.
7191
7464
  *
7465
+ * The effect is built from three layers with distinct roles: a frame carrying
7466
+ * a short accent of light, a small halo that belongs to the frame and keeps
7467
+ * that light attached to the edge, and a wide static halo that belongs to the
7468
+ * object and gives it a permanent, diffuse presence. Only the small halo
7469
+ * follows the animated gradient, so a rotating aura repaints one blurred layer
7470
+ * per frame, not two.
7471
+ *
7192
7472
  * Public hooks:
7193
7473
  * --aura-color
7194
7474
  * --aura-width
7195
7475
  * --aura-blur
7476
+ * --aura-intensity
7196
7477
  * --aura-radius
7197
7478
  * --aura-duration
7479
+ *
7480
+ * Internal:
7481
+ * --aura-angle
7198
7482
  */
7199
7483
  @property --aura-angle {
7200
7484
  syntax: "<angle>";
@@ -7211,7 +7495,8 @@ a.list-item:hover {
7211
7495
  .aura {
7212
7496
  --aura-color: var(--intent, var(--primary));
7213
7497
  --aura-width: 2px;
7214
- --aura-blur: 0.75rem;
7498
+ --aura-blur: 0.25rem;
7499
+ --aura-intensity: 1;
7215
7500
  --aura-radius: var(--radius-lg);
7216
7501
  --aura-duration: 6s;
7217
7502
 
@@ -7222,9 +7507,19 @@ a.list-item:hover {
7222
7507
  border-radius: calc(var(--aura-radius) + var(--aura-width));
7223
7508
  border: var(--border-width) solid transparent;
7224
7509
  background: var(--aura-color);
7225
- background: conic-gradient(from var(--aura-angle), transparent 0 65%, var(--aura-color));
7510
+
7511
+ /* A short feathered accent rather than a long wedge with a hard edge, which
7512
+ reads as one lit side. Nothing tints the frame outside it: a constant tint
7513
+ across 2px draws an outline, the opposite of the effect. */
7514
+ background: conic-gradient(
7515
+ from var(--aura-angle),
7516
+ transparent 0 78%,
7517
+ var(--aura-color) 93%,
7518
+ transparent
7519
+ );
7226
7520
  }
7227
7521
 
7522
+ /* Near halo — inherits the frame gradient, so it tracks the lit accent. */
7228
7523
  .aura::before {
7229
7524
  position: absolute;
7230
7525
  z-index: -1;
@@ -7232,15 +7527,40 @@ a.list-item:hover {
7232
7527
  border-radius: inherit;
7233
7528
  background: inherit;
7234
7529
  filter: blur(var(--aura-blur));
7235
- opacity: 0.45;
7530
+ opacity: calc(0.55 * var(--aura-intensity));
7531
+ pointer-events: none;
7532
+ content: "";
7533
+ }
7534
+
7535
+ /* Far halo — flat and static. At this blur the rotating detail is no longer
7536
+ legible, so there is nothing to gain from animating a second wide blurred
7537
+ layer. It outgrows the frame, otherwise the child covers it and the whole
7538
+ effect reads as a drop shadow. This is the layer that stays lit at all
7539
+ times, so it — not the frame — is what keeps the object present between two
7540
+ passes of the accent. */
7541
+ .aura::after {
7542
+ position: absolute;
7543
+ z-index: -1;
7544
+ inset: calc(-1 * var(--aura-blur));
7545
+ border-radius: calc(var(--aura-radius) + var(--aura-width) + var(--aura-blur));
7546
+ background: var(--aura-color);
7547
+ filter: blur(calc(var(--aura-blur) * 4));
7548
+ opacity: calc(0.15 * var(--aura-intensity));
7236
7549
  pointer-events: none;
7237
7550
  content: "";
7238
7551
  }
7239
7552
 
7553
+ /* Lifts the child above both halos. It also has to be opaque: the frame paints
7554
+ across the whole box and only its edge is meant to show, so a transparent
7555
+ child (an .outline alert, a .ghost button) is filled with the frame color
7556
+ rather than framed by it. Documented as a constraint, not worked around —
7557
+ masking a ring would cost more than the case is worth. */
7240
7558
  .aura > * {
7241
7559
  position: relative;
7242
7560
  }
7243
7561
 
7562
+ /* Glow lights the whole perimeter instead of a short accent, so both halos
7563
+ become a single soft field around the child. */
7244
7564
  .aura-glow {
7245
7565
  background: var(--aura-color);
7246
7566
  }
@@ -7249,6 +7569,29 @@ a.list-item:hover {
7249
7569
  .aura:not(.aura-glow) {
7250
7570
  animation: aura-rotate var(--aura-duration) linear infinite;
7251
7571
  }
7572
+
7573
+ /* Both halos breathe: moving the near one alone changes a line's brightness,
7574
+ which barely registers. Opening the wide field with it is what reads as a
7575
+ breath. */
7576
+ .aura-glow::before {
7577
+ animation: aura-breathe-near var(--aura-duration) ease-in-out infinite;
7578
+ }
7579
+
7580
+ .aura-glow::after {
7581
+ animation: aura-breathe-far var(--aura-duration) ease-in-out infinite;
7582
+ }
7583
+ }
7584
+
7585
+ /* Without motion the accent would freeze at an arbitrary angle and read as a
7586
+ broken animation, so it becomes a deliberate composition: two opposite
7587
+ corners. Fixed radii keep those corners stable across aspect ratios;
7588
+ proportional radii turn wide or tall elements into lit edges instead. */
7589
+ @media (prefers-reduced-motion: reduce) {
7590
+ .aura:not(.aura-glow) {
7591
+ background:
7592
+ radial-gradient(4rem 4rem at 0% 0%, var(--aura-color), transparent 70%),
7593
+ radial-gradient(4rem 4rem at 100% 100%, var(--aura-color), transparent 70%);
7594
+ }
7252
7595
  }
7253
7596
 
7254
7597
  @keyframes aura-rotate {
@@ -7257,18 +7600,38 @@ a.list-item:hover {
7257
7600
  }
7258
7601
  }
7259
7602
 
7260
- @supports selector(:has(*)) {
7261
- .aura:has(> .btn) {
7262
- --aura-radius: var(--radius);
7603
+ /* Only the peak is declared, so each halo breathes up from its own resting
7604
+ opacity and never dips below the static glow. The peaks scale with the same
7605
+ intensity as the resting values, which keeps the breath proportional at
7606
+ every setting. */
7607
+ @keyframes aura-breathe-near {
7608
+ 50% {
7609
+ opacity: calc(0.95 * var(--aura-intensity));
7610
+ transform: scale(1.03);
7263
7611
  }
7612
+ }
7264
7613
 
7265
- .aura:has(> :is(.card, .alert)) {
7266
- --aura-radius: var(--radius-lg);
7614
+ @keyframes aura-breathe-far {
7615
+ 50% {
7616
+ opacity: calc(0.28 * var(--aura-intensity));
7617
+ transform: scale(1.07);
7267
7618
  }
7619
+ }
7268
7620
 
7269
- .aura:has(> .badge) {
7270
- --aura-radius: var(--radius-full);
7271
- }
7621
+ /* Each rule is a lone :has() setting a decorative radius, so a browser without
7622
+ :has() simply drops it and keeps the default. No @supports needed — that
7623
+ guard only earns its place around a selector list that mixes :has() with
7624
+ selectors worth keeping. */
7625
+ .aura:has(> .btn) {
7626
+ --aura-radius: var(--radius);
7627
+ }
7628
+
7629
+ .aura:has(> :is(.card, .alert)) {
7630
+ --aura-radius: var(--radius-lg);
7631
+ }
7632
+
7633
+ .aura:has(> .badge) {
7634
+ --aura-radius: var(--radius-full);
7272
7635
  }
7273
7636
 
7274
7637
  /* Utilities — small, single-purpose escape hatches. */