# DESIGN.md — LlamaCasty visual design standard > **Status: the standard** (decided 2026-10-30). Token values here are normative; > `Themes/Tokens.xaml` is the implementation, and no other file may hardcode a hex once the sweep > lands. Two Google Stitch exports feed it (2026-10-26 edit screen, 2026-10-29 audio dock). > > **The exports' layouts and control drawings are exploratory, not spec.** The dock, the four > bays, the page-nav sidebar and the skeuomorphic faders are *not* adopted — the product's design > principle is a surface so obvious that a right-brained person can use it without a manual, and > hardware cosplay is the opposite of that. Only the colour, type, spacing, radius and elevation > system is adopted. ## Colour ### Surfaces (dark navy scale, darkest to lightest) | Token | Hex | Use | |---|---|---| | `background` | `#0b1326` | App window background | | `surface-container-lowest` | `#060e20` | Deepest recess (scene strip background, canvas housing) | | `surface-container-low` | `#131b2e` | Panel background (sidebars, headers) | | `surface-container` | `#171f33` | Card / grouped-section background | | `surface-container-high` | `#222a3d` | Hover state, input fields | | `surface-container-highest` | `#2d3449` | Selected/active row background | | `surface-bright` | `#31394d` | Lightest surface, rare use (strong hover) | | `outline` | `#334155` | Borders, dividers | | `outline-variant` | `#1e293b` | Subtle dividers, inactive meter segments | | `scrim-soft` | `#22FFFFFF` | Hover/press wash over a surface (13% white) | | `scrim-strong` | `#26FFFFFF` | Stronger wash — selected icon, dimmed image (15% white) | | `scrim-dark` | `#26000000` | Dim strip / shading over the preview (15% black) | | `scrim-dark-strong` | `#00000080` | Heavy overlay — modal veil, poster frame (50% black) | ### Text | Token | Hex | Use | |---|---|---| | `on-surface` | `#dae2fd` | Primary text | | `on-surface-variant` | `#94a3b8` | Secondary / label text | | `on-surface-disabled` | `#5b6478` | Disabled text and icons | ### Accent — one role each, never mixed | Token | Hex | Role | |---|---|---| | `tertiary` (electric blue) | `#38bdf8` | **The** interactive accent: selection, focus, sliders, primary buttons, transform handles | | `on-tertiary` | `#0b1326` | Text/icon on a blue fill | | `primary` (red) | `#ff5451` | **On-air / recording only** — LIVE badge, program border, End/Go Live | | `primary-container` | `#b91a24` | Red pressed/hover state | | `secondary` (amber) | `#ffb95f` | Game audio channel, Super Chat highlight | | `secondary-container` | `#ee9800` | Amber pressed/hover state | | `attention` (orange) | `#ff8c3a` | Dropped frames, the live-screen indicator, blinking warnings | | `positive` | `#22c55e` | **Status only** — connected, applied, healthy. Never an accent, never a button fill | | `error` | `#ef4444` | Error text and icons | | `error-container` | `#93000a` | Error surface (health banner, toast) — keeps error visually distinct from on-air red | | `on-error` | `#ffdad6` | Text on an error surface | **The rule:** red means "live or recording" and nothing else. Never a Send button, a delete action, a brand flourish, or a general accent — that was the defect in the first Stitch draft. **`#e94560` is retired** as the app's general accent (it was in 15 files). Blue takes over every interactive role; red keeps only live/record. The brand/wordmark *artwork* is unchanged — those assets are the creator's art, not a theme value. ### Meter thresholds (both meters, all bays) | Band | Range | Brush | |---|---|---| | Nominal | −∞ … −12 dB | `#38bdf8` | | Caution | −12 … −3 dB | `#ffb95f` | | Peak / clip | −3 … 0 dB+ | `#ff5451` | Meters are **measured, never decorative** (see `docs/ai/output.md`): the ladder shows the live level scaled by the current gain, not the gain setting. ## Typography | Token | Family | Use | |---|---|---| | `sans` | Inter (400/500/600/700) | All UI text — labels, buttons, panel titles | | `mono` | JetBrains Mono (500/600) | Numeric/telemetry values — bitrate, FPS, dB, coordinates, timers. Tabular figures stop digits shifting width as values change | Icons: Material Symbols Outlined (bundled, not fetched). ### Scale | Token | Size / line | Weight | Tracking | Use | |---|---|---|---|---| | `headline-lg` | 32 / 40 | 700 | −0.02em | Landing/empty-state titles only | | `headline-md` | 24 / 32 | 600 | −0.015em | Dialog titles | | `headline-sm` | 18 / 24 | 600 | −0.01em | Panel and section headers | | `body-lg` | 15 / 22 | 400 | — | Default body, chat | | `body-md` | 13 / 18 | 400 | — | Panel rows, form text | | `body-sm` | 12 / 16 | 400 | — | Secondary text, hints | | `label-lg` | 13 / 16 | 600 | 0.02em | `mono` — primary telemetry values | | `label-md` | 11 / 14 | 500 | 0.04em | `mono` — labels, hotkey chips | | `label-sm` | 10 / 12 | 500 | 0.05em | `mono` — dense telemetry only | **Floors and implementation notes** - **11px is the default label size; 10px is the absolute floor** and only for dense readouts that sit beside a larger value. Nothing readable at 100% zoom may be smaller than 10px. - **WPF has no letter-spacing.** Tracking values above are design intent, not a property; implement as-is (no tracking) rather than faking it with padded strings. - Mono numerics use `Typography.NumeralAlignment="Tabular"` so widths stay stable. ## Spacing | Token | px | Use | |---|---|---| | `space-xs` | 4 | Inside a chip, icon-to-text | | `space-sm` | 8 | Between related controls | | `space-md` | 12 | Default gap inside a panel | | `space-lg` | 16 | Panel padding, section separation | | `space-xl` | 24 | Dialog padding, major separation | | `gutter` | 12 | Between page columns | | `margin` | 16 | Window edge to content | | `margin-docked` | 8 | Edge to content when a rail/drawer is docked | ## Shape | Token | px | Use | |---|---|---| | `radius-sm` | 4 | Chips, badges, keycap tags | | `radius-md` | 8 | Buttons, inputs, cards, scene tiles | | `radius-lg` | 16 | Preview viewport, drawers, dialogs | | `radius-full` | pill | Status chips (LIVE, EDIT), toggles | Recessed controls (fader wells, inputs) use the same outer radius as their housing so the seams line up — do not mix a 4px well inside an 8px card. ## Elevation | Level | Use | Implementation | |---|---|---| | −1 recessed | Fader wells, input fields, table headers | inset top shadow + `surface-container-lowest` | | 0 panel | The default chassis | `surface-container*` + **1px `outline` border**, no shadow | | 1 raised | Scene tiles, floating chips, action buttons | subtle drop shadow, 0 1px 2px | | 2 floating | Drawers, popups, dialogs | larger spread + 1px top highlight | **Budget rule:** `DropShadowEffect` is software-rendered in WPF. Shadows on small controls only — **never** on the preview viewport, the scene strip, or either wing. Depth on large surfaces comes from the surface scale and the 1px border, not from effects. ## Interaction - Focus ring: 1px `#38bdf8` outside a 1px gap — visible against every surface level. - Hover: step one surface level up; active: step two. No colour change on hover except for destructive actions. - Motion: 150ms `CubicEase(EaseOut)` for drawers/popovers; 90ms for hover/press. No springs — WPF has no spring easing and hand-rolled overshoot reads as jank at 60Hz. - Every action produces a visible outcome. No dead ends, no silent failures. ## Not in this standard - The audio dock, its four bays, per-bay faders, 24-segment ladders, transport controls, lip-sync slider and master telemetry strip — **exploratory**. The controls they re-house already exist in the app; whether any of them gets a dock is a separate product decision. - The page-nav sidebar (Broadcast Studio / Audio Mixer / Scene Matrix / Telemetry / Hotkeys) — the app is one surface with an EDIT toggle and drawers, not a page router. - CPU / GPU / NVENC readouts — no telemetry exists, and none of it changes a live decision. - Acrylic/backdrop blur — WPF has no sub-region backdrop blur; panels are solid. - **On-stream output** (chat box, social bar, alerts, brand flash) is styled by its own renderers, not by this theme. Do not apply UI tokens to the broadcast picture. ## Rejected from the 2026-10-29 export The export is a Material-3 dump from a warm seed; four tokens came out tinted by that seed and are rejected in favour of the values above. | Export value | Token | Why rejected | |---|---|---| | `#4cd7f6` | `tertiary` | Neon cyan beside the amber reads "toy"; `#38bdf8` is calmer and already used in-app | | `#ab8986` | `outline` | Warm pink borders from the seed — wrong for a navy chassis | | `#5b403e` | `outline-variant` | Same seed artifact | | `#e4beba` | `on-surface-variant` | Pink secondary text; slate `#94a3b8` is legible and neutral | ## Implementation status - **`Themes/Tokens.xaml` exists and is merged first in `App.xaml`** (2026-10-30). Guard: `ytLive.Tests/DesignTokensTests.cs` — it parses this document and fails if a documented token has no brush, if a value disagrees, or if the retired `#e94560` returns as a token. - **Still to sweep:** `Themes/Controls.xaml` and the dozens of hardcoded hexes in XAML/C# still use the legacy palette. Surfaces convert one at a time; the end state is that every colour, radius and spacing value resolves from `Tokens.xaml` and a guard fails on any raw `#hex` outside it. - **Fonts ship with the app** (`Assets/Fonts/*.ttf`, OFL-1.1) — no installation, no network, no dependence on what the machine happens to have. Notice and license text travel with them (`THIRD-PARTY-NOTICES.txt` items 13-14).