# Step 4 — Compose: one mind, whole canvas

Build the app — all surfaces in one continuous context, the chosen package's design.md in view throughout. This step is never decomposed into per-surface handoffs with frozen intermediate artifacts; the composing mind holds the whole canvas.

## Chrome

Per the primitives switch recorded in the project's `design.md`:

- **`nurix`** — compose chrome from `@nurix/components`. The chrome recipes (`data/chrome/` — read only the recipes for surfaces in the structure doc) carry composition, states, and when-to-use; the component dictionary (`components/`) carries props, imports, and usage across both provenances — consult the index for what exists, and open the touched category's API reference on need.
- **`native`** — the recipes are *specifications*: anatomy, the eight states, keyboard behavior, token reach — implement them with the incumbent stack's own components. The index's shadcn/fallback columns carry the composition; category API references never load.

Recipes are starting points, not jails: restyle or extend them in place when the package demands it.

## Composition rules

- **Every surface starts from a chosen layout blueprint** (`data/blueprints/<archetype>.md`) — pick the shape reading the brief, record it in one line. Structural variety, not visual variety, is what reads as designed.
- **Values come from the token artifact** — components consume the emitted custom properties and never invent raw values ([emission.md](emission.md) has the pipe rules). Switching packages must re-skin the whole app.
- **Positive obligations apply** — load the surface's archetype checklist (`data/checklists/<archetype>.md`); the checklists are ceilings: a sidebar ships with brand block, icons, and user context; a dashboard tile ships with number + comparison + trend + state + door; every surface ships its state matrix.
- **The register scope holds**: the display face never touches cells, buttons, labels, or form chrome; data numerals run the mono face with `tabular-nums`.
- **Motion follows the package's §7** and the frequency law: actions performed 100+ times a day, and any keyboard-initiated action, get no animation, ever; panel/drawer motion is interruptible; entrances never `ease-in`.
- **No ownership jails.** Any file may be edited at any time, including generated ones and library compositions.
- **No paper-trail bureaucracy.** Decision-log lines only, through `record`; attention goes to pixels.
- **A finishing pass with authority over everything is part of this step** — walk the whole canvas once at the end: rhythm, alignment, density, identity-consistency across surfaces, the one-accent discipline.

## Library reach — under `primitives: nurix`

The library carries behavior stock shadcn does not, and each item below is reachable only when the composition routes to it; a hand-rolled equivalent is a defect, not a style choice.

- **Density is the trait.** Control height and padding come from `personality.density` through `--density-control-*`; container padding and gaps on scanned surfaces (cards in a grid, list rows, panel bodies) read `--density-pad-xs|md|lg` and `--density-gap-md` — `p-(--density-pad-md)`, `gap-(--density-gap-md)` — so one trait re-spaces the product. A `p-3` on a data surface is the per-surface mood foundations forbid; structural spacing (a divider, an icon's optical nudge) stays literal.
- **Motion rides the seam.** A `transition-*` utility with no `duration-*` class inherits `--motion-duration-fast`, which `pace`, the reduced-motion block, and the inline clamp all reach; a `duration-200` opts that element out of all three. App-owned animation reads `var(--motion-duration-base)` and `var(--motion-ease-standard)`, never a literal; the library's `effect/*` primitives already read them, so an identity moment composes from those before a bespoke variant.
- **Collections are inline.** Any container of repeated rows the operator scans — a table body, a list, a feed, a board column — carries `data-mode="inline"`: every DOM-descendant transition collapses to instant (portaled tooltips escape) while loaders keep breathing on the ambient duration. Only `<Cell mode="inline">` stamps it, on the cell alone; `block/table` and `block/list` stamp nothing on their containers, so the app stamps it on the collection — including ones built on those blocks.
- **Every time on screen is `<Timestamp>`** (`element/timestamp`): `format="auto"` reads relative inside `thresholdMs` and absolute beyond it, the full absolute stamp sits on the native `title` by default, `live` ticks on one shared interval. A custom live label subscribes through `useNow` (`lib/time`) — one ticker per interval, never a timer per row.
- **Typing is not saving.** Input (commits on blur), Textarea (blur, Cmd/Ctrl+Enter), NumberInput (blur, Enter, the steppers), Slider (release), Combobox (close), `DatePicker` and `DatePicker.Range` (the pick) carry `onChange` for the draft and `onSave` for the commit; `InlineEdit` carries `onSave` alone. Mutations wire to `onSave`; `onChange` never reaches the server. A form with a Save button is the one exception — a transaction the button commits whole.
- **Remote and long option lists are the Combobox's** (`compound/combobox`): `onSearch` returns a page for a query (debounced by `debounceMs`), `onLoadMore` appends the next page, `maxSelections` caps a multi-select. A fetch-in-effect feeding a filtered `<Select>` is the pattern this replaces.
- **Time-range filters are `DatePicker.Range` with `presets`** (`{ label, from, to }`; its preset column shows by default, `DatePicker`'s needs `showPresets`), `weekStartsOn` from the locale decision, `fromYear`/`toYear` where the domain bounds them; a single date in a table cell is `DatePicker` with `inline` and `compact`. A bespoke window picker is a defect.
- **Uploads reject before they upload.** `FileUpload` (`compound/file-picker`) takes `accept`, `maxSize`, `maxFiles`, and `onFileValidate` (return the rejection message, or nothing to accept); a rejected file never enters the list and `onFileReject(file, message)` receives it. The dropzone only flashes an invalid border — the composition surfaces the message (a field error or a toast).
- **Shell affordances come from `block/shell-affordances`**: `Freshness` for "as of" (`staleAfterMs`, `onRefresh`), `GateAffordance` for an action a gate blocks, `SystemBanner` for a system-wide notice. A bespoke freshness pill or banner is a defect.
- **Status colour is the role.** The package's `success` / `warning` / `info` reach the library as `--tone-success|warning|info` (library fallbacks when undeclared): a status dot is `<Chip prefix="dot" color="var(--tone-success)">`, never a raw hue, and app utilities map onto the same roles (`--color-success: var(--success)` in the app's `@theme inline`), so a chart legend and a chip dot never disagree.
- **Targets under 24px grow through `hit-area-*`** (`hit-area-2` adds 0.5rem of pointer box on every side; `hit-area-x-*` / `-y-*` / `-t-*` / `-b-*` / `-l-*` / `-r-*` where a neighbour would overlap) — the pointer box grows, the paint box does not. `hit-area-debug` shows the box while composing.
- **Clipping is `clip`.** `clip` for one line, `clip-<n>` for n lines, `clip-fade` where prose is expected to overflow — each carries `min-width: 0` and `overflow-wrap: anywhere`, which Tailwind's `truncate` and `line-clamp-*` lack (the flex-child overflow bug those leave open).
- **Type roles are utilities** — `text-display`, `text-title`, `text-h1`, `text-body`, `text-dense`, `text-meta`, … from the library's `@theme` — and `cn()` (tailwind-merge tables) classifies `text-<role>` as a colour: a role and a colour passed through `cn`, together or apart, keep only the later one. Keep both in a `className` string that never goes through `cn`, or put the colour on the parent.
