The EDIT button's armed face used on-tertiary (#0b1326 navy) on blue — the creator read it as grey on blue and could not read it. Same problem, latent, on the armed REC/ON-AIR pills, which used the error ink. One rule instead of two exceptions: a saturated accent fill carries WHITE ink. New token on-accent #ffffff, used by the EDIT trigger and RecordPillToggle; on-tertiary stays for large blue surfaces where dark ink is right. UiShellTests pins the EDIT foreground to it.
193 lines
10 KiB
Markdown
193 lines
10 KiB
Markdown
# 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` | Dark ink, for large blue **surfaces** only |
|
||
| `on-accent` | `#ffffff` | Text/icon on any saturated accent fill — the blue EDIT button, the armed REC/ON-AIR pills. One rule: a coloured fill carries white ink |
|
||
| `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 |
|
||
| `info-container` | `#3a3b52` | Neutral notice surface (info toast) |
|
||
| `success-container` | `#1d5c38` | Success notice surface (success toast) |
|
||
| `warning-container` | `#b8860b` | Warning surface — health banner, warning toast |
|
||
| `test-container` | `#9c6f1c` | Test-stream chrome: the gold top bar and glow |
|
||
| `youtube` | `#ff0000` | **YouTube-branded surfaces only** — the Stream Info and YPP rails, the YouTube account row. Not for state: live stays `primary` |
|
||
|
||
**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).
|