# CheckInHub — DESIGN.md

> The full specification for the CheckInHub design system. Drop this file into an agent alongside [`ui.nuxt.com`](https://ui.nuxt.com) and it will build product UI that signs the brand from the first render.

**Last updated** · 24 Apr 2026 · v0.2
**Token format** · CSS custom properties (`--chk-*`)
**Icon library** · Lucide · outline only
**Display family** · Instrument Serif
**UI family** · Inter · Mono · JetBrains Mono
**Signal** · `#00ED64` · **Accent** · `#00684A` · **Forest-950** · `#071521`
**Governing team** · Brand & Product

---

## 1 · The Brand in One Breath

CheckInHub is a **professional event check-in and attendee management platform**. It registers attendees, issues QR codes and barcodes, scans them at the door, tracks equipment on loan, and dispatches confirmation emails — all under one calm, structured interface.

The visual language is deliberately dual:

- **Forest-dark for live surfaces.** The navigation, marketing hero, scanner, kiosk, and admin sidebar sit on a deep blue-teal canvas (`#071521`). This is the CheckInHub signature — the dark band that introduces every context and carries the single neon signal pulse.
- **Paper-light for the working surfaces.** The event list, attendee tables, forms, and configuration pages sit on near-white sage paper. Tabular figures, hairline borders, generous whitespace.

The product moves between these two worlds seamlessly. The header is always dark. Admin sidebars are always dark. The content you read and edit is always light. The same **neon signal green** pulse and the same **Instrument Serif + Inter** letterforms stitch them together.

### Principles

1. **Dark shells, light content.** The header, sidebar, hero, scanner, and kiosk are forest-dark. Tables, forms, cards, and pages are paper-light. The rhythm is dark-frame / light-fill — never inverted, never mixed arbitrarily.
2. **One signal per surface.** Neon green `#00ED64` carries "state changed", "checked in", "live". Never two chromatic signals on one surface.
3. **Content-first chrome.** The UI recedes so the count, the name, the next action can breathe.
4. **Precision beats polish.** Tabular numerics, 4 px grid, hairline borders, consistent duration formats.
5. **Restraint over reach.** Two type styles per surface at most (serif display + sans body). No emoji. No exclamation marks. No hype.
6. **Token layer is sovereign.** A hex literal in a component is a bug. Every colour, font, radius, and shadow flows from `main.css`.
7. **Professional, event-driven voice.** Plainspoken English. Event terminology ("registered", "checked in", "departed", "absent"). No US-casual slang.

---

## 2 · Brand Identity

### Name and voicing

Always written exactly **CheckInHub** — one word, three capitals. Never "Check In Hub", never "checkinhub", never "Checkin Hub". The product's name is a noun: the hub where check-in happens.

### Logo system

The mark is a check-glyph inside a rounded square, with a signal-green dot in the lower-right corner. The dot is the only coloured element in every mono lockup — it represents the live signal, the "this thing is on" indicator.

| Variant | File | Use |
|---|---|---|
| Lockup | `/images/logo.svg` | Marketing header, invoice header, docs chrome |
| Wordmark | `/images/wordmark.svg` | Compact product header |
| Mark (square) | `/images/mark.svg` | Favicon, app icon, mobile splash, footer chip |
| Mark on dark | `/images/mark-on-dark.svg` | Kiosk mode, scanner overlay |

### Clear space & minimum size

Clear space on all sides equals the height of the wordmark. Never crowd with text or imagery.

- Mark: 16 px on screen, 8 mm in print
- Lockup: 96 px wide on screen, 22 mm in print

### Do not

- Don't stretch, recolour, rotate, or outline.
- Don't drop the signal dot — it carries the brand.
- Don't animate the mark on load. The only permitted animation is the slow 2.4 s pulse on the signal dot in kiosk mode.
- Don't use the wordmark at smaller than 96 px — use the mark instead.

---

## 3 · Token Architecture

Three layers, in strict order of resolution. Every decision lands in exactly one of these — never inline, never ad hoc.

### Layer 1 — Tailwind palette ramps

Declared once in `app/assets/css/main.css` via the `@theme static` block. Full 50–950 ramps so Nuxt UI can resolve its semantic aliases and so `bg-chk-green-500` / `text-chk-forest-900` utilities work out of the box.

| Family | Purpose |
|---|---|
| `chk-green` | Brand accent, status `success`, Nuxt UI `primary` |
| `chk-forest` | Dark-surface scale (forest-teal darks for kiosk / scanner / marketing hero) |
| `chk-sage` | Cool-sage neutrals for light admin surfaces, Nuxt UI `neutral` |
| `chk-amber` | Status `warning` |
| `chk-coral` | Status `error` / destructive |
| `chk-sky` | Status `info` / "expected" attendee state |

### Layer 2 — Semantic CSS variables

Brand-facing names live under `:root` and `[data-theme="dark"]`. Components reference these — never the raw Tailwind ramps.

- Accent family: `--chk-signal`, `--chk-signal-soft`, `--chk-signal-tint`, `--chk-signal-glow`, `--chk-signal-fg`
- Functional green: `--chk-accent`, `--chk-accent-hover`, `--chk-accent-pressed` (deeper emerald for button fills, link text, pressed states)
- Surface: `--chk-bg-0` through `--chk-bg-4`
- Ink: `--chk-ink-0` through `--chk-ink-3`
- Borders: `--chk-border`, `--chk-border-soft`, `--chk-border-hard`
- Status: `--chk-success`, `--chk-warning`, `--chk-danger`, `--chk-info` plus their `-soft` and `-fg` companions

The semantic layer is what a component references. A re-skin is always "change Layer 2". A new property added mid-build is always "add to Layer 2 first".

### Layer 3 — Per-team overrides

A future team-branding surface will set `<html data-team-theme="teal" style="--chk-signal: #…; --chk-signal-soft: #…;">`. Overrides only ever touch the `--chk-signal-*` and `--chk-accent-*` sets — never neutrals, never status colours. This is the re-skinning contract and it is non-negotiable.

### Nuxt UI bridge

`--ui-primary: var(--chk-accent)` in `:root` means `<UButton color="primary">` and every other Nuxt UI primary-tinted primitive resolve to the CheckInHub accent automatically. Do not override Nuxt UI tokens in a component — update the bridge in `main.css` if the mapping is wrong.

---

## 4 · Colour

### Signal green — the electric pulse

Neon green that carries "live / powered on / state changed" on dark surfaces. Electric enough to pop from across a venue, deliberate enough to stay scarce — never a fill, never decoration.

| Role | Token | Value | Notes |
|---|---|---|---|
| Signal | `--chk-signal` | `#00ED64` | Underlines, focus rings, signal dot, scan-success flash, active nav tick. |
| Signal soft | `--chk-signal-soft` | `#C8FADC` | "Checked in" row highlights, selected state fills on light. |
| Signal tint | `--chk-signal-tint` | `rgba(0, 237, 100, 0.14)` | Hover fills, focus-ring outer glow. |
| Signal glow | `--chk-signal-glow` | `rgba(0, 237, 100, 0.28)` | Scanner line glow, kiosk "powered on" halo. |
| Signal foreground | `--chk-signal-fg` | `#002F1A` | Text on solid-signal surfaces (rare). |

### Accent green — the workhorse

Deep forest-green used as the primary button fill, link text, and pressed state on both dark and light. Quieter than the signal by design — the signal flashes, the accent sits.

| Role | Token | Value | Notes |
|---|---|---|---|
| Accent | `--chk-accent` | `#00684A` | Primary button fill, link text. |
| Accent hover | `--chk-accent-hover` | `#005339` | — |
| Accent pressed | `--chk-accent-pressed` | `#00402B` | — |
| Accent foreground | `--chk-accent-fg` | `#FFFFFF` | Text on accent fills. |

### Full `chk-green` ramp

The ramp is workhorse-aligned: stop 500 is the colour Nuxt UI uses for solid `primary` buttons, so it must pass AA contrast with white text. The neon **signal** green is a *separate* `--chk-signal` token (see above) — it intentionally does not live on this ramp.

| Stop | Hex | Used for |
|---|---|---|
| 50  | `#E6FDF1` | Soft tints, "expected" row backgrounds, accent-soft buttons |
| 100 | `#C8FADC` | Hover on soft surfaces, toast-success backgrounds |
| 200 | `#8FF3B8` | Chart fills, progress bars (low density) |
| 300 | `#51E88D` | Chart fills, progress bars (mid density) |
| 400 | `#1FB868` | Chart fills, progress bars (high density) |
| 500 | `#00874A` | **Workhorse.** Primary button fill, link text — white text 4.6 : 1. |
| 600 | `#00744A` | Hover on accent |
| 700 | `#00604A` | Pressed on accent |
| 800 | `#00492F` | Deep accent text |
| 900 | `#003A26` | Deepest accent text |
| 950 | `#001A12` | Outline / shadow alpha base |

**Signal** (the neon `#00ED64`) lives at `--chk-signal` and is sized for small electric moments — focus rings, the active-section dot, the scan-success flash, the live-chip pulse. It is never a fill on a button or a card.

### Forest darks — the permanent dark shell

Blue-teal darkness centred on `#071521`, never pure black. The product's shell — header, sidebar, hero, scanner, kiosk, footer — always sits on these values. Content (tables, forms, cards, pages) sits on light paper within the shell. The whole product has a dark-frame / light-fill rhythm; the forest scale is the frame.

| Token | Hex | Role |
|---|---|---|
| `--chk-forest-950` | `#071521` | Deepest page / kiosk backdrop |
| `--chk-forest-900` | `#0C2232` | Card surface on dark, section background |
| `--chk-forest-800` | `#143044` | Elevated surface, nav pill fill |
| `--chk-forest-700` | `#1F3E53` | Border on dark (hairline) |
| `--chk-forest-600` | `#2E4D62` | Pressed / hard border on dark |
| `--chk-forest-500` | `#42657B` | Tertiary ink on dark, muted blue-gray |
| `--chk-forest-400` | `#5A7F94` | Placeholder ink on dark |
| `--chk-forest-300` | `#83A5B8` | Meta / tertiary ink on dark |
| `--chk-forest-200` | `#B4CCD8` | Secondary ink on dark |
| `--chk-forest-100` | `#D7E5EC` | Primary ink on dark |
| `--chk-forest-50`  | `#ECF2F5` | Max-contrast display ink on dark |

### Sage neutrals — the light content surfaces

Cool sage with a whisper of green. Reads as "paper" on the admin surface but with a warmer character than pure grey — the product surface looks like a venue's ops binder, not a bank printout.

| Stop | Hex | Role |
|---|---|---|
| 50 | `#F7F9F7` | Page backdrop (light) |
| 100 | `#EEF1EF` | Soft surface, subtle inset |
| 150 | `#E4E8E5` | Hairline border (the standard border colour) |
| 200 | `#D4DAD6` | Input border rest, pressed border on light |
| 300 | `#B7BFB9` | Placeholder ink, disabled text |
| 400 | `#8C9590` | Tertiary ink, meta |
| 500 | `#6B7470` | Meta ink |
| 600 | `#4B5350` | Secondary ink |
| 700 | `#353A37` | — |
| 800 | `#1F2320` | — |
| 900 | `#111412` | — |
| 950 | `#0A0C0B` | Primary ink on light |

### Surface tokens (by mode)

**Light**

| Role | Token | Value |
|---|---|---|
| Page | `--chk-bg-0` | `#F7F9F7` |
| Card / panel | `--chk-bg-1` | `#FFFFFF` |
| Inset | `--chk-bg-2` | `#F1F5F2` |
| Hover surface | `--chk-bg-3` | `#E8EEEA` |
| Selected surface | `--chk-bg-4` | `var(--chk-signal-soft)` |

**Dark**

| Role | Token | Value |
|---|---|---|
| Page | `--chk-bg-0` | `#050C0A` |
| Card / panel | `--chk-bg-1` | `#0A1612` |
| Inset | `--chk-bg-2` | `#0F2018` |
| Hover surface | `--chk-bg-3` | `#163028` |
| Selected surface | `--chk-bg-4` | `rgba(0, 193, 118, 0.08)` |

### Ink (text)

**Light mode**

| Role | Token | Value |
|---|---|---|
| Primary | `--chk-ink-0` | `#0A0C0B` |
| Secondary | `--chk-ink-1` | `#353A37` |
| Tertiary / meta | `--chk-ink-2` | `#6B7470` |
| Placeholder / disabled | `--chk-ink-3` | `#8C9590` |

**Dark mode**

| Role | Token | Value |
|---|---|---|
| Primary | `--chk-ink-0` | `#DAE9E2` |
| Secondary | `--chk-ink-1` | `#B6CDC2` |
| Tertiary / meta | `--chk-ink-2` | `#8AA69A` |
| Placeholder / disabled | `--chk-ink-3` | `#557C6E` |

### Status

| Role | Solid | Soft | Foreground |
|---|---|---|---|
| Success / Checked-in | `#047857` | `#ECFDF5` | `#024E37` |
| Warning | `#B4620A` | `#FCF1DC` | `#6E3B04` |
| Danger | `#BE2F2A` | `#FBE9E7` | `#761714` |
| Info / Registered | `#0B6BD8` | `#E4EFFD` | `#164890` |

### The attendee-state quartet

A hard-coded semantic mapping. Every row, dot, or pill that represents an attendee's state uses exactly this set. Use the English event-language labels — never "no-show", always "Absent"; never "checked out" in product copy, always "Departed".

| State | Label | Colour | Icon |
|---|---|---|---|
| Registered | "Registered" | Info (sky blue) | `i-lucide-clock` |
| Checked in | "Checked in" | Success (accent green) | `i-lucide-check` |
| Departed | "Departed" | Neutral (sage 500) | `i-lucide-log-out` |
| Absent | "Absent" | Danger (coral) | `i-lucide-circle-x` |

### Colour rules

- **DO** use `--chk-signal` for accent underlines, focus rings, and "state changed" signals. Small, deliberate, electric.
- **DO** use `--chk-accent` for button fills and link text. Bigger surfaces get the quieter workhorse green.
- **DO** pair status colours with an icon. Colour is never the only signal.
- **DO** use `--chk-signal-tint` for focus-ring outer glow — always paired with a crisp 2 px ring in `--chk-signal`.
- **DON'T** use the signal green on large surfaces. It's an accent, not a fill. No full-card signal backgrounds. No full-button signal fills (the button uses `--chk-accent` instead).
- **DON'T** introduce warm colours (orange / yellow / red) outside their status roles.
- **DON'T** mix two soft tints on one surface.

---

## 5 · Signature Visual Elements

The patterns that give the product its fingerprint. Use these intentionally — they should feel scarce enough to signal something specific when they appear.

### The signal underline

A 1.5 px neon-green underline applied to a single word or phrase per section, used to mark the most important heading on the page. Implemented via `text-decoration` so it wraps cleanly across line breaks and never forces a layout reflow.

```css
.chk-signal-underline {
  text-decoration-line: underline;
  text-decoration-color: var(--chk-signal);
  text-decoration-thickness: 1.5px;
  text-underline-offset: 0.12em;
  text-decoration-skip-ink: none;
}
```

Only one per section. Preferred on the serif display at hero scale, where the neon underline reads like a highlighter stroke across an editorial headline.

### Uppercase mono eyebrow

Section category label above a heading. Wide-tracked JetBrains Mono, 11 px, 600 weight, 0.14em letter-spacing, uppercase. Colour is `--chk-ink-2` on light surfaces, `--chk-forest-300` on dark, or `--chk-signal` when the eyebrow itself is carrying the "live / ops" accent moment.

### Atmospheric halo

Radial gradient behind dark hero sections. Centre `rgba(0, 237, 100, 0.18)` fading to transparent at 65% stop, composed with a second, offset halo for depth. Used once per marketing page, never on product surfaces.

### Signal flash

120 ms opacity-in / 280 ms opacity-out flash of `--chk-signal-tint` over a card, row, or scanner frame on state change (check-in success, row selection). Never a scale, rotate, or shake.

### Teal-tinted shadows

All elevation shadows carry a blue-teal undertone — `rgba(10, 22, 18, 0.10)` rather than pure neutral black. This keeps the product in its colour world even on light surfaces.

### Inset top-glow on dark cards

Cards on forest-dark surfaces get a 1 px inset white highlight at the top edge: `box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04)`. Reads as a soft light source from above — the "lit-from-within" feel MongoDB's dark cards use.

### The live chip

A dark translucent pill containing the pulsing signal dot + the word `Live` (or a compact count). Used top-right on scanners and kiosk surfaces. `rgba(10, 22, 18, 0.7)` background + `rgba(0, 237, 100, 0.28)` border + 12 px blur.

### The ticker band

A thin horizontal ticker across the top of the marketing hero. Forest-950 background, scrolling mono feed of attendee counts, scanner status, and event context. A CheckInHub signature move — reads like a venue's ops wall display.

---

## 6 · Typography

Three families, each with a specific role. The combination — a refined editorial serif at display scale, a geometric sans for everything smaller, and a technical monospace for data tokens — is the CheckInHub letterform system.

### Families

| Family | Used for |
|---|---|
| **Instrument Serif** | Display, hero, H1 — the editorial voice at scale |
| **Inter** | H2 downwards, body, UI, buttons, forms |
| **JetBrains Mono** | QR / barcode payloads, equipment IDs, data tokens, uppercase technical labels, code |

Fallback stacks:
- Instrument Serif: `'Instrument Serif', 'Inter', ui-serif, Georgia, 'Times New Roman', serif`
- Inter: `'Inter', -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif`
- JetBrains Mono: `'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, Monaco, Consolas, monospace`

OpenType features: `'ss02', 'cv11', 'calt', 'liga'` on Inter (single-storey `a`, open `6/9`, contextual alternates).

Google Fonts URL (loaded once in `app.head.link` of `nuxt.config.ts`):
`family=Inter:wght@400;500;600;700&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500`

### Scale

| Class | Size | Family | Weight | Line | Tracking | Use |
|---|---|---|---|---|---|---|
| `.type-display-xl` | 104 px (clamped 64–104) | Instrument Serif | 400 | 1.00 | −0.025em | Marketing hero, kiosk attendee name |
| `.type-display` | 80 px (clamped 56–80) | Instrument Serif | 400 | 1.05 | −0.022em | Section hero, big-screen counters |
| `.type-h1` | 52 px (clamped 34–52) | Instrument Serif | 400 | 1.10 | −0.018em | Page titles |
| `.type-h2` | 36 px (clamped 26–36) | Inter | 500 | 1.16 | −0.01em | Section titles |
| `.type-h3` | 24 px | Inter | 600 | 1.25 | −0.005em | Card titles |
| `.type-h4` | 20 px | Inter | 600 | 1.30 | 0 | Subtitles, modal headings |
| `.type-body-lg` | 18 px | Inter | 400 | 1.55 | 0 | Lead paragraphs, marketing body |
| `.type-body` | 15 px | Inter | 400 | 1.55 | 0 | Prose, table cells, form input text |
| `.type-body-sm` | 13 px | Inter | 400 | 1.50 | 0 | Secondary prose, table meta |
| `.type-label` | 13 px | Inter | 500 | 1.40 | 0 | Form labels, field names |
| `.type-meta` | 12 px | Inter | 400 | 1.50 | 0 | Timestamps, counts, inline meta |
| `.type-eyebrow` | 11 px | JetBrains Mono | 600 | 1.40 | 0.14em UPPER | Section category label |
| `.type-mono-tag` | 11 px | JetBrains Mono | 500 | 1.30 | 0.08em UPPER | Data tokens, equipment IDs |

### Pairing philosophy

**Editorial serif display, geometric sans body, technical mono labels.** The serif carries the marketing voice — weight 400 at display scale reads as refined authority, not literary fussiness. Inter takes over at H2 and down, giving the product UI its structured sans-serif clarity. JetBrains Mono uppercase with wide tracking is reserved for technical labels and data tokens — it marks anything system-generated so a reader knows to treat it as a fact, not copy.

### Rules

- Never put Instrument Serif below 32 px — it loses its editorial character at small sizes.
- Never bold Instrument Serif. The default 400 weight is the intended presence.
- Never italicise for emphasis. Use colour, weight, or the signal underline.
- Mono is for *tokens* (barcode IDs, dates, equipment tags, code). It is never for body copy.

### Rules

- Sentence case everywhere. One heading per card.
- Never more than two weights on a single surface.
- No italics. Emphasis comes from weight bump, signal underline, or `.type-label` colour.
- Body copy is `15 px`, never `14 px` — keeps iOS Safari from zooming on input focus.
- Max line length `75ch` on marketing prose; tables have no cap.

### Numbers

- Tabular always. `.chk-tnum` utility or `font-variant-numeric: tabular-nums`.
- Counts: bare below 1,000 (`142 attendees`), thin-space group above (`1 240 attendees`). Never a comma.
- Durations short: `2h 14m` (no leading zeros).
- Durations timer: `00:14:22` (`HH:MM:SS`) in live counters.
- Percentages: `68%` (no space).
- Currency (if ever relevant): `£12.40` — symbol precedes amount, no space.

### Dates

- Product UI: `3 Jan 2026` — day-first, three-letter month, four-digit year.
- Export / machine: `2026-01-03` (ISO-8601).
- Time: 24-hour `14:22`. Prefix with relative terms sparingly (`today 14:22`, `yesterday 14:22`).
- Time zone: local unless stated. Append ` · UTC` when not.

### Technical labels — the mono treatment

Every system-ID, equipment tag, QR payload, and data token renders in JetBrains Mono. Uppercase category labels (the "eyebrow" treatment above headings) use `.type-eyebrow` with wide tracking — this is a signature CheckInHub move that borrows its rhythm from engineering docs, not marketing copy.

---

## 7 · Iconography

**Lucide outline, 1.5 stroke.** No filled icons. No mixed libraries. Icons are scanning aids, never decorative.

### Sizing

| Context | Size |
|---|---|
| Inside inputs | 16 px |
| Inside buttons (sm / md / lg) | 14 / 16 / 18 px |
| Standalone in list rows | 16 px |
| Nav sidebar | 18 px |
| Hero / feature tiles | 24 or 28 px |
| Empty-state illustrations | 32 px |

### Rules

- Every action button has an icon if its label is ≤ 2 words. Icon before label, 8 px gap.
- Status tags: icon optional. When used, same colour as text, 14 px.
- Never spin or rotate except `i-lucide-loader-2` on loading.
- Decorative icons use `aria-hidden="true"`; icons that replace labels need an `aria-label`.

---

## 8 · Spacing

Strict 4 px base grid. Every offset, padding, margin, and gap is a multiple of 4.

```
4 · 8 · 12 · 16 · 20 · 24 · 32 · 40 · 48 · 64 · 80 · 96 · 128
```

### Inside containers

| Container | Padding |
|---|---|
| Card (compact) | 16 px |
| Card (default) | 20–24 px |
| Card (spacious) | 32 px |
| Modal body | 24 px |
| Drawer body | 24–32 px |
| Page (side gutters) | 20 px mobile, 24–32 px desktop |
| Dashboard main column | 32–48 px |
| Marketing section | 64–96 px vertical |
| Kiosk tap target | 48 px minimum padding |

### Stacks

| Context | Gap |
|---|---|
| Form field group | 16 px |
| Label → input | 6 px |
| Button row | 8 or 12 px |
| Table rows (row padding y) | 12 px |
| List item → meta | 4 px |
| Card group | 16 or 24 px |
| Marketing section → section | 80–120 px |

---

## 9 · Corner Radii

Generously modern. The range tops out at pill (`100px`) for primary CTAs — a deliberate MongoDB-adjacent move that softens the ops-tool density.

| Token | Value | Used on |
|---|---|---|
| `--chk-radius-sm` | 4 px | Inline pills inside inputs, small badges |
| `--chk-radius-md` | 8 px | Inputs, small buttons, tags |
| `--chk-radius-lg` | 12 px | Cards, panels, alerts |
| `--chk-radius-xl` | 16 px | Modals, drawers, hero cards |
| `--chk-radius-2xl` | 24 px | Marketing splash tiles, stat blocks |
| `--chk-radius-3xl` | 32 px | Oversized marketing features |
| `--chk-radius-pill` | 100 px | **Primary CTAs.** Pill buttons are the default for primary actions. |
| `--chk-radius-round` | 9999 px | Avatars, status dots, circular icon buttons |

### Button shape conventions

- Primary CTA → `--chk-radius-pill` (100 px). Pill is non-negotiable for the primary action on any marketing or hero surface.
- Secondary / utility button → `--chk-radius-md` (8 px). Used in dense admin surfaces where the pill would feel out of place.
- Icon-only button → `--chk-radius-round` (circle).

---

## 10 · Borders

Hairline always. Never thicker than 1 px except on focus rings and the deliberate 2 px signal underline.

| Token | Light | Dark | Used on |
|---|---|---|---|
| `--chk-border` | `#E4E8E5` | `#163028` | Cards, hairlines, input rest |
| `--chk-border-soft` | `#EEF1EF` | `#0F2018` | Table row dividers |
| `--chk-border-hard` | `#D4DAD6` | `#1F4036` | Input focus rest, pressed button |
| Focus ring | 2 px outline in `--chk-signal-glow` + 2 px offset + 2 px inner `--chk-signal` | Same | Every focusable element |
| Signature underline | 2 px solid `--chk-signal` | Same | Active tab, feature heading |

---

## 11 · Shadows

Teal-tinted, multi-layer, never coloured (no signal-green shadows). In dark mode the ramp darkens — never glows.

### Light-mode shadows

| Token | Value | Used for |
|---|---|---|
| `--chk-shadow-xs` | `0 1px 2px rgba(10, 22, 18, 0.06)` | Raised card rest |
| `--chk-shadow-sm` | `0 2px 6px rgba(10, 22, 18, 0.08)` | Card hover |
| `--chk-shadow-md` | `0 6px 16px rgba(10, 22, 18, 0.10)` | Dropdown |
| `--chk-shadow-lg` | `0 12px 28px rgba(10, 22, 18, 0.12)` | Popover, context menu |
| `--chk-shadow-xl` | `0 26px 44px rgba(10, 22, 18, 0.14), 0 7px 13px rgba(10, 22, 18, 0.10)` | Modal, hero card |

### Dark-mode shadows

| Token | Value |
|---|---|
| `--chk-shadow-xs` | `0 1px 2px rgba(0, 0, 0, 0.40), inset 0 1px 0 rgba(255, 255, 255, 0.03)` |
| `--chk-shadow-sm` | `0 2px 6px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.04)` |
| `--chk-shadow-md` | `0 6px 16px rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.04)` |
| `--chk-shadow-lg` | `0 12px 28px rgba(0, 0, 0, 0.60), inset 0 1px 0 rgba(255, 255, 255, 0.04)` |
| `--chk-shadow-xl` | `0 26px 44px rgba(0, 0, 0, 0.65), inset 0 1px 0 rgba(255, 255, 255, 0.05)` |

The inset top-highlight on dark shadows reads as a soft overhead light — the card feels like it sits in a lit environment.

---

## 12 · Motion

Motion should feel like a turnstile light going green — quick, confident, not decorative. Fades and 2–4 px slides only. No bounces, no scale-from-zero entrances.

| Token | Duration | Uses |
|---|---|---|
| `--chk-dur-fast` | 120 ms | Hover, press, tooltip, scan-success flash |
| `--chk-dur-base` | 180 ms | Tab changes, reveals, state changes |
| `--chk-dur-slow` | 280 ms | Modal open, drawer slide, route transitions |
| `--chk-ease-standard` | `cubic-bezier(0.2, 0, 0, 1)` | All motion |
| `--chk-ease-emphasis` | `cubic-bezier(0.3, 0, 0, 1)` | Hero reveals, kiosk state changes |

### Rules

- Don't animate text. Animate containers.
- Scan success: 120 ms signal-tint flash over the scanner frame, then 280 ms fade. No scale, no rotate.
- Row state change: 180 ms colour + icon crossfade. No height change unless the row's content size actually changed.
- The signal dot on the logo pulses `2.4s ease-out infinite` between full-opacity and 0.6, only on the kiosk / scanner surfaces. Never on the admin dashboard.
- Respect `prefers-reduced-motion`: all motion under 120 ms disables; scanner pulse pauses; drawer slides become fades.

---

## 13 · Layout

### Breakpoints

| Name | Min width | Primary use |
|---|---|---|
| `sm` | 640 | Phone landscape |
| `md` | 768 | Tablet portrait |
| `lg` | 1024 | Tablet landscape, small laptop |
| `xl` | 1280 | Laptop, desk admin |
| `2xl` | 1536 | Large display, venue wall screen |

### Container widths

| Name | Max width | Used for |
|---|---|---|
| `sm` | 640 px | Auth screens, narrow forms |
| `md` | 768 px | Standard forms, single-column prose |
| `lg` | 960 px | Docs, settings |
| `xl` | 1200 px | Dashboards, tables |
| `2xl` | 1440 px | Full-width dashboards |
| `3xl` | 1680 px | Marketing hero on ultrawide |

### Dashboard grid (≥ `lg`)

- **Sidebar** · 240 px fixed
- **Main** · fluid
- **Right rail** · 320–360 px (selected-row preview) — collapses at `lg`

Below `lg`, collapses to a single scroll column; sidebar becomes a top drawer triggered by a hamburger in the app bar.

### Marketing grid

- 12-column fluid grid with 24 px gutters
- Max content width 1200 px, centred
- Hero sections edge-to-edge with 96 px vertical padding

---

## 14 · Components — The Contract

Every primitive below has a contract: what it's for, when to use it, when not to. The Nuxt UI primitives carry CheckInHub tokens via `app.config.ts`; the custom components listed at the end live under `app/components/`.

### Button

Variants carry state. Never decoration.

| Variant | Shape | Use |
|---|---|---|
| Solid primary | Pill (100 px) | The single primary action on a surface. Never two solids on one card. |
| Outline | Pill (100 px) | Secondary action. Always paired with a primary. |
| Ghost | md (8 px) | Tertiary action (Cancel, row-level). |
| Solid danger | md (8 px) | Irreversible actions. |
| Subtle | md (8 px) | Dense admin tables / toolbars. Low-weight secondary. |

Sizes: `xs · sm · md · lg · xl`. Default `md` (32 px). Primary page CTAs and form submits use `xl` (40 px). Kiosk uses `xl` minimum 48 px height.

Icon before label when label ≤ 2 words. Loading state: `i-lucide-loader-2` spinning in place of the leading icon — never in place of the whole label.

### Badge / Tag

Solid, soft, outline, or subtle. Used to carry state on rows and cards. Pair with the right colour from §4's attendee-state triad.

**Chips** are for user-authored content tokens (tags on an attendee, labels on an event). **Badges** are for system-authored state. They look similar; the distinction matters for voice — chips can be removed, badges can't.

### Chip (removable)

Pill (100 px), soft-tint background, 12 px × 4 px padding, `i-lucide-x` remove affordance on the trailing edge.

### Avatar

Deterministic soft-tint background from the name hash. Circle radius. Initials in `.type-label` weight, `--chk-ink-0` foreground. Never show a broken-image icon — fall back to initials on load failure.

### Avatar Group

Up to 4 avatars stacked with −8 px overlap. Fifth+ collapses into a `+N` avatar in the same style.

### Card

1 px `--chk-border` border, 12 px radius, `--chk-shadow-xs` at rest, `--chk-shadow-sm` on hover when the whole card is clickable. One heading + one primary action per card. A card with two primary buttons is a layout bug.

Dark-surface cards add the inset top-glow shadow and use `--chk-forest-700` for the border.

### Input

40 px height default (`size="xl"` via `app.config.ts`). 8 px radius. Left icon optional (always 16 px, `--chk-ink-2`). Error state: `--chk-danger` on border, helper text turns danger-coloured. Focus: 2 px `--chk-signal` ring, 2 px offset.

### Select, Textarea, NumberInput, PinInput, ColorPicker, FileInput

Same size / radius / focus contract as Input. Textarea min-height 96 px, resize vertical only.

### Switch

Track 44 × 24 px, thumb 20 px. Active: `--chk-signal` track fill, white thumb. Inactive: `--chk-sage-200` track, `--chk-sage-400` thumb.

### Checkbox, Radio

18 × 18 px. Active fill `--chk-signal`, foreground `--chk-signal-fg`. Unchecked border `--chk-border-hard`.

### Slider

4 px track in `--chk-border-hard`, progress in `--chk-signal`. Thumb 18 px, `--chk-bg-1`, 2 px border `--chk-signal`, shadow `--chk-shadow-sm`.

### Progress / Meter

4 px track, 999 px radius. Progress `--chk-signal`; at 100% auto-flash to `--chk-success` (deeper emerald). Meter adds tick marks at 25/50/75 in `--chk-border-soft`.

### Tabs

Underline variant for page-level navigation; pill variant for content-level segmentation. Active tab: 2 px `--chk-signal` underline + `--chk-ink-0` text. Inactive: `--chk-ink-2`. Hover: `--chk-ink-1`.

### Breadcrumb

`.type-meta` weight, `--chk-ink-2` non-active, `--chk-ink-0` current page, `i-lucide-chevron-right` separator at 12 px.

### Pagination

Pill buttons, only current page is solid accent; others are ghost. `i-lucide-chevron-left/right` for prev/next.

### Table

13 px body, 12 px vertical row padding, 1 px row divider (`--chk-border-soft`). Amount / count / duration columns always tabular. Status columns always a badge. Row hover `--chk-bg-3`. Selected row: 2 px `--chk-signal` left-edge bar + `--chk-bg-4` background.

### Data Table

Table + column-resize + sort + per-column filter + pinned first column. Header row 32 px, sticky on scroll. Sort indicator is a single chevron in `--chk-ink-2`, flipping colour to `--chk-signal` when active.

### Accordion

Chevron on the trailing edge rotates 90° (not 180°) on expand. Header 48 px, `.type-h4` weight.

### Collapsible

Headerless accordion variant. Used inside forms to reveal optional field groups.

### Alert

Used for persistent messages only (ongoing issue, pending migration, config warning). Transient feedback goes through toast.

Four variants: `info`, `success`, `warning`, `danger`. Always carry an icon. Body in `.type-body-sm`.

### Tooltip

Describes the *what*, not the *why*. Max one line, 28 ch. If it needs more, it's a helper text or a popover.

Dark tooltip on light surface, light tooltip on dark. 6 px radius. 8 px offset.

### Popover

Free-form content, 280–360 px max width. Anchored to a trigger; arrow at 6 px offset. Use instead of Modal for non-task UI (info panel, filter builder).

### Modal

For focused tasks with their own form. Max width by task: `sm` (confirmations) = 400 px; `md` (single-form) = 560 px; `lg` (wizard step) = 720 px. Close on ESC + overlay click. Primary action bottom-right, Cancel bottom-left.

### Drawer / Slideover

For detail views — an attendee, an event, an equipment item — where surrounding context stays visible. 480 px default width, slides from right. 280 ms slide duration. Cannot be nested.

### Command Palette

`⌘K` / `Ctrl+K`. 560 px modal with input + grouped results. Shortcut hints render in `<UKbd>` with JetBrains Mono 11 px.

### Context Menu

Right-click on rows. Uses dropdown-menu styling. Destructive actions isolated at the bottom with a separator above.

### Dropdown Menu

Ghost trigger, anchored menu with 8 px radius + `--chk-shadow-md`. Items 32 px tall, `.type-body-sm` weight.

### Navigation Menu

Top-nav pattern for marketing. Grouped mega-menu supported for product family expansion.

### App header (persistent)

**Always dark.** `forest-900 → forest-950` gradient at 78% opacity with `backdrop-filter: saturate(180%) blur(18px)`, yielding a frosted-glass nav over the content below. Forest-800 pill segment for view-switching (lighter-dark inside the dark). A 1 px signal-green glow line along the bottom edge marks the boundary between nav and content. `z-index: 10000` — sits above every other element including cursor orb and section rail.

### App sidebar (dashboard)

**Always dark.** Same `forest-900 → forest-950` gradient as the header. Nav items:
- Inactive: `--chk-forest-300` text, transparent background.
- Hover: `--chk-forest-100` text, `--chk-forest-800` background.
- Active: `--chk-forest-100` text, `--chk-forest-800` background, 2 px `--chk-signal` inset left-edge bar.

240 px fixed width on desktop, collapses to a top drawer below `lg`. A single 1 px `--chk-forest-800` vertical hairline separates it from the light main column.

### Toast

Transient feedback, top-right on desktop, top-centre on mobile. Never for errors that require action. Auto-dismiss after 4 s (success), 6 s (info), never (danger without explicit dismiss).

### Skeleton

Content shape placeholder. Never spinners in content areas. 5 rows on table load, 40 ms staggered fade-in. `.chk-skeleton` utility: `--chk-bg-3` base, animated gradient sweep in `--chk-bg-2`.

### Kbd

JetBrains Mono 11 px, 8 px padding, 4 px radius, 1 px `--chk-border-hard` border. `⌘` and `⌥` render as glyphs, never as "cmd".

### Separator

1 px `--chk-border-soft` horizontal rule. 16 px vertical margins inside cards, 32 px between sections.

### Stepper

Horizontal on desktop, vertical on mobile. Completed step: `--chk-signal` dot. Active step: `--chk-signal` ring + `--chk-ink-0` label. Future step: `--chk-border-hard` dot.

### Carousel

Used on marketing only. 40 ms drag friction, snap on release. Dots in `--chk-border-hard`, active dot `--chk-signal`.

### Custom: ScannerFrame

Full-bleed camera container with 4 corner brackets in `--chk-signal`. Scan line animates `2.4s ease-out infinite` vertically. Scan-success flashes `--chk-signal-tint` over the frame for 120 ms, then auto-resumes. Top-right chip shows event name + live count.

### Custom: KioskCard

Large vertical card (720 × 960 min), always dark-themed. Attendee name in `.type-display-xl` (Instrument Serif, 104 px, weight 400). Three tap targets: `Check in` (solid primary pill, 64 px height), `Depart` (outline pill, 64 px), `Not you?` (ghost, 48 px). 48 px gutters. Optimised for tablet portrait in a wall mount.

### Custom: LiveCount

Circular count ring — SVG circle with `--chk-signal` stroke, starting at 12 o'clock and filling clockwise with the checked-in percentage. Centre: count in `.type-display` (Instrument Serif, 80 px). Below: `.type-meta` labels for registered and checked-in totals.

### Custom: AttendeeRow

Table row template with avatar + name + expected-arrival + status badge + actions menu. Selected state uses the 2 px signal left-edge bar.

### Custom: EquipmentChip

JetBrains Mono equipment ID + Lucide equipment-type icon. Chip radius, soft-tint by type (audio / video / computer / furniture / other each mapped to a reserved hue).

### Custom: EmptyState

Centred icon in a 48 px signal-soft circle, title in `.type-h4`, body in `.type-body` max 42 ch, single primary + optional outline CTA below.

---

## 15 · Form Patterns

- **Labels above inputs.** 6 px gap. `.type-label` weight.
- **Helper text below.** `.type-meta`, grey ink. Grows to `--chk-danger` on error.
- **Required fields have no asterisk.** Instead, mark optional fields with ` (optional)` in the label.
- **Submit right-aligned** on desktop, full-width on mobile. `Cancel` to the left of `Submit` on desktop.
- **Two-column forms** only when fields are naturally paired (`first name` / `last name`, `start date` / `end date`). Never for unrelated fields.
- **Validation on blur**, not on keystroke. On submit if the field has never been touched.
- **Password fields** render as `PIN` length constraints when short (4-digit event PIN → `<UPinInput>`; 8-char access code → `<UInput>` with `font-family: mono`).

---

## 16 · States

### Empty

- Icon in `--chk-signal-soft` circle, 48 px
- Title: `.type-h4`, sentence case
- Body: `.type-body`, max 42 ch
- Primary CTA + optional outline CTA

### Loading — skeleton-first, always

**Skeleton-first is non-negotiable.** Every async surface in CheckInHub renders a skeleton placeholder that mirrors the shape of the real content while it loads — never a spinner in the content area, never a blank page, never a "Loading…" string. The skeleton is the state, not a transition.

The rule applies to:

- Tables (5 skeleton rows, 40 ms staggered fade-in, matching the real row height + 1-px divider)
- Cards (one skeleton card per expected slot, matching radius / padding / shadow)
- Stats strips (each stat shows a 24 px label-line skeleton + 32 px value-line skeleton)
- Charts (axis frame visible, plotted area is a skeleton shimmer)
- Forms (each field rendered as label + skeleton input until data hydrates)
- Avatars (round skeleton at the same diameter)
- Live count rings (full ring at neutral border colour, no fill until data lands)
- Detail slideovers / modals (skeleton header + body until data resolves)

Implementation:

- Use Nuxt UI's `<USkeleton>` for primitives that match a single token shape.
- Use the brand `.chk-skeleton` utility (`main.css`) for custom shapes — gradient sweep over `--chk-bg-3` → `--chk-bg-2`, 1.2 s loop, `prefers-reduced-motion: reduce` disables animation but keeps the placeholder visible.
- On dark surfaces (forest), skeletons use `--chk-forest-800` → `--chk-forest-700` so they're visible against the dark background.
- Skeleton DOM matches the real content's DOM tree as closely as is reasonable so the layout doesn't reflow when data arrives.

Spinners are reserved for **two** narrow cases only:

1. Inline button progress (the leading icon swaps for `i-lucide-loader-2` while the click is in flight, and only when the action takes > 500 ms).
2. Full-screen route transitions during cold-start hydration (very rare, only on first paint).

Never use a spinner inside a card, table, modal body, slideover body, or chart area. If the data takes more than a few seconds to load, surface a contextual message via DESIGN.md §14 **Alert** ("Still loading — large dataset, hold tight"), keep the skeleton on screen, and offer a cancel.

### Skeleton component contract

The reusable component name is `Skeleton<Thing>` (e.g. `SkeletonTableRow`, `SkeletonStatCard`, `SkeletonAttendeeRow`). Each skeleton lives next to its real component in `app/components/` and is registered in §45 of SYSTEM.md.

Every list / detail surface ships **two** components:

- `<Thing>` — the rendered surface
- `<Thing.Skeleton>` (or sibling component) — the skeleton placeholder

Suspense / `<Suspense>` boundaries fall back to the matching skeleton automatically.

### Error

- Alert for persistent errors ("Couldn't load attendees. Retry")
- Form field errors go inline (helper text turns danger)
- Fullscreen error is last-resort and always includes a retry action

### Success (transient)

- Toast, top-right, auto-dismiss 4 s
- Icon in success-soft circle, `.type-label` title, `.type-meta` body
- Signal-flash on the card / row that caused the success

---

## 17 · Dark Surfaces

Opt-in via `[data-theme="dark"]` on any subtree. Only surface / ink / border / shadow tokens move — signal, accent, and status stay constant.

The primary dark-mode surfaces:
- **Kiosk mode** — tablet-mount check-in card
- **Scanner** — full-bleed camera / QR capture surface
- **Marketing hero** — homepage hero, feature tiles
- **Overlay modals on dark origin** — kiosk-triggered confirmations

Shadows darken in dark mode and include an inset top-highlight (see §11). Accent and signal tokens are `rgba()` so they render identically on both themes.

---

## 18 · Accessibility

- **Contrast.** Body text ≥ 4.5 : 1 vs background. Large text (≥ 18 pt or 14 pt bold) ≥ 3 : 1. Accent on white is 5.70 : 1, signal on forest-900 is 7.20 : 1 — both exceed AA.
- **Focus rings.** Always visible, 2 px `--chk-signal` outline + 2 px offset + outer `--chk-signal-glow`. Never removed.
- **Keyboard.** Every interactive reachable by Tab. Skip-link at top of every page lands on `#main-content`.
- **Screen readers.** Icons inside icon-only buttons need `aria-label`. Decorative icons use `aria-hidden="true"`.
- **Motion.** Respect `prefers-reduced-motion`. Scanner pulse pauses; drawer slides become fades; signal flash stays (it's informational).
- **Colour alone is never the only signal.** Checked-in rows have a check icon, not just green. Overdue rows have an icon, not just red.
- **Touch targets.** 44 × 44 px minimum on desktop, 48 × 48 px on kiosk / mobile.

---

## 19 · Tone & Voice

Professional, plainspoken, event-driven. The voice of a competent event manager writing for their team — factual, calm, efficient. British English spellings, full words rather than contractions where natural, precise event terminology. No hype. No exclamation marks. No emoji. Address the user as "you".

| Context | DO | AVOID |
|---|---|---|
| Primary action | "Open check-in" | "Let's get scanning!" |
| Secondary action | "Create event" | "Create a shiny new event" |
| Empty state | "No events yet. Create one to begin." | "Oh no, it's quiet in here…" |
| Error | "Attendee not found. Check the barcode and try again." | "Oops! Something went wrong." |
| Success message | "142 attendees imported." | "All done! 🎉" |
| Field label | "Expected arrival" | "When are they arriving?" |
| Destructive confirm | "Delete event · this cannot be undone" | "Are you sure you want to do this?" |
| Inline helper | "Doors open 30 minutes before the event starts." | "Heads up — doors open soon!" |
| Kiosk prompt | "Not you?" | "Wrong attendee?" |
| Scanner subtitle | "Hold the code in the frame." | "Scanning... please wait!" |
| Status · Registered | "Registered" | "Expected" (acceptable as meta, not label) |
| Status · Checked in | "Checked in" | "Arrived" |
| Status · Departed | "Departed" | "Checked out" (avoid in product copy) |
| Status · Absent | "Absent" | "No-show" (too casual) |

### Proper nouns

Always capitalised exactly: **CheckInHub**, **QR**, **PIN**, **CSV**, **API**, **SSO**.

### Event terminology — the canonical set

Use these terms consistently throughout the product. They are the attendee lifecycle, in order:

1. **Registered** — the attendee exists in the event list; a QR code has been issued.
2. **Checked in** — the attendee has been scanned through the door.
3. **Departed** — the attendee has been scanned out.
4. **Absent** — the event has ended and the attendee never checked in.

Supporting terms: **Walk-in** (an attendee who was not registered but checked in at the door), **Capacity** (the configured maximum), **Expected arrival** (a soft target, not a hard time).

### Microcopy contracts

- Buttons never end in a full stop.
- Errors never start with "Oops" or "Sorry".
- Success messages state the outcome, not the emotion.
- Toast titles are nouns (`3 confirmation emails sent`), not verbs (`Sent!`).
- Counts precede the noun (`3 checked in`, not `checked in: 3`).
- Kiosk copy is instruction + reassurance in ≤ 6 words. "Hold the code in the frame."
- Marketing copy is claim + proof. "142 check-ins per minute. Without the queue."
- Use *British English* where it differs: "organisation", "recognise", "colour", "cancelled", "whilst".

---

## 20 · Do's & Don'ts

| Topic | DO | DON'T |
|---|---|---|
| Signal green | Small, deliberate, electric: underlines, focus rings, active dots. | Use on large surfaces or as a fill colour. |
| Accent green | Button fills, link text, pressed states. | Confuse with signal — it's the workhorse, not the pulse. |
| Dark surfaces | Blue-teal (`#071521`), never pure black. | Pure black. Warm-toned darks. |
| Display type | Instrument Serif, weight 400 at 52–104 px. | Serif below 32 px. Bold serif (reads wrong). |
| Header / sidebar | Always forest-dark with backdrop blur. | A light nav or sidebar — that's not the product. |
| Status | Carry state only. Pair with icon + tag. | Colour a whole card with status. |
| Numbers | Tabular. Consistent duration format. | Proportional figures. Mix `2:14` and `2h 14m`. |
| Composition | 4 px grid. One heading + one primary per card. | Off-grid offsets. Two primaries per card. |
| Shadows | Teal-tinted (`rgba(10, 22, 18, …)`), darker in dark mode. | Coloured shadows. Generic neutral-black shadows. |
| Motion | 120 / 180 / 280 ms, ease-out, fade-and-slide. | Bounce, elastic, scale-from-zero, spring. |
| Pill buttons | Primary CTA pill, always 100 px. | Pill on secondary / utility buttons. |
| Signal underline | One per section, on the single most important heading. | Two signal underlines on one section. |
| Voice | Plainspoken, brief, factual. | Exclamation marks. Emoji. Hype. Sorry-opening errors. |
| Branding | Hex only at the token layer. | Hex literals in components or `<style>` blocks. |

---

## 21 · Brand In Action

### Marketing homepage (dark hero on forest-950)

Full-bleed `#071521` canvas with layered radial halos (`rgba(0, 237, 100, 0.18)` and `rgba(0, 237, 100, 0.10)` offset). The CheckInHub ticker runs across the top edge. Headline in Instrument Serif display-xl ("Every attendee / accounted for.") with a neon-green underline under the final two words. One paragraph of `.type-body-lg` at `--chk-forest-200`. Two pill CTAs: primary solid accent, secondary ghost-on-dark. An `AttendeeLanyard` mock sits adjacent to the hero copy so the brand's primary artefact is immediately legible.

### Admin dashboard (dual-mode)

- **Sidebar** (240 px, forest-dark gradient) — nav items in `--chk-forest-300`, active item on `--chk-forest-800` with a 2 px signal inset bar.
- **Main column** (fluid, `--chk-bg-0`) — light paper surface, event list as cards or a data table, stats strip at the top.
- **Right rail** (320–360 px, light) — selected event's LiveCount ring + last-10 check-ins stream.

Every figure tabular. Active event row: 2 px signal left-edge bar + `--chk-bg-4` soft-tint background. Data table header sticky on scroll.

### Scanner (dark)

Forest-950 canvas. Full-bleed camera frame with 4 signal-green corner brackets. Scan line animates `2.4s ease-out infinite`. Persistent chip top-right: event name + live count on a `rgba(10, 22, 18, 0.7)` frosted pill with a signal-border. Scan success: 120 ms signal-tint flash over the frame. Below the frame, subtitle: "Hold the code in the frame." in `.type-body-sm` at `--chk-forest-200`.

### Kiosk (dark, tablet portrait)

Forest-950 canvas, no chrome. `KioskCard` centred, 720 × 960. Attendee name in `.type-display-xl` (Instrument Serif, 104 px). Primary `Check in` button 64 px solid pill. Outline `Not you?` button below. Signal dot in the top-right corner pulses 2.4 s.

### Email (light)

Plain single-column, 600 px wide. 2 px `--chk-signal` bar at the top. Logo lockup. Sentence-case subject lines. No marketing copy in transactional emails. Footer includes `event name · date · venue` so a forwarded mail still makes sense.

### Ticker band

Thin horizontal strip between header and hero. Forest-950 background, scrolling mono feed of live state ("89 checked in · 53 expected · Scanner 1 online · Last check-in 00:14:22 ago"). Signal dot + `LIVE` chip pinned to the left. 48 s scroll, pauses on `prefers-reduced-motion`.

---

## 22 · Versioning & Change Control

This spec is a living document. Every meaningful change bumps the "Last updated" date. Structural changes (new token families, breaking rename of a semantic variable) are noted under a Changelog section appended below this one.

The canonical file lives at `/DESIGN.md` in the Design repo; a runtime-readable copy lives at `/public/DESIGN.md` so the gallery can render it inline. Keep both in sync in the same commit.

A companion copy lives at `../App/DESIGN.md` so the product repo can reference the same spec locally. The Design repo is always the canonical source; the App-side copy is a mirror.

---

*CheckInHub · designed to be reliable, built to be re-skinnable, tuned to feel like infrastructure.*
