@codenhub/styles 0.5.0 → 0.7.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 (119) hide show
  1. package/NOTICE +18 -16
  2. package/README.es.md +61 -0
  3. package/README.md +27 -22
  4. package/README.pt-BR.md +61 -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/components.css +2 -2
  13. package/dist/index.css +2 -2
  14. package/dist/native.css +2 -2
  15. package/dist/palette.css +1 -1
  16. package/dist/theme.css +2 -2
  17. package/dist/tw/aesthetics/chunky-tile.css +120 -62
  18. package/dist/tw/aesthetics/cyber.css +102 -40
  19. package/dist/tw/aesthetics/glass.css +371 -19
  20. package/dist/tw/aesthetics/index.css +2 -1
  21. package/dist/tw/aesthetics/neobrutalism.css +121 -33
  22. package/dist/tw/aesthetics/pixel.css +111 -16
  23. package/dist/tw/aesthetics/sketch.css +100 -28
  24. package/dist/tw/box.css +69 -28
  25. package/dist/tw/components/button.css +35 -14
  26. package/dist/tw/components/feedback.css +61 -58
  27. package/dist/tw/components/form.css +255 -142
  28. package/dist/tw/components/index.css +4 -9
  29. package/dist/tw/components/surface.css +80 -68
  30. package/dist/tw/components/tooltip.css +75 -41
  31. package/dist/tw/elevation.css +14 -22
  32. package/dist/tw/index.css +15 -19
  33. package/dist/tw/intent.css +35 -27
  34. package/dist/tw/native.css +22 -12
  35. package/dist/tw/palette.css +14 -11
  36. package/dist/tw/presentation.css +16 -30
  37. package/dist/tw/reset.css +36 -43
  38. package/dist/tw/surface.css +11 -12
  39. package/dist/tw/theme.css +87 -103
  40. package/dist/tw/utilities/content.css +311 -200
  41. package/dist/tw/utilities/layout.css +41 -37
  42. package/docs/accessibility.md +104 -58
  43. package/docs/changelog/0.1.1.md +1 -0
  44. package/docs/changelog/0.2.0.md +1 -0
  45. package/docs/changelog/0.3.0.md +1 -0
  46. package/docs/changelog/0.4.0.md +1 -0
  47. package/docs/changelog/0.5.0.md +8 -3
  48. package/docs/changelog/0.6.0.md +53 -0
  49. package/docs/changelog/0.7.0.md +50 -0
  50. package/docs/changelog/index.md +2 -0
  51. package/docs/concepts.md +188 -46
  52. package/docs/es/accessibility.md +150 -0
  53. package/docs/es/concepts.md +212 -0
  54. package/docs/es/index.md +104 -0
  55. package/docs/es/integrating/astro.md +35 -0
  56. package/docs/es/integrating/index.md +47 -0
  57. package/docs/es/integrating/nextjs.md +67 -0
  58. package/docs/es/integrating/svelte.md +45 -0
  59. package/docs/es/integrating/tailwind.md +78 -0
  60. package/docs/es/integrating/vue.md +31 -0
  61. package/docs/es/integrating/without-a-bundler.md +115 -0
  62. package/docs/es/setup.md +180 -0
  63. package/docs/es/usage/aesthetics.md +276 -0
  64. package/docs/es/usage/buttons.md +131 -0
  65. package/docs/es/usage/composing.md +164 -0
  66. package/docs/es/usage/content-and-layout.md +215 -0
  67. package/docs/es/usage/customizing.md +298 -0
  68. package/docs/es/usage/feedback.md +153 -0
  69. package/docs/es/usage/forms.md +231 -0
  70. package/docs/es/usage/index.md +29 -0
  71. package/docs/es/usage/solo.md +84 -0
  72. package/docs/es/usage/theming.md +296 -0
  73. package/docs/es/usage/tooltips.md +127 -0
  74. package/docs/index.md +89 -21
  75. package/docs/integrating/astro.md +3 -4
  76. package/docs/integrating/index.md +18 -13
  77. package/docs/integrating/nextjs.md +4 -5
  78. package/docs/integrating/svelte.md +5 -4
  79. package/docs/integrating/tailwind.md +29 -8
  80. package/docs/integrating/vue.md +3 -4
  81. package/docs/integrating/without-a-bundler.md +114 -0
  82. package/docs/pt-BR/accessibility.md +150 -0
  83. package/docs/pt-BR/concepts.md +212 -0
  84. package/docs/pt-BR/index.md +104 -0
  85. package/docs/pt-BR/integrating/astro.md +35 -0
  86. package/docs/pt-BR/integrating/index.md +47 -0
  87. package/docs/pt-BR/integrating/nextjs.md +67 -0
  88. package/docs/pt-BR/integrating/svelte.md +45 -0
  89. package/docs/pt-BR/integrating/tailwind.md +78 -0
  90. package/docs/pt-BR/integrating/vue.md +31 -0
  91. package/docs/pt-BR/integrating/without-a-bundler.md +115 -0
  92. package/docs/pt-BR/setup.md +180 -0
  93. package/docs/pt-BR/usage/aesthetics.md +276 -0
  94. package/docs/pt-BR/usage/buttons.md +131 -0
  95. package/docs/pt-BR/usage/composing.md +164 -0
  96. package/docs/pt-BR/usage/content-and-layout.md +215 -0
  97. package/docs/pt-BR/usage/customizing.md +298 -0
  98. package/docs/pt-BR/usage/feedback.md +153 -0
  99. package/docs/pt-BR/usage/forms.md +231 -0
  100. package/docs/pt-BR/usage/index.md +29 -0
  101. package/docs/pt-BR/usage/solo.md +84 -0
  102. package/docs/pt-BR/usage/theming.md +296 -0
  103. package/docs/pt-BR/usage/tooltips.md +127 -0
  104. package/docs/setup.md +134 -91
  105. package/docs/usage/aesthetics.md +102 -65
  106. package/docs/usage/buttons.md +108 -40
  107. package/docs/usage/composing.md +105 -92
  108. package/docs/usage/content-and-layout.md +159 -127
  109. package/docs/usage/customizing.md +112 -43
  110. package/docs/usage/feedback.md +135 -70
  111. package/docs/usage/forms.md +184 -99
  112. package/docs/usage/index.md +18 -14
  113. package/docs/usage/solo.md +83 -0
  114. package/docs/usage/theming.md +260 -119
  115. package/docs/usage/tooltips.md +103 -42
  116. package/llms-full.txt +2465 -1272
  117. package/llms.txt +25 -17
  118. package/package.json +28 -35
  119. package/dist/tw/components/loader.css +0 -117
@@ -7,11 +7,14 @@
7
7
  @layer theme, base, components, utilities;
8
8
 
9
9
  @layer theme {
10
- :root {
11
- --palette-neutral-solid-bg: oklab(0.145 0 0 / 0.2);
10
+ :root,
11
+ .light,
12
+ .theme-light,
13
+ [data-theme="light"] {
14
+ --palette-neutral-solid-bg: oklab(0.817 0 0);
12
15
  --palette-neutral-solid-fg: oklab(0.205 0 0);
13
16
  --palette-neutral-solid-edge: oklab(0 0 0 / 0);
14
- --palette-neutral-solid-bg-hover: oklab(0.371 0 0 / 0.34);
17
+ --palette-neutral-solid-bg-hover: oklab(0.77624 0 0);
15
18
  --palette-neutral-solid-edge-hover: oklab(0 0 0 / 0);
16
19
  --palette-neutral-soft-fg: oklab(0.205 0 0);
17
20
  --palette-neutral-soft-bg: oklab(0.145 0 0 / 0.12);
@@ -225,18 +228,18 @@
225
228
  --palette-info-ghost-subtle-edge: oklab(0.511 0.0317755 -0.260066);
226
229
  --palette-info-ghost-subtle-bg-hover: oklab(0.8685 0.00575617 -0.0640304);
227
230
  --palette-info-ghost-subtle-edge-hover: oklab(0.490714 0.0309565 -0.24875);
228
- --palette-border: oklch(0.708 0 0);
229
- --palette-surface: oklch(0.922 0 0);
230
- --palette-text: oklch(0.145 0 0);
231
+ --palette-border: oklch(0.708 0 none);
232
+ --palette-surface: oklch(0.922 0 none);
233
+ --palette-text: oklch(0.145 0 none);
231
234
  }
232
235
 
233
236
  .dark,
234
237
  .theme-dark,
235
238
  [data-theme="dark"] {
236
- --palette-neutral-solid-bg: oklab(0.985 0 0 / 0.2);
239
+ --palette-neutral-solid-bg: oklab(0.313 0 0);
237
240
  --palette-neutral-solid-fg: oklab(0.97 0 0);
238
241
  --palette-neutral-solid-edge: oklab(0 0 0 / 0);
239
- --palette-neutral-solid-bg-hover: oklab(0.922 0 0 / 0.34);
242
+ --palette-neutral-solid-bg-hover: oklab(0.40918 0 0);
240
243
  --palette-neutral-solid-edge-hover: oklab(0 0 0 / 0);
241
244
  --palette-neutral-soft-fg: oklab(0.97 0 0);
242
245
  --palette-neutral-soft-bg: oklab(0.985 0 0 / 0.12);
@@ -450,8 +453,8 @@
450
453
  --palette-info-ghost-subtle-edge: oklab(0.511 0.0317755 -0.260066);
451
454
  --palette-info-ghost-subtle-bg-hover: oklab(0.38601 0.0239578 -0.155809);
452
455
  --palette-info-ghost-subtle-edge-hover: oklab(0.551929 0.0373722 -0.238521);
453
- --palette-border: oklch(0.371 0 0);
454
- --palette-surface: oklch(0.269 0 0);
455
- --palette-text: oklch(0.985 0 0);
456
+ --palette-border: oklch(0.371 0 none);
457
+ --palette-surface: oklch(0.269 0 none);
458
+ --palette-text: oklch(0.985 0 none);
456
459
  }
457
460
  }
@@ -7,21 +7,11 @@
7
7
  line at its boundary. Exactly one value from each set applies, so the six
8
8
  combinations are the whole vocabulary.
9
9
 
10
- A fill class never decides an edge and an edge class never decides a fill.
11
- `.solid` briefly broke that rule, writing `--ui-border: 0%` alongside its fill
12
- so that a filled box could not be ringed in another colour. It bought a real
13
- fix -- the edge blend runs the line toward the fill, which lands *on* the fill
14
- when the fill is opaque and on a second coat of the same tint when it is not,
15
- so every capped intent that drew a line drew a visible ring over its own plate
16
- -- at the cost of the one property that makes the six combinations readable:
17
- that each half means only what it says. Two axes that are independent in two
18
- cases out of three are not axes.
19
-
20
- The ring was a fault in the blend rather than in the class, and it was fixed
21
- there: the line fades toward `transparent` as the fill fills in, so a
22
- translucent plate shows through its own border as one coat (`box.css`;
23
- docs/internal/model.md#presentation). `.solid.edged` and `.solid.edgeless`
24
- render one box.
10
+ A fill class never decides an edge and an edge class never decides a fill, so
11
+ each half means only what it says. The line fades toward `transparent` as the
12
+ fill fills in (`box.css`; docs/internal/model.md#presentation), so
13
+ `.solid.edged` and `.solid.edgeless` render one box without either class
14
+ deciding the other's question.
25
15
 
26
16
  Presentation declares only unitless numbers and percentages. A custom
27
17
  property's value is resolved on the element that declares it, so anything
@@ -33,8 +23,8 @@
33
23
  container set the look for its subtree while any element still opts out.
34
24
 
35
25
  Hover is derived from these values rather than declared beside them:
36
- `min(100%, resting fill + --hover-step)`, always in `--intent-hover`. There is
37
- no hover token and no per-presentation branch. */
26
+ `min(100%, resting fill + --hover-step)`, always in `--intent-hover`. There
27
+ is no hover token and no per-presentation branch. */
38
28
 
39
29
  /* Filled with the intent color. Text is the contrast tone. */
40
30
  .solid {
@@ -48,11 +38,9 @@
48
38
  --ui-fg-on-fill: 0%;
49
39
  }
50
40
 
51
- /* No fill at rest; the derived hover gives it one. Text is the intent color.
52
-
53
- Named `.ghost` rather than `.bare` because `.bare` and `.edgeless` both read
54
- as "less of something", so an author wanting no border reached for `.bare`
55
- first and got no background instead. `.ghost` says fill and only fill.
41
+ /* No fill at rest; the derived hover gives it one. Text is the intent color. It
42
+ names a fill and only a fill: `.bare` and `.edgeless` both read as "less of
43
+ something", so an author wanting no border would reach for the wrong one.
56
44
 
57
45
  A component that rests on a ground rather than on nothing keeps that ground
58
46
  here: `.ghost` is zero fill, not zero paint. `.data-table` is the exception
@@ -62,16 +50,14 @@
62
50
  --ui-fg-on-fill: 0%;
63
51
  }
64
52
 
65
- /* A line in the intent color, at the aesthetic's material width. The width comes
66
- from the aesthetic and nothing else: there is no presentation scale on it, so
67
- small components need no ceilings to survive a thick one.
53
+ /* A line in the intent color, at the aesthetic's material width. The width
54
+ comes from the aesthetic and nothing else: there is no presentation scale on
55
+ it, so small components need no ceilings to survive a thick one.
68
56
 
69
57
  The edge is the silhouette and nothing else. Rules *inside* a component that
70
- has an inside are a separate decision with its own class -- `.data-table.ruled`
71
- -- because tying them here made them arrive by implication: a table drew rules
72
- only because its published edge default happened to be `edged`, so whether a
73
- consumer got them depended on a default they had not read and could not name.
74
- Two questions, two switches. */
58
+ has an inside are a separate decision with their own class --
59
+ `.data-table.ruled` -- so a consumer's own markup says whether they asked for
60
+ them. */
75
61
  .edged {
76
62
  --ui-border: 100%;
77
63
  }
package/dist/tw/reset.css CHANGED
@@ -3,13 +3,12 @@
3
3
  @import "./typography.css";
4
4
 
5
5
  @layer base {
6
- /* The package's own components no longer lean on this: `@utility box` sets
6
+ /* The package's own components do not lean on this: `@utility box` sets
7
7
  `box-sizing: border-box` on itself, so a sized control is right in every
8
8
  entrypoint including the reset-less `/components` and granular `/tw/*`
9
- ones. What is left for the universal rule is the consumer's own markup --
10
- a plain `<div>` laid out beside our components under the full reset -- so
11
- it stays part of the opinionated baseline the reset already is, rather
12
- than a correctness prop the components need. */
9
+ ones. The universal rule covers the consumer's own markup -- a plain
10
+ `<div>` laid out beside our components -- as part of the opinionated
11
+ baseline. */
13
12
  *,
14
13
  *::before,
15
14
  *::after {
@@ -52,12 +51,16 @@
52
51
  margin: 0;
53
52
  }
54
53
 
54
+ /* The whole font, not only the family: the user agent gives a button and a
55
+ field their own smaller size, so a `<button>`, a `.btn` link, and the
56
+ same `.btn` on another entrypoint each printed a different label size.
57
+ Tailwind's Preflight does the same on the entries that carry it. */
55
58
  button,
56
59
  input,
57
60
  optgroup,
58
61
  select,
59
62
  textarea {
60
- font-family: inherit;
63
+ font: inherit;
61
64
  }
62
65
 
63
66
  button,
@@ -72,28 +75,39 @@
72
75
  color: var(--color-primary-contrast);
73
76
  }
74
77
 
78
+ /* Negative, so the ring is drawn over the edge rather than beside it: at a
79
+ positive offset the outline floats a gap away and reads as a second,
80
+ detached frame around the control. Pulled in by exactly the line width the
81
+ aesthetic draws, the ring lands on top of the line and a focused control
82
+ reads as its own edge thickening -- which is what a focus indicator is
83
+ meant to look like. Computed here, on the focused element, because that is
84
+ where an aesthetic's `--ui-border-width` resolves; `--focus-ring-offset`,
85
+ when a consumer sets it, still wins.
86
+
87
+ A component drawing no line at rest loses nothing: the outline moves inward
88
+ over its own fill by a hairline, which is not a visible change. */
75
89
  :focus-visible {
76
90
  outline: var(--focus-ring-width) solid var(--focus-ring);
77
- outline-offset: var(--focus-ring-offset);
91
+ outline-offset: var(--focus-ring-offset, calc(-1 * var(--ui-border-width, var(--border-width))));
78
92
  }
79
93
 
80
94
  @media (forced-colors: active) {
81
95
  input:is(
82
- [type="text"],
83
- [type="email"],
84
- [type="password"],
85
- [type="url"],
86
- [type="tel"],
87
- [type="number"],
88
- [type="search"],
89
- [type="date"],
90
- [type="datetime-local"],
91
- [type="month"],
92
- [type="week"],
93
- [type="time"],
94
- [type="checkbox"],
95
- [type="radio"]
96
- ):focus-visible,
96
+ [type="text"],
97
+ [type="email"],
98
+ [type="password"],
99
+ [type="url"],
100
+ [type="tel"],
101
+ [type="number"],
102
+ [type="search"],
103
+ [type="date"],
104
+ [type="datetime-local"],
105
+ [type="month"],
106
+ [type="week"],
107
+ [type="time"],
108
+ [type="checkbox"],
109
+ [type="radio"]
110
+ ):focus-visible,
97
111
  textarea:focus-visible,
98
112
  select:focus-visible {
99
113
  outline: 2px solid Highlight !important;
@@ -127,24 +141,3 @@
127
141
  border-color: CanvasText;
128
142
  }
129
143
  }
130
-
131
- /* The user agent already promises both of these for a native `<dialog>` --
132
- `dialog:not([open]) { display: none }` and a centered `margin: auto` on the
133
- open box -- but both are user-agent origin, which loses to any author rule
134
- at all. `.card`'s own `display: block` (`surface`'s `@apply box block`)
135
- beats the first outright, so a `<dialog class="card">` renders open with no
136
- `open` attribute and no `.showModal()` call -- the registry has no
137
- dialog/modal component of its own, and `.card` is the closest primitive a
138
- consumer reaches for. A margin reset on every element -- Tailwind's own
139
- Preflight on the entries that carry it -- beats the second, so an opened
140
- dialog pins to the top-left instead of centering. Restated here, unlayered like
141
- the block above, for the same reason: a behavior override has to win over
142
- any layered rule whatever the specificity, and `.card` is layered in the
143
- utilities layer regardless of how it composes `box`. */
144
- dialog:not([open]) {
145
- display: none;
146
- }
147
-
148
- dialog[open] {
149
- margin: auto;
150
- }
@@ -22,14 +22,13 @@
22
22
  --_d-border: 100%;
23
23
 
24
24
  /* The public token first, so an aesthetic (glass's translucent ground) or a
25
- consumer names the ground outright; then the component's own quiet
26
- default, which `.card.soft`, a neutral `.panel`, and a neutral `.alert`
27
- write as a private rather than as the public token. Written as the public
28
- token, their own declaration beat glass's inherited one in a glass region,
29
- and once the aesthetics are layered below the components it beats glass
30
- on the same element too -- `.card.soft.glass` went opaque. Restated
31
- `initial` here so a quiet ground cannot inherit into a surface nested
32
- inside one, the way `box` restates `--_d-ground`. See
25
+ consumer names the ground outright; then the component's own quiet default,
26
+ which `.card.soft`, a neutral `.panel`, and a neutral `.alert` write as a
27
+ private rather than as the public token. Written as the public token, their
28
+ own declaration would beat glass's inherited one in a glass region, and
29
+ beat glass on the same element too (`.card.soft.glass` would go opaque).
30
+ Restated `initial` here so a quiet ground cannot inherit into a surface
31
+ nested inside one, the way `box` restates `--_d-ground`. See
33
32
  docs/internal/model.md#cascade-layers. */
34
33
  --_d-surface-ground: initial;
35
34
  --_d-ground: var(--ui-surface-ground, var(--_d-surface-ground, var(--color-background)));
@@ -47,16 +46,16 @@
47
46
  calc(var(--_corner-surface) * var(--ui-corner-tl, 1)) calc(var(--_corner-surface) * var(--ui-corner-tr, 1))
48
47
  calc(var(--_corner-surface) * var(--ui-corner-br, 1)) calc(var(--_corner-surface) * var(--ui-corner-bl, 1))
49
48
  );
50
- /* Prefixed property first, deliberately: written second the minifier folds the
51
- pair into the prefixed one alone, and the standard property never reaches
52
- `dist/` -- which drops the blur in Firefox (the one engine with
53
- `backdrop-filter` and not the `-webkit-` alias). Measured on the built CSS. */
54
49
  /* The painted layer: a pattern, a texture, or a gradient an aesthetic lays
55
50
  over the plate. Surfaces only, like the backdrop, so a control's label and
56
51
  `.select`'s chevron never sit on one. The aesthetic that sets it owns the
57
52
  contrast of the text over it, and a consumer's own `background-image` still
58
53
  beats this declaration. */
59
54
  background-image: var(--ui-surface-image, none);
55
+ /* Prefixed property first, deliberately: written second the minifier folds the
56
+ pair into the prefixed one alone, and the standard property never reaches
57
+ `dist/` -- which drops the blur in Firefox (the one engine with
58
+ `backdrop-filter` and not the `-webkit-` alias). Measured on the built CSS. */
60
59
  -webkit-backdrop-filter: var(--ui-backdrop, none);
61
60
  backdrop-filter: var(--ui-backdrop, none);
62
61
  }
package/dist/tw/theme.css CHANGED
@@ -21,8 +21,15 @@
21
21
  selectors below therefore set `color-scheme` and nothing else. Verified
22
22
  across Chromium, Firefox, and WebKit, including resolution through
23
23
  `color-mix()` and through Tailwind's `@theme`, which emits theme values as
24
- unregistered custom properties rather than `@property` registrations. */
25
- @theme {
24
+ unregistered custom properties rather than `@property` registrations.
25
+
26
+ `static`, because every token here is documented as public. Tailwind keeps a
27
+ plain `@theme` value only where it sees the entry read it, and `/theme`
28
+ carries no reset or component to read `--color-background`,
29
+ `--color-foreground`, `--color-text-secondary`, or `--font-default`, so it
30
+ shipped without them. `exports.test.ts` holds every compiled entry to the
31
+ tokens `docs/usage/theming.md` lists. */
32
+ @theme static {
26
33
  --font-default: "Segoe UI", system-ui, sans-serif;
27
34
 
28
35
  --color-primary: light-dark(var(--color-neutral-950), var(--color-neutral-50));
@@ -35,30 +42,25 @@
35
42
  --color-accent-hover: light-dark(var(--color-neutral-700), var(--color-neutral-300));
36
43
  --color-accent-subtle: light-dark(var(--color-neutral-100), var(--color-neutral-800));
37
44
  --color-accent-strong: light-dark(var(--color-neutral-800), var(--color-neutral-200));
38
- /* Light was neutral-300 (1.42:1 against the page, already under 3:1 by the
39
- text-control comment below) until a `.card.soft.edged` measured its own
40
- border against its own fill rather than against the page: 12% of
41
- `--intent-color` mixed into the ground and a line resolving to nearly the
42
- same tone landed the two 4 RGB units apart, which reads as no boundary at
43
- all. Dark's neutral-700 pairing already separated -- 27 units, still under
44
- 3:1 but at least legible -- so only light moved. Neutral-400 is not a new
45
- number: `.chunky-tile` already measured it at 2.48:1 against the light
46
- page and picked it for the same reason, "the heaviest grey that still
47
- leaves a field legible." Dark is unchanged. */
45
+ /* Neutral-400 in light: the heaviest grey that still leaves a field legible
46
+ (2.48:1 against the page). A lighter line lands within a few RGB units of a
47
+ `.card.soft.edged`'s own fill and reads as no boundary. Dark's neutral-700
48
+ already separates (27 units). */
48
49
  --color-border: light-dark(var(--color-neutral-400), var(--color-neutral-700));
50
+ /* Nothing in the package draws a border that darkens on hover; it is a
51
+ public override point for a consumer's own. */
52
+ --color-border-hover: light-dark(var(--color-neutral-600), var(--color-neutral-500));
49
53
 
50
54
  /* A text control's line is the only thing identifying it: the box is
51
55
  transparent at rest, so nothing else marks where typing goes.
52
- `--color-border` is neutral-300/700 and measures 1.42:1 on the light page,
53
- which is a line you can barely see.
56
+ `--color-border` is the quiet surface line, too light for that.
54
57
 
55
- One value rather than a `light-dark()` pair, and that is the point: the page
56
- is neutral-50 in light and neutral-950 in dark, so a tone halfway between
57
- them reads on both grounds at once. A text control draws it at
58
- `--_line-rest` rather than whole, which is what keeps the field quiet while
59
- leaving the pointer somewhere to go -- so the line as painted lands under
60
- the 3:1 WCAG 1.4.11 asks of a control boundary. Recorded in
61
- docs/accessibility.md. */
58
+ One value rather than a `light-dark()` pair: the page is neutral-50 in
59
+ light and neutral-950 in dark, so a tone halfway between them reads on both
60
+ grounds at once. A text control draws it at `--_line-rest` rather than
61
+ whole, which keeps the field quiet while leaving the pointer somewhere to
62
+ go, so the line as painted lands under the 3:1 WCAG 1.4.11 asks of a
63
+ control boundary. Recorded in docs/accessibility.md. */
62
64
  --color-control-border: var(--color-neutral-500);
63
65
  --color-background: light-dark(var(--color-neutral-50), var(--color-neutral-950));
64
66
  --color-foreground: light-dark(var(--color-neutral-100), var(--color-neutral-900));
@@ -68,40 +70,30 @@
68
70
  --color-text-contrast: light-dark(var(--color-neutral-50), var(--color-neutral-950));
69
71
  --color-text-hover: light-dark(var(--color-neutral-700), var(--color-neutral-200));
70
72
  /* No `--color-text-subtle`: the neutral intent points `--intent-subtle` at
71
- `--color-surface`, not at a text-family tint, so the slot the other families
72
- fill had no reader here. */
73
+ `--color-surface`, not at a text-family tint. */
73
74
  --color-text-strong: light-dark(var(--color-neutral-900), var(--color-neutral-100));
74
75
 
75
- /* The tooltip bubble is the one component whose plate is chosen rather than
76
- derived. Everything else composes its ground from the intent, and for the
77
- bubble that produced the same 20% of the page's ink in both themes -- which
78
- steps *lighter* than the surface on a dark page and *darker* on a light one.
79
- Dark got the conventional reading, a floating thing lifted off its page;
80
- light got a mid-grey slab two steps darker than every card on the screen,
81
- carrying dark ink. Measured, the light bubble was neutral-400 at 2.01:1
82
- against the page while the dark one was 2.27:1 the other way.
76
+ /* The tooltip bubble's plate is chosen per theme rather than derived: 20% of
77
+ the page's ink over the surface tone steps lighter than the surface on a
78
+ dark page and darker on a light one, and one derivation cannot serve both
79
+ directions. Both ends read as lifted: near-white over a light page, a mid
80
+ grey over a dark one.
83
81
 
84
- One derivation cannot serve both directions, so the pair is stated. Both
85
- ends read as lifted: near-white over a light page, a mid grey over a dark
86
- one. Near-white is only 1.04:1 against the page, so the bubble draws a
87
- hairline as well -- see `components/tooltip.css`. */
82
+ Near-white is only 1.04:1 against the page, so the bubble draws a hairline
83
+ as well -- see `components/tooltip.css`. */
88
84
  --color-tooltip: light-dark(var(--color-white), var(--color-neutral-700));
89
85
  --color-tooltip-contrast: light-dark(var(--color-neutral-900), var(--color-neutral-100));
90
86
 
91
- /* Emerald-700 rather than 600. A filled button's label is normal-size text, so
92
- WCAG 1.4.3 asks 4.5:1, and white on emerald-600 measures 3.50:1 -- it passed
93
- only as large text, which `.btn` is not. One step darker carries the white
94
- label and leaves the hue where it was. */
87
+ /* Emerald-700 rather than 600: a filled button's label is normal-size text
88
+ (WCAG 1.4.3 asks 4.5:1), and white on emerald-600 measures 3.50:1. */
95
89
  --color-success: var(--color-emerald-700);
96
90
  --color-success-contrast: var(--color-neutral-50);
97
- /* Baked rather than mixed live. `box-hover` nests this token two `color-mix()`
98
- levels deeper to compose the fill and the edge, and three levels of
99
- `color-mix()` over a `light-dark()` crashes the WebKit renderer outright on
100
- hover. Confirmed on all four semantic intents, not only this one. Each
101
- pinned pair below is
102
- `color-mix(in srgb, <base> 82%, <strong>)`, resolved once per theme and
103
- screenshotted back so the blend stays exact; re-bake it if the base or
104
- strong tone it was mixed from changes. */
91
+ /* Baked rather than mixed live: `box-hover` nests this token two
92
+ `color-mix()` levels deeper to compose the fill and the edge, and three
93
+ levels of `color-mix()` over a `light-dark()` crash the WebKit renderer on
94
+ hover. Each pinned pair below is `color-mix(in srgb, <base> 82%,
95
+ <strong>)`, resolved once per theme; re-bake it if the base or strong tone
96
+ it was mixed from changes. */
105
97
  --color-success-hover: light-dark(#007552, #008e66);
106
98
  /* The dark ground is `-900` to make room for the `-300` ink above it. `-subtle`
107
99
  is what `kbd`, `code`, `pre` and the tooltip bubble rest on, and those are the
@@ -110,21 +102,16 @@
110
102
  WCAG 1.4.3 asks of normal text -- and the other three sat at 4.9 to 5.0 with
111
103
  no headroom. One step darker puts all four past 9:1. */
112
104
  --color-success-subtle: light-dark(var(--color-emerald-100), var(--color-emerald-900));
113
- /* The dark tone is `-300`, not `-100`. `-strong` is what a partial fill prints
114
- -- the label of a soft badge, the dot of a soft radio -- and on a dark page
115
- the `-100` shade of every hue is a pale tint that reads white at small
116
- sizes. The intent's colour survived only in the fill and the border, so a
117
- soft success badge and a soft destructive one had the same white label and a
118
- radio's dot stopped matching its own ring. `-300` keeps the hue and stays
119
- far above the contrast a mark or a label needs. Light keeps `-800`, which
120
- was never the problem: it is saturated and dark against a light page. */
105
+ /* The dark tone is `-300`, not `-100`: `-strong` is what a partial fill
106
+ prints -- the label of a soft badge, the dot of a soft radio -- and on a
107
+ dark page the `-100` shade of every hue is a pale tint that reads white at
108
+ small sizes. `-300` keeps the hue. Light keeps `-800`, which is saturated
109
+ and dark against a light page. */
121
110
  --color-success-strong: light-dark(var(--color-emerald-800), var(--color-emerald-300));
122
- /* Amber is the one family that cannot solve this by darkening. White on
123
- amber-600 measures 3.07:1, and the amber dark enough to carry white has
124
- stopped being amber -- it reads brown, which is not the colour a warning is
125
- supposed to be. So the ink moves instead of the hue: dark ink on amber is
126
- what a warning looks like nearly everywhere, and it is the only intent whose
127
- contrast tone is not the page colour. */
111
+ /* Amber cannot solve contrast by darkening: white on amber-600 measures
112
+ 3.07:1, and amber dark enough to carry white reads brown. So the ink moves
113
+ instead of the hue: dark ink on amber is what a warning looks like, and
114
+ warning is the only intent whose contrast tone is not the page colour. */
128
115
  --color-warning: var(--color-amber-600);
129
116
  --color-warning-contrast: var(--color-neutral-950);
130
117
  /* Same crash, same fix as `--color-success-hover` above. */
@@ -143,23 +130,13 @@
143
130
  --color-info-hover: light-dark(#4b36e9, #5e4ff9);
144
131
  --color-info-subtle: light-dark(var(--color-indigo-100), var(--color-indigo-900));
145
132
  --color-info-strong: light-dark(var(--color-indigo-800), var(--color-indigo-300));
146
-
147
- --breakpoint-xs: 30rem;
148
- --breakpoint-2xl: 90rem;
149
133
  }
150
134
 
151
- /* Tailwind only keeps a `@theme` variable in the compiled output when it
152
- detects that variable being used somewhere in this package's own source --
153
- everything above earns its keep that way, through `intent.css` or a
154
- component reading it. `--color-border-hover` is different: `docs/usage/
155
- theming.md` documents it as a public override point like every other row
156
- in that table, but nothing in this package draws a border that darkens on
157
- hover, so nothing here ever references it, and Tailwind quietly dropped it
158
- from every compiled stylesheet. `static` forces it to compile regardless,
159
- so the token a consumer's docs promise is the token their `var()` actually
160
- resolves. */
161
- @theme static {
162
- --color-border-hover: light-dark(var(--color-neutral-600), var(--color-neutral-500));
135
+ /* Not `static`: a breakpoint is a variant for a `/tw` consumer's own build and
136
+ means nothing as a custom property, since a media query cannot read `var()`.
137
+ Tailwind keeps the variable only where an `xs:` utility uses it. */
138
+ @theme {
139
+ --breakpoint-xs: 30rem;
163
140
  }
164
141
 
165
142
  /* The foundation tokens and the colour-scheme selectors, in `theme` so every
@@ -202,15 +179,11 @@
202
179
  --elevation-lg: 0 20px 25px -5px var(--elevation-color), 0 8px 10px -6px var(--elevation-color);
203
180
 
204
181
  --focus-ring: color-mix(in srgb, var(--color-primary) 64%, transparent);
205
- /* Negative, so the ring is drawn over the edge rather than beside it: at a
206
- positive offset the outline floats a gap away and reads as a second, detached
207
- frame around the control. Pulled in by exactly the material line width, the
208
- ring lands on top of the line and a focused control reads as its own edge
209
- thickening -- which is what a focus indicator is meant to look like.
210
-
211
- A component drawing no line at rest loses nothing: the outline moves one
212
- pixel inward over its own fill, which at a hairline is not a visible change. */
213
- --focus-ring-offset: calc(-1 * var(--border-width));
182
+ /* `--focus-ring-offset` is not declared: its default is the negative of the
183
+ line width of whatever is focused, which only the focused element knows,
184
+ so the outline computes it there -- see `reset.css`. Declared here it
185
+ resolved against the base 1px once and inherited, and under a 2px
186
+ aesthetic the ring covered half the edge and left a stripe of it inside. */
214
187
  --focus-ring-width: 3px;
215
188
 
216
189
  --motion-duration-fast: 120ms;
@@ -254,24 +227,24 @@
254
227
  }
255
228
 
256
229
  /* The first arm mirrors the `color-scheme: dark` block above, all three
257
- spellings included -- a consumer writing `.theme-dark` for the token theme
258
- was otherwise left without the matching `dark:` variant.
230
+ spellings included.
231
+
232
+ The second arm follows the OS preference. `--color-*` tokens resolve through
233
+ `light-dark()` against the computed `color-scheme`, so with no explicit
234
+ selector `:root`'s `color-scheme: light dark` already follows it -- see
235
+ `concepts.md#theme-selection`. A selector list cannot query computed style,
236
+ but it can match "OS prefers dark, and not under a forced-light subtree" with
237
+ a plain descendant exclusion: `:not(:where(.light, .light *, ...))` matches
238
+ only when no ancestor (or the element itself) carries one of the three light
239
+ spellings. The exclusion cannot see a subtree forced light by other means,
240
+ where `light-dark()` would still honor it; that is out of contract, since
241
+ those three spellings are the only supported way to force a theme.
259
242
 
260
- The second arm closes the gap that left open: `--color-*` tokens resolve
261
- through `light-dark()` against the computed `color-scheme`, so with no
262
- explicit selector at all `:root`'s `color-scheme: light dark` already
263
- follows the OS preference -- see `concepts.md#theme-selection`. A selector
264
- list cannot query computed style the way `light-dark()` does, but it can
265
- still match "OS prefers dark, and not under a forced-light subtree" with a
266
- plain descendant exclusion: `:not(:where(.light, .light *, ...))` matches
267
- only when no ancestor (or the element itself) carries one of the three
268
- light spellings. That is the one case the exclusion cannot see -- a
269
- subtree forced light through some means other than these three selectors,
270
- where `light-dark()` would still honor it but this variant could not
271
- detect it. Out of contract: those three spellings are the only supported
272
- way to force a theme, so nothing else is expected to set `color-scheme`
273
- directly. Each arm is independent, so an explicit selector still wins at
274
- any depth even inside a subtree the other arm would otherwise match. */
243
+ Each arm is independent, so an explicit selector wins over the OS preference
244
+ at any depth. It does not win over an explicit selector of the opposite
245
+ theme further up: `.dark *` matches every descendant of a `.dark` element,
246
+ including one inside a nested `.light`, so a `dark:` utility still fires
247
+ there although its tokens are light. `setup.md#dark-variant` documents it. */
275
248
  @custom-variant dark {
276
249
  &:where(.dark, .dark *, .theme-dark, .theme-dark *, [data-theme="dark"], [data-theme="dark"] *) {
277
250
  @slot;
@@ -288,9 +261,20 @@
288
261
  and nothing an aesthetic layers on top. Unlayered, so it beats every layered
289
262
  declaration of these tokens -- an aesthetic's and a consumer's utility alike --
290
263
  the way the other `forced-colors` blocks do. See
291
- docs/internal/model.md#material-tokens. */
264
+ docs/internal/model.md#material-tokens.
265
+
266
+ The border comes back and the clip goes. Forced colours remove every box
267
+ shadow, so an aesthetic that draws its edge as an inset ring and takes the
268
+ border out with `--ui-border-max` (`.pixel`) left buttons, fields, and cards
269
+ with no boundary at all. With the ceiling lifted the border is drawn at the
270
+ aesthetic's own width and the system colours it; with no clip the corners
271
+ it would cut are whole and the focus outline shows. */
292
272
  @media (forced-colors: active) {
293
273
  * {
274
+ --ui-border-max: initial;
275
+ --ui-clip: none;
276
+ --ui-clip-tight: none;
277
+ --ui-shadow-edge: initial;
294
278
  --ui-line-style: solid;
295
279
  --ui-rule-style: solid;
296
280
  --ui-halo-ink: initial;