@codenhub/styles 0.4.0 → 0.6.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 (61) hide show
  1. package/README.md +7 -7
  2. package/dist/aesthetics/chunky-tile.css +1 -1
  3. package/dist/aesthetics/cyber.css +1 -1
  4. package/dist/aesthetics/glass.css +1 -1
  5. package/dist/aesthetics/index.css +1 -1
  6. package/dist/aesthetics/neobrutalism.css +1 -1
  7. package/dist/aesthetics/pixel.css +1 -1
  8. package/dist/aesthetics/sketch.css +1 -0
  9. package/dist/components.css +1 -1
  10. package/dist/index.css +1 -1
  11. package/dist/native.css +1 -1
  12. package/dist/palette.css +1 -1
  13. package/dist/theme.css +1 -1
  14. package/dist/tw/aesthetics/chunky-tile.css +144 -128
  15. package/dist/tw/aesthetics/cyber.css +138 -96
  16. package/dist/tw/aesthetics/glass.css +332 -66
  17. package/dist/tw/aesthetics/index.css +3 -1
  18. package/dist/tw/aesthetics/neobrutalism.css +113 -54
  19. package/dist/tw/aesthetics/pixel.css +137 -87
  20. package/dist/tw/aesthetics/sketch.css +333 -0
  21. package/dist/tw/box.css +139 -52
  22. package/dist/tw/components/button.css +67 -28
  23. package/dist/tw/components/feedback.css +85 -67
  24. package/dist/tw/components/form.css +300 -230
  25. package/dist/tw/components/index.css +17 -9
  26. package/dist/tw/components/loader.css +38 -30
  27. package/dist/tw/components/surface.css +89 -129
  28. package/dist/tw/components/tooltip.css +91 -47
  29. package/dist/tw/corner.css +26 -0
  30. package/dist/tw/elevation.css +19 -28
  31. package/dist/tw/index.css +15 -19
  32. package/dist/tw/intent.css +47 -32
  33. package/dist/tw/native.css +58 -42
  34. package/dist/tw/palette.css +455 -446
  35. package/dist/tw/presentation.css +20 -29
  36. package/dist/tw/reset.css +21 -28
  37. package/dist/tw/surface.css +61 -0
  38. package/dist/tw/theme.css +156 -153
  39. package/dist/tw/utilities/content.css +283 -205
  40. package/dist/tw/utilities/layout.css +17 -27
  41. package/docs/accessibility.md +63 -46
  42. package/docs/changelog/0.5.0.md +38 -0
  43. package/docs/changelog/0.6.0.md +52 -0
  44. package/docs/changelog/index.md +2 -0
  45. package/docs/concepts.md +6 -6
  46. package/docs/index.md +12 -3
  47. package/docs/integrating/nextjs.md +1 -1
  48. package/docs/integrating/tailwind.md +14 -6
  49. package/docs/setup.md +28 -8
  50. package/docs/usage/aesthetics.md +88 -18
  51. package/docs/usage/buttons.md +14 -14
  52. package/docs/usage/composing.md +12 -12
  53. package/docs/usage/content-and-layout.md +25 -24
  54. package/docs/usage/customizing.md +92 -55
  55. package/docs/usage/feedback.md +5 -3
  56. package/docs/usage/forms.md +61 -45
  57. package/docs/usage/theming.md +41 -42
  58. package/docs/usage/tooltips.md +37 -17
  59. package/llms-full.txt +590 -304
  60. package/llms.txt +6 -0
  61. package/package.json +15 -5
@@ -1,22 +1,17 @@
1
+ /* Imported into the `components` layer by `theme.css`, below every utility, so
2
+ a consumer's own utility or unlayered CSS beats these classes. See
3
+ docs/internal/model.md#cascade-layers. */
4
+
1
5
  /* Presentation answers two independent questions, and each is its own closed
2
6
  set: how much of the intent color fills the box, and whether the box draws a
3
7
  line at its boundary. Exactly one value from each set applies, so the six
4
8
  combinations are the whole vocabulary.
5
9
 
6
- A fill class never decides an edge and an edge class never decides a fill.
7
- `.solid` briefly broke that rule, writing `--ui-border: 0%` alongside its fill
8
- so that a filled box could not be ringed in another colour. It bought a real
9
- fix -- the edge blend runs the line toward the fill, which lands *on* the fill
10
- when the fill is opaque and on a second coat of the same tint when it is not,
11
- so every capped intent that drew a line drew a visible ring over its own plate
12
- -- at the cost of the one property that makes the six combinations readable:
13
- that each half means only what it says. Two axes that are independent in two
14
- cases out of three are not axes.
15
-
16
- The ring is a fault in the blend rather than in the class, and it is recorded
17
- as such: `.solid.edged` on a capped intent measures 1.53:1 against its own
18
- plate in light and 1.82:1 in dark. `.solid.edgeless` is how a consumer asks
19
- for a filled box with no line.
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.
20
15
 
21
16
  Presentation declares only unitless numbers and percentages. A custom
22
17
  property's value is resolved on the element that declares it, so anything
@@ -28,8 +23,8 @@
28
23
  container set the look for its subtree while any element still opts out.
29
24
 
30
25
  Hover is derived from these values rather than declared beside them:
31
- `min(100%, resting fill + --hover-step)`, always in `--intent-hover`. There is
32
- 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. */
33
28
 
34
29
  /* Filled with the intent color. Text is the contrast tone. */
35
30
  .solid {
@@ -43,11 +38,9 @@
43
38
  --ui-fg-on-fill: 0%;
44
39
  }
45
40
 
46
- /* No fill at rest; the derived hover gives it one. Text is the intent color.
47
-
48
- Named `.ghost` rather than `.bare` because `.bare` and `.edgeless` both read
49
- as "less of something", so an author wanting no border reached for `.bare`
50
- 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.
51
44
 
52
45
  A component that rests on a ground rather than on nothing keeps that ground
53
46
  here: `.ghost` is zero fill, not zero paint. `.data-table` is the exception
@@ -57,16 +50,14 @@
57
50
  --ui-fg-on-fill: 0%;
58
51
  }
59
52
 
60
- /* A line in the intent color, at the aesthetic's material width. The width comes
61
- from the aesthetic and nothing else: there is no presentation scale on it, so
62
- 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.
63
56
 
64
57
  The edge is the silhouette and nothing else. Rules *inside* a component that
65
- has an inside are a separate decision with its own class -- `.data-table.ruled`
66
- -- because tying them here made them arrive by implication: a table drew rules
67
- only because its published edge default happened to be `edged`, so whether a
68
- consumer got them depended on a default they had not read and could not name.
69
- 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. */
70
61
  .edged {
71
62
  --ui-border: 100%;
72
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,9 +75,20 @@
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) {
@@ -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
- }
@@ -0,0 +1,61 @@
1
+ @reference "./box.css";
2
+
3
+ /* What every surface shares. Two slots exist here and nowhere else, and that is
4
+ how an aesthetic reaches surfaces without naming one: only a surface resolves
5
+ `--ui-backdrop`, so glass blurs cards and panels while controls stay solid,
6
+ and only a surface resolves `--ui-surface-shadow`, so a two-layer drop shadow
7
+ does not land under every button on the page.
8
+
9
+ A primitive beside `box`, and like `box` it imports neither the theme nor
10
+ Tailwind: `feedback.css` and `tooltip.css` `@reference` it, and a referenced
11
+ file that reaches the theme strips its tokens from every entry that pulls it
12
+ in. See docs/internal/model.md (Which files import the theme). */
13
+ @utility surface {
14
+ @apply box block;
15
+
16
+ /* Bare and edged. `box` reads these three with no fallback, so `surface` --
17
+ typeable on its own -- owes all three; `card` restates its own. No shadow
18
+ geometry: depth is asked for (`.raised`/`.floating`, an aesthetic, the
19
+ registry elevation) rather than carried, so a plain card is flat. */
20
+ --_d-fill: 0%;
21
+ --_d-fg-on-fill: 0%;
22
+ --_d-border: 100%;
23
+
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 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
32
+ docs/internal/model.md#cascade-layers. */
33
+ --_d-surface-ground: initial;
34
+ --_d-ground: var(--ui-surface-ground, var(--_d-surface-ground, var(--color-background)));
35
+
36
+ /* The complete-shadow slot for a shadow that is not one layer (glass tucks two
37
+ under with negative spread). Only `surface` reads it -- through `--ui-shadow`
38
+ it would land an 18px drop under every button and chip -- like the backdrop. */
39
+ --_shadow: var(--ui-surface-shadow, var(--_shadow-parts)) var(--_halo-layer,);
40
+
41
+ /* The surface pair of `box`'s corner: the aesthetic's surface length, scaled
42
+ by the padding step and patterned by the same four corner switches. */
43
+ --_corner-surface: calc(var(--ui-corner-surface, var(--radius-surface)) * var(--_scale));
44
+ border-radius: var(
45
+ --ui-radius-surface,
46
+ calc(var(--_corner-surface) * var(--ui-corner-tl, 1)) calc(var(--_corner-surface) * var(--ui-corner-tr, 1))
47
+ calc(var(--_corner-surface) * var(--ui-corner-br, 1)) calc(var(--_corner-surface) * var(--ui-corner-bl, 1))
48
+ );
49
+ /* The painted layer: a pattern, a texture, or a gradient an aesthetic lays
50
+ over the plate. Surfaces only, like the backdrop, so a control's label and
51
+ `.select`'s chevron never sit on one. The aesthetic that sets it owns the
52
+ contrast of the text over it, and a consumer's own `background-image` still
53
+ beats this declaration. */
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. */
59
+ -webkit-backdrop-filter: var(--ui-backdrop, none);
60
+ backdrop-filter: var(--ui-backdrop, none);
61
+ }
package/dist/tw/theme.css CHANGED
@@ -1,7 +1,18 @@
1
- @import "tailwindcss/theme.css";
2
- @import "./intent.css";
3
- @import "./presentation.css";
4
- @import "./elevation.css";
1
+ /* The package's layer map, declared before anything uses a layer. A browser
2
+ orders layers by the first time it meets each name, so a sheet that opened
3
+ `components` first would rank it below `theme` and `base`; stating Tailwind's
4
+ own order here, and at the top of every entrypoint, makes every load order
5
+ rank the same. The four class families -- intent, presentation, elevation,
6
+ and every aesthetic -- sit in `components`, below every utility, so a
7
+ consumer's own utility or unlayered CSS beats them. Components are
8
+ `@utility` rules in `utilities`. See docs/internal/model.md#cascade-layers. */
9
+ @layer theme, base, components, utilities;
10
+
11
+ @import "tailwindcss/theme.css" layer(theme);
12
+ @import "./intent.css" layer(components);
13
+ @import "./presentation.css" layer(components);
14
+ @import "./elevation.css" layer(components);
15
+ @import "./corner.css" layer(components);
5
16
  /* Everything that is not `src/` -- see the note in `index.css`. Keeps a
6
17
  `--color-*` reference in a docs example from shipping as a real theme entry. */
7
18
 
@@ -24,30 +35,22 @@
24
35
  --color-accent-hover: light-dark(var(--color-neutral-700), var(--color-neutral-300));
25
36
  --color-accent-subtle: light-dark(var(--color-neutral-100), var(--color-neutral-800));
26
37
  --color-accent-strong: light-dark(var(--color-neutral-800), var(--color-neutral-200));
27
- /* Light was neutral-300 (1.42:1 against the page, already under 3:1 by the
28
- text-control comment below) until a `.card.soft.edged` measured its own
29
- border against its own fill rather than against the page: 12% of
30
- `--intent-color` mixed into the ground and a line resolving to nearly the
31
- same tone landed the two 4 RGB units apart, which reads as no boundary at
32
- all. Dark's neutral-700 pairing already separated -- 27 units, still under
33
- 3:1 but at least legible -- so only light moved. Neutral-400 is not a new
34
- number: `.chunky-tile` already measured it at 2.48:1 against the light
35
- page and picked it for the same reason, "the heaviest grey that still
36
- leaves a field legible." Dark is unchanged. */
38
+ /* Neutral-400 in light: the heaviest grey that still leaves a field legible
39
+ (2.48:1 against the page). A lighter line lands within a few RGB units of a
40
+ `.card.soft.edged`'s own fill and reads as no boundary. Dark's neutral-700
41
+ already separates (27 units). */
37
42
  --color-border: light-dark(var(--color-neutral-400), var(--color-neutral-700));
38
43
 
39
44
  /* A text control's line is the only thing identifying it: the box is
40
45
  transparent at rest, so nothing else marks where typing goes.
41
- `--color-border` is neutral-300/700 and measures 1.42:1 on the light page,
42
- which is a line you can barely see.
46
+ `--color-border` is the quiet surface line, too light for that.
43
47
 
44
- One value rather than a `light-dark()` pair, and that is the point: the page
45
- is neutral-50 in light and neutral-950 in dark, so a tone halfway between
46
- them reads on both grounds at once. A text control draws it at
47
- `--_line-rest` rather than whole, which is what keeps the field quiet while
48
- leaving the pointer somewhere to go -- so the line as painted lands under
49
- the 3:1 WCAG 1.4.11 asks of a control boundary. Recorded in
50
- docs/accessibility.md. */
48
+ One value rather than a `light-dark()` pair: the page is neutral-50 in
49
+ light and neutral-950 in dark, so a tone halfway between them reads on both
50
+ grounds at once. A text control draws it at `--_line-rest` rather than
51
+ whole, which keeps the field quiet while leaving the pointer somewhere to
52
+ go, so the line as painted lands under the 3:1 WCAG 1.4.11 asks of a
53
+ control boundary. Recorded in docs/accessibility.md. */
51
54
  --color-control-border: var(--color-neutral-500);
52
55
  --color-background: light-dark(var(--color-neutral-50), var(--color-neutral-950));
53
56
  --color-foreground: light-dark(var(--color-neutral-100), var(--color-neutral-900));
@@ -57,40 +60,30 @@
57
60
  --color-text-contrast: light-dark(var(--color-neutral-50), var(--color-neutral-950));
58
61
  --color-text-hover: light-dark(var(--color-neutral-700), var(--color-neutral-200));
59
62
  /* No `--color-text-subtle`: the neutral intent points `--intent-subtle` at
60
- `--color-surface`, not at a text-family tint, so the slot the other families
61
- fill had no reader here. */
63
+ `--color-surface`, not at a text-family tint. */
62
64
  --color-text-strong: light-dark(var(--color-neutral-900), var(--color-neutral-100));
63
65
 
64
- /* The tooltip bubble is the one component whose plate is chosen rather than
65
- derived. Everything else composes its ground from the intent, and for the
66
- bubble that produced the same 20% of the page's ink in both themes -- which
67
- steps *lighter* than the surface on a dark page and *darker* on a light one.
68
- Dark got the conventional reading, a floating thing lifted off its page;
69
- light got a mid-grey slab two steps darker than every card on the screen,
70
- carrying dark ink. Measured, the light bubble was neutral-400 at 2.01:1
71
- against the page while the dark one was 2.27:1 the other way.
66
+ /* The tooltip bubble's plate is chosen per theme rather than derived: 20% of
67
+ the page's ink over the surface tone steps lighter than the surface on a
68
+ dark page and darker on a light one, and one derivation cannot serve both
69
+ directions. Both ends read as lifted: near-white over a light page, a mid
70
+ grey over a dark one.
72
71
 
73
- One derivation cannot serve both directions, so the pair is stated. Both
74
- ends read as lifted: near-white over a light page, a mid grey over a dark
75
- one. Near-white is only 1.04:1 against the page, so the bubble draws a
76
- hairline as well -- see `components/tooltip.css`. */
72
+ Near-white is only 1.04:1 against the page, so the bubble draws a hairline
73
+ as well -- see `components/tooltip.css`. */
77
74
  --color-tooltip: light-dark(var(--color-white), var(--color-neutral-700));
78
75
  --color-tooltip-contrast: light-dark(var(--color-neutral-900), var(--color-neutral-100));
79
76
 
80
- /* Emerald-700 rather than 600. A filled button's label is normal-size text, so
81
- WCAG 1.4.3 asks 4.5:1, and white on emerald-600 measures 3.50:1 -- it passed
82
- only as large text, which `.btn` is not. One step darker carries the white
83
- label and leaves the hue where it was. */
77
+ /* Emerald-700 rather than 600: a filled button's label is normal-size text
78
+ (WCAG 1.4.3 asks 4.5:1), and white on emerald-600 measures 3.50:1. */
84
79
  --color-success: var(--color-emerald-700);
85
80
  --color-success-contrast: var(--color-neutral-50);
86
- /* Baked rather than mixed live. `box-hover` nests this token two `color-mix()`
87
- levels deeper to compose the fill and the edge, and three levels of
88
- `color-mix()` over a `light-dark()` crashes the WebKit renderer outright on
89
- hover. Confirmed on all four semantic intents, not only this one. Each
90
- pinned pair below is
91
- `color-mix(in srgb, <base> 82%, <strong>)`, resolved once per theme and
92
- screenshotted back so the blend stays exact; re-bake it if the base or
93
- strong tone it was mixed from changes. */
81
+ /* Baked rather than mixed live: `box-hover` nests this token two
82
+ `color-mix()` levels deeper to compose the fill and the edge, and three
83
+ levels of `color-mix()` over a `light-dark()` crash the WebKit renderer on
84
+ hover. Each pinned pair below is `color-mix(in srgb, <base> 82%,
85
+ <strong>)`, resolved once per theme; re-bake it if the base or strong tone
86
+ it was mixed from changes. */
94
87
  --color-success-hover: light-dark(#007552, #008e66);
95
88
  /* The dark ground is `-900` to make room for the `-300` ink above it. `-subtle`
96
89
  is what `kbd`, `code`, `pre` and the tooltip bubble rest on, and those are the
@@ -99,21 +92,16 @@
99
92
  WCAG 1.4.3 asks of normal text -- and the other three sat at 4.9 to 5.0 with
100
93
  no headroom. One step darker puts all four past 9:1. */
101
94
  --color-success-subtle: light-dark(var(--color-emerald-100), var(--color-emerald-900));
102
- /* The dark tone is `-300`, not `-100`. `-strong` is what a partial fill prints
103
- -- the label of a soft badge, the dot of a soft radio -- and on a dark page
104
- the `-100` shade of every hue is a pale tint that reads white at small
105
- sizes. The intent's colour survived only in the fill and the border, so a
106
- soft success badge and a soft destructive one had the same white label and a
107
- radio's dot stopped matching its own ring. `-300` keeps the hue and stays
108
- far above the contrast a mark or a label needs. Light keeps `-800`, which
109
- was never the problem: it is saturated and dark against a light page. */
95
+ /* The dark tone is `-300`, not `-100`: `-strong` is what a partial fill
96
+ prints -- the label of a soft badge, the dot of a soft radio -- and on a
97
+ dark page the `-100` shade of every hue is a pale tint that reads white at
98
+ small sizes. `-300` keeps the hue. Light keeps `-800`, which is saturated
99
+ and dark against a light page. */
110
100
  --color-success-strong: light-dark(var(--color-emerald-800), var(--color-emerald-300));
111
- /* Amber is the one family that cannot solve this by darkening. White on
112
- amber-600 measures 3.07:1, and the amber dark enough to carry white has
113
- stopped being amber -- it reads brown, which is not the colour a warning is
114
- supposed to be. So the ink moves instead of the hue: dark ink on amber is
115
- what a warning looks like nearly everywhere, and it is the only intent whose
116
- contrast tone is not the page colour. */
101
+ /* Amber cannot solve contrast by darkening: white on amber-600 measures
102
+ 3.07:1, and amber dark enough to carry white reads brown. So the ink moves
103
+ instead of the hue: dark ink on amber is what a warning looks like, and
104
+ warning is the only intent whose contrast tone is not the page colour. */
117
105
  --color-warning: var(--color-amber-600);
118
106
  --color-warning-contrast: var(--color-neutral-950);
119
107
  /* Same crash, same fix as `--color-success-hover` above. */
@@ -134,7 +122,6 @@
134
122
  --color-info-strong: light-dark(var(--color-indigo-800), var(--color-indigo-300));
135
123
 
136
124
  --breakpoint-xs: 30rem;
137
- --breakpoint-2xl: 90rem;
138
125
  }
139
126
 
140
127
  /* Tailwind only keeps a `@theme` variable in the compiled output when it
@@ -151,111 +138,112 @@
151
138
  --color-border-hover: light-dark(var(--color-neutral-600), var(--color-neutral-500));
152
139
  }
153
140
 
154
- :root {
155
- /* Both keywords: the system preference decides, and the explicit theme
156
- selectors below override it at any depth because `color-scheme` inherits. */
157
- color-scheme: light dark;
158
-
159
- --container-narrow: 48rem;
160
- --container-max: 80rem;
161
- --container-wide: 90rem;
162
- --layout-gutter: clamp(1rem, 4vw, 2rem);
163
- --layout-section-block: clamp(4rem, 9vw, 7rem);
164
- --layout-gap: 1rem;
165
- --layout-grid-min: 16rem;
166
-
167
- --radius-small: 0.25rem;
168
- --radius-control: 0.5rem;
169
- --radius-surface: 0.875rem;
170
- --control-height: 2.5rem;
171
- --border-width: 1px;
141
+ /* The foundation tokens and the colour-scheme selectors, in `theme` so every
142
+ class family in `components` beats them on the element both land on -- an
143
+ aesthetic on `<html>` keeps its own press and depth colour over `:root`'s,
144
+ whatever order the sheets load in. */
145
+ @layer theme {
146
+ :root {
147
+ /* Both keywords: the system preference decides, and the explicit theme
148
+ selectors below override it at any depth because `color-scheme` inherits. */
149
+ color-scheme: light dark;
172
150
 
173
- /* `light-dark()` takes colors only, so the shadow color is the token and the
174
- three elevations compose from it. Dark themes need a much heavier color to
175
- read at all against a dark background. */
176
- --elevation-color: light-dark(rgb(15 23 42 / 0.08), rgb(0 0 0 / 0.55));
177
- /* Four rungs: no shadow, then a Tailwind-style sm/md/lg progression, so a
178
- consumer unsure which step to reach for has a named "no depth" value
179
- instead of inferring it by omitting a shadow declaration. The elevation
180
- modifier classes (`.elevation-none`/`.elevation-sm`/`.elevation-md` in
181
- elevation.css) share this naming on purpose -- same weight, same word --
182
- though they multiply an aesthetic's shadow parts rather than declaring a
183
- fixed value, so the two are not read from the same place. */
184
- --elevation-none: none;
185
- --elevation-sm: 0 1px 2px var(--elevation-color), 0 1px 3px var(--elevation-color);
186
- --elevation-md: 0 4px 6px -1px var(--elevation-color), 0 2px 4px -2px var(--elevation-color);
187
- --elevation-lg: 0 20px 25px -5px var(--elevation-color), 0 8px 10px -6px var(--elevation-color);
151
+ --container-narrow: 48rem;
152
+ --container-max: 80rem;
153
+ --container-wide: 90rem;
154
+ --layout-gutter: clamp(1rem, 4vw, 2rem);
155
+ --layout-section-block: clamp(4rem, 9vw, 7rem);
156
+ --layout-gap: 1rem;
157
+ --layout-grid-min: 16rem;
188
158
 
189
- --focus-ring: color-mix(in srgb, var(--color-primary) 64%, transparent);
190
- /* Negative, so the ring is drawn over the edge rather than beside it: at a
191
- positive offset the outline floats a gap away and reads as a second, detached
192
- frame around the control. Pulled in by exactly the material line width, the
193
- ring lands on top of the line and a focused control reads as its own edge
194
- thickening -- which is what a focus indicator is meant to look like.
159
+ --radius-small: 0.25rem;
160
+ --radius-control: 0.5rem;
161
+ --radius-surface: 0.875rem;
162
+ --control-height: 2.5rem;
163
+ --border-width: 1px;
195
164
 
196
- A component drawing no line at rest loses nothing: the outline moves one
197
- pixel inward over its own fill, which at a hairline is not a visible change. */
198
- --focus-ring-offset: calc(-1 * var(--border-width));
199
- --focus-ring-width: 3px;
165
+ /* `light-dark()` takes colors only, so the shadow color is the token and the
166
+ three elevations compose from it. Dark themes need a much heavier color to
167
+ read at all against a dark background. */
168
+ --elevation-color: light-dark(rgb(15 23 42 / 0.08), rgb(0 0 0 / 0.55));
169
+ /* Four rungs: no shadow, then a Tailwind-style sm/md/lg progression, so a
170
+ consumer unsure which step to reach for has a named "no depth" value
171
+ instead of inferring it by omitting a shadow declaration. These are raw
172
+ shadows for elements a consumer styles; the elevation modifier classes
173
+ (`.flat`/`.raised`/`.floating` in elevation.css) multiply an aesthetic's
174
+ shadow parts instead, and are not read from here. */
175
+ --elevation-none: none;
176
+ --elevation-sm: 0 1px 2px var(--elevation-color), 0 1px 3px var(--elevation-color);
177
+ --elevation-md: 0 4px 6px -1px var(--elevation-color), 0 2px 4px -2px var(--elevation-color);
178
+ --elevation-lg: 0 20px 25px -5px var(--elevation-color), 0 8px 10px -6px var(--elevation-color);
200
179
 
201
- --motion-duration-fast: 120ms;
202
- --motion-duration-normal: 200ms;
203
- --motion-duration-slow: 400ms;
204
- --motion-ease: cubic-bezier(0.2, 0, 0, 1);
180
+ --focus-ring: color-mix(in srgb, var(--color-primary) 64%, transparent);
181
+ /* `--focus-ring-offset` is not declared: its default is the negative of the
182
+ line width of whatever is focused, which only the focused element knows,
183
+ so the outline computes it there -- see `reset.css`. Declared here it
184
+ resolved against the base 1px once and inherited, and under a 2px
185
+ aesthetic the ring covered half the edge and left a stripe of it inside. */
186
+ --focus-ring-width: 3px;
205
187
 
206
- --z-popover: 40;
188
+ --motion-duration-fast: 120ms;
189
+ --motion-duration-normal: 200ms;
190
+ --motion-duration-slow: 400ms;
191
+ --motion-ease: cubic-bezier(0.2, 0, 0, 1);
207
192
 
208
- /* The one number the derived-hover formula spends: how much fill a hover adds
209
- on top of whatever the element rests at. */
210
- --hover-step: 14%;
211
- }
193
+ --z-popover: 40;
212
194
 
213
- /* The base look answers a press with a small scale-in. `box-active` applies it on
214
- `.btn` and `.card.interactive`; every aesthetic sets its own
215
- `--ui-active-transform` on its class, loaded after this file, so those win.
216
- Reduced motion opts out. Kept outside `@theme` because a media query cannot go
217
- inside it. */
218
- :root {
219
- --ui-active-transform: scale(0.97);
220
- }
195
+ /* The one number the derived-hover formula spends: how much fill a hover adds
196
+ on top of whatever the element rests at. */
197
+ --hover-step: 14%;
198
+ }
221
199
 
222
- @media (prefers-reduced-motion: reduce) {
200
+ /* The base look answers a press with a small scale-in. `box-active` applies it on
201
+ `.btn` and `.card.interactive`; every aesthetic sets its own
202
+ `--ui-active-transform` on its class, in `components`, so those win over
203
+ this whichever sheet loads first. Reduced motion opts out. Kept outside `@theme` because a media query cannot go
204
+ inside it. */
223
205
  :root {
224
- --ui-active-transform: none;
206
+ --ui-active-transform: scale(0.97);
225
207
  }
226
- }
227
208
 
228
- .light,
229
- .theme-light,
230
- [data-theme="light"] {
231
- color-scheme: light;
232
- }
209
+ @media (prefers-reduced-motion: reduce) {
210
+ :root {
211
+ --ui-active-transform: none;
212
+ }
213
+ }
233
214
 
234
- .dark,
235
- .theme-dark,
236
- [data-theme="dark"] {
237
- color-scheme: dark;
215
+ .light,
216
+ .theme-light,
217
+ [data-theme="light"] {
218
+ color-scheme: light;
219
+ }
220
+
221
+ .dark,
222
+ .theme-dark,
223
+ [data-theme="dark"] {
224
+ color-scheme: dark;
225
+ }
238
226
  }
239
227
 
240
228
  /* The first arm mirrors the `color-scheme: dark` block above, all three
241
- spellings included -- a consumer writing `.theme-dark` for the token theme
242
- was otherwise left without the matching `dark:` variant.
229
+ spellings included.
243
230
 
244
- The second arm closes the gap that left open: `--color-*` tokens resolve
245
- through `light-dark()` against the computed `color-scheme`, so with no
246
- explicit selector at all `:root`'s `color-scheme: light dark` already
247
- follows the OS preference -- see `concepts.md#theme-selection`. A selector
248
- list cannot query computed style the way `light-dark()` does, but it can
249
- still match "OS prefers dark, and not under a forced-light subtree" with a
250
- plain descendant exclusion: `:not(:where(.light, .light *, ...))` matches
251
- only when no ancestor (or the element itself) carries one of the three
252
- light spellings. That is the one case the exclusion cannot see -- a
253
- subtree forced light through some means other than these three selectors,
254
- where `light-dark()` would still honor it but this variant could not
255
- detect it. Out of contract: those three spellings are the only supported
256
- way to force a theme, so nothing else is expected to set `color-scheme`
257
- directly. Each arm is independent, so an explicit selector still wins at
258
- any depth even inside a subtree the other arm would otherwise match. */
231
+ The second arm follows the OS preference. `--color-*` tokens resolve through
232
+ `light-dark()` against the computed `color-scheme`, so with no explicit
233
+ selector `:root`'s `color-scheme: light dark` already follows it -- see
234
+ `concepts.md#theme-selection`. A selector list cannot query computed style,
235
+ but it can match "OS prefers dark, and not under a forced-light subtree" with
236
+ a plain descendant exclusion: `:not(:where(.light, .light *, ...))` matches
237
+ only when no ancestor (or the element itself) carries one of the three light
238
+ spellings. The exclusion cannot see a subtree forced light by other means,
239
+ where `light-dark()` would still honor it; that is out of contract, since
240
+ those three spellings are the only supported way to force a theme.
241
+
242
+ Each arm is independent, so an explicit selector wins over the OS preference
243
+ at any depth. It does not win over an explicit selector of the opposite
244
+ theme further up: `.dark *` matches every descendant of a `.dark` element,
245
+ including one inside a nested `.light`, so a `dark:` utility still fires
246
+ there although its tokens are light. `setup.md#dark-variant` documents it. */
259
247
  @custom-variant dark {
260
248
  &:where(.dark, .dark *, .theme-dark, .theme-dark *, [data-theme="dark"], [data-theme="dark"] *) {
261
249
  @slot;
@@ -267,3 +255,18 @@
267
255
  }
268
256
  }
269
257
  }
258
+
259
+ /* Forced colours draw the plain version of every structural part: a solid line
260
+ and nothing an aesthetic layers on top. Unlayered, so it beats every layered
261
+ declaration of these tokens -- an aesthetic's and a consumer's utility alike --
262
+ the way the other `forced-colors` blocks do. See
263
+ docs/internal/model.md#material-tokens. */
264
+ @media (forced-colors: active) {
265
+ * {
266
+ --ui-line-style: solid;
267
+ --ui-rule-style: solid;
268
+ --ui-halo-ink: initial;
269
+ --ui-surface-image: none;
270
+ --ui-label-shadow: none;
271
+ }
272
+ }