# Components · RJ DTC Admin design system

Generated by `phase-4/docs/components/src/build.py` from `phase-2/kit/` (tokens.css, base.css, family.css files) and the approved surfaces in `phase-3/pages/`. Do not edit by hand; edit `src/catalog*.py` and rebuild.

**90 components**, each with a desktop page (`components/<slug>/`) and a dedicated phone page (`components/<slug>/m/`).

**shadcn/ui in `apps/admin` (rj-dtc `origin/main` fc7ff3e8 (2026-10-11)):** 12 of the 13 existing primitives are reused (Alert, Badge, Button, Card, Checkbox, Dialog, Input, Label, Progress, Separator, Skeleton, Switch); 23 are new (AlertDialog, Avatar, Breadcrumb, Calendar, Chart, Collapsible, Command, Drawer, DropdownMenu, Form, HoverCard, Pagination, Popover, RadioGroup, Select, Sheet, Sidebar, Sonner, Table, Tabs, Textarea, ToggleGroup, Tooltip).

Existing but not needed by the kit: Accordion.

## Index

- **Actions:** [Button](#button), [Icon button](#icon-button), [Link button](#link-button)
- **Status and feedback:** [Status chip](#status-chip), [Status filter chip](#status-filter-chip), [Environment badge](#environment-badge), [Alert and banner](#alert), [Toast](#toast), [Empty state](#empty-state), [Skeleton](#skeleton), [Spinner and progress](#spinner-progress), [Error states](#error-states)
- **Form controls:** [Field layout](#field), [Input](#input), [Textarea with counter](#textarea), [Select](#select), [Combobox](#combobox), [Cloth picker](#cloth-picker), [Checkbox](#checkbox), [Radio group](#radio), [Switch](#switch), [Segmented control](#segmented), [Date and date range](#date), [Money input](#money-input), [Multi-currency price group](#price-group), [Tag input](#tag-input), [File and photo uploader](#uploader), [Form section](#form-section), [Sticky save bar](#save-bar)
- **Overlays:** [Side sheet and bottom sheet](#sheet), [Unsaved-changes guard](#unsaved-guard), [Dialog](#dialog), [Confirm](#confirm), [Confirm unverified currency](#confirm-unverified-currency), [Dropdown menu](#dropdown-menu), [Popover](#popover), [Tooltip](#tooltip), [Hover card](#hover-card), [Overlay layer](#overlay-layer)
- **Navigation and shell:** [App shell and sidebar](#sidebar), [Menu item](#menu-item), [Settings sub-nav](#settings-subnav), [Top bar](#top-bar), [Breadcrumbs](#breadcrumbs), [Place switcher](#place-switcher), [Command palette](#command-palette), [Phone tab bar](#tab-bar), [Tabs](#tabs), [Page header](#page-header), [Record header](#record-header), [Phone action bar](#action-bar), [Phone list row](#list-row), [Embedded Payload CMS frame](#payload-frame)
- **Data table:** [Data table](#data-table), [Sortable header](#table-sort), [Filter bar](#filter-bar), [Saved views](#saved-views), [Density](#density), [Sticky header](#sticky-header), [Row and row actions](#row-actions), [Bulk select and bulk bar](#bulk-bar), [Pagination](#pagination), [Column types](#column-types), [Table states](#table-states)
- **Cards and gallery:** [Product card](#product-card), [Cloth card](#cloth-card), [Global Store gallery](#gallery), [Picker mode](#picker)
- **Data display:** [KPI tile](#kpi-tile), [Chart frame](#chart-frame), [Kanban](#kanban), [Timeline and activity feed](#timeline), [Audit row](#audit-row), [Description list](#description-list), [Key/value aside](#key-value), [Support thread and suggested reply](#thread)
- **Wizard and stepper:** [Guided editor](#guided-editor), [Stepper](#stepper), [Step rail and section progress](#step-rail), [Step header](#step-header), [Wizard action bar](#wizard-action-bar), [Step validation](#step-validation), [Review before publish](#review-step), [Step transition](#step-transition)
- **Page help:** [Help trigger](#help-trigger), [Help drawer](#help-drawer)
- **Small primitives:** [Icon](#icon), [Avatar](#avatar), [Keyboard key](#kbd), [Garment thumbnail](#thumb)

---

<a id="button"></a>
## Button

*Actions · family `form-controls` · page `components/button/` · phone `components/button/m/`*

The ranked action. One primary per screen, then secondary, tertiary, ghost and the two danger forms. Labels say what happens: "Publish to brands", "Save draft", "Sync cloths".

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Container | `.btn` | Inline-flex box, 40px tall, 6px radius, 1px border slot (transparent unless outlined). |
| 2 | Leading icon | `svg 16px` | Optional Lucide icon, 16px at 1.75 stroke, `currentColor`. |
| 3 | Label | `text` | Archivo 600 at 13.5px, normal width, never tracked out. |
| 4 | Spinner | `.spin` | Replaces the icon while `aria-busy=true`. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Primary | `.btn.btn-primary` | The one action that finishes the job on this screen. Navy `--primary`, `--e-btn` shadow. |
| Secondary | `.btn.btn-secondary` | Every other real action. White, 1px `--secondary-border`. |
| Tertiary | `.btn.btn-tertiary` | Low-weight, text-like actions beside a primary (Preview, Reset to cloth). |
| Ghost | `.btn.btn-ghost` | Cancel and dismiss. Muted text, no border. |
| Danger | `.btn.btn-danger` | Only inside a confirm, as its primary (Delete price list). |
| Danger quiet | `.btn.btn-danger-quiet` | A destructive entry point on a page (Retire product) that opens a confirm. |
| Small | `.btn.sm` | 34px: row actions, toolbars, toasts. 44px on the phone. |
| Large | `.btn.lg` | 48px: phone sheets and the phone action bar. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Resting fill and border from the variant. | — |
| hover | Fill steps one shade (`--primary-hover`, `--muted`), `--motion-hover` 120ms. | `:hover · .is-hover` |
| focus | 2px `--ring` outline, 2px offset. Keyboard only. | `:focus-visible · .is-focus` |
| active | Settles `--press-shift` (1px) down, `--motion-press` 80ms. | `:active` |
| disabled | 45% opacity, no shadow, no pointer events. Keep a reason nearby. | `[disabled] · [aria-disabled=true]` |
| error | A control has no error state of its own; the form or toast reports it. | — |
| loading | `.spin` replaces the icon, the label becomes the verb in progress ("Publishing…"), clicks are blocked and the width holds. | `[aria-busy=true]` |
| empty | Not applicable: an action always has a label. | — |
| read-only | Hide the action rather than show a dead one; if it must show, use disabled with a reason. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 8px | `var(--s-2)` |
| height | 40px | `var(--control-h)` |
| padding | 0 16px | `0 var(--s-4)` |
| border-radius | 6px | `var(--r-control)` |
| font-size | 13.5px | `var(--text-btn)` |
| font-weight | 600 | `var(--w-semibold)` |
| border | 1px solid transparent | `1px solid transparent` |
| height (sm / default / lg) | 34 / 40 / 48px | `--control-h-sm / --control-h / --control-h-m-lg` |
| phone height | 44px (48px for .lg and sheet buttons) | `--control-h-m / --control-h-m-lg` |
| label size (sm / default / lg) | 12.5 / 13.5 / 15px | `--text-btn-sm / --text-btn / --text-btn-lg` |
| icon | 16px, 1.75 stroke (14px in .sm) | — |
| spinner | 14px, 2px border, 0.8s linear | `.spin` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--primary-hover` | `223 42% 18%  (#1B2641)` | #1B2641 |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--secondary-border-hover` | `220 10% 70%  (#ABB0BA)` | #ABB0BA |
| `--control-h-m` | `44px` | 44px |
| `--control-h-m-lg` | `48px` | 48px |
| `--text-btn-lg` | `15px` | 15px |
| `--s-2` | `8px` | 8px |
| `--control-h` | `40px` | 40px |
| `--s-4` | `16px` | 16px |
| `--r-control` | `6px` | 6px |
| `--text-btn` | `13.5px` | 13.5px |
| `--w-semibold` | `600` | 600 |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--motion-focus` | `140ms var(--ease-standard)` | 140ms cubic-bezier(.2,0,0,1) |
| `--motion-press` | `80ms var(--ease-standard)` | 80ms cubic-bezier(.2,0,0,1) |
| `--press-shift` | `1px` | 1px |
| `--control-h-sm` | `34px` | 34px |
| `--s-3` | `12px` | 12px |
| `--text-btn-sm` | `12.5px` | 12.5px |
| `--s-5` | `20px` | 20px |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--primary-foreground` | `60 17% 98%  (#FBFBF9)` | #FBFBF9 |
| `--e-btn` | `0 1px 2px rgba(14,23,41,.20)` | 0 1px 2px rgba(14,23,41,.20) |
| `--secondary` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--secondary-foreground` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--secondary-border` | `219 14% 79%  (#C2C7D1)` | #C2C7D1 |
| `--e-btn-2` | `0 1px 1px rgba(14,23,41,.05)` | 0 1px 1px rgba(14,23,41,.05) |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--destructive-foreground` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--motion-spin` | `.8s` | .8s |
| `--ease-linear` | `linear` | linear |
| `--motion-spin-reduced` | `2.4s` | 2.4s |

### Motion

- Colour and border on `--motion-hover` (120ms, `--ease-standard`).
- Press: `translateY(var(--press-shift))`, 1px, on `--motion-press` (80ms).
- Focus ring fades in on `--motion-focus` (140ms).
- **No glow.** The sheen belongs to the sidebar only; a button never gets an outer or inner glow.

### Accessibility

- **Keyboard:** Enter and Space activate. Tab order follows reading order; the primary is last in a footer row.
- **ARIA:** Use a real `<button>` (or `<a>` for navigation, see Link button).
- **ARIA:** Loading: `aria-busy="true"` and keep the visible label; a polite live region announces the result.
- **ARIA:** Icon-only? Use Icon button with `aria-label`.
- **Focus:** `:focus-visible` 2px `--ring` outline, 2px offset; never removed.

### Content rules

- Verb first, the object after: "Publish to brands", "Save draft", "Sync cloths".
- Sentence case, no full stop, no exclamation mark.
- Loading labels use the progressive verb: "Publishing…", "Syncing…".

**Do**

- One `btn-primary` per screen or sheet.
- Rank left to right: tertiary, secondary, primary (primary at the end of the row).
- Keep the danger primary inside the confirm that names the consequence.

**Don't**

- Two primaries side by side.
- "OK", "Submit" or "Click here".
- A glow, gradient or coloured shadow on any button.
- Hide a disabled reason; say why in help text or a tooltip.

### Build it

- **shadcn / Radix:** shadcn `Button` (cva) with `@radix-ui/react-slot` for `asChild`. Add variants `tertiary` and `dangerQuiet`; map `default`→primary, `outline`→secondary, `destructive`→danger.
- **Exists in apps/admin today:** Button. `apps/admin/src/components/ui/button.tsx` exists (h-9 = 36px, `hover:bg-primary/90`). Restyle: heights 34/40/48, Archivo 600, `--primary-hover` on hover, `--e-btn` shadow, press shift.
- **Rendering:** server component. Server-renderable; only the click handler makes its parent a client component.

```ts
type ButtonVariant = "primary" | "secondary" | "tertiary" | "ghost" | "danger" | "dangerQuiet";
type ButtonSize = "sm" | "md" | "lg";

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: ButtonVariant;          // default "secondary"; exactly one "primary" per screen
  size?: ButtonSize;                // sm 34px · md 40px · lg 48px (phone: min 44px)
  icon?: LucideIcon;                // leading, 16px (14px when size="sm")
  loading?: boolean;                // sets aria-busy, swaps icon for spinner, blocks clicks
  loadingLabel?: string;            // "Publishing…"
  asChild?: boolean;                // Radix Slot: render a Next <Link> with button styling
}
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [Sign in](../../phase-3/pages/sign-in/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="icon-button"></a>
## Icon button

*Actions · family `form-controls` · page `components/icon-button/` · phone `components/icon-button/m/`*

A square button that carries only an icon: row menus (⋯), edit, close, remove. Always named for assistive tech and usually paired with a tooltip.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Container | `.icon-btn` | 36×36 square, 6px radius, 1px `--border`. |
| 2 | Icon | `svg 16px` | Lucide, `currentColor`. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Outlined | `.icon-btn` | Stand-alone tools: edit, filter, copy. |
| Ghost | `.icon-btn.ghost` | Row menu ⋯, close ×, toolbar icons on a busy surface. |
| Small | `.icon-btn.sm` | 32px: inside rows, chips and price lines. |
| Danger (local) | `.icon-btn.sm.danger` | Remove a line; still opens nothing destructive without undo. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Resting fill and border from the variant. | — |
| hover | Fill steps one shade (`--primary-hover`, `--muted`), `--motion-hover` 120ms. | `:hover · .is-hover` |
| focus | 2px `--ring` outline, 2px offset. Keyboard only. | `:focus-visible · .is-focus` |
| active | Settles `--press-shift` (1px) down, `--motion-press` 80ms. | `:active` |
| disabled | 45% opacity, no shadow, no pointer events. Keep a reason nearby. | `[disabled] · [aria-disabled=true]` |
| error | A control has no error state of its own; the form or toast reports it. | — |
| loading | Label becomes the progressive verb, `.spin` leads, clicks are blocked, width holds. | `[aria-busy=true]` |
| empty | Not applicable: an action always has a label. | — |
| read-only | Hide the action rather than show a dead one; if it must show, use disabled with a reason. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| width | 36px | `36px` |
| height | 36px | `36px` |
| border-radius | 6px | `var(--r-control)` |
| border | 1px solid #E3E7ED | `1px solid hsl(var(--border))` |
| background | #FFFFFF | `hsl(var(--secondary))` |
| color | #0F1524 | `hsl(var(--foreground))` |
| size (default / sm / phone) | 36 / 32 / 44px | `--control-h-m on .kit-m` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--control-h-m` | `44px` | 44px |
| `--r-control` | `6px` | 6px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--secondary` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |

### Motion

- Background and border on `--motion-hover`.

### Accessibility

- **Keyboard:** Enter and Space activate.
- **ARIA:** `aria-label` is mandatory and names the action plus object: "More actions for Navy birdseye two-piece suit".
- **ARIA:** A menu trigger carries `aria-haspopup="menu"` and `aria-expanded`.
- **Focus:** 2px `--ring` outline, 2px offset.

### Content rules

- The label is the tooltip text too; keep it to the verb and object.

**Do**

- Pair with a `Tooltip` on desktop.
- Use ghost inside tables so rows stay quiet.

**Don't**

- Use an icon button for the primary action.
- Invent a glyph: use the Lucide name from the icon list.

### Build it

- **shadcn / Radix:** shadcn `Button` with `size="icon"` (+ `iconSm`).
- **Exists in apps/admin today:** Button. Exists as `Button size="icon"` (36px). Add `iconSm` (32px) and the ghost muted colour.
- **Rendering:** server component. 

```ts
interface IconButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
  icon: LucideIcon;
  label: string;                    // becomes aria-label and the tooltip
  variant?: "outline" | "ghost" | "danger";
  size?: "sm" | "md";               // 32 / 36px (44px on the phone)
  tooltip?: boolean;                // default true on desktop
}
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [Sign in](../../phase-3/pages/sign-in/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="link-button"></a>
## Link button

*Actions · family `page-primitives` · page `components/link-button/` · phone `components/link-button/m/`*

An anchor styled as a button, for navigation that looks like an action ("Open order", "View in Global Store"). Kit v2 scopes the link-colour reset so a page's `a{color:inherit}` cannot strip a filled button.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Anchor | `a.btn` | Same box as Button; `text-decoration:none`. |
| 2 | Label | `text` | Keeps `--primary-foreground` / `--destructive-foreground` on filled variants. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Primary link | `a.btn.btn-primary` | Navigates to the next step of a flow. |
| Secondary link | `a.btn.btn-secondary` | Open a related record. |
| Danger link | `a.btn.btn-danger` | Rare: navigates to a destructive flow's confirm page. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Resting fill and border from the variant. | — |
| hover | Fill steps one shade (`--primary-hover`, `--muted`), `--motion-hover` 120ms. | `:hover · .is-hover` |
| focus | 2px `--ring` outline, 2px offset. Keyboard only. | `:focus-visible · .is-focus` |
| active | Settles `--press-shift` (1px) down, `--motion-press` 80ms. | `:active` |
| disabled | Anchors cannot be disabled: render a `<button disabled>` or plain text instead. | — |
| error | A control has no error state of its own; the form or toast reports it. | — |
| loading | Not applicable: navigation shows the route loader. | — |
| empty | Not applicable: an action always has a label. | — |
| read-only | Hide the action rather than show a dead one; if it must show, use disabled with a reason. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| box | identical to Button (34 / 40 / 48px, 6px radius) | `--control-h-sm / --control-h / --control-h-m-lg` |
| text-decoration | none | `a.btn` |
| filled text colour | kept under a page-level a{color:inherit} | `--primary-foreground / --destructive-foreground` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--primary-foreground` | `60 17% 98%  (#FBFBF9)` | #FBFBF9 |
| `--destructive-foreground` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |

### Accessibility

- **Keyboard:** Enter follows the link; Space does not (it is a link).
- **ARIA:** Never `role="button"` on an anchor that navigates.
- **Focus:** Same ring as Button.

### Content rules

- Name the destination: "Open order RJC-20418".

**Do**

- Use `<Button asChild><Link href=…/></Button>` in Next.js.

**Don't**

- Use a link button to submit or mutate.

### Build it

- **shadcn / Radix:** shadcn `Button asChild` wrapping `next/link`.
- **Exists in apps/admin today:** Button. Button exists; `asChild` is supported via Slot.
- **Rendering:** server component. 

```ts
interface LinkButtonProps extends Omit<ButtonProps, "loading" | "asChild"> {
  href: string;                     // next/link href
  external?: boolean;               // adds the external-link icon and rel="noopener"
}
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [How customers buy](../../phase-3/pages/selling/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Sign in](../../phase-3/pages/sign-in/), [Inbox](../../phase-3/pages/support/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="status-chip"></a>
## Status chip

*Status and feedback · family `status-feedback` · page `components/status-chip/` · phone `components/status-chip/m/`*

The square badge that says what state a thing is in: dot plus label on a 12% soft fill. One vocabulary for the whole admin, owned by status-feedback; catalogue items are always Draft, Available or Retired.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Container | `.chip` | 22px tall, 4px radius (`--r-chip`), 8px side padding, status at 12% fill. |
| 2 | Dot | `.chip::before` | 6px circle in `currentColor`; hidden by `.plain` and `.ico`. |
| 3 | Label | `text` | Archivo 600 at 11.5px; the colour is the status colour (≥ 4.5:1 on its fill). |
| 4 | Count (optional) | `.cnt` | Tabular count on a white pill. |
| 5 | Icon (optional) | `.chip.ico svg` | 12px Lucide icon instead of the dot (Locked, Not read). |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Draft · Available · Retired | `.chip.draft \| .available \| .retired` | Catalogue lifecycle. Never use the generic role names in product UI. |
| Info · Success · Warning · Danger | `.chip.info \| .success \| .warning \| .danger` | Order, payment, ERP, release and support states (see the vocabulary table). |
| Locked | `.chip.locked` | Admin-only or locked record; muted ground. |
| Ended | `.chip.ended` | A campaign that ran its course: neutral, not red. |
| Not mapped | `.chip.unmapped` | Dashed not-wired outline: a value the backend does not send yet. |
| Plain / mono | `.chip.plain · .chip.mono` | No dot (Special order, Low); mono for versions like V4. |
| Large | `.chip.lg` | 26px, beside a page title in a record header. |
| Dot only | `.dot.success\|warning\|danger\|info\|neutral` | 8px dot + text label inside dense table cells. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Static: chips are not buttons. Filters use Status filter chip. | — |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Not applicable. | — |
| disabled | Not applicable: hide data the person cannot see, never grey it. | — |
| error | Names the real cause and offers Try again (see Error states). | `.sf-alert.danger` |
| loading | `.skel` shapes in the final layout, pulsing on `--motion-skeleton`. | `.skel` |
| empty | A missing status renders "—" in muted text, never an empty chip. | — |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 6px | `6px` |
| height | 22px | `22px` |
| padding | 0 8px | `0 var(--s-2)` |
| border-radius | 4px | `var(--r-chip)` |
| font | 600 11.5px/1 'Archivo', system-ui, sans-serif | `var(--w-semibold) var(--text-micro)/1 var(--font-voice)` |
| color | #575E70 | `hsl(var(--neutral))` |
| background | #575E70 @0.12 | `hsl(var(--neutral)/var(--status-soft-alpha))` |
| height (default / lg) | 22 / 26px | `--chip-h / --chip-h-lg` |
| dot | 6px (7px in .lg) | — |
| soft fill | status colour at 12% | `--status-soft-alpha` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--s-2` | `8px` | 8px |
| `--r-chip` | `4px` | 4px |
| `--w-semibold` | `600` | 600 |
| `--text-micro` | `11.5px` | 11.5px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--neutral` | `222 13% 39%  (#575E70)` | #575E70 |
| `--status-soft-alpha` | `.12` | .12 |
| `--draft` | `var(--info)` | #335694 |
| `--available` | `var(--success)` | #20644B |
| `--retired` | `var(--neutral)` | #575E70 |
| `--info` | `218 49% 39%  (#335694)` | #335694 |
| `--warning` | `36 79% 29%  (#845610)` | #845610 |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--locked` | `var(--neutral)` | #575E70 |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--chip-h-lg` | `26px` | 26px |
| `--text-btn-sm` | `12.5px` | 12.5px |
| `--font-code` | `'IBM Plex Mono', ui-monospace, monospace` | 'IBM Plex Mono', ui-monospace, monospace |
| `--w-medium` | `500` | 500 |
| `--text-11` | `11px` | 11px |
| `--font-ui` | `'IBM Plex Sans', system-ui, sans-serif` | 'IBM Plex Sans', system-ui, sans-serif |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--success` | `158 51% 26%  (#20644B)` | #20644B |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--not-wired` | `var(--warning)` | #845610 |
| `--r-control` | `6px` | 6px |
| `--s-3` | `12px` | 12px |
| `--text-small` | `12px` | 12px |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |

### Accessibility

- **Keyboard:** Not focusable.
- **ARIA:** The text label carries the meaning; colour is never the only signal.
- **ARIA:** A chip that changes live sits inside `aria-live="polite"` (e.g. sync state).
- **Focus:** None.

### Content rules

- Catalogue: Draft, Available, Retired (+ version: "Available v4").
- Payment: Awaiting payment, Paid, Payment failed. Fulfilment: Awaiting, In production, Shipped (info blue, not green), Delivered. Refunded is neutral.
- ERP: In sync, Retrying, ERP failed, "3 stuck", Not read. Support: Open, Re-opened, Waiting on us, Resolved; severity Low / Medium / High.
- Campaign: Draft, Scheduled, Live, Ended. Refund request: Raised, In review, Cleared, Declined.
- Two words at most; sentence case; no full stop.

**Do**

- Use the exact vocabulary from status-feedback.
- Put the version next to the catalogue state when it matters ("Available v4").

**Don't**

- Red for Refunded or Ended.
- Green for Shipped.
- Invent a new state name for one page.

### Build it

- **shadcn / Radix:** shadcn `Badge` with status variants (draft, available, retired, info, success, warning, danger, locked, ended, unmapped) and `size` sm | lg.
- **Exists in apps/admin today:** Badge. `ui/badge.tsx` exists (rounded-md pill, zinc variants). Rework to the 4px square chip with dot and soft fill.
- **Rendering:** server component. 

```ts
type Status =
  | "draft" | "available" | "retired"            // catalogue (use these, never the role names)
  | "info" | "success" | "warning" | "danger"
  | "locked" | "ended" | "unmapped" | "neutral";

interface StatusChipProps extends React.HTMLAttributes<HTMLSpanElement> {
  status: Status;
  children: React.ReactNode;        // the label, e.g. "Available v4"
  size?: "sm" | "lg";               // 22 / 26px
  dot?: boolean;                    // default true; false = .plain
  icon?: LucideIcon;                // replaces the dot (.ico)
  mono?: boolean;                   // versions and codes
  count?: number;                   // .cnt pill
}
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="status-filter-chip"></a>
## Status filter chip

*Status and feedback · family `status-feedback` · page `components/status-filter-chip/` · phone `components/status-filter-chip/m/`*

A pressable chip with a count that filters a list by status. The counts on a filter row always add up to the total the page shows.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Toggle | `.sf-filter[aria-pressed]` | Outlined chip button, 6px radius. |
| 2 | Dot | `.dot` | Status colour. |
| 3 | Label | `text` | Status name. |
| 4 | Count | `.n` | Tabular count; amber when it needs attention (`.warn`). |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Default | `.sf-filter` | One status. |
| All | `.sf-filter (no dot)` | Clears the status filter; first in the row. |
| Attention | `.sf-filter.warn` | Count in warning amber (stuck, failed). |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Unselected mark on a 1.5px `--input` edge. | — |
| hover | Edge darkens to `--input-hover`. | `:hover · .is-hover` |
| focus | `--ring` border plus `--focus-ring` halo. | `.is-focus · :focus-visible` |
| active | Pressed: `--accent` fill, border `--primary` at 40%, count goes semibold foreground. | `[aria-pressed=true]` |
| disabled | 45% opacity: no rows with that status. | `[disabled]` |
| error | Edge `--destructive`; the group's `.help.err` explains. | `.err` |
| loading | Disable while saving; the row shows `.spin` after the label. | `.busy` |
| empty | Count 0 stays visible but disabled. | — |
| read-only | Shown as the value in text, or locked (`.lock`) with the reason on hover. | `.lock` |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 8px | `var(--s-2)` |
| height | 32px | `32px` |
| padding | 0 12px | `0 var(--s-3)` |
| border-radius | 6px | `var(--r-control)` |
| border | 1px solid #C2C7D1 | `1px solid hsl(var(--secondary-border))` |
| background | #FFFFFF | `hsl(var(--background))` |
| font | 500 12.5px/1 'Archivo', system-ui, sans-serif | `var(--w-medium) 12.5px/1 var(--font-voice)` |
| color | #0F1524 | `hsl(var(--foreground))` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--s-2` | `8px` | 8px |
| `--s-3` | `12px` | 12px |
| `--r-control` | `6px` | 6px |
| `--secondary-border` | `219 14% 79%  (#C2C7D1)` | #C2C7D1 |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--w-medium` | `500` | 500 |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--motion-focus` | `140ms var(--ease-standard)` | 140ms cubic-bezier(.2,0,0,1) |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--w-regular` | `400` | 400 |
| `--font-ui` | `'IBM Plex Sans', system-ui, sans-serif` | 'IBM Plex Sans', system-ui, sans-serif |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--w-semibold` | `600` | 600 |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--focus-ring` | `0 0 0 1px hsl(var(--ring)), 0 0 0 4px hsl(var(--ring) / .14)` | 0 0 0 1px #3F60A6, 0 0 0 4px #3F60A6 @0.14 |
| `--warning` | `36 79% 29%  (#845610)` | #845610 |

### Accessibility

- **Keyboard:** Tab to each chip; Space toggles.
- **ARIA:** `aria-pressed` on each; the row is a `role="group"` with an `aria-label` ("Filter by status").
- **ARIA:** When single-select, use `ToggleGroup type="single"` semantics.
- **Focus:** Halo ring as fields.

### Content rules

- Label = the status name exactly; count after it.

**Do**

- Make counts add up to the page total.

**Don't**

- Use the chip colour fill for the pressed state; pressed is the accent ground.

### Build it

- **shadcn / Radix:** Radix `ToggleGroup` (single or multiple) via shadcn `toggle-group`.
- **Exists in apps/admin today:** no · new: ToggleGroup. Not in apps/admin.
- **Rendering:** client component (`"use client"`). 
- **Install:** `npx shadcn@latest add toggle-group`

```ts
interface StatusFilterOption { value: Status | "all"; label: string; count: number; attention?: boolean }

interface StatusFilterChipsProps {
  options: StatusFilterOption[];
  value: string[];                  // pressed values
  onValueChange: (value: string[]) => void;
  multiple?: boolean;               // default false
  "aria-label"?: string;            // "Filter by status"
}
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [Sign in](../../phase-3/pages/sign-in/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="environment-badge"></a>
## Environment badge

*Status and feedback · family `status-feedback` · page `components/environment-badge/` · phone `components/environment-badge/m/`*

Tells staff which copy of the admin they are in: STAGING in ochre, PRODUCTION and LOCAL in slate. Sits in the top bar, right, before the help trigger.

> **Open in the kit.** The badge is 10.5px, under the 11.5px type floor; kept because board 3 approved it (KIT-CHANGELOG, Open).

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Badge | `.staging · .env-prod · .env-local` | Uppercase Plex 600 at 10.5px, tracked .08em (Plex, not Archivo), 6px radius. |
| 2 | Strip (optional) | `.sf-strip` | Full-width banner variant on staging pages. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Staging | `.staging` | Ochre `--warning` on 12% fill, 40% border. |
| Production | `.env-prod` | Slate `--neutral`, as live. |
| Local | `.env-local` | Slate; developer machines. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Static. | — |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Not applicable. | — |
| disabled | Not applicable: hide data the person cannot see, never grey it. | — |
| error | Names the real cause and offers Try again (see Error states). | `.sf-alert.danger` |
| loading | `.skel` shapes in the final layout, pulsing on `--motion-skeleton`. | `.skel` |
| empty | Says what is missing and the one next step (see Empty state). | `.sf-empty` |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| font | 600 10.5px/1 'IBM Plex Sans', system-ui, sans-serif | `var(--w-semibold) var(--text-10-5)/1 var(--font-ui)` |
| color | #845610 | `hsl(var(--warning))` |
| background | #845610 @0.12 | `hsl(var(--warning)/.12)` |
| border | 1px solid #845610 @0.4 | `1px solid hsl(var(--warning)/.4)` |
| padding | 5px 8px | `5px 8px` |
| border-radius | 6px | `var(--r-control)` |
| font | 600 10.5px/1 IBM Plex Sans, uppercase, .08em | `board 3 value; below the 11.5px floor (open item)` |
| padding | 5px 8px | — |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--w-semibold` | `600` | 600 |
| `--text-10-5` | `10.5px` | 10.5px |
| `--font-ui` | `'IBM Plex Sans', system-ui, sans-serif` | 'IBM Plex Sans', system-ui, sans-serif |
| `--warning` | `36 79% 29%  (#845610)` | #845610 |
| `--r-control` | `6px` | 6px |
| `--neutral` | `222 13% 39%  (#575E70)` | #575E70 |
| `--s-3` | `12px` | 12px |
| `--s-5` | `20px` | 20px |
| `--text-small` | `12px` | 12px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--s-6` | `24px` | 24px |
| `--s-2` | `8px` | 8px |
| `--w-medium` | `500` | 500 |
| `--text-micro` | `11.5px` | 11.5px |
| `--font-code` | `'IBM Plex Mono', ui-monospace, monospace` | 'IBM Plex Mono', ui-monospace, monospace |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |

### Accessibility

- **Keyboard:** Not focusable.
- **ARIA:** Plain text; screen readers read "Staging". Add `title` with the host name.
- **Focus:** None.

### Content rules

- One word: STAGING, PRODUCTION, LOCAL.

**Do**

- Show on every page of every environment.

**Don't**

- Green for production.
- Hide it on phone: it sits in the phone bar.

### Build it

- **shadcn / Radix:** shadcn `Badge` variant `env`.
- **Exists in apps/admin today:** Badge. Badge exists; env variants are new.
- **Rendering:** server component. 

```ts
interface EnvironmentBadgeProps { env: "staging" | "production" | "local"; host?: string }
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [Sign in](../../phase-3/pages/sign-in/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="alert"></a>
## Alert and banner

*Status and feedback · family `status-feedback` · page `components/alert/` · phone `components/alert/m/`*

An inline message block (Alert) or a full-width strip under the top bar (Banner). Six kinds: info, success, warning, danger, **locked** and **not wired** (the feature exists but the backend is not connected).

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Container | `.sf-alert` | 8px radius, 1px border at 34% of the kind colour, 7% tint ground. |
| 2 | Icon | `.sf-alert>svg` | 18px Lucide in the kind colour. |
| 3 | Title | `b` | Archivo 600, one line. |
| 4 | Body | `p` | Plex 13px foreground (16:1 on the tint). |
| 5 | Actions (optional) | `.act` | Small secondary buttons; one at most is primary-weight. |
| 6 | Dismiss (optional) | `.icon-btn.ghost.sm` | Only for info and success. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Info | `.sf-alert.info` | Neutral explanation. |
| Success | `.sf-alert.success` | Completed, with a next step. |
| Warning | `.sf-alert.warning` | Will cause trouble if ignored. |
| Danger | `.sf-alert.danger` | Something failed; name the cause and the fix. |
| Locked | `.sf-alert.locked` | Muted ground: you cannot change this here, and why. |
| Not wired | `.sf-alert.wired` | Dashed amber edge: the UI is real, the backend is not connected yet. |
| Banner | `.sf-banner.(warning\|danger\|info)` | Page-wide notice directly under the top bar. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Static. | — |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Not applicable. | — |
| disabled | Not applicable: hide data the person cannot see, never grey it. | — |
| error | Names the real cause and offers Try again (see Error states). | `.sf-alert.danger` |
| loading | An alert never loads; show it when the fact is known. | — |
| empty | Says what is missing and the one next step (see Empty state). | `.sf-empty` |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 12px | `var(--s-3)` |
| padding | 12px 16px | `var(--s-3) var(--s-4)` |
| border | 1px solid hsl(var(--c)/.34) | `1px solid hsl(var(--c)/.34)` |
| background | hsl(var(--c)/.07) | `hsl(var(--c)/.07)` |
| border-radius | 8px | `var(--r-card)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--info` | `218 49% 39%  (#335694)` | #335694 |
| `--s-3` | `12px` | 12px |
| `--s-4` | `16px` | 16px |
| `--r-card` | `8px` | 8px |
| `--success` | `158 51% 26%  (#20644B)` | #20644B |
| `--warning` | `36 79% 29%  (#845610)` | #845610 |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--locked` | `var(--neutral)` | #575E70 |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--secondary-border` | `219 14% 79%  (#C2C7D1)` | #C2C7D1 |
| `--not-wired` | `var(--warning)` | #845610 |
| `--w-semibold` | `600` | 600 |
| `--text-card` | `13.5px` | 13.5px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--text-body` | `13px` | 13px |
| `--lh-body` | `1.45` | 1.45 |
| `--s-1` | `4px` | 4px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--s-2` | `8px` | 8px |
| `--s-10` | `40px` | 40px |
| `--w-medium` | `500` | 500 |
| `--motion-toast-out` | `140ms var(--ease-out)` | 140ms cubic-bezier(.4,0,1,1) |
| `--s-5` | `20px` | 20px |

### Accessibility

- **Keyboard:** Actions inside are ordinary buttons.
- **ARIA:** `role="alert"` only for danger raised by the person's own action; otherwise `role="status"`.
- **ARIA:** The icon is decorative (`aria-hidden`); the title states the kind in words.
- **Focus:** Do not move focus to an alert; a validation summary is the exception (see Step validation).

### Content rules

- Title: the fact ("Cloth sync failed"). Body: the cause and the fix ("Holland & Sherry did not answer. Try again in a minute.").
- Not wired: "Not connected yet" + what will happen when it is.
- Locked: who can change it ("Only HQ can change brand currency").

**Do**

- Name the real cause.
- Use not wired honestly rather than faking data.

**Don't**

- "Something went wrong".
- Stack more than two alerts; summarise instead.

### Build it

- **shadcn / Radix:** shadcn `Alert` with variants info, success, warning, destructive, locked, notWired; `Banner` is a full-width Alert variant.
- **Exists in apps/admin today:** Alert. `ui/alert.tsx` exists (default + destructive). Add the four kinds, the tint and the dashed not-wired edge.
- **Rendering:** server component. 

```ts
interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
  kind: "info" | "success" | "warning" | "danger" | "locked" | "notWired";
  title: string;
  children?: React.ReactNode;       // cause + fix
  actions?: React.ReactNode;        // small buttons
  onDismiss?: () => void;           // info / success only
  layout?: "inline" | "banner";
}
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [API Access](../../phase-3/pages/super-api-access/), [Cloth library](../../phase-3/pages/super-cloths/), [Inbox](../../phase-3/pages/support/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="toast"></a>
## Toast

*Status and feedback · family `status-feedback` · page `components/toast/` · phone `components/toast/m/`*

A short confirmation that rises from the bottom right and leaves on its own: success, error, warning, info, **undo** and progress. Built as Sonner in the app.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Surface | `.toast` | White popover, 6px radius, `--e-toast`, 11px 12px padding, 380px max on desktop. |
| 2 | Icon | `.toast>svg` | Kind colour (success green by default). |
| 3 | Title | `b` | 13px semibold, one line. |
| 4 | Detail (optional) | `small` | 11.5px muted. |
| 5 | Action (optional) | `.act .btn` | Undo / View; 30px (44px on the phone). |
| 6 | Close (optional) | `.act .icon-btn` | 30px. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Success | `.toast` | Saved, published, synced. |
| Error | `.toast.error` | The action failed; offer Try again. |
| Warning | `.toast.warning` | Done, with a caveat. |
| Info | `.toast.info` | Background job started. |
| Undo | `.toast.undo [data-k-undo]` | Reversible destructive action; stays `--toast-dwell-undo` (5.2s). |
| Progress | `.toast.progress` | Long job; spinner, then becomes success or error. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Hidden until fired. | — |
| hover | Not applicable to the surface; its buttons and rows hover as usual. | — |
| focus | Toasts do not take focus; their action is reachable with the Sonner hotkey (F8) and Tab. | — |
| active | Shown: rises `--toast-shift` (8px) on `--motion-toast-in` (200ms). | `.k-toast.show` |
| disabled | The opener is disabled, never the overlay. | — |
| error | The body shows an inline `.sf-alert.danger`; the overlay stays open. | `.sf-alert.danger` |
| loading | Progress kind with `.spin`. | `.toast.progress` |
| empty | Body shows a compact empty state. | `.sf-empty.compact` |
| read-only | Footer drops the primary and keeps Close. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 10px | `10px` |
| padding | 11px 12px | `11px 12px` |
| border-radius | 6px | `var(--r-control)` |
| background | #FFFFFF | `hsl(var(--popover))` |
| color | #0F1524 | `hsl(var(--popover-foreground))` |
| border | 1px solid #E3E7ED | `1px solid hsl(var(--border))` |
| box-shadow | 0 12px 28px -14px rgba(14,23,41,.35) | `var(--e-toast)` |
| width | 100% | `100%` |
| dwell | 2600ms (undo 5200ms) | `--toast-dwell / --toast-dwell-undo` |
| stack gap | 8px | `--toast-gap` |
| max stacked | 3 visible | — |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--toast-shift` | `8px` | 8px |
| `--motion-toast-out` | `140ms var(--ease-out)` | 140ms cubic-bezier(.4,0,1,1) |
| `--motion-toast-in` | `200ms var(--ease-in)` | 200ms cubic-bezier(.22,1,.36,1) |
| `--r-control` | `6px` | 6px |
| `--popover` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--popover-foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--e-toast` | `0 12px 28px -14px rgba(14,23,41,.35)` | 0 12px 28px -14px rgba(14,23,41,.35) |
| `--success` | `158 51% 26%  (#20644B)` | #20644B |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--warning` | `36 79% 29%  (#845610)` | #845610 |
| `--info` | `218 49% 39%  (#335694)` | #335694 |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--w-semibold` | `600` | 600 |
| `--text-micro` | `11.5px` | 11.5px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--s-2` | `8px` | 8px |
| `--control-h-m` | `44px` | 44px |
| `--s-3` | `12px` | 12px |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--s-4` | `16px` | 16px |
| `--s-8` | `32px` | 32px |
| `--s-1` | `4px` | 4px |

### Motion

- In: opacity + `translateY(8px → 0)` on `--motion-toast-in` (200ms, `--ease-in`).
- Out: `--motion-toast-out` (140ms, `--ease-out`).
- Stack: older toasts scale to .96 / .92 and rise 8 / 16px; hover expands the stack.

### Accessibility

- **Keyboard:** F8 focuses the toaster (Sonner default); Escape dismisses the focused toast.
- **ARIA:** Region `role="status"` `aria-live="polite"`; errors use `aria-live="assertive"`.
- **Focus:** Never steals focus; dwell pauses while hovered or focused.

### Content rules

- Past tense fact: "Published to 2 brands", "Price list deleted".
- Undo label is just "Undo".

**Do**

- Use undo instead of a confirm for reversible deletes.

**Don't**

- Put a form or a choice in a toast.
- Toast an error that needs reading; use an Alert.

### Build it

- **shadcn / Radix:** shadcn `sonner` (`<Toaster position="bottom-right" />`), styled with the kit `.toast` anatomy.
- **Exists in apps/admin today:** no · new: Sonner. Not in apps/admin.
- **Rendering:** client component (`"use client"`). 
- **Install:** `npx shadcn@latest add sonner`

```ts
type ToastKind = "success" | "error" | "warning" | "info" | "undo" | "progress";

interface ToastOptions {
  kind?: ToastKind;
  title: string;                    // past-tense fact
  detail?: string;
  action?: { label: string; onClick: () => void };   // "Undo", "View"
  duration?: number;                // default --toast-dwell 2600; undo 5200
}
declare function toast(opts: ToastOptions): string | number;
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [Sign in](../../phase-3/pages/sign-in/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="empty-state"></a>
## Empty state

*Status and feedback · family `status-feedback` · page `components/empty-state/` · phone `components/empty-state/m/`*

What a list or panel shows when it has nothing: first use, no results for a filter, or no permission. Says what is missing and offers the one next step.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Container | `.sf-empty` | Centred column, 48px × 24px padding. |
| 2 | Icon | `svg` | 24px Lucide in a muted tile. |
| 3 | Title | `h3` | Archivo 600 16px. |
| 4 | Body | `p` | 13px muted, 42ch max. |
| 5 | Actions | `.act` | One primary (first use) or a secondary Clear filters (no results). |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| First use | `.sf-empty` | Nothing created yet: explain and offer Create. |
| No results | `.sf-empty` | Filters hide everything: show the active filters and Clear filters. |
| No permission | `.sf-empty` | Locked: who can grant access. |
| Compact | `.sf-compact` | Inside a panel or a sheet: one line with an icon. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Shown in place of the list body, under the toolbar, which stays. | — |
| hover | Rows and cards that open something take `--muted` or lift; static blocks do not react. | `:hover` |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Not applicable. | — |
| disabled | Not applicable: hide data the person cannot see, never grey it. | — |
| error | Names the real cause and offers Try again (see Error states). | `.sf-alert.danger` |
| loading | `.skel` shapes in the final layout, pulsing on `--motion-skeleton`. | `.skel` |
| empty | This is the empty state. | — |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 8px | `var(--s-2)` |
| padding | 48px 24px | `var(--s-12) var(--s-6)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--s-2` | `8px` | 8px |
| `--s-12` | `48px` | 48px |
| `--s-6` | `24px` | 24px |
| `--w-semibold` | `600` | 600 |
| `--text-section` | `16px` | 16px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--tr-section` | `-.005em` | -.005em |
| `--text-body` | `13px` | 13px |
| `--lh-body` | `1.45` | 1.45 |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--s-3` | `12px` | 12px |
| `--text-small` | `12px` | 12px |
| `--s-4` | `16px` | 16px |
| `--success` | `158 51% 26%  (#20644B)` | #20644B |

### Accessibility

- **Keyboard:** Actions are buttons.
- **ARIA:** Heading level follows the page outline (usually h3).
- **Focus:** After Clear filters, focus returns to the search field.

### Content rules

- Title: "No cloths match these filters". Body: name the filters. Action: "Clear filters".
- First use: "No products yet" + "Add from Global Store".

**Do**

- Keep the toolbar so filters can be changed.

**Don't**

- A grey placeholder box.
- Mascots or jokes.

### Build it

- **shadcn / Radix:** No shadcn primitive: a small `EmptyState` component composed of `Button`.
- **Exists in apps/admin today:** no. Not in apps/admin as a shared component.
- **Rendering:** server component. 

```ts
interface EmptyStateProps {
  kind: "firstUse" | "noResults" | "noPermission";
  icon?: LucideIcon;
  title: string;
  children?: React.ReactNode;       // body copy
  action?: React.ReactNode;         // one Button
  compact?: boolean;                // one-line form for panels and sheets
}
```

### Used on

[System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Tailor Roster](../../phase-3/pages/tailors/), [Groups](../../phase-3/pages/variant-groups/), [Variants](../../phase-3/pages/variants/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="skeleton"></a>
## Skeleton

*Status and feedback · family `status-feedback` · page `components/skeleton/` · phone `components/skeleton/m/`*

Grey shapes in the final layout while data loads, pulsing slowly on one shared token. Off under reduced motion.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Bar | `.skel` | 10px tall, 4px radius, foreground at 7.5%. |
| 2 | Square | `.skel.sq` | 28px square for avatars and thumbs. |
| 3 | Row | `.sf-skrow` | A table row in the real column grid. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Text line | `.skel` | Width matches a typical value. |
| Thumb | `.sf-sk.th` | 30×40 garment thumb. |
| Photo | `.sf-sk.ph3` | 3:4 card photo. |
| Chip | `.sf-sk.pill` | 76×22 status chip. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Rows and cards that open something take `--muted` or lift; static blocks do not react. | `:hover` |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Not applicable. | — |
| disabled | Not applicable: hide data the person cannot see, never grey it. | — |
| error | Names the real cause and offers Try again (see Error states). | `.sf-alert.danger` |
| loading | This is the loading state. Every third row is delayed .2s so the pulse does not march. | `.skel` |
| empty | Says what is missing and the one next step (see Empty state). | `.sf-empty` |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| height | 10px | `10px` |
| border-radius | 4px | `var(--r-chip)` |
| background | #0F1524 @0.075 | `hsl(var(--foreground)/.075)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--r-chip` | `4px` | 4px |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--motion-skeleton` | `1.6s var(--ease-standard) infinite alternate` | 1.6s cubic-bezier(.2,0,0,1) infinite alternate |
| `--r-media` | `4px` | 4px |
| `--s-4` | `16px` | 16px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--row-th` | `36px` | 36px |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--w-semibold` | `600` | 600 |
| `--text-micro` | `11.5px` | 11.5px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |

### Motion

- Pulse: opacity .55 → 1 on `--motion-skeleton` (1.6s, `--ease-standard`, infinite alternate).
- Reduced motion: `--motion-skeleton: none` (static).

### Accessibility

- **Keyboard:** Not focusable.
- **ARIA:** The container sets `aria-busy="true"`; skeleton shapes are `aria-hidden`.
- **ARIA:** A visually hidden "Loading cloths" live message.
- **Focus:** None.

### Content rules

- No text inside skeletons.

**Do**

- Match the final layout so nothing jumps.

**Don't**

- A shimmer sweep (not approved; it became the pulse).
- Spinners for a list that has a known shape.

### Build it

- **shadcn / Radix:** shadcn `Skeleton`.
- **Exists in apps/admin today:** Skeleton. `ui/skeleton.tsx` exists (`animate-pulse bg-primary/10`). Swap to the kit pulse token.
- **Rendering:** server component. 

```ts
interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {
  shape?: "line" | "square" | "thumb" | "photo" | "chip";
  width?: number | string;
}
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [Variants](../../phase-3/pages/variants/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="spinner-progress"></a>
## Spinner and progress

*Status and feedback · family `status-feedback` · page `components/spinner-progress/` · phone `components/spinner-progress/m/`*

A spinner for short, unknown waits inside a control; a progress bar for jobs with a known size (cloth sync, bulk import) and an indeterminate bar for the page loader.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Spinner | `.spin` | 14px ring, 2px border, open right quarter, 0.8s linear. |
| 2 | Inline status | `.sf-inl` | Spinner + "Syncing 214 cloths…". |
| 3 | Progress bar | `.sf-bar` | Track + fill (`i`); `.lg` is 8px. |
| 4 | Label row | `.sf-prog .l` | Job name + "120 of 214". |
| 5 | Step list | `.sf-steps` | Multi-step job, each row done / running / waiting. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Spinner 12 / 14 / 20 / 28 | `.spin.s12 · .spin · .s20 · .s28` | Inside buttons (14), inline (12), panels (20), page (28). |
| Determinate | `.sf-bar` | Known count. |
| Indeterminate | `.sf-bar.ind` | Unknown length; page top loader. |
| Done / stopped | `.sf-bar.ok · .sf-bar.bad` | Green or red fill when the job ends. |
| Step progress | `.sf-steps` | Provisioning a brand, publishing a release. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Rows and cards that open something take `--muted` or lift; static blocks do not react. | `:hover` |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Not applicable. | — |
| disabled | Not applicable: hide data the person cannot see, never grey it. | — |
| error | Bar turns `--destructive` (`.bad`) and the label names what failed. | `.sf-bar.bad` |
| loading | `.skel` shapes in the final layout, pulsing on `--motion-skeleton`. | `.skel` |
| empty | Says what is missing and the one next step (see Empty state). | `.sf-empty` |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| width | 14px | `14px` |
| height | 14px | `14px` |
| border | 2px solid currentColor | `2px solid currentColor` |
| border-radius | 50% | `50%` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--motion-spin` | `.8s` | .8s |
| `--ease-linear` | `linear` | linear |
| `--motion-spin-reduced` | `2.4s` | 2.4s |
| `--r-pill` | `999px` | 999px |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--motion-sheet-in` | `260ms var(--ease-in)` | 260ms cubic-bezier(.22,1,.36,1) |
| `--motion-indeterminate` | `1.5s` | 1.5s |
| `--ease-standard` | `cubic-bezier(.2,0,0,1)` | cubic-bezier(.2,0,0,1) |
| `--success` | `158 51% 26%  (#20644B)` | #20644B |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--s-6` | `24px` | 24px |
| `--s-2` | `8px` | 8px |
| `--text-body` | `13px` | 13px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--info` | `218 49% 39%  (#335694)` | #335694 |
| `--s-3` | `12px` | 12px |
| `--w-semibold` | `600` | 600 |
| `--text-card` | `13.5px` | 13.5px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--text-small` | `12px` | 12px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--w-medium` | `500` | 500 |
| `--secondary-border` | `219 14% 79%  (#C2C7D1)` | #C2C7D1 |
| `--w-regular` | `400` | 400 |
| `--text-micro` | `11.5px` | 11.5px |
| `--font-code` | `'IBM Plex Mono', ui-monospace, monospace` | 'IBM Plex Mono', ui-monospace, monospace |

### Motion

- Spinner: `kit-spin` 0.8s linear infinite; 2.4s under reduced motion (still turning so it reads as busy).
- Indeterminate bar: 38% segment slides on 1.5s `--ease-standard`.

### Accessibility

- **Keyboard:** Not focusable.
- **ARIA:** Progress: `role="progressbar"` with `aria-valuenow/min/max` and an `aria-label`; indeterminate omits `aria-valuenow`.
- **ARIA:** Spinner alone is `aria-hidden`; the text beside it carries the meaning.
- **Focus:** None.

### Content rules

- Count, not percent, when the unit is real: "120 of 214 cloths".

**Do**

- Spinner inside the button that started the action.

**Don't**

- A full-page spinner over content that could show skeletons.

### Build it

- **shadcn / Radix:** shadcn `Progress` (Radix Progress); spinner is a small local component.
- **Exists in apps/admin today:** Progress. `ui/progress.tsx` exists. Add the indeterminate and ok/bad variants; spinner is new markup.
- **Rendering:** server component. 

```ts
interface SpinnerProps { size?: 12 | 14 | 20 | 28; label?: string }

interface ProgressBarProps {
  value?: number;                   // omit for indeterminate
  max?: number;
  label: string;                    // "Syncing cloths"
  detail?: string;                  // "120 of 214"
  tone?: "default" | "ok" | "bad";
  size?: "sm" | "lg";
}
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [Sign in](../../phase-3/pages/sign-in/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="error-states"></a>
## Error states

*Status and feedback · family `status-feedback` · page `components/error-states/` · phone `components/error-states/m/`*

Three sizes of failure: an inline error inside a panel, a partial failure that lists what failed and what worked, and a page-level error when the whole surface cannot load. Each names the real cause.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Inline error | `.sf-inerr` | Danger-edged panel: icon, h4, cause, meta (time, request id), actions. |
| 2 | Partial failure list | `.sf-fails` | One row per failed item with its code and a Retry. |
| 3 | Page error | `.sf-errpage` | Centred: 22px title, explanation, technical box (`.tech`, Plex Mono), Try again + Go back. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Inline | `.sf-inerr` | One panel failed; the rest of the page works. |
| Retried OK | `.sf-inerr.is-ok` | After a successful retry the panel says so and fades back to content. |
| Partial | `.sf-fails` | Bulk action: 3 of 41 failed. |
| Page | `.sf-errpage` | Route-level `error.tsx`. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Rows and cards that open something take `--muted` or lift; static blocks do not react. | `:hover` |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Not applicable. | — |
| disabled | Not applicable: hide data the person cannot see, never grey it. | — |
| error | This is the error state. | — |
| loading | Try again shows `.spin` + "Trying again…". | `[aria-busy=true]` |
| empty | Says what is missing and the one next step (see Empty state). | `.sf-empty` |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 12px | `var(--s-3)` |
| padding | 16px | `var(--s-4)` |
| border | 1px solid #A43240 @0.34 | `1px solid hsl(var(--destructive)/.34)` |
| border-radius | 8px | `var(--r-card)` |
| background | #FFFFFF | `hsl(var(--card))` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--s-3` | `12px` | 12px |
| `--s-12` | `48px` | 48px |
| `--s-10` | `40px` | 40px |
| `--w-semibold` | `600` | 600 |
| `--text-22` | `22px` | 22px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--tr-page` | `-.015em` | -.015em |
| `--text-read` | `15px` | 15px |
| `--lh-read` | `1.55` | 1.55 |
| `--w-regular` | `400` | 400 |
| `--text-code` | `12px` | 12px |
| `--font-code` | `'IBM Plex Mono', ui-monospace, monospace` | 'IBM Plex Mono', ui-monospace, monospace |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--r-card` | `8px` | 8px |
| `--s-4` | `16px` | 16px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--s-2` | `8px` | 8px |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--card` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--text-card` | `13.5px` | 13.5px |
| `--text-body` | `13px` | 13px |
| `--text-small` | `12px` | 12px |
| `--s-1` | `4px` | 4px |
| `--success` | `158 51% 26%  (#20644B)` | #20644B |
| `--r-control` | `6px` | 6px |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |

### Accessibility

- **Keyboard:** Try again is the first focusable element.
- **ARIA:** Inline errors raised by the person's action use `role="alert"`; errors on load use `role="status"`.
- **ARIA:** The technical box is selectable text with a Copy button.
- **Focus:** After a retry, focus stays on the panel heading.

### Content rules

- Cause, then fix: "Cloth sync failed: Holland & Sherry did not answer. Try again".
- Show the request id for support in Plex Mono.

**Do**

- Keep what worked visible in a partial failure.

**Don't**

- "Something went wrong".
- Blank the whole page when one panel failed.

### Build it

- **shadcn / Radix:** shadcn `Alert` (destructive) for inline; Next.js `error.tsx` for the page level.
- **Exists in apps/admin today:** Alert. Alert exists; the patterns are new.
- **Rendering:** client component (`"use client"`). Next.js `error.tsx` boundaries must be client components (they receive `reset`).

```ts
interface InlineErrorProps {
  title: string;                    // "Cloth sync failed"
  cause: string;                    // "Holland & Sherry did not answer."
  requestId?: string;
  at?: Date;
  onRetry?: () => Promise<void>;
}
interface PartialFailureProps {
  summary: string;                  // "3 of 41 orders did not sync"
  failures: { id: string; label: string; code?: string; onRetry?: () => void }[];
}
interface PageErrorProps { error: Error & { digest?: string }; reset: () => void }
```

### Used on

[Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Orders](../../phase-3/pages/orders/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [Create a brand](../../phase-3/pages/settings-brand-new/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Website](../../phase-3/pages/website/)

---

<a id="field"></a>
## Field layout

*Form controls · family `form-controls` · page `components/field/` · phone `components/field/m/`*

The label, control, help text and error stack every input shares. Labels sit above the control in Archivo 600; help and errors sit below in 11.5px.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Label | `.lbl` | Archivo 600 13px, 7px gap to the control. |
| 2 | Optional hint | `.lbl small` | Plex 12px muted, after the label ("Optional"). |
| 3 | Required mark | `.fc-req` | Destructive asterisk; only when most fields are optional. |
| 4 | Control | `.field / .input` | Any control on this page. |
| 5 | Help | `.help` | 11.5px muted, 6px above. |
| 6 | Error | `.help.err` | Replaces help; destructive colour; says how to fix. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Stacked | `.lbl + .field + .help` | Default on desktop and phone. |
| Inline row | `.fc-row` | Short controls side by side (currency + amount). |
| Two columns | `.fc-form-b` | Form section bodies on desktop; one column on the phone. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Holds its children at the documented spacing. | — |
| hover | Not applicable. | — |
| focus | Not applicable; children carry focus. | — |
| active | Not applicable. | — |
| disabled | Not applicable. | — |
| error | Control edge `--destructive`; `.help.err` with the fix. | `.err + .help.err` |
| loading | Children show skeletons in place. | `.skel` |
| empty | Shows the compact empty state inside. | `.sf-empty.compact` |
| read-only | Value as text under the label; no control chrome. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| font-size | 13px | `var(--text-label)` |
| font-weight | 600 | `var(--w-semibold)` |
| label → control | 7px | `margin-bottom on .lbl` |
| control → help | 6px | `margin-top on .help` |
| field → field (vertical) | 20px | `--s-5` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--text-label` | `13px` | 13px |
| `--w-semibold` | `600` | 600 |
| `--w-regular` | `400` | 400 |
| `--text-small` | `12px` | 12px |
| `--font-ui` | `'IBM Plex Sans', system-ui, sans-serif` | 'IBM Plex Sans', system-ui, sans-serif |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--text-micro` | `11.5px` | 11.5px |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--s-2` | `8px` | 8px |
| `--s-3` | `12px` | 12px |

### Accessibility

- **Keyboard:** Clicking the label focuses the control.
- **ARIA:** `<label for>` or wrap; help and error joined with `aria-describedby`; error sets `aria-invalid="true"`.
- **ARIA:** Required: `aria-required` / `required`, never colour alone.
- **Focus:** Focus lives on the control, not the label.

### Content rules

- Labels are nouns ("Handle", "Recommended price"); never instructions.
- Help says what the value is used for, not how to type it.
- Errors: what is wrong and the fix ("Handle is taken. Try navy-birdseye-two-piece-2").

**Do**

- Validate on blur and on submit, not on every key.

**Don't**

- Placeholder as label.
- Labels that explain the UI.

### Build it

- **shadcn / Radix:** shadcn `Form` (react-hook-form + zod) with `FormField`, `FormLabel`, `FormDescription`, `FormMessage`; `Label` (Radix Label).
- **Exists in apps/admin today:** Label · new: Form. `ui/label.tsx` exists. `form.tsx` does not.
- **Rendering:** client component (`"use client"`). Form state (react-hook-form) runs in the client.
- **Install:** `npx shadcn@latest add form`

```ts
interface FieldProps {
  label: string;
  htmlFor: string;
  hint?: string;                    // "Optional"
  help?: string;
  error?: string;                   // replaces help; sets aria-invalid on the control
  required?: boolean;
  children: React.ReactElement;     // the control
}
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [Sign in](../../phase-3/pages/sign-in/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="input"></a>
## Input

*Form controls · family `form-controls` · page `components/input/` · phone `components/input/m/`*

Single-line text, number, search and code inputs. 40px on desktop, 44px and 16px text on the phone so iOS never zooms.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Box | `.field` | 40px, 1px `--input`, 6px radius, 12px side padding, 8px gap. |
| 2 | Leading icon | `svg` | 16px muted (search, link). |
| 3 | Value | `input.grow` | Plex 13px; codes in Plex Mono. |
| 4 | Affix | `.fc-aff` | Unit or prefix ("rjmtm.com/"). |
| 5 | Trailing action | `.icon-btn.ghost.sm` | Clear, copy, reveal. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Text | `.field > input` | Names, titles. |
| Code / handle | `.field > input.code` | Handles, cloth codes, ids in Plex Mono. |
| Search | `.field + search icon` | Toolbar search; clears with ×. |
| With affix | `.field + .fc-aff` | Domain prefix, units. |
| Auto height | `.field.auto` | Phone fields that wrap (tags). |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | 1px `--input` border (3.1:1 on white), white ground, 40px tall (44px on the phone). | — |
| hover | Border darkens to `--input-hover`. | `:hover · .is-hover` |
| focus | Border turns `--ring` with the 4px soft `--focus-ring` halo. | `:focus-within · .is-focus` |
| active | Same as focus while typing. | — |
| disabled | `--muted` ground, muted text, 80% opacity, border stays `--input`. | `[disabled] · [aria-disabled=true]` |
| error | Border `--destructive`; `.help.err` below names the problem and the fix. | `.err + .help.err` |
| loading | `.skel` bar in place of the value. | `.skel` |
| empty | Placeholder in `--muted-foreground`, never a label substitute. | `::placeholder` |
| read-only | No border change on hover, text stays `--foreground`, selectable for copy. | `[readonly]` |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| height | 40px | `var(--control-h)` |
| width | 100% | `100%` |
| border | 1px solid #8892A5 | `1px solid hsl(var(--input))` |
| border-radius | 6px | `var(--r-control)` |
| gap | 8px | `var(--s-2)` |
| padding | 0 12px | `0 var(--s-3)` |
| background | #FFFFFF | `hsl(var(--background))` |
| height desktop / phone | 40 / 44px | `--control-h / --control-h-m` |
| phone text | 16px | `--text-input-m` |
| focus halo | 1px ring + 4px ring at 14% | `--focus-ring` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--focus-ring` | `0 0 0 1px hsl(var(--ring)), 0 0 0 4px hsl(var(--ring) / .14)` | 0 0 0 1px #3F60A6, 0 0 0 4px #3F60A6 @0.14 |
| `--control-h-m` | `44px` | 44px |
| `--control-h` | `40px` | 40px |
| `--input` | `219 14% 59%  (#8892A5)` | #8892A5 |
| `--r-control` | `6px` | 6px |
| `--s-2` | `8px` | 8px |
| `--s-3` | `12px` | 12px |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--motion-focus` | `140ms var(--ease-standard)` | 140ms cubic-bezier(.2,0,0,1) |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--input-hover` | `223 17% 45%  (#5F6A86)` | #5F6A86 |
| `--text-input-m` | `16px` | 16px |

### Motion

- Border on `--motion-hover`; halo on `--motion-focus` (140ms).

### Accessibility

- **Keyboard:** Tab moves in; typing edits; Escape does not clear.
- **ARIA:** `aria-invalid` + `aria-describedby` from Field.
- **ARIA:** Search inputs: `type="search"`, `aria-label` when there is no visible label.
- **Focus:** Halo ring (`--focus-ring`), never removed.

### Content rules

- Placeholders show an example in the real format ("navy-birdseye-two-piece"), never the label.

**Do**

- Use `inputMode` for numbers on the phone.

**Don't**

- Font under 16px on the phone.

### Build it

- **shadcn / Radix:** shadcn `Input`.
- **Exists in apps/admin today:** Input. `ui/input.tsx` exists (h-9). Restyle to 40/44px, `--input` edge and the halo focus.
- **Rendering:** server component. 

```ts
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  icon?: LucideIcon;                // leading
  affix?: string;                   // "rjmtm.com/"
  mono?: boolean;                   // handles, codes
  onClear?: () => void;             // shows the × action
  invalid?: boolean;
}
```

### Used on

[Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Design pricing](../../phase-3/pages/design-pricing/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Orders](../../phase-3/pages/orders/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Sign in](../../phase-3/pages/sign-in/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Inbox](../../phase-3/pages/support/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="textarea"></a>
## Textarea with counter

*Form controls · family `form-controls` · page `components/textarea/` · phone `components/textarea/m/`*

Multi-line text (product descriptions, notes, support replies) with a live character counter that turns amber near the limit and red over it.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Box | `.fc-ta` | Same edge, radius and focus as Input; min 96px, vertical resize. |
| 2 | Counter | `.fc-count` | 11.5px tabular, right-aligned under the box: "212 / 300". |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| With counter | `.fc-ta + .fc-count` | Any limited field. |
| Near limit | `.fc-count.near` | ≥ 90% of the limit, warning amber. |
| Over limit | `.fc-count.over` | Destructive; save is blocked with an error. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | 1px `--input` border (3.1:1 on white), white ground, 40px tall (44px on the phone). | — |
| hover | Border darkens to `--input-hover`. | `:hover · .is-hover` |
| focus | Border turns `--ring` with the 4px soft `--focus-ring` halo. | `:focus-within · .is-focus` |
| active | Same as focus while typing. | — |
| disabled | `--muted` ground, muted text, 80% opacity, border stays `--input`. | `[disabled] · [aria-disabled=true]` |
| error | Edge destructive; counter red; `.help.err` says how many to remove. | `.err · .fc-count.over` |
| loading | Value replaced by a `.skel` bar the width of a typical value. | `.skel` |
| empty | Placeholder in `--muted-foreground`, never a label substitute. | `::placeholder` |
| read-only | No border change on hover, text stays `--foreground`, selectable for copy. | `[readonly]` |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| width | 100% | `100%` |
| min-height | 84px | `84px` |
| padding | 9px 12px | `9px var(--s-3)` |
| border | 1px solid #8892A5 | `1px solid hsl(var(--input))` |
| border-radius | 6px | `var(--r-control)` |
| background | #FFFFFF | `hsl(var(--background))` |
| font | 13px/1.45 'IBM Plex Sans', system-ui, sans-serif | `var(--text-body)/var(--lh-body) var(--font-ui)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--s-3` | `12px` | 12px |
| `--input` | `219 14% 59%  (#8892A5)` | #8892A5 |
| `--r-control` | `6px` | 6px |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--text-body` | `13px` | 13px |
| `--lh-body` | `1.45` | 1.45 |
| `--font-ui` | `'IBM Plex Sans', system-ui, sans-serif` | 'IBM Plex Sans', system-ui, sans-serif |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--focus-ring` | `0 0 0 1px hsl(var(--ring)), 0 0 0 4px hsl(var(--ring) / .14)` | 0 0 0 1px #3F60A6, 0 0 0 4px #3F60A6 @0.14 |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--w-medium` | `500` | 500 |
| `--text-micro` | `11.5px` | 11.5px |
| `--warning` | `36 79% 29%  (#845610)` | #845610 |
| `--control-h` | `40px` | 40px |
| `--s-2` | `8px` | 8px |
| `--r-chip` | `4px` | 4px |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--accent-foreground` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--text-small` | `12px` | 12px |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |

### Accessibility

- **Keyboard:** Enter adds a line; Ctrl/⌘ + Enter submits in a composer.
- **ARIA:** Counter is `aria-live="polite"` and only announces at 90% and over.
- **Focus:** Halo ring.

### Content rules

- Counter format "212 / 300".

**Do**

- Show the limit before it is hit.

**Don't**

- Truncate silently.

### Build it

- **shadcn / Radix:** shadcn `Textarea` + a counter.
- **Exists in apps/admin today:** no · new: Textarea. Not in apps/admin.
- **Rendering:** client component (`"use client"`). 
- **Install:** `npx shadcn@latest add textarea`

```ts
interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
  maxLength?: number;               // shows the counter
  warnAt?: number;                  // default 0.9 * maxLength
  invalid?: boolean;
}
```

### Used on

[Oliver Hartley](../../phase-3/pages/customer-detail/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="select"></a>
## Select

*Form controls · family `form-controls` · page `components/select/` · phone `components/select/m/`*

Pick one value from a short list (under ~10). The trigger looks like a field; the list drops 4px on the menu surface. Statuses show their dot.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Trigger | `button.field[data-k-trigger]` | Field box with value and a chevrons-up-down icon. |
| 2 | Listbox | `.k-pop.menu` | Menu surface (`--e-pop`, 8px radius) under the trigger, same width. |
| 3 | Option | `.fc-opt / .mi` | 34px row; tick on the selected one; optional description. |
| 4 | Group label | `.fc-pop-g` | 11.5px Archivo muted. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Plain | `Select` | Currency, country, garment type. |
| Status | `.fc-opt.st-*` | Draft / Available / Retired with coloured label and dot. |
| Phone | `.k-sheet.k-bottom` | Opens a bottom sheet list with 44px rows. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | 1px `--input` border (3.1:1 on white), white ground, 40px tall (44px on the phone). | — |
| hover | Border darkens to `--input-hover`. | `:hover · .is-hover` |
| focus | Border turns `--ring` with the 4px soft `--focus-ring` halo. | `:focus-within · .is-focus` |
| active | Open: trigger takes the focus halo; list visible. | `.open > .field[data-k-trigger]` |
| disabled | `button.field[disabled]`: muted ground, not-allowed cursor. | `[disabled]` |
| error | Border `--destructive`; `.help.err` below names the problem and the fix. | `.err + .help.err` |
| loading | Value replaced by a `.skel` bar the width of a typical value. | `.skel` |
| empty | Trigger shows the placeholder in muted text ("Choose garment type"). | `.fc-ph` |
| read-only | No border change on hover, text stays `--foreground`, selectable for copy. | `[readonly]` |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| width | 100% | `100%` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--pop-shift` | `-4px` | -4px |
| `--motion-pop-out` | `110ms var(--ease-out)` | 110ms cubic-bezier(.4,0,1,1) |
| `--motion-handoff` | `.2s` | .2s |
| `--motion-pop-in` | `160ms var(--ease-in)` | 160ms cubic-bezier(.22,1,.36,1) |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--s-1` | `4px` | 4px |
| `--z-pop` | `20` | 20 |
| `--s-2` | `8px` | 8px |
| `--s-3` | `12px` | 12px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--text-body` | `13px` | 13px |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--w-semibold` | `600` | 600 |
| `--text-micro` | `11.5px` | 11.5px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--r-chip` | `4px` | 4px |
| `--popover-foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--accent-foreground` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--w-medium` | `500` | 500 |
| `--text-small` | `12px` | 12px |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--warning` | `36 79% 29%  (#845610)` | #845610 |
| `--s-4` | `16px` | 16px |
| `--draft` | `var(--info)` | #335694 |
| `--available` | `var(--success)` | #20644B |
| `--retired` | `var(--neutral)` | #575E70 |

### Motion

- List in: opacity + `translateY(-4px)` + scale .985 on `--motion-pop-in` (160ms); out on `--motion-pop-out` (110ms).

### Accessibility

- **Keyboard:** Enter / Space / ↓ opens; ↑ ↓ move; Enter picks; Escape closes and returns focus; type-ahead jumps.
- **ARIA:** Radix Select gives `role="combobox"` + `listbox` / `option` with `aria-selected`.
- **Focus:** Focus returns to the trigger after a pick.

### Content rules

- Placeholder is a verb phrase: "Choose garment type".

**Do**

- Use Combobox past ~10 options.

**Don't**

- A native `<select>` styled to look custom on desktop (fine on the phone sheet).

### Build it

- **shadcn / Radix:** shadcn `Select` (Radix Select). Phone: the same options in a `Drawer`.
- **Exists in apps/admin today:** no · new: Select. Not in apps/admin.
- **Rendering:** client component (`"use client"`). 
- **Install:** `npx shadcn@latest add select`

```ts
interface SelectOption<V extends string = string> { value: V; label: string; description?: string; status?: Status; disabled?: boolean }

interface SelectProps<V extends string = string> {
  options: SelectOption<V>[];
  value?: V;
  onValueChange: (value: V) => void;
  placeholder?: string;
  disabled?: boolean;
  invalid?: boolean;
  groups?: { label: string; values: V[] }[];
}
```

### Used on

[Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Design pricing](../../phase-3/pages/design-pricing/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [Audit log](../../phase-3/pages/settings-audit-log/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Rules](../../phase-3/pages/settings-rules/), [API Access](../../phase-3/pages/super-api-access/), [Cloth library](../../phase-3/pages/super-cloths/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [New SKU](../../phase-3/pages/variant-new/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="combobox"></a>
## Combobox

*Form controls · family `form-controls` · page `components/combobox/` · phone `components/combobox/m/`*

A searchable picker for long lists: brands, staff, products, cloth collections. Typing filters and highlights the match; results group under small headings.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Trigger | `button.field` | As Select. |
| 2 | Search row | `.fc-pop-s` | Search icon + borderless input on the popover's first row. |
| 3 | Groups | `.fc-pop-g` | Group headings. |
| 4 | Option | `.fc-opt` | Label, description, match highlighted with `mark`. |
| 5 | Empty | `.fc-pop-empty` | "No brand matches 'mtn'". |
| 6 | Footer (optional) | `.fc-pop-f` | Create new … |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Single | `Combobox` | Pick one. |
| Multi | `Combobox + chips` | Pick several; chips appear in the trigger. |
| Wide | `.fc-pop.wide` | 340px popover when options carry descriptions. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | 1px `--input` border (3.1:1 on white), white ground, 40px tall (44px on the phone). | — |
| hover | Border darkens to `--input-hover`. | `:hover · .is-hover` |
| focus | Border turns `--ring` with the 4px soft `--focus-ring` halo. | `:focus-within · .is-focus` |
| active | Same as focus while typing. | — |
| disabled | `--muted` ground, muted text, 80% opacity, border stays `--input`. | `[disabled] · [aria-disabled=true]` |
| error | Border `--destructive`; `.help.err` below names the problem and the fix. | `.err + .help.err` |
| loading | Spinner in the search row while results load. | `.spin` |
| empty | No matches: `.fc-pop-empty` names the query. | `.fc-pop-empty` |
| read-only | No border change on hover, text stays `--foreground`, selectable for copy. | `[readonly]` |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| max-height | 300px | `300px` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--s-1` | `4px` | 4px |
| `--z-pop` | `20` | 20 |
| `--s-2` | `8px` | 8px |
| `--s-3` | `12px` | 12px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--text-body` | `13px` | 13px |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--w-semibold` | `600` | 600 |
| `--text-micro` | `11.5px` | 11.5px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--r-chip` | `4px` | 4px |
| `--popover-foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--accent-foreground` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--w-medium` | `500` | 500 |
| `--text-small` | `12px` | 12px |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--warning` | `36 79% 29%  (#845610)` | #845610 |
| `--s-4` | `16px` | 16px |
| `--draft` | `var(--info)` | #335694 |
| `--available` | `var(--success)` | #20644B |
| `--retired` | `var(--neutral)` | #575E70 |

### Motion

- As Select (pop in 160ms, out 110ms).

### Accessibility

- **Keyboard:** Typing filters; ↑ ↓ move; Enter picks; Escape clears the query then closes.
- **ARIA:** cmdk gives `role="combobox"`, `aria-activedescendant` and a `listbox`.
- **Focus:** Focus stays in the search input while the list is open.

### Content rules

- Highlight the matched substring only.

**Do**

- Group options ("Brands", "HQ").

**Don't**

- Auto-pick the first result on blur.

### Build it

- **shadcn / Radix:** shadcn Combobox recipe: `Popover` + `Command` (cmdk).
- **Exists in apps/admin today:** no · new: Command, Popover. cmdk and @radix-ui/react-popover are installed (used by shell/brand-picker); no ui/ wrappers.
- **Rendering:** client component (`"use client"`). 
- **Install:** `npx shadcn@latest add command popover`

```ts
interface ComboboxProps<T> {
  items: T[];
  getKey: (item: T) => string;
  getLabel: (item: T) => string;
  getDescription?: (item: T) => string;
  groupBy?: (item: T) => string;
  value: T | T[] | null;
  onChange: (value: T | T[] | null) => void;
  multiple?: boolean;
  placeholder?: string;             // "Choose a brand"
  searchPlaceholder?: string;       // "Search brands"
  onSearch?: (query: string) => Promise<T[]>;   // server search
  emptyText?: (query: string) => string;
}
```

### Used on

[Global Store product editor](../../phase-3/pages/global-store-editor/)

---

<a id="cloth-picker"></a>
## Cloth picker

*Form controls · family `form-controls` · page `components/cloth-picker/` · phone `components/cloth-picker/m/`*

The large picker for choosing a cloth from the library: search, supplier and collection filters, an in-stock switch, a table of cloths and a detail side panel with the swatch.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Frame | `.fc-cp` | 8px radius card. |
| 2 | Head | `.fc-cp-head` | Title + count + close. |
| 3 | Tools | `.fc-cp-tools` | Search (280px), supplier and collection selects, in-stock switch. |
| 4 | Table | `.fc-cp-tbl` | 46px rows: swatch, code (mono), collection, supplier, stock. |
| 5 | Side panel | `.fc-cp-side` | 300px: swatch, details, Use this cloth. |
| 6 | Foot | `.fc-cp-foot` | Count and selection summary. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Single | `Cloth picker` | One cloth per product. |
| Phone | `full-screen sheet` | List rows with swatches; detail opens a bottom sheet. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | 1px `--input` border (3.1:1 on white), white ground, 40px tall (44px on the phone). | — |
| hover | Border darkens to `--input-hover`. | `:hover · .is-hover` |
| focus | Border turns `--ring` with the 4px soft `--focus-ring` halo. | `:focus-within · .is-focus` |
| active | Selected row: `--accent` ground, 2px `--primary` inset on the first cell, tick visible. | `tr[aria-selected=true]` |
| disabled | Out-of-stock rows: muted text, still pickable with a warning chip. | `tr.oos` |
| error | Border `--destructive`; `.help.err` below names the problem and the fix. | `.err + .help.err` |
| loading | Value replaced by a `.skel` bar the width of a typical value. | `.skel` |
| empty | `.fc-cp-empty`: "No cloths match" + Clear filters. | `.fc-cp-empty` |
| read-only | No border change on hover, text stays `--foreground`, selectable for copy. | `[readonly]` |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| border | 1px solid #E3E7ED | `1px solid hsl(var(--border))` |
| border-radius | 8px | `var(--r-card)` |
| background | #FFFFFF | `hsl(var(--background))` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--r-card` | `8px` | 8px |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--s-3` | `12px` | 12px |
| `--s-4` | `16px` | 16px |
| `--w-semibold` | `600` | 600 |
| `--text-card` | `13.5px` | 13.5px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--s-2` | `8px` | 8px |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--control-h-sm` | `34px` | 34px |
| `--text-body` | `13px` | 13px |
| `--row-th` | `36px` | 36px |
| `--text-micro` | `11.5px` | 11.5px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--e-rail` | `inset 2px 0 0 hsl(var(--primary))` | inset 2px 0 0 #0E172A |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--text-small` | `12px` | 12px |
| `--text-section` | `16px` | 16px |
| `--tr-section` | `-.005em` | -.005em |
| `--s-10` | `40px` | 40px |
| `--s-6` | `24px` | 24px |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |

### Accessibility

- **Keyboard:** ↑ ↓ move rows; Enter selects; Tab reaches the side panel action.
- **ARIA:** Grid with `aria-selected` rows, or a `listbox`.
- **Focus:** Focus returns to the trigger field after Use this cloth.

### Content rules

- Codes in Plex Mono (HS 81045, VBC 218.1003); collection names in italics in prose only.

**Do**

- Keep filters visible while scrolling.

**Don't**

- Hide out-of-stock cloths; mark them.

### Build it

- **shadcn / Radix:** `Dialog` (or a large `Sheet`) holding `Command` search + `Table`.
- **Exists in apps/admin today:** Dialog · new: Command, Table. Dialog exists; Command and Table are new.
- **Rendering:** client component (`"use client"`). 

```ts
interface Cloth { code: string; collection: string; supplier: "Holland & Sherry" | "Vitale Barberis Canonico" | string; inStock: boolean; swatchUrl?: string }

interface ClothPickerProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  value?: string;                   // cloth code
  onSelect: (cloth: Cloth) => void;
  fetchCloths: (q: { search?: string; supplier?: string; collection?: string; inStock?: boolean }) => Promise<Cloth[]>;
}
```

### Used on

[Global Store product editor](../../phase-3/pages/global-store-editor/)

---

<a id="checkbox"></a>
## Checkbox

*Form controls · family `form-controls` · page `components/checkbox/` · phone `components/checkbox/m/`*

One bare `<input type=checkbox class="cb">`: 18px, navy when checked, with an indeterminate dash for a partly selected table header.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Box | `input.cb` | 18px (20px on the phone), 1.5px `--input` edge, 4px radius. |
| 2 | Tick | `background-image` | Lucide check, 2.4 stroke, 14px. |
| 3 | Label | `label` | Plex 13px beside it; the whole row is the hit area. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Checked | `.cb:checked` | Navy fill + white tick. |
| Indeterminate | `.cb:indeterminate` | Navy fill + dash: some rows selected. |
| Card | `.fc-rcard` | Bordered choice card with title and description. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Unselected mark on a 1.5px `--input` edge. | — |
| hover | Edge darkens to `--input-hover`. | `:hover · .is-hover` |
| focus | `--focus-ring` around the mark (or 2px `--ring` outline). | `:focus-visible · .is-focus` |
| active | Selected: `--primary` fill. | `:checked · [aria-checked=true]` |
| disabled | `--muted` ground, `--border` edge; checked-disabled is muted-foreground at 55%. | `:disabled` |
| error | Edge `--destructive`; the group's `.help.err` explains. | `.err` |
| loading | Disable while saving; the row shows `.spin` after the label. | `.busy` |
| empty | Not applicable. | — |
| read-only | Shown as the value in text, or locked (`.lock`) with the reason on hover. | `.lock` |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| width | 18px | `18px` |
| height | 18px | `18px` |
| border | 1.5px solid #8892A5 | `1.5px solid hsl(var(--input))` |
| border-radius | 4px | `var(--r-chip)` |
| background | #FFFFFF no-repeat center / 14px 14px | `hsl(var(--background)) no-repeat center / 14px 14px` |
| size desktop / phone | 18 / 20px | — |
| edge | 1.5px | `--input` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--input` | `219 14% 59%  (#8892A5)` | #8892A5 |
| `--r-chip` | `4px` | 4px |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--motion-focus` | `140ms var(--ease-standard)` | 140ms cubic-bezier(.2,0,0,1) |
| `--input-hover` | `223 17% 45%  (#5F6A86)` | #5F6A86 |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--focus-ring` | `0 0 0 1px hsl(var(--ring)), 0 0 0 4px hsl(var(--ring) / .14)` | 0 0 0 1px #3F60A6, 0 0 0 4px #3F60A6 @0.14 |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |

### Motion

- Fill and edge on `--motion-hover`; halo on `--motion-focus`.

### Accessibility

- **Keyboard:** Space toggles.
- **ARIA:** Native input, or Radix `Checkbox` (`role="checkbox"`, `aria-checked="mixed"` for indeterminate).
- **Focus:** `--focus-ring` halo on the box.

### Content rules

- Label states the positive ("Show on the storefront").

**Do**

- Make the label clickable.

**Don't**

- Use a checkbox for an instant setting; that is a Switch.

### Build it

- **shadcn / Radix:** shadcn `Checkbox` (Radix).
- **Exists in apps/admin today:** Checkbox. `ui/checkbox.tsx` exists. Restyle to 18px, 1.5px edge, navy fill.
- **Rendering:** client component (`"use client"`). 

```ts
interface CheckboxProps {
  checked: boolean | "indeterminate";
  onCheckedChange: (checked: boolean) => void;
  label?: string;
  description?: string;             // card form
  disabled?: boolean;
  invalid?: boolean;
}
```

### Used on

[Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Design pricing](../../phase-3/pages/design-pricing/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [How customers buy](../../phase-3/pages/selling/), [Create a brand](../../phase-3/pages/settings-brand-new/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="radio"></a>
## Radio group

*Form controls · family `form-controls` · page `components/radio/` · phone `components/radio/m/`*

Choose one of two to five visible options. The same `.cb` input on a circle; radio cards for options that need a description.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Group | `.fc-group` | Column, 12px gap (`.h` = row). |
| 2 | Radio | `input[type=radio].cb` | 18px circle; checked = 4px navy ring inside. |
| 3 | Label | `label` | Option name. |
| 4 | Card (optional) | `.fc-rcard` | Bordered card; checked edge `--primary` + 1px inset. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Vertical | `.fc-group` | Default. |
| Horizontal | `.fc-group.h` | Two or three short options. |
| Cards | `.fc-rcard` | Options with a sentence each (pricing mode). |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Unselected mark on a 1.5px `--input` edge. | — |
| hover | Edge darkens to `--input-hover`. | `:hover · .is-hover` |
| focus | `--focus-ring` around the mark (or 2px `--ring` outline). | `:focus-visible · .is-focus` |
| active | Selected: `--primary` fill. | `:checked · [aria-checked=true]` |
| disabled | `--muted` ground, `--border` edge, not-allowed cursor. | `:disabled · [aria-disabled=true]` |
| error | Edge `--destructive`; the group's `.help.err` explains. | `.err` |
| loading | Disable while saving; the row shows `.spin` after the label. | `.busy` |
| empty | Not applicable. | — |
| read-only | Shown as the value in text, or locked (`.lock`) with the reason on hover. | `.lock` |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 12px | `var(--s-3)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--focus-ring` | `0 0 0 1px hsl(var(--ring)), 0 0 0 4px hsl(var(--ring) / .14)` | 0 0 0 1px #3F60A6, 0 0 0 4px #3F60A6 @0.14 |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--s-3` | `12px` | 12px |
| `--s-6` | `24px` | 24px |
| `--s-4` | `16px` | 16px |
| `--input` | `219 14% 59%  (#8892A5)` | #8892A5 |
| `--r-card` | `8px` | 8px |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--motion-focus` | `140ms var(--ease-standard)` | 140ms cubic-bezier(.2,0,0,1) |
| `--w-semibold` | `600` | 600 |
| `--text-card` | `13.5px` | 13.5px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--text-small` | `12px` | 12px |

### Accessibility

- **Keyboard:** Tab enters the group at the checked option; ↑ ↓ ← → move and select.
- **ARIA:** Radix `RadioGroup` (`role="radiogroup"` with an `aria-labelledby`).
- **Focus:** Ring on the focused radio.

### Content rules

- Options are parallel nouns or short phrases.

**Do**

- Preselect the safe default.

**Don't**

- More than five options: use Select.

### Build it

- **shadcn / Radix:** shadcn `RadioGroup`.
- **Exists in apps/admin today:** no · new: RadioGroup. Not in apps/admin.
- **Rendering:** client component (`"use client"`). 
- **Install:** `npx shadcn@latest add radio-group`

```ts
interface RadioGroupProps<V extends string> {
  options: { value: V; label: string; description?: string; disabled?: boolean }[];
  value: V;
  onValueChange: (value: V) => void;
  orientation?: "vertical" | "horizontal";
  variant?: "plain" | "card";
  "aria-label"?: string;
}
```

### Used on

[Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="switch"></a>
## Switch

*Form controls · family `form-controls` · page `components/switch/` · phone `components/switch/m/`*

An instant on/off setting that saves without a Save button (Combos offered, Show on the storefront). 36×20 on desktop, 44×26 on the phone. Kit v2 flips `aria-checked` for you unless a confirm opens.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Track | `.switch` | 36×20 pill, `--input` off, `--primary` on. |
| 2 | Thumb | `.switch::after` | 16px white circle, `--e-btn-2`, travels 16px. |
| 3 | Label | `label` | Left of the switch in rows; states the setting, not the action. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Default | `.switch[role=switch]` | Saves immediately. |
| Locked | `.switch.lock` | On and locked (navy at 45%); reason in a tooltip. |
| Busy | `.switch.busy` | Thumb dims while the save runs. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Unselected mark on a 1.5px `--input` edge. | — |
| hover | Edge darkens to `--input-hover`. | `:hover · .is-hover` |
| focus | `--focus-ring` around the mark (or 2px `--ring` outline). | `:focus-visible · .is-focus` |
| active | On: navy track, thumb at the right. | `[aria-checked=true]` |
| disabled | 45% opacity, not-allowed. | `[disabled] · [aria-disabled=true]` |
| error | Edge `--destructive`; the group's `.help.err` explains. | `.err` |
| loading | Thumb at 85% while saving; then a toast confirms. | `.busy` |
| empty | Not applicable. | — |
| read-only | Shown as the value in text, or locked (`.lock`) with the reason on hover. | `.lock` |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| width | 36px | `36px` |
| height | 20px | `20px` |
| border-radius | 999px | `var(--r-pill)` |
| background | #8892A5 | `hsl(var(--input))` |
| track desktop / phone | 36×20 / 44×26px | — |
| thumb desktop / phone | 16 / 22px | — |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--r-pill` | `999px` | 999px |
| `--input` | `219 14% 59%  (#8892A5)` | #8892A5 |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--motion-focus` | `140ms var(--ease-standard)` | 140ms cubic-bezier(.2,0,0,1) |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--e-btn-2` | `0 1px 1px rgba(14,23,41,.05)` | 0 1px 1px rgba(14,23,41,.05) |
| `--motion-pop-in` | `160ms var(--ease-in)` | 160ms cubic-bezier(.22,1,.36,1) |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--input-hover` | `223 17% 45%  (#5F6A86)` | #5F6A86 |
| `--primary-hover` | `223 42% 18%  (#1B2641)` | #1B2641 |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |

### Motion

- Thumb slides on `--motion-pop-in` (160ms, `--ease-in`); track colour on `--motion-hover`.

### Accessibility

- **Keyboard:** Space toggles.
- **ARIA:** `role="switch"` + `aria-checked`; label via `aria-labelledby`.
- **Focus:** 2px `--ring` outline, 2px offset.

### Content rules

- Label = the setting ("Combos offered"), never "Turn on combos".

**Do**

- Confirm before switching off something customers see.

**Don't**

- Use a switch inside a form that has a Save button: use a Checkbox.

### Build it

- **shadcn / Radix:** shadcn `Switch` (Radix).
- **Exists in apps/admin today:** Switch. `ui/switch.tsx` exists. Restyle to 36×20 / 44×26 and the navy track.
- **Rendering:** client component (`"use client"`). 

```ts
interface SwitchProps {
  checked: boolean;
  onCheckedChange: (checked: boolean) => void | Promise<void>;   // async = shows busy
  label: string;
  locked?: { reason: string };
  disabled?: boolean;
  confirm?: { title: string; body: string; when: "on" | "off" };  // opens a confirm first
}
```

### Used on

[Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Design pricing](../../phase-3/pages/design-pricing/), [Founder](../../phase-3/pages/founder/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Rules](../../phase-3/pages/settings-rules/), [API Access](../../phase-3/pages/super-api-access/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="segmented"></a>
## Segmented control

*Form controls · family `form-controls` · page `components/segmented/` · phone `components/segmented/m/`*

Two to five mutually exclusive views or modes in one pill row: Gallery / Table, date ranges on a chart, density. A control, not navigation (that is Tabs).

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Track | `.seg` | 3px padding, 2px gap, `--muted` ground, 8px radius, 1px `--border`. |
| 2 | Segment | `.seg>button` | 32px (26px `.sm`, 38px phone), Archivo 500 13px. |
| 3 | Selected | `[aria-checked=true]` | White, `--e-btn-2` + 1px border ring. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Default | `.seg` | Inline. |
| Small | `.seg.sm` | Toolbars and chart headers. |
| Full width | `.seg.full` | Phone and sheets. |
| Icon | `.seg.ic` | Icon-only (gallery/table) with labels for AT. |
| Read-only | `.seg.ro` | Shows the mode without letting it change. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Unselected mark on a 1.5px `--input` edge. | — |
| hover | Edge darkens to `--input-hover`. | `:hover · .is-hover` |
| focus | `--focus-ring` around the mark (or 2px `--ring` outline). | `:focus-visible · .is-focus` |
| active | Selected segment white with the 1px ring. | `[aria-checked=true] · [aria-pressed=true] · .on` |
| disabled | Whole control at 50%. | `[aria-disabled=true]` |
| error | Track edge destructive. | `.seg.err` |
| loading | Disable while saving; the row shows `.spin` after the label. | `.busy` |
| empty | Not applicable. | — |
| read-only | Muted ground, default cursor. | `.seg.ro` |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| padding | 3px | `3px` |
| gap | 2px | `2px` |
| background | #F3F4F7 | `hsl(var(--muted))` |
| border-radius | 8px | `var(--r-card)` |
| border | 1px solid #E3E7ED | `1px solid hsl(var(--border))` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--r-card` | `8px` | 8px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--s-3` | `12px` | 12px |
| `--r-control` | `6px` | 6px |
| `--text-body` | `13px` | 13px |
| `--w-medium` | `500` | 500 |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--e-btn-2` | `0 1px 1px rgba(14,23,41,.05)` | 0 1px 1px rgba(14,23,41,.05) |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--text-small` | `12px` | 12px |
| `--s-2` | `8px` | 8px |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |

### Motion

- Background, colour and shadow on `--motion-hover`.

### Accessibility

- **Keyboard:** Tab focuses the selected segment; ← → move and select.
- **ARIA:** `ToggleGroup type="single"` (`role="radiogroup"` / `radio`) with an `aria-label`.
- **Focus:** 2px `--ring` outline on the segment.

### Content rules

- One or two words per segment.

**Do**

- Keep the selection in the URL when it changes the view.

**Don't**

- Use for page navigation: use Tabs.

### Build it

- **shadcn / Radix:** shadcn `ToggleGroup` (Radix) `type="single"`.
- **Exists in apps/admin today:** no · new: ToggleGroup. Not in apps/admin.
- **Rendering:** client component (`"use client"`). 
- **Install:** `npx shadcn@latest add toggle-group`

```ts
interface SegmentedProps<V extends string> {
  options: { value: V; label: string; icon?: LucideIcon }[];
  value: V;
  onValueChange: (value: V) => void;
  size?: "sm" | "md";
  fullWidth?: boolean;
  readOnly?: boolean;
  "aria-label": string;
}
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="date"></a>
## Date and date range

*Form controls · family `form-controls` · page `components/date/` · phone `components/date/m/`*

A field that opens a calendar. Ranges add presets on the left (Today, Last 7 days, This month) and two months side by side.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Trigger | `.field + calendar icon` | Shows "2 Oct 2026" or "2 Oct – 11 Oct". |
| 2 | Popover | `.fc-pop.cal` | Menu surface. |
| 3 | Presets | `.fc-presets` | 150px column of quick ranges. |
| 4 | Month grid | `.fc-cal-g` | 7 columns, 34px day buttons, tabular. |
| 5 | Footer | `.fc-cal-f` | Clear + Apply for ranges. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Single date | `.fc-cal` | 288px popover. |
| Range | `.fc-cal.two` | Presets + two months. |
| Split range field | `.fc-range` | From / To fields joined. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | 1px `--input` border (3.1:1 on white), white ground, 40px tall (44px on the phone). | — |
| hover | Border darkens to `--input-hover`. | `:hover · .is-hover` |
| focus | Border turns `--ring` with the 4px soft `--focus-ring` halo. | `:focus-within · .is-focus` |
| active | Selected day navy; range ends rounded, days between `--accent`. | `.sel · .a · .b · .in` |
| disabled | Unavailable days struck through at 50%. | `button[disabled]` |
| error | Border `--destructive`; `.help.err` below names the problem and the fix. | `.err + .help.err` |
| loading | Value replaced by a `.skel` bar the width of a typical value. | `.skel` |
| empty | Placeholder "Any date". | — |
| read-only | No border change on hover, text stays `--foreground`, selectable for copy. | `[readonly]` |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| width | 288px | `288px` |
| padding | 12px | `var(--s-3)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--s-3` | `12px` | 12px |
| `--s-4` | `16px` | 16px |
| `--s-2` | `8px` | 8px |
| `--w-semibold` | `600` | 600 |
| `--text-card` | `13.5px` | 13.5px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--w-medium` | `500` | 500 |
| `--text-micro` | `11.5px` | 11.5px |
| `--font-ui` | `'IBM Plex Sans', system-ui, sans-serif` | 'IBM Plex Sans', system-ui, sans-serif |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--text-body` | `13px` | 13px |
| `--r-control` | `6px` | 6px |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--secondary-border` | `219 14% 79%  (#C2C7D1)` | #C2C7D1 |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--primary-foreground` | `60 17% 98%  (#FBFBF9)` | #FBFBF9 |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |

### Accessibility

- **Keyboard:** Arrow keys move days; PageUp/PageDown months; Enter picks; Escape closes.
- **ARIA:** react-day-picker grid semantics; today marked with `aria-current="date"`.
- **Focus:** Focus moves into the grid on open, back to the trigger on close.

### Content rules

- Format: "2 Oct 2026"; relative words in lists ("Yesterday").

**Do**

- Offer presets for reports.

**Don't**

- US date order.

### Build it

- **shadcn / Radix:** shadcn `Calendar` (react-day-picker) in a `Popover`.
- **Exists in apps/admin today:** no · new: Calendar, Popover. Not in apps/admin.
- **Rendering:** client component (`"use client"`). 
- **Install:** `npx shadcn@latest add calendar popover`

```ts
interface DatePickerProps {
  mode: "single" | "range";
  value: Date | { from: Date; to?: Date } | undefined;
  onChange: (value: DatePickerProps["value"]) => void;
  presets?: { label: string; range: { from: Date; to: Date } }[];
  disabled?: (day: Date) => boolean;
  placeholder?: string;             // "Any date"
}
```

### Used on

[Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/)

---

<a id="money-input"></a>
## Money input

*Form controls · family `form-controls` · page `components/money-input/` · phone `components/money-input/m/`*

An amount with its currency: a currency menu on the left, the symbol, then a right-aligned tabular amount. GBP is the base; EUR and USD are shown beside it. An unverified rate shows amber.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Box | `.field.fc-money` | Field box with no padding. |
| 2 | Currency | `.fc-cur` | Button with code (GBP) and chevron; opens the currency menu. |
| 3 | Symbol | `.fc-sym` | £ € $ muted. |
| 4 | Amount | `input` | Right-aligned, tabular, Plex 13px; `£1,150.00` on edit. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Editable | `.fc-money` | Price fields. |
| Read-only currency | `.fc-money.ro` | Currency fixed by the brand. |
| Unverified | `.fc-money.unv` | Converted at a rate nobody confirmed: amber edge and code; saving opens Confirm unverified currency. |
| Locked | `.fc-money > .fc-lock` | Price set by HQ. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | 1px `--input` border (3.1:1 on white), white ground, 40px tall (44px on the phone). | — |
| hover | Border darkens to `--input-hover`. | `:hover · .is-hover` |
| focus | Border turns `--ring` with the 4px soft `--focus-ring` halo. | `:focus-within · .is-focus` |
| active | Same as focus while typing. | — |
| disabled | `--muted` ground, muted text, 80% opacity, border stays `--input`. | `[disabled] · [aria-disabled=true]` |
| error | Destructive edge; "Enter a price above £0". | `.err` |
| loading | Value replaced by a `.skel` bar the width of a typical value. | `.skel` |
| empty | Placeholder in `--muted-foreground`, never a label substitute. | `::placeholder` |
| read-only | Currency button inert. | `.fc-money.ro` |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| padding | 0 | `0` |
| amount format | £1,150.00 on edit · £1,150 in lists | `font-variant-numeric: tabular-nums` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--s-3` | `12px` | 12px |
| `--input` | `219 14% 59%  (#8892A5)` | #8892A5 |
| `--w-semibold` | `600` | 600 |
| `--text-small` | `12px` | 12px |
| `--font-code` | `'IBM Plex Mono', ui-monospace, monospace` | 'IBM Plex Mono', ui-monospace, monospace |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--r-control` | `6px` | 6px |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--text-body` | `13px` | 13px |
| `--r-chip` | `4px` | 4px |
| `--text-micro` | `11.5px` | 11.5px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--warning` | `36 79% 29%  (#845610)` | #845610 |
| `--status-soft-alpha` | `.12` | .12 |

### Accessibility

- **Keyboard:** Tab: currency button, then amount.
- **ARIA:** Amount `inputMode="decimal"`, `aria-label` includes the currency ("Price in GBP").
- **Focus:** Halo on the whole field (`:focus-within`).

### Content rules

- Two decimals while editing, none in lists when whole.

**Do**

- Store minor units (pence).

**Don't**

- Mix currencies in one column without the code.

### Build it

- **shadcn / Radix:** Composed: `Input` + `DropdownMenu` (currency).
- **Exists in apps/admin today:** no. No money input in apps/admin.
- **Rendering:** client component (`"use client"`). 

```ts
type Currency = "GBP" | "EUR" | "USD";

interface MoneyInputProps {
  amountMinor: number | null;       // pence / cents
  currency: Currency;
  onChange: (v: { amountMinor: number | null; currency: Currency }) => void;
  currencies?: Currency[];          // omit = currency is read-only
  unverified?: boolean;             // amber; saving routes through ConfirmUnverifiedCurrency
  locked?: { reason: string };
  invalid?: boolean;
}
```

### Used on

[Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/)

---

<a id="price-group"></a>
## Multi-currency price group

*Form controls · family `form-controls` · page `components/price-group/` · phone `components/price-group/m/`*

The price lines for one product: GBP base plus EUR and USD, each with a source (manual or converted), an override and a remove action.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Header | `.fc-pg-h` | Columns 150px · 1fr · 92px · 36px (currency, amount, source, remove). |
| 2 | Line | `.fc-prow` | One Money input per currency. |
| 3 | Add line | `.btn.btn-tertiary.sm` | Add a currency. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Base + converted | `.fc-pg` | GBP manual, others converted. |
| All manual | `.fc-pg` | Each currency typed. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Holds its children at the documented spacing. | — |
| hover | Not applicable. | — |
| focus | Not applicable; children carry focus. | — |
| active | Not applicable. | — |
| disabled | Not applicable. | — |
| error | Line with a missing amount shows the field error; group summary at the top. | `.err` |
| loading | Children show skeletons in place. | `.skel` |
| empty | Shows the compact empty state inside. | `.sf-empty.compact` |
| read-only | Children render as values, not inputs. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| grid-template-columns | 150px minmax(0,1fr) 92px 36px | `150px minmax(0,1fr) 92px 36px` |
| gap | 8px | `var(--s-2)` |
| font | 600 11.5px/1 'Archivo', system-ui, sans-serif | `var(--w-semibold) var(--text-micro)/1 var(--font-voice)` |
| color | #505B71 | `hsl(var(--muted-foreground))` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--s-2` | `8px` | 8px |
| `--w-semibold` | `600` | 600 |
| `--text-micro` | `11.5px` | 11.5px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |

### Accessibility

- **Keyboard:** Tab row by row.
- **ARIA:** A `fieldset` with `legend` "Prices"; each row labelled by its currency.
- **Focus:** After Add line, focus the new amount.

### Content rules

- Source column: "Manual" or "From GBP at 1.15".

**Do**

- Keep GBP first.

**Don't**

- Allow two lines in one currency.

### Build it

- **shadcn / Radix:** Composed from Money input rows (react-hook-form `useFieldArray`).
- **Exists in apps/admin today:** no. Not in apps/admin.
- **Rendering:** client component (`"use client"`). 

```ts
interface PriceLine { currency: Currency; amountMinor: number | null; source: "manual" | { from: Currency; rate: number; verified: boolean } }

interface PriceGroupProps {
  lines: PriceLine[];
  onChange: (lines: PriceLine[]) => void;
  baseCurrency?: Currency;          // "GBP"
  allowed?: Currency[];
}
```

### Used on

[Global Store product editor](../../phase-3/pages/global-store-editor/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/)

---

<a id="tag-input"></a>
## Tag input

*Form controls · family `form-controls` · page `components/tag-input/` · phone `components/tag-input/m/`*

Free tags and keywords (search terms, collections): chips inside a growing field, Enter or comma to add, × to remove.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Box | `.field.fc-tags` | Auto height, min 40px, wraps, 5px 8px padding, 6px gap. |
| 2 | Tag | `.fc-tag` | Chip with a 22px remove button. |
| 3 | Input | `input` | Flexible, min 120px. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Editable | `.fc-tags` | Default. |
| Read-only | `.fc-tags.ro` | No remove buttons. |
| With suggestions | `.fc-tags + .fc-pop` | Combobox suggestions under the field. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | 1px `--input` border (3.1:1 on white), white ground, 40px tall (44px on the phone). | — |
| hover | Border darkens to `--input-hover`. | `:hover · .is-hover` |
| focus | Border turns `--ring` with the 4px soft `--focus-ring` halo. | `:focus-within · .is-focus` |
| active | Same as focus while typing. | — |
| disabled | `--muted` ground, muted text, 80% opacity, border stays `--input`. | `[disabled] · [aria-disabled=true]` |
| error | Duplicate or too long: tag flashes, `.help.err` explains. | `.err` |
| loading | Value replaced by a `.skel` bar the width of a typical value. | `.skel` |
| empty | Placeholder "Add a keyword". | — |
| read-only | No border change on hover, text stays `--foreground`, selectable for copy. | `[readonly]` |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| height | auto | `auto` |
| min-height | 40px | `var(--control-h)` |
| padding | 5px 8px | `5px var(--s-2)` |
| gap | 6px | `6px` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--control-h` | `40px` | 40px |
| `--s-2` | `8px` | 8px |
| `--r-chip` | `4px` | 4px |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--accent-foreground` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--w-medium` | `500` | 500 |
| `--text-small` | `12px` | 12px |
| `--font-ui` | `'IBM Plex Sans', system-ui, sans-serif` | 'IBM Plex Sans', system-ui, sans-serif |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |

### Accessibility

- **Keyboard:** Enter / comma adds; Backspace on an empty input selects then removes the last tag.
- **ARIA:** Each remove button `aria-label="Remove tag navy"`; list is `role="list"`.
- **Focus:** Focus stays in the input after add/remove.

### Content rules

- Tags lower case unless proper nouns.

**Do**

- Trim and de-duplicate.

**Don't**

- Use for statuses.

### Build it

- **shadcn / Radix:** Composed: `Badge` + `Input` (+ `Command` for suggestions).
- **Exists in apps/admin today:** no. Not in apps/admin.
- **Rendering:** client component (`"use client"`). 

```ts
interface TagInputProps {
  value: string[];
  onChange: (tags: string[]) => void;
  suggestions?: string[];
  maxTags?: number;
  readOnly?: boolean;
  placeholder?: string;
}
```

### Used on

No approved surface uses this yet.

---

<a id="uploader"></a>
## File and photo uploader

*Form controls · family `form-controls` · page `components/uploader/` · phone `components/uploader/m/`*

A drop zone plus a numbered photo strip you can reorder by drag (or keyboard). Each slot shows whether its alt text is written. Files show progress, done and failed rows.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Drop zone | `.fc-drop` | Dashed `--secondary-border` edge, icon tile, title, hint, Browse button. |
| 2 | File row | `.fc-file` | Icon, name, size, progress meter (160px), remove. |
| 3 | Photo strip | `.fc-slots` | 9 columns on desktop; numbered slots, slot 1 is the main photo. |
| 4 | Slot | `.fc-slot` | 3:4 media, number badge, grip, alt status (`.alt.ok` / `.no`). |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Files | `.fc-drop + .fc-file` | CSV price imports, PDFs. |
| Photos | `.fc-slots` | Product gallery with order. |
| Phone | `list + Move up / Move down` | No drag on touch; explicit move buttons. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | 1px `--input` border (3.1:1 on white), white ground, 40px tall (44px on the phone). | — |
| hover | Drop zone `--ring` edge on `--accent` 60%. | `.fc-drop:hover · .over` |
| focus | Border turns `--ring` with the 4px soft `--focus-ring` halo. | `:focus-within · .is-focus` |
| active | Dragging slot at 40%; target slot ring inset. | `.dragging · .target` |
| disabled | `--muted` ground, muted text, 80% opacity, border stays `--input`. | `[disabled] · [aria-disabled=true]` |
| error | Row edge destructive at 50%, reason in small text ("Over 10 MB"). | `.fc-file.err · .fc-drop.err` |
| loading | Meter fills; label "Uploading 2 of 5". | `.fc-meter` |
| empty | Strip shows empty dashed slots. | `.fc-empty` |
| read-only | No border change on hover, text stays `--foreground`, selectable for copy. | `[readonly]` |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 12px | `var(--s-3)` |
| padding | 10px 12px | `10px var(--s-3)` |
| border | 1px solid #E3E7ED | `1px solid hsl(var(--border))` |
| border-radius | 8px | `var(--r-card)` |
| background | #FFFFFF | `hsl(var(--background))` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--r-pill` | `999px` | 999px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--warning` | `36 79% 29%  (#845610)` | #845610 |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--s-4` | `16px` | 16px |
| `--s-5` | `20px` | 20px |
| `--secondary-border` | `219 14% 79%  (#C2C7D1)` | #C2C7D1 |
| `--r-card` | `8px` | 8px |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--r-control` | `6px` | 6px |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--w-semibold` | `600` | 600 |
| `--text-card` | `13.5px` | 13.5px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--text-small` | `12px` | 12px |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--s-3` | `12px` | 12px |
| `--w-medium` | `500` | 500 |
| `--text-body` | `13px` | 13px |
| `--font-ui` | `'IBM Plex Sans', system-ui, sans-serif` | 'IBM Plex Sans', system-ui, sans-serif |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--success` | `158 51% 26%  (#20644B)` | #20644B |
| `--studio` | `radial-gradient(120% 90% at 50% 18%, #f7f8fa 0%, #e3e7ee 80%)` | radial-gradient(120% 90% at 50% 18%, #f7f8fa 0%, #e3e7ee 80%) |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--motion-press` | `80ms var(--ease-standard)` | 80ms cubic-bezier(.2,0,0,1) |
| `--r-chip` | `4px` | 4px |
| `--text-micro` | `11.5px` | 11.5px |
| `--primary-foreground` | `60 17% 98%  (#FBFBF9)` | #FBFBF9 |

### Accessibility

- **Keyboard:** Browse is a real button. Slot: Space picks up, ← → moves, Space drops, Escape cancels (dnd-kit keyboard sensor).
- **ARIA:** Live region announces moves ("Photo 3 moved to position 1").
- **Focus:** Focus follows the moved slot.

### Content rules

- Hint lists formats and size: "JPG or PNG, up to 10 MB, 3:4".

**Do**

- Mark slot 1 as the main photo.

**Don't**

- Upload without alt text status.

### Build it

- **shadcn / Radix:** Composed: `Button` + `Progress`; ordering with `@dnd-kit/sortable` (installed).
- **Exists in apps/admin today:** no. @dnd-kit is installed (website menu sections); no uploader component.
- **Rendering:** client component (`"use client"`). 

```ts
interface UploadItem { id: string; name: string; size: number; progress?: number; error?: string; url?: string; alt?: string }

interface PhotoUploaderProps {
  items: UploadItem[];
  onAdd: (files: File[]) => void;
  onReorder: (ids: string[]) => void;
  onRemove: (id: string) => void;
  onAltChange?: (id: string, alt: string) => void;
  accept?: string;                  // "image/jpeg,image/png"
  maxBytes?: number;                // 10 * 1024 * 1024
  maxItems?: number;                // 9
}
```

### Used on

[Global Store product editor](../../phase-3/pages/global-store-editor/), [Bulk price import](../../phase-3/pages/pricing-bulk/)

---

<a id="form-section"></a>
## Form section

*Form controls · family `form-controls` · page `components/form-section/` · phone `components/form-section/m/`*

How a long form is laid out: a 240px left column with the section title and one line of context, the fields in a two-column grid on the right. One column on the phone.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Section | `.fc-form` | Grid 240px + 1fr, 32px gap, 24px vertical padding, top hairline. |
| 2 | Head | `.fc-form-h` | h3 Archivo 16px + 12px muted explanation (30ch). |
| 3 | Body | `.fc-form-b` | Two columns, 20px gaps; `.full` spans both. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Two-column body | `.fc-form-b` | Desktop default. |
| Full-width field | `.full` | Textareas, uploaders, price groups. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Holds its children at the documented spacing. | — |
| hover | Not applicable. | — |
| focus | Not applicable; children carry focus. | — |
| active | Not applicable. | — |
| disabled | Not applicable. | — |
| error | Children report errors; the container may add a summary at the top. | — |
| loading | Children show skeletons in place. | `.skel` |
| empty | Shows the compact empty state inside. | `.sf-empty.compact` |
| read-only | Children render as values, not inputs. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| grid-template-columns | 240px minmax(0,1fr) | `240px minmax(0,1fr)` |
| gap | 32px | `var(--s-8)` |
| padding | 24px 0 | `var(--s-6) 0` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--s-8` | `32px` | 32px |
| `--s-6` | `24px` | 24px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--w-semibold` | `600` | 600 |
| `--text-section` | `16px` | 16px |
| `--lh-section` | `1.25` | 1.25 |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--tr-section` | `-.005em` | -.005em |
| `--text-small` | `12px` | 12px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--s-3` | `12px` | 12px |
| `--s-5` | `20px` | 20px |

### Accessibility

- **Keyboard:** Reading order: head, then fields left to right.
- **ARIA:** Each section a `<fieldset>` or `<section aria-labelledby>`.
- **Focus:** On submit with errors, focus the first invalid field.

### Content rules

- Section titles are nouns ("Pricing", "Photos").

**Do**

- Pair with the Save bar on long pages.

**Don't**

- Nest sections.

### Build it

- **shadcn / Radix:** Layout only; `Separator` for the hairline.
- **Exists in apps/admin today:** Separator. `ui/separator.tsx` exists.
- **Rendering:** server component. 

```ts
interface FormSectionProps { title: string; description?: string; children: React.ReactNode }
```

### Used on

[Brand](../../phase-3/pages/settings-brand/), [Profile](../../phase-3/pages/settings-profile/), [New SKU](../../phase-3/pages/variant-new/)

---

<a id="save-bar"></a>
## Sticky save bar

*Form controls · family `page-primitives` · page `components/save-bar/` · phone `components/save-bar/m/`*

Keeps Save in view on a whole-page form: saved / unsaved changes / error on the left, Discard and Save on the right. A card on desktop, a glass bar above the tab bar on the phone. Kit v2 marks it dirty for you.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Bar | `.k-savebar` | Sticky bottom, 8px radius card, `--e-pop`, 12px 16px padding. |
| 2 | State | `.k-saved / .k-dirty / .k-err` | Exactly one shows. |
| 3 | Actions | `.r` | Discard (ghost) + Save changes (primary). |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Desktop card | `.k-savebar` | Floats at the bottom of the content column. |
| Phone glass | `.kit-m .k-savebar` | Full width, blur `--actbar-blur`, above the tab bar, buttons 48px. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | "All changes saved" muted. | `.k-saved` |
| hover | Not applicable. | — |
| focus | Not applicable; children carry focus. | — |
| active | Dirty: "Unsaved changes" in foreground. | `.is-dirty` |
| disabled | Not applicable. | — |
| error | "Could not save: …" destructive. | `.is-error` |
| loading | Save shows `.spin` + "Saving…". | `[aria-busy=true]` |
| empty | Shows the compact empty state inside. | `.sf-empty.compact` |
| read-only | Children render as values, not inputs. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 12px | `var(--s-3)` |
| padding | 12px 16px | `var(--s-3) var(--s-4)` |
| background | #FFFFFF | `hsl(var(--card))` |
| border | 1px solid #E3E7ED | `1px solid hsl(var(--border))` |
| border-radius | 8px | `var(--r-card)` |
| box-shadow | 0 12px 32px -10px rgba(14,23,41,.25) | `var(--e-pop)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--z-sticky` | `3` | 3 |
| `--s-3` | `12px` | 12px |
| `--s-6` | `24px` | 24px |
| `--s-4` | `16px` | 16px |
| `--card` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--r-card` | `8px` | 8px |
| `--e-pop` | `0 12px 32px -10px rgba(14,23,41,.25)` | 0 12px 32px -10px rgba(14,23,41,.25) |
| `--s-2` | `8px` | 8px |
| `--text-small` | `12px` | 12px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--w-medium` | `500` | 500 |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--tabbar-h` | `62px` | 62px |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--actbar-blur` | `8px` | 8px |
| `--control-h-m-lg` | `48px` | 48px |
| `--text-btn-lg` | `15px` | 15px |
| `--warning` | `36 79% 29%  (#845610)` | #845610 |

### Accessibility

- **Keyboard:** ⌘/Ctrl + S saves (optional).
- **ARIA:** State text in `aria-live="polite"`.
- **Focus:** Leaving with unsaved changes opens the Unsaved-changes guard.

### Content rules

- "All changes saved" · "Unsaved changes" · "Could not save: price list is locked".

**Do**

- Wire `[data-k-form]` / form dirty state.

**Don't**

- Show Save when nothing changed (disable it).

### Build it

- **shadcn / Radix:** Composed: `Button` ×2 + form dirty state (react-hook-form `formState.isDirty`).
- **Exists in apps/admin today:** no. Not in apps/admin.
- **Rendering:** client component (`"use client"`). 

```ts
interface SaveBarProps {
  state: "saved" | "dirty" | "saving" | "error";
  error?: string;
  onSave: () => void;
  onDiscard: () => void;
  saveLabel?: string;               // "Save changes"
}
```

### Used on

[Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [Brand](../../phase-3/pages/settings-brand/), [New SKU](../../phase-3/pages/variant-new/)

---

<a id="sheet"></a>
## Side sheet and bottom sheet

*Overlays · family `overlays` · page `components/sheet/` · phone `components/sheet/m/`*

Quick edit without leaving the list. A side sheet slides in from the right on desktop (404 / 532 / 856px); on the phone it becomes a bottom sheet or a full-screen sheet. Header, scrolling body, sticky footer with the one primary.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Scrim | `.k-scrim` | `--scrim`, click closes (unless dirty). |
| 2 | Sheet | `.k-sheet` | Popover white, `--e-sheet`, full height, flex column. |
| 3 | Header | `.ov-sh-h` | Thumb + title + code + close; 16px 20px 12px padding. |
| 4 | Body | `.ov-sh-b / .k-sheet-b` | Scrolls on its own (`overscroll-behavior: contain`), 20px gaps. |
| 5 | Footer | `.ov-sh-f` | Sticky: summary left, Cancel + primary right. |
| 6 | Grab handle (phone) | `.k-grab` | 36×4 pill on bottom sheets. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Small 404 | `.k-sheet` | Quick edit (price, status, cloth). |
| Medium 532 | `.k-sheet.md` | Detail views (order lines, customer). |
| Large 856 | `.k-sheet.lg` | Long forms: create API key, bulk edit. |
| Bottom (phone) | `.k-sheet.k-bottom` | Up to 88% tall, 15px top radius. |
| Full screen (phone) | `.k-sheet.k-bottom.k-full` | Pickers and long forms. |
| Below the top bar | `.k-stage.below-bar` | The help drawer, so "?" stays clickable. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Closed: nothing rendered in the flow. | — |
| hover | Not applicable to the surface; its buttons and rows hover as usual. | — |
| focus | Open: focus moves inside (`[data-k-autofocus]` first), Tab is trapped, Escape closes and returns focus. | `[data-k-stage].open` |
| active | Open: slid in, scrim visible. | `[data-k-stage].open` |
| disabled | The opener is disabled, never the overlay. | — |
| error | Inline danger alert at the top of the body; sheet stays open with the values kept. | `.sf-alert.danger` |
| loading | Body shows skeleton fields; footer primary disabled. | `.skel` |
| empty | Body shows a compact empty state. | `.sf-empty.compact` |
| read-only | Footer drops the primary and keeps Close. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| width | min(404px,92%) | `min(var(--sheet-w),92%)` |
| background | #FFFFFF | `hsl(var(--popover))` |
| box-shadow | -24px 0 60px -20px rgba(14,23,41,.35) | `var(--e-sheet)` |
| width sm / md / lg | 404 / 532 / 856px (max 92% / 94%) | `--sheet-w / --sheet-w-md / --sheet-w-lg` |
| z-index scrim / sheet | 30 / 31 | `--z-scrim / --z-sheet` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--scrim` | `rgba(10,14,24,.38)` | rgba(10,14,24,.38) |
| `--motion-sheet-out` | `180ms var(--ease-out)` | 180ms cubic-bezier(.4,0,1,1) |
| `--z-scrim` | `30` | 30 |
| `--sheet-w` | `404px` | 404px |
| `--popover` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--e-sheet` | `-24px 0 60px -20px rgba(14,23,41,.35)` | -24px 0 60px -20px rgba(14,23,41,.35) |
| `--z-sheet` | `31` | 31 |
| `--motion-handoff-sheet` | `.3s` | .3s |
| `--motion-sheet-in` | `260ms var(--ease-in)` | 260ms cubic-bezier(.22,1,.36,1) |
| `--r-frame` | `15px` | 15px |
| `--e-phone-sheet` | `0 -20px 50px -20px rgba(14,23,41,.50)` | 0 -20px 50px -20px rgba(14,23,41,.50) |
| `--r-pill` | `999px` | 999px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--input` | `219 14% 59%  (#8892A5)` | #8892A5 |
| `--s-2` | `8px` | 8px |
| `--topbar-h` | `56px` | 56px |
| `--sheet-w-md` | `532px` | 532px |
| `--sheet-w-lg` | `856px` | 856px |
| `--s-3` | `12px` | 12px |
| `--s-4` | `16px` | 16px |
| `--s-5` | `20px` | 20px |
| `--s-8` | `32px` | 32px |
| `--s-1` | `4px` | 4px |
| `--s-12` | `48px` | 48px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |

### Motion

- Side sheet in: `translateX(104% → 0)` on `--motion-sheet-in` (260ms, `--ease-in`); out on `--motion-sheet-out` (180ms, `--ease-out`).
- Scrim fades to `--scrim` (rgba(10,14,24,.38)) on the same timings.
- Phone bottom sheet travels on Y with the same tokens and `--e-phone-sheet`.

### Accessibility

- **Keyboard:** Escape closes the top layer and returns focus to the opener.
- **Keyboard:** Tab and Shift+Tab cycle inside the open layer only.
- **ARIA:** `role="dialog"`, `aria-modal="true"`, `aria-labelledby` the title.
- **ARIA:** Opener carries `aria-expanded`.
- **Focus:** Opening moves focus inside (`[data-k-autofocus]` first, else the first field); closing returns it to the opener.

### Content rules

- Title = the record ("Navy birdseye two-piece suit"), code below in mono.
- Primary names the result: "Save price", not "Done".

**Do**

- Use for edits that keep the list context.
- Keep footer actions visible while the body scrolls.

**Don't**

- Stack two sheets; use the guard dialog above one.
- Put a whole page in a sheet: route instead.

### Build it

- **shadcn / Radix:** shadcn `Sheet` (Radix Dialog, side=right); phone bottom sheet = shadcn `Drawer` (vaul) or `Sheet side="bottom"`.
- **Exists in apps/admin today:** no · new: Sheet, Drawer. @radix-ui/react-dialog is installed (via ui/dialog.tsx); `sheet.tsx` and `drawer.tsx` are new.
- **Rendering:** client component (`"use client"`). 
- **Install:** `npx shadcn@latest add sheet drawer`

```ts
interface SideSheetProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  title: string;
  subtitle?: string;                // code / handle in mono
  media?: React.ReactNode;          // garment thumb
  size?: "sm" | "md" | "lg";        // 404 / 532 / 856
  dirty?: boolean;                  // close attempts open the unsaved guard
  footer: React.ReactNode;          // Cancel + one primary
  summary?: React.ReactNode;        // footer left
  belowTopBar?: boolean;            // help drawer
  children: React.ReactNode;
}
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [Sign in](../../phase-3/pages/sign-in/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="unsaved-guard"></a>
## Unsaved-changes guard

*Overlays · family `overlays` · page `components/unsaved-guard/` · phone `components/unsaved-guard/m/`*

The small dialog that appears above an open sheet (or page) when you try to leave with unsaved edits: Keep editing, Discard, Save and close.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Guard layer | `.k-guard` | Second layer above the sheet: scrim z 40, dialog z 41. |
| 2 | Dialog | `.k-dialog` | Title, one sentence, three actions. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Over a sheet | `.k-guard > .k-dialog` | Escape / scrim / Cancel on a dirty sheet. |
| Route leave | `same dialog` | Navigating away from a dirty page. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Closed: nothing rendered in the flow. | — |
| hover | Not applicable to the surface; its buttons and rows hover as usual. | — |
| focus | Open: focus moves inside (`[data-k-autofocus]` first), Tab is trapped, Escape closes and returns focus. | `[data-k-stage].open` |
| active | Open above the sheet; the sheet's scrim stays. | `.k-guard.open` |
| disabled | The opener is disabled, never the overlay. | — |
| error | The body shows an inline `.sf-alert.danger`; the overlay stays open. | `.sf-alert.danger` |
| loading | Primary shows `.spin` and the progressive verb; Escape still closes unless saving. | `[aria-busy=true]` |
| empty | Body shows a compact empty state. | `.sf-empty.compact` |
| read-only | Footer drops the primary and keeps Close. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| z-index scrim / dialog | 40 / 41 | `--z-guard-scrim / --z-guard` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--z-guard-scrim` | `40` | 40 |
| `--z-guard` | `41` | 41 |
| `--text-small` | `12px` | 12px |
| `--warning` | `36 79% 29%  (#845610)` | #845610 |
| `--w-medium` | `500` | 500 |

### Accessibility

- **Keyboard:** Escape = Keep editing.
- **Keyboard:** Enter does not discard.
- **ARIA:** `role="alertdialog"`, described by the sentence.
- **Focus:** Focus lands on Keep editing; returns to the sheet field after.

### Content rules

- Title "Discard your changes?" · body names what is lost ("The new GBP price and 2 photos") · actions Keep editing / Discard / Save and close.

**Do**

- Say exactly what will be lost.

**Don't**

- Show the guard when nothing changed.

### Build it

- **shadcn / Radix:** shadcn `AlertDialog` rendered above the open `Sheet`.
- **Exists in apps/admin today:** no · new: AlertDialog. Not in apps/admin.
- **Rendering:** client component (`"use client"`). 
- **Install:** `npx shadcn@latest add alert-dialog`

```ts
interface UnsavedGuardProps {
  open: boolean;
  lost: string;                     // "The new GBP price and 2 photos"
  onKeepEditing: () => void;
  onDiscard: () => void;
  onSave?: () => Promise<void>;
}
```

### Used on

[Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [Brand](../../phase-3/pages/settings-brand/)

---

<a id="dialog"></a>
## Dialog

*Overlays · family `overlays` · page `components/dialog/` · phone `components/dialog/m/`*

A centred, focused task that needs the person's full attention but is short: rename, choose a scope, connect an app. 480px wide, 15px radius.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Scrim | `.k-scrim` | As the sheet. |
| 2 | Panel | `.k-dialog` | 480px (92% max), `--r-frame` 15px, `--e-dialog`, overflow hidden. |
| 3 | Header | `.ov-dg-h` | Title + close, 20px 24px 8px. |
| 4 | Body | `.ov-dg-b` | Muted paragraph + fields, 16px gaps. |
| 5 | Footer | `.ov-dg-f` | Muted 50% ground, actions right, optional left link. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Default | `.k-dialog` | Short forms. |
| Phone | `.k-sheet.k-bottom` | Dialogs become bottom sheets on the phone. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Closed: nothing rendered in the flow. | — |
| hover | Not applicable to the surface; its buttons and rows hover as usual. | — |
| focus | Open: focus moves inside (`[data-k-autofocus]` first), Tab is trapped, Escape closes and returns focus. | `[data-k-stage].open` |
| active | Open: fades + rises from 48% to 50% and scale .98 → 1. | `.open > .k-dialog` |
| disabled | The opener is disabled, never the overlay. | — |
| error | The body shows an inline `.sf-alert.danger`; the overlay stays open. | `.sf-alert.danger` |
| loading | Primary shows `.spin` and the progressive verb; Escape still closes unless saving. | `[aria-busy=true]` |
| empty | Body shows a compact empty state. | `.sf-empty.compact` |
| read-only | Footer drops the primary and keeps Close. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| width | min(480px,92%) | `min(var(--dialog-w),92%)` |
| background | #FFFFFF | `hsl(var(--popover))` |
| border-radius | 15px | `var(--r-frame)` |
| box-shadow | 0 24px 64px -20px rgba(14,23,41,.40) | `var(--e-dialog)` |
| width | 480px (max 92%) | `--dialog-w` |
| radius | 15px | `--r-frame` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--dialog-w` | `480px` | 480px |
| `--popover` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--r-frame` | `15px` | 15px |
| `--e-dialog` | `0 24px 64px -20px rgba(14,23,41,.40)` | 0 24px 64px -20px rgba(14,23,41,.40) |
| `--z-sheet` | `31` | 31 |
| `--motion-pop-out` | `110ms var(--ease-out)` | 110ms cubic-bezier(.4,0,1,1) |
| `--motion-handoff` | `.2s` | .2s |
| `--motion-sheet-in` | `260ms var(--ease-in)` | 260ms cubic-bezier(.22,1,.36,1) |
| `--s-3` | `12px` | 12px |
| `--s-5` | `20px` | 20px |
| `--s-6` | `24px` | 24px |
| `--s-2` | `8px` | 8px |
| `--s-4` | `16px` | 16px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--w-medium` | `500` | 500 |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |

### Motion

- In: opacity + `translate(-50%,-48%) scale(.98)` → centred on `--motion-sheet-in`; out on `--motion-pop-out` (110ms).

### Accessibility

- **Keyboard:** Escape closes the top layer and returns focus to the opener.
- **Keyboard:** Tab and Shift+Tab cycle inside the open layer only.
- **ARIA:** `role="dialog"` `aria-modal`, `aria-labelledby` + `aria-describedby`.
- **Focus:** Opening moves focus inside (`[data-k-autofocus]` first, else the first field); closing returns it to the opener.

### Content rules

- Title is a question or task ("Rename saved view").

**Do**

- Keep to one decision.

**Don't**

- Scroll a dialog: use a sheet.

### Build it

- **shadcn / Radix:** shadcn `Dialog` (Radix).
- **Exists in apps/admin today:** Dialog. `ui/dialog.tsx` exists. Restyle panel radius to 15px and the muted footer.
- **Rendering:** client component (`"use client"`). 

```ts
interface DialogProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  title: string;
  description?: string;
  footer: React.ReactNode;
  footerStart?: React.ReactNode;    // left link
  children?: React.ReactNode;
}
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="confirm"></a>
## Confirm

*Overlays · family `overlays` · page `components/confirm/` · phone `components/confirm/m/`*

Asks before something consequential: neutral (Publish to brands), destructive (Delete price list) and typed (type the handle to retire a product used by live orders).

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Dialog | `.k-dialog` | As Dialog, `role=alertdialog`. |
| 2 | Consequence | `.ov-dg-b p` | What happens and to how many things, in bold numbers. |
| 3 | Typed field (optional) | `.ov-type .input` | Plex Mono; the exact handle shown in a code pill. |
| 4 | Actions | `.ov-dg-f` | Cancel + the verb (danger for destructive). |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Neutral | `.btn-primary` | Reversible but visible to others. |
| Destructive | `.btn-danger` | Deletes or retires. |
| Typed | `.ov-type` | Irreversible and widely used: type the handle to enable the button. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Closed: nothing rendered in the flow. | — |
| hover | Not applicable to the surface; its buttons and rows hover as usual. | — |
| focus | Open: focus moves inside (`[data-k-autofocus]` first), Tab is trapped, Escape closes and returns focus. | `[data-k-stage].open` |
| active | Open, with the `--motion-*-in` entrance. | `.open` |
| disabled | Typed confirm: the danger button stays disabled until the text matches exactly. | `[disabled]` |
| error | The body shows an inline `.sf-alert.danger`; the overlay stays open. | `.sf-alert.danger` |
| loading | Danger button "Deleting…" with spinner; Cancel disabled. | `[aria-busy=true]` |
| empty | Body shows a compact empty state. | `.sf-empty.compact` |
| read-only | Footer drops the primary and keeps Close. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 8px | `var(--s-2)` |
| padding | 12px 24px | `var(--s-3) var(--s-6)` |
| background | #F3F4F7 @0.5 | `hsl(var(--muted)/.5)` |
| width | 480px (max 92%) | `--dialog-w` |
| typed field | Plex Mono 12px | `--font-code / --text-code` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--s-3` | `12px` | 12px |
| `--s-5` | `20px` | 20px |
| `--s-6` | `24px` | 24px |
| `--s-2` | `8px` | 8px |
| `--s-4` | `16px` | 16px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--w-medium` | `500` | 500 |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--r-card` | `8px` | 8px |
| `--input` | `219 14% 59%  (#8892A5)` | #8892A5 |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--text-small` | `12px` | 12px |
| `--font-code` | `'IBM Plex Mono', ui-monospace, monospace` | 'IBM Plex Mono', ui-monospace, monospace |
| `--text-code` | `12px` | 12px |
| `--r-chip` | `4px` | 4px |

### Accessibility

- **Keyboard:** Escape cancels.
- **Keyboard:** Focus starts on Cancel for destructive confirms.
- **ARIA:** `role="alertdialog"`; the consequence is `aria-describedby`.
- **Focus:** Opening moves focus inside (`[data-k-autofocus]` first, else the first field); closing returns it to the opener.

### Content rules

- Title: the action as a question ("Delete the EU price list?").
- Body: the consequence with numbers ("14 products lose their EUR price").
- Button repeats the verb: "Delete price list".

**Do**

- Prefer an Undo toast for reversible deletes.

**Don't**

- "Are you sure?"
- OK / Cancel.

### Build it

- **shadcn / Radix:** shadcn `AlertDialog`.
- **Exists in apps/admin today:** no · new: AlertDialog. Not in apps/admin.
- **Rendering:** client component (`"use client"`). 
- **Install:** `npx shadcn@latest add alert-dialog`

```ts
interface ConfirmProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  tone: "neutral" | "destructive";
  title: string;                    // "Delete the EU price list?"
  consequence: React.ReactNode;     // "14 products lose their EUR price"
  confirmLabel: string;             // "Delete price list"
  typeToConfirm?: string;           // exact handle to type
  onConfirm: () => Promise<void>;
}
```

### Used on

[Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [Releases](../../phase-3/pages/releases/), [API keys](../../phase-3/pages/settings-api-keys/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="confirm-unverified-currency"></a>
## Confirm unverified currency

*Overlays · family `overlays` · page `components/confirm-unverified-currency/` · phone `components/confirm-unverified-currency/m/`*

Before saving a price converted at a rate nobody has verified, the admin shows the amounts side by side and asks the person to acknowledge the rate. Also has an inline form inside the price editor.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Dialog | `.k-dialog` | Confirm layout. |
| 2 | Rate line | `.ov-fx` | "1 GBP = 1.15 EUR · source ECB · 2 Oct" in tabular numbers. |
| 3 | Amount table | `.ov-tbl` | Base vs converted per currency. |
| 4 | Acknowledge | `.ov-ack` | Checkbox card: "I have checked these prices"; checked edge `--primary`. |
| 5 | Actions | `.ov-dg-f` | Edit prices · Save with this rate (disabled until acknowledged). |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Dialog | `.k-dialog` | On save. |
| Inline | `.fc-money.unv + note` | Amber field with a Verify link inside the editor. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Closed: nothing rendered in the flow. | — |
| hover | Not applicable to the surface; its buttons and rows hover as usual. | — |
| focus | Open: focus moves inside (`[data-k-autofocus]` first), Tab is trapped, Escape closes and returns focus. | `[data-k-stage].open` |
| active | Open, with the `--motion-*-in` entrance. | `.open` |
| disabled | Save disabled until the acknowledgement is ticked. | `[disabled]` |
| error | Rate unavailable: warning alert, only Edit prices offered. | `.sf-alert.warning` |
| loading | Primary shows `.spin` and the progressive verb; Escape still closes unless saving. | `[aria-busy=true]` |
| empty | Body shows a compact empty state. | `.sf-empty.compact` |
| read-only | Footer drops the primary and keeps Close. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 8px 16px | `var(--s-2) var(--s-4)` |
| font-size | 12px | `var(--text-small)` |
| color | #505B71 | `hsl(var(--muted-foreground))` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--warning` | `36 79% 29%  (#845610)` | #845610 |
| `--control-h` | `40px` | 40px |
| `--input` | `219 14% 59%  (#8892A5)` | #8892A5 |
| `--r-control` | `6px` | 6px |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--motion-focus` | `140ms var(--ease-standard)` | 140ms cubic-bezier(.2,0,0,1) |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--focus-ring` | `0 0 0 1px hsl(var(--ring)), 0 0 0 4px hsl(var(--ring) / .14)` | 0 0 0 1px #3F60A6, 0 0 0 4px #3F60A6 @0.14 |
| `--s-8` | `32px` | 32px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--s-3` | `12px` | 12px |
| `--w-medium` | `500` | 500 |
| `--text-btn-sm` | `12.5px` | 12.5px |
| `--s-2` | `8px` | 8px |
| `--s-4` | `16px` | 16px |
| `--text-small` | `12px` | 12px |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--r-card` | `8px` | 8px |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--control-h-sm` | `34px` | 34px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |

### Accessibility

- **Keyboard:** Space ticks the acknowledgement.
- **ARIA:** Table has a caption; the checkbox is `aria-describedby` the rate line.
- **Focus:** Opening moves focus inside (`[data-k-autofocus]` first, else the first field); closing returns it to the opener.

### Content rules

- Show the rate, its source and date.
- Never call a conversion "verified" unless a person ticked it.

**Do**

- Keep both amounts visible in the dialog.

**Don't**

- Auto-save converted prices.

### Build it

- **shadcn / Radix:** `AlertDialog` + `Checkbox` + `Table`.
- **Exists in apps/admin today:** Checkbox · new: AlertDialog. Checkbox exists; AlertDialog new.
- **Rendering:** client component (`"use client"`). 

```ts
interface ConfirmUnverifiedCurrencyProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  base: { currency: Currency; amountMinor: number };
  converted: { currency: Currency; amountMinor: number; rate: number; source: string; at: Date }[];
  onConfirm: () => Promise<void>;   // enabled only after acknowledgement
  onEdit: () => void;
}
```

### Used on

[Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/)

---

<a id="dropdown-menu"></a>
## Dropdown menu

*Overlays · family `overlays` · page `components/dropdown-menu/` · phone `components/dropdown-menu/m/`*

The ⋯ row menu and every action menu: items with icons, a selected tick, disabled items that say why, a separator and a danger item last. On the phone it becomes an action sheet.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Trigger | `.icon-btn.ghost[data-k-trigger]` | ⋯ or a labelled button with chevron. |
| 2 | Surface | `.menu` | Min 208px, 4px padding, 8px radius, `--e-pop`. |
| 3 | Item | `.mi` | 34px (44px phone), Archivo 500 13px, 14px icon, optional `kbd` or `.r` meta. |
| 4 | Label | `.mlabel` | Group heading 11.5px. |
| 5 | Separator | `.msep` | 1px hairline full bleed. |
| 6 | Danger item | `.mi.danger` | Last, after a separator. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Actions | `.menu > .mi` | Row menu. |
| Selectable | `.mi[aria-checked]` | Sort, view options with a tick. |
| Disabled with reason | `.mi[aria-disabled] .why` | "Retire · in 3 live orders". |
| Action sheet (phone) | `.k-sheet.k-bottom` | Same items at 44px. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Closed: nothing rendered in the flow. | — |
| hover | Item `--muted`. | `.mi:hover` |
| focus | Item `--accent` + inset 1px ring at 55%. | `.mi:focus-visible` |
| active | Selected item `--accent` and tick. | `.mi.on · [aria-checked=true]` |
| disabled | Muted text, icon at 60%, reason right-aligned. | `.mi[aria-disabled=true]` |
| error | The body shows an inline `.sf-alert.danger`; the overlay stays open. | `.sf-alert.danger` |
| loading | Primary shows `.spin` and the progressive verb; Escape still closes unless saving. | `[aria-busy=true]` |
| empty | Body shows a compact empty state. | `.sf-empty.compact` |
| read-only | Footer drops the primary and keeps Close. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| background | #FFFFFF | `hsl(var(--popover))` |
| color | #0F1524 | `hsl(var(--popover-foreground))` |
| border | 1px solid #E3E7ED | `1px solid hsl(var(--border))` |
| border-radius | 8px | `var(--r-card)` |
| box-shadow | 0 12px 32px -10px rgba(14,23,41,.25) | `var(--e-pop)` |
| padding | 4px | `var(--s-1)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--popover` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--popover-foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--r-card` | `8px` | 8px |
| `--e-pop` | `0 12px 32px -10px rgba(14,23,41,.25)` | 0 12px 32px -10px rgba(14,23,41,.25) |
| `--s-1` | `4px` | 4px |
| `--z-pop` | `20` | 20 |
| `--s-2` | `8px` | 8px |
| `--text-micro` | `11.5px` | 11.5px |
| `--w-medium` | `500` | 500 |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--control-h-sm` | `34px` | 34px |
| `--r-control` | `6px` | 6px |
| `--text-body` | `13px` | 13px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--w-regular` | `400` | 400 |
| `--text-small` | `12px` | 12px |
| `--font-ui` | `'IBM Plex Sans', system-ui, sans-serif` | 'IBM Plex Sans', system-ui, sans-serif |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--accent-foreground` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--control-h-m` | `44px` | 44px |
| `--text-read` | `15px` | 15px |

### Motion

- Pop in: opacity, `translateY(--pop-shift -4px)` and scale .985 → 1 on `--motion-pop-in` (160ms); out 110ms. Origin top right (`.k-anchor`).

### Accessibility

- **Keyboard:** Enter / Space / ↓ open; ↑ ↓ move; Home / End; Enter activates; Escape closes and returns focus; type-ahead.
- **ARIA:** Radix DropdownMenu: `role="menu"`, `menuitem`, `menuitemcheckbox`; trigger `aria-haspopup` + `aria-expanded`.
- **Focus:** First item focused on keyboard open; trigger refocused on close.

### Content rules

- Verbs: "Edit", "Duplicate", "Copy handle", "Retire product".
- Danger item last, separated.

**Do**

- Explain disabled items.

**Don't**

- More than ~8 items; split or use a sheet.

### Build it

- **shadcn / Radix:** shadcn `DropdownMenu` (Radix).
- **Exists in apps/admin today:** no · new: DropdownMenu. @radix-ui/react-dropdown-menu is installed and used directly (shell/account-menu, catalogue-more-menu); `dropdown-menu.tsx` wrapper is new.
- **Rendering:** client component (`"use client"`). 
- **Install:** `npx shadcn@latest add dropdown-menu`

```ts
type MenuItem =
  | { type: "item"; label: string; icon?: LucideIcon; shortcut?: string; onSelect: () => void; danger?: boolean; disabledReason?: string }
  | { type: "checkbox"; label: string; checked: boolean; onCheckedChange: (v: boolean) => void }
  | { type: "label"; label: string }
  | { type: "separator" };

interface ActionMenuProps {
  items: MenuItem[];
  trigger?: React.ReactNode;        // default: ghost ⋯ icon button
  label: string;                    // "More actions for Navy birdseye two-piece suit"
  align?: "start" | "end";
}
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [Sign in](../../phase-3/pages/sign-in/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="popover"></a>
## Popover

*Overlays · family `overlays` · page `components/popover/` · phone `components/popover/m/`*

A small non-modal panel anchored to a control: filters, column options, a quick note. Clicks inside keep it open; Apply or `[data-k-close-pop]` closes it. Phone: a short bottom sheet.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Anchor | `.k-anchor[data-k-pop]` | Positions the panel below the trigger (`.l` left, `.up` above). |
| 2 | Surface | `.pop` | 16px padding, 8px radius, `--e-pop`. |
| 3 | Footer (optional) | `.ov-pop-f` | Reset + Apply. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Filter | `.pop + fields` | Table filters. |
| Info | `.pop` | Explains a figure. |
| Phone | `.k-sheet.k-bottom` | Short bottom sheet. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Closed: nothing rendered in the flow. | — |
| hover | Not applicable to the surface; its buttons and rows hover as usual. | — |
| focus | Open: focus moves inside (`[data-k-autofocus]` first), Tab is trapped, Escape closes and returns focus. | `[data-k-stage].open` |
| active | Open below the trigger. | `.open > .k-pop` |
| disabled | The opener is disabled, never the overlay. | — |
| error | The body shows an inline `.sf-alert.danger`; the overlay stays open. | `.sf-alert.danger` |
| loading | Primary shows `.spin` and the progressive verb; Escape still closes unless saving. | `[aria-busy=true]` |
| empty | Body shows a compact empty state. | `.sf-empty.compact` |
| read-only | Footer drops the primary and keeps Close. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| background | #FFFFFF | `hsl(var(--popover))` |
| color | #0F1524 | `hsl(var(--popover-foreground))` |
| border | 1px solid #E3E7ED | `1px solid hsl(var(--border))` |
| border-radius | 8px | `var(--r-card)` |
| box-shadow | 0 12px 32px -10px rgba(14,23,41,.25) | `var(--e-pop)` |
| padding | 16px | `var(--s-4)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--popover` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--popover-foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--r-card` | `8px` | 8px |
| `--e-pop` | `0 12px 32px -10px rgba(14,23,41,.25)` | 0 12px 32px -10px rgba(14,23,41,.25) |
| `--s-4` | `16px` | 16px |
| `--s-1` | `4px` | 4px |
| `--z-pop` | `20` | 20 |
| `--w-semibold` | `600` | 600 |
| `--text-card` | `13.5px` | 13.5px |
| `--font-title` | `'Archivo', system-ui, sans-serif` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--text-small` | `12px` | 12px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--s-3` | `12px` | 12px |
| `--s-2` | `8px` | 8px |

### Motion

- As the dropdown menu (pop in 160ms, out 110ms).

### Accessibility

- **Keyboard:** Escape closes; Tab moves through fields (not trapped: non-modal).
- **ARIA:** Radix Popover: trigger `aria-expanded` + `aria-controls`; panel `role="dialog"` when it holds fields.
- **Focus:** Focus moves to the first field; returns to the trigger on close.

### Content rules

- Popover titles optional; Apply names the result ("Show 41 orders").

**Do**

- Live-count the result on the apply button.

**Don't**

- Nest popovers.

### Build it

- **shadcn / Radix:** shadcn `Popover` (Radix).
- **Exists in apps/admin today:** no · new: Popover. @radix-ui/react-popover installed and used directly (shell/brand-picker); `popover.tsx` wrapper is new.
- **Rendering:** client component (`"use client"`). 
- **Install:** `npx shadcn@latest add popover`

```ts
interface PopoverPanelProps {
  trigger: React.ReactNode;
  title?: string;
  align?: "start" | "end";
  side?: "bottom" | "top";
  onApply?: () => void;
  applyLabel?: string;              // "Show 41 orders"
  onReset?: () => void;
  children: React.ReactNode;
}
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [Sign in](../../phase-3/pages/sign-in/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="tooltip"></a>
## Tooltip

*Overlays · family `overlays` · page `components/tooltip/` · phone `components/tooltip/m/`*

A navy label for icon buttons and truncated text. Waits 400ms on hover, shows at once on keyboard focus, never holds anything interactive. Phones have no tooltips: use visible labels.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Wrapper | `.tt` | Inline-flex anchor. |
| 2 | Label | `.tip` | Navy `--primary`, Plex 500 12px, 6px 8px, 6px radius, z 25. |
| 3 | Arrow | `.tip::after` | 4px triangle. |
| 4 | Shortcut (optional) | `.tip kbd` | Transparent key cap. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Above | `.tt` | Default. |
| Below | `.tt.below` | Near the top edge. |
| End-aligned | `.tt.end` | At the right edge. |
| Wrapping | `.tip.wrap` | 232px, for a sentence. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Closed: nothing rendered in the flow. | — |
| hover | Shows after `--tooltip-delay` 400ms. | `.tt:hover > .tip` |
| focus | Shows immediately. | `.tt:focus-within > .tip` |
| active | Hidden while its trigger's menu is open. | `[aria-expanded=true] ~ .tip` |
| disabled | The opener is disabled, never the overlay. | — |
| error | The body shows an inline `.sf-alert.danger`; the overlay stays open. | `.sf-alert.danger` |
| loading | Primary shows `.spin` and the progressive verb; Escape still closes unless saving. | `[aria-busy=true]` |
| empty | Body shows a compact empty state. | `.sf-empty.compact` |
| read-only | Footer drops the primary and keeps Close. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 8px | `var(--s-2)` |
| padding | 6px 8px | `6px var(--s-2)` |
| border-radius | 6px | `var(--r-control)` |
| background | #0E172A | `hsl(var(--primary))` |
| color | #FBFBF9 | `hsl(var(--primary-foreground))` |
| font | 500 12px/1.25 'IBM Plex Sans', system-ui, sans-serif | `var(--w-medium) var(--text-small)/1.25 var(--font-ui)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--s-2` | `8px` | 8px |
| `--z-tip` | `25` | 25 |
| `--r-control` | `6px` | 6px |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--primary-foreground` | `60 17% 98%  (#FBFBF9)` | #FBFBF9 |
| `--w-medium` | `500` | 500 |
| `--text-small` | `12px` | 12px |
| `--font-ui` | `'IBM Plex Sans', system-ui, sans-serif` | 'IBM Plex Sans', system-ui, sans-serif |
| `--motion-pop-out` | `110ms var(--ease-out)` | 110ms cubic-bezier(.4,0,1,1) |
| `--motion-handoff` | `.2s` | .2s |
| `--motion-pop-in` | `160ms var(--ease-in)` | 160ms cubic-bezier(.22,1,.36,1) |
| `--tooltip-delay` | `400ms` | 400ms |

### Motion

- In: opacity + 2px rise on `--motion-pop-in` after `--tooltip-delay`; 0ms delay on focus and under reduced motion.

### Accessibility

- **Keyboard:** Escape hides it.
- **ARIA:** Radix Tooltip: `aria-describedby` on the trigger; the trigger still has its own `aria-label`.
- **Focus:** Never focusable; never contains links.

### Content rules

- Short label, sentence case, no full stop.

**Do**

- Pair with every icon-only button on desktop.

**Don't**

- Put essential information only in a tooltip.
- Tooltips on the phone.

### Build it

- **shadcn / Radix:** shadcn `Tooltip` (Radix) with `delayDuration={400}`.
- **Exists in apps/admin today:** no · new: Tooltip. Not in apps/admin.
- **Rendering:** client component (`"use client"`). 
- **Install:** `npx shadcn@latest add tooltip`

```ts
interface TooltipProps {
  content: React.ReactNode;
  side?: "top" | "bottom";
  align?: "center" | "end";
  shortcut?: string;
  children: React.ReactElement;     // the trigger
}
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="hover-card"></a>
## Hover card

*Overlays · family `overlays` · page `components/hover-card/` · phone `components/hover-card/m/`*

A richer preview on hover over a reference (a cloth code, a product handle): swatch, facts and where it is used. Waits 500ms. On the phone it is tap-to-peek in a bottom sheet.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Trigger | `.ov-hc` | Underlined code or name. |
| 2 | Card | `.ov-hcard` | Popover surface with media, title, `dl` facts and a "Used in" line. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Cloth | `.ov-hcard` | Swatch + supplier + stock. |
| Product | `.ov-hcard` | Thumb + status + price. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Closed: nothing rendered in the flow. | — |
| hover | Opens after `--hovercard-delay` 500ms. | — |
| focus | Opens on keyboard focus. | — |
| active | Open, with the `--motion-*-in` entrance. | `.open` |
| disabled | The opener is disabled, never the overlay. | — |
| error | The body shows an inline `.sf-alert.danger`; the overlay stays open. | `.sf-alert.danger` |
| loading | Primary shows `.spin` and the progressive verb; Escape still closes unless saving. | `[aria-busy=true]` |
| empty | Body shows a compact empty state. | `.sf-empty.compact` |
| read-only | Footer drops the primary and keeps Close. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| width | 320px | `320px` |
| padding | 16px | `var(--s-4)` |
| border | 1px solid #E3E7ED | `1px solid hsl(var(--border))` |
| border-radius | 8px | `var(--r-card)` |
| background | #FFFFFF | `hsl(var(--popover))` |
| box-shadow | 0 12px 32px -10px rgba(14,23,41,.25) | `var(--e-pop)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--font-code` | `'IBM Plex Mono', ui-monospace, monospace` | 'IBM Plex Mono', ui-monospace, monospace |
| `--text-code` | `12px` | 12px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--r-chip` | `4px` | 4px |
| `--s-2` | `8px` | 8px |
| `--z-tip` | `25` | 25 |
| `--s-4` | `16px` | 16px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--r-card` | `8px` | 8px |
| `--popover` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--e-pop` | `0 12px 32px -10px rgba(14,23,41,.25)` | 0 12px 32px -10px rgba(14,23,41,.25) |
| `--pop-shift` | `-4px` | -4px |
| `--motion-pop-out` | `110ms var(--ease-out)` | 110ms cubic-bezier(.4,0,1,1) |
| `--motion-handoff` | `.2s` | .2s |
| `--motion-pop-in` | `160ms var(--ease-in)` | 160ms cubic-bezier(.22,1,.36,1) |
| `--hovercard-delay` | `500ms` | 500ms |
| `--s-3` | `12px` | 12px |
| `--w-semibold` | `600` | 600 |
| `--text-card` | `13.5px` | 13.5px |
| `--font-title` | `'Archivo', system-ui, sans-serif` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--text-small` | `12px` | 12px |
| `--w-regular` | `400` | 400 |

### Accessibility

- **Keyboard:** Focus on the trigger opens; Escape closes.
- **ARIA:** Supplementary only; the link still navigates.
- **Focus:** Content is not focus-trapped.

### Content rules

- Facts only; no actions.

**Do**

- Use for codes that are otherwise opaque.

**Don't**

- Put the only path to an action in a hover card.

### Build it

- **shadcn / Radix:** shadcn `HoverCard` (Radix) with `openDelay={500}`.
- **Exists in apps/admin today:** no · new: HoverCard. Not in apps/admin.
- **Rendering:** client component (`"use client"`). 
- **Install:** `npx shadcn@latest add hover-card`

```ts
interface HoverPreviewProps {
  trigger: React.ReactNode;         // the code / link
  title: string;
  media?: React.ReactNode;
  facts: { label: string; value: React.ReactNode }[];
  usedIn?: string;                  // "Used in 6 products"
}
```

### Used on

Proposed for cloth codes and handles; no approved surface uses it yet.

---

<a id="overlay-layer"></a>
## Overlay layer

*Overlays · family `page-primitives` · page `components/overlay-layer/` · phone `components/overlay-layer/m/`*

Kit v2: the full-viewport fixed layer real pages mount sheets, dialogs, guards and the help drawer into, so they cover the whole window instead of a specimen stage. The React build gets this from Radix Portal.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Layer | `.k-layer` | `position:fixed; inset:0`, z 30, pointer-events none until open. |
| 2 | Scrim / sheet / dialog | `.k-scrim .k-sheet .k-dialog` | Children take pointer events. |
| 3 | Scroll body | `.k-sheet-b` | Flex 1, own scroll, contained overscroll. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Default | `.k-layer` | Covers the viewport. |
| Below the bar | `.k-layer.below-bar` | Starts right of the sidebar and under the top bar (help drawer). |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Closed: nothing rendered in the flow. | — |
| hover | Not applicable to the surface; its buttons and rows hover as usual. | — |
| focus | Open: focus moves inside (`[data-k-autofocus]` first), Tab is trapped, Escape closes and returns focus. | `[data-k-stage].open` |
| active | Open: children receive pointer events. | `.k-layer.open` |
| disabled | The opener is disabled, never the overlay. | — |
| error | The body shows an inline `.sf-alert.danger`; the overlay stays open. | `.sf-alert.danger` |
| loading | Primary shows `.spin` and the progressive verb; Escape still closes unless saving. | `[aria-busy=true]` |
| empty | Body shows a compact empty state. | `.sf-empty.compact` |
| read-only | Footer drops the primary and keeps Close. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| position | fixed, inset 0 | — |
| z-index | 30 (scrim) · sheet 31 · guard 40/41 · toast 50 | `--z-scrim / --z-sheet / --z-guard / --z-toast` |
| below-bar offset | left 232px, top 56px | `--sidebar-w / --topbar-h` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--z-scrim` | `30` | 30 |
| `--sidebar-w` | `232px` | 232px |
| `--topbar-h` | `56px` | 56px |

### Accessibility

- **Keyboard:** Escape closes the top layer and returns focus to the opener.
- **Keyboard:** Tab and Shift+Tab cycle inside the open layer only.
- **ARIA:** Background gets `inert` (Radix sets `aria-hidden` on siblings).
- **Focus:** Opening moves focus inside (`[data-k-autofocus]` first, else the first field); closing returns it to the opener.

### Content rules

- None; structural.

**Do**

- Portal every overlay to the body.

**Don't**

- Render overlays inside a `overflow:hidden` or transformed parent.

### Build it

- **shadcn / Radix:** Radix `Portal` (built into Dialog / Sheet / Popover).
- **Exists in apps/admin today:** no. Comes with every Radix overlay.
- **Rendering:** client component (`"use client"`). 

```ts
// Provided by Radix: <Dialog.Portal> / <SheetPortal>. A custom layer only if needed:
interface OverlayLayerProps { open: boolean; belowTopBar?: boolean; children: React.ReactNode }
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [Sign in](../../phase-3/pages/sign-in/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="sidebar"></a>
## App shell and sidebar

*Navigation and shell · family `app-shell` · page `components/sidebar/` · phone `components/sidebar/m/`*

Two shells, one system: HQ super admin and brand admin. A 232px navy sidebar with the satin sheen (the only glow in the product), the place card on top, grouped menu items and the account at the foot. Phone: place bar, tab bar and a Menu drawer.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Frame | `.frame > .app` | Grid `232px 1fr`, `overflow:clip` so sticky children work. |
| 2 | Sidebar | `.sidebar` | Navy `--sidebar-sheen` gradient + radial `--sidebar-sheen-light`, 14px 10px padding. |
| 3 | Place card | `.sb-brand / .sh-place` | Monogram + place name + scope; opens the place switcher. |
| 4 | Jump to | `.sh-jump` | ⌘K trigger. |
| 5 | Groups | `.sb-group` | 11.5px labels at 60%. |
| 6 | Menu items | `.sb-item` | See Menu item. |
| 7 | Foot | `.sb-foot` | Account avatar + name; hairline above. |
| 8 | Main | `.main` | Top bar + page; `overflow:clip`. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Brand admin | `.app` | White monogram, brand menu (Products, Orders, Customers, Website…). |
| HQ super admin | `.app.sh-hq` | Steel-blue HQ tile, 2px steel hairline on top, "Every brand, read and write" scope line, HQ menu. |
| Phone | `.phone + .p-bar + .tabbar` | No sidebar: place bar on top, four tabs, Menu opens a bottom-sheet drawer. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Muted label, icon at 80% opacity. | — |
| hover | Soft fill (`--muted`, or sidebar foreground at 6%). | `:hover · .is-hover` |
| focus | 2px ring outline (`--ring`, `--sidebar-ring` on navy). | `:focus-visible · .is-focus` |
| active | Current item: foreground colour, semibold, and its indicator. | `[aria-current=page] · .on` |
| disabled | Muted at 45%, not focusable, reason in a tooltip. | `[aria-disabled=true]` |
| error | Not applicable. | — |
| loading | Skeleton lines while the place's menu loads. | `.skel` |
| empty | Not applicable: navigation is never empty. | — |
| read-only | Not applicable. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| background | linear-gradient(180deg, #131D33 0%, #0E1729 52%, #0C1425 100%) | `var(--sidebar-sheen)` |
| color | #C8CEDA | `hsl(var(--sidebar-foreground))` |
| padding | 14px 10px | `14px 10px` |
| gap | 4px | `var(--s-1)` |
| box-shadow | inset -1px 0 0 rgba(201,206,218,.07) | `var(--e-sidebar-edge)` |
| sidebar width | 232px | `--sidebar-w` |
| top bar | 56px | `--topbar-h` |
| content max | 1280px | `--content-max` |
| phone bar / tab bar | 52 / 62px | `--pbar-h / --tabbar-h` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--r-frame` | `15px` | 15px |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--e-frame` | `0 0 0 1px rgba(14,23,41,.05), 0 16px 36px -24px rgba(14,23,41,.20)` | 0 0 0 1px rgba(14,23,41,.05), 0 16px 36px -24px rgba(14,23,41,.20) |
| `--sidebar-w` | `232px` | 232px |
| `--sidebar-sheen` | `linear-gradient(180deg, #131D33 0%, #0E1729 52%, #0C1425 100%)` | linear-gradient(180deg, #131D33 0%, #0E1729 52%, #0C1425 100%) |
| `--sidebar-foreground` | `222 19% 82%  (#C8CEDA)` | #C8CEDA |
| `--s-1` | `4px` | 4px |
| `--e-sidebar-edge` | `inset -1px 0 0 rgba(201,206,218,.07)` | inset -1px 0 0 rgba(201,206,218,.07) |
| `--sidebar-sheen-light` | `radial-gradient(closest-side, rgba(130,160,220,.11), transparent)` | radial-gradient(closest-side, rgba(130,160,220,.11), transparent) |
| `--r-control` | `6px` | 6px |
| `--w-bold` | `700` | 700 |
| `--brand-mark` | `linear-gradient(145deg,#EEF1F8,#CBD5EA)` | linear-gradient(145deg,#EEF1F8,#CBD5EA) |
| `--brand-mark-foreground` | `#0E1729` | #0E1729 |
| `--e-brand-mark` | `0 0 12px -4px rgba(150,175,225,.22)` | 0 0 12px -4px rgba(150,175,225,.22) |
| `--w-semibold` | `600` | 600 |
| `--text-card` | `13.5px` | 13.5px |
| `--text-micro` | `11.5px` | 11.5px |
| `--w-medium` | `500` | 500 |
| `--text-small` | `12px` | 12px |
| `--sidebar-primary` | `221 54% 75%  (#9DB3E2)` | #9DB3E2 |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--sidebar` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--sidebar-accent` | `222 43% 19%  (#1C2845)` | #1C2845 |
| `--sidebar-border` | `222 33% 22%  (#26314B)` | #26314B |
| `--sidebar-ring` | `221 54% 75%  (#9DB3E2)` | #9DB3E2 |

### Accessibility

- **Keyboard:** Skip link to main content first.
- **Keyboard:** Sidebar items are links in tab order.
- **ARIA:** `<nav aria-label="Main">`; current page `aria-current="page"`.
- **Focus:** Ring on navy uses `--sidebar-ring`.

### Content rules

- Place names exactly: RJ HQ, RJ Clothing, RJ MTM.
- Group labels short nouns (Catalogue, Selling, Settings).

**Do**

- Keep the sheen on the sidebar only.

**Don't**

- Collapse the sidebar into icons on desktop.
- Show HQ-only items in brand mode.

### Build it

- **shadcn / Radix:** shadcn `Sidebar` block (SidebarProvider, SidebarHeader, SidebarGroup, SidebarMenu, SidebarFooter) with the kit's sidebar tokens.
- **Exists in apps/admin today:** no · new: Sidebar. apps/admin has its own shell components (shell/brand-picker, shell/account-menu, shell/command-palette); no ui/sidebar.
- **Rendering:** client component (`"use client"`). SidebarProvider holds open state; menu items can stay server components.
- **Install:** `npx shadcn@latest add sidebar`

```ts
type Place = { kind: "hq" } | { kind: "brand"; id: string; name: string; channel: "dtc" | "b2b" };

interface AppShellProps {
  place: Place;
  nav: { group: string; items: NavItem[] }[];
  user: { name: string; role: string };
  environment: "staging" | "production" | "local";
  children: React.ReactNode;
}
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="menu-item"></a>
## Menu item

*Navigation and shell · family `app-shell` · page `components/menu-item/` · phone `components/menu-item/m/`*

One sidebar entry: 16px icon, label, optional count or an amber hold / stuck pill, nested children for Settings and Website. The current item gets a 2px periwinkle rail.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Row | `.sb-item` | 36px, 10px padding and gap, 6px radius, Archivo 500 13.5px. |
| 2 | Icon | `svg` | 16px at 80% opacity. |
| 3 | Label | `text` |  |
| 4 | Count | `em / .sh-cnt` | Muted count; amber solid pill for hold / stuck (6.4:1 on navy). |
| 5 | Current rail | `.sb-item.on::before` | 2px `--sidebar-primary` bar, inset 9px top and bottom. |
| 6 | Children | `.sh-tree` | Indented 18px with a hairline; 32px rows. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Default | `.sb-item` |  |
| With count | `.sb-item em` | Open tickets. |
| Attention | `.sh-cnt.warn` | "hold 1", "3 stuck". |
| Nested | `.sh-tree .sb-item` | Settings pages. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Muted label, icon at 80% opacity. | — |
| hover | Soft fill (`--muted`, or sidebar foreground at 6%). | `:hover · .is-hover` |
| focus | 2px ring outline (`--ring`, `--sidebar-ring` on navy). | `:focus-visible · .is-focus` |
| active | Current: white 7% ground, inset hairline, soft shadow, rail, icon `#B7C7EA`. | `.on · [aria-current=page]` |
| disabled | Muted at 45%, not focusable, reason in a tooltip. | `[aria-disabled=true]` |
| error | Not applicable. | — |
| loading | Skeleton lines while the place's menu loads. | `.skel` |
| empty | Not applicable: navigation is never empty. | — |
| read-only | Not applicable. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 10px | `10px` |
| height | 36px | `36px` |
| padding | 0 10px | `0 10px` |
| border-radius | 6px | `var(--r-control)` |
| font-size | 13.5px | `var(--text-card)` |
| font-weight | 500 | `var(--w-medium)` |
| color | inherit | `inherit` |
| height (nested) | 36px (32px) | — |
| rail | 2px × (36 − 18)px | `--sidebar-primary` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--r-control` | `6px` | 6px |
| `--text-card` | `13.5px` | 13.5px |
| `--w-medium` | `500` | 500 |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--sidebar-foreground` | `222 19% 82%  (#C8CEDA)` | #C8CEDA |
| `--sidebar-item-on` | `rgba(255,255,255,.07)` | rgba(255,255,255,.07) |
| `--sidebar-accent-foreground` | `60 17% 98%  (#FBFBF9)` | #FBFBF9 |
| `--e-sidebar-item-on` | `inset 0 0 0 1px rgba(201,206,218,.09),0 6px 18px -12px rgba(110,140,210,.22)` | inset 0 0 0 1px rgba(201,206,218,.09),0 6px 18px -12px rgba(110,140,210,.22) |
| `--sidebar-primary` | `221 54% 75%  (#9DB3E2)` | #9DB3E2 |
| `--sidebar-item-on-icon` | `#B7C7EA` | #B7C7EA |
| `--text-micro` | `11.5px` | 11.5px |
| `--font-ui` | `'IBM Plex Sans', system-ui, sans-serif` | 'IBM Plex Sans', system-ui, sans-serif |
| `--sidebar-ring` | `221 54% 75%  (#9DB3E2)` | #9DB3E2 |
| `--warning-foreground` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--warning` | `36 79% 29%  (#845610)` | #845610 |
| `--r-chip` | `4px` | 4px |
| `--w-semibold` | `600` | 600 |
| `--text-body` | `13px` | 13px |

### Motion

- Background and colour on `--motion-hover`.

### Accessibility

- **Keyboard:** Links; Enter follows.
- **ARIA:** `aria-current="page"` on the current item; counts read as "3 stuck" via text, not colour.
- **Focus:** 2px `--sidebar-ring` outline, 1px offset.

### Content rules

- Labels match page titles.

**Do**

- Show counts only when they need action.

**Don't**

- Badges for vanity counts.

### Build it

- **shadcn / Radix:** `SidebarMenuButton` + `SidebarMenuBadge` + `SidebarMenuSub`.
- **Exists in apps/admin today:** no · new: Sidebar. No ui/sidebar.
- **Rendering:** server component. 

```ts
interface NavItem {
  href: string;
  label: string;
  icon: LucideIcon;
  count?: number;
  attention?: string;               // "hold 1", "3 stuck"
  children?: NavItem[];
  hqOnly?: boolean;
}
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="settings-subnav"></a>
## Settings sub-nav

*Navigation and shell · family `app-shell` · page `components/settings-subnav/` · phone `components/settings-subnav/m/`*

The second column inside Settings (Brand, Users and access, API keys, Rules, Audit log…). On the phone it is a plain list that drills in.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Heading | `.sh-sub-h` | 16px semibold. |
| 2 | Items | `.sb-item` | Menu item styling. |
| 3 | Phone list | `.sh-mlist / .sh-mrow` | 44px rows with chevrons. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Desktop column | `.sh-sub-h + items` |  |
| Phone list | `.sh-mlist` |  |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Muted label, icon at 80% opacity. | — |
| hover | Soft fill (`--muted`, or sidebar foreground at 6%). | `:hover · .is-hover` |
| focus | 2px ring outline (`--ring`, `--sidebar-ring` on navy). | `:focus-visible · .is-focus` |
| active | Current item: foreground colour, semibold, and its indicator. | `[aria-current=page] · .on` |
| disabled | Muted at 45%, not focusable, reason in a tooltip. | `[aria-disabled=true]` |
| error | Not applicable. | — |
| loading | Skeleton lines while the place's menu loads. | `.skel` |
| empty | Not applicable: navigation is never empty. | — |
| read-only | Not applicable. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| font-size | 16px | `var(--text-section)` |
| font-weight | 600 | `var(--w-semibold)` |
| color | #FBFBF9 | `hsl(var(--sidebar-accent-foreground))` |
| padding | 6px 10px 8px | `6px 10px 8px` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--text-section` | `16px` | 16px |
| `--w-semibold` | `600` | 600 |
| `--sidebar-accent-foreground` | `60 17% 98%  (#FBFBF9)` | #FBFBF9 |
| `--s-3` | `12px` | 12px |
| `--s-4` | `16px` | 16px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--text-read` | `15px` | 15px |
| `--w-medium` | `500` | 500 |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--warning-foreground` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--s-1` | `4px` | 4px |
| `--text-micro` | `11.5px` | 11.5px |

### Accessibility

- **Keyboard:** Links in order.
- **ARIA:** `<nav aria-label="Settings">`; `aria-current`.
- **Focus:** Ring.

### Content rules

- Same names as the page titles.

**Do**

- Keep HQ-only items out of brand settings.

**Don't**

- Tabs for settings sections.

### Build it

- **shadcn / Radix:** `SidebarMenuSub` or a simple nav list.
- **Exists in apps/admin today:** no · new: Sidebar. Not in apps/admin.
- **Rendering:** server component. 

```ts
interface SettingsNavProps { items: { href: string; label: string; hqOnly?: boolean }[]; current: string }
```

### Used on

[Appointments](../../phase-3/pages/appointments/), [Content](../../phase-3/pages/content-frame/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [Releases](../../phase-3/pages/releases/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="top-bar"></a>
## Top bar

*Navigation and shell · family `app-shell` · page `components/top-bar/` · phone `components/top-bar/m/`*

56px, white at 86% with a 10px blur, sticky. Breadcrumbs on the left; on the right the environment badge, the "?" help trigger (same spot on every page) and the account button.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Bar | `.topbar` | 56px, 20px side padding, hairline below, blur 10px, z 2. |
| 2 | Breadcrumbs | `.crumbs` | See Breadcrumbs. |
| 3 | Right cluster | `.top-r` | 10px gap: env badge, help "?", account. |
| 4 | Staging banner (optional) | `.sh-banner` | Amber strip under the bar on staging. |
| 5 | Access notice (optional) | `.sh-notice` | "You are viewing RJ MTM as HQ". |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Desktop | `.topbar` |  |
| Phone bar | `.p-bar` | 52px: place + title + env + ⋯. |
| With banner | `.topbar + .sh-banner` | Staging copy. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Muted label, icon at 80% opacity. | — |
| hover | Soft fill (`--muted`, or sidebar foreground at 6%). | `:hover · .is-hover` |
| focus | 2px ring outline (`--ring`, `--sidebar-ring` on navy). | `:focus-visible · .is-focus` |
| active | Current item: foreground colour, semibold, and its indicator. | `[aria-current=page] · .on` |
| disabled | Muted at 45%, not focusable, reason in a tooltip. | `[aria-disabled=true]` |
| error | Not applicable. | — |
| loading | Skeleton lines while the place's menu loads. | `.skel` |
| empty | Not applicable: navigation is never empty. | — |
| read-only | Not applicable. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| height | 56px | `var(--topbar-h)` |
| gap | 12px | `var(--s-3)` |
| padding | 0 20px | `0 var(--s-5)` |
| background | #FFFFFF @0.86 | `hsl(var(--background)/.86)` |
| height desktop / phone | 56 / 52px | `--topbar-h / --pbar-h` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--topbar-h` | `56px` | 56px |
| `--s-3` | `12px` | 12px |
| `--s-5` | `20px` | 20px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--blur-glass` | `10px` | 10px |
| `--pbar-h` | `52px` | 52px |
| `--w-semibold` | `600` | 600 |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--control-h-m` | `44px` | 44px |
| `--font-stretch-title` | `100%` | 100% |
| `--r-chip` | `4px` | 4px |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--s-2` | `8px` | 8px |
| `--text-small` | `12px` | 12px |
| `--warning` | `36 79% 29%  (#845610)` | #845610 |
| `--s-1` | `4px` | 4px |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--r-pill` | `999px` | 999px |
| `--motion-focus` | `140ms var(--ease-standard)` | 140ms cubic-bezier(.2,0,0,1) |
| `--secondary-border` | `219 14% 79%  (#C2C7D1)` | #C2C7D1 |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--text-card` | `13.5px` | 13.5px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |

### Accessibility

- **Keyboard:** Order: crumbs, env (not focusable), help, account.
- **ARIA:** `<header>` landmark; help trigger `aria-expanded` + `aria-controls` the drawer.
- **Focus:** Ring on each control.

### Content rules

- Banner copy: "Engineers' staging copy. Orders here are not real."

**Do**

- Keep "?" in the same place on every page.

**Don't**

- Page actions in the top bar; they belong in the page header.

### Build it

- **shadcn / Radix:** Layout; composes `Breadcrumb`, `Badge`, `Button`, `DropdownMenu`.
- **Exists in apps/admin today:** no. apps/admin has a header in its shell.
- **Rendering:** server component. 

```ts
interface TopBarProps {
  crumbs: Crumb[];
  environment: "staging" | "production" | "local";
  onHelp: () => void;
  helpOpen: boolean;
  user: { name: string; initials: string };
  banner?: React.ReactNode;
}
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="breadcrumbs"></a>
## Breadcrumbs

*Navigation and shell · family `app-shell` · page `components/breadcrumbs/` · phone `components/breadcrumbs/m/`*

Where you are: place › section › record. Archivo 13.5px muted with the current page in semibold foreground. Phone shows only a back link to the parent.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Trail | `.crumbs` | 8px gap, chevron separators. |
| 2 | Ancestor | `a` | Muted link. |
| 3 | Current | `b` | Foreground semibold, not a link. |
| 4 | Phone back | `.sh-back / .sh-pcrumb` | "‹ Orders". |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Desktop | `.crumbs` |  |
| Truncated | `.crumbs + …` | Middle crumbs collapse into a menu past 4 levels. |
| Phone | `.sh-back` | Back to parent. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Muted label, icon at 80% opacity. | — |
| hover | Soft fill (`--muted`, or sidebar foreground at 6%). | `:hover · .is-hover` |
| focus | 2px ring outline (`--ring`, `--sidebar-ring` on navy). | `:focus-visible · .is-focus` |
| active | Current page: `b`, `aria-current="page"`. | — |
| disabled | Muted at 45%, not focusable, reason in a tooltip. | `[aria-disabled=true]` |
| error | Not applicable. | — |
| loading | Skeleton lines while the place's menu loads. | `.skel` |
| empty | Not applicable: navigation is never empty. | — |
| read-only | Not applicable. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 8px | `var(--s-2)` |
| color | #505B71 | `hsl(var(--muted-foreground))` |
| font | 13.5px/1 'Archivo', system-ui, sans-serif | `var(--text-card)/1 var(--font-voice)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--s-2` | `8px` | 8px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--text-card` | `13.5px` | 13.5px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--w-semibold` | `600` | 600 |
| `--font-stretch-title` | `100%` | 100% |
| `--s-1` | `4px` | 4px |
| `--text-small` | `12px` | 12px |
| `--sidebar-foreground` | `222 19% 82%  (#C8CEDA)` | #C8CEDA |
| `--r-control` | `6px` | 6px |
| `--r-chip` | `4px` | 4px |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--s-3` | `12px` | 12px |
| `--s-4` | `16px` | 16px |
| `--w-medium` | `500` | 500 |

### Accessibility

- **Keyboard:** Links in order.
- **ARIA:** `<nav aria-label="Breadcrumb">` + `<ol>`; separators `aria-hidden`.
- **Focus:** Ring on links.

### Content rules

- Record crumbs use the record name or code ("RJC-20418").

**Do**

- Start at the place (RJ Clothing).

**Don't**

- Repeat the page title as a link.

### Build it

- **shadcn / Radix:** shadcn `Breadcrumb`.
- **Exists in apps/admin today:** no · new: Breadcrumb. Not in apps/admin/ui.
- **Rendering:** server component. 
- **Install:** `npx shadcn@latest add breadcrumb`

```ts
interface Crumb { label: string; href?: string }   // last crumb has no href
interface BreadcrumbsProps { items: Crumb[] }
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="place-switcher"></a>
## Place switcher

*Navigation and shell · family `app-shell` · page `components/place-switcher/` · phone `components/place-switcher/m/`*

Switch between RJ HQ and each brand. The place card in the sidebar opens a searchable list with monogram tiles (HQ blue, DTC navy, B2B slate). Phone: a bottom sheet.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Trigger | `.sh-place` | Place card: monogram, name, scope line, chevrons. |
| 2 | Monogram | `.sh-mono` | 30px tile; `.hq`, `.br`, `.l-dtc`, `.l-b2b`, `.l-err`. |
| 3 | Panel | `.menu / .sh-pop` | Search, HQ row, brands list, footer "Create a brand" (HQ only). |
| 4 | Row | `.sh-row / .mi` | Monogram + name + channel; tick on current. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| HQ user | `all places` | HQ + every brand. |
| Brand user | `own brands` | Only assigned brands. |
| Phone | `.k-sheet.k-bottom` | Sheet with 44px rows. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Muted label, icon at 80% opacity. | — |
| hover | Soft fill (`--muted`, or sidebar foreground at 6%). | `:hover · .is-hover` |
| focus | 2px ring outline (`--ring`, `--sidebar-ring` on navy). | `:focus-visible · .is-focus` |
| active | Current item: foreground colour, semibold, and its indicator. | `[aria-current=page] · .on` |
| disabled | Unresolved brand row: "Unresolved brand name" disabled with reason. | `[aria-disabled=true]` |
| error | Failed to load places: error monogram + Retry. | `.sh-place.err` |
| loading | Skeleton monogram + lines. | `.sh-skel-mono` |
| empty | No search match: "No place matches 'mtn'". | `.sh-pop-msg` |
| read-only | Not applicable. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 10px | `10px` |
| padding | 6px 8px | `6px 8px` |
| border-radius | 6px | `var(--r-control)` |
| color | inherit | `inherit` |
| width | 100% | `100%` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--r-control` | `6px` | 6px |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--sidebar-foreground` | `222 19% 82%  (#C8CEDA)` | #C8CEDA |
| `--sidebar-ring` | `221 54% 75%  (#9DB3E2)` | #9DB3E2 |
| `--w-semibold` | `600` | 600 |
| `--text-card` | `13.5px` | 13.5px |
| `--sidebar-accent-foreground` | `60 17% 98%  (#FBFBF9)` | #FBFBF9 |
| `--text-micro` | `11.5px` | 11.5px |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--destructive-foreground` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--w-bold` | `700` | 700 |
| `--sidebar-primary` | `221 54% 75%  (#9DB3E2)` | #9DB3E2 |
| `--sidebar-primary-foreground` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--text-9-5` | `9.5px` | 9.5px |
| `--r-chip` | `4px` | 4px |
| `--info` | `218 49% 39%  (#335694)` | #335694 |
| `--info-foreground` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--primary-foreground` | `60 17% 98%  (#FBFBF9)` | #FBFBF9 |
| `--neutral` | `222 13% 39%  (#575E70)` | #575E70 |
| `--neutral-foreground` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--s-2` | `8px` | 8px |
| `--s-3` | `12px` | 12px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--motion-caret` | `1.1s` | 1.1s |
| `--ease-blink` | `steps(1)` | steps(1) |
| `--s-1` | `4px` | 4px |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--text-small` | `12px` | 12px |
| `--w-medium` | `500` | 500 |
| `--s-6` | `24px` | 24px |
| `--s-4` | `16px` | 16px |
| `--text-body` | `13px` | 13px |
| `--z-pop` | `20` | 20 |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--s-5` | `20px` | 20px |
| `--text-18` | `18px` | 18px |
| `--font-ui` | `'IBM Plex Sans', system-ui, sans-serif` | 'IBM Plex Sans', system-ui, sans-serif |

### Accessibility

- **Keyboard:** Enter opens; typing filters; ↑ ↓; Enter switches.
- **ARIA:** Combobox semantics (cmdk) in a Popover.
- **Focus:** Ring `--sidebar-ring` on navy.

### Content rules

- Count reflects reality: 3 brands.

**Do**

- Keep the current place ticked.

**Don't**

- Hide HQ from HQ users.

### Build it

- **shadcn / Radix:** `Popover` + `Command`; phone `Drawer`.
- **Exists in apps/admin today:** no · new: Popover, Command. shell/brand-picker exists (uses radix popover + cmdk directly).
- **Rendering:** client component (`"use client"`). 

```ts
interface PlaceSwitcherProps {
  current: Place;
  places: (Place & { monogram: string; disabledReason?: string })[];
  onSwitch: (place: Place) => void;
  canCreateBrand?: boolean;
  state?: "ready" | "loading" | "error";
}
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [Sign in](../../phase-3/pages/sign-in/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="command-palette"></a>
## Command palette

*Navigation and shell · family `app-shell` · page `components/command-palette/` · phone `components/command-palette/m/`*

⌘K "Jump to…": search pages, products, orders and actions from anywhere. Opens over the shell; groups results; Enter goes. Phone: full-screen Jump to.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Trigger | `.sh-jump` | In the sidebar (navy) or toolbar (`.l`), with ⌘K key cap. |
| 2 | Dialog | `.sh-cmd` | Centred panel over a scrim. |
| 3 | Input | `.sh-cmd-in` | Large search row. |
| 4 | Groups and items | `.mlabel + .mi` | Pages, Products, Orders, Actions. |
| 5 | Footer | `.sh-cmd-f` | ↑↓ to move, Enter to open, Esc to close. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Desktop | `.sh-cmd` |  |
| Phone | `.k-sheet.k-full` | Full-screen Jump to. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Closed: nothing rendered in the flow. | — |
| hover | Not applicable to the surface; its buttons and rows hover as usual. | — |
| focus | Open: focus moves inside (`[data-k-autofocus]` first), Tab is trapped, Escape closes and returns focus. | `[data-k-stage].open` |
| active | Open, with the `--motion-*-in` entrance. | `.open` |
| disabled | The opener is disabled, never the overlay. | — |
| error | The body shows an inline `.sf-alert.danger`; the overlay stays open. | `.sf-alert.danger` |
| loading | Spinner in the input while searching records. | `.spin` |
| empty | Nothing typed: recent pages; no results: "No match for 'navy chalk'". | — |
| read-only | Footer drops the primary and keeps Close. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| width | 560px | `560px` |
| background | #FFFFFF | `hsl(var(--popover))` |
| border | 1px solid #E3E7ED | `1px solid hsl(var(--border))` |
| border-radius | 15px | `var(--r-frame)` |
| box-shadow | 0 24px 64px -20px rgba(14,23,41,.40) | `var(--e-dialog)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--s-2` | `8px` | 8px |
| `--r-control` | `6px` | 6px |
| `--sidebar-foreground` | `222 19% 82%  (#C8CEDA)` | #C8CEDA |
| `--w-medium` | `500` | 500 |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--sidebar-ring` | `221 54% 75%  (#9DB3E2)` | #9DB3E2 |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--input` | `219 14% 59%  (#8892A5)` | #8892A5 |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--focus-ring` | `0 0 0 1px hsl(var(--ring)), 0 0 0 4px hsl(var(--ring) / .14)` | 0 0 0 1px #3F60A6, 0 0 0 4px #3F60A6 @0.14 |
| `--popover` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--r-frame` | `15px` | 15px |
| `--e-dialog` | `0 24px 64px -20px rgba(14,23,41,.40)` | 0 24px 64px -20px rgba(14,23,41,.40) |
| `--s-4` | `16px` | 16px |
| `--text-section` | `16px` | 16px |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--text-small` | `12px` | 12px |

### Accessibility

- **Keyboard:** ⌘K / Ctrl+K toggles; ↑ ↓; Enter; Escape.
- **ARIA:** cmdk combobox + listbox inside a Dialog.
- **Focus:** Input focused on open; focus returns to where it was.

### Content rules

- Placeholder "Jump to a page, product or order".

**Do**

- Include actions (Create product).

**Don't**

- Duplicate the whole nav as the only content.

### Build it

- **shadcn / Radix:** shadcn `CommandDialog` (cmdk).
- **Exists in apps/admin today:** Dialog · new: Command. shell/command-palette.tsx exists (cmdk directly); `command.tsx` wrapper new.
- **Rendering:** client component (`"use client"`). 
- **Install:** `npx shadcn@latest add command`

```ts
interface CommandPaletteProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  groups: { heading: string; items: { id: string; label: string; icon?: LucideIcon; href?: string; onRun?: () => void; meta?: string }[] }[];
  onSearch?: (q: string) => Promise<void>;
  recent?: { label: string; href: string }[];
}
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="tab-bar"></a>
## Phone tab bar

*Navigation and shell · family `app-shell` · page `components/tab-bar/` · phone `components/tab-bar/m/`*

Phone only: four tabs at the bottom (the place's top destinations, then Menu), 62px with the safe area, frosted white. Menu opens the full navigation drawer.

> **Open in the kit.** `--tabbar-h` is declared but not yet applied to `.tabbar`; the 10.5px label is below the 11.5px floor (KIT-CHANGELOG, Open).

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Bar | `.tabbar` | Sticky bottom, 4 columns, white 96% + 8px blur, hairline top, 6px 0 10px padding. |
| 2 | Tab | `.tabbar a` | 46px, icon over 10.5px Archivo label. |
| 3 | Current | `.tabbar a.on` | `--primary` colour. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Brand | `Home · Products · Orders · Menu` |  |
| HQ | `Home · Global Store · Brands · Menu` |  |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Muted label, icon at 80% opacity. | — |
| hover | Touch: no hover. | — |
| focus | 2px ring outline (`--ring`, `--sidebar-ring` on navy). | `:focus-visible · .is-focus` |
| active | Current item: foreground colour, semibold, and its indicator. | `[aria-current=page] · .on` |
| disabled | Muted at 45%, not focusable, reason in a tooltip. | `[aria-disabled=true]` |
| error | Not applicable. | — |
| loading | Skeleton lines while the place's menu loads. | `.skel` |
| empty | Not applicable: navigation is never empty. | — |
| read-only | Not applicable. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| grid-template-columns | repeat(4,1fr) | `repeat(4,1fr)` |
| background | #FFFFFF @0.96 | `hsl(var(--background)/.96)` |
| padding | 6px 0 10px | `6px 0 10px` |
| height | 62px incl. padding | `--tabbar-h` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--actbar-blur` | `8px` | 8px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--text-10-5` | `10.5px` | 10.5px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--font-stretch-title` | `100%` | 100% |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--r-control` | `6px` | 6px |
| `--r-pill` | `999px` | 999px |
| `--primary-foreground` | `60 17% 98%  (#FBFBF9)` | #FBFBF9 |
| `--w-semibold` | `600` | 600 |
| `--text-10` | `10px` | 10px |
| `--font-ui` | `'IBM Plex Sans', system-ui, sans-serif` | 'IBM Plex Sans', system-ui, sans-serif |
| `--warning` | `36 79% 29%  (#845610)` | #845610 |

### Accessibility

- **Keyboard:** Links.
- **ARIA:** `<nav aria-label="Main">`, `aria-current`.
- **Focus:** Ring.

### Content rules

- One-word labels.

**Do**

- Hide inside guided flows (wizard focus mode).

**Don't**

- More than four tabs + Menu.

### Build it

- **shadcn / Radix:** Custom component (no shadcn equivalent).
- **Exists in apps/admin today:** no. Not in apps/admin.
- **Rendering:** server component. 

```ts
interface TabBarProps { items: { href: string; label: string; icon: LucideIcon }[]; current: string; onMenu: () => void }
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="tabs"></a>
## Tabs

*Navigation and shell · family `page-primitives` · page `components/tabs/` · phone `components/tabs/m/`*

Kit v2 tab strip for page or section navigation (customer Overview / Addresses / Fit quiz / GDPR). 40px tall, a 2px navy underline on the current tab, counts in `em`. Scrolls sideways on the phone.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Strip | `.tabs` | Flex, 20px gap, hairline below. |
| 2 | Tab | `.tab` | 40px, Archivo 500 13px muted. |
| 3 | Indicator | `.tab.on::after` | 2px `--primary` underline. |
| 4 | Count | `.tab em` | 11.5px muted. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Panel tabs | `[role=tab] + [role=tabpanel]` | Switch panels in place. |
| Route tabs | `a.tab[aria-current=page]` | Each tab is a URL (?tab=). |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Muted label, icon at 80% opacity. | — |
| hover | Soft fill (`--muted`, or sidebar foreground at 6%). | `:hover · .is-hover` |
| focus | 2px ring outline (`--ring`, `--sidebar-ring` on navy). | `:focus-visible · .is-focus` |
| active | Current: foreground semibold + underline. | `.on · [aria-selected=true] · [aria-current=page]` |
| disabled | Muted at 45%, not focusable, reason in a tooltip. | `[aria-disabled=true]` |
| error | Not applicable. | — |
| loading | Skeleton lines while the place's menu loads. | `.skel` |
| empty | Not applicable: navigation is never empty. | — |
| read-only | Not applicable. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 20px | `var(--s-5)` |
| height desktop / phone | 40 / 44px | `--tab-h / --tab-h-m` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--s-5` | `20px` | 20px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--s-2` | `8px` | 8px |
| `--tab-h` | `40px` | 40px |
| `--w-medium` | `500` | 500 |
| `--text-body` | `13px` | 13px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--w-semibold` | `600` | 600 |
| `--r-pill` | `999px` | 999px |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--r-chip` | `4px` | 4px |
| `--text-micro` | `11.5px` | 11.5px |
| `--font-ui` | `'IBM Plex Sans', system-ui, sans-serif` | 'IBM Plex Sans', system-ui, sans-serif |
| `--s-4` | `16px` | 16px |
| `--tab-h-m` | `44px` | 44px |

### Accessibility

- **Keyboard:** ← → move between tabs (kit.js / Radix); Tab moves into the panel.
- **ARIA:** `role="tablist"` / `tab` / `tabpanel`, `aria-controls`, `aria-selected`.
- **Focus:** 2px ring, 4px radius.

### Content rules

- Nouns; counts only if useful.

**Do**

- Use route tabs when each tab deserves a URL.

**Don't**

- Use tabs to switch a control mode: that is Segmented.

### Build it

- **shadcn / Radix:** shadcn `Tabs` (Radix) restyled as an underline strip.
- **Exists in apps/admin today:** no · new: Tabs. Not in apps/admin.
- **Rendering:** client component (`"use client"`). 
- **Install:** `npx shadcn@latest add tabs`

```ts
interface TabsNavProps<V extends string> {
  tabs: { value: V; label: string; count?: number; href?: string }[];
  value: V;
  onValueChange?: (v: V) => void;   // omit when tabs are links
}
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [API Access](../../phase-3/pages/super-api-access/), [Cloth library](../../phase-3/pages/super-cloths/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="page-header"></a>
## Page header

*Navigation and shell · family `base` · page `components/page-header/` · phone `components/page-header/m/`*

Title, one line of context and the ranked actions: tertiary, secondary, then the one primary at the end. 26px Archivo 600 title. On the phone the primary moves to the action bar.

> **Open in the kit.** `.page` 22px and `.ph` 18px are off the 4px grid (board 3 literals kept; fix in the build lane).

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Head | `.ph` | Flex, bottom-aligned, 16px gap, 18px below. |
| 2 | Title | `.page-title` | Archivo 600 26px / 1.15, −.015em. |
| 3 | Context | `.ph p` | Muted 13px. |
| 4 | Actions | `.ph-r` | 8px gap, ranked left to right. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| List page | `.ph` | Count chip beside the title. |
| Phone | `.p-head` | 24px title; actions move out. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Holds its children at the documented spacing. | — |
| hover | Not applicable. | — |
| focus | Not applicable; children carry focus. | — |
| active | Not applicable. | — |
| disabled | Not applicable. | — |
| error | Children report errors; the container may add a summary at the top. | — |
| loading | Children show skeletons in place. | `.skel` |
| empty | Shows the compact empty state inside. | `.sf-empty.compact` |
| read-only | Children render as values, not inputs. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 16px | `var(--s-4)` |
| title | 26px / 1.15 / −.015em (24px phone) | `--text-page / --lh-page / --tr-page` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--s-6` | `24px` | 24px |
| `--s-4` | `16px` | 16px |
| `--title-w` | `600` | 600 |
| `--text-page` | `26px` | 26px |
| `--lh-page` | `1.15` | 1.15 |
| `--tr-page` | `-.015em` | -.015em |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--s-1` | `4px` | 4px |
| `--s-2` | `8px` | 8px |
| `--s-3` | `12px` | 12px |
| `--text-24` | `24px` | 24px |

### Accessibility

- **Keyboard:** Actions follow the title in tab order.
- **ARIA:** One `h1` per page.
- **Focus:** —

### Content rules

- Title = nav label.
- Context line: a fact (count, last sync), not instructions.

**Do**

- One primary.

**Don't**

- Help text in the header: use page help.

### Build it

- **shadcn / Radix:** Layout component.
- **Exists in apps/admin today:** no. Pages have ad-hoc headers.
- **Rendering:** server component. 

```ts
interface PageHeaderProps { title: string; count?: number; description?: string; actions?: React.ReactNode }
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="record-header"></a>
## Record header

*Navigation and shell · family `page-primitives` · page `components/record-header/` · phone `components/record-header/m/`*

Kit v2 header for detail pages (customer, tailor, fit review, brand): avatar, name with status chips, a meta row and ranked actions.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Avatar | `.k-rec>.avatar` | 44px (40px phone). |
| 2 | Title row | `.k-rec-t` | Page title + chips, wraps. |
| 3 | Meta | `.k-rec-m` | 12px muted facts with icons, 4px × 16px gaps. |
| 4 | Actions | `.k-rec-a` | Right; full width row on the phone. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Person | `avatar initials` | Customer, tailor, staff. |
| Brand | `monogram` | Brand detail. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Holds its children at the documented spacing. | — |
| hover | Not applicable. | — |
| focus | Not applicable; children carry focus. | — |
| active | Not applicable. | — |
| disabled | Not applicable. | — |
| error | Children report errors; the container may add a summary at the top. | — |
| loading | Children show skeletons in place. | `.skel` |
| empty | Shows the compact empty state inside. | `.sf-empty.compact` |
| read-only | Children render as values, not inputs. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 16px | `var(--s-4)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--s-4` | `16px` | 16px |
| `--s-5` | `20px` | 20px |
| `--text-card` | `13.5px` | 13.5px |
| `--s-2` | `8px` | 8px |
| `--s-1` | `4px` | 4px |
| `--text-small` | `12px` | 12px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--s-3` | `12px` | 12px |

### Accessibility

- **Keyboard:** Actions in order.
- **ARIA:** Title is the `h1`.
- **Focus:** —

### Content rules

- Meta: since, location, counts ("Customer since 2023 · 3 orders").

**Do**

- Status chips right after the name.

**Don't**

- More than one primary.

### Build it

- **shadcn / Radix:** Layout using `Avatar` + `Badge` + `Button`.
- **Exists in apps/admin today:** Badge · new: Avatar. Badge exists; Avatar new.
- **Rendering:** server component. 

```ts
interface RecordHeaderProps {
  name: string;
  initials?: string;
  statuses?: { status: Status; label: string }[];
  meta?: { icon?: LucideIcon; label: string }[];
  actions?: React.ReactNode;
}
```

### Used on

Kit v2 primitive: customer, tailor, fit-review and brand detail still compose `.ph` + `.avatar` + `.chip` locally (KIT-CHANGELOG v2-10); swap in the build lane.

---

<a id="action-bar"></a>
## Phone action bar

*Navigation and shell · family `page-primitives` · page `components/action-bar/` · phone `components/action-bar/m/`*

Kit v2: the page's one primary in thumb reach, sticky above the tab bar. Selecting rows swaps it to a selection bar ("3 selected" + bulk action).

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Bar | `.k-actbar` | Sticky `bottom: var(--tabbar-h)`, white 96% + blur 8px, 12px 16px padding. |
| 2 | Primary | `.btn` | Flex 1, 48px, 15px label. |
| 3 | Selection message | `.k-selmsg` | "3 selected" in tabular Archivo. |
| 4 | Swap slots | `[data-k-when=sel\|idle]` | Buttons for each mode. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Idle | `.k-actbar` | One primary (+ one secondary). |
| Selection | `.k-actbar.sel` | Count + bulk action. |
| No tab bar | `.k-actbar.no-tabbar` | Inside guided flows. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Holds its children at the documented spacing. | — |
| hover | Not applicable. | — |
| focus | Not applicable; children carry focus. | — |
| active | Selection mode. | `.sel` |
| disabled | Not applicable. | — |
| error | Children report errors; the container may add a summary at the top. | — |
| loading | Children show skeletons in place. | `.skel` |
| empty | Shows the compact empty state inside. | `.sf-empty.compact` |
| read-only | Children render as values, not inputs. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 8px | `var(--s-2)` |
| padding | 12px 16px | `var(--s-3) var(--s-4)` |
| background | #FFFFFF @0.96 | `hsl(var(--background)/.96)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--tabbar-h` | `62px` | 62px |
| `--z-sticky` | `3` | 3 |
| `--s-2` | `8px` | 8px |
| `--s-3` | `12px` | 12px |
| `--s-4` | `16px` | 16px |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--actbar-blur` | `8px` | 8px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--control-h-m-lg` | `48px` | 48px |
| `--text-btn-lg` | `15px` | 15px |
| `--w-semibold` | `600` | 600 |
| `--text-card` | `13.5px` | 13.5px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |

### Accessibility

- **Keyboard:** —
- **ARIA:** Selection count in `aria-live="polite"`.
- **Focus:** Bar never covers the focused field (scroll padding = bar height).

### Content rules

- Same label as the desktop primary.

**Do**

- Keep it to two buttons at most.

**Don't**

- Duplicate the primary in the page body.

### Build it

- **shadcn / Radix:** Custom component.
- **Exists in apps/admin today:** no. Not in apps/admin.
- **Rendering:** client component (`"use client"`). 

```ts
interface ActionBarProps {
  primary: React.ReactNode;
  secondary?: React.ReactNode;
  selection?: { count: number; actions: React.ReactNode; onClear: () => void };
  aboveTabBar?: boolean;            // default true
}
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Content](../../phase-3/pages/content-frame/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [RJC-20418](../../phase-3/pages/order-detail/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Rules](../../phase-3/pages/settings-rules/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="list-row"></a>
## Phone list row

*Navigation and shell · family `page-primitives` · page `components/list-row/` · phone `components/list-row/m/`*

Kit v2: the phone's replacement for a table row when there is no thumbnail (campaigns, rules, tickets, releases): title, meta line, chips, and a trailing slot for a figure or date.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Row | `.k-li` | Min 72px, 12px 16px padding, hairline below, full-width button or link. |
| 2 | Title | `b` | Archivo 600 15px, one line, ellipsis. |
| 3 | Meta | `.m` | 12px muted, one line. |
| 4 | Chips | `.s` | Status chips, 8px gap. |
| 5 | Trailing | `.r` | Right column: money, date, chevron (tabular). |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Link row | `a.k-li` | Opens the record. |
| Selectable | `.k-li.is-sel` | In selection mode. |
| With thumbnail | `data-table / cards-gallery list` | Use the family list row (`.dt-li`, `.cg-li`). |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Rows and cards that open something take `--muted` or lift; static blocks do not react. | `:hover` |
| focus | 2px ring inset (−2px offset). | `:focus-visible` |
| active | Pressed / selected: `--muted` / `--accent`. | `:active · .is-sel` |
| disabled | Not applicable: hide data the person cannot see, never grey it. | — |
| error | Names the real cause and offers Try again (see Error states). | `.sf-alert.danger` |
| loading | `.skel` shapes in the final layout, pulsing on `--motion-skeleton`. | `.skel` |
| empty | Says what is missing and the one next step (see Empty state). | `.sf-empty` |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 12px | `var(--s-3)` |
| width | 100% | `100%` |
| min-height | 72px | `var(--li-min-h)` |
| padding | 12px 16px | `var(--s-3) var(--s-4)` |
| border | 0 | `0` |
| background | transparent | `transparent` |
| color | inherit | `inherit` |
| font | inherit | `inherit` |
| min height | 72px | `--li-min-h` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--s-4` | `16px` | 16px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--s-3` | `12px` | 12px |
| `--li-min-h` | `72px` | 72px |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--w-semibold` | `600` | 600 |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--text-small` | `12px` | 12px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--s-2` | `8px` | 8px |
| `--s-1` | `4px` | 4px |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |

### Accessibility

- **Keyboard:** Rows are links or buttons.
- **ARIA:** List `role="list"`; selection rows `aria-selected`.
- **Focus:** Inset ring.

### Content rules

- Title = the record name; meta = the most useful secondary fact.

**Do**

- Keep one tap target per row.

**Don't**

- Tables on the phone.

### Build it

- **shadcn / Radix:** Custom component.
- **Exists in apps/admin today:** no. Not in apps/admin.
- **Rendering:** server component. 

```ts
interface ListRowProps {
  href?: string;
  title: string;
  meta?: string;
  statuses?: { status: Status; label: string }[];
  trailing?: React.ReactNode;
  selected?: boolean;
  onSelect?: () => void;
}
```

### Used on

[System status · Analytics](../../phase-3/pages/analytics-system-status/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Fit review](../../phase-3/pages/fit-review/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Releases](../../phase-3/pages/releases/), [API keys](../../phase-3/pages/settings-api-keys/), [Brands](../../phase-3/pages/settings-brands/), [Users & access](../../phase-3/pages/settings-users/), [API Access](../../phase-3/pages/super-api-access/), [Cloth library](../../phase-3/pages/super-cloths/), [Inbox](../../phase-3/pages/support/), [Support templates](../../phase-3/pages/support-templates/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Tailor Roster](../../phase-3/pages/tailors/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/)

---

<a id="payload-frame"></a>
## Embedded Payload CMS frame

*Navigation and shell · family `app-shell` · page `components/payload-frame/` · phone `components/payload-frame/m/`*

The Content area embeds the Payload CMS admin in a frame under the admin's own header. States for loaded, loading, error, empty and not wired.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Frame header | `.sh-frame-h` | Title + Open in new tab. |
| 2 | Frame body | `.sh-frame-b` | The iframe area. |
| 3 | State overlay | `.sf-* inside` | Loading skeleton, error, not wired. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Loaded | `iframe` |  |
| Not wired | `.sf-alert.wired` | Payload not configured for this brand. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Holds its children at the documented spacing. | — |
| hover | Not applicable. | — |
| focus | Not applicable; children carry focus. | — |
| active | Not applicable. | — |
| disabled | Not applicable. | — |
| error | "Payload did not load: …" + Try again + Open in new tab. | — |
| loading | Skeleton blocks in the frame. | `.skel` |
| empty | No content collections yet. | — |
| read-only | Children render as values, not inputs. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| border | 1px solid #E3E7ED | `1px solid hsl(var(--border))` |
| border-radius | 8px | `var(--r-card)` |
| background | #FFFFFF | `hsl(var(--background))` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--r-card` | `8px` | 8px |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--s-2` | `8px` | 8px |
| `--s-3` | `12px` | 12px |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--text-card` | `13.5px` | 13.5px |
| `--w-semibold` | `600` | 600 |
| `--s-1` | `4px` | 4px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--s-6` | `24px` | 24px |
| `--text-body` | `13px` | 13px |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--r-pill` | `999px` | 999px |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--warning` | `36 79% 29%  (#845610)` | #845610 |
| `--focus-ring` | `0 0 0 1px hsl(var(--ring)), 0 0 0 4px hsl(var(--ring) / .14)` | 0 0 0 1px #3F60A6, 0 0 0 4px #3F60A6 @0.14 |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |

### Accessibility

- **Keyboard:** Iframe is one tab stop; Open in new tab before it.
- **ARIA:** `<iframe title="Payload CMS">`.
- **Focus:** —

### Content rules

- Name Payload explicitly.

**Do**

- Offer Open in new tab.

**Don't**

- Nest the admin shell inside Payload.

### Build it

- **shadcn / Radix:** `<iframe>` in a Card.
- **Exists in apps/admin today:** no. ContentEmbedPage exists in apps/admin.
- **Rendering:** client component (`"use client"`). 

```ts
interface ContentFrameProps { src: string; title: string; state: "loading" | "ready" | "error" | "notWired"; onRetry?: () => void }
```

### Used on

[Content](../../phase-3/pages/content-frame/)

---

<a id="data-table"></a>
## Data table

*Data table · family `data-table` · page `components/data-table/` · phone `components/data-table/m/`*

The admin's main list: a white card with a muted 36px header row and 44px body rows in Plex 13px, typed columns (product, cloth, status, money, date, code, version) and a row menu. Wide tables freeze their first columns and scroll sideways.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Wrap | `.dt-wrap` | Card: 1px `--border`, 8px radius. |
| 2 | Scroll | `.dt-scroll` | Horizontal scroll inside the card. |
| 3 | Header row | `.dt th / .th` | 36px, `--muted` ground, Archivo 600 11.5px muted, sortable. |
| 4 | Body row | `.dt td` | 44px (36px compact), 1px hairline, Plex 13px. |
| 5 | Product cell | `.dt-prod` | Thumb + name + handle (mono). |
| 6 | Money cell | `.dt-money` | Right-aligned, tabular; other currencies under it. |
| 7 | Row menu | `.dt-ra` | Edit + ⋯ at the end of the row. |
| 8 | Phone list | `.dt-list / .dt-li` | Each row becomes a list card with thumb, title, chips and figure. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Standard | `.dt` | Global Store, products, customers. |
| Wide + frozen | `.dt-frozen .fz` | Orders: checkbox and order columns stick; edge shade `--sticky-edge` while scrolled. |
| Phone list | `.dt-list` | Lists, never a squeezed table. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Row `--muted` (solid on frozen cells). | `tr:hover · .is-hover` |
| focus | Row inset 1px `--ring` top and bottom, 2px on the left. | `tr.is-focus` |
| active | Selected row `--accent`. | `tr.is-sel` |
| disabled | Off rows (taken offline): muted text, thumb at 60%. | `tr.is-off` |
| error | Names the real cause and offers Try again (see Error states). | `.sf-alert.danger` |
| loading | `.skel` shapes in the final layout, pulsing on `--motion-skeleton`. | `.skel` |
| empty | Says what is missing and the one next step (see Empty state). | `.sf-empty` |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| background | #FFFFFF | `hsl(var(--card))` |
| border | 1px solid #E3E7ED | `1px solid hsl(var(--border))` |
| border-radius | 8px | `var(--r-card)` |
| row / compact / header | 44 / 36 / 36px | `--row / --row-compact / --row-th` |
| frozen edge shade | foreground at 9% | `--sticky-edge` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--card` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--r-card` | `8px` | 8px |
| `--text-body` | `13px` | 13px |
| `--row-th` | `36px` | 36px |
| `--s-3` | `12px` | 12px |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--text-small` | `12px` | 12px |
| `--w-semibold` | `600` | 600 |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--row` | `44px` | 44px |
| `--s-2` | `8px` | 8px |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--motion-sheet-in` | `260ms var(--ease-in)` | 260ms cubic-bezier(.22,1,.36,1) |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--e-row-focus` | `inset 0 1px 0 hsl(var(--ring)),inset 0 -1px 0 hsl(var(--ring))` | inset 0 1px 0 #3F60A6,inset 0 -1px 0 #3F60A6 |
| `--e-row-focus-first` | `inset 2px 0 0 hsl(var(--ring)),inset 0 1px 0 hsl(var(--ring)),inset 0 -1px 0 hsl(var(--ring))` | inset 2px 0 0 #3F60A6,inset 0 1px 0 #3F60A6,inset 0 -1px 0 #3F60A6 |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--w-medium` | `500` | 500 |
| `--w-regular` | `400` | 400 |
| `--font-code` | `'IBM Plex Mono', ui-monospace, monospace` | 'IBM Plex Mono', ui-monospace, monospace |
| `--r-media` | `4px` | 4px |
| `--studio` | `radial-gradient(120% 90% at 50% 18%, #f7f8fa 0%, #e3e7ee 80%)` | radial-gradient(120% 90% at 50% 18%, #f7f8fa 0%, #e3e7ee 80%) |
| `--text-8` | `8px` | 8px |
| `--primary-foreground` | `60 17% 98%  (#FBFBF9)` | #FBFBF9 |
| `--text-code` | `12px` | 12px |
| `--s-1` | `4px` | 4px |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--warning` | `36 79% 29%  (#845610)` | #845610 |
| `--s-4` | `16px` | 16px |
| `--sticky-edge` | `hsl(var(--foreground) / .09)` | #0F1524 @0.09 |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--r-chip` | `4px` | 4px |
| `--text-8-5` | `8.5px` | 8.5px |
| `--text-9` | `9px` | 9px |
| `--row-compact` | `36px` | 36px |

### Accessibility

- **Keyboard:** Rows are not tab stops; the name link and row actions are.
- **Keyboard:** Optional grid navigation (↑ ↓) when rows are selectable.
- **ARIA:** Real `<table>` with `<th scope="col">`; sort state `aria-sort`.
- **ARIA:** Row checkboxes labelled with the row name.
- **Focus:** Ring inside the row, never clipped by the scroll container.

### Content rules

- Money: £1,150 in lists, two decimals only when not whole.
- Dates: 2 Oct, Yesterday, Today 14:20.
- Codes, handles, versions in Plex Mono.

**Do**

- Right-align numbers and money.
- Keep the first column the record name.

**Don't**

- Truncate money.
- Show a table on the phone.

### Build it

- **shadcn / Radix:** shadcn `Table` + TanStack Table (`@tanstack/react-table`) as a `DataTable` with toolbar, saved views, bulk bar and `Pagination`.
- **Exists in apps/admin today:** no · new: Table. No `table.tsx` and no @tanstack/react-table in apps/admin; both are new.
- **Rendering:** client component (`"use client"`). Sorting, selection and column state run in the client (TanStack).
- **Install:** `npx shadcn@latest add table && npm i @tanstack/react-table`

```ts
import type { ColumnDef, SortingState, RowSelectionState } from "@tanstack/react-table";

interface DataTableProps<T> {
  columns: ColumnDef<T>[];          // column meta: { type: "product" | "cloth" | "status" | "money" | "date" | "code" | "version" }
  data: T[];
  getRowId: (row: T) => string;
  density?: "comfortable" | "compact";
  frozenColumns?: number;           // sticky left columns
  sorting?: SortingState;
  onSortingChange?: (s: SortingState) => void;
  rowSelection?: RowSelectionState;
  onRowSelectionChange?: (s: RowSelectionState) => void;
  rowActions?: (row: T) => MenuItem[];
  onRowOpen?: (row: T) => void;
  state?: "ready" | "loading" | "empty" | "noResults" | "error" | "notWired";
  renderPhoneRow?: (row: T) => React.ReactNode;   // /m/ composition
}
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Campaigns](../../phase-3/pages/campaigns/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Orders](../../phase-3/pages/orders/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [Releases](../../phase-3/pages/releases/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Tailor Roster](../../phase-3/pages/tailors/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="table-sort"></a>
## Sortable header

*Data table · family `data-table` · page `components/table-sort/` · phone `components/table-sort/m/`*

Header cells that sort on click: a quiet up-down icon at 45% until hovered, then an arrow for the active direction. One sort at a time.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Header button | `.dt-sort` | Full-cell button, Archivo 600 11.5px. |
| 2 | Icon | `svg` | arrow-up-down at 45% → arrow up / down when active. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Text column | `th .dt-sort` | Icon after the label. |
| Numeric column | `th.num .dt-sort` | Right-aligned, icon before the label. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Resting fill and border from the variant. | — |
| hover | Fill steps one shade (`--primary-hover`, `--muted`), `--motion-hover` 120ms. | `:hover · .is-hover` |
| focus | 2px `--ring` outline, 2px offset. Keyboard only. | `:focus-visible · .is-focus` |
| active | Sorted: foreground label, direction arrow. | `[aria-sort=ascending\|descending]` |
| disabled | Unsortable columns are plain text. | `[disabled]` |
| error | A control has no error state of its own; the form or toast reports it. | — |
| loading | Label becomes the progressive verb, `.spin` leads, clicks are blocked, width holds. | `[aria-busy=true]` |
| empty | Not applicable: an action always has a label. | — |
| read-only | Hide the action rather than show a dead one; if it must show, use disabled with a reason. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 4px | `var(--s-1)` |
| height | 28px | `28px` |
| padding | 0 6px | `0 6px` |
| border-radius | 6px | `var(--r-control)` |
| font | inherit | `inherit` |
| color | inherit | `inherit` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--row-th` | `36px` | 36px |
| `--s-3` | `12px` | 12px |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--text-small` | `12px` | 12px |
| `--w-semibold` | `600` | 600 |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--s-1` | `4px` | 4px |
| `--r-control` | `6px` | 6px |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |

### Motion

- Icon opacity and rotation on `--motion-hover`.

### Accessibility

- **Keyboard:** Enter / Space cycles ascending → descending → none.
- **ARIA:** `aria-sort` on the `th`, not the button.
- **Focus:** 2px ring, 0 offset (inside the cell).

### Content rules

- Phone: sort lives in the filter sheet ("Sort by Updated, newest first").

**Do**

- Default sort = most useful (Updated, newest).

**Don't**

- Multi-column sort without a visible order.

### Build it

- **shadcn / Radix:** TanStack `getSortedRowModel` + a header button.
- **Exists in apps/admin today:** no · new: Table. No `table.tsx` and no @tanstack/react-table in apps/admin; both are new.
- **Rendering:** client component (`"use client"`). 

```ts
interface SortHeaderProps { label: string; direction: "asc" | "desc" | false; onToggle: () => void; numeric?: boolean }
```

### Used on

[Appointments](../../phase-3/pages/appointments/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Global Store](../../phase-3/pages/global-store/), [Orders](../../phase-3/pages/orders/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/)

---

<a id="filter-bar"></a>
## Filter bar

*Data table · family `data-table` · page `components/filter-bar/` · phone `components/filter-bar/m/`*

The toolbar above a table: search (300px), status filter chips with counts, a Filters popover for the rest, and view options on the right. Counts always add up to the total.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Bar | `.dt-bar` | Flex, 8px gap, wraps. |
| 2 | Search | `.dt-search` | 300px field with search icon and clear. |
| 3 | Status chips | `.dt-fchip` | Toggle chips with counts (see Status filter chip). |
| 4 | Filters popover | `.dt-fpop` | Fields for supplier, collection, date; Reset + Apply. |
| 5 | Right tools | `.r` | Density, columns, export. |
| 6 | Phone tools | `.dt-m-tools` | Search + Filter button opening a bottom sheet; count line. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Desktop | `.dt-bar` |  |
| With active filters | `.dt-chips` | Removable chips for each applied filter + Clear all. |
| Phone | `.dt-m-tools + sheet` |  |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Holds its children at the documented spacing. | — |
| hover | Not applicable. | — |
| focus | Not applicable; children carry focus. | — |
| active | Applied filter chips shown under the bar. | `.dt-chips` |
| disabled | Not applicable. | — |
| error | Children report errors; the container may add a summary at the top. | — |
| loading | Children show skeletons in place. | `.skel` |
| empty | No filters: only search + status chips. | — |
| read-only | Children render as values, not inputs. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 8px | `var(--s-2)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--s-2` | `8px` | 8px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--s-3` | `12px` | 12px |
| `--secondary-border` | `219 14% 79%  (#C2C7D1)` | #C2C7D1 |
| `--r-control` | `6px` | 6px |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--text-btn-sm` | `12.5px` | 12.5px |
| `--w-medium` | `500` | 500 |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--w-regular` | `400` | 400 |
| `--text-micro` | `11.5px` | 11.5px |
| `--font-ui` | `'IBM Plex Sans', system-ui, sans-serif` | 'IBM Plex Sans', system-ui, sans-serif |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--accent-foreground` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--s-1` | `4px` | 4px |
| `--w-semibold` | `600` | 600 |
| `--r-pill` | `999px` | 999px |
| `--primary-foreground` | `60 17% 98%  (#FBFBF9)` | #FBFBF9 |
| `--text-10-5` | `10.5px` | 10.5px |
| `--s-4` | `16px` | 16px |
| `--text-small` | `12px` | 12px |
| `--text-body` | `13px` | 13px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |

### Accessibility

- **Keyboard:** Search first in tab order; / focuses search.
- **ARIA:** Result count in `aria-live="polite"` ("41 orders").
- **Focus:** After Apply, focus returns to the Filters button.

### Content rules

- Search placeholder names what is searched: "Search handle, cloth or code".

**Do**

- Keep filters in the URL.

**Don't**

- Hide the active filters.

### Build it

- **shadcn / Radix:** `Input` + `ToggleGroup` + `Popover`; phone `Drawer`.
- **Exists in apps/admin today:** Input · new: Popover, ToggleGroup. Input exists; others new.
- **Rendering:** client component (`"use client"`). 

```ts
interface FilterBarProps {
  search: string;
  onSearch: (q: string) => void;
  searchPlaceholder: string;
  statusOptions?: StatusFilterOption[];
  status?: string[];
  onStatusChange?: (v: string[]) => void;
  filters?: React.ReactNode;        // popover body
  applied?: { key: string; label: string; onRemove: () => void }[];
  resultCount: number;
  tools?: React.ReactNode;
}
```

### Used on

[Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Customers](../../phase-3/pages/customers/), [Design pricing](../../phase-3/pages/design-pricing/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Orders](../../phase-3/pages/orders/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Audit log](../../phase-3/pages/settings-audit-log/), [Tailor Roster](../../phase-3/pages/tailors/)

---

<a id="saved-views"></a>
## Saved views

*Data table · family `data-table` · page `components/saved-views/` · phone `components/saved-views/m/`*

Named filter sets as a tab-like strip above the table (All, Needs cloth, Draft this week…), with counts and a dot when the current view has unsaved changes.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Strip | `.dt-views` | Flex, 20px gap, hairline below. |
| 2 | View | `.dt-view` | Label + count `em`; 2px underline when on. |
| 3 | Modified dot | `.dt-mod` | 6px info dot: unsaved changes. |
| 4 | Actions | `.r` | Save view, ⋯ (rename, delete). |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| System views | `.dt-view` | Built in, cannot be deleted. |
| Personal views | `.dt-view` | Created by the person. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Muted label, icon at 80% opacity. | — |
| hover | Soft fill (`--muted`, or sidebar foreground at 6%). | `:hover · .is-hover` |
| focus | 2px ring outline (`--ring`, `--sidebar-ring` on navy). | `:focus-visible · .is-focus` |
| active | Current view: foreground + `--primary` underline. | `.dt-view.on` |
| disabled | 45%. | `[disabled]` |
| error | Not applicable. | — |
| loading | Skeleton lines while the place's menu loads. | `.skel` |
| empty | Not applicable: navigation is never empty. | — |
| read-only | Not applicable. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 20px | `var(--s-5)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--s-5` | `20px` | 20px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--s-2` | `8px` | 8px |
| `--text-card` | `13.5px` | 13.5px |
| `--w-semibold` | `600` | 600 |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--r-control` | `6px` | 6px |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--w-regular` | `400` | 400 |
| `--text-micro` | `11.5px` | 11.5px |
| `--font-ui` | `'IBM Plex Sans', system-ui, sans-serif` | 'IBM Plex Sans', system-ui, sans-serif |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--info` | `218 49% 39%  (#335694)` | #335694 |
| `--s-1` | `4px` | 4px |

### Accessibility

- **Keyboard:** Behaves as Tabs (← →).
- **ARIA:** `role="tablist"`; modified dot has text "(changed)" for AT.
- **Focus:** Ring.

### Content rules

- Names describe the filter: "Needs cloth".

**Do**

- Offer Save as new view when modified.

**Don't**

- More than ~6 views in the strip; overflow into a menu.

### Build it

- **shadcn / Radix:** `Tabs` (underline) + `DropdownMenu` for view actions.
- **Exists in apps/admin today:** no · new: Tabs. Not in apps/admin.
- **Rendering:** client component (`"use client"`). 

```ts
interface SavedView { id: string; name: string; count?: number; system?: boolean; query: string }
interface SavedViewsProps { views: SavedView[]; current: string; modified?: boolean; onSelect: (id: string) => void; onSave: () => void; onRename: (id: string) => void; onDelete: (id: string) => void }
```

### Used on

[Appointments](../../phase-3/pages/appointments/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="density"></a>
## Density

*Data table · family `data-table` · page `components/density/` · phone `components/density/m/`*

Comfortable (44px rows) or compact (36px) for long lists like the audit log. A small segmented control in the table toolbar; the phone has no density (lists).

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Control | `.seg.sm` | Two icon segments with labels for AT. |
| 2 | Rows | `.dt td` | Height switches between `--row` and `--row-compact`. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Comfortable | `[data-density=comfortable] · --row 44px` | Default. |
| Compact | `[data-density=compact] · --row-compact 36px` | Audit log, ERP queue. Hides the second text line, the FX line and cloth sub-text; thumb shrinks to 22×30. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Unselected mark on a 1.5px `--input` edge. | — |
| hover | Edge darkens to `--input-hover`. | `:hover · .is-hover` |
| focus | `--focus-ring` around the mark (or 2px `--ring` outline). | `:focus-visible · .is-focus` |
| active | Selected: `--primary` fill. | `:checked · [aria-checked=true]` |
| disabled | `--muted` ground, `--border` edge, not-allowed cursor. | `:disabled · [aria-disabled=true]` |
| error | Edge `--destructive`; the group's `.help.err` explains. | `.err` |
| loading | Disable while saving; the row shows `.spin` after the label. | `.busy` |
| empty | Not applicable. | — |
| read-only | Shown as the value in text, or locked (`.lock`) with the reason on hover. | `.lock` |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| row comfortable / compact | 44 / 36px | `--row / --row-compact` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--row-compact` | `36px` | 36px |
| `--s-1` | `4px` | 4px |
| `--row` | `44px` | 44px |

### Accessibility

- **Keyboard:** As Segmented.
- **ARIA:** `aria-label="Row density"`.
- **Focus:** Ring.

### Content rules

- Labels: Comfortable, Compact.

**Do**

- Remember per table per person.

**Don't**

- A third density.

### Build it

- **shadcn / Radix:** `ToggleGroup` in the toolbar.
- **Exists in apps/admin today:** no · new: ToggleGroup. Not in apps/admin.
- **Rendering:** client component (`"use client"`). 

```ts
type Density = "comfortable" | "compact";
interface DensityToggleProps { value: Density; onValueChange: (d: Density) => void }
```

### Used on

[Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Global Store product editor](../../phase-3/pages/global-store-editor/)

---

<a id="sticky-header"></a>
## Sticky header

*Data table · family `data-table` · page `components/sticky-header/` · phone `components/sticky-header/m/`*

On long tables (the audit log) the header row sticks under the top bar while the body scrolls, and the toolbar sticks above it. Needs the kit v2 `overflow:clip` shell so sticky works on a real page.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Scroll body | `.dt-scroll.y` | Vertical scroll container (specimen) or the page itself. |
| 2 | Sticky header | `th { position: sticky; top: 0 }` | Solid `--muted` ground, z 3. |
| 3 | Shell | `.frame / .main` | `overflow: clip`, not hidden. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| In-page | `sticky top = topbar height` | Real pages. |
| In a box | `.dt-scroll.y` | Inside a fixed-height panel. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Rows and cards that open something take `--muted` or lift; static blocks do not react. | `:hover` |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Not applicable. | — |
| disabled | Not applicable: hide data the person cannot see, never grey it. | — |
| error | Names the real cause and offers Try again (see Error states). | `.sf-alert.danger` |
| loading | `.skel` shapes in the final layout, pulsing on `--motion-skeleton`. | `.skel` |
| empty | Says what is missing and the one next step (see Empty state). | `.sf-empty` |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| height | 36px | `var(--row-th)` |
| padding | 0 12px | `0 var(--s-3)` |
| background | #F3F4F7 | `hsl(var(--muted))` |
| font-size | 12px | `var(--text-small)` |
| font-weight | 600 | `var(--w-semibold)` |
| color | #505B71 | `hsl(var(--muted-foreground))` |
| z-index | 3 | `--z-sticky` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--r-frame` | `15px` | 15px |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--e-frame` | `0 0 0 1px rgba(14,23,41,.05), 0 16px 36px -24px rgba(14,23,41,.20)` | 0 0 0 1px rgba(14,23,41,.05), 0 16px 36px -24px rgba(14,23,41,.20) |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--row-th` | `36px` | 36px |
| `--s-3` | `12px` | 12px |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--text-small` | `12px` | 12px |
| `--w-semibold` | `600` | 600 |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--text-code` | `12px` | 12px |
| `--font-code` | `'IBM Plex Mono', ui-monospace, monospace` | 'IBM Plex Mono', ui-monospace, monospace |
| `--z-sticky` | `3` | 3 |

### Accessibility

- **Keyboard:** —
- **ARIA:** Header remains the `thead`.
- **Focus:** Focused row scrolls into view below the sticky header (`scroll-margin-top`).

### Content rules

- —

**Do**

- Use `overflow: clip` on shell ancestors.

**Don't**

- `overflow: hidden` on any ancestor of a sticky element (it silently breaks).

### Build it

- **shadcn / Radix:** CSS only (`sticky top-[56px]`).
- **Exists in apps/admin today:** no · new: Table. No `table.tsx` and no @tanstack/react-table in apps/admin; both are new.
- **Rendering:** server component. 

```ts
// a DataTable option:
interface StickyHeaderOptions { stickyHeader?: boolean; stickyOffset?: number /* px, default 56 = --topbar-h */ }
```

### Used on

Every table surface inherits it from the kit `.dt th` rule (sticky top 0); the audit logs (`hq-settings-audit-log`, `settings-audit-log`) depend on it most.

---

<a id="row-actions"></a>
## Row and row actions

*Data table · family `data-table` · page `components/row-actions/` · phone `components/row-actions/m/`*

Each row opens its record on click; the end of the row holds a quiet Edit (opens quick edit) and the ⋯ menu for the rest. Rows can expand to show detail.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Row | `tr` | Click target for open. |
| 2 | Edit | `.btn.btn-secondary.sm / .icon-btn.ghost` | Quick edit in a sheet. |
| 3 | More | `.icon-btn.ghost[data-k-trigger] + .menu` | Copy handle, Duplicate, Retire product… |
| 4 | Expander (optional) | `.dt-exp` | Chevron that reveals a detail row. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Standard | `.dt-ra` | Edit + ⋯. |
| Expandable | `.dt-exp` | Order lines under an order. |
| Phone | `⋯ on the list card → action sheet` |  |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Resting fill and border from the variant. | — |
| hover | Row `--muted`; actions reach full contrast. | `tr:hover` |
| focus | Row ring; ⋯ ring 1px offset. | `.is-focus` |
| active | Menu open: row keeps the hover ground. | — |
| disabled | 45% opacity, no shadow, no pointer events. Keep a reason nearby. | `[disabled] · [aria-disabled=true]` |
| error | A control has no error state of its own; the form or toast reports it. | — |
| loading | Label becomes the progressive verb, `.spin` leads, clicks are blocked, width holds. | `[aria-busy=true]` |
| empty | Not applicable: an action always has a label. | — |
| read-only | Hide the action rather than show a dead one; if it must show, use disabled with a reason. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 2px | `2px` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--s-2` | `8px` | 8px |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--e-row-focus` | `inset 0 1px 0 hsl(var(--ring)),inset 0 -1px 0 hsl(var(--ring))` | inset 0 1px 0 #3F60A6,inset 0 -1px 0 #3F60A6 |
| `--e-row-focus-first` | `inset 2px 0 0 hsl(var(--ring)),inset 0 1px 0 hsl(var(--ring)),inset 0 -1px 0 hsl(var(--ring))` | inset 2px 0 0 #3F60A6,inset 0 1px 0 #3F60A6,inset 0 -1px 0 #3F60A6 |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--r-control` | `6px` | 6px |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--s-4` | `16px` | 16px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--text-small` | `12px` | 12px |

### Accessibility

- **Keyboard:** Row name is a link; Edit and ⋯ are buttons after it.
- **ARIA:** ⋯ `aria-label="More actions for {row}"`.
- **Focus:** Menu returns focus to ⋯.

### Content rules

- Menu verbs: Edit, Duplicate, Copy handle, Retire product.

**Do**

- Put destructive items last.

**Don't**

- More than one visible action besides ⋯.

### Build it

- **shadcn / Radix:** TanStack row + `DropdownMenu`.
- **Exists in apps/admin today:** no · new: DropdownMenu, Table. dropdown-menu wrapper new.
- **Rendering:** client component (`"use client"`). 

```ts
interface RowActionsProps<T> { row: T; onEdit?: (row: T) => void; menu: MenuItem[]; label: string }
```

### Used on

[Global Store](../../phase-3/pages/global-store/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="bulk-bar"></a>
## Bulk select and bulk bar

*Data table · family `data-table` · page `components/bulk-bar/` · phone `components/bulk-bar/m/`*

Select rows with checkboxes (header tick = all on the page, indeterminate when some); a navy bar rises at the bottom with the count and the bulk actions. Not-wired actions are shown as such.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Row checkbox | `input.cb` | First column. |
| 2 | Header checkbox | `th input.cb` | All / indeterminate. |
| 3 | Bulk bar | `.dt-bulk` | Navy floating bar, centred, rises like a toast. |
| 4 | Count | `b` | "3 selected" + Select all 214. |
| 5 | Actions | `.btn` | Set status, Add to section… |
| 6 | Phone | `.dt-mbulk / .k-actbar.sel` | Selection mode in the action bar. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Desktop | `.dt-bulk` |  |
| Phone | `.k-actbar.sel` |  |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Resting fill and border from the variant. | — |
| hover | Fill steps one shade (`--primary-hover`, `--muted`), `--motion-hover` 120ms. | `:hover · .is-hover` |
| focus | 2px `--ring` outline, 2px offset. Keyboard only. | `:focus-visible · .is-focus` |
| active | Bar visible while ≥ 1 row is selected. | `.dt-bulk.show` |
| disabled | Not-wired action: plain locked chip "Not wired". | `.chip.plain.locked` |
| error | A control has no error state of its own; the form or toast reports it. | — |
| loading | Action button spinner; rows stay selected. | `[aria-busy=true]` |
| empty | Not applicable: an action always has a label. | — |
| read-only | Hide the action rather than show a dead one; if it must show, use disabled with a reason. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 8px | `var(--s-2)` |
| padding | 6px 6px 6px 16px | `6px 6px 6px var(--s-4)` |
| background | #0E172A | `hsl(var(--primary))` |
| color | #FBFBF9 | `hsl(var(--primary-foreground))` |
| border-radius | 8px | `var(--r-card)` |
| box-shadow | 0 12px 28px -14px rgba(14,23,41,.35) | `var(--e-toast)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--r-control` | `6px` | 6px |
| `--input-hover` | `223 17% 45%  (#5F6A86)` | #5F6A86 |
| `--s-5` | `20px` | 20px |
| `--z-pop` | `20` | 20 |
| `--s-2` | `8px` | 8px |
| `--s-4` | `16px` | 16px |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--primary-foreground` | `60 17% 98%  (#FBFBF9)` | #FBFBF9 |
| `--r-card` | `8px` | 8px |
| `--e-toast` | `0 12px 28px -14px rgba(14,23,41,.35)` | 0 12px 28px -14px rgba(14,23,41,.35) |
| `--toast-shift` | `8px` | 8px |
| `--motion-toast-out` | `140ms var(--ease-out)` | 140ms cubic-bezier(.4,0,1,1) |
| `--motion-handoff` | `.2s` | .2s |
| `--motion-toast-in` | `200ms var(--ease-in)` | 200ms cubic-bezier(.22,1,.36,1) |
| `--text-card` | `13.5px` | 13.5px |
| `--w-semibold` | `600` | 600 |
| `--s-1` | `4px` | 4px |
| `--s-3` | `12px` | 12px |
| `--e-phone-sheet` | `0 -20px 50px -20px rgba(14,23,41,.50)` | 0 -20px 50px -20px rgba(14,23,41,.50) |
| `--motion-sheet-out` | `180ms var(--ease-out)` | 180ms cubic-bezier(.4,0,1,1) |
| `--motion-sheet-in` | `260ms var(--ease-in)` | 260ms cubic-bezier(.22,1,.36,1) |
| `--text-14` | `14px` | 14px |
| `--control-h-m` | `44px` | 44px |

### Motion

- Bar in/out on the toast motion (`--motion-toast-in/out`, rises 8px).

### Accessibility

- **Keyboard:** Space on a row checkbox; Shift+click range.
- **ARIA:** Count in `aria-live`; header checkbox `aria-checked="mixed"`.
- **Focus:** Bar actions reachable by Tab after the table.

### Content rules

- "3 selected" · "Select all 214 products".

**Do**

- Offer Clear selection.

**Don't**

- Hide the count.

### Build it

- **shadcn / Radix:** TanStack row selection + `Checkbox` + a floating bar.
- **Exists in apps/admin today:** Checkbox · new: Table. Checkbox exists.
- **Rendering:** client component (`"use client"`). 

```ts
interface BulkBarProps { count: number; total: number; onSelectAll: () => void; onClear: () => void; actions: { label: string; onRun: () => void; notWired?: boolean }[] }
```

### Used on

[Global Store](../../phase-3/pages/global-store/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Brands](../../phase-3/pages/super-brands/), [Variants](../../phase-3/pages/variants/)

---

<a id="pagination"></a>
## Pagination

*Data table · family `data-table` · page `components/pagination/` · phone `components/pagination/m/`*

Under the table: "1–25 of 214", rows per page, and previous / page numbers / next. The phone uses Load more at the end of the list.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Summary | `.dt-sum` | "1–25 of 214" tabular. |
| 2 | Rows per page | `.dt-rpp` | Small select (25 / 50 / 100). |
| 3 | Pages | `.dt-pages` | Prev, numbers (current filled), next. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Numbered | `.dt-pages` | Default. |
| Load more (phone) | `.btn.btn-secondary` | Appends 25. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Muted label, icon at 80% opacity. | — |
| hover | Soft fill (`--muted`, or sidebar foreground at 6%). | `:hover · .is-hover` |
| focus | 2px ring outline (`--ring`, `--sidebar-ring` on navy). | `:focus-visible · .is-focus` |
| active | Current page navy. | `[aria-current=page]` |
| disabled | Prev on page 1 / Next on the last page. | `[disabled]` |
| error | Not applicable. | — |
| loading | Skeleton lines while the place's menu loads. | `.skel` |
| empty | Not applicable: navigation is never empty. | — |
| read-only | Not applicable. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 12px | `var(--s-3)` |
| padding | 8px 12px 8px 16px | `var(--s-2) var(--s-3) var(--s-2) var(--s-4)` |
| font-size | 12px | `var(--text-small)` |
| color | #505B71 | `hsl(var(--muted-foreground))` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--s-2` | `8px` | 8px |
| `--text-small` | `12px` | 12px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--s-1` | `4px` | 4px |
| `--r-chip` | `4px` | 4px |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--accent-foreground` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--text-micro` | `11.5px` | 11.5px |
| `--font-code` | `'IBM Plex Mono', ui-monospace, monospace` | 'IBM Plex Mono', ui-monospace, monospace |
| `--s-3` | `12px` | 12px |
| `--s-4` | `16px` | 16px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--r-card` | `8px` | 8px |
| `--card` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--w-medium` | `500` | 500 |
| `--r-control` | `6px` | 6px |
| `--text-btn-sm` | `12.5px` | 12.5px |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--w-semibold` | `600` | 600 |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |

### Accessibility

- **Keyboard:** Buttons.
- **ARIA:** `<nav aria-label="Pagination">`; current `aria-current="page"`.
- **Focus:** After paging, focus moves to the table caption.

### Content rules

- Summary uses an en dash: 1–25 of 214.

**Do**

- Keep page in the URL.

**Don't**

- Infinite scroll on desktop tables.

### Build it

- **shadcn / Radix:** shadcn `Pagination`.
- **Exists in apps/admin today:** no · new: Pagination. Not in apps/admin.
- **Rendering:** client component (`"use client"`). 
- **Install:** `npx shadcn@latest add pagination`

```ts
interface PaginationProps { page: number; pageSize: 25 | 50 | 100; total: number; onPageChange: (p: number) => void; onPageSizeChange: (n: number) => void }
```

### Used on

[Orders](../../phase-3/pages/orders/), [Inbox](../../phase-3/pages/support/), [Support templates](../../phase-3/pages/support-templates/), [AI training pairs](../../phase-3/pages/support-training-pairs/)

---

<a id="column-types"></a>
## Column types

*Data table · family `data-table` · page `components/column-types/` · phone `components/column-types/m/`*

The typed cells every table reuses: image + product, cloth swatch + code, status chip, money (multi-currency), date, code / handle, version, ERP state with a reason.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Product | `.dt-prod` | Thumb 30×40 + name + handle. |
| 2 | Cloth | `.dt-cloth` | Swatch + code (mono) + collection. |
| 3 | Status | `.chip` | Status chip. |
| 4 | Money | `.dt-money` | Right, tabular; EUR / USD under GBP in muted. |
| 5 | Date | `.dt-date` | Relative + exact on hover. |
| 6 | Code | `.dt-code` | Mono + copy button on hover. |
| 7 | Version | `.dt-ver` | Mono `v4`. |
| 8 | ERP | `.dt-erp` | Dot + state + reason. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Each type | `column meta.type` | Pick by data, not by look. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Rows and cards that open something take `--muted` or lift; static blocks do not react. | `:hover` |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Not applicable. | — |
| disabled | Not applicable: hide data the person cannot see, never grey it. | — |
| error | Names the real cause and offers Try again (see Error states). | `.sf-alert.danger` |
| loading | `.skel` shapes in the final layout, pulsing on `--motion-skeleton`. | `.skel` |
| empty | Says what is missing and the one next step (see Empty state). | `.sf-empty` |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| product thumb | 30 × 40px (22 × 30 compact) | `.dt-ph` |
| money | right-aligned, tabular-nums | `.dt-money` |
| code / handle / version | Plex Mono 12px | `--font-code / --text-code` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--w-semibold` | `600` | 600 |
| `--text-card` | `13.5px` | 13.5px |
| `--s-3` | `12px` | 12px |
| `--w-medium` | `500` | 500 |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--w-regular` | `400` | 400 |
| `--text-small` | `12px` | 12px |
| `--font-code` | `'IBM Plex Mono', ui-monospace, monospace` | 'IBM Plex Mono', ui-monospace, monospace |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--r-media` | `4px` | 4px |
| `--studio` | `radial-gradient(120% 90% at 50% 18%, #f7f8fa 0%, #e3e7ee 80%)` | radial-gradient(120% 90% at 50% 18%, #f7f8fa 0%, #e3e7ee 80%) |
| `--text-8` | `8px` | 8px |
| `--primary-foreground` | `60 17% 98%  (#FBFBF9)` | #FBFBF9 |
| `--s-2` | `8px` | 8px |
| `--text-code` | `12px` | 12px |
| `--s-1` | `4px` | 4px |
| `--r-chip` | `4px` | 4px |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--warning` | `36 79% 29%  (#845610)` | #845610 |
| `--s-4` | `16px` | 16px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--r-card` | `8px` | 8px |
| `--card` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--text-micro` | `11.5px` | 11.5px |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--text-8-5` | `8.5px` | 8.5px |
| `--text-9` | `9px` | 9px |

### Accessibility

- **Keyboard:** Copy buttons appear on focus too.
- **ARIA:** Exact date in `<time datetime>` + `title`.
- **Focus:** Ring on copy.

### Content rules

- £1,150 · v4 · HS 81045 · navy-birdseye-two-piece · 2 Oct.

**Do**

- One formatter per type, shared.

**Don't**

- Format money by hand per page.

### Build it

- **shadcn / Radix:** TanStack `ColumnDef.meta.type` + one cell renderer per type.
- **Exists in apps/admin today:** no · new: Table. No `table.tsx` and no @tanstack/react-table in apps/admin; both are new.
- **Rendering:** server component. Cell renderers are plain components.

```ts
type ColumnType = "product" | "cloth" | "status" | "money" | "date" | "code" | "version" | "erp";
interface ColumnMeta { type: ColumnType; align?: "left" | "right"; currencies?: Currency[] }
```

### Used on

[Dashboard](../../phase-3/pages/dashboard/), [Global Store](../../phase-3/pages/global-store/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Brands](../../phase-3/pages/settings-brands/), [Users & access](../../phase-3/pages/settings-users/), [Brands](../../phase-3/pages/super-brands/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/)

---

<a id="table-states"></a>
## Table states

*Data table · family `data-table` · page `components/table-states/` · phone `components/table-states/m/`*

Empty, no results, loading skeleton rows, error, partial and not wired: the table frame and toolbar stay; only the body changes.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Frame | `.dt-wrap` | Unchanged. |
| 2 | Body state | `.dt-empty / skeleton rows / alert` | Fills the body. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Empty (first use) | `.dt-empty` | Create / Add from Global Store. |
| No results | `.dt-empty` | Names filters + Clear filters. |
| Loading | `skeleton rows` | 8 rows in the real grid. |
| Error | `.sf-alert.danger in the body` | Cause + Try again. |
| Partial | `warning strip + rows` | "12 rows could not load". |
| Not wired | `.sf-alert.wired` | Backend not connected. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Rows and cards that open something take `--muted` or lift; static blocks do not react. | `:hover` |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Not applicable. | — |
| disabled | Not applicable: hide data the person cannot see, never grey it. | — |
| error | Danger alert inside the frame. | — |
| loading | Skeleton rows pulse. | `.dt-sk` |
| empty | See variants. | `.dt-empty` |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| skeleton rows | 8 rows at the real row height (44px) | `--row` |
| empty block padding | 48px 24px | `--s-12 / --s-6` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--warning` | `36 79% 29%  (#845610)` | #845610 |
| `--w-medium` | `500` | 500 |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--s-2` | `8px` | 8px |
| `--r-chip` | `4px` | 4px |
| `--status-soft-alpha` | `.12` | .12 |
| `--w-semibold` | `600` | 600 |
| `--text-10-5` | `10.5px` | 10.5px |
| `--font-ui` | `'IBM Plex Sans', system-ui, sans-serif` | 'IBM Plex Sans', system-ui, sans-serif |
| `--s-12` | `48px` | 48px |
| `--s-6` | `24px` | 24px |
| `--card` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--s-1` | `4px` | 4px |
| `--text-section` | `16px` | 16px |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--text-body` | `13px` | 13px |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--r-media` | `4px` | 4px |
| `--s-3` | `12px` | 12px |
| `--s-4` | `16px` | 16px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--info` | `218 49% 39%  (#335694)` | #335694 |
| `--r-card` | `8px` | 8px |

### Accessibility

- **Keyboard:** —
- **ARIA:** Table `aria-busy` while loading.
- **Focus:** Clear filters returns focus to search.

### Content rules

- Never "No data". Say what is missing.

**Do**

- Keep the header row visible.

**Don't**

- Collapse the table frame.

### Build it

- **shadcn / Radix:** DataTable `state` prop.
- **Exists in apps/admin today:** Skeleton · new: Table. Skeleton exists.
- **Rendering:** server component. 

```ts
type TableState = "ready" | "loading" | "empty" | "noResults" | "error" | "partial" | "notWired";
```

### Used on

[Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [Variants](../../phase-3/pages/variants/)

---

<a id="product-card"></a>
## Product card

*Cards and gallery · family `cards-gallery` · page `components/product-card/` · phone `components/product-card/m/`*

A garment card for galleries: 3:4 photo on the studio ground with the status chip on it, then name, cloth code, price and version, with a ⋯ menu. Lifts 2px on hover.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Card | `.cg-card.lift` | 1px `--border`, 8px radius, `--e-1`. |
| 2 | Media | `.cg-media` | 3:4, `--studio` ground, inner radius `--r-card-inner`. |
| 3 | Status | `.cg-media>.chip` | Top left on white 94%. |
| 4 | Title | `.cg-title` | Archivo 600 13.5px, two lines max. |
| 5 | Meta | `.cg-meta` | Cloth code mono + collection. |
| 6 | Price + version | `.cg-price / .cg-ver` | Tabular £1,150 · v4. |
| 7 | Menu | `.cg-more` | ⋯ appears on hover/focus. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Default | `.cg-card` | Gallery. |
| Disabled | `.cg-card.is-disabled` | Not available to this brand. |
| Image missing | `.cg-nophoto` | Garment silhouette, never a grey box. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Lifts `--lift-shift` (−2px) with `--e-card-hover`; border `--secondary-border`. | `.lift:hover` |
| focus | `--focus-ring` + ring border. | `:focus-visible` |
| active | Not applicable. | — |
| disabled | Muted ground 60%, media at 50%, not-allowed. | `.is-disabled` |
| error | Names the real cause and offers Try again (see Error states). | `.sf-alert.danger` |
| loading | 3:4 skeleton + two lines. | `.cg-card.is-loading` |
| empty | Says what is missing and the one next step (see Empty state). | `.sf-empty` |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| background | #FFFFFF | `hsl(var(--card))` |
| border | 1px solid #E3E7ED | `1px solid hsl(var(--border))` |
| border-radius | 8px | `var(--r-card)` |
| box-shadow | 0 1px 2px rgba(14,23,41,.04) | `var(--e-1)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--lift-shift` | `-2px` | -2px |
| `--e-card-hover` | `0 10px 24px -14px rgba(14,23,41,.28)` | 0 10px 24px -14px rgba(14,23,41,.28) |
| `--s-4` | `16px` | 16px |
| `--s-6` | `24px` | 24px |
| `--text-small` | `12px` | 12px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--card` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--r-card` | `8px` | 8px |
| `--e-1` | `0 1px 2px rgba(14,23,41,.04)` | 0 1px 2px rgba(14,23,41,.04) |
| `--secondary-border` | `219 14% 79%  (#C2C7D1)` | #C2C7D1 |
| `--focus-ring` | `0 0 0 1px hsl(var(--ring)), 0 0 0 4px hsl(var(--ring) / .14)` | 0 0 0 1px #3F60A6, 0 0 0 4px #3F60A6 @0.14 |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--studio` | `radial-gradient(120% 90% at 50% 18%, #f7f8fa 0%, #e3e7ee 80%)` | radial-gradient(120% 90% at 50% 18%, #f7f8fa 0%, #e3e7ee 80%) |
| `--r-card-inner` | `calc(var(--r-card) - 1px)` | 7px |
| `--s-2` | `8px` | 8px |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--s-3` | `12px` | 12px |
| `--w-semibold` | `600` | 600 |
| `--text-card` | `13.5px` | 13.5px |
| `--lh-card` | `1.3` | 1.3 |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--font-ui` | `'IBM Plex Sans', system-ui, sans-serif` | 'IBM Plex Sans', system-ui, sans-serif |
| `--text-micro` | `11.5px` | 11.5px |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--w-medium` | `500` | 500 |
| `--font-code` | `'IBM Plex Mono', ui-monospace, monospace` | 'IBM Plex Mono', ui-monospace, monospace |
| `--control-h-m-lg` | `48px` | 48px |
| `--text-read` | `15px` | 15px |
| `--s-1` | `4px` | 4px |

### Motion

- Lift: transform + shadow on `--motion-hover`; 0px under reduced motion.

### Accessibility

- **Keyboard:** Card is one link (the title); ⋯ is a separate button.
- **ARIA:** Image `alt` = product name; status in text.
- **Focus:** Card ring via `:focus-within`.

### Content rules

- Navy birdseye two-piece suit · HS 81045 · £1,150 · v4.

**Do**

- Keep figures identical to the table view.

**Don't**

- Put the price on the photo.

### Build it

- **shadcn / Radix:** shadcn `Card` + `next/image` + `DropdownMenu`.
- **Exists in apps/admin today:** Card. `ui/card.tsx` exists.
- **Rendering:** server component. 

```ts
interface ProductCardProps {
  href: string;
  name: string;
  imageUrl?: string;
  garmentType: "jacket" | "trousers" | "waistcoat" | "suit";
  status: "draft" | "available" | "retired";
  cloth: { code: string; collection: string };
  priceMinor: number;
  currency: Currency;
  version: number;
  disabledReason?: string;
  menu?: MenuItem[];
}
```

### Used on

[Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Cloth library](../../phase-3/pages/super-cloths/)

---

<a id="cloth-card"></a>
## Cloth card

*Cards and gallery · family `cards-gallery` · page `components/cloth-card/` · phone `components/cloth-card/m/`*

A square cloth swatch card for the cloth library: texture, stock chip, code in mono, collection and supplier.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Card | `.cg-card` | As product card. |
| 2 | Swatch | `.cg-swatch` | 1:1 texture, inner radius top. |
| 3 | Stock chip | `.chip` | In stock / Low / Out of stock (warning). |
| 4 | Code | `.code` | VBC 218.1003. |
| 5 | Collection / supplier | `.cg-meta` | Perennial · Vitale Barberis Canonico. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Default | `.cg-card + .cg-swatch` |  |
| Out of stock | `chip warning` | Amber, never red. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Lift. | `.lift:hover` |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Not applicable. | — |
| disabled | Swatch 50%. | `.is-disabled` |
| error | Names the real cause and offers Try again (see Error states). | `.sf-alert.danger` |
| loading | `.skel` shapes in the final layout, pulsing on `--motion-skeleton`. | `.skel` |
| empty | Says what is missing and the one next step (see Empty state). | `.sf-empty` |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| border-radius | 7px 7px 0 0 | `var(--r-card-inner) var(--r-card-inner) 0 0` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--r-card-inner` | `calc(var(--r-card) - 1px)` | 7px |
| `--s-2` | `8px` | 8px |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |

### Accessibility

- **Keyboard:** Link.
- **ARIA:** Swatch `alt` = collection + colour.
- **Focus:** Ring.

### Content rules

- Out of stock is amber.

**Do**

- Show the code prominently.

**Don't**

- Use cloth photos with people.

### Build it

- **shadcn / Radix:** `Card`.
- **Exists in apps/admin today:** Card. Card exists.
- **Rendering:** server component. 

```ts
interface ClothCardProps { href?: string; code: string; collection: string; supplier: string; swatchUrl?: string; stock: "in" | "low" | "out" }
```

### Used on

[Cloth library](../../phase-3/pages/super-cloths/)

---

<a id="gallery"></a>
## Global Store gallery

*Cards and gallery · family `cards-gallery` · page `components/gallery/` · phone `components/gallery/m/`*

The gallery page pattern: toolbar (search, status chips that add up, sort, gallery/table toggle), a 5-column grid (2 on the phone), and loading, empty, no-match and image-missing states.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Toolbar | `.cg-toolbar` | Search, status chips, sort, view toggle (`.seg.sm`). |
| 2 | Count | `.cg-count` | "214 products" beside the title. |
| 3 | Grid | `.cg-grid.c5` | 5 columns, 20px × 16px gaps (2 columns, 16 × 12 on the phone). |
| 4 | Pager | `.cg-pager` | Load more. |
| 5 | Table view | `.dt` | Same data as data-table. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Grid 4 / 5 / 6 | `.cg-grid.c4\|c5\|c6` | Density by card size. |
| Table view | `.seg toggle → .dt` | Same columns as Data table. |
| Phone list | `.cg-list / .cg-li` |  |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Rows and cards that open something take `--muted` or lift; static blocks do not react. | `:hover` |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Not applicable. | — |
| disabled | Not applicable: hide data the person cannot see, never grey it. | — |
| error | "Cloth sync failed: Holland & Sherry did not answer. Try again". | — |
| loading | Ghost grid of skeleton cards + a thin load line. | `.cg-ghostgrid · .cg-loadline` |
| empty | First use, no matches, or sync failed (names the supplier). | `.cg-empty` |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 20px 16px | `var(--s-5) var(--s-4)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--w-medium` | `500` | 500 |
| `--text-small` | `12px` | 12px |
| `--font-ui` | `'IBM Plex Sans', system-ui, sans-serif` | 'IBM Plex Sans', system-ui, sans-serif |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--r-chip` | `4px` | 4px |
| `--s-1` | `4px` | 4px |
| `--s-2` | `8px` | 8px |
| `--s-3` | `12px` | 12px |
| `--s-6` | `24px` | 24px |
| `--s-4` | `16px` | 16px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--control-h-sm` | `34px` | 34px |
| `--r-control` | `6px` | 6px |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--text-btn-sm` | `12.5px` | 12.5px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--w-regular` | `400` | 400 |
| `--text-micro` | `11.5px` | 11.5px |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--accent-foreground` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--text-body` | `13px` | 13px |
| `--s-5` | `20px` | 20px |
| `--motion-skeleton` | `1.6s var(--ease-standard) infinite alternate` | 1.6s cubic-bezier(.2,0,0,1) infinite alternate |
| `--r-card-inner` | `calc(var(--r-card) - 1px)` | 7px |
| `--s-10` | `40px` | 40px |
| `--r-frame` | `15px` | 15px |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--w-semibold` | `600` | 600 |
| `--text-section` | `16px` | 16px |
| `--lh-section` | `1.25` | 1.25 |
| `--tr-section` | `-.005em` | -.005em |
| `--secondary-border` | `219 14% 79%  (#C2C7D1)` | #C2C7D1 |

### Accessibility

- **Keyboard:** Grid is a list of links.
- **ARIA:** `<ul role="list">`; count live region.
- **Focus:** Toggle keeps focus.

### Content rules

- Chip counts add up to the total (214).

**Do**

- Persist view and filters in the URL.

**Don't**

- Different data in gallery and table views.

### Build it

- **shadcn / Radix:** Grid of `Card` + toolbar.
- **Exists in apps/admin today:** Card · new: ToggleGroup. Card exists.
- **Rendering:** client component (`"use client"`). 

```ts
interface GalleryProps<T> { items: T[]; renderCard: (item: T) => React.ReactNode; view: "gallery" | "table"; onViewChange: (v: "gallery" | "table") => void; columns?: 4 | 5 | 6; state?: TableState; total: number }
```

### Used on

[Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Global Store](../../phase-3/pages/global-store/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Cloth library](../../phase-3/pages/super-cloths/)

---

<a id="picker"></a>
## Picker mode

*Cards and gallery · family `cards-gallery` · page `components/picker/` · phone `components/picker/m/`*

The gallery as a chooser: tick to select cards (cloths for a product family), an anchor card fixed in navy, siblings tagged, and a selection bar with mini swatches and the one primary.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Pick card | `.cg-pick` | Button card. |
| 2 | Tick | `.cg-tick` | Top right circle; navy when selected. |
| 3 | Tag | `.cg-tag.anchor\|.sibling` | Anchor = navy solid; sibling = accent. |
| 4 | Selection bar | `.cg-selbar` | Mini swatches + count + Cancel + primary. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Multi | `.cg-pick[aria-selected]` |  |
| Anchor | `.cg-pick.is-anchor` | Pre-selected and locked. |
| Disabled | `.cg-pick[aria-disabled]` | Already used / unavailable. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Unselected mark on a 1.5px `--input` edge. | — |
| hover | Edge darkens to `--input-hover`. | `:hover · .is-hover` |
| focus | `--focus-ring` around the mark (or 2px `--ring` outline). | `:focus-visible · .is-focus` |
| active | Selected: 1px `--primary` ring + tick. | `[aria-selected=true]` |
| disabled | Muted 60%, swatch 45%. | `[aria-disabled=true]` |
| error | Edge `--destructive`; the group's `.help.err` explains. | `.err` |
| loading | Disable while saving; the row shows `.spin` after the label. | `.busy` |
| empty | Not applicable. | — |
| read-only | Shown as the value in text, or locked (`.lock`) with the reason on hover. | `.lock` |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| width | 100% | `100%` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--s-3` | `12px` | 12px |
| `--s-4` | `16px` | 16px |
| `--s-5` | `20px` | 20px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--s-2` | `8px` | 8px |
| `--r-card-inner` | `calc(var(--r-card) - 1px)` | 7px |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--input` | `219 14% 59%  (#8892A5)` | #8892A5 |
| `--e-1` | `0 1px 2px rgba(14,23,41,.04)` | 0 1px 2px rgba(14,23,41,.04) |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--r-chip` | `4px` | 4px |
| `--r-pill` | `999px` | 999px |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--e-pop` | `0 12px 32px -10px rgba(14,23,41,.25)` | 0 12px 32px -10px rgba(14,23,41,.25) |
| `--primary-foreground` | `60 17% 98%  (#FBFBF9)` | #FBFBF9 |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--card` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--w-semibold` | `600` | 600 |
| `--text-micro` | `11.5px` | 11.5px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--accent-foreground` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--s-1` | `4px` | 4px |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--text-card` | `13.5px` | 13.5px |
| `--text-small` | `12px` | 12px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--secondary-border` | `219 14% 79%  (#C2C7D1)` | #C2C7D1 |
| `--r-card` | `8px` | 8px |

### Accessibility

- **Keyboard:** Space toggles a card; Enter confirms the selection bar.
- **ARIA:** `role="listbox" aria-multiselectable` with `option` cards, or checkbox cards.
- **Focus:** Ring on the card.

### Content rules

- Primary: "Use 4 cloths".

**Do**

- Show the anchor clearly.

**Don't**

- Allow deselecting the anchor.

### Build it

- **shadcn / Radix:** `Command` (search) + selectable `Card`s; phone full-screen `Drawer`.
- **Exists in apps/admin today:** Card · new: Command. Card exists.
- **Rendering:** client component (`"use client"`). 

```ts
interface PickerProps<T> { items: T[]; selected: string[]; onSelectedChange: (ids: string[]) => void; anchorId?: string; disabled?: (item: T) => string | undefined; primaryLabel: (n: number) => string; onConfirm: () => void }
```

### Used on

Specified for the cloth family picker; the approved surfaces pick cloths through the Cloth picker for now.

---

<a id="kpi-tile"></a>
## KPI tile

*Data display · family `data-display` · page `components/kpi-tile/` · phone `components/kpi-tile/m/`*

A headline number with its label, change against the previous period and an optional sparkline: Revenue £48,620, Paid orders 41, AOV £1,186. Multi-currency tiles split the total by currency.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Tile | `.dd-kpi` | Card. |
| 2 | Label | `.dd-kpi-k` | 12px muted + info icon. |
| 3 | Value | `.dd-kpi-v` | 28px Plex 600 tabular (22px in the phone stack). |
| 4 | Delta | `.dd-delta` | ▲ 12% vs last month (green / red / neutral). |
| 5 | Sparkline | `.dd-spark` | `--chart-1` line. |
| 6 | Foot | `.dd-kpi-foot` | Period. |
| 7 | Currency split | `.dd-split` | GBP / EUR / USD with ≈ GBP. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Default | `.dd-kpi` |  |
| Multi-currency | `.dd-split` | Totals per currency + converted. |
| Phone carousel / 2×2 | `.dd-carousel · .dd-kpi-stack` |  |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Rows and cards that open something take `--muted` or lift; static blocks do not react. | `:hover` |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Not applicable. | — |
| disabled | Not applicable: hide data the person cannot see, never grey it. | — |
| error | Red one-liner with the cause. | `.dd-kpi-err` |
| loading | Value and foot transparent over skeletons. | `.dd-kpi.is-loading` |
| empty | "—" in muted at KPI size + "No paid orders yet". | `.dd-kpi-empty` |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| font | 600 28px/1.05 'IBM Plex Sans', system-ui, sans-serif | `var(--w-semibold) var(--text-kpi)/1.05 var(--font-ui)` |
| color | #0F1524 | `hsl(var(--foreground))` |
| value | 28px (22px phone stack) | `--text-kpi` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--w-semibold` | `600` | 600 |
| `--text-kpi` | `28px` | 28px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--tr-page` | `-.015em` | -.015em |
| `--s-4` | `16px` | 16px |
| `--s-2` | `8px` | 8px |
| `--s-3` | `12px` | 12px |
| `--card` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--r-card` | `8px` | 8px |
| `--e-1` | `0 1px 2px rgba(14,23,41,.04)` | 0 1px 2px rgba(14,23,41,.04) |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--motion-focus` | `140ms var(--ease-standard)` | 140ms cubic-bezier(.2,0,0,1) |
| `--secondary-border` | `219 14% 79%  (#C2C7D1)` | #C2C7D1 |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--focus-ring` | `0 0 0 1px hsl(var(--ring)), 0 0 0 4px hsl(var(--ring) / .14)` | 0 0 0 1px #3F60A6, 0 0 0 4px #3F60A6 @0.14 |
| `--w-medium` | `500` | 500 |
| `--text-small` | `12px` | 12px |
| `--font-ui` | `'IBM Plex Sans', system-ui, sans-serif` | 'IBM Plex Sans', system-ui, sans-serif |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--text-body` | `13px` | 13px |
| `--s-1` | `4px` | 4px |
| `--r-chip` | `4px` | 4px |
| `--success` | `158 51% 26%  (#20644B)` | #20644B |
| `--status-soft-alpha` | `.12` | .12 |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--neutral` | `222 13% 39%  (#575E70)` | #575E70 |
| `--chart-muted` | `221 25% 58%  (#798AAF)` | #798AAF |
| `--chart-1` | `219 61% 47%  (#2F62C1)` | #2F62C1 |
| `--text-22` | `22px` | 22px |

### Accessibility

- **Keyboard:** Tile may be a link to the report.
- **ARIA:** Delta spelled out for AT ("up 12 percent").
- **Focus:** Ring if linked.

### Content rules

- Numbers agree across the page (stages sum to paid orders).

**Do**

- Say the comparison period.

**Don't**

- Colour the value itself.

### Build it

- **shadcn / Radix:** `Card`; sparkline via shadcn `chart` (Recharts).
- **Exists in apps/admin today:** Card. Card exists.
- **Rendering:** server component. 

```ts
interface KpiTileProps {
  label: string;
  value: number;
  format: "money" | "count" | "percent";
  currency?: Currency;
  delta?: { value: number; period: string; goodWhen: "up" | "down" };
  spark?: number[];
  split?: { currency: Currency; amountMinor: number; approxBaseMinor: number }[];
  state?: "ready" | "loading" | "empty" | "error";
  error?: string;
  href?: string;
}
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [Campaigns](../../phase-3/pages/campaigns/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Dashboard](../../phase-3/pages/dashboard/), [Founder](../../phase-3/pages/founder/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [API Access](../../phase-3/pages/super-api-access/), [Cloth library](../../phase-3/pages/super-cloths/), [Variant](../../phase-3/pages/variant-detail/), [Sync & OOS](../../phase-3/pages/variants-sync/)

---

<a id="chart-frame"></a>
## Chart frame

*Data display · family `data-display` · page `components/chart-frame/` · phone `components/chart-frame/m/`*

The frame every chart sits in: title, subtitle, range control, legend, the plot (line, bar, funnel), a crosshair tooltip, a table view toggle and the footer source. Palette `--chart-1..5` in order, never cycled; previous period dotted in `--chart-muted`.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Frame | `.dd-chart` | Card, 8px radius. |
| 2 | Header | `.dd-chart-h` | Title (16px Archivo) + subtitle + range `.seg.sm` + Plot/Table toggle. |
| 3 | Hero figure | `.dd-hero` | 30px headline. |
| 4 | Plot | `.dd-plot` | SVG; grid in `--secondary-border`. |
| 5 | Tooltip | `.dd-tip` | Crosshair data tooltip (white, not the navy label tooltip). |
| 6 | Legend | `.dd-legend` | Swatch + name + value; direct labels at line ends. |
| 7 | Footer | `.dd-chart-f` | Source + updated time. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Line | `LineChart` | Revenue over time (single series `--chart-1`). |
| Bar | `BarChart layout=vertical` | Revenue by garment. |
| Funnel | `.dd-fun` | Sessions → paid. |
| Table view | `.dd-table` | Same numbers as a table. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Rows and cards that open something take `--muted` or lift; static blocks do not react. | `:hover` |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Not applicable. | — |
| disabled | Not wired: PostHog steps not connected. | `.sf-alert.wired` |
| error | Cause + Try again. | — |
| loading | Skeleton plot. | `.dd-sk` |
| empty | "No paid orders in this range". | — |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| background | #FFFFFF | `hsl(var(--card))` |
| border | 1px solid #E3E7ED | `1px solid hsl(var(--border))` |
| border-radius | 8px | `var(--r-card)` |
| hero figure | 30px | `--text-hero` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--s-2` | `8px` | 8px |
| `--s-3` | `12px` | 12px |
| `--r-chip` | `4px` | 4px |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--motion-skeleton` | `1.6s var(--ease-standard) infinite alternate` | 1.6s cubic-bezier(.2,0,0,1) infinite alternate |
| `--chart-1` | `219 61% 47%  (#2F62C1)` | #2F62C1 |
| `--card` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--r-card` | `8px` | 8px |
| `--s-4` | `16px` | 16px |
| `--w-semibold` | `600` | 600 |
| `--text-section` | `16px` | 16px |
| `--lh-section` | `1.25` | 1.25 |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--tr-section` | `-.005em` | -.005em |
| `--text-small` | `12px` | 12px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--chart-muted` | `221 25% 58%  (#798AAF)` | #798AAF |
| `--text-micro` | `11.5px` | 11.5px |
| `--font-ui` | `'IBM Plex Sans', system-ui, sans-serif` | 'IBM Plex Sans', system-ui, sans-serif |
| `--secondary-border` | `219 14% 79%  (#C2C7D1)` | #C2C7D1 |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--popover` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--e-pop` | `0 12px 32px -10px rgba(14,23,41,.25)` | 0 12px 32px -10px rgba(14,23,41,.25) |
| `--pop-shift` | `-4px` | -4px |
| `--motion-pop-out` | `110ms var(--ease-out)` | 110ms cubic-bezier(.4,0,1,1) |
| `--motion-pop-in` | `160ms var(--ease-in)` | 160ms cubic-bezier(.22,1,.36,1) |
| `--s-1` | `4px` | 4px |
| `--chart-2` | `166 76% 36%  (#16A281)` | #16A281 |
| `--chart-3` | `20 70% 53%  (#DB6B33)` | #DB6B33 |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--r-control` | `6px` | 6px |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--w-medium` | `500` | 500 |
| `--font-code` | `'IBM Plex Mono', ui-monospace, monospace` | 'IBM Plex Mono', ui-monospace, monospace |
| `--chart-4` | `229 62% 68%  (#7B8DE0)` | #7B8DE0 |
| `--chart-5` | `334 58% 58%  (#D2568C)` | #D2568C |
| `--text-hero` | `30px` | 30px |

### Accessibility

- **Keyboard:** ← → move the crosshair point; the table view is the accessible alternative.
- **ARIA:** SVG `role="img"` with a summary `aria-label`; table view is a real table.
- **Focus:** Plot is one focus stop.

### Content rules

- Title says the measure and unit ("Revenue, GBP").

**Do**

- Direct-label lines.
- Use `--chart-1` for one series.

**Don't**

- Status colours as series.
- Cycle the palette past 5.

### Build it

- **shadcn / Radix:** shadcn `chart` (`ChartContainer`, `ChartTooltip`, `ChartLegend`, Recharts).
- **Exists in apps/admin today:** no · new: Chart, ToggleGroup. Not in apps/admin.
- **Rendering:** client component (`"use client"`). 
- **Install:** `npx shadcn@latest add chart`

```ts
interface ChartFrameProps {
  title: string;
  subtitle?: string;
  hero?: string;                    // "£48,620"
  range?: { value: string; options: string[]; onChange: (v: string) => void };
  series: { key: string; label: string; colour: 1 | 2 | 3 | 4 | 5 | "muted"; dashed?: boolean }[];
  data: Record<string, number | string>[];
  kind: "line" | "bar" | "funnel";
  source?: string;
  state?: "ready" | "loading" | "empty" | "error" | "notWired";
}
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/)

---

<a id="kanban"></a>
## Kanban

*Data display · family `data-display` · page `components/kanban/` · phone `components/kanban/m/`*

Production stages as columns (Awaiting, Cutting, Stitching, Finishing, Packed, Shipped) with order cards you drag or move with the keyboard. WIP limits warn when a column is full or over. Phone: stage tabs.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Board | `.dd-board` | Horizontal columns. |
| 2 | Column | `.dd-col` | Header with name, count and WIP (8). |
| 3 | Card | `.dd-card.lift` | Order RJC-20418, garment thumb, customer initials, due date, flags. |
| 4 | Grip | `.dd-grip` | Drag handle. |
| 5 | More | `.dd-col-more` | "+ 5 more". |
| 6 | Phone stage tabs | `.dd-stabs` | One stage at a time. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Default | `.dd-col` |  |
| Full | `.dd-col.full` | At WIP limit: amber count. |
| Over | `.dd-col.over` | Over limit: red count. |
| Drop target | `.dd-col.is-drop` | Accent ground while dragging over. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Card lifts. | `.lift:hover` |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Dragging: card tilts and shadows; origin dashed. | `.dd-card.drag` |
| disabled | Blocked card: lock flag, cannot move. | — |
| error | Move failed: card snaps back + toast. | — |
| loading | `.skel` shapes in the final layout, pulsing on `--motion-skeleton`. | `.skel` |
| empty | Column "Nothing here". | `.dd-col-empty` |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 6px | `6px` |
| padding | 10px 12px 12px | `10px var(--s-3) var(--s-3)` |
| background | #FFFFFF | `hsl(var(--card))` |
| border | 1px solid #E3E7ED | `1px solid hsl(var(--border))` |
| border-radius | 8px | `var(--r-card)` |
| box-shadow | 0 1px 2px rgba(14,23,41,.04) | `var(--e-1)` |
| color | inherit | `inherit` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--text-micro` | `11.5px` | 11.5px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--s-3` | `12px` | 12px |
| `--s-4` | `16px` | 16px |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--s-2` | `8px` | 8px |
| `--r-card` | `8px` | 8px |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--s-1` | `4px` | 4px |
| `--w-semibold` | `600` | 600 |
| `--text-card` | `13.5px` | 13.5px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--w-medium` | `500` | 500 |
| `--font-ui` | `'IBM Plex Sans', system-ui, sans-serif` | 'IBM Plex Sans', system-ui, sans-serif |
| `--r-chip` | `4px` | 4px |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--warning` | `36 79% 29%  (#845610)` | #845610 |
| `--status-soft-alpha` | `.12` | .12 |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--text-small` | `12px` | 12px |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--secondary-border` | `219 14% 79%  (#C2C7D1)` | #C2C7D1 |
| `--r-control` | `6px` | 6px |
| `--card` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--e-1` | `0 1px 2px rgba(14,23,41,.04)` | 0 1px 2px rgba(14,23,41,.04) |
| `--motion-focus` | `140ms var(--ease-standard)` | 140ms cubic-bezier(.2,0,0,1) |
| `--motion-press` | `80ms var(--ease-standard)` | 80ms cubic-bezier(.2,0,0,1) |
| `--e-card-hover` | `0 10px 24px -14px rgba(14,23,41,.28)` | 0 10px 24px -14px rgba(14,23,41,.28) |
| `--focus-ring` | `0 0 0 1px hsl(var(--ring)), 0 0 0 4px hsl(var(--ring) / .14)` | 0 0 0 1px #3F60A6, 0 0 0 4px #3F60A6 @0.14 |
| `--e-pop` | `0 12px 32px -10px rgba(14,23,41,.25)` | 0 12px 32px -10px rgba(14,23,41,.25) |
| `--e-rail-strong-warning` | `inset 3px 0 0 hsl(var(--warning))` | inset 3px 0 0 #845610 |
| `--e-rail-strong-destructive` | `inset 3px 0 0 hsl(var(--destructive))` | inset 3px 0 0 #A43240 |
| `--control-h-m` | `44px` | 44px |
| `--text-btn-sm` | `12.5px` | 12.5px |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--primary-foreground` | `60 17% 98%  (#FBFBF9)` | #FBFBF9 |
| `--text-10` | `10px` | 10px |
| `--text-body` | `13px` | 13px |

### Accessibility

- **Keyboard:** Space lifts a card; ← → change column; ↑ ↓ reorder; Space drops; Escape cancels.
- **ARIA:** Live region announces moves ("RJC-20418 moved to Stitching").
- **Focus:** Focus stays on the moved card.

### Content rules

- Stage names are production stages, not status chips.

**Do**

- Confirm moves that notify the customer.

**Don't**

- Columns that scroll vertically inside a scrolling page without a header.

### Build it

- **shadcn / Radix:** `@dnd-kit/core` + `sortable` (installed) with `Card`; phone `Tabs`.
- **Exists in apps/admin today:** Card. @dnd-kit installed.
- **Rendering:** client component (`"use client"`). 

```ts
interface KanbanProps<T> {
  stages: { id: string; label: string; wip?: number }[];
  items: T[];
  getStage: (item: T) => string;
  renderCard: (item: T) => React.ReactNode;
  onMove: (itemId: string, toStage: string, index: number) => Promise<void>;
}
```

### Used on

[Dashboard](../../phase-3/pages/dashboard/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/)

---

<a id="timeline"></a>
## Timeline and activity feed

*Data display · family `data-display` · page `components/timeline/` · phone `components/timeline/m/`*

An order's history as a vertical timeline (placed, paid, cutting started…) and the activity feed grouped by day.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Rail | `.dd-tl` | Vertical line with dots. |
| 2 | Event | `.dd-tl-b` | Title + who + time. |
| 3 | Day group | `.dd-feed-day` | "Today", "Yesterday", "2 Oct". |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Timeline | `.dd-tl` | One record. |
| Feed | `.dd-feed` | Many records. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Rows and cards that open something take `--muted` or lift; static blocks do not react. | `:hover` |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Not applicable. | — |
| disabled | Not applicable: hide data the person cannot see, never grey it. | — |
| error | Names the real cause and offers Try again (see Error states). | `.sf-alert.danger` |
| loading | `.skel` shapes in the final layout, pulsing on `--motion-skeleton`. | `.skel` |
| empty | Says what is missing and the one next step (see Empty state). | `.sf-empty` |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| padding | 4px 0 | `var(--s-1) 0` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--s-1` | `4px` | 4px |
| `--secondary-border` | `219 14% 79%  (#C2C7D1)` | #C2C7D1 |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--r-pill` | `999px` | 999px |
| `--s-3` | `12px` | 12px |
| `--s-4` | `16px` | 16px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--success` | `158 51% 26%  (#20644B)` | #20644B |
| `--status-soft-alpha` | `.12` | .12 |
| `--info` | `218 49% 39%  (#335694)` | #335694 |
| `--warning` | `36 79% 29%  (#845610)` | #845610 |
| `--primary-foreground` | `60 17% 98%  (#FBFBF9)` | #FBFBF9 |
| `--s-2` | `8px` | 8px |
| `--w-semibold` | `600` | 600 |
| `--text-body` | `13px` | 13px |
| `--font-ui` | `'IBM Plex Sans', system-ui, sans-serif` | 'IBM Plex Sans', system-ui, sans-serif |
| `--text-small` | `12px` | 12px |
| `--w-medium` | `500` | 500 |
| `--text-micro` | `11.5px` | 11.5px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |

### Accessibility

- **Keyboard:** Links inside events.
- **ARIA:** `<ol>`; times in `<time>`.
- **Focus:** —

### Content rules

- Past tense, who first: "Camilla marked it Paid".

**Do**

- Group by day.

**Don't**

- Show raw event names.

### Build it

- **shadcn / Radix:** Custom list.
- **Exists in apps/admin today:** no. Not in apps/admin.
- **Rendering:** server component. 

```ts
interface TimelineEvent { id: string; at: Date; actor?: string; title: string; detail?: string; tone?: "default" | "success" | "warning" | "danger" }
interface TimelineProps { events: TimelineEvent[]; groupByDay?: boolean }
```

### Used on

[RJC-20418](../../phase-3/pages/order-detail/), [Orders](../../phase-3/pages/orders/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Variant](../../phase-3/pages/variant-detail/)

---

<a id="audit-row"></a>
## Audit row

*Data display · family `data-display` · page `components/audit-row/` · phone `components/audit-row/m/`*

One audit-log entry that expands to show the field diff (old struck, new highlighted) and the raw JSON with request id. Phone: cards.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Row | `.dd-audit-h` | Time, who, action, entity, request; chevron. |
| 2 | Diff | `.dd-diff` | Field · old (struck, muted) → new. |
| 3 | Raw | `.dd-raw` | JSON in Plex Mono with Copy. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Collapsed | `.dd-audit` |  |
| Expanded | `.dd-audit[open]` |  |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Rows and cards that open something take `--muted` or lift; static blocks do not react. | `:hover` |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Expanded. | `[open]` |
| disabled | Not applicable: hide data the person cannot see, never grey it. | — |
| error | Names the real cause and offers Try again (see Error states). | `.sf-alert.danger` |
| loading | Skeleton rows. | — |
| empty | "No changes in this range". | — |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| width | 100% | `100%` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--s-3` | `12px` | 12px |
| `--s-4` | `16px` | 16px |
| `--row-th` | `36px` | 36px |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--w-semibold` | `600` | 600 |
| `--text-micro` | `11.5px` | 11.5px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--row` | `44px` | 44px |
| `--text-body` | `13px` | 13px |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--font-code` | `'IBM Plex Mono', ui-monospace, monospace` | 'IBM Plex Mono', ui-monospace, monospace |
| `--text-small` | `12px` | 12px |
| `--r-control` | `6px` | 6px |
| `--card` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--s-2` | `8px` | 8px |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--success` | `158 51% 26%  (#20644B)` | #20644B |
| `--s-1` | `4px` | 4px |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--w-medium` | `500` | 500 |
| `--r-card` | `8px` | 8px |
| `--control-h-m` | `44px` | 44px |
| `--r-chip` | `4px` | 4px |

### Accessibility

- **Keyboard:** Enter toggles.
- **ARIA:** `<details>`/Collapsible with `aria-expanded`.
- **Focus:** Ring on the row header.

### Content rules

- Action verbs: created, updated, published, retired.

**Do**

- Show request id for support.

**Don't**

- Hide who did it.

### Build it

- **shadcn / Radix:** shadcn `Collapsible` (Radix).
- **Exists in apps/admin today:** no · new: Collapsible. Not in apps/admin (Accordion exists).
- **Rendering:** client component (`"use client"`). 
- **Install:** `npx shadcn@latest add collapsible`

```ts
interface AuditEntry { id: string; at: Date; actor: string; action: string; entity: { type: string; id: string }; requestId?: string; changes: { field: string; from: unknown; to: unknown }[]; raw?: unknown }
interface AuditRowProps { entry: AuditEntry; defaultOpen?: boolean }
```

### Used on

[Envelope audit](../../phase-3/pages/order-envelope/), [Audit log](../../phase-3/pages/settings-audit-log/)

---

<a id="description-list"></a>
## Description list

*Data display · family `data-display` · page `components/description-list/` · phone `components/description-list/m/`*

Facts about a record in panels (Customer, Delivery, Payment): label left, value right, codes in mono.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Panel | `.dd-panel` | Card with a header. |
| 2 | List | `.dd-dl` | `dt` / `dd` grid. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Two-column | `.dd-dl` |  |
| Stacked (phone) | `.dd-dl` | Label above value. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Rows and cards that open something take `--muted` or lift; static blocks do not react. | `:hover` |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Not applicable. | — |
| disabled | Not applicable: hide data the person cannot see, never grey it. | — |
| error | Names the real cause and offers Try again (see Error states). | `.sf-alert.danger` |
| loading | `.skel` shapes in the final layout, pulsing on `--motion-skeleton`. | `.skel` |
| empty | Says what is missing and the one next step (see Empty state). | `.sf-empty` |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| grid-template-columns | minmax(120px,40%) minmax(0,1fr) | `minmax(120px,40%) minmax(0,1fr)` |
| font-size | 13px | `var(--text-body)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--card` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--r-card` | `8px` | 8px |
| `--s-2` | `8px` | 8px |
| `--s-3` | `12px` | 12px |
| `--s-4` | `16px` | 16px |
| `--text-body` | `13px` | 13px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--text-micro` | `11.5px` | 11.5px |
| `--w-semibold` | `600` | 600 |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--text-section` | `16px` | 16px |
| `--success` | `158 51% 26%  (#20644B)` | #20644B |

### Accessibility

- **Keyboard:** —
- **ARIA:** `<dl>`.
- **Focus:** —

### Content rules

- Labels are nouns.

**Do**

- Copy buttons on ids.

**Don't**

- Editable fields inside a description list.

### Build it

- **shadcn / Radix:** `Card` + `<dl>`.
- **Exists in apps/admin today:** Card. Card exists.
- **Rendering:** server component. 

```ts
interface DescriptionListProps { title?: string; items: { label: string; value: React.ReactNode; mono?: boolean; copy?: boolean }[] }
```

### Used on

[Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [RJC-20418](../../phase-3/pages/order-detail/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [Releases](../../phase-3/pages/releases/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Profile](../../phase-3/pages/settings-profile/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [Website](../../phase-3/pages/website/)

---

<a id="key-value"></a>
## Key/value aside

*Data display · family `page-primitives` · page `components/key-value/` · phone `components/key-value/m/`*

Kit v2: the compact aside on detail pages (order id, Stripe id, ERP id…). Labels hold a 120px column; long ids wrap anywhere instead of overflowing.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | List | `dl.k-kv` | Column. |
| 2 | Row | `.k-kv>div` | Grid `120px 1fr`, 12px gap, 8px vertical padding, hairline. |
| 3 | Value | `dd` | `overflow-wrap: anywhere`; `.mono` for ids. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Desktop | `.k-kv` | 120px labels. |
| Phone | `.kit-m .k-kv` | 104px labels. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Rows and cards that open something take `--muted` or lift; static blocks do not react. | `:hover` |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Not applicable. | — |
| disabled | Not applicable: hide data the person cannot see, never grey it. | — |
| error | Names the real cause and offers Try again (see Error states). | `.sf-alert.danger` |
| loading | `.skel` shapes in the final layout, pulsing on `--motion-skeleton`. | `.skel` |
| empty | Says what is missing and the one next step (see Empty state). | `.sf-empty` |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| label column | 120px (104px phone) | `--kv-label / --kv-label-m` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--kv-label` | `120px` | 120px |
| `--s-3` | `12px` | 12px |
| `--s-2` | `8px` | 8px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--text-body` | `13px` | 13px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--font-code` | `'IBM Plex Mono', ui-monospace, monospace` | 'IBM Plex Mono', ui-monospace, monospace |
| `--text-small` | `12px` | 12px |
| `--kv-label-m` | `104px` | 104px |

### Accessibility

- **Keyboard:** —
- **ARIA:** `<dl>`.
- **Focus:** —

### Content rules

- Ids in Plex Mono.

**Do**

- Let ids wrap.

**Don't**

- Truncate ids without a copy button.

### Build it

- **shadcn / Radix:** Plain `<dl>`.
- **Exists in apps/admin today:** no. —
- **Rendering:** server component. 

```ts
interface KeyValueProps { items: { label: string; value: React.ReactNode; mono?: boolean }[] }
```

### Used on

[Envelope audit](../../phase-3/pages/order-envelope/)

---

<a id="thread"></a>
## Support thread and suggested reply

*Data display · family `data-display` · page `components/thread/` · phone `components/thread/m/`*

A support conversation with the case rail (customer, order, SLA) and the AI suggested reply: confidence, sources, Insert into reply, and states for loading, low confidence and failed.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Messages | `.dd-msg` | Customer left, staff right; who + time. |
| 2 | Composer | `.dd-compose` | Textarea + Send (sticky on the phone). |
| 3 | Suggested reply | `.dd-ai` | Header with confidence, body, sources, Insert. |
| 4 | Case rail | `.dd-rail` | Customer, order, status chips. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Desktop with rail | `.dd-ticket` |  |
| Phone full width | `.dd-mcase` | Rail collapses into a header. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Rows and cards that open something take `--muted` or lift; static blocks do not react. | `:hover` |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Not applicable. | — |
| disabled | Low confidence: muted with a warning. | — |
| error | "Could not draft a reply" + Try again. | — |
| loading | "Drafting a reply…" with spinner. | `.dd-ai-load` |
| empty | No suggestion for this ticket. | — |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 16px | `var(--s-4)` |
| padding | 20px | `var(--s-5)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--s-1` | `4px` | 4px |
| `--text-micro` | `11.5px` | 11.5px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--s-2` | `8px` | 8px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--s-4` | `16px` | 16px |
| `--s-5` | `20px` | 20px |
| `--text-small` | `12px` | 12px |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--w-semibold` | `600` | 600 |
| `--text-20` | `20px` | 20px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--s-3` | `12px` | 12px |
| `--r-card` | `8px` | 8px |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--text-body` | `13px` | 13px |
| `--lh-body` | `1.45` | 1.45 |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--info` | `218 49% 39%  (#335694)` | #335694 |
| `--card` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--e-1` | `0 1px 2px rgba(14,23,41,.04)` | 0 1px 2px rgba(14,23,41,.04) |
| `--locked` | `var(--neutral)` | #575E70 |
| `--warning` | `36 79% 29%  (#845610)` | #845610 |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--success` | `158 51% 26%  (#20644B)` | #20644B |
| `--w-medium` | `500` | 500 |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--input` | `219 14% 59%  (#8892A5)` | #8892A5 |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--motion-focus` | `140ms var(--ease-standard)` | 140ms cubic-bezier(.2,0,0,1) |
| `--focus-ring` | `0 0 0 1px hsl(var(--ring)), 0 0 0 4px hsl(var(--ring) / .14)` | 0 0 0 1px #3F60A6, 0 0 0 4px #3F60A6 @0.14 |

### Accessibility

- **Keyboard:** ⌘/Ctrl + Enter sends.
- **ARIA:** Messages `role="log"`; suggestion is a region labelled "Suggested reply".
- **Focus:** Insert moves focus into the composer.

### Content rules

- Support status: Waiting on us, Resolved.

**Do**

- Show sources for AI text.

**Don't**

- Auto-send suggested replies.

### Build it

- **shadcn / Radix:** Composed: `Textarea`, `Avatar`, `Badge`, `Button`.
- **Exists in apps/admin today:** Badge · new: Textarea, Avatar. Badge exists.
- **Rendering:** client component (`"use client"`). 

```ts
interface ThreadProps {
  messages: { id: string; from: "customer" | "staff"; author: string; at: Date; body: string }[];
  suggestion?: { state: "loading" | "ready" | "low" | "error"; body?: string; confidence?: number; sources?: { label: string; href: string }[] };
  onInsert?: (text: string) => void;
  onSend: (text: string) => Promise<void>;
}
```

### Used on

[Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/)

---

<a id="guided-editor"></a>
## Guided editor

*Wizard and stepper · family `wizard-stepper` · page `components/guided-editor/` · phone `components/guided-editor/m/`*

The long Global Store product editor as a guided flow: a step rail on the left, section cards that show complete, current, error or locked, and the sticky action bar with one primary. Phone: one section at a time.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Rail | `.wz-ed-rail` | Sticky step rail (see Step rail). |
| 2 | Section card | `.wz-sec` | Header with mark + title + summary; body with fields. |
| 3 | Action bar | `.wz-bar` | Sticky footer with save state and one primary. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Complete | `.wz-sec.is-complete` | Collapsed to a summary line + Edit. |
| Current | `.wz-sec.is-current` | Open, ring edge. |
| Error | `.wz-sec.is-error` | Destructive edge + count of problems. |
| Locked | `.wz-sec.is-locked` | Needs an earlier step; muted with the reason. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Holds its children at the documented spacing. | — |
| hover | Not applicable. | — |
| focus | Not applicable; children carry focus. | — |
| active | Not applicable. | — |
| disabled | Locked section. | `.is-locked` |
| error | Section edge destructive; validation summary lists fields. | `.is-error` |
| loading | Saving: action bar shows Saving… | `.wz-save` |
| empty | Shows the compact empty state inside. | `.sf-empty.compact` |
| read-only | Children render as values, not inputs. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| border | 1px solid #E3E7ED | `1px solid hsl(var(--border))` |
| border-radius | 8px | `var(--r-card)` |
| background | #FFFFFF | `hsl(var(--card))` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--primary-foreground` | `60 17% 98%  (#FBFBF9)` | #FBFBF9 |
| `--r-card` | `8px` | 8px |
| `--card` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--s-4` | `16px` | 16px |
| `--s-3` | `12px` | 12px |
| `--s-2` | `8px` | 8px |
| `--text-card` | `13.5px` | 13.5px |
| `--w-semibold` | `600` | 600 |
| `--text-small` | `12px` | 12px |
| `--e-1` | `0 1px 2px rgba(14,23,41,.04)` | 0 1px 2px rgba(14,23,41,.04) |
| `--text-section` | `16px` | 16px |
| `--s-5` | `20px` | 20px |
| `--s-6` | `24px` | 24px |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |

### Accessibility

- **Keyboard:** Rail links jump to sections; Tab through fields in order.
- **ARIA:** Each section `<section aria-labelledby>`; rail `aria-current="step"`.
- **Focus:** Jumping focuses the section heading.

### Content rules

- Section titles: Pick a product, Options & prices, Price & combos, Photos, Review & publish.

**Do**

- Let people move freely between complete sections.

**Don't**

- Force a linear wizard when sections are independent.

### Build it

- **shadcn / Radix:** No shadcn stepper: an `<ol>` of step buttons with `aria-current="step"`; Radix `Progress` for meters.
- **Exists in apps/admin today:** Card, Progress. Card and Progress exist.
- **Rendering:** client component (`"use client"`). 

```ts
interface GuidedEditorProps {
  sections: { id: string; title: string; state: "upcoming" | "current" | "complete" | "error" | "locked"; summary?: string; lockedReason?: string; errors?: number; content: React.ReactNode }[];
  current: string;
  onNavigate: (id: string) => void;
  actionBar: React.ReactNode;
}
```

### Used on

[Create a brand](../../phase-3/pages/settings-brand-new/)

---

<a id="stepper"></a>
## Stepper

*Wizard and stepper · family `wizard-stepper` · page `components/stepper/` · phone `components/stepper/m/`*

Horizontal steps for short linear flows (Create product: Pick, Read, Confirm): numbered marks for upcoming, current, complete, error and locked. Phone: a compact "Step 2 of 3" indicator.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | List | `.wz-steps` | `<ol>` row with connecting lines. |
| 2 | Mark | `.wz-mark` | Circle: number, tick (complete), ! (error), lock (locked). |
| 3 | Label | `.wz-st` | Archivo step name + optional sub-line. |
| 4 | Phone indicator | `.wz-ci` | "Step 2 of 3" + title at `--text-step-m` 17px. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Upcoming | `.is-upcoming` | Outline mark. |
| Current | `.is-current` | Navy mark, semibold label. |
| Complete | `.is-complete` | Navy tick, link back. |
| Error | `.is-error` | Destructive mark. |
| Locked | `.is-locked` | Lock icon, not clickable. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Muted label, icon at 80% opacity. | — |
| hover | Soft fill (`--muted`, or sidebar foreground at 6%). | `:hover · .is-hover` |
| focus | 2px ring outline (`--ring`, `--sidebar-ring` on navy). | `:focus-visible · .is-focus` |
| active | Current step `aria-current="step"`. | `.is-current` |
| disabled | Locked. | `.is-locked` |
| error | Error mark + label in destructive. | `.is-error` |
| loading | Current mark shows a spinner while the step loads. | `.spin` |
| empty | Not applicable: navigation is never empty. | — |
| read-only | Not applicable. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| width | 24px | `24px` |
| height | 24px | `24px` |
| border-radius | 999px | `var(--r-pill)` |
| font-weight | 600 | `var(--w-semibold)` |
| font-size | 12px | `var(--text-small)` |
| line-height | 1 | `1` |
| border | 1px solid #8892A5 | `1px solid hsl(var(--input))` |
| color | #505B71 | `hsl(var(--muted-foreground))` |
| background | #FFFFFF | `hsl(var(--background))` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--r-pill` | `999px` | 999px |
| `--w-semibold` | `600` | 600 |
| `--text-small` | `12px` | 12px |
| `--input` | `219 14% 59%  (#8892A5)` | #8892A5 |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--s-2` | `8px` | 8px |
| `--s-6` | `24px` | 24px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--motion-sheet-in` | `260ms var(--ease-in)` | 260ms cubic-bezier(.22,1,.36,1) |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--w-medium` | `500` | 500 |
| `--text-code` | `12px` | 12px |
| `--font-code` | `'IBM Plex Mono', ui-monospace, monospace` | 'IBM Plex Mono', ui-monospace, monospace |
| `--warning` | `36 79% 29%  (#845610)` | #845610 |
| `--s-3` | `12px` | 12px |
| `--s-4` | `16px` | 16px |
| `--success` | `158 51% 26%  (#20644B)` | #20644B |
| `--r-card` | `8px` | 8px |
| `--text-step-m` | `17px` | 17px |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |

### Accessibility

- **Keyboard:** Complete steps are buttons; others are not focusable.
- **ARIA:** `<ol>`; `aria-current="step"`; state in text ("completed").
- **Focus:** Ring on step buttons.

### Content rules

- Two-word step names.

**Do**

- Three to five steps.

**Don't**

- A stepper for independent sections (use the guided editor rail).

### Build it

- **shadcn / Radix:** No shadcn stepper: an `<ol>` of step buttons with `aria-current="step"`; Radix `Progress` for meters.
- **Exists in apps/admin today:** no. Not in apps/admin.
- **Rendering:** client component (`"use client"`). 

```ts
type StepState = "upcoming" | "current" | "complete" | "error" | "locked";
interface StepperProps { steps: { id: string; label: string; state: StepState; sub?: string }[]; onStepClick?: (id: string) => void; compact?: boolean }
```

### Used on

[Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Bulk price import](../../phase-3/pages/pricing-bulk/)

---

<a id="step-rail"></a>
## Step rail and section progress

*Wizard and stepper · family `wizard-stepper` · page `components/step-rail/` · phone `components/step-rail/m/`*

The vertical rail beside a guided editor (sections with marks and summaries) and the segmented progress meter (navy done, ring current, red error, border to do).

> **Open in the kit.** No named token for the current segment colour (uses `--ring`); a `--progress-current` alias is proposed (wizard-stepper note 4).

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Rail | `.wz-rail` | Sticky column. |
| 2 | Item | `.wz-ri` | Mark + title + one-line summary. |
| 3 | Meter | `.wz-meter` | One segment per section. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Rail | `.wz-rail` | Desktop. |
| Meter | `.wz-meter` | Top of the phone flow. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Muted label, icon at 80% opacity. | — |
| hover | Soft fill (`--muted`, or sidebar foreground at 6%). | `:hover · .is-hover` |
| focus | 2px ring outline (`--ring`, `--sidebar-ring` on navy). | `:focus-visible · .is-focus` |
| active | Current item `--accent` ground. | `[aria-current=step]` |
| disabled | Muted at 45%, not focusable, reason in a tooltip. | `[aria-disabled=true]` |
| error | Red segment and mark. | `.is-error` |
| loading | Skeleton lines while the place's menu loads. | `.skel` |
| empty | Not applicable: navigation is never empty. | — |
| read-only | Not applicable. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| grid-template-columns | 20px minmax(0,1fr) auto | `20px minmax(0,1fr) auto` |
| gap | 10px | `10px` |
| min-height | 38px | `38px` |
| padding | 8px | `var(--s-2)` |
| border-radius | 6px | `var(--r-control)` |
| color | #0F1524 | `hsl(var(--foreground))` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--primary-foreground` | `60 17% 98%  (#FBFBF9)` | #FBFBF9 |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--s-2` | `8px` | 8px |
| `--s-4` | `16px` | 16px |
| `--text-card` | `13.5px` | 13.5px |
| `--w-semibold` | `600` | 600 |
| `--text-small` | `12px` | 12px |
| `--r-chip` | `4px` | 4px |
| `--motion-sheet-in` | `260ms var(--ease-in)` | 260ms cubic-bezier(.22,1,.36,1) |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--r-control` | `6px` | 6px |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--text-11` | `11px` | 11px |
| `--w-medium` | `500` | 500 |
| `--text-micro` | `11.5px` | 11.5px |
| `--r-card` | `8px` | 8px |
| `--s-3` | `12px` | 12px |
| `--card` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--control-h-m` | `44px` | 44px |
| `--motion-pop-in` | `160ms var(--ease-in)` | 160ms cubic-bezier(.22,1,.36,1) |

### Accessibility

- **Keyboard:** Items are links to sections.
- **ARIA:** Meter: `role="progressbar"` with `aria-valuetext="3 of 6 sections complete"`.
- **Focus:** Ring.

### Content rules

- Summaries are the chosen values ("VBC Perennial · 3 cloths").

**Do**

- Keep the rail sticky.

**Don't**

- Use percentages.

### Build it

- **shadcn / Radix:** Rail = nav list; meter = segmented `Progress`.
- **Exists in apps/admin today:** Progress. Progress exists.
- **Rendering:** server component. 

```ts
interface StepRailProps { sections: { id: string; title: string; state: StepState; summary?: string }[]; current: string; onNavigate: (id: string) => void }
```

### Used on

[Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Create a brand](../../phase-3/pages/settings-brand-new/)

---

<a id="step-header"></a>
## Step header

*Wizard and stepper · family `wizard-stepper` · page `components/step-header/` · phone `components/step-header/m/`*

The top of each step: step number, title, one sentence of what to do, and the single next step named in the primary.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Kicker | `.wz-k` | "Step 2 of 3". |
| 2 | Title | `.wz-t` | Archivo 600. |
| 3 | Lede | `.wz-lede` | One sentence. |
| 4 | Hint (optional) | `.wz-hint` | Muted tip. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Desktop | `.wz-head` |  |
| Phone | `.wz-ci` | Compact. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Holds its children at the documented spacing. | — |
| hover | Not applicable. | — |
| focus | Not applicable; children carry focus. | — |
| active | Not applicable. | — |
| disabled | Not applicable. | — |
| error | Children report errors; the container may add a summary at the top. | — |
| loading | Children show skeletons in place. | `.skel` |
| empty | Shows the compact empty state inside. | `.sf-empty.compact` |
| read-only | Children render as values, not inputs. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 32px | `var(--s-8)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--s-8` | `32px` | 32px |
| `--text-small` | `12px` | 12px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--s-1` | `4px` | 4px |
| `--title-w` | `600` | 600 |
| `--text-page` | `26px` | 26px |
| `--lh-page` | `1.15` | 1.15 |
| `--tr-page` | `-.015em` | -.015em |
| `--text-body` | `13px` | 13px |

### Accessibility

- **Keyboard:** —
- **ARIA:** Title is the step `h2`; focus moves to it on step change.
- **Focus:** Heading `tabIndex=-1` receives focus.

### Content rules

- Lede says the outcome, not the UI.

**Do**

- Name the next step in the primary ("Continue to prices").

**Don't**

- Paragraphs of instructions.

### Build it

- **shadcn / Radix:** Layout.
- **Exists in apps/admin today:** no. —
- **Rendering:** server component. 

```ts
interface StepHeaderProps { index: number; total: number; title: string; lede?: string; hint?: string }
```

### Used on

[Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Global Store product editor](../../phase-3/pages/global-store-editor/)

---

<a id="wizard-action-bar"></a>
## Wizard action bar

*Wizard and stepper · family `wizard-stepper` · page `components/wizard-action-bar/` · phone `components/wizard-action-bar/m/`*

The sticky bar under a flow: Back on the left, the save state (saved, unsaved, saving, failed) in the middle, and the one primary on the right. Phone: full width, 48px buttons, tab bar hidden.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Bar | `.wz-bar` | Sticky bottom, hairline top, blur. |
| 2 | Back | `.btn.btn-ghost` |  |
| 3 | Save state | `.wz-save` | Dot + "Saved 14:20" / "Unsaved changes" / "Saving…" / "Could not save". |
| 4 | Primary | `.btn.btn-primary` | Names the next step. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Desktop | `.wz-bar` |  |
| Phone | `.wz-mbar` | Full-width primary, Back as icon. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Holds its children at the documented spacing. | — |
| hover | Not applicable. | — |
| focus | Not applicable; children carry focus. | — |
| active | Not applicable. | — |
| disabled | Primary disabled until the step is valid, with a reason. | `[disabled]` |
| error | Could not save + Try again. | `.wz-save.failed` |
| loading | Saving… | `.wz-save.saving` |
| empty | Shows the compact empty state inside. | `.sf-empty.compact` |
| read-only | Children render as values, not inputs. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 8px | `var(--s-2)` |
| padding | 12px 24px | `var(--s-3) var(--s-6)` |
| background | #FFFFFF @0.95 | `hsl(var(--background)/.95)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--s-2` | `8px` | 8px |
| `--text-small` | `12px` | 12px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--w-semibold` | `600` | 600 |
| `--s-3` | `12px` | 12px |
| `--s-6` | `24px` | 24px |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--actbar-blur` | `8px` | 8px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--success` | `158 51% 26%  (#20644B)` | #20644B |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--s-1` | `4px` | 4px |
| `--s-4` | `16px` | 16px |
| `--control-h-m-lg` | `48px` | 48px |

### Accessibility

- **Keyboard:** Enter in the last field does not skip validation.
- **ARIA:** Save state `aria-live="polite"`.
- **Focus:** On Continue, focus moves to the next step heading.

### Content rules

- Primary: "Continue to prices", last step "Publish to brands".

**Do**

- One primary.

**Don't**

- Next and Finish both visible.

### Build it

- **shadcn / Radix:** Custom bar (shares anatomy with Save bar).
- **Exists in apps/admin today:** no. —
- **Rendering:** client component (`"use client"`). 

```ts
interface WizardActionBarProps { onBack?: () => void; save: "saved" | "unsaved" | "saving" | "failed"; savedAt?: Date; primaryLabel: string; onPrimary: () => void; primaryDisabledReason?: string }
```

### Used on

[Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [Create a brand](../../phase-3/pages/settings-brand-new/)

---

<a id="step-validation"></a>
## Step validation

*Wizard and stepper · family `wizard-stepper` · page `components/step-validation/` · phone `components/step-validation/m/`*

When Continue is pressed with problems: a summary at the top lists each problem as a jump link, and each field shows its own error. Focus moves to the summary.

> **Open in the kit.** Wizard-specific; could become an Alert with a list slot (wizard-stepper note 6).

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Summary | `.wz-errs` | Danger-tinted box with title ("2 things to fix") and a list of links. |
| 2 | Field errors | `.help.err` | Under each field. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Summary + fields | `.wz-errs` |  |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Rows and cards that open something take `--muted` or lift; static blocks do not react. | `:hover` |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Not applicable. | — |
| disabled | Not applicable: hide data the person cannot see, never grey it. | — |
| error | This is the error state. | `.wz-errs` |
| loading | `.skel` shapes in the final layout, pulsing on `--motion-skeleton`. | `.skel` |
| empty | Says what is missing and the one next step (see Empty state). | `.sf-empty` |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| gap | 12px | `var(--s-3)` |
| padding | 12px 16px | `var(--s-3) var(--s-4)` |
| border | 1px solid #A43240 @0.35 | `1px solid hsl(var(--destructive)/.35)` |
| background | #A43240 @0.05 | `hsl(var(--destructive)/.05)` |
| border-radius | 8px | `var(--r-card)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--s-3` | `12px` | 12px |
| `--s-4` | `16px` | 16px |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--r-card` | `8px` | 8px |
| `--text-card` | `13.5px` | 13.5px |
| `--w-semibold` | `600` | 600 |
| `--s-1` | `4px` | 4px |
| `--text-small` | `12px` | 12px |

### Accessibility

- **Keyboard:** Links jump to fields and focus them.
- **ARIA:** Summary `role="alert"` with `tabIndex=-1`, focused on submit.
- **Focus:** Summary first, then the field from a link.

### Content rules

- "Add a GBP price", "Choose at least one cloth".

**Do**

- Match summary text to field errors.

**Don't**

- Clear the summary until the problems are fixed.

### Build it

- **shadcn / Radix:** `Alert` destructive + react-hook-form errors.
- **Exists in apps/admin today:** Alert. Alert exists.
- **Rendering:** client component (`"use client"`). 

```ts
interface ValidationSummaryProps { errors: { fieldId: string; message: string }[] }
```

### Used on

[Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [Create a brand](../../phase-3/pages/settings-brand-new/)

---

<a id="review-step"></a>
## Review before publish

*Wizard and stepper · family `wizard-stepper` · page `components/review-step/` · phone `components/review-step/m/`*

The last step: every section summarised with Edit links, the changes since the live version highlighted, and Publish to brands as the single primary.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Summary blocks | `.wz-rv` | One per section with Edit. |
| 2 | Change marks | `.wz-chg` | "Changed since v4". |
| 3 | Publish | `.btn.btn-primary` | In the action bar. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| New product | `.wz-rv` | No diff. |
| Edit of a live product | `.wz-rv + .wz-chg` | Diffs shown. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Rows and cards that open something take `--muted` or lift; static blocks do not react. | `:hover` |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Not applicable. | — |
| disabled | Not applicable: hide data the person cannot see, never grey it. | — |
| error | Publish failed alert with the cause. | — |
| loading | Publishing… then a toast + redirect. | `[aria-busy=true]` |
| empty | Says what is missing and the one next step (see Empty state). | `.sf-empty` |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| grid-template-columns | minmax(0,1.55fr) minmax(0,1fr) | `minmax(0,1.55fr) minmax(0,1fr)` |
| gap | 24px | `var(--s-6)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--font-stretch-title` | `100%` | 100% |
| `--s-5` | `20px` | 20px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--r-card` | `8px` | 8px |
| `--s-4` | `16px` | 16px |
| `--s-2` | `8px` | 8px |
| `--s-6` | `24px` | 24px |
| `--text-card` | `13.5px` | 13.5px |
| `--w-semibold` | `600` | 600 |
| `--text-small` | `12px` | 12px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |

### Accessibility

- **Keyboard:** Edit links return to the section.
- **ARIA:** Each block a `<section>` with heading.
- **Focus:** After Edit and back, focus returns to that block.

### Content rules

- Name the audience: "Publish to 3 brands".

**Do**

- Show what changes for customers.

**Don't**

- Surprise: no hidden side effects.

### Build it

- **shadcn / Radix:** `Card` blocks.
- **Exists in apps/admin today:** Card. Card exists.
- **Rendering:** server component. 

```ts
interface ReviewStepProps { blocks: { id: string; title: string; rows: { label: string; value: React.ReactNode; changed?: boolean }[]; onEdit: () => void }[]; publishLabel: string }
```

### Used on

[Global Store product editor](../../phase-3/pages/global-store-editor/)

---

<a id="step-transition"></a>
## Step transition

*Wizard and stepper · family `wizard-stepper` · page `components/step-transition/` · phone `components/step-transition/m/`*

Calm motion between steps: the incoming pane fades in and settles 8px in the direction of travel on the sheet timing. Instant under reduced motion.

> **Open in the kit.** No `--motion-step-in` token or `[data-k-steps]` hook in kit.js yet; built from existing tokens (wizard-stepper note 1).

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Stage | `.wz-stage` | Clips the panes. |
| 2 | Pane | `.wz-pane` | One step's content. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Forward | `translateX(8px → 0)` |  |
| Back | `translateX(−8px → 0)` |  |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Rows and cards that open something take `--muted` or lift; static blocks do not react. | `:hover` |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Not applicable. | — |
| disabled | Not applicable: hide data the person cannot see, never grey it. | — |
| error | Names the real cause and offers Try again (see Error states). | `.sf-alert.danger` |
| loading | `.skel` shapes in the final layout, pulsing on `--motion-skeleton`. | `.skel` |
| empty | Says what is missing and the one next step (see Empty state). | `.sf-empty` |
| read-only | The default: display components never edit in place. | — |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--motion-sheet-in` | `260ms var(--ease-in)` | 260ms cubic-bezier(.22,1,.36,1) |
| `--toast-shift` | `8px` | 8px |

### Motion

- Fade + `--toast-shift` (8px) settle on `--motion-sheet-in` (260ms, `--ease-in`), direction-aware.
- Reduced motion: tokens collapse to 1ms, 0px.

### Accessibility

- **Keyboard:** —
- **ARIA:** Announce the new step title (focus the heading).
- **Focus:** Heading.

### Content rules

- —

**Do**

- Keep motion under 300ms.

**Don't**

- Slide whole pages.

### Build it

- **shadcn / Radix:** CSS transition on the step container (or `framer-motion` if already present).
- **Exists in apps/admin today:** no. —
- **Rendering:** client component (`"use client"`). 

```ts
interface StepTransitionProps { stepKey: string; direction: 1 | -1; children: React.ReactNode }
```

### Used on

[Global Store product editor](../../phase-3/pages/global-store-editor/)

---

<a id="help-trigger"></a>
## Help trigger

*Page help · family `page-help` · page `components/help-trigger/` · phone `components/help-trigger/m/`*

The round "?" in the top bar, in the same spot on every page (right, before the account). 32px with a 44px hit area; it toggles the help drawer and shows pressed while open.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Button | `.help-q` | 32px circle, 1px `--border`, Archivo 600 13px "?". |
| 2 | Hit area | `.help-q::after` | −6px inset → 44px. |
| 3 | Tooltip | `.tt .tip` | "Help for this page". |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Desktop | `.help-q` | 32px. |
| Phone | `.kit-m .help-q` | 36px, in the phone bar. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Resting fill and border from the variant. | — |
| hover | `--accent` ground. | `:hover` |
| focus | 2px `--ring` outline, 2px offset. Keyboard only. | `:focus-visible · .is-focus` |
| active | Open: ring-tinted border, primary colour; press shift 1px. | `[aria-expanded=true]` |
| disabled | 45% opacity, no shadow, no pointer events. Keep a reason nearby. | `[disabled] · [aria-disabled=true]` |
| error | A control has no error state of its own; the form or toast reports it. | — |
| loading | Label becomes the progressive verb, `.spin` leads, clicks are blocked, width holds. | `[aria-busy=true]` |
| empty | Not applicable: an action always has a label. | — |
| read-only | Hide the action rather than show a dead one; if it must show, use disabled with a reason. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| width | 32px | `32px` |
| height | 32px | `32px` |
| border-radius | 50% | `50%` |
| border | 1px solid #E3E7ED | `1px solid hsl(var(--border))` |
| background | #FFFFFF | `hsl(var(--background))` |
| color | #505B71 | `hsl(var(--muted-foreground))` |
| font | 600 13px/1 'Archivo', system-ui, sans-serif | `var(--w-semibold) 13px/1 var(--font-voice)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--background` | `0 0% 100%  (#FFFFFF)` | #FFFFFF |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--w-semibold` | `600` | 600 |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--accent-foreground` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--motion-press` | `80ms var(--ease-standard)` | 80ms cubic-bezier(.2,0,0,1) |
| `--press-shift` | `1px` | 1px |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |

### Accessibility

- **Keyboard:** Enter / Space toggles the drawer; Escape closes it and returns focus here.
- **ARIA:** `aria-label="Help for this page"`, `aria-expanded`, `aria-controls`.
- **Focus:** Ring.

### Content rules

- Always "?"; never a word.

**Do**

- Same position on every page.

**Don't**

- Page-specific help buttons elsewhere.

### Build it

- **shadcn / Radix:** `Button` (icon, round) as the `SheetTrigger`.
- **Exists in apps/admin today:** Button. Button exists.
- **Rendering:** client component (`"use client"`). 

```ts
interface HelpTriggerProps { open: boolean; onToggle: () => void; controls: string }
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [Sign in](../../phase-3/pages/sign-in/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="help-drawer"></a>
## Help drawer

*Page help · family `page-help` · page `components/help-drawer/` · phone `components/help-drawer/m/`*

The page-help sheet (380px, below the top bar so "?" stays clickable): Summary, What you can do here, Connected to, Where the data comes from, Key terms. Loading, empty and error states. Phone: a bottom sheet.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Drawer | `.k-sheet (width `--help-w`)` | 380px, under the top bar (`.k-stage.below-bar`). |
| 2 | Header | `.ph-d-head` | Page icon + title + close. |
| 3 | Summary | `.ph-blk .ph-lead` | 15px reading text (`--text-read`). |
| 4 | What you can do here | `.ph-do` | Bulleted verbs. |
| 5 | Connected to | `.ph-links` | Link rows to related pages. |
| 6 | Where the data comes from | `.ph-src` | Source rows: database, sync, file, manual. |
| 7 | Key terms | `.ph-terms` | Definitions (Collapsible on the phone). |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Desktop drawer | `.k-stage.below-bar > .k-sheet` |  |
| Phone sheet | `.k-sheet.k-bottom` |  |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Closed: nothing rendered in the flow. | — |
| hover | Not applicable to the surface; its buttons and rows hover as usual. | — |
| focus | Open: focus moves inside (`[data-k-autofocus]` first), Tab is trapped, Escape closes and returns focus. | `[data-k-stage].open` |
| active | Open, with the `--motion-*-in` entrance. | `.open` |
| disabled | The opener is disabled, never the overlay. | — |
| error | "Help did not load" + Try again. | — |
| loading | Skeleton blocks. | `.ph-sk` |
| empty | "Help for this page is being written". | — |
| read-only | Footer drops the primary and keeps Close. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| padding | 20px 0 | `var(--s-5) 0` |
| width | 380px | `--help-w` |
| lead text | 15px / 1.55 | `--text-read / --lh-read` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--w-medium` | `500` | 500 |
| `--text-micro` | `11.5px` | 11.5px |
| `--font-ui` | `'IBM Plex Sans', system-ui, sans-serif` | 'IBM Plex Sans', system-ui, sans-serif |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--r-chip` | `4px` | 4px |
| `--help-w` | `380px` | 380px |
| `--s-3` | `12px` | 12px |
| `--s-4` | `16px` | 16px |
| `--s-5` | `20px` | 20px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--r-control` | `6px` | 6px |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--primary` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--w-semibold` | `600` | 600 |
| `--text-section` | `16px` | 16px |
| `--lh-section` | `1.25` | 1.25 |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--tr-section` | `-.005em` | -.005em |
| `--font-stretch-title` | `100%` | 100% |
| `--text-small` | `12px` | 12px |
| `--s-2` | `8px` | 8px |
| `--text-card` | `13.5px` | 13.5px |
| `--lh-card` | `1.3` | 1.3 |
| `--text-read` | `15px` | 15px |
| `--lh-read` | `1.55` | 1.55 |
| `--motion-hover` | `120ms var(--ease-standard)` | 120ms cubic-bezier(.2,0,0,1) |
| `--ring` | `221 45% 45%  (#3F60A6)` | #3F60A6 |
| `--text-body` | `13px` | 13px |
| `--warning` | `36 79% 29%  (#845610)` | #845610 |
| `--destructive` | `353 53% 42%  (#A43240)` | #A43240 |
| `--text-label` | `13px` | 13px |
| `--motion-skeleton` | `1.6s var(--ease-standard) infinite alternate` | 1.6s cubic-bezier(.2,0,0,1) infinite alternate |
| `--s-8` | `32px` | 32px |
| `--text-18` | `18px` | 18px |
| `--control-h-m-lg` | `48px` | 48px |

### Motion

- Sheet motion (`--motion-sheet-in` 260ms / out 180ms).

### Accessibility

- **Keyboard:** Escape closes; focus returns to "?".
- **ARIA:** `role="dialog"` labelled by the page title; not modal on desktop (page stays usable).
- **Focus:** Focus moves to the drawer heading.

### Content rules

- Plain language; the "mum test": no jargon.
- Sources name the system (Laravel ERP, Holland & Sherry feed).

**Do**

- Keep blocks in the fixed order.

**Don't**

- Duplicate the page's own labels as help.

### Build it

- **shadcn / Radix:** shadcn `Sheet` (modal={false} on desktop) / `Drawer` on the phone; `Collapsible` for terms.
- **Exists in apps/admin today:** no · new: Sheet. Sheet is new.
- **Rendering:** client component (`"use client"`). 

```ts
interface PageHelp {
  title: string;
  summary: string;
  canDo: string[];
  connectedTo: { label: string; href: string; why: string }[];
  sources: { kind: "database" | "sync" | "file" | "manual"; name: string; detail: string }[];
  terms?: { term: string; meaning: string }[];
}
interface HelpDrawerProps { open: boolean; onOpenChange: (o: boolean) => void; help?: PageHelp; state?: "ready" | "loading" | "empty" | "error" }
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [Sign in](../../phase-3/pages/sign-in/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="icon"></a>
## Icon

*Small primitives · family `specimens` · page `components/icon/` · phone `components/icon/m/`*

Lucide, the one icon set: 24px grid, 1.75 stroke, round caps, shown at 16px (14px in chips and menus, 20px in empty states). Icons take the text colour and never stand alone without a label or tooltip.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Glyph | `svg` | `viewBox="0 0 24 24"`, `stroke="currentColor"`, `fill="none"`, stroke-width 1.75. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| 16px | `size={16}` | Default: buttons, inputs, nav. |
| 14px | `size={14}` | Chips, menus, small buttons. |
| 18–20px | `size={18\|20}` | Alerts, empty states, the phone bar. |
| 12px | `size={12}` | Inside chips with `.ico`. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Inherits its parent's colour change. | — |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Not applicable. | — |
| disabled | Inherits the parent's opacity. | — |
| error | Names the real cause and offers Try again (see Error states). | `.sf-alert.danger` |
| loading | `.skel` shapes in the final layout, pulsing on `--motion-skeleton`. | `.skel` |
| empty | Says what is missing and the one next step (see Empty state). | `.sf-empty` |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| stroke | 1.75 | `strokeWidth={1.75}` |
| caps / joins | round / round | — |
| library | lucide-react 0.469.0 | `rj-dtc workspace` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--s-6` | `24px` | 24px |
| `--s-3` | `12px` | 12px |
| `--s-2` | `8px` | 8px |
| `--s-1` | `4px` | 4px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--r-card` | `8px` | 8px |
| `--w-medium` | `500` | 500 |
| `--text-small` | `12px` | 12px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--text-micro` | `11.5px` | 11.5px |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--foreground` | `222 43% 10%  (#0F1524)` | #0F1524 |
| `--s-8` | `32px` | 32px |
| `--s-4` | `16px` | 16px |

### Accessibility

- **Keyboard:** Icons are never focusable.
- **ARIA:** Decorative icons: `aria-hidden="true"`.
- **ARIA:** Meaningful icon alone: the parent control carries `aria-label`.
- **Focus:** None.

### Content rules

- Use the Lucide name shown in the list (`layers` = Global Store, `swatch-book` = Cloths…).

**Do**

- Set `strokeWidth={1.75}` globally.

**Don't**

- Mix another set or the retired hand-drawn `icons.py` glyphs.
- Fill an icon.

### Build it

- **shadcn / Radix:** `lucide-react` (already the shadcn icon library in components.json).
- **Exists in apps/admin today:** no. lucide-react ^0.469.0 is installed.
- **Rendering:** server component. 

```ts
import type { LucideProps } from "lucide-react";
// One wrapper so the stroke is never forgotten:
interface IconProps extends LucideProps { icon: React.ComponentType<LucideProps>; size?: 12 | 14 | 16 | 18 | 20 }
// <Icon icon={Layers} />  ->  <Layers size={16} strokeWidth={1.75} aria-hidden />
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [Sign in](../../phase-3/pages/sign-in/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="avatar"></a>
## Avatar

*Small primitives · family `base` · page `components/avatar/` · phone `components/avatar/m/`*

Initials in a round tile for staff and customers (no photos of real people). 28px in lists and the top bar, 44px in a record header.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Tile | `.avatar` | Round (`--r-pill`), `--accent` ground. |
| 2 | Initials | `text` | Archivo 600 11px, two letters. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| 28px | `.avatar` | Lists, top bar account button. |
| 40–44px | `.k-rec>.avatar` | Record header (40px on the phone). |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Rows and cards that open something take `--muted` or lift; static blocks do not react. | `:hover` |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Not applicable. | — |
| disabled | Not applicable: hide data the person cannot see, never grey it. | — |
| error | Names the real cause and offers Try again (see Error states). | `.sf-alert.danger` |
| loading | `.skel.sq` with full radius. | `.skel.sq` |
| empty | Says what is missing and the one next step (see Empty state). | `.sf-empty` |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| width | 28px | `28px` |
| height | 28px | `28px` |
| border-radius | 999px | `var(--r-pill)` |
| background | #EAEDF6 | `hsl(var(--accent))` |
| color | #0E172A | `hsl(var(--accent-foreground))` |
| font | 600 11px/1 'Archivo', system-ui, sans-serif | `var(--w-semibold) var(--text-11)/1 var(--font-voice)` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--r-pill` | `999px` | 999px |
| `--accent` | `224 38% 94%  (#EAEDF6)` | #EAEDF6 |
| `--accent-foreground` | `220 49% 11%  (#0E172A)` | #0E172A |
| `--w-semibold` | `600` | 600 |
| `--text-11` | `11px` | 11px |
| `--font-voice` | `var(--font-title)` | 'Archivo', system-ui, sans-serif |
| `--text-card` | `13.5px` | 13.5px |

### Accessibility

- **Keyboard:** Not focusable unless it is the account button.
- **ARIA:** Decorative next to a visible name (`aria-hidden`); alone, `aria-label` with the name.
- **Focus:** Account button: ring as icon button.

### Content rules

- Two initials, uppercase.

**Do**

- Use initials only.

**Don't**

- Real customer photos.
- Colour-code people.

### Build it

- **shadcn / Radix:** shadcn `Avatar` (Radix Avatar) with `AvatarFallback` only.
- **Exists in apps/admin today:** no · new: Avatar. Not in apps/admin/ui.
- **Rendering:** server component. 
- **Install:** `npx shadcn@latest add avatar`

```ts
interface AvatarProps { name: string; size?: 28 | 36 | 40 | 44 }   // initials derived from name
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [Sign in](../../phase-3/pages/sign-in/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="kbd"></a>
## Keyboard key

*Small primitives · family `base` · page `components/kbd/` · phone `components/kbd/m/`*

A key cap for shortcuts: ⌘K in the command palette trigger, Esc in sheet headers, arrows in menu hints.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Cap | `kbd` | Plex Mono 500 11px, 3px 5px, 4px radius, `--muted` ground, 1px `--border`. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Default | `kbd` | On white. |
| On tooltip | `.tip kbd` | Transparent with a 30% light edge on navy. |
| In a menu item | `.mi kbd` | Pushed to the right end. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Static. | — |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Not applicable. | — |
| disabled | Not applicable: hide data the person cannot see, never grey it. | — |
| error | Names the real cause and offers Try again (see Error states). | `.sf-alert.danger` |
| loading | `.skel` shapes in the final layout, pulsing on `--motion-skeleton`. | `.skel` |
| empty | Says what is missing and the one next step (see Empty state). | `.sf-empty` |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| font | 500 11px/1 'IBM Plex Mono', ui-monospace, monospace | `var(--w-medium) var(--text-11)/1 var(--font-code)` |
| padding | 3px 5px | `3px 5px` |
| border | 1px solid #E3E7ED | `1px solid hsl(var(--border))` |
| border-radius | 4px | `4px` |
| background | #F3F4F7 | `hsl(var(--muted))` |
| color | #505B71 | `hsl(var(--muted-foreground))` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--font-code` | `'IBM Plex Mono', ui-monospace, monospace` | 'IBM Plex Mono', ui-monospace, monospace |
| `--w-medium` | `500` | 500 |
| `--text-11` | `11px` | 11px |
| `--border` | `220 20% 91%  (#E3E7ED)` | #E3E7ED |
| `--muted` | `225 22% 96%  (#F3F4F7)` | #F3F4F7 |
| `--muted-foreground` | `221 17% 38%  (#505B71)` | #505B71 |
| `--primary-foreground` | `60 17% 98%  (#FBFBF9)` | #FBFBF9 |

### Accessibility

- **Keyboard:** Not focusable.
- **ARIA:** Use `<kbd>`; spell the shortcut in `aria-keyshortcuts` on the control it triggers.
- **Focus:** None.

### Content rules

- Use symbols on macOS (⌘), words elsewhere (Ctrl).

**Do**

- Show shortcuts only where they exist.

**Don't**

- Show key caps on the phone.

### Build it

- **shadcn / Radix:** Plain `<kbd>` styled by the kit.
- **Exists in apps/admin today:** no. Not a shared component.
- **Rendering:** server component. 

```ts
interface KbdProps { keys: string[] }   // ["⌘", "K"]
```

### Used on

[Funnel · Analytics](../../phase-3/pages/analytics-funnel/), [Revenue · Analytics](../../phase-3/pages/analytics-revenue/), [System status · Analytics](../../phase-3/pages/analytics-system-status/), [Appointments](../../phase-3/pages/appointments/), [Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Returning customer · Campaigns](../../phase-3/pages/campaign-detail/), [Campaigns](../../phase-3/pages/campaigns/), [Email broadcasts](../../phase-3/pages/campaigns-email/), [Connected apps](../../phase-3/pages/connected-apps/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Oliver Hartley](../../phase-3/pages/customer-detail/), [Customers](../../phase-3/pages/customers/), [Dashboard](../../phase-3/pages/dashboard/), [Design pricing](../../phase-3/pages/design-pricing/), [Fit review](../../phase-3/pages/fit-review/), [Fit review · RJC-20431](../../phase-3/pages/fit-review-detail/), [Founder](../../phase-3/pages/founder/), [Refund-gate queue](../../phase-3/pages/founder-refund-requests/), [Global Store](../../phase-3/pages/global-store/), [Global Store product editor](../../phase-3/pages/global-store-editor/), [Audit log](../../phase-3/pages/hq-settings-audit-log/), [Users & access](../../phase-3/pages/hq-settings-users/), [RJC-20418](../../phase-3/pages/order-detail/), [Envelope audit](../../phase-3/pages/order-envelope/), [Orders](../../phase-3/pages/orders/), [Production kanban](../../phase-3/pages/orders-kanban/), [ERP retry queue](../../phase-3/pages/orders-retry-erp/), [Pricing](../../phase-3/pages/pricing/), [Bulk price import](../../phase-3/pages/pricing-bulk/), [Variant pricing](../../phase-3/pages/pricing-variant/), [Prices · Navy birdseye two-piece suit](../../phase-3/pages/product-prices/), [My Products · RJ Clothing](../../phase-3/pages/products/), [Releases](../../phase-3/pages/releases/), [How customers buy](../../phase-3/pages/selling/), [Contact channels](../../phase-3/pages/selling-contact-channels/), [API keys](../../phase-3/pages/settings-api-keys/), [Audit log](../../phase-3/pages/settings-audit-log/), [Brand](../../phase-3/pages/settings-brand/), [Brand detail](../../phase-3/pages/settings-brand-detail/), [Create a brand](../../phase-3/pages/settings-brand-new/), [Brands](../../phase-3/pages/settings-brands/), [Profile](../../phase-3/pages/settings-profile/), [Rules](../../phase-3/pages/settings-rules/), [Users & access](../../phase-3/pages/settings-users/), [Sign in](../../phase-3/pages/sign-in/), [API Access](../../phase-3/pages/super-api-access/), [Brands](../../phase-3/pages/super-brands/), [Cloth library](../../phase-3/pages/super-cloths/), [Connect MCP client](../../phase-3/pages/super-mcp-connect/), [Inbox](../../phase-3/pages/support/), [Support categories](../../phase-3/pages/support-categories/), [Support templates](../../phase-3/pages/support-templates/), [#7f3a21c0 · Support](../../phase-3/pages/support-thread/), [AI training pairs](../../phase-3/pages/support-training-pairs/), [Marchetti Tailoring](../../phase-3/pages/tailor-detail/), [Tailor Roster](../../phase-3/pages/tailors/), [Variant](../../phase-3/pages/variant-detail/), [Groups](../../phase-3/pages/variant-groups/), [New SKU](../../phase-3/pages/variant-new/), [Variants](../../phase-3/pages/variants/), [Sync & OOS](../../phase-3/pages/variants-sync/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)

---

<a id="thumb"></a>
## Garment thumbnail

*Small primitives · family `base` · page `components/thumb/` · phone `components/thumb/m/`*

The 30×40 garment or cloth thumbnail in tables, lists and pickers, on the studio ground. A missing image shows the garment silhouette, never a grey box.

### Anatomy

| # | Part | Kit class | Role |
|---|---|---|---|
| 1 | Frame | `.thumb` | 30×40, 4px radius (`--r-media`), `--studio` radial ground, 8% inset edge. |
| 2 | Image | `img / cloth fill` | Garment photo, cloth texture or the silhouette SVG. |

### Variants

| Variant | Class / prop | When to use |
|---|---|---|
| Garment | `.thumb + img` | Product rows. |
| Cloth | `.thumb + cloth texture` | Cloth rows and pickers. |
| Missing | `.thumb + silhouette` | No photo yet: silhouette in muted ink. |

### States

| State | What it looks like | Trigger |
|---|---|---|
| default | Populated with real data. | — |
| hover | Rows and cards that open something take `--muted` or lift; static blocks do not react. | `:hover` |
| focus | Only the interactive parts take the 2px `--ring` outline. | `:focus-visible` |
| active | Not applicable. | — |
| disabled | Not applicable: hide data the person cannot see, never grey it. | — |
| error | Names the real cause and offers Try again (see Error states). | `.sf-alert.danger` |
| loading | `.sf-sk.th` (30×40 skeleton). | `.sf-sk.th` |
| empty | Silhouette of the garment type. | — |
| read-only | The default: display components never edit in place. | — |

### Specs

| Property | Value | Token / CSS |
|---|---|---|
| width | 30px | `30px` |
| height | 40px | `40px` |
| border-radius | 4px | `var(--r-media)` |
| background | radial-gradient(120% 90% at 50% 18%, #f7f8fa 0%, #e3e7ee 80%) | `var(--studio)` |
| box-shadow | inset 0 0 0 1px rgba(0,0,0,.08) | `inset 0 0 0 1px rgba(0,0,0,.08)` |
| size | 30 × 40px (3:4) | — |
| ground | radial-gradient studio | `--studio` |

### Tokens used

| Token | Value | Resolves to |
|---|---|---|
| `--r-media` | `4px` | 4px |
| `--studio` | `radial-gradient(120% 90% at 50% 18%, #f7f8fa 0%, #e3e7ee 80%)` | radial-gradient(120% 90% at 50% 18%, #f7f8fa 0%, #e3e7ee 80%) |

### Accessibility

- **Keyboard:** Not focusable.
- **ARIA:** `alt` names the product ("Navy birdseye two-piece suit"); empty `alt` when the name is in the same row.
- **Focus:** None.

### Content rules

- 3:4 portrait crops only.

**Do**

- Use `next/image` with a fixed 30×40 box.

**Don't**

- Grey placeholder boxes where a garment belongs.

### Build it

- **shadcn / Radix:** `next/image` in a styled wrapper.
- **Exists in apps/admin today:** no. Not a shared component.
- **Rendering:** server component. 

```ts
interface GarmentThumbProps { src?: string; alt: string; kind?: "garment" | "cloth"; garmentType?: "jacket" | "trousers" | "waistcoat" | "suit" }
```

### Used on

[Global Store · RJ Clothing](../../phase-3/pages/brand-global-store/), [Content](../../phase-3/pages/content-frame/), [Create Product · RJ Clothing](../../phase-3/pages/create-product/), [Global Store](../../phase-3/pages/global-store/), [RJC-20418](../../phase-3/pages/order-detail/), [Production kanban](../../phase-3/pages/orders-kanban/), [Releases](../../phase-3/pages/releases/), [Cloth library](../../phase-3/pages/super-cloths/), [Website](../../phase-3/pages/website/), [Menu & sections](../../phase-3/pages/website-menu/)
