Generated token reference · built from app/css/main.css @ bc1f3e8f · v1.5.1. · 2026-11-03 · regenerate with node tools/gen-design-tokens.mjs in the app repo — the suite fails when this page goes stale
Sections: colors · colors-light · colors-brutal · typography · spacing · radii · structure · components · principles · maintenance — every token has its own id: #--bg, #--accent, #--nb-ink …
Pair table — computed from the tokens above (WCAG 2.x), not hand-measured:
| Pair | Ratio | Grade |
|---|---|---|
| Dark: --text on --bg | 17.11 | AAA |
| Dark: --accent on --bg | 7.05 | AAA |
| Dark: --text on outgoing bubble | 7.28 | AAA |
| Dark: --text-dim on --bg | 5.67 | AA |
| Dark: --text-meta-in on incoming bubble | 5.29 | AA |
| Dark: --text-meta-out on outgoing bubble | 4.59 | AA |
| Light: --text on --bg | 16.15 | AAA |
| Light: --text on outgoing bubble | 15.27 | AAA |
| Light: --text-dim on --bg | 5.56 | AA |
| Light: --accent-text on --bg | 5.14 | AA |
| Light: --accent on --bg | 3.01 | AA large/UI |
| Light: --green on --bg | 5.51 | AA |
| Light: --danger on --bg | 5.33 | AA |
| Light: --accent-2 on --bg | 3.40 | AA large/UI |
Single stack: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif. Half-pixel steps are deliberate (compact ladder).
| Role | Size | Where |
|---|---|---|
| Page/brand title | 22–32 | drawer name 17, modal title 18, splash 22+ |
| Body / message text | 15.5 | msg-text, composer-input |
| Buttons / rows | 14–15 | btn-primary 15, toast 14, info-row 14.5, file-name 14.5 |
| Secondary | 13–13.5 | reply 13.5, msg-sender 13.5, day-chip 13, tabs 13.5 |
| Meta | 11–12.5 | msg-meta 11.5, chips 11–12.5, service 12.5 |
Bar height scales with the value. Blue = 2px grid. Amber (†) = odd step, documented exception — not a bug (optical bubble pad, 48px composer/button, media half-bleed, pill chrome). Counts 2026-10-14, padding/margin/gap only (var/calc/env excluded): 1×6, 3×8, 5×17, 7×9, 9×3, 11×3, 13×2, 15×1. On-grid ~89.5%. Do not snap the exceptions. Above the ladder: 24×4, 26×1, 32×1 (structural).
| Tier | Values | Used for |
|---|---|---|
| Gap — tight | 2 · 4 | icon↔label, head-actions, meta |
| Gap — default | 6 · 8 | rows, bubbles, most flex stacks |
| Gap — loose | 10 · 12 · 14 | chat-item↔avatar (11†), modals, drawer-head |
| Padding — tight | 4–6 | chips, service-msg, tabs |
| Padding — standard | 8 · 10 · 12 | rows 9†/12, bubbles 7† 10 6†, inputs 10 12 |
| Padding — wide | 14 · 18 | composer 14, toasts 14, modal/drawer 18 |
Ladder: 6 (small controls) · 8 (rows/buttons) · 10 (cards) · 12 (chips/badges) · 14 (bubbles, --radius-bubble) · 16 (modals/composer) · 999 (pills) · 50% (avatars/round btns). The 9/11/18 one-offs were snapped in d3526f4; the remaining 9px is the pin-tray count bubble — a deliberate half-height circle.
| Token / element | Value | Notes |
|---|---|---|
| --head-h | 56px | sidebar + chat header, + env(safe-area-inset-top) |
| --list-bar-h | calc(64px + safe) | 46px bar-btn + 18px padding |
| --radius-bubble | 14px (dark/light) · 6px (brutal) | only per-theme radius token |
| Bar buttons | 46px, svg 22 | sidebar bottom bar |
| Composer | min 48px, svg 26 | input pad 13/16 tuned to hit 48 with 15.5px text |
| Icon buttons | 40px, svg 20–22 | 50% radius |
| Avatars | 56 (drawer) · 44 · 42 · 36 · 34 | 22% tile radius, photo pad 2.5% |
| Shadow | 0 4px 18px rgba(16,16,16,.45/.18) | brutal: 4px 4px 0 ink |
| --ico-1..5 | 14 / 16 / 20 / 22 / 26px | icon ladder: inline · small ui · rows/buttons · bars · composer |
| Motion | .15s hover · .2s panels | see SVG “Motion & states” |
Live renders of every component and its states: design/components — generated from the same CSS as the cards below.
How the shared controls are meant to be used — this resolves the ghost-button/link ambiguity from the design review (#182). States everywhere: hover changes surfaces, never text; keyboard focus is a 2px --focus-ring outline; pressed fills use --bg-active / --accent-active.
| Component | Class | Contract |
|---|---|---|
| Button — primary | .btn-primary | The one filled accent action per view (accent fill, --on-accent ink). One per dialog foot or screen; never in lists. |
| Button — text (tertiary) | .btn-text | Compact accent-text action for rows, footers and secondary choices. It is a BUTTON, not a link: use it for something that happens (Retry, Use for sending), never inline inside a sentence. Borderless — the accent text at weight 600 is the affordance. |
| Button — destructive | .btn-text + --danger ink | Destructive = .btn-text colored var(--danger) (Remove, Delete). No filled red variant: destructive is a choice, not a destination — the confirm dialog carries the weight. Red fills are reserved for the call decline control. |
| Link | <a> | Navigation, underlined. On app surfaces accent-colored links read --accent-text (AA in every theme); raw --accent is UI-grade only in light. |
| Icon button | .icon-btn | 40px round, svg 20–22, 50% radius; icon-only controls carry an aria-label. (≥44px effective touch target: under review, #182.) |
| Tabs | .chat-cats / .side-tabs | Equal-width chips; active = accent fill + --on-accent-chip ink; a disabled category hides its chip entirely (#75), it never greys out. |
| Chip | .relay-flag, day-chip, bot-chip… | Passive metadata pill (meta sizes 11–12.5). Status chips use their status token, never accent. |
| Text field | .text-field | 15.5px text, --text-dim border, 8px radius. The placeholder is a hint, not a label — label above or aria-label. |
| Dialog | .modal | Bottom sheet on mobile, centered panel on desktop; one primary action in the foot, cancel as .btn-text; history-back integration follows the modalHistorySettled contract (app AGENTS). |
| Menu item | .ctx-item | Row menu entry: icon + label; hover = --bg-hover surface; destructive entries take --danger ink; focus-visible 2px --focus-ring. |
Button states: default → hover (--bg-hover on ghost/tonal, --accent-hover on fills) → pressed (--bg-active / --accent-active) → focus-visible (2px --focus-ring, offset 2px) → disabled (not-allowed cursor, --border edge, dimmed ink — see .ivc-btn, the reference implementation). There is no button-level loading state: async actions disable or move progress to the surface it belongs to (corner pill, transfer pill, steps list) — buttons stay truthful.
| Pair (WCAG 2.x) | Ratio | Grade |
|---|---|---|
| Dark: --text on --bg | 17.11 | AAA |
| Dark: --accent on --bg | 7.05 | AAA |
| Dark: --text on outgoing bubble | 7.28 | AAA |
| Dark: --text-dim on --bg | 5.67 | AA |
| Dark: --text-meta-in on incoming bubble | 5.29 | AA |
| Dark: --text-meta-out on outgoing bubble | 4.59 | AA (borderline) |
| Light: --text on --bg | 16.15 | AAA |
| Light: --text on outgoing bubble | 15.27 | AAA |
| Light: --text-dim on --bg | 4.93 | AA (fixed #656a76) |
| Light: --accent on --bg | 3.01 | AA large/UI only (accepted) |
| Light: --green on --bg | 3.58 | UI (fixed #3d9146) |