## Problem
The Studio animation/manual-editing alpha had grown into one oversized PR. Reviewers need the actual user-facing editor changes separated from the lower-level preview API support, and the alpha controls need safer defaults while we keep testing manual dragging and the motion panel.
## What this fixes
- Moves Clip Layers into the left sidebar as a takeover surface so it no longer looks like part of the video canvas.
- Hides the normal left-panel tabs while Clip Layers is open, keeps nested layer counts visible, and keeps timeline clip inspector buttons available.
- Keeps Motion and preview manual dragging behind env flags: `VITE_STUDIO_ENABLE_MOTION_PANEL` and `VITE_STUDIO_ENABLE_PREVIEW_MANUAL_DRAGGING`.
- Keeps the default inspector focused on Design/Renders while preserving the opt-in GSAP motion panel for alpha testing.
- Carries the timeline thumbnail/player performance work and selection timing fixes needed for the layer inspector flow.
- Adds/updates tests for layer inspection, motion availability, DOM editing, timeline behavior, and player seeking.
## Root cause
The old overlay-based layer picker competed visually with the preview content, and the motion/manual-dragging alpha controls were always present even when the team wanted to test the design panel without those experimental surfaces. Splitting this into a Graphite stack also avoids reviewing Core API changes and Studio UI changes in one giant diff.
## Verification
### Local checks
- `bun run --filter @hyperframes/studio test -- src/components/editor/TimelineLayerPanel.test.ts src/components/editor/manualEditingAvailability.test.ts src/components/editor/PropertyPanel.test.ts src/components/editor/domEditing.test.ts src/components/editor/studioMotion.test.ts src/utils/timelineInspector.test.ts src/player/components/Timeline.test.ts src/player/components/timelineEditing.test.ts src/player/hooks/useTimelinePlayer.test.ts`
- `bun run --filter @hyperframes/core test -- src/studio-api/routes/preview.test.ts src/studio-api/routes/thumbnail.test.ts src/studio-api/helpers/studioMotionRenderScript.test.ts`
- `bun run --filter @hyperframes/core build`
- `bun run --filter @hyperframes/studio build`
### Browser verification
Tested with `agent-browser` against `hf-manual-editing-demo` on the split stack.
- Default flags on `http://127.0.0.1:5298/?v=graphite-default-proof#project/hf-manual-editing-demo`: Clip Layers opens in the left sidebar, Design/Renders remain visible, and Motion is hidden.
- Env flags on `http://127.0.0.1:5299/?v=graphite-env-proof#project/hf-manual-editing-demo`: Inspector exposes the Motion tab.
- Screenshots: `qa-artifacts/graphite-stack/default-loaded.png`, `qa-artifacts/graphite-stack/default-layer-panel.png`, `qa-artifacts/graphite-stack/env-enabled-inspector.png`, `qa-artifacts/graphite-stack/env-enabled-motion-tab.png`.
- Recording: `qa-artifacts/graphite-stack/default-layer-panel-flow.webm`.
## Notes
- This PR intentionally depends on #682 for the Core Studio API support.
- `qa-artifacts/` remains local/untracked.
- Existing PR #680 is superseded by this Graphite stack.
## Problem
The Studio alpha editor work needs runtime/server support before the UI can be reviewed cleanly: previews and thumbnails need cache-aware project signatures, and render paths need a deterministic way to apply Studio-authored GSAP motion during preview/render without mixing that logic into the Studio UI commit.
## What this fixes
- Adds a project signature helper used by Studio preview/thumbnail responses.
- Adds the Studio motion render-body script helper and package export.
- Extends preview and thumbnail route coverage for the animation/layer inspector stack.
- Keeps this bottom PR scoped to Core Studio API support so the upstack UI PR can stay reviewable.
## Root cause
The previous single PR mixed server API support with the full editor surface. Reviewers could not reason about whether deterministic preview/thumbnail behavior was correct independently from the UI interactions.
## Verification
### Local checks
- `bun run --filter @hyperframes/core test -- src/studio-api/routes/preview.test.ts src/studio-api/routes/thumbnail.test.ts src/studio-api/helpers/studioMotionRenderScript.test.ts`
- `bun run --filter @hyperframes/core build`
- `bun run --filter @hyperframes/studio build` from the top of the stack
### Browser verification
Browser proof is exercised in the upstack Studio UI PR because this PR only adds the server/runtime support. The full stacked behavior was verified at:
- `qa-artifacts/graphite-stack/default-layer-panel.png`
- `qa-artifacts/graphite-stack/default-layer-panel-flow.webm`
- `qa-artifacts/graphite-stack/env-enabled-motion-tab.png`
## Notes
- This is the bottom of the Graphite stack.
- Local browser artifacts are intentionally untracked and not part of the PR diff.
Clips whose compositionStart is ahead of the current timeline position
were starting immediately because sourceNode.start() always received
when=0. Use the AudioContext scheduling API to defer future clips:
sourceNode.start(ctx.currentTime + delay, mediaStart).
Closes#674
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the two-clock architecture (GSAP rAF ticker + HTMLMediaElement
pipeline reconciled by a 50ms polling loop) with a single TransportClock.
GSAP is always paused and seeked to clock.now() on each rAF tick.
Drift between visual timeline and audio is structurally impossible.
Architecture:
TransportClock.now() ──rAF──▶ timeline.seek(t) + el.currentTime
▲
AudioContext.currentTime (~21µs) ← WebAudio active
OR
audio.currentTime (~33ms) ← HTMLMediaElement fallback
OR
performance.now() (~1ms) ← no audio
Key changes:
- TransportClock class with monotonic + audio-master clock sources
- WebAudioTransport: routes audio through AudioBufferSourceNode for
sample-accurate scheduling, falls back gracefully to HTMLMediaElement
- rAF tick loop replaces 50ms setInterval poll; GSAP always paused
- Strict sync (40ms threshold, consecutive-sample gated) + forceSync
on play/pause/seek transitions for sub-frame media accuracy
- Buffer-stall: visuals freeze when audio is buffering instead of
running ahead
- Frame quantization preserved in seek/renderSeek (parity contract)
Browser-verified: 0.0ms drift after 40 pause/play cycles (was 400ms+).
Also fixes: CDN script HTML error responses in validate (pre-existing).
54 tests across clock, clock-drift, webAudioTransport, and media.
Closes#668
The autobuild-2026-04-23-13-16 release was rotated out of BtbN's
recent-dailies window, returning 404 on the Windows test/render jobs.
Switch to the 2026-04-30 month-end snapshot, which BtbN keeps long-term
(visible in the persistent monthly-snapshot history).
## What
Adds `landscape-4k` (3840×2160) and `portrait-4k` (2160×3840) presets to `CanvasResolution` and `CANVAS_DIMENSIONS` in `@hyperframes/core`. Foundation for end-to-end 4K rendering support.
## Why
There is no codified way today to mark a composition as 4K. The string union `CanvasResolution = "landscape" | "portrait"` is the only enum used by templates, generators, and stage-zoom math; without 4K members, scaffolds and helpers always emit 1080p dimensions even when the underlying engine + encoder pipeline can already handle larger viewports (Chrome `setViewport`, ffmpeg `libx264`/`libvpx-vp9`/`prores_ks` all scale fine).
This is PR 1 of a 3-PR stack making 4K a first-class option:
1. **PR #660 (this)** — core types/constants + parser detection.
2. **PR #661** — `hyperframes init --resolution 4k` flag to scaffold 4K projects.
3. **PR #662** — byte-budget the frame data-URI cache so 4K renders don't OOM.
## How
- `CanvasResolution` extended additively (no breaking change).
- `CANVAS_DIMENSIONS` gains `landscape-4k` / `portrait-4k` entries.
- `parseResolutionFromHtml` accepts `data-resolution="landscape-4k|portrait-4k"` and infers 4K from `data-composition-width`/`data-composition-height` (long side ≥ 2560 → UHD variant).
- `parseResolutionFromCss` reuses the same dimension-aware classifier so inline `#stage { width/height }` styles are detected too.
- Helper extracted: `resolveResolutionFromDimensions(w, h)`.
- `cli/info.ts` cleanup: replaces a nested `parsed.resolution === "portrait" ? 1080 : 1920` ternary with a `CANVAS_DIMENSIONS[parsed.resolution]` lookup so it stays correct for 4K.
Stage-CSS generators (`templates/base.ts`, `generators/hyperframes.ts`) already index `CANVAS_DIMENSIONS[resolution]`, so they pick up the new presets automatically.
## Test plan
- [x] Unit tests added/updated — 4 new parser tests, 1 expanded constants test
- [x] Manual testing performed — `bun run --cwd packages/core test` (679 pass), `bun run --cwd packages/cli test` (277 pass)
- [ ] Documentation updated (deferred to PR #661 where the user-facing flag lands)