actual-css 0.7.0 → 0.8.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 (141) hide show
  1. package/CHANGELOG.md +166 -0
  2. package/README.md +13 -11
  3. package/components.json +668 -0
  4. package/dist/actual.css +166 -37
  5. package/dist/actual.full.css +963 -603
  6. package/dist/actual.full.js +1 -1
  7. package/dist/actual.full.min.css +1 -1
  8. package/dist/actual.js +1 -1
  9. package/dist/actual.min.css +1 -1
  10. package/docs/design-notes/accessibility-decisions.md +33 -0
  11. package/docs/design-notes/browser-support.md +62 -30
  12. package/docs/design-notes/column-layout.md +3 -3
  13. package/docs/design-notes/css-anchor.md +75 -21
  14. package/docs/design-notes/enhancement-contract.md +14 -6
  15. package/docs/design-notes/grid.md +4 -4
  16. package/docs/design-notes/naming.md +6 -1
  17. package/docs/design-notes/platform-alignment.md +53 -7
  18. package/docs/design-notes/popover-manual-poc.md +105 -25
  19. package/docs/design-notes/surface-reparenting.md +56 -47
  20. package/docs/design-notes/tooltip-coordinate-space.md +132 -0
  21. package/docs/design-notes/widget-primitives.md +7 -4
  22. package/docs/pages/components/accordion.md +41 -2
  23. package/docs/pages/components/alert.md +80 -30
  24. package/docs/pages/components/app-navigation.md +6 -0
  25. package/docs/pages/components/avatar.md +6 -6
  26. package/docs/pages/components/badge.md +101 -16
  27. package/docs/pages/components/button.md +16 -8
  28. package/docs/pages/components/card.md +10 -10
  29. package/docs/pages/components/dialog.md +16 -6
  30. package/docs/pages/components/fab.md +4 -3
  31. package/docs/pages/components/list.md +42 -4
  32. package/docs/pages/components/navbar.md +17 -1
  33. package/docs/pages/components/pagination.md +2 -2
  34. package/docs/pages/components/steps.md +19 -0
  35. package/docs/pages/components/table.md +3 -2
  36. package/docs/pages/enhancements/flyout.md +66 -36
  37. package/docs/pages/enhancements/overview.md +25 -0
  38. package/docs/pages/enhancements/status-bar.md +1 -1
  39. package/docs/pages/enhancements/tabs.md +45 -37
  40. package/docs/pages/enhancements/tooltip.md +36 -10
  41. package/docs/pages/examples/overview.md +38 -5
  42. package/docs/pages/forms/choice-cards.md +6 -2
  43. package/docs/pages/forms/combobox.md +1 -1
  44. package/docs/pages/forms/floating-field.md +4 -3
  45. package/docs/pages/forms/joined-controls.md +22 -0
  46. package/docs/pages/forms/otp.md +2 -1
  47. package/docs/pages/forms/overview.md +1 -1
  48. package/docs/pages/forms/states-and-sizes.md +16 -18
  49. package/docs/pages/forms/sticky-actions.md +2 -2
  50. package/docs/pages/forms/switch.md +1 -1
  51. package/docs/pages/foundations/philosophy.md +57 -26
  52. package/docs/pages/foundations/theming.md +2 -2
  53. package/docs/pages/foundations/tokens.md +118 -21
  54. package/docs/pages/foundations/typography.md +35 -14
  55. package/docs/pages/guides/introduction.md +11 -9
  56. package/docs/pages/guides/progressive-enhancement.md +115 -30
  57. package/docs/pages/guides/tailwind.md +3 -2
  58. package/docs/pages/layout/media.md +1 -1
  59. package/docs/pages/layout/overview.md +11 -1
  60. package/docs/pages/layout/scroll-snap.md +3 -3
  61. package/docs/pages/layout/stack.md +17 -9
  62. package/docs/pages/patterns/icon-input.md +4 -2
  63. package/docs/pages/patterns/icon-slot.md +2 -0
  64. package/docs/pages/patterns/intent-nesting.md +1 -1
  65. package/docs/pages/utilities/overview.md +23 -2
  66. package/docs/pages/utilities/sizing-wrapping.md +1 -1
  67. package/docs/pages/utilities/spacing.md +10 -8
  68. package/llms.txt +2 -1
  69. package/package.json +17 -8
  70. package/{scripts/reserved-classes.json → reserved-classes.json} +4 -6
  71. package/src/css/components/accordion.css +42 -3
  72. package/src/css/components/alert.css +25 -13
  73. package/src/css/components/app-nav.css +18 -8
  74. package/src/css/components/avatar.css +13 -1
  75. package/src/css/components/badge.css +78 -12
  76. package/src/css/components/breadcrumb.css +2 -3
  77. package/src/css/components/button.css +9 -16
  78. package/src/css/components/card.css +9 -2
  79. package/src/css/components/chat.css +1 -0
  80. package/src/css/components/data-list.css +1 -1
  81. package/src/css/components/drawer.css +79 -25
  82. package/src/css/components/fab.css +0 -6
  83. package/src/css/components/flyout.css +35 -98
  84. package/src/css/components/index.css +0 -1
  85. package/src/css/components/indicator.css +21 -0
  86. package/src/css/components/key.css +10 -0
  87. package/src/css/components/list.css +40 -4
  88. package/src/css/components/menu.css +2 -1
  89. package/src/css/components/modal.css +48 -33
  90. package/src/css/components/navbar.css +37 -4
  91. package/src/css/components/pagination.css +1 -0
  92. package/src/css/components/spinner.css +6 -6
  93. package/src/css/components/status-bar.css +0 -5
  94. package/src/css/components/steps.css +45 -59
  95. package/src/css/components/tab.css +29 -9
  96. package/src/css/components/table.css +5 -5
  97. package/src/css/components/tooltip.css +36 -40
  98. package/src/css/core/base.css +23 -1
  99. package/src/css/core/intents.css +17 -6
  100. package/src/css/core/theme.css +27 -0
  101. package/src/css/core/tokens.css +35 -15
  102. package/src/css/core/variants.css +64 -15
  103. package/src/css/forms/choice-card.css +4 -4
  104. package/src/css/forms/choice.css +6 -3
  105. package/src/css/forms/control.css +2 -1
  106. package/src/css/forms/floating-field.css +3 -3
  107. package/src/css/forms/form.css +4 -3
  108. package/src/css/forms/input-icon.css +20 -14
  109. package/src/css/forms/native.css +6 -1
  110. package/src/css/forms/otp.css +9 -1
  111. package/src/css/forms/range.css +2 -0
  112. package/src/css/forms/select.css +29 -9
  113. package/src/css/forms/switch.css +19 -5
  114. package/src/css/layout/app-layout.css +1 -1
  115. package/src/css/layout/grid.css +50 -76
  116. package/src/css/layout/scroll-snap.css +11 -7
  117. package/src/css/layout/scroller.css +3 -4
  118. package/src/css/layout/stack.css +2 -2
  119. package/src/css/typography/fluid.css +4 -3
  120. package/src/css/typography/prose.css +6 -5
  121. package/src/css/utilities/base.css +2 -1
  122. package/src/css/utilities/extra.css +20 -17
  123. package/src/js/context-menu.js +27 -26
  124. package/src/js/dialog.js +2 -1
  125. package/src/js/dismiss.js +5 -0
  126. package/src/js/enhance.js +34 -2
  127. package/src/js/enhancement-loader.js +36 -43
  128. package/src/js/events.js +0 -3
  129. package/src/js/filter.js +0 -1
  130. package/src/js/floating-space.js +42 -0
  131. package/src/js/flyout.js +9 -44
  132. package/src/js/focus-group.js +7 -0
  133. package/src/js/focus.js +6 -0
  134. package/src/js/menu.js +10 -1
  135. package/src/js/selectors.js +5 -9
  136. package/src/js/status.js +6 -5
  137. package/src/js/surface.js +97 -139
  138. package/src/js/tab.js +6 -1
  139. package/src/js/tooltip.js +150 -51
  140. package/src/js/validation.js +1 -1
  141. package/src/css/components/surface.css +0 -49
@@ -9,8 +9,15 @@
9
9
  * --gap (local value for the shared gap token)
10
10
  */
11
11
  /* Intent boundary — inherited intents do not tint this component.
12
- Local intent classes still win because :where() has zero specificity. */
12
+ Local intent classes still win because :where() has zero specificity.
13
+ The intent tokens are reset alongside --ui-*: the shared variants
14
+ (.soft/.solid/.outline/.surface) read --intent / --intent-fg / --intent-soft-fg
15
+ when they paint this surface, so an ancestor intent would otherwise leak
16
+ through a variant that has no local intent class. */
13
17
  :where(.navbar) {
18
+ --intent: initial;
19
+ --intent-fg: initial;
20
+ --intent-soft-fg: initial;
14
21
  --ui-bg: initial;
15
22
  --ui-fg: initial;
16
23
  --ui-border: initial;
@@ -66,10 +73,15 @@
66
73
  text-decoration: none;
67
74
  }
68
75
 
69
- .nav-link:where([aria-current]:not([aria-current="false"])) {
76
+ /* nav-link serves two contracts: plain page navigation (aria-current="page")
77
+ and scrollspy, which toggles aria-current="location" on the active link.
78
+ Presence is the contract: the attribute is set on the current link and
79
+ removed everywhere else, never serialized as aria-current="false".
80
+ Selection never changes text metrics: the shared medium weight is kept and
81
+ the current link is carried by the selected accent. */
82
+ .nav-link[aria-current] {
70
83
  background: var(--surface-subtle);
71
- color: var(--text);
72
- font-weight: var(--font-weight-bold);
84
+ color: var(--state-selected);
73
85
  }
74
86
 
75
87
  .nav-link:hover {
@@ -77,11 +89,18 @@
77
89
  color: var(--text);
78
90
  }
79
91
 
92
+ /* A current link keeps its accent on hover: without this, the hover rule above
93
+ would drop it back to --text. The surface hover still applies. */
94
+ .nav-link[aria-current]:hover {
95
+ color: var(--state-selected);
96
+ }
97
+
80
98
  /* Nav list — a vertical navigation list. Used by .drawer for stacked links.
81
99
  Links fill each row so the whole line is an interactive target; the
82
100
  horizontal .navbar-nav links keep their intrinsic size. */
83
101
  .nav-list {
84
102
  display: grid;
103
+ grid-template-columns: minmax(0, 1fr);
85
104
  --gap: var(--space-20);
86
105
  gap: var(--gap);
87
106
  margin: 0;
@@ -94,5 +113,19 @@
94
113
  }
95
114
 
96
115
  .nav-list .nav-link {
116
+ position: relative;
97
117
  inline-size: 100%;
98
118
  }
119
+
120
+ /* Vertical current indicator: inset inline-start trait in currentColor, so it
121
+ follows the forced palette without a forced-colors block and never touches
122
+ the row separators. Navbar horizontal links keep color + surface only. The
123
+ positioning context lives on every row link, so becoming current never moves
124
+ an app-positioned decoration to a new containing block. */
125
+ .nav-list .nav-link[aria-current]::before {
126
+ content: "";
127
+ position: absolute;
128
+ inset-block: var(--space-20);
129
+ inset-inline-start: 0;
130
+ border-inline-start: 2px solid currentColor;
131
+ }
@@ -17,6 +17,7 @@
17
17
  margin: 0;
18
18
  padding: 0;
19
19
  list-style: none;
20
+ font-size: var(--control-font-size);
20
21
 
21
22
  /* Arrows, page numbers, and truncation ellipses share one geometric
22
23
  system: equal digit widths keep the row from re-flowing as numbers grow
@@ -31,7 +31,6 @@
31
31
  border-inline-end-color: var(--spinner-value);
32
32
  border-radius: var(--radius-full);
33
33
  opacity: 0.85;
34
- animation: actual-spin var(--duration-spin) linear infinite;
35
34
  vertical-align: text-bottom;
36
35
  }
37
36
 
@@ -52,13 +51,14 @@
52
51
  --spinner-width: 3px;
53
52
  }
54
53
 
54
+ @media (prefers-reduced-motion: no-preference) {
55
+ .spinner {
56
+ animation: actual-spin var(--duration-spin) linear infinite;
57
+ }
58
+ }
59
+
55
60
  @keyframes actual-spin {
56
61
  to {
57
62
  transform: rotate(360deg);
58
63
  }
59
64
  }
60
- @media print {
61
- .spinner {
62
- display: none !important;
63
- }
64
- }
@@ -86,8 +86,3 @@
86
86
  translate var(--duration-slow) var(--ease-enter),
87
87
  background-color var(--duration) ease;
88
88
  }
89
- @media print {
90
- .status-bar {
91
- display: none !important;
92
- }
93
- }
@@ -8,23 +8,16 @@
8
8
  *
9
9
  * `.steps` is what a step *is* — the counter, the marker, `.step-label`,
10
10
  * `.step-complete`, `aria-current="step"`. It carries no orientation, so it is
11
- * never written alone. `.steps-horizontal` and `.steps-vertical` are what a
12
- * sequence *looks like*, and they are peers: neither is the default the other
13
- * has to exclude, so no rule says `:not(...)` and no rule undoes another's
14
- * layout.
11
+ * never written alone. The two orientations are peers: no rule says
12
+ * `:not(...)` and no rule undoes another's layout.
15
13
  *
16
- * Orientation and labels are independent dimensions. `.step-label` is optional
17
- * in either orientation; when a sequence carries names, every step carries
18
- * one:
14
+ * Orientation and labels are independent. `.step-label` is optional in either;
15
+ * when a sequence carries names, every step carries one:
19
16
  *
20
17
  * empty steps .step-label on every step
21
18
  * horizontal compact progression named sequence, stacked or inline
22
19
  * vertical compact rail named sequence, labels beside markers
23
20
  *
24
- * An empty step is a marker and nothing else, so it is compact by structure —
25
- * `> li:empty` in both orientations, no variant class, and nothing authored
26
- * only to be hidden again. The page around it owns the step name.
27
- *
28
21
  * A label may be an `a[href]`, which is the whole interactive surface: the
29
22
  * marker is never navigable, so navigating a flow means naming its
30
23
  * destinations. Anything else focusable in a step is out of contract.
@@ -95,7 +88,7 @@
95
88
  background: var(--step-marker-bg);
96
89
  color: var(--step-marker-fg);
97
90
  font-size: var(--font-size-sm);
98
- font-weight: var(--font-weight-strong);
91
+ font-weight: var(--font-weight-semibold);
99
92
  line-height: 1;
100
93
  }
101
94
 
@@ -155,11 +148,9 @@
155
148
  border-width: calc(var(--border-width) * 2);
156
149
  }
157
150
 
158
- /* Weight marks "where I am now". Complete needs none: its filled disc already
159
- reads as strongly. */
160
- .steps > [aria-current="step"] .step-label {
161
- font-weight: var(--font-weight-strong);
162
- }
151
+ /* No label weight change for current: the accented marker with its doubled
152
+ border already carries the state, and selection never changes text metrics.
153
+ Complete needs none either: its filled disc already reads as strongly. */
163
154
 
164
155
  .steps > .step-complete:not([aria-current="step"])::before {
165
156
  content: var(--step-complete-mark, counter(actual-step));
@@ -217,8 +208,8 @@
217
208
  `:empty` says exactly what the markup says, the way `.badge:empty` does, and
218
209
  it reads the item's own content: bare text in an `<li>` keeps a reading width
219
210
  like its wrapped peers, where `> li:has(.step-label)` would give it a marker
220
- floor and skew the row. It also predates `:has()` by a decade, so the compact
221
- form reaches the Degraded tier and only the inline bonus needs `:has()`. */
211
+ floor and skew the row. `:empty` also keeps the compact form reachable on the
212
+ Degraded tier, where only the inline bonus needs `:has()`. */
222
213
  .steps-horizontal > li:empty {
223
214
  flex: 1 1 0;
224
215
  justify-items: start;
@@ -249,48 +240,43 @@
249
240
  }
250
241
 
251
242
  /*
252
- * The one horizontal container query. Past 60rem a labelled row has the room
253
- * to put marker and label inline and still leave the connector a real region
254
- * between groups, rather than a stub. Calibrated on five steps — the widest
255
- * supported row, and therefore the binding case — and it counts nothing, so it
256
- * serves every count. Below it, stacked; a marker row is already the shape
257
- * this query produces and stays out of it.
243
+ * The one horizontal container query: past 60rem a labelled row can put marker
244
+ * and label inline and still leave the connector a real region between groups.
245
+ * It counts nothing, so it serves every count; below it, stacked. A marker row
246
+ * is already the shape this query produces and stays out of it.
258
247
  *
259
- * Horizontal is a 2-5 step component. Outside that domain the rule simply
260
- * applies as written, because a row that is out of contract is not owed a
261
- * curated fallback.
248
+ * Horizontal is a 2-5 step component. Outside that domain the rule applies as
249
+ * written — a row out of contract is not owed a curated fallback.
262
250
  */
263
- @supports (container-type: inline-size) {
264
- @container actual-container (inline-size >= 60rem) {
265
- .steps-horizontal:has(.step-label) > li {
266
- display: flex;
267
- flex: 1 1 auto;
268
- align-items: center;
269
- gap: var(--step-inline-gap);
270
- min-inline-size: 0;
271
- text-align: start;
272
- white-space: nowrap;
273
- }
274
-
275
- .steps-horizontal:has(.step-label) > li::after {
276
- position: static;
277
- flex: 1 1 2rem;
278
- inline-size: auto;
279
- block-size: var(--step-size);
280
- margin-inline-end: var(--step-inline-gap);
281
- background: var(
282
- --step-inline-connector,
283
- var(
284
- --step-connector,
285
- linear-gradient(var(--step-line), var(--step-line)) center /
286
- 100% var(--step-line-size) no-repeat
287
- )
288
- );
289
- }
290
-
291
- .steps-horizontal:has(.step-label) > li:last-child {
292
- flex: 0 0 auto;
293
- }
251
+ @container actual-container (inline-size >= 60rem) {
252
+ .steps-horizontal:has(.step-label) > li {
253
+ display: flex;
254
+ flex: 1 1 auto;
255
+ align-items: center;
256
+ gap: var(--step-inline-gap);
257
+ min-inline-size: 0;
258
+ text-align: start;
259
+ white-space: nowrap;
260
+ }
261
+
262
+ .steps-horizontal:has(.step-label) > li::after {
263
+ position: static;
264
+ flex: 1 1 2rem;
265
+ inline-size: auto;
266
+ block-size: var(--step-size);
267
+ margin-inline-end: var(--step-inline-gap);
268
+ background: var(
269
+ --step-inline-connector,
270
+ var(
271
+ --step-connector,
272
+ linear-gradient(var(--step-line), var(--step-line)) center /
273
+ 100% var(--step-line-size) no-repeat
274
+ )
275
+ );
276
+ }
277
+
278
+ .steps-horizontal:has(.step-label) > li:last-child {
279
+ flex: 0 0 auto;
294
280
  }
295
281
  }
296
282
 
@@ -5,6 +5,11 @@
5
5
  * and arrow-key navigation. CSS owns the tab strip appearance and
6
6
  * active indicator.
7
7
  *
8
+ * aria-orientation is matched inside :where() so it keeps describing which
9
+ * arrow keys move between tabs without also carrying cascade weight: an
10
+ * author class can then adjust a vertical strip without restating the
11
+ * attribute to reach its specificity.
12
+ *
8
13
  * Public hooks:
9
14
  * --tab-gap
10
15
  */
@@ -25,7 +30,7 @@
25
30
  list-style: none;
26
31
  }
27
32
 
28
- .tabs[aria-orientation="vertical"] {
33
+ .tabs:where([aria-orientation="vertical"]) {
29
34
  display: grid;
30
35
  align-content: start;
31
36
  justify-items: stretch;
@@ -46,12 +51,15 @@
46
51
  background: transparent;
47
52
  color: var(--text-muted);
48
53
  font: inherit;
49
- font-weight: var(--font-weight-strong);
54
+ font-size: var(--control-font-size);
55
+ /* Selection must not change text metrics: a heavier active label would shift
56
+ the strip. The active state is carried by color and the indicator line. */
57
+ font-weight: var(--font-weight-semibold);
50
58
  text-decoration: none;
51
59
  cursor: pointer;
52
60
  }
53
61
 
54
- .tabs[aria-orientation="vertical"] .tab {
62
+ .tabs:where([aria-orientation="vertical"]) .tab {
55
63
  justify-content: flex-start;
56
64
  padding-inline-end: var(--space-40);
57
65
  }
@@ -65,6 +73,8 @@
65
73
  color: var(--text);
66
74
  }
67
75
 
76
+ /* The ring is square on the edge the tab is attached to, so it reads as part
77
+ of the strip rather than a floating chip. */
68
78
  .tab:focus-visible {
69
79
  outline: var(--focus-outline);
70
80
  outline-color: var(--focus);
@@ -72,18 +82,27 @@
72
82
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
73
83
  }
74
84
 
75
- .tab[aria-selected="true"] {
85
+ /* A vertical tab is attached along its inline end, not its block end, so the
86
+ horizontal geometry above would round the wrong pair of corners. Logical
87
+ corners keep it correct in RTL. */
88
+ .tabs:where([aria-orientation="vertical"]) .tab:focus-visible {
89
+ border-start-start-radius: var(--radius-sm);
90
+ border-start-end-radius: 0;
91
+ border-end-start-radius: var(--radius-sm);
92
+ border-end-end-radius: 0;
93
+ }
94
+
95
+ .tab[aria-selected="true"],
96
+ .tab[aria-current="page"] {
76
97
  color: var(--intent, var(--state-selected));
77
- /* Second, layout-free selected cue — the underline alone is color-only in
78
- forced colors. */
79
- font-weight: var(--font-weight-bold);
80
98
  }
81
99
 
82
100
  /* The selected indicator lives on a positioned pseudo so that only the active
83
101
  tab owns an underline: a transparent border on every tab would surface in
84
102
  forced colors and erase the distinction. currentColor follows the palette the
85
103
  UA forces, so no forced-colors block is needed. */
86
- .tab[aria-selected="true"]::after {
104
+ .tab[aria-selected="true"]::after,
105
+ .tab[aria-current="page"]::after {
87
106
  content: "";
88
107
  position: absolute;
89
108
  inset-inline: 0;
@@ -91,7 +110,8 @@
91
110
  border-block-end: 2px solid currentColor;
92
111
  }
93
112
 
94
- .tabs[aria-orientation="vertical"] .tab[aria-selected="true"]::after {
113
+ .tabs:where([aria-orientation="vertical"]) .tab[aria-selected="true"]::after,
114
+ .tabs:where([aria-orientation="vertical"]) .tab[aria-current="page"]::after {
95
115
  inset-block: 0;
96
116
  inset-inline: auto 0;
97
117
  border-block-end: 0;
@@ -43,13 +43,13 @@
43
43
  vertical-align: baseline;
44
44
  }
45
45
 
46
- .table thead th {
47
- vertical-align: bottom;
48
- }
49
-
50
46
  .table th {
51
47
  color: var(--text-muted);
52
- font-weight: var(--font-weight-strong);
48
+ font-weight: var(--font-weight-semibold);
49
+ }
50
+
51
+ .table thead th {
52
+ vertical-align: bottom;
53
53
  }
54
54
 
55
55
  .table thead {
@@ -1,9 +1,13 @@
1
1
  /*
2
2
  * Tooltip — supplemental label overlaid near a trigger.
3
3
  *
4
- * Position, placement, and arrow position are managed by JavaScript
5
- * via position: fixed, left, top, data-placement, and --arrow-x.
6
- * Visibility is managed with hidden; CSS owns entry and exit animation.
4
+ * Position, placement, and arrow position are managed by JavaScript via left,
5
+ * top, data-placement, and --arrow-x. The runtime also owns `position`: it
6
+ * writes `absolute` for a tip whose trigger scrolls with the page, so the
7
+ * browser carries the tip along instead of the positioner correcting it a
8
+ * frame later, and `fixed` — the declaration below — for every other trigger.
9
+ * Visibility is the Popover lifecycle: tooltip.js promotes the tip to the top
10
+ * layer with popover="manual". CSS owns entry and exit animation.
7
11
  *
8
12
  * Public hooks:
9
13
  * --tooltip-bg
@@ -11,8 +15,8 @@
11
15
  * --tooltip-arrow-size
12
16
  *
13
17
  * Internal:
14
- * --arrow-x (arrow position, written by floating.js)
15
- * --arrow-y (arrow position, written by floating.js)
18
+ * --arrow-x (arrow position, written by the positioner)
19
+ * --arrow-y (arrow position, written by the positioner)
16
20
  */
17
21
  .tooltip {
18
22
  --tooltip-bg: var(--surface-solid);
@@ -20,7 +24,6 @@
20
24
  --tooltip-arrow-size: 5px;
21
25
 
22
26
  position: fixed;
23
- z-index: var(--z-tooltip);
24
27
  max-inline-size: min(24rem, calc(var(--viewport-inline) - 2rem));
25
28
  padding: 0.25em 0.6em;
26
29
  border-radius: var(--radius-sm);
@@ -41,43 +44,42 @@
41
44
 
42
45
  /* Neutralize only the native geometry that conflicts with a positioned
43
46
  tooltip. In particular, the UA overflow would turn the arrow into scroll
44
- overflow instead of letting it project beyond the bubble. Gated on real
45
- Popover support: the attribute selector matches whether or not the engine
46
- implements Popover, and without it nothing hides the element, so an ungated
47
- reset would leave a stray bubble parked at inset: auto. Popover sits outside
48
- Actual's Minimal contract, so the gate tracks the capability rather than the
49
- floor. */
50
- @supports selector(:popover-open) {
51
- .tooltip[popover] {
52
- inset: auto;
53
- margin: 0;
54
- overflow: visible;
55
- }
47
+ overflow instead of letting it project beyond the bubble.
56
48
 
57
- /* .tooltip declares no display of its own, so the UA already hides a closed
58
- popover here. The rule is written anyway: both components advertised as
59
- Popover-compatible should cede the hidden state the same way, and the
60
- opacity target is what actually makes the native exit fade — see the
61
- overlay note below. */
62
- .tooltip[popover]:not(:popover-open) {
63
- display: none;
64
- opacity: 0;
65
- }
49
+ One rule for both lifecycles: the runtime writes popover="manual" on the
50
+ tips it manages, and an application driving a .tooltip[popover] itself lands
51
+ on the same declarations. Who owns which is in
52
+ docs/pages/enhancements/overview.md. */
53
+ .tooltip[popover] {
54
+ inset: auto;
55
+ margin: 0;
56
+ overflow: visible;
66
57
  }
67
58
 
59
+ /* The closed state, for either lifecycle. .tooltip declares no display of its
60
+ own so the UA would already hide it, but the opacity target is what makes
61
+ the exit fade at all — see the overlay note below. */
62
+ .tooltip[popover]:not(:popover-open) {
63
+ display: none;
64
+ opacity: 0;
65
+ }
66
+
67
+ /* The state before enhancement, and the author's to write — not the runtime's.
68
+ Wiring is lazy: tooltip.js adopts an explicit tip on its trigger's first
69
+ hover, so a tooltip nobody hovers keeps [hidden] for the whole life of the
70
+ page. Without this rule it would sit on screen at full opacity until then.
71
+ The runtime removes the attribute at the moment it takes the lifecycle over,
72
+ because an author declaration outranks the platform's own hidden state. */
68
73
  .tooltip[hidden] {
69
74
  display: none;
70
75
  opacity: 0;
71
76
  }
72
77
 
73
- /* Entry opacity for both lifecycles: a runtime-driven tip is visible when it
74
- is neither a popover nor [hidden], a native one when it is :popover-open.
75
- compat-ok: [popover] appears here only under negation, which every engine
76
- evaluates correctly whether or not it implements Popover, :popover-open
77
- rides in a forgiving :is(), and @starting-style is itself unsupported across
78
- part of the Minimal range — so the rule does not exist there at all. */
78
+ /* Entry opacity, for either lifecycle — both are :popover-open.
79
+ @starting-style is above the floor (Safari 17.5); where it is missing the
80
+ tip simply appears at full opacity. */
79
81
  @starting-style {
80
- .tooltip:is(:not([popover]):not([hidden]), :popover-open) {
82
+ .tooltip:popover-open {
81
83
  opacity: 0;
82
84
  }
83
85
  }
@@ -135,9 +137,3 @@
135
137
  translate: 0 -50%;
136
138
  clip-path: polygon(0 50%, 100% 0, 100% 100%);
137
139
  }
138
-
139
- @media print {
140
- .tooltip {
141
- display: none !important;
142
- }
143
- }
@@ -6,6 +6,7 @@
6
6
  *
7
7
  * Public hooks:
8
8
  * --link
9
+ * --hr-space
9
10
  */
10
11
 
11
12
  body {
@@ -47,10 +48,23 @@ hgroup > * {
47
48
  margin-block: 0;
48
49
  }
49
50
 
51
+ /* Section break.
52
+
53
+ margin-inline: 0 overrides the UA's `margin: 0.5em auto`. In block flow the
54
+ auto margins resolve to zero and nothing changes, but as a flex or grid item
55
+ an inline auto margin replaces the stretch — an <hr> in a .stack computed to
56
+ width 0 and vanished. .stack deliberately leaves the inline axis free, so
57
+ the reset belongs here.
58
+
59
+ --hr-space tunes the block rhythm on the instance: a rule closing a page
60
+ section and a rule separating two rows in a panel are the same element at
61
+ very different scales. It applies to an hr in document flow; inside .stack
62
+ or .prose the parent owns the spacing between siblings and wins. */
50
63
  hr {
51
64
  border: 0;
52
65
  border-block-start: var(--border-width) solid var(--border);
53
- margin-block: var(--space-60);
66
+ margin-block: var(--hr-space, var(--space-60));
67
+ margin-inline: 0;
54
68
  }
55
69
 
56
70
  code,
@@ -60,6 +74,14 @@ pre {
60
74
  font-family: var(--font-mono);
61
75
  }
62
76
 
77
+ /* Overflow containment only. One unbreakable line — a URL, a minified token, a
78
+ wide ASCII table — otherwise widens the whole page, and an author cannot see
79
+ it coming from the markup. Background, padding and radius stay out: those
80
+ belong to .prose or to an application code-block component. */
81
+ pre {
82
+ overflow-x: auto;
83
+ }
84
+
63
85
  small {
64
86
  font-size: 0.875em;
65
87
  }
@@ -1,35 +1,46 @@
1
1
  /*
2
2
  * Intent classes do not style elements directly.
3
- * They only expose --intent and --intent-fg.
4
- * Components and variants decide how to consume them. Each component that
5
- * consumes --intent or --ui-* opens a local intent boundary in a :where()
6
- * block that resets exactly the tokens it consumes to initial. Parent intents
7
- * do not leak into nested components. Local intent classes (.primary, .danger,
8
- * etc.) still apply because they have higher specificity than :where().
3
+ * They only expose --intent, --intent-fg, and the optional per-intent soft
4
+ * foreground --intent-soft-fg, relayed from the per-role --*-soft-fg hooks.
5
+ * The default theme declares those hooks for its soft ink (see theme.css);
6
+ * a theme without them lets the relay resolve to an invalid value, so the soft
7
+ * recipe falls back to its --soft-fg-mix derivation and a theme opts in one
8
+ * role at a time (docs/pages/foundations/tokens.md). Components and variants
9
+ * decide how to consume them. Each component that consumes --intent,
10
+ * --intent-soft-fg or --ui-* opens a local intent boundary in a :where() block
11
+ * that resets exactly the tokens it consumes to initial. Parent intents do not
12
+ * leak into nested components. Local intent classes (.primary, .danger, etc.)
13
+ * still apply because they have higher specificity than :where().
9
14
  */
10
15
  .primary {
11
16
  --intent: var(--primary);
12
17
  --intent-fg: var(--primary-fg);
18
+ --intent-soft-fg: var(--primary-soft-fg);
13
19
  }
14
20
  .secondary {
15
21
  --intent: var(--secondary);
16
22
  --intent-fg: var(--secondary-fg);
23
+ --intent-soft-fg: var(--secondary-soft-fg);
17
24
  }
18
25
  .success {
19
26
  --intent: var(--success);
20
27
  --intent-fg: var(--success-fg);
28
+ --intent-soft-fg: var(--success-soft-fg);
21
29
  }
22
30
  .warning {
23
31
  --intent: var(--warning);
24
32
  --intent-fg: var(--warning-fg);
33
+ --intent-soft-fg: var(--warning-soft-fg);
25
34
  }
26
35
  .danger {
27
36
  --intent: var(--danger);
28
37
  --intent-fg: var(--danger-fg);
38
+ --intent-soft-fg: var(--danger-soft-fg);
29
39
  }
30
40
  .neutral {
31
41
  --intent: var(--neutral);
32
42
  --intent-fg: var(--neutral-fg);
43
+ --intent-soft-fg: var(--neutral-soft-fg);
33
44
  }
34
45
 
35
46
  /* Paint an inherited or local intent on a simple foreground element without
@@ -101,6 +101,33 @@
101
101
  --focus-ring-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring);
102
102
  }
103
103
 
104
+ /* Soft foreground hooks — the soft ink is the intent rebated toward the local
105
+ text so it keeps 4.5:1 against the soft hover fill (the resting fill carries
106
+ the extra margin; variants.css soft recipe). Deliberately scoped to the
107
+ default theme: a custom property bakes its var() references where it is
108
+ declared, so these resolve against root/light/dark tokens on every boundary.
109
+ Presets and custom islands keep their --soft-fg-mix / raw-intent ink unless
110
+ they declare hooks themselves — :where() keeps this reset from outranking a
111
+ theme's own declaration (e.g. brutalist's 45% soft-fg-mix). */
112
+ :where([data-theme]:not([data-theme="light"]):not([data-theme="dark"])) {
113
+ --primary-soft-fg: initial;
114
+ --secondary-soft-fg: initial;
115
+ --success-soft-fg: initial;
116
+ --warning-soft-fg: initial;
117
+ --danger-soft-fg: initial;
118
+ --neutral-soft-fg: initial;
119
+ }
120
+
121
+ :root,
122
+ [data-theme="light"],
123
+ [data-theme="dark"] {
124
+ --secondary-soft-fg: color-mix(in oklab, var(--text) 10%, var(--secondary) 90%);
125
+ --success-soft-fg: color-mix(in oklab, var(--text) 15%, var(--success) 85%);
126
+ --warning-soft-fg: color-mix(in oklab, var(--text) 25%, var(--warning) 75%);
127
+ --danger-soft-fg: color-mix(in oklab, var(--text) 15%, var(--danger) 85%);
128
+ --neutral-soft-fg: color-mix(in oklab, var(--text) 20%, var(--neutral) 80%);
129
+ }
130
+
104
131
  /* Native platform controls (checkbox, radio, range, progress) that are not
105
132
  restyled by a component class still follow the theme accent. Declared on
106
133
  every island: accent-color inherits as a computed value, so islands must