@codenhub/styles 0.0.3 → 0.1.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 (75) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +52 -0
  3. package/README.md +60 -90
  4. package/dist/aesthetics/chunky-tile.css +1 -0
  5. package/dist/aesthetics/glass.css +1 -0
  6. package/dist/aesthetics/index.css +2 -0
  7. package/dist/aesthetics/neobrutalism.css +1 -0
  8. package/dist/aesthetics/pixel.css +1 -0
  9. package/dist/components.css +2 -2
  10. package/dist/index.css +2 -2
  11. package/dist/native.css +2 -0
  12. package/dist/theme.css +2 -2
  13. package/dist/tw/aesthetics/chunky-tile.css +107 -0
  14. package/dist/tw/aesthetics/glass.css +66 -0
  15. package/dist/tw/aesthetics/index.css +9 -0
  16. package/dist/tw/aesthetics/neobrutalism.css +63 -0
  17. package/dist/tw/aesthetics/pixel.css +88 -0
  18. package/dist/tw/box.css +185 -0
  19. package/dist/tw/components/button.css +88 -0
  20. package/dist/tw/components/feedback.css +163 -0
  21. package/dist/tw/components/form.css +601 -0
  22. package/dist/tw/components/index.css +18 -0
  23. package/dist/tw/components/loader.css +122 -0
  24. package/dist/tw/components/surface.css +98 -0
  25. package/dist/tw/components/tooltip.css +132 -0
  26. package/dist/tw/elevation.css +39 -0
  27. package/dist/tw/index.css +12 -0
  28. package/dist/tw/intent.css +189 -0
  29. package/dist/tw/native.css +203 -0
  30. package/dist/tw/presentation.css +77 -0
  31. package/dist/tw/reset.css +142 -0
  32. package/dist/tw/theme.css +218 -0
  33. package/dist/tw/typography.css +34 -0
  34. package/dist/tw/utilities/content.css +349 -0
  35. package/dist/tw/utilities/index.css +6 -0
  36. package/dist/tw/utilities/layout.css +107 -0
  37. package/docs/accessibility.md +43 -14
  38. package/docs/concepts.md +94 -0
  39. package/docs/index.md +62 -73
  40. package/docs/integrating/astro.md +29 -0
  41. package/docs/integrating/index.md +32 -0
  42. package/docs/integrating/nextjs.md +44 -0
  43. package/docs/integrating/svelte.md +37 -0
  44. package/docs/integrating/tailwind.md +40 -0
  45. package/docs/integrating/vue.md +25 -0
  46. package/docs/setup.md +116 -0
  47. package/docs/usage/aesthetics.md +197 -0
  48. package/docs/usage/buttons.md +74 -0
  49. package/docs/usage/composing.md +218 -0
  50. package/docs/usage/content-and-layout.md +200 -0
  51. package/docs/usage/customizing.md +213 -0
  52. package/docs/usage/feedback.md +117 -0
  53. package/docs/usage/forms.md +177 -0
  54. package/docs/usage/index.md +32 -0
  55. package/docs/usage/theming.md +210 -0
  56. package/docs/usage/tooltips.md +45 -0
  57. package/llms-full.txt +2122 -0
  58. package/llms.txt +26 -0
  59. package/package.json +96 -32
  60. package/docs/classes.md +0 -198
  61. package/docs/roadmap.md +0 -34
  62. package/docs/tests.md +0 -128
  63. package/docs/tokens.md +0 -101
  64. package/src/components/button.css +0 -155
  65. package/src/components/feedback.css +0 -214
  66. package/src/components/form.css +0 -296
  67. package/src/components/index.css +0 -9
  68. package/src/components/loader.css +0 -56
  69. package/src/components/surface.css +0 -6
  70. package/src/components/tooltip.css +0 -60
  71. package/src/index.css +0 -7
  72. package/src/overrides.css +0 -76
  73. package/src/theme.css +0 -136
  74. package/src/typography.css +0 -30
  75. package/src/utilities.css +0 -40
package/llms-full.txt ADDED
@@ -0,0 +1,2122 @@
1
+ <!-- Source: README.md -->
2
+
3
+ # @codenhub/styles
4
+
5
+ CSS-only Codenhub design tokens, base styles, and composable UI helper classes.
6
+
7
+ ## Installation
8
+
9
+ ```sh
10
+ pnpm add @codenhub/styles
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ Import the compiled stylesheet for the complete token, reset, utility, and
16
+ component-class surface:
17
+
18
+ ```css
19
+ @import "@codenhub/styles";
20
+ ```
21
+
22
+ ```html
23
+ <main class="stack">
24
+ <p class="text-label">Status</p>
25
+ <h1 class="text-title">Ready to publish</h1>
26
+ <button class="btn primary">Continue</button>
27
+ </main>
28
+ ```
29
+
30
+ Apps using Tailwind CSS v4 can instead process the source entrypoint:
31
+
32
+ ```css
33
+ @import "tailwindcss";
34
+ @import "@codenhub/styles/tw";
35
+
36
+ @source "./src";
37
+ ```
38
+
39
+ The root follows the system color-scheme preference. Apply `.light` or `.dark`
40
+ to force a theme for the root or a subtree.
41
+
42
+ ```html
43
+ <html class="dark">
44
+ <body>
45
+ <button class="btn primary">Dark themed button</button>
46
+ </body>
47
+ </html>
48
+ ```
49
+
50
+ The aliases `.theme-light`, `.theme-dark`, and `data-theme="light|dark"` are
51
+ also supported. See [Concepts](./docs/concepts.md#theme-selection) for
52
+ precedence details.
53
+
54
+ `.solid`, `.soft`, and `.ghost` set how much intent fills a component; `.edged`
55
+ and `.edgeless` set whether it draws a boundary. Both axes cascade, so a
56
+ container sets the look below it and any element can still override it.
57
+ `.solid` also takes the boundary away, because a filled box ringed in another
58
+ color is what the blend behind `.edged` exists to prevent; `.solid.edged` is the
59
+ way back to a line.
60
+
61
+ ```html
62
+ <div class="soft">
63
+ <button class="btn primary">Soft</button>
64
+ <span class="badge success">Soft</span>
65
+ <button class="btn primary ghost edged">Outlined</button>
66
+ </div>
67
+ ```
68
+
69
+ Aesthetics decide what a component is made of. They are opt-in, so import the
70
+ stylesheet after the base one to make `.neobrutalism`, `.glass`, `.pixel`, and
71
+ `.chunky-tile` available. They cascade to any subtree the same way, and each is
72
+ also importable on its own from `@codenhub/styles/aesthetics/<name>`.
73
+
74
+ ```css
75
+ @import "@codenhub/styles";
76
+ @import "@codenhub/styles/aesthetics";
77
+ ```
78
+
79
+ ```html
80
+ <section class="neobrutalism">
81
+ <button class="btn primary">Thick ink and a hard shadow</button>
82
+ </section>
83
+ ```
84
+
85
+ ## Documentation
86
+
87
+ - [Documentation overview](./docs/index.md): What the package is, what it
88
+ does and does not do, and where to go next.
89
+ - [Setup](./docs/setup.md): Installation, entrypoints, and configuration.
90
+ - [Concepts](./docs/concepts.md): The three-axis model and the token system.
91
+ - [Usage](./docs/usage/index.md): Composing, theming, customizing,
92
+ aesthetics, and each component family.
93
+ - [Integrating](./docs/integrating/index.md): Wiring the stylesheet into
94
+ Next.js, Vue, Svelte, Astro, or a Tailwind CSS v4 build.
95
+ - [Accessibility](./docs/accessibility.md): CSS accessibility hooks and non-goals.
96
+
97
+ ## Requirements
98
+
99
+ | Requirement | Details |
100
+ | ------------- | -------------------------------------------------------------------------------------------------------------- |
101
+ | CSS imports | Consumer tooling must resolve package CSS imports. |
102
+ | Browsers | Chrome 123, Safari 17.5, or Firefox 121 and newer (`light-dark()` theming; `:has()` for `.input-group` state). |
103
+ | Tailwind CSS | Version 4 or newer is required only for `/tw` source entrypoints. |
104
+ | Accessibility | Consumers provide semantic HTML, ARIA, keyboard behavior, and focus management. |
105
+
106
+ ## Notes
107
+
108
+ This package has no JavaScript behavior. `--layout-gap` replaces the removed
109
+ `--layout-stack-gap` and `--layout-cluster-gap` tokens; no compatibility aliases
110
+ are provided.
111
+
112
+ ## License
113
+
114
+ Licensed under Apache-2.0. Embedded SVG icons derived from
115
+ [Lucide](https://lucide.dev) are ISC-licensed, and loaders derived from
116
+ [svg-spinners](https://github.com/n3r4zzurr0/svg-spinners) are MIT-licensed.
117
+ See [NOTICE](NOTICE) for the required notices.
118
+
119
+ <!-- Source: docs/index.md -->
120
+
121
+ # @codenhub/styles
122
+
123
+ `@codenhub/styles` is a CSS-only design system: tokens, a reset, typography and
124
+ layout utilities, and composable classes for buttons, forms, feedback,
125
+ surfaces, and tooltips. Every component separates three independent choices —
126
+ which color, how much of it, and what it's made of — so changing one never
127
+ requires a component-specific variant. Four optional aesthetics
128
+ (`.neobrutalism`, `.glass`, `.pixel`, `.chunky-tile`) restyle that same
129
+ component set without touching markup.
130
+
131
+ The package ships as ready-to-import compiled CSS, or as Tailwind CSS v4
132
+ source entrypoints for a project that already runs Tailwind. Both cover the
133
+ same classes; the difference is which build processes them.
134
+
135
+ ## What it does
136
+
137
+ - Publishes color, foundation, intent, presentation, and material design
138
+ tokens as CSS custom properties, themed with `light-dark()`.
139
+ - Provides a reset, focus-visible handling, and responsive layout/content
140
+ utilities.
141
+ - Provides composable classes for buttons, form controls and toggles, alerts,
142
+ badges, loaders, progress, cards/panels, tables, quotes, code, and
143
+ tooltips.
144
+ - Provides four opt-in aesthetics that restyle the same components by setting
145
+ material tokens, not by adding component variants.
146
+ - Provides classless mappings so plain HTML elements pick up base styling
147
+ without added classes, from a dedicated entrypoint.
148
+ - Publishes Tailwind CSS v4 source entrypoints alongside the compiled output.
149
+
150
+ ## What it does not do
151
+
152
+ - No JavaScript. There is no runtime, no framework bindings, and no build-time
153
+ code generation beyond the CSS itself.
154
+ - No semantic HTML, ARIA, keyboard behavior, focus management, validation, or
155
+ announcement timing. See [Accessibility](docs/accessibility.md) for the exact
156
+ line between what the CSS provides and what an application must still add.
157
+ - No theme-toggling logic. Theme selection is a class or attribute a consumer
158
+ applies; wiring a toggle to system preference, storage, or a user setting is
159
+ outside this package. The `@codenhub/theme` package covers that need for
160
+ consumers who want it.
161
+ - No shipped font binaries. `.pixel` and `.chunky-tile` accept a
162
+ consumer-supplied font and fall back to a system stack when none is given.
163
+ - No compatibility layer for removed tokens or classes. Breaking changes are
164
+ documented, not silently aliased.
165
+
166
+ ## Next steps
167
+
168
+ - [Setup](docs/setup.md): Install the package, choose an entrypoint, and render a
169
+ first component.
170
+ - [Concepts](docs/concepts.md): The three-axis model, the token system, and how
171
+ theme selection works — read this before the usage guides.
172
+ - [Usage](docs/usage/index.md): Guides for composing axes, theming, customizing,
173
+ aesthetics, and each component family.
174
+ - [Integrating](docs/integrating/index.md): Wiring the stylesheet into Next.js,
175
+ Vue, Svelte, Astro, or a Tailwind CSS v4 build.
176
+ - [Accessibility](docs/accessibility.md): CSS accessibility hooks and what
177
+ remains an application responsibility.
178
+
179
+ <!-- Source: docs/accessibility.md -->
180
+
181
+ # Accessibility responsibilities
182
+
183
+ This package provides CSS hooks for accessible states. It does not provide semantic HTML, ARIA attributes, keyboard behavior, focus management, validation, announcement timing, or JavaScript behavior.
184
+
185
+ ## Provided By CSS
186
+
187
+ | Feature | Behavior |
188
+ | ----------------------- | -------------------------------------------------------------------------------------------------------------------------- |
189
+ | `:focus-visible` | Global focus-visible outline using `--focus-ring`, `--focus-ring-width`, and `--focus-ring-offset`. |
190
+ | Form control focus | `.ipt`, `.textarea`, and `.select` draw the global focus ring and move their line to the intent color. |
191
+ | Control boundaries | Text controls rest at `--_line-rest` of their intent, under 3:1 by choice; hover and focus take the tone whole. |
192
+ | Toggle boundaries | `.checkbox`, `.radio`, and `.switch` draw their line whole, because it is the only thing marking an unchecked box. |
193
+ | Cascaded `.edgeless` | Floored on every text control: a container cannot erase the line of a field or toggle nobody classed. |
194
+ | Own `.edgeless` | On the element, `.ipt`, `.textarea`, `.select`, and `.switch` do drop their line and do not meet 1.4.11 at rest. |
195
+ | `.checkbox`/`.radio` | Never drop their line, on the element or from a container. `.edgeless` on them is unsupported, not merely discouraged. |
196
+ | Invalid controls | `[aria-invalid="true"]` applies destructive border/focus color on form controls. |
197
+ | Disabled controls | `[disabled]`, `[aria-disabled="true"]`, `[data-disabled]`, and `.disabled` apply disabled cursor/opacity where supported. |
198
+ | Current page | `[aria-current="page"]` applies primary color and stronger font weight. |
199
+ | Open state | `[data-state="open"]` styles supported surfaces and tooltips. |
200
+ | Reduced-motion loaders | Loader variants use animated embedded SVGs normally and static masks when `prefers-reduced-motion: reduce` matches. |
201
+ | Reduced-motion document | The reset additionally shortens document animations and transitions when `prefers-reduced-motion: reduce` matches. |
202
+ | Reduced transparency | `.glass` drops its blur and becomes an opaque surface when `prefers-reduced-transparency: reduce` matches. |
203
+ | Filled labels | Every intent's `.solid` label meets 1.4.3 (4.5:1) as normal text, in both themes. Lowest is success at 5.14:1. |
204
+ | Warning ink | `--color-warning-contrast` is the near-black tone, not the page tone. Amber dark enough to carry white is not amber. |
205
+ | Partial fills | Contrast ink appears only past a half fill, so a capped plate keeps `--intent-strong` rather than walking toward the page. |
206
+ | Intent hue in dark | `--intent-strong` is the `-300` shade in dark, so a soft or ghost label keeps its own hue instead of reading white. |
207
+ | Toggle marks | A checked mark meets 3:1 as a state indicator on every plate its fill class can paint, in both themes. |
208
+ | Forced colors | `forced-colors: active` preserves visible borders and checked states using system colors. |
209
+ | Text direction | Quote bar, alert icon, progress fill, vertical divider, and switch knob mirror under `dir="rtl"`. No class needed. |
210
+
211
+ The loader fallback is embedded in loader CSS, so focused imports that include
212
+ loaders honor reduced motion without the reset. This includes
213
+ `@codenhub/styles/components`, `@codenhub/styles/tw/components`,
214
+ `@codenhub/styles/tw/button`, and `@codenhub/styles/tw/loader`. The complete and
215
+ native entrypoints include both that fallback and the reset's broader animation
216
+ and transition shortening.
217
+
218
+ In forced colors, class-based form controls including `.text-control` receive a
219
+ 2px `Highlight` system-color focus outline. The native entrypoint provides the
220
+ same visible system outline for unclassed text inputs, selects, textareas,
221
+ checkboxes, and radios.
222
+
223
+ Custom checkboxes and radios use the system `Canvas`, `CanvasText`, `Highlight`,
224
+ and `HighlightText` colors in forced-colors mode so checked and unchecked states
225
+ remain distinct. Shape, size, and spacing remain unchanged.
226
+
227
+ ## Required Outside CSS
228
+
229
+ Use semantic HTML and behavior appropriate for the component.
230
+
231
+ | UI | Required outside CSS |
232
+ | ----------------- | --------------------------------------------------------------------------------------------------------------- |
233
+ | Buttons | Use `<button>` for actions or accessible links for navigation. Add accessible names for `.btn.icon`. |
234
+ | Forms | Use labels, `type`, validation logic, `aria-describedby`, and error message relationships. |
235
+ | Alerts | Add `role="status"` or `role="alert"` based on announcement urgency. |
236
+ | Toasts | Add live-region behavior, dismissal behavior, focus rules, and pause/timeout logic when needed. |
237
+ | Tooltips | Provide accessible names/descriptions. CSS pseudo-element content is not enough for all assistive tech. |
238
+ | Progress | Use semantic progress elements or ARIA values when numeric progress must be announced. |
239
+ | Skeletons/loaders | Mark decorative loading visuals with `aria-hidden="true"` and expose loading state elsewhere when needed. |
240
+ | Popovers/modals | Provide focus trapping, escape handling, inert background behavior, labels, and roles outside this CSS package. |
241
+
242
+ ## State Attribute Guidance
243
+
244
+ Prefer native attributes first:
245
+
246
+ ```html
247
+ <button class="btn primary" disabled>Saving</button>
248
+ <input class="ipt" aria-invalid="true" aria-describedby="email-error" />
249
+ ```
250
+
251
+ Use ARIA or data attributes when native attributes are not available for the element or library:
252
+
253
+ ```html
254
+ <a class="btn secondary" aria-disabled="true">Unavailable</a>
255
+ <button class="tooltip tooltip-icon" data-state="open" data-tooltip="More details" aria-label="More details">?</button>
256
+ ```
257
+
258
+ `aria-disabled="true"` communicates disabled state but does not prevent activation. JavaScript or element choice must prevent activation when required.
259
+
260
+ <!-- Source: docs/concepts.md -->
261
+
262
+ # Concepts
263
+
264
+ Read this before the usage guides. It explains the mental model every
265
+ component, token, and class in this package shares.
266
+
267
+ ## The three axes
268
+
269
+ Every component separates three independent choices, so each can change
270
+ without a component-specific variant:
271
+
272
+ | Axis | Question | Classes | Inheritance |
273
+ | ------------ | --------------- | ----------------------------------------------------------------------------------------- | ------------ |
274
+ | Intent | Which color? | `.primary`, `.secondary`, `.success`, `.warning`, `.destructive`, `.info` | Element only |
275
+ | Presentation | How much of it? | `.solid`, `.soft`, `.ghost`, `.edged`, `.edgeless` | Cascades |
276
+ | Aesthetic | Made of what? | `.neobrutalism`, `.glass`, `.pixel`, `.chunky-tile` from an optional aesthetic stylesheet | Cascades |
277
+
278
+ ```html
279
+ <section class="soft pixel">
280
+ <button class="btn primary">Soft primary pixel button</button>
281
+ <button class="btn destructive ghost edged">Outlined destructive override</button>
282
+ </section>
283
+ ```
284
+
285
+ Intent stays on the component so a semantic container cannot accidentally
286
+ recolor nested controls. Presentation and aesthetic cascade from a container;
287
+ putting either class directly on a component overrides the inherited choice.
288
+ Components apply documented clamps when an axis would remove an essential edge
289
+ or overwhelm compact geometry.
290
+
291
+ The maintained contract is the intersection of the combinations documented in
292
+ [Usage](docs/usage/index.md) and demonstrated by the playground. Undocumented and
293
+ undemonstrated combinations may produce CSS but are not supported behavior.
294
+
295
+ - [Usage → Composing](docs/usage/composing.md) shows how the three axes combine
296
+ on each component, with the full axis-support matrix.
297
+ - [Usage → Aesthetics](docs/usage/aesthetics.md) covers each aesthetic and its
298
+ documented exceptions in depth.
299
+
300
+ ## The token system
301
+
302
+ Tokens are CSS custom properties, grouped by what they control rather than
303
+ where they're used:
304
+
305
+ | Category | Controls | Reference |
306
+ | ------------ | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
307
+ | Color | The palette: primary, accent, success, warning, destructive, info, text, borders, surfaces. | [Usage → Theming](docs/usage/theming.md#color-tokens) |
308
+ | Intent | Which color slot a component reads once an intent class is applied. | [Usage → Theming](docs/usage/theming.md#intent-tokens) |
309
+ | Foundation | Layout, shape, motion, focus, depth, and layering defaults shared across components. | [Usage → Theming](docs/usage/theming.md#foundation-tokens) |
310
+ | Presentation | How much of an intent a component shows — fill and border percentages. | [Usage → Customizing](docs/usage/customizing.md#presentation-tokens) |
311
+ | Material | What a component is made of — radius, border width, shadow geometry, translucency. | [Usage → Customizing](docs/usage/customizing.md#material-tokens) |
312
+ | Aesthetic | Per-aesthetic knobs such as `--pixel-unit` or `--tile-lift`. | [Usage → Customizing](docs/usage/customizing.md#aesthetic-tokens) |
313
+
314
+ Color and foundation tokens are the ones most apps touch first, to match a
315
+ brand palette or spacing scale. Intent, presentation, and material tokens are
316
+ lower-level: components read them, and only presentation classes, aesthetic
317
+ classes, or a consumer override should set them.
318
+
319
+ ## Theme selection
320
+
321
+ The root follows the operating-system `color-scheme` preference until an
322
+ explicit selector overrides it.
323
+
324
+ | Theme | Equivalent selectors |
325
+ | ----- | ------------------------------------------------ |
326
+ | Light | `.light`, `.theme-light`, `[data-theme="light"]` |
327
+ | Dark | `.dark`, `.theme-dark`, `[data-theme="dark"]` |
328
+
329
+ - Theme selectors set `color-scheme` and nothing else. Because `color-scheme`
330
+ inherits, a selector themes its entire subtree.
331
+ - With no explicit selector, `:root` uses `color-scheme: light dark`, so the
332
+ system preference decides.
333
+ - A theme selector on `:root` overrides the system preference in either
334
+ direction.
335
+ - A nested explicit selector overrides an inherited theme, so the nearest
336
+ themed ancestor controls that subtree at any depth.
337
+ - If conflicting light and dark selectors are placed on the same element, the
338
+ dark declaration wins because it appears later with equal specificity. Do
339
+ not rely on this conflict behavior; apply one theme per element.
340
+ - Component colors resolve from tokens or `currentColor`, so they follow the
341
+ theme without separate dark-variant rules, with no exception: every glyph
342
+ the package draws is a `currentColor` mask. Icon artwork a consumer brings
343
+ is theirs to theme.
344
+
345
+ Setting `color-scheme` yourself on an element also re-themes the tokens below
346
+ it, because that is the only signal the palette reads. This also themes native
347
+ UI such as scrollbars and form-control internals to match.
348
+
349
+ This mechanism works from Chrome 123, Safari 17.5, and Firefox 120; the package
350
+ overall requires Firefox 121 for `:has()` (see [Setup](docs/setup.md#requirements)).
351
+
352
+ <!-- Source: docs/integrating/astro.md -->
353
+
354
+ # Astro
355
+
356
+ Import the package from a shared layout's frontmatter so every page that
357
+ uses that layout loads it once:
358
+
359
+ ```astro
360
+ ---
361
+ // src/layouts/Layout.astro
362
+ import "@codenhub/styles";
363
+ ---
364
+
365
+ <html lang="en">
366
+ <body>
367
+ <slot />
368
+ </body>
369
+ </html>
370
+ ```
371
+
372
+ Apply `.dark` or `.light` on `<html>` to force a theme instead of following
373
+ the system preference; see [Setup → Configuration](docs/setup.md#configuration).
374
+
375
+ A project running Tailwind CSS v4 imports `@codenhub/styles/tw` through
376
+ Astro's Tailwind integration instead; see [Tailwind CSS v4](docs/integrating/tailwind.md)
377
+ for the `@source` directive Astro needs to scan `src/`.
378
+
379
+ <!-- Source: docs/integrating/index.md -->
380
+
381
+ # Integrating
382
+
383
+ This package has no JavaScript and no framework bindings; see
384
+ [Introduction](docs/index.md#what-it-does-not-do). Integrating it into a
385
+ framework is entirely about where that framework loads a global stylesheet,
386
+ and whether the project runs Tailwind CSS v4.
387
+
388
+ Every stack does the same two things:
389
+
390
+ 1. Import one [entrypoint](docs/setup.md#import-paths) — the compiled
391
+ `@codenhub/styles` for any build, or `@codenhub/styles/tw` when the
392
+ project already runs Tailwind CSS v4 — from wherever that framework loads
393
+ global CSS.
394
+ 2. Import `@codenhub/styles/aesthetics` (or `/tw/aesthetics`) after it, only
395
+ if the project uses an [aesthetic](docs/usage/aesthetics.md).
396
+
397
+ | Stack | Guide |
398
+ | ---------------------------------------- | -------------------------------- |
399
+ | A Tailwind CSS v4 project on any bundler | [Tailwind CSS v4](docs/integrating/tailwind.md) |
400
+ | Next.js / React | [Next.js](docs/integrating/nextjs.md) |
401
+ | Vue | [Vue](docs/integrating/vue.md) |
402
+ | Svelte / SvelteKit | [Svelte](docs/integrating/svelte.md) |
403
+ | Astro | [Astro](docs/integrating/astro.md) |
404
+
405
+ A project with no framework — a plain Vite, webpack, or Parcel build — needs
406
+ nothing beyond [Setup](docs/setup.md#quick-start): import the compiled
407
+ stylesheet from the project's existing global CSS entry.
408
+
409
+ <!-- Source: docs/integrating/nextjs.md -->
410
+
411
+ # Next.js
412
+
413
+ Next.js only allows global CSS to be imported from one place. Import the
414
+ package from the root layout in the App Router, or from the custom App in
415
+ the Pages Router — not from a page or component further down the tree.
416
+
417
+ ```tsx
418
+ // app/layout.tsx
419
+ import "@codenhub/styles";
420
+
421
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
422
+ return (
423
+ <html lang="en">
424
+ <body>{children}</body>
425
+ </html>
426
+ );
427
+ }
428
+ ```
429
+
430
+ ```tsx
431
+ // pages/_app.tsx
432
+ import "@codenhub/styles";
433
+ import type { AppProps } from "next/app";
434
+
435
+ export default function App({ Component, pageProps }: AppProps) {
436
+ return <Component {...pageProps} />;
437
+ }
438
+ ```
439
+
440
+ Apply `.dark` or `.light` on `<html>` to force a theme instead of following
441
+ the system preference; see [Setup → Configuration](docs/setup.md#configuration).
442
+
443
+ ```tsx
444
+ <html lang="en" className="dark">
445
+ ```
446
+
447
+ A project running Tailwind CSS v4 imports `@codenhub/styles/tw` from the
448
+ same file instead; see [Tailwind CSS v4](docs/integrating/tailwind.md) for the `@source`
449
+ directive Next.js needs to scan its own `app`/`src` directories.
450
+
451
+ <!-- Source: docs/integrating/svelte.md -->
452
+
453
+ # Svelte
454
+
455
+ In SvelteKit, import the package from the root layout so it loads once for
456
+ every route:
457
+
458
+ ```svelte
459
+ <!-- src/routes/+layout.svelte -->
460
+ <script>
461
+ import "@codenhub/styles";
462
+ let { children } = $props();
463
+ </script>
464
+
465
+ {@render children()}
466
+ ```
467
+
468
+ In a plain Svelte + Vite app, import it from the entry file instead:
469
+
470
+ ```ts
471
+ // main.ts
472
+ import "@codenhub/styles";
473
+ import { mount } from "svelte";
474
+ import App from "./App.svelte";
475
+
476
+ mount(App, { target: document.getElementById("app")! });
477
+ ```
478
+
479
+ Apply `.dark` or `.light` on `<html>` (edit `src/app.html` in SvelteKit) to
480
+ force a theme instead of following the system preference; see
481
+ [Setup → Configuration](docs/setup.md#configuration).
482
+
483
+ A project running Tailwind CSS v4 imports `@codenhub/styles/tw` from the
484
+ app's CSS entry instead; see [Tailwind CSS v4](docs/integrating/tailwind.md).
485
+
486
+ <!-- Source: docs/integrating/tailwind.md -->
487
+
488
+ # Tailwind CSS v4
489
+
490
+ A project that already runs Tailwind CSS v4 can process this package's
491
+ source instead of its compiled output. `/tw` entrypoints publish copied,
492
+ uncompiled source from `dist/tw`; your own Tailwind build compiles it
493
+ alongside your app, so its `@theme`, `@utility`, and `@apply` directives
494
+ resolve together with yours.
495
+
496
+ ```css
497
+ @import "tailwindcss";
498
+ @import "@codenhub/styles/tw";
499
+
500
+ @source "./src";
501
+ ```
502
+
503
+ `@source` tells Tailwind's build where to scan for the utility and component
504
+ class names actually used in your markup, so it can generate only what is
505
+ referenced. Point it at your app's source, not at this package — `/tw`
506
+ already ships every class definition it publishes.
507
+
508
+ Add aesthetics from the source entrypoint the same way as the compiled one,
509
+ after the base import:
510
+
511
+ ```css
512
+ @import "tailwindcss";
513
+ @import "@codenhub/styles/tw";
514
+ @import "@codenhub/styles/tw/aesthetics";
515
+
516
+ @source "./src";
517
+ ```
518
+
519
+ Import a focused `/tw/*` entrypoint instead of the combined one — such as
520
+ `/tw/button` or `/tw/form` — when a build only needs one component family;
521
+ see the full [import path table](docs/setup.md#import-paths) for what each one
522
+ composes. Tailwind CSS 4 or newer is required for every `/tw` entrypoint.
523
+
524
+ <!-- Source: docs/integrating/vue.md -->
525
+
526
+ # Vue
527
+
528
+ Import the package once, from the app's entry file, before the app mounts:
529
+
530
+ ```ts
531
+ // main.ts
532
+ import "@codenhub/styles";
533
+ import { createApp } from "vue";
534
+ import App from "./App.vue";
535
+
536
+ createApp(App).mount("#app");
537
+ ```
538
+
539
+ Apply `.dark` or `.light` on the root element to force a theme instead of
540
+ following the system preference; see
541
+ [Setup → Configuration](docs/setup.md#configuration).
542
+
543
+ A project running Tailwind CSS v4 imports `@codenhub/styles/tw` from a CSS
544
+ entry file instead of `main.ts`, using the `@tailwindcss/vite` plugin; see
545
+ [Tailwind CSS v4](docs/integrating/tailwind.md).
546
+
547
+ <!-- Source: docs/setup.md -->
548
+
549
+ # Setup
550
+
551
+ ## Installation
552
+
553
+ ```sh
554
+ pnpm add @codenhub/styles
555
+ ```
556
+
557
+ ## Quick start
558
+
559
+ Import the compiled stylesheet for the complete token, reset, utility, and
560
+ component-class surface, then compose helper classes in markup:
561
+
562
+ ```css
563
+ @import "@codenhub/styles";
564
+ ```
565
+
566
+ ```html
567
+ <main class="stack">
568
+ <p class="text-label">Status</p>
569
+ <h1 class="text-title">Ready to publish</h1>
570
+ <button class="btn primary">Continue</button>
571
+ </main>
572
+ ```
573
+
574
+ The full stylesheet applies global reset and focus-visible rules, but does not
575
+ apply the optional classless native-element mappings, and does not include
576
+ aesthetics. Import focused entrypoints when global rules are not appropriate;
577
+ see [Import paths](#import-paths) below.
578
+
579
+ Aesthetics are opt-in. Import the combined aesthetics stylesheet, or one
580
+ aesthetic on its own, after the base stylesheet so its rules win:
581
+
582
+ ```css
583
+ @import "@codenhub/styles";
584
+ @import "@codenhub/styles/aesthetics";
585
+ ```
586
+
587
+ ```html
588
+ <section class="neobrutalism">
589
+ <button class="btn primary">Thick ink and a hard shadow</button>
590
+ </section>
591
+ ```
592
+
593
+ A project already running Tailwind CSS v4 imports the source entrypoint
594
+ instead; see [Integrating → Tailwind CSS v4](docs/integrating/tailwind.md).
595
+
596
+ ## Configuration
597
+
598
+ The root follows the operating-system color-scheme preference. Apply `.light`
599
+ or `.dark` to the root or any ancestor to force token values for that subtree.
600
+ `.theme-light`, `.theme-dark`, and `data-theme="light|dark"` are aliases.
601
+
602
+ ```html
603
+ <html class="dark">
604
+ <body>
605
+ <button class="btn primary">Dark themed button</button>
606
+ </body>
607
+ </html>
608
+ ```
609
+
610
+ See [Concepts → Theme selection](docs/concepts.md#theme-selection) for how
611
+ selectors resolve and [Usage → Theming](docs/usage/theming.md) for the full color
612
+ token reference.
613
+
614
+ ## Import paths
615
+
616
+ Compiled entrypoints are ready-to-import CSS and require only tooling that can
617
+ resolve package CSS imports. `/tw` entrypoints publish copied, uncompiled
618
+ source from `dist/tw`; a Tailwind v4 build must still process their `@theme`,
619
+ `@utility`, `@apply`, and related directives. Focused source component
620
+ entrypoints include theme tokens so their classes can work independently.
621
+
622
+ | Import path | Composition and effects |
623
+ | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
624
+ | `@codenhub/styles` | Compiled complete surface: Tailwind base/theme output, tokens, typography, utilities, components, and reset. Applies global reset, root theme, and focus rules. |
625
+ | `@codenhub/styles/theme` | Compiled Tailwind theme variables, Codenhub tokens, and the presentation classes. Sets root theme properties and system/explicit theme selectors; no reset or component classes. |
626
+ | `@codenhub/styles/components` | Compiled theme tokens, typography utilities, and every component class. Includes static reduced-motion loader masks; no reset, layout/content utilities, or native mappings. |
627
+ | `@codenhub/styles/native` | Compiled complete surface plus classless mappings for headings, text, content elements, form controls, and buttons. Applies the reset and broad element selectors. |
628
+ | `@codenhub/styles/aesthetics` | Compiled `.neobrutalism`, `.glass`, `.pixel`, and `.chunky-tile` classes. Adds no tokens or components of its own; import after the base stylesheet. |
629
+ | `@codenhub/styles/aesthetics/*` | One compiled aesthetic: `/neobrutalism`, `/glass`, `/pixel`, or `/chunky-tile`. Same rules and ordering as the combined file. |
630
+ | `@codenhub/styles/tw` | Source equivalent of the complete stylesheet. Imports Tailwind itself, theme, typography, utilities, components, and reset; applies the same global rules. |
631
+ | `@codenhub/styles/tw/theme` | Source theme, tokens, and presentation classes. Imports Tailwind's theme layer; emits root/system/explicit theme selectors and defines the package's custom `dark:` variant. |
632
+ | `@codenhub/styles/tw/components` | Published Tailwind source for theme, typography, buttons, feedback, forms, loaders, surfaces, and tooltips, including static reduced-motion loader masks. No reset, layout/content utilities, or native mappings. |
633
+ | `@codenhub/styles/tw/surface` | Source theme plus `.surface`, `.card`, and `.panel`. |
634
+ | `@codenhub/styles/tw/button` | Published Tailwind source for theme, activity indicators, and `.btn`; includes static reduced-motion masks because buttons compose `.loader-mask` for loading state. |
635
+ | `@codenhub/styles/tw/form` | Source theme plus field, public `text-control`, input, textarea, select, checkbox, radio, and switch utilities. |
636
+ | `@codenhub/styles/tw/feedback` | Source theme plus alert, badge, skeleton, and progress utilities and their keyframes. |
637
+ | `@codenhub/styles/tw/loader` | Published Tailwind source for activity indicators and loaders, including static reduced-motion masks. It has no theme or reset side effects and uses `currentColor`. |
638
+ | `@codenhub/styles/tw/tooltip` | Source theme plus tooltip utilities. |
639
+ | `@codenhub/styles/tw/reset` | Source theme, typography utilities, and global reset/accessibility rules. It changes root/body/elements and selection, motion, focus, scrollbar, and forced colors. |
640
+ | `@codenhub/styles/tw/native` | Source complete surface plus reset and classless native-element mappings; broadest global effects. |
641
+ | `@codenhub/styles/tw/typography` | Source theme plus typography utilities; sets theme selectors but no reset or classless mappings. |
642
+ | `@codenhub/styles/tw/utilities` | Source theme and typography composition plus layout and content utilities; safelists the package utility names. |
643
+ | `@codenhub/styles/tw/aesthetics` | Source aesthetics, combined. `/tw/aesthetics/neobrutalism`, `/glass`, `/pixel`, and `/chunky-tile` publish one apiece. Plain CSS with no theme or reset side effects. |
644
+
645
+ Avoid importing overlapping entrypoints in the same build unless duplicate
646
+ generated CSS is acceptable; pick the narrowest entrypoint that covers what a
647
+ page needs.
648
+
649
+ ## Requirements
650
+
651
+ - Consumer tooling must resolve package CSS imports.
652
+ - Chrome 123, Safari 17.5, or Firefox 121 and newer. Color tokens are declared
653
+ with `light-dark()` and selected by `color-scheme` (Firefox 120), and
654
+ `.input-group` propagates a nested control's invalid and disabled state with
655
+ `:has()` (Firefox 121).
656
+ - Tailwind CSS 4 or newer is required only for `/tw` source entrypoints.
657
+ - The package has no JavaScript runtime. Apps must provide semantic HTML,
658
+ ARIA, keyboard behavior, focus management, validation, and announcements;
659
+ see [Accessibility](docs/accessibility.md).
660
+
661
+ <!-- Source: docs/usage/aesthetics.md -->
662
+
663
+ # Aesthetics
664
+
665
+ An aesthetic decides what a component is _made of_: its radius, border
666
+ thickness, shadow, and shape. Aesthetics ship from opt-in entrypoints, so
667
+ importing the stylesheet is what makes the classes available:
668
+
669
+ ```css
670
+ @import "@codenhub/styles";
671
+ @import "@codenhub/styles/aesthetics";
672
+ ```
673
+
674
+ Import them after the base stylesheet. `.neobrutalism` and `.pixel` replace
675
+ the neutral border color, and they do so at zero specificity, so source order
676
+ is what lets them win.
677
+
678
+ Like presentation, an aesthetic class cascades to any subtree:
679
+
680
+ ```html
681
+ <section class="neobrutalism">
682
+ <button class="btn primary">Thick ink and a hard shadow</button>
683
+ <div class="card destructive">The intent still wins over the aesthetic ink</div>
684
+ </section>
685
+ ```
686
+
687
+ | Class | Look |
688
+ | --------------- | ------------------------------------------------------------------------------------------------------------------------------- |
689
+ | `.neobrutalism` | No radius, a thick ink outline, a hard unblurred offset shadow, and a press that moves the element into it. |
690
+ | `.glass` | Translucent surfaces over a blurred backdrop with a hairline highlight edge. |
691
+ | `.pixel` | Corners cut by one grid unit, a chunky outline drawn as an inset ring, and the consumer-supplied `--font-pixel` over monospace. |
692
+ | `.chunky-tile` | Rounded slabs seated on a darker shade of themselves, with a press that moves the element down into its own bar. |
693
+
694
+ Aesthetics compose with every supported fill and edge pair. `.ghost.edgeless`
695
+ intentionally removes the visible material traits from components whose
696
+ aesthetic is expressed only through their fill, edge, or elevation.
697
+
698
+ An explicit presentation on the element still wins over the aesthetic's
699
+ defaults. The aesthetic supplies edge thickness; presentation only decides
700
+ whether that edge is drawn.
701
+
702
+ An aesthetic directly on a component likewise wins over an inherited
703
+ aesthetic. This includes tooltip pseudo-elements: `.tooltip.glass` gets the
704
+ complete glass bubble under a pixel ancestor, and `.tooltip.pixel` gets the
705
+ complete stepped bubble under a glass ancestor.
706
+
707
+ Each aesthetic that scales from one number publishes it as a knob — see
708
+ [Customizing → Aesthetic tokens](docs/usage/customizing.md#aesthetic-tokens) for how
709
+ knobs resolve and where they can be set. This page covers what each
710
+ aesthetic looks like and where it makes documented exceptions.
711
+
712
+ ## Neobrutalism
713
+
714
+ The shadow is cast in the component's own intent, so a success button throws
715
+ a green shadow and a destructive card a red one. With no intent, both the
716
+ outline and the shadow use the ink, which follows the theme rather than the
717
+ palette.
718
+
719
+ `--neo-offset` is the knob: the shadow offset and the distance the press
720
+ travels are the same number, so scaling the look scales both at once.
721
+
722
+ ```html
723
+ <section class="neobrutalism" style="--neo-offset: 6px">
724
+ <button class="btn primary">A longer throw</button>
725
+ </section>
726
+ ```
727
+
728
+ **Exceptions:**
729
+
730
+ - Content chips are left alone. A `.kbd`, `.code`, or `.pre` is content
731
+ rather than structure, and an ink edge on one reads as a defect: a key cap
732
+ keeps the quiet border color and a code chip keeps no border at all.
733
+ - Only `.btn` and `.card.interactive` respond to hover and press. A card
734
+ becomes a control when it opts in with `.interactive`; a plain card, an
735
+ alert, or a panel is a container and stays put.
736
+ - An alert rests on the slab under this aesthetic and nowhere else. Here the
737
+ slab is a second ink line rather than depth, and an alert is the only
738
+ container the package rests flat, so without it a neobrutalist alert is
739
+ the one box on the page that reads as unfinished. It is a resting default,
740
+ so `.flat` still takes it off and `.floating` still doubles it.
741
+
742
+ ## Glass
743
+
744
+ `.glass` needs something behind it to blur. On a flat page background it
745
+ renders as a plain translucent panel.
746
+
747
+ `--glass-radius` and `--glass-radius-surface` are the corners, at `0.75rem`
748
+ and `1rem`. Glass is rounder than the base geometry because a translucent
749
+ panel with a tight corner reads as a cut-out rather than as a pane.
750
+
751
+ ```html
752
+ <section class="glass" style="--glass-radius-surface: 1.5rem">
753
+ <div class="card">Softer</div>
754
+ </section>
755
+ ```
756
+
757
+ **Exceptions:**
758
+
759
+ - The blur applies to `.card`, `.panel`, `.alert`, and the tooltip bubble
760
+ only. Controls stay solid and sit on the glass: a blur under every control
761
+ of a dense cluster costs a composited layer apiece and reads as noise.
762
+ Controls still take the aesthetic's radius, border, and shadow.
763
+ - Under `prefers-reduced-transparency: reduce`, glass surfaces drop the blur
764
+ and become opaque.
765
+
766
+ ## Pixel
767
+
768
+ `--pixel-unit` is one pixel of the imaginary low-resolution grid, `4px` by
769
+ default. One square unit is cut from each corner and the outline is one unit
770
+ thick, so the cut and what covers it are the same size.
771
+
772
+ ```html
773
+ <section class="pixel" style="--pixel-unit: 6px">
774
+ <button class="btn primary">Chunkier</button>
775
+ </section>
776
+ ```
777
+
778
+ **Exceptions:**
779
+
780
+ - Corners are one unit or nothing. Chips square instead of stepping —
781
+ badges, key caps, code, checkboxes, and switches all read
782
+ `--ui-clip-tight`, which this aesthetic sets to none, because one unit off
783
+ each corner of a 24px badge is a bite rather than a corner. Tables,
784
+ progress bars, and skeletons square too, each for its own reason: a
785
+ table's `overflow: hidden` fights the clip and the corners square off
786
+ where the two meet, and progress and skeleton never read a clip at all —
787
+ a squaring aesthetic reaches them through `border-radius` alone. `.pre` is
788
+ the one exception that steps: it carries no clip override, so it inherits
789
+ the same polygon a button or a card gets, and with no border by default
790
+ the cut shows with no ring around it.
791
+ - The outline is an inset ring, because a clip removes a real border — and
792
+ the focus ring, for the same reason, is a second inset layer rather than
793
+ an outline. Both are the element's own edge rather than a shadow, so the
794
+ border answers `.edged` and `.edgeless` the way a border does: a
795
+ `.edgeless` badge and a `.solid` button draw none, a `.edged` card draws
796
+ one, and a field keeps one whatever a container asks for.
797
+ - The tooltip trigger and `.radio` are hardcoded past the clip rather than
798
+ reached by it. `.tooltip-icon` forces `clip-path: none`, since a stepped
799
+ badge would clip its own bubble away with it. `.radio` forces the same,
800
+ because the circle is the only thing telling it from a checkbox at a
801
+ glance, and a stepped polygon would square it.
802
+ - `--font-pixel` is yours to supply. The package ships no font binary, so the
803
+ aesthetic has no network side effect and falls back to the monospace
804
+ stack.
805
+
806
+ ## Chunky tile
807
+
808
+ Rounded slabs sitting on a darker shade of themselves, pressed flat on
809
+ click. The look mobile learning and game apps use; it is named for what it
810
+ is made of rather than for any one product.
811
+
812
+ `--tile-lift` is how deep the bar is and how far a press travels, `4px` by
813
+ default. One number drives both, so the top edge drops by exactly the bar's
814
+ depth and the bottom edge does not move. `--tile-radius` is the corner,
815
+ `0.75rem`, and it is one value rather than a control/surface pair on
816
+ purpose — the shared corner is what makes a button and the card it sits in
817
+ read as the same object.
818
+
819
+ ```html
820
+ <section class="chunky-tile" style="--tile-lift: 6px; --tile-radius: 0.5rem">
821
+ <button class="btn success">Continue</button>
822
+ </section>
823
+ ```
824
+
825
+ The bar is a darker shade of the element's own colour rather than a shadow
826
+ under it, so it follows every intent: a success button sits on a dark green
827
+ edge, a destructive one on a dark red edge, and a plain card on a grey edge
828
+ that matches the line it already draws. Measured on the light palette,
829
+ `.btn.success` is `#007a55` on `#004c34`. On a dark page a near-black
830
+ surface cannot go darker, so its bar lands lighter and reads as a rim.
831
+
832
+ **Exceptions:**
833
+
834
+ - Only what the registry rests above zero sits on a bar — buttons and cards.
835
+ A badge, chip, field, or tooltip is flat until it asks for depth with
836
+ `.raised`, and `.flat` takes the bar off anything:
837
+
838
+ ```html
839
+ <div class="card soft edgeless info flat">Promo panel, deliberately flat</div>
840
+ ```
841
+
842
+ - `--font-rounded` is yours to supply. A heavy rounded grotesque is most of
843
+ this look and the package ships no font binary, so with nothing supplied
844
+ the page's own stack is used.
845
+ - Action labels are drawn heavier and slightly tracked. Casing is left
846
+ alone: how a label is worded is the application's decision, so if you want
847
+ uppercase buttons, write that rule in your own stylesheet.
848
+ - `.card.interactive` presses like a `.btn`, so a chunky answer tile drops
849
+ onto its bar on click. A plain `.card` stays put.
850
+ - The shipped `.primary` is a near-black monochrome, which leaves a primary
851
+ button's bar almost invisible against its own plate; give `.primary` a hue
852
+ if it is the call to action.
853
+ - Depth in this aesthetic is the bar, never a blurred drop shadow. Anything
854
+ the registry rests flat sits flush on the page.
855
+
856
+ <!-- Source: docs/usage/buttons.md -->
857
+
858
+ # Buttons
859
+
860
+ Use `.btn` with one optional intent class, one optional fill class, one
861
+ optional edge class, optional size class, and optional state. See
862
+ [Composing](docs/usage/composing.md) for how the intent, presentation, and aesthetic
863
+ axes combine on any component.
864
+
865
+ Intent classes map color tokens into button tone slots.
866
+ [Presentation](docs/usage/composing.md#presentation) decides how much of that intent
867
+ reaches background, text, border, and hover.
868
+
869
+ Intent classes:
870
+
871
+ | Class | Meaning |
872
+ | ----------------------------------- | ------------------------------ |
873
+ | `.neutral` _(default)_ | No specific intent. |
874
+ | `.primary` | Primary action. |
875
+ | `.secondary` | Secondary/accent action. |
876
+ | `.success` | Successful or positive action. |
877
+ | `.warning` | Warning/caution action. |
878
+ | `.destructive`, `.danger`, `.error` | Destructive or error action. |
879
+ | `.info` | Informational action. |
880
+
881
+ Every component that supports intent accepts the same list and reads the
882
+ same [intent tokens](docs/usage/theming.md#intent-tokens), so a custom intent class
883
+ works everywhere without touching a component.
884
+
885
+ `.neutral` is the only intent that does not fill all the way. Its color is
886
+ the page's own text color, so a full fill of it would be a black or white
887
+ slab — the loudest element on the page, for the intent that means nothing in
888
+ particular. It stops at `--intent-fill-max`, which is why a `.solid` neutral
889
+ is a quiet plate a step past `.soft` rather than a block of ink. Set the slot
890
+ on your own intent classes; see [Theming → Intent tokens](docs/usage/theming.md#intent-tokens).
891
+
892
+ Size and shape classes:
893
+
894
+ | Class | Purpose |
895
+ | ---------------------------------------------------------------------- | ----------------------------------------------------------- |
896
+ | `.pill` | Fully rounded button corners (`border-radius: 9999px`). |
897
+ | `.sm` | Smaller button. |
898
+ | `.lg` | Larger button. |
899
+ | `.p-sm`, `.compact` | Compact padding modifier (`px-2.5 py-1`). |
900
+ | `.p-lg`, `.spacious` | Spacious padding modifier (`px-6 py-3`). |
901
+ | `.icon` | Square icon button. Use an accessible name in HTML. |
902
+ | `.loading` | Loading state. Hides text and shows CSS activity indicator. |
903
+ | `.disabled`, `[disabled]`, `[aria-disabled="true"]`, `[data-disabled]` | Disabled styling. |
904
+
905
+ Examples:
906
+
907
+ ```html
908
+ <button class="btn primary">Primary</button>
909
+ <button class="btn success ghost edged">Success outline</button>
910
+ <button class="btn warning soft">Warning soft</button>
911
+ <button class="btn destructive ghost edgeless">Danger ghost</button>
912
+ <button class="btn icon primary" aria-label="Create">+</button>
913
+ <button class="btn primary loading" disabled>Saving</button>
914
+ ```
915
+
916
+ `.loading` is a state, not a color or presentation class. Prefer combining
917
+ it with disabled behavior so users cannot trigger duplicate work. Loading
918
+ buttons keep only opacity and transform transitions active so spinner and
919
+ surface colors stay synchronized when theme tokens change.
920
+
921
+ On the base look a press scales the button down a little
922
+ (`--ui-active-transform`, `scale(0.97)`). [Aesthetics](docs/usage/aesthetics.md) swap
923
+ in their own press gesture, and `prefers-reduced-motion` turns it off.
924
+
925
+ Use `<button>` for actions or an accessible link for navigation; see
926
+ [Accessibility](docs/accessibility.md).
927
+
928
+ <!-- Source: docs/usage/composing.md -->
929
+
930
+ # Composing the three axes
931
+
932
+ See [Concepts](docs/concepts.md#the-three-axes) for what each axis means. This
933
+ page covers how they combine in markup, what the presentation classes mean in
934
+ depth, which components support which axis, and elevation.
935
+
936
+ ## Combining axes
937
+
938
+ ```html
939
+ <div class="soft">
940
+ <button class="btn primary">Soft primary button</button>
941
+ <span class="badge success">Soft success badge</span>
942
+ <button class="btn primary ghost edged">Outlined; the element wins</button>
943
+ </div>
944
+ ```
945
+
946
+ A declaration on the element always beats one inherited from a container, so
947
+ a container sets the default look for its subtree and any element opts out.
948
+
949
+ Intent classes do not cascade. `.primary` and its siblings stay on the
950
+ element that shows the intent, because a container silently recoloring every
951
+ descendant is a trap rather than a feature — see
952
+ [Theming → Intent tokens](docs/usage/theming.md#intent-tokens). Use `.neutral` to opt
953
+ an element back out of an inherited intent context. Presentation and
954
+ [material tokens](docs/usage/customizing.md#material-tokens) do cascade, so a
955
+ container can set the look of its whole subtree while any element overrides
956
+ it.
957
+
958
+ ## Presentation
959
+
960
+ Presentation has two independent axes. `.solid`, `.soft`, and `.ghost`
961
+ decide how much intent fills a component; `.edged` and `.edgeless` decide
962
+ whether it draws a boundary. They set
963
+ [presentation tokens](docs/usage/customizing.md#presentation-tokens) and nothing
964
+ else, so they work on the component itself or on any ancestor.
965
+
966
+ | Class | Look |
967
+ | ----------- | ---------------------------------------------------------- |
968
+ | `.solid` | Intent-colored fill with contrast text. |
969
+ | `.soft` | Lightly tinted fill with intent-colored text. |
970
+ | `.ghost` | No fill at rest, with intent-colored text. |
971
+ | `.edged` | Intent-colored boundary at the aesthetic's material width. |
972
+ | `.edgeless` | No boundary. |
973
+
974
+ Values each class ships:
975
+
976
+ | Class | `--ui-fill` | `--ui-fg-on-fill` | `--ui-border` |
977
+ | ----------- | ----------- | ----------------- | ------------- |
978
+ | `.solid` | `100%` | `100%` | |
979
+ | `.soft` | `12%` | `0%` | |
980
+ | `.ghost` | `0%` | `0%` | |
981
+ | `.edged` | | | `100%` |
982
+ | `.edgeless` | | | `0%` |
983
+
984
+ The combinations mean exactly what they spell:
985
+
986
+ ```html
987
+ <button class="btn primary">Solid, the button default</button>
988
+ <button class="btn soft edged">Tinted with a border</button>
989
+ <button class="btn primary ghost edged">Outline</button>
990
+ <button class="btn ghost">Ghost; picks up a tint on hover</button>
991
+ <input class="ipt soft edgeless" />
992
+ <!-- A field sunk into the page -->
993
+ ```
994
+
995
+ A fill class never decides an edge and an edge class never decides a fill.
996
+ Use `.solid.edgeless` for a filled box with no line — and prefer it on a
997
+ neutral one, because a capped fill leaves the box translucent and the edge
998
+ blend then paints a second coat of the same tint rather than disappearing
999
+ into it (measured 1.53:1 against its own plate in light, 1.82:1 in dark).
1000
+
1001
+ The edge is the silhouette and nothing else. Rules _inside_ a component that
1002
+ has an inside are a separate switch — see
1003
+ [Content and layout → Table rules](docs/usage/content-and-layout.md#table-rules) —
1004
+ because tying them to this axis made them arrive by implication rather than
1005
+ by request.
1006
+
1007
+ With no presentation class in scope, each component keeps its own default:
1008
+ buttons are filled, alerts and badges are tinted, surfaces and controls are
1009
+ neutral and bordered.
1010
+
1011
+ | Component | Reads |
1012
+ | ----------------------------------------------- | ------------------------------------------- |
1013
+ | `.btn`, `.alert`, `.badge`, `.card`, `.panel` | Fill, text, border, and border width. |
1014
+ | `.kbd` | Fill, text, border, and border width. |
1015
+ | `.data-table` | Header fill and text, border, border width. |
1016
+ | `.ipt`, `.textarea`, `.select`, `.text-control` | Border, border width, and a capped fill. |
1017
+ | `.input-group` | Border, border width, and a capped fill. |
1018
+ | `.checkbox`, `.radio` | A capped fill, checked and unchecked. |
1019
+ | `.switch` | Border, border width, and a capped fill. |
1020
+ | `.quote` | Fill, text, border, and width. |
1021
+ | `.code`, `.pre`, `.tooltip` | Fill, text, border, and material. |
1022
+ | `.loader`, `.skeleton`, `.progress`, `.divider` | Intent only, not presentation. |
1023
+
1024
+ Text controls and toggles cap a **cascaded** fill at `6%`, keeping typed
1025
+ text legible when a `.solid` container reaches a field nobody classed. A
1026
+ fill class written on the control itself names its own cap instead, because
1027
+ that is a consumer describing what they want: `.soft` takes `12%` and
1028
+ `.solid` takes `20%`. A switch caps at `40%`, where its three fills separate
1029
+ far enough to read on a track.
1030
+
1031
+ | On a text control | fill |
1032
+ | ----------------- | ---- |
1033
+ | `.ghost` | 0% |
1034
+ | cascaded `.solid` | 6% |
1035
+ | `.soft` | 12% |
1036
+ | `.solid` | 20% |
1037
+
1038
+ The same split governs the edge: a cascaded `.edgeless` is floored and the
1039
+ element's own is honoured — except on `.checkbox` and `.radio`, which never
1040
+ drop their line at all. See [Forms](docs/usage/forms.md) for the full toggle and text
1041
+ control reference.
1042
+
1043
+ A tooltip bubble is filled, over a tinted ground. An intent fills it with
1044
+ that intent's own color: `.tooltip.primary` is the primary color, black on a
1045
+ light page and white on a dark one, and `.tooltip.destructive` is a red
1046
+ bubble.
1047
+
1048
+ With no intent the bubble is `--color-tooltip`, the one plate in the package
1049
+ chosen per theme rather than derived from an intent. Composed like
1050
+ everything else it was `20%` of the page's ink over the surface tone, which
1051
+ steps _lighter_ than the surface on a dark page and _darker_ on a light
1052
+ one — so the dark bubble read as lifted and the light one as a mid-grey
1053
+ slab two steps darker than every card on the screen. The token states each
1054
+ direction instead: near-white over a light page, a mid grey over a dark
1055
+ one. `--color-tooltip-contrast` is its ink.
1056
+
1057
+ Near-white is only 1.04:1 against a light page, so the bubble draws a
1058
+ hairline as well, and floors it: a container's `.solid` or `.edgeless`
1059
+ cannot take the boundary away from a bubble nobody classed, while
1060
+ `.tooltip.edgeless` still can. The ground keeps the bubble opaque at every
1061
+ fill, so a cascaded presentation leaves it readable over whatever it floats
1062
+ above. It reads the material tokens too, so an [aesthetic](docs/usage/aesthetics.md)
1063
+ in scope shapes the bubble like any other component. See
1064
+ [Tooltips](docs/usage/tooltips.md) for the full tooltip reference.
1065
+
1066
+ ## Component axis reference
1067
+
1068
+ This table is the authoritative axis map for demonstrated components. "Shape"
1069
+ means the component reads applicable radius, border-width, shadow, hover, or
1070
+ clip/edge material tokens; an aesthetic may intentionally affect only the
1071
+ materials that component draws.
1072
+
1073
+ Presentation is two independent axes, so they get a column each. A `Yes` means
1074
+ the class changes what the component computes, which
1075
+ `tests/browser/axes.spec.ts` asserts against `registry.json` in both
1076
+ directions: an axis marked `Yes` must move, and an axis marked `No` must not.
1077
+
1078
+ | Components | Intent | Fill | Edge | Aesthetic material |
1079
+ | --------------------------------------------------------- | ------ | ---- | ---- | ------------------ |
1080
+ | `.btn`, `.alert`, `.badge`, `.card`, `.panel` | Yes | Yes | Yes | Shape |
1081
+ | `.loader` | Yes | No | No | No |
1082
+ | `.kbd`, `.data-table` | Yes | Yes | Yes | Radius and edge |
1083
+ | `.quote` | Yes | Yes | Yes | Edge width |
1084
+ | `.divider`, `.skeleton` | Yes | No | No | No |
1085
+ | `.ipt`, `.textarea`, `.select`, `.text-control` | Yes | Yes | Yes | Shape |
1086
+ | `.input-group` | Yes | Yes | Yes | Shape |
1087
+ | `.surface` | Yes | Yes | Yes | Shape |
1088
+ | `.checkbox`, `.radio` | Yes | Yes | No | Radius and edge |
1089
+ | `.switch` | Yes | Yes | Yes | Radius and edge |
1090
+ | `.progress` | Yes | No | No | No |
1091
+ | `.code`, `.pre` | Yes | Yes | Yes | Radius and clip |
1092
+ | `.tooltip` | Yes | Yes | Yes | Shape and overlay |
1093
+ | `.table-wrap`, `.quote-inline`, layout and text utilities | No | No | No | No |
1094
+
1095
+ `.checkbox` and `.radio` are the only components that read one axis and not
1096
+ the other. Their line is the only thing marking an unchecked box, so it is
1097
+ drawn whatever the edge class says, from a container or from the element
1098
+ itself. `.edgeless` on either is unsupported rather than merely discouraged.
1099
+
1100
+ Every other text control floors a _cascaded_ `.edgeless` and honours its own:
1101
+ a `.edgeless` toolbar will not erase the line of a field nobody classed, while
1102
+ `.ipt.edgeless` is a consumer describing what they want and gets it.
1103
+
1104
+ Intent aliases such as `.danger` and `.error` occupy the same intent axis as
1105
+ `.destructive`; they do not add component behavior. State and modifier
1106
+ classes such as `.interactive`, `.loading`, `.invalid`, `.compact`, and
1107
+ `.vertical` sit above this map and are documented with their component.
1108
+
1109
+ ## Elevation
1110
+
1111
+ Depth is a modifier, not an axis. Nothing is raised until an
1112
+ [aesthetic](docs/usage/aesthetics.md) draws depth or one of these classes asks for it.
1113
+
1114
+ | Class | Alias | Depth |
1115
+ | ----------------- | ----------- | ------------------------------------------------------------------------ |
1116
+ | `.elevation-none` | `.flat` | Removes part-based elevation; glass surfaces keep their complete shadow. |
1117
+ | `.elevation-sm` | `.raised` | One unit of the depth in scope. Cards, tiles, popovers that sit close. |
1118
+ | `.elevation-md` | `.floating` | Twice it, for menus and popovers. |
1119
+
1120
+ Both names in a row are equivalent, interchangeable classes — `.elevation-sm`
1121
+ and `.raised` are aliases, not a canonical form and a deprecated one, the way
1122
+ `.destructive` and `.danger` are on the intent axis. The `none`/`sm`/`md`
1123
+ naming matches the raw `--elevation-*` tokens in
1124
+ [Theming](docs/usage/theming.md#foundation-tokens) for the same weight of depth, even
1125
+ though the two are read from different places.
1126
+
1127
+ Each is one unitless multiplier over whatever shadow geometry is in scope, so
1128
+ the same class reads as a soft blur on a plain page and as a hard offset slab
1129
+ under `.neobrutalism` — the aesthetic decides what depth looks like, the
1130
+ class decides how much of it this element takes.
1131
+
1132
+ ```html
1133
+ <article class="card elevation-sm">Lifted</article>
1134
+ <div class="elevation-md panel">A menu surface</div>
1135
+ <article class="card elevation-none">Flat, whatever the page or container says</article>
1136
+ ```
1137
+
1138
+ The multiplier is a plain number, so it inherits: a container lifts or
1139
+ flattens a whole region and any element inside it still opts out on itself.
1140
+ Spread is deliberately left out of the multiplication, because an aesthetic
1141
+ that draws its edge as an inset ring spends spread on it and scaling that
1142
+ would erase the edge.
1143
+
1144
+ <!-- Source: docs/usage/content-and-layout.md -->
1145
+
1146
+ # Content and layout
1147
+
1148
+ ## Layout
1149
+
1150
+ Layout helpers use the shared `--layout-gap` token. `.tight` sets it to
1151
+ `0.5rem` and `.loose` sets it to `1.5rem` within a view, stack, cluster, or
1152
+ auto-grid.
1153
+
1154
+ - `.view` is a flex container; `.vertical` and `.horizontal` set its
1155
+ direction, and horizontal views wrap with centered cross-axis alignment.
1156
+ - `.stack` is a vertical flex stack.
1157
+ - `.cluster` is a wrapping horizontal flex row; `.between` adds
1158
+ `space-between` alignment.
1159
+ - `.auto-grid` is a responsive auto-fit grid using `--layout-grid-min`.
1160
+ - `.tight` and `.loose` set the shared gap to `0.5rem` or `1.5rem` on views,
1161
+ stacks, clusters, and auto-grids.
1162
+ - `.section` adds responsive block padding and an inline gutter.
1163
+ - `.section-content` centers content at `--container-max`; `.narrow` and
1164
+ `.wide` select the corresponding container tokens.
1165
+ - `.divider` is horizontal; `.vertical` makes it self-stretch vertically. It
1166
+ takes intent, but presentation classes do not affect it.
1167
+
1168
+ The removed `--layout-stack-gap` and `--layout-cluster-gap` tokens have no
1169
+ compatibility aliases.
1170
+
1171
+ ## Content
1172
+
1173
+ | Class | Purpose | Intent affects |
1174
+ | --------------- | ---------------------------------------------------------------------------------------- | ---------------------- |
1175
+ | `.table-wrap` | Full-width horizontal overflow wrapper for wide tables. | Nothing. |
1176
+ | `.data-table` | Rounded nested table styling for captions, heads, footers, cells, and rows. | Head, border, hover. |
1177
+ | `.ruled` | On `.data-table`. Draws a rule between body rows. | Rule color. |
1178
+ | `.ruleless` | On `.data-table`. Draws no lines inside the table at all. | Nothing. |
1179
+ | `.kbd` | Inline keyboard-input styling. | Surface, border, text. |
1180
+ | `.quote` | Block quote styling; a nested `cite` is set upright and takes the quotation's own color. | Bar, surface, text. |
1181
+ | `.quote-inline` | Inline quotation styling. | Nothing. |
1182
+ | `.code` | Inline code formatting. | Surface. |
1183
+ | `.pre` | Scrollable block code formatting with larger padding. | Surface. |
1184
+
1185
+ ### Table rules
1186
+
1187
+ A table draws three kinds of line, and `.ruled` and `.ruleless` move all of
1188
+ them together:
1189
+
1190
+ | Class | Head and foot boundaries | Rules between body rows |
1191
+ | ----------- | ------------------------ | ----------------------- |
1192
+ | `.ruleless` | No | No |
1193
+ | _no class_ | Yes | No |
1194
+ | `.ruled` | Yes | Yes |
1195
+
1196
+ ```html
1197
+ <table class="data-table">Head and foot boundaries only</table>
1198
+ <table class="data-table ruled">Every row separated</table>
1199
+ <table class="data-table ruleless">No lines inside the table at all</table>
1200
+ ```
1201
+
1202
+ The boundaries under a head and above a foot separate the parts of a table
1203
+ from each other, so they are drawn by default where the row rules are not.
1204
+ Rules between body rows used to arrive on their own, because the
1205
+ component's published edge default is `edged` and the edge axis wrote the
1206
+ rules along with the boundary — so whether you got them depended on a
1207
+ registry default rather than on anything in your markup.
1208
+
1209
+ `--ui-rule` is the token behind both classes, so a container can set the
1210
+ answer for a whole region without classing each table:
1211
+
1212
+ ```html
1213
+ <section style="--ui-rule: 100%">
1214
+ <table class="data-table">Ruled by the region</table>
1215
+ </section>
1216
+ ```
1217
+
1218
+ The frame around the table is the edge axis and is asked for the same way:
1219
+ `.data-table` publishes `edgeless`, so `.edged` draws one. None of the
1220
+ three switches reaches the others — `.data-table.ruleless.edged` is a
1221
+ framed table with no lines inside it, and `.data-table.ruled` is a
1222
+ frameless one with every row separated.
1223
+
1224
+ A table's rows deliberately inherit its intent instead of resetting it, so
1225
+ `.data-table.success` tints throughout. A row carrying its own intent still
1226
+ wins:
1227
+
1228
+ ```html
1229
+ <table class="data-table success">
1230
+ <tbody>
1231
+ <tr>
1232
+ <td>Inherits the table intent</td>
1233
+ </tr>
1234
+ <tr class="destructive">
1235
+ <td>Overrides it for this row</td>
1236
+ </tr>
1237
+ </tbody>
1238
+ </table>
1239
+ ```
1240
+
1241
+ This is the one place intent cascades, because a table's rows are parts of
1242
+ the table rather than independent components.
1243
+
1244
+ `.data-table`, `.kbd`, `.code`, and `.pre` also read
1245
+ [presentation](docs/usage/composing.md#presentation). A table applies it to its
1246
+ header, its rules and its border; a chip to its whole plate:
1247
+
1248
+ ```html
1249
+ <table class="data-table success soft edged">...</table>
1250
+ <table class="data-table">A ghost table: rules and type, no header plate</table>
1251
+ <kbd class="kbd primary solid">Ctrl</kbd>
1252
+ <kbd class="kbd primary soft edged">Shift</kbd>
1253
+ ```
1254
+
1255
+ `.ghost` means two different things on these components, and both are worth
1256
+ knowing before you reach for it.
1257
+
1258
+ On `.data-table` it removes the header plate along with the body tint, so
1259
+ `.data-table.ghost` is boundaries and type alone. The component rests at
1260
+ `soft`, so that is something to ask for rather than the default.
1261
+
1262
+ On `.kbd`, `.code`, and `.pre` it is **not supported**. All three rest on
1263
+ `--intent-subtle`, and that ground draws the plate whatever the fill says —
1264
+ so `.ghost` took the fill away and changed nothing visible. Worse, the
1265
+ ground alone is a near-page tint: a ghost chip measured the same `#e5e5e5`
1266
+ for neutral and primary on the light page, and `#f5f5f5` at 1.04:1 against
1267
+ it for secondary. All three rest at `.soft` instead, which puts a real `12%`
1268
+ of the intent over the ground and separates them.
1269
+
1270
+ Use `.table-wrap` around `.data-table` when table width may exceed its
1271
+ container:
1272
+
1273
+ ```html
1274
+ <div class="table-wrap">
1275
+ <table class="data-table">
1276
+ <thead>
1277
+ <tr>
1278
+ <th>Package</th>
1279
+ <th>Status</th>
1280
+ </tr>
1281
+ </thead>
1282
+ <tbody>
1283
+ <tr>
1284
+ <td>styles</td>
1285
+ <td>Ready</td>
1286
+ </tr>
1287
+ </tbody>
1288
+ </table>
1289
+ </div>
1290
+ ```
1291
+
1292
+ ## Surfaces
1293
+
1294
+ | Class | Purpose |
1295
+ | -------------------- | ---------------------------------------------------------------------------- |
1296
+ | `.card` | Bordered container, surface radius, padded. Rests at elevation `1`. |
1297
+ | `.panel` | Flush container for sidebars, toolbars, and wells. No elevation. |
1298
+ | `.interactive` | On `.card`. Adds pointer cursor, hover and press response, and a focus ring. |
1299
+ | `.p-sm`, `.compact` | On `.card` or `.panel`. Reduces padding. |
1300
+ | `.p-lg`, `.spacious` | On `.card` or `.panel`. Increases padding. |
1301
+ | `.flush` | On `.card` or `.panel`. Removes padding, for edge-to-edge content. |
1302
+
1303
+ Both read intent, [presentation](docs/usage/composing.md#presentation), and
1304
+ [material tokens](docs/usage/customizing.md#material-tokens). A plain `.card` is a
1305
+ neutral bordered container; only an explicit presentation tints it. Resting
1306
+ at elevation `1` is not a visible shadow by itself — nothing draws depth
1307
+ until an [aesthetic](docs/usage/aesthetics.md) supplies shadow geometry or
1308
+ `.elevation-sm`/`.elevation-md` asks for it; see
1309
+ [Composing → Elevation](docs/usage/composing.md#elevation).
1310
+
1311
+ On the base look, `.card.interactive` scales down slightly on press
1312
+ (`--ui-active-transform`, `scale(0.97)`); an [aesthetic](docs/usage/aesthetics.md)
1313
+ substitutes its own press gesture, and `prefers-reduced-motion` turns it off.
1314
+
1315
+ ```html
1316
+ <article class="card">Neutral card</article>
1317
+ <article class="card success soft">Tinted success card</article>
1318
+ <article class="card primary ghost edged">Intent border, no fill</article>
1319
+ <a class="card interactive" href="/package">Responds to hover and press</a>
1320
+ <aside class="panel">Flush panel</aside>
1321
+ ```
1322
+
1323
+ `.interactive` is styling only. Use a real interactive element and give it
1324
+ an accessible name; a `<div class="card interactive">` is not focusable or
1325
+ operable by keyboard.
1326
+
1327
+ ## Typography utilities
1328
+
1329
+ All typography classes and `.selection-contrast` are `@utility` classes.
1330
+
1331
+ | Class | Purpose |
1332
+ | --------------------- | ------------------------------------------------------------ |
1333
+ | `.text-display` | Large display headings. |
1334
+ | `.text-title-lg` | Large section titles. |
1335
+ | `.text-title` | Default section titles. |
1336
+ | `.text-title-sm` | Smaller titles or card titles. |
1337
+ | `.text-label-lg` | Large label text. |
1338
+ | `.text-label` | Default label text. |
1339
+ | `.text-body` | Default body copy. |
1340
+ | `.selection-contrast` | Inverts `::selection` colors to primary-contrast background. |
1341
+
1342
+ <!-- Source: docs/usage/customizing.md -->
1343
+
1344
+ # Customizing
1345
+
1346
+ Presentation tokens decide how much of an intent shows. Material tokens
1347
+ decide what a component is made of. Aesthetic tokens are the small set of
1348
+ named knobs a shipped aesthetic exposes to scale its own look.
1349
+
1350
+ ## Presentation tokens
1351
+
1352
+ Presentation tokens describe _how much_ of an intent a component shows,
1353
+ never _which_ intent it shows. They are unitless numbers and percentages, so
1354
+ they carry no color and inherit safely: a container sets them once and every
1355
+ component below resolves them against its own intent.
1356
+
1357
+ | Token | Purpose |
1358
+ | ----------------- | -------------------------------------------------------------------------- |
1359
+ | `--ui-fill` | Percentage of the intent color mixed into the background. |
1360
+ | `--ui-fg-on-fill` | Percentage blended from the intent readable tone toward its contrast tone. |
1361
+ | `--ui-border` | Percentage of the intent color mixed into the border color. |
1362
+
1363
+ A component resolves them with `color-mix` from its published default. Hover
1364
+ is derived by adding `--hover-step` to the resting fill and using
1365
+ `--intent-hover`; there are no per-presentation hover tokens.
1366
+
1367
+ No fill class writes `--ui-border` and no edge class writes `--ui-fill`. A
1368
+ filled box with no line is `.solid.edgeless`, which is worth reaching for on
1369
+ a neutral component: neutral caps its fill, so the box stays translucent and
1370
+ a border over it paints a second coat of the same tint instead of blending
1371
+ into it.
1372
+
1373
+ ```css
1374
+ background: color-mix(in oklab, var(--color-primary) var(--ui-fill, 100%), transparent);
1375
+ ```
1376
+
1377
+ Two choices there are deliberate. Mixing toward `transparent` rather than a
1378
+ background token lets a tinted surface adapt to whatever it is placed on.
1379
+ Mixing in `oklab` keeps the wide-gamut token palette intact and blends
1380
+ perceptually, where `srgb` would clip it.
1381
+
1382
+ A computed color therefore reaches the page as a resolved `color-mix` result
1383
+ rather than the token's own `oklch` syntax. It is the same color; code that
1384
+ compares computed color strings should compare colors instead.
1385
+
1386
+ > **Presentation token contract**: only presentation classes and consumer
1387
+ > overrides set these tokens; components only read them. Setting a value
1388
+ > that no presentation class produces is supported but unvalidated. See
1389
+ > [Composing](docs/usage/composing.md) for the classes that ship these values.
1390
+
1391
+ ## Material tokens
1392
+
1393
+ Material tokens describe what a component is _made of_ rather than which
1394
+ intent it shows or how strongly. They control shape, borders, shadows,
1395
+ translucency, filters, and transforms.
1396
+
1397
+ Components read each with a fallback, so leaving them unset produces the
1398
+ default look. Setting them on a container restyles the whole subtree. Only
1399
+ shadow geometry is split into colorless parts that inherit safely;
1400
+ color-capable inputs such as `--ui-ink` and `--ui-surface-shadow` may
1401
+ include their own colors.
1402
+
1403
+ | Token | Purpose | Fallback |
1404
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------- | -------------------- |
1405
+ | `--ui-radius` | Corner radius for controls. | `--radius-control` |
1406
+ | `--ui-radius-surface` | Corner radius for surfaces. | `--radius-surface` |
1407
+ | `--ui-border-width` | Edge thickness. | `--border-width` |
1408
+ | `--ui-border-max` | Ceiling on computed edge width. | `100px` |
1409
+ | `--ui-ink` | Neutral line color when no intent is set. | `--color-border` |
1410
+ | `--ui-rule` | Lines inside a table. Unset draws the head and foot boundaries only; `0%` draws none; `100%` rules every row. | unset |
1411
+ | `--ui-shadow-x` | Shadow horizontal offset. | `0px` |
1412
+ | `--ui-shadow-y` | Shadow vertical offset. | `0px` |
1413
+ | `--ui-shadow-blur` | Shadow blur radius. | `0px` |
1414
+ | `--ui-shadow-spread` | Shadow spread radius. | `0px` |
1415
+ | `--ui-shadow-inset` | The `inset` keyword for an inner ring. | Empty |
1416
+ | `--ui-hover-shadow-x` | Shadow horizontal offset while hovered. | `--ui-shadow-x` |
1417
+ | `--ui-hover-shadow-y` | Shadow vertical offset while hovered. | `--ui-shadow-y` |
1418
+ | `--ui-active-shadow-x` | Shadow horizontal offset while pressed. | `--ui-shadow-x` |
1419
+ | `--ui-active-shadow-y` | Shadow vertical offset while pressed. | `--ui-shadow-y` |
1420
+ | `--ui-active-transform` | Transform while pressed. `none` under `prefers-reduced-motion`. | `scale(0.97)` |
1421
+ | `--ui-shadow-ink` | Percentage of shadow color taken from intent ink. | `0%` |
1422
+ | `--ui-shadow-edge` | Declared, even empty, when the shadow is the element's edge rather than its depth. Read for presence. | Unset |
1423
+ | `--ui-elevation` | Unitless multiplier over shadow geometry. | `1` |
1424
+ | `--ui-surface-shadow` | Complete multi-layer shadow accepted by surfaces only. | Unset |
1425
+ | `--ui-surface-ground` | Ground a surface sits on. | `--color-background` |
1426
+ | `--ui-bg-alpha` | Multiplier over fill for translucent materials. | `1` |
1427
+ | `--ui-backdrop` | Backdrop filter accepted by surfaces only. | `none` |
1428
+ | `--ui-hover-transform` | Transform while an interactive element is hovered. | `none` |
1429
+ | `--ui-clip` | Structural component silhouette. | `none` |
1430
+ | `--ui-clip-tight` | Compact component silhouette. | `--ui-clip` |
1431
+ | `--ui-focus-inset` | Inset focus layer width. | Unset |
1432
+
1433
+ ```html
1434
+ <section style="--ui-radius: 0; --ui-border-width: 3px">
1435
+ <button class="btn primary">Square and heavy</button>
1436
+ <input class="ipt" placeholder="Matches" />
1437
+ </section>
1438
+ ```
1439
+
1440
+ > **Material token contract**: components read these; aesthetic classes and
1441
+ > consumer overrides set them.
1442
+
1443
+ ### Shape and ring composition
1444
+
1445
+ Clipping removes borders, outlines, and outer shadows. An aesthetic that uses
1446
+ a clip can replace the border with an inset shadow by setting
1447
+ `--ui-shadow-inset` and the four shadow geometry parts. Set
1448
+ `--ui-border-max: 0px` when that ring replaces the component border.
1449
+ Focusable components restore the clipped focus outline through
1450
+ `--ui-focus-inset`.
1451
+
1452
+ Declare `--ui-shadow-edge` when that ring _is_ the border. The ring then
1453
+ takes the element's edge color, which carries the edge axis as its alpha, so
1454
+ `.edged` draws it and `.edgeless` does not — the same answer a real border
1455
+ gives. Without it the ring paints on every component the aesthetic reaches
1456
+ and the edge classes have no effect. Use `--ui-shadow-ink` instead when the
1457
+ shadow is depth cast in the intent's own ink, such as a hard offset slab,
1458
+ which an `.edgeless` element still gets.
1459
+
1460
+ The token is read for its presence, not its value: declare it empty to
1461
+ switch the ring on, and `initial` to switch it back off in an aesthetic
1462
+ nested inside one that declares it.
1463
+
1464
+ ```css
1465
+ .stepped {
1466
+ --ui-clip: polygon(/* structural silhouette */);
1467
+ --ui-clip-tight: none;
1468
+ --ui-border-max: 0px;
1469
+ --ui-shadow-inset: inset;
1470
+ --ui-shadow-edge: ;
1471
+ --ui-shadow-x: 0px;
1472
+ --ui-shadow-y: 0px;
1473
+ --ui-shadow-blur: 0px;
1474
+ --ui-shadow-spread: 4px;
1475
+ --ui-focus-inset: var(--focus-ring-width);
1476
+ }
1477
+ ```
1478
+
1479
+ Variables named `--shape-*` are internal composition outputs. Do not set or
1480
+ read them; only the `--ui-*` inputs above are public.
1481
+
1482
+ ## Aesthetic tokens
1483
+
1484
+ The shipped [aesthetic classes](docs/usage/aesthetics.md) set material tokens for you.
1485
+ Only aesthetics with a token listed below expose an aesthetic-specific
1486
+ token. Tune all other behavior through the shared material tokens above.
1487
+ Listed tokens are available only where that aesthetic's stylesheet is
1488
+ imported.
1489
+
1490
+ | Token | Aesthetic | Purpose | Default |
1491
+ | ------------------------ | --------------- | ----------------------------------------------------------- | --------------------------------- |
1492
+ | `--neo-offset` | `.neobrutalism` | Hard shadow offset and distance the press travels. | `4px` |
1493
+ | `--glass-radius` | `.glass` | Control corner radius. | `0.75rem` |
1494
+ | `--glass-radius-surface` | `.glass` | Surface corner radius. | `1rem` |
1495
+ | `--pixel-unit` | `.pixel` | One pixel of the grid. The corner cut and the ring are one. | `4px` |
1496
+ | `--font-pixel` | `.pixel` | Consumer-supplied pixel font stack. | Falls back to the monospace stack |
1497
+ | `--tile-radius` | `.chunky-tile` | Corner radius, on controls and surfaces alike. | `0.75rem` |
1498
+ | `--tile-lift` | `.chunky-tile` | Depth of the seated bar, and how far a press travels. | `4px` |
1499
+ | `--font-rounded` | `.chunky-tile` | Consumer-supplied rounded font stack. | Falls back to the page stack |
1500
+
1501
+ All four aesthetics reach the same way: each knob is _read_ with its default
1502
+ as a `var()` fallback rather than declared, so it resolves once into a
1503
+ private on the aesthetic's own class (`--_pixel-unit`, `--_neo-offset`,
1504
+ `--_tile-radius`, `--_tile-lift`, and so on). Set a knob on the element
1505
+ carrying the aesthetic class, or on any ancestor including `:root`:
1506
+
1507
+ ```html
1508
+ <section class="pixel" style="--pixel-unit: 6px">
1509
+ <button class="btn primary">Chunkier</button>
1510
+ </section>
1511
+ ```
1512
+
1513
+ ```html
1514
+ <html class="pixel" style="--pixel-unit: 6px">
1515
+ <body>
1516
+ <button class="btn primary">Also chunkier, set from the root</button>
1517
+ </body>
1518
+ </html>
1519
+ ```
1520
+
1521
+ Knobs and material tokens reach in opposite directions, and telling them
1522
+ apart matters. A knob resolves once at the aesthetic's own class and
1523
+ inherits down already-resolved, so it reaches the aesthetic **from above**:
1524
+ setting it on the element or any ancestor works, but setting it on a
1525
+ _descendant_ of the aesthetic class changes nothing — the value has already
1526
+ been read. `--ui-radius` and the other material tokens above are read fresh
1527
+ by each component as it draws, so they reach **at or below** the element
1528
+ carrying them, which is how you round one card differently from its
1529
+ neighbours without touching the knob at all.
1530
+
1531
+ Shipped aesthetics set the shared material tokens above. `.neobrutalism` and
1532
+ `.pixel` set `--ui-ink` for their neutral outline; intent classes still
1533
+ override the intent slots on each component.
1534
+
1535
+ ## Component internals
1536
+
1537
+ Component classes may define scoped implementation variables such as
1538
+ `--button-*`, `--surface-*`, `--control-*`, `--feedback-*`, and `--tooltip-*`.
1539
+ These variables are internal wiring for class composition and are not the
1540
+ public token contract.
1541
+
1542
+ Consumers should customize broad behavior through color, foundation, motion,
1543
+ elevation, radius, layout, and focus tokens first. The only supported
1544
+ component-scoped input is `--progress-value` on the `.progress` element,
1545
+ because consumers must provide a value such as `64%`.
1546
+
1547
+ `--layout-gap` replaces the removed `--layout-stack-gap` and
1548
+ `--layout-cluster-gap` tokens. No compatibility aliases are provided.
1549
+
1550
+ Next: [Aesthetics](docs/usage/aesthetics.md) covers each shipped aesthetic's complete
1551
+ look and its documented exceptions.
1552
+
1553
+ <!-- Source: docs/usage/feedback.md -->
1554
+
1555
+ # Feedback
1556
+
1557
+ | Class | Purpose |
1558
+ | ---------------- | ---------------------------------------------------------------------------- |
1559
+ | `.alert` | Inline feedback surface. |
1560
+ | `.icon` | Subclass of `.alert`. Adds a corresponding intent icon and padding. |
1561
+ | `.badge` | Compact status pill. |
1562
+ | `.loader-mask` | Low-level activity indicator composition utility. Applies the spinner mask. |
1563
+ | `.loader` | Standalone inline loader artwork. |
1564
+ | `.skeleton` | Ambient loading placeholder. |
1565
+ | `.progress` | Progress track. Uses `--progress-value` variable. |
1566
+ | `.active` | Optional on `.progress` to add a track shimmer. |
1567
+ | `.indeterminate` | Optional on `.progress` to animate a moving fill without `--progress-value`. |
1568
+
1569
+ Activity indicator modifier classes compose with `.loader`:
1570
+
1571
+ | Class | Animation style |
1572
+ | ---------------------- | --------------------------------------- |
1573
+ | _(default)_ | Circular spinner (rotating arc). |
1574
+ | `.dots-wave` | Three dots bouncing up/down in a wave. |
1575
+ | `.dots-fade` | Three dots fading in and out. |
1576
+ | `.dots-queue` | Dot queuing from left to right. |
1577
+ | `.dots-rotate` | Side dots rotating around a center dot. |
1578
+ | `.dots-grow` | Three dots growing and shrinking. |
1579
+ | `.dots-grow-alternate` | Outer dots small, center dot pulses. |
1580
+ | `.dot-bounce` | Single dot bouncing with squash effect. |
1581
+ | `.bars-wave` | Three vertical bars scaling in a wave. |
1582
+ | `.pulse-ring` | Two concentric rings pulsing outward. |
1583
+
1584
+ These variants retain their embedded SVG animations normally. Under
1585
+ `prefers-reduced-motion: reduce`, loader CSS substitutes a corresponding
1586
+ static mask. This fallback is part of focused loader, button, and components
1587
+ imports; it does not depend on the global reset. See
1588
+ [Accessibility](docs/accessibility.md).
1589
+
1590
+ Loader size modifiers:
1591
+
1592
+ | Class | Size |
1593
+ | -------- | ----------------- |
1594
+ | _(none)_ | Default (`2rem`). |
1595
+ | `.sm` | Small (`1.5rem`). |
1596
+ | `.lg` | Large (`2.5rem`). |
1597
+
1598
+ Alerts, badges, progress bars, skeletons, and loaders accept `.primary`,
1599
+ `.secondary`, `.success`, `.warning`, `.destructive`, `.danger`, `.error`,
1600
+ and `.info`. Without an intent, they use the text palette, except `.loader`,
1601
+ which keeps `currentColor` so it matches whatever content surrounds it.
1602
+
1603
+ Skeletons, loaders, progress bars, and dividers are indicators. They read
1604
+ intent but ignore fill and edge [presentation](docs/usage/composing.md#presentation).
1605
+
1606
+ ```html
1607
+ <span class="loader success" aria-hidden="true"></span>
1608
+ <span class="skeleton info"></span>
1609
+ ```
1610
+
1611
+ `.icon` is a subclass of `.alert`. When applied as `.alert.icon`, it
1612
+ increases the left padding and adds an embedded SVG. Success, warning, and
1613
+ destructive intents use corresponding symbols; other intents use the
1614
+ information symbol.
1615
+
1616
+ Alerts and badges read the shared [presentation](docs/usage/composing.md#presentation)
1617
+ classes. Without one they use a tinted surface, intent-colored text, and a
1618
+ mixed intent border.
1619
+
1620
+ ## Example
1621
+
1622
+ ```html
1623
+ <div class="alert success" role="status">Saved successfully.</div>
1624
+ <div class="alert primary solid" role="status">Deployment started.</div>
1625
+ <div class="alert warning soft icon" role="status">Review required.</div>
1626
+ <div class="alert destructive ghost edged" role="alert">Deployment failed.</div>
1627
+ <span class="badge warning">Queued</span>
1628
+ <span class="badge success solid">Live</span>
1629
+ <span class="badge info soft">Draft</span>
1630
+ <span class="loader" aria-hidden="true"></span>
1631
+ <span class="loader dots-wave" aria-hidden="true"></span>
1632
+ <span class="loader dots-fade" aria-hidden="true"></span>
1633
+ <span class="loader bars-wave" aria-hidden="true"></span>
1634
+ <span class="loader pulse-ring sm" aria-hidden="true"></span>
1635
+ <span class="loader dots-grow lg" aria-hidden="true"></span>
1636
+ <div
1637
+ class="progress"
1638
+ role="progressbar"
1639
+ aria-label="Upload progress"
1640
+ aria-valuemin="0"
1641
+ aria-valuemax="100"
1642
+ aria-valuenow="64"
1643
+ style="--progress-value: 64%"
1644
+ ></div>
1645
+ <div
1646
+ class="progress secondary active"
1647
+ role="progressbar"
1648
+ aria-label="Upload progress"
1649
+ aria-valuemin="0"
1650
+ aria-valuemax="100"
1651
+ aria-valuenow="64"
1652
+ style="--progress-value: 64%"
1653
+ ></div>
1654
+ <div
1655
+ class="progress info indeterminate"
1656
+ role="progressbar"
1657
+ aria-label="Loading"
1658
+ aria-valuemin="0"
1659
+ aria-valuemax="100"
1660
+ ></div>
1661
+ ```
1662
+
1663
+ Add `role="status"` or `role="alert"` to alerts based on announcement
1664
+ urgency, mark decorative loading visuals with `aria-hidden="true"`, and use
1665
+ semantic progress elements or ARIA values when numeric progress must be
1666
+ announced; see [Accessibility](docs/accessibility.md).
1667
+
1668
+ <!-- Source: docs/usage/forms.md -->
1669
+
1670
+ # Forms
1671
+
1672
+ | Class or Selector | Purpose |
1673
+ | --------------------------------------------------------------------- | ------------------------------------------------------------- |
1674
+ | `.field` | Vertical field wrapper. |
1675
+ | `.label` | Form label text. |
1676
+ | `.hint` | Secondary helper text. |
1677
+ | `.hint.error` | Helper text with destructive intent. |
1678
+ | `.surface` | Shared public container composition utility. |
1679
+ | `.text-control` | Shared public text-control composition utility. |
1680
+ | `.ipt` | Input control styling. |
1681
+ | `.input-group` | Wrapper that owns the field box so a control can carry icons. |
1682
+ | `.textarea` | Textarea control styling. |
1683
+ | `.select` | Select control styling. |
1684
+ | `input[type="checkbox"].checkbox` | Custom checkbox control styling. |
1685
+ | `input[type="radio"].radio` | Custom radio control styling. |
1686
+ | `input[type="checkbox"].switch` | Custom switch control styling. |
1687
+ | `[aria-invalid="true"]` on controls | Destructive border and focus color. |
1688
+ | `[disabled]`, `[aria-disabled="true"]`, `[data-disabled]` on controls | Disabled styling. |
1689
+
1690
+ ## Icons
1691
+
1692
+ The package ships no icons. It used to paint one per input type as a
1693
+ `background-image` — a `data:` URI, which cannot read `currentColor` or a
1694
+ custom property, so every glyph shipped a light and a dark copy and a theme
1695
+ selector chose between them. That put icon artwork, and the package's only
1696
+ theme-by-selector branch, inside a CSS-only design system whose glyphs did
1697
+ not fit every aesthetic. Icons are the consumer's to choose now.
1698
+
1699
+ `.input-group` is the wrapper for a control that carries one. It owns the
1700
+ field box — border, radius, fill, focus ring, invalid and disabled state —
1701
+ and the control inside it goes flush, so the boundary is drawn once. Any
1702
+ icon element works, placed before the control for a leading icon or after it
1703
+ for a trailing one: an inline `<svg>`, an `<img>`, or a class from an icon
1704
+ set such as `@codenhub/icons`.
1705
+
1706
+ ```html
1707
+ <label class="field">
1708
+ <span class="label">Email</span>
1709
+ <div class="input-group">
1710
+ <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" aria-hidden="true">
1711
+ <rect width="20" height="16" x="2" y="4" rx="2" />
1712
+ <path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7" />
1713
+ </svg>
1714
+ <input class="ipt" type="email" />
1715
+ </div>
1716
+ </label>
1717
+ <label class="field">
1718
+ <span class="label">Search</span>
1719
+ <div class="input-group">
1720
+ <input class="ipt" type="search" />
1721
+ <i class="ic-search" aria-hidden="true"></i>
1722
+ </div>
1723
+ </label>
1724
+ ```
1725
+
1726
+ The group reads intent and both [presentation](docs/usage/composing.md#presentation)
1727
+ axes the same way a lone `.ipt` does — `.input-group.soft.edgeless` is the
1728
+ sunk variant, `.input-group.primary` colors the boundary — and follows
1729
+ whichever [aesthetic](docs/usage/aesthetics.md) is in scope. `aria-invalid` or
1730
+ `disabled` on the control inside propagates to the group; so does either on
1731
+ the group itself. Focus is shown with `:focus-within`, so unlike a lone
1732
+ field the ring also appears on a mouse click.
1733
+
1734
+ Non-icon input types keep the browser's native `date` and `datetime-local`
1735
+ pickers. WebKit's native `search` decorations are still suppressed on
1736
+ `.text-control` to keep them from overlapping the value.
1737
+
1738
+ ## Toggles
1739
+
1740
+ `.checkbox`, `.radio`, and `.switch` accept the same intent classes as
1741
+ buttons to set the checked color:
1742
+
1743
+ | Class | Meaning |
1744
+ | ----------------------------------- | ----------------------- |
1745
+ | `.neutral` _(default)_ | Text color, capped. |
1746
+ | `.primary` | Primary color. |
1747
+ | `.secondary` | Secondary/accent color. |
1748
+ | `.success` | Success color. |
1749
+ | `.warning` | Warning color. |
1750
+ | `.destructive`, `.danger`, `.error` | Destructive color. |
1751
+ | `.info` | Info color. |
1752
+
1753
+ All three toggles rest at `.solid`, and **`.ghost` is not supported on any of
1754
+ them.** An unchecked ghost toggle is the silhouette every toggle already
1755
+ has, and a checked one is a mark on nothing — a tick floating on the page, a
1756
+ dot with no ring around it. A container that cascades `.ghost` onto a
1757
+ toggle is floored rather than obeyed.
1758
+
1759
+ Presentation decides the fill in both states. Being checked lifts the cap
1760
+ rather than pinning a fill, so the class still chooses:
1761
+
1762
+ | Toggle | unchecked | checked |
1763
+ | ----------------- | --------------- | ------------------------------- |
1764
+ | `.solid`, default | 20%, switch 40% | 100%, mark in the contrast tone |
1765
+ | `.soft` | 12% | 12%, mark in the strong tone |
1766
+
1767
+ A checked checkbox and a checked switch cut their mark out of that plate. A
1768
+ checked radio thickens its ring to twice the resting line and takes the
1769
+ intent whole on it, so `.radio.soft` is the classic ring-and-dot radio and
1770
+ `.radio.solid` is a filled circle whose dot stays readable — the mark
1771
+ follows the plate rather than being pinned to the intent color.
1772
+
1773
+ ## Text controls
1774
+
1775
+ Text controls also take intent, which colors the resting border and the
1776
+ focus-visible border, and both [presentation](docs/usage/composing.md#presentation)
1777
+ axes. `.ghost` fills nothing, `.soft` takes `12%`, and `.solid` takes `20%`
1778
+ — quiet enough that typed text still reads on it, and ordered so the louder
1779
+ name draws the stronger tint. None of them touches the line.
1780
+
1781
+ The boundary is the edge axis's, and it splits the same way. A container's
1782
+ `.edgeless` is floored, so a toolbar cannot leave a field with no mark of
1783
+ where typing goes. `.edgeless` on the control itself is honoured, which is
1784
+ how the borderless field is spelled — it does not meet WCAG 1.4.11 at rest,
1785
+ so it is opt-in and never a default, and the focus ring still shows.
1786
+
1787
+ ```html
1788
+ <input class="ipt success" placeholder="Valid" />
1789
+ <input class="ipt ghost edged" placeholder="Outlined" />
1790
+ <input class="ipt soft edged" placeholder="Tinted with a border" />
1791
+ <input class="ipt soft edgeless" placeholder="Sunk into the page" />
1792
+ <div class="soft">
1793
+ <input class="ipt" placeholder="Tinted from the container, capped at 6%" />
1794
+ </div>
1795
+ <div class="edgeless">
1796
+ <input class="ipt" placeholder="Keeps its line; the floor holds" />
1797
+ </div>
1798
+ ```
1799
+
1800
+ `.surface` is a public low-level utility. It is the box every container in
1801
+ the package composes: ghost, edged, surface radius, and the two slots only
1802
+ a surface resolves — the backdrop filter and the complete surface shadow.
1803
+ Use it to paint a container the package does not ship, under whichever
1804
+ aesthetic is in scope. Prefer `.card` or `.panel` when they fit.
1805
+
1806
+ ```html
1807
+ <div class="surface glass">Blurred, translucent, glass corners</div>
1808
+ <div class="surface pixel">Cut corners and an inset ring</div>
1809
+ <div class="surface primary solid">Any intent, any presentation</div>
1810
+ ```
1811
+
1812
+ `.text-control` is a public low-level utility from the form entrypoint. It
1813
+ provides the shared control dimensions, border, placeholder, focus-visible,
1814
+ invalid, and disabled styles composed by `ipt`, `textarea`, and `select`.
1815
+ Use it for custom text-like controls; prefer those higher-level utilities
1816
+ when they fit.
1817
+
1818
+ ## Example
1819
+
1820
+ ```html
1821
+ <label class="field">
1822
+ <span class="label">Email</span>
1823
+ <input class="ipt" type="email" aria-invalid="true" aria-describedby="email-error" />
1824
+ <span class="hint error" id="email-error">Enter a valid email.</span>
1825
+ </label>
1826
+ <label style="display: flex; gap: 0.5rem; align-items: center">
1827
+ <input type="checkbox" class="checkbox success" />
1828
+ <span>Accept terms</span>
1829
+ </label>
1830
+ <label style="display: flex; gap: 0.5rem; align-items: center">
1831
+ <input type="radio" class="radio secondary" name="plan" />
1832
+ <span>Standard plan</span>
1833
+ </label>
1834
+ <label style="display: flex; gap: 0.5rem; align-items: center">
1835
+ <input type="checkbox" class="switch destructive" />
1836
+ <span>Enable</span>
1837
+ </label>
1838
+ ```
1839
+
1840
+ Use labels, `type`, validation logic, `aria-describedby`, and error message
1841
+ relationships; see [Accessibility](docs/accessibility.md).
1842
+
1843
+ <!-- Source: docs/usage/index.md -->
1844
+
1845
+ # Usage
1846
+
1847
+ These guides assume the [Concepts](docs/concepts.md) mental model: intent,
1848
+ presentation, and aesthetic as three independent axes, resolved through
1849
+ tokens a component reads.
1850
+
1851
+ - [Composing](docs/usage/composing.md): Combining the three axes on a component, the
1852
+ axis-support matrix, and elevation modifiers.
1853
+ - [Theming](docs/usage/theming.md): The color, intent, and foundation token reference.
1854
+ - [Customizing](docs/usage/customizing.md): The presentation and material token
1855
+ reference, aesthetic knobs, and defining a custom intent.
1856
+ - [Aesthetics](docs/usage/aesthetics.md): Each shipped aesthetic in depth, including
1857
+ its documented exceptions.
1858
+ - [Buttons](docs/usage/buttons.md)
1859
+ - [Forms](docs/usage/forms.md)
1860
+ - [Feedback](docs/usage/feedback.md)
1861
+ - [Tooltips](docs/usage/tooltips.md)
1862
+ - [Content and layout](docs/usage/content-and-layout.md): Layout helpers, tables,
1863
+ quotes, code, surfaces, and typography utilities.
1864
+
1865
+ ## Supported surface
1866
+
1867
+ A class, component, and axis combination is supported when this documentation
1868
+ describes it and the package playground demonstrates it. Undocumented and
1869
+ undemonstrated combinations may still produce CSS, but are not maintained
1870
+ behavior. Playground variant matrices are the exhaustive demonstrations; small
1871
+ examples elsewhere illustrate usage without widening that contract.
1872
+
1873
+ <!-- Source: docs/usage/theming.md -->
1874
+
1875
+ # Theming
1876
+
1877
+ Color tokens are how you restyle the palette; see
1878
+ [Concepts → Theme selection](docs/concepts.md#theme-selection) for how a theme
1879
+ is chosen. Intent tokens are how a component reads that palette once an
1880
+ intent class is applied. Foundation tokens are the shared layout, shape, and
1881
+ motion defaults underneath everything.
1882
+
1883
+ ## Color tokens
1884
+
1885
+ Each color token holds both of its theme values at once through
1886
+ `light-dark()`, and the element's `color-scheme` picks one.
1887
+
1888
+ ```html
1889
+ <section data-theme="dark">
1890
+ <article>Dark token subtree</article>
1891
+ </section>
1892
+ ```
1893
+
1894
+ | Token | Purpose |
1895
+ | ------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
1896
+ | `--color-primary` | Primary action/content color. |
1897
+ | `--color-primary-contrast` | Text/icon color on primary filled surface. |
1898
+ | `--color-primary-hover` | Primary hover state. |
1899
+ | `--color-primary-subtle` | Low-emphasis companion tone for primary. Use as soft surface or subtle background. |
1900
+ | `--color-primary-strong` | High-emphasis companion tone for primary. Use as readable text, icon, or border on subtle surfaces. |
1901
+ | `--color-accent` | Secondary/accent surface. |
1902
+ | `--color-accent-contrast` | Text/icon color on accent filled surface. |
1903
+ | `--color-accent-hover` | Accent hover state. |
1904
+ | `--color-accent-subtle` | Low-emphasis companion tone for accent. Use as soft surface or subtle background. |
1905
+ | `--color-accent-strong` | High-emphasis companion tone for accent. Use as readable text, icon, or border on subtle surfaces. |
1906
+ | `--color-border` | Default border color. |
1907
+ | `--color-border-hover` | Interactive border color. |
1908
+ | `--color-control-border` | Resting line on the six text controls; one value, drawn at a fraction on text inputs. |
1909
+ | `--color-background` | Page and default raised surface background. |
1910
+ | `--color-foreground` | Subtle raised foreground surface. |
1911
+ | `--color-surface` | Nested or muted surface. |
1912
+ | `--color-text` | Primary text color. |
1913
+ | `--color-text-secondary` | Secondary text color. |
1914
+ | `--color-text-contrast` | Readable color on top of a full fill of `--color-text`. A checked toggle prints it. |
1915
+ | `--color-text-hover` | Text hover state. |
1916
+ | `--color-text-subtle` | Low-emphasis companion tone for text. Use as soft surface or subtle background. |
1917
+ | `--color-text-strong` | High-emphasis companion tone for text. Use as readable text, icon, or border on subtle surfaces. |
1918
+ | `--color-tooltip` | Tooltip bubble plate with no intent. Chosen per theme rather than derived: near-white in light, mid grey in dark. |
1919
+ | `--color-tooltip-contrast` | Ink on `--color-tooltip`. |
1920
+ | `--color-success` | Success state color. |
1921
+ | `--color-success-contrast` | Text/icon color on success filled surface. Meets 4.5:1 as normal text on `--color-success` (5.14:1). |
1922
+ | `--color-success-hover` | Success hover tone. |
1923
+ | `--color-success-subtle` | Low-emphasis companion tone for success. |
1924
+ | `--color-success-strong` | High-emphasis companion tone for success. |
1925
+ | `--color-warning` | Warning state color. |
1926
+ | `--color-warning-contrast` | The near-black tone, not the page tone. Meets 4.5:1 as normal text on `--color-warning` (6.19:1). |
1927
+ | `--color-warning-hover` | Warning hover tone. |
1928
+ | `--color-warning-subtle` | Low-emphasis companion tone for warning. |
1929
+ | `--color-warning-strong` | High-emphasis companion tone for warning. |
1930
+ | `--color-destructive` | Destructive/error state color. |
1931
+ | `--color-destructive-contrast` | Text/icon color on destructive filled surface. Meets 4.5:1 as normal text on `--color-destructive` (5.78:1). |
1932
+ | `--color-destructive-hover` | Destructive hover tone. |
1933
+ | `--color-destructive-subtle` | Low-emphasis companion tone for destructive. |
1934
+ | `--color-destructive-strong` | High-emphasis companion tone for destructive. |
1935
+ | `--color-info` | Informational state color. |
1936
+ | `--color-info-contrast` | Text/icon color on info filled surface. Meets 4.5:1 as normal text on `--color-info` (6.19:1). |
1937
+ | `--color-info-hover` | Info hover tone. |
1938
+ | `--color-info-subtle` | Low-emphasis companion tone for info. |
1939
+ | `--color-info-strong` | High-emphasis companion tone for info. |
1940
+
1941
+ > **Intent token contract**: color intent tokens own meaning and tone
1942
+ > variants. Intent classes map one family onto the shared intent slots below,
1943
+ > and presentation classes decide how much of it a component shows. Theme
1944
+ > changes belong in token values, not broad component-level theme checks.
1945
+ > Component-level theme handling should exist only when a component has an
1946
+ > internal structure that cannot be expressed through the token palette
1947
+ > alone.
1948
+
1949
+ ## Intent tokens
1950
+
1951
+ Intent classes set these seven slots; components read them. Because every
1952
+ supporting component reads the same seven names, an intent works on every
1953
+ component that supports intent, and a custom intent needs no component
1954
+ changes.
1955
+
1956
+ | Token | Purpose |
1957
+ | ------------------- | ------------------------------------------------------------------------------------- |
1958
+ | `--intent-color` | The intent's base color. What a fill is made of. |
1959
+ | `--intent-contrast` | Readable color on top of a filled `--intent-color`. |
1960
+ | `--intent-hover` | The intent's hovered base color. |
1961
+ | `--intent-strong` | The intent printed on a page. Text, icon, or border wherever the fill is not full. |
1962
+ | `--intent-subtle` | Low-emphasis tone. Tinted surfaces and tracks. Neutral resolves to `--color-surface`. |
1963
+ | `--intent-border` | Line color. Resolves to `--color-border` when no intent is set. |
1964
+ | `--intent-fill-max` | How far a fill of this intent is allowed to go. `100%` for every intent but neutral. |
1965
+
1966
+ `--intent-color` and `--intent-strong` are two ends of the same family and not
1967
+ interchangeable. The base is a ground, chosen to be filled with; the strong
1968
+ tone is chosen to be read against a page. A `.soft` or `.ghost` component
1969
+ prints the strong one, which is why an unfilled warning reads in amber-800
1970
+ rather than the amber-600 its solid sibling is filled with.
1971
+
1972
+ The strong tone has to stay recognisably its own hue, not just readable. On a
1973
+ dark page the `-100` shade of every family is a pale tint that carries almost
1974
+ no chroma at small sizes: a soft success badge and a soft destructive one
1975
+ printed labels that both read white, and a soft radio's dot stopped matching
1976
+ the ring around it. The dark tones are `-300` for that reason, and the tinted
1977
+ grounds under them are `-900` so there is room — measured at 5.0:1 or better
1978
+ everywhere a partial fill prints text, and past 9:1 on the page.
1979
+
1980
+ `--intent-border` exists because a neutral border must stay the quiet border
1981
+ gray rather than the text color. Everything that draws a line reads it:
1982
+ control borders, table rules, dividers, key caps, and quote bars.
1983
+
1984
+ `--intent-fill-max` exists for the neutral intent, whose color is the page's
1985
+ own ink. Every other intent is a hue, legible pale at 12% and saturated at
1986
+ 100%; a grey filled to 100% is a black or white slab, which is the loudest
1987
+ thing on the page and the wrong look for the intent that means _nothing in
1988
+ particular_. Neutral stops at `20%`, so `.solid` gives it a quiet plate that
1989
+ still reads as a step past `.soft`. Hover adds its step on top of the cap, so
1990
+ a capped fill still deepens under the pointer.
1991
+
1992
+ The foreground is gated by the plate rather than tied to the fill.
1993
+ `--intent-contrast` is the ink for a _full_ fill, so printing it at full
1994
+ strength on a fifth of one puts white on light grey. Contrast ink therefore
1995
+ appears only once the fill is past halfway and reaches full at a full fill;
1996
+ below that a component prints `--intent-strong`, the tone chosen to be read.
1997
+
1998
+ That means a capped neutral takes the strong tone whole rather than a
1999
+ fraction of the way toward the page. Tying it to the fill instead cost a
2000
+ neutral `.solid` button four points of contrast and a neutral tooltip nearly
2001
+ ten. A state that lifts the cap on purpose — a checked checkbox is filled
2002
+ with its own intent by definition — reaches a full fill and gets the contrast
2003
+ whole.
2004
+
2005
+ Set them directly to define an intent this package does not ship. **State
2006
+ every slot**, including `--intent-fill-max`: components reset all seven to
2007
+ the neutral values, so a slot your intent leaves out keeps neutral's — and an
2008
+ intent that omits the cap fills to 20% of its color instead of 100%.
2009
+
2010
+ ```css
2011
+ .brand {
2012
+ --intent-color: var(--color-violet-600);
2013
+ --intent-contrast: var(--color-neutral-50);
2014
+ --intent-hover: var(--color-violet-700);
2015
+ --intent-strong: var(--color-violet-800);
2016
+ --intent-subtle: var(--color-violet-100);
2017
+ --intent-border: var(--color-violet-600);
2018
+ --intent-fill-max: 100%;
2019
+ }
2020
+ ```
2021
+
2022
+ ```html
2023
+ <button class="btn brand">Custom intent</button>
2024
+ <span class="badge brand soft">Works everywhere</span>
2025
+ ```
2026
+
2027
+ > **Intent does not cascade.** Components reset these slots to the text
2028
+ > palette at their own root, so a `.success` container never recolors a
2029
+ > nested destructive button. Put the intent class on the element that shows
2030
+ > the intent.
2031
+
2032
+ ## Foundation tokens
2033
+
2034
+ Foundation tokens are not aliases for one color. They define layout, shape,
2035
+ motion, focus, depth, and layering behavior used by helper classes.
2036
+
2037
+ The four `--elevation-*` values below are raw shadows for elements you style
2038
+ yourself. They are a separate thing from `.elevation-none`/`.elevation-sm`/
2039
+ `.elevation-md` (aliased as `.flat`/`.raised`/`.floating`), which scale a
2040
+ _component's own_ shadow geometry and default to no shadow at all until an
2041
+ aesthetic or one of those classes asks for depth — see
2042
+ [Composing → Elevation](docs/usage/composing.md#elevation) for that system. The two
2043
+ share `none`/`sm`/`md`/`lg` naming for the same weight of depth, but one is a
2044
+ fixed value and the other multiplies shadow parts, so they will not always
2045
+ paint identically.
2046
+
2047
+ | Token | Purpose |
2048
+ | -------------------------- | ----------------------------------------------------------------------- |
2049
+ | `--font-default` | Default app font family. |
2050
+ | `--container-narrow` | Narrow content width. |
2051
+ | `--container-max` | Default content width. |
2052
+ | `--container-wide` | Wide content width. |
2053
+ | `--layout-gutter` | Responsive inline page padding. |
2054
+ | `--layout-section-block` | Responsive section vertical padding. |
2055
+ | `--layout-gap` | Shared gap for `.view`, `.stack`, `.cluster`, and `.auto-grid`. |
2056
+ | `--layout-grid-min` | Minimum column width for `.auto-grid`. |
2057
+ | `--radius-small` | Small radius for compact utility surfaces such as code elements. |
2058
+ | `--radius-control` | Radius for buttons, inputs, tooltips, and compact UI. |
2059
+ | `--radius-surface` | Radius for cards, panels, and alerts. |
2060
+ | `--control-height` | Default minimum height for controls. |
2061
+ | `--border-width` | Default border width. |
2062
+ | `--elevation-color` | Shadow color the four elevations compose from. Heavier in dark themes. |
2063
+ | `--elevation-none` | No shadow. Use on an element that should read as flat against its page. |
2064
+ | `--elevation-sm` | A light shadow for your own surface-level elements, such as cards. |
2065
+ | `--elevation-md` | A shadow for your own raised elements, such as dropdowns and popovers. |
2066
+ | `--elevation-lg` | A shadow for your own full overlays, such as modal dialogs. |
2067
+ | `--focus-ring` | Focus-visible ring color. |
2068
+ | `--focus-ring-offset` | Focus-visible outline offset. Negative, so the ring lands on the edge. |
2069
+ | `--focus-ring-width` | Focus-visible outline/ring width. |
2070
+ | `--motion-duration-fast` | Fast transition for buttons and input interactions (`120ms`). |
2071
+ | `--motion-duration-normal` | Normal transition for layout and modals (`200ms`). |
2072
+ | `--motion-duration-slow` | Slow animation for skeleton loaders and progress bars (`400ms`). |
2073
+ | `--motion-ease` | Default easing curve. |
2074
+ | `--z-popover` | Popover/tooltip z-index. |
2075
+ | `--breakpoint-xs` | Extra-small Tailwind responsive breakpoint. |
2076
+ | `--breakpoint-2xl` | Extended large Tailwind responsive breakpoint. |
2077
+
2078
+ Next: [Customizing](docs/usage/customizing.md) covers presentation and material
2079
+ tokens — how much of an intent shows, and what a component is made of.
2080
+
2081
+ <!-- Source: docs/usage/tooltips.md -->
2082
+
2083
+ # Tooltips
2084
+
2085
+ | Class or Attribute | Purpose |
2086
+ | --------------------------------------- | -------------------------------------------------------------- |
2087
+ | `.tooltip` | Tooltip host. Uses `data-tooltip` as pseudo-element content. |
2088
+ | `.tooltip.tooltip-icon` | Circular icon-style tooltip host. |
2089
+ | `[data-tooltip-position="top"]` | Positions tooltip above host. |
2090
+ | `[data-tooltip-position="bottom"]` | Positions tooltip below host. |
2091
+ | `[data-tooltip-position="left"]` | Positions tooltip left of host. |
2092
+ | `[data-tooltip-position="right"]` | Positions tooltip right of host. |
2093
+ | No `data-tooltip-position` | Defaults to top placement. |
2094
+ | `[data-state="open"]` | Shows tooltip without hover. |
2095
+ | `.tooltip.raised` / `.tooltip.floating` | Adds one or two units of depth. The bubble is flat by default. |
2096
+
2097
+ A tooltip bubble's fill, ground color, and floor behavior are covered in
2098
+ [Composing → Presentation](docs/usage/composing.md#presentation), and its material
2099
+ follows whichever [aesthetic](docs/usage/aesthetics.md) is in scope, including the
2100
+ pseudo-element itself.
2101
+
2102
+ The bubble rests flat. It draws no shadow of its own on a plain page, and
2103
+ zeroes an aesthetic's part-based depth (such as `.neobrutalism`'s slab) the
2104
+ same way `.flat` does elsewhere. Add [`.raised` or
2105
+ `.floating`](docs/usage/composing.md#elevation) to lift it — `.tooltip.floating` is the
2106
+ pre-0.1.0 look. An aesthetic whose shadow is a complete value, like `.glass`,
2107
+ still reaches the bubble as it reaches any surface.
2108
+
2109
+ ```html
2110
+ <button
2111
+ class="tooltip tooltip-icon"
2112
+ data-tooltip="More details"
2113
+ data-tooltip-position="right"
2114
+ aria-label="More details"
2115
+ >
2116
+ ?
2117
+ </button>
2118
+ ```
2119
+
2120
+ CSS pseudo-element tooltips are presentational. They are not a complete
2121
+ accessible tooltip implementation by themselves — provide accessible
2122
+ names/descriptions outside the CSS; see [Accessibility](docs/accessibility.md).