{"id":"29c3bb17-95a5-4f2a-b819-bfd05c8bb4ab","entityType":"agent","slug":"clawhub-heygen-com-hyperframes-core","name":"hyperframes-core","canonicalUrl":"https://www.xpersona.co/agent/clawhub-heygen-com-hyperframes-core","canonicalPath":"/agent/clawhub-heygen-com-hyperframes-core","generatedAt":"2026-10-09T23:49:08.637Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"editorial-content","verified":true,"confidence":"high","updatedAt":"2026-10-09T09:58:47.867Z","emptyReason":null},"description":"The HyperFrames composition contract — build one renderable project. Use for composition structure, the `data-*` timing attributes, `class=\"clip\"`, tracks, sub-compositions, variables, framework-owned media playback, deterministic-render rules, and validation. Read before writing composition HTML. Skill: hyperframes-core Owner: heygen-com Summary: The HyperFrames composition contract — build one renderable project. Use for composition structure, the data-* timing attributes, class=\"clip\", tracks, sub-compositions, variables, framework-owned media playback, deterministic-render rules, and validation. Read before writing composition HTML. Tags: latest:1.0.42 Version history: v1.0.42 | 2026-10-02T13:01:36.130Z |","descriptionLabel":"Technical summary","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 3.1K downloads reported by the source. Last updated 10/9/2026.","installCommand":"clawhub skill install s17fpgb0p797dzkbtbrxw5x1hh89qs64:hyperframes-core","sourceUrl":"https://clawhub.ai/heygen-com/hyperframes-core","homepage":"https://clawhub.ai/heygen-com/skills/hyperframes-core","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/heygen-com/hyperframes-core","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/heygen-com/skills/hyperframes-core","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":70,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"The HyperFrames composition contract — build one renderable project. Use for composition structure, the `data-*` timing attributes, `class=\"clip\"`, tracks, sub-"},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-09T09:58:47.867Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T09:58:47.867Z","emptyReason":null},"stars":null,"forks":null,"downloads":3065,"packageName":null,"latestVersion":"1.0.42","tractionLabel":"3.1K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T09:58:47.867Z","emptyReason":null},"lastUpdatedAt":"2026-10-09T09:58:47.867Z","lastCrawledAt":"2026-10-09T09:58:47.867Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-10T09:58:47.867Z","lastVerifiedAt":null,"highlights":[{"version":"1.0.42","createdAt":"2026-10-02T13:01:36.130Z","changelog":"Synced from 6f799aa (main)","fileCount":13,"zipByteSize":41050},{"version":"1.0.41","createdAt":"2026-10-02T07:30:05.150Z","changelog":"Synced from 7090021 (main)","fileCount":13,"zipByteSize":40874},{"version":"1.0.40","createdAt":"2026-10-02T06:37:42.766Z","changelog":"Synced from 45b8e17 (main)","fileCount":13,"zipByteSize":40677},{"version":"1.0.39","createdAt":"2026-10-02T05:43:41.996Z","changelog":"Synced from 0559be0 (main)","fileCount":13,"zipByteSize":40404},{"version":"1.0.38","createdAt":"2026-10-02T00:11:53.316Z","changelog":"Synced from 37f30b1 (main)","fileCount":13,"zipByteSize":39644},{"version":"1.0.37","createdAt":"2026-09-28T06:09:54.906Z","changelog":"Synced from 13bee74 (main)","fileCount":13,"zipByteSize":39005},{"version":"1.0.36","createdAt":"2026-09-27T21:19:55.722Z","changelog":"Synced from ff6e210 (main)","fileCount":13,"zipByteSize":38989},{"version":"1.0.35","createdAt":"2026-09-20T16:22:38.040Z","changelog":"Synced from 0c158be (main)","fileCount":13,"zipByteSize":39183}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17fpgb0p797dzkbtbrxw5x1hh89qs64:hyperframes-core","setupComplexity":"low","setupSteps":["Setup complexity is classified as HIGH. You must provision dedicated cloud infrastructure or an isolated VM. Do not run this directly on your local workstation.","Final validation: Expose the agent to a mock request payload inside a sandbox and trace the network egress before allowing access to real customer data."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-hyperframes-core/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-hyperframes-core/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-hyperframes-core/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-hyperframes-core/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-hyperframes-core/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-hyperframes-core/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-09T23:49:08.631Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-hyperframes-core/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-hyperframes-core/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-hyperframes-core/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-hyperframes-core/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"high","updatedAt":"2026-10-09T09:58:47.867Z","emptyReason":null},"readme":"Skill: hyperframes-core\n\nOwner: heygen-com\n\nSummary: The HyperFrames composition contract — build one renderable project. Use for composition structure, the `data-*` timing attributes, `class=\"clip\"`, tracks, sub-compositions, variables, framework-owned media playback, deterministic-render rules, and validation. Read before writing composition HTML.\n\nTags: latest:1.0.42\n\nVersion history:\n\nv1.0.42 | 2026-10-02T13:01:36.130Z | user\n\nSynced from 6f799aa (main)\n\nv1.0.41 | 2026-10-02T07:30:05.150Z | user\n\nSynced from 7090021 (main)\n\nv1.0.40 | 2026-10-02T06:37:42.766Z | user\n\nSynced from 45b8e17 (main)\n\nv1.0.39 | 2026-10-02T05:43:41.996Z | user\n\nSynced from 0559be0 (main)\n\nv1.0.38 | 2026-10-02T00:11:53.316Z | user\n\nSynced from 37f30b1 (main)\n\nv1.0.37 | 2026-09-28T06:09:54.906Z | user\n\nSynced from 13bee74 (main)\n\nv1.0.36 | 2026-09-27T21:19:55.722Z | user\n\nSynced from ff6e210 (main)\n\nv1.0.35 | 2026-09-20T16:22:38.040Z | user\n\nSynced from 0c158be (main)\n\nv1.0.34 | 2026-09-19T15:28:08.665Z | user\n\nSynced from fa44d76 (main)\n\nv1.0.33 | 2026-09-19T13:08:06.243Z | user\n\nSynced from 2f24573 (main)\n\nv1.0.32 | 2026-09-19T12:02:01.721Z | user\n\nSynced from 09aadd8 (main)\n\nv1.0.31 | 2026-09-19T08:28:36.638Z | user\n\nSynced from 0a5c235 (main)\n\nv1.0.30 | 2026-09-19T03:21:31.296Z | user\n\nSynced from 2db126d (main)\n\nv1.0.29 | 2026-09-18T22:48:04.070Z | user\n\nSynced from 6995a4c (main)\n\nv1.0.28 | 2026-09-14T01:45:05.592Z | user\n\nSynced from f059a6e (main)\n\nv1.0.27 | 2026-09-14T01:23:40.506Z | user\n\nSynced from 95bea16 (main)\n\nv1.0.26 | 2026-09-13T03:20:44.161Z | user\n\nSynced from 4aa6e17 (main)\n\nv1.0.25 | 2026-09-11T21:20:53.998Z | user\n\nSynced from b848d81 (main)\n\nv1.0.24 | 2026-09-08T18:12:45.439Z | user\n\nSynced from 01744f2 (main)\n\nv1.0.23 | 2026-09-06T03:11:42.383Z | user\n\nSynced from 672ea84 (main)\n\nv1.0.22 | 2026-08-24T22:06:14.966Z | user\n\nSynced from b2fc18b (main)\n\nv1.0.21 | 2026-08-20T20:38:19.443Z | user\n\nSynced from d1482b0 (main)\n\nv1.0.20 | 2026-08-19T22:09:11.751Z | user\n\nSynced from 228eabd (main)\n\nv1.0.19 | 2026-08-19T21:03:52.952Z | user\n\nSynced from 9da422f (main)\n\nv1.0.18 | 2026-08-18T14:18:14.765Z | user\n\nSynced from afafca4 (main)\n\nv1.0.17 | 2026-08-17T20:43:06.786Z | user\n\nSynced from 0285a71 (main)\n\nv1.0.16 | 2026-07-30T12:11:16.730Z | user\n\nSynced from 2e4c2c4 (main)\n\nv1.0.15 | 2026-07-28T22:57:30.312Z | user\n\nSynced from 3a7950f (main)\n\nv1.0.14 | 2026-07-24T21:13:42.997Z | user\n\nSynced from e7f9918 (main)\n\nv1.0.13 | 2026-07-21T16:44:13.459Z | user\n\nSynced from 696cbdb (main)\n\nv1.0.12 | 2026-07-20T15:20:38.132Z | user\n\nSynced from 6ad738b (main)\n\nv1.0.11 | 2026-07-17T23:08:37.442Z | user\n\nSynced from 4a2c9ee (main)\n\nv1.0.10 | 2026-07-16T22:20:50.744Z | user\n\nSynced from 428e571 (main)\n\nv1.0.9 | 2026-07-15T13:22:00.943Z | user\n\nSynced from b9be0b2 (main)\n\nv1.0.8 | 2026-07-11T01:38:14.640Z | user\n\nSynced from de4e85a (main)\n\nv1.0.7 | 2026-07-10T22:49:02.049Z | user\n\nSynced from 00d059b (main)\n\nv1.0.6 | 2026-07-09T20:44:00.186Z | user\n\nSynced from ac0c8e9 (main)\n\nv1.0.5 | 2026-07-08T18:00:20.448Z | user\n\nSynced from 17b8527 (main)\n\nv1.0.4 | 2026-07-07T20:27:43.617Z | user\n\nSynced from 7286b00 (main)\n\nv1.0.3 | 2026-07-07T18:57:40.303Z | user\n\nSynced from 5fe9573 (main)\n\nv1.0.2 | 2026-07-06T22:41:14.025Z | user\n\nSynced from 972bedf (main)\n\nv1.0.1 | 2026-07-02T17:09:04.281Z | user\n\nSynced from a7c3cc7 (main)\n\nv1.0.0 | 2026-07-01T10:05:21.071Z | user\n\nOfficial HyperFrames skills from heygen-com/hyperframes\n\nArchive index:\n\nArchive v1.0.42: 13 files, 41050 bytes\n\nFiles: references/composition-patterns.md (10853b), references/creator-editing-recipes.md (18420b), references/data-attributes.md (17370b), references/determinism-rules.md (7033b), references/full-screen-motion.md (2906b), references/minimal-composition.md (2507b), references/sub-compositions.md (8074b), references/tailwind.md (4078b), references/tracks-and-clips.md (8171b), references/variables-and-media.md (8420b), skill-card.md (1944b), SKILL.md (12643b), _meta.json (136b)\n\nFile v1.0.42:SKILL.md\n\n---\nname: hyperframes-core\ndescription: The HyperFrames composition contract — build one renderable project. Use for composition structure, the `data-*` timing attributes, `class=\"clip\"`, tracks, sub-compositions, variables, framework-owned media playback, deterministic-render rules, and validation. Read before writing composition HTML.\n---\n\n**Plugin installs:** Before setup or freshness commands, follow [plugin execution rules](../hyperframes/references/plugin-installation.md) when this skill is inside a HyperFrames plugin. Standalone installs keep the update instructions below.\n\n# HyperFrames Core\n\n**Agent pitfalls (read first):**\n\n- Center with flex/`inset`, not CSS `transform: translate(-50%,-50%)` on a node you then GSAP `x`/`y`. Lint: `gsap_css_transform_conflict`. Use `fromTo` or `xPercent`/`yPercent`.\n- Do not add a scene-exit `tl.set(..., {visibility:\"hidden\"})`. The runtime already hides timed clips. Opacity fades on inner nodes (or `opacity` on `.clip`) are enough. Caption hard-kills are a different rule.\n- `window.__timelines[\"id\"]` must match the root `data-composition-id`.\n- After `render`, read the summary's second line: `beginframe` vs `screenshot`, GPU mode, stage timings. `screenshot` + `software gpu` on Linux is the slow path.\n\nHyperFrames renders video from HTML. A composition is an HTML file whose DOM declares timing with `data-*` attributes, whose animation runtime is seekable, and whose media playback is owned by the framework.\n\nThis skill is the **technical contract** — how to build one hyperframes project. The body below is the build guide; per-topic detail lives in `references/` (index next), read on demand. Process docs (brief, storyboard, review, production, dispatch, frame-worker) live in `/hyperframes` → `references/`. Other concerns live in the sibling domain skills — `hyperframes-animation`, `hyperframes-creative`, `media-use`, `hyperframes-cli`, `hyperframes-registry`. The capability map in `/hyperframes` says what each one covers.\n\n## References\n\n| File                                    | Read it to…                                                                                                                                                         |\n| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `references/minimal-composition.md`     | start from the smallest renderable composition skeleton                                                                                                             |\n| `references/composition-patterns.md`    | choose monolithic vs modular; structure a modular `index.html`; pick a sub-comp archetype                                                                           |\n| `references/data-attributes.md`         | look up any `data-*` (root / clip / sub-comp host / legacy aliases); use `class=\"clip\"`                                                                             |\n| `references/tracks-and-clips.md`        | understand what `data-track-index` does (and does not) control, z-index, time a clip relative to another; list every track and clip with `npx hyperframes timeline` |\n| `references/creator-editing-recipes.md` | copy truthful cut/trim/reorder/retime/freeze/camera/mask/crossfade/audio editing recipes and their limits                                                           |\n| `references/sub-compositions.md`        | wire a sub-composition (host attrs, `<template>`, per-instance vars) and animate inside it                                                                          |\n| `references/variables-and-media.md`     | declare variables; place `<video>`/`<audio>`, set volume, trim                                                                                                      |\n| `references/determinism-rules.md`       | build a seekable timeline; determinism bans; layout / text fit                                                                                                      |\n| `references/full-screen-motion.md`      | author full-frame motion with shared backgrounds                                                                                                                    |\n| `references/tailwind.md`                | work in a Tailwind v4 project (`init --tailwind`; runtime contract differs from Studio's v3)                                                                        |\n\nFor animation runtime specifics (GSAP API, Lottie, Three.js, etc.) go to `hyperframes-animation` → `adapters/<runtime>.md`.\n\n## Building a composition\n\n### Two root forms (not interchangeable)\n\n- **Standalone** (top-level `index.html`): root `<div data-composition-id=\"…\">` sits directly in `<body>`, **no `<template>` wrapper**. Wrapping a standalone root hides all content and `lint` rejects it (`standalone_composition_wrapped_in_template`, error).\n- **Sub-composition** (loaded via `data-composition-src`): wrap the root in `<template>`. This is the shape to write: the loader also accepts a plain full document and falls back to its `<body>`, but the templated form is what the examples and tooling assume.\n\n> ⚠ Transport rule: for a **templated** sub-composition the assembler drops the file's own `<head>` `<style>`/`<script>` (`packages/core/src/compiler/compositionAssembly.ts`, the `hasTemplate` gate), so put `<style>`/`<script>` **inside** the template. `<link>` is hoisted either way.\n> ⚠ Host-id convention: give the host slot, the inner template, and the `window.__timelines[\"<id>\"]` key the **same** id. A different local id is supported (the assembler falls back to the first root in the file) but the mismatch is silent, so match them unless you have a reason not to.\n\nFile shape, host wiring, and the pre-render checklist → `references/sub-compositions.md`.\n\n### Root must be sized (silent layout bug)\n\nThe standalone root authors `width`/`height: 100%`. Canvas size is `data-width`/`data-height`. The runtime stamps those pixels onto the composition root. Do not hardcode `1920px`/`1080px` on `#root`. Skeleton → `references/minimal-composition.md`.\n\n### One paused timeline\n\nEach composition registers **exactly one** `gsap.timeline({ paused: true })` at `window.__timelines[\"<id>\"]` (key = root `data-composition-id`). Building it inside an async callback (`document.fonts.ready`) is supported; what matters is that you **register only after the build completes**. Render length is the root's `data-duration`, **not** the timeline's length: a timeline that runs past it is cut off, and one that ends early holds its last frame. Omit the root `data-duration` and the length is inferred instead (timeline, media window, or adapter). You do not need `window.__timelines = window.__timelines || {}`: the runtime creates the registry before your inline scripts run, and `lint` no longer asks for it. Don't manually nest sub-timelines into the host; the runtime auto-nests registered child timelines. Full contract (incl. non-GSAP runtimes) → `references/determinism-rules.md` + `hyperframes-animation/adapters/`.\n\n### First-pass lint gotchas (a guaranteed first build failure)\n\nRules that `lint` **does** catch, but only after the fact. Write them right the first time:\n\n- Never pair a CSS initial `transform` with a GSAP tween on the **same** property — the CSS value and the tween's start fight and `lint` rejects it with `gsap_css_transform_conflict`. Set the initial state inside the tween with `gsap.fromTo(el, { x: -40 }, { x: 0 })` instead of a CSS `transform: translateX(-40px)`.\n- Never put `crossorigin` on `<video>`/`<audio>`. `lint` rejects it unconditionally with `media_crossorigin_breaks_preview` (error), including for canvas/WebGL/WebAudio readback. There is no suppression.\n- Never give a `<video data-start>` an ancestor that also carries `data-start`. `lint` rejects it with `video_nested_in_timed_element` (error). Time the wrapper **or** the video, not both.\n- Every `<audio>` needs an `id`. `lint` rejects it with `media_missing_id`, and an id-less `<audio>` is never picked up by the mixer, so the render is **silent**.\n- Never tween a `.clip` with `autoAlpha` or `visibility` — `lint` rejects it with `gsap_animates_clip_element`. Animate a child instead.\n- A named CSS `font-family` needs an in-file `@font-face` to a shipped local file, or `lint` fires `font_family_without_font_face`.\n- Sub-composition `#root` uses `width`/`height: 100%` (or `inset: 0`), not hardcoded `1920px`/`1080px`. Canvas size is `data-width`/`data-height`.\n\nA lint **error** also switches off the layout and contrast audits: `check` then reports `0 sample(s)` and `0/0 text checks`, which reads like a clean file but means nothing ran. Clear lint errors before you trust those numbers.\n\n### Non-negotiable rules (silent bugs automated gates may miss)\n\nSurfaced here; full rationale in the linked reference. Do not violate:\n\n- No render-time clocks / unseeded `Math.random` / network / input-state; `repeat: -1` only under a finite root `data-duration` (export clips to it — otherwise use a finite count). → `determinism-rules.md`\n- Never tween `display`, `visibility`, or `autoAlpha` on a `.clip` element. The framework owns clip visibility, and `lint` rejects it (`gsap_animates_clip_element`). Animate a child instead. → `determinism-rules.md`\n- No `<br>` in body text; transformed elements must be block-level + sized; pulsing absolute decoratives need peak clearance. → `determinism-rules.md`\n- `<video>`/`<audio>` are found by a flat document query, so the framework seeks and decodes them at **any nesting depth** (including inside a sub-comp `<template>` or wrapper). One hard limit: `lint` errors if a `<video data-start>` sits inside another **plain** element that also has `data-start`, and the failure is real (wrong source frames, then the clip vanishes mid-slot), so put the timing on the wrapper or on the video, never both. Sub-composition hosts are exempt: media inside a sub-composition renders correctly. The other caveat is timelines, not placement: a sub-comp timeline can't animate host-root elements. → `variables-and-media.md`\n- Keep every `id` unique across the **assembled** page (prefix sub-comp ids with the composition id, `#<id>-hero`) so your own `#id` CSS and `getElementById` calls resolve. Frame injection no longer depends on it: the compiler stamps a document-unique `data-hf-render-id` on every `video[src]`/`audio[src]`/`img[src]`. Media that uses `<source>` children instead of a `src` attribute is **not** stamped, so unique ids still matter there. → `composition-patterns.md`\n- A full-screen fill on the composition **root** is fine on a normal render. It is dropped only on the layered-composite path (HDR content, or a composition using shader transitions), where the engine forces every composition root transparent so the layer beneath shows through. If your composition uses shader transitions or HDR media, put the fill on a full-bleed **child** (`position:absolute; inset:0`). → `composition-patterns.md`\n\n## Editing existing compositions\n\n- Read the files first. Preserve unrelated timing, tracks, IDs, variables, media paths.\n- To know what is on a project's timeline (tracks, clips, starts, ends, what plays), run `npx hyperframes timeline [--json]` instead of reading `index.html` and every sub-composition file.\n- Match existing composition IDs and timeline keys.\n- Adding a clip: set its `data-start`/`data-duration` intentionally against the clips around it. `data-track-index` is a Studio display lane, not a timing constraint, so it does not need to be free.\n- A clip that ends past the root `data-duration` is cut off: extend the root `data-duration` to the clip's end in the same edit (`lint` warns `clip_ends_past_root_duration`).\n- `data-hidden` on any composition element hides it in BOTH preview and render, overriding its time window; it is non-destructive/reversible and toggled by Studio's timeline eye icon.\n- Adding a sub-composition: verify its internal `data-composition-id` before wiring the host.\n\n## Validation\n\nUse `hyperframes-cli` for command details\n\n- [ ] `npx hyperframes check` passes (0 findings across lint, runtime, layout, motion, and contrast)\n- [ ] Projects with sub-compositions: `npx hyperframes snapshot --at <midpoints>` and eyeball each frame\n- [ ] `npx hyperframes preview --background` for review (the user can edit anything in Studio's timeline, and the server survives the invoking command)\n- [ ] `npx hyperframes render` only after the user approves\n\nFile v1.0.42:_meta.json\n\n{\n  \"ownerId\": \"kn77d06grj6xqp3dqwkk4bavhn89pegt\",\n  \"slug\": \"hyperframes-core\",\n  \"version\": \"1.0.42\",\n  \"publishedAt\": 1790946096130\n}\n\nFile v1.0.42:references/composition-patterns.md\n\n# Composition Patterns\n\nHow to architect a project: the `index.html` orchestrator at scale, and the common sub-composition archetypes. Pair with `minimal-composition.md` (single-file shape) and `sub-compositions.md` (mechanics of a sub-comp file).\n\n## Two Architectures\n\n|                       | Monolithic (single file)                                | Modular (sub-compositions)                                                           |\n| --------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------ |\n| Project layout        | `index.html` only                                       | `index.html` + `compositions/<scene>.html` per scene                                 |\n| Where scenes live     | Inline `<section class=\"clip\">` siblings under the root | Each scene is a separate file wrapped in `<template>`                                |\n| Timeline registration | One timeline keyed at the root's `data-composition-id`  | Root timeline (often near-empty) + one timeline per sub-comp, each keyed by its `id` |\n| Routing entry         | `references/minimal-composition.md`                     | `references/sub-compositions.md`                                                     |\n\nBoth architectures use the same runtime contract — `data-*` attributes + `window.__timelines[id]`. The choice is structural, not behavioral.\n\n## Modular Orchestrator Pattern\n\nWhen using sub-compositions, `index.html` should be **thin**. Its job is to declare slots, lay them out in time, mount the audio track, and register a (usually empty) root timeline. All scene animation lives inside the sub-comps.\n\n```html\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <script src=\"https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js\"></script>\n    <style>\n      body {\n        margin: 0;\n        background: #000;\n      }\n      #root {\n        position: relative;\n        width: 100%;\n        height: 100%;\n        overflow: hidden;\n      }\n      /* Sub-comp slots stretch to fill the root. */\n      [data-composition-id=\"root\"] > div[data-composition-src] {\n        position: absolute;\n        inset: 0;\n      }\n    </style>\n  </head>\n  <body>\n    <div\n      id=\"root\"\n      data-composition-id=\"root\"\n      data-width=\"1920\"\n      data-height=\"1080\"\n      data-duration=\"30\"\n    >\n      <!-- Sequential scenes — each one a sub-composition slot. -->\n      <div\n        id=\"el-intro\"\n        data-composition-id=\"intro\"\n        data-composition-src=\"compositions/intro.html\"\n        data-start=\"0\"\n        data-duration=\"6\"\n        data-track-index=\"1\"\n      ></div>\n\n      <div\n        id=\"el-body\"\n        data-composition-id=\"body\"\n        data-composition-src=\"compositions/body.html\"\n        data-start=\"6\"\n        data-duration=\"18\"\n        data-track-index=\"1\"\n      ></div>\n\n      <div\n        id=\"el-outro\"\n        data-composition-id=\"outro\"\n        data-composition-src=\"compositions/outro.html\"\n        data-start=\"24\"\n        data-duration=\"6\"\n        data-track-index=\"1\"\n      ></div>\n\n      <!-- Continuous audio at the root — survives scene cuts. -->\n      <audio\n        id=\"el-bgm\"\n        src=\"assets/bgm.mp3\"\n        data-start=\"0\"\n        data-duration=\"30\"\n        data-track-index=\"10\"\n        data-volume=\"0.6\"\n      ></audio>\n    </div>\n\n    <script>\n      window.__timelines[\"root\"] = gsap.timeline({ paused: true });\n    </script>\n  </body>\n</html>\n```\n\nKey properties of this layout:\n\n- **Visual scenes on the same `data-track-index`** (e.g. `1`), authored sequentially. For a cross-fade, overlap their times by the fade duration; giving the incoming scene its own track keeps Studio's timeline readable, but the render accepts an overlap either way.\n- **Audio on a separate, higher track index** (e.g. `10`). Keeps the linter's overlap rules clear of any visual collisions.\n- **Root timeline is near-empty.** All animation lives in the sub-comps. A root-level fade-to-black at the very end is fine; do not stage a parallel animation track from the root.\n- **Host slot ids** use `el-<name>` or `<scene-id>`. The slot's `data-composition-id` must still equal the sub-comp's internal id (see `sub-compositions.md`).\n\n## Sub-Composition Archetypes\n\n### A. Content scene (default)\n\nThe sub-comp contains the scene's full DOM, scoped CSS, and timeline. This is the standard pattern in `sub-compositions.md` — most scenes are this.\n\n### B. Host media + main-timeline driver (one pattern for `<video>`/`<audio>`)\n\n`<video>`/`<audio>` seek and decode at any nesting depth, so a scene-specific clip can live inside its scene's sub-comp with scene-local `data-start` and be driven by that sub-comp's own timeline. Use this host-media pattern instead when you want the media's motion authored on the **main** timeline: put the `<video>`/`<audio>` as a host-root sibling positioned over the scene's frame.\n\nThe reason to reach for it: a sub-comp timeline **cannot** drive host elements (a global selector or `document.querySelector` does not resolve across the boundary). So if the media lives at the host root, author its per-scene motion (scale/opacity/morph/tilt/breathing) on the **main timeline** in `index.html`, at **global time** = scene-local time + the scene slot's `data-start`.\n\n```html\n<!-- index.html (host) -->\n<div\n  id=\"el-final\"\n  data-composition-id=\"final-anim\"\n  data-composition-src=\"compositions/final-anim.html\"\n  data-start=\"20\"\n  data-duration=\"6\"\n  data-track-index=\"1\"\n></div>\n\n<!-- media is a DIRECT root child; sits over the sub-comp's frame -->\n<video\n  id=\"final-video\"\n  class=\"clip\"\n  src=\"assets/final.mp4\"\n  data-start=\"20\"\n  data-duration=\"6\"\n  data-track-index=\"2\"\n  muted\n  playsinline\n  style=\"position:absolute; left:360px; top:100px; width:1200px; height:680px; object-fit:cover; border-radius:24px;\"\n></video>\n\n<script>\n  // MAIN timeline drives the host video. Global time: scene starts at 20.\n  const main = window.__timelines[\"main\"];\n  main.fromTo(\n    \"#final-video\",\n    { scale: 1.4, filter: \"blur(14px)\" },\n    { scale: 1.0, filter: \"blur(0px)\", duration: 0.9, ease: \"power3.out\" },\n    20,\n  ); // = slot data-start (+ any scene-local offset)\n</script>\n\n<!-- compositions/final-anim.html — frame/shell only, no <video>, no host-element animation -->\n<template>\n  <div\n    data-composition-id=\"final-anim\"\n    data-width=\"1920\"\n    data-height=\"1080\"\n    data-duration=\"6\"\n    style=\"position:absolute; inset:0; pointer-events:none;\"\n  >\n    <script>\n      const tl = gsap.timeline({ paused: true });\n      // animate ONLY this sub-comp's own elements here (labels, frame, overlays)\n      window.__timelines[\"final-anim\"] = tl;\n    </script>\n  </div>\n</template>\n```\n\nCaveats:\n\n- In this pattern the media is a host-root child, static in `index.html`, so the main timeline's selector resolves it. (Media nested in a sub-comp is also driven fine; it just can't be reached by the main timeline's selectors — drive it from the sub-comp's own timeline.)\n- Clip lifecycle owns the media element's visibility across its `[data-start, data-start+data-duration]` window. The main-timeline opacity/scale tweens compose with it fine; for an opacity reveal/crossfade prefer a host **wrapper** so you are not fighting the lifecycle on the media element itself.\n- Two media elements sharing the same `src` + `data-start` trigger `duplicate_media_discovery_risk` (benign — both still render).\n\n### C. Multi-scene merge\n\nWhen several beat-level scenes share continuous state — a chat thread that grows, a persistent headline word that carries across the cut, a single canvas with internal phase changes — collapse them into one sub-comp and use **internal phase divs** rather than multiple sub-comp slots.\n\n```html\n<!-- compositions/act2-merged.html -->\n<template>\n  <div data-composition-id=\"act2-merged\" data-width=\"1920\" data-height=\"1080\" data-duration=\"9\">\n    <style>\n      [data-composition-id=\"act2-merged\"] .phase {\n        position: absolute;\n        inset: 0;\n        opacity: 0;\n      }\n    </style>\n    <div class=\"phase\" id=\"phase-a\">…</div>\n    <div class=\"phase\" id=\"phase-b\">…</div>\n    <div class=\"phase\" id=\"phase-c\">…</div>\n    <script>\n      const tl = gsap.timeline({ paused: true });\n      tl.set(\"#phase-a\", { opacity: 1 }, 0);\n      tl.to(\"#phase-a\", { opacity: 0, duration: 0.4 }, 3.0);\n      tl.set(\"#phase-b\", { opacity: 1 }, 3.0);\n      // …\n      window.__timelines[\"act2-merged\"] = tl;\n    </script>\n  </div>\n</template>\n```\n\nReach for this over multiple sequential slots when scenes share DOM, share a canvas, or need to cross-fade with persistent elements (a headline that survives the cut between phases). Each phase is just a div inside the same sub-comp — the parent timeline never has to know about the internal phase boundaries.\n\n### D. Audio at root, reactive visual inside\n\nAudio always lives at the host (`index.html`) as a root-level `<audio>` so playback survives scene cuts. A sub-comp that visualizes audio should read a **pre-baked** frequency curve at init, then sample the baked curve from its timeline — the visual must still be a deterministic function of `tl.time()`, not of `audio.currentTime`. See `determinism-rules.md` and `hyperframes-creative` for the authoring pattern.\n\n## Naming Conventions\n\n| Thing                               | Convention                                  | Example                                    |\n| ----------------------------------- | ------------------------------------------- | ------------------------------------------ |\n| Sub-comp file                       | `compositions/<scene-id>.html`              | `compositions/act0-intro-bell.html`        |\n| Sub-comp `<template>` id (optional) | `<scene-id>-template`                       | `<template id=\"act0-intro-bell-template\">` |\n| Sub-comp root `data-composition-id` | `<scene-id>` (must match host slot)         | `data-composition-id=\"act0-intro-bell\"`    |\n| Timeline registry key               | matches `data-composition-id`               | `window.__timelines[\"act0-intro-bell\"]`    |\n| Host slot `id`                      | `el-<short>` or `<scene-id>`                | `id=\"el-intro\"`, `id=\"act0\"`               |\n| Element ids inside a sub-comp       | prefix with the scene id                    | `#act0-bell`, `#b1-tape`                   |\n| Audio at root                       | `data-track-index` well above visual tracks | `10` while visuals use `1`                 |\n\nThe `-template` suffix on `<template>` is conventional but not required — the runtime extracts contents from whichever `<template>` is in `<body>`, regardless of id. The prefix on inner element ids is the only safeguard against id collisions when multiple sub-comps are mounted into the same host page at once.\n\nFile v1.0.42:references/creator-editing-recipes.md\n\n# Creator Editing Recipes\n\nUse these copyable contracts after `tracks-and-clips.md`. Global math: **consumed source = timeline duration × rate**; **natural timeline duration = remaining source / rate**.\n\nBefore any edit, run `npx hyperframes timeline` (add `--json` for a machine-readable list) to see the project's tracks and clips instead of reading the HTML.\n\nThese recipes keep a video's sound on the `<video>` (`data-has-audio=\"true\"`, no `muted`), so cutting the video cuts its sound. Use a separate `<audio>` only when picture and sound must be cut independently (J/L cuts, replacement audio) or for other sound (music, voiceover) — the recipes that do say so. Silent footage or b-roll: replace `data-has-audio=\"true\"` with `muted` in these blocks.\n\n**Every `<video>` and `<audio>` below carries an `id`, and that is not cosmetic**: `lint` errors with `media_missing_id` on timed media without one, and an id-less `<audio>` is never picked up by the mixer, so the render comes out silent. Keep the ids when you copy a recipe.\n\n## Hard cut\n\n```html\n<video\n  id=\"a\"\n  src=\"take.mp4\"\n  data-start=\"0\"\n  data-duration=\"2\"\n  data-media-start=\"4\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n<video\n  id=\"b\"\n  src=\"take.mp4\"\n  data-start=\"2\"\n  data-duration=\"3\"\n  data-media-start=\"10\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n```\n\nTimeline math: B starts at A start + duration. Source math: each range starts at `data-media-start`; consumed source = timeline duration × rate. Audio follows: the sound moves with each video clip. Owner: `/hyperframes-core`. Limit: adjacent windows only; author the two windows edge to edge. Same-track overlap is valid; both clips paint in CSS order.\n\n## Trim in/out\n\n```html\n<video\n  id=\"shot-1\"\n  src=\"take.mp4\"\n  data-start=\"1\"\n  data-duration=\"3\"\n  data-media-start=\"6\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n```\n\nTimeline math: visible window is `[1,4]`. Source math: in=6, out=6+3 at 1x; never invent source-end syntax. Audio follows: the sound moves with the video clip, using the same three attributes. Owner: `/hyperframes-core`. Limit: use another clip for another range.\n\n## Split / splice\n\n```html\n<video\n  id=\"shot-1\"\n  src=\"take.mp4\"\n  data-start=\"0\"\n  data-duration=\"2\"\n  data-media-start=\"0\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n<video\n  id=\"shot-2\"\n  src=\"take.mp4\"\n  data-start=\"2\"\n  data-duration=\"2\"\n  data-media-start=\"8\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n```\n\nTimeline math: splice at t=2. Source math: independent source offsets select kept pieces. Audio follows: the sound moves with each video clip, so it splits identically. Owner: `/hyperframes-core`. Limit: source cuts are core, never keyframes.\n\n## Duplicate / reuse same source\n\n```html\n<video\n  id=\"shot-1\"\n  src=\"take.mp4\"\n  data-start=\"0\"\n  data-duration=\"1\"\n  data-media-start=\"2\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n<video\n  id=\"shot-2\"\n  src=\"take.mp4\"\n  data-start=\"4\"\n  data-duration=\"1\"\n  data-media-start=\"2\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n```\n\nTimeline math: copies may occupy different starts. Source math: identical offsets reuse identical source. Audio follows: the sound moves with each video clip, so each copy carries its own. Owner: `/hyperframes-core`. Limit: every element needs a unique id when ids are present.\n\n## Reorder\n\n```html\n<video\n  id=\"shot-1\"\n  src=\"take.mp4\"\n  data-start=\"0\"\n  data-duration=\"2\"\n  data-media-start=\"10\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n<video\n  id=\"shot-2\"\n  src=\"take.mp4\"\n  data-start=\"2\"\n  data-duration=\"2\"\n  data-media-start=\"2\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n```\n\nTimeline math: `data-start` defines authored order. Source math: source offsets need not be chronological. Audio follows: the sound moves with the video clip, so reordering clips reorders their sound. Owner: `/hyperframes-core`. Limit: reordering changes placement only, not source ranges.\n\n## Freeze / hold\n\n```html\n<img src=\"held-frame.png\" data-start=\"2\" data-duration=\"1\" data-track-index=\"0\" class=\"clip\" />\n```\n\nTimeline math: the still owns its hold duration. Source math: final-source frame, subcomp final state, and visual pose holds are supported. Audio follows: continue, trim, or silence audio deliberately. Owner: `/hyperframes-core` + `/media-use`. Limit: arbitrary mid-source freeze requires preprocess of a still/segment.\n\n## Constant speed / slow motion\n\n```html\n<video\n  id=\"shot-1\"\n  src=\"take.mp4\"\n  data-start=\"0\"\n  data-duration=\"2\"\n  data-media-start=\"4\"\n  data-playback-rate=\"0.5\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n```\n\nTimeline math: duration is authored timeline time. Source math: consumed source = timeline duration × rate; natural timeline duration = remaining source / rate. Audio follows: the sound moves with the video clip and plays at the same constant rate. Owner: `/hyperframes-core`. Limit: normalized 0.1..10. For a speed ramp put a `rate` lane in `data-automation`, e.g. `{\"version\":1,\"lanes\":[{\"target\":\"rate\",\"points\":[{\"t\":0,\"v\":1},{\"t\":2,\"v\":4}]}]}`; it wins over the constant.\n\n## Zoom / punch\n\n```js\ntl.to(\"#clip .inner\", { scale: 1.35, xPercent: -8, duration: 0.18 }, 1);\n```\n\nTimeline math: tween positions are composition seconds. Source math: unchanged; the core clip still selects source time. Audio follows: unchanged unless separately edited. Owner: `/hyperframes-keyframes`. Limit: target the inner wrapper, not the timed clip element.\n\n## Pan / Ken Burns\n\n```js\ntl.fromTo(\n  \"#clip .inner\",\n  { scale: 1.05, xPercent: 0 },\n  { scale: 1.2, xPercent: -12, duration: 4, ease: \"none\" },\n  0,\n);\n```\n\nTimeline math: move spans four authored seconds. Source math: unchanged. Audio follows: the sound stays on the video clip; the tween does not touch it. Owner: `/hyperframes-keyframes`. Limit: authored geometry, not automatic face tracking.\n\n## Crop / reframe\n\n```js\ntl.to(\"#clip .inner\", { clipPath: \"inset(8% 12% 6% 10%)\", xPercent: -4, duration: 1 }, 2);\n```\n\nTimeline math: crop interpolates over `[2,3]`. Source math: unchanged. Audio follows: no automatic change. Owner: `/hyperframes-keyframes`. Limit: inner wrapper only, not temporal trim.\n\n## Clip-path wipe / reveal / mask / split-screen\n\n```js\ntl.fromTo(\n  \"#next .inner\",\n  { clipPath: \"polygon(0 0,0 0,0 100%,0 100%)\" },\n  { clipPath: \"polygon(0 0,100% 0,100% 100%,0 100%)\", duration: 0.5 },\n  2,\n);\n```\n\nTimeline math: overlap placed clips for the 0.5s handoff. Source math: each clip keeps its own range. Audio follows: the sound stays on each video clip. Owner: `/hyperframes-keyframes` + `/hyperframes-animation`. Limit: visual mask/polygon/split-screen only; source cuts stay `/hyperframes-core`.\n\n## Crossfade\n\n```html\n<div id=\"a-visual\" class=\"inner\">\n  <video\n    id=\"a\"\n    data-start=\"0\"\n    data-duration=\"3\"\n    data-track-index=\"0\"\n    src=\"a.mp4\"\n    data-automation='{\"version\":1,\"lanes\":[{\"target\":\"volume\",\"points\":[{\"t\":0,\"v\":1},{\"t\":2.5,\"v\":1},{\"t\":3,\"v\":0}]}]}'\n    playsinline\n    data-has-audio=\"true\"\n  ></video>\n</div>\n<div id=\"b-visual\" class=\"inner\">\n  <video\n    id=\"b\"\n    data-start=\"2.5\"\n    data-duration=\"3\"\n    data-track-index=\"1\"\n    src=\"b.mp4\"\n    data-automation='{\"version\":1,\"lanes\":[{\"target\":\"volume\",\"points\":[{\"t\":0,\"v\":0},{\"t\":0.5,\"v\":1},{\"t\":3,\"v\":1}]}]}'\n    playsinline\n    data-has-audio=\"true\"\n  ></video>\n</div>\n<script>\n  const tl = gsap.timeline({ paused: true });\n  tl.set(\"#b-visual\", { opacity: 0 }, 0)\n    .to(\"#a-visual\", { opacity: 0, duration: 0.5 }, 2.5)\n    .to(\"#b-visual\", { opacity: 1, duration: 0.5 }, 2.5);\n  window.__timelines[\"main\"] = tl;\n</script>\n```\n\nTimeline math: distinct tracks overlap by 0.5s with opposing opacity envelopes. Source math: each source range remains independent. Audio follows: opposing volume envelopes on each video's own `data-automation`, because the sound stays on the video. Owner: `/hyperframes-core` + `/hyperframes-keyframes` + `/hyperframes-audio`. Limit: the crossfade is the opacity/volume envelopes, not a source-level dissolve.\n\n## Volume fades / ducking\n\n```html\n<audio\n  id=\"music-bed\"\n  src=\"music.wav\"\n  data-start=\"0\"\n  data-duration=\"5\"\n  data-track-index=\"10\"\n  data-automation='{\"version\":1,\"lanes\":[{\"target\":\"volume\",\"points\":[{\"t\":0,\"v\":0},{\"t\":1,\"v\":1},{\"t\":2,\"v\":1},{\"t\":2.2,\"v\":0.3},{\"t\":3,\"v\":0.3},{\"t\":3.2,\"v\":1},{\"t\":4,\"v\":1},{\"t\":5,\"v\":0}]}]}'\n></audio>\n```\n\nTimeline math: lane `t` is clip-local authored time: fade-in 0–1, duck down 2–2.2, hold 2.2–3, duck up 3–3.2, fade-out 4–5. Source math: source selection still uses core attributes. Audio follows: the explicit down-hold-up envelope affects this `<audio>` (music is separate sound). The same lane works on a `<video data-has-audio=\"true\">`. Owner: `/hyperframes-audio`. Limit: automation is not source retiming.\n\n**One rule for volume over time: use the lane.** `lint` accepts a timeline tween on `volume` too, but when a track has both, the lane wins and the tween is ignored (`audio_volume_double_automation`). Never add a lane to a track that already has a `volume` tween, and never add a tween to a track that has a lane; edit the one that exists. To ramp 0.1 to 0.5 over ten seconds, write `{\"t\":0,\"v\":0.1},{\"t\":10,\"v\":0.5}`. `t` is seconds from the clip's own start, so a ramp past `data-duration` never finishes: check the clip's length before choosing the times. `data-volume` stays as the static level of the clip and combines with nothing else you author here.\n\n## Audio alignment\n\n```html\n<video\n  id=\"shot-1\"\n  src=\"take.mp4\"\n  data-start=\"3\"\n  data-duration=\"2\"\n  data-media-start=\"8\"\n  data-playback-rate=\"2\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n```\n\nTimeline math: picture and sound share start/duration because the sound stays on the clip (`data-has-audio=\"true\"`). Source math: both consume four source seconds. Audio follows: identical timing, range, and rate, with nothing to keep in sync. Owner: `/hyperframes-core` + `/hyperframes-audio`. Limit: no waveform auto-sync or drift correction.\n\nA J cut or L cut is the case that needs a separate `<audio>`: picture and sound are cut independently, so the sound gets its own element (the same goes for replacement audio, a voiceover, or music). Mute the video whose sound you are replacing.\n\n```html\n<!-- Outgoing shot: picture runs 0-5, its own sound is a separate clip that ends at the audio cut (4). -->\n<video\n  id=\"shot-1\"\n  src=\"intro.mp4\"\n  data-start=\"0\"\n  data-duration=\"5\"\n  data-track-index=\"0\"\n  muted\n  playsinline\n></video>\n<audio\n  id=\"shot-1-audio\"\n  src=\"intro.mp4\"\n  data-start=\"0\"\n  data-duration=\"4\"\n  data-track-index=\"10\"\n></audio>\n<!-- Incoming shot: picture starts at 5, its sound leads it by one second. -->\n<video\n  id=\"shot-2\"\n  src=\"take.mp4\"\n  data-start=\"5\"\n  data-duration=\"3\"\n  data-media-start=\"12\"\n  data-track-index=\"0\"\n  muted\n  playsinline\n></video>\n<audio\n  id=\"shot-2-audio\"\n  src=\"take.mp4\"\n  data-start=\"4\"\n  data-duration=\"4\"\n  data-media-start=\"11\"\n  data-track-index=\"10\"\n></audio>\n```\n\nThe sound leads the picture by one second (a J cut): `shot-2-audio` starts at 4 and reads from source 11, while the picture starts at 5 and reads from 12. Both stay on the same source clock. Every video in a J or L cut is `muted` and its sound is its own `<audio>`: the outgoing shot's `<audio>` ends at the audio cut (4) while its picture carries on to 5, so two sounds never overlap on the same source. An audible `<video>` and an `<audio>` on the same file are only flagged when their time windows overlap.\n\n## Align a sound to an on-screen event\n\n```html\n<audio\n  id=\"sfx-click-3\"\n  src=\"click.mp3\"\n  data-start=\"7.48\"\n  data-duration=\"0.07\"\n  data-track-index=\"103\"\n  data-volume=\"0.85\"\n></audio>\n```\n\nTimeline math: an audio element in the root composition has `data-start` in absolute root time; audio inside a scene file uses scene-local time and the host's `data-start` is added for you. An event inside a sub-composition happens at the host's `data-start` plus the event's local time in that sub-composition's own timeline, so `data-start = host start + local time`. Move only the audio's `data-start`; leave the picture alone. Source math: if the sound's transient is not at the file's first sample, subtract that lead-in from `data-start` (or trim it with `data-media-start`). Audio follows: nothing links audio to picture, so re-derive after every retime of the host. Owner: `/hyperframes-core`. Limit: no waveform auto-sync; for a beat grid use `hyperframes beats` and place each start on a beat time.\n\n## Copy a group of clips to another time\n\n```html\n<audio\n  id=\"sfx-click-0\"\n  src=\"click.mp3\"\n  data-start=\"1.6\"\n  data-duration=\"0.07\"\n  data-track-index=\"100\"\n></audio>\n<audio\n  id=\"sfx-type-0\"\n  src=\"typenew.mp3\"\n  data-start=\"7.8\"\n  data-duration=\"0.57\"\n  data-track-index=\"109\"\n></audio>\n<audio\n  id=\"sfx-click-0-copy\"\n  src=\"click.mp3\"\n  data-start=\"41.6\"\n  data-duration=\"0.07\"\n  data-track-index=\"186\"\n></audio>\n<audio\n  id=\"sfx-type-0-copy\"\n  src=\"typenew.mp3\"\n  data-start=\"47.8\"\n  data-duration=\"0.57\"\n  data-track-index=\"187\"\n></audio>\n```\n\nTimeline math: pick the clips first and say which ones you picked (by id) if the request does not match the file exactly; then add one `delta` to every member's `data-start`, so relative spacing is preserved (here `delta = 40`). Give each copy a new unique `id` and the next unused `data-track-index`; keep `src`, `data-duration`, `data-media-start`, `data-volume` and any `data-automation` as they are. Leave the originals untouched. Check the copies still end inside the composition's duration. Owner: `/hyperframes-core`. Limit: copies of a `<video>` or a sub-composition host follow the same rule, and a copied sub-composition needs its own host `id`.\n\n## Add media (image, video, audio)\n\nWrite what Studio writes when a person drops a file on the timeline, so an agent-added clip behaves the same as a dropped one; the one difference is that video and audio need no `data-duration`. Studio's source of truth is `DEFAULT_TIMELINE_ASSET_DURATION` in `packages/studio/src/utils/studioHelpers.ts` and `buildTimelineAssetInsertHtml` in `packages/studio/src/utils/timelineAssetDrop.ts`; a test keeps this section equal to them.\n\n- **Image: `data-duration` is optional and defaults to 3 seconds**, the same as a dropped image, because a still has no length of its own. Write it only for another length. A test keeps the 3 equal to the default in code.\n- **Video and audio: `data-start` is enough.** The length comes from the media itself. An authored `data-duration` shorter than the file is a trim, never a requirement; leave it out unless the request asks for a shorter clip.\n- **Start: the playhead or the requested time, never a silent `0`.** Studio's asset-panel Add uses the playhead time on track `0`; a drop uses the drop point.\n- Give every clip `id`, `class=\"clip\"`, `data-start` and `data-track-index`. A video with sound is `playsinline data-has-audio=\"true\"`; silent footage and b-roll is `muted playsinline`. Audio carries `data-volume=\"1\"`.\n- Then make sure the root composition's `data-duration` is at least the clip's end (`data-start` plus its length: 3 for an image unless you set another, the media's length for video and audio): Studio raises a declared root duration to cover the new clip, so an agent must too, or the clip lies past the end and never plays.\n- **Images and video fill the whole frame**: absolutely positioned at `left: 0; top: 0`, `width` and `height` equal to the composition's `data-width` and `data-height`, `object-fit: contain`. Studio does not know a dropped file's natural size, so it does not centre a smaller one.\n- `z-index` is the number of top-level clips already in that file plus one (at least `1`); later clips stack above earlier ones.\n- Several files dropped together share the drop's track and run end to end.\n\n```html\n<img\n  id=\"photo\"\n  class=\"clip\"\n  src=\"assets/photo.png\"\n  data-start=\"4\"\n  data-track-index=\"1\"\n  style=\"position: absolute; left: 0px; top: 0px; width: 1920px; height: 1080px; object-fit: contain; z-index: 2\"\n/>\n```\n\n```html\n<video\n  id=\"broll\"\n  class=\"clip\"\n  src=\"assets/broll.mp4\"\n  data-start=\"4\"\n  data-track-index=\"2\"\n  muted\n  playsinline\n  style=\"position: absolute; left: 0px; top: 0px; width: 1920px; height: 1080px; object-fit: contain; z-index: 3\"\n></video>\n```\n\n```html\n<audio\n  id=\"whoosh\"\n  class=\"clip\"\n  src=\"assets/whoosh.mp3\"\n  data-start=\"4\"\n  data-track-index=\"3\"\n  data-volume=\"1\"\n></audio>\n```\n\nInside a sub-composition file, `data-start` is scene-local (see `## Align a sound to an on-screen event`). Owner: `/hyperframes-core`.\n\n## Swap a media file\n\n```html\n<video\n  id=\"hero\"\n  src=\"assets/product-v2.mp4\"\n  data-start=\"2\"\n  data-duration=\"4\"\n  data-media-start=\"0\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n```\n\nTimeline math: change only `src`. Source math: reset `data-media-start` to the offset you want in the NEW file, and set `data-duration` no longer than the new file's remaining length (probe it with `ffprobe`). Audio follows: the sound moves with the video clip; a separate `<audio>` that pointed at the old file (music, voiceover) needs its own `src` swap. Keep `id`, `data-start`, `data-track-index` and any `data-automation` so nothing else moves. Run `lint`: `audio_src_not_found` and `media_src_kind_mismatch` catch a wrong path or kind. Owner: `/hyperframes-core`. Limit: a still swapped for a video (or the reverse) is a tag change, not a swap.\n\n## Split a section and change its speed\n\nTimeline math: a section that is a sub-composition or a group of clips has no `data-playback-rate` of its own to set; split it by giving each half its own host or clips and shift everything after the cut by the length change. New length of a part = old length / rate. Every later `data-start` (clips, audio, root-timeline tweens) moves by the same delta. Source math: `<video>` and `<audio>` parts use `data-playback-rate` (0.1 to 10, constant) per the constant-speed recipe above, the sound moves with the video. A speed ramp (a rate that changes within one clip) is a `rate` lane in `data-automation` on the `<video>`/`<audio>`; see `docs/reference/speed-ramps`. Say which you did.\n\nFile v1.0.42:references/data-attributes.md\n\n# Data Attributes Reference\n\nEvery HyperFrames composition uses `data-*` attributes to declare timing and structure to the framework. This is the full attribute table — pair with `tracks-and-clips.md` for the rules behind `data-track-index`.\n\n## Composition Root\n\nEvery renderable composition needs one root element:\n\n| Attribute                    | Required      | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |\n| ---------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `data-composition-id`        | Yes           | Unique ID. Must match the animation registry key on `window.__timelines`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |\n| `data-width` / `data-height` | Yes           | Pixel frame size. Common values: `1920x1080`, `1080x1920`, `1080x1080`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |\n| `data-duration`              | Conditional\\* | Render duration in seconds (total length / frame count), not the GSAP timeline length. **Read once at compile time, like `data-width` / `data-height`**: a static root `data-duration` is locked before scripts run, so a script (`root.setAttribute(\"data-duration\", ...)`) or a `--variables`-driven value cannot change the render length. To vary length per render, author the root `data-duration` directly. (A clip's `data-duration` is different: re-read from the live DOM, so scripts/variables can drive it.) Only when the root omits `data-duration` does the renderer derive total length from the live DOM / timeline after scripts run. |\n| `data-fps`                   | No            | Optional frame rate hint. CLI render flags can override output fps.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |\n| `data-composition-variables` | No            | JSON array of variable declarations (on `<html>`). See `variables-and-media.md`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |\n\n\\*`data-duration` is optional whenever the runtime can auto-infer duration: a registered GSAP timeline, a finite CSS animation, a finite WAAPI `element.animate()`, a registered Lottie animation, or timed clips (the root then ends at its latest clip end, and lint only warns `root_composition_duration_derived`). It is **required** for Three.js (no auto-inference), for infinite/unbounded CSS or WAAPI animations, and for any composition with no GSAP timeline, no animation signal and no timed clip with a length. `npx hyperframes lint` enforces this (`root_composition_missing_duration_source`). Per-runtime inference lives in `hyperframes-animation/adapters/`.\n\nThe root should be `position: relative`, have explicit pixel dimensions, and hide overflow unless intentionally composing outside the frame.\n\n## Clip Attributes\n\n**`data-start` is what makes an element a clip.** The runtime collects `[data-start]` and drives visibility off that attribute, so any element carrying it is timed.\n\n`class=\"clip\"` is a **convention, not a requirement**: the runtime never reads it. Keep writing it: drop it and the full-frame box collapses unless you supply that layout yourself, Studio uses it as an edit hint, and `lint` warns (`timed_element_missing_clip_class`) when a timed element lacks it. Omit it on `<video>` and `<audio>`.\n\n**Nesting is allowed.** A timed element inside a wrapper is still timed, and a timed ancestor clamps its descendants: a child cannot be visible while its timed ancestor is hidden. Direct children of the root get automatic layout (see \"Root-level clips get automatic layout\" below); nested ones do not, so give them their own positioning.\n\n| Attribute          | Required                                           | Meaning                                                                                                                                                                                                                                                                                                                                                            |\n| ------------------ | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `id`               | Yes on `<video>`/`<audio>`, else recommended       | `lint` errors with `media_missing_id` on media without one, and an id-less `<audio>` is never mixed, so the render is **silent**. Elsewhere it is a warning (`studio_missing_editable_id`): Studio needs a stable edit target, and timeline targets reference it.                                                                                                  |\n| `data-start`       | Yes                                                | Start time in seconds, or a supported clip-time reference. This attribute is what marks the element as timed.                                                                                                                                                                                                                                                      |\n| `data-duration`    | Required for `div` and sub-compositions            | Duration in seconds. An `img` with `data-start` defaults to 3 s; video/audio default to the source length (less the playback offset, over the rate) once known; an authored value trims. Without any resolvable duration the element has no end and stays visible for the rest of the composition.                                                                 |\n| `data-track-index` | No                                                 | Studio timeline lane, display only. The render never reads it, and clips on one track may overlap in time. Absent, the parser defaults it and Studio lays out one lane per clip. Two `<audio>` elements on the same index that overlap in time raise a `lint` warning.                                                                                             |\n| `data-media-start` | No                                                 | Offset into the media source, in seconds.                                                                                                                                                                                                                                                                                                                          |\n| `data-volume`      | No                                                 | Static audio gain, default `1` (0 dB). `0` is silence and values above `1` boost, up to `3.98` (+12 dB) — Studio's fader writes this. For fades and ducking, use the `data-automation` volume lane (see `creator-editing-recipes.md`).                                                                                                                             |\n| `data-has-audio`   | Required on a timed `<video>` unless it is `muted` | `\"true\"` keeps the file's sound on this clip (the default for footage with sound). Silent footage uses `muted` instead; a video with neither fails lint (`video_missing_muted`).                                                                                                                                                                                   |\n| `data-link`        | No                                                 | Editing contract only (render ignores it): clips sharing an id, e.g. a video and the `<audio>` detached from it in Studio, are edited as one. Keep every member's `data-start`, `data-duration`, `data-media-start` and `data-playback-rate` equal; to unlink, remove the attribute from all members. See [tracks-and-clips.md](tracks-and-clips.md#linked-clips). |\n| `data-sync-origin` | No                                                 | Editing contract only: a video and audio from one source file share it (written by Detach, and by Link for same-file pairs; kept by Unlink). Studio flags the pair out of sync when their source-zero points differ. See [tracks-and-clips.md](tracks-and-clips.md#linked-clips).                                                                                  |\n\n**The visibility window is half-open: `[start, start + duration)`.** A clip shows while `start ≤ t < start + duration` and is hidden at exactly `t = start + duration`. Land an animation's resolved end state slightly **before** `data-duration`, not on it, or its last frame is never rendered. Two clips can therefore be authored back to back (`b.start === a.start + a.duration`) with no overlapping frame.\n\n**Root-level clips get automatic layout.** For direct children of the composition root that carry `data-start`, the runtime forces `position: absolute` and anchors them at `top: 0; left: 0`, sizing them to 100% when they have no computed size, so scenes stack in the same viewport layer. Elements **without** `data-start` are skipped entirely: an untimed full-bleed background needs its own `position: absolute; inset: 0`, or it collapses to zero height.\n\n## Sub-Composition Host Attributes\n\nWhen a clip is a sub-composition host (loads another composition file):\n\n| Attribute                    | Required    | Meaning                                                                                                                                                     |\n| ---------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `data-composition-id`        | Recommended | The composition ID of the loaded file. Matching it is the convention; a host that names a different id, or none at all, is supported but resolves silently. |\n| `data-composition-src`       | Yes         | Path to the sub-composition HTML file.                                                                                                                      |\n| `data-width` / `data-height` | No          | Render dimensions for the instance. The compiler backfills them from the loaded file's root when absent.                                                    |\n| `data-variable-values`       | No          | Per-instance variable overrides as JSON. See `variables-and-media.md`.                                                                                      |\n| `data-var-src`               | No          | Binds the element's `src` to a declared variable id (media/image substitution, authored src = fallback).                                                    |\n| `data-var-text`              | No          | Binds the element's own text to a scalar variable id; children are preserved.                                                                               |\n\nSee `sub-compositions.md` for the full wiring pattern.\n\n## Authoring Hints\n\n- `id=\"root\"` — template convention used by scaffolds and the transition catalog so CSS can target the composition root with `#root` instead of `[data-composition-id=\"main\"]`. Not required by the runtime, but consistent with the rest of the ecosystem.\n- `class=\"clip\"`: layout and tooling convention on visible timed elements (`<div>`, `<img>`, …), not a runtime requirement. See Clip Attributes above.\n- `data-root=\"true\"`: names the composition root explicitly. Without it the runtime picks the outermost `[data-composition-id]` element, which is right for almost every file; set it when compositions nest and you need to be unambiguous.\n- `data-layout-allow-overflow` — tells `hyperframes check` that overflow on this element (or its descendants) is intentional. Notes:\n  - The `check` layout audit measures `getBoundingClientRect` at sampled timestamps, not rendered pixels. `overflow: hidden` clips the visual but does **not** suppress a layout finding. This attribute is the escape hatch; CSS overflow is not.\n  - Can be set on the composition **root** as well as on any child. When the cited offender is `div.<comp>-root inside div.<comp>-root` (the root reports its own children's union as overflowing), the fix goes on the root, not on individual text descendants — shrinking font sizes will not converge.\n  - In a multi-scene `group_wN.html` (continue runs), every scene-local element stays in the DOM during the other scenes' time windows; the layout-box union almost always overflows the canvas during morph seams. Mark the root and every scene-local primary/supporting element with this attribute **at construction**, not after `check` flags it.\n  - **Blast radius — it silences more than the overflow audit.** The attribute is inherited down the subtree (the perception probe walks ancestors), so it also suppresses the rendered-perception checks `text-clipping`, `content-cramped-container`, and `foreground-over-panel` for every descendant. Putting it on a persistent panel that also hosts real foreground content disables collision checks on that content for the panel's whole lifetime. Prefer the narrowest opt-out: scope it to the smallest decorative wrapper, or use per-element `data-layout-bleed=\"true\"` for one intentional primary-text crop. The two canvas/edge checks `primary-offscreen` and `foreground-over-panel` deliberately run **even under** allow-overflow, so it cannot hide a wordmark sliced by the frame or text bleeding onto a panel edge.\n- `data-layout-ignore` — exclude this element from layout audits entirely.\n- `data-layout-allow-caption-zone` — opt out of `--caption-zone` / `caption_zone_collision` for intentional lower-third copy (applies to the element and every descendant via `closest`; does **not** suppress overflow, overlap, occlusion, or other layout audits — pair those attrs if needed).\n\n## Legacy / Removed Attributes\n\nThese names appear in older projects and examples. Use the current names when authoring or editing:\n\n| Legacy name  | Use instead        |\n| ------------ | ------------------ |\n| `data-layer` | `data-track-index` |\n| `data-end`   | `data-duration`    |\n\nFile v1.0.42:references/determinism-rules.md\n\n# Determinism, Animation Runtime, and Layout\n\nHyperFrames seeks compositions frame-by-frame. Every frame must be reproducible from its time value alone — same input time → same pixels. Three contracts enforce this: the **animation runtime contract**, the **determinism rules**, and the **layout contract**.\n\n## Animation Runtime Contract\n\nGSAP is the primary runtime. The core requirement is generic: animation state must be seekable from HyperFrames time.\n\nFor GSAP:\n\n- Use `gsap.timeline({ paused: true })`.\n- Register it on `window.__timelines[\"<composition-id>\"]`, keyed by the composition root's `data-composition-id`. You do **not** need to write `window.__timelines = window.__timelines || {}` first: the runtime creates the registry before your inline scripts evaluate.\n- **Building inside an async callback is supported.** `document.fonts.ready(...)` and friends are the documented setup path. What you must not do is **register the key before the build finishes**. An empty timeline registered early is treated as ready and nested empty, so the animation renders blank (`lint`: `gsap_timeline_registered_before_async_build`, error). Assign `window.__timelines[id] = tl` at the **end** of the callback, after the tweens are added, and optionally call `window.__hfForceTimelineRebind()` right after.\n- If the key does not match the root's `data-composition-id`, the runtime still binds it **when it is the only registered timeline**. With two or more registered, a mismatched key leaves the render frozen at t=0.\n- **Do not** call `tl.play()` for render-critical motion.\n- **Do not** create empty tweens only to set duration; use `data-duration` on the clip instead.\n\nUse the `hyperframes-animation` skill for tween syntax, position parameters, eases, and performance rules. Non-GSAP duration inference lives in `hyperframes-animation/adapters/`.\n\n## Determinism Rules\n\nRendered frames must be reproducible from the requested time. Do **not** use any of the following for visual state:\n\n- `Date.now()`, `performance.now()`, or any render-time clock.\n- Unseeded `Math.random()`. Use a seeded PRNG if random-looking placement is needed.\n- Render-time network fetches for required assets. Inline or pre-bundle them.\n- Hover, scroll, pointer, or focus state. The renderer has no input events.\n- Unbounded infinite loops. `repeat: -1` is allowed only when the root declares a finite `data-duration` — deterministic seeking and export clip to that explicit window (`gsap_infinite_repeat` demotes to a warning). Without a finite composition duration it stays a hard error: the timeline can report an unbounded length and render planning fails. When the loop itself must end **before** the composition does, compute a finite count: `repeat: Math.max(0, Math.floor(duration / cycleDuration) - 1)` — **`floor`, not `ceil`** (`ceil` overshoots `data-duration` and trips the `gsap_repeat_ceil_overshoot` lint; `max(0, …)` avoids a negative repeat = infinite).\n\nAlso avoid:\n\n- Tweening `display`, raw `visibility`, or `autoAlpha` **on a clip element**: HyperFrames timing owns a clip's visibility, and `lint` rejects it (`gsap_animates_clip_element`). Fade with `opacity`, or tween a child wrapper. Do not tween `class=\"clip\"`.\n- There is no fixed allowlist of animatable properties. `lint` enforces a **denylist**, so `filter`, `clipPath`, `strokeDashoffset`, `width`, `height` and similar are all legitimate targets. Prefer transforms and opacity where you have the choice, for performance rather than correctness. The per-runtime detail lives in `hyperframes-animation/adapters/`.\n- Animating the same property on the same element from multiple timelines at the same time — GSAP's overwrite behavior is order-dependent and can flip between renders.\n\n## Layout Contract\n\nBuild the visible end-state in static HTML and CSS first, then animate from/to that state.\n\n- The composition root has fixed pixel frame dimensions.\n- **The root composition's total duration (render length / frame count) is fixed at compile time**, read once from the static root `data-duration` before scripts run, like `data-width` / `data-height`. A script or `--variables` value that rewrites the root `data-duration` afterward is ignored. To vary render length per output, author the root `data-duration` directly. (A _clip's_ own `data-duration` is re-read from the live DOM, so scripts/variables can still drive clip lengths. Only when the root omits `data-duration` does the renderer probe the live DOM / timeline for total length.)\n- Scene containers should fill the scene with `width: 100%; height: 100%; box-sizing: border-box`.\n- Use padding, flex, grid, and `max-width` for layout. Avoid positioning main content with hardcoded `top`/`left` offsets when a layout container can do it.\n- Use `position: absolute` for layers and decorative elements, not as the default content-layout strategy.\n- Prefer transforms and opacity for animation.\n- Keep text inside its intended container. For dynamic text, use `max-width`, wrapping, or `window.__hyperframes.fitTextFontSize(text, { maxWidth, fontFamily, fontWeight })`.\n- For text measurement without DOM reflow, use `window.__hyperframes.pretext`. Measure off a canvas instead of writing into the page and reading it back, so nothing reflows: `pretext.prepare(text, font)` then `pretext.layout(prepared, maxWidth, lineHeight)` → `{ lineCount, height }`. `prepare` does the font measurement; everything downstream of a prepared string is arithmetic and cheap enough to run per frame. `fitTextFontSize` is built on it.\n  - `layout` gives you height, not width. To size a container to its text (shrinkwrap), use `pretext.prepareWithSegments(text, font)` and then `pretext.measureNaturalWidth(prepared)` for the single-line width, or `pretext.measureLineStats(prepared, maxWidth)` for `{ lineCount, maxLineWidth }`.\n  - `font` is a CSS font shorthand string, e.g. `\"700 90px Inter\"`.\n  - `clearCache` and `setLocale` are deliberately not exposed: they mutate state shared across compositions, which would make a render depend on what ran before it.\n- **Do not** use `<br>` in body text. Forced breaks ignore the actual rendered font width and produce an extra break when the line already wraps naturally, causing overlap. Let text wrap via `max-width`. Exception: short display titles where each word is deliberately on its own line.\n- **Transformed elements must be block-level + sized.** `transform`/`scaleX`/`scaleY` is a no-op on an inline `<span>`, and scaling an auto-width (0px) element shows nothing → invisible bars/fills. Give them `display: block`/`inline-block`/flex-item **and** a real `width`/`height` (e.g. `width: 100%` inside a sized parent). _(Silent — automated gates may miss it.)_\n- **Absolutely-positioned decoratives that pulse or overshoot** (`yoyo` scale, `back.out`) need clearance at their **peak** size and must not straddle an `overflow: hidden` edge — else they overlap a neighbor or get clipped. Position for the largest frame, not the resting one. _(silent.)_\n\nFile v1.0.42:references/full-screen-motion.md\n\n# Full-Screen Motion Pattern\n\nFor full-frame motion (continuous backgrounds, color washes, full-bleed visual states that span multiple clips), prefer a **shared background layer + transparent timed content layers** over stacked opaque scene backgrounds.\n\n## Pattern\n\n```html\n<style>\n  /* The runtime auto-positions root children that carry data-start. The shared\n     background deliberately has none, so it gets NO automatic layout and must\n     size itself, or #bg is 0px tall and the tween paints nothing. */\n  #bg.full-bleed {\n    position: absolute;\n    inset: 0;\n  }\n  .clip.transparent {\n    background: transparent;\n  }\n</style>\n\n<div id=\"root\" data-composition-id=\"main\" data-width=\"1920\" data-height=\"1080\" data-duration=\"20\">\n  <!-- Shared background — NOT a clip. Always visible. Driven by the timeline. -->\n  <div id=\"bg\" class=\"full-bleed\"></div>\n\n  <!-- Timed content layers — transparent backgrounds. -->\n  <section\n    id=\"scene1\"\n    class=\"clip transparent\"\n    data-start=\"0\"\n    data-duration=\"6\"\n    data-track-index=\"1\"\n  >\n    <!-- content -->\n  </section>\n  <section\n    id=\"scene2\"\n    class=\"clip transparent\"\n    data-start=\"6\"\n    data-duration=\"14\"\n    data-track-index=\"1\"\n  >\n    <!-- content -->\n  </section>\n</div>\n\n<script>\n  const tl = gsap.timeline({ paused: true });\n\n  // Drive the shared background from the seekable timeline.\n  tl.to(\"#bg\", { backgroundColor: \"#0a1530\", duration: 6, ease: \"sine.inOut\" }, 0);\n  tl.to(\"#bg\", { backgroundColor: \"#1a0a30\", duration: 14, ease: \"sine.inOut\" }, 6);\n\n  // Scene-local animations stay transparent on top.\n  tl.from(\"#scene1 h1\", { y: 48, opacity: 0, duration: 0.6 }, 0.2);\n\n  window.__timelines[\"main\"] = tl;\n</script>\n```\n\n## Rules\n\n- **The background is not a clip.** No `data-start` / `data-duration`. It exists for the whole composition.\n- **Because it is not a clip, it gets no automatic layout.** The runtime only positions and sizes root children that carry `data-start`. An untimed background must set its own `position: absolute; inset: 0`, or it collapses to zero height and nothing you animate on it is visible. This is the most common way this pattern is copied wrong.\n- **Content scenes have transparent backgrounds.** Whatever you put in the shared `#bg` shows through.\n- **Drive global state from the shared layer.** Hue shifts, vignettes, grain, film-look filters — animate them once on the shared layer, not per-scene.\n- **Do not animate visibility on `.clip` elements.** HyperFrames already shows/hides clips based on `data-start` and `data-duration`. Animating `display` / `visibility` on the clip itself races with the framework's own show/hide. Animate a _child wrapper_ inside the clip instead.\n- **Verify intentional overflow with snapshots.** Before adding `data-layout-allow-overflow` to silence an inspect warning, run `npx hyperframes snapshot` and confirm the overflow is what you want.\n\nFile v1.0.42:references/minimal-composition.md\n\n# Minimal Composition\n\nThe smallest renderable HyperFrames composition — a standalone (top-level) root with one clip and one tween:\n\n```html\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=1920, height=1080\" />\n    <title>Minimal HyperFrames Composition</title>\n    <script src=\"https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js\"></script>\n    <style>\n      body {\n        margin: 0;\n        background: #0b0f14;\n        color: white;\n        font-family: Inter, system-ui, sans-serif;\n      }\n      #root {\n        position: relative;\n        width: 100%;\n        height: 100%;\n        overflow: hidden;\n      }\n      .clip {\n        position: absolute;\n        inset: 0;\n        display: grid;\n        place-items: center;\n      }\n      h1 {\n        margin: 0;\n        font-size: 96px;\n      }\n    </style>\n  </head>\n  <body>\n    <div\n      id=\"root\"\n      data-composition-id=\"main\"\n      data-start=\"0\"\n      data-width=\"1920\"\n      data-height=\"1080\"\n      data-duration=\"5\"\n    >\n      <section id=\"title-card\" class=\"clip\" data-start=\"0\" data-duration=\"5\">\n        <h1 id=\"title\">Hello HyperFrames</h1>\n      </section>\n    </div>\n    <script>\n      const tl = gsap.timeline({ paused: true });\n      tl.from(\"#title\", { y: 48, opacity: 0, duration: 0.6, ease: \"power3.out\" }, 0.2);\n      window.__timelines[\"main\"] = tl;\n    </script>\n  </body>\n</html>\n```\n\nWhat the runtime actually requires:\n\n- Root `<div>` with `data-composition-id`, `data-width`, `data-height`. Root `data-start=\"0\"` is written above by convention and every shipped block has it, but the runtime stamps it when absent, so it is not required.\n- A duration source: root `data-duration` (as above), or a GSAP timeline, or media, or an adapter that can infer one.\n- Timed elements carry `data-start` plus a duration. That attribute alone is what makes an element a clip: `class=\"clip\"` is a layout and tooling convention, and `data-track-index` is a Studio display lane. Neither is required, and a composition with no clips at all renders fine.\n- A GSAP timeline created paused and registered on `window.__timelines[\"<composition-id>\"]`.\n\nEverything else in the skeleton is ordinary HTML and CSS: the `#root` box, `.clip` positioning, and fonts are yours to choose.\n\nThis pattern is **standalone** (top-level `index.html`) — no `<template>` wrapper around the root. For sub-compositions (files loaded by `data-composition-src`), see `sub-compositions.md`.\n\nFile v1.0.42:references/sub-compositions.md\n\n# Sub-Compositions\n\nA sub-composition is a separate HTML file embedded in a host composition. HyperFrames loads it, seeks it independently, and composites the result into the host at `data-start`.\n\n## Host Wiring\n\nIn the host composition, the sub-composition appears as a clip with `data-composition-src`:\n\n```html\n<div\n  id=\"chart\"\n  data-composition-id=\"data-chart\"\n  data-composition-src=\"compositions/data-chart.html\"\n  data-start=\"2\"\n  data-duration=\"8\"\n  data-track-index=\"2\"\n  data-width=\"1920\"\n  data-height=\"1080\"\n></div>\n```\n\n- `data-composition-id` on the host must match the internal `data-composition-id` of the file at `data-composition-src`.\n- The host clip needs its own `data-start`, `data-duration`, `data-track-index`, `data-width`, `data-height`.\n\n## Sub-Composition File Structure\n\n### Mental model — what the runtime actually does\n\nWhen a host loads a sub-composition via `data-composition-src`, the runtime:\n\n1. `fetch`es the HTML file.\n2. Parses it with `DOMParser`.\n3. **Finds the `<template>` element and clones ONLY its contents into the host slot.**\n4. Everything **outside** the `<template>` (including the entire `<head>`) is **discarded**.\n\nSo `<template>` is not just a wrapper — it is the **transport container**. If a node needs to exist in the live render, it must be inside `<template>`. Full stop.\n\n### File shape\n\n```html\n<!doctype html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <!-- head is metadata for the source file only; the runtime ignores it -->\n  </head>\n  <body>\n    <template>\n      <!-- EVERYTHING the runtime needs goes here: styles, markup, scripts -->\n      <style>\n        /* Root: style by #root, never a class. lint: subcomposition_root_styled_by_class. See Pitfall 3. */\n        #root {\n          position: absolute;\n          inset: 0;\n          color: #fff;\n        }\n        /* .label, #bar, … — descendants, plain selectors */\n      </style>\n\n      <div id=\"root\" data-composition-id=\"data-chart\" data-width=\"1920\" data-height=\"1080\">\n        <!-- sub-composition markup -->\n      </div>\n\n      <script>\n        const tl = gsap.timeline({ paused: true });\n        // ... build timeline ...\n        window.__timelines[\"data-chart\"] = tl;\n      </script>\n    </template>\n  </body>\n</html>\n```\n\nContrast with **standalone** compositions, which put the root directly in `<body>` with no `<template>` wrapper.\n\n## Common pitfalls that pass static checks but break at render\n\nStatic file checks cannot prove the **cross-file mount contract**. These failures appear only when the runtime mounts the sub-composition.\n\n### Pitfall 1 — `<style>` in `<head>` instead of inside `<template>`\n\n```html\n<!-- ❌ WRONG — looks normal, ships catastrophically broken -->\n<head>\n  <style>\n    #root { font-size: 88px; ... }\n  </style>\n</head>\n<body>\n  <template>\n    <div id=\"root\" data-composition-id=\"data-chart\" ...>...</div>\n  </template>\n</body>\n\n<!-- ✅ RIGHT — styles are inside the template, root styled by #root (see Pitfall 3) -->\n<head></head>\n<body>\n  <template>\n    <style>\n      #root { font-size: 88px; ... }\n    </style>\n    <div id=\"root\" data-composition-id=\"data-chart\" ...>...</div>\n  </template>\n</body>\n```\n\n**Why this happens:** standard HTML conventions tell you to put `<style>` in `<head>`. In a standalone HTML file that's correct. In a HyperFrames sub-composition it is **not** — the runtime only clones `<template>` contents, so `<head><style>` is dropped on the floor.\n\n**Symptom:** isolated checks pass and the render completes, but every text element appears as tiny unstyled default text in the top-left and SVGs expand to canvas size because no CSS reached the live DOM. The same trap applies to `<script>` blocks, `<link rel=\"stylesheet\">`, and custom-element registrations: anything that must execute or apply in the render belongs inside `<template>`.\n\n### Pitfall 2 — Host `data-composition-id` ≠ inner template `data-composition-id`\n\n```html\n<!-- ❌ WRONG — host renames the slot; runtime can't find the timeline -->\n<!-- host file (e.g. index.html) -->\n<div data-composition-id=\"chart-mount\" data-composition-src=\"compositions/chart.html\" ...></div>\n\n<!-- chart.html -->\n<template>\n  <div data-composition-id=\"data-chart\" ...>...</div>\n  <script>\n    window.__timelines[\"data-chart\"] = tl;\n  </script>\n</template>\n\n<!-- ✅ RIGHT — both ids match, and the timeline key matches them too -->\n<div data-composition-id=\"data-chart\" data-composition-src=\"compositions/chart.html\" ...></div>\n<!-- chart.html template root: data-composition-id=\"data-chart\" -->\n<!-- timeline: window.__timelines[\"data-chart\"] = tl; -->\n```\n\n**Why this happens:** it feels natural to give the host slot a different name like `chart-mount` (\"the mount point\") vs `data-chart` (\"the actual chart\"). HyperFrames does not work that way — **the host's `data-composition-id` is the lookup key the framework uses to find the registered timeline**. Lint passes because each file's ids are individually valid; the cross-file mismatch only blows up at render.\n\n**Symptom:** the render logs `Sub-composition timelines not registered after 45000ms: <host-id>` for every mismatched slot, waits 45s per scene, then captures static initial-state frames (so the video is full-length but no animation plays).\n\n### Pitfall 3 — Styling the root by a class instead of `#root`\n\n`lint` still errors (`subcomposition_root_styled_by_class`) if a sub-composition styles the host root's own class. Style the root with `#root`.\n\n## What HyperFrames Does With the Sub-Composition\n\n- Loads the file and registers its timeline under its internal `data-composition-id`.\n- Seeks the sub-composition's timeline independently from the host's playhead.\n- Plays the sub-composition's content from `data-start` of the host clip, for `data-duration` seconds.\n\n**Do not** manually `master.add(child)` a sub-composition timeline into the host timeline. HyperFrames already drives them independently — nesting them in GSAP causes double-seeks.\n\n### The host clip's `data-duration` is the slot's visible window\n\n`data-duration` on the host clip defines **how long the slot is visible**, and it takes precedence over the sub-composition's internal GSAP timeline length. Two consequences follow:\n\n- **Internal timeline shorter than the slot → the slot holds.** If the sub-composition's GSAP timeline finishes before `data-duration` elapses, the slot keeps showing its final frame for the rest of the window. You do **not** need to pad the timeline with empty tweens.\n- **`data-duration` shorter than the host composition → the slot ends (and goes blank) when its own `data-duration` elapses.** This is intended: the clip is a fixed-length window on the timeline, not \"fill until the composition ends.\" To keep a sub-composition visible for the whole composition, set its `data-duration` to span the host window (or add another clip to cover the remaining time). Leaving a single full-bleed sub-composition shorter than the composition is almost always a mistake — the linter flags it as `subcomposition_blanks_before_host`.\n\n## Animations Inside Sub-Compositions\n\nPrefer `gsap.fromTo()` over `gsap.from()` for entrance tweens. The host re-seeks the sub-composition every time its clip becomes visible; `gsap.from()` records the starting state at registration and can desync on seek-back, while `gsap.fromTo()` declares both endpoints explicitly and replays cleanly.\n\n## Per-Instance Variables\n\nIf the sub-composition declares variables on its `<html>` element (`data-composition-variables`), the host can override values per instance:\n\n```html\n<div\n  data-composition-id=\"data-chart\"\n  data-composition-src=\"compositions/data-chart.html\"\n  data-variable-values='{\"title\":\"Q4 Revenue\",\"accent\":\"#66d9ef\"}'\n  data-start=\"2\"\n  data-duration=\"8\"\n  data-track-index=\"2\"\n  data-width=\"1920\"\n  data-height=\"1080\"\n></div>\n```\n\nThe host can render the same sub-composition multiple times with different `data-variable-values` to produce per-instance variations. See `variables-and-media.md` for variable declaration syntax.\n\nFile v1.0.42:references/tailwind.md\n\n# HyperFrames Tailwind\n\nHyperFrames `init --tailwind` uses the Tailwind browser runtime pinned by the scaffold. Treat it as Tailwind v4, not Studio's Tailwind v3 setup.\n\n## Version Contract\n\n- **Pinned: `@tailwindcss/browser@4.2.4`** (source of truth: `packages/cli/src/commands/init.ts` `TAILWIND_BROWSER_VERSION`).\n- Do not replace the scaffolded runtime with `cdn.tailwindcss.com` (unpinned, defeats reproducibility).\n- Keep the readiness shim deterministic; HyperFrames waits for `window.__tailwindReady` before frame 0 capture.\n- For offline / locked-down / production-stable renders, compile Tailwind to CSS and ship the stylesheet instead of the browser runtime.\n\n## v4 Browser Runtime Rules\n\nTailwind v4 is CSS-first:\n\n```html\n<style type=\"text/tailwindcss\">\n  @theme {\n    --color-brand: oklch(0.68 0.2 252);\n    --font-display: \"Inter\", sans-serif;\n  }\n\n  @utility headline-balance {\n    text-wrap: balance;\n    letter-spacing: 0;\n  }\n</style>\n```\n\nAvoid v3-only patterns in browser-runtime compositions:\n\n```css\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n```\n\nDo not add `tailwind.config.js` only for composition colors, fonts, spacing, or utilities. Use `@theme` and `@utility`.\n\n**`@config` / `@plugin` abort the browser compile.** The pinned `@tailwindcss/browser` build does not support JS config or plugins. Theme and utilities stay in a `text/tailwindcss` block.\n\n## Composition Pattern\n\nUse Tailwind for static layout and style. Keep render-critical timing in GSAP or another seekable HyperFrames adapter.\n\n```html\n<section\n  id=\"hero\"\n  class=\"clip absolute inset-0 grid place-items-center bg-zinc-950 text-white\"\n  data-start=\"0\"\n  data-duration=\"5\"\n  data-track-index=\"1\"\n>\n  <div class=\"w-[1280px] max-w-[82vw] text-center\">\n    <h1 class=\"text-7xl font-black leading-none text-balance\">Render-ready Tailwind</h1>\n  </div>\n</section>\n```\n\nFor repeated items, **parameterize via CSS variables** — keep the class list static so the runtime sees every utility:\n\n```html\n<span class=\"translate-y-[calc(var(--i)*6px)] opacity-80\" style=\"--i: 0\"></span>\n<span class=\"translate-y-[calc(var(--i)*6px)] opacity-80\" style=\"--i: 1\"></span>\n<span class=\"translate-y-[calc(var(--i)*6px)] opacity-80\" style=\"--i: 2\"></span>\n```\n\n## Dynamic Class Safety\n\nThe browser runtime scans classes it can see. Do not build render-critical class names only at seek time:\n\n```js\n// Risky: the runtime may never see every generated class.\nelement.className = `bg-${color}-500`;\n```\n\nPrefer complete class tokens in HTML, data variants, or explicit CSS:\n\n```html\n<div data-tone=\"blue\" class=\"bg-blue-500 data-[tone=rose]:bg-rose-500\"></div>\n```\n\nIf a generated class is unavoidable, make sure the full class token appears in a `text/tailwindcss` block before validation.\n\n## Video-Specific Guardrails\n\nv4 + render-mode footguns. Every bullet is a hard rule:\n\n- **Stable dimensions only** — use `w-[…]` / `h-[…]` / `aspect-video` / grid / flex. **No `md:` / `lg:` breakpoints** (renderer is fixed-viewport).\n- **Animate via transforms / opacity** — `translate-*`, `scale-*`, `opacity-*` are seek-safe; animating Tailwind sizing utilities is not.\n- **No `transition-*` for render-critical motion** — a seekable runtime (GSAP) must own the state.\n- **No interaction variants** — `hover:` / `focus:` / `active:` / `group-*:` / `peer-*:` / scroll / pointer variants never fire during render.\n- **Bare `border` is broken in v4** — v4 default is `currentColor` (v3 was `gray-200`). Always write the color: `border border-white/20`.\n- **v4 utility renames** — `shadow-sm` → `shadow-xs`, `rounded-sm` → `rounded-xs`, `outline-none` → `outline-hidden`, `flex-shrink-*` → `shrink-*`, `flex-grow-*` → `grow-*`.\n- **Modern CSS is fine** — `color-mix()`, container queries, logical properties work; the renderer is current Chrome.\n\n## Validation\n\n```bash\nnpx hyperframes check\n\n# Render proof — frame 0 must NOT flash unstyled content. Preview alone can hide this.\nnpx hyperframes render . --workers 1 --quality draft --output tailwind-proof.mp4\n```\n\nFile v1.0.42:references/tracks-and-clips.md\n\n# Tracks and Clips\n\nClips are timed elements inside a composition. Tracks are a Studio display concept: the render never reads them.\n\n## What is a Clip\n\nA clip is any DOM element with `data-start` and, where required, `data-duration`. `data-track-index` is optional. Common kinds:\n\n- **Visual `<div>` clips** — scenes, cards, overlays. Always require `data-duration`.\n- **Sub-composition hosts** — `<div>` with `data-composition-src`. Always require `data-duration`.\n- **Video clips** — `<video playsinline>`. With sound: `data-has-audio=\"true\"` (the sound stays on the clip). Silent: `muted`. Duration can default to media length.\n- **Audio clips** — `<audio>`. Duration can default to media length.\n- **Image clips** — `<img>`. `data-duration` is optional and defaults to 3 seconds; write it only for another length.\n\nAdd `class=\"clip\"` to authored visual clips. The runtime does not read it, but the scaffold's shared `.clip { position: absolute; inset: 0 }` rule is what gives a scene its full-frame box, Studio treats it as an edit hint, and `lint` warns without it.\n\n## Tracks Are a Display Lane\n\n`data-track-index` is the row a clip occupies in Studio's timeline. It is **not** read by the render, and it constrains nothing:\n\n- **Two clips on the same track may overlap in time.** Nothing rejects it and the render is well defined: both are visible, painted in CSS order.\n- **Visual layering (front/back)** is controlled by CSS `z-index`, not by track index.\n- **Omitting it is fine.** The parser defaults it, and Studio then lays out one lane per clip.\n\nA clip on track `5` is not \"above\" a clip on track `1`. Use CSS for layering, `data-start`/`data-duration` for sequencing.\n\nThe one place the value carries meaning: two `<audio>` elements that share a track index **and** overlap in time raise a `lint` warning (`duplicate_audio_track`), which is a useful nudge that you are about to double up a bed.\n\n## Picking a Track Index\n\nPurely a readability choice for whoever opens the file in Studio. Common patterns, owned by `/hyperframes-studio` (one caption track, one element kind per track).\n\nWhen adding a clip to an existing composition, set its `data-start`/`data-duration` against the clips around it. You do not need to hunt for a free lane, and you never need to renumber tracks after a retime.\n\n## Clip Time Inside the Composition\n\n`data-start` is in seconds, measured from the start of the _composition_. For sub-compositions, the sub-composition's internal timeline (its own `data-duration` and child clips) runs from `data-start` to `data-start + data-duration` of the host.\n\n`data-media-start` (on `<video>`/`<audio>`) is an offset _into the source media_. Use it to skip the first few seconds of a media file without trimming the file itself.\n\n## Cut one source into multiple ranges\n\nFor a hard cut, trim, splice, or reorder, duplicate the same video source into\nmultiple clip elements. Each copy selects its source range with\n`data-media-start` plus `data-duration`, and places that range on the authored\ntimeline with `data-start`. Change the source offsets and placement order; do\nnot try to keyframe source cutting.\n\nEach video segment keeps its sound: the sound stays on the clip (`data-has-audio=\"true\"`), so cutting the video cuts its sound. A separate `<audio>` is for other sound (music, voiceover, replacement audio, J/L cuts).\n\n## Linked clips\n\n`data-link=\"<id>\"` marks clips that are edited as one: in Studio, moving, trimming, splitting or deleting one member does the same to the others. Detach audio in Studio produces a pair, a muted `<video>` and an `<audio>` over the same file:\n\n```html\n<video\n  id=\"talk\"\n  src=\"talk.mp4\"\n  muted\n  data-link=\"lk-1\"\n  data-sync-origin=\"lk-1\"\n  data-start=\"2\"\n  data-duration=\"6\"\n  data-media-start=\"1\"\n  data-track-index=\"0\"\n></video>\n<audio\n  id=\"talk-audio\"\n  src=\"talk.mp4\"\n  data-link=\"lk-1\"\n  data-sync-origin=\"lk-1\"\n  data-start=\"2\"\n  data-duration=\"6\"\n  data-media-start=\"1\"\n  data-track-index=\"2\"\n></audio>\n```\n\n- **Link offer.** Studio offers Link for exactly one video and one audio, neither already linked; their timing and source file don't matter. A linked pair that is offset moves together (the offset is kept), and a trim carries to the partner only when its edge sits at the same time. Merge back needs the same file and an in-sync pair.\n- **Keep members in sync.** Every member needs the same `data-start`, `data-duration`, `data-media-start` (absent = 0) and `data-playback-rate` (absent = 1). Track index, volume, fades and FX may differ. When you retime one member by hand, retime all of them, or `lint` warns `linked_clips_out_of_sync`.\n- **Unlink** by removing `data-link` from every member. Removing it from one leaves the other alone with the id, which `lint` flags as `linked_clip_orphan`.\n- The render ignores `data-link`: an out-of-sync pair still plays exactly what its timings say.\n- Prefer a single `<video data-has-audio=\"true\">` for footage with sound. Link only when the sound needs its own clip (its own track, volume or FX); to undo a detach, move the audio attributes back onto the video and delete the `<audio>`.\n- **Sync origin.** `data-sync-origin=\"<id>\"` marks a video and an audio from one source file; Detach writes it, Link writes it only for a pair from one source file, and Unlink keeps it. When the pair drifts (their source-zero points, `data-start − data-media-start / data-playback-rate`, differ), Studio shows a red offset in frames on both halves with Move into Sync / Slip into Sync, `hyperframes timeline` prints `out-of-sync=±Nf`, and the SDK offers `syncOffset`, `moveIntoSync` and `slipIntoSync`. Leave it alone when retiming by hand; remove it only when the clips are no longer one source.\n- `@hyperframes/sdk` `setTiming` applies to link partners by default; pass `{ linked: false }` to edit one member, which unlinks it.\n\n## Relative Timing\n\n`data-start` accepts a clip ID instead of a number, meaning \"start when that clip ends\". Add `+ N` / `- N` to offset; negative produces overlap (useful for crossfades).\n\n```html\n<video id=\"intro\" data-start=\"0\" data-duration=\"10\" data-track-index=\"0\" src=\"...\"></video>\n<video id=\"main\" data-start=\"intro\" data-duration=\"20\" data-track-index=\"0\" src=\"...\"></video>\n<video\n  id=\"scene-a\"\n  data-start=\"intro + 2\"\n  data-duration=\"20\"\n  data-track-index=\"0\"\n  src=\"...\"\n></video>\n<video\n  id=\"scene-b\"\n  data-start=\"intro - 0.5\"\n  data-duration=\"20\"\n  data-track-index=\"1\"\n  src=\"...\"\n></video>\n```\n\nRules, and three ways this fails **silently**. Nothing in `lint` checks any of them, so read them before you use a reference:\n\n- **Spaces around the operator are required.** `data-start=\"intro - 0.5\"` means \"0.5s before `intro` ends\". `data-start=\"intro-0.5\"` (no spaces) is parsed as a reference to an element whose id is literally `intro-0.5`; that element does not exist, so the clip silently starts at 0.\n- **An unresolved reference resolves to 0**, it does not error. A typo'd id, or a target that is not in the document, puts the clip at the start of the composition.\n- **If the target has no resolvable duration, the reference lands on the target's START, not its end.** So `data-start=\"hero\"` where `hero` has no `data-duration` and no known media length silently means \"same time as `hero`\" rather than \"after `hero`\".\n- **A cycle resolves to 0** rather than erroring. `A → B → A` puts one of them at 0.\n- Lookup is **document-wide** (`getElementById`, then `[data-composition-id]`). A reference can therefore reach a target in another composition on the assembled page. Keep referenced ids unique and keep the reference and its target in the same file, or the result depends on assembly order.\n- A value that parses as a number is always absolute seconds. Otherwise the resolver expects `<id>`, `<id> + <number>`, or `<id> - <number>`.\n- References can chain (`A → B → C`). Keep chains under 3-4 levels for readability.\n- Negative offsets create overlap, which is allowed. Overlapping clips do **not** need different tracks.\n\nBecause every failure mode above is a silent 0, snapshot a reference-timed composition and check the clip actually starts where you meant.\n\nFile v1.0.42:references/variables-and-media.md\n\n# Variables and Media\n\nTwo separate concerns, grouped because both control \"what flows in from outside the HTML\": runtime parameters (variables) and external media files (video/audio).\n\n## Variables\n\nDeclare variables on the `<html>` element with `data-composition-variables`. Each declaration needs `id`, `type`, `label`, and `default`:\n\n```html\n<html\n  data-composition-variables='[\n    {\"id\":\"title\",\"type\":\"string\",\"label\":\"Title\",\"default\":\"Hello\"},\n    {\"id\":\"accent\",\"type\":\"color\",\"label\":\"Accent\",\"default\":\"#66d9ef\"}\n  ]'\n></html>\n```\n\n**Prefer declarative bindings — no script needed** for direct substitution:\n\n```html\n<img class=\"clip\" data-start=\"0\" data-duration=\"5\" data-var-src=\"heroImage\" src=\"fallback.jpg\" />\n<h1 class=\"clip\" data-start=\"0\" data-duration=\"5\" data-var-text=\"title\">Fallback</h1>\n<style>\n  .card {\n    color: var(--accent);\n  }\n</style>\n```\n\n- `data-var-src=\"id\"` substitutes the element's `src` (URL string or image `{url}`); the authored `src` is the fallback.\n- `data-var-text=\"id\"` substitutes the element's own text; element children (nested clips, animated spans) are preserved.\n- Every scalar variable is applied automatically as a `--{id}` CSS custom property on the composition root, so `var(--id)` CSS responds to overrides — no `setProperty` boilerplate.\n- Bindings resolve identically in preview and render, and per-instance for sub-compositions.\n- Caveat: media with audio should keep a real fallback `src` — render audio extraction reads the authored attribute (lint: `media_variable_src_no_fallback`).\n\nFor logic beyond direct substitution (loops, conditionals, derived values), read values once during initialization:\n\n```js\nconst { title, accent } = window.__hyperframes.getVariables();\ndocument.getElementById(\"title\").textContent = title;\n```\n\n### Variable Rules\n\n- Supported types and their extra options (consumed by Studio's editing UI):\n  - `string` — optional `placeholder`, `maxLength`\n  - `number` — optional `min`, `max`, `step`, `unit`\n  - `color` — none\n  - `boolean` — none\n  - `enum` — **required** `options: [{ \"value\": \"...\", \"label\": \"...\" }, ...]`\n- Always provide useful `default` values so preview works without CLI overrides.\n- Use `data-variable-values='{\"title\":\"Pro\"}'` on sub-composition hosts for per-instance overrides.\n- Use `npx hyperframes render --variables '{\"title\":\"Q4 Report\"}'` or `--variables-file` for render-time overrides.\n- Add `--strict-variables` in CI: turns undeclared keys, type mismatches, and enum values not in `options` into errors instead of warnings.\n- Read values once during init, not on every animation tick — variables don't change mid-render.\n- Media color grading can use exact variable references inside `data-color-grading` JSON. Use `$gradingPreset` or `${gradingIntensity}` as the whole field value; the runtime resolves it from the current composition's variables before applying shader adjustments, finishing details, blur/pixelate effects, and custom LUTs.\n\n### Two JSON Shapes (Easy to Confuse)\n\n- `data-composition-variables` is an **array of declarations** (the schema): `[{id, type, label, default}, ...]`\n- `--variables` and `data-variable-values` are **objects keyed by id** (the values): `{\"title\":\"Q4\",\"accent\":\"#fff\"}`\n\n## Media\n\n**`<video>`/`<audio>` work at any nesting depth, including inside a sub-composition `<template>` or a wrapper `<div>`.** The runtime discovers media with a flat `document.querySelectorAll(\"video, audio\")`, resolves each element's host composition via `element.closest(\"[data-composition-id]\")`, and rebases its local `data-start` by the accumulated absolute start of every ancestor composition. A host at `2` with child media at `2` therefore starts that media at root time `4`, consistently in preview, snapshot, extraction, and render. Legacy projects that deliberately authored a media start in root time must mark that element with `data-hf-media-start-basis=\"global\"`; never infer the basis from overlapping numbers. New compositions should always use scene-local `data-start`. If a panel renders blank after a render, capture a per-frame `snapshot` and treat it as render-blocking.\n\nThe one real constraint is about **timelines, not media placement**: a sub-composition timeline **cannot reach or animate host elements** — neither `document.querySelector(\"#host-id\")` nor a gsap selector string (`tl.to(\"#host-id\", …)`) resolves across the boundary; a sub-comp timeline only drives its own subtree. So if a media element lives at the host root, **its per-scene motion (scale/opacity/morph/tilt/breathing) must be authored on the MAIN timeline in `index.html`, at GLOBAL time** (scene-local time + the scene slot's `data-start`). Keeping the media inside the scene sub-comp instead lets that sub-comp's own timeline animate it with scene-local time. For 3D tilt without a perspective parent, use gsap `transformPerspective` on the element. See `composition-patterns.md` archetype B.\n\nA video with sound keeps it on the `<video>` (`data-has-audio=\"true\"`, no `muted`). Use a separate `<audio>` for music, voiceover, replacement audio, J/L cuts, or audio detached in Studio. Silent footage and b-roll: `muted`.\n\n```html\n<video\n  id=\"a-roll\"\n  class=\"clip\"\n  src=\"assets/demo.mp4\"\n  playsinline\n  data-has-audio=\"true\"\n  data-start=\"0\"\n  data-duration=\"12\"\n  data-track-index=\"0\"\n  data-volume=\"1\"\n></video>\n\n<!-- Separate <audio> only for other sound: music, voiceover, replacement audio. -->\n<audio\n  id=\"music\"\n  src=\"assets/bed.mp3\"\n  data-start=\"0\"\n  data-duration=\"12\"\n  data-track-index=\"2\"\n  data-volume=\"0.4\"\n></audio>\n```\n\n### Media Rules\n\n- **Do not** call `video.play()`, `audio.play()`, pause, or seek in composition code. HyperFrames owns playback.\n- **Do not** drive host-root media from a sub-comp timeline: a sub-comp timeline cannot reach elements outside its subtree, so it has no effect. Drive host-root media from the main timeline at global time (or keep the media inside the sub-comp whose timeline animates it).\n- **Do not** animate timed media element dimensions; animate a non-timed wrapper instead.\n- **Do not** nest video inside a timed wrapper. `lint` rejects a `<video data-start>` whose ancestor also carries `data-start` (`video_nested_in_timed_element`, error), and the failure is real: the frame extractor resolves the video's start from its own `data-start` without the wrapper's offset, while visibility uses the wrapper's window. The clip then shows the wrong source frames and disappears partway through its slot. Put the timing on the wrapper **or** on the media element, never both.\n- **Sub-compositions are exempt and work.** A `<video>`/`<audio>` inside a sub-composition renders identically to one at the host root, because a composition host propagates its offset. Only plain timed wrappers (a `<section data-start>` around a `<video data-start>`) break.\n- **Never** add `crossorigin` to `<video>`/`<audio>`. `lint` rejects it unconditionally (`media_crossorigin_breaks_preview`, error) because a media host without `Access-Control-Allow-Origin` then fails silently in preview while renders still work, hiding the bug. There is no suppression, so this holds even for the canvas/WebGL/WebAudio readback case.\n- **Every `<audio>` needs an `id`.** The mixer selects `audio[id][src]`, so an id-less `<audio>` is never mixed and the render is **silent**. `lint` catches it as `media_missing_id`.\n- A video's own sound stays on the `<video>` (`data-has-audio=\"true\"`, no `muted`). Add a separate `<audio>` only for other sound (music, voiceover, replacement audio, J/L cuts) and mute the video it replaces.\n- For volume fades and ducking, use the `data-automation` volume lane; the exact form is in `creator-editing-recipes.md`. `data-volume` is the static baseline. A timeline `volume` tween is ignored when a lane is present.\n\nFor media duration: `<video>` and `<audio>` can omit `data-duration` if the media's intrinsic length is known and you want the full clip. Otherwise provide `data-duration` explicitly.\n\nInput codecs: render decodes video via FFmpeg (frames are pre-extracted and injected), so HEVC/H.265 assets (8/10-bit) render correctly everywhere; live preview auto-proxies any browser-hostile asset (transcodes and caches an H.264 copy on first use, opt out with `--no-proxy` or `media.autoProxy: false`), and `lint` emits an info-level `hevc_preview_codec` note naming affected assets.\n\nArchive v1.0.41: 13 files, 40874 bytes\n\nFiles: references/composition-patterns.md (10853b), references/creator-editing-recipes.md (18420b), references/data-attributes.md (17370b), references/determinism-rules.md (6659b), references/full-screen-motion.md (2906b), references/minimal-composition.md (2507b), references/sub-compositions.md (8074b), references/tailwind.md (4078b), references/tracks-and-clips.md (8171b), references/variables-and-media.md (8420b), skill-card.md (1998b), SKILL.md (12572b), _meta.json (136b)\n\nFile v1.0.41:SKILL.md\n\n---\nname: hyperframes-core\ndescription: The HyperFrames composition contract — build one renderable project. Use for composition structure, the `data-*` timing attributes, `class=\"clip\"`, tracks, sub-compositions, variables, framework-owned media playback, deterministic-render rules, and validation. Read before writing composition HTML.\n---\n\n**Plugin installs:** Before setup or freshness commands, follow [plugin execution rules](../hyperframes/references/plugin-installation.md) when this skill is inside a HyperFrames plugin. Standalone installs keep the update instructions below.\n\n# HyperFrames Core\n\n**Agent pitfalls (read first):**\n\n- Center with flex/`inset`, not CSS `transform: translate(-50%,-50%)` on a node you then GSAP `x`/`y`. Lint: `gsap_css_transform_conflict`. Use `fromTo` or `xPercent`/`yPercent`.\n- Do not add a scene-exit `tl.set(..., {visibility:\"hidden\"})`. The runtime already hides timed clips. Opacity fades on inner nodes (or `opacity` on `.clip`) are enough. Caption hard-kills are a different rule.\n- `window.__timelines[\"id\"]` must match the root `data-composition-id`.\n- After `render`, read the summary's second line: `beginframe` vs `screenshot`, GPU mode, stage timings. `screenshot` + `software gpu` on Linux is the slow path.\n\nHyperFrames renders video from HTML. A composition is an HTML file whose DOM declares timing with `data-*` attributes, whose animation runtime is seekable, and whose media playback is owned by the framework.\n\nThis skill is the **technical contract** — how to build one hyperframes project. The body below is the build guide; per-topic detail lives in `references/` (index next), read on demand. Process docs (brief, storyboard, review, production, dispatch, frame-worker) live in `/hyperframes` → `references/`. Other concerns live in the sibling domain skills — `hyperframes-animation`, `hyperframes-creative`, `media-use`, `hyperframes-cli`, `hyperframes-registry`. The capability map in `/hyperframes` says what each one covers.\n\n## References\n\n| File                                    | Read it to…                                                                                                                                                         |\n| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `references/minimal-composition.md`     | start from the smallest renderable composition skeleton                                                                                                             |\n| `references/composition-patterns.md`    | choose monolithic vs modular; structure a modular `index.html`; pick a sub-comp archetype                                                                           |\n| `references/data-attributes.md`         | look up any `data-*` (root / clip / sub-comp host / legacy aliases); use `class=\"clip\"`                                                                             |\n| `references/tracks-and-clips.md`        | understand what `data-track-index` does (and does not) control, z-index, time a clip relative to another; list every track and clip with `npx hyperframes timeline` |\n| `references/creator-editing-recipes.md` | copy truthful cut/trim/reorder/retime/freeze/camera/mask/crossfade/audio editing recipes and their limits                                                           |\n| `references/sub-compositions.md`        | wire a sub-composition (host attrs, `<template>`, per-instance vars) and animate inside it                                                                          |\n| `references/variables-and-media.md`     | declare variables; place `<video>`/`<audio>`, set volume, trim                                                                                                      |\n| `references/determinism-rules.md`       | build a seekable timeline; determinism bans; layout / text fit                                                                                                      |\n| `references/full-screen-motion.md`      | author full-frame motion with shared backgrounds                                                                                                                    |\n| `references/tailwind.md`                | work in a Tailwind v4 project (`init --tailwind`; runtime contract differs from Studio's v3)                                                                        |\n\nFor animation runtime specifics (GSAP API, Lottie, Three.js, etc.) go to `hyperframes-animation` → `adapters/<runtime>.md`.\n\n## Building a composition\n\n### Two root forms (not interchangeable)\n\n- **Standalone** (top-level `index.html`): root `<div data-composition-id=\"…\">` sits directly in `<body>`, **no `<template>` wrapper**. Wrapping a standalone root hides all content and `lint` rejects it (`standalone_composition_wrapped_in_template`, error).\n- **Sub-composition** (loaded via `data-composition-src`): wrap the root in `<template>`. This is the shape to write: the loader also accepts a plain full document and falls back to its `<body>`, but the templated form is what the examples and tooling assume.\n\n> ⚠ Transport rule: for a **templated** sub-composition the assembler drops the file's own `<head>` `<style>`/`<script>` (`packages/core/src/compiler/compositionAssembly.ts`, the `hasTemplate` gate), so put `<style>`/`<script>` **inside** the template. `<link>` is hoisted either way.\n> ⚠ Host-id convention: give the host slot, the inner template, and the `window.__timelines[\"<id>\"]` key the **same** id. A different local id is supported (the assembler falls back to the first root in the file) but the mismatch is silent, so match them unless you have a reason not to.\n\nFile shape, host wiring, and the pre-render checklist → `references/sub-compositions.md`.\n\n### Root must be sized (silent layout bug)\n\nThe standalone root authors `width`/`height: 100%`. Canvas size is `data-width`/`data-height`. The runtime stamps those pixels onto the composition root. Do not hardcode `1920px`/`1080px` on `#root`. Skeleton → `references/minimal-composition.md`.\n\n### One paused timeline\n\nEach composition registers **exactly one** `gsap.timeline({ paused: true })` at `window.__timelines[\"<id>\"]` (key = root `data-composition-id`). Building it inside an async callback (`document.fonts.ready`) is supported; what matters is that you **register only after the build completes**. Render length is the root's `data-duration`, **not** the timeline's length: a timeline that runs past it is cut off, and one that ends early holds its last frame. Omit the root `data-duration` and the length is inferred instead (timeline, media window, or adapter). You do not need `window.__timelines = window.__timelines || {}`: the runtime creates the registry before your inline scripts run, and `lint` no longer asks for it. Don't manually nest sub-timelines into the host; the runtime auto-nests registered child timelines. Full contract (incl. non-GSAP runtimes) → `references/determinism-rules.md` + `hyperframes-animation/adapters/`.\n\n### First-pass lint gotchas (a guaranteed first build failure)\n\nRules that `lint` **does** catch, but only after the fact. Write them right the first time:\n\n- Never pair a CSS initial `transform` with a GSAP tween on the **same** property — the CSS value and the tween's start fight and `lint` rejects it with `gsap_css_transform_conflict`. Set the initial state inside the tween with `gsap.fromTo(el, { x: -40 }, { x: 0 })` instead of a CSS `transform: translateX(-40px)`.\n- Never put `crossorigin` on `<video>`/`<audio>`. `lint` rejects it unconditionally with `media_crossorigin_breaks_preview` (error), including for canvas/WebGL/WebAudio readback. There is no suppression.\n- Never give a `<video data-start>` an ancestor that also carries `data-start`. `lint` rejects it with `video_nested_in_timed_element` (error). Time the wrapper **or** the video, not both.\n- Every `<audio>` needs an `id`. `lint` rejects it with `media_missing_id`, and an id-less `<audio>` is never picked up by the mixer, so the render is **silent**.\n- Never tween a `.clip` with `autoAlpha` or `visibility` — `lint` rejects it with `gsap_animates_clip_element`. Animate a child instead.\n- A named CSS `font-family` needs an in-file `@font-face` to a shipped local file, or `lint` fires `font_family_without_font_face`.\n- Sub-composition `#root` uses `width`/`height: 100%` (or `inset: 0`), not hardcoded `1920px`/`1080px`. Canvas size is `data-width`/`data-height`.\n\nA lint **error** also switches off the layout and contrast audits: `check` then reports `0 sample(s)` and `0/0 text checks`, which reads like a clean file but means nothing ran. Clear lint errors before you trust those numbers.\n\n### Non-negotiable rules (silent bugs automated gates may miss)\n\nSurfaced here; full rationale in the linked reference. Do not violate:\n\n- No render-time clocks / unseeded `Math.random` / network / input-state; no `repeat: -1` (use a finite count). → `determinism-rules.md`\n- Never tween `display`, `visibility`, or `autoAlpha` on a `.clip` element. The framework owns clip visibility, and `lint` rejects it (`gsap_animates_clip_element`). Animate a child instead. → `determinism-rules.md`\n- No `<br>` in body text; transformed elements must be block-level + sized; pulsing absolute decoratives need peak clearance. → `determinism-rules.md`\n- `<video>`/`<audio>` are found by a flat document query, so the framework seeks and decodes them at **any nesting depth** (including inside a sub-comp `<template>` or wrapper). One hard limit: `lint` errors if a `<video data-start>` sits inside another **plain** element that also has `data-start`, and the failure is real (wrong source frames, then the clip vanishes mid-slot), so put the timing on the wrapper or on the video, never both. Sub-composition hosts are exempt: media inside a sub-composition renders correctly. The other caveat is timelines, not placement: a sub-comp timeline can't animate host-root elements. → `variables-and-media.md`\n- Keep every `id` unique across the **assembled** page (prefix sub-comp ids with the composition id, `#<id>-hero`) so your own `#id` CSS and `getElementById` calls resolve. Frame injection no longer depends on it: the compiler stamps a document-unique `data-hf-render-id` on every `video[src]`/`audio[src]`/`img[src]`. Media that uses `<source>` children instead of a `src` attribute is **not** stamped, so unique ids still matter there. → `composition-patterns.md`\n- A full-screen fill on the composition **root** is fine on a normal render. It is dropped only on the layered-composite path (HDR content, or a composition using shader transitions), where the engine forces every composition root transparent so the layer beneath shows through. If your composition uses shader transitions or HDR media, put the fill on a full-bleed **child** (`position:absolute; inset:0`). → `composition-patterns.md`\n\n## Editing existing compositions\n\n- Read the files first. Preserve unrelated timing, tracks, IDs, variables, media paths.\n- To know what is on a project's timeline (tracks, clips, starts, ends, what plays), run `npx hyperframes timeline [--json]` instead of reading `index.html` and every sub-composition file.\n- Match existing composition IDs and timeline keys.\n- Adding a clip: set its `data-start`/`data-duration` intentionally against the clips around it. `data-track-index` is a Studio display lane, not a timing constraint, so it does not need to be free.\n- A clip that ends past the root `data-duration` is cut off: extend the root `data-duration` to the clip's end in the same edit (`lint` warns `clip_ends_past_root_duration`).\n- `data-hidden` on any composition element hides it in BOTH preview and render, overriding its time window; it is non-destructive/reversible and toggled by Studio's timeline eye icon.\n- Adding a sub-composition: verify its internal `data-composition-id` before wiring the host.\n\n## Validation\n\nUse `hyperframes-cli` for command details\n\n- [ ] `npx hyperframes check` passes (0 findings across lint, runtime, layout, motion, and contrast)\n- [ ] Projects with sub-compositions: `npx hyperframes snapshot --at <midpoints>` and eyeball each frame\n- [ ] `npx hyperframes preview --background` for review (the user can edit anything in Studio's timeline, and the server survives the invoking command)\n- [ ] `npx hyperframes render` only after the user approves\n\nFile v1.0.41:_meta.json\n\n{\n  \"ownerId\": \"kn77d06grj6xqp3dqwkk4bavhn89pegt\",\n  \"slug\": \"hyperframes-core\",\n  \"version\": \"1.0.41\",\n  \"publishedAt\": 1790926205150\n}\n\nFile v1.0.41:references/composition-patterns.md\n\n# Composition Patterns\n\nHow to architect a project: the `index.html` orchestrator at scale, and the common sub-composition archetypes. Pair with `minimal-composition.md` (single-file shape) and `sub-compositions.md` (mechanics of a sub-comp file).\n\n## Two Architectures\n\n|                       | Monolithic (single file)                                | Modular (sub-compositions)                                                           |\n| --------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------ |\n| Project layout        | `index.html` only                                       | `index.html` + `compositions/<scene>.html` per scene                                 |\n| Where scenes live     | Inline `<section class=\"clip\">` siblings under the root | Each scene is a separate file wrapped in `<template>`                                |\n| Timeline registration | One timeline keyed at the root's `data-composition-id`  | Root timeline (often near-empty) + one timeline per sub-comp, each keyed by its `id` |\n| Routing entry         | `references/minimal-composition.md`                     | `references/sub-compositions.md`                                                     |\n\nBoth architectures use the same runtime contract — `data-*` attributes + `window.__timelines[id]`. The choice is structural, not behavioral.\n\n## Modular Orchestrator Pattern\n\nWhen using sub-compositions, `index.html` should be **thin**. Its job is to declare slots, lay them out in time, mount the audio track, and register a (usually empty) root timeline. All scene animation lives inside the sub-comps.\n\n```html\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <script src=\"https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js\"></script>\n    <style>\n      body {\n        margin: 0;\n        background: #000;\n      }\n      #root {\n        position: relative;\n        width: 100%;\n        height: 100%;\n        overflow: hidden;\n      }\n      /* Sub-comp slots stretch to fill the root. */\n      [data-composition-id=\"root\"] > div[data-composition-src] {\n        position: absolute;\n        inset: 0;\n      }\n    </style>\n  </head>\n  <body>\n    <div\n      id=\"root\"\n      data-composition-id=\"root\"\n      data-width=\"1920\"\n      data-height=\"1080\"\n      data-duration=\"30\"\n    >\n      <!-- Sequential scenes — each one a sub-composition slot. -->\n      <div\n        id=\"el-intro\"\n        data-composition-id=\"intro\"\n        data-composition-src=\"compositions/intro.html\"\n        data-start=\"0\"\n        data-duration=\"6\"\n        data-track-index=\"1\"\n      ></div>\n\n      <div\n        id=\"el-body\"\n        data-composition-id=\"body\"\n        data-composition-src=\"compositions/body.html\"\n        data-start=\"6\"\n        data-duration=\"18\"\n        data-track-index=\"1\"\n      ></div>\n\n      <div\n        id=\"el-outro\"\n        data-composition-id=\"outro\"\n        data-composition-src=\"compositions/outro.html\"\n        data-start=\"24\"\n        data-duration=\"6\"\n        data-track-index=\"1\"\n      ></div>\n\n      <!-- Continuous audio at the root — survives scene cuts. -->\n      <audio\n        id=\"el-bgm\"\n        src=\"assets/bgm.mp3\"\n        data-start=\"0\"\n        data-duration=\"30\"\n        data-track-index=\"10\"\n        data-volume=\"0.6\"\n      ></audio>\n    </div>\n\n    <script>\n      window.__timelines[\"root\"] = gsap.timeline({ paused: true });\n    </script>\n  </body>\n</html>\n```\n\nKey properties of this layout:\n\n- **Visual scenes on the same `data-track-index`** (e.g. `1`), authored sequentially. For a cross-fade, overlap their times by the fade duration; giving the incoming scene its own track keeps Studio's timeline readable, but the render accepts an overlap either way.\n- **Audio on a separate, higher track index** (e.g. `10`). Keeps the linter's overlap rules clear of any visual collisions.\n- **Root timeline is near-empty.** All animation lives in the sub-comps. A root-level fade-to-black at the very end is fine; do not stage a parallel animation track from the root.\n- **Host slot ids** use `el-<name>` or `<scene-id>`. The slot's `data-composition-id` must still equal the sub-comp's internal id (see `sub-compositions.md`).\n\n## Sub-Composition Archetypes\n\n### A. Content scene (default)\n\nThe sub-comp contains the scene's full DOM, scoped CSS, and timeline. This is the standard pattern in `sub-compositions.md` — most scenes are this.\n\n### B. Host media + main-timeline driver (one pattern for `<video>`/`<audio>`)\n\n`<video>`/`<audio>` seek and decode at any nesting depth, so a scene-specific clip can live inside its scene's sub-comp with scene-local `data-start` and be driven by that sub-comp's own timeline. Use this host-media pattern instead when you want the media's motion authored on the **main** timeline: put the `<video>`/`<audio>` as a host-root sibling positioned over the scene's frame.\n\nThe reason to reach for it: a sub-comp timeline **cannot** drive host elements (a global selector or `document.querySelector` does not resolve across the boundary). So if the media lives at the host root, author its per-scene motion (scale/opacity/morph/tilt/breathing) on the **main timeline** in `index.html`, at **global time** = scene-local time + the scene slot's `data-start`.\n\n```html\n<!-- index.html (host) -->\n<div\n  id=\"el-final\"\n  data-composition-id=\"final-anim\"\n  data-composition-src=\"compositions/final-anim.html\"\n  data-start=\"20\"\n  data-duration=\"6\"\n  data-track-index=\"1\"\n></div>\n\n<!-- media is a DIRECT root child; sits over the sub-comp's frame -->\n<video\n  id=\"final-video\"\n  class=\"clip\"\n  src=\"assets/final.mp4\"\n  data-start=\"20\"\n  data-duration=\"6\"\n  data-track-index=\"2\"\n  muted\n  playsinline\n  style=\"position:absolute; left:360px; top:100px; width:1200px; height:680px; object-fit:cover; border-radius:24px;\"\n></video>\n\n<script>\n  // MAIN timeline drives the host video. Global time: scene starts at 20.\n  const main = window.__timelines[\"main\"];\n  main.fromTo(\n    \"#final-video\",\n    { scale: 1.4, filter: \"blur(14px)\" },\n    { scale: 1.0, filter: \"blur(0px)\", duration: 0.9, ease: \"power3.out\" },\n    20,\n  ); // = slot data-start (+ any scene-local offset)\n</script>\n\n<!-- compositions/final-anim.html — frame/shell only, no <video>, no host-element animation -->\n<template>\n  <div\n    data-composition-id=\"final-anim\"\n    data-width=\"1920\"\n    data-height=\"1080\"\n    data-duration=\"6\"\n    style=\"position:absolute; inset:0; pointer-events:none;\"\n  >\n    <script>\n      const tl = gsap.timeline({ paused: true });\n      // animate ONLY this sub-comp's own elements here (labels, frame, overlays)\n      window.__timelines[\"final-anim\"] = tl;\n    </script>\n  </div>\n</template>\n```\n\nCaveats:\n\n- In this pattern the media is a host-root child, static in `index.html`, so the main timeline's selector resolves it. (Media nested in a sub-comp is also driven fine; it just can't be reached by the main timeline's selectors — drive it from the sub-comp's own timeline.)\n- Clip lifecycle owns the media element's visibility across its `[data-start, data-start+data-duration]` window. The main-timeline opacity/scale tweens compose with it fine; for an opacity reveal/crossfade prefer a host **wrapper** so you are not fighting the lifecycle on the media element itself.\n- Two media elements sharing the same `src` + `data-start` trigger `duplicate_media_discovery_risk` (benign — both still render).\n\n### C. Multi-scene merge\n\nWhen several beat-level scenes share continuous state — a chat thread that grows, a persistent headline word that carries across the cut, a single canvas with internal phase changes — collapse them into one sub-comp and use **internal phase divs** rather than multiple sub-comp slots.\n\n```html\n<!-- compositions/act2-merged.html -->\n<template>\n  <div data-composition-id=\"act2-merged\" data-width=\"1920\" data-height=\"1080\" data-duration=\"9\">\n    <style>\n      [data-composition-id=\"act2-merged\"] .phase {\n        position: absolute;\n        inset: 0;\n        opacity: 0;\n      }\n    </style>\n    <div class=\"phase\" id=\"phase-a\">…</div>\n    <div class=\"phase\" id=\"phase-b\">…</div>\n    <div class=\"phase\" id=\"phase-c\">…</div>\n    <script>\n      const tl = gsap.timeline({ paused: true });\n      tl.set(\"#phase-a\", { opacity: 1 }, 0);\n      tl.to(\"#phase-a\", { opacity: 0, duration: 0.4 }, 3.0);\n      tl.set(\"#phase-b\", { opacity: 1 }, 3.0);\n      // …\n      window.__timelines[\"act2-merged\"] = tl;\n    </script>\n  </div>\n</template>\n```\n\nReach for this over multiple sequential slots when scenes share DOM, share a canvas, or need to cross-fade with persistent elements (a headline that survives the cut between phases). Each phase is just a div inside the same sub-comp — the parent timeline never has to know about the internal phase boundaries.\n\n### D. Audio at root, reactive visual inside\n\nAudio always lives at the host (`index.html`) as a root-level `<audio>` so playback survives scene cuts. A sub-comp that visualizes audio should read a **pre-baked** frequency curve at init, then sample the baked curve from its timeline — the visual must still be a deterministic function of `tl.time()`, not of `audio.currentTime`. See `determinism-rules.md` and `hyperframes-creative` for the authoring pattern.\n\n## Naming Conventions\n\n| Thing                               | Convention                                  | Example                                    |\n| ----------------------------------- | ------------------------------------------- | ------------------------------------------ |\n| Sub-comp file                       | `compositions/<scene-id>.html`              | `compositions/act0-intro-bell.html`        |\n| Sub-comp `<template>` id (optional) | `<scene-id>-template`                       | `<template id=\"act0-intro-bell-template\">` |\n| Sub-comp root `data-composition-id` | `<scene-id>` (must match host slot)         | `data-composition-id=\"act0-intro-bell\"`    |\n| Timeline registry key               | matches `data-composition-id`               | `window.__timelines[\"act0-intro-bell\"]`    |\n| Host slot `id`                      | `el-<short>` or `<scene-id>`                | `id=\"el-intro\"`, `id=\"act0\"`               |\n| Element ids inside a sub-comp       | prefix with the scene id                    | `#act0-bell`, `#b1-tape`                   |\n| Audio at root                       | `data-track-index` well above visual tracks | `10` while visuals use `1`                 |\n\nThe `-template` suffix on `<template>` is conventional but not required — the runtime extracts contents from whichever `<template>` is in `<body>`, regardless of id. The prefix on inner element ids is the only safeguard against id collisions when multiple sub-comps are mounted into the same host page at once.\n\nFile v1.0.41:references/creator-editing-recipes.md\n\n# Creator Editing Recipes\n\nUse these copyable contracts after `tracks-and-clips.md`. Global math: **consumed source = timeline duration × rate**; **natural timeline duration = remaining source / rate**.\n\nBefore any edit, run `npx hyperframes timeline` (add `--json` for a machine-readable list) to see the project's tracks and clips instead of reading the HTML.\n\nThese recipes keep a video's sound on the `<video>` (`data-has-audio=\"true\"`, no `muted`), so cutting the video cuts its sound. Use a separate `<audio>` only when picture and sound must be cut independently (J/L cuts, replacement audio) or for other sound (music, voiceover) — the recipes that do say so. Silent footage or b-roll: replace `data-has-audio=\"true\"` with `muted` in these blocks.\n\n**Every `<video>` and `<audio>` below carries an `id`, and that is not cosmetic**: `lint` errors with `media_missing_id` on timed media without one, and an id-less `<audio>` is never picked up by the mixer, so the render comes out silent. Keep the ids when you copy a recipe.\n\n## Hard cut\n\n```html\n<video\n  id=\"a\"\n  src=\"take.mp4\"\n  data-start=\"0\"\n  data-duration=\"2\"\n  data-media-start=\"4\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n<video\n  id=\"b\"\n  src=\"take.mp4\"\n  data-start=\"2\"\n  data-duration=\"3\"\n  data-media-start=\"10\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n```\n\nTimeline math: B starts at A start + duration. Source math: each range starts at `data-media-start`; consumed source = timeline duration × rate. Audio follows: the sound moves with each video clip. Owner: `/hyperframes-core`. Limit: adjacent windows only; author the two windows edge to edge. Same-track overlap is valid; both clips paint in CSS order.\n\n## Trim in/out\n\n```html\n<video\n  id=\"shot-1\"\n  src=\"take.mp4\"\n  data-start=\"1\"\n  data-duration=\"3\"\n  data-media-start=\"6\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n```\n\nTimeline math: visible window is `[1,4]`. Source math: in=6, out=6+3 at 1x; never invent source-end syntax. Audio follows: the sound moves with the video clip, using the same three attributes. Owner: `/hyperframes-core`. Limit: use another clip for another range.\n\n## Split / splice\n\n```html\n<video\n  id=\"shot-1\"\n  src=\"take.mp4\"\n  data-start=\"0\"\n  data-duration=\"2\"\n  data-media-start=\"0\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n<video\n  id=\"shot-2\"\n  src=\"take.mp4\"\n  data-start=\"2\"\n  data-duration=\"2\"\n  data-media-start=\"8\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n```\n\nTimeline math: splice at t=2. Source math: independent source offsets select kept pieces. Audio follows: the sound moves with each video clip, so it splits identically. Owner: `/hyperframes-core`. Limit: source cuts are core, never keyframes.\n\n## Duplicate / reuse same source\n\n```html\n<video\n  id=\"shot-1\"\n  src=\"take.mp4\"\n  data-start=\"0\"\n  data-duration=\"1\"\n  data-media-start=\"2\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n<video\n  id=\"shot-2\"\n  src=\"take.mp4\"\n  data-start=\"4\"\n  data-duration=\"1\"\n  data-media-start=\"2\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n```\n\nTimeline math: copies may occupy different starts. Source math: identical offsets reuse identical source. Audio follows: the sound moves with each video clip, so each copy carries its own. Owner: `/hyperframes-core`. Limit: every element needs a unique id when ids are present.\n\n## Reorder\n\n```html\n<video\n  id=\"shot-1\"\n  src=\"take.mp4\"\n  data-start=\"0\"\n  data-duration=\"2\"\n  data-media-start=\"10\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n<video\n  id=\"shot-2\"\n  src=\"take.mp4\"\n  data-start=\"2\"\n  data-duration=\"2\"\n  data-media-start=\"2\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n```\n\nTimeline math: `data-start` defines authored order. Source math: source offsets need not be chronological. Audio follows: the sound moves with the video clip, so reordering clips reorders their sound. Owner: `/hyperframes-core`. Limit: reordering changes placement only, not source ranges.\n\n## Freeze / hold\n\n```html\n<img src=\"held-frame.png\" data-start=\"2\" data-duration=\"1\" data-track-index=\"0\" class=\"clip\" />\n```\n\nTimeline math: the still owns its hold duration. Source math: final-source frame, subcomp final state, and visual pose holds are supported. Audio follows: continue, trim, or silence audio deliberately. Owner: `/hyperframes-core` + `/media-use`. Limit: arbitrary mid-source freeze requires preprocess of a still/segment.\n\n## Constant speed / slow motion\n\n```html\n<video\n  id=\"shot-1\"\n  src=\"take.mp4\"\n  data-start=\"0\"\n  data-duration=\"2\"\n  data-media-start=\"4\"\n  data-playback-rate=\"0.5\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n```\n\nTimeline math: duration is authored timeline time. Source math: consumed source = timeline duration × rate; natural timeline duration = remaining source / rate. Audio follows: the sound moves with the video clip and plays at the same constant rate. Owner: `/hyperframes-core`. Limit: normalized 0.1..10. For a speed ramp put a `rate` lane in `data-automation`, e.g. `{\"version\":1,\"lanes\":[{\"target\":\"rate\",\"points\":[{\"t\":0,\"v\":1},{\"t\":2,\"v\":4}]}]}`; it wins over the constant.\n\n## Zoom / punch\n\n```js\ntl.to(\"#clip .inner\", { scale: 1.35, xPercent: -8, duration: 0.18 }, 1);\n```\n\nTimeline math: tween positions are composition seconds. Source math: unchanged; the core clip still selects source time. Audio follows: unchanged unless separately edited. Owner: `/hyperframes-keyframes`. Limit: target the inner wrapper, not the timed clip element.\n\n## Pan / Ken Burns\n\n```js\ntl.fromTo(\n  \"#clip .inner\",\n  { scale: 1.05, xPercent: 0 },\n  { scale: 1.2, xPercent: -12, duration: 4, ease: \"none\" },\n  0,\n);\n```\n\nTimeline math: move spans four authored seconds. Source math: unchanged. Audio follows: the sound stays on the video clip; the tween does not touch it. Owner: `/hyperframes-keyframes`. Limit: authored geometry, not automatic face tracking.\n\n## Crop / reframe\n\n```js\ntl.to(\"#clip .inner\", { clipPath: \"inset(8% 12% 6% 10%)\", xPercent: -4, duration: 1 }, 2);\n```\n\nTimeline math: crop interpolates over `[2,3]`. Source math: unchanged. Audio follows: no automatic change. Owner: `/hyperframes-keyframes`. Limit: inner wrapper only, not temporal trim.\n\n## Clip-path wipe / reveal / mask / split-screen\n\n```js\ntl.fromTo(\n  \"#next .inner\",\n  { clipPath: \"polygon(0 0,0 0,0 100%,0 100%)\" },\n  { clipPath: \"polygon(0 0,100% 0,100% 100%,0 100%)\", duration: 0.5 },\n  2,\n);\n```\n\nTimeline math: overlap placed clips for the 0.5s handoff. Source math: each clip keeps its own range. Audio follows: the sound stays on each video clip. Owner: `/hyperframes-keyframes` + `/hyperframes-animation`. Limit: visual mask/polygon/split-screen only; source cuts stay `/hyperframes-core`.\n\n## Crossfade\n\n```html\n<div id=\"a-visual\" class=\"inner\">\n  <video\n    id=\"a\"\n    data-start=\"0\"\n    data-duration=\"3\"\n    data-track-index=\"0\"\n    src=\"a.mp4\"\n    data-automation='{\"version\":1,\"lanes\":[{\"target\":\"volume\",\"points\":[{\"t\":0,\"v\":1},{\"t\":2.5,\"v\":1},{\"t\":3,\"v\":0}]}]}'\n    playsinline\n    data-has-audio=\"true\"\n  ></video>\n</div>\n<div id=\"b-visual\" class=\"inner\">\n  <video\n    id=\"b\"\n    data-start=\"2.5\"\n    data-duration=\"3\"\n    data-track-index=\"1\"\n    src=\"b.mp4\"\n    data-automation='{\"version\":1,\"lanes\":[{\"target\":\"volume\",\"points\":[{\"t\":0,\"v\":0},{\"t\":0.5,\"v\":1},{\"t\":3,\"v\":1}]}]}'\n    playsinline\n    data-has-audio=\"true\"\n  ></video>\n</div>\n<script>\n  const tl = gsap.timeline({ paused: true });\n  tl.set(\"#b-visual\", { opacity: 0 }, 0)\n    .to(\"#a-visual\", { opacity: 0, duration: 0.5 }, 2.5)\n    .to(\"#b-visual\", { opacity: 1, duration: 0.5 }, 2.5);\n  window.__timelines[\"main\"] = tl;\n</script>\n```\n\nTimeline math: distinct tracks overlap by 0.5s with opposing opacity envelopes. Source math: each source range remains independent. Audio follows: opposing volume envelopes on each video's own `data-automation`, because the sound stays on the video. Owner: `/hyperframes-core` + `/hyperframes-keyframes` + `/hyperframes-audio`. Limit: the crossfade is the opacity/volume envelopes, not a source-level dissolve.\n\n## Volume fades / ducking\n\n```html\n<audio\n  id=\"music-bed\"\n  src=\"music.wav\"\n  data-start=\"0\"\n  data-duration=\"5\"\n  data-track-index=\"10\"\n  data-automation='{\"version\":1,\"lanes\":[{\"target\":\"volume\",\"points\":[{\"t\":0,\"v\":0},{\"t\":1,\"v\":1},{\"t\":2,\"v\":1},{\"t\":2.2,\"v\":0.3},{\"t\":3,\"v\":0.3},{\"t\":3.2,\"v\":1},{\"t\":4,\"v\":1},{\"t\":5,\"v\":0}]}]}'\n></audio>\n```\n\nTimeline math: lane `t` is clip-local authored time: fade-in 0–1, duck down 2–2.2, hold 2.2–3, duck up 3–3.2, fade-out 4–5. Source math: source selection still uses core attributes. Audio follows: the explicit down-hold-up envelope affects this `<audio>` (music is separate sound). The same lane works on a `<video data-has-audio=\"true\">`. Owner: `/hyperframes-audio`. Limit: automation is not source retiming.\n\n**One rule for volume over time: use the lane.** `lint` accepts a timeline tween on `volume` too, but when a track has both, the lane wins and the tween is ignored (`audio_volume_double_automation`). Never add a lane to a track that already has a `volume` tween, and never add a tween to a track that has a lane; edit the one that exists. To ramp 0.1 to 0.5 over ten seconds, write `{\"t\":0,\"v\":0.1},{\"t\":10,\"v\":0.5}`. `t` is seconds from the clip's own start, so a ramp past `data-duration` never finishes: check the clip's length before choosing the times. `data-volume` stays as the static level of the clip and combines with nothing else you author here.\n\n## Audio alignment\n\n```html\n<video\n  id=\"shot-1\"\n  src=\"take.mp4\"\n  data-start=\"3\"\n  data-duration=\"2\"\n  data-media-start=\"8\"\n  data-playback-rate=\"2\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n```\n\nTimeline math: picture and sound share start/duration because the sound stays on the clip (`data-has-audio=\"true\"`). Source math: both consume four source seconds. Audio follows: identical timing, range, and rate, with nothing to keep in sync. Owner: `/hyperframes-core` + `/hyperframes-audio`. Limit: no waveform auto-sync or drift correction.\n\nA J cut or L cut is the case that needs a separate `<audio>`: picture and sound are cut independently, so the sound gets its own element (the same goes for replacement audio, a voiceover, or music). Mute the video whose sound you are replacing.\n\n```html\n<!-- Outgoing shot: picture runs 0-5, its own sound is a separate clip that ends at the audio cut (4). -->\n<video\n  id=\"shot-1\"\n  src=\"intro.mp4\"\n  data-start=\"0\"\n  data-duration=\"5\"\n  data-track-index=\"0\"\n  muted\n  playsinline\n></video>\n<audio\n  id=\"shot-1-audio\"\n  src=\"intro.mp4\"\n  data-start=\"0\"\n  data-duration=\"4\"\n  data-track-index=\"10\"\n></audio>\n<!-- Incoming shot: picture starts at 5, its sound leads it by one second. -->\n<video\n  id=\"shot-2\"\n  src=\"take.mp4\"\n  data-start=\"5\"\n  data-duration=\"3\"\n  data-media-start=\"12\"\n  data-track-index=\"0\"\n  muted\n  playsinline\n></video>\n<audio\n  id=\"shot-2-audio\"\n  src=\"take.mp4\"\n  data-start=\"4\"\n  data-duration=\"4\"\n  data-media-start=\"11\"\n  data-track-index=\"10\"\n></audio>\n```\n\nThe sound leads the picture by one second (a J cut): `shot-2-audio` starts at 4 and reads from source 11, while the picture starts at 5 and reads from 12. Both stay on the same source clock. Every video in a J or L cut is `muted` and its sound is its own `<audio>`: the outgoing shot's `<audio>` ends at the audio cut (4) while its picture carries on to 5, so two sounds never overlap on the same source. An audible `<video>` and an `<audio>` on the same file are only flagged when their time windows overlap.\n\n## Align a sound to an on-screen event\n\n```html\n<audio\n  id=\"sfx-click-3\"\n  src=\"click.mp3\"\n  data-start=\"7.48\"\n  data-duration=\"0.07\"\n  data-track-index=\"103\"\n  data-volume=\"0.85\"\n></audio>\n```\n\nTimeline math: an audio element in the root composition has `data-start` in absolute root time; audio inside a scene file uses scene-local time and the host's `data-start` is added for you. An event inside a sub-composition happens at the host's `data-start` plus the event's local time in that sub-composition's own timeline, so `data-start = host start + local time`. Move only the audio's `data-start`; leave the picture alone. Source math: if the sound's transient is not at the file's first sample, subtract that lead-in from `data-start` (or trim it with `data-media-start`). Audio follows: nothing links audio to picture, so re-derive after every retime of the host. Owner: `/hyperframes-core`. Limit: no waveform auto-sync; for a beat grid use `hyperframes beats` and place each start on a beat time.\n\n## Copy a group of clips to another time\n\n```html\n<audio\n  id=\"sfx-click-0\"\n  src=\"click.mp3\"\n  data-start=\"1.6\"\n  data-duration=\"0.07\"\n  data-track-index=\"100\"\n></audio>\n<audio\n  id=\"sfx-type-0\"\n  src=\"typenew.mp3\"\n  data-start=\"7.8\"\n  data-duration=\"0.57\"\n  data-track-index=\"109\"\n></audio>\n<audio\n  id=\"sfx-click-0-copy\"\n  src=\"click.mp3\"\n  data-start=\"41.6\"\n  data-duration=\"0.07\"\n  data-track-index=\"186\"\n></audio>\n<audio\n  id=\"sfx-type-0-copy\"\n  src=\"typenew.mp3\"\n  data-start=\"47.8\"\n  data-duration=\"0.57\"\n  data-track-index=\"187\"\n></audio>\n```\n\nTimeline math: pick the clips first and say which ones you picked (by id) if the request does not match the file exactly; then add one `delta` to every member's `data-start`, so relative spacing is preserved (here `delta = 40`). Give each copy a new unique `id` and the next unused `data-track-index`; keep `src`, `data-duration`, `data-media-start`, `data-volume` and any `data-automation` as they are. Leave the originals untouched. Check the copies still end inside the composition's duration. Owner: `/hyperframes-core`. Limit: copies of a `<video>` or a sub-composition host follow the same rule, and a copied sub-composition needs its own host `id`.\n\n## Add media (image, video, audio)\n\nWrite what Studio writes when a person drops a file on the timeline, so an agent-added clip behaves the same as a dropped one; the one difference is that video and audio need no `data-duration`. Studio's source of truth is `DEFAULT_TIMELINE_ASSET_DURATION` in `packages/studio/src/utils/studioHelpers.ts` and `buildTimelineAssetInsertHtml` in `packages/studio/src/utils/timelineAssetDrop.ts`; a test keeps this section equal to them.\n\n- **Image: `data-duration` is optional and defaults to 3 seconds**, the same as a dropped image, because a still has no length of its own. Write it only for another length. A test keeps the 3 equal to the default in code.\n- **Video and audio: `data-start` is enough.** The length comes from the media itself. An authored `data-duration` shorter than the file is a trim, never a requirement; leave it out unless the request asks for a shorter clip.\n- **Start: the playhead or the requested time, never a silent `0`.** Studio's asset-panel Add uses the playhead time on track `0`; a drop uses the drop point.\n- Give every clip `id`, `class=\"clip\"`, `data-start` and `data-track-index`. A video with sound is `playsinline data-has-audio=\"true\"`; silent footage and b-roll is `muted playsinline`. Audio carries `data-volume=\"1\"`.\n- Then make sure the root composition's `data-duration` is at least the clip's end (`data-start` plus its length: 3 for an image unless you set another, the media's length for video and audio): Studio raises a declared root duration to cover the new clip, so an agent must too, or the clip lies past the end and never plays.\n- **Images and video fill the whole frame**: absolutely positioned at `left: 0; top: 0`, `width` and `height` equal to the composition's `data-width` and `data-height`, `object-fit: contain`. Studio does not know a dropped file's natural size, so it does not centre a smaller one.\n- `z-index` is the number of top-level clips already in that file plus one (at least `1`); later clips stack above earlier ones.\n- Several files dropped together share the drop's track and run end to end.\n\n```html\n<img\n  id=\"photo\"\n  class=\"clip\"\n  src=\"assets/photo.png\"\n  data-start=\"4\"\n  data-track-index=\"1\"\n  style=\"position: a\n\nArchive v1.0.40: 13 files, 40677 bytes\n\nFiles: references/composition-patterns.md (10853b), references/creator-editing-recipes.md (18420b), references/data-attributes.md (17370b), references/determinism-rules.md (6659b), references/full-screen-motion.md (2906b), references/minimal-composition.md (2507b), references/sub-compositions.md (8074b), references/tailwind.md (4078b), references/tracks-and-clips.md (7791b), references/variables-and-media.md (8420b), skill-card.md (1871b), SKILL.md (12572b), _meta.json (136b)\n\nArchive v1.0.39: 13 files, 40404 bytes\n\nFiles: references/composition-patterns.md (10853b), references/creator-editing-recipes.md (18420b), references/data-attributes.md (16937b), references/determinism-rules.md (6659b), references/full-screen-motion.md (2906b), references/minimal-composition.md (2507b), references/sub-compositions.md (8074b), references/tailwind.md (4078b), references/tracks-and-clips.md (7184b), references/variables-and-media.md (8420b), skill-card.md (2007b), SKILL.md (12572b), _meta.json (136b)\n\nArchive v1.0.38: 13 files, 39644 bytes\n\nFiles: references/composition-patterns.md (10853b), references/creator-editing-recipes.md (18420b), references/data-attributes.md (15928b), references/determinism-rules.md (6659b), references/full-screen-motion.md (2906b), references/minimal-composition.md (2507b), references/sub-compositions.md (8074b), references/tailwind.md (4078b), references/tracks-and-clips.md (5672b), references/variables-and-media.md (8420b), skill-card.md (2060b), SKILL.md (12572b), _meta.json (136b)\n\nArchive v1.0.37: 13 files, 39005 bytes\n\nFiles: references/composition-patterns.md (10853b), references/creator-editing-recipes.md (17738b), references/data-attributes.md (15874b), references/determinism-rules.md (6659b), references/full-screen-motion.md (2906b), references/minimal-composition.md (2507b), references/sub-compositions.md (8074b), references/tailwind.md (4078b), references/tracks-and-clips.md (5620b), references/variables-and-media.md (8160b), skill-card.md (2038b), SKILL.md (12572b), _meta.json (136b)\n\nArchive v1.0.36: 13 files, 38989 bytes\n\nFiles: references/composition-patterns.md (10853b), references/creator-editing-recipes.md (17738b), references/data-attributes.md (15874b), references/determinism-rules.md (6659b), references/full-screen-motion.md (2906b), references/minimal-composition.md (2507b), references/sub-compositions.md (8074b), references/tailwind.md (4078b), references/tracks-and-clips.md (5620b), references/variables-and-media.md (8160b), skill-card.md (2110b), SKILL.md (12397b), _meta.json (136b)\n\nArchive v1.0.35: 13 files, 39183 bytes\n\nFiles: references/composition-patterns.md (10853b), references/creator-editing-recipes.md (17738b), references/data-attributes.md (15874b), references/determinism-rules.md (6659b), references/full-screen-motion.md (2906b), references/minimal-composition.md (2507b), references/sub-compositions.md (8074b), references/tailwind.md (4078b), references/tracks-and-clips.md (5620b), references/variables-and-media.md (8160b), skill-card.md (2905b), SKILL.md (12153b), _meta.json (136b)\n\nArchive v1.0.34: 13 files, 39024 bytes\n\nFiles: references/composition-patterns.md (10853b), references/creator-editing-recipes.md (17663b), references/data-attributes.md (15874b), references/determinism-rules.md (6659b), references/full-screen-motion.md (2906b), references/minimal-composition.md (2507b), references/sub-compositions.md (8074b), references/tailwind.md (4078b), references/tracks-and-clips.md (5620b), references/variables-and-media.md (8160b), skill-card.md (2587b), SKILL.md (12153b), _meta.json (136b)\n\nArchive v1.0.33: 13 files, 38986 bytes\n\nFiles: references/composition-patterns.md (10853b), references/creator-editing-recipes.md (17663b), references/data-attributes.md (15738b), references/determinism-rules.md (6659b), references/full-screen-motion.md (2906b), references/minimal-composition.md (2507b), references/sub-compositions.md (8074b), references/tailwind.md (4078b), references/tracks-and-clips.md (5620b), references/variables-and-media.md (8160b), skill-card.md (2619b), SKILL.md (12153b), _meta.json (136b)","readmeExcerpt":"Skill: hyperframes-core Owner: heygen-com Summary: The HyperFrames composition contract — build one renderable project. Use for composition structure, the data-* timing attributes, class=\"clip\", tracks, sub-compositions, variables, framework-owned media playback, deterministic-render rules, and validation. Read before writing composition HTML. Tags: latest:1.0.42 Version history: v1.0.42 | 2026-10-02T13:01:36.130Z | ","codeSnippets":[],"executableExamples":[{"language":"html","snippet":"<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <script src=\"https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js\"></script>\n    <style>\n      body {\n        margin: 0;\n        background: #000;\n      }\n      #root {\n        position: relative;\n        width: 100%;\n        height: 100%;\n        overflow: hidden;\n      }\n      /* Sub-comp slots stretch to fill the root. */\n      [data-composition-id=\"root\"] > div[data-composition-src] {\n        position: absolute;\n        inset: 0;\n      }\n    </style>\n  </head>\n  <body>\n    <div\n      id=\"root\"\n      data-composition-id=\"root\"\n      data-width=\"1920\"\n      data-height=\"1080\"\n      data-duration=\"30\"\n    >\n      <!-- Sequential scenes — each one a sub-composition slot. -->\n      <div\n        id=\"el-intro\"\n        data-composition-id=\"intro\"\n        data-composition-src=\"compositions/intro.html\"\n        data-start=\"0\"\n        data-duration=\"6\"\n        data-track-index=\"1\"\n      ></div>\n\n      <div\n        id=\"el-body\"\n        data-composition-id=\"body\"\n        data-composition-src=\"compositions/body.html\"\n        data-start=\"6\"\n        data-duration=\"18\"\n        data-track-index=\"1\"\n      ></div>\n\n      <div\n        id=\"el-outro\"\n        data-composition-id=\"outro\"\n        data-composition-src=\"compositions/outro.html\"\n        data-start=\"24\"\n        data-duration=\"6\"\n        data-track-index=\"1\"\n      ></div>\n\n      <!-- Continuous audio at the root — survives scene cuts. -->\n      <audio\n        id=\"el-bgm\"\n        src=\"assets/bgm.mp3\"\n        data-start=\"0\"\n        data-duration=\"30\"\n        data-track-index=\"10\"\n        data-volume=\"0.6\"\n      ></audio>\n    </div>\n\n    <script>\n      window.__timelines[\"root\"] = gsap.timeline({ paused: true });\n    </script>\n  </body>\n</html>"},{"language":"html","snippet":"<!-- index.html (host) -->\n<div\n  id=\"el-final\"\n  data-composition-id=\"final-anim\"\n  data-composition-src=\"compositions/final-anim.html\"\n  data-start=\"20\"\n  data-duration=\"6\"\n  data-track-index=\"1\"\n></div>\n\n<!-- media is a DIRECT root child; sits over the sub-comp's frame -->\n<video\n  id=\"final-video\"\n  class=\"clip\"\n  src=\"assets/final.mp4\"\n  data-start=\"20\"\n  data-duration=\"6\"\n  data-track-index=\"2\"\n  muted\n  playsinline\n  style=\"position:absolute; left:360px; top:100px; width:1200px; height:680px; object-fit:cover; border-radius:24px;\"\n></video>\n\n<script>\n  // MAIN timeline drives the host video. Global time: scene starts at 20.\n  const main = window.__timelines[\"main\"];\n  main.fromTo(\n    \"#final-video\",\n    { scale: 1.4, filter: \"blur(14px)\" },\n    { scale: 1.0, filter: \"blur(0px)\", duration: 0.9, ease: \"power3.out\" },\n    20,\n  ); // = slot data-start (+ any scene-local offset)\n</script>\n\n<!-- compositions/final-anim.html — frame/shell only, no <video>, no host-element animation -->\n<template>\n  <div\n    data-composition-id=\"final-anim\"\n    data-width=\"1920\"\n    data-height=\"1080\"\n    data-duration=\"6\"\n    style=\"position:absolute; inset:0; pointer-events:none;\"\n  >\n    <script>\n      const tl = gsap.timeline({ paused: true });\n      // animate ONLY this sub-comp's own elements here (labels, frame, overlays)\n      window.__timelines[\"final-anim\"] = tl;\n    </script>\n  </div>\n</template>"},{"language":"html","snippet":"<!-- compositions/act2-merged.html -->\n<template>\n  <div data-composition-id=\"act2-merged\" data-width=\"1920\" data-height=\"1080\" data-duration=\"9\">\n    <style>\n      [data-composition-id=\"act2-merged\"] .phase {\n        position: absolute;\n        inset: 0;\n        opacity: 0;\n      }\n    </style>\n    <div class=\"phase\" id=\"phase-a\">…</div>\n    <div class=\"phase\" id=\"phase-b\">…</div>\n    <div class=\"phase\" id=\"phase-c\">…</div>\n    <script>\n      const tl = gsap.timeline({ paused: true });\n      tl.set(\"#phase-a\", { opacity: 1 }, 0);\n      tl.to(\"#phase-a\", { opacity: 0, duration: 0.4 }, 3.0);\n      tl.set(\"#phase-b\", { opacity: 1 }, 3.0);\n      // …\n      window.__timelines[\"act2-merged\"] = tl;\n    </script>\n  </div>\n</template>"},{"language":"html","snippet":"<video\n  id=\"a\"\n  src=\"take.mp4\"\n  data-start=\"0\"\n  data-duration=\"2\"\n  data-media-start=\"4\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n<video\n  id=\"b\"\n  src=\"take.mp4\"\n  data-start=\"2\"\n  data-duration=\"3\"\n  data-media-start=\"10\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>"},{"language":"html","snippet":"<video\n  id=\"shot-1\"\n  src=\"take.mp4\"\n  data-start=\"1\"\n  data-duration=\"3\"\n  data-media-start=\"6\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>"},{"language":"html","snippet":"<video\n  id=\"shot-1\"\n  src=\"take.mp4\"\n  data-start=\"0\"\n  data-duration=\"2\"\n  data-media-start=\"0\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n<video\n  id=\"shot-2\"\n  src=\"take.mp4\"\n  data-start=\"2\"\n  data-duration=\"2\"\n  data-media-start=\"8\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: hyperframes-core\ndescription: The HyperFrames composition contract — build one renderable project. Use for composition structure, the `data-*` timing attributes, `class=\"clip\"`, tracks, sub-compositions, variables, framework-owned media playback, deterministic-render rules, and validation. Read before writing composition HTML.\n---\n\n**Plugin installs:** Before setup or freshness commands, follow [plugin execution rules](../hyperframes/references/plugin-installation.md) when this skill is inside a HyperFrames plugin. Standalone installs keep the update instructions below.\n\n# HyperFrames Core\n\n**Agent pitfalls (read first):**\n\n- Center with flex/`inset`, not CSS `transform: translate(-50%,-50%)` on a node you then GSAP `x`/`y`. Lint: `gsap_css_transform_conflict`. Use `fromTo` or `xPercent`/`yPercent`.\n- Do not add a scene-exit `tl.set(..., {visibility:\"hidden\"})`. The runtime already hides timed clips. Opacity fades on inner nodes (or `opacity` on `.clip`) are enough. Caption hard-kills are a different rule.\n- `window.__timelines[\"id\"]` must match the root `data-composition-id`.\n- After `render`, read the summary's second line: `beginframe` vs `screenshot`, GPU mode, stage timings. `screenshot` + `software gpu` on Linux is the slow path.\n\nHyperFrames renders video from HTML. A composition is an HTML file whose DOM declares timing with `data-*` attributes, whose animation runtime is seekable, and whose media playback is owned by the framework.\n\nThis skill is the **technical contract** — how to build one hyperframes project. The body below is the build guide; per-topic detail lives in `references/` (index next), read on demand. Process docs (brief, storyboard, review, production, dispatch, frame-worker) live in `/hyperframes` → `references/`. Other concerns live in the sibling domain skills — `hyperframes-animation`, `hyperframes-creative`, `media-use`, `hyperframes-cli`, `hyperframes-registry`. The capability map in `/hyperframes` says what each one covers.\n\n## References\n\n| File                                    | Read it to…                                                                                                                                                         |\n| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `references/minimal-composition.md`     | start from the smallest renderable composition skeleton                                                                                                             |\n| `references/composition-patterns.md`    | choose monolithic vs modular; structure a modular `index.html`; pick a sub-comp archetype                                                                           |\n| `references/data-attributes.md`         | look up any `data-*` (root / clip / sub-comp host / legacy aliases); use `class=\"clip\"`              "},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn77d06grj6xqp3dqwkk4bavhn89pegt\",\n  \"slug\": \"hyperframes-core\",\n  \"version\": \"1.0.42\",\n  \"publishedAt\": 1790946096130\n}"},{"path":"references/composition-patterns.md","content":"# Composition Patterns\n\nHow to architect a project: the `index.html` orchestrator at scale, and the common sub-composition archetypes. Pair with `minimal-composition.md` (single-file shape) and `sub-compositions.md` (mechanics of a sub-comp file).\n\n## Two Architectures\n\n|                       | Monolithic (single file)                                | Modular (sub-compositions)                                                           |\n| --------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------ |\n| Project layout        | `index.html` only                                       | `index.html` + `compositions/<scene>.html` per scene                                 |\n| Where scenes live     | Inline `<section class=\"clip\">` siblings under the root | Each scene is a separate file wrapped in `<template>`                                |\n| Timeline registration | One timeline keyed at the root's `data-composition-id`  | Root timeline (often near-empty) + one timeline per sub-comp, each keyed by its `id` |\n| Routing entry         | `references/minimal-composition.md`                     | `references/sub-compositions.md`                                                     |\n\nBoth architectures use the same runtime contract — `data-*` attributes + `window.__timelines[id]`. The choice is structural, not behavioral.\n\n## Modular Orchestrator Pattern\n\nWhen using sub-compositions, `index.html` should be **thin**. Its job is to declare slots, lay them out in time, mount the audio track, and register a (usually empty) root timeline. All scene animation lives inside the sub-comps.\n\n```html\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <script src=\"https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js\"></script>\n    <style>\n      body {\n        margin: 0;\n        background: #000;\n      }\n      #root {\n        position: relative;\n        width: 100%;\n        height: 100%;\n        overflow: hidden;\n      }\n      /* Sub-comp slots stretch to fill the root. */\n      [data-composition-id=\"root\"] > div[data-composition-src] {\n        position: absolute;\n        inset: 0;\n      }\n    </style>\n  </head>\n  <body>\n    <div\n      id=\"root\"\n      data-composition-id=\"root\"\n      data-width=\"1920\"\n      data-height=\"1080\"\n      data-duration=\"30\"\n    >\n      <!-- Sequential scenes — each one a sub-composition slot. -->\n      <div\n        id=\"el-intro\"\n        data-composition-id=\"intro\"\n        data-composition-src=\"compositions/intro.html\"\n        data-start=\"0\"\n        data-duration=\"6\"\n        data-track-index=\"1\"\n      ></div>\n\n      <div\n        id=\"el-body\"\n        data-composition-id=\"body\"\n        data-composition-src=\"compositions/body.html\"\n        data-start=\"6\"\n        data-duration=\"18\"\n        data-track-index=\"1\"\n      ></div>\n\n      <div\n        id=\"el-outro\"\n        data-composition-id=\"outro\"\n        data-compositio"},{"path":"references/creator-editing-recipes.md","content":"# Creator Editing Recipes\n\nUse these copyable contracts after `tracks-and-clips.md`. Global math: **consumed source = timeline duration × rate**; **natural timeline duration = remaining source / rate**.\n\nBefore any edit, run `npx hyperframes timeline` (add `--json` for a machine-readable list) to see the project's tracks and clips instead of reading the HTML.\n\nThese recipes keep a video's sound on the `<video>` (`data-has-audio=\"true\"`, no `muted`), so cutting the video cuts its sound. Use a separate `<audio>` only when picture and sound must be cut independently (J/L cuts, replacement audio) or for other sound (music, voiceover) — the recipes that do say so. Silent footage or b-roll: replace `data-has-audio=\"true\"` with `muted` in these blocks.\n\n**Every `<video>` and `<audio>` below carries an `id`, and that is not cosmetic**: `lint` errors with `media_missing_id` on timed media without one, and an id-less `<audio>` is never picked up by the mixer, so the render comes out silent. Keep the ids when you copy a recipe.\n\n## Hard cut\n\n```html\n<video\n  id=\"a\"\n  src=\"take.mp4\"\n  data-start=\"0\"\n  data-duration=\"2\"\n  data-media-start=\"4\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n<video\n  id=\"b\"\n  src=\"take.mp4\"\n  data-start=\"2\"\n  data-duration=\"3\"\n  data-media-start=\"10\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n```\n\nTimeline math: B starts at A start + duration. Source math: each range starts at `data-media-start`; consumed source = timeline duration × rate. Audio follows: the sound moves with each video clip. Owner: `/hyperframes-core`. Limit: adjacent windows only; author the two windows edge to edge. Same-track overlap is valid; both clips paint in CSS order.\n\n## Trim in/out\n\n```html\n<video\n  id=\"shot-1\"\n  src=\"take.mp4\"\n  data-start=\"1\"\n  data-duration=\"3\"\n  data-media-start=\"6\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n```\n\nTimeline math: visible window is `[1,4]`. Source math: in=6, out=6+3 at 1x; never invent source-end syntax. Audio follows: the sound moves with the video clip, using the same three attributes. Owner: `/hyperframes-core`. Limit: use another clip for another range.\n\n## Split / splice\n\n```html\n<video\n  id=\"shot-1\"\n  src=\"take.mp4\"\n  data-start=\"0\"\n  data-duration=\"2\"\n  data-media-start=\"0\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n<video\n  id=\"shot-2\"\n  src=\"take.mp4\"\n  data-start=\"2\"\n  data-duration=\"2\"\n  data-media-start=\"8\"\n  data-track-index=\"0\"\n  playsinline\n  data-has-audio=\"true\"\n></video>\n```\n\nTimeline math: splice at t=2. Source math: independent source offsets select kept pieces. Audio follows: the sound moves with each video clip, so it splits identically. Owner: `/hyperframes-core`. Limit: source cuts are core, never keyframes.\n\n## Duplicate / reuse same source\n\n```html\n<video\n  id=\"shot-1\"\n  src=\"take.mp4\"\n  data-start=\"0\"\n  data-duration=\"1\"\n  data-media-start=\"2\"\n  data-track-index=\"0\"\n  playsinline\n  d"},{"path":"references/data-attributes.md","content":"# Data Attributes Reference\n\nEvery HyperFrames composition uses `data-*` attributes to declare timing and structure to the framework. This is the full attribute table — pair with `tracks-and-clips.md` for the rules behind `data-track-index`.\n\n## Composition Root\n\nEvery renderable composition needs one root element:\n\n| Attribute                    | Required      | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |\n| ---------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `data-composition-id`        | Yes           | Unique ID. Must match the animation registry key on `window.__timelines`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |\n| `data-width` / `data-height` | Yes           | Pixel frame size. Common values: `1920x1080`, `1080x1920`, `1080x1080`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              "}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":"The HyperFrames composition contract — build one renderable project. Use for composition structure, the `data-*` timing attributes, `class=\"clip\"`, tracks, sub-compositions, variables, framework-owned media playback, deterministic-render rules, and validation. Read before writing composition HTML. Skill: hyperframes-core Owner: heygen-com Summary: The HyperFrames composition contract — build one renderable project. Use for composition structure, the data-* timing attributes, class=\"clip\", tracks, sub-compositions, variables, framework-owned media playback, deterministic-render rules, and validation. Read before writing composition HTML. Tags: latest:1.0.42 Version history: v1.0.42 | 2026-10-02T13:01:36.130Z |","editorialQuality":{"score":100,"threshold":65,"status":"ready","wordCount":1141,"uniquenessScore":47,"reasons":[]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-09T09:58:47.867Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-09T09:58:47.867Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-09T23:49:08.637Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}