actual-css 0.7.0 → 0.9.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 (144) hide show
  1. package/CHANGELOG.md +217 -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 +1087 -643
  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 +85 -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 +21 -6
  30. package/docs/pages/components/drawer.md +13 -0
  31. package/docs/pages/components/fab.md +4 -3
  32. package/docs/pages/components/list.md +42 -4
  33. package/docs/pages/components/navbar.md +17 -1
  34. package/docs/pages/components/pagination.md +2 -2
  35. package/docs/pages/components/skeleton.md +3 -1
  36. package/docs/pages/components/steps.md +19 -0
  37. package/docs/pages/components/table.md +3 -2
  38. package/docs/pages/enhancements/flyout.md +82 -38
  39. package/docs/pages/enhancements/overview.md +25 -0
  40. package/docs/pages/enhancements/status-bar.md +1 -1
  41. package/docs/pages/enhancements/tabs.md +45 -37
  42. package/docs/pages/enhancements/tooltip.md +36 -10
  43. package/docs/pages/examples/overview.md +38 -5
  44. package/docs/pages/forms/choice-cards.md +11 -6
  45. package/docs/pages/forms/combobox.md +1 -1
  46. package/docs/pages/forms/floating-field.md +4 -3
  47. package/docs/pages/forms/joined-controls.md +22 -0
  48. package/docs/pages/forms/otp.md +2 -1
  49. package/docs/pages/forms/overview.md +1 -1
  50. package/docs/pages/forms/states-and-sizes.md +16 -18
  51. package/docs/pages/forms/sticky-actions.md +2 -2
  52. package/docs/pages/forms/switch.md +1 -1
  53. package/docs/pages/foundations/philosophy.md +57 -26
  54. package/docs/pages/foundations/theming.md +2 -2
  55. package/docs/pages/foundations/tokens.md +118 -21
  56. package/docs/pages/foundations/typography.md +35 -14
  57. package/docs/pages/guides/introduction.md +11 -9
  58. package/docs/pages/guides/progressive-enhancement.md +115 -30
  59. package/docs/pages/guides/tailwind.md +3 -2
  60. package/docs/pages/layout/media.md +1 -1
  61. package/docs/pages/layout/overview.md +11 -1
  62. package/docs/pages/layout/scroll-snap.md +3 -3
  63. package/docs/pages/layout/stack.md +17 -9
  64. package/docs/pages/patterns/icon-input.md +4 -2
  65. package/docs/pages/patterns/icon-slot.md +2 -0
  66. package/docs/pages/patterns/intent-nesting.md +1 -1
  67. package/docs/pages/utilities/overview.md +23 -2
  68. package/docs/pages/utilities/sizing-wrapping.md +1 -1
  69. package/docs/pages/utilities/spacing.md +10 -8
  70. package/llms.txt +2 -1
  71. package/package.json +159 -149
  72. package/{scripts/reserved-classes.json → reserved-classes.json} +4 -6
  73. package/src/css/components/accordion.css +42 -3
  74. package/src/css/components/alert.css +25 -13
  75. package/src/css/components/app-nav.css +18 -8
  76. package/src/css/components/avatar.css +13 -1
  77. package/src/css/components/badge.css +78 -12
  78. package/src/css/components/breadcrumb.css +2 -3
  79. package/src/css/components/button.css +9 -16
  80. package/src/css/components/card.css +9 -2
  81. package/src/css/components/chat.css +1 -0
  82. package/src/css/components/data-list.css +1 -1
  83. package/src/css/components/drawer.css +101 -32
  84. package/src/css/components/fab.css +0 -6
  85. package/src/css/components/flyout.css +35 -98
  86. package/src/css/components/index.css +0 -1
  87. package/src/css/components/indicator.css +21 -0
  88. package/src/css/components/key.css +10 -0
  89. package/src/css/components/list.css +40 -4
  90. package/src/css/components/menu.css +2 -1
  91. package/src/css/components/modal.css +48 -33
  92. package/src/css/components/navbar.css +37 -4
  93. package/src/css/components/pagination.css +1 -0
  94. package/src/css/components/skeleton.css +15 -4
  95. package/src/css/components/spinner.css +6 -6
  96. package/src/css/components/status-bar.css +0 -5
  97. package/src/css/components/steps.css +45 -59
  98. package/src/css/components/tab.css +29 -9
  99. package/src/css/components/table.css +13 -5
  100. package/src/css/components/tooltip.css +36 -40
  101. package/src/css/core/base.css +23 -1
  102. package/src/css/core/intents.css +17 -6
  103. package/src/css/core/theme.css +27 -0
  104. package/src/css/core/tokens.css +35 -15
  105. package/src/css/core/variants.css +64 -15
  106. package/src/css/forms/choice-card.css +4 -4
  107. package/src/css/forms/choice.css +76 -29
  108. package/src/css/forms/control.css +2 -1
  109. package/src/css/forms/floating-field.css +3 -3
  110. package/src/css/forms/form.css +4 -3
  111. package/src/css/forms/input-icon.css +20 -14
  112. package/src/css/forms/native.css +6 -1
  113. package/src/css/forms/otp.css +9 -1
  114. package/src/css/forms/range.css +2 -0
  115. package/src/css/forms/select.css +29 -9
  116. package/src/css/forms/switch.css +28 -8
  117. package/src/css/layout/app-layout.css +1 -1
  118. package/src/css/layout/grid.css +50 -76
  119. package/src/css/layout/scroll-snap.css +11 -7
  120. package/src/css/layout/scroller.css +3 -4
  121. package/src/css/layout/stack.css +2 -2
  122. package/src/css/typography/fluid.css +4 -3
  123. package/src/css/typography/prose.css +6 -5
  124. package/src/css/utilities/base.css +2 -1
  125. package/src/css/utilities/extra.css +20 -17
  126. package/src/js/context-menu.js +27 -26
  127. package/src/js/dialog.js +2 -1
  128. package/src/js/dismiss.js +5 -0
  129. package/src/js/enhance.js +34 -2
  130. package/src/js/enhancement-loader.js +36 -43
  131. package/src/js/events.js +0 -3
  132. package/src/js/filter.js +0 -1
  133. package/src/js/floating-space.js +42 -0
  134. package/src/js/flyout.js +9 -44
  135. package/src/js/focus-group.js +7 -0
  136. package/src/js/focus.js +6 -0
  137. package/src/js/menu.js +10 -1
  138. package/src/js/selectors.js +5 -9
  139. package/src/js/status.js +6 -5
  140. package/src/js/surface.js +97 -139
  141. package/src/js/tab.js +6 -1
  142. package/src/js/tooltip.js +150 -51
  143. package/src/js/validation.js +1 -1
  144. package/src/css/components/surface.css +0 -49
@@ -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;
@@ -9,6 +9,14 @@
9
9
  * --table-cell-pad
10
10
  */
11
11
  .table-wrap {
12
+ /* Containing block for anything absolutely positioned inside the table —
13
+ `.sr-only` on a column header above all. Without it that box resolves
14
+ against the initial containing block, so it keeps the static position of a
15
+ cell that may be a thousand pixels along the scrolled row, escapes this
16
+ scroller, and extends the *document's* scrollable width. The page then
17
+ scrolls sideways on a phone with nothing painted out there. */
18
+ position: relative;
19
+
12
20
  inline-size: 100%;
13
21
  max-inline-size: 100%;
14
22
  min-inline-size: 0;
@@ -43,13 +51,13 @@
43
51
  vertical-align: baseline;
44
52
  }
45
53
 
46
- .table thead th {
47
- vertical-align: bottom;
48
- }
49
-
50
54
  .table th {
51
55
  color: var(--text-muted);
52
- font-weight: var(--font-weight-strong);
56
+ font-weight: var(--font-weight-semibold);
57
+ }
58
+
59
+ .table thead th {
60
+ vertical-align: bottom;
53
61
  }
54
62
 
55
63
  .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
@@ -22,9 +22,9 @@
22
22
  --line-height: 1.5;
23
23
  --line-height-tight: 1.25;
24
24
  --line-height-relaxed: 1.75;
25
- --font-weight: 400;
25
+ --font-weight-normal: 400;
26
26
  --font-weight-medium: 500;
27
- --font-weight-strong: 650;
27
+ --font-weight-semibold: 600;
28
28
  --font-weight-bold: 700;
29
29
 
30
30
  /* Typography scale */
@@ -39,7 +39,6 @@
39
39
  applications, and no future framework step can collide with them. */
40
40
  --gap: 0.75rem;
41
41
  --density-space: var(--space-40);
42
- --density-compact-size: 1.5rem;
43
42
  --space-10: 0.25rem;
44
43
  --space-20: 0.5rem;
45
44
  --space-30: 0.75rem;
@@ -65,7 +64,10 @@
65
64
  beyond this vocabulary. Toggles that share one transition list at
66
65
  --duration-fast (flyout, tooltip) and pure-opacity backdrops stay on
67
66
  `ease`: at 100ms the curve has almost no resolving time, and a scrim is
68
- not traveling anywhere. */
67
+ not traveling anywhere. Animation policy: reset.css is the global safety
68
+ net; every meaningful @keyframes is explicitly gated behind
69
+ prefers-reduced-motion: no-preference; ordinary transitions rely on the
70
+ reset alone. */
69
71
  --duration: 150ms;
70
72
  --duration-fast: 100ms;
71
73
  --duration-slow: 200ms;
@@ -96,20 +98,29 @@
96
98
  --bar-height: 0.65rem;
97
99
 
98
100
  /* Controls — inputs and buttons share --control-size so heights always match.
99
- .sm/.lg (variants.css) switch --control-size; --control-font-size stays
100
- at its baseline because density does not change typography.
101
+ .compact/.spacious (variants.css) switch geometry without changing
102
+ --control-font-size. Local .sm/.lg component sizes switch all three.
101
103
 
102
104
  Inline choices (checkbox, radio, switch) are typographic, not field-sized:
103
105
  they sit on a line of text, so they follow --control-font-size through
104
106
  --choice-control-size instead of --control-size. Being an em value in
105
107
  :root is deliberate — an unregistered custom property resolves its units
106
108
  at the element that consumes it, so one declaration tracks whatever font
107
- size the choice actually renders at. */
109
+ size the choice actually renders at.
110
+
111
+ --control-pad-x is the opposite kind of token: a resolved geometry length,
112
+ not an em ratio. Every part of one control must see the same horizontal
113
+ distance, so it is stated in the typographic tokens rather than an em
114
+ whose value would change on a descendant with its own font size (the
115
+ input-icon glyph, made larger by --input-icon-size). It mirrors
116
+ --control-font-size and is rebound beside it at the .sm/.lg boundaries
117
+ (variants.css); declaring it only in :root would freeze the md value,
118
+ because a var() inside a custom property resolves where it is declared. */
108
119
  --control-size-sm: 2rem;
109
120
  --control-size-md: 2.375rem;
110
121
  --control-size-lg: 2.75rem;
111
122
  --control-size: var(--control-size-md);
112
- --control-pad-x: 1em;
123
+ --control-pad-x: var(--font-size-md);
113
124
  --control-font-size: var(--font-size-md);
114
125
  --choice-control-size: 1.125em;
115
126
  --disabled-opacity: 0.65;
@@ -142,7 +153,10 @@
142
153
  --soft-hover-alpha: 12%;
143
154
  /* Soft foreground: the share of raw intent in soft text. 100% keeps the
144
155
  pure intent color; lower values rebate it toward --text, which vivid or
145
- light intent palettes need to stay legible on their own soft surface. */
156
+ light intent palettes need to stay legible on their own soft surface.
157
+ The default palette ships per-role --*-soft-fg hooks calibrated against
158
+ the hovered soft fill instead (theme.css); this mix is the fallback a
159
+ theme without hooks falls back to. */
146
160
  --soft-fg-mix: 100%;
147
161
 
148
162
  /* Shared icon assets. Override these URLs when a strict CSP disallows data:. */
@@ -158,13 +172,14 @@
158
172
  --viewport-block: 100vh;
159
173
  --viewport-inline: 100vw;
160
174
 
161
- /* Z-index scale — used ONLY by non-dialog overlays (see the layering
162
- note below for the full model). Actual CSS reserves multiples of 10 for
163
- framework layers; applications may use intermediate values for local
164
- layering. */
175
+ /* Z-index scale — orders document-layer components only. Native top-layer
176
+ elements, dialogs and popovers alike, do not participate in this ordering:
177
+ they always paint above the document, and their order among themselves is
178
+ their top-layer promotion order. No z-index value here can reach them.
179
+ Actual CSS reserves multiples of 10 for framework layers; applications may
180
+ use intermediate values for local layering. */
165
181
  --z-sticky: 10;
166
182
  --z-menu: 20;
167
- --z-tooltip: 50;
168
183
  --z-status: 60;
169
184
  }
170
185
 
@@ -199,7 +214,12 @@
199
214
  --shadow-popout: 0 0.25rem 0.75rem color-mix(in oklch, var(--shadow-color) 15%, transparent);
200
215
  }
201
216
 
202
- :root {
217
+ /* Theme-derived like the shadows above, so it is declared on the same
218
+ boundaries: an alias resolved only on :root is inherited already composed,
219
+ and a [data-theme] island overriding --backdrop-color or
220
+ --backdrop-opacity would not change the scrim of a dialog inside it. */
221
+ :root,
222
+ [data-theme] {
203
223
  --backdrop-fill: color-mix(
204
224
  in oklch,
205
225
  var(--backdrop-color) calc(var(--backdrop-opacity) * 100%),
@@ -15,16 +15,15 @@
15
15
  * components/badge.css — .badge default soft behind explicit --ui-*
16
16
  * components/alert.css — .alert default soft behind explicit --ui-*
17
17
  *
18
- * .sm/.lg set shared density tokens: spacing rhythm and geometry only.
19
- * Density never changes typography — controls keep their font-size and
20
- * content components keep theirs. Controls consume --control-size; non-control
21
- * components may consume --density-* when the density context should affect
22
- * their local rhythm. Compact components may consume --density-compact-size.
23
- * Components with bespoke sizing (spinner, avatar) define local .sm/.lg;
24
- * applying .sm/.lg to card, dialog, accordion, navbar, table, progress, meter
25
- * is a no-op. Progress and meter are deliberate: a bar keeps its inline size
26
- * and only changes thickness, which --bar-height expresses better than a
27
- * three-step scale.
18
+ * .compact/.spacious set inherited density tokens: spacing rhythm and geometry
19
+ * only. Density never changes typography or icon size. Controls consume
20
+ * --control-size; non-control components may consume --density-* when the
21
+ * context should affect their local rhythm.
22
+ *
23
+ * .sm/.lg are local size roles, not shared measurements. Shared control
24
+ * participants below map both typography and geometry; components with
25
+ * bespoke geometry (badge, spinner, avatar, rating, key, prose) own those
26
+ * mappings locally. Components without an explicit mapping do not participate.
28
27
  *
29
28
  * Intent list source of truth: intents.css. Components consume --intent* tokens
30
29
  * instead of re-listing intent classes.
@@ -80,17 +79,64 @@
80
79
  border-color: var(--ui-border);
81
80
  }
82
81
 
83
- .sm {
82
+ .compact {
84
83
  --gap: var(--space-20);
85
84
  --density-space: var(--space-20);
86
85
  --control-size: var(--control-size-sm);
87
- --density-compact-size: 1.25rem;
88
86
  }
89
- .lg {
87
+
88
+ .spacious {
90
89
  --gap: var(--space-50);
91
90
  --density-space: var(--space-50);
92
91
  --control-size: var(--control-size-lg);
93
- --density-compact-size: 1.875rem;
92
+ }
93
+
94
+ /* Shared control scale. Wrappers are listed when their descendants form one
95
+ component (field, input-icon, join, pagination, tabs, flyout). A local size
96
+ declaration beats inherited density because it is declared on the sized
97
+ component itself. */
98
+ :where(
99
+ .btn,
100
+ .input,
101
+ .textarea,
102
+ .select,
103
+ .color,
104
+ .file,
105
+ .field,
106
+ .choice,
107
+ .input-icon,
108
+ .otp,
109
+ .join,
110
+ .pagination,
111
+ .tabs,
112
+ .tab,
113
+ .flyout
114
+ ).sm {
115
+ --control-size: var(--control-size-sm);
116
+ --control-font-size: var(--font-size-sm);
117
+ --control-pad-x: var(--font-size-sm);
118
+ }
119
+
120
+ :where(
121
+ .btn,
122
+ .input,
123
+ .textarea,
124
+ .select,
125
+ .color,
126
+ .file,
127
+ .field,
128
+ .choice,
129
+ .input-icon,
130
+ .otp,
131
+ .join,
132
+ .pagination,
133
+ .tabs,
134
+ .tab,
135
+ .flyout
136
+ ).lg {
137
+ --control-size: var(--control-size-lg);
138
+ --control-font-size: var(--font-size-lg);
139
+ --control-pad-x: var(--font-size-lg);
94
140
  }
95
141
 
96
142
  /* Modern color enhancement. Baseline variant values above remain usable
@@ -104,7 +150,10 @@
104
150
  var(--surface) var(--soft-border-mix),
105
151
  var(--intent, var(--neutral))
106
152
  );
107
- --ui-fg: color-mix(in oklab, var(--text), var(--intent, var(--text)) var(--soft-fg-mix));
153
+ --ui-fg: var(
154
+ --intent-soft-fg,
155
+ color-mix(in oklab, var(--text), var(--intent, var(--text)) var(--soft-fg-mix))
156
+ );
108
157
  /* @sync soft-recipe */
109
158
  }
110
159
 
@@ -79,13 +79,13 @@
79
79
 
80
80
  /* Checked border — shared by radio and checkbox */
81
81
  .choice-card:has(:checked) {
82
- --choice-card-border: var(--intent, var(--primary));
82
+ --choice-card-border: var(--intent, var(--state-selected));
83
83
  }
84
84
 
85
85
  /* Subtle background tint on checked — modern browsers only */
86
86
  @supports (color: color-mix(in oklch, red, white)) {
87
87
  .choice-card:has(:checked) {
88
- --choice-card-bg: color-mix(in oklab, var(--surface) 96%, var(--intent, var(--primary)));
88
+ --choice-card-bg: color-mix(in oklab, var(--surface) 96%, var(--intent, var(--state-selected)));
89
89
  }
90
90
  }
91
91
 
@@ -105,7 +105,7 @@
105
105
 
106
106
  /* Badge disc — circle for radio, rounded square for checkbox */
107
107
  .choice-card::before {
108
- background: var(--intent, var(--primary));
108
+ background: var(--intent, var(--state-selected));
109
109
  box-shadow: 0 0 0 2px var(--surface);
110
110
  z-index: 1;
111
111
  }
@@ -120,7 +120,7 @@
120
120
 
121
121
  /* Check mark */
122
122
  .choice-card::after {
123
- background: var(--intent-fg, var(--primary-fg));
123
+ background: var(--intent-fg, var(--state-selected-fg));
124
124
  -webkit-mask: var(--icon-check) center / 60% no-repeat;
125
125
  mask: var(--icon-check) center / 60% no-repeat;
126
126
  z-index: 2;