actual-css 0.3.1 → 0.4.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 (203) hide show
  1. package/CHANGELOG.md +67 -3
  2. package/README.md +35 -30
  3. package/dist/actual.css +73 -4946
  4. package/dist/actual.full.css +7090 -0
  5. package/dist/actual.full.js +1 -0
  6. package/dist/actual.full.min.css +1 -0
  7. package/dist/actual.js +1 -1
  8. package/dist/actual.min.css +1 -1
  9. package/docs/design-notes/cascade-layer.md +34 -13
  10. package/docs/design-notes/column-layout.md +677 -0
  11. package/docs/design-notes/forced-colors.md +3 -0
  12. package/docs/design-notes/grid.md +612 -0
  13. package/docs/design-notes/naming.md +15 -0
  14. package/docs/design-notes/widget-primitives.md +1 -0
  15. package/docs/navigation.json +5 -1
  16. package/docs/pages/components/alert.md +16 -14
  17. package/docs/pages/components/aura.md +1 -1
  18. package/docs/pages/components/avatar.md +6 -6
  19. package/docs/pages/components/badge.md +7 -7
  20. package/docs/pages/components/button.md +16 -16
  21. package/docs/pages/components/card.md +13 -9
  22. package/docs/pages/components/chat.md +8 -8
  23. package/docs/pages/components/dialog.md +7 -7
  24. package/docs/pages/components/drawer.md +3 -3
  25. package/docs/pages/components/fab.md +7 -7
  26. package/docs/pages/components/navbar.md +9 -8
  27. package/docs/pages/components/pagination.md +4 -3
  28. package/docs/pages/components/skeleton.md +4 -4
  29. package/docs/pages/components/spinner.md +5 -5
  30. package/docs/pages/components/table.md +30 -5
  31. package/docs/pages/enhancements/flyout.md +13 -13
  32. package/docs/pages/enhancements/overview.md +1 -1
  33. package/docs/pages/enhancements/scrollspy.md +6 -6
  34. package/docs/pages/enhancements/status-bar.md +5 -1
  35. package/docs/pages/enhancements/tabs.md +6 -6
  36. package/docs/pages/enhancements/tooltip.md +1 -1
  37. package/docs/pages/examples/overview.md +39 -22
  38. package/docs/pages/forms/choice-cards.md +11 -6
  39. package/docs/pages/forms/controls.md +1 -1
  40. package/docs/pages/forms/detached-actions.md +1 -1
  41. package/docs/pages/forms/floating-field.md +10 -5
  42. package/docs/pages/forms/form-actions.md +1 -1
  43. package/docs/pages/forms/input-policy.md +1 -1
  44. package/docs/pages/forms/joined-controls.md +4 -4
  45. package/docs/pages/forms/labels.md +6 -6
  46. package/docs/pages/forms/otp.md +1 -1
  47. package/docs/pages/forms/overview.md +19 -19
  48. package/docs/pages/forms/password.md +1 -1
  49. package/docs/pages/forms/range.md +1 -1
  50. package/docs/pages/forms/states-and-sizes.md +41 -45
  51. package/docs/pages/forms/sticky-actions.md +3 -3
  52. package/docs/pages/forms/switch.md +110 -2
  53. package/docs/pages/forms/validation.md +9 -9
  54. package/docs/pages/foundations/philosophy.md +1 -1
  55. package/docs/pages/foundations/theming.md +17 -2
  56. package/docs/pages/foundations/tokens.md +22 -10
  57. package/docs/pages/foundations/typography.md +50 -20
  58. package/docs/pages/guides/bootstrap.md +11 -11
  59. package/docs/pages/guides/integrating-actual-css.md +186 -0
  60. package/docs/pages/guides/introduction.md +23 -26
  61. package/docs/pages/guides/modular-import.md +173 -0
  62. package/docs/pages/guides/progressive-enhancement.md +81 -78
  63. package/docs/pages/guides/tailwind.md +6 -8
  64. package/docs/pages/layout/app-shell.md +1 -1
  65. package/docs/pages/layout/center.md +1 -1
  66. package/docs/pages/layout/choosing.md +39 -0
  67. package/docs/pages/layout/column-layout.md +206 -0
  68. package/docs/pages/layout/frame.md +6 -1
  69. package/docs/pages/layout/grid.md +100 -102
  70. package/docs/pages/layout/list.md +1 -1
  71. package/docs/pages/layout/media.md +1 -1
  72. package/docs/pages/layout/overview.md +15 -2
  73. package/docs/pages/layout/scroll-snap.md +2 -2
  74. package/docs/pages/layout/scroll-target.md +45 -0
  75. package/docs/pages/layout/sidebar-layout.md +20 -19
  76. package/docs/pages/layout/stack.md +53 -1
  77. package/docs/pages/layout/switcher.md +10 -26
  78. package/docs/pages/layout/topbar.md +2 -2
  79. package/docs/pages/patterns/actions.md +1 -1
  80. package/docs/pages/patterns/code-block.md +1 -1
  81. package/docs/pages/patterns/exclusive-accordion.md +1 -1
  82. package/docs/pages/patterns/hierarchical-nav.md +1 -1
  83. package/docs/pages/patterns/icon-input.md +1 -1
  84. package/docs/pages/patterns/intent-nesting.md +1 -1
  85. package/docs/pages/patterns/navigation-list.md +1 -1
  86. package/docs/pages/patterns/overview.md +1 -1
  87. package/docs/pages/patterns/structured-lists.md +1 -1
  88. package/docs/pages/utilities/accessibility.md +3 -3
  89. package/docs/pages/utilities/flex.md +15 -15
  90. package/docs/pages/utilities/links.md +4 -4
  91. package/docs/pages/utilities/list.md +8 -8
  92. package/docs/pages/utilities/overflow.md +8 -8
  93. package/docs/pages/utilities/overview.md +1 -1
  94. package/docs/pages/utilities/shape.md +3 -3
  95. package/docs/pages/utilities/sizing-wrapping.md +23 -16
  96. package/docs/pages/utilities/spacing.md +20 -20
  97. package/docs/pages/utilities/text.md +10 -10
  98. package/llms.txt +46 -4
  99. package/package.json +42 -13
  100. package/scripts/reserved-classes.json +261 -0
  101. package/src/cli/actual-css.js +133 -0
  102. package/src/css/actual.css +1 -31
  103. package/src/css/actual.full.css +11 -2
  104. package/src/css/components/alert.css +17 -4
  105. package/src/css/components/badge.css +18 -6
  106. package/src/css/components/breadcrumb.css +1 -1
  107. package/src/css/components/button.css +19 -1
  108. package/src/css/components/card.css +27 -2
  109. package/src/css/components/drawer.css +27 -2
  110. package/src/css/components/flyout.css +5 -0
  111. package/src/css/components/index.css +32 -27
  112. package/src/css/components/join.css +8 -1
  113. package/src/css/components/key.css +2 -0
  114. package/src/css/components/menu.css +11 -1
  115. package/src/css/components/meter.css +4 -1
  116. package/src/css/components/modal.css +5 -0
  117. package/src/css/components/pagination.css +11 -0
  118. package/src/css/components/spinner.css +5 -0
  119. package/src/css/components/status-bar.css +5 -0
  120. package/src/css/components/surface.css +5 -0
  121. package/src/css/components/table.css +6 -0
  122. package/src/css/components/tooltip.css +5 -0
  123. package/src/css/core/index.css +9 -0
  124. package/src/css/core/print.css +18 -0
  125. package/src/css/{theme.css → core/theme.css} +20 -10
  126. package/src/css/{tokens.css → core/tokens.css} +15 -2
  127. package/src/css/{variants.css → core/variants.css} +3 -2
  128. package/src/css/effects/index.css +2 -0
  129. package/src/css/forms/base.css +8 -0
  130. package/src/css/forms/choice-card.css +5 -4
  131. package/src/css/forms/choice.css +49 -37
  132. package/src/css/forms/control.css +4 -1
  133. package/src/css/forms/custom-select.css +2 -0
  134. package/src/css/{optional → forms}/floating-field.css +25 -7
  135. package/src/css/forms/index.css +10 -13
  136. package/src/css/forms/native.css +15 -2
  137. package/src/css/{optional → forms}/otp.css +20 -16
  138. package/src/css/forms/switch.css +31 -29
  139. package/src/css/layout/app-shell.css +6 -0
  140. package/src/css/layout/center.css +13 -0
  141. package/src/css/layout/cluster.css +21 -0
  142. package/src/css/layout/column-layout.css +144 -0
  143. package/src/css/layout/container-query.css +13 -0
  144. package/src/css/layout/frame.css +19 -0
  145. package/src/css/layout/grid.css +141 -0
  146. package/src/css/layout/index.css +42 -0
  147. package/src/css/layout/measure.css +9 -0
  148. package/src/css/layout/media.css +10 -0
  149. package/src/css/layout/scroll-target.css +15 -0
  150. package/src/css/layout/scroller.css +41 -0
  151. package/src/css/layout/sidebar-layout.css +31 -0
  152. package/src/css/layout/stack.css +21 -0
  153. package/src/css/layout/switcher.css +23 -0
  154. package/src/css/{optional/layout-extra.css → layout/topbar.css} +0 -20
  155. package/src/css/typography/index.css +5 -0
  156. package/src/css/typography/lead.css +7 -0
  157. package/src/css/{components → typography}/overline.css +2 -2
  158. package/src/css/{prose.css → typography/prose.css} +53 -38
  159. package/src/css/{utilities.css → utilities/base.css} +2 -0
  160. package/src/css/utilities/index.css +3 -0
  161. package/src/js/escape.js +36 -0
  162. package/src/js/flyout.js +22 -12
  163. package/src/js/full.js +26 -0
  164. package/src/js/index.js +5 -21
  165. package/src/js/surface.js +1 -31
  166. package/src/js/tooltip.js +46 -18
  167. package/src/js/validation.js +1 -1
  168. package/src/tooling/css-bundle.js +308 -0
  169. package/dist/actual-themes.min.css +0 -1
  170. package/dist/optional.css +0 -1048
  171. package/dist/optional.min.css +0 -1
  172. package/docs/pages/guides/optional-components.md +0 -33
  173. package/src/css/grid.css +0 -109
  174. package/src/css/layout.css +0 -123
  175. package/src/css/optional/index.css +0 -10
  176. package/src/css/optional/scroller.css +0 -63
  177. package/src/css/print.css +0 -47
  178. package/src/css/themes/corporate.css +0 -60
  179. package/src/css/themes/cyberpunk.css +0 -60
  180. package/src/css/themes/dim.css +0 -33
  181. package/src/css/themes/edge.css +0 -117
  182. package/src/css/themes/forest.css +0 -65
  183. package/src/css/themes/gradient.css +0 -97
  184. package/src/css/themes/index.css +0 -47
  185. package/src/css/themes/indigo.css +0 -33
  186. package/src/css/themes/lavender.css +0 -66
  187. package/src/css/themes/mono.css +0 -60
  188. package/src/css/themes/neon.css +0 -66
  189. package/src/css/themes/ocean.css +0 -60
  190. package/src/css/themes/petrol.css +0 -64
  191. package/src/css/themes/spruce.css +0 -64
  192. package/src/css/themes/square.css +0 -65
  193. package/src/css/themes/sunset.css +0 -66
  194. /package/src/css/{optional → components}/chat.css +0 -0
  195. /package/src/css/{optional → components}/fab.css +0 -0
  196. /package/src/css/{base.css → core/base.css} +0 -0
  197. /package/src/css/{focus.css → core/focus.css} +0 -0
  198. /package/src/css/{intents.css → core/intents.css} +0 -0
  199. /package/src/css/{reset.css → core/reset.css} +0 -0
  200. /package/src/css/{optional → effects}/aura.css +0 -0
  201. /package/src/css/{optional → layout}/scroll-snap.css +0 -0
  202. /package/src/css/{optional/typography-fluid.css → typography/fluid.css} +0 -0
  203. /package/src/css/{optional/utilities-extra.css → utilities/extra.css} +0 -0
@@ -14,6 +14,10 @@
14
14
  * --choice-border
15
15
  * --choice-bg
16
16
  * --choice-mark
17
+ *
18
+ * --choice-control-size (tokens.css) is the shared typographic size for every
19
+ * inline choice; switch.css derives its track from it so a checkbox and a
20
+ * switch on the same line stay proportionate.
17
21
  */
18
22
  /* Intent boundary — inherited intents do not tint this component.
19
23
  Local intent classes still win because :where() has zero specificity. */
@@ -30,8 +34,6 @@
30
34
  }
31
35
 
32
36
  .choice {
33
- --choice-control-offset: 0.25em;
34
-
35
37
  display: inline-flex;
36
38
  align-items: flex-start;
37
39
  gap: var(--space-20);
@@ -40,17 +42,47 @@
40
42
  cursor: pointer;
41
43
  }
42
44
 
43
- /* Optical alignment: nudge the control down so the checkbox/radio sits
44
- visually centered with the first line of label text, not flush with the
45
- top of the line-box. Default 0.25em (~4px at 16px) places the control just
46
- below the cap-height, matching what Bootstrap, Material, and most polished
47
- form libraries ship. Multi-line labels still keep the control at the top
48
- thanks to align-items: flex-start. Override per-instance via
49
- --choice-control-offset for themes with a different line-height or control
50
- size. */
45
+ /* Optical alignment: nudge the control down so it sits visually centered with
46
+ the first line of label text, not flush with the top of the line-box. Every
47
+ inline choice runs the same formula — center the control's own block size on
48
+ the first line box, then drop it 0.0625em so it reads level with the
49
+ cap-height rather than the em-box. A checkbox and a switch therefore stay
50
+ level with each other under any line-height or --choice-control-size, which
51
+ a per-control constant cannot do (a taller switch and a shorter checkbox
52
+ need different offsets to land on the same optical line).
53
+
54
+ The constants are what the formula yields at the default 1.5 line-height:
55
+ 0.25em (4px) for a checkbox, 0.1875em (3px) for the taller switch track.
56
+ They serve browsers without lh units. Multi-line labels still keep the
57
+ control at the top thanks to align-items: flex-start.
58
+
59
+ --choice-control-offset stays fallback-only rather than declared here, so an
60
+ override placed anywhere up the tree — :root included — reaches the control
61
+ instead of losing to a component-level declaration. */
51
62
  .choice > :is(input[type="checkbox"]:not([role="switch"]), input[type="radio"]) {
52
63
  flex: 0 0 auto;
53
- margin-block-start: var(--choice-control-offset);
64
+ margin-block-start: var(--choice-control-offset, 0.25em);
65
+ }
66
+
67
+ .choice > .switch {
68
+ flex: 0 0 auto;
69
+ margin-block-start: var(--choice-control-offset, 0.1875em);
70
+ }
71
+
72
+ @supports (margin-block-start: 1lh) {
73
+ .choice > :is(input[type="checkbox"]:not([role="switch"]), input[type="radio"]) {
74
+ margin-block-start: var(
75
+ --choice-control-offset,
76
+ calc((1lh - var(--choice-control-size)) / 2 + 0.0625em)
77
+ );
78
+ }
79
+
80
+ .choice > .switch {
81
+ margin-block-start: var(
82
+ --choice-control-offset,
83
+ calc((1lh - var(--switch-block-size)) / 2 + 0.0625em)
84
+ );
85
+ }
54
86
  }
55
87
 
56
88
  .choice > span > .field-help {
@@ -76,8 +108,8 @@
76
108
  vertical-align: middle;
77
109
  display: inline-grid;
78
110
  place-content: center;
79
- inline-size: 1.125em;
80
- block-size: 1.125em;
111
+ inline-size: var(--choice-control-size);
112
+ block-size: var(--choice-control-size);
81
113
  margin: 0;
82
114
  border: var(--border-width) solid var(--form-invalid-border, var(--choice-border));
83
115
  background: var(--choice-bg);
@@ -163,27 +195,7 @@
163
195
  opacity: 1;
164
196
  }
165
197
 
166
- @media (forced-colors: active) {
167
- .check,
168
- .radio {
169
- --choice-border: ButtonText;
170
- --choice-bg: Canvas;
171
- --choice-mark: ButtonText;
172
- }
173
-
174
- .check:checked,
175
- .radio:checked,
176
- .check:indeterminate {
177
- --choice-border: Highlight;
178
- --choice-bg: Highlight;
179
- --choice-mark: HighlightText;
180
- forced-color-adjust: none;
181
- }
182
-
183
- .check:disabled,
184
- .radio:disabled {
185
- --choice-border: GrayText;
186
- --choice-bg: Canvas;
187
- --choice-mark: GrayText;
188
- }
189
- }
198
+ /* Forced colors intentionally stay under UA control: the visible check/dot
199
+ carries state while native remapping preserves platform contrast and focus.
200
+ If real HCM testing ever loses a mark, opt out on that pseudo only—never on
201
+ the interactive input host. */
@@ -46,7 +46,10 @@
46
46
  color: var(--control-fg);
47
47
  font-size: var(--control-font-size);
48
48
  cursor: var(--control-cursor);
49
- outline: none;
49
+ /* No outline: none here — the core :focus fallback must survive for browsers
50
+ without :focus-visible. Pointer focus is hidden by core's
51
+ :focus:not(:focus-visible); keyboard focus swaps in the transparent
52
+ outline + ring below. */
50
53
  transition:
51
54
  border-color var(--duration),
52
55
  box-shadow var(--duration),
@@ -12,6 +12,8 @@
12
12
  * --control-border
13
13
  * --control-bg
14
14
  */
15
+ /* This enhancement is excluded from forced-colors, where the native/shared
16
+ outline remains the focus fallback. */
15
17
  @media (forced-colors: none) and (prefers-contrast: no-preference) and (hover: hover) and (
16
18
  pointer: fine
17
19
  ) {
@@ -16,9 +16,13 @@
16
16
  * and the reserved headroom with it, leaving typography untouched.
17
17
  *
18
18
  * Public hooks:
19
+ * --floating-label-top
20
+ * --floating-label-opacity
19
21
  * --floating-pad-block-start
20
22
  */
21
23
  .floating-field {
24
+ --floating-label-top: var(--space-10);
25
+ --floating-label-opacity: 0.8;
22
26
  --floating-pad-block-start: calc(var(--control-size) / 2 + var(--space-20));
23
27
 
24
28
  /* Grid blockifies the control, removing the inline descender gap a textarea
@@ -37,24 +41,30 @@
37
41
 
38
42
  .floating-field > .field-label {
39
43
  position: absolute;
40
- inset-block-start: 50%;
41
44
  inset-inline-start: calc(var(--space-30) + var(--border-width));
42
- transform: translateY(-50%);
43
45
  color: var(--text-muted);
44
46
  font-size: var(--control-font-size);
45
47
  font-weight: var(--font-weight);
48
+ opacity: var(--floating-label-opacity);
46
49
  pointer-events: none;
47
50
  transition:
48
51
  inset-block-start var(--duration),
49
52
  transform var(--duration),
50
53
  font-size var(--duration),
54
+ opacity var(--duration),
51
55
  color var(--duration);
52
56
  }
53
57
 
54
- /* A textarea's resting label aligns with the first text line instead of
55
- centering in the (tall) control. */
58
+ /* Single-line controls rest in the middle until they receive a value. */
59
+ .floating-field > :is(.input, .select) + .field-label {
60
+ inset-block-start: 50%;
61
+ transform: translateY(-50%);
62
+ }
63
+
64
+ /* An empty textarea label follows the control's normal first-line position;
65
+ the control itself keeps the extra headroom reserved for its floated state. */
56
66
  .floating-field > .textarea + .field-label {
57
- inset-block-start: var(--floating-pad-block-start);
67
+ inset-block-start: calc(1em + var(--border-width));
58
68
  transform: none;
59
69
  }
60
70
 
@@ -63,9 +73,10 @@
63
73
  .floating-field
64
74
  > :is(.input, .textarea):is(:focus, :autofill, :not(:placeholder-shown))
65
75
  + .field-label {
66
- inset-block-start: var(--space-10);
76
+ inset-block-start: var(--floating-label-top);
67
77
  transform: none;
68
78
  font-size: var(--font-size-xs);
79
+ opacity: 1;
69
80
  }
70
81
 
71
82
  /* Controls without a dependable :placeholder-shown empty state stay floated:
@@ -81,7 +92,14 @@
81
92
  .input[type="week"]
82
93
  )
83
94
  + .field-label {
84
- inset-block-start: var(--space-10);
95
+ inset-block-start: var(--floating-label-top);
85
96
  transform: none;
86
97
  font-size: var(--font-size-xs);
98
+ opacity: 1;
99
+ }
100
+
101
+ @media (forced-colors: active) {
102
+ .floating-field > .field-label {
103
+ opacity: 1;
104
+ }
87
105
  }
@@ -1,16 +1,13 @@
1
- /* Forms — import order matters: switch.css follows choice.css so its rules
2
- land after the shared choice recipe. validation.css is part of the default
3
- forms bundle, but custom builds may omit it when validation styling is
4
- app-owned. */
5
- @import "./form.css";
6
- @import "./control.css";
1
+ /* Forms — the complete Forms family. base covers the ordinary native
2
+ controls; the leaves below add opt-in control skins. switch.css follows
3
+ choice.css (via base) so its rules land after the shared choice recipe.
4
+ validation.css stays part of base, but custom builds may omit it when
5
+ validation styling is app-owned. */
6
+ @import "./base.css";
7
7
  @import "./input-icon.css";
8
- @import "./choice.css";
8
+ @import "./switch.css";
9
+ @import "./range.css";
9
10
  @import "./choice-card.css";
10
- @import "./select.css";
11
11
  @import "./custom-select.css";
12
- @import "./native.css";
13
- @import "./range.css";
14
- @import "./switch.css";
15
- @import "./validation.css";
16
- @import "./form-actions.css";
12
+ @import "./floating-field.css";
13
+ @import "./otp.css";
@@ -41,11 +41,13 @@
41
41
  box-shadow: var(--focus-ring-shadow);
42
42
  }
43
43
 
44
- /* Native color picker — keep the browser's dialog and selected-color UI. */
44
+ /* Native color picker — keep the browser's dialog and selected-color UI. The
45
+ swatch fills almost the whole control so it reads as a color, not as a tiny
46
+ square inside a button; the 1px padding keeps a hair of --surface around it. */
45
47
  .color {
46
48
  inline-size: var(--control-size);
47
49
  block-size: var(--control-size);
48
- padding: var(--space-10);
50
+ padding: var(--border-width);
49
51
  border: var(--border-width) solid var(--border);
50
52
  border-radius: var(--radius);
51
53
  background: var(--surface);
@@ -58,10 +60,12 @@
58
60
 
59
61
  .color::-webkit-color-swatch {
60
62
  border: 0;
63
+ border-radius: calc(var(--radius) - var(--border-width));
61
64
  }
62
65
 
63
66
  .color::-moz-color-swatch {
64
67
  border: 0;
68
+ border-radius: calc(var(--radius) - var(--border-width));
65
69
  }
66
70
 
67
71
  .color:disabled {
@@ -78,6 +82,15 @@
78
82
  .color {
79
83
  border-color: ButtonText;
80
84
  background: ButtonFace;
85
+ }
86
+
87
+ /* The swatch color is content, so preserve it without disabling the host's
88
+ native forced-color focus treatment. Vendor pseudos cannot share a rule. */
89
+ .color::-webkit-color-swatch {
90
+ forced-color-adjust: none;
91
+ }
92
+
93
+ .color::-moz-color-swatch {
81
94
  forced-color-adjust: none;
82
95
  }
83
96
  }
@@ -11,7 +11,9 @@
11
11
  *
12
12
  * Internal:
13
13
  * --otp-cell-border
14
+ * --otp-cell-border-width
14
15
  * --otp-cell-bg
16
+ * --otp-caret-space
15
17
  */
16
18
  .otp {
17
19
  --otp-cell-size: var(--control-size);
@@ -19,6 +21,7 @@
19
21
  --otp-font-size: 1.5rem;
20
22
  --otp-cell-border: var(--border);
21
23
  --otp-cell-bg: var(--surface);
24
+ --otp-caret-space: calc((var(--otp-cell-size) - 1ch) / 2 + var(--otp-gap) + 1px);
22
25
 
23
26
  position: relative;
24
27
  display: inline-grid;
@@ -28,31 +31,35 @@
28
31
  max-inline-size: 100%;
29
32
  gap: var(--otp-gap);
30
33
  border-radius: var(--radius);
34
+ /* Unlike hidden, clip cannot become a scroll container when the caret moves
35
+ into the input extension. */
36
+ overflow: clip;
31
37
  direction: ltr;
32
38
  }
33
39
 
34
40
  .otp > span {
35
41
  inline-size: var(--otp-cell-size);
36
42
  block-size: var(--otp-cell-size);
37
- border: var(--border-width) solid var(--otp-cell-border);
43
+ border: var(--otp-cell-border-width, var(--border-width)) solid var(--otp-cell-border);
38
44
  border-radius: var(--radius);
39
45
  background: var(--otp-cell-bg);
40
46
  transition:
41
47
  border-color var(--duration),
42
- background-color var(--duration),
43
- box-shadow var(--duration);
48
+ background-color var(--duration);
44
49
  }
45
50
 
46
51
  /* The native input owns focus, selection, autofill, paste, and one-time-code
47
- integration. Its spacing aligns monospace glyphs with the visual cells. */
52
+ integration. Its clipped extension accommodates the trailing letter-space
53
+ and final caret so the browser never scrolls the glyphs out of alignment. */
48
54
  .otp > input {
49
55
  position: absolute;
50
56
  z-index: 1;
51
57
  inset: 0;
52
- inline-size: 100%;
58
+ inline-size: calc(100% + var(--otp-caret-space));
53
59
  block-size: 100%;
54
60
  margin: 0;
55
- padding: 0 calc((var(--otp-cell-size) - 1ch) / 2);
61
+ padding-block: 0;
62
+ padding-inline: calc((var(--otp-cell-size) - 1ch) / 2) 0;
56
63
  border: 0;
57
64
  outline: 2px solid transparent;
58
65
  box-shadow: none;
@@ -67,12 +74,15 @@
67
74
  appearance: none;
68
75
  }
69
76
 
70
- .otp:focus-within {
71
- box-shadow: var(--focus-ring-shadow);
72
- }
73
-
77
+ /* Deliberately no group ring: a box around all the cells reads as a second
78
+ component. The caret already carries focus while the code is being typed,
79
+ but it sits in the clipped input extension once every cell is filled, so
80
+ the focused edge must be unambiguous on its own — hence the doubled width,
81
+ not only the color swap. Width is safe to change: box-sizing is border-box
82
+ and the cells have a fixed inline/block size, so nothing reflows. */
74
83
  .otp > input:focus ~ span {
75
84
  --otp-cell-border: var(--focus);
85
+ --otp-cell-border-width: calc(var(--border-width) * 2);
76
86
  }
77
87
 
78
88
  .otp > input[aria-invalid="true"] ~ span,
@@ -98,12 +108,6 @@
98
108
  }
99
109
 
100
110
  @media (forced-colors: active) {
101
- .otp:focus-within {
102
- outline: var(--focus-outline);
103
- outline-offset: var(--focus-outline-offset);
104
- box-shadow: none;
105
- }
106
-
107
111
  .otp > input:focus ~ span {
108
112
  --otp-cell-border: Highlight;
109
113
  }
@@ -13,6 +13,10 @@
13
13
  * --switch-knob-margin
14
14
  * --switch-knob-size
15
15
  * --switch-travel
16
+ *
17
+ * The track is sized from --choice-control-size (tokens.css), not
18
+ * --control-size: a switch sits on a line of text next to a checkbox, so it
19
+ * follows the control typography rather than field density.
16
20
  */
17
21
  /* Intent boundary — inherited intents do not tint this component.
18
22
  Local intent classes still win because :where() has zero specificity. */
@@ -25,10 +29,22 @@
25
29
  --switch-bg: var(--border);
26
30
  --switch-border: var(--border);
27
31
  --switch-knob: var(--surface-raised);
28
- --switch-width: calc(var(--control-size) * 1.16);
29
- --switch-block-size: calc(var(--control-size) * 0.63);
30
- --switch-knob-margin: calc(var(--switch-block-size) * 0.12);
31
- --switch-knob-size: calc(var(--switch-block-size) - 2 * var(--switch-knob-margin));
32
+ --switch-block-size: calc(var(--choice-control-size) + 0.125em);
33
+ --switch-width: calc(var(--switch-block-size) * 1.8);
34
+ --switch-knob-margin: calc(var(--switch-block-size) * 0.08);
35
+
36
+ /* The knob is concentric in the track: --switch-block-size is a border-box
37
+ height, so the borders have to come out before the inset is applied on
38
+ both axes. Deriving from the outer height instead leaves the knob
39
+ --border-width closer to the rails than to the ends — a 1px error on a
40
+ ~1.6px inset, which reads as a knob squeezed against the track. */
41
+ --switch-knob-size: calc(
42
+ var(--switch-block-size) -
43
+ 2 *
44
+ var(--border-width) -
45
+ 2 *
46
+ var(--switch-knob-margin)
47
+ );
32
48
  --switch-travel: calc(
33
49
  var(--switch-width) -
34
50
  var(--switch-knob-size) -
@@ -52,6 +68,11 @@
52
68
  cursor: pointer;
53
69
  }
54
70
 
71
+ /* No elevation on the knob: --shadow is a surface elevation (card, modal,
72
+ drawer, fab), and at this size its 12px blur reads as a smudge that spills
73
+ past the rail while carrying no state. The range thumb — the same kind of
74
+ small moving part — stays flat for the same reason. The concentric inset
75
+ against the track is what makes the knob legible. */
55
76
  .switch::before {
56
77
  content: "";
57
78
  inline-size: var(--switch-knob-size);
@@ -59,11 +80,9 @@
59
80
  margin-inline: var(--switch-knob-margin);
60
81
  border-radius: var(--radius-full);
61
82
  background: var(--switch-knob);
62
- box-shadow: var(--shadow);
63
83
  transition:
64
84
  transform var(--duration),
65
- background-color var(--duration),
66
- box-shadow var(--duration);
85
+ background-color var(--duration);
67
86
  }
68
87
 
69
88
  .switch:hover:not(:disabled) {
@@ -101,28 +120,11 @@
101
120
  }
102
121
 
103
122
  @media (forced-colors: active) {
104
- /* --switch-knob must be set on the element, not on ::before: a custom
105
- property declared on the pseudo would shadow the :checked override. */
106
- .switch {
107
- --switch-border: ButtonText;
108
- --switch-knob: ButtonText;
109
- }
110
-
123
+ /* The host intentionally stays under UA control for platform contrast and
124
+ focus. The custom knob has no native edge, so this system-color border
125
+ keeps it distinct from the remapped track. If real HCM testing ever needs
126
+ an opt-out, scope it to ::before—never to the interactive input host. */
111
127
  .switch::before {
112
- box-shadow: none;
113
- }
114
-
115
- .switch:checked {
116
- --switch-bg: Highlight;
117
- --switch-border: Highlight;
118
- --switch-knob: HighlightText;
119
- forced-color-adjust: none;
120
- }
121
-
122
- .switch:disabled {
123
- --switch-bg: Canvas;
124
- --switch-border: GrayText;
125
- --switch-knob: GrayText;
126
- forced-color-adjust: auto;
128
+ border: var(--border-width) solid ButtonText;
127
129
  }
128
130
  }
@@ -0,0 +1,6 @@
1
+ /* Full-height application shell. */
2
+ .app-shell {
3
+ min-block-size: var(--viewport-block);
4
+ display: flex;
5
+ flex-direction: column;
6
+ }
@@ -0,0 +1,13 @@
1
+ /*
2
+ * Centered content column with a maximum measure.
3
+ *
4
+ * Public hooks:
5
+ * --center-size
6
+ * --center-pad
7
+ */
8
+ .center {
9
+ box-sizing: content-box;
10
+ max-inline-size: var(--center-size, 72rem);
11
+ margin-inline: auto;
12
+ padding-inline: var(--center-pad, 1rem);
13
+ }
@@ -0,0 +1,21 @@
1
+ /*
2
+ * Inline group that wraps when space runs out.
3
+ *
4
+ * Public hooks:
5
+ * --cluster-align
6
+ * --cluster-justify
7
+ * --cluster-wrap
8
+ *
9
+ * Child contract: direct-child margins are reset, spacing comes from --gap.
10
+ */
11
+ .cluster {
12
+ display: flex;
13
+ flex-wrap: var(--cluster-wrap, wrap);
14
+ align-items: var(--cluster-align, center);
15
+ justify-content: var(--cluster-justify, flex-start);
16
+ gap: var(--gap);
17
+ }
18
+
19
+ .cluster > * {
20
+ margin: 0;
21
+ }
@@ -0,0 +1,144 @@
1
+ /*
2
+ * Explicit 12-column composition canvas.
3
+ *
4
+ * Neither space-driven like .grid nor count-driven like .grid-N: this is a
5
+ * coordinate system. The author places regions with .column-span-N /
6
+ * .column-start-N and owns narrow-container recomposition. There is
7
+ * deliberately no automatic collapse — no threshold, no hidden
8
+ * .container-query behavior, no breakpoint variants. A single framework
9
+ * threshold cannot be right for arbitrary per-region compositions, and a
10
+ * hidden default would have to be undone rather than extended.
11
+ *
12
+ * See docs/design-notes/column-layout.md.
13
+ *
14
+ * Selection — reach for this only when placement is part of the design:
15
+ * independent repeated items -> .grid
16
+ * a known peer density -> .grid-N
17
+ * peers switching together -> .switcher
18
+ * content + secondary region -> .sidebar-layout
19
+ * media + flexible content -> .media
20
+ * exact custom track templates -> --grid-columns on .grid
21
+ *
22
+ * Child contract: each direct child is placed on the canvas via its own
23
+ * .column-span-N / .column-start-N.
24
+ *
25
+ * Unlike .grid-N, the track template does not read --gap, so the gap
26
+ * utilities are exact here at every width.
27
+ */
28
+ .column-layout {
29
+ display: grid;
30
+ gap: var(--gap);
31
+ grid-template-columns: repeat(12, minmax(0, 1fr));
32
+ }
33
+
34
+ /* Three zero-specificity tiers, applied in this order and only ever separated
35
+ * by source order. ORDER IS LOAD-BEARING:
36
+ *
37
+ * 1. the child reset auto-placed, spanning the whole canvas
38
+ * 2. .column-start-* a definite start line, running to the canvas end
39
+ * 3. .column-span-* the end only, so it refines either of the above
40
+ *
41
+ * Moving tier 3 above tier 2 makes every start+span pair run to column 12;
42
+ * moving either above tier 1 makes every placement full-span. Both failures
43
+ * are silent. tests/css-audit.test.js locks the order and
44
+ * tests/browser/column-layout.test.js locks the resulting geometry.
45
+ *
46
+ * Zero specificity is the point: an application's own container-query rules
47
+ * override placement additively, with a single-class selector and without
48
+ * having to undo framework specificity first.
49
+ */
50
+
51
+ /* Tier 1. A child with no placement class stays a readable full-width region
52
+ * instead of collapsing to one twelfth. Expressed as a span, not as
53
+ * `1 / -1`: a definite start line would give every child a definite column
54
+ * position and disable auto-placement, so `.column-span-8` next to
55
+ * `.column-span-4` would stack instead of composing 8 + 4.
56
+ *
57
+ * min-inline-size: 0 keeps the item box inside its track so the canvas never
58
+ * grows past its container. It does not stop content from painting outside a
59
+ * narrow track — that distinction is the primitive's contract, not a bug. */
60
+ :where(.column-layout) > * {
61
+ grid-column-end: span 12;
62
+ min-inline-size: 0;
63
+ }
64
+
65
+ /* Tier 2. A definite start line, running to the end of the canvas unless a
66
+ * .column-span-* refines the end. `column-start-9` alone is therefore the
67
+ * last four columns rather than an implicit 21-line overflow. */
68
+ :where(.column-start-1) {
69
+ grid-column: 1 / -1;
70
+ }
71
+ :where(.column-start-2) {
72
+ grid-column: 2 / -1;
73
+ }
74
+ :where(.column-start-3) {
75
+ grid-column: 3 / -1;
76
+ }
77
+ :where(.column-start-4) {
78
+ grid-column: 4 / -1;
79
+ }
80
+ :where(.column-start-5) {
81
+ grid-column: 5 / -1;
82
+ }
83
+ :where(.column-start-6) {
84
+ grid-column: 6 / -1;
85
+ }
86
+ :where(.column-start-7) {
87
+ grid-column: 7 / -1;
88
+ }
89
+ :where(.column-start-8) {
90
+ grid-column: 8 / -1;
91
+ }
92
+ :where(.column-start-9) {
93
+ grid-column: 9 / -1;
94
+ }
95
+ :where(.column-start-10) {
96
+ grid-column: 10 / -1;
97
+ }
98
+ :where(.column-start-11) {
99
+ grid-column: 11 / -1;
100
+ }
101
+ :where(.column-start-12) {
102
+ grid-column: 12 / -1;
103
+ }
104
+
105
+ /* Tier 3. The end only. Combined with a start, keep the placement inside the
106
+ * canvas: S + N - 1 <= 12. Exceeding it adds an auto-sized implicit column,
107
+ * which reshapes every 1fr track — the whole canvas loses its alignment, not
108
+ * just the offending region. */
109
+ :where(.column-span-1) {
110
+ grid-column-end: span 1;
111
+ }
112
+ :where(.column-span-2) {
113
+ grid-column-end: span 2;
114
+ }
115
+ :where(.column-span-3) {
116
+ grid-column-end: span 3;
117
+ }
118
+ :where(.column-span-4) {
119
+ grid-column-end: span 4;
120
+ }
121
+ :where(.column-span-5) {
122
+ grid-column-end: span 5;
123
+ }
124
+ :where(.column-span-6) {
125
+ grid-column-end: span 6;
126
+ }
127
+ :where(.column-span-7) {
128
+ grid-column-end: span 7;
129
+ }
130
+ :where(.column-span-8) {
131
+ grid-column-end: span 8;
132
+ }
133
+ :where(.column-span-9) {
134
+ grid-column-end: span 9;
135
+ }
136
+ :where(.column-span-10) {
137
+ grid-column-end: span 10;
138
+ }
139
+ :where(.column-span-11) {
140
+ grid-column-end: span 11;
141
+ }
142
+ :where(.column-span-12) {
143
+ grid-column-end: span 12;
144
+ }
@@ -0,0 +1,13 @@
1
+ /* Local container-query opt-in: establishes an inline-size query context for
2
+ descendants. Use it for a component that changes representation according
3
+ to its allocated inline size, and to opt .grid-N into balanced subdivision
4
+ (see grid.css). Layout primitives are responsive without it — this adds
5
+ precision, it is never a prerequisite.
6
+
7
+ The element takes inline-size containment, so put it on a normal
8
+ block-level wrapper, never on a shrink-to-fit item (a .cluster child, a
9
+ float, an inline-block): its contents would stop contributing to its own
10
+ inline size and it would collapse. */
11
+ .container-query {
12
+ container-type: inline-size;
13
+ }