@codenhub/styles 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/NOTICE +18 -16
  2. package/README.es.md +62 -0
  3. package/README.md +28 -22
  4. package/README.pt-BR.md +62 -0
  5. package/dist/aesthetics/chunky-tile.css +1 -1
  6. package/dist/aesthetics/cyber.css +1 -1
  7. package/dist/aesthetics/glass.css +1 -1
  8. package/dist/aesthetics/index.css +2 -2
  9. package/dist/aesthetics/neobrutalism.css +1 -1
  10. package/dist/aesthetics/pixel.css +1 -1
  11. package/dist/aesthetics/sketch.css +1 -1
  12. package/dist/cli.js +189 -0
  13. package/dist/components.css +2 -2
  14. package/dist/index.css +2 -2
  15. package/dist/index.d.ts +1 -0
  16. package/dist/native.css +2 -2
  17. package/dist/palette.css +1 -1
  18. package/dist/theme.css +2 -2
  19. package/docs/accessibility.md +108 -60
  20. package/docs/changelog/0.1.1.md +1 -0
  21. package/docs/changelog/0.2.0.md +1 -0
  22. package/docs/changelog/0.3.0.md +1 -0
  23. package/docs/changelog/0.4.0.md +1 -0
  24. package/docs/changelog/0.5.0.md +8 -3
  25. package/docs/changelog/0.6.0.md +2 -1
  26. package/docs/changelog/0.7.0.md +50 -0
  27. package/docs/changelog/0.8.0.md +70 -0
  28. package/docs/changelog/index.md +2 -0
  29. package/docs/concepts.md +188 -46
  30. package/docs/es/accessibility.md +169 -0
  31. package/docs/es/concepts.md +212 -0
  32. package/docs/es/index.md +105 -0
  33. package/docs/es/integrating/astro.md +35 -0
  34. package/docs/es/integrating/index.md +75 -0
  35. package/docs/es/integrating/nextjs.md +67 -0
  36. package/docs/es/integrating/svelte.md +45 -0
  37. package/docs/es/integrating/tailwind.md +148 -0
  38. package/docs/es/integrating/vue.md +31 -0
  39. package/docs/es/integrating/without-a-bundler.md +238 -0
  40. package/docs/es/setup.md +188 -0
  41. package/docs/es/usage/aesthetics.md +282 -0
  42. package/docs/es/usage/buttons.md +135 -0
  43. package/docs/es/usage/composing.md +168 -0
  44. package/docs/es/usage/content-and-layout.md +219 -0
  45. package/docs/es/usage/customizing.md +304 -0
  46. package/docs/es/usage/feedback.md +157 -0
  47. package/docs/es/usage/forms.md +256 -0
  48. package/docs/es/usage/index.md +33 -0
  49. package/docs/es/usage/presets.md +92 -0
  50. package/docs/es/usage/solo.md +86 -0
  51. package/docs/es/usage/theming.md +309 -0
  52. package/docs/es/usage/tooltips.md +127 -0
  53. package/docs/index.md +86 -26
  54. package/docs/integrating/astro.md +3 -4
  55. package/docs/integrating/index.md +46 -13
  56. package/docs/integrating/nextjs.md +3 -4
  57. package/docs/integrating/svelte.md +5 -4
  58. package/docs/integrating/tailwind.md +89 -6
  59. package/docs/integrating/vue.md +3 -4
  60. package/docs/integrating/without-a-bundler.md +237 -0
  61. package/docs/pt-BR/accessibility.md +169 -0
  62. package/docs/pt-BR/concepts.md +212 -0
  63. package/docs/pt-BR/index.md +105 -0
  64. package/docs/pt-BR/integrating/astro.md +35 -0
  65. package/docs/pt-BR/integrating/index.md +75 -0
  66. package/docs/pt-BR/integrating/nextjs.md +67 -0
  67. package/docs/pt-BR/integrating/svelte.md +45 -0
  68. package/docs/pt-BR/integrating/tailwind.md +148 -0
  69. package/docs/pt-BR/integrating/vue.md +31 -0
  70. package/docs/pt-BR/integrating/without-a-bundler.md +238 -0
  71. package/docs/pt-BR/setup.md +188 -0
  72. package/docs/pt-BR/usage/aesthetics.md +282 -0
  73. package/docs/pt-BR/usage/buttons.md +135 -0
  74. package/docs/pt-BR/usage/composing.md +168 -0
  75. package/docs/pt-BR/usage/content-and-layout.md +219 -0
  76. package/docs/pt-BR/usage/customizing.md +304 -0
  77. package/docs/pt-BR/usage/feedback.md +157 -0
  78. package/docs/pt-BR/usage/forms.md +256 -0
  79. package/docs/pt-BR/usage/index.md +33 -0
  80. package/docs/pt-BR/usage/presets.md +92 -0
  81. package/docs/pt-BR/usage/solo.md +86 -0
  82. package/docs/pt-BR/usage/theming.md +309 -0
  83. package/docs/pt-BR/usage/tooltips.md +127 -0
  84. package/docs/setup.md +142 -91
  85. package/docs/usage/aesthetics.md +89 -73
  86. package/docs/usage/buttons.md +112 -40
  87. package/docs/usage/composing.md +109 -92
  88. package/docs/usage/content-and-layout.md +160 -125
  89. package/docs/usage/customizing.md +117 -41
  90. package/docs/usage/feedback.md +138 -71
  91. package/docs/usage/forms.md +201 -107
  92. package/docs/usage/index.md +22 -14
  93. package/docs/usage/presets.md +91 -0
  94. package/docs/usage/solo.md +85 -0
  95. package/docs/usage/theming.md +273 -118
  96. package/docs/usage/tooltips.md +90 -49
  97. package/llms-full.txt +2820 -1298
  98. package/llms.txt +24 -22
  99. package/package.json +134 -91
  100. package/{dist/tw → tw}/aesthetics/chunky-tile.css +107 -29
  101. package/{dist/tw → tw}/aesthetics/cyber.css +88 -40
  102. package/{dist/tw → tw}/aesthetics/glass.css +257 -61
  103. package/{dist/tw → tw}/aesthetics/neobrutalism.css +113 -20
  104. package/{dist/tw → tw}/aesthetics/pixel.css +93 -8
  105. package/tw/aesthetics/sketch.css +417 -0
  106. package/{dist/tw → tw}/box.css +105 -9
  107. package/tw/components/button.css +11 -0
  108. package/tw/components/button.rules.css +187 -0
  109. package/tw/components/feedback.css +11 -0
  110. package/{dist/tw/components/feedback.css → tw/components/feedback.rules.css} +136 -32
  111. package/tw/components/form.css +11 -0
  112. package/{dist/tw/components/form.css → tw/components/form.rules.css} +577 -125
  113. package/tw/components/index.css +16 -0
  114. package/tw/components/rules.css +41 -0
  115. package/tw/components/surface.css +11 -0
  116. package/tw/components/surface.rules.css +222 -0
  117. package/tw/components/tooltip.css +11 -0
  118. package/{dist/tw/components/tooltip.css → tw/components/tooltip.rules.css} +43 -27
  119. package/{dist/tw → tw}/corner.css +18 -12
  120. package/{dist/tw → tw}/elevation.css +23 -17
  121. package/{dist/tw → tw}/index.css +12 -10
  122. package/tw/index.d.ts +1 -0
  123. package/tw/intent.css +312 -0
  124. package/tw/modifiers.css +127 -0
  125. package/tw/native.css +320 -0
  126. package/tw/presentation.css +139 -0
  127. package/tw/reset.css +10 -0
  128. package/{dist/tw/reset.css → tw/reset.rules.css} +21 -22
  129. package/tw/size.css +34 -0
  130. package/tw/surface.css +9 -0
  131. package/tw/surface.rules.css +212 -0
  132. package/{dist/tw → tw}/theme.css +121 -45
  133. package/tw/typography.css +8 -0
  134. package/{dist/tw/typography.css → tw/typography.rules.css} +0 -3
  135. package/{dist/tw → tw}/utilities/content.css +212 -50
  136. package/tw/utilities/index.css +11 -0
  137. package/tw/utilities/layout.css +208 -0
  138. package/tw/utilities/rules.css +8 -0
  139. package/dist/tw/aesthetics/sketch.css +0 -333
  140. package/dist/tw/components/button.css +0 -139
  141. package/dist/tw/components/index.css +0 -36
  142. package/dist/tw/components/loader.css +0 -116
  143. package/dist/tw/components/surface.css +0 -175
  144. package/dist/tw/intent.css +0 -228
  145. package/dist/tw/native.css +0 -203
  146. package/dist/tw/palette.css +0 -460
  147. package/dist/tw/presentation.css +0 -68
  148. package/dist/tw/surface.css +0 -61
  149. package/dist/tw/utilities/index.css +0 -6
  150. package/dist/tw/utilities/layout.css +0 -130
  151. /package/{dist/tw → tw}/aesthetics/index.css +0 -0
@@ -1,7 +1,3 @@
1
- @reference "tailwindcss";
2
- @import "../theme.css";
3
- @reference "../box.css";
4
-
5
1
  /* What all six text controls share: the box, the chrome, and two bounds that
6
2
  exist because presentation cascades -- a cascaded-fill cap so a container's
7
3
  `.solid` cannot fill a field with its own text colour, and an edge floor so a
@@ -24,6 +20,13 @@
24
20
  --_d-fg-on-fill: 0%;
25
21
  --_d-border: 100%;
26
22
 
23
+ /* A field's fill is at most a wash, never a plate, so it never holds an ink
24
+ of its own, even under a `.solid` it or a container names: it prints in
25
+ the ink it was handed. */
26
+ --_held-ink: inherit;
27
+ --_held-on: inherit;
28
+ --_own-hold: 0%;
29
+
27
30
  /* 6% is a quiet wash, and it is the cap rather than a presentation: `.soft`
28
31
  names the tint it wants and takes it whole below, so what this governs is
29
32
  `.solid` and the container that cascades one onto a field nobody classed. A
@@ -32,13 +35,21 @@
32
35
 
33
36
  Named rather than written into the `min()`, because the soft hover below is
34
37
  derived from it and the two must not drift apart. Typed text is
35
- `--intent-color` over a wash of its own tone, past 10:1 in both themes at
36
- any value this cap is likely to take.
38
+ `--intent-strong` over a wash of its own tone, which holds 4.5:1 for every
39
+ intent in both themes up to `.solid`'s 20% below.
37
40
 
38
41
  The fill and the ink are `box`'s own, unrestated: its `--_capped` reads
39
42
  this cap, so the plate and the ink ramp stay in step at every bound,
40
- including the checked state below that lifts it. */
41
- --_fill-cap: 6%;
43
+ including the checked state below that lifts it.
44
+
45
+ The element's own fill class names its own cap: `.soft` its 12% whole,
46
+ `.solid` 20%, which clears `.soft` and matches the plate a `.solid`
47
+ badge draws. Read from `--_own-fill`, which the class writes and which
48
+ does not inherit, so the 6% still answers a container's class. Raised
49
+ here rather than written as a fill (P6), which is what keeps `.soft` <
50
+ `.solid`. See
51
+ model.md#the-bounds-that-survive-and-the-test-for-keeping-one. */
52
+ --_fill-cap: max(6%, min(var(--_own-fill), 20%));
42
53
  /* The resting line is a fraction of the intent rather than all of it, and the
43
54
  fraction is what makes the pointer visible. Drawn at full strength a control
44
55
  already sits at its intent's darkest, and hover moves it to a neighbouring
@@ -52,6 +63,13 @@
52
63
  back to 100% -- its line is a silhouette rather than a field's edge, and
53
64
  there is nothing else marking an unchecked box. */
54
65
  --_line-rest: 60%;
66
+ /* Someone who asks for more contrast gets the line whole: above 4:1
67
+ against the page in both themes, where the quiet line is under 3:1. Hover
68
+ and focus then change nothing in the tone, which is the trade the
69
+ preference asks for. See docs/accessibility.md. */
70
+ @media (prefers-contrast: more) {
71
+ --_line-rest: 100%;
72
+ }
55
73
  /* Two seams rather than one composed result. `--_line-tone` is what colour
56
74
  the line is, and the states below move it; `--_line` folds in the fill,
57
75
  walking the line toward the intent colour by the fill amount, so a state
@@ -70,26 +88,43 @@
70
88
  typing goes and a toggle with no silhouette at all (WCAG 1.4.11). That is
71
89
  our own cascade producing the broken result, so our own floor answers it.
72
90
 
73
- An element's own `.edgeless` lowers the floor -- see the rule below the
74
- utility -- which is a consumer naming what they want rather than a cascade
75
- reaching something nobody classed. `.checkbox` and `.radio` are left off
76
- that rule and keep the floor absolute: they have nothing outside their
77
- line, so removing it removes the control. */
78
- --_edge-floor: 100%;
91
+ An element's own `.edgeless` lowers the floor -- `--_own-edge`, which
92
+ the class writes and which does not inherit -- because that is a consumer
93
+ naming what they want rather than a cascade reaching something nobody
94
+ classed. `.checkbox` and `.radio` restate the floor absolute: they have
95
+ nothing outside their line, so removing it removes the control, and
96
+ `.edgeless` on them is published unsupported. `.input-group` keeps the
97
+ lowering because the group is the field boundary. */
98
+ --_edge-floor: var(--_own-edge);
79
99
 
80
100
  @apply w-full appearance-none;
81
101
 
82
102
  /* The surrounding font rather than the user agent's smaller field one, on
83
- every entry: `/components` carries no reset to say so. */
103
+ every entry: `/components` carries no reset to say so. Its tracking is
104
+ typed text's own, `normal`, rather than the page's: body copy's wider
105
+ tracking is set on `body`, and Preflight's `letter-spacing: inherit` on
106
+ `/tw` handed it, or a container's, to every field. */
84
107
  font-family: inherit;
85
108
  font-size: inherit;
86
109
  line-height: inherit;
110
+ letter-spacing: normal;
87
111
 
88
112
  &::placeholder {
89
113
  color: var(--color-text-secondary);
90
114
  }
91
115
 
92
- &:hover:not(:disabled, [aria-disabled="true"], [data-disabled], :focus-visible) {
116
+ /* The last exclusion is `.input-group`'s: a group whose control is disabled
117
+ dims as a disabled field does (see `input-group`), so it does not answer
118
+ the pointer either. The list is the group's own, so a `<select>` with a
119
+ disabled `<option>` is not caught by it. */
120
+ &:hover:not(
121
+ :disabled,
122
+ .disabled,
123
+ [aria-disabled="true"],
124
+ [data-disabled],
125
+ :focus-visible,
126
+ :has(> :is(.ipt, .textarea, .select, .text-control, input, textarea, select):disabled)
127
+ ) {
93
128
  --_line-tone: var(--intent-hover);
94
129
  }
95
130
 
@@ -101,12 +136,115 @@
101
136
  --_line-tone: var(--intent-color);
102
137
  }
103
138
 
139
+ /* The forced-colours ring, carried so `@apply text-control` draws it: the
140
+ unlayered block at the end of this file names the package's own classes,
141
+ and `reset.css` names only typed native fields, so an applied control --
142
+ an `<input>` with no `type`, a `contenteditable` -- kept the 3px ring in a
143
+ system colour instead of the 2px `Highlight` one beside a `.text-control`.
144
+
145
+ `!important`, as the reset's ring is: the copy lands in whichever layer the
146
+ consumer applied it in, and an accessibility override has to win over any
147
+ layered rule there. So it cannot be suppressed and is kept off a control
148
+ inside an `.input-group`, which draws the field's ring and has the control
149
+ inside draw none; the offset and the cleared shadow are not important and
150
+ reach that control too, as the unlayered block's do. */
151
+ @media (forced-colors: active) {
152
+ &:focus-visible {
153
+ outline-offset: 2px;
154
+ box-shadow: none;
155
+ }
156
+
157
+ &:focus-visible:where(:not(.input-group > *)) {
158
+ outline: 2px solid Highlight !important;
159
+ }
160
+ }
161
+
162
+ /* Everything below until `:checked` is keyed on the class, and lives inside
163
+ the utility so `@apply text-control` carries it: `@apply` copies this body
164
+ with the consumer's own selector, and a rule naming `.text-control` from
165
+ outside it never reaches their element. See model.md#the-composition-api.
166
+
167
+ The neutral reset, for an element that carries none of the package's own
168
+ text-control classes -- a consumer's `.my-control { @apply text-control }`.
169
+ Those classes keep the reset in `intent.css`, in `components`; this one
170
+ lands in `utilities` and would beat a consumer's own intent class declared
171
+ in `components` there. It writes the values that reset and the control
172
+ line do, so a field inside a `.card.success` stays neutral and draws the
173
+ 3:1 control line rather than `--color-border`.
174
+
175
+ Zero specificity, and never on an element with an intent class, so it
176
+ cannot overrule one: the intent class is excluded, and every rule that
177
+ writes an intent slot in `utilities` -- `aria-invalid` below, the
178
+ `:checked` arm, `.input-group`'s inherited `--intent-strong` -- carries at
179
+ least 0-1-0. `.neutral` is not excluded, so `.neutral` takes the control
180
+ line here as it does on `.ipt.neutral`. */
181
+ /* In `families`, beside the intent classes: applied with an intent --
182
+ `@apply text-control info` -- the reset and the intent land in one layer, where the
183
+ intent's 0-1-0 beats this. Left in the rule's own layer it would beat
184
+ every intent applied with it. */
185
+ @layer families {
186
+ :where(
187
+ &:not(
188
+ .text-control,
189
+ .ipt,
190
+ .textarea,
191
+ .select,
192
+ .input-group,
193
+ .checkbox,
194
+ .radio,
195
+ .switch,
196
+ .primary,
197
+ .secondary,
198
+ .success,
199
+ .warning,
200
+ .destructive,
201
+ .info
202
+ )
203
+ ) {
204
+ --intent-color: var(--color-text);
205
+ --intent-contrast: var(--color-text-contrast);
206
+ --intent-hover: var(--color-text-hover);
207
+ --intent-strong: var(--color-text-strong);
208
+ --intent-subtle: var(--color-surface);
209
+ --intent-border: var(--ui-control-ink, var(--ui-ink, var(--color-control-border)));
210
+ --intent-fill-max: 20%;
211
+ }
212
+ }
213
+
214
+ /* The component seams: rules that beat their own component's body on
215
+ specificity, 0-2-0 against 0-1-0, in `utilities`. They write only package
216
+ privates or a state's intent slots, so nothing a consumer writes competes
217
+ with them. */
218
+
219
+ /* An invalid control is destructive whatever else it was asked to be. In
220
+ `utilities` above every intent class in `utilities.families` and both
221
+ resets in `components`, and
222
+ 0-2-0 so it also outranks the control's own body and the reset above; a
223
+ consumer's utility setting an intent slot on an invalid field loses to it
224
+ too, because state wins over every choice (model.md#precedence).
225
+ `--intent-hover` is redirected with the others so the line does not go grey
226
+ under the pointer, and `--intent-strong` so typed text is destructive too.
227
+ `--intent-contrast` and `--intent-subtle` go with them, so an invalid
228
+ toggle is a destructive one in every part: left neutral, a checked invalid
229
+ checkbox in the dark theme drew the near-black neutral mark on a red plate
230
+ (3.28:1). The bare elements and the `.input-group` bridge are below the
231
+ utility, since they name no class `@apply` could carry. */
232
+ &[aria-invalid="true"] {
233
+ --intent-color: var(--color-destructive);
234
+ --intent-contrast: var(--color-destructive-contrast);
235
+ --intent-hover: var(--color-destructive-hover);
236
+ --intent-border: var(--color-destructive);
237
+ --intent-strong: var(--color-destructive-strong);
238
+ --intent-subtle: var(--color-destructive-subtle);
239
+ }
240
+
104
241
  /* S1, P6: `:checked` is a state, so it re-declares the presentation inputs --
105
242
  a full fill and full contrast ink -- and lifts the cap that would bound
106
243
  them, and `box` composes the rest. It does not write a result: pinning
107
244
  `--_fill` and `--_fg` directly would make a checked toggle ignore its own
108
245
  aesthetic and elevation along with presentation. It sits in `utilities` at
109
- 0-2-0, above the fill classes in `components`, so a checked toggle renders
246
+ 0-2-0, above the fill classes in `utilities.families`, so a checked toggle
247
+ renders
110
248
  one plate whatever presentation it or a container asks for, and a
111
249
  consumer's utility loses to it because state wins over every choice
112
250
  (model.md#precedence). A toggle is a void `<input>`, so the inputs it
@@ -167,37 +305,21 @@
167
305
  }
168
306
  }
169
307
 
170
- /* The component seams: rules that exist to beat their own component's
171
- `@utility`, placed in `utilities` and winning there on specificity, each at
172
- least 0-2-0 against the component's 0-1-0. They write only package privates
173
- (`--_fill-cap`, `--_edge-floor`) or a state's intent slots, so nothing a
174
- consumer writes competes with them. See
308
+ /* The seams that name no class `@apply text-control` could carry: bare
309
+ elements, the toggles' shared lift, and one component's own combination.
310
+ Placed in `utilities` and winning there on specificity, like the seams inside
311
+ the utility. They write only package privates or a state's intent slots, so
312
+ nothing a consumer writes competes with them. See
175
313
  docs/internal/model.md#cascade-layers. */
176
314
  @layer utilities {
177
- /* A fill class on the element names its own cap; the 6% cascade cap in the
178
- utility is for a container reaching a field nobody classed. Both raise
179
- `--_fill-cap` rather than writing `--_fill` (P6), which is also what keeps
180
- `.soft` < `.solid`: written as a rewrite, the louder name renders the
181
- quieter box. 20% clears `.soft`'s 12% and matches the plate a `.solid`
182
- badge draws.
183
-
184
- `.switch` is deliberately absent: its track caps at 40% in its own utility,
185
- and a 0-2-0 rule here would overrule that. See
186
- model.md#the-bounds-that-survive-and-the-test-for-keeping-one. */
187
- :is(.text-control, .ipt, .textarea, .select, .input-group).soft {
188
- --_fill-cap: 12%;
189
- }
190
-
191
- :is(.text-control, .ipt, .textarea, .select, .input-group).solid {
192
- --_fill-cap: 20%;
193
- }
194
-
195
315
  /* A checked text-control fills with its own intent, so neutral has to drop its
196
316
  20% cap here. No toggle needs this arm any more (see below), but a text
197
317
  input is never `:checked` in practice either -- kept for whatever custom
198
- `.text-control` composition does check itself. A plain rule at 0-2-0 in
199
- `utilities`, above every intent class in `components`, so `.neutral` on the
200
- element cannot put the cap back. */
318
+ `.text-control` composition does check itself; only an `<input>` can be,
319
+ so the `input` arm reaches an `@apply text-control` one too. A plain rule
320
+ at 0-2-0 in `utilities`, above every intent class in
321
+ `utilities.families`, so
322
+ `.neutral` on the element cannot put the cap back. */
201
323
  :is(.text-control, input):checked {
202
324
  --intent-fill-max: 100%;
203
325
  }
@@ -211,58 +333,28 @@
211
333
  controls carry no intent class at all, and it has to look like a present,
212
334
  interactive box rather than a washed-out one just because nothing colored
213
335
  it. Declared where the intents are, for the reason given above. */
214
- :is(.checkbox, .radio, .switch) {
215
- --intent-fill-max: 100%;
216
- }
217
-
218
- /* The element's own `.edgeless` lowers the floor `text-control` raises -- a
219
- class on the element is a consumer's request, a class on a container is a
220
- cascade. `.checkbox` and `.radio` are absent: an unchecked one is a
221
- transparent square/circle with nothing but its line, so the floor is
222
- absolute and `.edgeless` on them is published unsupported. `.input-group`
223
- is on it because the group is the field boundary. */
224
- :is(.text-control, .ipt, .textarea, .select, .switch, .input-group).edgeless {
225
- --_edge-floor: 0%;
226
- }
227
-
228
- /* `.switch`'s one exception to the opt-out above: `.ghost` already asks for
229
- zero fill, so a `.switch.ghost.edgeless` track would have neither a line
230
- nor a plate and disappear -- nothing would mark its bounds but the floating
231
- knob. Every other fill on this component has something to paint
232
- (`.soft`/`.solid`/default all rest above 0%), so only this pairing needs
233
- the floor held up. `.switch.ghost.edgeless` therefore renders identically
234
- to `.switch.ghost.edged`, and is published unsupported as a distinct look.
235
- Higher specificity than the rule above (three classes, not an `:is()` of
236
- one) wins regardless of source order. */
237
- .switch.ghost.edgeless {
238
- --_edge-floor: 100%;
239
- }
240
336
 
241
- /* An invalid control is destructive whatever else it was asked to be. It sits
242
- in `utilities`, above every intent class and both intent resets in
243
- `components`, and `:is()` gives it 0-2-0 so it also outranks the control's
244
- own utility; a consumer's utility setting an intent slot on an invalid
245
- field loses to it too, because state wins over every choice
246
- (model.md#precedence). `--intent-hover` is redirected with the others so
247
- the line does not go grey under the pointer. The `:has()` arm bridges an
248
- inner control's state to an `.input-group`. */
249
- :is(
250
- .text-control,
251
- .ipt,
252
- .textarea,
253
- .select,
254
- .checkbox,
255
- .radio,
256
- .switch,
257
- .input-group,
258
- input,
259
- textarea,
260
- select
261
- )[aria-invalid="true"],
337
+ /* `.switch`'s one exception to the `.edgeless` opt-out in `text-control`:
338
+ `.ghost` already asks for zero fill, so a `.switch.ghost.edgeless` track
339
+ would have neither a line nor a plate and disappear -- nothing would mark
340
+ its bounds but the floating knob. Every other fill on this component has
341
+ something to paint (`.soft`/`.solid`/default all rest above 0%), so only
342
+ this pairing needs the floor held up. `.switch.ghost.edgeless` therefore
343
+ renders identically to `.switch.ghost.edged`, and is published unsupported
344
+ as a distinct look. Three classes against the opt-out's two, so it wins
345
+ regardless of source order. */
346
+
347
+ /* `text-control`'s invalid state, for a bare field no class reaches and for
348
+ the `.input-group` whose control is invalid. The values are the
349
+ utility's; see the state there. */
350
+ :is(input, textarea, select)[aria-invalid="true"],
262
351
  .input-group:has(> :is(.ipt, .textarea, .select, .text-control, input, textarea, select)[aria-invalid="true"]) {
263
352
  --intent-color: var(--color-destructive);
353
+ --intent-contrast: var(--color-destructive-contrast);
264
354
  --intent-hover: var(--color-destructive-hover);
265
355
  --intent-border: var(--color-destructive);
356
+ --intent-strong: var(--color-destructive-strong);
357
+ --intent-subtle: var(--color-destructive-subtle);
266
358
  }
267
359
  }
268
360
 
@@ -278,12 +370,36 @@
278
370
  --_d-border: 100%;
279
371
  --_d-elevation: 0;
280
372
 
281
- /* Typed text is the intent colour at full strength -- on the three text
282
- inputs, not `text-control`, because a toggle's mark takes `box`'s composed
283
- `--_fg` instead. */
284
- --_fg: var(--intent-color);
285
-
286
- min-height: var(--control-height);
373
+ /* Typed text is the intent printed on a page, `--intent-strong`: a field's
374
+ plate is at most a wash, and `--intent-color`, a fill colour, reads below
375
+ 4.5:1 there for several intents. On the three text inputs, not
376
+ `text-control`, because a toggle's mark takes `box`'s composed `--_fg`
377
+ instead. See docs/internal/model.md#intent. */
378
+ --_fg-own: var(--intent-strong);
379
+
380
+ /* The button's size steps, so an `.ipt.sm` and a `.btn.sm` line up in a row:
381
+ the same 2rem and 3rem heights, the same text sizes, and the same
382
+ `--_scale-size` step `box` scales the corner by. Flags, read here, for the
383
+ reason `btn` gives. */
384
+ --_sm-h: var(--_size-sm) 2rem;
385
+ --_lg-h: var(--_size-lg) 3rem;
386
+ --_sm-px: var(--_size-sm) 0.625rem;
387
+ --_lg-px: var(--_size-lg) 1rem;
388
+ --_sm-py: var(--_size-sm) 0.25rem;
389
+ --_lg-py: var(--_size-lg) 0.625rem;
390
+ --_sm-text: var(--_size-sm) var(--text-sm);
391
+ --_lg-text: var(--_size-lg) 1.0625rem;
392
+ --_sm-leading: var(--_size-sm) var(--tw-leading, var(--text-sm--line-height));
393
+ --_sm-scale: var(--_size-sm) 0.75;
394
+ --_lg-scale: var(--_size-lg) 1.25;
395
+ --_scale-size: var(--_sm-scale, var(--_lg-scale));
396
+
397
+ height: var(--_sm-h, var(--_lg-h, var(--control-height, 2.5rem)));
398
+ min-height: var(--_sm-h, var(--_lg-h, var(--control-height)));
399
+ padding-inline: var(--_sm-px, var(--_lg-px, 0.75rem));
400
+ padding-block: var(--_sm-py, var(--_lg-py, 0.5rem));
401
+ font-size: var(--_sm-text, var(--_lg-text, inherit));
402
+ line-height: var(--_sm-leading, inherit);
287
403
  }
288
404
 
289
405
  @utility field {
@@ -293,9 +409,13 @@
293
409
  }
294
410
 
295
411
  @utility label {
296
- font-size: 1rem;
297
- font-weight: 500;
298
- letter-spacing: 0.025em;
412
+ /* In `:where()`: a label's type is a default, and Tailwind emits this rule
413
+ after its own type utilities, so at 0-1-0 `.label.text-sm` stayed 16px. */
414
+ :where(&) {
415
+ font-size: 1rem;
416
+ font-weight: 500;
417
+ letter-spacing: 0.025em;
418
+ }
299
419
  }
300
420
 
301
421
  /* A hint wraps, so it takes `text-sm`'s leading like the package's other 14px
@@ -307,8 +427,15 @@
307
427
  font-weight: 400;
308
428
  letter-spacing: 0.05em;
309
429
 
310
- &.destructive {
311
- color: var(--intent-color);
430
+ /* A named intent prints its readable tone, `--intent-strong`, the colour
431
+ typed text takes: a hint is text on the page, and `--intent-color` is a
432
+ fill colour that reads below 4.5:1 there (destructive measured 3.28:1 on
433
+ the dark page). No intent, or `.neutral`, keeps the secondary grey that
434
+ marks a hint as helper text. A hint is not a component, so the intent
435
+ reset does not reach it and an intent on a container does not recolour
436
+ it; only a class on the hint itself does. */
437
+ &:is(.primary, .secondary, .success, .warning, .destructive, .info) {
438
+ color: var(--intent-strong);
312
439
  }
313
440
  }
314
441
 
@@ -321,12 +448,32 @@
321
448
  --_d-border: 100%;
322
449
  --_d-elevation: 0;
323
450
 
324
- /* Typed text is the intent colour at full strength -- on the three text
325
- inputs, not `text-control`, because a toggle's mark takes `box`'s composed
326
- `--_fg` instead. */
327
- --_fg: var(--intent-color);
451
+ /* Typed text is the intent printed on a page, `--intent-strong`: a field's
452
+ plate is at most a wash, and `--intent-color`, a fill colour, reads below
453
+ 4.5:1 there for several intents. On the three text inputs, not
454
+ `text-control`, because a toggle's mark takes `box`'s composed `--_fg`
455
+ instead. See docs/internal/model.md#intent. */
456
+ --_fg-own: var(--intent-strong);
328
457
 
329
458
  @apply min-h-28 resize-y;
459
+
460
+ /* The size steps `ipt` takes, for its text, padding, and corner. Its
461
+ height is its rows, so it keeps its own floor. */
462
+ --_sm-px: var(--_size-sm) 0.625rem;
463
+ --_lg-px: var(--_size-lg) 1rem;
464
+ --_sm-py: var(--_size-sm) 0.25rem;
465
+ --_lg-py: var(--_size-lg) 0.625rem;
466
+ --_sm-text: var(--_size-sm) var(--text-sm);
467
+ --_lg-text: var(--_size-lg) 1.0625rem;
468
+ --_sm-leading: var(--_size-sm) var(--tw-leading, var(--text-sm--line-height));
469
+ --_sm-scale: var(--_size-sm) 0.75;
470
+ --_lg-scale: var(--_size-lg) 1.25;
471
+ --_scale-size: var(--_sm-scale, var(--_lg-scale));
472
+
473
+ padding-inline: var(--_sm-px, var(--_lg-px, 0.75rem));
474
+ padding-block: var(--_sm-py, var(--_lg-py, 0.5rem));
475
+ font-size: var(--_sm-text, var(--_lg-text, inherit));
476
+ line-height: var(--_sm-leading, inherit);
330
477
  }
331
478
 
332
479
  /* The same control with a native list behind it. The chevron is drawn from two
@@ -340,10 +487,12 @@
340
487
  --_d-border: 100%;
341
488
  --_d-elevation: 0;
342
489
 
343
- /* Typed text is the intent colour at full strength -- on the three text
344
- inputs, not `text-control`, because a toggle's mark takes `box`'s composed
345
- `--_fg` instead. */
346
- --_fg: var(--intent-color);
490
+ /* Typed text is the intent printed on a page, `--intent-strong`: a field's
491
+ plate is at most a wash, and `--intent-color`, a fill colour, reads below
492
+ 4.5:1 there for several intents. On the three text inputs, not
493
+ `text-control`, because a toggle's mark takes `box`'s composed `--_fg`
494
+ instead. See docs/internal/model.md#intent. */
495
+ --_fg-own: var(--intent-strong);
347
496
 
348
497
  min-height: var(--control-height);
349
498
  background-image:
@@ -355,6 +504,27 @@
355
504
  background-repeat: no-repeat;
356
505
  background-size: 0.3125rem 0.3125rem;
357
506
 
507
+ /* The size steps `ipt` takes. The inline end keeps the chevron's room. */
508
+ --_sm-h: var(--_size-sm) 2rem;
509
+ --_lg-h: var(--_size-lg) 3rem;
510
+ --_sm-px: var(--_size-sm) 0.625rem;
511
+ --_lg-px: var(--_size-lg) 1rem;
512
+ --_sm-py: var(--_size-sm) 0.25rem;
513
+ --_lg-py: var(--_size-lg) 0.625rem;
514
+ --_sm-text: var(--_size-sm) var(--text-sm);
515
+ --_lg-text: var(--_size-lg) 1.0625rem;
516
+ --_sm-leading: var(--_size-sm) var(--tw-leading, var(--text-sm--line-height));
517
+ --_sm-scale: var(--_size-sm) 0.75;
518
+ --_lg-scale: var(--_size-lg) 1.25;
519
+ --_scale-size: var(--_sm-scale, var(--_lg-scale));
520
+
521
+ height: var(--_sm-h, var(--_lg-h, var(--control-height, 2.5rem)));
522
+ min-height: var(--_sm-h, var(--_lg-h, var(--control-height)));
523
+ padding-inline-start: var(--_sm-px, var(--_lg-px, 0.75rem));
524
+ padding-block: var(--_sm-py, var(--_lg-py, 0.5rem));
525
+ font-size: var(--_sm-text, var(--_lg-text, inherit));
526
+ line-height: var(--_sm-leading, inherit);
527
+
358
528
  /* The chevron sits at the inline end, like the padding kept for it, so a
359
529
  right-to-left select draws it at the left. `background-position` has no
360
530
  logical form, so the mirror is named here. */
@@ -417,12 +587,29 @@
417
587
  --_d-border: 100%;
418
588
  --_d-elevation: 0;
419
589
 
590
+ /* The size steps `ipt` takes, on the group rather than the control inside:
591
+ the group is the field, and the control and its adornments inherit the
592
+ text size. The control's height is the group's (below), so the group's
593
+ floor is its height. */
594
+ --_sm-h: var(--_size-sm) 2rem;
595
+ --_lg-h: var(--_size-lg) 3rem;
596
+ --_sm-px: var(--_size-sm) 0.625rem;
597
+ --_lg-px: var(--_size-lg) 1rem;
598
+ --_sm-text: var(--_size-sm) var(--text-sm);
599
+ --_lg-text: var(--_size-lg) 1.0625rem;
600
+ --_sm-leading: var(--_size-sm) var(--tw-leading, var(--text-sm--line-height));
601
+ --_sm-scale: var(--_size-sm) 0.75;
602
+ --_lg-scale: var(--_size-lg) 1.25;
603
+ --_scale-size: var(--_sm-scale, var(--_lg-scale));
604
+
420
605
  --input-group-gap: 0.5rem;
421
- --input-group-pad: 0.75rem;
606
+ --input-group-pad: var(--_sm-px, var(--_lg-px, 0.75rem));
422
607
 
423
- min-height: var(--control-height);
608
+ min-height: var(--_sm-h, var(--_lg-h, var(--control-height)));
424
609
  gap: var(--input-group-gap);
425
610
  padding-inline: var(--input-group-pad);
611
+ font-size: var(--_sm-text, var(--_lg-text, inherit));
612
+ line-height: var(--_sm-leading, inherit);
426
613
 
427
614
  /* The focus ring the wrapper cannot get from `:focus-visible` -- a `<div>` is
428
615
  not focusable, and `:has(:focus-visible)` is unreliable because the inner
@@ -435,27 +622,92 @@
435
622
  &:focus-within {
436
623
  --_line-tone: var(--intent-color);
437
624
 
438
- outline: var(--focus-ring-width) solid var(--focus-ring);
625
+ outline: var(--focus-ring-width) solid var(--focus-ring, color-mix(in srgb, var(--color-primary) 64%, transparent));
439
626
  outline-offset: var(--focus-ring-offset, calc(-1 * var(--ui-border-width, var(--border-width))));
440
- box-shadow: var(--_focus-stack, var(--_shadow));
627
+ box-shadow: var(--_focus-stack, var(--_shadow), inset 0 0 0 2px var(--_focus-gap));
628
+ }
629
+
630
+ /* Focus on anything but the field -- the clear button the docs show, a
631
+ toggle -- is focus on that control, so it keeps its own ring and the group
632
+ drops the field's. Enter presses a focused button rather than typing into
633
+ the field, and two rings around one target say neither. The line keeps its
634
+ focus tone: something in the field still has focus. A separate rule rather
635
+ than a `:not(:has())` on the ring above, so an engine without `:has()`
636
+ drops only this rule and keeps the group's ring. */
637
+ &:has(> :not(.ipt, .textarea, .select, .text-control, input, textarea, select):focus) {
638
+ outline: none;
639
+ box-shadow: var(--_shadow);
441
640
  }
442
641
 
443
642
  /* A disabled control inside dims the whole group. `.input-group.disabled` and
444
643
  the aria/data spellings are already handled by `box`'s own rule, carried in
445
644
  by `text-control`; this is the child-driven case a wrapper cannot see
446
645
  without `:has()`. */
447
- &:has(> :is(.ipt, .textarea, .select, .text-control, input, textarea, select):disabled) {
646
+ &:has(
647
+ > :is(.ipt, .textarea, .select, .text-control, input, textarea, select):where(
648
+ :not(
649
+ .checkbox,
650
+ .radio,
651
+ .switch,
652
+ .btn,
653
+ [type="checkbox"],
654
+ [type="radio"],
655
+ [type="button"],
656
+ [type="submit"],
657
+ [type="reset"],
658
+ [type="image"],
659
+ [type="range"],
660
+ [type="color"],
661
+ [type="file"],
662
+ [type="hidden"]
663
+ )
664
+ ):disabled
665
+ ) {
448
666
  @apply cursor-not-allowed;
449
667
 
450
668
  opacity: 0.58;
451
669
  }
452
670
 
453
- /* The control goes flush: the group drew the box, so the control paints no
671
+ /* "The control inside" is a text field. Every selector below that names it
672
+ ends in the same `:where(:not(...))`, which takes out the toggles, the
673
+ button-like inputs, and the inputs that draw their own widget: matched,
674
+ a checkbox in a group lost its box and its line, and stretched like a
675
+ field. In `:where()`, so the exclusion adds no specificity.
676
+
677
+ The control goes flush: the group drew the box, so the control paints no
454
678
  fill, no line and no ring, and gives its horizontal padding to the group.
455
679
  `border: none` rather than a transparent 1px, so the control does not add
456
- its own width inside the group. */
457
- & > :is(.ipt, .textarea, .select, .text-control, input, textarea, select) {
680
+ its own width inside the group.
681
+
682
+ The typed text is the one paint the control keeps, and it is the group's
683
+ intent: the control resets its intent to neutral at its own root, like
684
+ every component, so an `.input-group.success` drew a success line around
685
+ neutral text. `--intent-strong` is the only slot the text reads, so it
686
+ alone inherits, and an invalid group -- by its own `aria-invalid` or the
687
+ control's, through the `:has()` arm -- prints destructive text the way a
688
+ lone invalid field does. The group owns the field, so an intent class on
689
+ the control inside does not colour its text. */
690
+ &
691
+ > :is(.ipt, .textarea, .select, .text-control, input, textarea, select):where(
692
+ :not(
693
+ .checkbox,
694
+ .radio,
695
+ .switch,
696
+ .btn,
697
+ [type="checkbox"],
698
+ [type="radio"],
699
+ [type="button"],
700
+ [type="submit"],
701
+ [type="reset"],
702
+ [type="image"],
703
+ [type="range"],
704
+ [type="color"],
705
+ [type="file"],
706
+ [type="hidden"]
707
+ )
708
+ ) {
458
709
  --_fill-cap: 0%;
710
+ --intent-strong: inherit;
459
711
 
460
712
  flex: 1 1 auto;
461
713
  min-width: 0;
@@ -468,15 +720,96 @@
468
720
  outline: none;
469
721
  box-shadow: none;
470
722
  }
723
+
724
+ /* The group dims for a disabled control, so the control does not dim
725
+ again inside it: opacity multiplies, and the two 0.58s left the value
726
+ at about 0.34. Only `:disabled`, the state the group's `:has()` above
727
+ bridges; a control disabled by class or aria dims itself as a lone one
728
+ does, because the group does not dim for it. */
729
+ &:disabled {
730
+ opacity: 1;
731
+ }
732
+ }
733
+
734
+ /* A one-row control gives its height to the group too. Keeping its own
735
+ `--control-height` inside a box that draws its own line made the group
736
+ 42px next to 40px fields and buttons. It stretches to the group's inside
737
+ instead, with no block padding, so the group is the control height and the
738
+ whole of its inside still takes a click. A textarea and a list box keep
739
+ their own height: their rows are the height. */
740
+ &
741
+ > :is(.ipt, .select, input, select):not([multiple], [size]:not([size="1"])):where(
742
+ :not(
743
+ .checkbox,
744
+ .radio,
745
+ .switch,
746
+ .btn,
747
+ [type="checkbox"],
748
+ [type="radio"],
749
+ [type="button"],
750
+ [type="submit"],
751
+ [type="reset"],
752
+ [type="image"],
753
+ [type="range"],
754
+ [type="color"],
755
+ [type="file"],
756
+ [type="hidden"]
757
+ )
758
+ ) {
759
+ align-self: stretch;
760
+ height: auto;
761
+ min-height: 0;
762
+ padding-block: 0;
763
+ }
764
+
765
+ /* Except a drop-down's end: its chevron is the select's own background,
766
+ drawn inside its box 0.6875-1.3125rem from the inline end, so a long label
767
+ with no padding there ran under it. A list box draws none and keeps the
768
+ padding its own rule gives it. */
769
+ & > :is(.select, select):not([multiple], [size]:not([size="1"])) {
770
+ padding-inline-end: 1.75rem;
471
771
  }
472
772
 
473
773
  /* Anything else in the group -- an icon, affix text -- does not stretch and
474
- reads as secondary. A nested `<button>` keeps its own styling. */
475
- & > :not(.ipt, .textarea, .select, .text-control, input, textarea, select, button) {
774
+ reads as secondary. A button, a link button, and any `<input>` keep their
775
+ own styling. Zero specificity: it is a default for unstyled content, and at
776
+ the 0-2-0 it had, a badge or a text utility on the adornment lost its
777
+ colour to this grey. */
778
+ :where(& > :not(.ipt, .textarea, .select, .text-control, input, textarea, select, button, .btn)) {
476
779
  @apply inline-flex flex-none items-center;
477
780
 
478
781
  color: var(--color-text-secondary);
479
782
  }
783
+
784
+ /* The group is destructive when its control is invalid. Carried here for an
785
+ applied group; the package's own keeps the plain rule above the utility. */
786
+ &:has(
787
+ > :is(.ipt, .textarea, .select, .text-control, input, textarea, select):where(
788
+ :not(
789
+ .checkbox,
790
+ .radio,
791
+ .switch,
792
+ .btn,
793
+ [type="checkbox"],
794
+ [type="radio"],
795
+ [type="button"],
796
+ [type="submit"],
797
+ [type="reset"],
798
+ [type="image"],
799
+ [type="range"],
800
+ [type="color"],
801
+ [type="file"],
802
+ [type="hidden"]
803
+ )
804
+ )[aria-invalid="true"]
805
+ ) {
806
+ --intent-color: var(--color-destructive);
807
+ --intent-contrast: var(--color-destructive-contrast);
808
+ --intent-hover: var(--color-destructive-hover);
809
+ --intent-border: var(--color-destructive);
810
+ --intent-strong: var(--color-destructive-strong);
811
+ --intent-subtle: var(--color-destructive-subtle);
812
+ }
480
813
  }
481
814
 
482
815
  /* The three toggles compose `text-control` for its bounds, not its chrome: the
@@ -496,6 +829,8 @@
496
829
  --_d-fill: 12%;
497
830
  --_d-fg-on-fill: 0%;
498
831
  --_fill-cap: 40%;
832
+ /* Absolute, whatever the element's own edge class: see `text-control`. */
833
+ --_edge-floor: 100%;
499
834
  --_line-rest: 100%;
500
835
  --_d-border: 100%;
501
836
  --_d-elevation: 0;
@@ -521,7 +856,9 @@
521
856
  rather than as an `outline`, so a clipping aesthetic clips it with
522
857
  everything else and its own inset focus layer still paints over the top. */
523
858
  &:focus-visible {
524
- --_shadow: var(--_shadow-parts) var(--_halo-layer,), 0 0 0 var(--focus-ring-width) var(--focus-ring);
859
+ --_shadow:
860
+ var(--_shadow-parts) var(--_halo-layer,),
861
+ 0 0 0 var(--focus-ring-width) var(--focus-ring, color-mix(in srgb, var(--color-primary) 64%, transparent));
525
862
  }
526
863
 
527
864
  &::after {
@@ -566,6 +903,33 @@
566
903
  transition-duration: 0.01ms;
567
904
  }
568
905
  }
906
+
907
+ /* The toggle's cap is its own (`--_fill-cap`), whatever the intent: a
908
+ neutral fill stops at 20% elsewhere because it is the page's ink, and a
909
+ neutral checkbox is the common case and has to look like a present,
910
+ interactive box. In the body so `@apply` carries it; it beats the intent
911
+ classes from here as it did as a plain rule in `utilities`. */
912
+ --intent-fill-max: 100%;
913
+
914
+ /* Forced colours, carried for an applied toggle. The package's own classes
915
+ also keep the unlayered copy at the foot of this file, which beats every
916
+ layered rule. */
917
+ @media (forced-colors: active) {
918
+ forced-color-adjust: none;
919
+ background-color: Canvas;
920
+ border-color: CanvasText;
921
+ transition: none;
922
+
923
+ &:checked,
924
+ &:indeterminate {
925
+ background-color: Highlight;
926
+ border-color: Highlight;
927
+
928
+ &::after {
929
+ background-color: HighlightText;
930
+ }
931
+ }
932
+ }
569
933
  }
570
934
 
571
935
  /* The same control with a round silhouette and a dot for its mark, and the
@@ -578,6 +942,8 @@
578
942
  --_d-fill: 12%;
579
943
  --_d-fg-on-fill: 0%;
580
944
  --_fill-cap: 40%;
945
+ /* Absolute, whatever the element's own edge class: see `text-control`. */
946
+ --_edge-floor: 100%;
581
947
  --_line-rest: 100%;
582
948
  --_d-border: 100%;
583
949
  --_d-elevation: 0;
@@ -592,7 +958,9 @@
592
958
 
593
959
  /* The ring, for the reason given on `checkbox`. */
594
960
  &:focus-visible {
595
- --_shadow: var(--_shadow-parts) var(--_halo-layer,), 0 0 0 var(--focus-ring-width) var(--focus-ring);
961
+ --_shadow:
962
+ var(--_shadow-parts) var(--_halo-layer,),
963
+ 0 0 0 var(--focus-ring-width) var(--focus-ring, color-mix(in srgb, var(--color-primary) 64%, transparent));
596
964
  }
597
965
 
598
966
  &::after {
@@ -633,6 +1001,32 @@
633
1001
  transition-duration: 0.01ms;
634
1002
  }
635
1003
  }
1004
+
1005
+ /* The toggle's cap is its own (`--_fill-cap`), whatever the intent: a
1006
+ neutral fill stops at 20% elsewhere because it is the page's ink, and a
1007
+ neutral checkbox is the common case and has to look like a present,
1008
+ interactive box. In the body so `@apply` carries it; it beats the intent
1009
+ classes from here as it did as a plain rule in `utilities`. */
1010
+ --intent-fill-max: 100%;
1011
+
1012
+ /* Forced colours, carried for an applied toggle. The package's own classes
1013
+ also keep the unlayered copy at the foot of this file, which beats every
1014
+ layered rule. */
1015
+ @media (forced-colors: active) {
1016
+ forced-color-adjust: none;
1017
+ background-color: Canvas;
1018
+ border-color: CanvasText;
1019
+ transition: none;
1020
+
1021
+ &:checked {
1022
+ background-color: Highlight;
1023
+ border-color: Highlight;
1024
+
1025
+ &::after {
1026
+ background-color: HighlightText;
1027
+ }
1028
+ }
1029
+ }
636
1030
  }
637
1031
 
638
1032
  /* A2: every dimension derives from the border width `box` resolved, so the knob
@@ -688,7 +1082,9 @@
688
1082
 
689
1083
  /* The ring, for the reason given on `checkbox`. */
690
1084
  &:focus-visible {
691
- --_shadow: var(--_shadow-parts) var(--_halo-layer,), 0 0 0 var(--focus-ring-width) var(--focus-ring);
1085
+ --_shadow:
1086
+ var(--_shadow-parts) var(--_halo-layer,),
1087
+ 0 0 0 var(--focus-ring-width) var(--focus-ring, color-mix(in srgb, var(--color-primary) 64%, transparent));
692
1088
  }
693
1089
 
694
1090
  &::after {
@@ -716,10 +1112,13 @@
716
1112
  physical x offset and mirrored under :dir(rtl) to travel toward the
717
1113
  opposite edge, matching the inset-inline-start resting position. */
718
1114
  translate: var(--switch-travel);
1115
+ }
719
1116
 
720
- :dir(rtl) & {
721
- translate: calc(-1 * var(--switch-travel));
722
- }
1117
+ /* Direction sits on the control, ahead of the pseudo-element: nested as
1118
+ `:dir(rtl) &` inside `&:checked::after`, `&` carries `::after`, which
1119
+ cannot appear inside `:is()`, and the emitted rule matched nothing. */
1120
+ &:checked:dir(rtl)::after {
1121
+ translate: calc(-1 * var(--switch-travel));
723
1122
  }
724
1123
 
725
1124
  /* The mark appears without moving under reduced motion, for the entries
@@ -729,6 +1128,43 @@
729
1128
  transition-duration: 0.01ms;
730
1129
  }
731
1130
  }
1131
+
1132
+ /* `.ghost.edgeless` would leave the track neither a line nor a plate, so the
1133
+ floor is held up: three classes against the opt-out's two in
1134
+ `text-control`. Published unsupported as a distinct look. */
1135
+ &.ghost.edgeless {
1136
+ --_edge-floor: 100%;
1137
+ }
1138
+
1139
+ /* The toggle's cap is its own (`--_fill-cap`), whatever the intent: a
1140
+ neutral fill stops at 20% elsewhere because it is the page's ink, and a
1141
+ neutral checkbox is the common case and has to look like a present,
1142
+ interactive box. In the body so `@apply` carries it; it beats the intent
1143
+ classes from here as it did as a plain rule in `utilities`. */
1144
+ --intent-fill-max: 100%;
1145
+
1146
+ /* Forced colours, carried for an applied toggle. The package's own classes
1147
+ also keep the unlayered copy at the foot of this file, which beats every
1148
+ layered rule. */
1149
+ @media (forced-colors: active) {
1150
+ forced-color-adjust: none;
1151
+ background-color: Canvas;
1152
+ border-color: CanvasText;
1153
+ transition: none;
1154
+
1155
+ &::after {
1156
+ background-color: CanvasText;
1157
+ }
1158
+
1159
+ &:checked {
1160
+ background-color: Highlight;
1161
+ border-color: Highlight;
1162
+
1163
+ &::after {
1164
+ background-color: HighlightText;
1165
+ }
1166
+ }
1167
+ }
732
1168
  }
733
1169
 
734
1170
  @media (forced-colors: active) {
@@ -748,6 +1184,15 @@
748
1184
  background-color: CanvasText;
749
1185
  }
750
1186
 
1187
+ /* The chevron is two gradients, and forced colours drop every gradient, so a
1188
+ drop-down with `appearance: none` showed no arrow at all and read as a text
1189
+ field. The platform's own arrow comes back instead: it is drawn in system
1190
+ colours, and the author border, padding and focus outline still apply. A
1191
+ list box has no arrow to restore. */
1192
+ .select:not([multiple], [size]:not([size="1"])) {
1193
+ appearance: auto;
1194
+ }
1195
+
751
1196
  .checkbox:checked,
752
1197
  .checkbox:indeterminate,
753
1198
  .radio:checked,
@@ -771,4 +1216,11 @@
771
1216
  outline-offset: 2px;
772
1217
  box-shadow: none;
773
1218
  }
1219
+
1220
+ /* The group draws the field's ring, so the control inside draws none here
1221
+ either: this block is unlayered, and the ring above beat the group's
1222
+ layered suppression and drew a second ring inside the group's. */
1223
+ .input-group > :is(.ipt, .textarea, .select, .text-control, input, textarea, select):focus-visible {
1224
+ outline: none;
1225
+ }
774
1226
  }