{"id":"49a6ccc8-2df9-41de-9ffb-bd13554f64a8","entityType":"agent","slug":"clawhub-heygen-com-slideshow","name":"slideshow","canonicalUrl":"https://www.xpersona.co/agent/clawhub-heygen-com-slideshow","canonicalPath":"/agent/clawhub-heygen-com-slideshow","generatedAt":"2026-10-10T07:45:12.937Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"editorial-content","verified":true,"confidence":"high","updatedAt":"2026-10-09T21:44:35.069Z","emptyReason":null},"description":"Author a HyperFrames slideshow — a presentation, pitch deck, or interactive deck with discrete slides, fragment reveals, branching, hotspot navigation, and built-in presenter mode with speaker notes; also converts an existing page into a deck. Output is a navigable deck, not a rendered MP4. If the user didn't explicitly ask for a slideshow, confirm before authoring. Unclear → /hyperframes. Skill: slideshow Owner: heygen-com Summary: Author a HyperFrames slideshow — a presentation, pitch deck, or interactive deck with discrete slides, fragment reveals, branching, hotspot navigation, and built-in presenter mode with speaker notes; also converts an existing page into a deck. Output is a navigable deck, not a rendered MP4. If the user didn't explicitly ask for a slideshow, confirm before authoring. Unclear","descriptionLabel":"Technical summary","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 2K downloads reported by the source. Last updated 10/9/2026.","installCommand":"clawhub skill install s17fpgb0p797dzkbtbrxw5x1hh89qs64:slideshow","sourceUrl":"https://clawhub.ai/heygen-com/slideshow","homepage":"https://clawhub.ai/heygen-com/skills/slideshow","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/heygen-com/slideshow","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/heygen-com/skills/slideshow","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":66,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"Author a HyperFrames slideshow — a presentation, pitch deck, or interactive deck with discrete slides, fragment reveals, branching, hotspot navigation, and buil"},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-09T21:44:35.069Z","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-09T21:44:35.069Z","emptyReason":null},"stars":null,"forks":null,"downloads":1966,"packageName":null,"latestVersion":"1.0.16","tractionLabel":"2K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T21:44:35.069Z","emptyReason":null},"lastUpdatedAt":"2026-10-09T21:44:35.069Z","lastCrawledAt":"2026-10-09T21:44:35.069Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-10T21:44:35.069Z","lastVerifiedAt":null,"highlights":[{"version":"1.0.16","createdAt":"2026-10-04T19:33:39.358Z","changelog":"Synced from 0c3e244 (main)","fileCount":4,"zipByteSize":29453},{"version":"1.0.15","createdAt":"2026-10-04T19:20:21.527Z","changelog":"Synced from 173103d (main)","fileCount":4,"zipByteSize":29484},{"version":"1.0.14","createdAt":"2026-09-27T21:23:55.607Z","changelog":"Synced from ff6e210 (main)","fileCount":4,"zipByteSize":29515},{"version":"1.0.13","createdAt":"2026-09-22T11:07:02.798Z","changelog":"Synced from 05d09a7 (main)","fileCount":4,"zipByteSize":29619},{"version":"1.0.12","createdAt":"2026-09-14T01:27:16.406Z","changelog":"Synced from 95bea16 (main)","fileCount":4,"zipByteSize":29874},{"version":"1.0.11","createdAt":"2026-09-10T03:26:30.850Z","changelog":"Synced from 0f8eb89 (main)","fileCount":4,"zipByteSize":29625},{"version":"1.0.10","createdAt":"2026-08-21T03:11:00.035Z","changelog":"Synced from efc2e19 (main)","fileCount":4,"zipByteSize":29420},{"version":"1.0.9","createdAt":"2026-08-19T21:04:26.993Z","changelog":"Synced from 9da422f (main)","fileCount":4,"zipByteSize":29458}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17fpgb0p797dzkbtbrxw5x1hh89qs64:slideshow","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-slideshow/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-slideshow/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-slideshow/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-slideshow/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-slideshow/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-slideshow/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-10T07:45:12.934Z"}},"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-slideshow/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-slideshow/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-slideshow/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-slideshow/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-09T21:44:35.069Z","emptyReason":null},"readme":"Skill: slideshow\n\nOwner: heygen-com\n\nSummary: Author a HyperFrames slideshow — a presentation, pitch deck, or interactive deck with discrete slides, fragment reveals, branching, hotspot navigation, and built-in presenter mode with speaker notes; also converts an existing page into a deck. Output is a navigable deck, not a rendered MP4. If the user didn't explicitly ask for a slideshow, confirm before authoring. Unclear → /hyperframes.\n\nTags: latest:1.0.16\n\nVersion history:\n\nv1.0.16 | 2026-10-04T19:33:39.358Z | user\n\nSynced from 0c3e244 (main)\n\nv1.0.15 | 2026-10-04T19:20:21.527Z | user\n\nSynced from 173103d (main)\n\nv1.0.14 | 2026-09-27T21:23:55.607Z | user\n\nSynced from ff6e210 (main)\n\nv1.0.13 | 2026-09-22T11:07:02.798Z | user\n\nSynced from 05d09a7 (main)\n\nv1.0.12 | 2026-09-14T01:27:16.406Z | user\n\nSynced from 95bea16 (main)\n\nv1.0.11 | 2026-09-10T03:26:30.850Z | user\n\nSynced from 0f8eb89 (main)\n\nv1.0.10 | 2026-08-21T03:11:00.035Z | user\n\nSynced from efc2e19 (main)\n\nv1.0.9 | 2026-08-19T21:04:26.993Z | user\n\nSynced from 9da422f (main)\n\nv1.0.8 | 2026-08-11T18:58:58.233Z | user\n\nSynced from d3a4e90 (main)\n\nv1.0.7 | 2026-07-15T13:25:29.484Z | user\n\nSynced from b9be0b2 (main)\n\nv1.0.6 | 2026-07-10T22:50:35.262Z | user\n\nSynced from 00d059b (main)\n\nv1.0.5 | 2026-07-10T02:57:53.980Z | user\n\nSynced from a8f242e (main)\n\nv1.0.4 | 2026-07-08T18:01:57.481Z | user\n\nSynced from 17b8527 (main)\n\nv1.0.3 | 2026-07-08T17:33:46.071Z | user\n\nSynced from 81884a7 (main)\n\nv1.0.2 | 2026-07-07T19:52:39.661Z | user\n\nSynced from 306a291 (main)\n\nv1.0.1 | 2026-07-02T17:09:16.135Z | user\n\nSynced from a7c3cc7 (main)\n\nv1.0.0 | 2026-07-01T12:06:36.754Z | user\n\nOfficial HyperFrames skills from heygen-com/hyperframes\n\nArchive index:\n\nArchive v1.0.16: 4 files, 29453 bytes\n\nFiles: references/standalone-harness.md (44170b), skill-card.md (1678b), SKILL.md (33915b), _meta.json (129b)\n\nFile v1.0.16:SKILL.md\n\n---\nname: slideshow\ndescription: >\n  Author a HyperFrames slideshow — a presentation, pitch deck, or interactive\n  deck with discrete slides, fragment reveals, branching, hotspot navigation,\n  and built-in presenter mode with speaker notes; also converts an existing\n  page into a deck. Output is a navigable deck, not a rendered MP4. If the\n  user didn't explicitly ask for a slideshow, confirm before authoring.\n  Unclear → /hyperframes.\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> **First, keep this skill fresh — confirm with the user before running:** `npx hyperframes skills update slideshow`. A fast no-op when everything is current; otherwise it refreshes this skill plus the core domain skills it depends on before you rely on them.\n\n> **figma source**: If the deck's content or storyboard comes from a figma.com URL, run `/figma` first — asset export, brand tokens, and storyboard reconstruction if the source is a strip of scene frames — then build from its output. Don't drive Figma via raw MCP tools directly: that skips SVG sanitization, `.media/manifest.jsonl` provenance, and brand-token `var()` binding, so a later brand change can't propagate without a full re-import.\n\n# Slideshow authoring contract\n\nA HyperFrames slideshow is a normal HyperFrames composition — scenes, clips, GSAP timelines — with one extra ingredient: a **JSON island** that declares which scenes are slides and how they connect. The player's `SlideshowController` reads the island and turns the continuous GSAP timeline into a discrete, navigable deck.\n\n**Read `/hyperframes-core` first** for the base composition contract (clips, tracks, `data-*` attributes, determinism rules). This skill covers only what is new: the island schema, slide writing rules, fragments, branching, validation, and the wrapping component.\n\n## Output — a navigable deck, not a linear MP4\n\nA slideshow's output is the **running deck**: serve it with `hyperframes present <project-dir>` (or Studio present mode) — the player's `SlideshowController` reads the island and drives navigation, fragments, branching, and presenter mode. See **Presenting and handoff** below.\n\n**Do not `hyperframes render` a slideshow into a single MP4.** A deck is authored as several top-level scene compositions (one `data-composition-id` per slide) with **no master-root composition** wrapping them, so `render` resolves only the **first** composition and emits a **silently truncated** MP4 (e.g. 6s of a 40-second deck). A linear main-line export (main slides only, branch sequences excluded) is **deferred** — until it ships, the supported outputs are the live `present` deck and per-slide `snapshot` stills. If a user needs a linear MP4 today, surface this limitation rather than pointing `render` at the deck.\n\n## Intent confirmation\n\nIf the user explicitly asks for a slideshow, slide show, or HyperFrames slideshow, proceed with this skill. When the request arrived through `/hyperframes`, the intent layer's triage owns this confirmation — routed here means already confirmed, so don't re-ask; the layer's run-shape questions don't apply (the deliverable is a deck, not a rendered video). A `BRIEF.md`, when present, carries the confirmed intent — read it.\n\nIf the skill triggered from an adjacent request such as \"presentation\", \"pitch deck\", \"deck\", \"interactive deck\", or \"convert this page\", pause before authoring and frame the choice before asking for confirmation. Briefly explain that a HyperFrames slideshow means a runnable deck with discrete slides, built-in navigation and presenter mode, editable speaker notes, shared media handling, and validation before handoff. For source-page conversions, also mention that the goal is to preserve the original page's visual design, interactions, motion, and media behavior while translating page movement into slide-to-slide transitions.\n\nThen ask a short confirmation question:\n\n> Do you want this as a HyperFrames slideshow?\n\nUse a yes/no choice UI when the environment provides one; otherwise ask the question in plain text.\n\nDo not implement the slideshow until the user says yes. If they say no, stop using this skill — read `/hyperframes` and let the intent layer re-route. This confirmation is a **routing decision**, not a preference gate — per `../hyperframes/references/brief-contract.md` § 1 it survives autonomous mode (\"surprise me\" does not skip it): building the wrong deliverable type is a quality failure, not a creative call.\n\n---\n\n## The two pieces\n\n### 1. Scenes — declared the normal way\n\nEvery slide is backed by a scene. Declare scenes with `data-composition-id`, `data-start`, `data-duration`, and `data-label`:\n\n```html\n<div\n  data-composition-id=\"problem\"\n  data-start=\"0\"\n  data-duration=\"8\"\n  data-label=\"The problem\"\n  data-width=\"1920\"\n  data-height=\"1080\"\n>\n  <!-- clips go here -->\n</div>\n```\n\nBranch slides (reachable only via a hotspot, excluded from the main line) are declared exactly the same way — they just appear only in a `slideSequences` entry in the island, not in the main `slides` array.\n\n### 2. The JSON island — one script block per composition\n\nAdd exactly one `<script type=\"application/hyperframes-slideshow+json\">` block to the composition HTML. It holds all slideshow metadata:\n\n```html\n<script type=\"application/hyperframes-slideshow+json\">\n  {\n    \"slides\": [...],\n    \"slideSequences\": [...]\n  }\n</script>\n```\n\nThe island is the single source of truth for slide order, notes, fragment hold-points, hotspots, and branch sequences. Keep it near the top of the `<body>`, before the scene divs, so it is easy to find.\n\nDo not hide the slideshow manifest behind an alternate `<script type=\"application/json\">` block plus runtime code that creates the island. The `present` command reads the composition HTML statically and expects the real `application/hyperframes-slideshow+json` island to already be present.\n\n---\n\n## Schema\n\n### `SlideshowManifest` (the top-level island object)\n\n```json\n{\n  \"slides\": [\n    /* SlideRef[] — the main line, in order */\n  ],\n  \"slideSequences\": [\n    /* SlideSequence[] — off-line branch sequences */\n  ]\n}\n```\n\n### `SlideRef`\n\n```json\n{\n  \"sceneId\": \"problem\",\n  \"notes\": \"Lead with the pain, not the company.\",\n  \"fragments\": [3.5, 5.2, 7.0],\n  \"hotspots\": [\n    /* SlideHotspot[] */\n  ],\n\n  \"ttsScript\": null,\n  \"ttsAudioUrl\": null,\n  \"ttsDurationMs\": null\n}\n```\n\n| Field                                       | Required | Notes                                                                                                                                                   |\n| ------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `sceneId`                                   | yes      | Must match a scene's `data-composition-id` exactly (or provide explicit `startTime`/`endTime`). The lint rule resolves scenes by `data-composition-id`. |\n| `notes`                                     | no       | Presenter-only text. Never shown to the audience.                                                                                                       |\n| `fragments`                                 | no       | Array of times (seconds) within the slide's `[start, end]` range — see Fragments below.                                                                 |\n| `hotspots`                                  | no       | Interactive overlays that trigger a branch — see Branching below.                                                                                       |\n| `startTime`                                 | no       | Optional. Override the matched scene's time bounds; defaults to the scene's start/end.                                                                  |\n| `endTime`                                   | no       | Optional. Override the matched scene's time bounds; defaults to the scene's start/end.                                                                  |\n| `ttsScript`, `ttsAudioUrl`, `ttsDurationMs` | no       | **Reserved.** Schema fields exist but TTS playback is not yet wired. Omit unless you are pre-populating for a future build.                             |\n\n### `SlideHotspot`\n\n```json\n{\n  \"id\": \"h1\",\n  \"label\": \"How did we calculate this?\",\n  \"target\": \"market-deep-dive\",\n  \"region\": { \"x\": 60, \"y\": 10, \"w\": 35, \"h\": 20 }\n}\n```\n\n| Field    | Required | Notes                                                                                                                           |\n| -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- |\n| `id`     | yes      | Unique within the slide.                                                                                                        |\n| `label`  | yes      | Tooltip / button text shown to the audience.                                                                                    |\n| `target` | yes      | Must match a `SlideSequence.id` in `slideSequences`.                                                                            |\n| `region` | no       | Percentage-of-slide bounding box: `{x, y, w, h}` in `0–100`. Omit to render the hotspot as a full-slide labeled button instead. |\n\n### `SlideSequence`\n\n```json\n{\n  \"id\": \"market-deep-dive\",\n  \"label\": \"Market sizing methodology\",\n  \"slides\": [{ \"sceneId\": \"mkt-1\" }, { \"sceneId\": \"mkt-2\" }]\n}\n```\n\n`slides` inside a sequence uses the same `SlideRef` shape as the main line. Fragments and nested hotspots are allowed.\n\n---\n\n## Slide writing rules\n\nThese are hard constraints, not suggestions. A slide that violates them will be outright replaced when a reviewer sees it.\n\n- **Headline is a complete-sentence claim, not a label.** Write \"SMBs spend 14 hours/week on manual scheduling\" not \"Scheduling problem\". The sentence should stand alone if the visual is ignored.\n- **One idea + one visual per slide.** If you are tempted to add a second bullet cluster or a second chart, split the slide.\n- **Lead with the punchline.** The strongest point goes first — on the slide and in the deck order. Investors read left-to-right, top-to-bottom, and they stop.\n- **Bottom-up market sizing only.** Never write \"$50B TAM\" without showing the math. Build from unit economics up: accounts × ACV, or transactions × take-rate.\n- **Font minimum 30pt equivalent.** At 1920×1080, a headline is 72–96px; body copy is 48px. Never go below 40px for any text the audience must read.\n- **Search the live catalog before hand-building any named visual.** For every look, effect, chart, treatment or transition a slide needs — \"CRT scanlines\", \"glitch\", \"bar chart race\", \"shimmer sweep\", \"terminal window\" — run `npx hyperframes catalog --query \"<the visual, in plain English>\" --json` and read the top results before you author the slide's clips. The search needs **nothing installed**: no project, no prior `add`, no account. It ranks the whole hosted registry (~400 blocks and components) from any directory. `npx hyperframes add <name>` drops the block's source into the deck, where you customize it in place. This applies with extra force when porting a source page: a real block beats the simplified approximation the porting rules below forbid.\n\n## Porting source pages\n\nWhen converting an existing page into a slideshow, source fidelity is part of the contract. Do not replace source-specific widgets with simplified approximations unless the user explicitly asks for a redesign.\n\n- Preserve the original page's visual design, motion language, interactive behavior, media behavior, and presentation affordances as closely as practical. When the slideshow system supports presenter mode, include speaker notes using the shared editable-notes behavior rather than a deck-specific implementation.\n- Port mechanical visuals from the source DOM/CSS/JS as exactly as practical: custom players, canvas visualizers, timelines, playheads, stems, expanding circles, hover states, and other interactive details should survive the conversion.\n- Treat native `<video>` / `<audio>` elements as the source of truth for any custom media chrome, canvas visualizer, waveform, beat grid, or playhead. Wire the source's media events (`play`, `pause`, `timeupdate`, `seeking`, `seeked`, `ended`, `ratechange`, `volumechange`) and derive visual state from `media.currentTime`; do not run a separate timer that can drift away from actual playback.\n- Every copied `<video>` or `<audio>` with `src` must have HyperFrames timing attributes before lint: `data-start` and `data-duration`, plus `data-has-audio=\"true\"` when audible native audio should be preserved. Use the scene's time range for slide-specific media; for user-controlled evidence videos that may be played from multiple focused slides, use a deck-wide range. Do not leave `preload=\"none\"` on media; use `metadata` or `auto`.\n- Resolve source font tokens before validation. If preserving custom source fonts, add `@font-face` rules for local/captured font files. If using system fallbacks, replace tokenized declarations such as `font-family: var(--f-body)` with concrete render-safe stacks such as `system-ui, sans-serif` or `ui-monospace, monospace`; do not leave `var(...)` as the font family value.\n- Audit the source for atypical page movement, especially behavior driven by scroll, wheel, touch, hash state, resize, or a requestAnimationFrame loop. Treat fixed viewports with translated/scaled \"world\" layers, parallax, pinned panels, horizontal scrollers, scroll-scrubbed timelines, section snapping, and zoom-to-element cameras as source behavior. Scroll is often the source's transition trigger, so preserve the transition by extracting its progress stops, easing, and camera/focus states, then re-host that motion on slideshow navigation through timeline positions, fragments, or a reusable player/harness hook. Standalone wrappers that jump to slide hold-points still need an explicit navigation-camera transition hook; computing per-slide camera transforms is not enough. Do not simulate a literal page-scroll-down transition inside the slide; the viewer should feel camera travel/zoom from one focal point to another, not see a webpage being scrolled. Keep each slide-to-slide camera move continuous: avoid intermediate route stops that reverse x/y direction or zoom unless the source visibly does that at the same boundary. A transition that darts around before landing is worse than a simpler direct focal move.\n- Preserve the source's media crop semantics. Treat screenshots, tweets/social posts, product UI captures, charts, docs, code, leaderboards, and any image with readable text as content evidence, not decorative media: use the source aspect ratio (`height: auto`) or `object-fit: contain` inside a stable frame. Use `object-fit: cover` only when the source did, or for intentionally decorative/background/cinematic thumbnails. After fitting these captures into a slide, inspect all four edges for truncated text, logos, controls, or captions; a visible crop on meaningful content is a bug unless the source itself cropped it.\n- If a behavior is generic to slideshows, put it in the player/controller or in a reusable skill snippet. Do not solve it with one-off deck scripts.\n- Stacked scene frames must never block interaction on the active slide. Hidden frames need both visual hiding and event gating:\n\n```css\n.scene-frame {\n  opacity: 0;\n  visibility: hidden;\n  pointer-events: none;\n}\n\n.scene-frame.is-active {\n  opacity: 1;\n  visibility: visible;\n  pointer-events: auto;\n}\n```\n\nIf visibility is driven imperatively, set all three properties (`opacity`, `visibility`, and `pointerEvents`) in the visibility controller. `opacity: 0` alone still leaves an invisible layer that can swallow clicks.\n\n---\n\n## Fragments: reveal hold-points within a slide\n\nA fragment is an absolute composition-timeline time (seconds) within a slide's `[start, end]` range where the controller should hold a reveal state.\n\n**How it works:**\n\n1. Player enters a fragmented slide — seeks directly to `fragments[0]` and holds there.\n2. User presses Next (or →) — controller seeks to `fragments[1]` and holds.\n3. After the last fragment, Next advances to the next slide.\n4. A slide without fragments enters at a rest frame inside the slide, usually its midpoint, not exactly at `slide.end`.\n\nFragment times must fall within `[start, end]` (inclusive of both bounds). The lint rule rejects only fragments outside that range (`time < start` or `time > end`).\n\nFragment times are **absolute composition-timeline positions** — the same coordinate space as `data-start` — not offsets relative to the scene's start.\n\nNavigation is seek-driven, not play-driven. The controller never starts playback just to move between fragments; each navigation command is a deterministic seek to the target hold time. Design fragment states so they are correct at the target timeline time.\n\n---\n\n## Branching: hotspots and slide sequences\n\nBranch slides are real scenes in the same composition timeline. They are listed only under `slideSequences` and are excluded from main-line navigation — the player never visits them unless a hotspot fires.\n\n**Navigation model:**\n\n- Clicking a hotspot pushes `{sequenceId, slideIndex: 0}` onto the nav stack and enters the branch's first slide.\n- **back()** pops the stack and returns to the exact parent slide (the one that held the hotspot).\n- **backToMain()** clears the entire stack and returns to the root slide.\n- Breadcrumb renders from the stack: `Main deck › Market sizing methodology › Slide 2`.\n- The slide counter inside a branch is scoped to that sequence (`1 of 2`, not the main-deck total).\n\n**What to avoid:**\n\n- Do not add branch scene IDs to the main `slides` array. They must appear only inside a `slideSequences` entry. The lint rule flags overlap.\n- Branch scenes are included in the continuous timeline, so a naive linear video export would include them. Export reads main-line slides only (deferred; flagged in the spec).\n\n---\n\n## Worked example: 3-slide deck with fragments and a branch\n\n### Scene HTML (skeleton)\n\n```html\n<body style=\"margin: 0\">\n  <script type=\"application/hyperframes-slideshow+json\">\n    {\n      \"slides\": [\n        {\n          \"sceneId\": \"hook\",\n          \"notes\": \"Open with the stat. Pause on the $40B number.\"\n        },\n        {\n          \"sceneId\": \"problem\",\n          \"notes\": \"Walk through each pain point one at a time.\",\n          \"fragments\": [11.0, 15.0],\n          \"hotspots\": [\n            {\n              \"id\": \"h1\",\n              \"label\": \"Where does the $40B figure come from?\",\n              \"target\": \"market-detail\",\n              \"region\": { \"x\": 55, \"y\": 60, \"w\": 40, \"h\": 20 }\n            }\n          ]\n        },\n        {\n          \"sceneId\": \"solution\",\n          \"notes\": \"One sentence: what we do and who it is for.\"\n        }\n      ],\n      \"slideSequences\": [\n        {\n          \"id\": \"market-detail\",\n          \"label\": \"Market sizing methodology\",\n          \"slides\": [{ \"sceneId\": \"mkt-math\", \"notes\": \"Bottom-up: 2.3M SMBs × $17k ACV.\" }]\n        }\n      ]\n    }\n  </script>\n\n  <!-- Slide 1 — hook -->\n  <div\n    data-composition-id=\"hook\"\n    data-start=\"0\"\n    data-duration=\"6\"\n    data-label=\"The hook\"\n    data-width=\"1920\"\n    data-height=\"1080\"\n    style=\"position: relative; width: 1920px; height: 1080px; overflow: hidden; background: #0a0a0a\"\n  >\n    <section\n      class=\"clip\"\n      data-start=\"0\"\n      data-duration=\"6\"\n      data-track-index=\"1\"\n      style=\"position: absolute; inset: 0; display: grid; place-items: center\"\n    >\n      <h1 id=\"hook-headline\" style=\"font-size: 80px; color: #fff; font-family: sans-serif\">\n        SMBs lose $40B/year to manual scheduling\n      </h1>\n    </section>\n  </div>\n\n  <!-- Slide 2 — problem (3 fragments) -->\n  <div\n    data-composition-id=\"problem\"\n    data-start=\"6\"\n    data-duration=\"15\"\n    data-label=\"The problem\"\n    data-width=\"1920\"\n    data-height=\"1080\"\n    style=\"position: relative; width: 1920px; height: 1080px; overflow: hidden; background: #0a0a0a\"\n  >\n    <section\n      class=\"clip\"\n      data-start=\"6\"\n      data-duration=\"15\"\n      data-track-index=\"1\"\n      style=\"position: absolute; inset: 0; padding: 120px 160px; box-sizing: border-box\"\n    >\n      <h2 id=\"pain-headline\" style=\"font-size: 64px; color: #fff; font-family: sans-serif\">\n        Three gaps operators can not close\n      </h2>\n      <p id=\"pain-1\" style=\"font-size: 48px; color: #ccc; opacity: 0; font-family: sans-serif\">\n        No-shows cost 23% of booked revenue\n      </p>\n      <p id=\"pain-2\" style=\"font-size: 48px; color: #ccc; opacity: 0; font-family: sans-serif\">\n        Manual reminders take 4h/week per staff\n      </p>\n      <p id=\"pain-3\" style=\"font-size: 48px; color: #ccc; opacity: 0; font-family: sans-serif\">\n        Rescheduling friction drives 40% churn\n      </p>\n    </section>\n  </div>\n\n  <!-- Slide 3 — solution -->\n  <div\n    data-composition-id=\"solution\"\n    data-start=\"21\"\n    data-duration=\"8\"\n    data-label=\"The solution\"\n    data-width=\"1920\"\n    data-height=\"1080\"\n    style=\"position: relative; width: 1920px; height: 1080px; overflow: hidden; background: #0a0a0a\"\n  >\n    <section\n      class=\"clip\"\n      data-start=\"21\"\n      data-duration=\"8\"\n      data-track-index=\"1\"\n      style=\"position: absolute; inset: 0; display: grid; place-items: center\"\n    >\n      <h2 id=\"solution-headline\" style=\"font-size: 72px; color: #fff; font-family: sans-serif\">\n        Acme automates scheduling for service SMBs — no-shows down 80% in 90 days\n      </h2>\n    </section>\n  </div>\n\n  <!-- Branch slide — excluded from main line -->\n  <div\n    data-composition-id=\"mkt-math\"\n    data-start=\"29\"\n    data-duration=\"7\"\n    data-label=\"Market math\"\n    data-width=\"1920\"\n    data-height=\"1080\"\n    style=\"position: relative; width: 1920px; height: 1080px; overflow: hidden; background: #111\"\n  >\n    <section\n      class=\"clip\"\n      data-start=\"29\"\n      data-duration=\"7\"\n      data-track-index=\"1\"\n      style=\"position: absolute; inset: 0; display: grid; place-items: center\"\n    >\n      <p id=\"mkt-formula\" style=\"font-size: 56px; color: #fff; font-family: sans-serif\">\n        2.3M SMBs × $17k ACV = $39B serviceable market\n      </p>\n    </section>\n  </div>\n\n  <script>\n    window.__timelines = window.__timelines || {};\n\n    // Slide 2 fragment entrance animations\n    gsap.registerPlugin(); // load any plugins before use\n\n    const tl = gsap.timeline({ paused: true });\n    window.__timelines[\"problem\"] = tl;\n\n    // Insert positions are absolute composition-timeline times (same as data-start / fragment values).\n    tl.from(\"#pain-1\", { opacity: 0, y: 20, duration: 0.4 }, 11.0);\n    tl.from(\"#pain-2\", { opacity: 0, y: 20, duration: 0.4 }, 15.0);\n    // pain-3 lands at end of slide\n    tl.from(\"#pain-3\", { opacity: 0, y: 20, duration: 0.4 }, 13.0);\n  </script>\n</body>\n```\n\n### Key points in the example\n\n- The island `sceneId` values (`\"hook\"`, `\"problem\"`, `\"solution\"`, `\"mkt-math\"`) exactly match `data-composition-id` values on scene divs.\n- `mkt-math` appears only in `slideSequences` — it is never in the top-level `slides` array.\n- Fragment times (`11.0`, `15.0`) are within the `problem` scene's `[6, 21]` range (times are absolute composition-timeline positions).\n- The hotspot `region` (`x: 55, y: 60, w: 40, h: 20`) positions the clickable area in the lower-right quadrant of the problem slide.\n- GSAP timelines are registered on `window.__timelines` and are paused — the HyperFrames engine drives playback; do not call `.play()` at construction time.\n\n---\n\n## Wrapping component\n\nWrap the composition in `<hyperframes-slideshow>` around `<hyperframes-player>` in any embedding context:\n\n```html\n<hyperframes-slideshow>\n  <hyperframes-player src=\"deck.html\"></hyperframes-player>\n</hyperframes-slideshow>\n```\n\n`<hyperframes-slideshow>` provides the navigation chrome (Present, Prev / Next, counter, global mute when `sound` is present, fullscreen), keyboard handling (← / →, Space / Backspace, and P for Present), touch swipe, and hotspot overlays.\n\nThe slideshow automatically sets the `interactive` attribute on every inner `<hyperframes-player>` at mount time, so clickable controls, links, native media controls, and custom players inside the composition iframe receive pointer events as expected. (Outside a slideshow wrapper, you must add `interactive` manually on `<hyperframes-player>` — the player defaults to `pointer-events: none` on the iframe so clicks on the player host don't get hijacked into toggling timeline playback.)\n\n**Presenter mode:** use the built-in Present icon button in the slideshow nav capsule, or press P. It calls `window.open('?mode=audience')` for a fullscreen audience tab; the originating tab becomes the presenter view (current slide reduced, next-slide preview, notes, elapsed timer). The two tabs sync via `BroadcastChannel('hf-slideshow:' + location.pathname)`. Do not add a custom wrapper-level Present button; the shared component owns its placement, icon, styling, and audience-mode hiding.\n\n**Presenting over Google Meet / Zoom (screen share):** share the _audience_ surface, keep the presenter view on your own screen.\n\n- **Google Meet (or any in-Chrome share):** Present → in Meet choose **Share screen → A tab** → pick the audience tab → switch back to the presenter tab. Chrome keeps a captured tab rendering while backgrounded, so animations and slide nav stay live. Do **not** share \"A window\" or \"Entire screen\" — a fully covered window stops rendering (frozen slides for viewers), and entire-screen exposes your notes.\n- **Zoom (desktop app):** drag the audience tab out into its own window and share that window. Zoom captures via the OS, so if the audience window becomes _fully_ covered it freezes — use a second monitor, or keep a sliver of the audience window visible behind the presenter view.\n\nPresenter-driven media playback has an autoplay-policy constraint: `BroadcastChannel` can sync intent, time, and state, but it cannot transfer the presenter's user activation to the audience tab. The shared slideshow player mirrors native media events and starts remote audience playback muted first; only fall back to the standalone harness's audience unlock behavior if muted `media.play()` is rejected or if the deck specifically requires audible audience playback. Do not keep applying remote `timeupdate` messages after a rejected play, or the audience will silently seek through the video without playback.\n\nPresenter notes are editable in the presenter view. Edits are stored in `localStorage` per deck and slide, layered over the manifest notes without rewriting the composition file. Do not add one-off note-editing scripts to decks; rely on the shared slideshow player behavior. If a standalone/custom wrapper truly needs to implement this outside the shared player, use the deterministic storage snippet in `references/standalone-harness.md`.\n\n### Media cleanup on slide exit\n\nThe slideshow controller owns slide-exit media cleanup. When navigation changes slide or sequence, it calls `hyperframes-player.stopMedia()` before entering the next slide. That command:\n\n- posts `stop-media` to the iframe runtime, which stops WebAudio and pauses native `<video>` / `<audio>` elements;\n- pauses same-origin iframe media directly as a fallback; and\n- pauses parent-frame proxies adopted from iframe media.\n\nSame-slide fragment navigation does **not** stop media. Global/deck-level parent audio, such as a background track wired through `audio-src`, is not treated as slide media.\n\nDo not add per-slide cleanup scripts for normal media players. Keep slide video/audio as normal media in the composition; use `data-has-audio=\"true\"` only when the player should preserve audible native video audio instead of treating it as silent visual media.\n\nIf the source page has custom controls or visualizations attached to media, those controls must listen to the same native element the slideshow player stops and mutes. A pause caused by slide exit, presenter sync, native controls, custom controls, or the global mute button should all update the visible custom UI through media events, not through parallel state.\n\nWhen implementing direct iframe fallback cleanup, treat iframe media as cross-realm DOM. Do not test iframe nodes with the parent page's `el instanceof HTMLMediaElement`; that returns false in real browsers. Use `el.ownerDocument.defaultView.HTMLMediaElement` (or an equivalent tag/duck-type guard) before setting `muted` or calling `pause()`.\n\n### Global nav mute\n\nWhen `<hyperframes-slideshow sound>` renders the nav mute button, that button is the global mute control for the page. It must mute:\n\n- child `<hyperframes-player>` instances, including same-origin iframe media;\n- top-level page `<audio>` / `<video>` elements; and\n- wrapper-owned SFX/global `Audio` objects via the `hf-sound` event.\n\nDo not add a second mute button inside the composition. If a wrapper script creates `new Audio(...)` objects that are not attached to the DOM, it must listen for `hf-sound` and set `clip.muted = detail.muted` on each object, not merely skip future plays.\n\nThe same cross-realm rule applies here: global mute must reach iframe `<video>` / `<audio>` elements through the child frame's DOM realm. A passing unit test in a single DOM realm is not enough; verify in a browser that the actual iframe media elements report `muted: true` after clicking the nav mute button.\n\n`hyperframes present` serves built bundles from `packages/player/dist`. After changing player or slideshow chrome behavior, run `bun run build` in `packages/player` and restart the present server before testing in a browser.\n\n---\n\n## Running a slideshow standalone (interim)\n\nThe **durable answer** is engine-hosted: `hyperframes preview --slideshow` / studio present mode will host the composition over the real HyperFrames engine, which drives seek-timelines, owns the gesture frame, and reads the island from the composition. That path is coming; prefer it once it ships.\n\nUntil then, standalone demos (a composition opened via the bare player bundle in a browser, without the engine) require workarounds for three gaps: the composition must expose a seekable root timeline, the island must be duplicated into the wrapper, and wrapper-owned SFX/global audio should live in the parent frame. These patterns are documented in:\n\n```\nreferences/standalone-harness.md\n```\n\nDo not treat the patterns there as the blessed model — they exist only to bridge the gap until the engine-hosted path lands.\n\n## Handoff\n\nFor a public or user-facing slideshow project, the root `index.html` should be a runnable slideshow entrypoint. Opening it in a browser should show slideshow navigation and respond to Next/Prev; it should not expose only the raw composition and require the user to know about Studio or an internal wrapper file. If the raw HyperFrames composition must remain separate for CLI compatibility, put it in a subdirectory such as `composition/index.html` and point scripts/commands at that directory.\n\nThe direct-open wrapper must rely on the built-in Present icon button rendered by `<hyperframes-slideshow>`. Do not add a bespoke `#present-btn`, fixed-position button, or wrapper-specific Present styling. The shared component owns the control bar, hides Present in `?mode=audience`, and supports P as a keyboard shortcut.\n\nValidate the direct-open path before handoff. If `file://` browser restrictions break iframe media, local scripts, or same-origin player access, use a self-contained wrapper or make the handoff command start a local server and open the working URL; do not leave `index.html` in a broken or ambiguous state.\n\nFor a completed slideshow deck, the primary user-facing next step is presenter mode, not Studio. Run or provide:\n\n```bash\nnpx hyperframes present <project-dir>\n```\n\nStudio/`preview` is useful for editing a composition, but it is not a clear final destination for a slideshow user. If you create a `package.json` for a slideshow project where the raw composition lives in `composition/`, make the default runnable script start presenter mode:\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"npx hyperframes present ./composition\",\n    \"studio\": \"npx hyperframes preview ./composition --background\"\n  }\n}\n```\n\nAt handoff, include the local presenter URL printed by the command and the minimal instruction: \"Click Present, or press P, to open the audience tab.\" If the user will present over Google Meet or Zoom, also pass on the screen-share guidance from the Presenting section above (share the audience tab in Meet; a dragged-out audience window in Zoom). Keep the server running if the user asked you to start it.\n\n---\n\n## Validation\n\nAfter authoring or editing a slideshow composition, run validation (it reruns lint before opening the browser):\n\n```bash\nnpx hyperframes check\n```\n\nTreat lint errors and validation `StaticGuard` contract messages as blockers even if a command exits successfully. Fix the file and rerun until `check` reports no lint or runtime errors.\n\nThe slideshow lint rule checks:\n\n- Every `slide.sceneId` resolves to an existing scene (by `data-composition-id`).\n- Every `hotspot.target` references a defined `slideSequence` id.\n- Fragment times fall within each slide's `[start, end]` range.\n- No two main-line slides overlap in time.\n\nFix all violations before previewing. A composition that fails lint will not parse correctly in the player.\n\nFile v1.0.16:_meta.json\n\n{\n  \"ownerId\": \"kn77d06grj6xqp3dqwkk4bavhn89pegt\",\n  \"slug\": \"slideshow\",\n  \"version\": \"1.0.16\",\n  \"publishedAt\": 1791142419358\n}\n\nFile v1.0.16:references/standalone-harness.md\n\n# Standalone HyperFrames Slideshow Harness\n\n## 1. Interim framing — why this exists\n\nThese patterns are a **temporary workaround** for standalone demos. The durable solution is engine-hosted: a future `hyperframes preview --slideshow` / studio present mode will host the composition over the real HyperFrames engine, which drives seek-timelines frame-by-frame, owns the gesture frame, and reads the slideshow island directly from the composition. When that path ships, most of what follows collapses.\n\nUntil then, a standalone slideshow opened via the bare player bundle must work around three facts:\n\n1. The composition must expose a seekable `window.__timelines.root` timeline. Anything outside that seek path, such as Three.js loops or imperative entrance effects, must be self-driving.\n2. `<hyperframes-slideshow>` reads the slideshow island from its **own innerHTML** (the wrapper element), not from the composition the player loads. The island must be duplicated into the wrapper.\n3. The composition runs in the player's **iframe**; user keypresses and pointer events land on the **parent page**. Wrapper-owned SFX/global audio should live in the parent, where the activation token is reliable. Normal slide media stays in the composition and is stopped by the slideshow player on slide exit.\n\nDo not treat these as the blessed authoring model. When the engine-hosted path ships, compositions authored the normal way will just work.\n\n**Living reference implementations:**\n\n---\n\n## 2. The parent wrapper (`index.html` for deliverables, `demo.html` in examples)\n\nThe parent page hosts the two dist bundles, wraps the components, duplicates the island, and owns all audio.\n\nFor public or user-facing generated projects, make this wrapper the root `index.html` so opening the project in a browser runs the slideshow. Put the raw HyperFrames composition in a separate path such as `composition/index.html`. In repo examples you may still see this file called `demo.html`; that name is a reference pattern, not the preferred handoff for a standalone deck.\n\n```html\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>My Deck — Slideshow Demo</title>\n\n    <!--\n      Load both bundles from packages/player/dist.\n      The global builds register <hyperframes-player> and <hyperframes-slideshow>\n      as custom elements — no import map needed.\n    -->\n    <script src=\"../../../packages/player/dist/hyperframes-player.global.js\"></script>\n    <script src=\"../../../packages/player/dist/slideshow/hyperframes-slideshow.global.js\"></script>\n\n    <style>\n      *,\n      *::before,\n      *::after {\n        box-sizing: border-box;\n        margin: 0;\n        padding: 0;\n      }\n      html,\n      body {\n        width: 100%;\n        height: 100%;\n        overflow: hidden;\n        background: #111;\n      }\n    </style>\n  </head>\n  <body>\n    <!--\n      tabindex=\"0\" is critical — <hyperframes-slideshow> binds keydown\n      (ArrowLeft/Right, Space, Backspace) to itself. Without tabindex the\n      element cannot receive focus and arrow keys are dead.\n    -->\n    <hyperframes-slideshow\n      tabindex=\"0\"\n      style=\"display: block; position: relative; width: 100vw; height: 100vh\"\n    >\n      <hyperframes-player\n        interactive\n        style=\"position: absolute; inset: 0\"\n        src=\"composition/index.html\"\n      ></hyperframes-player>\n\n      <!--\n        DUPLICATED ISLAND — keep in sync with the island inside index.html.\n        <hyperframes-slideshow> reads from its own innerHTML, not from the\n        composition the player loads. Every time slides/fragments/hotspots/\n        sequences change in index.html, update this copy too.\n      -->\n      <script type=\"application/hyperframes-slideshow+json\">\n        {\n          \"slides\": [\n            { \"sceneId\": \"scene-one\", \"notes\": \"...\" },\n            {\n              \"sceneId\": \"scene-two\",\n              \"notes\": \"...\",\n              \"fragments\": [8.3, 8.6]\n            }\n          ],\n          \"slideSequences\": [\n            {\n              \"id\": \"branch-one\",\n              \"label\": \"Branch label\",\n              \"slides\": [{ \"sceneId\": \"branch-scene\", \"notes\": \"...\" }]\n            }\n          ]\n        }\n      </script>\n    </hyperframes-slideshow>\n    <!-- The built-in slideshow nav capsule renders Present; do not add a wrapper-level button. -->\n\n    <!-- Audio player lives here — see Section 6 -->\n  </body>\n</html>\n```\n\n`interactive` is required for decks with clickable page content or media controls. Without it, iframe pointer events are disabled by the player shell and a click on the composition can be interpreted as a player play/pause toggle instead of a slide interaction.\n\n### Per-slide `autoplay`\n\nAdd `\"autoplay\": true` to a slide in the island to play that slide's first `<video>` from the start when the presenter lands on it. The slideshow still holds — it never auto-advances — so the presenter clicks Next when ready; autoplay only saves a manual play click into the composition.\n\n```json\n{\n  \"sceneId\": \"cold-open\",\n  \"autoplay\": true,\n  \"notes\": \"Promo plays on enter; click Next when it ends.\"\n}\n```\n\nUse `autoplay` when the video **is** the slide's primary content and its natural end is the cue to advance — a cold-open promo, a demo clip you let run and then move on from. Do **not** use it for background/ambient loops or for footage the presenter talks over; those should start on the presenter's own cue (a click on the clip's controls via `interactive`), not automatically. One autoplay clip per slide (the first `<video>` in the scene).\n\n### Presenter media bridge for interactive media\n\nPresenter/audience mode syncs slide position through a deck-scoped `BroadcastChannel`. If the presenter is expected to play, pause, seek, mute, or change rate on media inside the composition, mirror those native media events over the same channel. Keep the media element as the source of truth; do not mirror a custom button's private state.\n\nAudible playback has one extra browser constraint: a `BroadcastChannel` message does not carry the presenter's user activation into the audience window. The audience window should try presenter-driven playback muted first, because browsers usually allow muted autoplay; it may still reject `media.play()` even while it accepts remote `currentTime` updates. If that happens, do not keep chasing presenter `timeupdate` messages; show an audience-side unlock control, store the latest play intent, and retry playback from that intent after the audience window receives a click/key gesture.\n\n```js\n(function () {\n  if (typeof BroadcastChannel === \"undefined\") return;\n\n  var sender =\n    new URLSearchParams(location.search).get(\"mode\") === \"audience\" ? \"audience\" : \"presenter\";\n  var channel = new BroadcastChannel(\"hf-slideshow:\" + location.pathname);\n  var applyingRemote = false;\n  var lastTimeBroadcast = 0;\n  var pendingPlayByKey = {};\n  var blockedPlayByKey = {};\n  var mutedPlaybackByKey = {};\n  var unlockButton = null;\n\n  function frameDocument() {\n    var player = document.querySelector(\"hyperframes-player\");\n    var frame = player && player.iframeElement;\n    try {\n      return frame && frame.contentDocument ? frame.contentDocument : null;\n    } catch {\n      return null;\n    }\n  }\n  function mediaNodes() {\n    var doc = frameDocument();\n    return doc ? Array.from(doc.querySelectorAll(\"video,audio\")) : [];\n  }\n  function mediaKey(el, index) {\n    return el.id ? \"id:\" + el.id : el.tagName.toLowerCase() + \":\" + index;\n  }\n  function findMedia(key) {\n    return mediaNodes().find(function (el, index) {\n      return mediaKey(el, index) === key;\n    });\n  }\n  function syncMediaState(el, msg, allowTimeSync) {\n    if (typeof msg.playbackRate === \"number\") el.playbackRate = msg.playbackRate;\n    if (typeof msg.volume === \"number\") el.volume = Math.max(0, Math.min(1, msg.volume));\n    if (sender === \"audience\" && mutedPlaybackByKey[msg.key]) {\n      el.muted = true;\n    } else if (typeof msg.muted === \"boolean\") {\n      el.muted = msg.muted;\n    }\n    if (\n      allowTimeSync &&\n      typeof msg.currentTime === \"number\" &&\n      Math.abs((el.currentTime || 0) - msg.currentTime) > 0.35\n    ) {\n      el.currentTime = Math.max(0, msg.currentTime);\n    }\n  }\n  function hasBlockedPlay() {\n    return Object.keys(blockedPlayByKey).length > 0;\n  }\n  function hideAudienceUnlockIfClear() {\n    if (hasBlockedPlay() || !unlockButton) return;\n    unlockButton.remove();\n    unlockButton = null;\n  }\n  function showAudienceUnlock() {\n    if (sender !== \"audience\" || unlockButton) return;\n    unlockButton = document.createElement(\"button\");\n    unlockButton.type = \"button\";\n    unlockButton.textContent = \"Enable audience media\";\n    unlockButton.style.cssText =\n      \"position:fixed;left:50%;bottom:96px;transform:translateX(-50%);z-index:100000;border:0;border-radius:999px;padding:12px 18px;background:#fff;color:#111827;box-shadow:0 10px 32px rgba(0,0,0,.28);font:700 14px/1 system-ui,sans-serif;cursor:pointer;\";\n    unlockButton.addEventListener(\"click\", retryBlockedPlays);\n    document.body.appendChild(unlockButton);\n  }\n  function rememberBlockedPlay(msg) {\n    pendingPlayByKey[msg.key] = msg;\n    blockedPlayByKey[msg.key] = true;\n    showAudienceUnlock();\n  }\n  function clearBlockedPlay(key) {\n    delete blockedPlayByKey[key];\n    hideAudienceUnlockIfClear();\n  }\n  function tryPlay(el, msg) {\n    if (sender === \"audience\") mutedPlaybackByKey[msg.key] = true;\n    syncMediaState(el, msg, true);\n    if (sender === \"audience\") el.muted = true;\n    try {\n      var playResult = el.play();\n      if (playResult && typeof playResult.then === \"function\") {\n        playResult\n          .then(function () {\n            clearBlockedPlay(msg.key);\n          })\n          .catch(function () {\n            rememberBlockedPlay(msg);\n          });\n      } else {\n        clearBlockedPlay(msg.key);\n      }\n    } catch (e) {\n      rememberBlockedPlay(msg);\n    }\n  }\n  function retryBlockedPlays() {\n    wireMedia();\n    applyingRemote = true;\n    try {\n      Object.keys(pendingPlayByKey).forEach(function (key) {\n        var msg = pendingPlayByKey[key];\n        var el = findMedia(key);\n        if (el && msg) tryPlay(el, msg);\n      });\n    } finally {\n      setTimeout(function () {\n        applyingRemote = false;\n      }, 300);\n    }\n  }\n  function publish(el, index, action) {\n    if (sender !== \"presenter\") return;\n    if (applyingRemote) return;\n    if (action === \"timeupdate\") {\n      var now = performance.now();\n      if (now - lastTimeBroadcast < 450 && !el.paused) return;\n      lastTimeBroadcast = now;\n    }\n    channel.postMessage({\n      type: \"media\",\n      sender,\n      key: mediaKey(el, index),\n      action,\n      currentTime: el.currentTime || 0,\n      paused: el.paused,\n      ended: el.ended,\n      muted: el.muted,\n      volume: el.volume,\n      playbackRate: el.playbackRate,\n    });\n  }\n  function wireMedia() {\n    mediaNodes().forEach(function (el, index) {\n      if (el.dataset.hfPresenterMediaSync === \"1\") return;\n      el.dataset.hfPresenterMediaSync = \"1\";\n      [\n        \"play\",\n        \"pause\",\n        \"seeking\",\n        \"seeked\",\n        \"ratechange\",\n        \"volumechange\",\n        \"ended\",\n        \"timeupdate\",\n      ].forEach(function (name) {\n        el.addEventListener(name, function () {\n          publish(el, index, name);\n        });\n      });\n    });\n  }\n  channel.addEventListener(\"message\", function (event) {\n    var msg = event.data;\n    if (!msg || msg.type !== \"media\" || msg.sender === sender) return;\n    if (sender === \"audience\" && blockedPlayByKey[msg.key] && msg.action === \"timeupdate\") {\n      pendingPlayByKey[msg.key] = msg;\n      showAudienceUnlock();\n      return;\n    }\n    var el = findMedia(msg.key);\n    if (!el) return;\n    applyingRemote = true;\n    try {\n      if (\n        msg.action === \"play\" ||\n        (sender === \"audience\" && msg.action === \"timeupdate\" && msg.paused === false && el.paused)\n      ) {\n        pendingPlayByKey[msg.key] = msg;\n        tryPlay(el, msg);\n      } else {\n        syncMediaState(el, msg, true);\n      }\n      if (msg.action === \"pause\" || msg.action === \"ended\") {\n        delete pendingPlayByKey[msg.key];\n        delete mutedPlaybackByKey[msg.key];\n        clearBlockedPlay(msg.key);\n        el.pause();\n      }\n    } catch {\n    } finally {\n      setTimeout(function () {\n        applyingRemote = false;\n      }, 300);\n    }\n  });\n  wireMedia();\n  window.addEventListener(\"load\", wireMedia);\n  window.addEventListener(\"keydown\", retryBlockedPlays, true);\n  window.addEventListener(\"pointerdown\", retryBlockedPlays, true);\n  setInterval(wireMedia, 1000);\n})();\n```\n\n### Custom media visualizers\n\nFor waveform, beat-grid, canvas, or timeline players, wire visual state to the native media element. This keeps native controls, custom controls, presenter sync, slide-exit cleanup, and global mute in one event path.\n\n```js\nfunction wireMediaDrivenVisualizer(media, renderFrame, fireCrossedEvents) {\n  var mediaFrame = 0;\n  var lastTime = media.currentTime || 0;\n\n  function update() {\n    var time = media.currentTime || 0;\n    if (Math.abs(time - lastTime) < 1.5 && time >= lastTime) {\n      fireCrossedEvents(lastTime, time);\n    }\n    lastTime = time;\n    renderFrame(time, media);\n  }\n\n  function start() {\n    if (!media.requestVideoFrameCallback || mediaFrame) return;\n    mediaFrame = media.requestVideoFrameCallback(function () {\n      mediaFrame = 0;\n      update();\n      if (!media.paused && !media.ended) start();\n    });\n  }\n\n  media.addEventListener(\"play\", start);\n  media.addEventListener(\"playing\", start);\n  media.addEventListener(\"pause\", update);\n  media.addEventListener(\"ended\", update);\n  media.addEventListener(\"timeupdate\", update);\n  media.addEventListener(\"seeking\", function () {\n    lastTime = media.currentTime || 0;\n    renderFrame(lastTime, media);\n  });\n  media.addEventListener(\"seeked\", update);\n  media.addEventListener(\"ratechange\", update);\n  media.addEventListener(\"volumechange\", update);\n  renderFrame(media.currentTime || 0, media);\n}\n```\n\nDo not use `requestAnimationFrame` inside compositions for media sync; composition lint rejects wall-clock loops. Prefer `HTMLVideoElement.requestVideoFrameCallback()` for smooth video-tied updates and rely on native `timeupdate`/seek events as the fallback.\n\nUse a dedicated wiring marker such as `data-media-sync-wired`. Do not reuse a marker like `data-wired` for both \"timeline DOM already rendered\" and \"media event listeners attached\"; pre-rendered timeline HTML will otherwise skip listener setup.\n\n### Editable presenter notes\n\nThe shared `<hyperframes-slideshow>` presenter already renders speaker notes as an editable textarea and stores edits in `localStorage`. Do not add deck-specific note editors when the shared player is available.\n\nFor interim custom wrappers that cannot use the shared presenter chrome, use this deterministic storage contract exactly so notes migrate cleanly:\n\n```js\nconst NOTES_STORAGE_PREFIX = \"hf-slideshow:presenter-notes:v1:\";\n\nfunction notesDeckKey(slideshowEl) {\n  const explicit = slideshowEl.getAttribute(\"notes-storage-key\");\n  if (explicit && explicit.trim()) return explicit.trim();\n\n  const playerSrc = slideshowEl.querySelector(\"hyperframes-player\")?.getAttribute(\"src\") || \"\";\n  let resolvedPlayerSrc = playerSrc;\n  try {\n    resolvedPlayerSrc = new URL(playerSrc, location.href).href;\n  } catch {}\n\n  return `${location.origin}${location.pathname}|${document.title}|${resolvedPlayerSrc}`;\n}\n\nfunction notesStorageKey(slideshowEl, position, slide) {\n  return `${NOTES_STORAGE_PREFIX}${JSON.stringify([\n    notesDeckKey(slideshowEl),\n    position.sequenceId,\n    position.slideIndex,\n    slide.sceneId || \"\",\n  ])}`;\n}\n\nfunction readPresenterNotes(slideshowEl, position, slide) {\n  const key = notesStorageKey(slideshowEl, position, slide);\n  try {\n    const stored = localStorage.getItem(key);\n    return stored == null ? slide.notes || \"\" : stored;\n  } catch {\n    return slide.notes || \"\";\n  }\n}\n\nfunction wirePresenterNotes(textarea, slideshowEl, position, slide) {\n  const key = notesStorageKey(slideshowEl, position, slide);\n  textarea.value = readPresenterNotes(slideshowEl, position, slide);\n  textarea.addEventListener(\"input\", function () {\n    try {\n      localStorage.setItem(key, textarea.value);\n    } catch {}\n  });\n}\n```\n\nClearing the textarea must save an empty string, not remove the local value, because a presenter may intentionally blank a manifest note for their run. Use `notes-storage-key=\"stable-deck-id\"` on `<hyperframes-slideshow>` when a standalone demo has a stable project id; otherwise the fallback key isolates by page, title, and player `src`.\n\n---\n\n## 3. Playhead-driven scene visibility\n\nWithout the engine, scenes are driven by a `root` GSAP timeline that the composition manages on its own clock. The visibility controller reads `window.__timelines.root.time()` via that timeline's `onUpdate` callback and sets `opacity` accordingly. Only the active scene is visible.\n\nThe key insight: scene backgrounds must be `transparent` (not opaque) if you want a Three.js canvas behind them; the body/html background and scene inline `background` set the visual fill.\n\nFor converted source pages, preserve the original page's visual design, motion language, interactive behavior, media behavior, and presentation affordances as closely as practical. Port source-specific widgets exactly where practical: custom canvas players, waveform/timeline decorations, expanding rings, playheads, hover states, and event wiring are source material, not optional polish. Also audit for atypical page movement: scroll-scrubbed cameras, parallax, pinned sections, horizontal scrollers, section snapping, translated/scaled world layers, or zoom-to-element navigation. Scroll is often the source's transition trigger, so extract the scroll-progress stops, easing, and camera/focus states, then re-host that motion on slideshow navigation through timeline positions, fragments, or reusable harness hooks. Do not recreate the browser's literal page-scroll-down motion inside a slide; translate it into camera travel/zoom from one focus area to the next. If the same mechanical behavior appears across decks, move it into the player or this harness instead of copying a fragile one-off script.\n\n### Navigation camera transitions for converted pages\n\nWhen a source page uses scroll to move a translated/scaled world, slideshow navigation usually seeks directly to each slide's hold frame. That seek bypasses any in-timeline interpolation near the scene boundary, so a deck can compute the right camera positions and still appear to jump. Add an explicit standalone navigation transition for manual slide changes, while keeping normal HyperFrames timeline seeks static and deterministic.\n\nUse this pattern only for direct-open/presenter slideshow UI. Do not depend on CSS transitions for rendered video output; rendered compositions must still be correct when seeking a single frame.\n\n```css\n#world {\n  transform-origin: 0 0;\n  will-change: transform;\n  transition:\n    transform 760ms cubic-bezier(0.22, 1, 0.36, 1),\n    opacity 0.3s ease;\n}\n\n#world.hf-camera-static {\n  transition: opacity 0.3s ease;\n}\n```\n\n```js\nvar currentCamera = null;\nvar currentSlideIndex = null;\n\nfunction cameraTransform(cam) {\n  return \"translate(\" + cam.tx + \"px,\" + cam.ty + \"px) scale(\" + cam.s + \")\";\n}\n\nfunction setWorldCamera(world, cam, animate) {\n  if (!cam) return;\n  if (!animate) world.classList.add(\"hf-camera-static\");\n  world.style.transform = cameraTransform(cam);\n  world.style.opacity = \"1\";\n  if (!animate) {\n    world.getBoundingClientRect();\n    world.classList.remove(\"hf-camera-static\");\n  }\n  currentCamera = cam;\n}\n\nfunction slideIndexAtTime(t, slideDuration, slideCount) {\n  return Math.max(0, Math.min(slideCount - 1, Math.floor(t / slideDuration)));\n}\n\nfunction updateCameraForTime(t, opts) {\n  var nextSlideIndex = slideIndexAtTime(t, SLIDE_DURATION, SLIDES.length);\n  var jumpedBetweenSlides =\n    currentSlideIndex !== null &&\n    nextSlideIndex !== currentSlideIndex &&\n    Math.abs(t - lastTime) > 1.2;\n  var animateCamera = Boolean(\n    window.__hfCameraTransitionsEnabled && jumpedBetweenSlides && !(opts && opts.staticCamera),\n  );\n  var cam = cameraAtTime(t);\n  setWorldCamera(world, cam, animateCamera);\n  currentSlideIndex = nextSlideIndex;\n}\n```\n\nDuring measurement, temporarily remove the transform with `hf-camera-static`, compute all element union rects, then restore `currentCamera` without animation. On initial load, resize, and validation-style seeks, call `updateCameraForTime(t, { staticCamera: true })`. In the standalone wrapper, set `iframe.contentWindow.__hfCameraTransitionsEnabled = true` after the player iframe is available. That keeps the exported composition seekable while letting presenter navigation glide between focal points.\n\nBefore validation, resolve source font variables. HyperFrames lint accepts concrete generic stacks such as `system-ui, sans-serif` and `ui-monospace, monospace`, or real `@font-face` declarations pointing at local font files. It does not accept `font-family: var(--f-body)` / `var(--f-mono)` as a render-safe family.\n\n```html\n<!-- In index.html (composition) -->\n\n<!-- Shared scene CSS — all scenes start hidden -->\n<style>\n  .scene-frame {\n    position: absolute;\n    top: 0;\n    left: 0;\n    width: 1920px;\n    height: 1080px;\n    overflow: hidden;\n    opacity: 0; /* hidden at rest — visibility controller shows the active one */\n    visibility: hidden; /* opacity:0 alone still lets invisible frames block clicks */\n    pointer-events: none; /* inactive scenes must not swallow events */\n  }\n</style>\n\n<!--\n  content-visible-at-rest: mark the first scene's elements with their\n  final non-hidden state so the deck is not blank before the controller\n  fires. The controller calls updateVisibility(0) synchronously on load.\n\n  If using Three.js canvas behind scenes, set background: transparent\n  here and let the 3D canvas + body color supply the fill.\n-->\n<div\n  id=\"scene-cover\"\n  class=\"scene-frame clip\"\n  data-composition-id=\"cover\"\n  data-start=\"0\"\n  data-duration=\"9\"\n  style=\"background: transparent\"\n>\n  <!-- content here -->\n</div>\n\n<div\n  id=\"scene-problem\"\n  class=\"scene-frame clip\"\n  data-composition-id=\"problem\"\n  data-start=\"9\"\n  data-duration=\"9\"\n  style=\"background: transparent\"\n>\n  <!-- content here -->\n</div>\n\n<!-- Root timeline — spans the full composition duration -->\n<script>\n  (function () {\n    window.__timelines = window.__timelines || {};\n    var tl = gsap.timeline({ paused: true });\n    // A single to() for the full duration establishes the seekable range\n    tl.to({}, { duration: 108 }); // replace 108 with your total seconds\n    window.__timelines[\"root\"] = tl;\n  })();\n</script>\n\n<!-- Visibility controller -->\n<script>\n  (function () {\n    var scenes = [\n      { id: \"scene-cover\", start: 0, end: 9 },\n      { id: \"scene-problem\", start: 9, end: 18 },\n      // ... all scenes including branch scenes\n    ];\n\n    var lastActiveId = null;\n\n    function updateVisibility(t) {\n      for (var i = 0; i < scenes.length; i++) {\n        var s = scenes[i];\n        var el = document.getElementById(s.id);\n        if (!el) continue;\n        var active = t >= s.start && t < s.end;\n        el.style.opacity = active ? \"1\" : \"0\";\n        el.style.visibility = active ? \"visible\" : \"hidden\";\n        el.style.pointerEvents = active ? \"auto\" : \"none\";\n\n        if (active && lastActiveId !== s.id) {\n          lastActiveId = s.id;\n          fireEntrance(el); // see Section 4\n        }\n      }\n      // fragment reveals here — see Section 4\n    }\n\n    window.__hfSetTime = updateVisibility;\n\n    // Show first slide immediately — avoids blank on load\n    updateVisibility(0);\n\n    // Hook the root timeline so every seek drives visibility\n    var root = window.__timelines && window.__timelines[\"root\"];\n    if (root) {\n      root.eventCallback(\"onUpdate\", function () {\n        updateVisibility(root.time());\n      });\n    }\n  })();\n</script>\n```\n\n---\n\n## 4. Imperative entrances on slide-activate\n\nThe engine-hosted path drives GSAP seek-timelines frame by frame. Without it, seek-timeline tweens never fire. Instead, fire imperative `gsap.from()` calls each time a scene becomes active — these run on GSAP's own ticker and are independent of any playhead.\n\nFragment reveals use playhead-crossing: the visibility controller checks whether the playhead has passed each fragment's hold-time and fires an animation on the first crossing. Bunch fragment hold-times near the scene start (within the first 300–500 ms of the scene) so successive ArrowRight presses feel like snappy sequential reveals rather than long waits.\n\n```js\n// --- Entrance animations ---\n\nfunction fireEntrance(sceneEl) {\n  // [data-anim] marks elements that should entrance on slide-activate.\n  // Add data-anim to eyebrows, headlines, subheads, and card grids.\n  var animEls = sceneEl.querySelectorAll(\"[data-anim]\");\n  if (!animEls.length) return;\n  gsap.from(animEls, {\n    opacity: 0,\n    y: 28,\n    duration: 0.4,\n    stagger: 0.07,\n    ease: \"power2.out\",\n    overwrite: true, // cancel any in-flight animation on rapid slide changes\n  });\n}\n\n// --- Fragment reveals ---\n\n// Fragment config: times in absolute composition timeline seconds,\n// bunched near the scene start for snappy successive reveals.\nvar fragments = [\n  { time: 9.3, id: \"prob-item1\", revealed: false },\n  { time: 9.6, id: \"prob-item2\", revealed: false },\n];\n\nfunction revealFragment(id) {\n  var el = document.getElementById(id);\n  if (!el) return;\n  gsap.fromTo(\n    el,\n    { opacity: 0, x: -24 },\n    { opacity: 1, x: 0, duration: 0.35, ease: \"power2.out\", overwrite: true },\n  );\n}\n\n// Inside updateVisibility(t):\nfor (var f = 0; f < fragments.length; f++) {\n  if (!fragments[f].revealed && t >= fragments[f].time) {\n    fragments[f].revealed = true;\n    revealFragment(fragments[f].id);\n  }\n}\n\n// On problem scene re-entry, reset all fragment states:\nif (active && lastActiveId !== s.id && s.id === \"scene-problem\") {\n  for (var f = 0; f < fragments.length; f++) {\n    fragments[f].revealed = false;\n    var pEl = document.getElementById(fragments[f].id);\n    if (pEl) gsap.set(pEl, { opacity: 0, clearProps: \"transform\" });\n  }\n}\n```\n\nFragment items start with `opacity: 0` in CSS. The visibility controller reveals them; the entrance driver does not touch them until crossing.\n\n---\n\n## 5. The scenes bootstrap postMessage\n\n`<hyperframes-slideshow>` must know each scene's time range to map a `sceneId` to a playhead position. Without the engine injecting this at runtime, the composition must post it manually after load.\n\nPost the manifest from the composition (index.html), not the parent wrapper:\n\n```js\n// In index.html — post after a brief delay so the parent frame has settled\n(function () {\n  var FPS = 30;\n  var totalSeconds = 108; // match your composition's data-duration\n  var totalFrames = totalSeconds * FPS;\n\n  var scenes = [\n    // EVERY scene — including branch scenes — must appear here.\n    // id must match data-composition-id; start/duration in seconds.\n    { id: \"cover\", start: 0, duration: 9 },\n    { id: \"problem\", start: 9, duration: 9 },\n    { id: \"solution\", start: 18, duration: 9 },\n    // ... all main-line scenes ...\n    // branch scene — listed last, NOT in main slides array in the island\n    { id: \"market-sizing\", start: 99, duration: 9 },\n  ];\n\n  function postTimeline() {\n    parent.postMessage(\n      {\n        source: \"hf-preview\",\n        type: \"timeline\",\n        durationInFrames: totalFrames,\n        scenes: scenes,\n      },\n      \"*\",\n    );\n  }\n\n  // ~300ms delay after load to let the parent settle\n  if (document.readyState === \"complete\") {\n    setTimeout(postTimeline, 300);\n  } else {\n    window.addEventListener(\"load\", function () {\n      setTimeout(postTimeline, 300);\n    });\n  }\n})();\n```\n\nOmitting any scene (including branch scenes) from this manifest means the slideshow component cannot seek to it. Include every scene declared in the HTML, even scenes only reachable via a hotspot.\n\n---\n\n## 6. Audio/SFX — built-in mute control via `<hyperframes-slideshow sound>`\n\nWrapper-owned SFX should live in the parent page. Browsers enforce user-activation for AudioContext and HTMLAudioElement.play() — an iframe without its own activation (i.e., the user never clicked inside it) is often autoplay-blocked. The user's keypress lands on the parent, so the parent is the reliable frame for click/transition sound effects.\n\nNormal slide media should stay in the composition. The slideshow player now stops slide media automatically on slide/sequence changes by calling `hyperframes-player.stopMedia()`, which pauses iframe `<video>` / `<audio>`, runtime WebAudio, and parent proxies adopted from iframe media. Same-slide fragment reveals do not stop media, and global/deck-level parent audio such as `audio-src` is left alone. Do not hand-roll per-slide cleanup scripts for regular video/audio players.\n\nEvery copied `<video>` / `<audio>` with a `src` must be timed for HyperFrames ownership:\n\n```html\n<video\n  src=\"assets/demo.mp4\"\n  controls\n  playsinline\n  preload=\"metadata\"\n  data-start=\"0\"\n  data-duration=\"96\"\n  data-has-audio=\"true\"\n></video>\n```\n\nUse `data-has-audio=\"true\"` only for audible media. Muted autoplay loops can omit it. Do not leave `preload=\"none\"` in converted compositions.\n\nImplementation detail: iframe media elements belong to the iframe's DOM realm. Fallback code in the parent page/player must not use the parent page's `el instanceof HTMLMediaElement` check for iframe nodes; in real browsers that fails and leaves videos audible. Use `el.ownerDocument.defaultView.HTMLMediaElement` or a tag/duck-type guard before setting `muted` or calling `pause()`.\n\n### Mute toggle — built-in chrome control\n\nAdd the `sound` boolean attribute to `<hyperframes-slideshow>` in demo.html. The component renders a speaker/speaker-muted SVG button as the **leftmost item in the nav capsule**, styled identically to the prev/next ghost buttons. No separate mute button in the composition.\n\n```html\n<hyperframes-slideshow tabindex=\"0\" sound style=\"...\"> ... </hyperframes-slideshow>\n```\n\nThe component:\n\n- Tracks `muted` state (default `false`); exposes a `muted` getter\n- Reflects to a `data-hf-muted` attribute on the host when muted\n- Applies mute globally to child `<hyperframes-player>` media and top-level page `<audio>` / `<video>` elements\n- Dispatches `CustomEvent(\"hf-sound\", { detail: { muted }, bubbles: true, composed: true })` on every toggle\n- Browser-checks the actual iframe media state after changes; every composition `<video>` / `<audio>` should report `muted: true` after clicking the nav mute button\n\nWrapper-owned `new Audio(...)` objects are not attached to the DOM, so the parent audio player must mirror the `hf-sound` event onto each clip:\n\n```js\nvar muted = false;\nvar slideshow = document.querySelector(\"hyperframes-slideshow\");\nif (slideshow) {\n  slideshow.addEventListener(\"hf-sound\", function (e) {\n    muted = e.detail && e.detail.muted === true;\n    Object.keys(clips).forEach(function (name) {\n      clips[name].muted = muted;\n    });\n  });\n}\n// In message handler:\nif (muted) return; // skip play\n```\n\nIf `sound` is **not** present on `<hyperframes-slideshow>` (decks without audio), the mute control is hidden — the capsule shows only nav.\n\n### Composition: post cues unconditionally\n\nThe composition posts sfx cues **unconditionally** — it does not track mute state. The parent gates on `muted`:\n\n**In the composition (index.html):**\n\n```js\n// Post an sfx cue at transition points — unconditionally.\n// The parent audio player gates on the slideshow component's mute state.\nfunction playSfx(name) {\n  try {\n    parent.postMessage({ type: \"hf-sfx\", name: name }, \"*\");\n  } catch (e) {}\n}\n\n// Fire at scene transitions:\n//   playSfx(\"advance\")      — moving to the next main-line slide\n//   playSfx(\"back\")         — returning from a branch\n//   playSfx(\"branch-enter\") — entering a branch\n//   playSfx(\"fragment\")     — a fragment item is revealed\n```\n\nDo NOT add a mute button inside the composition. The `#sfx-mute` coral button pattern is removed; the nav capsule in the parent chrome owns mute.\n\n**In the parent (demo.html):**\n\n```html\n<script>\n  (function () {\n    // Audio elements are preloaded here, in the frame that receives user gestures.\n    var clips = {\n      advance: new Audio(\"sfx/advance.mp3\"),\n      fragment: new Audio(\"sfx/fragment.mp3\"),\n      \"branch-enter\": new Audio(\"sfx/branch-enter.mp3\"),\n      back: new Audio(\"sfx/back.mp3\"),\n    };\n    clips.advance.volume = 0.45;\n    clips.fragment.volume = 0.4;\n    clips[\"branch-enter\"].volume = 0.4;\n    clips.back.volume = 0.4;\n    Object.keys(clips).forEach(function (k) {\n      clips[k].preload = \"auto\";\n    });\n\n    // Track mute state from the slideshow component's hf-sound event.\n    var muted = false;\n    var slideshow = document.querySelector(\"hyperframes-slideshow\");\n    if (slideshow) {\n      slideshow.addEventListener(\"hf-sound\", function (e) {\n        muted = e.detail && e.detail.muted === true;\n      });\n    }\n\n    var unlocked = false;\n\n    function unlock() {\n      if (unlocked) return;\n      unlocked = true;\n      // Prime wrapper-owned SFX clips: play muted then immediately pause/reset.\n      // This moves the clip into the \"allowed\" state so later plays are instant.\n      Object.keys(clips).forEach(function (name) {\n        var el = clips[name];\n        var v = el.volume;\n        el.volume = 0;\n        el.play()\n          .then(function () {\n            el.pause();\n            el.currentTime = 0;\n            el.volume = v;\n          })\n          .catch(function () {\n            el.volume = v;\n          });\n      });\n    }\n\n    // Unlock on the first user gesture in the parent frame.\n    window.addEventListener(\"keydown\", unlock, true);\n    window.addEventListener(\"pointerdown\", unlock, true);\n    window.addEventListener(\"click\", unlock, true);\n\n    window.addEventListener(\"message\", function (e) {\n      var d = e.data;\n      if (!d || d.type !== \"hf-sfx\") return;\n      // Gate on mute state — the component owns this.\n      if (muted) return;\n      var el = clips[d.name];\n      if (!el || !unlocked) return;\n      try {\n        el.currentTime = 0;\n        el.play().catch(function () {});\n      } catch (err) {}\n    });\n  })();\n</script>\n```\n\n**Sourcing SFX files:** use the HeyGen MCP `search_audio_sounds` tool with `type=sound_effects` and keywords like \"whoosh\", \"click\", \"transition\". Download the results to a local `sfx/` directory next to `demo.html` and reference them by relative path. Do not fetch SFX at render time — the HyperFrames determinism rule forbids runtime network requests; pre-download and commit them.\n\n---\n\n## 7. Three.js (optional)\n\nAdd a Three.js scene behind the slides for ambient motion. The key rules:\n\n- **Own rAF loop** — do not integrate with the HF seek timeline. Three.js runs its own `requestAnimationFrame` loop independent of playhead position.\n- **One persistent canvas** — create the canvas once; update geometry/materials in-place per scene.\n- **Guard renderer creation** — WebGL may be unavailable (software-GL environments, some CI contexts). Create the renderer inside try/catch once; if it fails, hide the canvas and expose no-op stubs. Do not spam `console.error` — silence it during creation and restore it in `finally`.\n- **Full-bleed, behind content** — fix the canvas at `z-index: 0`, `pointer-events: none`, behind scene frames at `z-index: 1`.\n- **Transparent scene frames** — set scene backgrounds to `transparent` so the 3D canvas shows through. Use a radial-gradient scrim on the text container (not the scene frame itself) to keep type legible while letting 3D show in the margins.\n- **Expose a mood hook** — export `window.__threeApplyMood(sceneKey)` so the visibility controller can switch particle colors, toggle sub-objects, or change the clear color when the active scene changes.\n\n```js\n// In index.html — Three.js setup (module script)\nimport * as THREE from \"https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js\";\n\nvar canvas = document.getElementById(\"three-canvas\");\nvar renderer = null;\nvar _err = console.error;\nconsole.error = function () {}; // silence THREE's multi-line GPU error during init\ntry {\n  renderer = new THREE.WebGLRenderer({ canvas: canvas, alpha: true, antialias: true });\n} catch (e) {\n  // renderer stays null\n} finally {\n  console.error = _err;\n}\n\nif (!renderer) {\n  // Graceful degradation — branded layout is the fallback.\n  canvas.style.display = \"none\";\n  window.__threeApplyMood = function () {};\n  // Do NOT start the rAF loop.\n} else {\n  renderer.setSize(1920, 1080);\n  renderer.setPixelRatio(1);\n  canvas.style.cssText =\n    \"position:fixed;top:0;left:0;width:1920px;height:1080px;z-index:0;pointer-events:none\";\n\n  var scene = new THREE.Scene();\n  var camera = new THREE.PerspectiveCamera(60, 1920 / 1080, 0.1, 1000);\n  camera.position.set(0, 0, 5);\n\n  // --- build your particle system, meshes, etc. here ---\n\n  // Mood config: map sceneId → visual state (colors, sub-object visibility, bg color)\n  var MOODS = {\n    cover: {\n      /* particle color, opacity, bg ... */\n    },\n    problem: {\n      /* ... */\n    },\n    // one entry per scene key\n  };\n\n  window.__threeApplyMood = function (sceneKey) {\n    var m = MOODS[sceneKey] || MOODS[\"cover\"];\n    // update geometry attributes, material opacity, sub-group visibility, etc.\n  };\n  window.__threeApplyMood(\"cover\"); // apply initial state\n\n  // --- own rAF loop ---\n  var lastTime = null;\n  function animate(ts) {\n    requestAnimationFrame(animate);\n    if (lastTime === null) lastTime = ts;\n    var dt = Math.min((ts - lastTime) / 1000, 0.05);\n    lastTime = ts;\n    // update particles, rotate objects, etc.\n    renderer.render(scene, camera);\n  }\n  requestAnimationFrame(animate);\n}\n```\n\n**CSS for transparent scene frames + scrim:**\n\n```css\n/* Three.js canvas — always behind everything */\n#three-canvas {\n  position: fixed;\n  top: 0;\n  left: 0;\n  width: 1920px;\n  height: 1080px;\n  z-index: 0;\n  pointer-events: none;\n}\n\n/* Scene frames are transparent so the 3D canvas shows through */\n.scene-frame {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 1920px;\n  height: 1080px;\n  background: transparent; /* NOT opaque — 3D would be occluded */\n  z-index: 1;\n}\n\n/* Scrim on the TEXT container — not the scene frame.\n   Radial gradient: opaque in the center where text is, transparent at edges\n   so 3D shows in the whitespace margins. */\n.slide-inner.scrim-light {\n  background: radial-gradient(\n    ellipse 75% 80% at 50% 50%,\n    rgba(255, 255, 255, 0.88) 30%,\n    rgba(255, 255, 255, 0.6) 65%,\n    rgba(255, 255, 255, 0) 100%\n  );\n}\n```\n\n---\n\n## 8. Foot-gun checklist\n\n| Failure                                               | Symptom                                                         | One-line fix                                                                                                                                     |\n| ----------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |\n| Island not duplicated in wrapper                      | Slideshow chrome never renders; no slide counter, no prev/next  | Copy the `<script type=\"application/hyperframes-slideshow+json\">` block verbatim into the `<hyperframes-slideshow>` element in demo.html         |\n| Wrapper SFX in the iframe                             | Click/transition sounds silent                                  | Move SFX Audio elements and unlock logic to demo.html; post `{type:'hf-sfx',name}` from index.html                                               |\n| No self-clock in composition                          | All scene frames stacked / wrong slide visible at load          | Add the root GSAP timeline (`window.__timelines[\"root\"]`) and the `onUpdate` visibility controller as shown in Section 3                         |\n| Content opacity:0 with no engine                      | Blank slides — `[data-anim]` elements invisible at rest         | Call `updateVisibility(0)` synchronously after defining the controller so the first slide is shown immediately                                   |\n| Keydown bound to the element without focus            | ArrowLeft/Right dead                                            | Add `tabindex=\"0\"` to `<hyperframes-slideshow>` so it can receive keyboard focus                                                                 |\n| Opaque scene background occluding Three.js canvas     | 3D never visible                                                | Set `background: transparent` on `.scene-frame`; put the visual fill on the text scrim container instead                                         |\n| WebGL renderer creation spams errors in headless envs | Console noise, rAF loop starts anyway                           | Silence `console.error` during `new THREE.WebGLRenderer(...)`, restore in `finally`, guard the rAF start on `renderer !== null`                  |\n| Branch scene missing from postMessage manifest        | Hotspot navigates but slide is blank                            | Include every scene — main line and branch — in the `scenes` array of the `postTimeline()` message                                               |\n| Prominent 3D/content in nav-capsule zone              | Bright element bleeds behind/beside the nav pill                | Keep the bottom-right ~360×140px region clear; add a background-matching gradient overlay on any slide whose 3D mood is bright in that corner    |\n| Custom media visualizer uses its own timer            | Canvas/playhead drifts from the actual video or native controls | Drive visual state from media events and `media.currentTime`; do not use an independent `setTimeout` clock                                       |\n| One `data-wired` flag means two different things      | Pre-rendered timeline HTML skips media listener setup           | Use separate markers such as `data-timeline-rendered` and `data-media-sync-wired`                                                                |\n| Presenter media events are not bridged                | Audience follows slides but not play/pause/seek/mute            | Mirror native media events over `BroadcastChannel(\"hf-slideshow:\" + location.pathname)` in standalone wrappers with interactive media            |\n| Remote play is blocked in the audience window         | Audience media time jumps but video never plays                 | Try muted playback first; if `media.play()` rejects, show an audience unlock button and ignore live `timeupdate` chasing until playback succeeds |\n| Audience muted autoplay publishes back to presenter   | Presenter audio starts, then mutes or cuts out                  | Publish media events only from presenter mode; audience mute is a local browser-autoplay workaround, not shared media state                      |\n| Copied media lacks HyperFrames timing                 | Lint errors on untimed media; preview/render diverge            | Add `data-start`, `data-duration`, and `data-has-audio=\"true\"` when audible; avoid `preload=\"none\"`                                              |\n| Source font CSS variables kept as font-family values  | StaticGuard font-family contract errors                         | Replace with concrete render-safe stacks or add local `@font-face` declarations                                                                  |\n| Converted scroll/camera source jumps between slides   | Per-slide focal points are correct but manual navigation snaps  | Add a standalone navigation-camera transition hook; disable it for measurement, initial load, resize, and render/validation seeks                |\n\nFile v1.0.16:skill-card.md\n\n## Description:\n\nAuthors navigable HyperFrames presentations with slide reveals, branching, hotspots, and presenter notes, including decks converted from existing pages.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[heygen-com](https://clawhub.ai/user/heygen-com)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nCreators, designers, and developers use this skill to build or adapt interactive slide decks with presenter notes and branching navigation.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Live package commands may fetch and run changed code or update installed skills.\n\nMitigation: Review commands before execution and use a lockfile-backed local HyperFrames install or exact-version commands.\n\nRisk: Standalone decks may load optional browser dependencies from a third-party CDN.\n\nMitigation: Vendor optional dependencies such as Three.js when distributing standalone decks.\n\n## Reference(s):\n\n- [Standalone slideshow harness](references/standalone-harness.md)\n\n## Skill Output:\n\n**Output Type(s):** [Code, Configuration instructions, Guidance]\n\n**Output Format:** [HTML and JSON deck files with Markdown guidance]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Produces a navigable presentation and optional per-slide stills, not a single MP4.]\n\n## Skill Version(s):\n\n1.0.16 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.0.15: 4 files, 29484 bytes\n\nFiles: references/standalone-harness.md (44170b), skill-card.md (1804b), SKILL.md (33949b), _meta.json (129b)\n\nFile v1.0.15:SKILL.md\n\n---\nname: slideshow\ndescription: >\n  Author a HyperFrames slideshow — a presentation, pitch deck, or interactive\n  deck with discrete slides, fragment reveals, branching, hotspot navigation,\n  and built-in presenter mode with speaker notes; also converts an existing\n  page into a deck. Output is a navigable deck, not a rendered MP4. If the\n  user didn't explicitly ask for a slideshow, confirm before authoring.\n  Unclear → /hyperframes.\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> **First, keep this skill fresh — confirm with the user before running:** `npx hyperframes skills update slideshow`. A fast no-op when everything is current; otherwise it refreshes this skill plus the core domain skills it depends on before you rely on them.\n\n> **figma source**: If the deck's content or storyboard comes from a figma.com URL, run `/figma` first — asset export, brand tokens, and storyboard reconstruction if the source is a strip of scene frames — then build from its output. Don't drive Figma via raw MCP tools directly: that skips SVG sanitization, `.media/manifest.jsonl` provenance, and brand-token `var()` binding, so a later brand change can't propagate without a full re-import.\n\n# Slideshow authoring contract\n\nA HyperFrames slideshow is a normal HyperFrames composition — scenes, clips, GSAP timelines — with one extra ingredient: a **JSON island** that declares which scenes are slides and how they connect. The player's `SlideshowController` reads the island and turns the continuous GSAP timeline into a discrete, navigable deck.\n\n**Read `/hyperframes-core` first** for the base composition contract (clips, tracks, `data-*` attributes, determinism rules). This skill covers only what is new: the island schema, slide writing rules, fragments, branching, validation, and the wrapping component.\n\n## Output — a navigable deck, not a linear MP4\n\nA slideshow's output is the **running deck**: serve it with `hyperframes present <project-dir>` (or Studio present mode) — the player's `SlideshowController` reads the island and drives navigation, fragments, branching, and presenter mode. See **Presenting and handoff** below.\n\n**Do not `hyperframes render` a slideshow into a single MP4.** A deck is authored as several top-level scene compositions (one `data-composition-id` per slide) with **no master-root composition** wrapping them, so `render` resolves only the **first** composition and emits a **silently truncated** MP4 (e.g. 6s of a 40-second deck). A linear main-line export (main slides only, branch sequences excluded) is **deferred** — until it ships, the supported outputs are the live `present` deck and per-slide `snapshot` stills. If a user needs a linear MP4 today, surface this limitation rather than pointing `render` at the deck.\n\n## Intent confirmation\n\nIf the user explicitly asks for a slideshow, slide show, or HyperFrames slideshow, proceed with this skill. When the request arrived through `/hyperframes`, the intent layer's triage owns this confirmation — routed here means already confirmed, so don't re-ask; the layer's run-shape questions don't apply (the deliverable is a deck, not a rendered video). A `BRIEF.md`, when present, carries the confirmed intent — read it.\n\nIf the skill triggered from an adjacent request such as \"presentation\", \"pitch deck\", \"deck\", \"interactive deck\", or \"convert this page\", pause before authoring and frame the choice before asking for confirmation. Briefly explain that a HyperFrames slideshow means a runnable deck with discrete slides, built-in navigation and presenter mode, editable speaker notes, shared media handling, and validation before handoff. For source-page conversions, also mention that the goal is to preserve the original page's visual design, interactions, motion, and media behavior while translating page movement into slide-to-slide transitions.\n\nThen ask a short confirmation question:\n\n> Do you want this as a HyperFrames slideshow?\n\nUse a yes/no choice UI when the environment provides one; otherwise ask the question in plain text.\n\nDo not implement the slideshow until the user says yes. If they say no, stop using this skill — read `/hyperframes` and let the intent layer re-route. This confirmation is a **routing decision**, not a preference gate — per `../hyperframes/references/brief-contract.md` § 1 it survives autonomous mode (\"surprise me\" does not skip it): building the wrong deliverable type is a quality failure, not a creative call.\n\n---\n\n## The two pieces\n\n### 1. Scenes — declared the normal way\n\nEvery slide is backed by a scene. Declare scenes with `data-composition-id`, `data-start`, `data-duration`, and `data-label`:\n\n```html\n<div\n  data-composition-id=\"problem\"\n  data-start=\"0\"\n  data-duration=\"8\"\n  data-label=\"The problem\"\n  data-width=\"1920\"\n  data-height=\"1080\"\n>\n  <!-- clips go here -->\n</div>\n```\n\nBranch slides (reachable only via a hotspot, excluded from the main line) are declared exactly the same way — they just appear only in a `slideSequences` entry in the island, not in the main `slides` array.\n\n### 2. The JSON island — one script block per composition\n\nAdd exactly one `<script type=\"application/hyperframes-slideshow+json\">` block to the composition HTML. It holds all slideshow metadata:\n\n```html\n<script type=\"application/hyperframes-slideshow+json\">\n  {\n    \"slides\": [...],\n    \"slideSequences\": [...]\n  }\n</script>\n```\n\nThe island is the single source of truth for slide order, notes, fragment hold-points, hotspots, and branch sequences. Keep it near the top of the `<body>`, before the scene divs, so it is easy to find.\n\nDo not hide the slideshow manifest behind an alternate `<script type=\"application/json\">` block plus runtime code that creates the island. The `present` command reads the composition HTML statically and expects the real `application/hyperframes-slideshow+json` island to already be present.\n\n---\n\n## Schema\n\n### `SlideshowManifest` (the top-level island object)\n\n```json\n{\n  \"slides\": [\n    /* SlideRef[] — the main line, in order */\n  ],\n  \"slideSequences\": [\n    /* SlideSequence[] — off-line branch sequences */\n  ]\n}\n```\n\n### `SlideRef`\n\n```json\n{\n  \"sceneId\": \"problem\",\n  \"notes\": \"Lead with the pain, not the company.\",\n  \"fragments\": [3.5, 5.2, 7.0],\n  \"hotspots\": [\n    /* SlideHotspot[] */\n  ],\n\n  \"ttsScript\": null,\n  \"ttsAudioUrl\": null,\n  \"ttsDurationMs\": null\n}\n```\n\n| Field                                       | Required | Notes                                                                                                                                                   |\n| ------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `sceneId`                                   | yes      | Must match a scene's `data-composition-id` exactly (or provide explicit `startTime`/`endTime`). The lint rule resolves scenes by `data-composition-id`. |\n| `notes`                                     | no       | Presenter-only text. Never shown to the audience.                                                                                                       |\n| `fragments`                                 | no       | Array of times (seconds) within the slide's `[start, end]` range — see Fragments below.                                                                 |\n| `hotspots`                                  | no       | Interactive overlays that trigger a branch — see Branching below.                                                                                       |\n| `startTime`                                 | no       | Optional. Override the matched scene's time bounds; defaults to the scene's start/end.                                                                  |\n| `endTime`                                   | no       | Optional. Override the matched scene's time bounds; defaults to the scene's start/end.                                                                  |\n| `ttsScript`, `ttsAudioUrl`, `ttsDurationMs` | no       | **Reserved.** Schema fields exist but TTS playback is not yet wired. Omit unless you are pre-populating for a future build.                             |\n\n### `SlideHotspot`\n\n```json\n{\n  \"id\": \"h1\",\n  \"label\": \"How did we calculate this?\",\n  \"target\": \"market-deep-dive\",\n  \"region\": { \"x\": 60, \"y\": 10, \"w\": 35, \"h\": 20 }\n}\n```\n\n| Field    | Required | Notes                                                                                                                           |\n| -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- |\n| `id`     | yes      | Unique within the slide.                                                                                                        |\n| `label`  | yes      | Tooltip / button text shown to the audience.                                                                                    |\n| `target` | yes      | Must match a `SlideSequence.id` in `slideSequences`.                                                                            |\n| `region` | no       | Percentage-of-slide bounding box: `{x, y, w, h}` in `0–100`. Omit to render the hotspot as a full-slide labeled button instead. |\n\n### `SlideSequence`\n\n```json\n{\n  \"id\": \"market-deep-dive\",\n  \"label\": \"Market sizing methodology\",\n  \"slides\": [{ \"sceneId\": \"mkt-1\" }, { \"sceneId\": \"mkt-2\" }]\n}\n```\n\n`slides` inside a sequence uses the same `SlideRef` shape as the main line. Fragments and nested hotspots are allowed.\n\n---\n\n## Slide writing rules\n\nThese are hard constraints, not suggestions. A slide that violates them will be outright replaced when a reviewer sees it.\n\n- **Headline is a complete-sentence claim, not a label.** Write \"SMBs spend 14 hours/week on manual scheduling\" not \"Scheduling problem\". The sentence should stand alone if the visual is ignored.\n- **One idea + one visual per slide.** If you are tempted to add a second bullet cluster or a second chart, split the slide.\n- **Lead with the punchline.** The strongest point goes first — on the slide and in the deck order. Investors read left-to-right, top-to-bottom, and they stop.\n- **Bottom-up market sizing only.** Never write \"$50B TAM\" without showing the math. Build from unit economics up: accounts × ACV, or transactions × take-rate.\n- **Font minimum 30pt equivalent.** At 1920×1080, a headline is 72–96px; body copy is 48px. Never go below 40px for any text the audience must read.\n- **Search the live catalog before hand-building any named visual.** For every look, effect, chart, treatment or transition a slide needs — \"CRT scanlines\", \"glitch\", \"bar chart race\", \"shimmer sweep\", \"terminal window\" — run `npx hyperframes catalog --query \"<the visual, in plain English>\" --json` and read the top results before you author the slide's clips. The search needs **nothing installed**: no project, no prior `add`, no account. It ranks the whole hosted registry (~400 blocks and components) from any directory. `npx hyperframes add <name>` drops the block's source into the deck, where you customize it in place. This applies with extra force when porting a source page: a real block beats the simplified approximation the porting rules below forbid.\n\n## Porting source pages\n\nWhen converting an existing page into a slideshow, source fidelity is part of the contract. Do not replace source-specific widgets with simplified approximations unless the user explicitly asks for a redesign.\n\n- Preserve the original page's visual design, motion language, interactive behavior, media behavior, and presentation affordances as closely as practical. When the slideshow system supports presenter mode, include speaker notes using the shared editable-notes behavior rather than a deck-specific implementation.\n- Port mechanical visuals from the source DOM/CSS/JS as exactly as practical: custom players, canvas visualizers, timelines, playheads, stems, expanding circles, hover states, and other interactive details should survive the conversion.\n- Treat native `<video>` / `<audio>` elements as the source of truth for any custom media chrome, canvas visualizer, waveform, beat grid, or playhead. Wire the source's media events (`play`, `pause`, `timeupdate`, `seeking`, `seeked`, `ended`, `ratechange`, `volumechange`) and derive visual state from `media.currentTime`; do not run a separate timer that can drift away from actual playback.\n- Every copied `<video>` or `<audio>` with `src` must have HyperFrames timing attributes before lint: `data-start` and `data-duration`, plus `data-has-audio=\"true\"` when audible native audio should be preserved. Use the scene's time range for slide-specific media; for user-controlled evidence videos that may be played from multiple focused slides, use a deck-wide range. Do not leave `preload=\"none\"` on media; use `metadata` or `auto`.\n- Resolve source font tokens before validation. If preserving custom source fonts, add `@font-face` rules for local/captured font files. If using system fallbacks, replace tokenized declarations such as `font-family: var(--f-body)` with concrete render-safe stacks such as `system-ui, sans-serif` or `ui-monospace, monospace`; do not leave `var(...)` as the font family value.\n- Audit the source for atypical page movement, especially behavior driven by scroll, wheel, touch, hash state, resize, or a requestAnimationFrame loop. Treat fixed viewports with translated/scaled \"world\" layers, parallax, pinned panels, horizontal scrollers, scroll-scrubbed timelines, section snapping, and zoom-to-element cameras as source behavior. Scroll is often the source's transition trigger, so preserve the transition by extracting its progress stops, easing, and camera/focus states, then re-host that motion on slideshow navigation through timeline positions, fragments, or a reusable player/harness hook. Standalone wrappers that jump to slide hold-points still need an explicit navigation-camera transition hook; computing per-slide camera transforms is not enough. Do not simulate a literal page-scroll-down transition inside the slide; the viewer should feel camera travel/zoom from one focal point to another, not see a webpage being scrolled. Keep each slide-to-slide camera move continuous: avoid intermediate route stops that reverse x/y direction or zoom unless the source visibly does that at the same boundary. A transition that darts around before landing is worse than a simpler direct focal move.\n- Preserve the source's media crop semantics. Treat screenshots, tweets/social posts, product UI captures, charts, docs, code, leaderboards, and any image with readable text as content evidence, not decorative media: use the source aspect ratio (`height: auto`) or `object-fit: contain` inside a stable frame. Use `object-fit: cover` only when the source did, or for intentionally decorative/background/cinematic thumbnails. After fitting these captures into a slide, inspect all four edges for truncated text, logos, controls, or captions; a visible crop on meaningful content is a bug unless the source itself cropped it.\n- If a behavior is generic to slideshows, put it in the player/controller or in a reusable skill snippet. Do not solve it with one-off deck scripts.\n- Stacked scene frames must never block interaction on the active slide. Hidden frames need both visual hiding and event gating:\n\n```css\n.scene-frame {\n  opacity: 0;\n  visibility: hidden;\n  pointer-events: none;\n}\n\n.scene-frame.is-active {\n  opacity: 1;\n  visibility: visible;\n  pointer-events: auto;\n}\n```\n\nIf visibility is driven imperatively, set all three properties (`opacity`, `visibility`, and `pointerEvents`) in the visibility controller. `opacity: 0` alone still leaves an invisible layer that can swallow clicks.\n\n---\n\n## Fragments: reveal hold-points within a slide\n\nA fragment is an absolute composition-timeline time (seconds) within a slide's `[start, end]` range where the controller should hold a reveal state.\n\n**How it works:**\n\n1. Player enters a fragmented slide — seeks directly to `fragments[0]` and holds there.\n2. User presses Next (or →) — controller seeks to `fragments[1]` and holds.\n3. After the last fragment, Next advances to the next slide.\n4. A slide without fragments enters at a rest frame inside the slide, usually its midpoint, not exactly at `slide.end`.\n\nFragment times must fall within `[start, end]` (inclusive of both bounds). The lint rule rejects only fragments outside that range (`time < start` or `time > end`).\n\nFragment times are **absolute composition-timeline positions** — the same coordinate space as `data-start` — not offsets relative to the scene's start.\n\nNavigation is seek-driven, not play-driven. The controller never starts playback just to move between fragments; each navigation command is a deterministic seek to the target hold time. Design fragment states so they are correct at the target timeline time.\n\n---\n\n## Branching: hotspots and slide sequences\n\nBranch slides are real scenes in the same composition timeline. They are listed only under `slideSequences` and are excluded from main-line navigation — the player never visits them unless a hotspot fires.\n\n**Navigation model:**\n\n- Clicking a hotspot pushes `{sequenceId, slideIndex: 0}` onto the nav stack and enters the branch's first slide.\n- **back()** pops the stack and returns to the exact parent slide (the one that held the hotspot).\n- **backToMain()** clears the entire stack and returns to the root slide.\n- Breadcrumb renders from the stack: `Main deck › Market sizing methodology › Slide 2`.\n- The slide counter inside a branch is scoped to that sequence (`1 of 2`, not the main-deck total).\n\n**What to avoid:**\n\n- Do not add branch scene IDs to the main `slides` array. They must appear only inside a `slideSequences` entry. The lint rule flags overlap.\n- Branch scenes are included in the continuous timeline, so a naive linear video export would include them. Export reads main-line slides only (deferred; flagged in the spec).\n\n---\n\n## Worked example: 3-slide deck with fragments and a branch\n\n### Scene HTML (skeleton)\n\n```html\n<body style=\"margin: 0\">\n  <script type=\"application/hyperframes-slideshow+json\">\n    {\n      \"slides\": [\n        {\n          \"sceneId\": \"hook\",\n          \"notes\": \"Open with the stat. Pause on the $40B number.\"\n        },\n        {\n          \"sceneId\": \"problem\",\n          \"notes\": \"Walk through each pain point one at a time.\",\n          \"fragments\": [11.0, 15.0],\n          \"hotspots\": [\n            {\n              \"id\": \"h1\",\n              \"label\": \"Where does the $40B figure come from?\",\n              \"target\": \"market-detail\",\n              \"region\": { \"x\": 55, \"y\": 60, \"w\": 40, \"h\": 20 }\n            }\n          ]\n        },\n        {\n          \"sceneId\": \"solution\",\n          \"notes\": \"One sentence: what we do and who it is for.\"\n        }\n      ],\n      \"slideSequences\": [\n        {\n          \"id\": \"market-detail\",\n          \"label\": \"Market sizing methodology\",\n          \"slides\": [{ \"sceneId\": \"mkt-math\", \"notes\": \"Bottom-up: 2.3M SMBs × $17k ACV.\" }]\n        }\n      ]\n    }\n  </script>\n\n  <!-- Slide 1 — hook -->\n  <div\n    data-composition-id=\"hook\"\n    data-start=\"0\"\n    data-duration=\"6\"\n    data-label=\"The hook\"\n    data-width=\"1920\"\n    data-height=\"1080\"\n    style=\"position: relative; width: 1920px; height: 1080px; overflow: hidden; background: #0a0a0a\"\n  >\n    <section\n      class=\"clip\"\n      data-start=\"0\"\n      data-duration=\"6\"\n      data-track-index=\"1\"\n      style=\"position: absolute; inset: 0; display: grid; place-items: center\"\n    >\n      <h1 id=\"hook-headline\" style=\"font-size: 80px; color: #fff; font-family: sans-serif\">\n        SMBs lose $40B/year to manual scheduling\n      </h1>\n    </section>\n  </div>\n\n  <!-- Slide 2 — problem (3 fragments) -->\n  <div\n    data-composition-id=\"problem\"\n    data-start=\"6\"\n    data-duration=\"15\"\n    data-label=\"The problem\"\n    data-width=\"1920\"\n    data-height=\"1080\"\n    style=\"position: relative; width: 1920px; height: 1080px; overflow: hidden; background: #0a0a0a\"\n  >\n    <section\n      class=\"clip\"\n      data-start=\"6\"\n      data-duration=\"15\"\n      data-track-index=\"1\"\n      style=\"position: absolute; inset: 0; padding: 120px 160px; box-sizing: border-box\"\n    >\n      <h2 id=\"pain-headline\" style=\"font-size: 64px; color: #fff; font-family: sans-serif\">\n        Three gaps operators can not close\n      </h2>\n      <p id=\"pain-1\" style=\"font-size: 48px; color: #ccc; opacity: 0; font-family: sans-serif\">\n        No-shows cost 23% of booked revenue\n      </p>\n      <p id=\"pain-2\" style=\"font-size: 48px; color: #ccc; opacity: 0; font-family: sans-serif\">\n        Manual reminders take 4h/week per staff\n      </p>\n      <p id=\"pain-3\" style=\"font-size: 48px; color: #ccc; opacity: 0; font-family: sans-serif\">\n        Rescheduling friction drives 40% churn\n      </p>\n    </section>\n  </div>\n\n  <!-- Slide 3 — solution -->\n  <div\n    data-composition-id=\"solution\"\n    data-start=\"21\"\n    data-duration=\"8\"\n    data-label=\"The solution\"\n    data-width=\"1920\"\n    data-height=\"1080\"\n    style=\"position: relative; width: 1920px; height: 1080px; overflow: hidden; background: #0a0a0a\"\n  >\n    <section\n      class=\"clip\"\n      data-start=\"21\"\n      data-duration=\"8\"\n      data-track-index=\"1\"\n      style=\"position: absolute; inset: 0; display: grid; place-items: center\"\n    >\n      <h2 id=\"solution-headline\" style=\"font-size: 72px; color: #fff; font-family: sans-serif\">\n        Acme automates scheduling for service SMBs — no-shows down 80% in 90 days\n      </h2>\n    </section>\n  </div>\n\n  <!-- Branch slide — excluded from main line -->\n  <div\n    data-composition-id=\"mkt-math\"\n    data-start=\"29\"\n    data-duration=\"7\"\n    data-label=\"Market math\"\n    data-width=\"1920\"\n    data-height=\"1080\"\n    style=\"position: relative; width: 1920px; height: 1080px; overflow: hidden; background: #111\"\n  >\n    <section\n      class=\"clip\"\n      data-start=\"29\"\n      data-duration=\"7\"\n      data-track-index=\"1\"\n      style=\"position: absolute; inset: 0; display: grid; place-items: center\"\n    >\n      <p id=\"mkt-formula\" style=\"font-size: 56px; color: #fff; font-family: sans-serif\">\n        2.3M SMBs × $17k ACV = $39B serviceable market\n      </p>\n    </section>\n  </div>\n\n  <script>\n    window.__timelines = window.__timelines || {};\n\n    // Slide 2 fragment entrance animations\n    gsap.registerPlugin(); // load any plugins before use\n\n    const tl = gsap.timeline({ paused: true });\n    window.__timelines[\"problem\"] = tl;\n\n    // Insert positions are absolute composition-timeline times (same as data-start / fragment values).\n    tl.from(\"#pain-1\", { opacity: 0, y: 20, duration: 0.4 }, 11.0);\n    tl.from(\"#pain-2\", { opacity: 0, y: 20, duration: 0.4 }, 15.0);\n    // pain-3 lands at end of slide\n    tl.from(\"#pain-3\", { opacity: 0, y: 20, duration: 0.4 }, 13.0);\n  </script>\n</body>\n```\n\n### Key points in the example\n\n- The island `sceneId` values (`\"hook\"`, `\"problem\"`, `\"solution\"`, `\"mkt-math\"`) exactly match `data-composition-id` values on scene divs.\n- `mkt-math` appears only in `slideSequences` — it is never in the top-level `slides` array.\n- Fragment times (`11.0`, `15.0`) are within the `problem` scene's `[6, 21]` range (times are absolute composition-timeline positions).\n- The hotspot `region` (`x: 55, y: 60, w: 40, h: 20`) positions the clickable area in the lower-right quadrant of the problem slide.\n- GSAP timelines are registered on `window.__timelines` and are paused — the HyperFrames engine drives playback; do not call `.play()` at construction time.\n\n---\n\n## Wrapping component\n\nWrap the composition in `<hyperframes-slideshow>` around `<hyperframes-player>` in any embedding context:\n\n```html\n<hyperframes-slideshow>\n  <hyperframes-player src=\"deck.html\"></hyperframes-player>\n</hyperframes-slideshow>\n```\n\n`<hyperframes-slideshow>` provides the navigation chrome (Present, Prev / Next, counter, global mute when `sound` is present, fullscreen), keyboard handling (← / →, Space / Backspace, and P for Present), touch swipe, and hotspot overlays.\n\nThe slideshow automatically sets the `interactive` attribute on every inner `<hyperframes-player>` at mount time, so clickable controls, links, native media controls, and custom players inside the composition iframe receive pointer events as expected. (Outside a slideshow wrapper, you must add `interactive` manually on `<hyperframes-player>` — the player defaults to `pointer-events: none` on the iframe so clicks on the player host don't get hijacked into toggling timeline playback.)\n\n**Presenter mode:** use the built-in Present icon button in the slideshow nav capsule, or press P. It calls `window.open('?mode=audience')` for a fullscreen audience tab; the originating tab becomes the presenter view (current slide reduced, next-slide preview, notes, elapsed timer). The two tabs sync via `BroadcastChannel('hf-slideshow:' + location.pathname)`. Do not add a custom wrapper-level Present button; the shared component owns its placement, icon, styling, and audience-mode hiding.\n\n**Presenting over Google Meet / Zoom (screen share):** share the _audience_ surface, keep the presenter view on your own screen.\n\n- **Google Meet (or any in-Chrome share):** Present → in Meet choose **Share screen → A tab** → pick the audience tab → switch back to the presenter tab. Chrome keeps a captured tab rendering while backgrounded, so animations and slide nav stay live. Do **not** share \"A window\" or \"Entire screen\" — a fully covered window stops rendering (frozen slides for viewers), and entire-screen exposes your notes.\n- **Zoom (desktop app):** drag the audience tab out into its own window and share that window. Zoom captures via the OS, so if the audience window becomes _fully_ covered it freezes — use a second monitor, or keep a sliver of the audience window visible behind the presenter view.\n\nPresenter-driven media playback has an autoplay-policy constraint: `BroadcastChannel` can sync intent, time, and state, but it cannot transfer the presenter's user activation to the audience tab. The shared slideshow player mirrors native media events and starts remote audience playback muted first; only fall back to the standalone harness's audience unlock behavior if muted `media.play()` is rejected or if the deck specifically requires audible audience playback. Do not keep applying remote `timeupdate` messages after a rejected play, or the audience will silently seek through the video without playback.\n\nPresenter notes are editable in the presenter view. Edits are stored in `localStorage` per deck and slide, layered over the manifest notes without rewriting the composition file. Do not add one-off note-editing scripts to decks; rely on the shared slideshow player behavior. If a standalone/custom wrapper truly needs to implement this outside the shared player, use the deterministic storage snippet in `skills/slideshow/references/standalone-harness.md`.\n\n### Media cleanup on slide exit\n\nThe slideshow controller owns slide-exit media cleanup. When navigation changes slide or sequence, it calls `hyperframes-player.stopMedia()` before entering the next slide. That command:\n\n- posts `stop-media` to the iframe runtime, which stops WebAudio and pauses native `<video>` / `<audio>` elements;\n- pauses same-origin iframe media directly as a fallback; and\n- pauses parent-frame proxies adopted from iframe media.\n\nSame-slide fragment navigation does **not** stop media. Global/deck-level parent audio, such as a background track wired through `audio-src`, is not treated as slide media.\n\nDo not add per-slide cleanup scripts for normal media players. Keep slide video/audio as normal media in the composition; use `data-has-audio=\"true\"` only when the player should preserve audible native video audio instead of treating it as silent visual media.\n\nIf the source page has custom controls or visualizations attached to media, those controls must listen to the same native element the slideshow player stops and mutes. A pause caused by slide exit, presenter sync, native controls, custom controls, or the global mute button should all update the visible custom UI through media events, not through parallel state.\n\nWhen implementing direct iframe fallback cleanup, treat iframe media as cross-realm DOM. Do not test iframe nodes with the parent page's `el instanceof HTMLMediaElement`; that returns false in real browsers. Use `el.ownerDocument.defaultView.HTMLMediaElement` (or an equivalent tag/duck-type guard) before setting `muted` or calling `pause()`.\n\n### Global nav mute\n\nWhen `<hyperframes-slideshow sound>` renders the nav mute button, that button is the global mute control for the page. It must mute:\n\n- child `<hyperframes-player>` instances, including same-origin iframe media;\n- top-level page `<audio>` / `<video>` elements; and\n- wrapper-owned SFX/global `Audio` objects via the `hf-sound` event.\n\nDo not add a second mute button inside the composition. If a wrapper script creates `new Audio(...)` objects that are not attached to the DOM, it must listen for `hf-sound` and set `clip.muted = detail.muted` on each object, not merely skip future plays.\n\nThe same cross-realm rule applies here: global mute must reach iframe `<video>` / `<audio>` elements through the child frame's DOM realm. A passing unit test in a single DOM realm is not enough; verify in a browser that the actual iframe media elements report `muted: true` after clicking the nav mute button.\n\n`hyperframes present` serves built bundles from `packages/player/dist`. After changing player or slideshow chrome behavior, run `bun run build` in `packages/player` and restart the present server before testing in a browser.\n\n---\n\n## Running a slideshow standalone (interim)\n\nThe **durable answer** is engine-hosted: `hyperframes preview --slideshow` / studio present mode will host the composition over the real HyperFrames engine, which drives seek-timelines, owns the gesture frame, and reads the island from the composition. That path is coming; prefer it once it ships.\n\nUntil then, standalone demos (a composition opened via the bare player bundle in a browser, without the engine) require workarounds for three gaps: the composition must expose a seekable root timeline, the island must be duplicated into the wrapper, and wrapper-owned SFX/global audio should live in the parent frame. These patterns are documented in:\n\n```\nskills/slideshow/references/standalone-harness.md\n```\n\nDo not treat the patterns there as the blessed model — they exist only to bridge the gap until the engine-hosted path lands.\n\n## Handoff\n\nFor a public or user-facing slideshow project, the root `index.html` should be a runnable slideshow entrypoint. Opening it in a browser should show slideshow navigation and respond to Next/Prev; it should not expose only the raw composition and require the user to know about Studio or an internal wrapper file. If the raw HyperFrames composition must remain separate for CLI compatibility, put it in a subdirectory such as `composition/index.html` and point scripts/commands at that directory.\n\nThe direct-open wrapper must rely on the built-in Present icon button rendered by `<hyperframes-slideshow>`. Do not add a bespoke `#present-btn`, fixed-position button, or wrapper-specific Present styling. The shared component owns the control bar, hides Present in `?mode=audience`, and supports P as a keyboard shortcut.\n\nValidate the direct-open path before handoff. If `file://` browser restrictions break iframe media, local scripts, or same-origin player access, use a self-contained wrapper or make the handoff command start a local server and open the working URL; do not leave `index.html` in a broken or ambiguous state.\n\nFor a completed slideshow deck, the primary user-facing next step is presenter mode, not Studio. Run or provide:\n\n```bash\nnpx hyperframes present <project-dir>\n```\n\nStudio/`preview` is useful for editing a composition, but it is not a clear final destination for a slideshow user. If you create a `package.json` for a slideshow project where the raw composition lives in `composition/`, make the default runnable script start presenter mode:\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"npx hyperframes present ./composition\",\n    \"studio\": \"npx hyperframes preview ./composition --background\"\n  }\n}\n```\n\nAt handoff, include the local presenter URL printed by the command and the minimal instruction: \"Click Present, or press P, to open the audience tab.\" If the user will present over Google Meet or Zoom, also pass on the screen-share guidance from the Presenting section above (share the audience tab in Meet; a dragged-out audience window in Zoom). Keep the server running if the user asked you to start it.\n\n---\n\n## Validation\n\nAfter authoring or editing a slideshow composition, run validation (it reruns lint before opening the browser):\n\n```bash\nnpx hyperframes check\n```\n\nTreat lint errors and validation `StaticGuard` contract messages as blockers even if a command exits successfully. Fix the file and rerun until `check` reports no lint or runtime errors.\n\nThe slideshow lint rule checks:\n\n- Every `slide.sceneId` resolves to an existing scene (by `data-composition-id`).\n- Every `hotspot.target` references a defined `slideSequence` id.\n- Fragment times fall within each slide's `[start, end]` range.\n- No two main-line slides overlap in time.\n\nFix all violations before previewing. A composition that fails lint will not parse correctly in the player.\n\nFile v1.0.15:_meta.json\n\n{\n  \"ownerId\": \"kn77d06grj6xqp3dqwkk4bavhn89pegt\",\n  \"slug\": \"slideshow\",\n  \"version\": \"1.0.15\",\n  \"publishedAt\": 1791141621527\n}\n\nFile v1.0.15:references/standalone-harness.md\n\n# Standalone HyperFrames Slideshow Harness\n\n## 1. Interim framing — why this exists\n\nThese patterns are a **temporary workaround** for standalone demos. The durable solution is engine-hosted: a future `hyperframes preview --slideshow` / studio present mode will host the composition over the real HyperFrames engine, which drives seek-timelines frame-by-frame, owns the gesture frame, and reads the slideshow island directly from the composition. When that path ships, most of what follows collapses.\n\nUntil then, a standalone slideshow opened via the bare player bundle must work around three facts:\n\n1. The composition must expose a seekable `window.__timelines.root` timeline. Anything outside that seek path, such as Three.js loops or imperative entrance effects, must be self-driving.\n2. `<hyperframes-slideshow>` reads the slideshow island from its **own innerHTML** (the wrapper element), not from the composition the player loads. The island must be duplicated into the wrapper.\n3. The composition runs in the player's **iframe**; user keypresses and pointer events land on the **parent page**. Wrapper-owned SFX/global audio should live in the parent, where the activation token is reliable. Normal slide media stays in the composition and is stopped by the slideshow player on slide exit.\n\nDo not treat these as the blessed authoring model. When the engine-hosted path ships, compositions authored the normal way will just work.\n\n**Living reference implementations:**\n\n---\n\n## 2. The parent wrapper (`index.html` for deliverables, `demo.html` in examples)\n\nThe parent page hosts the two dist bundles, wraps the components, duplicates the island, and owns all audio.\n\nFor public or user-facing generated projects, make this wrapper the root `index.html` so opening the project in a browser runs the slideshow. Put the raw HyperFrames composition in a separate path such as `composition/index.html`. In repo examples you may still see this file called `demo.html`; that name is a reference pattern, not the preferred handoff for a standalone deck.\n\n```html\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>My Deck — Slideshow Demo</title>\n\n    <!--\n      Load both bundles from packages/player/dist.\n      The global builds register <hyperframes-player> and <hyperframes-slideshow>\n      as custom elements — no import map needed.\n    -->\n    <script src=\"../../../packages/player/dist/hyperframes-player.global.js\"></script>\n    <script src=\"../../../packages/player/dist/slideshow/hyperframes-slideshow.global.js\"></script>\n\n    <style>\n      *,\n      *::before,\n      *::after {\n        box-sizing: border-box;\n        margin: 0;\n        padding: 0;\n      }\n      html,\n      body {\n        width: 100%;\n        height: 100%;\n        overflow: hidden;\n        background: #111;\n      }\n    </style>\n  </head>\n  <body>\n    <!--\n      tabindex=\"0\" is critical — <hyperframes-slideshow> binds keydown\n      (ArrowLeft/Right, Space, Backspace) to itself. Without tabindex the\n      element cannot receive focus and arrow keys are dead.\n    -->\n    <hyperframes-slideshow\n      tabindex=\"0\"\n      style=\"display: block; position: relative; width: 100vw; height: 100vh\"\n    >\n      <hyperframes-player\n        interactive\n        style=\"position: absolute; inset: 0\"\n        src=\"composition/index.html\"\n      ></hyperframes-player>\n\n      <!--\n        DUPLICATED ISLAND — keep in sync with the island inside index.html.\n        <hyperframes-slideshow> reads from its own innerHTML, not from the\n        composition the player loads. Every time slides/fragments/hotspots/\n        sequences change in index.html, update this copy too.\n      -->\n      <script type=\"application/hyperframes-slideshow+json\">\n        {\n          \"slides\": [\n            { \"sceneId\": \"scene-one\", \"notes\": \"...\" },\n            {\n              \"sceneId\": \"scene-two\",\n              \"notes\": \"...\",\n              \"fragments\": [8.3, 8.6]\n            }\n          ],\n          \"slideSequences\": [\n            {\n              \"id\": \"branch-one\",\n              \"label\": \"Branch label\",\n              \"slides\": [{ \"sceneId\": \"branch-scene\", \"notes\": \"...\" }]\n            }\n          ]\n        }\n      </script>\n    </hyperframes-slideshow>\n    <!-- The built-in slideshow nav capsule renders Present; do not add a wrapper-level button. -->\n\n    <!-- Audio player lives here — see Section 6 -->\n  </body>\n</html>\n```\n\n`interactive` is required for decks with clickable page content or media controls. Without it, iframe pointer events are disabled by the player shell and a click on the composition can be interpreted as a player play/pause toggle instead of a slide interaction.\n\n### Per-slide `autoplay`\n\nAdd `\"autoplay\": true` to a slide in the island to play that slide's first `<video>` from the start when the presenter lands on it. The slideshow still holds — it never auto-advances — so the presenter clicks Next when ready; autoplay only saves a manual play click into the composition.\n\n```json\n{\n  \"sceneId\": \"cold-open\",\n  \"autoplay\": true,\n  \"notes\": \"Promo plays on enter; click Next when it ends.\"\n}\n```\n\nUse `autoplay` when the video **is** the slide's primary content and its natural end is the cue to advance — a cold-open promo, a demo clip you let run and then move on from. Do **not** use it for background/ambient loops or for footage the presenter talks over; those should start on the presenter's own cue (a click on the clip's controls via `interactive`), not automatically. One autoplay clip per slide (the first `<video>` in the scene).\n\n### Presenter media bridge for interactive media\n\nPresenter/audience mode syncs slide position through a deck-scoped `BroadcastChannel`. If the presenter is expected to play, pause, seek, mute, or change rate on media inside the composition, mirror those native media events over the same channel. Keep the media element as the source of truth; do not mirror a custom button's private state.\n\nAudible playback has one extra browser constraint: a `BroadcastChannel` message does not carry the presenter's user activation into the audience window. The audience window should try presenter-driven playback muted first, because browsers usually allow muted autoplay; it may still reject `media.play()` even while it accepts remote `currentTime` updates. If that happens, do not keep chasing presenter `timeupdate` messages; show an audience-side unlock control, store the latest play intent, and retry playback from that intent after the audience window receives a click/key gesture.\n\n```js\n(function () {\n  if (typeof BroadcastChannel === \"undefined\") return;\n\n  var sender =\n    new URLSearchParams(location.search).get(\"mode\") === \"audience\" ? \"audience\" : \"presenter\";\n  var channel = new BroadcastChannel(\"hf-slideshow:\" + location.pathname);\n  var applyingRemote = false;\n  var lastTimeBroadcast = 0;\n  var pendingPlayByKey = {};\n  var blockedPlayByKey = {};\n  var mutedPlaybackByKey = {};\n  var unlockButton = null;\n\n  function frameDocument() {\n    var player = document.querySelector(\"hyperframes-player\");\n    var frame = player && player.iframeElement;\n    try {\n      return frame && frame.contentDocument ? frame.contentDocument : null;\n    } catch {\n      return null;\n    }\n  }\n  function mediaNodes() {\n    var doc = frameDocument();\n    return doc ? Array.from(doc.querySelectorAll(\"video,audio\")) : [];\n  }\n  function mediaKey(el, index) {\n    return el.id ? \"id:\" + el.id : el.tagName.toLowerCase() + \":\" + index;\n  }\n  function findMedia(key) {\n    return mediaNodes().find(function (el, index) {\n      return mediaKey(el, index) === key;\n    });\n  }\n  function syncMediaState(el, msg, allowTimeSync) {\n    if (typeof msg.playbackRate === \"number\") el.playbackRate = msg.playbackRate;\n    if (typeof msg.volume === \"number\") el.volume = Math.max(0, Math.min(1, msg.volume));\n    if (sender === \"audience\" && mutedPlaybackByKey[msg.key]) {\n      el.muted = true;\n    } else if (typeof msg.muted === \"boolean\") {\n      el.muted = msg.muted;\n    }\n    if (\n      allowTimeSync &&\n      typeof msg.currentTime === \"number\" &&\n      Math.abs((el.currentTime || 0) - msg.currentTime) > 0.35\n    ) {\n      el.currentTime = Math.max(0, msg.currentTime);\n    }\n  }\n  function hasBlockedPlay() {\n    return Object.keys(blockedPlayByKey).length > 0;\n  }\n  function hideAudienceUnlockIfClear() {\n    if (hasBlockedPlay() || !unlockButton) return;\n    unlockButton.remove();\n    unlockButton = null;\n  }\n  function showAudienceUnlock() {\n    if (sender !== \"audience\" || unlockButton) return;\n    unlockButton = document.createElement(\"button\");\n    unlockButton.type = \"button\";\n    unlockButton.textContent = \"Enable audience media\";\n    unlockButton.style.cssText =\n      \"position:fixed;left:50%;bottom:96px;transform:translateX(-50%);z-index:100000;border:0;border-radius:999px;padding:12px 18px;background:#fff;color:#111827;box-shadow:0 10px 32px rgba(0,0,0,.28);font:700 14px/1 system-ui,sans-serif;cursor:pointer;\";\n    unlockButton.addEventListener(\"click\", retryBlockedPlays);\n    document.body.appendChild(unlockButton);\n  }\n  function rememberBlockedPlay(msg) {\n    pendingPlayByKey[msg.key] = msg;\n    blockedPlayByKey[msg.key] = true;\n    showAudienceUnlock();\n  }\n  function clearBlockedPlay(key) {\n    delete blockedPlayByKey[key];\n    hideAudienceUnlockIfClear();\n  }\n  function tryPlay(el, msg) {\n    if (sender === \"audience\") mutedPlaybackByKey[msg.key] = true;\n    syncMediaState(el, msg, true);\n    if (sender === \"audience\") el.muted = true;\n    try {\n      var playResult = el.play();\n      if (playResult && typeof playResult.then === \"function\") {\n        playResult\n          .then(function () {\n            clearBlockedPlay(msg.key);\n          })\n          .catch(function () {\n            rememberBlockedPlay(msg);\n          });\n      } else {\n        clearBlockedPlay(msg.key);\n      }\n    } catch (e) {\n      rememberBlockedPlay(msg);\n    }\n  }\n  function retryBlockedPlays() {\n    wireMedia();\n    applyingRemote = true;\n    try {\n      Object.keys(pendingPlayByKey).forEach(function (key) {\n        var msg = pendingPlayByKey[key];\n        var el = findMedia(key);\n        if (el && msg) tryPlay(el, msg);\n      });\n    } finally {\n      setTimeout(function () {\n        applyingRemote = false;\n      }, 300);\n    }\n  }\n  function publish(el, index, action) {\n    if (sender !== \"presenter\") return;\n    if (applyingRemote) return;\n    if (action === \"timeupdate\") {\n      var now = performance.now();\n      if (now - lastTimeBroadcast < 450 && !el.paused) return;\n      lastTimeBroadcast = now;\n    }\n    channel.postMessage({\n      type: \"media\",\n      sender,\n      key: mediaKey(el, index),\n      action,\n      currentTime: el.currentTime || 0,\n      paused: el.paused,\n      ended: el.ended,\n      muted: el.muted,\n      volume: el.volume,\n      playbackRate: el.playbackRate,\n    });\n  }\n  function wireMedia() {\n    mediaNodes().forEach(function (el, index) {\n      if (el.dataset.hfPresenterMediaSync === \"1\") return;\n      el.dataset.hfPresenterMediaSync = \"1\";\n      [\n        \"play\",\n        \"pause\",\n        \"seeking\",\n        \"seeked\",\n        \"ratechange\",\n        \"volumechange\",\n        \"ended\",\n        \"timeupdate\",\n      ].forEach(function (name) {\n        el.addEventListener(name, function () {\n          publish(el, index, name);\n        });\n      });\n    });\n  }\n  channel.addEventListener(\"message\", function (event) {\n    var msg = event.data;\n    if (!msg || msg.type !== \"media\" || msg.sender === sender) return;\n    if (sender === \"audience\" && blockedPlayByKey[msg.key] && msg.action === \"timeupdate\") {\n      pendingPlayByKey[msg.key] = msg;\n      showAudienceUnlock();\n      return;\n    }\n    var el = findMedia(msg.key);\n    if (!el) return;\n    applyingRemote = true;\n    try {\n      if (\n        msg.action === \"play\" ||\n        (sender === \"audience\" && msg.action === \"timeupdate\" && msg.paused === false && el.paused)\n      ) {\n        pendingPlayByKey[msg.key] = msg;\n        tryPlay(el, msg);\n      } else {\n        syncMediaState(el, msg, true);\n      }\n      if (msg.action === \"pause\" || msg.action === \"ended\") {\n        delete pendingPlayByKey[msg.key];\n        delete mutedPlaybackByKey[msg.key];\n        clearBlockedPlay(msg.key);\n        el.pause();\n      }\n    } catch {\n    } finally {\n      setTimeout(function () {\n        applyingRemote = false;\n      }, 300);\n    }\n  });\n  wireMedia();\n  window.addEventListener(\"load\", wireMedia);\n  window.addEventListener(\"keydown\", retryBlockedPlays, true);\n  window.addEventListener(\"pointerdown\", retryBlockedPlays, true);\n  setInterval(wireMedia, 1000);\n})();\n```\n\n### Custom media visualizers\n\nFor waveform, beat-grid, canvas, or timeline players, wire visual state to the native media element. This keeps native controls, custom controls, presenter sync, slide-exit cleanup, and global mute in one event path.\n\n```js\nfunction wireMediaDrivenVisualizer(media, renderFrame, fireCrossedEvents) {\n  var mediaFrame = 0;\n  var lastTime = media.currentTime || 0;\n\n  function update() {\n    var time = media.currentTime || 0;\n    if (Math.abs(time - lastTime) < 1.5 && time >= lastTime) {\n      fireCrossedEvents(lastTime, time);\n    }\n    lastTime = time;\n    renderFrame(time, media);\n  }\n\n  function start() {\n    if (!media.requestVideoFrameCallback || mediaFrame) return;\n    mediaFrame = media.requestVideoFrameCallback(function () {\n      mediaFrame = 0;\n      update();\n      if (!media.paused && !media.ended) start();\n    });\n  }\n\n  media.addEventListener(\"play\", start);\n  media.addEventListener(\"playing\", start);\n  media.addEventListener(\"pause\", update);\n  media.addEventListener(\"ended\", update);\n  media.addEventListener(\"timeupdate\", update);\n  media.addEventListener(\"seeking\", function () {\n    lastTime = media.currentTime || 0;\n    renderFrame(lastTime, media);\n  });\n  media.addEventListener(\"seeked\", update);\n  media.addEventListener(\"ratechange\", update);\n  media.addEventListener(\"volumechange\", update);\n  renderFrame(media.currentTime || 0, media);\n}\n```\n\nDo not use `requestAnimationFrame` inside compositions for media sync; composition lint rejects wall-clock loops. Prefer `HTMLVideoElement.requestVideoFrameCallback()` for smooth video-tied updates and rely on native `timeupdate`/seek events as the fallback.\n\nUse a dedicated wiring marker such as `data-media-sync-wired`. Do not reuse a marker like `data-wired` for both \"timeline DOM already rendered\" and \"media event listeners attached\"; pre-rendered timeline HTML will otherwise skip listener setup.\n\n### Editable presenter notes\n\nThe shared `<hyperframes-slideshow>` presenter already renders speaker notes as an editable textarea and stores edits in `localStorage`. Do not add deck-specific note editors when the shared player is available.\n\nFor interim custom wrappers that cannot use the shared presenter chrome, use this deterministic storage contract exactly so notes migrate cleanly:\n\n```js\nconst NOTES_STORAGE_PREFIX = \"hf-slideshow:presenter-notes:v1:\";\n\nfunction notesDeckKey(slideshowEl) {\n  const explicit = slideshowEl.getAttribute(\"notes-storage-key\");\n  if (explicit && explicit.trim()) return explicit.trim();\n\n  const playerSrc = slideshowEl.querySelector(\"hyperframes-player\")?.getAttribute(\"src\") || \"\";\n  let resolvedPlayerSrc = playerSrc;\n  try {\n    resolvedPlayerSrc = new URL(playerSrc, location.href).href;\n  } catch {}\n\n  return `${location.origin}${location.pathname}|${document.title}|${resolvedPlayerSrc}`;\n}\n\nfunction notesStorageKey(slideshowEl, position, slide) {\n  return `${NOTES_STORAGE_PREFIX}${JSON.stringify([\n    notesDeckKey(slideshowEl),\n    position.sequenceId,\n    position.slideIndex,\n    slide.sceneId || \"\",\n  ])}`;\n}\n\nfunction readPresenterNotes(slideshowEl, position, slide) {\n  const key = notesStorageKey(slideshowEl, position, slide);\n  try {\n    const stored = localStorage.getItem(key);\n    return stored == null ? slide.notes || \"\" : stored;\n  } catch {\n    return slide.notes || \"\";\n  }\n}\n\nfunction wirePresenterNotes(textarea, slideshowEl, position, slide) {\n  const key = notesStorageKey(slideshowEl, position, slide);\n  textarea.value = readPresenterNotes(slideshowEl, position, slide);\n  textarea.addEventListener(\"input\", function () {\n    try {\n      localStorage.setItem(key, textarea.value);\n    } catch {}\n  });\n}\n```\n\nClearing the textarea must save an empty string, not remove the local value, because a presenter may intentionally blank a manifest note for their run. Use `notes-storage-key=\"stable-deck-id\"` on `<hyperframes-slideshow>` when a standalone demo has a stable project id; otherwise the fallback key isolates by page, title, and player `src`.\n\n---\n\n## 3. Playhead-driven scene visibility\n\nWithout the engine, scenes are driven by a `root` GSAP timeline that the composition manages on its own clock. The visibility controller reads `window.__timelines.root.time()` via that timeline's `onUpdate` callback and sets `opacity` accordingly. Only the active scene is visible.\n\nThe key insight: scene backgrounds must be `transparent` (not opaque) if you want a Three.js canvas behind them; the body/html background and scene inline `background` set the visual fill.\n\nFor converted source pages, preserve the original page's visual design, motion language, interactive behavior, media behavior, and presentation affordances as closely as practical. Port source-specific widgets exactly where practical: custom canvas players, waveform/timeline decorations, expanding rings, playheads, hover states, and event wiring are source material, not optional polish. Also audit for atypical page movement: scroll-scrubbed cameras, parallax, pinned sections, horizontal scrollers, section snapping, translated/scaled world layers, or zoom-to-element navigation. Scroll is often the source's transition trigger, so extract the scroll-progress stops, easing, and camera/focus states, then re-host that motion on slideshow navigation through timeline positions, fragments, or reusable harness hooks. Do not recreate the browser's literal page-scroll-down motion inside a slide; translate it into camera travel/zoom from one focus area to the next. If the same mechanical behavior appears across decks, move it into the player or this harness instead of copying a fragile one-off script.\n\n### Navigation camera transitions for converted pages\n\nWhen a source page uses scroll to move a translated/scaled world, slideshow navigation usually seeks directly to each slide's hold frame. That seek bypasses any in-timeline interpolation near the scene boundary, so a deck can compute the right camera positions and still appear to jump. Add an explicit standalone navigation transition for manual slide changes, while keeping normal HyperFrames timeline seeks static and deterministic.\n\nUse this pattern only for direct-open/presenter slideshow UI. Do not depend on CSS transitions for rendered video output; rendered compositions must still be correct when seeking a single frame.\n\n```css\n#world {\n  transform-origin: 0 0;\n  will-change: transform;\n  transition:\n    transform 760ms cubic-bezier(0.22, 1, 0.36, 1),\n    opacity 0.3s ease;\n}\n\n#world.hf-camera-static {\n  transition: opacity 0.3s ease;\n}\n```\n\n```js\nvar currentCamera = null;\nvar currentSlideIndex = null;\n\nfunction cameraTransform(cam) {\n  return \"translate(\" + cam.tx + \"px,\" + cam.ty + \"px) scale(\" + cam.s + \")\";\n}\n\nfunction setWorldCamera(world, cam, animate) {\n  if (!cam) return;\n  if (!animate) world.classList.add(\"hf-camera-static\");\n  world.style.transform = cameraTransform(cam);\n  world.style.opacity = \"1\";\n  if (!animate) {\n    world.getBoundingClientRect();\n    world.classList.remove(\"hf-camera-static\");\n  }\n  currentCamera = cam;\n}\n\nfunction slideIndexAtTime(t, slideDuration, slideCount) {\n  return Math.max(0, Math.min(slideCount - 1, Math.floor(t / slideDuration)));\n}\n\nfunction updateCameraForTime(t, opts) {\n  var nextSlideIndex = slideIndexAtTime(t, SLIDE_DURATION, SLIDES.length);\n  var jumpedBetweenSlides =\n    currentSlideIndex !== null &&\n    nextSlideIndex !== currentSlideIndex &&\n    Math.abs(t - lastTime) > 1.2;\n  var animateCamera = Boolean(\n    window.__hfCameraTransitionsEnabled && jumpedBetweenSlides && !(opts && opts.staticCamera),\n  );\n  var cam = cameraAtTime(t);\n  setWorldCamera(world, cam, animateCamera);\n  currentSlideIndex = nextSlideIndex;\n}\n```\n\nDuring measurement, temporarily remove the transform with `hf-camera-static`, compute all element union rects, then restore `currentCamera` without animation. On initial load, resize, and validation-style seeks, call `updateCameraForTime(t, { staticCamera: true })`. In the standalone wrapper, set `iframe.contentWindow.__hfCameraTransitionsEnabled = true` after the player iframe is available. That keeps the exported composition seekable while letting presenter navigation glide between focal points.\n\nBefore validation, resolve source font variables. HyperFrames lint accepts concrete generic stacks such as `system-ui, sans-serif` and `ui-monospace, monospace`, or real `@font-face` declarations pointing at local font files. It does not accept `font-family: var(--f-body)` / `var(--f-mono)` as a render-safe family.\n\n```html\n<!-- In index.html (composition) -->\n\n<!-- Shared scene CSS — all scenes start hidden -->\n<style>\n  .scene-frame {\n    position: absolute;\n    top: 0;\n    left: 0;\n    width: 1920px;\n    height: 1080px;\n    overflow: hidden;\n    opacity: 0; /* hidden at rest — visibility controller shows the active one */\n    visibility: hidden; /* opacity:0 alone still lets invisible frames block clicks */\n    pointer-events: none; /* inactive scenes must not swallow events */\n  }\n</style>\n\n<!--\n  content-visible-at-rest: mark the first scene's elements with their\n  final non-hidden state so the deck is not blank before the controller\n  fires. The controller calls updateVisibility(0) synchronously on load.\n\n  If using Three.js canvas behind scenes, set background: transparent\n  here and let the 3D canvas + body color supply the fill.\n-->\n<div\n  id=\"scene-cover\"\n  class=\"scene-frame clip\"\n  data-composition-id=\"cover\"\n  data-start=\"0\"\n  data-duration=\"9\"\n  style=\"background: transparent\"\n>\n  <!-- content here -->\n</div>\n\n<div\n  id=\"scene-problem\"\n  class=\"scene-frame clip\"\n  data-composition-id=\"problem\"\n  data-start=\"9\"\n  data-duration=\"9\"\n  style=\"background: transparent\"\n>\n  <!-- content here -->\n</div>\n\n<!-- Root timeline — spans the full composition duration -->\n<script>\n  (function () {\n    window.__timelines = window.__timelines || {};\n    var tl = gsap.timeline({ paused: true });\n    // A single to() for the full duration establishes the seekable range\n    tl.to({}, { duration: 108 }); // replace 108 with your total seconds\n    window.__timelines[\"root\"] = tl;\n  })();\n</script>\n\n<!-- Visibility controller -->\n<script>\n  (function () {\n    var scenes = [\n      { id: \"scene-cover\", start: 0, end: 9 },\n      { id: \"scene-problem\", start: 9, end: 18 },\n      // ... all scenes including branch scenes\n    ];\n\n    var lastActiveId = null;\n\n    function updateVisibility(t) {\n      for (var i = 0; i < scenes.length; i++) {\n        var s = scenes[i];\n        var el = document.getElementById(s.id);\n        if (!el) continue;\n        var active = t >= s.start && t < s.end;\n        el.style.opacity = active ? \"1\" : \"0\";\n        el.style.visibility = active ? \"visible\" : \"hidden\";\n        el.style.pointerEvents = active ? \"auto\" : \"none\";\n\n        if (active && lastActiveId !== s.id) {\n          lastActiveId = s.id;\n          fireEntrance(el); // see Section 4\n        }\n      }\n      // fragment reveals here — see Section 4\n    }\n\n    window.__hfSetTime = updateVisibility;\n\n    // Show first slide immediately — avoids blank on load\n    updateVisibility(0);\n\n    // Hook the root timeline so every seek drives visibility\n    var root = window.__timelines && window.__timelines[\"root\"];\n    if (root) {\n      root.eventCallback(\"onUpdate\", function () {\n        updateVisibility(root.time());\n      });\n    }\n  })();\n</script>\n```\n\n---\n\n## 4. Imperative entrances on slide-activate\n\nThe engine-hosted path drives GSAP seek-timelines frame by frame. Without it, seek-timeline tweens never fire. Instead, fire imperative `gsap.from()` calls each time a scene becomes active — these run on GSAP's own ticker and are independent of any playhead.\n\nFragment reveals use playhead-crossing: the visibility controller checks whether the playhead has passed each fragment's hold-time and fires an animation on the first crossing. Bunch fragment hold-times near the scene start (within the first 300–500 ms of the scene) so successive ArrowRight presses feel like snappy sequential reveals rather than long waits.\n\n```js\n// --- Entrance animations ---\n\nfunction fireEntrance(sceneEl) {\n  // [data-anim] marks elements that should entrance on slide-activate.\n  // Add data-anim to eyebrows, headlines, subheads, and card grids.\n  var animEls = sceneEl.querySelectorAll(\"[data-anim]\");\n  if (!animEls.length) return;\n  gsap.from(animEls, {\n    opacity: 0,\n    y: 28,\n    duration: 0.4,\n    stagger: 0.07,\n    ease: \"power2.out\",\n    overwrite: true, // cancel any in-flight animation on rapid slide changes\n  });\n}\n\n// --- Fragment reveals ---\n\n// Fragment config: times in absolute composition timeline seconds,\n// bunched near the scene start for snappy successive reveals.\nvar fragments = [\n  { time: 9.3, id: \"prob-item1\", revealed: false },\n  { time: 9.6, id: \"prob-item2\", revealed: false },\n];\n\nfunction revealFragment(id) {\n  var el = document.getElementById(id);\n  if (!el) return;\n  gsap.fromTo(\n    el,\n    { opacity: 0, x: -24 },\n    { opacity: 1, x: 0, duration: 0.35, ease: \"power2.out\", overwrite: true },\n  );\n}\n\n// Inside updateVisibility(t):\nfor (var f = 0; f < fragments.length; f++) {\n  if (!fragments[f].revealed && t >= fragments[f].time) {\n    fragments[f].revealed = true;\n    revealFragment(fragments[f].id);\n  }\n}\n\n// On problem scene re-entry, reset all fragment states:\nif (active && lastActiveId !== s.id && s.id === \"scene-problem\") {\n  for (var f = 0; f < fragments.length; f++) {\n    fragments[f].revealed = false;\n    var pEl = document.getElementById(fragments[f].id);\n    if (pEl) gsap.set(pEl, {\n\nArchive v1.0.14: 4 files, 29515 bytes\n\nFiles: references/standalone-harness.md (44170b), skill-card.md (1854b), SKILL.md (33983b), _meta.json (129b)\n\nArchive v1.0.13: 4 files, 29619 bytes\n\nFiles: references/standalone-harness.md (44170b), skill-card.md (2330b), SKILL.md (33739b), _meta.json (129b)\n\nArchive v1.0.12: 4 files, 29874 bytes\n\nFiles: references/standalone-harness.md (44388b), skill-card.md (2682b), SKILL.md (33739b), _meta.json (129b)\n\nArchive v1.0.11: 4 files, 29625 bytes\n\nFiles: references/standalone-harness.md (44388b), skill-card.md (2169b), SKILL.md (33744b), _meta.json (129b)\n\nArchive v1.0.10: 4 files, 29420 bytes\n\nFiles: references/standalone-harness.md (44388b), skill-card.md (2399b), SKILL.md (32974b), _meta.json (129b)\n\nArchive v1.0.9: 4 files, 29458 bytes\n\nFiles: references/standalone-harness.md (44388b), skill-card.md (2469b), SKILL.md (32961b), _meta.json (128b)\n\nArchive v1.0.8: 4 files, 29327 bytes\n\nFiles: references/standalone-harness.md (44388b), skill-card.md (2171b), SKILL.md (32948b), _meta.json (128b)\n\nArchive v1.0.7: 4 files, 29245 bytes\n\nFiles: references/standalone-harness.md (44388b), skill-card.md (1942b), SKILL.md (35792b), _meta.json (128b)","readmeExcerpt":"Skill: slideshow Owner: heygen-com Summary: Author a HyperFrames slideshow — a presentation, pitch deck, or interactive deck with discrete slides, fragment reveals, branching, hotspot navigation, and built-in presenter mode with speaker notes; also converts an existing page into a deck. Output is a navigable deck, not a rendered MP4. If the user didn't explicitly ask for a slideshow, confirm before authoring. Unclear","codeSnippets":[],"executableExamples":[{"language":"html","snippet":"<div\n  data-composition-id=\"problem\"\n  data-start=\"0\"\n  data-duration=\"8\"\n  data-label=\"The problem\"\n  data-width=\"1920\"\n  data-height=\"1080\"\n>\n  <!-- clips go here -->\n</div>"},{"language":"html","snippet":"<script type=\"application/hyperframes-slideshow+json\">\n  {\n    \"slides\": [...],\n    \"slideSequences\": [...]\n  }\n</script>"},{"language":"json","snippet":"{\n  \"slides\": [\n    /* SlideRef[] — the main line, in order */\n  ],\n  \"slideSequences\": [\n    /* SlideSequence[] — off-line branch sequences */\n  ]\n}"},{"language":"json","snippet":"{\n  \"sceneId\": \"problem\",\n  \"notes\": \"Lead with the pain, not the company.\",\n  \"fragments\": [3.5, 5.2, 7.0],\n  \"hotspots\": [\n    /* SlideHotspot[] */\n  ],\n\n  \"ttsScript\": null,\n  \"ttsAudioUrl\": null,\n  \"ttsDurationMs\": null\n}"},{"language":"json","snippet":"{\n  \"id\": \"h1\",\n  \"label\": \"How did we calculate this?\",\n  \"target\": \"market-deep-dive\",\n  \"region\": { \"x\": 60, \"y\": 10, \"w\": 35, \"h\": 20 }\n}"},{"language":"json","snippet":"{\n  \"id\": \"market-deep-dive\",\n  \"label\": \"Market sizing methodology\",\n  \"slides\": [{ \"sceneId\": \"mkt-1\" }, { \"sceneId\": \"mkt-2\" }]\n}"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: slideshow\ndescription: >\n  Author a HyperFrames slideshow — a presentation, pitch deck, or interactive\n  deck with discrete slides, fragment reveals, branching, hotspot navigation,\n  and built-in presenter mode with speaker notes; also converts an existing\n  page into a deck. Output is a navigable deck, not a rendered MP4. If the\n  user didn't explicitly ask for a slideshow, confirm before authoring.\n  Unclear → /hyperframes.\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> **First, keep this skill fresh — confirm with the user before running:** `npx hyperframes skills update slideshow`. A fast no-op when everything is current; otherwise it refreshes this skill plus the core domain skills it depends on before you rely on them.\n\n> **figma source**: If the deck's content or storyboard comes from a figma.com URL, run `/figma` first — asset export, brand tokens, and storyboard reconstruction if the source is a strip of scene frames — then build from its output. Don't drive Figma via raw MCP tools directly: that skips SVG sanitization, `.media/manifest.jsonl` provenance, and brand-token `var()` binding, so a later brand change can't propagate without a full re-import.\n\n# Slideshow authoring contract\n\nA HyperFrames slideshow is a normal HyperFrames composition — scenes, clips, GSAP timelines — with one extra ingredient: a **JSON island** that declares which scenes are slides and how they connect. The player's `SlideshowController` reads the island and turns the continuous GSAP timeline into a discrete, navigable deck.\n\n**Read `/hyperframes-core` first** for the base composition contract (clips, tracks, `data-*` attributes, determinism rules). This skill covers only what is new: the island schema, slide writing rules, fragments, branching, validation, and the wrapping component.\n\n## Output — a navigable deck, not a linear MP4\n\nA slideshow's output is the **running deck**: serve it with `hyperframes present <project-dir>` (or Studio present mode) — the player's `SlideshowController` reads the island and drives navigation, fragments, branching, and presenter mode. See **Presenting and handoff** below.\n\n**Do not `hyperframes render` a slideshow into a single MP4.** A deck is authored as several top-level scene compositions (one `data-composition-id` per slide) with **no master-root composition** wrapping them, so `render` resolves only the **first** composition and emits a **silently truncated** MP4 (e.g. 6s of a 40-second deck). A linear main-line export (main slides only, branch sequences excluded) is **deferred** — until it ships, the supported outputs are the live `present` deck and per-slide `snapshot` stills. If a user needs a linear MP4 today, surface this limitation rather than pointing `render` at the deck.\n\n## Intent confirmation\n\nIf the "},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn77d06grj6xqp3dqwkk4bavhn89pegt\",\n  \"slug\": \"slideshow\",\n  \"version\": \"1.0.16\",\n  \"publishedAt\": 1791142419358\n}"},{"path":"references/standalone-harness.md","content":"# Standalone HyperFrames Slideshow Harness\n\n## 1. Interim framing — why this exists\n\nThese patterns are a **temporary workaround** for standalone demos. The durable solution is engine-hosted: a future `hyperframes preview --slideshow` / studio present mode will host the composition over the real HyperFrames engine, which drives seek-timelines frame-by-frame, owns the gesture frame, and reads the slideshow island directly from the composition. When that path ships, most of what follows collapses.\n\nUntil then, a standalone slideshow opened via the bare player bundle must work around three facts:\n\n1. The composition must expose a seekable `window.__timelines.root` timeline. Anything outside that seek path, such as Three.js loops or imperative entrance effects, must be self-driving.\n2. `<hyperframes-slideshow>` reads the slideshow island from its **own innerHTML** (the wrapper element), not from the composition the player loads. The island must be duplicated into the wrapper.\n3. The composition runs in the player's **iframe**; user keypresses and pointer events land on the **parent page**. Wrapper-owned SFX/global audio should live in the parent, where the activation token is reliable. Normal slide media stays in the composition and is stopped by the slideshow player on slide exit.\n\nDo not treat these as the blessed authoring model. When the engine-hosted path ships, compositions authored the normal way will just work.\n\n**Living reference implementations:**\n\n---\n\n## 2. The parent wrapper (`index.html` for deliverables, `demo.html` in examples)\n\nThe parent page hosts the two dist bundles, wraps the components, duplicates the island, and owns all audio.\n\nFor public or user-facing generated projects, make this wrapper the root `index.html` so opening the project in a browser runs the slideshow. Put the raw HyperFrames composition in a separate path such as `composition/index.html`. In repo examples you may still see this file called `demo.html`; that name is a reference pattern, not the preferred handoff for a standalone deck.\n\n```html\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>My Deck — Slideshow Demo</title>\n\n    <!--\n      Load both bundles from packages/player/dist.\n      The global builds register <hyperframes-player> and <hyperframes-slideshow>\n      as custom elements — no import map needed.\n    -->\n    <script src=\"../../../packages/player/dist/hyperframes-player.global.js\"></script>\n    <script src=\"../../../packages/player/dist/slideshow/hyperframes-slideshow.global.js\"></script>\n\n    <style>\n      *,\n      *::before,\n      *::after {\n        box-sizing: border-box;\n        margin: 0;\n        padding: 0;\n      }\n      html,\n      body {\n        width: 100%;\n        height: 100%;\n        overflow: hidden;\n        background: #111;\n      }\n    </style>\n  </head>\n  <body>\n    <!--\n      tabindex=\"0\" is critical — <hyperframes-slides"},{"path":"skill-card.md","content":"## Description:\n\nAuthors navigable HyperFrames presentations with slide reveals, branching, hotspots, and presenter notes, including decks converted from existing pages.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[heygen-com](https://clawhub.ai/user/heygen-com)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nCreators, designers, and developers use this skill to build or adapt interactive slide decks with presenter notes and branching navigation.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Live package commands may fetch and run changed code or update installed skills.\n\nMitigation: Review commands before execution and use a lockfile-backed local HyperFrames install or exact-version commands.\n\nRisk: Standalone decks may load optional browser dependencies from a third-party CDN.\n\nMitigation: Vendor optional dependencies such as Three.js when distributing standalone decks.\n\n## Reference(s):\n\n- [Standalone slideshow harness](references/standalone-harness.md)\n\n## Skill Output:\n\n**Output Type(s):** [Code, Configuration instructions, Guidance]\n\n**Output Format:** [HTML and JSON deck files with Markdown guidance]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Produces a navigable presentation and optional per-slide stills, not a single MP4.]\n\n## Skill Version(s):\n\n1.0.16 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment."}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":"Author a HyperFrames slideshow — a presentation, pitch deck, or interactive deck with discrete slides, fragment reveals, branching, hotspot navigation, and built-in presenter mode with speaker notes; also converts an existing page into a deck. Output is a navigable deck, not a rendered MP4. If the user didn't explicitly ask for a slideshow, confirm before authoring. Unclear → /hyperframes. Skill: slideshow Owner: heygen-com Summary: Author a HyperFrames slideshow — a presentation, pitch deck, or interactive deck with discrete slides, fragment reveals, branching, hotspot navigation, and built-in presenter mode with speaker notes; also converts an existing page into a deck. Output is a navigable deck, not a rendered MP4. If the user didn't explicitly ask for a slideshow, confirm before authoring. Unclear","editorialQuality":{"score":100,"threshold":65,"status":"ready","wordCount":1464,"uniquenessScore":49,"reasons":[]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-09T21:44:35.069Z","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-09T21:44:35.069Z","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-10T07:45:12.937Z","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"}]}}}