diff --git a/.planning/phases/04-timeline-editor/04-UI-SPEC.md b/.planning/phases/04-timeline-editor/04-UI-SPEC.md
deleted file mode 100644
index 128101d..0000000
--- a/.planning/phases/04-timeline-editor/04-UI-SPEC.md
+++ /dev/null
@@ -1,318 +0,0 @@
----
-phase: 4
-slug: timeline-editor
-status: draft
-shadcn_initialized: false
-preset: none
-created: 2026-04-06
----
-
-# Phase 4 — UI Design Contract
-
-> Visual and interaction contract for the Timeline Editor phase. Generated by gsd-ui-researcher.
-> Stack is plain HTML5/CSS/JS served by FastAPI — no React, no shadcn, no build step.
-
----
-
-## Design System
-
-| Property | Value |
-|----------|-------|
-| Tool | none — hand-rolled CSS variables |
-| Preset | not applicable |
-| Component library | none — plain DOM elements |
-| Icon library | Unicode glyphs inline (▶, ■, ▼, ✕, >) — no icon font |
-| Font | JetBrains Mono, Fira Code, Consolas, monospace (system fallback chain) |
-
-Source: `lightsync/frontend/style.css` — CSS variable declarations at `:root`.
-
----
-
-## Spacing Scale
-
-Declared values (must be multiples of 4):
-
-| Token | Value | Usage |
-|-------|-------|-------|
-| xs | 4px | Gap between inline inspector fields, scrollbar width |
-| sm | 8px | Panel content padding, element gaps |
-| md | 16px | Default spacing between grouped elements |
-| lg | 24px | Section-level padding |
-| xl | 32px | Layout gaps |
-| 2xl | 48px | TRACK_HEIGHT — per-device track row height on canvas |
-| 3xl | 64px | Reserved; not used in this phase |
-
-Exceptions:
-- TIME_AXIS_HEIGHT: 24px — time ruler height above tracks
-- HEADER_WIDTH: 140px — track label column width (device names)
-- Timeline toolbar height: 32px — compact single row above canvas
-- Block inspector strip height: 40px — compact horizontal strip (D-02)
-- Resize handle hit zone: 8px on left/right edge of each canvas block
-- Snap threshold: 100ms (not a pixel dimension, but a time threshold for beat snap)
-
-Source: RESEARCH.md Pattern 1 (coordinate system constants); CONTEXT.md D-02.
-
----
-
-## Typography
-
-| Role | Size | Weight | Line Height | Usage |
-|------|------|--------|-------------|-------|
-| Body | 13px | 400 | 1.4 | Default text, form inputs, device names |
-| Label | 10px | 400 | 1.2 | Panel headers (DEVICES, ANIMATIONS, TIMELINE), toolbar labels, canvas track names |
-| Compact | 12px | 400 | 1.3 | Transport controls, animation names, inspector field values, beat tick labels |
-| Heading | 14px | 600 | 1.2 | Header title (LIGHTSYNC), block animation type label in inspector |
-
-All text: font-family `var(--font-mono)` — monospace throughout.
-Text transform: uppercase for all panel headers, labels, and inspector field names.
-Letter spacing: 0.14em on panel headers; 0.06em on animation names; 0.15em on header title.
-
-Source: `style.css` — `.panel-header` (10px/0.14em), `.header-title` (14px/600), `.transport-time` (12px), `:root` (13px base).
-
----
-
-## Color
-
-| Role | Value | Usage |
-|------|-------|-------|
-| Dominant (60%) | `#0a0a0a` | Body background, main-area background, canvas background |
-| Secondary (30%) | `#0f0f0f` (`--bg-panel`) / `#080808` (`--bg-panel-dark`) | Sidebar, header, transport bar, block inspector strip, timeline toolbar |
-| Accent (10%) | `#00ffff` | See reserved list below |
-| Destructive | `#ff3333` | Delete block hover state on remove controls only |
-| Dim border | `#1a4a4a` (`--border-dim`) | All panel borders, track separator lines, time axis tick marks |
-| Waveform background | `#1a3a3a` | Waveform peaks drawn behind blocks (very dim cyan — not accent) |
-| Beat marks | `#00ffff` at 40% opacity | Thin vertical beat tick marks on timeline canvas |
-| Playback cursor | `#00ffff` | 1px vertical line spanning full canvas height |
-| Selected block | `#00ffff` border at 1px | Selected block outline |
-| Block fill (default) | `#0a2a2a` | Unselected animation block fill |
-| Block fill (selected) | `#0d3a3a` | Selected animation block fill |
-| Ghost block (drag) | `#00ffff` at 20% opacity | Block preview during drag-and-place |
-
-Accent (`#00ffff`) reserved for:
-- Playback cursor line
-- Selected block border
-- Active animation type label in inspector (`.inspector-label`)
-- Panel header text (`--text-accent`)
-- Transport button hover state (accent fills button bg, primary bg for text)
-- Beat indicator flash in header
-- Timeline toolbar labels in focus/active state
-- `>` cursor glyph in active animation palette row
-
-Accent is NOT used for:
-- Block fills (blocks use `#0a2a2a` / `#0d3a3a`)
-- Waveform strokes (use `#1a3a3a`)
-- Time axis background ticks (use `--border-dim` at reduced opacity)
-
-Source: `style.css` CSS variables; CONTEXT.md "terminal aesthetic carries forward"; RESEARCH.md Pattern 6 (waveform color `#1a3a3a`).
-
----
-
-## Component Inventory
-
-### New in Phase 4
-
-| Component | Type | Description |
-|-----------|------|-------------|
-| `#timeline-toolbar` | HTML div (flex row) | 32px strip above canvas: zoom control, beat offset input, snap toggle |
-| `#timeline-canvas` | HTML5 Canvas | Fills remaining flex space in main-area; custom hit-testing, virtual scroll |
-| `#block-inspector` | HTML div (flex row) | 40px horizontal strip between canvas and transport-bar; hidden when no block selected |
-| Animation palette rows | HTML buttons in sidebar | Compact single-line rows below DEVICES panel; drag source for placing blocks |
-
-### Modified in Phase 4
-
-| Component | Change |
-|-----------|--------|
-| `.main-area` | Replaces animation tile grid with: toolbar + canvas + inspector (flex column) |
-| `.sidebar` | Adds ANIMATIONS panel below DEVICES; items are compact drag-source rows |
-
-### Retained Unchanged
-
-Transport bar, header, device panel, all CSS variables, all form element styles.
-
----
-
-## Canvas Rendering Contract
-
-All canvas rendering uses HTML5 Canvas 2D API. No third-party canvas library.
-
-### Coordinate System
-
-```
-HEADER_WIDTH = 140px // track label column
-TIME_AXIS_HEIGHT = 24px
-TRACK_HEIGHT = 48px
-PIXELS_PER_SECOND = 100 // default zoom; adjustable via toolbar
-```
-
-### Layer Draw Order (back to front)
-
-1. Background fill `#0a0a0a`
-2. Track row alternating banding: even `#0a0a0a`, odd `#0c0c0c` (2px brightness difference only)
-3. Waveform peaks: `#1a3a3a` vertical bars behind blocks
-4. Beat marks: `#00ffff` at 40% opacity, 1px wide vertical lines at calibrated beat times
-5. Animation blocks: filled rects `#0a2a2a`, 1px border `#1a4a4a`; selected: `#0d3a3a` fill, `#00ffff` 1px border
-6. Block labels: animation name in `#00ffff`, 10px, uppercase, clipped to block width
-7. Drag ghost: 1px border `#00ffff`, `rgba(0,255,255,0.15)` fill
-8. Track headers: `#0f0f0f` fill, device name in `--text-primary` 12px
-9. Time axis: `#080808` fill, time labels `--text-dim` 10px, tick marks `--border-dim`
-10. Playback cursor: `#00ffff` 1px line, full height
-
-### devicePixelRatio Scaling
-
-Canvas must call `resizeCanvas()` on init and on `window resize`. Scale ctx by `window.devicePixelRatio` to prevent blur on HiDPI.
-
----
-
-## Interaction Contract
-
-### Block Placement (TL-02)
-
-1. User drags from animation palette row in sidebar.
-2. Ghost block follows mouse across canvas at target track Y position.
-3. On `mouseup` over a track: snap to nearest beat if within 100ms threshold; place block at snapped time.
-4. On `mouseup` outside canvas: cancel drag, remove ghost.
-5. Attach `mousemove` and `mouseup` to `document` during drag; remove on `mouseup`. (Pitfall 2 prevention.)
-
-### Block Move (TL-03)
-
-1. `mousedown` on block center region (not within 8px edge): enter move mode.
-2. `mousemove`: update block start time live on canvas (visual only, no API call during drag).
-3. `mouseup`: snap to beat; fire `MoveBlockCommand.execute()` → updates local state → async PATCH to backend.
-
-### Block Resize (TL-03)
-
-1. `mousedown` within 8px of left or right block edge: enter resize mode; cursor changes to `ew-resize`.
-2. `mousemove`: update duration or start time live on canvas.
-3. `mouseup`: snap to beat; fire `ResizeBlockCommand.execute()`.
-
-### Block Delete (TL-03)
-
-- `Delete` key when block selected: fire `DeleteBlockCommand.execute()` immediately. No confirmation dialog for individual block delete.
-- Block remove button in inspector (`✕`): same behavior as Delete key.
-
-### Block Selection
-
-- `mousedown` on block body (not dragging): select block, show inspector strip.
-- `mousedown` on canvas background: deselect, hide inspector strip.
-- Only one block selected at a time (no multi-select in Phase 4).
-
-### Undo/Redo (TL-06)
-
-- `Ctrl+Z`: `CommandHistory.undo()`
-- `Ctrl+Shift+Z` or `Ctrl+Y`: `CommandHistory.redo()`
-- Minimum 50 undo steps (CommandHistory `maxSteps = 50`).
-- All mutations (place, move, resize, delete, param change) must go through `CommandHistory.execute()`.
-
-### Beat Snap (TL-08)
-
-- Snap active by default; toggle via "SNAP" button in toolbar (lit accent when on, dim when off).
-- Snap threshold: 100ms.
-- Apply calibration offset before snap: `displayBeat = rawBeat - calibrationOffset`.
-
-### Timeline Scroll
-
-- Horizontal scroll via mouse wheel on canvas: updates `scrollX` offset.
-- No native CSS overflow scrollbar on canvas — virtual scroll only.
-- Auto-scroll: when playback cursor would exit the visible window, scroll to keep cursor at 20% from left edge.
-
----
-
-## Inspector Strip Contract (D-01, D-02, D-03)
-
-Height: 40px. Display: flex row. Background: `--bg-panel-dark`. Border-top: `1px solid var(--border-dim)`.
-Hidden (`display: none`) when no block selected. Shown (`display: flex`) when block selected.
-
-Field order (left to right):
-1. `LABEL` — animation type name, `--text-accent`, 14px, 600 weight, uppercase, `flex-shrink: 0`, min-width 120px
-2. `COLOR` — label + `` hidden, visible swatch 16×16px with 1px `--border-dim` border; click swatch triggers color input
-3. `SPEED` — label + ``, width 60px
-4. `DIR` — label + `