actual-css 0.9.0 → 0.9.1

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 (84) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/components.json +2 -0
  3. package/dist/actual.css +10 -3
  4. package/dist/actual.full.css +238 -71
  5. package/dist/actual.full.js +1 -1
  6. package/dist/actual.full.min.css +1 -1
  7. package/dist/actual.min.css +1 -1
  8. package/docs/pages/foundations/tokens.md +1 -1
  9. package/docs/pages/guides/progressive-enhancement.md +12 -8
  10. package/package.json +159 -159
  11. package/src/css/components/accordion.css +2 -0
  12. package/src/css/components/actions.css +2 -0
  13. package/src/css/components/alert.css +2 -0
  14. package/src/css/components/app-nav.css +2 -0
  15. package/src/css/components/avatar.css +2 -0
  16. package/src/css/components/badge.css +2 -0
  17. package/src/css/components/breadcrumb.css +2 -0
  18. package/src/css/components/busy.css +2 -0
  19. package/src/css/components/button.css +2 -0
  20. package/src/css/components/card.css +2 -0
  21. package/src/css/components/chat.css +2 -0
  22. package/src/css/components/data-list.css +2 -0
  23. package/src/css/components/drawer.css +35 -22
  24. package/src/css/components/fab.css +2 -0
  25. package/src/css/components/flyout.css +2 -0
  26. package/src/css/components/indicator.css +2 -0
  27. package/src/css/components/join.css +2 -0
  28. package/src/css/components/key.css +2 -0
  29. package/src/css/components/list.css +2 -0
  30. package/src/css/components/meter.css +2 -0
  31. package/src/css/components/modal.css +47 -40
  32. package/src/css/components/navbar.css +2 -0
  33. package/src/css/components/pagination.css +2 -0
  34. package/src/css/components/progress.css +2 -0
  35. package/src/css/components/rating.css +2 -0
  36. package/src/css/components/scrollspy.css +2 -0
  37. package/src/css/components/skeleton.css +2 -0
  38. package/src/css/components/spinner.css +2 -0
  39. package/src/css/components/status-bar.css +2 -0
  40. package/src/css/components/steps.css +2 -0
  41. package/src/css/components/tab.css +2 -0
  42. package/src/css/components/table.css +2 -0
  43. package/src/css/components/tooltip.css +2 -0
  44. package/src/css/core/print.css +3 -0
  45. package/src/css/core/variants.css +7 -3
  46. package/src/css/effects/aura.css +2 -0
  47. package/src/css/forms/choice-card.css +4 -0
  48. package/src/css/forms/control.css +2 -0
  49. package/src/css/forms/floating-field.css +5 -1
  50. package/src/css/forms/form-actions.css +2 -0
  51. package/src/css/forms/form.css +2 -0
  52. package/src/css/forms/input-icon.css +2 -0
  53. package/src/css/forms/native.css +2 -0
  54. package/src/css/forms/otp.css +10 -0
  55. package/src/css/forms/range.css +2 -0
  56. package/src/css/forms/select.css +2 -0
  57. package/src/css/forms/switch.css +2 -0
  58. package/src/css/forms/validation.css +2 -0
  59. package/src/css/layout/app-layout.css +2 -0
  60. package/src/css/layout/app-shell.css +5 -1
  61. package/src/css/layout/center.css +2 -0
  62. package/src/css/layout/cluster.css +2 -0
  63. package/src/css/layout/column-layout.css +2 -0
  64. package/src/css/layout/frame.css +2 -0
  65. package/src/css/layout/grid.css +2 -0
  66. package/src/css/layout/media.css +2 -1
  67. package/src/css/layout/scroll-snap.css +2 -0
  68. package/src/css/layout/scroll-target.css +2 -0
  69. package/src/css/layout/sidebar-layout.css +2 -0
  70. package/src/css/layout/stack.css +2 -1
  71. package/src/css/layout/switcher.css +2 -0
  72. package/src/css/layout/topbar.css +2 -0
  73. package/src/css/typography/fluid.css +2 -0
  74. package/src/css/typography/lead.css +2 -1
  75. package/src/css/typography/overline.css +2 -0
  76. package/src/css/typography/prose.css +2 -0
  77. package/src/css/utilities/base.css +6 -1
  78. package/src/js/context-menu.js +56 -15
  79. package/src/js/enhancement-loader.js +5 -1
  80. package/src/js/escape.js +11 -1
  81. package/src/js/flyout.js +6 -1
  82. package/src/js/focus.js +8 -3
  83. package/src/js/surface.js +28 -0
  84. package/src/js/tooltip.js +10 -4
@@ -1,6 +1,12 @@
1
1
  /*
2
2
  * Drawer — modal side-sheet built on the native dialog element.
3
3
  *
4
+ * Docs: docs/pages/components/drawer.md
5
+ *
6
+ * Open with a native command button:
7
+ * command="show-modal" commandfor="<drawer-id>".
8
+ * For stacked navigation, compose with .nav-list from navbar.css.
9
+ *
4
10
  * Top-layer rendering, ::backdrop, Escape, and modal focus come from the
5
11
  * native dialog. CSS owns progressive enter/exit motion where browsers support
6
12
  * discrete display/overlay transitions. The drawer is a layout shell only —
@@ -13,10 +19,11 @@
13
19
  *
14
20
  * Internal:
15
21
  * --drawer-closed-translate (side/RTL positioning state)
22
+ * --drawer-focus-bleed
16
23
  * --drawer-motion-duration (shared by panel and ::backdrop so exit duration
17
24
  * changes in one place; entry state redefines it longer)
18
25
  */
19
- dialog.drawer {
26
+ :where(dialog).drawer {
20
27
  --drawer-size: 20rem;
21
28
  --drawer-pad: var(--space-40);
22
29
  --drawer-closed-translate: -100%;
@@ -59,7 +66,7 @@ dialog.drawer {
59
66
  transform var(--drawer-motion-duration) var(--ease-exit);
60
67
  }
61
68
 
62
- dialog.drawer[open] {
69
+ :where(dialog).drawer[open] {
63
70
  --drawer-motion-duration: var(--duration-slow);
64
71
  display: grid;
65
72
  grid-template-rows: auto minmax(0, 1fr) auto;
@@ -74,7 +81,7 @@ dialog.drawer[open] {
74
81
  /* A form is the drawer's content wrapper when actions need method="dialog".
75
82
  Span the drawer tracks so the form can keep the header at the top, let the
76
83
  body absorb the available height, and keep the footer at the bottom. */
77
- dialog.drawer > form {
84
+ :where(dialog).drawer > form {
78
85
  display: grid;
79
86
  grid-row: 1 / -1;
80
87
  grid-template-rows: auto minmax(0, 1fr) auto;
@@ -83,7 +90,7 @@ dialog.drawer > form {
83
90
  }
84
91
 
85
92
  /* The shared backdrop token keeps drawer and modal scrims consistent. */
86
- dialog.drawer::backdrop {
93
+ :where(dialog).drawer::backdrop {
87
94
  background: var(--backdrop-fill);
88
95
  opacity: 0;
89
96
  /* Scrim curve stays neutral `ease`; only the panel's character curves
@@ -91,7 +98,7 @@ dialog.drawer::backdrop {
91
98
  transition: opacity var(--drawer-motion-duration) ease;
92
99
  }
93
100
 
94
- dialog.drawer[data-side="end"] {
101
+ :where(dialog).drawer[data-side="end"] {
95
102
  --drawer-closed-translate: 100%;
96
103
 
97
104
  margin-inline-start: auto;
@@ -100,17 +107,17 @@ dialog.drawer[data-side="end"] {
100
107
  border-inline-start: var(--border-width) solid var(--border);
101
108
  }
102
109
 
103
- dialog.drawer[open]::backdrop {
110
+ :where(dialog).drawer[open]::backdrop {
104
111
  opacity: 1;
105
112
  }
106
113
 
107
114
  @starting-style {
108
- dialog.drawer[open] {
115
+ :where(dialog).drawer[open] {
109
116
  opacity: 0;
110
117
  transform: translateX(var(--drawer-closed-translate));
111
118
  }
112
119
 
113
- dialog.drawer[open]::backdrop {
120
+ :where(dialog).drawer[open]::backdrop {
114
121
  opacity: 0;
115
122
  }
116
123
  }
@@ -120,15 +127,21 @@ dialog.drawer[open]::backdrop {
120
127
  modal, which sizes to its content and opts in with .scrollable. Header and
121
128
  footer keep their own tracks; .drawer-close is named because a generic
122
129
  exclusion list is easier to trust than its `position: absolute`. */
123
- dialog.drawer > :not(:is(header, footer, form, .drawer-close)),
124
- dialog.drawer > form > :not(:is(header, footer, .drawer-close)) {
130
+ :where(dialog).drawer > :not(:is(header, footer, form, .drawer-close)),
131
+ :where(dialog).drawer > form > :not(:is(header, footer, .drawer-close)) {
132
+ --drawer-focus-bleed: calc(var(--focus-outline-offset) + var(--border-width) * 2);
133
+
125
134
  min-block-size: 0;
135
+ /* Keep focus paint inside the clip without moving the body off the fixed
136
+ header and footer edges. */
137
+ margin-inline: calc(var(--drawer-focus-bleed) * -1);
138
+ padding-inline: var(--drawer-focus-bleed);
126
139
  overflow: auto;
127
140
  overscroll-behavior: contain;
128
141
  }
129
142
 
130
143
  @supports (transition-behavior: allow-discrete) {
131
- dialog.drawer {
144
+ :where(dialog).drawer {
132
145
  transition:
133
146
  opacity var(--drawer-motion-duration) var(--ease-exit),
134
147
  transform var(--drawer-motion-duration) var(--ease-exit),
@@ -136,7 +149,7 @@ dialog.drawer > form > :not(:is(header, footer, .drawer-close)) {
136
149
  display var(--drawer-motion-duration) allow-discrete;
137
150
  }
138
151
 
139
- dialog.drawer[open] {
152
+ :where(dialog).drawer[open] {
140
153
  transition:
141
154
  opacity var(--drawer-motion-duration) var(--ease-enter),
142
155
  transform var(--drawer-motion-duration) var(--ease-enter),
@@ -144,7 +157,7 @@ dialog.drawer > form > :not(:is(header, footer, .drawer-close)) {
144
157
  display var(--drawer-motion-duration) allow-discrete;
145
158
  }
146
159
 
147
- dialog.drawer::backdrop {
160
+ :where(dialog).drawer::backdrop {
148
161
  transition:
149
162
  opacity var(--drawer-motion-duration) ease,
150
163
  overlay var(--drawer-motion-duration) allow-discrete,
@@ -158,40 +171,40 @@ dialog.drawer > form > :not(:is(header, footer, .drawer-close)) {
158
171
  directionality and handles nested direction changes; the explicit ltr rules
159
172
  neutralize an outer RTL fallback selector. Each specificity group keeps the
160
173
  fallback before the :dir() rules. */
161
- [dir="rtl"] dialog.drawer {
174
+ [dir="rtl"] :where(dialog).drawer {
162
175
  --drawer-closed-translate: 100%;
163
176
  }
164
177
 
165
- dialog.drawer:dir(ltr) {
178
+ :where(dialog).drawer:dir(ltr) {
166
179
  --drawer-closed-translate: -100%;
167
180
  }
168
181
 
169
- dialog.drawer:dir(rtl) {
182
+ :where(dialog).drawer:dir(rtl) {
170
183
  --drawer-closed-translate: 100%;
171
184
  }
172
185
 
173
- [dir="rtl"] dialog.drawer[data-side="end"] {
186
+ [dir="rtl"] :where(dialog).drawer[data-side="end"] {
174
187
  --drawer-closed-translate: -100%;
175
188
  }
176
189
 
177
- dialog.drawer[data-side="end"]:dir(ltr) {
190
+ :where(dialog).drawer[data-side="end"]:dir(ltr) {
178
191
  --drawer-closed-translate: 100%;
179
192
  }
180
193
 
181
- dialog.drawer[data-side="end"]:dir(rtl) {
194
+ :where(dialog).drawer[data-side="end"]:dir(rtl) {
182
195
  --drawer-closed-translate: -100%;
183
196
  }
184
197
 
185
198
  @media (prefers-reduced-motion: no-preference) {
186
- dialog.drawer.is-static {
199
+ :where(dialog).drawer.is-static {
187
200
  animation: actual-dialog-static var(--duration-slow) ease;
188
201
  }
189
202
  }
190
203
 
191
204
  /* The header reserves inline room so the title never runs under the close
192
205
  control. */
193
- dialog.drawer > header,
194
- dialog.drawer > form > header {
206
+ :where(dialog).drawer > header,
207
+ :where(dialog).drawer > form > header {
195
208
  padding-inline-end: calc(var(--control-size) + var(--space-30));
196
209
  }
197
210
 
@@ -3,6 +3,8 @@
3
3
  * Button owns the trigger's size, shape, and intent; FAB owns placement and
4
4
  * the speed dial's structural open/close markers (--icon-plus / --icon-close).
5
5
  *
6
+ * Docs: docs/pages/components/fab.md
7
+ *
6
8
  * Public hooks:
7
9
  * --fab-offset
8
10
  * --fab-gap
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Flyout — floating panel for actions and links.
3
3
  *
4
+ * Docs: docs/pages/enhancements/flyout.md
5
+ *
4
6
  * JavaScript promotes the panel to the top layer and chooses viewport or
5
7
  * document coordinates from its anchor. It also owns state (.is-open), focus,
6
8
  * and keyboard behavior. CSS owns the surfaced panel appearance, item styles,
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Indicator — generic corner positioning for overlaid component content.
3
3
  *
4
+ * Docs: docs/pages/components/indicator.md
5
+ *
4
6
  * The container only establishes the containing block. The item owns no
5
7
  * paint, size, or ring so badges and other components keep their own visual
6
8
  * contract.
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Join — visually groups adjacent controls into a single unit.
3
3
  *
4
+ * Docs: docs/pages/forms/joined-controls.md
5
+ *
4
6
  * This is a cross-cutting control primitive, not a forms-only component:
5
7
  * it applies equally to inputs, selects, buttons, and button-like links.
6
8
  *
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Key — keyboard keys and shortcut tokens for UI chrome.
3
3
  *
4
+ * Docs: docs/pages/components/key.md
5
+ *
4
6
  * Use .key outside prose. Plain kbd inside .prose gets the same visual
5
7
  * treatment from prose.css so authored content stays convenient.
6
8
  *
@@ -3,6 +3,8 @@
3
3
  * Content determines the number of lines; the component only owns the row
4
4
  * geometry, dividers, and supporting-text treatment.
5
5
  *
6
+ * Docs: docs/pages/components/list.md
7
+ *
6
8
  * Public hooks:
7
9
  * --list-item-min-size
8
10
  * --list-item-pad-block
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Meter — native and segmented gauges for known-range values.
3
3
  *
4
+ * Docs: docs/pages/components/meter.md
5
+ *
4
6
  * Maps the three standard meter zones (optimum / suboptimum / even-less-good)
5
7
  * to the shared success / warning / danger intent colors.
6
8
  *
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Modal shell styling for native dialog elements.
3
3
  *
4
+ * Docs: docs/pages/components/dialog.md
5
+ *
4
6
  * Spacing, radius, transitions, and focus treatment come from shared tokens.
5
7
  * Opening behavior and intent-specific decisions stay outside this component.
6
8
  *
@@ -12,16 +14,15 @@
12
14
  *
13
15
  * Internal:
14
16
  * --dialog-close-icon-size
17
+ * --dialog-focus-bleed
15
18
  * --dialog-viewport-block
16
19
  * --dialog-viewport-gap-default
17
20
  * --dialog-viewport-gap-2x
18
21
  */
19
- dialog.modal {
20
- /* --dialog-viewport-gap stays fallback-only for the same reason as
21
- --modal-size below: declaring the public hook here would give it
22
- type+class specificity that a single author class cannot outrank. The
23
- responsive default lives in an internal token instead, so a media query
24
- can still move it without closing the hook.
22
+ :where(dialog).modal {
23
+ /* --dialog-viewport-gap stays fallback-only so the responsive default can
24
+ live in an internal token: a media query moves it without closing the
25
+ public hook.
25
26
 
26
27
  calc() multiplication is not in the supported browser range, so the
27
28
  doubled gap is spelled out with an additive calc. */
@@ -35,10 +36,8 @@ dialog.modal {
35
36
  provides the containing block for the absolutely positioned close button. */
36
37
  position: fixed;
37
38
 
38
- /* --modal-size stays fallback-only: declaring it on dialog.modal gives it
39
- type+class specificity, which a single author class cannot outrank, so the
40
- public hook would silently keep this default. Same reason choice.css never
41
- declares --choice-control-nudge. */
39
+ /* The root is :where(dialog).modal (0-1-0), so a single author class can
40
+ outrank these defaults — no fallback-only indirection needed here. */
42
41
  max-inline-size: min(calc(100% - var(--dialog-viewport-gap-2x)), var(--modal-size, 32rem));
43
42
 
44
43
  /* Stable viewport cap for the panel: dvh tracks the dynamic browser toolbar
@@ -68,8 +67,8 @@ dialog.modal {
68
67
  transform var(--duration) var(--ease-exit);
69
68
  }
70
69
 
71
- dialog.modal > form,
72
- dialog.modal > .stack {
70
+ :where(dialog).modal > form,
71
+ :where(dialog).modal > .stack {
73
72
  display: grid;
74
73
  gap: var(--space-40);
75
74
  max-block-size: calc(var(--dialog-viewport-block) - var(--dialog-viewport-gap-2x));
@@ -79,7 +78,7 @@ dialog.modal > .stack {
79
78
  }
80
79
 
81
80
  @supports (block-size: 100svh) {
82
- dialog.modal {
81
+ :where(dialog).modal {
83
82
  --dialog-viewport-block: 100svh;
84
83
  }
85
84
  }
@@ -87,42 +86,50 @@ dialog.modal > .stack {
87
86
  /* Keep the dialog off the viewport edges on small screens. Moves the internal
88
87
  default, not the public hook, so an author override still wins here too. */
89
88
  @media (max-width: 30rem) {
90
- dialog.modal {
89
+ :where(dialog).modal {
91
90
  --dialog-viewport-gap-default: var(--space-30);
92
91
  }
93
92
  }
94
93
 
95
- dialog.modal.scrollable > form,
96
- dialog.modal.scrollable > .stack {
94
+ :where(dialog).modal.scrollable > form,
95
+ :where(dialog).modal.scrollable > .stack {
97
96
  grid-template-rows: auto minmax(0, 1fr) auto;
98
97
  overflow: hidden;
99
98
  }
100
99
 
101
- dialog.modal.scrollable > form > :not(:is(header, footer)),
102
- dialog.modal.scrollable > .stack > :not(:is(header, footer)) {
100
+ :where(dialog).modal.scrollable > form > :not(:is(header, footer)),
101
+ :where(dialog).modal.scrollable > .stack > :not(:is(header, footer)) {
102
+ --dialog-focus-bleed: calc(var(--focus-outline-offset) + var(--border-width) * 2);
103
+
103
104
  min-block-size: 0;
105
+ /* Keep focus paint inside the clip without moving the body off the fixed
106
+ header and footer edges. */
107
+ margin-inline: calc(var(--dialog-focus-bleed) * -1);
108
+ padding-inline: var(--dialog-focus-bleed);
104
109
  overflow: auto;
105
110
  overscroll-behavior: contain;
106
111
  }
107
112
 
108
- dialog.modal header > :first-child,
109
- dialog.modal footer > :first-child {
113
+ /* Anatomy is bounded to the structural slots (direct children of the
114
+ form/.stack body): a header or footer deeper in the content keeps its own
115
+ rhythm and is not restyled as the modal's chrome. */
116
+ :where(dialog).modal > :where(form, .stack) > :is(header, footer) > :first-child {
110
117
  margin-block-start: 0;
111
118
  }
112
119
 
113
- dialog.modal header > :last-child,
114
- dialog.modal footer > :last-child {
120
+ :where(dialog).modal > :where(form, .stack) > :is(header, footer) > :last-child {
115
121
  margin-block-end: 0;
116
122
  }
117
123
 
118
124
  /* The close button is anchored to the dialog at top inline-end, out of the
119
- content flow; headers reserve inline room so titles never run under it. */
120
- dialog.modal > form > header,
121
- dialog.modal > .stack > header {
125
+ content flow; headers reserve inline room so titles never run under it.
126
+ The form/.stack anatomy is structural, so it stays in :where() and never
127
+ lends .stack's specificity to a bare <form>. */
128
+ :where(dialog).modal > :where(form, .stack) > header {
122
129
  padding-inline-end: calc(var(--control-size) + var(--space-30));
123
130
  }
124
131
 
125
- dialog.modal footer {
132
+ :where(dialog).modal > :where(form, .stack) > footer {
126
133
  display: flex;
127
134
  flex-wrap: wrap;
128
135
  justify-content: flex-end;
@@ -132,17 +139,17 @@ dialog.modal footer {
132
139
  /* Confirmation layout — a focused message followed by a visually distinct
133
140
  action band. The media primitive owns icon/content alignment; this variant
134
141
  only removes the standard form inset so both regions can own their padding. */
135
- dialog.modal.dialog-confirmation > form {
142
+ :where(dialog).modal.dialog-confirmation > form {
136
143
  gap: 0;
137
144
  padding: 0;
138
145
  }
139
146
 
140
- dialog.modal.dialog-confirmation > form > .media {
147
+ :where(dialog).modal.dialog-confirmation > form > .media {
141
148
  gap: var(--space-40);
142
149
  padding: var(--space-50);
143
150
  }
144
151
 
145
- dialog.modal.dialog-confirmation > form > footer {
152
+ :where(dialog).modal.dialog-confirmation > form > footer {
146
153
  padding: var(--space-40) var(--space-50);
147
154
  border-end-start-radius: calc(var(--radius-lg) - var(--border-width));
148
155
  border-end-end-radius: calc(var(--radius-lg) - var(--border-width));
@@ -183,13 +190,13 @@ dialog.modal.dialog-confirmation > form > footer {
183
190
  }
184
191
 
185
192
  @media (max-width: 30rem) {
186
- dialog.modal.dialog-confirmation > form > .media,
187
- dialog.modal.dialog-confirmation > form > footer {
193
+ :where(dialog).modal.dialog-confirmation > form > .media,
194
+ :where(dialog).modal.dialog-confirmation > form > footer {
188
195
  padding: var(--space-40);
189
196
  }
190
197
  }
191
198
 
192
- dialog.modal[open] {
199
+ :where(dialog).modal[open] {
193
200
  opacity: 1;
194
201
  transform: none;
195
202
  transition:
@@ -198,13 +205,13 @@ dialog.modal[open] {
198
205
  }
199
206
 
200
207
  @starting-style {
201
- dialog.modal[open] {
208
+ :where(dialog).modal[open] {
202
209
  opacity: 0;
203
210
  transform: translateY(0.35rem);
204
211
  }
205
212
  }
206
213
 
207
- dialog.modal::backdrop {
214
+ :where(dialog).modal::backdrop {
208
215
  background: var(--backdrop-fill);
209
216
  opacity: 0;
210
217
  /* Scrim curve stays neutral `ease`; only the panel carries the character
@@ -213,7 +220,7 @@ dialog.modal::backdrop {
213
220
  }
214
221
 
215
222
  @supports (transition-behavior: allow-discrete) {
216
- dialog.modal {
223
+ :where(dialog).modal {
217
224
  transition:
218
225
  opacity var(--duration) var(--ease-exit),
219
226
  transform var(--duration) var(--ease-exit),
@@ -221,7 +228,7 @@ dialog.modal::backdrop {
221
228
  display var(--duration) allow-discrete;
222
229
  }
223
230
 
224
- dialog.modal[open] {
231
+ :where(dialog).modal[open] {
225
232
  transition:
226
233
  opacity var(--duration) var(--ease-enter),
227
234
  transform var(--duration) var(--ease-enter),
@@ -229,7 +236,7 @@ dialog.modal::backdrop {
229
236
  display var(--duration) allow-discrete;
230
237
  }
231
238
 
232
- dialog.modal::backdrop {
239
+ :where(dialog).modal::backdrop {
233
240
  transition:
234
241
  opacity var(--duration) ease,
235
242
  overlay var(--duration) allow-discrete,
@@ -237,7 +244,7 @@ dialog.modal::backdrop {
237
244
  }
238
245
  }
239
246
 
240
- dialog.modal[open]::backdrop {
247
+ :where(dialog).modal[open]::backdrop {
241
248
  opacity: 1;
242
249
  }
243
250
 
@@ -250,7 +257,7 @@ html.has-modal-open.had-scrollbar {
250
257
  }
251
258
 
252
259
  @starting-style {
253
- dialog.modal[open]::backdrop {
260
+ :where(dialog).modal[open]::backdrop {
254
261
  opacity: 0;
255
262
  }
256
263
  }
@@ -258,7 +265,7 @@ html.has-modal-open.had-scrollbar {
258
265
  @media (prefers-reduced-motion: no-preference) {
259
266
  /* Wrong-close feedback and view-transition transport are motion quirks, not
260
267
  presence enter/exit, so they stay on the plain CSS default easing. */
261
- dialog.modal.is-static {
268
+ :where(dialog).modal.is-static {
262
269
  animation: actual-dialog-static var(--duration-slow) ease;
263
270
  }
264
271
 
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Navbar — top-level navigation bar.
3
3
  *
4
+ * Docs: docs/pages/components/navbar.md
5
+ *
4
6
  * .navbar is a horizontal bar; .navbar-brand is the brand link; .navbar-nav is
5
7
  * the inline link list using .nav-link items. .nav-list is the vertical nav
6
8
  * list shared with .drawer, fed by the shared --gap token.
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Pagination — page navigation for paginated lists.
3
3
  *
4
+ * Docs: docs/pages/components/pagination.md
5
+ *
4
6
  * Render .btn elements inside a .pagination list. The active page is marked
5
7
  * with aria-current="page"; .btn owns the active state styling.
6
8
  *
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Progress — native progress bar with theme-aware track and value.
3
3
  *
4
+ * Docs: docs/pages/components/progress.md
5
+ *
4
6
  * Consumes the shared --intent/--primary for the value bar and --bar-height
5
7
  * for thickness. Indeterminate state animates a shimmer using --duration-shimmer.
6
8
  *
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Rating — integer star rating drawn on native radio inputs.
3
3
  *
4
+ * Docs: docs/pages/components/rating.md
5
+ *
4
6
  * The checked radio remains the functional baseline. :has() progressively
5
7
  * fills preceding stars without reversing DOM or keyboard order. Forced
6
8
  * colors restores the platform radio presentation.
@@ -2,6 +2,8 @@
2
2
  * Scrollspy — behavior hook for navigation that marks the active section
3
3
  * while the page scrolls.
4
4
  *
5
+ * Docs: docs/pages/enhancements/scrollspy.md
6
+ *
5
7
  * Does not add visual styles by itself. scrollspy.js watches
6
8
  * <nav class="scrollspy">
7
9
  * containing <a href="#section"> links and toggles aria-current="location" on
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Skeleton placeholders for loading states.
3
3
  *
4
+ * Docs: docs/pages/components/skeleton.md
5
+ *
4
6
  * Shows approximate loading structure without implying real content hierarchy.
5
7
  * Geometry, animation, and color stay theme-aware.
6
8
  *
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Spinner — compact loading feedback.
3
3
  *
4
+ * Docs: docs/pages/components/spinner.md
5
+ *
4
6
  * Uses currentColor so it adapts to buttons, alerts, badges, and text. The
5
7
  * [aria-busy] overlay composition lives in components/busy.css.
6
8
  *
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Status bar — singleton floating status area.
3
3
  *
4
+ * Docs: docs/pages/enhancements/status-bar.md
5
+ *
4
6
  * One element in the HTML, empty by default. .is-open is the visual state, set
5
7
  * by status.js like any other surface: it lands after the message is written
6
8
  * and lifts before the bar is cleared, so the bar leaves in the exact state it
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Steps — semantic ordered-list progression for a multi-step process.
3
3
  *
4
+ * Docs: docs/pages/components/steps.md
5
+ *
4
6
  * One primitive, two explicit compositions. Both classes are required:
5
7
  *
6
8
  * <ol class="steps steps-horizontal"> horizontal, 2 to 5 stages
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Tabs — in-place panel switcher with real tab semantics.
3
3
  *
4
+ * Docs: docs/pages/enhancements/tabs.md
5
+ *
4
6
  * JavaScript owns roving tabindex, aria-selected, panel visibility,
5
7
  * and arrow-key navigation. CSS owns the tab strip appearance and
6
8
  * active indicator.
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Table — structured tabular data.
3
3
  *
4
+ * Docs: docs/pages/components/table.md
5
+ *
4
6
  * .table-wrap provides the scroll container and outer chrome; .table owns row
5
7
  * and cell rules. Tokens are sourced from the shared scale.
6
8
  *
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Tooltip — supplemental label overlaid near a trigger.
3
3
  *
4
+ * Docs: docs/pages/enhancements/tooltip.md
5
+ *
4
6
  * Position, placement, and arrow position are managed by JavaScript via left,
5
7
  * top, data-placement, and --arrow-x. The runtime also owns `position`: it
6
8
  * writes `absolute` for a tip whose trigger scrolls with the page, so the
@@ -1,5 +1,8 @@
1
1
  @media print {
2
2
  :root,
3
+ /* The doubled attribute intentionally outranks single-attribute preset
4
+ islands (data-theme="dark", paired themes) loaded after the core. */
5
+ [data-theme][data-theme],
3
6
  body {
4
7
  /* Save some ink in dark mode */
5
8
  color-scheme: light;
@@ -92,9 +92,9 @@
92
92
  }
93
93
 
94
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. */
95
+ component (field, choice-card, floating-field, input-icon, join, pagination,
96
+ tabs, flyout). A local size declaration beats inherited density because it
97
+ is declared on the sized component itself. */
98
98
  :where(
99
99
  .btn,
100
100
  .input,
@@ -104,7 +104,9 @@
104
104
  .file,
105
105
  .field,
106
106
  .choice,
107
+ .choice-card,
107
108
  .input-icon,
109
+ .floating-field,
108
110
  .otp,
109
111
  .join,
110
112
  .pagination,
@@ -126,7 +128,9 @@
126
128
  .file,
127
129
  .field,
128
130
  .choice,
131
+ .choice-card,
129
132
  .input-icon,
133
+ .floating-field,
130
134
  .otp,
131
135
  .join,
132
136
  .pagination,
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Decorative intent-colored frame for one direct child.
3
3
  *
4
+ * Docs: docs/pages/components/aura.md
5
+ *
4
6
  * The effect is built from three layers with distinct roles: a frame carrying
5
7
  * a short accent of light, a small halo that belongs to the frame and keeps
6
8
  * that light attached to the edge, and a wide static halo that belongs to the
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Choice cards — card-style radio and checkbox labels.
3
3
  *
4
+ * Docs: docs/pages/forms/choice-cards.md
5
+ *
4
6
  * Each .choice-card is a <label> wrapping a native radio or checkbox. When
5
7
  * :has() is supported the input is visually hidden and the card mirrors its
6
8
  * state: checked border and tint, checkmark badge (round for radio, rounded
@@ -37,6 +39,8 @@
37
39
  border-radius: var(--choice-card-radius);
38
40
  background: var(--choice-card-bg);
39
41
  color: var(--text);
42
+ /* Consumed so .sm/.lg reach the card; the badge's em sizing follows. */
43
+ font-size: var(--control-font-size);
40
44
  cursor: pointer;
41
45
  transition:
42
46
  border-color var(--duration),