<!--
  Generated from docs/ — do not edit here; run `pnpm regen`.
  Canonical: https://cascivo.com/docs/ai-rules.md
  registry v1.6.0 · generated 2026-10-02
-->

# AI rules for building with cascivo

Drop this into your AI agent's system prompt, Cursor rules (`.cursor/rules`), or
`AGENTS.md` / `CLAUDE.md` so it generates CSS that keeps cascivo's cascade intact.
`cascivo create` scaffolds an `AGENTS.md` with the same contract automatically; this
page is for **existing** projects.

## Looking things up (copy-paste)

Add this first. The contracts below tell an agent the rules; this tells it where to
check anything the rules don't cover — against the versions you actually installed,
rather than whatever it remembers or whatever cascivo.com documents today.

Install the docs as a local search index:

```sh
pnpm add -D @cascivo/docspack docspack
npx docspack sync
```

Then give the agent one line:

```md
Run `docspack ask "<question>"` for documentation on this project's
dependencies, including cascivo. It answers offline from the installed
versions. Prefer it over recalled knowledge and over anything fetched from
cascivo.com, which documents the latest release rather than ours. Ask a
question, not a name — `docspack ask "verify a theme is applied"` beats
`docspack ask "theming"` — and prefer several narrow questions over one
broad one, because an answer is capped at three chunks.
```

Answers are bounded (three chunks, ~3,000 tokens), so asking costs a fraction of
fetching the page that holds the answer. Without an index, `https://cascivo.com/llms.txt`
and `npx -y @cascivo/docs` are the read-everything fallbacks.

## The CSS layer contract (copy-paste)

```md
## cascivo CSS layer contract

cascivo styles live in CSS cascade layers. Layer order beats selector specificity, so
follow these rules whenever you generate or edit CSS:

1. Every declaration goes inside an `@layer` block. Unlayered CSS beats all layers
   regardless of specificity — never emit it.
2. Never invent layer names. Write only: your app's own slot for page/app styles, and
   `@layer cascivo.override { … }` for hotfixes / one-off overrides — it beats
   everything cascivo ships. Your app slot goes **between `cascivo.blocks` and
   `cascivo.override`** in the order statement below (rename `cascivo.myapp` after your
   app) — high enough to beat cascivo's components, themes and blocks, low enough to
   leave `cascivo.override` as the escape hatch.
3. Never nest layers deeper than the shipped `cascivo.blocks.<name>` pattern. For
   sub-elements (a badge in a card, a dot in a badge) use native CSS nesting inside one
   layer block, not new sublayers.
4. Third-party CSS: `@import url('lib/styles.css') layer(vendor);` with `vendor`
   declared before the cascivo layers. Don't import vendor CSS from JavaScript — route
   it through a CSS file, or use `@cascivo/vite-plugin` (`cascivoLayers`) to layer it.
5. Style with `--cascivo-*` tokens, not raw values.

Canonical layer order (lowest → highest priority):
`@layer vendor, cascivo.reset, cascivo.base, cascivo.tokens, cascivo.component, cascivo.platform, cascivo.theme, cascivo.blocks, cascivo.myapp, cascivo.override;`

The full machine-readable guide is at https://cascivo.com/llms.txt.
```

## The reactivity contract (copy-paste)

The CSS contract above keeps the cascade intact; this one keeps the reactivity model
intact. Drop it into the same agent rules file. Without it, an agent defaults to
`useState`/`useEffect`/`useContext` and concludes cascivo "has no state story" — the exact
mistake that makes a signal-native system look layout-only.

```md
## cascivo reactivity contract

cascivo is signal-driven. Do not mix React state with signals — it causes toggles that
don't toggle and UIs that freeze. Reach for the cascivo primitive, not the React hook.

**Import every primitive below from `@cascivo/react`** if you installed the prebuilt
package, or from `@cascivo/core` if you copied component source in with the CLI. Never add
`@cascivo/core` to a prebuilt-path app's package.json — it is only a transitive dependency
there, and everything is re-exported from `@cascivo/react`.

1. Local state -> `const [count, setCount] = useSignalState(initial)`: read `count.value` in
   render, write with `setCount(next)` or `setCount((n) => n + 1)`. Derived ->
   `useComputed(fn)`. Never `useState`. The signal IS the state.
   > **Write through the setter, never `count.value = next` in a component.** Assigning to a
   > value returned from a hook fails the React Compiler build ("This value cannot be
   > modified") and is reported by `react-hooks/immutability`, which
   > `eslint-plugin-react-hooks@7` turns on by default. The setter form passes both — checked
   > by `pnpm compiler:check`. Plain `useSignal` still works, but its writes carry that cost;
   > see [USING-WITH-STRICT-ESLINT.md](/docs/using-with-strict-eslint.md). Writing
   > `.value` on a **module-level** `signal()` (rule 3) is fine — it is not a hook value.
2. Side effects (DOM, listeners, `showModal()`) -> `useSignalEffect(fn)`. Never `useEffect`.
3. Shared/app-wide state -> a module-level `signal` imported anywhere. Never `useContext`.
4. A controlled/uncontrolled prop bridged to a signal ->
   `useControllableSignal({ value, defaultValue, onChange })`.
5. Avoiding signal leaks across route/tenant changes -> `useScope()` / `createScope()`
   (disposes owned effects on unmount).
6. Forms -> `createForm` / `useForm` / `<Form>` / `field()` (`@cascivo/react`): signal store,
   sync/async + Standard Schema (zod/valibot) validation, optional `validateOnChange`.
7. Theming (light/dark toggle, SSR no-FOUC) -> `<ThemeProvider>` + `useTheme()` /
   `setTheme()` + `themePreloadScript()` (`@cascivo/react`). `useTheme()` returns a **tuple**
   `[theme, setTheme]` where `theme` is a plain **`string`** (the current theme name) — use it
   directly (`theme === 'dark'`), never `theme.value`, and never destructure `{ theme, setTheme }`
   (that is next-themes' shape, not cascivo's). The hook calls `useSignals()` for you, so the
   component re-renders on theme changes with no signal handling. For signal-native code
   (`computed()`/`effect()`/Preact) use `themeSignal()` instead. A controlled
   `<ThemeProvider value=…>` is SSR-safe by itself (emits an inline attribute setter). Never a
   `useEffect` that adds a `.dark` class.
8. Token names in TypeScript -> `import type { CascivoToken, CascivoColorToken } from
'@cascivo/tokens/tokens'` (generated union — no CSS-file lookup).
9. `useSignals()` is needed ONLY for a signal you did not get from a cascivo hook: a
   module-level `signal()`, a signal passed in as a prop, or `currentLocale()`. Call it as
   the component's first statement. Signals returned by `useSignalState`, `useSignal`, `useComputed`,
   `useDisclosure`, `useMachine`, `useTheme` and the rest subscribe you already — do not
   sprinkle `useSignals()` everywhere. Symptom of getting this wrong: handlers fire, the
   UI never moves, no error.
10. Mirroring a controlled prop into a signal -> `useControllableSignal` when you read it
    in render; `useEffectPropSignal` when it is read only inside `useSignalEffect`. Never
    hand-roll `s.value = prop` for the effect case: signals run effects synchronously on
    write, so that runs the effect body during React's render phase.

Full catalogs: docs/HEADLESS.md (primitives) and docs/ENTERPRISE-READINESS.md (friction map).
```

## Event-handler naming

cascivo names change/activation callbacks by **what the handler receives**, so you can
predict the prop without checking the types:

| Handler receives                                                                       | Prop name                  | Examples                                                                                               |
| -------------------------------------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------ |
| The component's **value** (string / number / array / boolean / Date — not a DOM event) | **`onValueChange(value)`** | `Tabs`, `SegmentedControl`, `Combobox`, `MultiSelect`, `Toggle`, `Search`, `NumberInput`, `DatePicker` |
| A raw **DOM `ChangeEvent`** from a real underlying element                             | **`onChange(event)`**      | `Checkbox`, `NativeSelect`, `PasswordInput`, `Select`, `Slider`                                        |
| **Activation / selection** of a discrete item                                          | **`onSelect(value)`**      | `Dropdown`, `OverflowMenu`, `MenuItem`, `ContextMenuItem`, chart point clicks                          |
| A raw DOM click passthrough                                                            | **`onClick(event)`**       | nav items, buttons                                                                                     |

Rule of thumb when authoring or generating: **if your handler's first argument is a value,
name it `onValueChange`; if it's a DOM event, name it `onChange`.** The rule now holds with
no exceptions — the value-carrying `onChange` aliases that eight components once accepted
(`Combobox`, `DatePicker`, `Filter`, `NumberInput`, `Search`, `Swap`, `TimePicker`, `Toggle`)
were removed at 1.0.

> **These eight `Omit` the DOM `onChange` from their prop types and do not add it back.**
> That is deliberate: without the `Omit`, `onChange` would silently become React's
> `ChangeEventHandler`, so code passing a value-carrying handler would compile and then be
> called with an event. With it, `onChange` is a compile error naming `onValueChange`.

> **`Select` and `Slider` are native-element wrappers**, not composite components: they
> spread onto a real `<select>` / `<input type="range">` and carry only the DOM
> `onChange(event)`. Read the value off `event.target.value`. They were previously listed
> in the `onValueChange` row, which was wrong and cost an adopter a build cycle —
> `scripts/checks/handler-naming-parity.test.ts` now fails the build if this table names a
> handler a component does not have.

> **`onSelect` on menus lives on the item, not the menu.** `MenuItem` / `ContextMenuItem`
> take `onSelect: () => void` (the item already knows which item it is). `Dropdown` and
> `OverflowMenu` are config-driven, so their root takes `onSelect(value)`.

## Accessible-name and item-identity props

The sibling of the handler rule: name a prop by **what it is**, not by the component.

| The value is                                                                                       | Prop name                                         | Examples                                                               |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------- | ---------------------------------------------------------------------- |
| Text the component **renders**                                                                     | **`label`**                                       | `Field`, `Checkbox`, `Radio`, `Toggle`, `Slider`                       |
| An **invisible** accessible name for an icon-only control or a nav landmark (goes to `aria-label`) | **`ariaLabel`**                                   | `OverflowMenu`, `SideNav`, `Breadcrumb`, `Dock`, `Steps`               |
| The identity of an item that is **handed to a callback**                                           | **`value`**                                       | `OverflowMenu`, `Dropdown`, `Select`, `Combobox`                       |
| A **React key** for an item — never passed anywhere                                                | **`id`** (rows/items) / **`key`** (table columns) | `CommandMenu`, `StructuredList`, `Timeline`, `DataTable.columns[].key` |

**`ariaLabel` and `label` are two spellings of one idea, and every component that accepts one
accepts the other.** That is the whole rule, and it is mechanically true rather than merely
documented: `scripts/checks/aria-label-universality.test.ts` fails the build if a component
takes an invisible name under only one spelling. `ariaLabel` stays the preferred name in new
code — it says "invisible" out loud — but `label` is the guess an adopter makes before they
have read this page, and a guess that compiles costs nobody anything. `<OverflowMenu
label=…>` was a type error until 0.19 and is not any more (2026-08-21 report item 1).

Two components predate the rule and **require** the name, so they type it as an XOR union:
`IconButton` and `Fab` accept exactly one of `label` / `ariaLabel`, because a control with no
accessible name is a bug the compiler should catch. `Sparkline` does the same.

Two components are exempt, with reasons recorded in the guard: `DataTable`'s visible name is
`title` (a caption), and `Menubar`'s required name is an XOR of `ariaLabel` / `aria-label`.

Every component that took only the DOM spelling `aria-label` now accepts **both**
`ariaLabel` and `aria-label` — two spellings of one idea inside one package was a coin flip
on every component. That covers `Filter`, `StructuredList`, `Progress`, `Menubar`,
`NavigationMenu`, `TreeView`, `Swap`, `RadialProgress`, `SplitView` and `StatsBand`. Where
the name is **required** (`Menubar`, `IconButton`), an XOR union enforces that exactly one
is present, so the a11y guarantee survives the alias.

**`value` vs `id` is a real distinction, not an inconsistency.** `value` is the identity the
component _hands back to you_ — `onSelect(value)`. `id` is a React key the component uses
internally and never passes anywhere: `CommandMenu`'s items take `onSelect: () => void`, so
their `id` could not be delivered even in principle. Reach for `value` when a callback
receives it, `id` when it is only identity. `OverflowMenu` items additionally accept **`id`**
as an alias of `value`, because it is the common wrong guess by analogy with `CommandMenu`.

Guessing across components is the failure this prevents: an adopter wrote
`<OverflowMenu label=… items={[{ id, label }]}>` by analogy with `CommandMenu` and
`IconButton` and got two type errors — `OverflowMenu` takes `ariaLabel` and `value`. The
per-component pages were correct; the cost was that the convention was never stated.

## Data and shape props — the vocabulary an agent has to guess

The two tables above cover _handlers_ and _names_. This one covers the props that carry the
**data and the look**, which is where a 2026-08-08 adopter lost nine compile cycles in one
small dashboard — the single largest friction in that report.

| The prop carries                                 | Prop name                                                                  | Never                                 | Why                                                                                                                                                                                                                                                                                                                        |
| ------------------------------------------------ | -------------------------------------------------------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A config-driven **collection** (nav, list, menu) | **`items`**                                                                | `rows`, `entries`                     | Nav, list and menu components. The full, current list is generated into [`llms.txt`](https://cascivo.com/llms.txt) from the registry — it is not repeated here, because the hand-written version of this row named `Steps` and `CommandMenu` under `items` when they take `steps` and `groups` (2026-08-22 report item 10) |
| The **choices** on a form control                | **`options`**                                                              | `items`, `choices`                    | `Select`, `NativeSelect`, `Combobox`, `MultiSelect`, `Filter`, `SegmentedControl`, `WheelPicker`. A choice control takes `options`, not `items`                                                                                                                                                                            |
| A **chart's** data points                        | **`data`**                                                                 | `items`, `series`                     | Every chart — `PieChart`, `Sparkline`, `Heatmap`, `CalendarHeatmap`, … (`series` is the _grouping_ prop on multi-series charts, not the data)                                                                                                                                                                              |
| The rows of a **table**                          | **`rows`**                                                                 | `items`                               | `DataTable` only — it renders a `<table>`, where "rows" is the domain word, not a synonym for items. (`Textarea.rows` is the HTML attribute, not a collection.) Steppers keep the domain word too: `Steps.steps` and `ProgressIndicator.steps` — `Steps` also accepts `items`                                              |
| A **visual style** enum                          | **`variant`**                                                              | `shape`, `kind`, `type`, `appearance` | `Badge`, `Tag`, `Button`, `Alert`, `Card`, `Notification`                                                                                                                                                                                                                                                                  |
| The **tag of a discriminated union**             | **`kind`**                                                                 | `type`                                | `AreaChart.annotations[].kind`, and every new union — `type` is reserved for HTML-ish meanings (`input type`, edge/node renderer keys)                                                                                                                                                                                     |
| A **space-scale step**                           | numeric **`gap={4}`**                                                      | `gap="4"`                             | `Flex`, `Grid`, `AutoGrid`, `AppShell.padding`. ⚠ See the warning below — this is the one that breaks the pattern                                                                                                                                                                                                          |
| A **rich, replaceable slot**                     | **`actions`** (`ReactNode`)                                                | `action={{ label, onClick }}`         | `Notification`, `CardHeader`, `PageHeader`. `Alert.action` is the one `{label,onClick}` shorthand left; it is not the pattern to copy                                                                                                                                                                                      |
| The **body text** of a feedback component        | **`description`**                                                          | children                              | `Notification`, `Alert`, `EmptyState` — passing children renders nothing                                                                                                                                                                                                                                                   |
| **Supporting text under a form control**         | **`hint`**                                                                 | `description`                         | `Input`, `Textarea`, `Select`, `NumberInput`, `Combobox`, `DatePicker`, `TimePicker`, `FileUploader`. `Field` predates the split and takes `description`; it accepts `hint` as an alias, so either guess compiles                                                                                                          |
| A **visible** text label                         | **`label`**                                                                | `title`, `text`, `caption`            | The default — most components that take `label` render it on screen (`Toggle`, `Checkbox`, `Input`, `Slider`, `Stat`, `Kpi`, …). ⚠ See the warning below                                                                                                                                                                   |
| An **invisible** accessible name                 | **`ariaLabel`** — and `label` is accepted as an alias everywhere it exists | —                                     | `OverflowMenu`, `SideNav`, `Breadcrumb`, `Steps`, `Switcher`, `CommandMenu`, `Spinner`, `ProgressCircle`, `Resizable`, `Sparkline`. Always accepted alongside the raw `aria-label`                                                                                                                                         |

> ### ⚠ `label` renders on screen — check the prop docs before assuming it is a11y-only
>
> A 2026-08-14 adopter learned `label` from `Sparkline`, where it is explicitly an invisible
> accessible name, and passed `<Toggle label="Automatic deployments">` into a settings row
> that already had a visible title. The string rendered **next to the switch**, duplicating
> the row's own heading.
>
> The catalog rule is: **`label` is visible unless its own description says otherwise, and
> `ariaLabel` is never visible.** When a row, heading or `Field` already labels the control,
> omit `label` and pass `aria-label` instead — every component forwards it.
>
> Enforced by `vocabulary.test.ts`: a `label` prop whose manifest description states neither
> fails `pnpm meta:check`. Silence is the bug — the reader cannot tell it from either case.

### ⚠ Near-miss prop names — what you probably wrote, and what the prop is

Every row here is a real wrong guess from an adopter report, not a hypothetical. They are
near-misses rather than blunders: each one is the name the rest of the system, or the rest of
the ecosystem, would lead you to. Where the fix was to accept both spellings we did; where
accepting both would have made something else worse, the reason is in the last column.

| You probably wrote                                 | The prop is                                         | On                                               | Why not just accept yours                                                                                                                                                             |
| -------------------------------------------------- | --------------------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tone="subtle"`                                    | **`muted`** (boolean)                               | `Text`                                           | `tone` is the catalog's _severity_ vocabulary (`Status`, `Badge`, `Timeline`, `SideNav`). Text emphasis is a different idea; a third meaning for `tone` would cost more than it saves |
| `gap="4"`                                          | **`gap={4}`** (numeric `SpaceStep`)                 | every layout                                     | A string union would let `gap="7"` type-check into a token that does not exist                                                                                                        |
| `<Flex justify="between">` with no `direction`     | it is already **vertical**                          | `Flex`                                           | `direction="vertical"` is the default, unlike CSS and unlike Chakra/MUI/Radix. Pass `direction="horizontal"` for a row                                                                |
| `const { theme } = useTheme()`                     | a **tuple**: `const [theme, setTheme] = useTheme()` | `@cascivo/core`                                  | The object shape is next-themes'; the tuple is `useState`'s                                                                                                                           |
| `orientation="vertical"` meaning "stack the items" | it stacks the **value under its label**             | `DataList`                                       | Items stack vertically in both modes; `orientation` moves the value relative to its label                                                                                             |
| `<DataListItem>` as a component                    | `DataList` takes **`items`**                        | `DataList`                                       | `DataListItem` is the _interface_ describing an item, not a component                                                                                                                 |
| `import { Switch }`                                | **`Toggle`** — and `Switch` now works too           | `@cascivo/react`                                 | Fixed: `Switch` is exported as an alias, and `cascivo add switch` resolves                                                                                                            |
| `<OverflowMenu label=…>`                           | **`ariaLabel`** — and `label` now works too         | `OverflowMenu`, `SideNav`, `Breadcrumb`, `Steps` | Fixed: both spellings are accepted everywhere either was                                                                                                                              |
| `<Field hint=…>`                                   | **`description`** — and `hint` now works too        | `Field`                                          | Fixed: `hint` is the form-control word, `description` the feedback word; `Field` takes both                                                                                           |

### Importing the shared types

`Tone`, `Progress` and `SpaceStep` are the _types of published props_ — `Status.status` and
`Badge.variant` are `ToneInput`, every layout `gap` is a `SpaceStep` — so the first thing a
typed dashboard writes needs them:

```tsx
// Path B (prebuilt, `@cascivo/react`) — the vocabulary types ship from a subpath:
import type { Tone } from '@cascivo/react/types'
// Path A (copied source) — import from core directly, which you already depend on:
import type { Tone } from '@cascivo/core'

const DEPLOY_TONE: Record<DeployState, Tone> = {
  building: 'info',
  ready: 'success',
  error: 'danger',
}
<Status status={DEPLOY_TONE[deployment.state]} />
```

`@cascivo/react/types` re-exports `Tone`, `ToneAlias`, `ToneInput`, `Progress`,
`ProgressAlias`, `ProgressInput`, `SpaceStep` and `RovingOrientation`. Do **not** add
`@cascivo/core` to a prebuilt app's dependencies to reach them — on that path it is a
transitive dep, and pinning it invites a lockstep-version trap.

They sit on a subpath rather than the main entry for a mechanical reason: the component
sources already import those names from core, so re-exporting them from `@cascivo/react`
makes the dts bundler emit `ToneInput as ToneInput$1` and every prop switches to the aliased
name. `type-exports-parity` fails the build when a core type naming a published prop is
reachable from neither entry.

> ### ⚠ `gap` takes a NUMBER, and it is the one prop that breaks the pattern
>
> Every other size-ish prop in the catalog is a **string** union — `size="sm"`,
> `padding="md"`, `density="compact"`. The space scale is a **numeric** `SpaceStep`
> (`1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12`), so it is `gap={4}`, not `gap="4"`.
>
> This is deliberate: the steps are a scale with an order, and a string union would let
> `gap="7"` type-check into a token that does not exist. But it is genuinely surprising, and
> one adopter's `gap="4"` produced **20 type errors in a single run** — by far the largest
> single cost in their build. Write the braces.

### Items-prop-driven vs children-driven — and the types that look like components

`DataList`, `StructuredList`, `Timeline` and `Steps` are **items-prop-driven**: they take an
array and render it. `ListItem`, `ContainedListItem`, `MenuItem` and `TabsTrigger` are
**children-driven**: you compose them as elements. Nothing in the name tells you which, so
the catalog's export list mixes real components with the interfaces that describe their
items — `DataListItem` is an **interface**, not a component, and

```tsx
<DataList>
  <DataListItem label="Domain">{project.domain}</DataListItem>   {/* ✗ not a component */}
</DataList>

<DataList items={[{ label: 'Domain', value: project.domain }]} /> {/* ✓ */}
```

`DataList`'s items are `{ label, value }`. They render into a `<dl>`, so `term`/`description`
is the natural guess from the HTML and it is wrong — `label`/`value` is the catalog-wide
naming (see the accessible-name table above), and consistency across components beats
mirroring one element's vocabulary.

## Status and progress vocabularies — one set of words

Four display components and two sequence components used to ship six overlapping enums for
two ideas. There is now one canonical vocabulary for each, and every historical spelling is
accepted as an alias — so one domain enum drives the whole catalog with no lookup table.

**Severity — `Tone`** (`@cascivo/core`): `neutral | info | success | warning | danger`

| Component      | Prop      | Also accepts (aliases)                                                                         |
| -------------- | --------- | ---------------------------------------------------------------------------------------------- |
| `Badge`        | `variant` | `default`→neutral, `destructive`/`error`→danger, plus `secondary`/`outline` (looks, not tones) |
| `Tag`          | `variant` | `default`→neutral, `error`/`destructive`→danger                                                |
| `Status`       | `status`  | `error`/`destructive`→danger, `default`→neutral                                                |
| `Notification` | `variant` | `error`/`destructive`→danger                                                                   |

**Position in a sequence — `Progress`** (`@cascivo/core`): `pending | active | complete | error`

| Component  | Prop                  | Also accepts (aliases)               |
| ---------- | --------------------- | ------------------------------------ |
| `Steps`    | `Step.state`          | `current`→active, `upcoming`→pending |
| `Timeline` | `TimelineItem.status` | `current`→active, `upcoming`→pending |

Write the canonical value in new code. `scripts/checks/vocabulary.test.ts` fails a component
that models either idea with a private union.

## Links in a routed app

Two kinds of link, wired two different ways. Getting this wrong costs either a full page
reload or a hand-rolled copy of cascivo's link CSS — both were reported by adopters.

| The link is                                                                                                                  | Do this                                                      |
| ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| Rendered **by cascivo** from config (`SideNav`, `ShellHeader`, `Header`, `Breadcrumb`, `Switcher`, `Dock`, `NavigationMenu`) | `setLinkComponent(...)` **once** at app startup              |
| Written **by you** in page content (a project name, a branch in a table cell)                                                | `<Link asChild><RouterLink to="…">…</RouterLink></Link>`     |
| A call-to-action that navigates                                                                                              | `<Button asChild><RouterLink to="…">…</RouterLink></Button>` |

**Never write a bare `<Link href="/x">` in a routed app** — cascivo's `Link` renders a real
`<a>`, so it is a full page reload. `setLinkComponent` does **not** apply to it: `Link` is a
component you place, so it takes the child you hand it. **Never copy cascivo's link CSS into
your own layer** — override the tokens (`--cascivo-link-color`) instead.

Full guide: [USING-WITH-A-ROUTER.md](/docs/using-with-a-router.md).

## No styling at a distance

**Every style on an element comes from that element.** A component's own CSS never reaches
down into somebody else's subtree, and nothing outside a component reaches into its
internals except through a name that is published, versioned and checked.

This is a property cascivo has, not an aspiration. It is enforced three ways:

- **CSS Modules hash internal class names** (`_navWrapper_1r5fv_83`, different on every
  build), so there is no selector for a component's internals to accidentally depend on.
- **`data-cascivo-*` style hooks are the sanctioned exception**, and they are public API:
  semver'd, declared in each component's manifest, present in `registry.json` and the
  `llms/*.md` files, and checked in both directions by CI — a hook cannot be renamed
  without the manifest changing, and a manifest cannot promise a hook the component does
  not stamp. See [STYLING-INTERNALS.md](/docs/styling-internals.md).
- **`@layer` decides who wins**, not selector specificity. You never need a descendant
  selector to out-rank cascivo, because `cascivo.override` already does.

The point is not tidiness. `.card > div > nav` is a rule that works until the day somebody
adds a wrapper, and then fails **silently** — no error, no warning, just a component that
stopped being styled. Structural selectors are load-bearing dependencies on markup nobody
promised to keep. Write against a hook, a token, or a prop instead; all three are checked,
and all three survive a refactor.

## Overriding styles the sanctioned way

Every cascivo component spreads `{...props}` onto its root element, so `style` and
`className` **already pass through on every component** — you do not need (and there is
no) `sx`/`css` styling prop. When a component's props and tokens don't cover a one-off,
climb this ladder in order and stop at the first rung that works:

1. **Component props + tokens** — the intended path. `variant`, `size`, and setting a
   `--cascivo-*` component token cover almost everything.
2. **`className` + a rule in `cascivo.override`** — for a reusable override. The
   `cascivo.override` layer beats everything cascivo ships.
3. **Inline `style` with `var(--cascivo-*)` values** — for a fast one-off. This stays
   `cascivo audit --ai`-clean because the values are tokens. Type it and the token names
   are checked too:

   ```tsx
   import type { CSSProperties } from 'react'
   import type { CascivoTokenStyle } from '@cascivo/tokens/style-contract'

   const brand = {
     '--cascivo-link-color': 'var(--cascivo-color-accent)',
   } satisfies CascivoTokenStyle

   <Link style={brand as CSSProperties}>Docs</Link>
   ```

   The cast is unavoidable — React's `CSSProperties` has no index signature for `--*`
   keys — but `satisfies` runs before it, so a misspelled token is a compile error instead
   of a declaration that silently does nothing.

4. **`/* cascivo-audit: allow <rule> */`** — the rare remainder. A comment on the same
   line as, or the line above, a flagged line downgrades that finding so the audit no
   longer fails on it (e.g. `allow unknown-prop`, `allow hardcoded-value`, or `allow all`).
   Suppressed findings still print, so nothing is hidden.

`cascivo audit --ai` treats an inline `style` value that happens to equal a token as a
gentle **warning**, not an error — it never blocks a build on a fast-prototyping override.
Genuinely invented props (`sx`, `elevation`, …) remain errors; use rung 4 only when you
mean it.

### A name that does not exist is an error, everywhere

Two mistakes are **silent in CSS itself** and therefore checked for you, because nothing
else in the stack will ever mention them:

- **An unknown custom property is dropped.** `--cascivo-color-acent: red` does not warn,
  does not fail the build, does not appear in DevTools, and does not throw. It has no
  effect, and the hunt for the cause starts in the component.
- **A selector that matches nothing is not an error.** `[data-cascivo-modl-body] { … }`
  styles zero elements forever, and because CSS Modules hash the real class names you
  cannot tell a typo from a component that changed shape.

So the shipped name sets are enforced, not merely published:

| Where you are | What checks it                                                                |
| ------------- | ----------------------------------------------------------------------------- |
| Your editor   | `cascivo/token-values` (in `@cascivo/eslint-config`, at `warn`)               |
| Your types    | `CascivoTokenStyle` + `satisfies`, as in rung 3                               |
| Your CI       | `cascivo audit --ai` — `unknown-token` and `unknown-style-hook`, at **error** |

All three read the same generated set, so they cannot disagree with each other or with
the CSS. The names live in [`@cascivo/tokens/style-contract.json`](/docs/tokens.md).

### Running the audit

It works in any project with no setup — the contract ships inside the CLI, so there is
nothing to download and no network needed:

```sh
npx cascivo audit --ai src            # or add it to your lint script
npx cascivo audit --ai --json src     # machine-readable findings
npx cascivo audit --ai --fix src      # rewrite unambiguous literals to tokens
```

A good CI gate pairs it with `doctor`, which checks the install itself:

```jsonc
{ "scripts": { "lint": "cascivo doctor --ci && cascivo audit --ai src" } }
```

Only `error`-level findings fail `--ci`. `info` findings (a component using a `{...spread}`,
whose props can't be known statically) and `warn` findings (an inline `style` literal that
happens to equal a token) never do — so the gate stays green on correct code. A realistic
router-based dashboard is audited in cascivo's own CI
(`packages/cli/src/audit-ai/adopter-app.test.ts`) and must report **zero errors**; that
fixture is what keeps this recommendation honest.

`--contract <path>` points at a specific `audit-contract.json` (pin a version, or run
fully air-gapped); `--verbose` reports which contract source was used.

## Layout primitives — structure with these before writing CSS

Page structure (dashboard shells, toolbars, card grids, multi-column sections) has
first-class primitives, all exported from `@cascivo/react` — reach for them before
hand-writing grid/flex CSS or inline `style` layout:

- **`Flex`** — the gap-based flex container (`direction`, `gap`, `align`, `justify`, `wrap`).
- **`Grid`** / **`GridItem`** — CSS grid with responsive object props:
  `<Grid cols={{ base: 1, md: 2, lg: 3 }} gap={4}>`, `<GridItem span={{ base: 1, lg: 2 }}>`.
- **`AutoGrid`** — responsive card grid that fills columns by available width, no media queries.
- **`Columns`**, **`Center`**, **`Spacer`** — equal columns, centered max-width column, fixed gap.

The published `Stack` is a visual card-pile (overlaps children by an `offset`) — for
gap-based layout use `Flex`, not `Stack`.

## Coming from utility-first (Tailwind)?

cascivo has no utility classes. You express the same intent with plain CSS properties
reading `--cascivo-*` tokens, inside a layer. The mapping is mechanical:

| Tailwind utility          | cascivo CSS (inside `@layer …`)                                    |
| ------------------------- | ------------------------------------------------------------------ |
| `p-4`                     | `padding: var(--cascivo-space-4);`                                 |
| `px-2`                    | `padding-inline: var(--cascivo-space-2);`                          |
| `gap-2`                   | `gap: var(--cascivo-space-2);`                                     |
| `flex items-center`       | `display: flex; align-items: center;`                              |
| `flex items-center gap-2` | `display: flex; align-items: center; gap: var(--cascivo-space-2);` |
| `text-sm`                 | `font-size: var(--cascivo-text-sm);`                               |
| `text-muted-foreground`   | `color: var(--cascivo-color-text-subtle);`                         |
| `font-semibold`           | `font-weight: var(--cascivo-font-semibold);`                       |
| `rounded-md`              | `border-radius: var(--cascivo-radius-md);`                         |
| `bg-card`                 | `background: var(--cascivo-color-surface);`                        |

Two habit changes:

- **Structure vs. style split.** Markup stays semantic; all styling lives in a CSS
  module inside a layer. You are not decorating JSX with class strings.
- **Tokens, not values.** Reach for a `--cascivo-*` token instead of a raw `16px` /
  `#111`. The closed token set is at
  [`https://cascivo.com/tokens.catalog.json`](https://cascivo.com/tokens.catalog.json)
  and documented in [TOKENS.md](/docs/tokens.md).

## Server-rendering setup (Vite SSR / TanStack Start / Remix / workerd)

If you scaffold an app that server-renders through **Vite** (TanStack Start,
vite-ssr, Remix on Vite, or a workerd/Cloudflare target), do two things or the
build throws `Unknown file extension ".css"` and silently falls back to
client-only rendering:

1. Add `ssr: { noExternal: [/^@cascivo\//] }` to `vite.config.ts` — or add the
   `cascivoSsr()` plugin from `@cascivo/vite-plugin`. This makes Vite process the
   packages' per-component CSS imports instead of leaving them for the server
   runtime to load raw.
2. Import a theme (`@cascivo/themes/light-dark.css`) once in the root route/entry.
   Do **not** import `@cascivo/react/styles.css`: per-component CSS rides the client
   module graph and tree-shakes, and the aggregate replaces the few KB you use with
   all 199 components' worth.

No `<ClientOnly>` wrappers are needed — components ship `'use client'` and render
their server HTML normally. **Next.js App Router needs none of this** (the
`react-server` export condition handles it), and plain **Vite CSR/SPA** needs none
of it either — only Vite _SSR_ runtimes do. Full recipe:
[USING-WITH-VITE-SSR.md](/docs/using-with-vite-ssr.md).

**TypeScript + CSS imports.** The `import '@cascivo/themes/light-dark.css'` (and
`@cascivo/react/styles.css`) side-effect imports need ambient CSS-module types under
`tsc --noEmit`, or they error with `TS2307` (`TS2882` under
`noUncheckedSideEffectImports`). Add `src/vite-env.d.ts` with
`/// <reference types="vite/client" />`, or `declare module '*.css'`. This is a
type-only declaration — no runtime effect.

## Reading a UI instead of looking at it

An agent that needs to know what a rendered page currently says — not what its source
says — can serialize it to Markdown with `@cascivo/text`:

```ts
import { renderToStaticMarkup } from 'react-dom/server'
import { toMarkdown } from '@cascivo/text'

const doc = toMarkdown(renderToStaticMarkup(<Dashboard />))
```

No CSS, no hydration, no interactivity. Affordances come through named —
`[button: Save]`, `[input: Email = "ada@example.com"]`, `[tab: Overview (selected)]` — and a
chart arrives as a Markdown table. In the browser, `elementToMarkdown(element)` reads the
live DOM, so it reports what someone has actually typed, checked and opened.

See [MACHINE-MODE.md](/docs/machine-mode.md).

## See also

- [`@cascivo/docspack`](https://github.com/cascivo/cascivo/tree/main/packages/docspack) —
  every guide on this page as a searchable offline index (`docspack ask`, `docspack mcp`).
- [USING-WITH-A-ROUTER.md](/docs/using-with-a-router.md) — `setLinkComponent` vs `asChild`.
- [USING-WITH-VITE-SSR.md](/docs/using-with-vite-ssr.md) — the SSR `ssr.noExternal` recipe.
- [CSS-LAYERS-PITFALL.md](/docs/css-layers-pitfall.md) — the canonical order and the
  `cascivo.override` escape hatch.
- [THIRD-PARTY-CSS.md](/docs/third-party-css.md) — the `layer(vendor)` recipe.
- [USING-WITH-TAILWIND.md](/docs/using-with-tailwind.md) — running cascivo alongside an
  existing Tailwind v4 setup.
- [TOKENS.md](/docs/tokens.md) — the full token catalog.
- [MACHINE-MODE.md](/docs/machine-mode.md) — a rendered UI as a Markdown document.
