diff --git a/.planning/phases/05-live-show-execution/05-UI-SPEC.md b/.planning/phases/05-live-show-execution/05-UI-SPEC.md
new file mode 100644
index 0000000..9e5562f
--- /dev/null
+++ b/.planning/phases/05-live-show-execution/05-UI-SPEC.md
@@ -0,0 +1,340 @@
+---
+phase: 5
+slug: live-show-execution
+status: draft
+shadcn_initialized: false
+preset: none
+created: 2026-04-07
+---
+
+# Phase 5 — UI Design Contract
+
+> Visual and interaction contract for Phase 5: Live Show Execution.
+> Generated by gsd-ui-researcher, verified by gsd-ui-checker.
+
+---
+
+## Design System
+
+| Property | Value |
+|----------|-------|
+| Tool | none — vanilla CSS with custom properties |
+| Preset | not applicable |
+| Component library | none (hand-rolled terminal-aesthetic components) |
+| Icon library | none — ASCII/Unicode symbols inline (▶, ◼, ▶▶, ▼, ▶) |
+| Font | JetBrains Mono, Fira Code, Consolas, monospace (system fallback chain) |
+
+Source: `lightsync/frontend/style.css` `:root` block — no external design system; all tokens are CSS custom properties.
+
+---
+
+## Spacing Scale
+
+Declared values (multiples of 4 only):
+
+| Token | Value | Usage |
+|-------|-------|-------|
+| xs | 4px | Inner icon gaps, strip padding (`padding: 0 4px`) |
+| sm | 8px | Element gaps inside device strips, panel content padding top/bottom |
+| md | 12px | Header/transport bar horizontal padding |
+| lg | 16px | Timeline toolbar gap between field groups |
+| xl | 24px | — |
+| 2xl | 32px | Timeline toolbar fixed height |
+| transport | 52px | Transport bar grid row height (existing, do not change) |
+
+Exceptions:
+- Device strip row height: 18px (compact single-line strip — matches existing `.block-inspector` at 40px; preview is intentionally denser)
+- Gap between device strips: 1px (separator hairline, not a spacing token)
+- Strip color swatch: 40px wide × 10px tall (fixed decoration element)
+- Live preview panel max-height: 60px (D-06 from CONTEXT.md, overflows to scroll if many devices)
+
+Source: CONTEXT.md D-06 "Compact — 40-60px total" + RESEARCH.md Pattern 2 CSS.
+
+---
+
+## Typography
+
+All text uses `--font-mono` exclusively. No serif or sans-serif anywhere in this application.
+
+| Role | Size | Weight | Line Height |
+|------|------|--------|-------------|
+| Body / default | 13px | 400 | 1.4 |
+| Small label (panel headers, toolbar labels, strip labels) | 10px | 400 | 1.2 |
+| Compact UI text (device rows, inspector fields, transport) | 12px | 400 | 1.4 |
+| Active label / header title | 14px | 600 | 1.2 |
+
+Letter-spacing rules (terminal convention — do not deviate):
+- Panel headers, section headers, toolbar labels, strip labels: `letter-spacing: 0.10–0.14em`
+- Transport time display: `font-variant-numeric: tabular-nums` (prevents width jumping)
+- Animation name in strip: `letter-spacing: 0.05em`, `font-size: 10px` (same as `.strip-anim` pattern)
+
+Source: `style.css` — measured from `.panel-header` (10px/600 LS 0.14em), `.device-row-info` (12px), `.header-title` (14px/600), `.transport-time` (12px tabular-nums).
+
+---
+
+## Color
+
+All values use existing CSS custom properties. No new color tokens for Phase 5.
+
+| Role | CSS Variable | Hex | Usage |
+|------|-------------|-----|-------|
+| Dominant (60%) | `--bg-primary` | `#0a0a0a` | App background, main area, canvas background |
+| Secondary (30%) | `--bg-panel` / `--bg-panel-dark` | `#0f0f0f` / `#080808` | Panels, transport bar, toolbar, preview strip rows |
+| Accent (10%) | `--accent` | `#00ffff` | See accent reservation below |
+| Destructive | hardcoded `#ff3333` | `#ff3333` | Remove/delete actions only (existing pattern) |
+| Status positive | hardcoded `#00ff88` | `#00ff88` | WebSocket connected indicator dot only |
+| Beat flash | hardcoded `#ffcc00` | `#ffcc00` | Beat indicator dot flash only |
+| Border dim | `--border-dim` | `#1a4a4a` | All panel borders, strip separators |
+
+**Accent reserved for (explicit list — not for general interactive elements):**
+1. Panel header text (`color: var(--text-accent)`)
+2. Active animation tile highlight
+3. Active snap button state (background fill + border)
+4. Transport button hover state (background fill)
+5. Timeline cursor (playback position line)
+6. Selected block outline on timeline canvas
+7. Header title `LIGHTSYNC`
+8. Beat marks on timeline canvas
+
+**Live preview color fills:** The preview device strip color swatch uses the cue's `params.color` value (RGB array from the animation block) as `background-color: rgb(r, g, b)`. This is data-driven, not a design token. Inactive state uses `--text-dim` (`#555555`) as fill.
+
+Source: `style.css` entire file; CONTEXT.md D-11/D-12/D-15; Phase 1 CONTEXT.md D-11 through D-15.
+
+---
+
+## Component Inventory
+
+### New in Phase 5
+
+#### 1. Live Preview Panel (`#live-preview`)
+
+**Purpose:** Shows active animation per device, updated by `preview_update` WebSocket messages.
+
+**Placement:** Between `#timeline-canvas` and `#block-inspector` in `.main-area` flex column. (TIMELINE → LIVE PREVIEW → BLOCK INSPECTOR → TRANSPORT)
+
+**Structure:**
+```
+#live-preview.live-preview
+ └── [one per device] div.device-strip[data-device-id="{id}"]
+ ├── span.strip-label — device name, UPPERCASE, min-width 80px
+ ├── div.strip-color — 40×10px swatch, background = active animation color
+ └── span.strip-anim — animation name, UPPERCASE, dim when inactive
+```
+
+**Visual states:**
+
+| State | strip-color background | strip-label color | strip-anim text |
+|-------|----------------------|-------------------|-----------------|
+| Active (block firing) | `rgb(r, g, b)` from cue params | `--text-primary` | animation name in uppercase |
+| Inactive (no block) | `--text-dim` (#555555) | `--text-dim` | empty or `"—"` |
+| Panel hidden | `display: none` | — | — |
+
+Panel visibility: `display: flex` when a show is loaded, `display: none` before first show load.
+
+Collapsing: If device count > 3, panel height exceeds 60px and scrolls via `overflow-y: auto`. No explicit collapse toggle in v1 — scrolling is sufficient.
+
+**CSS contract (extend style.css):**
+```css
+.live-preview {
+ border-top: 1px solid var(--border-dim);
+ border-bottom: 1px solid var(--border-dim);
+ background: var(--bg-panel-dark);
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+ padding: 2px 0;
+ max-height: 60px;
+ overflow-y: auto;
+ flex-shrink: 0;
+}
+
+.device-strip {
+ display: flex;
+ align-items: center;
+ height: 18px;
+ padding: 0 4px;
+ background: var(--bg-panel);
+ gap: 8px;
+ font-size: 11px;
+ font-family: var(--font-mono);
+ letter-spacing: 0.05em;
+ transition: background-color 0.1s;
+}
+
+.strip-label {
+ color: var(--text-dim);
+ text-transform: uppercase;
+ min-width: 80px;
+ flex-shrink: 0;
+ font-size: 10px;
+ letter-spacing: 0.12em;
+}
+
+.strip-color {
+ width: 40px;
+ height: 10px;
+ background: var(--text-dim);
+ flex-shrink: 0;
+ transition: background-color 0.15s;
+}
+
+.strip-anim {
+ color: var(--text-dim);
+ font-size: 10px;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+}
+
+.device-strip.active .strip-label {
+ color: var(--text-primary);
+}
+
+.device-strip.active .strip-anim {
+ color: var(--text-primary);
+}
+```
+
+**Transition:** Color changes use `transition: background-color 0.15s` — fast enough to track 10Hz ticks without visual lag.
+
+#### 2. Show Selector UI
+
+**Purpose:** Load a saved show into the timeline. Replicates the existing audio file select pattern.
+
+**Placement:** In the transport bar, after the existing `transport-sep`, before or after the audio file controls. Use the existing `transport-file-select` + `transport-btn` pattern — no new CSS classes needed.
+
+**Structure:**
+```
+
+
+```
+
+**Label:** "LOAD" — consistent with existing "LOAD" button for audio.
+
+**Empty state:** `` when `GET /api/shows` returns empty list.
+
+**Loading state:** Button text changes to "..." while show loads, reverts to "LOAD" on complete.
+
+#### 3. Keyboard Shortcut Bindings
+
+No visual component — pure JS `keydown` handler. The executor must add `wireKeyboard()` to `app.js`.
+
+**Canonical bindings (locked — from CONTEXT.md Claude's Discretion):**
+
+| Key Combination | Action | Guard |
+|----------------|--------|-------|
+| `Space` | Play / Pause toggle | Skip if `activeElement` is input/select/textarea/button |
+| `ArrowLeft` | Seek −5 seconds | Skip if `activeElement` is input/select/textarea |
+| `ArrowRight` | Seek +5 seconds | Skip if `activeElement` is input/select/textarea |
+| `Ctrl+Z` | Undo | Skip if `activeElement` is input/select/textarea |
+| `Ctrl+Shift+Z` | Redo | Skip if `activeElement` is input/select/textarea |
+| `Ctrl+Y` | Redo (alternate) | Skip if `activeElement` is input/select/textarea |
+| `Ctrl+S` | Explicit save (triggers existing save endpoint) | Always prevent default; no-op if no show loaded |
+
+All handlers call `e.preventDefault()` before acting to suppress browser defaults (especially Space and Ctrl+S).
+
+---
+
+## Copywriting Contract
+
+| Element | Copy |
+|---------|------|
+| Primary CTA — show load | "LOAD" (button label, uppercase — matches existing transport button convention) |
+| Primary CTA — save | No button label needed; Ctrl+S only. Status feedback: "SAVED" flash on the show-select area (optional) |
+| Show selector placeholder | "— select show —" |
+| Show selector empty state | "— no shows —" |
+| Preview panel section header | None — the panel has no header. The `.strip-label` per device identifies context sufficiently |
+| Inactive strip animation text | "—" (em dash, single character — consistent with terminal dim convention) |
+| Keyboard hint (transport bar) | Optional dim label: `[SPACE] PLAY [←→] SEEK [^Z] UNDO [^S] SAVE` at far right of transport bar, `font-size: 10px`, `color: var(--text-dim)` |
+| Error: show load fails | Browser console `[show] failed to load {id}` — no modal or toast in v1 (terminal aesthetic = no popups) |
+| Error: cue fire fails (unknown animation) | Server log only: `[ws] unknown animation {name}` — not surfaced to UI |
+| Destructive action — none in Phase 5 | Phase 5 has no destructive actions. No confirmation dialogs needed. |
+
+Source: CONTEXT.md "Show save/load UX" + existing UI pattern (all error handling is console-only in terminal aesthetic).
+
+---
+
+## Interaction Contracts
+
+### Cue Scheduler Feedback Loop
+
+The UI must visually close the loop between audio position and cue firing:
+
+1. Audio plays in browser → `timeupdate` sends `tick` to server at 10Hz
+2. Server fires UDP cue → broadcasts `preview_update` message
+3. Browser receives `preview_update` → updates `.device-strip` color and animation label within one event loop tick
+4. Color transition duration: 0.15s (fast enough for 10Hz, prevents flicker)
+
+No loading spinner, no progress indicator — the live preview IS the execution feedback.
+
+### Seek Behavior
+
+When user drags the seek slider or presses ArrowLeft/ArrowRight:
+1. `audio.currentTime` updates
+2. Browser sends `{"type": "seek", "position": N}` to server
+3. Server rebuilds `fired_ids` (all cues before `position − 60ms` marked fired)
+4. All device strips reset to inactive state (dark) — browser clears `.device-strip.active` class on seek
+5. Show resumes scheduling from new position on next tick
+
+Visual contract for seek: device strips dim immediately on seek, re-light when next cue fires.
+
+### Show Load Sequence
+
+1. User selects show from dropdown → clicks "LOAD"
+2. Button shows "..." while `GET /api/shows/{id}` resolves
+3. Timeline tracks populate with saved blocks
+4. `setCurrentShowId(id)` called in commands.js
+5. WebSocket `load` message sent: `{"type": "load", "show_id": "{id}", "path": "{audio_path}"}`
+6. Server acquires cue list per-connection
+7. Preview panel becomes visible (`display: flex`)
+8. Button reverts to "LOAD"
+
+---
+
+## Layout Contract
+
+The `.main-area` flex column order for Phase 5:
+
+```
+.main-area (flex-direction: column)
+ 1. .timeline-toolbar (height: 32px, flex-shrink: 0)
+ 2. #timeline-canvas (flex: 1, min-height: 0)
+ 3. #live-preview (max-height: 60px, flex-shrink: 0) ← NEW
+ 4. #block-inspector (height: 40px, flex-shrink: 0, display:none when empty)
+```
+
+Transport bar remains in `.app-shell` grid `transport` area — it is NOT inside `.main-area`.
+
+The show selector controls are added to the existing `.transport-bar` flex row.
+
+---
+
+## Registry Safety
+
+| Registry | Blocks Used | Safety Gate |
+|----------|-------------|-------------|
+| shadcn official | none | not applicable — no shadcn in this project |
+| third-party | none | not applicable |
+
+No component registries are used. All UI is vanilla HTML/CSS/JS.
+
+---
+
+## Checker Sign-Off
+
+- [ ] Dimension 1 Copywriting: PASS
+- [ ] Dimension 2 Visuals: PASS
+- [ ] Dimension 3 Color: PASS
+- [ ] Dimension 4 Typography: PASS
+- [ ] Dimension 5 Spacing: PASS
+- [ ] Dimension 6 Registry Safety: PASS
+
+**Approval:** pending
+
+---
+
+*Phase: 05-live-show-execution*
+*UI-SPEC generated: 2026-04-07*
+*Pre-populated from: CONTEXT.md (7 decisions), RESEARCH.md (5 patterns), style.css (all tokens)*