# Agent instructions

Read this before generating any UI in this project. It is not a style guide —
styling is handled by the tokens and the components. This file says how to
**assemble** screens and how to **derive** the values on them.

Scope of this version: **sidebar shell, top-nav shell, auth**. No shell variants,
no frame/composition system, no register. Everything not covered here is your
call — take it, and say what you took in the handover.

---

## 0. Two modes

**Building mode** — a screen a user would see in the running product. It shows one
situation; everything on it is true at the same moment.

**Documenting mode** — a specimen page: variants side by side, all states at once,
props tables.

Never mix them. In building mode: no empty state beside populated rows, no
skeleton beside real content, no dropdown contents rendered as a static card, no
subtitles describing the component instead of the user's situation, no prose
listing options the screen does not offer.

---

## 1. Hard constraints

Absolute. No exceptions anywhere in this project.

1. **One data state per screen.** Loading, empty, no-results, error and populated
   are mutually exclusive branches of one conditional.
2. **Actions derive from record state.** Never a fixed action set on a detail
   view. Omit an action when the record's state makes it meaningless; disable it,
   with the reason stated, when it exists but the *user* can't take it yet
   (permission, plan, visible precondition). Never disable to avoid deciding.
   **One act-ability predicate at every altitude** — the row, the section count
   and the nav badge call the same named function, or the label says which looser
   thing it counts.
3. **One enum per status field**, from the product's own `enums.ts` (§4). Never
   pass a status string literal into a badge.
4. **Every screen has a route** in the product's own `nav.ts`. Sidebar,
   breadcrumbs and command palette read from it.
5. **Every destructive action opens a confirm dialog** — button, menu item, row
   action, bulk action.
6. **Dark mode is never optional.**
7. **Responsive from 360px to 1920px** — shells, page content, tables, rails,
   overlays, auth. A design verified at one width is unverified. No fixed pixel
   width on anything that must survive the narrow end; no horizontal scrolling
   except inside a container that declares it.

---

## 2. Derivation

Most failures here are numbers that don't reconcile. The convention is cheap:
**every figure on a screen names the array it comes from.**

```tsx
// Incorrect
<StatTile label="Outstanding" value="$87,485" />
<p>Showing 1–5 of 128 invoices</p>

// Correct
const openInvoices = invoices.filter(isOpen)
<StatTile label="Outstanding" value={formatCurrency(sum(openInvoices, 'amount'))} />
<p>Showing {page.from}–{page.to} of {filteredInvoices.length} invoices</p>
```

If you cannot name the array a number came from, the number is wrong.

- **All counts derive from one fixture.** Sidebar badges, footers, page counts and
  stat tiles read the same array.
- **Summaries compute from the rows they summarise** — `sum(rows)` and
  `rows.length` over the exact array the table renders.
- **Filtered lists filter.** A section titled "Open invoices" runs the predicate;
  never `array.slice(0, n)`.
- **Derived metrics recompute** from one helper called by every surface. Never
  author a plausible number.
- **Derived flags compute at render**, never stored, never in an enum:
  `const isOverdue = (t) => t.dueAt < now && t.status !== 'done'`. Status badge
  shows the status; the derived flag tones the cell it concerns.
- **A count in a label counts the thing the label names.** "All statuses (11)"
  claims eleven statuses.
- **A qualifier decomposes its figure's own set** — it may break the number down
  or name its scope, never introduce a different set.
- **On an ordered scale the headline includes the far end.** Records past a
  threshold are more urgent, not less; never outside the number meant to surface
  urgency.
- **Preview panels execute** the real evaluator on the real fixture.
- **One record, one fixture, across every screen.** Never a second literal written
  to match. If a screen needs a record the fixture lacks, add it to the fixture.
- **Never pad a count to look plausible.** One is a real count when one is the
  answer.
- **Copy promises are computed** — "two months free" and the total read one
  constant.
- **Never describe state in static copy** ("Current default" — the control already
  shows it).
- **Entitlements gate controls, not just copy.** Unavailable on this plan →
  disabled with an upgrade hint, not a sentence beside a live control.
- **A figure on two screens is one function called twice.** When two records
  describe the same money, one owns it and the other reads it.
- **A committing action writes into the fixture** and re-renders, so the figures
  that claim to derive from it actually move.

**Checkability has three legal forms:** the rows sit beside the figure, the figure
drills through to them, or it names a predicate whose list is reachable. Only the
first needs a table — a board, calendar, chart, matrix or canvas is a legitimate
primary display. **Every display's own aggregates are figures** and obey this
section: column headers, day chip counts, series, row totals.

**Fixture narrative.** Conversations, activity feeds, notes and notifications are
authored *against* the entity fixtures. A message naming a record must agree with
that record's status and timeline at that timestamp. Re-read the entities a
message mentions before returning it.

---

## 3. Contracts

Control shape is a promise about behaviour.

**Commit model** — one per screen, stated in a comment at the top of the file.
*Staged*: every control is a form field, nothing applies until Save; Save and
Discard disabled until dirty; Save shows pending. *Instant*: no Save button
anywhere, each control confirms itself. Never mix. A dialog carries its own model:
an instant screen may open a staged dialog; a staged screen may not hold instant
controls.

**Reversibility, three tiers.** Instant + harmless → bare control. Instant +
consequential → applies immediately **plus** an undo toast. Irreversible → confirm
dialog, always. Never a modal on a harmless toggle; never a silent consequential
one.

**Switch vs checkbox.** Switch = applies immediately. Checkbox = submitted with a
form. Never mixed in one group.

**Destructive actions.** `destructive-subtle` everywhere — never a filled
destructive button in a toolbar, bulk bar, header or danger zone; filled
`destructive` is only the confirm button inside the dialog. Always a confirm
dialog naming the specific consequence. Reversible actions ("Put on hold") are not
destructive.

**Forms.** Submit disabled until dirty **and** valid; Cancel appears once dirty;
submit shows pending; errors render on validation, never on first paint.
Placeholders are passed per usage, reference something present in that context,
and must fit the field at its **narrowest** declared width. **A control's width
comes from its container** — controls fill the width they're given, and a `width`
on the control sizes an inner element while the wrapper still claims the row.

---

## 4. The product's own files

A product is a route table, a status vocabulary and a fixture set before it is a
screen. Write the first two **first**.

**Everything a product needs lives inside the product's own folder.** `<product>/`
is `templates/<slug>/` here, or the product's source folder in a consuming
project. Nothing is written outside it, and nothing is imported from a sibling
product — deleting the folder deletes the product, leaving nothing orphaned.

| File | Holds | Lifetime |
| --- | --- | --- |
| `<product>/lib/contracts.ts` | Contract only: `Tone`, `StatusValue`, `StatusField`, `LabelledValue`, `labelOf`, `TONES`, `Route`, `indexRoutes`, `matchRoute`, `breadcrumb` | Copied in per product; identical across products |
| `<product>/lib/ui-runtime.js` | `StatusBadge` and the shared render helpers | Copied in per product |
| `<product>/lib/command-palette.js` | Palette shortcut, query, empty state, route check | Copied in only when the shell has app-scope search |
| `<product>/assets/providers/` | Official provider marks — only the ones the auth screens use | Copied in only when auth offers providers |
| `<product>/enums.ts` | This product's status fields, classification lists, derived-flag predicates | Dies with the product |
| `<product>/nav.ts` | This product's route table and its projections | Dies with the product |

**Never a domain value in `<product>/lib/`.** The test: could an unrelated product
use this exact value with the same meaning? `Tone`, `Route`, `labelOf` — yes.
`awaiting_survey`, `/claims/:id` — no. A domain noun in `lib/` is a bug; it belongs
in `enums.ts` or `nav.ts` beside it. The `lib/` copies start byte-identical
between products and stay that way — a fix to one is a fix to all of them, never a
local edit. Deleting a product should mean deleting exactly one folder.

**`<product>/enums.ts` carries**, in this order: a header comment restating the
refusal rule (a value not listed does not exist); one `export const
<ENTITY>_STATUS` per field, typed `readonly StatusValue[]`, in the order the
record moves through them, each with a comment naming what the field means, the
tone grouping and its reason, and any value **deliberately absent because it is
derived**; an id union per field; classification lists (`readonly
LabelledValue[]`, read through `labelOf`, never reaching a `Badge`); derived-flag
predicates grouped as `<ENTITY>_FLAGS`, each with the clock **injected**; a
`STATUS_FIELDS` registry; a coverage note recording which values still have no
fixture.

**`<product>/nav.ts` carries**: a header comment listing what deliberately is
**not** a route (panels, rails, dialogs); `<PRODUCT>_NAV: Route[]` in nav order,
with `offSidebar: true` for registered-but-not-nav screens and `roles` for
role-gated ones; `<PRODUCT>_ROUTES = indexRoutes(<PRODUCT>_NAV)` — breadcrumbs and
the palette read the index, never the tree; the projections the shell renders,
derived by filtering the tree, never retyped. **Never add a route to make a
breadcrumb resolve** — that's a bug in the screen.

**Using them.** Status renders through `StatusBadge` (`<product>/lib/ui-runtime.js`),
which reads the enum; no screen assigns a tone. Breadcrumbs are `breadcrumb(path,
<PRODUCT>_ROUTES)`. Nav badges count with a predicate from `<ENTITY>_FLAGS` — the
same one the rows use. Classification values render through `labelOf`.

---

## 5. Assembly

- **One entry point per action per screen.** A header "New invoice" and an inline
  new-invoice form on the same page is one too many.
- **Records get made somewhere.** Every entity the product lists needs the action
  that creates it: a `New <entity>` entry point in that list's header, and the
  screen or dialog behind it. The only exception is an entity that genuinely only
  ever arrives from outside the product — and then the shell comment says so in
  one line. Two or three fields is a staged dialog; a real form is its own
  registered route (`/<entity>/new`). A create writes into the fixture, so every
  count it feeds moves.
- **Navigation ownership.** The nav owner owns top-level destinations. Tabs own
  views *within* the current destination and never repeat a nav item.
- **Breadcrumbs derive from the route table.** Every segment before the last
  navigates to its own route — `href="#"` is a bug, not a placeholder. The last
  segment is never a link. A namespace parent with no screen renders as plain text.
- **One column, one renderer.** Emphasis is a tone within the renderer.
- **Empty-state actions address the problem.** Filter returned nothing → primary
  weight on "Clear filters", not "Create new".
- **List screens share an architecture**: filter bar, bulk action bar, table,
  pagination — same order, every entity.
- **Filters:** 2–4 exclusive options → segmented control; 5+ or multi-select →
  dropdown; text filter always leftmost. Same pattern on every list screen.
- **These have no lint rule — use the component anyway:** `<select>` → `Select`;
  `<input type="checkbox"/"radio"/"range">` and toggles → `Checkbox` /
  `RadioGroup` / `Slider` / `Switch`; `<thead> <tbody> <tr> <th> <td>` →
  `TableHeader` `TableBody` `TableRow` `TableHead` `TableCell`.

### 5.1 Cards and grids

- **Column stacks, not an auto-fitting grid of cards.** One `<div>` per column,
  cards stacked inside; the outer grid holds two or three children, never N.
  Balance the stacks; at one column they concatenate, so the stacking order is the
  reading order. Equal-weight metric tiles are the exception — they may auto-fit.
- **Cards side by side fill the row's height** (`align-items: stretch`,
  `height: 100%`, flex column inside so footers align).
- **A container drops its padding on the axis a child bleeds along**, but the
  bleeding child keeps the gutter on its own first and last cell — match
  `--gutter-page` inside a page, `--gutter-card` inside a card, on the child's own
  frame rather than per cell.
- **Never mix a `padding` shorthand with a conditional longhand** in one inline
  style — React clears both horizontal paddings. Write all four sides as longhands
  whenever one is conditional.
- **The last item in a divided list carries no trailing rule or padding** (per
  group in a grouped list, except the last group).
- **Header, cells and any subtotal resolve from one track definition.** One
  `<table>`, or one grid the header participates in. Verify by measuring: the
  subtotal's right edge equals its column's right edge. The same holds for a row
  of anchor figures: label, figure and qualifier line up across the row only if
  the columns share one row definition (`grid-template-rows: subgrid`), because
  one wrapped label or one missing qualifier otherwise shifts its own column.

### 5.2 Overlays inside scrolling regions

A container that scrolls on one axis clips both. Any overlay opened from inside
one — row menu, popover, cell tooltip — leaves the flow: portal it to the document
in viewport coordinates (`Menu`, `Popover`, `Tooltip` already do). Never an
absolutely-positioned panel in a table cell. Never "fix" it by removing the
container's overflow. Reposition on scroll and resize rather than closing; flip
above the trigger when there's no room below and clamp to the viewport.

### 5.3 Figures beside a filtered table

- **Scope filters move the metrics too** — period, owner, category are applied
  before the figures compute, and the control sits *above* the metric row.
- **Tabs and the text filter narrow the list only** — inside the list container,
  below the metrics, and the metrics don't move.
- **State the scope in words** when it isn't "everything".
- One reset for the scope, one for the list filters; clearing one never clears the
  other.

### 5.4 Weight and recession

Work the user cannot act on is **recessed, not disabled**: muted surface, muted
text, no primary control — the row keeps its link, its badge and its clock.
Recession is a property of a region, chosen once per product. **Name what is being
waited on** and who owes it. Never recess by count — always by a predicate.

---

## 6. Shells

Two product shells in this version, plus auth (§7). Pick one for the whole
product; never mix, never switch between screens.

| Shell | When | Navigation owner |
|---|---|---|
| **Sidebar** | 6+ top-level destinations, dense record work, per-destination counts | Sidebar owns destinations; any toolbar is utility only |
| **Top-nav** | ≤6 top-level destinations, wide canvases, content-adjacent apps | Top bar owns destinations; there is no sidebar |

If the request names a shell, that is the decision. If it's silent, destination
count is the tie-breaker; say in the handover that you inferred it. **A shell has
exactly one navigation owner** — two rows of top-level destinations, or a sidebar
plus a top-level nav, is a failure, not a variant.

Other shells (rail + canvas, split list-detail, focused/no-shell) are deliberately
out of scope here and may be added later.

### 6.1 App-scope duties

A shell owes the user four things. Place each in a named region, or decline it
with a reason written in the shell comment. **A duty lands in exactly one
region** — search in the toolbar *or* the sidebar header, never both. Placement is
product-wide.

| Duty | Satisfied by | May be declined when |
|---|---|---|
| **App-scope search** | One input, or a control that opens one | One destination, or nothing to search across |
| **Session and account** | Account menu meeting §6.2 | Never in a product shell |
| **Utility actions** | Icon buttons, or a menu holding them | The product genuinely has none |
| **Page context** | Breadcrumb, page title, or both | Single-destination products |

### 6.2 Element contracts

Hold wherever the element is rendered.

- **Search** is one input whose placeholder names what it searches ("Search
  invoices, customers…"). A `⌘K` hint renders only if bound. **App-scope search is
  a palette, not a filter**: the field is read-only and opens the same overlay the
  shortcut opens. `<product>/lib/command-palette.js` owns the shortcut, query, empty
  state and route check; the shell supplies its record groups. A plain filtering
  `Input` is still right *inside* a list.
- **Utility actions** are icon buttons, four at most per region, each with a
  tooltip, an `aria-label`, and something real to open. No icon that does nothing.
- **Badges derive from the panel they open**, anchored top-right and ringed in the
  background colour. Count when the number is useful, dot when only "something new"
  matters, never both. Zero renders no badge.
- **The theme control reflects the current theme**, instant and harmless, swapping
  its own icon — never two buttons, never a labelled switch.
- **The account menu** carries an avatar; if it shows a name it shows the role. Its
  entries are registered routes plus log out, which is not styled destructive.
- **Breadcrumbs live in one place.**
- **No app-scope region carries page-scope actions, or repeats a top-level
  destination.**

### 6.3 Sidebar shell

Mode, one per product: **labelled** (default, 6–12 destinations, 240–280px),
**icon rail** (≤8 destinations, canvas-heavy, 56–64px), or **rail + panel**.

- **Brand block at the top**, on the sidebar surface, separated from the nav by a
  border, carrying the mark and the workspace or product name.
- **Every item is icon + label, in that order**, one icon set, one size. Never mix
  items with and without icons.
- **Exactly one item is active**, derived from the route, marked by a background
  *and* a foreground token — never weight alone, never a left border alone.
- **Every item is a route.** An icon rail's labels become tooltips and
  `aria-label`s; they are never dropped.
- **Groups earn their labels**: two or more groups → an uppercase label on each;
  one group → none. 5–9 items per group.
- **Counts sit at the trailing edge**, derived from the array the destination
  renders, present only where actionable; zero renders nothing; in a rail they
  collapse to a dot. **A badge states what it counts** — one predicate over one
  array, recoverable by the reader. A badge that would sum different kinds of
  record counts one kind or degrades to a dot.
- **The sidebar keeps the full height of the screen, always.** The shell root is
  viewport-height and is not itself a scrolling box (`height: 100dvh;
  overflow: hidden`); the content column is the scroll container. A shell on
  `min-height: 100vh` scrolls as a document and carries the sidebar off the top of
  the screen with it, which takes the account menu — and often the active item —
  out of reach on any page longer than the viewport. Where the nav itself is
  taller than the screen, it scrolls *inside* the sidebar, between a fixed brand
  block and a fixed footer. **A grid shell needs its row bounded explicitly**
  (`grid-template-rows: minmax(0, 1fr)`): with columns declared and no rows, the
  implicit row sizes to content, the sidebar grows past the viewport, and the
  root's `overflow: hidden` clips the footer instead of scrolling the nav — a
  failure that only appears on viewports shorter than the nav.
- **The footer zone, if present, holds only account, help or collapse** — and if
  the account menu is there it is not also in the toolbar.
- **The sidebar owns top-level destinations.** The within-destination level lives
  on the page as tabs by default, or in the shell as nested children — one place,
  never both, and never a second set of top-level destinations.

### 6.4 Top-nav shell

No sidebar; the bar owns destinations and carries the brand. Baseline slot order:

`[ brand ] · [ destinations ] · — gap — · [ search ] · [ utility ] · [ divider ] · [ account ]`

- Brand is leftmost and links home.
- **Destinations are a single row, all visible** — 3–6 items. Never wrap, never
  hide overflow behind "More" at desktop width. Past six, use the sidebar shell.
- Items are text, optionally with an icon — whichever, all of them match. Exactly
  one active, derived from the route, marked by a foreground token plus a
  persistent indicator (underline or pill), never colour alone.
- **A destination with children opens a menu.** A **menu-only** destination is not
  a screen: clicking opens the menu and nothing else, its path is a namespace
  (`menuOnly`), it reads active while a child is open, and it supplies the first
  breadcrumb segment.
- Search may collapse to an icon button that expands in place, but only when
  destinations would otherwise be squeezed.
- Counts sit after the label, inside the item's active indicator.
- The page header carries breadcrumb and title; the bar never repeats them.

### 6.5 Responsiveness

| Width | Sidebar shell | Top-nav shell |
|---|---|---|
| ≥1280px | Sidebar expanded, full toolbar | Full destination row |
| 1024–1280px | Sidebar collapses to icon rail, labels become tooltips; search narrows | Full row; search collapses to an icon |
| 768–1024px | Sidebar becomes an off-canvas drawer; menu button takes the leading slot | Destinations collapse into one menu button |
| <768px | Drawer, plus a bottom tab bar if ≤5 primary destinations | Menu button; brand and account stay visible |

- **Declared once for the shell**, identical on every screen.
- **A collapsed region leaves the layout; it never keeps a zero-width track.**
  Take it out of flow (fixed drawer, absolute overlay) and give the remaining track
  the full width — hiding a grid item while shrinking its column renders children
  at zero width. Verify the content region's measured width equals the space
  available.
- **Nothing is lost on the way down**; collapsed does not mean unlabelled;
  **the account menu never collapses into an overflow**.
- **One scroll owner per shell, declared.** In a sidebar shell it is the content
  column; in a top-nav shell it is the document. Two nested scrolling boxes, or a
  navigation region that scrolls away with the content, is a bug at every width.
  **The scroll owner's children keep their natural size** — a scrolling flex
  column shrinks its children by default, which silently squashes any fixed-height
  toolbar or sticky header inside it.
- **Content follows.** Padding tightens, metric rows reflow to two columns then
  one, tables drop priority columns or stack — declared per table, once. The
  record's identifier, its anchor figure, its clock and its action never drop.
- **A rail + record page declares one collapse width for the product**; at one
  column the rail precedes the record when it carries the anchor figure.
- **A table inside a flex or grid item needs `min-width: 0` on every ancestor** up
  to the track, or the shell scrolls sideways instead of the table.
- **44px minimum hit targets below 768px.**
- **No fixed width on a content container** — `max-width` with a fluid basis.
- **Grids reflow by track**: `repeat(auto-fit,minmax(<floor>,1fr))`, floor wide
  enough for the longest value.
- **A side rail closes before its neighbour goes below readable width.**
- **Overlays follow the viewport**: `max-width` plus margins and internal scroll;
  below ~640px a dialog becomes a full-height sheet; popovers reposition.
- **Forms go single-column before their fields go narrow.**
- **Type and hit targets never shrink to fit** — reflow instead.
- **The shell never reflows into a different information architecture.**

### 6.6 Shell completeness

Not shippable unless all are true.

- One declared shell type, one navigation owner.
- Every §6.1 duty placed in a named region or declined with a reason.
- Every app-scope element meets its §6.2 contract.
- Sidebar: brand block, at least one group, at least three destinations, exactly
  one active. Top-nav: brand, destinations in one row, exactly one active.
- Page header with a title, and page-scope actions if any exist.
- A short shell comment at the top of the shell file naming the shell, the
  density and separation treatment, the active-state signature, and what each
  destination's primary content is (table, list-detail, board, calendar,
  chart-led, thread, document, form, matrix, canvas). One line each.
- Verified at 1440px, 1024px, 768px and 375px — all four coherent and complete.
- No slot filled with a placeholder, a lorem label, or a route resolving nowhere.

---

## 7. Auth

Auth is a shell decision before it is a form decision.

### 7.1 Its own shell

Auth is **the only shell with no navigation owner** — no sidebar, no top nav, no
destinations, no account menu. A sign-in page rendering the app shell is wrong.
One auth shell for the whole product, used by every screen in §7.2.

Layout variants — one per product: **centred card** (default, 360–420px column),
**split** (brand or proof beside the form; the form side is never the narrower
one), **full-bleed** (form on a brand surface, contrast still AA), or
**inline/embedded** (same field rules, no page chrome).

Whatever the variant: one column of fields, one primary action, the product mark
above the form as the only navigation. Never a marketing header, never a footer of
links competing with the primary action.

### 7.2 The route set

Registered in the product's `nav.ts`, in a namespace excluded from the shell nav.

```
/signin           /signup            /signout
/forgot-password  /reset-password    (token in URL)
/verify-email     /verify-otp        /mfa
/accept-invite    /sso               /account-locked
```

Not every product needs all of them — but **every entry point rendered has a
screen behind it**. If the design does not include a flow, it does not link to it.

### 7.3 Invariants

1. **One state per screen.** Auth has the most: idle, submitting, field-invalid,
   credentials-rejected, rate-limited, locked, expired-token, network-error,
   success-transitional. Never stack a validation error and a server error.
2. **Never confirm or deny that an account exists.** "If an account exists for that
   address, we've sent a link."
3. **Errors are actionable without leaking.** "Email or password is incorrect."
   Server errors once, above the form, in `Alert`; field errors on their field.
   Never both for one submission.
4. **Errors never render on first paint** and never clear the user's input beyond
   the password field.
5. **Every auth form is Staged.**
6. **Rate limiting and lockout are designed states, not toasts** — a bounded wait
   with a real countdown, or `/account-locked` with a recovery path.
7. **Password requirements shown before submission**, as a live checklist.
8. **Correct `autocomplete` tokens, always** — `email`, `username`,
   `current-password`, `new-password`, `one-time-code`.
9. **Nothing traps the user.** Every screen has a way back to `/signin` and a path
   to support.
10. **Dark mode from the first commit**, on the brand panel too.

### 7.4 Providers

`Button variant="outline"`, full width, one per row, stacked above or below the
email form — never a grid of icon-only tiles unless there are four or more and the
labels are redundant.

- Order by likelihood, SSO first for B2B; order once, product-wide.
- **Three at most on the primary path**; the rest behind "More options".
- **"Continue with X"** — one verb for all providers, signup and sign-in alike.
- **Marks come from `<product>/assets/providers/`**, official, never hand-drawn, recoloured,
  substituted or set as styled text. If the asset is missing, stop and ask.
- Mark and label treated identically across the whole stack.
- One `Separator` carrying "or" divides providers from the email form.
- **Show the returning user's last-used method** when known.
- **Provider errors are their own states**: cancelled, denied, already registered
  with a password — each saying what to do next.

### 7.5 Strategy, OTP, reset

**One primary credential strategy per product**, declared: password (reveal toggle
via `InputGroup`, "Forgot password?" at the password field's trailing label edge),
email-first (two steps, email editable on step two), magic link (the
sent-confirmation is a **screen**, stating address, expiry, and resend with
cooldown), OTP (§ below), SSO (interstitial naming the IdP, never a silent
redirect), or passkey (feature-detected, silent fallback). A sign-in page offering
three at equal weight is an unanswered question.

**Signup differs from sign-in in three ways only**: minimum extra fields, terms,
and what happens next. Everything else is identical — two auth pages that aren't
visibly siblings read as two products. The switch between them is one line at the
bottom, a link, never tabs and never two primary buttons.

**OTP / MFA** — use `InputOTP`, never a row of single-character `Input`s. State the
length and the channel ("the 6-digit code we sent to ••••••4821"). One code, one
purpose — four flows are four screens. Paste and SMS autofill fill the whole field.
Submit on completion with the explicit submit still present; a failed auto-submit
doesn't clear. Resend is a real control with a real countdown. Expiry stated up
front. A wrong code shows remaining attempts, then lockout with recovery. **MFA
always offers a backup path on the same screen.** Trust-this-device is a
`Checkbox`, and it names the duration.

**Password reset is four screens** — request, sent-confirmation, set-new (token in
URL), success-or-expired — all four required. Request and confirmation are
enumeration-safe. Expired-or-used token is a designed screen with one action.
Set-new uses one password field with a visible checklist, plus a confirm field only
where there's no reveal toggle. Success states the session consequence and routes
somewhere real. Requirements render from the one constant validation also reads.

### 7.6 Trust

One trust element at most. Terms and privacy are one line of small text under the
primary action on signup — real links, plain language; a checkbox only where the
jurisdiction requires it, and then it blocks submit and says so. No cookie banner,
no chat widget, no nav, no marketing footer. Say what the product is. Show invite
and org context when known ("Join Acme on Reckon", with the inviter's `Avatar`).

### 7.7 Auth completeness

Auth shell declared, one layout variant, no navigation owner · every entry point
has a screen and every screen a route · one primary strategy, three providers max,
official marks, one label verb · sign-in and signup visibly siblings · one state
per screen, every §7.3 state designed including expired, locked, rate-limited ·
enumeration-safe copy everywhere an address is touched · MFA and OTP carry a backup
path · correct `autocomplete`, autofill working · no dead ends · verified at
1440px, 768px and 375px, both themes.

---

## 8. Vocabulary

### 8.1 Status enums

One vocabulary per field — `live` is not sometimes "Enabled" and sometimes "On".
Tone carries meaning: `danger` is a failure event, `warning` is a deliberate state
needing attention; they never share a colour.

**Enums hold stored state only.** Anything computed from other fields is derived
and must not be added — `overdue`, `at_risk`, `expiring`, `unread`. A record
carries one status and any number of derived flags.

**A count over several values names its predicate, not one of the values.** Count
one value and use its label; count two or more and name what the predicate means.
Same for any strip, bar or pip row.

**Where a field has more values than tones**, group by the question the primary
screen asks — usually "who is this waiting on" or "can I act on it" — and record
the grouping in the comment beside the enum.

Generating a screen against an **existing** product and the enum lacks a value:
stop, report the field and values needed, wait. Building the product: writing
`enums.ts` is the job, and every value carries a tone grouping with a reason and at
least one fixture. Either way, never add a domain value to `lib/data/enums.ts`.

### 8.2 Verbs

One verb per action, product-wide.

| Action | Label |
|---|---|
| Retrieve a file | **Download** (format only when there's a choice) |
| Bring data in | **Import** |
| Send data out | **Export** |
| Create a record | **New \<entity\>** |
| Persist form edits | **Save \<object\>** |
| Discard form edits | **Cancel** |

Auth: **Continue with \<provider\>** · **Sign in** · **Create account** · **Send
reset link** · **Set new password** · **Verify** · **Resend code** · **Sign out**.
Never "log in" or "login", in any label, heading or link.

**Status transitions** are named once, product-wide, after the move — `checked_in`
is reached by **Check in** in the row menu, the detail header and the bulk bar
alike. The set offered derives from the current status: one function per entity,
called by every surface; a transition that doesn't apply is omitted, never
disabled. One transition is primary — the record's normal path. A transition that
closes a record against the customer's interest is destructive only when
irreversible; reversible transitions raise an undo toast instead of a confirm
dialog. Record the verbs beside the enum; the CRUD table above does not name a
domain's transitions.

### 8.3 Time

One format rule per column, every cell.

| Distance from now | Format |
|---|---|
| Under 24h | `08:00` |
| Under 7 days | `Tue 08:00` |
| Beyond 7 days | `Aug 14` |
| Null | `Never` |

Distance is absolute — three days ago and three days ahead format alike. Absolute
timestamp on hover, always; never mix relative and absolute strings in one column.

**Direction is never carried by the format alone.** Future and present: plain text.
Past where past is a problem (due, expiry, SLA): warning or danger tone matching
the derived state. Past where past is neutral (created, last seen, settled): plain
text. Whether past is a problem is a property of the column, declared once.

**Durations and countdowns get one spelling, product-wide, from one helper** —
`22 days left` / `11 days over`. Direction in the words, never colour alone. Never
a duration and a date in one cell; the date goes on hover or a declared second line.

---

## 9. Where creativity goes, and doesn't

Creativity is spent on composition, hierarchy, rhythm and density. Density,
separation treatment, active-state signature, brand presence, page-header shape,
table character, corner and motion character are all yours — chosen **once per
product** and applied everywhere. An axis chosen per screen is not a choice, it's
inconsistency.

Choosing the same primary display for every destination is a legitimate answer,
but make it deliberately: if every destination is a table, name in the shell
comment the one that would most plausibly not have been, and why it is.

Outside that: no new colour values, gradients or shadows — tokens only. No new
fonts, no type size off the scale. No one-off CSS standing in for a component that
exists. No bespoke layout for a pattern §5 covers. No decorative element carrying
no information. No animation on data. No hand-drawn brand or provider marks.

**Needing a component that doesn't exist is normal**: build it local to the product
and leave it there. The inventory is closed by default. Report it — the report is
how the system learns which local helpers converge.

---

## 10. Before you return output

- [ ] Building mode or documenting mode — anything on screen from the other?
- [ ] Exactly one data state?
- [ ] Can I name the source array for every number?
- [ ] Does every summary sum the rows beneath it, and every qualifier decompose
      its own figure's set?
- [ ] Do the row, the section count and the nav badge read one act-ability
      predicate?
- [ ] Does every titled list execute its own predicate?
- [ ] Do the header actions match the record's status?
- [ ] Does any record on more than one screen read one fixture entry, with no
      count padded to look plausible?
- [ ] Does the fixture narrative agree with the entities it names?
- [ ] Exactly one commit model? Every destructive action subtle-styled with a
      confirm dialog?
- [ ] Shell type declared, one nav owner, every duty placed or declined?
- [ ] Exactly one active destination, derived from the route?
- [ ] Holds at 1440px, 1024px, 768px and 375px, losing nothing on the way down —
      no sideways scroll of the shell, tables scrolling in their own container?
- [ ] Every status from the enum, and that enum in the product's own `enums.ts`?
- [ ] Every label from the verb table? Every screen and breadcrumb segment in the
      route table?
- [ ] Would deleting this product mean deleting exactly one folder, with nothing
      of it left anywhere else?
- [ ] If this is auth, does §7.7 pass?
- [ ] Does this look like a considered product, or like the default assembly?
