<!-- Source: README.md -->

# @codenhub/styles

Build a consistent, themeable interface by writing class names on plain HTML: one stylesheet with tokens, a reset, layout and type helpers, and classes for buttons, form fields, alerts, cards, tables, and tooltips. There is no JavaScript to load and no component library to wrap.

## Installation

```sh
pnpm add @codenhub/styles
```

## Usage

Import the compiled stylesheet, which holds everything the package ships:

```css
@import "@codenhub/styles";
```

```html
<form class="card stack">
  <h2 class="text-title-sm">Profile</h2>
  <div class="field">
    <label class="label" for="display-name">Display name</label>
    <input class="ipt" id="display-name" name="display-name" />
  </div>
  <button class="btn primary" type="submit">Save changes</button>
</form>
```

To use the Tailwind CSS v4 source entrypoints instead, or to load only part of the package, see [Setup](./docs/setup.md). The [demo](https://demo.codenhub.dev/styles/) shows every component in every intent, presentation, and aesthetic.

## Documentation

The docs build one account settings screen from start to finish, in this order:

- [Introduction](./docs/index.md): What the package is for, what it costs, and the reading order.
- [Setup](./docs/setup.md): Install the package, choose compiled CSS or Tailwind source, and render the page shell.
- [Integrating](./docs/integrating/index.md): Load the stylesheet in Next.js, Vue, Svelte, Astro, Tailwind CSS v4, or a page with no bundler.
- [Building a screen](./docs/concepts.md): Lay out the page and learn intent, fill, edge, and aesthetic.
- [Usage](./docs/usage/index.md): Guides for buttons, forms, feedback, tooltips, theming, aesthetics, and solo classes, then the lookup pages for every class, token, and default.
- [Accessibility](./docs/accessibility.md): What the CSS does for your users and what your application still owes them.
- [Changelog](./docs/changelog/index.md): Release history, including breaking changes.

## Requirements

| Requirement   | Details                                                                                                                                                  |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| CSS imports   | Consumer tooling must resolve package CSS imports.                                                                                                       |
| Browsers      | Chrome 123, Safari 17.5, or Firefox 128 and newer (`light-dark()` theming; `:has()` for `.input-group` state; `@property` for non-inheriting elevation). |
| Tailwind CSS  | 4.3.2 or newer, below 5, is required only for `/tw` source entrypoints.                                                                                  |
| Accessibility | Consumers provide semantic HTML, ARIA, keyboard behavior, and focus management.                                                                          |

## Notes

This package has no JavaScript behavior. It ships no compatibility aliases: a removed class or token is documented in the [changelog](./docs/changelog/index.md), not kept under a second name. Below 1.0 a minor release may break and a patch release never does; see [Versioning](./docs/index.md#versioning).

## License

Licensed under Apache-2.0. The embedded checkbox marks are derived from the [Lucide](https://lucide.dev) `check` and `minus` icons (ISC), which Lucide derives from [Feather](https://github.com/feathericons/feather) (MIT). See [NOTICE](NOTICE) for the required notices.

<!-- Source: docs/index.md -->

# @codenhub/styles

`@codenhub/styles` lets you build a consistent, themeable interface by writing class names on plain HTML. You import one stylesheet and get tokens, a reset, layout and type helpers, and classes for buttons, form fields, alerts, cards, tables, and tooltips. There is no JavaScript to load and no component library to wrap: a `<button class="btn primary">` is the whole integration.

These docs build one small screen from start to finish, an account settings page with a profile form, a plan summary, and a danger zone. Each page adds something to it, so by the end you have seen every part of the package working together instead of in isolation.

```html
<main class="section">
  <div class="section-content narrow stack">
    <h1 class="text-title">Account settings</h1>
    <form class="card stack">
      <h2 class="text-title-sm">Profile</h2>
      <div class="field">
        <label class="label" for="display-name">Display name</label>
        <input class="ipt" id="display-name" name="display-name" value="Ada Lovelace" />
      </div>
      <div class="cluster">
        <button class="btn primary" type="submit">Save changes</button>
        <button class="btn ghost" type="reset">Cancel</button>
      </div>
    </form>
  </div>
</main>
```

## Three choices per component

Every component answers three independent questions with classes, so a new look never needs a new component variant:

- **Intent: which color?** `.primary`, `.secondary`, `.success`, `.warning`, `.destructive`, or `.info`. Leave it off for neutral.
- **Presentation: how much of that color?** `.solid`, `.soft`, or `.ghost` sets the fill; `.edged` or `.edgeless` sets the border.
- **Aesthetic: what is it made of?** An optional class such as `.neobrutalism` or `.glass` changes corners, border width, shadows, and type for everything inside it.

`<button class="btn destructive ghost edged">Delete account</button>` is a red outlined button, and wrapping the page in `.pixel` turns it into a pixel-art one without touching the button. [Building a screen](docs/concepts.md) introduces each axis as the settings page needs it.

## Compiled CSS or Tailwind source

The package ships the same classes two ways:

- **Compiled CSS**, ready to use. Import it from any bundler, or link the file from a CDN. You do not need Tailwind.
- **Tailwind CSS v4 source**, under `@codenhub/styles/tw`. Use it when your project already runs Tailwind v4, so your build compiles the package with your own utilities and theme.

[Setup](docs/setup.md) helps you choose.

## What it does not do

- **No JavaScript.** There is no runtime, no framework bindings, and no generated code. Anything that needs a script, such as opening a dialog, toggling a theme, or dismissing a tooltip, is your code.
- **No semantics or behavior.** The classes paint elements; they do not add HTML semantics, ARIA, keyboard handling, focus management, validation, or announcements. [Accessibility](docs/accessibility.md) draws the exact line.
- **No theme switching.** Choosing light or dark is a class or attribute you set. The separate `@codenhub/theme` package can remember and apply the choice for you.
- **No fonts or icons.** No font file is downloaded. Aesthetics that want a special font fall back to a system font until you supply one. Icons, including loading spinners, come from your own icon set, such as `@codenhub/icons`.
- **No compatibility aliases.** A removed class or token is listed in the changelog, not kept under its old name.

## Trade-offs

Know these costs before you adopt it:

- **Class names are short and unprefixed.** `.primary`, `.success`, `.dark`, `.label`, `.field`, `.section`, `.stack`, `.flat`, and `.ghost` are global names. Another stylesheet on the same page that uses them collides with this one. The package sits in [cascade layers](docs/setup.md#cascade-layers), so your own unlayered CSS wins a collision, but a third-party library's rules for the same name still apply alongside the package's.
- **The complete stylesheets are large before compression.** At the time of writing, minified, the complete stylesheet is about 410 KB, `/native` about 575 KB, and `/components` about 320 KB. Served with gzip they are about 19 KB, 25 KB, and 15 KB. Focused files are much smaller: `/theme` is about 10 KB (2 KB with gzip) and one aesthetic is 4 to 21 KB (1 to 2 KB with gzip). Most of the size is composition that every component repeats, which is why compression removes so much of it. Import the narrowest entrypoint that covers a page; see [Setup → All entrypoints](docs/setup.md#all-entrypoints).
- **`--color-foreground` is a background, not a text color.** It is the tone one step off the page that a quiet card or panel sits on. The text color is `--color-text`.
- **Four hover colors are fixed values.** `--color-success-hover`, `--color-warning-hover`, `--color-destructive-hover`, and `--color-info-hover` are precomputed, because mixing them live crashed WebKit. If you override `--color-success` alone, its hover keeps the old hue, so replace the whole family; see [Theming → Color tokens](docs/usage/theming.md#color-tokens).
- **Modern browsers only.** Chrome 123, Safari 17.5, or Firefox 128 and newer. There is no fallback layer for older browsers.

## What counts as supported

A combination of classes is supported when these docs describe it and the public demo at [demo.codenhub.dev/styles](https://demo.codenhub.dev/styles/) shows it. The demo renders the package's test fixtures, including the full intent, presentation, and aesthetic matrices, against the built CSS. A combination that is neither documented nor shown may still produce CSS, but it is not maintained and can change in any release.

The demo is rebuilt from the main branch, not from each release. It can show work that has not been released yet, so when the demo and these docs disagree about a released version, the docs and the [changelog](docs/changelog/index.md) for that version win.

## Versioning

The package is below 1.0 and follows the [semantic versioning rule for 0.x releases](https://semver.org/#spec-item-4): the public surface can still change between minor versions.

- A minor release, such as 0.7.0 to 0.8.0, may break. It can rename or remove a class or token, or change a default. Every break is listed under **Changed** or **Removed** on that version's [changelog](docs/changelog/index.md) page, with what to use instead.
- A patch release, such as 0.7.0 to 0.7.1, never breaks.

npm's default range for a new dependency, `^0.7.0`, already accepts patches only, because below 1.0 a caret range does not cross into the next minor version. Read the changelog before raising it. When you link a file from a CDN, put the exact version in the URL; see [Without a bundler](docs/integrating/without-a-bundler.md).

Breaks are not softened with aliases because the model is still settling, and keeping each old name beside its replacement would grow the surface every release. A rename or removal usually costs one find-and-replace when you upgrade; a changed default may need your own CSS retuned.

## Reading path

Read these in order the first time. Each page adds to the settings screen and ends by pointing to the next one.

1. [Setup](docs/setup.md): install the package, choose compiled CSS or Tailwind source, and render the empty page shell.
2. [Integrating](docs/integrating/index.md): only if you use Next.js, Vue, Svelte, Astro, Tailwind v4, or no bundler at all. Each guide returns you to the main path.
3. [Building a screen](docs/concepts.md): lay out the page, set its type, and add a card and a Save button, learning intent, fill, and edge on the way.
4. [Usage](docs/usage/index.md): the rest of the screen, in order:
   - [Buttons](docs/usage/buttons.md): secondary and destructive actions, sizes, icon buttons, and disabled states.
   - [Forms](docs/usage/forms.md): labeled fields, hints, errors, icons, selects, checkboxes, radios, and switches.
   - [Feedback](docs/usage/feedback.md): a saving state, a success alert, a plan badge, a loading placeholder, and a usage bar.
   - [Tooltips](docs/usage/tooltips.md): field help that works with a mouse, a keyboard, and a screen reader.
   - [Theming](docs/usage/theming.md): light and dark mode, brand colors, and your own intents.
   - [Aesthetics](docs/usage/aesthetics.md): restyle the whole screen with one class.
   - [Solo classes](docs/usage/solo.md): paint an aesthetic onto a component this package does not style.
   - [Composing](docs/usage/composing.md), [Customizing](docs/usage/customizing.md), and [Content and layout](docs/usage/content-and-layout.md): lookup pages with every axis rule, token, default, and helper class.
5. [Accessibility](docs/accessibility.md): what the CSS handles for you and what your application must still do.

The [changelog](docs/changelog/index.md) lists every release and what changed in it.

<!-- Source: docs/setup.md -->

# Setup

This page gets the stylesheet into your project and puts the empty shell of the settings screen on the page. It takes one install, one import, and one choice: compiled CSS or Tailwind source. The rest of the page is reference you can come back to: every entrypoint, the classless option, the cascade layers, and what goes wrong when an import is missing.

## Install

```sh
pnpm add @codenhub/styles
```

`npm install @codenhub/styles` and `yarn add @codenhub/styles` work the same way.

## Import the stylesheet

Import the complete compiled stylesheet once, in the CSS file your app already loads globally:

```css
@import "@codenhub/styles";
```

Then write the shell of the settings screen. It is a page section with a centered, narrow column:

```html
<main class="section">
  <div class="section-content narrow stack">
    <h1 class="text-title">Account settings</h1>
    <p>Manage your profile, plan, and security.</p>
  </div>
</main>
```

If the heading renders in a bold, tight title face with the paragraph spaced below it, and the column sits in the middle of a wide window, the stylesheet is working. If it looks like unstyled browser defaults, see [When nothing is styled](#when-nothing-is-styled).

The complete stylesheet includes the reset, the tokens, the layout and type helpers, and every component class. It applies global rules to your page: the reset, the root theme, and focus rings. It does not include the aesthetics or the classless element mappings; both are separate imports, described below.

## Choose your route

Pick one:

- **Compiled CSS** (`@codenhub/styles`, shown above). Use it unless you have a reason not to. Any bundler that resolves CSS imports from packages works, such as Vite, webpack, or the Tailwind CLI.
- **Tailwind CSS v4 source** (`@codenhub/styles/tw`). Use it when your project already runs Tailwind v4. Your build compiles the package with your own utilities, so you get one stylesheet and Tailwind's `dark:` variant matches the package's theme. It needs Tailwind 4.3.2 or newer, below 5.
- **No bundler.** Link the compiled file from `node_modules` or a CDN with a `<link>` tag.

Your framework may need its own wiring, such as where the global stylesheet goes in Next.js or Astro. Each of these guides covers one setup and then sends you back here:

- [Tailwind CSS v4](docs/integrating/tailwind.md)
- [Next.js](docs/integrating/nextjs.md)
- [Vue](docs/integrating/vue.md)
- [Svelte](docs/integrating/svelte.md)
- [Astro](docs/integrating/astro.md)
- [Without a bundler](docs/integrating/without-a-bundler.md)

## Add an aesthetic (optional)

Aesthetics are separate stylesheets because most pages use one or none. Import all six at once, or only the one you use:

```css
@import "@codenhub/styles";
@import "@codenhub/styles/aesthetics/neobrutalism";
```

The order of the two imports does not matter, because every entrypoint declares the [cascade layers](#cascade-layers) before using one. [Aesthetics](docs/usage/aesthetics.md) shows what each one does; you can skip them until then.

## Requirements

- **Browsers:** Chrome 123, Safari 17.5, or Firefox 128 and newer. Colors use `light-dark()` and `color-scheme` (Firefox 120). `.input-group` reads a nested control's invalid and disabled state with `:has()` (Firefox 121). The elevation classes register a non-inheriting property with `@property` (Firefox 128); without it, an elevation class would also reach the unclassed elements inside it.
- **Tooling:** an `@import` of a package name needs a tool that resolves CSS imports from packages. Without one, link the compiled file by its path or from a CDN; see [Without a bundler](docs/integrating/without-a-bundler.md).
- **Tailwind:** 4.3.2 or newer, below 5, only for the `/tw` source entrypoints. It is an optional peer dependency, so the compiled entrypoints do not install it.
- **Your application:** the package has no JavaScript. Semantic HTML, ARIA, keyboard behavior, focus management, validation, and announcements are yours; see [Accessibility](docs/accessibility.md).

## When nothing is styled

- **The import is missing or not loaded globally.** The classes do nothing until the stylesheet is on the page. Import it once in a stylesheet every page loads, not in a component that may not render.
- **The package name does not resolve.** A plain `<link>` or a tool without CSS package resolution cannot read `@import "@codenhub/styles"`. Link `node_modules/@codenhub/styles/dist/index.css`, or follow [Without a bundler](docs/integrating/without-a-bundler.md).
- **A `/tw` path is loaded without Tailwind.** The `/tw` files are Tailwind source: they contain `@theme`, `@utility`, and `@apply`, which browsers do not understand. Your Tailwind build has to compile them; see [Tailwind CSS v4](docs/integrating/tailwind.md).
- **A class built at runtime is missing (Tailwind source only).** Tailwind generates a class only when it finds the name in your source files. A name assembled from strings, such as `` `btn ${tone}` ``, is invisible to that scan. Write full class names, or list them as described in [Tailwind CSS v4](docs/integrating/tailwind.md).

### Inlining critical CSS

You can paste a **compiled** stylesheet, from `dist/` or from your own build output, into an HTML `<style>` element, for example to inline critical CSS in a server-rendered head. None of the compiled files contains the `</style` sequence that would end the element early.

That does not apply to the `/tw` files. They are Tailwind source, not finished CSS, so pasting one into `<style>` gives the browser directives it ignores and the classes are never generated. Compile them with Tailwind first, then inline the output.

## All entrypoints

Compiled entrypoints are finished CSS. `/tw` entrypoints publish the uncompiled source from `dist/tw`, which a Tailwind v4 build must process. A focused `/tw` component entrypoint includes the theme tokens, so its classes work on their own.

Each import path resolves the same file under the `style`, `import`, and `default` export conditions. A CSS import is a side effect, so make sure your bundler does not drop it as unused.

| Import path                      | What it contains and what it changes                                                                                                                                                                                                                                                                                 |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `@codenhub/styles`               | Compiled complete stylesheet: Tailwind's base and theme output, tokens, typography, layout and content helpers, components, and the reset. Applies the global reset, root theme, and focus rules.                                                                                                                    |
| `@codenhub/styles/theme`         | Compiled tokens and the intent, presentation, elevation, and corner-pattern classes. Sets root theme properties and the light and dark selectors. No reset and no components.                                                                                                                                        |
| `@codenhub/styles/palette`       | Generated `--palette-*` custom properties: every intent and presentation combination's fill, text, and border color as a flat value, plus flat `border`, `surface`, and `text` colors. For code that cannot use the package itself; see [Customizing → Generated palette](docs/usage/customizing.md#generated-palette). |
| `@codenhub/styles/components`    | Compiled tokens, typography helpers, and every component class. Each component carries its own reduced-motion rule, and the closed-`<dialog>` fix comes with `.card`. No reset, no layout or content helpers, no classless mappings.                                                                                 |
| `@codenhub/styles/native`        | The complete compiled stylesheet plus [classless mappings](#classless-html) for plain HTML elements. Applies the reset and broad element selectors.                                                                                                                                                                  |
| `@codenhub/styles/aesthetics`    | All six compiled aesthetics, `.neobrutalism`, `.glass`, `.pixel`, `.chunky-tile`, `.cyber`, and `.sketch`, each with its `-solo` class. No tokens or components of its own; import it in any order.                                                                                                                  |
| `@codenhub/styles/aesthetics/*`  | One compiled aesthetic and its solo class: `/neobrutalism`, `/glass`, `/pixel`, `/chunky-tile`, `/cyber`, or `/sketch`.                                                                                                                                                                                              |
| `@codenhub/styles/tw`            | Source version of the complete stylesheet. Imports Tailwind itself, then the theme, typography, helpers, components, and reset. Same global rules.                                                                                                                                                                   |
| `@codenhub/styles/tw/theme`      | Source theme, tokens, and axis classes. Imports Tailwind's theme layer, emits the theme selectors, and defines the package's `dark:` variant.                                                                                                                                                                        |
| `@codenhub/styles/tw/components` | Source theme, typography, buttons, feedback, forms, surfaces, and tooltips. No reset, no layout or content helpers, no classless mappings.                                                                                                                                                                           |
| `@codenhub/styles/tw/surface`    | Source theme plus `.surface`, `.card`, and `.panel`.                                                                                                                                                                                                                                                                 |
| `@codenhub/styles/tw/button`     | Source theme plus `.btn`.                                                                                                                                                                                                                                                                                            |
| `@codenhub/styles/tw/form`       | Source theme plus `.field`, `.text-control`, `.ipt`, `.textarea`, `.select`, `.input-group`, `.checkbox`, `.radio`, and `.switch`.                                                                                                                                                                                   |
| `@codenhub/styles/tw/feedback`   | Source theme plus `.alert`, `.badge`, `.skeleton`, and `.progress`, with their animations.                                                                                                                                                                                                                           |
| `@codenhub/styles/tw/tooltip`    | Source theme plus the tooltip classes.                                                                                                                                                                                                                                                                               |
| `@codenhub/styles/tw/reset`      | Source theme, typography helpers, and the global reset. Changes the root, body, and elements, and sets selection, motion, focus, scrollbar, and forced-colors rules.                                                                                                                                                 |
| `@codenhub/styles/tw/native`     | Source version of `/native`: everything plus the reset and the classless mappings. The broadest global effect.                                                                                                                                                                                                       |
| `@codenhub/styles/tw/typography` | Source theme plus the typography helpers. Sets the theme selectors; no reset, no classless mappings.                                                                                                                                                                                                                 |
| `@codenhub/styles/tw/utilities`  | Source theme and typography plus the layout and content helpers. Lists the package's class names so Tailwind always generates them.                                                                                                                                                                                  |
| `@codenhub/styles/tw/aesthetics` | All six aesthetics as source. `/tw/aesthetics/neobrutalism`, `/glass`, `/pixel`, `/chunky-tile`, `/cyber`, and `/sketch` publish one each. Plain CSS with no theme or reset side effects.                                                                                                                            |
| `@codenhub/styles/package.json`  | The package manifest, as JSON. For tools that read the installed version or export map; it is not a stylesheet.                                                                                                                                                                                                      |

Do not import two entrypoints that overlap, such as `@codenhub/styles` and `@codenhub/styles/components`, in one build unless duplicated CSS is acceptable. Pick the narrowest entrypoint that covers what a page needs.

## Classless HTML

`@codenhub/styles/native` styles plain HTML elements without any classes, for content you cannot add classes to, such as Markdown output or a CMS body. It is the complete stylesheet plus these mappings:

| Element                                                                                                                                        | Styled as                                                                          |
| ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `h1`, `h2`, `h3`, `h4`, `h5` and `h6`                                                                                                          | `.text-title-lg`, `.text-title`, `.text-title-sm`, `.text-label-lg`, `.text-label` |
| `p`, `li`                                                                                                                                      | `.text-body`                                                                       |
| `table`                                                                                                                                        | `.data-table`                                                                      |
| `kbd`, `blockquote`, `q`, `code`, `pre`                                                                                                        | `.kbd`, `.quote`, `.quote-inline`, `.code`, `.pre`                                 |
| `hr`                                                                                                                                           | `.divider`                                                                         |
| `input` with no type, or type `text`, `number`, `email`, `password`, `url`, `tel`, `search`, `month`, `week`, `time`, `date`, `datetime-local` | `.ipt`                                                                             |
| `textarea`, `select`                                                                                                                           | `.textarea`, `.select`                                                             |
| `input type="checkbox"`, `input type="radio"`                                                                                                  | `.checkbox`, `.radio`                                                              |
| `button`, `input type="button"`, `type="submit"`, `type="reset"`                                                                               | `.btn`                                                                             |

```html
<article>
  <h2>Two-factor authentication</h2>
  <p>Press <kbd>Ctrl</kbd> + <kbd>K</kbd> to open the command menu.</p>
  <label for="code">Verification code</label>
  <input id="code" type="text" inputmode="numeric" />
  <label><input type="checkbox" /> Remember this device</label>
  <button type="submit">Verify</button>
</article>
```

What it does not do:

- **Some input types are left alone:** `file`, `range`, `color`, and `hidden` keep the browser's own look.
- **No switch.** A checkbox with `role="switch"` is still painted as a checkbox; the mapping does not read roles. A page that needs switches is better served by the class-based stylesheet.
- **Links keep the text around them.** A link inside a heading stays heading-sized; a link is never styled as a button.
- **`pre code` is not double-styled.** Code inside a `pre` drops its own background and padding.

The mappings are global: every matching element on the page is styled, including elements inside third-party widgets. They sit in the `base` [cascade layer](#cascade-layers), so any class beats them. Add an intent or presentation class to a mapped element, such as `<button class="destructive">`, and it applies as it would on `.btn`.

## Cascade layers

Every entrypoint puts its CSS into Tailwind's four cascade layers and declares their order before using one, so the result does not depend on which file loads first:

| Layer        | Holds                                                                                               |
| ------------ | --------------------------------------------------------------------------------------------------- |
| `theme`      | Tailwind's theme, the foundation tokens, the theme selectors, and `/palette`.                       |
| `base`       | The reset and the classless element styles.                                                         |
| `components` | The intent, presentation, elevation, and aesthetic classes.                                         |
| `utilities`  | Every component (`.btn`, `.card`, `.ipt`, and so on), next to your own Tailwind utilities.          |
| _unlayered_  | The forced-colors and reduced-motion overrides, the closed-`<dialog>` fix, and the `-solo` classes. |

What that means when you write your own CSS:

- **Your utility or unlayered CSS beats an intent, presentation, elevation, or aesthetic class** on the same element. `[--ui-fill:50%]` on a `.solid` button, `font-serif` on a `.pixel` region, or `.my-card { --ui-radius: 0 }` in your own stylesheet all apply.
- **A component and your utility share the `utilities` layer.** Specificity decides between them, then source order, as between any two utilities. Your unlayered CSS always beats a component.
- **State beats your intent.** A field with `aria-invalid="true"` stays destructive even with an intent utility on it.
- **The `-solo` classes are unlayered on purpose**, so they beat a third-party component's layered or zero-specificity styles. A more specific unlayered rule, or an equally specific one that loads later, still beats them; see [Solo classes](docs/usage/solo.md).

## Next

With the shell on the page, continue to [Building a screen](docs/concepts.md) to add a card, a form heading, and a Save button. If you use one of the frameworks above and have not wired it yet, do that first.

<!-- Source: docs/integrating/index.md -->

# Integrating

This section is a detour from the main reading path, for projects whose framework decides where global CSS may go. The package has no JavaScript and no framework bindings (see [Introduction](docs/index.md#what-it-does-not-do)), so integrating it is only about two things: where your framework lets you load a global stylesheet, and whether the project runs Tailwind CSS v4. Follow the one guide that matches your stack, then return to [Building a screen](docs/concepts.md).

## The shared model

Every stack does the same two imports, from the same kind of place — a root layout, an app entry file, or a global CSS file, never a page or a component further down the tree:

1. One [entrypoint](docs/setup.md#all-entrypoints) — the compiled `@codenhub/styles` for any build, or `@codenhub/styles/tw` when the project already runs Tailwind CSS v4.
2. `@codenhub/styles/aesthetics` (or `/tw/aesthetics`) after it, only if the project uses an [aesthetic](docs/usage/aesthetics.md).

Importing from one place matters for two reasons. Some frameworks only _allow_ global CSS from a designated root file and fail the build otherwise — Next.js's Pages Router is one, see [Next.js → Troubleshooting](docs/integrating/nextjs.md#troubleshooting). Where a framework allows it more broadly, as Next.js's App Router does, importing from the root remains the recommended choice for predictable CSS ordering — and importing per-component instead would load the same reset and token rules once per component rather than once per app.

## Which guide to follow

| Stack                                                        | Guide                                       |
| ------------------------------------------------------------ | ------------------------------------------- |
| A Tailwind CSS v4 project on any bundler                     | [Tailwind CSS v4](docs/integrating/tailwind.md)            |
| Next.js / React                                              | [Next.js](docs/integrating/nextjs.md)                      |
| Vue                                                          | [Vue](docs/integrating/vue.md)                             |
| Svelte / SvelteKit                                           | [Svelte](docs/integrating/svelte.md)                       |
| Astro                                                        | [Astro](docs/integrating/astro.md)                         |
| Plain HTML, Rails, Django, or any app with no CSS build step | [Without a bundler](docs/integrating/without-a-bundler.md) |

A project with no framework — a plain Vite, webpack, or Parcel build — needs nothing beyond [Setup → Import the stylesheet](docs/setup.md#import-the-stylesheet): import the compiled stylesheet from the project's existing global CSS entry.

## Verifying it worked

After the import, render one styled element and confirm it picked up the package's tokens rather than the browser default:

```html
<button class="btn primary">Save changes</button>
```

A rounded button filled with the primary color (near-black on a light page, near-white on a dark one) means the stylesheet loaded. Unstyled browser-default text most often means the import specifier didn't resolve — check the [entrypoint table](docs/setup.md#all-entrypoints) for the exact specifier to import, and confirm your build tooling resolves package CSS imports at all, see [Setup → Requirements](docs/setup.md#requirements). With no build tool, link the file by its path instead; see [Without a bundler](docs/integrating/without-a-bundler.md). [Setup → When nothing is styled](docs/setup.md#when-nothing-is-styled) lists the other common causes.

## Next

Once the button renders, go back to the main path: [Building a screen](docs/concepts.md) adds the first card and form to the settings page.

<!-- Source: docs/integrating/tailwind.md -->

# Tailwind CSS v4

A project that already runs Tailwind CSS v4 can compile this package's source instead of loading its compiled output. Your app's utilities and the package's classes then come out of one build, share one theme, and use one `dark:` variant. This page covers the import, aesthetics, the focused entrypoints, and the build problems you are likely to meet.

## Why source instead of compiled

`/tw` entrypoints publish copied, uncompiled source from `dist/tw`; your own Tailwind build compiles it alongside your app, so its `@theme`, `@utility`, and `@apply` directives resolve together with yours. The compiled entrypoints (`@codenhub/styles`) still work in a Tailwind project, but then two separate CSS pipelines run side by side instead of one — use `/tw` unless a specific reason rules it out.

## Import the source entrypoint

```css
@import "@codenhub/styles/tw";

@source "./src";
```

`/tw` imports Tailwind itself, so it replaces your own `@import "tailwindcss"` rather than joining it: with both, Tailwind's Preflight is emitted twice. Every other `/tw/*` entrypoint expects your import to come first; see [Using a focused entrypoint](#using-a-focused-entrypoint).

Tailwind CSS v4 scans your project automatically — most projects need nothing beyond the import above. `@source` paths resolve relative to the CSS file that declares them, not your project root, so `./src` here means "the `src/` next to this stylesheet"; adjust it to match where your own global stylesheet actually lives. Add `@source` only for markup Tailwind's automatic scan won't reach on its own: a path excluded by default (`.gitignore`d, or inside `node_modules` — an external component library, for instance) or, in a monorepo, one outside this file's own directory tree. Point it at your app's markup, not at this package — `/tw` already ships every class definition it publishes, so scanning it too only slows the build without adding classes.

## Add aesthetics

Add an aesthetic from the source entrypoint the same way as the compiled one, after the base import:

```css
@import "@codenhub/styles/tw";
@import "@codenhub/styles/tw/aesthetics";

@source "./src";
```

## Using a focused entrypoint

Import a focused `/tw/*` entrypoint instead of the combined one — such as `/tw/button` or `/tw/form` — when a build only needs one component family; see the full [entrypoint table](docs/setup.md#all-entrypoints) for what each one composes.

Unlike `/tw`, the focused entrypoints and `/tw/native` do not import Tailwind, so keep your own import first:

```css
@import "tailwindcss";
@import "@codenhub/styles/tw/button";
```

## Troubleshooting

**Classes resolve to no styling**

- **What happened:** components use documented classes like `.btn.primary`, but no styling appears even though `@import "@codenhub/styles/tw"` is in the stylesheet.
- **Why:** this is rarely `@source` itself — Tailwind already scans your project automatically. The more common cause is markup living somewhere that automatic scan excludes by default: a `.gitignore`d path, or a component pulled from `node_modules`. Tailwind may not see a class used only there and skips generating a rule for it.
- **Fix:** add an explicit `@source` pointing at that directory, resolved relative to this stylesheet — matching the [example above](#import-the-source-entrypoint).

**Duplicate token and reset rules**

- **What happened:** the built stylesheet is larger than expected, and browser devtools show the same custom properties or reset rules defined twice.
- **Why:** one of two things. Importing both a compiled entrypoint (`@codenhub/styles`) and a `/tw` entrypoint (`@codenhub/styles/tw`) in the same build compiles the same tokens and reset from two independent sources; Tailwind's build has no way to know they describe the same package. Or your stylesheet imports `tailwindcss` beside `/tw`, which already imports it, so Preflight is emitted twice.
- **Fix:** pick one path per project — `/tw` entrypoints for a project running Tailwind CSS v4, compiled entrypoints everywhere else — and drop your own `@import "tailwindcss"` when you import `/tw`.

**A class built at runtime has no styling**

- **What happened:** `<button class="btn primary">` is styled, but a button whose intent comes from a variable, such as `` `btn ${tone}` ``, is not.
- **Why:** Tailwind generates a class only when it finds the complete name in your source files. The package's own component and axis classes are always generated, but your own utilities are not, and a name assembled from strings is invisible to the scan.
- **Fix:** write complete class names in your source, for example in a lookup object (`{ save: "btn primary", remove: "btn destructive" }`), or list the names your code can produce with `@source inline("...")` in your stylesheet.

Every `/tw` entrypoint requires Tailwind CSS 4.3.2 or newer, below 5: the source uses `@source not` and `@source inline()`, which arrived in 4.1, and it is tested against 4.3.2.

## The `dark:` variant

The `/tw` entrypoints define their own `dark:` variant, so your utilities switch with the same light and dark selectors the package's tokens use. It has one known difference from the tokens when themes are nested; see [Theming → The dark: variant](docs/usage/theming.md#the-dark-variant).

## Next

Return to the main path: [Building a screen](docs/concepts.md) adds the first card and form to the settings page. Everything it shows works the same on `/tw`.

<!-- Source: docs/integrating/nextjs.md -->

# Next.js

Next.js has two router architectures, and they treat global CSS differently:

- **App Router** (an `app/` directory): global CSS — including an external package's stylesheet like this one — can be imported from any layout, page, or component under `app/`. Importing it from the root layout is only a recommendation, for predictable CSS ordering across routes; Next.js does not enforce it and no error occurs if you import it elsewhere.
- **Pages Router** (a `pages/` directory, no `app/`): global CSS is restricted to `pages/_app.tsx`. Importing it from anywhere else fails the build — Next.js enforces this itself, not this package; see [Troubleshooting](#troubleshooting).
- **Both directories present** (mid-migration): import the stylesheet in both root files. The two routers serve their own routes independently, so a route under `pages/` never sees a stylesheet imported only in `app/layout.tsx`, and vice versa.

## Import the stylesheet

Add the import to the root file for your router:

**App Router**

```tsx
// app/layout.tsx
import "@codenhub/styles";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}
```

**Pages Router**

```tsx
// pages/_app.tsx
import "@codenhub/styles";
import type { AppProps } from "next/app";

export default function App({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}
```

Running Tailwind CSS v4? `@import "@codenhub/styles/tw"` is a CSS directive, so it belongs in a CSS file, not a bare import in `layout.tsx`/`_app.tsx` — add it (with `@source` if needed; `/tw` imports Tailwind itself, so leave out your own `@import "tailwindcss"`) to your global CSS file, and import that file from the same root file shown above instead. See [Tailwind CSS v4](docs/integrating/tailwind.md).

## Force a theme

Apply `.dark` or `.light` on `<html>` to force a theme instead of following the system preference; see [Theming → Light and dark](docs/usage/theming.md#light-and-dark).

```tsx
<html lang="en" className="dark">
```

## Troubleshooting

**Pages Router build fails with "Global CSS cannot be imported from files other than your Custom `<App>`"**

- **What happened:** Next.js rejected a global CSS `import` statement outside `pages/_app.tsx`. This restriction is specific to the Pages Router — Next.js enforces it itself, not this package, and it applies to any global CSS import, not specifically to `@codenhub/styles`. It does not apply to the App Router, where the same import works from any layout, page, or component.
- **Why:** the Pages Router has no per-route CSS boundary the way the App Router does, so Next.js requires one predictable file for global stylesheets to avoid ordering conflicts between pages.
- **Fix:** move the `import "@codenhub/styles"` line to `pages/_app.tsx`, and remove it from wherever it currently is.

## Next

Return to the main path: [Building a screen](docs/concepts.md) adds the first card and form to the settings page. [Setup → All entrypoints](docs/setup.md#all-entrypoints) lists every other stylesheet you can import.

<!-- Source: docs/integrating/vue.md -->

# Vue

Vue places no restriction on where CSS is imported from, but importing it once, from the app's entry file, keeps the reset and tokens from loading more than once and guarantees they're in place before any component renders.

## Import the stylesheet

```ts
// main.ts
import "@codenhub/styles";
import { createApp } from "vue";
import App from "./App.vue";

createApp(App).mount("#app");
```

Running Tailwind CSS v4? Import `@codenhub/styles/tw` from a CSS entry file instead of `main.ts`, using the `@tailwindcss/vite` plugin — see [Tailwind CSS v4](docs/integrating/tailwind.md).

## Force a theme

Apply `.dark` or `.light` on the root element to force a theme instead of following the system preference; see [Theming → Light and dark](docs/usage/theming.md#light-and-dark).

## Next

Return to the main path: [Building a screen](docs/concepts.md) adds the first card and form to the settings page. [Setup → All entrypoints](docs/setup.md#all-entrypoints) lists every other stylesheet you can import.

<!-- Source: docs/integrating/svelte.md -->

# Svelte

Svelte and SvelteKit let you import CSS from any component, but the package's reset and tokens should load once, before any component renders. This page shows where that one import goes in SvelteKit and in a plain Svelte app.

## Import the stylesheet

**SvelteKit** — import from the root layout so it loads once for every route:

```svelte
<!-- src/routes/+layout.svelte -->
<script>
  import "@codenhub/styles";
  let { children } = $props();
</script>

{@render children()}
```

**Plain Svelte + Vite** — import from the entry file instead:

```ts
// main.ts
import "@codenhub/styles";
import { mount } from "svelte";
import App from "./App.svelte";

mount(App, { target: document.getElementById("app")! });
```

Running Tailwind CSS v4? Import `@codenhub/styles/tw` from the app's CSS entry instead — see [Tailwind CSS v4](docs/integrating/tailwind.md).

## Force a theme

Apply `.dark` or `.light` on `<html>` (edit `src/app.html` in SvelteKit) to force a theme instead of following the system preference; see [Theming → Light and dark](docs/usage/theming.md#light-and-dark).

## Next

Return to the main path: [Building a screen](docs/concepts.md) adds the first card and form to the settings page. [Setup → All entrypoints](docs/setup.md#all-entrypoints) lists every other stylesheet you can import.

<!-- Source: docs/integrating/astro.md -->

# Astro

Astro renders components statically by default, but a global stylesheet import still only needs to happen once — from a shared layout, not from individual pages or islands.

## Import the stylesheet

```astro
---
// src/layouts/Layout.astro
import "@codenhub/styles";
---

<html lang="en">
  <body>
    <slot />
  </body>
</html>
```

Every page that uses this layout loads the stylesheet once. Running Tailwind CSS v4? Import `@codenhub/styles/tw` through Astro's Tailwind integration instead — see [Tailwind CSS v4](docs/integrating/tailwind.md), including when an explicit `@source` is actually needed.

## Force a theme

Apply `.dark` or `.light` on `<html>` to force a theme instead of following the system preference; see [Theming → Light and dark](docs/usage/theming.md#light-and-dark).

## Next

Return to the main path: [Building a screen](docs/concepts.md) adds the first card and form to the settings page. [Setup → All entrypoints](docs/setup.md#all-entrypoints) lists every other stylesheet you can import.

<!-- Source: docs/integrating/without-a-bundler.md -->

# Without a bundler

The compiled stylesheets are finished CSS: each one is a single file with no `@import` and no reference to another file, and every image it draws is embedded as a `data:` URL: the checkbox tick and dash masks, and the SVG lens filters `.glass-liquid` uses. A page can load one with a plain `<link>` tag, from any server, with no build step, and the stylesheet makes no further requests.

What needs a build tool is the import _name_. `@import "@codenhub/styles"` names a package, and finding the file behind that name — `node_modules/@codenhub/styles/dist/index.css` — is done by Node-aware tooling such as Vite, webpack, or the Tailwind CLI, which read the package's `exports` map. A browser does not: it resolves an `@import` URL against the stylesheet that contains it and a `<link href>` against the page, so a bare package name becomes a request for a path that does not exist. Without a bundler, link the file by its path instead.

The `/tw` entrypoints are Tailwind source and need a Tailwind build. `/tw/aesthetics` is the exception, plain CSS with nothing for Tailwind to process, but without a bundler link the compiled `dist/aesthetics/` files instead, as below. This page covers the compiled entrypoints only, for a page that has no CSS build at all: plain HTML, Rails, Django, or any server-rendered app.

## Which file to link

Each compiled import path is one file in the package's `dist/` folder:

| Import path                     | File                                                              |
| ------------------------------- | ----------------------------------------------------------------- |
| `@codenhub/styles`              | `dist/index.css`                                                  |
| `@codenhub/styles/theme`        | `dist/theme.css`                                                  |
| `@codenhub/styles/components`   | `dist/components.css`                                             |
| `@codenhub/styles/native`       | `dist/native.css`                                                 |
| `@codenhub/styles/palette`      | `dist/palette.css`                                                |
| `@codenhub/styles/aesthetics`   | `dist/aesthetics/index.css`                                       |
| `@codenhub/styles/aesthetics/*` | `dist/aesthetics/<name>.css`, such as `dist/aesthetics/pixel.css` |

Link the base file first and an aesthetic after it. [Setup → All entrypoints](docs/setup.md#all-entrypoints) explains what each one contains; link one base file, not several, since they overlap.

## From a CDN

jsDelivr and unpkg serve every file of every published npm version, so a page can link one with nothing installed:

```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@codenhub/styles@0.7.0/dist/index.css" /> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@codenhub/styles@0.7.0/dist/aesthetics/pixel.css" />
```

Put the exact version in the URL. Without one, the CDN serves the latest release, and below 1.0 a minor release may rename or remove a class; see [Versioning](docs/index.md#versioning).

## From your own server

Install the package, then serve its `dist/` folder as static files:

```sh
npm install @codenhub/styles
```

How a framework serves a folder from `node_modules` differs, so each one is below. Where the framework reads the folder in place, as Rails and Django do, upgrading the package and redeploying is all an update takes.

### Plain HTML

Copy `node_modules/@codenhub/styles/dist` into the folder your server publishes, then link it:

```html
<link rel="stylesheet" href="/vendor/codenhub-styles/index.css" />
```

Repeat the copy after each upgrade, for example from an npm `postinstall` script.

### Ruby on Rails

A Rails app built with `cssbundling-rails` already runs a bundler: import the package from `app/assets/stylesheets/application.css` as in [Setup → Import the stylesheet](docs/setup.md#import-the-stylesheet).

With Propshaft and no CSS bundler, add the package's folder to the asset paths:

```ruby
# config/initializers/assets.rb
Rails.application.config.assets.paths << Rails.root.join("node_modules/@codenhub")
```

Then link it from the layout, by its path below that folder:

```erb
<%# app/views/layouts/application.html.erb %>
<%= stylesheet_link_tag "styles/dist/index" %>
<%= stylesheet_link_tag "styles/dist/aesthetics/pixel" %>
```

Point the path at `node_modules/@codenhub` rather than at `dist/` itself. Propshaft serves files by their path below each asset folder, so adding `dist/` directly would publish a bare `index.css` that collides with any `index.css` of your own.

### Django

Add the folder to `STATICFILES_DIRS` under a prefix, which keeps its file names apart from your own static files:

```python
# settings.py
STATICFILES_DIRS = [
    ("codenhub-styles", BASE_DIR / "node_modules/@codenhub/styles/dist"),
]
```

```django
{% load static %}
<link rel="stylesheet" href="{% static 'codenhub-styles/index.css' %}" />
```

### Laravel

A Laravel app runs Vite by default, so it has a bundler: import the package from `resources/css/app.css` as in [Setup → Import the stylesheet](docs/setup.md#import-the-stylesheet), and load that file with the `@vite` directive as usual.

## Verifying it worked

Render one styled element, as in [Integrating → Verifying it worked](docs/integrating/index.md#verifying-it-worked):

```html
<button class="btn primary">Save changes</button>
```

A rounded button filled with the primary color means the stylesheet loaded. If the button looks like the browser default, open the browser's network panel: a `404` on the stylesheet means the path or the static-file setup is wrong.

## Next

Return to the main path: [Building a screen](docs/concepts.md) adds the first card and form to the settings page. [Setup → All entrypoints](docs/setup.md#all-entrypoints) lists every other stylesheet you can import.

<!-- Source: docs/concepts.md -->

# Building a screen

This page turns the empty shell from [Setup](docs/setup.md) into the first real part of the settings screen: a page header, a profile card with Save and Cancel buttons, and a plan summary beside a usage card. On the way it introduces the three choices every component makes, intent, presentation, and aesthetic, at the moment the screen needs each one. Everything else in these docs builds on this model.

## The smallest screen

A page header and one card with one button:

```html
<main class="section">
  <div class="section-content narrow stack">
    <h1 class="text-title">Account settings</h1>
    <section class="card stack">
      <h2 class="text-title-sm">Profile</h2>
      <p>Your name appears on invoices and shared projects.</p>
      <button class="btn primary" type="button">Save changes</button>
    </section>
  </div>
</main>
```

Each class does one job:

- `.section` pads the page: a responsive gutter at the sides (1 to 2rem) and generous space above and below (4 to 7rem).
- `.section-content` centers a column with a maximum width; `.narrow` makes that 48rem instead of 80rem, a comfortable width for a form.
- `.stack` lays its children out vertically with a 1rem gap.
- `.text-title` and `.text-title-sm` set heading sizes: 1.875rem bold and 1.25rem semibold.
- `.card` is a bordered container with rounded corners and 1.25rem of padding.
- `.btn` makes a button, and `.primary` gives it the primary color.

Headings are not sized by their tag: the reset gives an `<h1>` with no type class the same size and weight as the text around it. That keeps the document structure, which heading level an element is, separate from the size you want it to look. If you want headings styled automatically, see [Setup → Classless HTML](docs/setup.md#classless-html).

## Lay out the page

Three layout helpers cover most screens, and all three use the same gap, `--layout-gap` (1rem):

- `.stack` for vertical lists of blocks.
- `.cluster` for a row that wraps when it runs out of room, such as a group of buttons. Add `.between` to push the first and last items to opposite ends.
- `.auto-grid` for a grid of equal cards that adds or drops columns as the space changes. A column is never narrower than `--layout-grid-min` (16rem) unless the container itself is.

`.tight` (0.5rem) and `.loose` (1.5rem) change the gap of the layout they sit on. A layout nested inside keeps its own gap.

Add a header and put the buttons in a row:

```html
<main class="section">
  <div class="section-content narrow stack loose">
    <header class="stack tight">
      <h1 class="text-title">Account settings</h1>
      <p>Manage your profile, plan, and security.</p>
    </header>

    <section class="card stack" aria-labelledby="profile-title">
      <h2 class="text-title-sm" id="profile-title">Profile</h2>
      <p>Your name appears on invoices and shared projects.</p>
      <div class="cluster">
        <button class="btn primary" type="button">Save changes</button>
        <button class="btn ghost" type="button">Cancel</button>
      </div>
    </section>
  </div>
</main>
```

[Content and layout](docs/usage/content-and-layout.md) lists every layout helper, including `.view`, `.divider`, and `.visually-hidden`, and the full type scale.

## Intent: which color

The Save button is `.primary` and the Cancel button has no intent, which makes it neutral. An intent class picks a color role:

| Class          | Use it for                            | Default color                           |
| -------------- | ------------------------------------- | --------------------------------------- |
| _none_         | Anything without a meaning of its own | The page's text and border grays        |
| `.primary`     | The main action on a screen           | Near-black on light, near-white on dark |
| `.secondary`   | A second, quieter action              | Mid gray                                |
| `.success`     | Confirmation and completed states     | Emerald green                           |
| `.warning`     | Something that needs attention soon   | Amber, with dark text                   |
| `.destructive` | Deleting, revoking, and errors        | Rose red                                |
| `.info`        | Neutral notices and help              | Indigo                                  |

Intent stays on the element that carries it. Putting `.destructive` on a `<section>` does not turn every button inside it red; you write it on each button. This is deliberate: a container that silently recolored everything inside it would turn a red danger zone's Cancel button red too.

`.neutral` names the default explicitly. You rarely need it, because no intent class already means neutral; it matters where color would otherwise come from elsewhere, such as a table row, which inherits its table's intent.

## Presentation: how much color

The Cancel button is `.ghost`: no fill, just colored text, so it reads as less important than Save. That is the presentation axis, two independent choices:

- **Fill:** `.solid` fills the component with its intent color and switches the text to the contrasting color. `.soft` mixes in a light 12% tint and keeps the text in the intent color. `.ghost` has no fill at rest.
- **Edge:** `.edged` draws a border in the intent color; `.edgeless` draws none.

```html
<button class="btn primary">Save changes</button>
<button class="btn primary soft">Preview</button>
<button class="btn primary ghost edged">Export data</button>
<button class="btn ghost">Cancel</button>
```

Every component has its own default when no presentation class applies. A button is solid with no border. A card has no fill and a border. A form field has no fill and a border. [Composing](docs/usage/composing.md#component-defaults) lists the default of every component.

Unlike intent, presentation passes down. A `.soft` on a container makes everything inside it soft unless an element says otherwise, and a class on the element itself always beats one inherited from a container:

```html
<div class="cluster soft">
  <button class="btn primary">Soft primary</button>
  <button class="btn success">Soft success</button>
  <button class="btn primary solid">Solid: its own class wins</button>
</div>
```

Some components limit what presentation can do, to protect something they need. A form field inherited into a `.solid` container fills to only 6%, so typed text stays readable. A checkbox always keeps its border, because the border is the only thing that shows an unchecked box. These limits are listed with each component and collected in [Composing](docs/usage/composing.md).

## Finish the first screen

Add a plan summary next to a usage card in a grid, and give the summary a soft primary tint so it stands out as the account's current state:

```html
<main class="section">
  <div class="section-content narrow stack loose">
    <header class="stack tight">
      <h1 class="text-title">Account settings</h1>
      <p>Manage your profile, plan, and security.</p>
    </header>

    <section class="card stack" aria-labelledby="profile-title">
      <h2 class="text-title-sm" id="profile-title">Profile</h2>
      <p>Your name appears on invoices and shared projects.</p>
      <div class="cluster">
        <button class="btn primary" type="button">Save changes</button>
        <button class="btn ghost" type="button">Cancel</button>
      </div>
    </section>

    <div class="auto-grid">
      <section class="card soft primary stack tight" aria-labelledby="plan-title">
        <h2 class="text-label" id="plan-title">Current plan</h2>
        <p class="text-title-sm">Team, billed yearly</p>
        <p class="text-body">Renews on 12 March 2027.</p>
      </section>
      <section class="card stack tight" aria-labelledby="usage-title">
        <h2 class="text-label" id="usage-title">Storage</h2>
        <p class="text-title-sm">48 GB of 100 GB</p>
        <p class="text-body">Shared by 6 members.</p>
      </section>
    </div>
  </div>
</main>
```

On a wide window the two cards sit side by side; when the column is too narrow for two 16rem columns and the gap between them, they stack. The [Feedback](docs/usage/feedback.md) page turns the storage card's number into a usage bar.

A `.card` rests at one unit of depth, but on the base look that depth has no visible shadow; it appears when an aesthetic draws one or when you add `.raised` or `.floating`. [Composing → Elevation](docs/usage/composing.md#elevation) covers depth.

## Aesthetic: what it is made of

The third axis changes the material of everything inside it: corners, border width, shadows, and sometimes the font. It needs its own import (see [Setup → Add an aesthetic](docs/setup.md#add-an-aesthetic-optional)):

```html
<main class="section neobrutalism">
  <!-- The same settings screen: square corners, thick dark lines, hard offset shadows -->
</main>
```

Like presentation, an aesthetic passes down, and a component inside it can carry a different one. You do not need it to build the screen; [Aesthetics](docs/usage/aesthetics.md) compares all six.

## Words these docs use

Some pages describe how a component is painted. These words have one meaning each:

| Word      | Meaning                                                                                                                                                          |
| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Fill      | How much of the intent color covers the component, from 0% (`.ghost`) to 100% (`.solid`).                                                                        |
| Cap       | The highest fill a component allows. A form field caps an inherited fill at 6%, for example.                                                                     |
| Plate     | The component's painted background: the fill, mixed over the ground.                                                                                             |
| Ground    | The color the fill is mixed over. Usually the page itself shows through; a few components set their own, such as the light intent tint under `.kbd` and `.code`. |
| Edge      | The border around the component. It is separate from rules, the lines inside a table.                                                                            |
| Ink       | A color used to draw lines: the edge, a table's rules, a checkbox's mark. "Text ink" means the color of text printed on a plate.                                 |
| Material  | What an aesthetic changes: corner radius and shape, border width and style, shadow, and transparency.                                                            |
| Slab      | A hard, unblurred shadow offset down and to one side, as `.neobrutalism` and `.chunky-tile` draw.                                                                |
| Elevation | How much of the shadow in scope an element takes: none (`.flat`), one unit (`.raised`), or two (`.floating`).                                                    |

## Tokens behind the classes

Every class reads CSS custom properties, called tokens, so you can change the result without rewriting a class:

| Kind         | Controls                                                                        | Where it is listed                                                              |
| ------------ | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Color        | The palette: intent colors, text, borders, and backgrounds, for light and dark. | [Theming → Color tokens](docs/usage/theming.md#color-tokens)                       |
| Foundation   | Layout widths and gaps, corner radii, control height, focus, motion.            | [Theming → Foundation tokens](docs/usage/theming.md#foundation-tokens)             |
| Intent       | The color slots a component reads after an intent class picks a role.           | [Theming → Intent tokens](docs/usage/theming.md#intent-tokens)                     |
| Presentation | The fill and border percentages the presentation classes set.                   | [Customizing → Presentation tokens](docs/usage/customizing.md#presentation-tokens) |
| Material     | Radius, border width, shadow geometry, and transparency.                        | [Customizing → Material tokens](docs/usage/customizing.md#material-tokens)         |
| Aesthetic    | One knob per aesthetic, such as `--pixel-unit` or `--tile-lift`.                | [Customizing → Aesthetic tokens](docs/usage/customizing.md#aesthetic-tokens)       |

Color and foundation tokens are what most apps change first, to match a brand. The rest are lower level: components read them, and the presentation and aesthetic classes set them for you.

## Limits to know now

- **Intent does not pass down.** Write it on each element that shows it. The only exception is table rows.
- **Classes are styling only.** `<div class="btn">` looks like a button but cannot be focused or pressed with a keyboard. Use `<button>` or `<a href>`, and see [Accessibility](docs/accessibility.md).
- **A combination the docs do not describe is not supported.** It may render, but it can change in any release; see [Introduction → What counts as supported](docs/index.md#what-counts-as-supported).

## Next

The screen has one primary action and one quiet one. [Buttons](docs/usage/buttons.md) adds the rest: a destructive action for the danger zone, icon buttons, sizes, and disabled states.

<!-- Source: docs/usage/index.md -->

# Usage

These guides finish the settings screen started in [Building a screen](docs/concepts.md). Read the first seven in order: each one adds a part of the screen and assumes the ones before it. The last three are lookup pages; you will be sent to them when a guide needs the full table, and you can come back whenever you need a default or a rule.

Guides, in order:

1. [Buttons](docs/usage/buttons.md): the danger zone's destructive action, icon buttons, sizes, link buttons, and disabled states, plus how to load icons.
2. [Forms](docs/usage/forms.md): the profile form, with labels, hints, validation errors, an input with an icon or a fixed prefix, a select, and checkbox, radio, and switch toggles.
3. [Feedback](docs/usage/feedback.md): what the screen shows while saving and after: a loading button, a success alert, a plan badge, a loading placeholder, and a storage usage bar.
4. [Tooltips](docs/usage/tooltips.md): help text beside a form field, how to wire it for keyboard and screen reader users, and where a tooltip can be cut off.
5. [Theming](docs/usage/theming.md): light and dark mode, replacing the palette with brand colors, and defining an intent of your own, with the full color and foundation token tables.
6. [Aesthetics](docs/usage/aesthetics.md): the six optional looks, what each one changes, and its limits.
7. [Solo classes](docs/usage/solo.md): painting an aesthetic onto a component from another library.

Lookup pages:

- [Composing](docs/usage/composing.md): the complete rules for combining intent, presentation, and elevation, every component's defaults, and which axes each component reads.
- [Customizing](docs/usage/customizing.md): presentation and material tokens, aesthetic knobs, the six public component variables, the generated palette, and the composition helpers for building your own components.
- [Content and layout](docs/usage/content-and-layout.md): every layout helper, the type scale, cards and panels, tables, quotes, code, and keyboard keys.

When you finish, [Accessibility](docs/accessibility.md) collects what the CSS does for you and what your application still owes its users.

<!-- Source: docs/usage/buttons.md -->

# Buttons

The settings screen has a Save button and a Cancel button. This page adds the rest of its actions: a danger zone with a destructive Delete button, an icon button that copies an API key, and smaller buttons for a dense row. It also covers disabled buttons and how to load the icons used here and on later pages.

## Add a danger zone

A destructive action goes in its own card at the bottom of the screen. The card is soft and destructive so it reads as a warning; the button is solid so it is the loudest thing in that card:

```html
<section class="card soft destructive stack" aria-labelledby="danger-title">
  <h2 class="text-title-sm" id="danger-title">Delete account</h2>
  <p>This removes your projects, invoices, and API keys. It cannot be undone.</p>
  <div class="cluster">
    <button class="btn destructive" type="button">Delete account</button>
    <button class="btn ghost" type="button">Export data first</button>
  </div>
</section>
```

The card and the button each name `.destructive`, because [intent does not pass down](docs/concepts.md#intent-which-color). The Export button keeps its neutral color inside the red card for the same reason.

## Choose a fill and an edge

A button accepts one intent, one fill class, and one edge class, all optional. With none, a button is solid and edgeless:

```html
<button class="btn primary">Save changes</button>
<button class="btn secondary">Preview</button>
<button class="btn primary soft">Invite member</button>
<button class="btn destructive ghost edged">Revoke key</button>
<button class="btn ghost">Cancel</button>
```

A **neutral** button never fills completely. Its color is the page's own text color, so a full fill would be a black or white block, the loudest thing on the page for the intent that means nothing in particular. A neutral `.solid` button stops at a 20% fill instead, a quiet gray a step darker than `.soft`. Every named intent fills to 100%.

On hover, a button's fill deepens by `--hover-step` (14%), so a `.ghost` button picks up a tint and a `.solid` one changes to its intent's hover color. On press it scales down slightly (`scale(0.97)`); an [aesthetic](docs/usage/aesthetics.md) can replace that with its own press, and a user's reduced-motion preference turns it off.

## Sizes and shapes

| Class   | Effect                                                                                                                                       |
| ------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| _none_  | At least 2.5rem tall (`--control-height`), 1rem side padding, and the same font size as the surrounding text.                                |
| `.sm`   | At least 2rem tall, 0.75rem side padding, 0.875rem text.                                                                                     |
| `.lg`   | At least 3rem tall, 1.25rem side padding, 1.0625rem text.                                                                                    |
| `.pill` | Fully rounded ends.                                                                                                                          |
| `.icon` | Square. At least 2.5rem on each side, 2rem with `.sm`, 3rem with `.lg`.                                                                      |
| `.p-xs` | Tightest padding (0.5rem sides, 0.125rem top and bottom). Also removes the minimum height, so a button fits a table cell or a dense toolbar. |
| `.p-sm` | Compact padding (0.625rem sides, 0.25rem top and bottom).                                                                                    |
| `.p-lg` | Roomy padding (1.5rem sides, 0.75rem top and bottom).                                                                                        |

A size class and a padding class can be combined. Only `.p-xs` removes the minimum height of a labeled button; every other combination keeps it. On an icon button, any `.p-*` class removes the minimum size and the button sizes to its icon plus that padding: 0.125rem, 0.25rem, or 0.75rem around it.

The base look draws corners that scale with the size: a `.sm` or `.p-sm` button has slightly smaller corners than a default one, and `.lg` slightly larger.

## Icon buttons

The API keys list on the settings screen has a copy button beside each key. An icon-only button needs an accessible name, because the icon has no text a screen reader can read:

```html
<button class="btn icon ghost" type="button" aria-label="Copy API key">
  <i class="ic-lucide-copy" aria-hidden="true"></i>
</button>
<button class="btn ghost sm" type="button">
  <i class="ic-lucide-download" aria-hidden="true"></i>
  Download invoice
</button>
```

A button is a flex row with a 0.5rem gap, so an icon next to a label is spaced without extra markup.

### Loading icons

This package ships no icons. The examples in these docs use `@codenhub/icons` with the Lucide family, and its class names are always written with the family, as `ic-lucide-copy`. Install it and load it once, next to this package:

```sh
pnpm add @codenhub/icons
```

With compiled CSS, import the base rules and each family you use:

```css
@import "@codenhub/styles";
@import "@codenhub/icons";
@import "@codenhub/icons/lucide";
```

With Tailwind source, import its Tailwind entrypoint after this package's:

```css
@import "@codenhub/styles/tw";
@import "@codenhub/icons/tw";
```

Any other icon works too: an inline `<svg>` with `stroke="currentColor"` or `fill="currentColor"` takes the button's text color the same way.

## Links that look like buttons

A link that navigates, such as "View billing history", is still a link. Write it as `<a href>` with `.btn`, and it gets the same look:

```html
<a class="btn secondary ghost edged" href="/settings/billing">View billing history</a>
```

Do not use `<div class="btn">` or `<span class="btn">`: they look like buttons but cannot be focused or activated from a keyboard. Use `<button>` for actions and `<a href>` for navigation.

## Disabled buttons

Any of these makes a button look disabled: the `disabled` attribute, `.disabled`, `aria-disabled="true"`, or `data-disabled`. A disabled button is drawn at 58% opacity with a not-allowed cursor, and does not change on hover or press.

```html
<button class="btn primary" type="submit" disabled>Save changes</button>

<a class="btn ghost" aria-disabled="true">Download invoice</a>
```

Only the `disabled` attribute stops the button from working. `.disabled`, `aria-disabled`, and `data-disabled` change the look only: a click or the Enter key still activates it, so your code has to ignore it. `aria-disabled` is the right choice when the button should stay focusable, for example so a tooltip can explain why it is disabled. A link has no `disabled` attribute, so remove its `href` as well when it should not navigate.

## Loading state

There is no loading class. A loading button is a disabled button with a spinner icon you bring, which [Feedback](docs/usage/feedback.md#show-that-the-form-is-saving) builds for the Save button. Disable it so a second click cannot submit twice.

## Next

The buttons are ready; the card they save has nothing to save yet. [Forms](docs/usage/forms.md) adds the profile fields.

<!-- Source: docs/usage/forms.md -->

# Forms

The profile card has a Save button but nothing to save. This page fills it in: a display name with a hint, an email address that shows a validation error, a profile address with a fixed prefix, a language select, and the toggles for notifications, billing, and profile visibility. The package styles each control; your HTML provides the labels, the validation, and the error messages, and this page shows how to connect them.

## A labeled field

A field is a label, a control, and an optional hint, stacked:

```html
<div class="field">
  <label class="label" for="display-name">Display name</label>
  <input class="ipt" id="display-name" name="display-name" aria-describedby="display-name-hint" />
  <span class="hint" id="display-name-hint">Shown on invoices and shared projects.</span>
</div>
```

- `.field` stacks its children with a small gap (0.35rem) and takes the full width.
- `.label` sets label text: 1rem, medium weight.
- `.hint` is smaller, secondary-colored helper text. Keep it outside the `<label>` and point `aria-describedby` at it, so a screen reader announces it as a description instead of reading it as part of the field's name.
- `.ipt` styles an `<input>`. Use `.textarea` for a `<textarea>` and `.select` for a `<select>`.

A text control is at least 2.5rem tall (`--control-height`), the same height as a default button, so a field and a button line up in a row. Its default look is no fill and a border. A `.textarea` is at least 7rem tall and can be resized vertically.

## Show a validation error

Mark the control invalid with `aria-invalid="true"` and give the error message `.hint.destructive`:

```html
<div class="field">
  <label class="label" for="email">Email</label>
  <input class="ipt" type="email" id="email" name="email" value="ada@" aria-invalid="true" aria-describedby="email-error" />
  <span class="hint destructive" id="email-error">Enter a full email address, such as ada@example.com.</span>
</div>
```

An invalid control draws its border, focus ring, and typed text in the destructive color, whatever intent class it has. The text uses the readable tone of that color, `--intent-strong`, so it stays legible on the page. The package only paints the state: deciding that the value is invalid, setting and clearing `aria-invalid`, and writing the message are your code. Remove `aria-invalid` once the value is valid again; leaving it set keeps the field red.

## Put the profile form together

```html
<form class="card stack" aria-labelledby="profile-title">
  <h2 class="text-title-sm" id="profile-title">Profile</h2>

  <div class="field">
    <label class="label" for="display-name">Display name</label>
    <input class="ipt" id="display-name" name="display-name" value="Ada Lovelace" aria-describedby="display-name-hint" />
    <span class="hint" id="display-name-hint">Shown on invoices and shared projects.</span>
  </div>

  <div class="field">
    <label class="label" for="email">Email</label>
    <input class="ipt" type="email" id="email" name="email" value="ada@example.com" />
  </div>

  <div class="field">
    <label class="label" for="bio">Bio</label>
    <textarea class="textarea" id="bio" name="bio" rows="3"></textarea>
  </div>

  <div class="field">
    <label class="label" for="language">Language</label>
    <select class="select" id="language" name="language">
      <option>English</option>
      <option>Português</option>
      <option>Español</option>
    </select>
  </div>

  <div class="cluster">
    <button class="btn primary" type="submit">Save changes</button>
    <button class="btn ghost" type="reset">Cancel</button>
  </div>
</form>
```

A `.select` draws its own arrow. With `multiple` or a `size` above 1 it becomes a list box instead: no arrow, as tall as the rows it shows. The open list of options is drawn by the browser and the operating system, so it does not take the package's look.

## Add an icon or a fixed prefix

`.input-group` puts an icon or a piece of text inside a field's border. The group draws the border, background, focus ring, and invalid and disabled states; the control inside it gives up its own, so the border is drawn once. Place the adornment before the control to put it at the start, after it to put it at the end.

The profile address always starts with the same host, so it is a fixed prefix:

```html
<div class="field">
  <label class="label" for="profile-url">Profile address</label>
  <div class="input-group" style="--input-group-gap: 0">
    <span>app.example.com/u/</span>
    <input class="ipt" id="profile-url" name="profile-url" value="ada" />
  </div>
</div>
```

The gap between the control and its adornment is `--input-group-gap`, 0.5rem by default. That suits an icon, but text meant to read as part of the value looks broken with a space after it, so the example sets it to `0`. Set it on the `.input-group` itself: the group declares its own default, so a value set on a parent element has no effect. It is one of six [public component variables](docs/usage/customizing.md#component-variables).

A members search, with an icon at the start (the `ic-lucide-*` classes need `@codenhub/icons` loaded; see [Buttons → Loading icons](docs/usage/buttons.md#loading-icons)):

```html
<div class="field">
  <label class="label" for="member-search">Find a member</label>
  <div class="input-group">
    <i class="ic-lucide-search" aria-hidden="true"></i>
    <input class="ipt" type="search" id="member-search" name="q" />
  </div>
</div>
```

A `.select` works in a group too. The group removes the control's side padding, except at the end of a single-row select, which keeps 1.75rem so the chosen option's text does not run under the arrow.

Any element works as the adornment: an icon class, an inline `<svg>`, an `<img>`, or text. The package ships no icons for fields, because a built-in image could not follow your text color, theme, or aesthetic.

The group reads intent and presentation the way a lone `.ipt` does: `.input-group.primary` colors the border, and `.input-group.soft.edgeless` is a field sunk into the page. `aria-invalid="true"` or `disabled` on the control inside applies to the whole group, and so does either on the group itself. The focus ring appears when anything inside the group has focus, so unlike a lone field it also shows after a mouse click.

## Browser decorations

Some input types come with built-in buttons: a date or time field's picker button, a number field's up and down arrows, and a search field's clear button. The package keeps them, because each one also does something: the picker button is the only way to open the picker with a mouse, and the clear button empties the field.

`.unadorned` removes them, for a field where you draw your own. Put it on the control, or on its `.input-group`. Removing a decoration removes what it did, so connect your replacement yourself: `input.showPicker()` opens a picker, `stepUp()` and `stepDown()` replace the arrows, and setting `value` to `""` and dispatching an `input` event replaces a clear button. Typing keeps working either way. Firefox gives no way to remove its date picker button, so it stays there.

```html
<div class="field">
  <label class="label" for="member-filter">Find a member</label>
  <div class="input-group unadorned">
    <i class="ic-lucide-search" aria-hidden="true"></i>
    <input class="ipt" type="search" id="member-filter" name="q" />
    <button class="btn icon ghost p-xs" type="button" aria-label="Clear search">
      <i class="ic-lucide-x" aria-hidden="true"></i>
    </button>
  </div>
</div>
```

The clear button above is a real `<button>` with an accessible name. A button inside a group keeps its own classes: a `.btn.ghost` has no fill and, like every button by default, no border, so it sits inside the field's border without drawing one of its own.

## Toggles

The settings screen has three kinds of toggle: a checkbox to opt into invoice emails, radio buttons to choose a billing period, and a switch to make the profile public. Each class goes on a native `<input>` of the matching type:

```html
<fieldset class="stack tight">
  <legend class="label">Notifications</legend>
  <label class="cluster tight">
    <input class="checkbox" type="checkbox" name="invoice-email" checked />
    Email me when an invoice is ready
  </label>
</fieldset>

<fieldset class="stack tight">
  <legend class="label">Billing period</legend>
  <label class="cluster tight">
    <input class="radio" type="radio" name="period" value="monthly" />
    Monthly
  </label>
  <label class="cluster tight">
    <input class="radio" type="radio" name="period" value="yearly" checked />
    Yearly, two months free
  </label>
</fieldset>

<label class="cluster tight">
  <input class="switch primary" type="checkbox" role="switch" name="public-profile" />
  Public profile
</label>
```

- `.checkbox` goes on `type="checkbox"`, `.radio` on `type="radio"`, and `.switch` on `type="checkbox"`. Give a switch `role="switch"` yourself, so it is announced as on or off rather than checked.
- `.cluster.tight` on the `<label>` lines the control up with its text and leaves a 0.5rem gap.
- A checkbox whose `indeterminate` property is set, for a group where only some items are checked, shows a dash, and keeps the dash while also `checked`, as the browser's own checkbox does. Set it from script (`input.indeterminate = true`); there is no attribute for it.

### Checked and unchecked colors

An intent class sets the checked color. With no intent, a checked toggle is filled with the text color, and the mark is drawn in the page color: a black box with a white check on a light page. Unlike a neutral button, a neutral toggle fills all the way when checked.

Presentation changes only the unchecked look. Every toggle rests at `.soft`; once checked, each has one look whatever its presentation:

| Toggle           | Unchecked fill | Checked                                        |
| ---------------- | -------------- | ---------------------------------------------- |
| `.ghost`         | 0%             | 100% fill, with the mark in the contrast color |
| `.soft`, default | 12%            | 100% fill, with the mark in the contrast color |
| `.solid`         | 40%            | 100% fill, with the mark in the contrast color |

A checked checkbox and a checked switch cut their mark out of the filled plate. A checked radio thickens its ring to twice the resting line width, up to 3px, takes the full intent color on it, and shows a dot. The 3px limit keeps the ring from swallowing the dot under an aesthetic with a thick line.

`.checkbox` and `.radio` always draw their border, even with `.edgeless`, because the border is the only thing that shows an unchecked box. `.switch.ghost` keeps its border too, since with no fill and no border nothing would mark the track. These are listed in [Composing](docs/usage/composing.md#component-axis-reference).

An aesthetic that draws its border as an inner shadow, such as `.pixel`, cannot make that shadow wider than the toggle's border width. A 16px checkbox or a 20px switch has no room for a thicker inner ring without covering the mark. This only matters if you set `--ui-shadow-spread` yourself; see [Customizing → Material tokens](docs/usage/customizing.md#material-tokens).

## Fill and border on text controls

Text controls take intent, which colors the border, the focus ring, and the typed text, the text in the intent's readable tone (`--intent-strong`). They also take both presentation axes. Their fill is capped low, so typed text stays readable:

| On a text control                               | Fill |
| ----------------------------------------------- | ---- |
| `.ghost`, default                               | 0%   |
| Any fill from a container (`.soft` or `.solid`) | 6%   |
| `.soft` on the control                          | 12%  |
| `.solid` on the control                         | 20%  |

A container's fill is meant for the surfaces in it, so a field nobody classed takes only a faint 6% of it. Put the class on the field itself for more.

The border works the same way. An `.edgeless` inherited from a container is ignored, so a toolbar cannot leave a field with no visible edge. `.edgeless` written on the control itself is honored: that is how you ask for a borderless field. A borderless field does not meet the WCAG 1.4.11 contrast requirement for a control's boundary, so it is never a default; its focus ring still shows.

```html
<input class="ipt success" placeholder="Matches your current password" />
<input class="ipt soft edged" placeholder="Tinted, with a border" />
<input class="ipt soft edgeless" placeholder="Sunk into the page" />
<div class="soft">
  <input class="ipt" placeholder="A container's .soft gives a 6% tint" />
</div>
```

At rest, a field's border is deliberately lighter than 3:1 against the page, so hover and focus have a visible change to make. [Accessibility → Control boundaries](docs/accessibility.md#control-boundaries) explains the trade-off and how to get a stronger border.

## Disabled controls

`disabled`, `aria-disabled="true"`, `data-disabled`, or `.disabled` on a control draws it at reduced opacity with a not-allowed cursor. As with buttons, only the `disabled` attribute stops input; the others change the look only.

## Building your own control

For a text-like control the package does not ship, `.text-control` gives an element the same border, placeholder, hover, focus, invalid, and disabled styles as `.ipt`, without its height or padding. [Customizing → Composition helpers](docs/usage/customizing.md#composition-helpers) explains it with the other building blocks.

## Next

The form can be filled in and submitted. [Feedback](docs/usage/feedback.md) shows what the screen does next: a Save button that says it is saving, a success message, a plan badge, and a storage usage bar.

<!-- Source: docs/usage/feedback.md -->

# Feedback

The profile form can be submitted, but nothing tells the user what happened. This page adds the screen's feedback: a Save button that shows it is working, an alert when the save succeeds, a badge on the plan card, a placeholder while the profile loads, and bars for storage use and a data export. The package draws each of these; your code decides when they appear, updates their values, and makes sure screen readers hear about them.

## Show that the form is saving

There is no loading class. A saving button is a disabled button with a spinning icon:

```html
<button class="btn primary" type="submit" disabled>
  <i class="ic-lucide-loader-circle ic-anim-spin" aria-hidden="true"></i>
  Saving…
</button>
```

The spinner comes from `@codenhub/icons`, which this package does not load. Load it once, as [Buttons → Loading icons](docs/usage/buttons.md#loading-icons) shows; with compiled CSS that is:

```css
@import "@codenhub/icons";
@import "@codenhub/icons/lucide";
```

`ic-anim-spin` rotates the icon and stops when the user asks the system for reduced motion. The animated `svg-spinners` family in `@codenhub/icons` is different: its artwork keeps moving under that preference. Either icon takes the button's text color.

Disabling the button stops a second click from submitting twice. Whether to keep or replace the label is up to you; the package has no way to hide it. A changing button label is not reliably announced, so tell screen reader users the outcome with a status message, as the next section does.

## Confirm the result with an alert

When the save succeeds, show an alert above the form:

```html
<div class="alert success" role="status">Profile saved.</div>
```

`role="status"` makes screen readers announce the text politely, after what they are reading. For an error the user must act on, use `role="alert"`, which interrupts:

```html
<div class="alert destructive" role="alert">Your changes were not saved. Check your connection and try again.</div>
```

Screen readers announce a live region when its content changes, so render the region empty when the page loads and put the message into it, rather than inserting a new alert element with the text already inside. The package adds no role, timing, or dismiss button; those are yours.

An alert is a row with a 0.75rem gap, so an icon placed before the message is spaced automatically. `.alert-icon` sizes an icon to 1.25rem and aligns it with the first line of text:

```html
<div class="alert warning" role="status">
  <i class="alert-icon ic-lucide-triangle-alert" aria-hidden="true"></i>
  Your card expires next month. Update it to keep your plan.
</div>
```

Any icon element works: a class from an icon set, an inline `<svg>`, or an `<img>`. `@codenhub/icons` sizes its glyphs in a rule no class can outrank, so `.alert-icon` also sets that set's `--ic-size`. To resize an alert icon, write your own rule that sets `width`, `height`, and, for `@codenhub/icons`, `--ic-size`.

### How alerts look

With no presentation class, an alert is soft and edged: a light tint of its intent, intent-colored text, and a border. Presentation classes change that like on any component, such as `.alert.primary.solid` for a filled banner.

An alert with no intent is an exception. It has no tint at rest; it uses `--color-foreground`, the quiet background tone one step off the page, the same as a neutral `.card.soft` or `.panel`. An alert with `.primary`, `.secondary`, `.success`, `.warning`, `.destructive`, or `.info` keeps its tint. Only those six classes count here: an alert with an intent class you defined yourself is treated as having no intent; see [Theming → Your own intent](docs/usage/theming.md#your-own-intent).

## Label the plan with a badge

A badge is a small pill for a status or a count:

```html
<section class="card soft primary stack tight" aria-labelledby="plan-title">
  <div class="cluster between">
    <h2 class="text-label" id="plan-title">Current plan</h2>
    <span class="badge success">Active</span>
  </div>
  <p class="text-title-sm">Team, billed yearly</p>
</section>
```

A badge is soft with no border by default, and takes the same intent and presentation classes as a button: `.badge.warning` for "Payment due", `.badge.info.solid` for a filled "New". Unlike an alert, a badge with no intent keeps its light tint, because it has no border, and without the tint it would disappear into the text around it.

## Show a placeholder while loading

While the profile card waits for data, show gray blocks where the text will be:

```html
<section class="card stack" aria-busy="true" aria-labelledby="profile-title">
  <h2 class="text-title-sm" id="profile-title">Profile</h2>
  <p class="visually-hidden">Loading your profile…</p>
  <div class="stack tight" aria-hidden="true">
    <div class="skeleton" style="height: 2.5rem"></div>
    <div class="skeleton" style="width: 60%"></div>
  </div>
</section>
```

A `.skeleton` fills the width of its container and is at least 1rem tall. It has no size or shape of its own: use a block element such as a `<div>`, and set its width and height to match the content it stands in for. An empty inline element such as `<span class="skeleton">` has no size and does not appear.

A skeleton takes an intent but ignores presentation. With no intent, it is a light gray tinted by the surrounding text color. It shows a moving shimmer that stops when the user asks for reduced motion.

Mark the blocks `aria-hidden="true"`, because they mean nothing to a screen reader. Say that the content is loading in text instead, as the visually hidden paragraph does, and remove `aria-busy` once the content is in place.

## Show progress

The storage card shows how much space is used, and the danger zone's "Export data first" button starts an export that takes a while. Both are `.progress` bars:

```html
<div class="progress" role="meter" aria-label="Storage used" aria-valuemin="0" aria-valuemax="100" aria-valuenow="48" aria-valuetext="48 GB of 100 GB" style="--progress-value: 48%"></div>

<div class="progress primary" role="progressbar" aria-label="Export progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="20" style="--progress-value: 20%"></div>
```

`--progress-value` is the width of the filled part, as a percentage of the track. It is 0% when unset.

- **Write it with `%`.** `--progress-value: 48` without a unit is not a valid width, and the bar shows empty.
- **Nothing is clamped or checked.** Above 100%, the fill is cut off at the end of the track and looks full; a negative value is not a valid width either. Keep the value between 0% and 100% in your code.
- **The visual value and the announced value are separate.** The bar's width comes from `--progress-value`; what a screen reader announces comes from `aria-valuenow`. The package does not connect them, so update both together.

```js
function setExportProgress(bar, percent) {
  const value = Math.min(100, Math.max(0, Math.round(percent)));
  bar.classList.remove("indeterminate");
  bar.style.setProperty("--progress-value", `${value}%`);
  bar.setAttribute("aria-valuenow", String(value));
}
```

When you do not know how far along the work is, such as while the export is being prepared, use `.indeterminate`. A bar about a third of the track wide sweeps from start to end, again and again, and `--progress-value` is ignored. Remove `aria-valuenow`, because there is no known value to announce:

```html
<div class="progress primary indeterminate" role="progressbar" aria-label="Preparing export"></div>
```

`.active` adds a shimmer over the track of a determinate bar, to show that work is still happening between updates. Both animations stop when the user asks for reduced motion; an indeterminate bar then rests with its fill at the start.

### How progress bars look

The fill is the intent color at full strength, or `currentColor` with no intent. The track is soft and edgeless by default: a light tint with no border.

The default track is faint. Against the page it measures about 1.1:1 to 1.3:1, below the 3:1 that WCAG 1.4.11 asks of the parts of a graphic needed to understand it. When the track's extent matters, use `.ghost.edged` with an intent class, which draws an outline around it in the intent's color. With no intent the outline is a light gray that measures below 3:1; [Accessibility → Progress outlines](docs/accessibility.md#progress-outlines) has the measurements and what to do for a neutral bar. `.solid` is not supported on the track, because the fill and the track would be the same color. `.ghost.edgeless` renders, but leaves the track with no visible frame.

To change the colors of the fill, the track, or its border without changing the intent, set `--progress-color`, `--progress-surface`, or `--progress-edge` on the bar or any element around it. They and `--progress-value` are listed in [Customizing → Component variables](docs/usage/customizing.md#component-variables).

## Animation that keeps running

Skeletons, `.active` shimmers, indeterminate bars, and spinners animate for as long as they are on the page. The package stops its own animations when the user asks the system for reduced motion, but that is not the only requirement. WCAG 2.2.2 asks that motion which starts automatically, lasts more than five seconds, and is shown alongside other content can be paused, stopped, or hidden by the user, unless the motion is essential.

A loading indicator shown while the user cannot do anything else, and that would confuse them if it were missing, counts as essential. A shimmer or spinner that keeps moving next to content the user is working with, such as a decorative "live" shimmer on a card, does not. Remove those as soon as their work is done, or give the user a control that stops them.

## Next

The screen now reports what it is doing. [Tooltips](docs/usage/tooltips.md) adds short help text beside a form field.

<!-- Source: docs/usage/tooltips.md -->

# Tooltips

The "Public profile" switch on the settings screen deserves a short explanation: what becomes visible when it is on. This page adds a help button beside it that shows a tooltip on hover and on keyboard focus. The CSS shows and hides the message and draws it; you connect it to its trigger for screen readers, add the Escape key, and check that nothing around it cuts it off.

## A help tooltip

```html
<div class="cluster tight">
  <label class="cluster tight">
    <input class="switch primary" type="checkbox" role="switch" name="public-profile" />
    Public profile
  </label>
  <span class="tooltip">
    <button class="tooltip-icon" type="button" aria-label="About public profiles" aria-describedby="public-profile-help">?</button>
    <span class="tooltip-bubble" role="tooltip" id="public-profile-help">Anyone with the link can see your name, photo, and public projects.</span>
  </span>
</div>
```

The three parts:

- `.tooltip` wraps the trigger and the message. It positions the message and paints nothing itself.
- `.tooltip-bubble` is the message. It is a real element, so it can have an `id` that the trigger points to.
- `.tooltip-icon` is a small round trigger, for a message with nothing else to attach to. Any focusable element can be the trigger instead: wrap an existing `.btn` in `.tooltip` and it keeps its own look.

Connect them yourself: an `id` on the bubble, `aria-describedby` on the trigger pointing to that `id`, and `role="tooltip"` on the bubble. A screen reader then reads the message as the trigger's description. The CSS cannot make this connection, and without it the message is visual only.

The trigger must be focusable, such as a `<button>`, so keyboard users can reach the message. The bubble opens when anything inside `.tooltip` has focus, so it also opens after a mouse click on the trigger.

## Placement

The bubble opens above its trigger, horizontally centered, with a 0.5rem gap. Set `data-tooltip-position` on `.tooltip` to choose another side:

```html
<span class="tooltip" data-tooltip-position="bottom">
  <button class="btn ghost sm" type="button" aria-describedby="renewal-help">Renews 12 March 2027</button>
  <span class="tooltip-bubble" role="tooltip" id="renewal-help">Your card is charged on this date.</span>
</span>
```

| `data-tooltip-position` | Bubble                                            |
| ----------------------- | ------------------------------------------------- |
| _none_ or `"top"`       | Above the trigger, centered.                      |
| `"bottom"`              | Below the trigger, centered.                      |
| `"left"`                | To the left of the trigger, centered vertically.  |
| `"right"`               | To the right of the trigger, centered vertically. |

A bubble is as wide as its text, up to 18rem, and then wraps.

### Where a tooltip gets cut off

The bubble is positioned with CSS only, on the side you chose. Nothing measures the page: it does not move to stay inside the window, flip to the other side when there is no room, or move to the top layer of the page. That has three consequences:

- **A scrolling or clipping container cuts it off.** An ancestor with `overflow: hidden`, `auto`, or `scroll` clips the bubble at its edge. The API keys table on the settings screen sits in a `.table-wrap`, which scrolls, so a top tooltip on the first row is cut off at the top of the wrapper. Raising `z-index` does not help: it changes what the bubble is painted above, not whether the container clips it.
- **The window edge cuts it off.** A top tooltip on the first line of a page, or a right tooltip near the right edge, extends off screen.
- **Left and right are physical sides.** In a right-to-left page, `"left"` still means the left side of the screen.

What to do:

- Choose a side with room: on a table's first row, use `data-tooltip-position="bottom"`.
- Move the tooltip's trigger out of the clipping container, or remove the `overflow` from a container that does not need it.
- If the trigger's position cannot be known in advance, such as in a scrolling list or a resizable panel, use a positioning library or your framework's popover component, which can measure and flip. You can still give its message element the `.tooltip-bubble` classes for the look, but the showing, hiding, and positioning described on this page then come from that library, not from `.tooltip`.

## Hover, linger, and Escape

WCAG 1.4.13 asks three things of content that appears on hover or focus. The package handles two:

- **Hoverable.** When the pointer leaves the trigger, the bubble stays for `--tooltip-linger` (300ms) and then fades, so the pointer can cross the gap onto it. It stays open while the pointer is over it. A closed bubble is `visibility: hidden`, so it never catches the pointer.
- **Persistent.** It stays open until the pointer and focus leave.
- **Dismissible.** The user must be able to close it without moving the pointer or focus, usually with Escape. CSS cannot listen for a key, so this part is yours.

To change the linger time, set `--tooltip-linger` on the `.tooltip` element, for example `style="--tooltip-linger: 500ms"`. The wrapper declares its own default, so a value set on a parent of `.tooltip` has no effect. Keep it long enough for the pointer to reach the bubble. It is one of six [public component variables](docs/usage/customizing.md#component-variables).

For Escape, write `data-state="closed"` on `.tooltip`: it hides the bubble even while it is hovered or focused. Remove the attribute only when both the pointer and focus have left the tooltip, so the next hover or focus opens it again. Removing it earlier, while the trigger still has focus, makes the message reappear without the user doing anything.

```js
for (const tooltip of document.querySelectorAll(".tooltip")) {
  let pointerInside = tooltip.matches(":hover");

  tooltip.addEventListener("pointerenter", () => {
    pointerInside = true;
  });

  tooltip.addEventListener("pointerleave", () => {
    pointerInside = false;
    if (!tooltip.contains(document.activeElement)) tooltip.removeAttribute("data-state");
  });

  tooltip.addEventListener("focusout", (event) => {
    const focusStaysInside = tooltip.contains(event.relatedTarget);
    if (!focusStaysInside && !pointerInside) tooltip.removeAttribute("data-state");
  });
}

document.addEventListener("keydown", (event) => {
  if (event.key !== "Escape") return;
  for (const tooltip of document.querySelectorAll(".tooltip:hover, .tooltip:focus-within")) {
    tooltip.dataset.state = "closed";
  }
});
```

`data-state="open"` does the opposite: it shows the bubble with no hover or focus, for a message that should stay visible, or for a test or a screenshot.

## How the bubble looks

With no intent class, the bubble uses `--color-tooltip`, a background chosen per theme: near-white on a light page, mid gray on a dark one, with `--color-tooltip-contrast` as its text color. Near-white measures only 1.04:1 against a light page, so the bubble also draws a thin border, and a container's `.edgeless` cannot remove it. `.edgeless` on the bubble itself still can. The background stays opaque under any presentation, so the bubble stays readable over whatever it covers.

An intent class fills the bubble with that intent's color and switches the text to the matching contrast color:

- `.tooltip-bubble.primary` has a near-black background with near-white text on a light page, and a near-white background with near-black text on a dark one.
- `.tooltip-bubble.destructive` has a red background with light text in both themes.

Put intent, presentation, aesthetic, and elevation classes on `.tooltip-bubble`, the element that is painted, not on `.tooltip`. The bubble is flat by default, even under an aesthetic that draws depth; add `.raised` or `.floating` to the bubble to lift it. An aesthetic in scope shapes the bubble like any other component; under `.glass` it is frosted like a card. `.tooltip-icon` takes the aesthetic's corner: square under `.neobrutalism` and `.pixel`, a diamond under `.cyber`.

The bubble appears without scaling up when the user asks for reduced motion.

## Next

The settings screen is complete in its default look. [Theming](docs/usage/theming.md) gives it a dark mode and your brand colors.

<!-- Source: docs/usage/theming.md -->

# Theming

The settings screen works and looks like the package's defaults: near-black actions on a near-white page. This page makes it yours. It covers light and dark mode, then replaces the primary color with a brand color in both modes, then defines an intent the package does not ship. The second half is the lookup reference: every color, intent, foundation, and font token, with its default.

## Light and dark

Out of the box, the page follows the operating system: light when the user's system is light, dark when it is dark. You do nothing for that.

To let the user choose, put a theme selector on the root element:

```html
<html data-theme="dark">
  …
</html>
```

| Theme | Equivalent selectors                             |
| ----- | ------------------------------------------------ |
| Light | `.light`, `.theme-light`, `[data-theme="light"]` |
| Dark  | `.dark`, `.theme-dark`, `[data-theme="dark"]`    |

A settings screen with a "System / Light / Dark" choice removes the attribute for System and sets it for the other two.

How it resolves:

- A theme selector sets the CSS `color-scheme` property and nothing else. Every color token holds its light and its dark value at once, through `light-dark()`, and the element's `color-scheme` picks one.
- `color-scheme` inherits, so a selector themes everything inside it. A selector on any element themes that part of the page: a dark sidebar on a light page is `<aside class="dark">`.
- The nearest selector wins, at any depth. A `.light` card inside a `.dark` page is light.
- With no selector, the root has `color-scheme: light dark`, which follows the system.
- Put one theme on an element. If an element carries both a light and a dark selector, dark wins, but do not rely on that.
- Setting `color-scheme` yourself on an element works the same way, because it is the only thing the palette reads. It also themes native parts such as scrollbars and the inside of form controls.

Every component color comes from a token or from `currentColor`, so components follow the theme with no dark-mode rules of their own. The marks the package draws, such as the checkbox tick and the select arrow, use `currentColor` too. Icons and images you add are yours to theme.

`light-dark()` works from Chrome 123, Safari 17.5, and Firefox 120. The package as a whole needs Firefox 128; see [Setup → Requirements](docs/setup.md#requirements).

### The `dark:` variant

With the Tailwind source entrypoints (`/tw`), the package replaces Tailwind's `dark:` variant so your own utilities follow the same theme as the tokens. A `dark:` utility applies under an explicit dark selector on the element or an ancestor, or under a dark system preference when no ancestor sets a theme:

```html
<html>
  <body>
    <p class="dark:text-white">Follows the system preference</p>
    <div class="light">
      <p class="dark:text-white">Stays light: this part of the page chose a theme</p>
    </div>
  </body>
</html>
```

It differs from the tokens in one case: an explicit theme nested inside the opposite one. A `.light` element inside a `.dark` one has light tokens, because the nearest `color-scheme` wins. But `dark:` matches by selector, and `.dark *` matches every element inside `.dark`, so a `dark:` utility inside that `.light` element still applies. If a page uses both tokens and `dark:` utilities, do not nest opposite themes.

The compiled entrypoints do not define a `dark:` variant; they have no Tailwind build to define it in.

## Change the brand color

The primary color is near-black on a light page and near-white on a dark one. To make it a brand color, replace the whole primary family: the base color and its four companions. Each holds a light and a dark value:

```css
:root {
  --color-primary: light-dark(#0f766e, #2dd4bf);
  --color-primary-contrast: light-dark(#ffffff, #042f2e);
  --color-primary-hover: light-dark(#115e59, #5eead4);
  --color-primary-subtle: light-dark(#ccfbf1, #134e4a);
  --color-primary-strong: light-dark(#115e59, #5eead4);
}
```

What each one is for:

| Token                      | Role                                                                   | Check                                          |
| -------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------- |
| `--color-primary`          | The fill of a solid primary button, a checked toggle, a progress fill. | Text in `-contrast` on it reaches 4.5:1.       |
| `--color-primary-contrast` | Text and icons on a full primary fill.                                 | See above.                                     |
| `--color-primary-hover`    | The fill under the pointer.                                            | Text in `-contrast` on it still reaches 4.5:1. |
| `--color-primary-subtle`   | The light tint behind soft components.                                 | Text in `-strong` on it reaches 4.5:1.         |
| `--color-primary-strong`   | Text, icons, and borders where the fill is not full: soft and ghost.   | Reaches 4.5:1 on the page and on `-subtle`.    |

The teal values above measure, in light and dark mode: 5.47:1 and 7.77:1 for the fill and its text, 7.27:1 and 13.38:1 for the strong tone on the page, and 6.73:1 and 6.41:1 for the strong tone on the subtle tint.

Replace the whole family. Changing only `--color-primary` leaves its companions on the old near-black, so a soft primary badge keeps gray text and a hovered button flashes black. The focus ring is drawn from `--color-primary` at 64% opacity, so it follows the brand color without a change of its own.

The same pattern works for every family in [Color tokens](#color-tokens): `--color-accent` is what `.secondary` reads, and the four status families are `success`, `warning`, `destructive`, and `info`. Set them on `:root` for the whole page, or on any element to retheme only what is inside it.

The values must be complete colors: hex, `rgb()`, `oklch()`, or a variable you declared yourself. Tailwind palette variables such as `--color-teal-700` exist in a `/tw` build, from your Tailwind theme, but the compiled CSS declares only the palette colors the package itself uses. With compiled CSS, a reference to any other palette variable is empty, and the color it feeds is lost.

## Your own intent

The package ships six intents: `.primary`, `.secondary`, `.success`, `.warning`, `.destructive`, and `.info`, and the default, `.neutral`. To add one, such as a "Beta" color for features in preview, write a class that sets the seven intent slots. Use complete colors, with `light-dark()` for anything that changes between themes:

```css
.beta {
  --intent-color: #7c3aed;
  --intent-contrast: #ffffff;
  --intent-hover: #6d28d9;
  --intent-strong: light-dark(#5b21b6, #c4b5fd);
  --intent-subtle: light-dark(#ede9fe, #4c1d95);
  --intent-border: light-dark(#7c3aed, #a78bfa);
  --intent-fill-max: 100%;
}
```

```html
<span class="badge beta">Beta</span> <button class="btn beta" type="button">Try the new editor</button>
```

These values measure 5.7:1 for white on the fill, 7.1:1 for white on the hover color, 8.61:1 (light) and 10.72:1 (dark) for the strong tone on the page, and 7.57:1 and 5.93:1 for the strong tone on the subtle tint.

**Set every slot**, including `--intent-fill-max`. Each component resets all seven slots to the neutral values at its own root, so a slot you leave out keeps neutral's value, and without `--intent-fill-max: 100%` your intent fills only to 20%, like neutral. What each slot does is in [Intent tokens](#intent-tokens).

The class works on everything that takes an intent: buttons, badges, fields and toggles, solid and ghost cards, progress bars, skeletons, tooltip bubbles, and the rest. A few behaviors are written for the six built-in names, and a custom intent does not get them:

- **A quiet, untinted rest.** A neutral `.card.soft`, `.panel`, and `.alert` rest on the quiet background tone, and they recognize an intent only by the six built-in classes. `.card.soft.beta`, `.panel.beta`, and `.alert.beta` are drawn like the neutral one: the quiet background tone instead of a violet tint. Your intent still colors their text and border.
- **The chip tint.** A `.beta` key cap or code block shows your `--intent-subtle` tint, but not the extra 12% of `--intent-color` a built-in intent adds over it.
- **Solo classes.** A [solo class](docs/usage/solo.md) such as `.neobrutalism-solo` takes an intent only from the six built-in classes, so `.neobrutalism-solo.beta` is drawn neutral.

If you need those, there are two ways:

- **Retheme a built-in intent instead.** Set the `--color-info` family (or another one) to your colors, as in [Change the brand color](#change-the-brand-color), and write `.info`. Everything, including the cases above, then works. The cost is that the change reaches every `.info` inside the element you set it on, so set it on the narrowest element that works, and do not do it with an intent the page already uses for its own meaning.
- **Paint it yourself.** Write your own CSS for that one component, such as a `background-color` on `.panel.beta`.

> **Intent does not pass down.** Each component resets the intent slots at its own root, so a `.success` container does not recolor a destructive button inside it. Put the intent class on the element that shows the color.

## Color tokens

Every color token holds a light and a dark value through `light-dark()`; one value means both themes use it. Palette names such as `neutral-950` are Tailwind's default palette, which the package's compiled CSS declares for the colors it uses.

Each row is an override point: set it on `:root` to retheme the page, or on any element to retheme what is inside it. The `-hover`, `-subtle`, `-strong`, and `-contrast` rows are companions of their family's base color and do not follow it, so retheme the whole family. The `text` family has no `-subtle` row: the neutral intent takes its subtle tone from `--color-surface`.

| Token                          | Light, dark                  | Purpose                                                                                        |
| ------------------------------ | ---------------------------- | ---------------------------------------------------------------------------------------------- |
| `--color-primary`              | `neutral-950`, `neutral-50`  | Fill of `.primary`.                                                                            |
| `--color-primary-contrast`     | `neutral-50`, `neutral-950`  | Text and icons on a full primary fill.                                                         |
| `--color-primary-hover`        | `neutral-700`, `neutral-200` | Primary fill under the pointer.                                                                |
| `--color-primary-subtle`       | `neutral-200`, `neutral-900` | Primary tint behind soft components.                                                           |
| `--color-primary-strong`       | `neutral-900`, `neutral-100` | Primary text, icons, and borders where the fill is not full.                                   |
| `--color-accent`               | `neutral-600`, `neutral-400` | Fill of `.secondary`.                                                                          |
| `--color-accent-contrast`      | `neutral-50`, `neutral-950`  | Text and icons on a full secondary fill.                                                       |
| `--color-accent-hover`         | `neutral-700`, `neutral-300` | Secondary fill under the pointer.                                                              |
| `--color-accent-subtle`        | `neutral-100`, `neutral-800` | Secondary tint behind soft components.                                                         |
| `--color-accent-strong`        | `neutral-800`, `neutral-200` | Secondary text, icons, and borders where the fill is not full.                                 |
| `--color-border`               | `neutral-400`, `neutral-700` | Border of neutral surfaces, tables, dividers, and key caps.                                    |
| `--color-border-hover`         | `neutral-600`, `neutral-500` | Border under the pointer.                                                                      |
| `--color-control-border`       | `neutral-500`                | Resting border of neutral text controls and toggles, drawn at a fraction on text controls.     |
| `--color-background`           | `neutral-50`, `neutral-950`  | The page.                                                                                      |
| `--color-foreground`           | `neutral-100`, `neutral-900` | The quiet background tone one step off the page: neutral soft cards, panels, and alerts.       |
| `--color-surface`              | `neutral-200`, `neutral-800` | A further step: nested and muted surfaces, and the neutral intent's tint.                      |
| `--color-text`                 | `neutral-950`, `neutral-50`  | Body text, and the neutral intent's color.                                                     |
| `--color-text-secondary`       | `neutral-700`, `neutral-400` | Secondary text, such as `.hint`.                                                               |
| `--color-text-contrast`        | `neutral-50`, `neutral-950`  | Text on a full fill of `--color-text`, such as the mark of a checked neutral toggle.           |
| `--color-text-hover`           | `neutral-700`, `neutral-200` | Neutral color under the pointer.                                                               |
| `--color-text-strong`          | `neutral-900`, `neutral-100` | Neutral text, icons, and borders where the fill is not full.                                   |
| `--color-tooltip`              | `white`, `neutral-700`       | Background of a tooltip bubble with no intent. Chosen per theme so it reads as lifted in both. |
| `--color-tooltip-contrast`     | `neutral-900`, `neutral-100` | Text on `--color-tooltip`.                                                                     |
| `--color-success`              | `emerald-700`                | Fill of `.success`.                                                                            |
| `--color-success-contrast`     | `neutral-50`                 | Text on a full success fill: 5.14:1.                                                           |
| `--color-success-hover`        | `#007552`, `#008e66`         | Success fill under the pointer.                                                                |
| `--color-success-subtle`       | `emerald-100`, `emerald-900` | Success tint behind soft components.                                                           |
| `--color-success-strong`       | `emerald-800`, `emerald-300` | Success text, icons, and borders where the fill is not full.                                   |
| `--color-warning`              | `amber-600`                  | Fill of `.warning`.                                                                            |
| `--color-warning-contrast`     | `neutral-950`                | Text on a full warning fill: near-black, not the page color. 6.19:1.                           |
| `--color-warning-hover`        | `#d46700`, `#e78300`         | Warning fill under the pointer.                                                                |
| `--color-warning-subtle`       | `amber-100`, `amber-900`     | Warning tint behind soft components.                                                           |
| `--color-warning-strong`       | `amber-800`, `amber-300`     | Warning text, icons, and borders where the fill is not full.                                   |
| `--color-destructive`          | `rose-700`                   | Fill of `.destructive`.                                                                        |
| `--color-destructive-contrast` | `neutral-50`                 | Text on a full destructive fill: 5.78:1.                                                       |
| `--color-destructive-hover`    | `#c00036`, `#d1054b`         | Destructive fill under the pointer.                                                            |
| `--color-destructive-subtle`   | `rose-100`, `rose-900`       | Destructive tint behind soft components.                                                       |
| `--color-destructive-strong`   | `rose-800`, `rose-300`       | Destructive text, icons, and borders where the fill is not full.                               |
| `--color-info`                 | `indigo-600`                 | Fill of `.info`.                                                                               |
| `--color-info-contrast`        | `neutral-50`                 | Text on a full info fill: 6.19:1.                                                              |
| `--color-info-hover`           | `#4b36e9`, `#5e4ff9`         | Info fill under the pointer.                                                                   |
| `--color-info-subtle`          | `indigo-100`, `indigo-900`   | Info tint behind soft components.                                                              |
| `--color-info-strong`          | `indigo-800`, `indigo-300`   | Info text, icons, and borders where the fill is not full.                                      |

Theme changes belong in these values, not in component rules that check the theme.

## Intent tokens

An intent class sets these seven slots, and components read them. Because every component reads the same seven names, a class that sets all seven works as an intent, as [Your own intent](#your-own-intent) shows.

| Token               | Purpose                                                                                                                                                                                                                       |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--intent-color`    | The intent's base color, what a fill is made of.                                                                                                                                                                              |
| `--intent-contrast` | Text on a full fill of `--intent-color`.                                                                                                                                                                                      |
| `--intent-hover`    | The base color under the pointer.                                                                                                                                                                                             |
| `--intent-strong`   | The intent as text, icon, or border wherever the fill is not full.                                                                                                                                                            |
| `--intent-subtle`   | The light tint behind soft components and progress tracks. Neutral uses `--color-surface`.                                                                                                                                    |
| `--intent-border`   | Line color. With no intent it is the aesthetic's `--ui-ink`, then `--color-border`; text controls and toggles read `--ui-control-ink`, then `--ui-ink`, then `--color-control-border`, and tables read `--ui-rule-ink` first. |
| `--intent-fill-max` | How far a fill of this intent may go. `100%` for every intent except neutral.                                                                                                                                                 |

`--intent-color` and `--intent-strong` are not interchangeable. The base color is chosen to be filled with; the strong tone is chosen to be read against the page. A soft or ghost component prints the strong tone, which is why a soft warning reads in amber-800 while a solid one is filled with amber-600.

The strong tone also has to stay recognizably its own hue. On a dark page, the `-100` shade of every family is a pale tint with almost no color at small sizes: a soft success badge and a soft destructive one would both read as white. The dark strong tones are `-300` for that reason, and the tints under them are `-900`, which measures 5.0:1 or better wherever a partial fill holds text, and over 9:1 on the page.

`--intent-border` exists so a neutral border stays the quiet border gray rather than the text color. Everything that draws a line reads it: control borders, table rules, dividers, key caps, and quote bars.

`--intent-fill-max` exists for the neutral intent, whose color is the page's own text color. Every other intent is a hue, readable as a pale tint at 12% and saturated at 100%; a gray filled to 100% is a black or white block, the loudest thing on the page for the intent that means nothing in particular. Neutral stops at `20%`, so `.solid` gives it a quiet gray that is still a step past `.soft`. Hover adds its step on top of the cap, so a capped fill still deepens under the pointer.

The text color follows the fill. `--intent-contrast` is the text color for a full fill; printed on a fifth of one, it would be white on light gray. So contrast text appears only once the fill is past halfway, and reaches full strength at a full fill; below that, a component prints `--intent-strong`. A capped neutral therefore prints the strong tone. A state that lifts the cap on purpose, such as a checked checkbox, reaches a full fill and prints the contrast color.

## Foundation tokens

Foundation tokens set layout, shape, motion, focus, depth, and stacking. Override them on `:root`, or on an element for what is inside it.

The four `--elevation-*` shadows are for elements you style yourself. They are separate from the `.flat`, `.raised`, and `.floating` classes, which scale a component's own shadow and draw nothing until an aesthetic or one of those classes asks for depth; see [Composing → Elevation](docs/usage/composing.md#elevation). Both use the weights none, small, medium, and large, but one is a fixed value and the other scales shadow parts, so they do not always look the same.

| Token                      | Default                                                | Purpose                                                                                                         |
| -------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| `--font-default`           | `"Segoe UI", system-ui, sans-serif`                    | Page font. See [Fonts](#fonts).                                                                                 |
| `--container-narrow`       | `48rem`                                                | Width of `.section-content.narrow`.                                                                             |
| `--container-max`          | `80rem`                                                | Width of `.section-content`.                                                                                    |
| `--container-wide`         | `90rem`                                                | Width of `.section-content.wide`.                                                                               |
| `--layout-gutter`          | `clamp(1rem, 4vw, 2rem)`                               | Side padding of `.section`, growing with the window.                                                            |
| `--layout-section-block`   | `clamp(4rem, 9vw, 7rem)`                               | Top and bottom padding of `.section`, growing with the window.                                                  |
| `--layout-gap`             | `1rem`                                                 | Gap of `.view`, `.stack`, `.cluster`, and `.auto-grid`.                                                         |
| `--layout-grid-min`        | `16rem`                                                | Narrowest column of `.auto-grid`.                                                                               |
| `--radius-small`           | `0.25rem`                                              | Corner of small chips: `.kbd` and `.code`.                                                                      |
| `--radius-control`         | `0.5rem`                                               | Corner of buttons, fields, and tooltip bubbles.                                                                 |
| `--radius-surface`         | `0.875rem`                                             | Corner of cards, panels, and alerts.                                                                            |
| `--control-height`         | `2.5rem`                                               | Minimum height of a default button and a text control.                                                          |
| `--border-width`           | `1px`                                                  | Border width.                                                                                                   |
| `--elevation-color`        | `rgb(15 23 42 / 0.08)` light, `rgb(0 0 0 / 0.55)` dark | The shadow color the four elevations use. Heavier on a dark page, where a light shadow would not show.          |
| `--elevation-none`         | `none`                                                 | No shadow.                                                                                                      |
| `--elevation-sm`           | `0 1px 2px`, `0 1px 3px`                               | A light shadow for your own surfaces, such as a card.                                                           |
| `--elevation-md`           | `0 4px 6px -1px`, `0 2px 4px -2px`                     | A shadow for your own raised elements, such as a dropdown menu.                                                 |
| `--elevation-lg`           | `0 20px 25px -5px`, `0 8px 10px -6px`                  | A shadow for your own overlays, such as a modal dialog.                                                         |
| `--focus-ring`             | `--color-primary` at 64% opacity                       | Color of the keyboard focus ring.                                                                               |
| `--focus-ring-offset`      | Not set                                                | Distance of the ring from the element. When unset, the ring sits on the element's border under every aesthetic. |
| `--focus-ring-width`       | `3px`                                                  | Width of the keyboard focus ring.                                                                               |
| `--motion-duration-fast`   | `120ms`                                                | A table row's hover.                                                                                            |
| `--motion-duration-normal` | `200ms`                                                | Color, border, shadow, and press transitions, toggle marks, and the tooltip bubble.                             |
| `--motion-duration-slow`   | `400ms`                                                | Progress value changes, and the base of the skeleton and progress shimmer cycle.                                |
| `--motion-ease`            | `cubic-bezier(0.2, 0, 0, 1)`                           | Easing for every transition.                                                                                    |
| `--z-popover`              | `40`                                                   | Stacking order of tooltip bubbles. Use it for your own popovers so they stack the same way.                     |
| `--breakpoint-xs`          | `30rem`                                                | The `xs:` responsive breakpoint. Only in a `/tw` build; the compiled entrypoints do not declare it.             |

Each `--elevation-*` shadow is made of the listed parts, each in `--elevation-color`. The hover step that deepens a fill under the pointer is a presentation token, `--hover-step`; see [Customizing](docs/usage/customizing.md#presentation-tokens).

## Fonts

The package ships no font files and makes no network requests. Every font token names fonts already installed on the reader's device until you supply your own.

| Token            | Read by                                                                                          | Without your value                                                                                                                              |
| ---------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `--font-default` | The reset, on `html` and `body`. Buttons and form controls inherit it.                           | `"Segoe UI", system-ui, sans-serif`                                                                                                             |
| `--font-mono`    | The `kbd`, `code`, and `pre` content helpers, and `<kbd>`, `<code>`, and `<pre>` under `/native` | `ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace`. Under `/tw`, your Tailwind theme's value. |
| `--font-pixel`   | [`.pixel`](docs/usage/aesthetics.md#pixel) and `.pixel-solo`                                              | Not declared. Falls back to a monospace stack.                                                                                                  |
| `--font-rounded` | [`.chunky-tile`](docs/usage/aesthetics.md#chunky-tile) and `.chunky-tile-solo`                            | Not declared. Falls back to `--font-default`, then a sans-serif stack.                                                                          |
| `--font-cyber`   | [`.cyber`](docs/usage/aesthetics.md#cyber) and `.cyber-solo`                                              | Not declared. Falls back to a monospace stack.                                                                                                  |
| `--font-sketch`  | [`.sketch`](docs/usage/aesthetics.md#sketch) and `.sketch-solo`                                           | Not declared. Falls back to the platform `cursive` family.                                                                                      |

`--font-default` reaches the page only through the reset, which `@codenhub/styles`, `/native`, `/tw`, `/tw/native`, and `/tw/reset` include. With `/theme` or `/components` alone, set `font-family` on `body` yourself. The aesthetic tokens are read by the aesthetic's own class, so they work with any entrypoint.

To use a web font, load it the way you load any font, then name it in the token. Set the token on `:root` for the whole page, or on any element for that part of it:

```html
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400..700&display=swap" />
```

```css
:root {
  --font-default: "Inter", system-ui, sans-serif;
}
```

A self-hosted font is declared with `@font-face` and named the same way:

```css
@font-face {
  font-family: "Pixelify Sans";
  src: url("/fonts/pixelify-sans.woff2") format("woff2");
  font-weight: 400 700;
  font-display: swap;
}

:root {
  --font-pixel: "Pixelify Sans", monospace;
}
```

Load weights 400 to 700, and 800 for `--font-rounded`: components set `font-weight` from 400 to 700, and `.chunky-tile` sets its labels in 800. A browser that lacks a weight fakes it by thickening the regular one, which smears a pixel or handwriting face. Always end the stack with a generic family, such as `sans-serif` or `monospace`, so text still renders while the font loads or if it fails to.

## Next

The screen has your colors in both themes. [Aesthetics](docs/usage/aesthetics.md) changes what it is made of: thick black outlines, frosted glass, pixels, and more.

<!-- Source: docs/usage/aesthetics.md -->

# Aesthetics

The settings screen has your colors. An aesthetic changes what its components are made of: the corners, the thickness of the borders, the shadow, and the shape. Put `.neobrutalism` on the `<main>` of the settings screen and every card, button, field, and badge in it gets square corners, a thick outline in the ink color, and a hard offset shadow, while keeping its colors. This page shows how to add one, then describes each of the six: what it changes, the number you can tune, and where it behaves differently from the default look.

## Add an aesthetic

Aesthetics ship in a separate entrypoint, so load it next to the stylesheet:

```css
@import "@codenhub/styles";
@import "@codenhub/styles/aesthetics";
```

Load order does not matter: the aesthetic classes sit in the `components` cascade layer, and every entrypoint declares the layer order before it uses one, so an aesthetic imported before the base stylesheet wins exactly as one imported after it. See [Setup → Cascade layers](docs/setup.md#cascade-layers).

Then put the class on any element. Unlike intent, an aesthetic passes down: everything inside the element takes it.

```html
<main class="neobrutalism stack">
  <section class="card stack" aria-labelledby="profile-title">…</section>
  <section class="card soft destructive stack" aria-labelledby="danger-title">
    …
    <button class="btn destructive" type="button">Delete account</button>
  </section>
</main>
```

The danger zone keeps its red: under neobrutalism its outline and shadow are red too, because this aesthetic draws them in the component's own intent. To load one aesthetic instead of all six, import it by name, such as `@codenhub/styles/aesthetics/neobrutalism`; [Setup → All entrypoints](docs/setup.md#all-entrypoints) lists them.

| Class           | Look                                                                                                                            |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `.neobrutalism` | No radius, a thick ink outline, a hard unblurred offset shadow, and a press that moves the element into it.                     |
| `.glass`        | Translucent surfaces over a blurred backdrop with a hairline highlight edge.                                                    |
| `.pixel`        | Corners cut by one grid unit, a chunky outline drawn as an inset ring, and the consumer-supplied `--font-pixel` over monospace. |
| `.chunky-tile`  | Rounded tiles seated on a darker bar of their own color, with a press that moves the element down into its own bar.             |
| `.cyber`        | Beveled corners, a thin bright edge, and a glow in the component's own color.                                                   |
| `.sketch`       | Varying elliptical corners, a solid ink line with dashed accents, a small offset shadow, and an optional handwriting face.      |

Aesthetics work with every fill and edge combination. `.ghost.edgeless` removes the fill and the border, so on a component whose aesthetic shows only through those and its shadow, little of the aesthetic is left; that is intended.

A presentation class on the element still wins over the aesthetic's defaults. The aesthetic sets how thick the border is; presentation decides whether it is drawn.

An aesthetic class on a component wins over one inherited from a container. This includes tooltip bubbles: `.tooltip-bubble.glass` gets the complete glass bubble under a pixel ancestor, and `.tooltip-bubble.pixel` gets the complete stepped bubble under a glass ancestor.

Your own CSS wins over an aesthetic. The aesthetic classes sit in the `components` cascade layer, so a rule in your own unlayered stylesheet beats them, and so does a Tailwind utility on the same element, such as `font-mono` on a `.pixel` region or `[--ui-radius:0]` on a `.cyber` one. See [Setup → Cascade layers](docs/setup.md#cascade-layers).

Most aesthetics scale from one number you can set, such as the offset of the neobrutalism shadow or the size of a pixel. These are the aesthetic's knobs, and each section below names its own. [Customizing → Aesthetic tokens](docs/usage/customizing.md#aesthetic-tokens) explains where they can be set.

To paint an aesthetic onto an element from another library, such as a toast or a dialog, use its [solo class](docs/usage/solo.md).

## Neobrutalism

The shadow is cast in the component's own intent, so a success button throws a green shadow and a destructive card a red one. With no intent, the outline and the shadow are the ink: near-black on a light page and near-white on a dark one. Fields and toggles are the exception. They draw the theme's control border, as they do with no aesthetic, because a resting checkbox outlined in near-black would look as loud as a checked one. The same holds under pixel, cyber, and sketch.

`--neo-offset` is the knob: the shadow offset and the distance the press travels are the same number, so scaling the look scales both at once.

```html
<section class="neobrutalism" style="--neo-offset: 6px">
  <button class="btn primary">A deeper shadow</button>
</section>
```

**Exceptions:**

- Content chips take the aesthetic too. A `.kbd` key cap's border changes from the quiet border gray to the ink, and from 1px to 2px, like every other border. A `.code` or `.pre` has no border by default, so it gets none; add `.edged` to give one an ink border.
- Only `.btn` and an opted-in `.card` respond to hover and press. A card opts in with `.interactive`, or with one of its halves — `.hoverable` for the hover response alone, `.pressable` for the press alone; a plain card, an alert, or a panel is a container and stays put.
- An alert sits on the offset shadow under this aesthetic, and under no other. An alert is otherwise flat, but here the shadow is a second outline rather than depth, and an alert without it looks unfinished next to everything else. This includes an alert inside a nested `.neobrutalism` region. `.flat` still removes it and `.floating` still doubles it.
- The tooltip trigger takes the zero corner like everything else and draws square; `.radio` keeps its circle.

## Glass

`.glass` needs something behind it to blur. On a flat page background it renders as a plain translucent panel.

`--glass-radius` and `--glass-radius-surface` are the corners, at `0.75rem` and `1rem`. Glass is rounder than the base geometry because a translucent panel with a tight corner reads as a cut-out rather than as a pane.

```html
<section class="glass" style="--glass-radius-surface: 1.5rem">
  <div class="card">Softer</div>
</section>
```

**Exceptions:**

- Every surface in a glass region is glass, a neutral `.card.soft`, `.panel`, and `.alert` included.
- The glass is the ground, not the fill. A presentation keeps its whole fill, so a `.solid` button, a checked toggle, and a `.solid` card or table are opaque; the default, `.soft`, and `.ghost` panes are the frosted ones.
- The blur applies to `.card`, `.panel`, `.alert`, the tooltip bubble, and a default or `.soft` `.data-table` only. Controls stay solid and sit on the glass: a blur under every control of a dense cluster costs a composited layer apiece and reads as noise. Controls still take the aesthetic's radius, border, and shadow.
- Under `prefers-reduced-transparency: reduce`, glass surfaces drop the blur and become opaque.
- Buttons and an opted-in `.card` press with the plain `scale(0.97)`; reduced motion turns it off.
- Text controls and toggles draw their line in the page's ink at 55% rather than the white hairline, which would erase an unchecked checkbox on a light page. It is `--ui-control-ink`; set it on the region to choose another.
- A table's frame and rules draw the page's ink at 20%, because the hairline vanishes on the table's plate. It is `--ui-rule-ink`.
- A table with a `<caption>` blurs behind the caption as well as its rows in Chromium and Safari, whose table box includes the caption, and under `.glass-liquid` the rim and sheen frame the caption too. Firefox keeps the caption off the pane. Label the table from a heading outside it with `aria-labelledby` where that matters.

### Liquid glass

`.glass-liquid` is a modifier of glass, after Apple's Liquid Glass: continuous corners, a bright rim where light catches the pane, a lighter blur, a clearer ground, and a lens that bends what is behind a surface at its edges. Put it beside `.glass` on the region, or on one surface inside a glass region.

```html
<section class="glass glass-liquid">
  <div class="card">A pane that bends the page behind it</div>
</section>
```

The lens is drawn by an SVG filter in `backdrop-filter`, which Chromium renders and Firefox and Safari do not yet. Elsewhere the pane keeps everything but the lens: the rim, the blur, the ground, and the corners, round rather than continuous where `corner-shape` is not drawn. Nothing needs to be in your markup: the filter ships inside the stylesheet.

**Exceptions:**

- It reaches `.card`, `.panel`, `.alert`, the tooltip bubble, and a default or `.soft` `.data-table`. Controls stay solid, as they do under glass, except a button that asks: see [Liquid buttons](#liquid-buttons).
- It draws its blur and lens on each surface's `::before` and `::after`, so a surface inside a liquid region cannot carry a `::before` or `::after` of your own. Each surface also becomes a positioned stacking context; a surface you position yourself keeps your placement.
- Anything else that frosts under glass keeps liquid's light blur and its clearer ground, without the lens or the rim: a bare `<table>` under `/native`, or a component of your own built with `@apply surface`.
- The lens is 8px of erosion blurred over 10px on every surface, and 3px blurred over 3.5px on a button; its strength is fixed. A filter cannot read a custom property, so neither is a knob. The corners are, through `--glass-radius` and `--glass-radius-surface`, with larger defaults: a continuous corner reads smaller than a round one of the same radius.
- A region of another aesthetic nested inside a liquid region, or of plain `.glass`, keeps its own material and takes no layers.
- Without `.glass` in scope it changes the corners and nothing else.
- Under `prefers-reduced-transparency: reduce` the blur and the lens go and the surface is opaque, as under glass. In forced colors the layers draw nothing.
- Text sits over a clearer pane than under glass. Over busy content, check that your text still reads: the ground is 30% of the page color, where glass's is 45%.

#### Liquid buttons

A button is a liquid pane when it carries `.glass-liquid` itself, inside a glass region. The modifier on the region alone leaves every button solid: a blur and a lens under each control of a dense toolbar cost two composited layers apiece and read as noise, so the one button that is meant to be glass says so.

```html
<section class="glass glass-liquid">
  <button class="btn primary">Solid, like every control</button>
  <button class="btn ghost glass-liquid">A clear pane</button>
  <button class="btn soft success glass-liquid">A tinted pane</button>
</section>
```

- The fill is still [presentation's](docs/usage/composing.md#presentation). `.ghost` is the clear pane and `.soft` the tinted one; a `.solid` button keeps its opaque plate and takes the rim and sheen over it. Intent, size, `.pill`, `.icon`, hover, and press all work as on any button.
- It works in a plain `.glass` region too, and on a button inside a liquid card.
- It spends the button's `::before` and `::after`, so a liquid button cannot carry one of your own.
- A disabled liquid button keeps the ground and the rim and loses the blur and the lens: it is dimmed with `opacity`, which cuts its layers off from the page behind it.
- The label sits over whatever is behind the button. Over busy content, check that it still reads, or use `.solid`.
- It reaches `.btn`, and a bare `<button>` only through its `.btn` class: the layers are a list of classes, which `/native`'s element mapping does not join.

## Pixel

`--pixel-unit` is one pixel of the imaginary low-resolution grid, `4px` by default. One square unit is cut from each corner and the outline is one unit thick, so the cut and what covers it are the same size.

```html
<section class="pixel" style="--pixel-unit: 6px">
  <button class="btn primary">Chunkier</button>
</section>
```

**Exceptions:**

- Corners are one unit or nothing. Small chips are square instead of stepped: badges, key caps, inline code, checkboxes, and switches. One unit off each corner of a 24px badge would look like a bite, not a corner.
- Tables, progress bars, and skeletons are square too. A table takes no cut because in Chromium and WebKit its box includes its caption, and the cut would land on the caption instead of the rows. Progress bars and skeletons take only the square corner.
- A `.pre` code block is stepped like a card. It has no border by default, so the cut shows with no outline around it.
- The outline is an inset ring, because a clip removes a real border — and the focus ring, for the same reason, is a second inset layer rather than an outline. Both are the element's own edge rather than a shadow, so the border answers `.edged` and `.edgeless` the way a border does: a `.edgeless` badge and a `.solid` button draw none, a `.edged` card draws one, and a field keeps one whatever a container asks for.
- The inset focus ring carries a 2px line of the page color inside it. The ring is the primary color, so on a `.solid.primary` button it would be drawn over its own color; the line is what shows there.
- In forced colors the ring and the cut both go, because the system removes every box shadow: components draw a real 2px border and square corners, and `.pixel-solo` an inward outline.
- `.radio` and `.tooltip-icon` are never stepped. The circle is what tells a radio from a checkbox at a glance, so a radio stays round. A stepped 20px tooltip trigger would look bitten, so it is a plain square.
- `--font-pixel` is yours to supply. The package ships no font binary, so the aesthetic has no network side effect and falls back to the monospace stack.
- Whether digits are easy to read depends on the font you pick. At small sizes, such as page numbers, counters, and badges, some pixel fonts draw `2` and `8`, or `3` and `9`, so alike that they are misread. The CSS cannot fix that. Where small numbers matter, choose a font with clearly different digits.

## Chunky tile

Rounded tiles seated on a darker bar of their own color, pressed flat on click. The look mobile learning and game apps use; it is named for what it is made of rather than for any one product.

`--tile-lift` is how deep the bar is and how far a press travels, `4px` by default. One number drives both, so the top edge drops by exactly the bar's depth and the bottom edge does not move. `--tile-radius` is the corner, `0.75rem`, and it is one value rather than a control/surface pair on purpose — the shared corner is what makes a button and the card it sits in read as the same object.

```html
<section class="chunky-tile" style="--tile-lift: 6px; --tile-radius: 0.5rem">
  <button class="btn success">Continue</button>
</section>
```

The bar is a darker shade of the element's own color rather than a shadow under it, so it follows every intent: a success button sits on a dark green edge, a destructive one on a dark red edge, and a plain card on a gray edge that matches the line it already draws. Measured on the light palette, `.btn.success` is `#007a55` on `#004c34`. On a dark page a near-black surface cannot go darker, so its bar lands lighter and reads as a rim.

**Exceptions:**

- Only what the registry rests above zero sits on a bar — buttons and cards. A badge, chip, field, or tooltip is flat until it asks for depth with `.raised`, and `.flat` takes the bar off anything:

  ```html
  <div class="card soft edgeless info flat">Promo panel, deliberately flat</div>
  ```

- `--font-rounded` is yours to supply. A heavy rounded grotesque is most of this look and the package ships no font binary, so with nothing supplied the page's own stack is used.
- Labels are drawn heavier and slightly tracked, through `--ui-label-weight` and `--ui-label-tracking`, so your own `font-*` or `tracking-*` utility still wins. It reaches `.btn` and `.badge`, and a bare `<button>` only where `/native` styles it. Casing is left alone: how a label is worded is the application's decision, so if you want uppercase buttons, write that rule in your own stylesheet.
- `.card.interactive` presses like a `.btn`, so a chunky answer tile drops onto its bar on click; `.card.pressable` gets the same press on its own. A plain `.card` stays put.
- The shipped `.primary` is near-black on a light page and near-white on a dark one, and the bar is always mixed toward black. On a light page, a primary button's bar is almost invisible against its near-black face (measured `1.04:1`). On a dark page, the near-white face against the same bar reads well (`2.48:1`). Give `.primary` a hue of its own if it is the call to action and needs the depth cue in both themes.
- Depth in this aesthetic is the bar, never a blurred drop shadow. Anything the registry rests flat sits flush on the page.
- Controls draw a heavier gray than the tiles around them, `neutral-600` in light and `neutral-400` in dark, through `--ui-control-ink`, so a field reads as a field inside a tile.

## Cyber

Beveled corners, a 1px edge, and a glow in the component's own color: a success button glows green, a destructive card red. The bevel is drawn with `corner-shape`, so the border, the glow, and the focus outline all follow the cut rather than being clipped by it.

Controls and surfaces take the cut on opposite diagonals, so a button and the card around it read as the same material and as different things: buttons and fields cut top-left and bottom-right, cards, panels, and alerts top-right and bottom-left. Anything fully round cuts to points: a radio, a switch knob, and the tooltip icon become diamonds, and a pill button or a badge becomes a pointed hexagon. Chips go square, so the checkbox reads as a plain square beside the diamond radio.

`--cyber-cut` is the cut at the default size, `0.625rem`. Controls scale it with their size step, so a `.p-xs` icon button takes half of it and keeps its corners, and it stays a true 45 degrees. `--cyber-shape` and `--cyber-shape-surface` replace the default placement with any `border-radius` value, used as written. `--cyber-glow` is the glow's blur, `8px`. `--cyber-ink` is the neutral line and glow color. By default it follows the theme, near-black on a light page and near-white on a dark one; set it for a neon neutral.

```html
<section class="cyber" style="--cyber-ink: rgb(0 229 255); --cyber-cut: 0.75rem">
  <button class="btn">Open terminal</button>
</section>
```

Other shapes are a value away. Set them on the `.cyber` element or any ancestor. `--ui-radius` set on an element reaches that element and every component inside it that does not set its own. A shape you set is used as written and does not scale, so for a scaled one on a single control use `.cut-diagonal-reverse` instead.

| Shape                  | Value                 |
| ---------------------- | --------------------- |
| All four corners       | `0.625rem`            |
| The other diagonal     | `0 0.625rem`          |
| Hexagon (pointed ends) | `0.625rem / 50%`      |
| Parallelogram          | `0.625rem 0 / 100% 0` |
| One notch, top-right   | `0 0.625rem 0 0`      |

```html
<section class="cyber" style="--cyber-shape: 0.625rem / 50%">
  <button class="btn primary">Hexagonal buttons</button>
</section>
```

A parallelogram slants its sides into the content, so give a surface that takes one extra inline padding.

**Exceptions:**

- The bevel is Chromium-only today. Firefox and Safari do not draw `corner-shape` yet, and there the aesthetic squares its cut corners instead of rounding them, and what is fully round stays round, so the look degrades to square, lined, and glowing. It picks up the bevel with no change once an engine ships it.
- The glow is a halo, not depth, so every component takes it, fields, badges, and alerts included, and `.flat` does not take it off. `.raised` and `.floating` add no shadow under this aesthetic.
- In the light theme the glow of a neutral or `.primary` component is its near-black ink, which reads as a soft shadow rather than as light. The look is at its strongest on a dark page, or with a hued `--cyber-ink`.
- A plain `.btn.icon` is a square control, so it takes the control diagonal; a `.btn.icon.pill` is round, so it becomes a diamond.
- `--font-cyber` is yours to supply. The package ships no font binary, so the aesthetic falls back to the monospace stack.
- Casing is left alone, for the reason [Chunky tile](#chunky-tile) gives.

## Sketch

Hand-drawn outlines: controls take long, uneven elliptical corners, and cards, panels, alerts, and tables take sharper ones that stay clear of their padding. Siblings rotate through four outlines by position, and a field's control takes its field's position, so a row of buttons or a stacked form does not repeat one shape. The rotation is stable for a given DOM order, not random on each render. Every outline is a complete `border-radius`, used as written rather than scaled by the size step or placed by `.cut-diagonal`.

The 1px ink line is solid; checkboxes, dividers, and table rules are dashed. `--ui-line-style: dashed` dashes any other component, and `--ui-rule-style: solid` puts a divider or table back to solid. The 2px hard shadow is a small neutral depth cue.

`--sketch-radius`, `--sketch-radius-surface`, and `--sketch-radius-pill` replace the control, surface, and pill-button outlines. Set one and every position takes that one outline. To shape a single component instead, set `--ui-radius` (`--ui-radius-surface` on a surface) on it.

```html
<section class="sketch" style='--font-sketch: "Comic Sans MS", cursive'>
  <button class="btn primary">Send note</button>
  <button class="btn ghost edged" style="--ui-line-style: dashed">Draft note</button>
</section>
```

The dashed button is `ghost edged` because a filled button has no separate line to dash.

`.sketch-rounded` trades the long corners for short, rounded ones, bounded so a wide element keeps straight top and bottom edges. Put it beside `.sketch` on the region, or on one component inside it. `--sketch-radius-rounded` and `--sketch-radius-surface-rounded` are its knobs.

```html
<section class="sketch sketch-rounded">
  <div class="card">
    <button class="btn primary">Rounded action</button>
  </div>
</section>
```

**Exceptions:**

- Sketch names components to rotate their outlines, so its outlines reach only this package's components; a region of another aesthetic nested inside a sketch region keeps that aesthetic's corners.
- Chips, such as checkboxes, key caps, code, badges, switches, and progress bars, rotate through small uneven outlines set in `--ui-radius-tight`. Set that token on a chip to choose its shape.
- A `.btn.pill` takes a rounded pill bounded to its own height, so a wide one does not become an oval. Badges, progress bars, and switches take their chip outline. Radios and the tooltip icon stay circles, slightly uneven, so a radio is never mistaken for a checkbox. An explicit `--ui-radius-pill` on an element still wins.
- `--font-sketch` is yours to supply. The package ships no handwriting font and falls back to the platform `cursive` family.
- Buttons press with the plain `scale(0.97)`; reduced motion turns it off.
- Forced-colors mode draws the dashed accents solid, like every other material line.

## Solo classes

An aesthetic class changes nothing on an element this package does not style, such as a toast from another library. Each aesthetic also ships a solo class, such as `.glass-solo`, that paints its look onto that one element. [Solo classes](docs/usage/solo.md) covers them.

## Next

[Solo classes](docs/usage/solo.md) applies these looks to components from other libraries. If every component on your screen comes from this package, the guides end here: [Composing](docs/usage/composing.md), [Customizing](docs/usage/customizing.md), and [Content and layout](docs/usage/content-and-layout.md) are the lookup pages, and [Accessibility](docs/accessibility.md) lists what your application still owes its users.

<!-- Source: docs/usage/solo.md -->

# Solo classes

Suppose the settings screen shows "Profile saved" in a toast from a notification library, not in this package's alert. Under `.neobrutalism`, every card and button has the thick outline and the hard shadow, and the toast does not: an aesthetic class only sets values that this package's components read, so it changes nothing on an element the package does not style. A solo class paints the aesthetic directly onto the one element that carries it:

```html
<div class="toast neobrutalism-solo success" role="status">Profile saved.</div>
```

## The six solo classes

Each aesthetic ships its solo class in the same entrypoint, and a solo class needs nothing else loaded:

```css
/* The only stylesheet from this package on the page. */
@import "@codenhub/styles/aesthetics/glass";
```

```html
<div class="toast glass-solo">A pane over whatever is behind it.</div>
```

| Aesthetic       | Solo class           | Paints                                                                                                                                                                                                           |
| --------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `.glass`        | `.glass-solo`        | A translucent background, a thin light edge, the blur, a soft two-layer shadow, and the surface corner. With `.glass-liquid`, the liquid pane, drawn on its `::before` and `::after`.                            |
| `.neobrutalism` | `.neobrutalism-solo` | A 2px ink edge, square corners, and the hard offset shadow.                                                                                                                                                      |
| `.pixel`        | `.pixel-solo`        | The stepped outline, the inner ring in place of a border, and the pixel font.                                                                                                                                    |
| `.chunky-tile`  | `.chunky-tile-solo`  | The tile corner, a 2px edge, the bar under it, and the rounded font.                                                                                                                                             |
| `.cyber`        | `.cyber-solo`        | The surface diagonal (the control diagonal on a button or link), a 1px edge, the glow, and the font.                                                                                                             |
| `.sketch`       | `.sketch-solo`       | Uneven elliptical corners (a sharper outline on a pane, a looser one on a button or link, and small rounded ones with `.sketch-rounded`), a 1px solid ink line, the 2px offset shadow, and the handwriting font. |

Do not put a solo class on this package's own components. A `.card` already draws its fill, edge, and intent, and a solo class would paint a second edge and fill over them. Use the aesthetic class there; it also works on a single element: `<button class="btn pixel">`.

## What a solo class reads

A solo element takes the same presentation, intent, and elevation classes a `.card` would:

```html
<div class="toast pixel-solo edgeless">No ring</div>
<div class="toast glass-solo solid success">An opaque green pane</div>
<div class="toast neobrutalism-solo flat">No offset shadow</div>

<section class="soft edged">
  <div class="toast chunky-tile-solo info">Soft and edged from the container, info from itself</div>
</section>
```

| What                                  | Read from                                                                                           |
| ------------------------------------- | --------------------------------------------------------------------------------------------------- |
| Fill and edge (`.solid`, `.edged`, …) | The element, or any container: presentation passes down, as it does to components.                  |
| Intent (`.success`, …)                | The element only, as on components. A `.success` container leaves a solo element inside it neutral. |
| Elevation (`.flat`, `.raised`, …)     | The element only, as on components. It scales the offset shadow, the bar, and sketch's shadow.      |
| The aesthetic around it (`.pixel`, …) | Never. See [What a solo class ignores](#what-a-solo-class-ignores).                                 |

With none of those classes, a solo class paints the material alone (edge, corner, depth, outline, blur, font, and press) and leaves the element's own background and text color as they were.

- **Only the six built-in intents.** A solo class takes an intent from `.primary`, `.secondary`, `.success`, `.warning`, `.destructive`, and `.info` only. An intent class you defined yourself, as in [Theming → Your own intent](docs/usage/theming.md#your-own-intent), is not read, and the element is drawn neutral. To give a solo element a custom color, retheme one of the built-in intents on it or a container, as that section describes.
- **The fill is a layer over the element's own background.** Five of the six paint the fill as an inner shadow and do not set `background`, so a `.soft` solo toast is a tint over whatever the toast already was, and a `.solid` one with a colored intent is opaque. Glass sets its own background, because the translucent background is its material, and mixes the fill over it. A neutral `.solid` is a 20% tint, not the opaque gray a component draws.
- **The text color assumes the theme.** It is set only where a fill or an intent is asked for, and it is the color the theme uses on the page or on the fill. On an element whose own background does not follow the theme, such as a white toast on a dark page, a `.soft` or `.ghost` intent can put light text on a light background; use `.solid`, or leave the intent off.
- **There is no hover tint.** A solo element is not known to be clickable, so it does not get the hover color a `.btn` gets.
- **It reads its aesthetic's knobs and the theme's tokens, with the shipped values as fallbacks.** `--glass-radius-surface`, `--neo-offset`, `--pixel-unit`, `--tile-lift`, `--tile-radius`, `--cyber-cut`, `--cyber-shape`, `--cyber-shape-surface`, `--cyber-glow`, `--cyber-ink`, `--sketch-radius`, `--sketch-radius-surface`, `--sketch-radius-rounded`, `--sketch-radius-surface-rounded`, and the font tokens such as `--font-sketch` all work, set on the element or any ancestor. With the theme loaded, it follows the theme; without it, it renders the shipped look.
- **Dark values need a color scheme.** Light and dark pairs use `light-dark()`, which follows the element's `color-scheme`. The package's theme sets it; a page without the theme gets the light values.
- **The classes it reads come from the theme.** `.solid`, `.edgeless`, `.success`, `.flat`, and the rest ship in the base stylesheet and in `@codenhub/styles/theme`. With an aesthetic entrypoint alone on the page, those classes do nothing, and the solo class paints its unclassed look.

## What a solo class ignores

- **The aesthetic around it.** It reads none of the values an aesthetic class sets, and not `--elevation-color`. A `.glass-solo` inside a `.pixel` region keeps its own corners, and `--ui-radius` or `--ui-border-width` set on it or above it do nothing. Tune it through its knobs.
- **Its own children.** A solo element is not an aesthetic region. A component inside it keeps the look of the page around it, even with `.glass-liquid` or `.sketch-rounded` beside the solo class. It passes down only what any container does: the solo class's font, and the fill or edge of a presentation class on it, so a `.card` inside a `.glass-solo.solid` pane is solid, as inside any `.solid` container.
- **Glass's shadow and `.flat`.** `.flat` does not remove the shadow from `.glass-solo`, as it does not under `.glass`.

## How it wins and when it moves

- **It beats the other library's styles.** The rules are unlayered and one class deep, so they win over a component's layered rules, and over its zero-specificity ones, wherever the two load. An unlayered foreign rule that is more specific, or as specific and later in the page, still wins. The other side of this is that a Tailwind utility on the same element, such as `rounded-none`, loses to the solo class: tune it through its knobs instead.
- **It presses only an action.** Neobrutalism, chunky tile, and cyber press a `button`, `a[href]`, `[role="button"]`, `summary`, or button-type `input` that is not disabled. Anything else stays put, so a toast does not sink when clicked. Reduced motion removes the movement. Chunky tile's heavier label follows the same rule.

## Next

The guides end here. [Composing](docs/usage/composing.md), [Customizing](docs/usage/customizing.md), and [Content and layout](docs/usage/content-and-layout.md) are the lookup pages, and [Accessibility](docs/accessibility.md) lists what the CSS does for your users and what your application still owes them.

<!-- Source: docs/usage/composing.md -->

# Composing

This is a lookup page. [Building a screen](docs/concepts.md) introduces intent, presentation, and aesthetic; this page has the complete rules for combining them, the default look of every component, which classes each component reads, and elevation.

## Combining classes

```html
<div class="soft">
  <button class="btn primary">Soft primary button</button>
  <span class="badge success">Soft success badge</span>
  <button class="btn primary ghost edged">Outlined: the class on the element wins</button>
</div>
```

- **Presentation passes down.** `.solid`, `.soft`, `.ghost`, `.edged`, and `.edgeless` work on the component or on any container, and a class on the element beats one inherited from a container. A container sets the look of everything inside it, and any element can choose its own.
- **Aesthetics and [material tokens](docs/usage/customizing.md#material-tokens) pass down** the same way.
- **Intent does not pass down.** `.primary` and the other intent classes color only the element they are on. A container that silently recolored everything inside it would turn a red danger-zone card's neutral Cancel button red. `.neutral` names the default; use it where an element would otherwise take an intent, such as a table row inside a `.success` table. See [Theming → Intent tokens](docs/usage/theming.md#intent-tokens).
- **Elevation does not pass down.** `.raised` lifts only the element it is on; see [Elevation](#elevation).

## Component defaults

With no presentation class on it or around it, each component has its own look. "Resting elevation" is how much depth it takes when an [aesthetic](docs/usage/aesthetics.md) draws depth; with no aesthetic, nothing has a shadow.

| Component                        | Fill        | Edge     | Resting elevation |
| -------------------------------- | ----------- | -------- | ----------------- |
| `.btn`                           | `.solid`    | edgeless | 1                 |
| `.card`                          | `.ghost`    | edged    | 1                 |
| `.panel`                         | `.soft`     | edgeless | 0                 |
| `.alert`                         | `.soft`     | edged    | 0                 |
| `.badge`                         | `.soft`     | edgeless | 0                 |
| `.ipt`, `.textarea`, `.select`   | `.ghost`    | edged    | 0                 |
| `.input-group`                   | `.ghost`    | edged    | 0                 |
| `.checkbox`, `.radio`, `.switch` | `.soft`     | edged    | 0                 |
| `.kbd`                           | `.soft`     | edged    | 0                 |
| `.code`, `.pre`                  | `.soft`     | edgeless | 0                 |
| `.data-table`                    | `.soft`     | edgeless | 0                 |
| `.progress` (the track)          | `.soft`     | edgeless | 0                 |
| `.tooltip-bubble`                | `.solid`    | edged    | 0                 |
| `.quote`                         | `.ghost`    | edged    | 0                 |
| `.skeleton`, `.divider`          | Intent only | None     | 0                 |

A neutral `.solid` fills only to 20%; see [Theming → Intent tokens](docs/usage/theming.md#intent-tokens). The neutral exceptions to these fills are in [Presentation](#presentation).

## Presentation

Presentation is two separate choices. `.solid`, `.soft`, and `.ghost` set how much of the intent color fills a component. `.edged` and `.edgeless` set whether it draws a border. They set [presentation tokens](docs/usage/customizing.md#presentation-tokens) and nothing else.

| Class       | Look                                                      | `--ui-fill` | `--ui-fg-on-fill` | `--ui-border` |
| ----------- | --------------------------------------------------------- | ----------- | ----------------- | ------------- |
| `.solid`    | Filled with the intent color, with contrast text.         | `100%`      | `100%`            |               |
| `.soft`     | A light tint, with intent-colored text.                   | `12%`       | `0%`              |               |
| `.ghost`    | No fill at rest, with intent-colored text.                | `0%`        | `0%`              |               |
| `.edged`    | An intent-colored border, as thick as the aesthetic sets. |             |                   | `100%`        |
| `.edgeless` | No border.                                                |             |                   | `0%`          |

```html
<button class="btn primary">Solid, the button default</button>
<button class="btn soft edged">Tinted, with a border</button>
<button class="btn primary ghost edged">Outline</button>
<button class="btn ghost">Ghost: picks up a tint on hover</button>
<input class="ipt soft edgeless" aria-label="Search" />
```

A fill class never sets the border, and a border class never sets the fill. `.solid.edged` looks the same as `.solid.edgeless`, because the border fades out as the fill fills in.

The border is the outline of the component and nothing else. Lines inside a component, such as the rules between table rows, are a separate switch; see [Content and layout → Table rules](docs/usage/content-and-layout.md#table-rules).

### Neutral surfaces rest untinted

A neutral `.card.soft`, `.panel`, and `.alert` show no tint at rest. They use the quiet background tone one step off the page instead (`--color-foreground`), because a gray tint on every neutral box looks dirty rather than quiet.

- A `.card.soft` with `.interactive` or `.hoverable` shows the tint on hover. A plain `.card.soft` and a `.panel` have no hover state.
- With one of the six built-in intents, all three keep the ordinary 12% tint, and a card deepens it on hover. An intent you defined yourself is treated as neutral here; see [Theming → Your own intent](docs/usage/theming.md#your-own-intent).
- A `.badge` keeps its 12% tint even when neutral. It has no border, so without the tint it would disappear into the text around it.

`.kbd`, `.code`, and `.pre` work differently. They rest on their intent's subtle tone, `--intent-subtle`, which is `--color-surface` for neutral, and a built-in intent adds 12% of its color over it. An intent you defined yourself shows its own `--intent-subtle` tint without that extra 12%.

### Text controls and toggles

- **Text controls** cap their fill so typed text stays readable: at most 6% from a container, 12% for their own `.soft`, and 20% for their own `.solid`. They ignore an `.edgeless` from a container and honor their own. See [Forms → Fill and border on text controls](docs/usage/forms.md#fill-and-border-on-text-controls).
- **Toggles** cap their unchecked fill at 40% and fill to 100% once checked. `.checkbox` and `.radio` always draw their border, and so does `.switch.ghost`. See [Forms → Checked and unchecked colors](docs/usage/forms.md#checked-and-unchecked-colors).

### Tooltip bubbles

A tooltip bubble is always filled and opaque, and draws a thin border that only its own `.edgeless` removes. With no intent it uses `--color-tooltip`; an intent fills it with that intent's color. [Tooltips → How the bubble looks](docs/usage/tooltips.md#how-the-bubble-looks) has the colors.

### Progress tracks

`.progress` reads both choices on its track. The filled part is always the intent color at full strength. `.solid` is not supported on the track, because the track and the filled part would be the same color. `.ghost.edgeless` renders but leaves the track with no visible frame; `.ghost.edged` draws an outline around it.

### Unsupported combinations

| Component               | Class       | What happens                                                                                 |
| ----------------------- | ----------- | -------------------------------------------------------------------------------------------- |
| `.kbd`, `.code`, `.pre` | `.ghost`    | The chip still draws its tinted background, because that background is what makes it a chip. |
| `.progress`             | `.solid`    | The track and the filled part are the same color, so the value cannot be seen.               |
| `.checkbox`, `.radio`   | `.edgeless` | Ignored: the border is always drawn.                                                         |

## Component axis reference

Which classes change each component. "Material" is what an [aesthetic](docs/usage/aesthetics.md) can change on it: "Shape" means corner, border width, shadow, hover, and outline; the narrower entries list what applies.

| Components                                                | Intent | Fill | Edge | Material           |
| --------------------------------------------------------- | ------ | ---- | ---- | ------------------ |
| `.btn`, `.alert`, `.badge`, `.card`, `.panel`             | Yes    | Yes  | Yes  | Shape              |
| `.kbd`, `.data-table`                                     | Yes    | Yes  | Yes  | Corner and edge    |
| `.quote`                                                  | Yes    | Yes  | Yes  | Edge width         |
| `.divider`, `.skeleton`                                   | Yes    | No   | No   | No                 |
| `.ipt`, `.textarea`, `.select`, `.text-control`           | Yes    | Yes  | Yes  | Shape              |
| `.input-group`                                            | Yes    | Yes  | Yes  | Shape              |
| `.surface`                                                | Yes    | Yes  | Yes  | Shape              |
| `.checkbox`, `.radio`                                     | Yes    | Yes  | No   | Corner and edge    |
| `.switch`                                                 | Yes    | Yes  | Yes  | Corner and edge    |
| `.progress`                                               | Yes    | Yes  | Yes  | Corner and edge    |
| `.code`, `.pre`                                           | Yes    | Yes  | Yes  | Corner and outline |
| `.tooltip-bubble`                                         | Yes    | Yes  | Yes  | Shape and blur     |
| `.table-wrap`, `.quote-inline`, layout and text utilities | No     | No   | No   | No                 |

"Yes" means the class changes the component, and "No" means it does not; the package's tests check both. What each component does with a class:

| Component                                       | Reads                                                                                  |
| ----------------------------------------------- | -------------------------------------------------------------------------------------- |
| `.btn`, `.alert`, `.badge`, `.card`, `.panel`   | Fill, text color, border, and border width.                                            |
| `.kbd`                                          | Fill, text color, border, and border width.                                            |
| `.data-table`                                   | Header fill and text color, border, and border width.                                  |
| `.ipt`, `.textarea`, `.select`, `.text-control` | Border, border width, and a capped fill.                                               |
| `.input-group`                                  | Border, border width, and a capped fill.                                               |
| `.checkbox`, `.radio`                           | A capped fill, checked and unchecked.                                                  |
| `.switch`                                       | Border, border width, and a capped fill.                                               |
| `.quote`                                        | Fill, text color, border, and border width.                                            |
| `.code`, `.pre`, `.tooltip-bubble`              | Fill, text color, border, and material.                                                |
| `.progress`                                     | Track fill, border, and border width. The filled part is always the full intent color. |
| `.skeleton`, `.divider`                         | Intent only.                                                                           |

State and modifier classes such as `.interactive`, `.hoverable`, `.pressable`, `.p-sm`, and `.vertical` are documented with their component. An invalid control is marked with `aria-invalid="true"`, not a class; see [Forms](docs/usage/forms.md#show-a-validation-error).

## Elevation

Elevation is how much depth a component takes. Nothing has a shadow until an [aesthetic](docs/usage/aesthetics.md) draws depth or one of these classes asks for it.

| Class       | Depth                                                                                |
| ----------- | ------------------------------------------------------------------------------------ |
| `.flat`     | None. Glass surfaces keep their shadow, because it is part of what glass is made of. |
| `.raised`   | One step of the depth in scope: cards and tiles that sit close to the page.          |
| `.floating` | Two steps: menus and popovers.                                                       |

```html
<article class="card raised">Lifted</article>
<div class="floating panel">A menu</div>
<article class="card flat">Flat, whatever the aesthetic says</article>
```

Each class multiplies the shadow of the aesthetic in scope. The same `.raised` is a soft blur on a plain page and a hard offset shadow under `.neobrutalism`: the aesthetic decides what depth looks like, and the class decides how much of it this element takes.

The class does not pass down. It lifts or flattens only the element it is on, and an element inside it takes no depth from it. Give each element that needs depth its own class. The spread of a shadow is not multiplied, because an aesthetic that draws its border as an inner shadow uses the spread for that border.

These classes are separate from the `--elevation-*` shadow tokens in [Theming → Foundation tokens](docs/usage/theming.md#foundation-tokens), which are fixed shadows for elements you style yourself.

<!-- Source: docs/usage/customizing.md -->

# Customizing

This is a lookup page for changing the package beyond its classes. [Theming](docs/usage/theming.md) covers colors and the foundation tokens; this page covers the rest:

- [Presentation tokens](#presentation-tokens): how much of an intent a component shows.
- [Material tokens](#material-tokens): what a component is made of.
- [Aesthetic tokens](#aesthetic-tokens): the knobs each aesthetic exposes.
- [Component variables](#component-variables): the six variables a single component reads.
- [Generated palette](#generated-palette): fixed color values for code that cannot load the package.
- [Composition helpers](#composition-helpers): the classes to build a component of your own from.

## Presentation tokens

Presentation tokens describe _how much_ of an intent a component shows, never _which_ intent it shows. They are unitless numbers and percentages, so they carry no color and inherit safely: a container sets them once and every component below resolves them against its own intent.

| Token             | Purpose                                                                                             | Default                             |
| ----------------- | --------------------------------------------------------------------------------------------------- | ----------------------------------- |
| `--ui-fill`       | Percentage of the intent color mixed into the background.                                           | The component's default; see below. |
| `--ui-fg-on-fill` | Percentage blended from the intent's strong tone toward its contrast color.                         | The component's default.            |
| `--ui-border`     | Percentage of the intent's border color in the border.                                              | The component's default.            |
| `--hover-step`    | Added to the resting fill under the pointer, in `--intent-hover`. Set it on `:root` or any element. | `14%`                               |

Unset, each component uses its own default, listed in [Composing → Component defaults](docs/usage/composing.md#component-defaults). The presentation classes set the first three to the values in [Composing → Presentation](docs/usage/composing.md#presentation). There are no hover tokens per presentation: hover is always the resting fill plus `--hover-step`, up to 100%.

No fill class writes `--ui-border` and no edge class writes `--ui-fill`. A filled box with no line is `.solid.edgeless`, and `.solid.edged` renders the same box: the line fades out as the fill fills in, so even a neutral fill, capped and translucent, shows through its own border as one coat.

```css
background: color-mix(in oklab, var(--color-primary) var(--ui-fill, 100%), transparent);
```

Two choices there are deliberate. Mixing toward `transparent` rather than a background token lets a tinted surface adapt to whatever it is placed on. Mixing in `oklab` keeps the wide-gamut token palette intact and blends perceptually, where `srgb` would clip it.

A computed color therefore reaches the page as a resolved `color-mix` result rather than the token's own `oklch` syntax. It is the same color; code that compares computed color strings should compare colors instead.

> **Presentation token contract**: only presentation classes and consumer overrides set these tokens; components only read them. Setting a value that no presentation class produces is supported but unvalidated. See [Composing](docs/usage/composing.md) for the classes that ship these values.

## Material tokens

Material tokens describe what a component is _made of_ rather than which intent it shows or how strongly. They control shape, borders, shadows, translucency, filters, and transforms.

Components read each with a fallback, so leaving them unset gives the default look. Set on a container, they restyle everything inside it. The one exception is `--ui-elevation`, which does not inherit, so it reaches only the element it is set on; see [Composing → Elevation](docs/usage/composing.md#elevation).

Shadow geometry is split into parts with no color in them, so they inherit safely. Tokens that take a color, such as `--ui-ink` and `--ui-surface-shadow`, may include their own.

| Token                     | Purpose                                                                                                                                              | Fallback                                  |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| `--ui-corner`             | Control corner at the default size; scaled by the size step.                                                                                         | `--radius-control`                        |
| `--ui-corner-surface`     | Surface corner at the default padding; scaled by the size step.                                                                                      | `--radius-surface`                        |
| `--ui-corner-tl`          | `0` or `1`: whether the top-left takes the corner. `-tr`, `-br`, and `-bl` for the others.                                                           | `1`                                       |
| `--ui-radius`             | A complete control radius, used as written: overrides the corner, the step, and the pattern.                                                         | Computed from the corner                  |
| `--ui-radius-surface`     | A complete surface radius, used as written.                                                                                                          | Computed from the corner                  |
| `--ui-scale`              | Overrides the size step the size and padding classes set.                                                                                            | The step                                  |
| `--ui-corner-shape`       | What every corner draws: `round`, `bevel` for a straight cut, or `squircle` for a continuous curve. Chromium only today.                             | `round`                                   |
| `--ui-radius-pill`        | Corner for what is fully round by default: badges, switches, progress, the tooltip icon; `.btn.pill` and `.radio` read it with no fallback but full. | `--ui-radius`, `--ui-corner`, then full   |
| `--ui-radius-tight`       | Complete radius for chips, such as a checkbox, `.kbd`, or `.code`, when set. Unset, a small computed default.                                        | `min(--ui-radius or --ui-corner, small)`  |
| `--ui-line-style`         | Style of every line a component draws: `solid`, `dashed`, `dotted`, or `double` (3px wide or more).                                                  | `solid`                                   |
| `--ui-border-width`       | Edge thickness.                                                                                                                                      | `--border-width`                          |
| `--ui-border-max`         | Ceiling on computed edge width.                                                                                                                      | `100px`                                   |
| `--ui-ink`                | Neutral line color when no intent is set.                                                                                                            | `--color-border`                          |
| `--ui-control-ink`        | Neutral line color of fields, selects, checkboxes, radios, and switches when no intent is set.                                                       | `--ui-ink`, then `--color-control-border` |
| `--ui-rule-ink`           | Neutral line color of a table's frame and rules when no intent is set.                                                                               | `--ui-ink`, then `--color-border`         |
| `--ui-rule`               | Lines inside a table. Unset draws the head and foot boundaries only; `0%` draws none; `100%` rules every row.                                        | unset                                     |
| `--ui-shadow-x`           | Shadow horizontal offset.                                                                                                                            | `0px`                                     |
| `--ui-shadow-y`           | Shadow vertical offset.                                                                                                                              | `0px`                                     |
| `--ui-shadow-blur`        | Shadow blur radius.                                                                                                                                  | `0px`                                     |
| `--ui-shadow-spread`      | Shadow spread radius.                                                                                                                                | `0px`                                     |
| `--ui-shadow-inset`       | The `inset` keyword for an inner ring.                                                                                                               | Empty                                     |
| `--ui-hover-shadow-x`     | Shadow horizontal offset while hovered.                                                                                                              | `--ui-shadow-x`                           |
| `--ui-hover-shadow-y`     | Shadow vertical offset while hovered.                                                                                                                | `--ui-shadow-y`                           |
| `--ui-active-shadow-x`    | Shadow horizontal offset while pressed.                                                                                                              | `--ui-shadow-x`                           |
| `--ui-active-shadow-y`    | Shadow vertical offset while pressed.                                                                                                                | `--ui-shadow-y`                           |
| `--ui-active-transform`   | Transform while pressed. `none` under `prefers-reduced-motion`.                                                                                      | `scale(0.97)`                             |
| `--ui-active-translate-x` | Distance a press travels into the element's depth; `-y` beside it. Scaled like the shadow.                                                           | `0px`                                     |
| `--ui-shadow-ink`         | Percentage of shadow color taken from intent ink.                                                                                                    | `0%`                                      |
| `--ui-halo-blur`          | Blur of a halo around the element; `--ui-halo-spread` beside it. Elevation does not scale it.                                                        | `0px`                                     |
| `--ui-halo-ink`           | The halo's share of the intent color, over nothing. Unset, there is no halo.                                                                         | Unset                                     |
| `--ui-shadow-edge`        | Declared, even empty, when the shadow is the element's edge rather than its depth. Read for presence.                                                | Unset                                     |
| `--ui-elevation`          | Unitless multiplier over shadow geometry. Does not inherit.                                                                                          | `1`                                       |
| `--ui-surface-shadow`     | Complete multi-layer shadow accepted by surfaces only.                                                                                               | Unset                                     |
| `--ui-surface-ground`     | Ground a surface, or a default or `.soft` table, sits on.                                                                                            | `--color-background`                      |
| `--ui-bg-alpha`           | Multiplier over every fill, `.solid` included. No shipped aesthetic sets it.                                                                         | `1`                                       |
| `--ui-backdrop`           | Backdrop filter accepted by surfaces, and by a default or `.soft` table, only.                                                                       | `none`                                    |
| `--ui-surface-image`      | A painted layer over a surface's plate: any `background-image` value. Surfaces only.                                                                 | `none`                                    |
| `--ui-hover-transform`    | Transform while an interactive element is hovered.                                                                                                   | `none`                                    |
| `--ui-clip`               | Structural component silhouette.                                                                                                                     | `none`                                    |
| `--ui-clip-tight`         | Compact component silhouette.                                                                                                                        | `--ui-clip`                               |
| `--ui-focus-inset`        | Inset focus layer width.                                                                                                                             | Unset                                     |
| `--ui-label-weight`       | Label weight of `.btn` (and, with `/native`, a bare `<button>`) and `.badge`.                                                                        | Semibold on a button, bold on a badge     |
| `--ui-label-tracking`     | Label letter spacing. Unset, the label inherits it.                                                                                                  | Unset                                     |
| `--ui-label-case`         | Label `text-transform`.                                                                                                                              | `none` on a button; inherited on a badge  |
| `--ui-label-shadow`       | Label `text-shadow`, written without a color so it takes the label's own.                                                                            | `none` on a button; inherited on a badge  |

```html
<section style="--ui-corner: 0; --ui-border-width: 3px">
  <button class="btn primary">Square and heavy</button>
  <input class="ipt" placeholder="Matches" />
</section>
```

> **Material token contract**: components read these; aesthetic classes and consumer overrides set them.

### Corner size and pattern

The corner scales with the element. `.p-xs` sets a size step of `0.5`, `.sm` and `.p-sm` `0.75`, and `.lg` and `.p-lg` `1.25`. When a size class and a padding class sit on one element, the smaller step wins, so `.lg.p-xs` is small. `box` multiplies `--ui-corner` by the step, so a small button takes a small corner and a bevel stays a true 45 degrees. Depth follows the step down but not up: a `.p-xs` button sits on half its aesthetic's shadow, and a `.lg` one on the whole of it. A press that travels into that depth, given as `--ui-active-translate-x` and `-y`, travels the same scaled distance, so it always lands flat.

Which corners take the corner is its own choice. `.cut-diagonal` keeps the top-left and bottom-right and squares the other two; `.cut-diagonal-reverse` keeps the other pair. Both set `--ui-corner-tl`, `-tr`, `-br`, and `-bl`, which you can also set yourself: a two-corner leaf on the default look, two cuts on a control under `.cyber`. A surface under `.cyber` keeps the aesthetic's own surface shape, which the pattern does not reach, and an explicit `--ui-radius` or `--ui-radius-surface` ignores the step and the pattern.

### Lines, layers, and labels

`--ui-line-style` reaches every line a component draws: the box's edge, the progress track, and dividers and table rules. `--ui-rule-style` optionally overrides just dividers and table rules, without changing the box edge; sketch uses it for dashed accents. A dashed or dotted line keeps its color, so it keeps its contrast. A `double` line needs a width of 3px or more to draw two lines.

`--ui-halo-*` is light rather than depth: a glow in the element's own intent color. Elevation does not scale it, and `.flat` leaves it on. It appears only when its ink is set.

`--ui-surface-image` lays any `background-image` over a surface's plate: cards, panels, alerts, and tooltips, not controls. Whoever sets it owns the contrast of the text over it, and your own `background-image` on a surface still wins.

`--ui-label-*` treats the label of a button or a badge: weight, tracking, case, and a text shadow. Write the shadow without a color, such as `0 0 6px`, so it takes the label's own color and cannot lower its contrast. Your own `font-*`, `tracking-*`, and case utilities still win.

Under forced colors each of these draws its plain version: a solid line, no halo, no painted layer, and no label shadow. The silhouette and the ring below go too. Forced colors remove every box shadow, so `--ui-clip`, `--ui-clip-tight`, `--ui-border-max`, and `--ui-shadow-edge` are cleared and a component draws its real border at `--ui-border-width`.

### Shape and ring composition

Clipping removes borders, outlines, and outer shadows. An aesthetic that uses a clip can replace the border with an inset shadow by setting `--ui-shadow-inset` and the four shadow geometry parts. Set `--ui-border-max: 0px` when that ring replaces the component border. Focusable components restore the clipped focus outline through `--ui-focus-inset`: an inset layer of that width in `--focus-ring`, with a 2px line of `--color-background` inside it so focus still shows on a component filled with the ring's own color.

A clip is not the only way to cut a corner. `--ui-corner-shape: bevel` draws the corner the radius sets as a straight cut instead of an arc, and the border, outer shadow, focus outline, and backdrop all follow it. A beveled element therefore keeps its glow and its line, where a clipped one loses both. `.cyber` uses it. Engines without `corner-shape` ignore it and draw the radius round, so an aesthetic that bevels should square its radius under `@supports not (corner-shape: bevel)`. A bevel on a full radius cuts to points: a radio, a `.btn.pill`, and anything reading `--ui-radius-pill` become diamonds or pointed hexagons.

`--ui-radius` accepts any `border-radius` value and is used as written, so it can place the corner per side: `10px 0` cuts only top-left and bottom-right. The pill and chip corners are separate tokens. An unset chip corner computes a small default from `--ui-radius`, then `--ui-corner`, capped at `--radius-small`; an explicit `--ui-radius-tight` is used as written, even for a per-corner or elliptical value. Aesthetic and consumer overrides can therefore give a chip a larger or more irregular shape. `.btn.pill` takes `--ui-radius-pill` ahead of the other radii; under sketch it takes an uneven hand-drawn pill outline whose ends stay the same size. An explicit `--ui-radius-pill` on that button still wins.

Declare `--ui-shadow-edge` when that ring _is_ the border. The ring then takes the element's edge color, which carries the edge axis as its alpha, so `.edged` draws it and `.edgeless` does not — the same answer a real border gives. Without it the ring paints on every component the aesthetic reaches and the edge classes have no effect. Use `--ui-shadow-ink` instead when the shadow is depth cast in the intent's own ink, such as a hard offset slab, which an `.edgeless` element still gets.

The token is read for its presence, not its value: declare it empty to switch the ring on, and `initial` to switch it back off in an aesthetic nested inside one that declares it.

```css
.stepped {
  --ui-clip: polygon(/* structural silhouette */);
  --ui-clip-tight: none;
  --ui-border-max: 0px;
  --ui-shadow-inset: inset;
  --ui-shadow-edge: ;
  --ui-shadow-x: 0px;
  --ui-shadow-y: 0px;
  --ui-shadow-blur: 0px;
  --ui-shadow-spread: 4px;
  --ui-focus-inset: var(--focus-ring-width);
}
```

Variables prefixed `--_` are internal composition outputs — `--_bg`, `--_fg`, `--_edge`, `--_fill`, `--_shadow`, and the `--_d-*` resting slots a component states for `box` to read. Do not set or read them; only the `--ui-*` inputs above are public.

## Aesthetic tokens

The shipped [aesthetic classes](docs/usage/aesthetics.md) set material tokens for you. Only aesthetics with a token listed below expose an aesthetic-specific token. Tune all other behavior through the shared material tokens above. Listed tokens are available only where that aesthetic's stylesheet is imported.

| Token                             | Aesthetic                    | Purpose                                                                                         | Default                                                            |
| --------------------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `--neo-offset`                    | `.neobrutalism`              | Hard shadow offset and distance the press travels.                                              | `4px`                                                              |
| `--glass-radius`                  | `.glass`, `.glass-liquid`    | Control corner radius. `1rem` under `.glass-liquid`.                                            | `0.75rem`                                                          |
| `--glass-radius-surface`          | `.glass`, `.glass-liquid`    | Surface corner radius. `1.5rem` under `.glass-liquid`.                                          | `1rem`                                                             |
| `--pixel-unit`                    | `.pixel`                     | One pixel of the grid. The corner cut and the ring are one.                                     | `4px`                                                              |
| `--font-pixel`                    | `.pixel`                     | Consumer-supplied pixel font stack.                                                             | Falls back to the monospace stack                                  |
| `--tile-radius`                   | `.chunky-tile`               | Corner radius, on controls and surfaces alike.                                                  | `0.75rem`                                                          |
| `--tile-lift`                     | `.chunky-tile`               | Depth of the seated bar, and how far a press travels.                                           | `4px`                                                              |
| `--font-rounded`                  | `.chunky-tile`               | Consumer-supplied rounded font stack.                                                           | Falls back to the page stack                                       |
| `--cyber-cut`                     | `.cyber`                     | The cut at the default size, on controls and surfaces alike; controls scale it with the step.   | `0.625rem`                                                         |
| `--cyber-shape`                   | `.cyber`                     | Where controls take the cut: any `border-radius` value.                                         | `<cut> 0` (top-left, bottom-right)                                 |
| `--cyber-shape-surface`           | `.cyber`                     | Where surfaces take the cut: any `border-radius` value.                                         | `0 <cut>` (top-right, bottom-left)                                 |
| `--cyber-glow`                    | `.cyber`                     | Blur of the glow.                                                                               | `8px`                                                              |
| `--cyber-ink`                     | `.cyber`                     | Neutral line and glow color, where no intent sets one.                                          | Theme ink (near-black, near-white)                                 |
| `--font-cyber`                    | `.cyber`                     | Consumer-supplied technical font stack.                                                         | Falls back to the monospace stack                                  |
| `--sketch-radius`                 | `.sketch`                    | Control outline radius: any `border-radius` value.                                              | Irregular box outline with sibling variation                       |
| `--sketch-radius-surface`         | `.sketch`                    | Surface container outline radius: any `border-radius` value.                                    | Sharp irregular square outline with sibling variation              |
| `--sketch-radius-rounded`         | `.sketch`, `.sketch-rounded` | Rounded control outline radius: expressive elliptical contour with sibling variation.           | `26px 10px 24px 11px / 12px 23px 11px 20px` with sibling variation |
| `--sketch-radius-surface-rounded` | `.sketch`, `.sketch-rounded` | Rounded surface container outline radius: expressive elliptical contour with sibling variation. | `38px 16px 36px 17px / 16px 34px 17px 32px` with sibling variation |
| `--sketch-radius-pill`            | `.sketch`                    | Radius of a pill button's outline: an uneven hand-drawn pill whose ends stay the same size.     | `24px 16px 23px 17px / 17px 21px 16px 20px` with sibling variation |
| `--font-sketch`                   | `.sketch`                    | Consumer-supplied handwriting font stack.                                                       | Falls back to `cursive`                                            |

Every aesthetic's knobs reach the same way: each knob is _read_ with its default as a `var()` fallback rather than declared, so it resolves once into a private on the aesthetic's own class (`--_pixel-unit`, `--_neo-offset`, `--_tile-radius`, `--_tile-lift`, and so on). Set a knob on the element carrying the aesthetic class, or on any ancestor including `:root`:

```html
<section class="pixel" style="--pixel-unit: 6px">
  <button class="btn primary">Chunkier</button>
</section>
```

```html
<html class="pixel" style="--pixel-unit: 6px">
  <body>
    <button class="btn primary">Also chunkier, set from the root</button>
  </body>
</html>
```

Knobs and material tokens reach in opposite directions. A knob is read once, at the element with the aesthetic class, and the result passes down. So set a knob on that element or on one above it; set on an element inside the aesthetic, it changes nothing, because the value has already been read.

`--ui-radius` and the other material tokens are read by each component as it draws. They work on the element they are set on and everything inside it, which is how you round one card differently from its neighbors without touching the knob.

Shipped aesthetics set the shared material tokens above. Every shipped aesthetic sets `--ui-ink` for its neutral line. Every shipped aesthetic also sets `--ui-control-ink`: `.glass` and `.chunky-tile` to a line of their own, heavier than their surfaces', and the others to `--color-control-border`, since their ink is near-black or near-white and a resting checkbox outlined in it would look checked. `.glass` sets `--ui-rule-ink` too, so a table's lines read on its plate; the others clear it. Intent classes still override the intent slots on each component.

The same knobs tune each aesthetic's [solo class](docs/usage/solo.md), which reads them on its own element.

## Generated palette

`@codenhub/styles/palette` is a generated stylesheet of fixed colors: for every intent and presentation, the background, text, and border color a component would draw, as flat `--palette-*` custom properties. It is for code that cannot load this package's stylesheet, such as a library that lists `@codenhub/styles` as an optional peer, but wants to match it. Where you can load the stylesheet, use the components instead.

**The values are a snapshot of the shipped colors.** They were computed from the package's default theme when it was built, and they do not change when you override a token. If you set `--color-primary` to your brand color, `--palette-primary-solid-bg` is still the shipped near-black. Code that must follow your theme has to read the `--color-*` tokens, which means loading `@codenhub/styles/theme`.

```css
@import "@codenhub/styles/palette";
```

```css
.my-badge {
  background: var(--palette-success-soft-page-bg);
  color: var(--palette-success-soft-fg);
  border-color: var(--palette-success-soft-page-edge);
}
```

Every value is a finished color, never a reference to another variable, so the file needs nothing else loaded.

Naming: `--palette-<intent>-<presentation>-<slot>`, where `<intent>` is one of the seven published intents (`neutral`, `primary`, `secondary`, `success`, `warning`, `destructive`, `info`) and `<presentation>` is `solid`, `soft`, or `ghost`. `<slot>` is `bg`, `fg`, or `edge`. Two qualifiers layer on only where they apply:

- A ground qualifier, `page` or `subtle`, before the slot. It appears only on the `bg` and `edge` of `.soft` and `.ghost`, because a `.solid` fill covers its ground and the text color never depends on it.
  - No qualifier is the transparent ground of `.badge` and `.btn`: `--palette-success-soft-bg`.
  - `page` is the ground of `.alert`, `--color-background`: `--palette-success-soft-page-bg`.
  - `subtle` is the ground of `.pre`, `.code`, `.kbd`, and `.tooltip-bubble`, the intent's own `--intent-subtle`: `--palette-success-soft-subtle-bg`.
- A `-hover` suffix, on `bg` and `edge` only, because the text color does not change on hover: `--palette-success-soft-page-bg-hover`.

Light values are set on `:root`. Dark values apply under `.dark`, `.theme-dark`, or `[data-theme="dark"]` on the element or an element above it, the same selectors the package's theme uses. The light values also apply under `.light`, `.theme-light`, and `[data-theme="light"]`, so the nearest explicit theme wins: a light area inside a dark one reads the light values.

The file does not follow `prefers-color-scheme`. Code that uses the palette on its own already switches themes itself, so it sets one of those classes or attributes.

Three more tokens have no intent, presentation, ground, or hover: `--palette-border`, `--palette-surface`, and `--palette-text`, fixed values of `--color-border`, `--color-surface`, and `--color-text`. They are for things that are not intent-colored, such as a container's border, a dialog's background, and its text.

## Component variables

Six variables are read by a single component and are supported inputs. Every other variable a component declares, such as `--surface-padding`, `--field-gap`, `--switch-*`, `--table-*`, or `--quote-*`, is internal and may change in any release.

| Variable             | Element        | Value                        | Default                                                                 | Effect                                                                                                | Where to set it                                                                               |
| -------------------- | -------------- | ---------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `--progress-value`   | `.progress`    | A percentage, `0%` to `100%` | `0%`                                                                    | Width of the filled part. Not clamped; a value without `%` is invalid and shows empty.                | The bar, or any element around it.                                                            |
| `--progress-color`   | `.progress`    | A color                      | The intent color                                                        | Color of the filled part and of the `.active` shimmer.                                                | The bar, or any element around it.                                                            |
| `--progress-surface` | `.progress`    | A color                      | The intent color at the presentation's fill: 12% by default             | Color of the track.                                                                                   | The bar, or any element around it.                                                            |
| `--progress-edge`    | `.progress`    | A color                      | The intent's border color at the presentation's border: none by default | Color of the track's border. It shows only where the track has a border width, such as with `.edged`. | The bar, or any element around it.                                                            |
| `--input-group-gap`  | `.input-group` | A length                     | `0.5rem`                                                                | Space between the control and its icon or text.                                                       | The `.input-group` itself. The group declares the default, so a value on a parent is ignored. |
| `--tooltip-linger`   | `.tooltip`     | A time                       | `300ms`                                                                 | How long the bubble stays after the pointer leaves the trigger.                                       | The `.tooltip` itself. The wrapper declares the default, so a value on a parent is ignored.   |

```html
<div class="progress" role="meter" aria-label="Storage used" aria-valuemin="0" aria-valuemax="100" aria-valuenow="92" style="--progress-value: 92%; --progress-color: var(--color-warning)"></div>

<div class="input-group" style="--input-group-gap: 0">
  <span>app.example.com/u/</span>
  <input class="ipt" id="profile-url" aria-label="Profile address" />
</div>
```

The examples in context are in [Feedback → Show progress](docs/usage/feedback.md#show-progress), [Forms → Add an icon or a fixed prefix](docs/usage/forms.md#add-an-icon-or-a-fixed-prefix), and [Tooltips → Hover, linger, and Escape](docs/usage/tooltips.md#hover-linger-and-escape).

## Composition helpers

To build a component the package does not ship, such as a segmented control or a filter chip, compose it from the same classes the package's components are built from. With compiled CSS, write them as classes; with Tailwind source, you can also `@apply` them.

| Class          | What it gives an element                                                                                                                                                                   |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `box`          | The painted box: background, text color, border, corner, shadow, the aesthetic's material, the focus ring, and the disabled look. It reads intent, presentation, aesthetic, and elevation. |
| `box-hover`    | The hover color, for something pressed rather than typed into. Add it with `box`.                                                                                                          |
| `box-active`   | The press: a slight shrink, or the aesthetic's own press. Add it with `box`.                                                                                                               |
| `surface`      | `box` with a surface's defaults: no fill and a border, the surface corner, and the aesthetic's surface background, blur, and shadow, as on a card.                                         |
| `text-control` | `box` with a text field's defaults: no fill and a border, the capped fill and border rules, placeholder color, a border that changes on hover and focus, and the invalid state.            |

Filter chips for the members list, one pressed and one not:

```html
<button class="box box-hover box-active neutral soft edgeless" type="button" aria-pressed="true" style="padding: 0.25rem 0.75rem; border-radius: 999px">Admins</button>

<button class="box box-hover box-active neutral ghost edgeless" type="button" aria-pressed="false" style="padding: 0.25rem 0.75rem; border-radius: 999px">Owners</button>
```

`box` does not read `aria-pressed`, so the pressed chip looks pressed only because it has `.soft`. When your code changes `aria-pressed`, change the presentation class with it: `.soft` when pressed, `.ghost` when not.

What to give each one:

- **`box` needs a fill and an edge class.** It has no defaults of its own, so with no `.solid`, `.soft`, or `.ghost` and no `.edged` or `.edgeless`, on the element or a container, it draws nothing. `surface` and `text-control` have defaults and work alone.
- **Name the intent.** A `box` element takes the intent of the nearest element around it that has one, unlike the package's components. Put `.neutral` or an intent class on it.
- **Size and spacing are yours.** None of the helpers sets padding, width, or height; `text-control` does not set the 2.5rem height that `.ipt` adds.
- **So are behavior and semantics.** Use a real `<button>`, `<input>`, or `<a href>`, or add the role and keyboard handling yourself, and set states such as `aria-pressed` or `aria-invalid`. `box` draws the disabled look for `disabled`, `.disabled`, `aria-disabled="true"`, and `data-disabled`, but only the `disabled` attribute stops input.

Do not set or read the `--_`-prefixed variables these helpers use internally; they change between releases.

## Next

[Content and layout](docs/usage/content-and-layout.md) lists the layout helpers, the type scale, surfaces, tables, and content chips.

<!-- Source: docs/usage/content-and-layout.md -->

# Content and layout

This is a lookup page for the classes that arrange a page and set its content: layout helpers, the type scale, cards and panels, tables, and the small content chips. [Building a screen](docs/concepts.md#lay-out-the-page) shows them in use.

## Layout

Every layout helper's gap is `--layout-gap`, `1rem` by default. Setting `--layout-gap` on an element changes the gap of every layout inside it. `.tight` and `.loose` change only the layout they are on; a layout nested inside keeps its own gap.

| Class              | Layout                                                                                                         | Gap                                              |
| ------------------ | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
| `.stack`           | A vertical column.                                                                                             | `--layout-gap`, `.tight` 0.5rem, `.loose` 1.5rem |
| `.cluster`         | A row that wraps, with items centered vertically. `.between` spreads the items to both ends.                   | Same                                             |
| `.view`            | A flex container. `.vertical` makes it a column; `.horizontal` a wrapping row with items centered.             | Same                                             |
| `.auto-grid`       | A grid of equal columns, each at least `--layout-grid-min` (16rem) wide, as many as fit.                       | Same                                             |
| `.section`         | Padding around a page section: `--layout-gutter` at the sides, `--layout-section-block` at the top and bottom. | None                                             |
| `.section-content` | Centers its content at a width of `--container-max` (80rem). `.narrow` uses 48rem, `.wide` 90rem.              | None                                             |
| `.divider`         | A horizontal line, as wide as its container. `.vertical` makes it a vertical line as tall as the row.          | None                                             |
| `.visually-hidden` | Hides content on screen and keeps it for screen readers.                                                       | None                                             |

`.auto-grid` fits two 16rem columns once its container is 32rem plus the gap wide, three at 48rem plus two gaps, and so on. Below one column's width, the column shrinks to fit. Set `--layout-grid-min` on the grid to change the column width.

`--layout-gutter` is `clamp(1rem, 4vw, 2rem)` and `--layout-section-block` is `clamp(4rem, 9vw, 7rem)`: both grow with the window between those bounds. The full list of foundation tokens is in [Theming → Foundation tokens](docs/usage/theming.md#foundation-tokens).

A `.divider` takes an intent, which colors it, and ignores presentation classes, so a `.ghost.edgeless` container does not hide the dividers inside it. Its width is the aesthetic's border width.

`.visually-hidden` is not named `.sr-only` because Tailwind ships a `.sr-only` of its own, and two classes with one name would conflict.

## Typography

| Class            | Size                          | Weight   | Letter spacing |
| ---------------- | ----------------------------- | -------- | -------------- |
| `.text-display`  | 3.75rem, 6rem from 48rem wide | Bold     | Tight          |
| `.text-title-lg` | 3rem, 3.75rem from 48rem wide | Bold     | Tight          |
| `.text-title`    | 1.875rem                      | Bold     | Tight          |
| `.text-title-sm` | 1.25rem                       | Semibold | Tight          |
| `.text-label-lg` | 1.125rem                      | Medium   | Wide           |
| `.text-label`    | 1rem                          | Medium   | Wide           |
| `.text-body`     | 0.875rem                      | Normal   | Wider          |

The two largest sizes grow at Tailwind's `md` breakpoint, 48rem by default. With the Tailwind source entrypoints, your own `md` breakpoint applies. The package sets no size on unclassed text: body text and unclassed headings take the browser's size, or the surrounding text size under the reset; see [Setup → Classless HTML](docs/setup.md#classless-html) for the classless mapping.

`.text-body` is 0.875rem, smaller than the 1rem page text; use it for secondary or dense copy, not for the page's main text.

`.selection-contrast` on an element swaps the colors of selected text inside it to `--color-primary` on `--color-primary-contrast`. Use it on a `.solid.primary` area, where the default selection color would match the background.

## Surfaces

`.card` and `.panel` are the two containers. Both take intent, [presentation](docs/usage/composing.md#presentation), and [material tokens](docs/usage/customizing.md#material-tokens).

| Class    | Default look                                                                | Padding by default |
| -------- | --------------------------------------------------------------------------- | ------------------ |
| `.card`  | No fill, a border, the surface corner, and a resting elevation of 1.        | 1.25rem            |
| `.panel` | The quiet background tone, no border, the surface corner, and no elevation. | 1rem               |

A resting elevation of 1 shows no shadow by itself. A card has a shadow only under an [aesthetic](docs/usage/aesthetics.md) that draws depth, or with `.raised` or `.floating`; see [Composing → Elevation](docs/usage/composing.md#elevation).

Padding classes:

| Class    | On `.card` | On `.panel` |
| -------- | ---------- | ----------- |
| `.p-xs`  | 0.5rem     | 0.25rem     |
| `.p-sm`  | 0.75rem    | 0.5rem      |
| _none_   | 1.25rem    | 1rem        |
| `.p-lg`  | 2rem       | 1.5rem      |
| `.flush` | 0          | 0           |

`.flush` is for content that runs edge to edge, such as an image or a table inside a card.

A padding class applies only to the card or panel it is on. A card or panel nested inside keeps its own padding, so a `.card.flush` holding a `.panel` still pads the panel by 1rem.

A panel is for sidebars, toolbars, and wells: areas that are part of the page, not objects on it. A card is a self-contained object, such as the profile card or the plan card on the settings screen.

### Neutral cards and panels

A neutral `.card.soft` and a neutral `.panel` show no tint: they rest on the quiet background tone one step off the page, so they read as a separate area without drawing attention. With a built-in intent they take the ordinary tint. [Composing → Neutral surfaces rest untinted](docs/usage/composing.md#neutral-surfaces-rest-untinted) has the details, including hover.

### Clickable cards

| Class          | Effect                                                                                                    |
| -------------- | --------------------------------------------------------------------------------------------------------- |
| `.interactive` | Pointer cursor, hover color, press, and a focus ring. The same as `.hoverable` and `.pressable` together. |
| `.hoverable`   | The hover color only, with no pointer cursor.                                                             |
| `.pressable`   | The press and the pointer cursor only.                                                                    |

On the default look, a press scales the card down slightly (`scale(0.97)`); an aesthetic can replace that with its own press, and a reduced-motion preference turns it off.

Use one half for a card that answers only one gesture. A card that holds its own button takes `.hoverable`, so that pressing the button does not press the whole card.

```html
<article class="card">Neutral card</article>
<article class="card success solid">Filled success card</article>
<article class="card primary ghost edged">Intent border, no fill</article>
<a class="card success soft interactive" href="/settings/billing">Billing: tinted at rest, deeper on hover</a>
<article class="card hoverable">Tinted on hover; the action is a nested button</article>
<aside class="panel">A sidebar</aside>
```

These classes only style. Put them on a real link or button and give it an accessible name; a `<div class="card interactive">` cannot be focused or used from a keyboard.

## Tables

The settings screen lists API keys in a table:

```html
<div class="table-wrap">
  <table class="data-table">
    <caption class="visually-hidden">
      API keys
    </caption>
    <thead>
      <tr>
        <th scope="col">Name</th>
        <th scope="col">Created</th>
        <th scope="col">Status</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Deploy bot</td>
        <td>3 March 2026</td>
        <td>Active</td>
      </tr>
      <tr class="destructive">
        <td>Old laptop</td>
        <td>12 June 2024</td>
        <td>Revoked</td>
      </tr>
    </tbody>
  </table>
</div>
```

| Class         | Effect                                                                                                                                    |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `.data-table` | Styles a `<table>`: full width, `.text-body` text, a header background, the surface corner, cell padding, and a hover color on body rows. |
| `.table-wrap` | Scrolls a table sideways when it is wider than its container.                                                                             |
| `.ruled`      | On `.data-table`. Draws a line between body rows.                                                                                         |
| `.ruleless`   | On `.data-table`. Draws no lines inside the table.                                                                                        |

### Row intent

A table's rows take the table's intent, so `.data-table.success` colors its header and lines green. A row with its own intent class uses that instead, as the revoked key's `<tr class="destructive">` does. `<tr class="neutral">` sets a row back to neutral inside a colored table. On a `.solid` table, a row with its own intent keeps the table's text color so it stays readable on the table's fill, and shows its intent through its row line and hover. This is the one place an intent passes down, because rows are parts of the table, not separate components.

### Table rules

A table draws lines under its header and above its footer. `.ruled` and `.ruleless` change them together with the lines between body rows:

| Class       | Under the header and above the footer | Between body rows |
| ----------- | ------------------------------------- | ----------------- |
| `.ruleless` | No                                    | No                |
| _none_      | Yes                                   | No                |
| `.ruled`    | Yes                                   | Yes               |

`--ui-rule` is the token behind both classes, so a container can rule every table inside it: `style="--ui-rule: 100%"` acts like `.ruled`, and `0%` like `.ruleless`.

The frame around the table is separate: a table has no border by default, and `.edged` adds one. The three choices do not affect each other, so `.data-table.ruleless.edged` is a framed table with no lines inside, and `.data-table.ruled` has every row separated and no frame.

### Presentation on tables

A table reads [presentation](docs/usage/composing.md#presentation) on its header, its lines, and its border:

```html
<table class="data-table success soft edged">
  …
</table>
<table class="data-table ghost">
  <!-- No header background: only lines and text -->
</table>
```

- `.soft`, the default, gives the header the quiet background tone, `--color-foreground`, for every intent. A table has no hover state for a tint, so a `.data-table.success.soft` does not turn green; its lines and border do.
- `.ghost` removes the header background, leaving lines and text.
- `.solid` fills the header with the intent color.

### Corners and scrolling

A table takes the surface corner, like a card beside it. The corner cells carry the corners, so a `<caption>` above the table is not cut off in any browser. A cell that spans rows down into a bottom corner leaves that corner square.

`.table-wrap` scrolls, and a scrolling element cuts off whatever is drawn outside it, including a tooltip on a cell; see [Tooltips → Where a tooltip gets cut off](docs/usage/tooltips.md#where-a-tooltip-gets-cut-off).

- Under an aesthetic that draws a glow around components, such as `.cyber`, the wrapper makes room for the glow so it is not cut off. The wrapper then extends that far past its parent, which only shows when the parent touches the edge of the window.
- The wrapper can only make room for a glow from an aesthetic around it. Put the aesthetic class on `.table-wrap` or an element above it, not on the table alone.
- Shadows, such as a `.raised` table's, get no room and are cut off.

Keep the wrapper on any table that could be wider than its container; without it, a wide table widens the whole page.

## Quotes, code, and keys

| Class           | Use                                                                            | Default look                  | Intent colors            |
| --------------- | ------------------------------------------------------------------------------ | ----------------------------- | ------------------------ |
| `.quote`        | A `<blockquote>`. A `<cite>` inside it is upright and takes the quote's color. | A bar at the start, no fill   | Bar, background, text    |
| `.quote-inline` | An inline `<q>` quotation.                                                     | Quotation styling only        | Nothing                  |
| `.kbd`          | A key the user presses, such as <kbd>Ctrl</kbd>.                               | A tinted chip with a border   | Background, border, text |
| `.code`         | Inline code.                                                                   | A tinted chip with no border  | Background, text         |
| `.pre`          | A block of code. It scrolls sideways when a line is too long.                  | A tinted block with no border | Background, text         |

```html
<p>Press <kbd class="kbd">Ctrl</kbd> + <kbd class="kbd">K</kbd> to search members.</p>
<p>Use the <code class="code">Authorization</code> header with your API key.</p>
<kbd class="kbd primary solid">Ctrl</kbd>
<kbd class="kbd primary soft edged">Shift</kbd>
```

A `.kbd`, `.code`, or `.pre` rests on its intent's subtle tone, `--color-surface` for neutral, and a built-in intent class adds 12% of its color over it. `.ghost` is not supported on these three: the tinted background is what makes them chips, so they keep it and `.ghost` changes nothing. Their font is `--font-mono`; see [Theming → Fonts](docs/usage/theming.md#fonts).

## Next

[Accessibility](docs/accessibility.md) lists what the CSS does for your users and what your application still owes them.

<!-- Source: docs/accessibility.md -->

# Accessibility

The settings screen is built, and the CSS has done its part along the way: every field shows where focus is, an invalid email turns red, and the spinner stops for users who ask for less motion. The rest is up to your HTML and JavaScript. This package styles states; it does not provide semantic elements, ARIA attributes, keyboard behavior, focus management, validation, or the timing of announcements.

This page lists both halves: what the CSS provides, and what your application has to add.

## Provided by CSS

### Focus and state

| Feature            | Behavior                                                                                                                                                                                                                                                                                                                                 |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `:focus-visible`   | A focus outline on every element, drawn with `--focus-ring`, `--focus-ring-width`, and `--focus-ring-offset`.                                                                                                                                                                                                                            |
| Form control focus | `.ipt`, `.textarea`, and `.select` draw the focus outline and change their border to the intent color. Typed text in a field with an intent class is in the intent's readable tone, `--intent-strong`.                                                                                                                                   |
| Invalid controls   | `aria-invalid="true"` on a form control turns its border, focus color, and typed text destructive, the text in the destructive readable tone.                                                                                                                                                                                            |
| Disabled controls  | `disabled`, `aria-disabled="true"`, `data-disabled`, and `.disabled` show the disabled cursor and opacity, and a disabled control does not react to hover, including inside an `.input-group`.                                                                                                                                           |
| Open state         | `data-state="open"` on a `.tooltip` shows its bubble without hover or focus, and `data-state="closed"` hides it while hovered or focused. No other component reads either.                                                                                                                                                               |
| Native `<dialog>`  | A `<dialog>` with `.card`, `.panel`, or `.surface` stays hidden until it is opened, and opens centered. A margin you set on the dialog, as a utility or in your own CSS, still overrides the centering. Other dialogs are left alone. Trapping focus, closing on Escape, and making the page behind it inert are up to your application. |

### Visually hidden content

`.visually-hidden` hides content on screen and keeps it for screen readers, as the API keys table's `<caption>` does in [Content and layout → Tables](docs/usage/content-and-layout.md#tables). It ships with the layout utilities: in `.`, `./native`, `./tw`, `./tw/native`, and `./tw/utilities`. It is not in `./components` or the `./tw/components` and `./tw/*` component entrypoints.

### Control boundaries

WCAG 1.4.11 asks that the boundary of a control you need to see to use it measures at least 3:1 against what is next to it.

| Feature                      | Behavior                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Text controls at rest        | A text control's border rests lighter than its intent's full border, below 3:1 by choice, so hover and focus have a stronger tone to change to.                                                                                                                                                                                                                                                                                                             |
| Toggles                      | `.checkbox`, `.radio`, and `.switch` draw their full border, because the border is the only thing that shows an unchecked box. A `.switch` with its own `.edgeless` is the exception, below.                                                                                                                                                                                                                                                                |
| `.edgeless` from a container | Ignored by every text control and toggle: a container cannot remove the border of a field or toggle inside it.                                                                                                                                                                                                                                                                                                                                              |
| `.edgeless` on the control   | `.ipt`, `.textarea`, `.select`, and `.switch` drop their border and no longer meet 1.4.11 at rest. `.switch.ghost` keeps its border even then, because with no fill and no border nothing would show the track.                                                                                                                                                                                                                                             |
| `.checkbox`, `.radio`        | Always draw their border, from a container or with their own `.edgeless`. `.edgeless` on them is not supported.                                                                                                                                                                                                                                                                                                                                             |
| Progress track               | By choice, the filled part carries the value and the track is a quiet background. The default track, `.soft.edgeless`, measures about 1.1:1 to 1.3:1 against the page. The filled part measures at least 3:1 against that track for every intent except warning in light (about 2.7:1) and info in dark (about 2.9:1). Where the track itself must meet 1.4.11, use `.ghost.edged` with an intent class; see [Progress outlines](#progress-outlines) below. |

### Progress outlines

On a `.progress.ghost.edged`, the track is the page, and its outline and the filled part are both drawn in the intent's color. Measured against the page, with no aesthetic:

| Intent         | Light page | Dark page | Meets 3:1                                                                |
| -------------- | ---------- | --------- | ------------------------------------------------------------------------ |
| `.primary`     | 18.96:1    | 18.96:1   | Yes                                                                      |
| `.secondary`   | 7.47:1     | 7.63:1    | Yes                                                                      |
| `.success`     | 5.14:1     | 3.69:1    | Yes                                                                      |
| `.warning`     | 3.06:1     | 6.20:1    | Yes                                                                      |
| `.destructive` | 5.80:1     | 3.27:1    | Yes                                                                      |
| `.info`        | 6.17:1     | 3.07:1    | Yes                                                                      |
| No intent      | 2.48:1     | 1.91:1    | No: the outline fails, though the filled part, in the text color, passes |

With no intent class, the outline is the quiet border gray, `--color-border`. To make a neutral track meet 1.4.11, give the bar an intent class, or set `--progress-edge` to a stronger color, such as `var(--color-accent)`, which measures 7.47:1 on a light page and 7.63:1 on a dark one. Warning on a light page and info on a dark one clear 3:1 by a small margin, so measure again if you change those colors.

### Motion and transparency

| Feature                    | Behavior                                                                                                                                                                                                 |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Reduced motion, components | With `prefers-reduced-motion: reduce`, every component stops its own transitions and animations: the skeleton and progress shimmer, the indeterminate bar, the tooltip, the toggle marks, and the press. |
| Reduced motion, whole page | The reset also shortens every other animation and transition on the page.                                                                                                                                |
| Reduced transparency       | With `prefers-reduced-transparency: reduce`, `.glass` and `.glass-liquid` drop their blur and lens and become opaque.                                                                                    |

Each component's reduced-motion rule ships with the component, so every entrypoint that includes it respects reduced motion without the reset, including `@codenhub/styles/components`, `@codenhub/styles/tw/components`, and each `/tw/*` component entrypoint. The complete and native entrypoints add the reset's page-wide rule.

The reset's rule is `!important` inside the `base` layer, and a layered `!important` beats an unlayered one, so your own `!important` cannot turn an animation back on. To keep one running for users who ask for reduced motion, such as an upload spinner you consider essential, declare it `!important` in a layer ordered before `base`. The first `@layer` statement the browser reads sets the order, so yours goes before the package:

```css
@layer essential-motion, theme, base, components, utilities;
@import "@codenhub/styles";

@layer essential-motion {
  @media (prefers-reduced-motion: reduce) {
    .upload-spinner {
      animation-duration: 1s !important;
      animation-iteration-count: infinite !important;
    }
  }
}
```

Respecting reduced motion is not the whole requirement. Motion that keeps going next to content, such as a shimmer left on a card after it has loaded, needs a way to stop it for every user; see [Feedback → Animation that keeps running](docs/usage/feedback.md#animation-that-keeps-running).

### Color and contrast

The measurements below are for the shipped colors. Once you change a color token, measure your own pairs again; [Theming → Change the brand color](docs/usage/theming.md#change-the-brand-color) shows which pairs to check.

| Feature                 | Behavior                                                                                                                                                                                                                                                                |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Filled labels           | Every intent's `.solid` label meets 4.5:1 (WCAG 1.4.3) as normal text, in both themes. The lowest is success, at 5.14:1.                                                                                                                                                |
| Warning text            | `--color-warning-contrast` is near-black, not the page color, because an amber dark enough to carry white text no longer looks amber.                                                                                                                                   |
| Partial fills           | The contrast text color appears only on fills above half strength, so a text control's capped fill keeps the intent's own text color.                                                                                                                                   |
| Intent color in dark    | In dark mode a `.soft` or `.ghost` label uses the `-300` shade of its intent, so it keeps its hue instead of turning white.                                                                                                                                             |
| Toggle marks            | A checked mark measures at least 3:1 against every fill its presentation can give it, in both themes.                                                                                                                                                                   |
| Soft fills with borders | `.edged` on a `.soft` `.alert`, `.panel`, `.badge`, `.kbd`, `.code`, or `.pre` draws a border that measures below 3:1 against the 12% fill for some intents, which ones depending on the theme. Measure the pair before relying on that border as a control's boundary. |

### Forced colors and direction

| Feature        | Behavior                                                                                                                                                                                                                                            |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Forced colors  | With `forced-colors: active`, borders, checked states, and a `.select`'s chevron stay visible, drawn in system colors, under every aesthetic. An aesthetic that draws its border as an inner shadow, such as `.pixel`, draws a real border instead. |
| Text direction | Under `dir="rtl"`, the quote bar, the alert icon, the progress fill, the vertical divider, the switch knob, table alignment, and a select's chevron all switch sides. No class is needed.                                                           |

In forced colors, `.text-control`, `.ipt`, `.textarea`, `.select`, and the toggles show a 2px `Highlight` focus outline. The native entrypoint gives unclassed text inputs, selects, textareas, checkboxes, and radios the same outline.

Checkboxes, radios, and switches use the system colors `Canvas`, `CanvasText`, `Highlight`, and `HighlightText`, so checked and unchecked stay distinct: a checked toggle is filled with `Highlight` and its mark is `HighlightText`, on the classes and in the native entrypoint alike. Shape, size, and spacing do not change.

## Required outside CSS

Use the HTML element that matches what the component does, and add the behavior the CSS cannot.

| UI               | What your application adds                                                                                                                                                                                                                              |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Buttons          | `<button>` for actions and `<a href>` for navigation. An accessible name, such as `aria-label`, on every `.btn.icon`.                                                                                                                                   |
| Forms            | A `<label>` for each control, the right `type`, validation, and `aria-describedby` linking hints and errors. Keep hints and errors outside the `<label>`, and give a `.switch` `role="switch"`.                                                         |
| Alerts           | `role="status"` for a confirmation such as "Profile saved", or `role="alert"` for an error that needs attention now.                                                                                                                                    |
| Toasts           | A live region, a way to dismiss it, focus rules, and a pause on any timeout.                                                                                                                                                                            |
| Tooltips         | A real `id` and `role="tooltip"` on the bubble, referenced from the trigger's `aria-describedby`. Dismissal on Escape by writing `data-state="closed"`; see [Tooltips](docs/usage/tooltips.md#hover-linger-and-escape). The bubble can already be hovered. |
| Progress         | A `<progress>` element, or `role="progressbar"` or `role="meter"` with ARIA values, when the value must be announced.                                                                                                                                   |
| Skeletons        | `aria-hidden="true"` on loading visuals, including a spinner you bring, and the loading state announced some other way when needed.                                                                                                                     |
| Ongoing motion   | A way to pause, stop, or hide motion that is not essential; see [Feedback → Animation that keeps running](docs/usage/feedback.md#animation-that-keeps-running).                                                                                            |
| Popovers, modals | Focus trapping, closing on Escape, an inert background, labels, and roles.                                                                                                                                                                              |

## State attributes

Prefer native attributes:

```html
<button class="btn primary" disabled>Saving…</button> <input class="ipt" type="email" aria-invalid="true" aria-describedby="email-error" />
```

Use ARIA or data attributes where the element or your component library has no native one:

```html
<a class="btn secondary" aria-disabled="true">Export data</a>
<span class="tooltip" data-state="open">
  <button type="button" class="tooltip-icon" aria-label="About public profiles" aria-describedby="public-profile-help">?</button>
  <span class="tooltip-bubble" role="tooltip" id="public-profile-help">Anyone with the link can see your name and photo.</span>
</span>
```

`aria-disabled="true"` tells assistive technology the control is disabled, but it does not stop a click or a key press. Use the `disabled` attribute, or stop the action in JavaScript.

## Next

That completes the reading path. The [Introduction](docs/index.md) links every page, and [Composing](docs/usage/composing.md), [Customizing](docs/usage/customizing.md), and [Content and layout](docs/usage/content-and-layout.md) are the lookup pages for later.

<!-- Source: docs/changelog/0.7.0.md -->

# 0.7.0

A button can be a liquid glass pane, the solo classes compose with presentation, intent, and elevation the way components do, the loaders are gone in favor of a spinner from an icon set, and a round of fixes to nested layout gaps, dialog placement, table rows, right-to-left switches, field groups, and forced colors.

## Added

- **Liquid glass buttons.** A `.btn` carrying `.glass-liquid` itself, in a glass scope, takes the blur, lens, and rim of a liquid pane, with a lens sized for a control. The fill is still the presentation's: `.solid` is an opaque plate under the rim, `.ghost` and `.soft` are clear panes. A liquid region alone still leaves its controls solid. See [Aesthetics → Liquid buttons](docs/usage/aesthetics.md#liquid-buttons).
- **Solo classes compose with presentation, intent, and elevation.** Fill and edge come from the element or a container, intent and elevation from the element's own class, so `.pixel-solo.edgeless` drops its ring, `.glass-solo.solid.success` is an opaque green pane, and `.neobrutalism-solo.flat` has no slab. With none of those classes a solo element paints what it did. See [Aesthetics → Solo classes](docs/usage/aesthetics.md#solo-classes).
- **`--font-mono` is a documented token.** It sets the font of the `kbd`, `code`, and `pre` helpers and of `<kbd>`, `<code>`, and `<pre>` under `/native`, and joins the other font tokens in [Theming → Fonts](docs/usage/theming.md#fonts), which also shows how to load a web font.
- **Linking without a bundler.** [Integrating → Without a bundler](docs/integrating/without-a-bundler.md) covers plain HTML, Rails, Django, and a CDN `<link>`, which load the compiled files by path rather than by package name.
- **A stated versioning policy.** Below 1.0 a minor release may break and a patch release never does; see [Versioning](docs/index.md#versioning).

## Changed

- **A solo element inside a `.solid`, `.soft`, or `.ghost` container takes that fill and the theme's text color**, since presentation cascades to it now as it does to components. A solo element whose own background does not follow the theme -- a dark toast on a light page -- inside such a container prints the theme's text; keep it out of the container, or give it `.solid` with an intent.
- **`.tight` and `.loose` stay on their own element.** They wrote `--layout-gap`, which inherits, so a plain `.stack`, `.view`, `.cluster`, or `.auto-grid` nested inside one took its gap. A nested layout keeps the default gap now; set `--layout-gap` on a container to change the gap of every layout inside it.
- **Your margin places a component dialog.** The centring of an open `<dialog class="card">` (or `.panel`, `.surface`) overruled your margin utilities and unlayered CSS; it now sits in `utilities` at zero specificity, so `mt-8` or your own rule wins, and it still beats a `base` reset such as Preflight's. A margin reset in a cascade layer of your own, declared after the package's, now wins over the centring too, as it already does over every component property.
- **`.cyber` glows from the outline.** Its glow is the intent color at 60% with a 1px spread, up from 40% with none, so the brightest point sits on the element's outline. An edgeless button, badge, or panel read as backlit -- a shadow on a dark page -- because the glow just outside it was brighter than its own fill. `.cyber-solo` takes the same glow. In the light theme the near-black glow of a neutral component is heavier too; a hued `--cyber-ink` is still the way to make it read as light there.
- **The neutral palette values carry no hue.** `--palette-border`, `--palette-surface`, and `--palette-text` are `oklch(… 0 none)` in both themes, where they were `oklch(… 0 0)`. The colors themselves are unchanged, but a missing hue takes its partner's in polar interpolation: `color-mix(in oklch, var(--palette-text), oklch(60% 0.2 240))` now lands on hue 240, a blue, where it travelled from hue 0 and landed on a purple. To keep the old result, give the neutral its old hue: `color-mix(in oklch, oklch(from var(--palette-text) l c 0), oklch(60% 0.2 240))`. Mixing in `oklab` does not, since a hueless neutral adds no hue there and the mix stays blue.

## Removed

- **The loaders.** `.loader`, its `.sm` and `.lg` sizes, the nine variants (`.dots-wave`, `.dots-fade`, `.dots-queue`, `.dots-rotate`, `.dots-grow`, `.dots-grow-alternate`, `.dot-bounce`, `.bars-wave`, `.pulse-ring`), `.loader-mask`, and the `@codenhub/styles/tw/loader` entrypoint are gone. Importing `/tw/loader` now fails to resolve, and `<span class="loader dots-wave">` in the complete CSS renders nothing. `.sm` and `.lg` on other components are unchanged. Use a spinner from an icon set instead, such as `@codenhub/icons`, which this package does not import: a glyph with `ic-anim-spin` (`ic-lucide-loader-circle ic-anim-spin`) or the animated `svg-spinners` family, both in `currentColor`. Under `prefers-reduced-motion: reduce` every loader showed a still ring; a glyph with `ic-anim-spin` stops there too, but `svg-spinners` artwork keeps moving, so choose the glyph where that matters. See [Feedback](docs/usage/feedback.md) for loading the icons.

## Fixed

- **A focused `.solid.primary` control under `.pixel` shows its focus.** The inset focus ring is the primary color, so it vanished into the fill; a line of the page color now sits inside it. `.pixel-solo` takes the same line.
- **Controls and surfaces under `.pixel` keep an edge in forced colors.** Pixel draws its edge as an inset shadow, which forced colors remove, so buttons, fields, and cards had no boundary; forced colors now draw a solid border and drop the clip. `.pixel-solo` draws an inward outline there.
- **A `.disabled` text field holds still under the pointer.** The class spelling of disabled still darkened an `.ipt`'s line on hover, unlike `disabled`, `aria-disabled`, and `data-disabled`.
- **`.sketch-rounded` beside `.sketch-solo` stays on the solo element.** Its radius tokens inherited into the element's contents, so a `.btn` inside a rounded sketch toast drew a sketch outline on a page with no sketch in scope.
- **A checked native radio looks like a checked `.radio` in forced colors.** Under `/native` it drew a Highlight ring and dot on Canvas, where `.radio` draws a Highlight disc with a HighlightText dot, so `<input type="radio" class="radio">` changed look with the entrypoint. Both draw the disc now.
- **`/theme` declares every documented color and foundation token**, except `--focus-ring-offset`, which is computed on the focused element, and the `/tw`-only `--breakpoint-xs`. The compiled entry left out `--color-background`, `--color-foreground`, `--color-text-secondary`, and `--font-default`, because nothing in it read them, so `var(--color-background)` in your own CSS over `/theme` alone resolved to nothing.
- **A liquid region blurs what takes no liquid layers.** A bare `<table>` under `/native` or your own `@apply surface` inside a `.glass-liquid` region took its clearer ground with nothing blurred behind it; it takes liquid's lighter blur now.
- **Liquid's corners stay off a solo element's contents.** `.glass-solo.glass-liquid` no longer hands liquid's corner tokens to what it holds, so a `.btn` inside a liquid solo toast keeps the page's corners.
- **A checked `.switch` stays in its track right to left.** Under `dir="rtl"` the checked knob moved the left-to-right way, out past the track's right edge; it sits at the inline end, the left, now.
- **A table row shows its own intent.** A `<tr class="destructive">` in a `.data-table.success` kept the table's green text, rule, and hover, because the table resolved them before the row's intent applied. A row with an intent draws them in its own now; on a `.solid` table its text keeps the table's ink -- the contrast ink on a hued table, the table's own text color on a neutral one -- which that plate needs. Other rows keep the table's.
- **A button in an `.input-group` keeps its own edge.** The field's line reached every component inside the group, so a `.ghost` or `.edgeless` clear button drew a border the field's color. It rests as it does outside the group now, and the group keeps its line.
- **An `.alert` in another aesthetic nested under `.neobrutalism` casts no slab.** Neobrutalism raised the elevation of every alert below it, so one inside a `.chunky-tile`, `.glass`, `.pixel`, `.cyber`, or `.sketch` region took neobrutalism's shadow, unlike a card there. It takes the nested aesthetic's look now, and a `.neobrutalism` region nested inside that one still rests its alerts on the slab.
- **A select keeps its drop-down arrow in forced colors.** The chevron is drawn with gradients, which forced colors remove, so in Chromium and Firefox a `.select` or a native `<select>` under `/native` read as a text field. A single-row select shows the platform's arrow there now.
- **An `.input-group` holding a disabled control holds still under the pointer.** The group dimmed and showed the not-allowed cursor, but its line still darkened on hover; it no longer does.
- **A padding modifier pads only its own card or panel.** `.p-xs`, `.p-sm`, `.p-lg`, and `.flush` on a `.card` or `.panel` reached every card and panel nested inside it, so a card in a `.card.p-xs` sat at 8px instead of its 20px, and one in a `.flush` card had none. A nested surface keeps its own padding now; give it a modifier of its own to change it.
- **A long select label in an `.input-group` stops short of the chevron.** The group took the select's inline padding, so a label too long for the field ran under the drop-down arrow, at the right or, right to left, at the left. A single-row select keeps room for its chevron at the inline end now.
- **Typed text in a hued or invalid field reads at 4.5:1.** The text in `.ipt`, `.textarea`, and `.select` was the intent's base color, which fell as low as 2.5:1 against the field: a `.warning` field in the light theme, and `.success`, `.destructive`, `.info`, and `aria-invalid="true"` fields in the dark one, worst on `.soft` and `.solid`. It is the intent's strong tone now, darker in light and lighter in dark, at 4.5:1 or better for every intent and presentation in both themes. The field's line, focus ring, and fill are unchanged. Native fields under `/native` take the same text. A field with no intent class takes the neutral strong tone too, so its text is a shade different from 0.6.
- **The package's NOTICE covers the checkbox marks' Feather origin.** The checkbox check and indeterminate minus come from Lucide icons derived from Feather, and NOTICE now carries Feather's MIT notice beside Lucide's ISC one.

<!-- Source: docs/changelog/0.6.0.md -->

# 0.6.0

A liquid-glass modifier, glass that keeps every presentation's fill whole, hoverable tooltips, native field decorations with an `.unadorned` opt-out, indeterminate checkboxes and list-box selects, and a round of fixes to tables, controls, the focus ring, right-to-left layout, and reduced motion on every entrypoint.

## Added

- **`.glass-liquid`**, a modifier of `.glass`: continuous corners, a bright rim, a lighter blur, a clearer ground, and a lens that bends what is behind a surface at its edges (Chromium today; Firefox and Safari keep the rim, blur, and ground). Put it beside `.glass` on the region, or on one surface inside a glass region; `.glass-solo.glass-liquid` paints the same pane on an element the package does not style. Without `.glass` in scope it changes the corners and nothing else. See [Aesthetics → Liquid glass](docs/usage/aesthetics.md#liquid-glass).
- **`.unadorned`** removes the browser's own field decorations -- the date picker button, number spin buttons, and search clear button and magnifier -- on a control or on its `.input-group`, for a field whose adornments you draw yourself. See [Forms → Browser decorations](docs/usage/forms.md#browser-decorations).
- **`[data-state="closed"]` on `.tooltip`** hides the bubble while it is hovered or focused, for an Escape handler, and **`--tooltip-linger`** (300ms) sets how long a bubble stays after the pointer leaves its trigger. See [Tooltips](docs/usage/tooltips.md).
- **An indeterminate `.checkbox`** (and a native one under `/native`) draws the checked plate with a dash for its mark, and a Highlight plate in forced colors. See [Forms → Toggles](docs/usage/forms.md#toggles).
- **A multi-row `.select`** -- `multiple`, or a `size` above 1 -- draws as a list box: no chevron, as tall as its rows, with the chevron's room given back. It used to keep the chevron and a one-row height.
- **`--ui-rule-ink`**, the neutral line color of a table's frame and rules, read ahead of `--ui-ink`. Glass sets it so a table's lines read on its plate; every other aesthetic clears it.

## Changed

- **Glass no longer thins fills.** It stopped setting `--ui-bg-alpha: 0.8`, so a `.solid` button, a checked toggle, and a `.solid` card are opaque under glass like everywhere else. Translucency comes from the surface ground alone. Glass buttons also take the base `scale(0.97)` press (it was `none`).
- **A neutral `.solid` rests on the page background.** Its capped 20% fill was painted over nothing and showed whatever was behind it; it is now an opaque grey. The generated `--palette-neutral-solid-bg` and `-bg-hover` values changed to match, in both themes.
- **Tables frost under glass and keep their lines.** A default or `.soft` `.data-table` blurs like a surface, its frame and rules read `--ui-rule-ink`, and a bare `<table>` under `/native` reads it too.
- **A table takes the surface corner and is never clipped.** The four corner cells round instead of an `overflow` clip, so a `<caption>` is untouched in every engine; a cell spanning rows into a bottom corner leaves that corner square. `.table-wrap` makes room for an aesthetic's halo (cyber's glow) inside its scroll box, so put the aesthetic class on the wrapper or an ancestor.
- **Table rules draw at the aesthetic's line width**, uncapped, so `.chunky-tile`'s 2px frame no longer sits over a 1px row rule and `.pixel` draws them at all.
- **The focus ring is pulled in by the focused element's line width.** `--focus-ring-offset` is no longer declared at `:root`; its default is computed on the element, so it lands on the edge under every aesthetic. Setting `--focus-ring-offset` yourself still wins, but reading `var(--focus-ring-offset)` in your own CSS now needs a fallback.
- **Ink aesthetics draw controls in the control border.** Under `.neobrutalism`, `.pixel`, `.cyber`, and `.sketch`, a resting neutral field or toggle uses `--color-control-border` instead of the near-primary ink. A bare native checkbox or radio under `/native` takes the control ink too, and a `.neutral` field under glass takes it like an unclassed one.
- **Text controls keep the browser's own decorations.** The date picker button, number spin buttons, and search clear button were hidden on every text control, which also removed what they do. They are back by default; add `.unadorned` to remove them.
- **Buttons and fields take the surrounding font on every entrypoint.** `<button>` under `/native`, and every `.btn` and text control under `/components`, used the user agent's smaller control font (13.33px), so the same label printed at three sizes across elements and entrypoints. They inherit the page's font now, as they already did on `.`.
- **A default `.btn` is `--control-height` tall**, 40px, the same as `.ipt` beside it; it was 42px, and 44px under a 2px aesthetic line. Its vertical padding gives back the line width.
- **`/tw` keeps Tailwind's `2xl` breakpoint.** It redefined `--breakpoint-2xl` as 90rem, which moved every `2xl:` utility in a consumer's own Tailwind build from 96rem. `--breakpoint-xs` (30rem) is still added. Nothing in the package used the override, so the compiled entrypoints are unchanged.
- **Components honour reduced motion and fix `<dialog>` on every entrypoint.** `/components` and the focused `/tw/*` entries carry no reset, so the skeleton and progress shimmer, the indeterminate sweep, and the tooltip, toggle, and press transitions kept moving under `prefers-reduced-motion: reduce`, and a `<dialog class="card">` rendered open on load. Each component now stops its own motion, and the `<dialog>` fix ships beside `.card`. The fix now reaches only a dialog carrying `.card`, `.panel`, or `.surface`, so a dialog the package does not style keeps its own margin.
- **A tooltip bubble is hoverable.** It lingers for `--tooltip-linger` after the pointer leaves the trigger and stays open while the pointer is over it, as WCAG 1.4.13 asks; it used to vanish on the way. A closed bubble is `visibility: hidden` rather than only transparent, so a bubble no `aria-describedby` points at is no longer read in place while closed.
- **`.alert-icon` sizes without `!important`.** It set its 1.25rem size `!important`, which no consumer rule could override. It sets `--ic-size` for `@codenhub/icons` glyphs and a plain size for any other icon now, so your own rule resizes it. An icon set that sizes itself in an unlayered rule under another knob keeps its own size.
- **The Tailwind CSS peer range is `>=4.3.2 <5`.** It claimed `>=4.0.0`, but the `/tw` source needs `@source not` and `@source inline()` (Tailwind 4.1) and is tested against 4.3.2. The peer is still optional, and only `/tw` needs it.
- **Browser floor is Firefox 128.** Non-inheriting elevation relies on `@property`; the documented floor was Firefox 121.

## Fixed

- **A switch's knob sits inside `.pixel`'s inset ring** instead of covering it, so an unchecked and a checked switch no longer read as two halves.
- **The sketch solo shadow is cast in the depth colour** rather than full ink.
- **No stray `.contents` rule.** The compiled `.`, `/native`, and `/components` stylesheets shipped Tailwind's `.contents{display:contents}`, picked up from the license text; the build no longer scans the package's root files.
- **Links under `/native` keep the size of their text.** `a` was mapped to body copy, so a link inside a heading printed at 14px and regular weight.
- **The Tailwind guide no longer imports Tailwind twice.** `/tw` imports Tailwind itself, so it replaces your own `@import "tailwindcss"`; with both, an unminified build carried two copies of Preflight. The focused `/tw/*` entrypoints and `/tw/native` still expect your import first.
- **`.tooltip-icon` keeps its size on a `<button>`.** The docs now use a real `<button type="button">` as the trigger, since `aria-label` on a role-less `<span>` is not reliably announced; the chip resets the padding and minimum size a button brings under `/native` and `/components`. The docs also no longer call it circular under every aesthetic: it is square under `.neobrutalism` and `.pixel`.
- **A wrapped `.hint` has room between its lines.** Its line height equalled its 14px size, so wrapped lines touched; it takes `text-sm`'s 20px leading now.
- **`/palette` follows a light subtree nested in a dark one.** Its light values sat on `:root` alone, so a `.light` region inside `.dark` kept the dark values. They answer the explicit light selectors too now.
- **An unchecked switch's knob is `CanvasText` in forced colors.** The switch opts out of forced colors to keep its states apart, and its unchecked knob kept the package grey on the system `Canvas`.
- **A select's chevron mirrors under `dir="rtl"`.** It and the padding kept for it stayed on the right, so a right-to-left value started beside the chevron.
- **`.btn.icon` follows the size step.** `.sm` left an icon button at the default 40px, and under WebKit a `.lg` one was 40px wide and 48px tall. They are square at 32px and 48px now.
- **Tables align to the inline start.** `.data-table` cells and its caption used `text-left`, so a right-to-left table stayed left-aligned.
- **A region of one aesthetic nested inside another keeps its own material.** Every aesthetic now restates the depth colour and clears the focus inset, hover transform, and shadow tokens it does not use, so glass inside `.chunky-tile` no longer casts a black slab under every card, glass inside `.cyber` casts its shadow again, and a region inside `.pixel` no longer inherits pixel's inset focus layer.

<!-- Source: docs/changelog/0.5.0.md -->

# 0.5.0

A foundation update with an opt-in sketch aesthetic and rounded sketch variant. Everything under [Changed](#changed) and [Removed](#removed) breaks against `0.4.0`.

## Changed

- **The package sits in named cascade layers.** Every entrypoint declares Tailwind's layer order first and places everything in it: foundation tokens and the palette in `theme`, resets in `base`, the intent, presentation, elevation, and aesthetic classes in `components`, and every component in `utilities` -- including on `/components` and `/native`, which emitted them unlayered. Your own utility or unlayered CSS now beats all four class families on the same element, and an aesthetic on `<html>` keeps its tokens over the theme's in any load order. CSS that relied on a package class beating it now loses to it, and a rule of yours in `@layer components` contests the class families by specificity instead of losing outright. See [Setup](docs/setup.md).
- **Aesthetics set `--ui-corner`, not `--ui-radius`.** The corner is now a length at the default size that scales with the element (below); `--ui-radius` and `--ui-radius-surface` take a complete radius used as written, and every shipped aesthetic sets `--ui-corner` and `--ui-corner-surface` instead. If you set `--ui-radius` yourself, nothing changes for you. An aesthetic of your own moves to `--ui-corner` to get the scale and the corner pattern. `--ui-radius-pill` and `--ui-radius-tight` fall back through `--ui-radius`, then `--ui-corner`.
- **`--ui-button-weight` and `--ui-button-tracking` are `--ui-label-weight` and `--ui-label-tracking`**, read by `.badge` as well as `.btn`. The old names are removed without an alias. Under `.chunky-tile`, badges now take the heavier label too.
- **Explicit chip radii are no longer capped.** Checkbox, `.kbd`, and `.code` keep a small default radius when `--ui-radius-tight` is unset, but a supplied value now applies as a complete `border-radius`, including elliptical shapes. A consumer setting a large value now gets the large corner it asked for instead of the previous `--radius-small` ceiling.
- **`.cyber` glows everything, flat or not.** Its glow moved from depth onto the new halo, so fields, chips, badges, and alerts glow too, `.flat` no longer puts the glow out, and `.raised` and `.floating` add no shadow under it.

## Removed

- **Alias classes removed**, each a second name for a class that already had one: `.dense`, `.compact`, and `.spacious` (use `.p-xs`, `.p-sm`, and `.p-lg`); `.danger` and `.error` (use `.destructive`, and `.hint.destructive` for `.hint.error`); and `.elevation-none`, `.elevation-sm`, and `.elevation-md` (use `.flat`, `.raised`, and `.floating`). `.theme-light` and `.theme-dark` stay.

## Added

- **`.sketch`, `.sketch-rounded`, and `.sketch-solo`**: a hand-drawn treatment with uneven elliptical corners, a solid ink line (dashed checkboxes and rules under `.sketch`), a small offset shadow, and an optional consumer-supplied handwriting font. Sibling components rotate between four stable outlines; pill buttons take organic hand-drawn rounded pill contours without oval distortion; switch tracks, knobs, and chips take compact irregular box outlines, and radios stay subtly circular. The opt-in `.sketch-rounded` modifier class provides subtle, organic hand-drawn rounded contours across controls, surfaces, chips, and solo elements with 4-step sibling rotation and no egg-shaped distortion on wide elements. The solo class paints an unstyled element directly; the cascading class restyles package components. Both ship in the aesthetic aggregate and dedicated compiled and Tailwind CSS entrypoints. See [Aesthetics](docs/usage/aesthetics.md#sketch).
- **Corner scale.** `.p-xs` sets a size step of `0.5`, `.sm` and `.p-sm` `0.75`, and `.lg` and `.p-lg` `1.25`, and the corner is multiplied by it; when a size and a padding class sit on one element, the smaller step wins. A bevel stays a true 45 degrees, so `.cyber`'s 25% cap is gone. Depth follows the step down, never up: a `.p-xs` button sits on half its aesthetic's shadow, which also retires chunky tile's one selector list. `--ui-scale` overrides the step. A press that travels into the depth -- `--ui-active-translate-x` and `-y`, which chunky tile and neobrutalism now set instead of a translating `--ui-active-transform` -- scales the same way, elevation included, so it always lands flat on the slab, and a `.flat` button under either no longer drops. See [Customizing → Corner size and pattern](docs/usage/customizing.md#corner-size-and-pattern).
- **Corner pattern.** `.cut-diagonal` keeps the top-left and bottom-right corners and squares the other two, `.cut-diagonal-reverse` the opposite pair, wherever the corner is computed -- on a control under `.cyber`, but not a surface, which keeps its own shape. Both set `--ui-corner-tl`, `-tr`, `-br`, and `-bl`, which you can set yourself.
- **`--ui-line-style`**: `solid`, `dashed`, `dotted`, or `double` for every line a component draws -- the box's edge, the progress track, and dividers and table rules. `--ui-rule-style` overrides only dividers and table rules.
- **A halo**, `--ui-halo-blur`, `--ui-halo-spread`, and `--ui-halo-ink`: light in the element's own intent colour, which elevation does not scale and `.flat` does not remove.
- **`--ui-surface-image`**, a painted layer over a surface's plate -- cards, panels, alerts, and tooltips, not controls. Your own `background-image` on a surface still wins.
- **`--ui-label-case` and `--ui-label-shadow`**, beside the weight and tracking: the label's `text-transform`, and a `text-shadow` written without a colour so it takes the label's own.
- **`--ui-control-ink`**, a neutral line for controls read ahead of `--ui-ink`. Glass and chunky tile set it, so an unchecked checkbox under glass (1.33:1 before) and a field under chunky tile (1.65:1 before) draw a boundary that reads.

Under forced colours, each new part draws its plain version: a solid line, no halo, no painted layer, and no label shadow.

## Fixed

- **A stylesheet survives being inlined.** The loader art's SVG data URIs carried a literal `</style>`, so a stylesheet pasted into an HTML `<style>` element ended at the first loader and silently dropped every rule after it. It is now percent-encoded.
- **Every entry ships the theme it promises.** `/components`, `/tw/components`, `/tw/feedback`, and `/tw/tooltip` shipped no colour token at all, and every other focused `/tw` component entry lost the palette ramp the tokens are built from. Each entry now declares every colour it reads, and `/components` works alone.
- **A line fades into a translucent plate instead of ringing it.** A neutral `.solid.edged`, and any plate `--ui-bg-alpha` thins, drew a ring 1.54:1 against its own fill; the line now fades toward transparent as the fill fills in. The generated palette's edge tokens follow.
- **A select's options read in the dark theme.** The open list is painted over the select's own background, and a ghost `.select` has none, so Chromium drew its default white popup under options that inherited the near-white text. Options and option groups now take the page's ground and text colour.
- **A region nested inside glass keeps its own surfaces.** Neobrutalism, pixel, chunky tile, and cyber left glass's translucent ground, fill alpha, and two-layer surface shadow in place, so their cards inside a glass region came out as half glass. Every aesthetic now clears them.
- **Surfaces under glass stay glass.** A neutral `.card.soft`, `.panel`, and `.alert` sat opaque inside a glass region, and `.card.soft.glass` did on the same element; their quiet ground is now a private default beneath `--ui-surface-ground`.

<!-- Source: docs/changelog/0.4.0.md -->

# 0.4.0

## Added

- **`.cyber`**, a fifth aesthetic from `@codenhub/styles/aesthetics/cyber` (and `/tw/aesthetics/cyber`, and both aggregates): bevelled corners, a 1px edge, and a glow in each component's own colour. Controls cut top-left and bottom-right, surfaces top-right and bottom-left; what is fully round (a radio, a switch, a pill, a badge) cuts to diamonds and pointed hexagons, and chips square. Knobs: `--cyber-cut`, `--cyber-shape`, `--cyber-shape-surface`, `--cyber-glow`, `--cyber-ink`, and the consumer-supplied `--font-cyber`. The bevel is drawn with `corner-shape`, which only Chromium supports today; Firefox and Safari get square cut corners and round round ones. See [Aesthetics → Cyber](docs/usage/aesthetics.md#cyber).
- **A solo class for every aesthetic**: `.glass-solo`, `.neobrutalism-solo`, `.pixel-solo`, `.chunky-tile-solo`, and `.cyber-solo` paint the aesthetic's look onto the one element carrying the class, for an element this package does not style -- a toast from another library, or a `<div>` on a page without the base stylesheet. Each ships in its aesthetic's existing entrypoint and needs nothing else loaded. See [Aesthetics → Solo classes](docs/usage/aesthetics.md#solo-classes).
- **Three material tokens**: `--ui-corner-shape` (what a corner draws: `round`, or a `bevel` cut), `--ui-radius-pill` (the corner of what is fully round by default), and `--ui-radius-tight` (the corner of chips, before their `--radius-small` cap). All three default to today's look, so nothing changes unless an aesthetic or your own CSS sets them. See [Customizing → Material tokens](docs/usage/customizing.md#material-tokens).

<!-- Source: docs/changelog/0.3.0.md -->

# 0.3.0

## Added

- **`@codenhub/styles/palette`**, a generated, self-contained stylesheet of every `intent x presentation` cell's composed colors -- `--palette-<intent>-<presentation>[-<ground>]-<slot>[-hover]` custom properties, already-composited light and dark values, for a consumer that cannot take this package as a build-time dependency (an optional peer, for example) but wants to look consistent with it. Not a replacement for the components themselves when Tailwind is available. Also includes `--palette-border`/`--palette-surface`/`--palette-text`, flat values for the three neutral surface tokens that sit outside the intent x presentation cross. See [Customizing → Generated palette](docs/usage/customizing.md#generated-palette).

## Changed

- **`.progress`'s track reads presentation.** It used to be intent-only, like `.loader`, `.skeleton`, and `.divider`; now it composes a fill and an edge like `.quote` does, while the moving value fill stays `--intent-color` at full strength, unaffected by presentation. The registered default is `.soft.edgeless`, chosen to stay closest to the previous fixed-neutral look, but it renders differently than before: it now tints by intent instead of a fixed grey, and every existing `.progress` gets that new look with no markup change. `.solid` is unsupported on the track (a fully-filled track and the value fill it carries would become the same color); `.ghost.edgeless` renders but is discouraged, since it leaves the track with neither fill nor line.
- **Known issue: the new `.progress.soft.edgeless` default does not meet WCAG 1.4.11's 3:1 non-text contrast floor.** Measured at roughly 1.1-1.3:1 against the page, in both themes and across intents -- down from the ~3.6-4.3:1 the prior fixed-neutral track cleared. An `.edged` presentation restores a boundary through the line for most intents, but not all of them clear 3:1 there either (`warning.soft.edged` measured ~2.7:1). A consumer whose track must meet 1.4.11 should choose an `.edged` presentation and verify the intents they use. See [Accessibility](docs/accessibility.md#control-boundaries).

<!-- Source: docs/changelog/0.2.0.md -->

# 0.2.0

Fixes from a stress-test pass composing real screens (`app-shell`, `form`, `settings`) against the published `0.1.x` model. Several change default token values rather than adding new surface, so this lands as one minor rather than a run of patches -- see [Changed](#changed) for what an upgrader needs to check.

## Added

- `.visually-hidden`, a utility that hides content from sighted users while keeping it in the accessibility tree. Ships wherever the layout utilities do (`.`, `/native`, `/tw`, `/tw/native`, `/tw/utilities`), including the vanilla entries that have no Tailwind utility layer of their own; not part of `/components` or the narrower `/tw/*` component slices.
- `.tooltip-bubble`, a real element for a tooltip's message that can carry its own `id` and be referenced by `aria-describedby`.
- `.alert-icon`, sizing and alignment for a consumer-supplied icon element dropped into `.alert` -- any inline SVG, `<img>`, or icon-set class works, not just a built-in glyph.
- `dialog:not([open])`/`dialog[open]` restated in author origin, so `.card` composed onto a `<dialog>` hides until opened and centers when shown, without a dialog primitive of its own.
- `.icon` added to `registry.json` under `modifiers.shape`. The button shape modifier was already shipped and documented; it was missing only from the machine-readable contract.
- `.switch.ghost.edgeless` now renders a track instead of disappearing.

## Changed

- **Tooltips are composition, not a single class.** `.tooltip` now wraps a trigger and a `.tooltip-bubble`, one more DOM node than before. Intent, presentation, aesthetic, and elevation classes move to `.tooltip-bubble`, the element that actually paints.
- **`.checkbox`, `.radio`, and `.switch` default to `.soft` instead of `.solid`, and their checked state now composes one plate regardless of presentation.** Unchecked, `.ghost` (0% fill), `.soft` (12%), and `.solid` now read as three distinct plates instead of three shades of one wash; `.ghost` is a supported look on every toggle for the first time.
- **A neutral (no named intent) `.card.soft`, `data-table`, `.panel`, or `.alert` rests untinted instead of mixing a 12% ink wash.** A named intent (`.card.soft.destructive`, `.alert.success`, and so on) is unaffected and keeps its ordinary tint; `.badge` is unaffected either way, since it has no border to fall back on if untinted.
- **Elevation no longer cascades onto descendants.** `--ui-elevation`, `--elevation-y`, and `--elevation-blur` are registered non-inheriting, so an elevation class lifts only the element it is written on. A container that used to lift a whole unclassed subtree in one class now needs an elevation class on each element that wants depth.
- `--input-group-gap` replaces a hardcoded gap in `.input-group`; unchanged by default for icon compositions, and settable to `0` on a text affix for a flush look.
- `.loader`'s sizes step down from 24/32/40px to 20/28/36px for `.sm`/default/`.lg`.
- A `/tw` consumer's own `dark:` utilities now also fire under `prefers-color-scheme: dark` when no explicit theme selector is present, matching how the token palette already follows the OS default.

## Removed

- **`.btn.loading`.** A loading button is now a disabled `.btn` composed with a real `.loader` child instead of a hardcoded `::after` spinner -- any loader variant works, not just one circular spinner.
- **`.alert.icon`'s hardcoded per-intent SVG mask.** Replaced by `.alert-icon` (above), sized for a consumer-supplied icon element.

## Fixed

- `.icon` buttons kept the wrong `min-width` floor under `.compact`/`.spacious`; all three padding tiers now size `.icon` to its own padding.
- `.chunky-tile`'s offset shadow read as a solid fill behind a small `.icon.ghost` button; halved for that combination.
- Table row and boundary rules stayed a 1px hairline under `.chunky-tile`'s 2px frame; both now track the same width token the frame uses.
- `.progress`'s track failed WCAG 1.4.11 in both themes; `--progress-surface` now mixes enough ink to clear 3:1.
- `.card.soft.edged`'s border was nearly invisible against its own fill in light theme; `--color-border`'s light value moved one step darker.
- A badge nested inside an untinted `data-table` lost its own fill; box now restates its fill cap on every element that uses it, so a nested badge, button, or chip no longer inherits its container's cap.

<!-- Source: docs/changelog/0.1.1.md -->

# 0.1.1

## Fixed

- `.p-xs` and its alias `.dense` now clear a button's `--control-height` minimum, so a chip-sized button fits inside a table cell or a dense toolbar. Before this, every button kept the full control-height floor even at the tightest padding, so a button that small could not be built. Every other size and padding class still keeps the floor, so a plain button is unchanged. An icon-only `.p-xs`/`.dense` button also gives up its `min-width`; size the glyph yourself, as the package sizes no icons.

<!-- Source: docs/changelog/0.1.0.md -->

# 0.1.0

`0.1.0` replaces the model published in `0.0.4` wholesale. The old model had three orthogonal axes with per-component clamps and no chosen default: a plain element got whatever `var()` fallback each component happened to declare. This release makes each decision explicitly and is the baseline the `0.x` line builds on. Nearly everything below is breaking against `0.0.4`; there are no compatibility aliases.

## Changed

- **Every element now composes from three named axes.** _Intent_ (`.primary`, `.success`, `.destructive`, …) sets hue only. _Presentation_ sets how much of that hue fills the element and whether it draws an edge. _Aesthetic_ sets the material — radius, edge width, shadow, silhouette, type. Intent is redeclared per component; presentation and aesthetic cascade, so a container can set the look of a subtree while any element opts out. [Concepts](docs/concepts.md) and [Composing](docs/usage/composing.md) describe the full model.
- **Presentation is two independent questions.** Fill is `.solid` / `.soft` / `.ghost`; edge is `.edged` / `.edgeless`. Exactly one of each applies, and they never decide each other. The common web button — a subtle fill with a border — is `.soft.edged`, which had no spelling before.
- **`.bare` is renamed `.ghost`.** `.bare` and `.edgeless` both read as "less of something", so the name is now a fill and only a fill: no fill at rest, a tint on hover.
- **Hover is derived, not declared.** A hovered element takes its resting fill plus one published step and its `--intent-hover` colour. There are no hover tokens to set. An outline button therefore tints on hover rather than filling completely.
- **A sunk text field is `.soft.edgeless`.** Two classes saying two things, where `.soft` alone used to silently suppress the field's border.
- **Table row rules are opt-in via `.ruled`.** Rules no longer arrive by implication from a component's edge default. The head and foot separators stay at every presentation.
- **Depth is a modifier, not part of a component.** `surface` no longer carries a structural shadow, so cards, buttons, and chips on a plain page rest flat. Use `.raised` / `.floating` (or `.elevation-sm` / `.elevation-md`) to add depth, and `.flat` to remove an aesthetic's.
- **The Tailwind v4 entrypoint moved from `/overrides` to `/native`.**

## Added

- **Opt-in aesthetics.** `.neobrutalism`, `.glass`, `.pixel`, and `.chunky-tile`, each shipped as its own entrypoint (`@codenhub/styles/aesthetics/pixel`, …) and as an aggregate (`@codenhub/styles/aesthetics`). An aesthetic sets material tokens only; it never repaints hue. See [Aesthetics](docs/usage/aesthetics.md).
- **An elevation modifier.** `.flat` / `.raised` / `.floating` (and the `.elevation-none` / `-sm` / `-md` names) multiply an aesthetic's shadow geometry. Unitless, so a container can flatten a whole toolbar and any element can opt back in.
- **Granular Tailwind v4 source entrypoints.** `@codenhub/styles/tw` and per-family subpaths (`/tw/button`, `/tw/form`, `/tw/feedback`, `/tw/tooltip`, `/tw/utilities`, `/tw/aesthetics/*`, …) for consumers compiling their own build. `package.json` is also exported.
- **`.input-group`.** A wrapper that draws one box and lets the control inside give up its own paint, so an icon can sit alongside a field. The package ships no icons — any element works in the slot.
- **`registry.json`.** A machine-readable record of every supported class, each component's default fill/edge/elevation, the axes it reads, and every bound and exception with its reason.
- **`--intent-strong`**, the intent tone chosen to be read on a page, used wherever a fill is partial.

## Removed

- **`.out` and `.fill`.** Outline is now `.ghost.edged`; there is no class that makes an outline button fill completely on hover.
- **`--ui-border-scale` / `.out`'s doubled border.** The edge width comes from the aesthetic and nothing else, which removed the per-component border ceilings small components needed.
- **`--color-text-subtle`**, which nothing read.
- **`--layout-stack-gap` and `--layout-cluster-gap`**, replaced by a single `--layout-gap`.
- **The bundled input-field icons.** Icons were a `background-image` data URI with a light and a dark copy per glyph; they are gone, and `.input-group` carries a consumer-supplied icon instead.
