Files
hyperframes/docs/public/catalog/blocks/beat-freeze-cut.json
T
Miguel Ángel 536165b6ef feat(catalog): play the real composition on catalog pages (#3168)
* feat(catalog): play the real composition on catalog pages

Catalog previews were uploaded MP4s, published by hand to the CDN. Each page
now embeds the composition itself, running in <hyperframes-player>, so a
preview is the block rather than a recording of it.

The composition is delivered as JSON under docs/public/catalog, and the player
is mounted inside an iframe. Both are forced: the docs host publishes only JSON
and images out of docs/public, and its MDX renderer strips unknown custom
elements, so a player written into the page never reaches the DOM and an .html
payload 404s in production.

Assets are inlined as data URIs, which sidesteps the file-type restriction for
fonts, scripts and models alike. 164 of 168 items build a payload; the rest
keep their MP4 and say so.

* chore: rebuild docs preview

* chore: route isolation probe

* chore: drop the route isolation probe page

* chore: probe which asset types the docs host publishes

* refactor(catalog): host preview assets instead of inlining them

Fonts were being base64'd into every payload that used them, so a handful of
files cost tens of megabytes in the repository to say the same thing over and
over. Assets are now written once, content-addressed, under
docs/public/catalog/assets and linked.

Which types can be hosted was settled by fetching one file of each from a
deployed preview: woff2, wav, mp4, svg and the image formats are published,
glb is not. Types the host drops still travel inside the payload, because a
link that 404s is worse than a larger payload.

Also fixes the four type errors the scripts typecheck caught, all of them
unchecked index access on a split() result.

* refactor(catalog): share preview assets instead of repeating them

* style: format the asset module
2026-08-10 17:54:48 -04:00

1 line
28 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"html":"<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=1920, height=1080\" />\n <title>Beat Freeze Cut</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\n <link\n href=\"https://fonts.googleapis.com/css2?family=Inter:wght@500;600;700;800;900&display=block\"\n rel=\"stylesheet\"\n />\n <script src=\"https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js\"></script>\n <style>\n *,\n *::before,\n *::after {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n\n html,\n body {\n width: 1920px;\n height: 1080px;\n margin: 0;\n overflow: hidden;\n background: #0b0d10;\n }\n\n /*\n EDIT / STUDIO PARAMS\n ────────────────────\n Studio color params (CSS custom properties on the root):\n --bg-color default #0B0D10\n --accent-color default #00E5C7\n\n Numeric / string params (beat times, labels, intensity) are NOT in the\n formal registry params schema today (only color keys are used by\n existing blocks). Edit the CONFIG object in the script below instead.\n */\n\n #bfc-root {\n position: relative;\n width: 1920px;\n height: 1080px;\n overflow: hidden;\n --bg-color: #0b0d10;\n --accent-color: #00e5c7;\n --bfc-ink: #f4f6f8;\n --bfc-muted: #a0a8b0;\n --bfc-panel: #14181f;\n --bfc-panel-2: #1a2029;\n --bfc-line: rgba(255, 255, 255, 0.08);\n font-family:\n \"Inter\",\n system-ui,\n -apple-system,\n sans-serif;\n color: var(--bfc-ink);\n }\n\n #bfc-bg {\n position: absolute;\n inset: 0;\n background:\n radial-gradient(ellipse 80% 60% at 50% 40%, rgba(0, 229, 199, 0.07) 0%, transparent 55%),\n radial-gradient(\n ellipse 50% 40% at 80% 80%,\n rgba(255, 255, 255, 0.03) 0%,\n transparent 50%\n ),\n var(--bg-color);\n }\n\n #bfc-grid {\n position: absolute;\n inset: 0;\n opacity: 0.22;\n background-image:\n linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px),\n linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px);\n background-size: 80px 80px;\n mask-image: radial-gradient(ellipse 70% 60% at 50% 45%, #000 20%, transparent 75%);\n pointer-events: none;\n }\n\n #bfc-stage {\n position: absolute;\n inset: 0;\n transform-origin: 50% 48%;\n will-change: transform, filter;\n }\n\n #bfc-shot-a,\n #bfc-shot-b {\n position: absolute;\n inset: 0;\n overflow: hidden;\n }\n\n #bfc-shot-b {\n visibility: hidden;\n opacity: 0;\n }\n\n /* ── Shot A: single product card hero ── */\n #bfc-a-crop {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n transform-origin: 50% 50%;\n }\n\n #bfc-a-card {\n position: relative;\n width: 680px;\n height: 720px;\n border-radius: 36px;\n background:\n linear-gradient(160deg, rgba(255, 255, 255, 0.06) 0%, transparent 40%),\n linear-gradient(180deg, var(--bfc-panel-2) 0%, var(--bfc-panel) 100%);\n border: 1px solid var(--bfc-line);\n box-shadow:\n 0 40px 120px rgba(0, 0, 0, 0.55),\n 0 0 0 1px rgba(0, 229, 199, 0.08),\n inset 0 1px 0 rgba(255, 255, 255, 0.06);\n overflow: hidden;\n transform-origin: 50% 50%;\n }\n\n #bfc-a-card-glow {\n position: absolute;\n left: 50%;\n top: 28%;\n width: 420px;\n height: 420px;\n margin-left: -210px;\n margin-top: -210px;\n border-radius: 50%;\n background: radial-gradient(circle, rgba(0, 229, 199, 0.28) 0%, transparent 68%);\n opacity: 0.85;\n pointer-events: none;\n }\n\n #bfc-a-wave {\n position: absolute;\n left: 64px;\n right: 64px;\n top: 120px;\n height: 220px;\n }\n\n #bfc-a-wave svg {\n width: 100%;\n height: 100%;\n display: block;\n }\n\n #bfc-a-wave-path {\n fill: none;\n stroke: var(--accent-color);\n stroke-width: 4;\n stroke-linecap: round;\n stroke-linejoin: round;\n opacity: 0.95;\n }\n\n #bfc-a-wave-fill {\n fill: var(--accent-color);\n opacity: 0.12;\n }\n\n #bfc-a-bars {\n position: absolute;\n left: 80px;\n right: 80px;\n bottom: 160px;\n height: 96px;\n display: flex;\n align-items: flex-end;\n gap: 10px;\n }\n\n .bfc-bar {\n flex: 1;\n border-radius: 6px 6px 2px 2px;\n background: linear-gradient(180deg, var(--accent-color) 0%, rgba(0, 229, 199, 0.25) 100%);\n transform-origin: 50% 100%;\n height: 40%;\n }\n\n #bfc-a-meta {\n position: absolute;\n left: 64px;\n right: 64px;\n bottom: 56px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n }\n\n #bfc-a-kicker {\n font-size: 18px;\n font-weight: 600;\n letter-spacing: 0.18em;\n text-transform: uppercase;\n color: var(--bfc-muted);\n }\n\n #bfc-a-pill {\n font-size: 15px;\n font-weight: 700;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: #04120f;\n background: var(--accent-color);\n padding: 8px 16px;\n border-radius: 999px;\n }\n\n /* ── Shot B: split layout, higher info density ── */\n #bfc-b-layout {\n position: absolute;\n inset: 0;\n display: grid;\n grid-template-columns: 1.05fr 0.95fr;\n gap: 48px;\n padding: 100px 110px;\n align-items: center;\n }\n\n #bfc-b-left {\n display: flex;\n flex-direction: column;\n gap: 28px;\n transform-origin: 0% 50%;\n }\n\n #bfc-b-eyebrow {\n font-size: 20px;\n font-weight: 700;\n letter-spacing: 0.22em;\n text-transform: uppercase;\n color: var(--accent-color);\n }\n\n #bfc-b-title {\n font-size: 128px;\n font-weight: 900;\n line-height: 0.92;\n letter-spacing: -0.04em;\n text-transform: uppercase;\n color: var(--bfc-ink);\n }\n\n #bfc-b-sub {\n font-size: 28px;\n font-weight: 500;\n color: var(--bfc-muted);\n max-width: 520px;\n line-height: 1.35;\n }\n\n #bfc-b-right {\n display: flex;\n flex-direction: column;\n gap: 18px;\n }\n\n .bfc-b-card {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 24px;\n padding: 28px 32px;\n border-radius: 20px;\n background: linear-gradient(135deg, var(--bfc-panel-2), var(--bfc-panel));\n border: 1px solid var(--bfc-line);\n box-shadow: 0 18px 48px rgba(0, 0, 0, 0.35);\n }\n\n .bfc-b-card-label {\n font-size: 18px;\n font-weight: 600;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--bfc-muted);\n }\n\n .bfc-b-card-value {\n font-size: 42px;\n font-weight: 800;\n letter-spacing: -0.03em;\n color: var(--bfc-ink);\n }\n\n .bfc-b-card-value.accent {\n color: var(--accent-color);\n }\n\n #bfc-b-accent-bar {\n width: 120px;\n height: 6px;\n border-radius: 999px;\n background: var(--accent-color);\n opacity: 0.9;\n }\n\n /* ── Intro / transient typography ── */\n #bfc-intro {\n position: absolute;\n left: 0;\n right: 0;\n top: 72px;\n display: flex;\n justify-content: center;\n pointer-events: none;\n z-index: 20;\n opacity: 0;\n visibility: hidden;\n }\n\n #bfc-intro-label {\n font-size: 18px;\n font-weight: 700;\n letter-spacing: 0.28em;\n text-transform: uppercase;\n color: var(--bfc-muted);\n padding: 10px 18px;\n border: 1px solid var(--bfc-line);\n border-radius: 999px;\n background: rgba(11, 13, 16, 0.55);\n backdrop-filter: blur(8px);\n }\n\n #bfc-hit-type {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n pointer-events: none;\n z-index: 30;\n opacity: 0;\n visibility: hidden;\n }\n\n #bfc-hit-type-text {\n font-size: 160px;\n font-weight: 900;\n letter-spacing: -0.04em;\n text-transform: uppercase;\n color: var(--bfc-ink);\n text-shadow:\n 0 0 40px rgba(0, 229, 199, 0.35),\n 0 8px 40px rgba(0, 0, 0, 0.55);\n -webkit-text-stroke: 2px rgba(0, 0, 0, 0.35);\n paint-order: stroke fill;\n }\n\n /* ── Freeze frame treatment ── */\n #bfc-freeze {\n position: absolute;\n inset: 0;\n pointer-events: none;\n z-index: 25;\n opacity: 0;\n visibility: hidden;\n }\n\n #bfc-freeze-outline {\n position: absolute;\n inset: 48px;\n border: 3px solid var(--accent-color);\n border-radius: 8px;\n box-shadow:\n 0 0 0 1px rgba(0, 229, 199, 0.25),\n inset 0 0 0 1px rgba(0, 229, 199, 0.15),\n 0 0 48px rgba(0, 229, 199, 0.2);\n opacity: 0;\n }\n\n #bfc-freeze-flash {\n position: absolute;\n inset: 0;\n background: #ffffff;\n opacity: 0;\n mix-blend-mode: screen;\n }\n\n #bfc-freeze-contour {\n position: absolute;\n inset: 0;\n background:\n linear-gradient(90deg, transparent 0%, rgba(0, 229, 199, 0.08) 48%, transparent 52%),\n radial-gradient(ellipse 40% 55% at 50% 48%, transparent 40%, rgba(0, 229, 199, 0.18) 100%);\n opacity: 0;\n mix-blend-mode: screen;\n }\n\n #bfc-freeze-badge {\n position: absolute;\n top: 80px;\n right: 96px;\n font-size: 16px;\n font-weight: 800;\n letter-spacing: 0.2em;\n text-transform: uppercase;\n color: #04120f;\n background: var(--accent-color);\n padding: 10px 18px;\n border-radius: 6px;\n opacity: 0;\n }\n\n /* ── Hard-cut connector (directional stretch / blur) ── */\n #bfc-cut {\n position: absolute;\n inset: -8% -20%;\n pointer-events: none;\n z-index: 40;\n opacity: 0;\n visibility: hidden;\n background: linear-gradient(\n 90deg,\n transparent 0%,\n rgba(0, 229, 199, 0.12) 35%,\n rgba(255, 255, 255, 0.55) 50%,\n rgba(0, 229, 199, 0.12) 65%,\n transparent 100%\n );\n filter: blur(18px);\n transform-origin: 50% 50%;\n }\n\n #bfc-vignette {\n position: absolute;\n inset: 0;\n pointer-events: none;\n z-index: 15;\n background: radial-gradient(\n ellipse 75% 70% at 50% 50%,\n transparent 45%,\n rgba(0, 0, 0, 0.55) 100%\n );\n opacity: 0.85;\n }\n\n #bfc-driver {\n position: absolute;\n width: 1px;\n height: 1px;\n opacity: 0;\n pointer-events: none;\n }\n </style>\n </head>\n <body>\n <div\n id=\"bfc-root\"\n data-composition-id=\"beat-freeze-cut\"\n data-start=\"0\"\n data-duration=\"6\"\n data-width=\"1920\"\n data-height=\"1080\"\n >\n <div id=\"bfc-bg\" aria-hidden=\"true\"></div>\n <div id=\"bfc-grid\" aria-hidden=\"true\"></div>\n\n <div id=\"bfc-stage\">\n <!-- Shot A -->\n <div id=\"bfc-shot-a\">\n <div id=\"bfc-a-crop\" data-layout-allow-overflow>\n <div id=\"bfc-a-card\" data-layout-allow-overflow>\n <div id=\"bfc-a-card-glow\" aria-hidden=\"true\"></div>\n <div id=\"bfc-a-wave\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 592 220\" preserveAspectRatio=\"none\">\n <path\n id=\"bfc-a-wave-fill\"\n d=\"M0,160 C40,150 60,90 100,100 C140,110 160,40 200,55 C240,70 260,150 300,140 C340,130 360,30 400,45 C440,60 460,130 500,120 C540,110 560,70 592,80 L592,220 L0,220 Z\"\n />\n <path\n id=\"bfc-a-wave-path\"\n d=\"M0,160 C40,150 60,90 100,100 C140,110 160,40 200,55 C240,70 260,150 300,140 C340,130 360,30 400,45 C440,60 460,130 500,120 C540,110 560,70 592,80\"\n />\n </svg>\n </div>\n <div id=\"bfc-a-bars\" aria-hidden=\"true\">\n <div class=\"bfc-bar\" id=\"bfc-bar-0\" style=\"height: 34%\"></div>\n <div class=\"bfc-bar\" id=\"bfc-bar-1\" style=\"height: 58%\"></div>\n <div class=\"bfc-bar\" id=\"bfc-bar-2\" style=\"height: 42%\"></div>\n <div class=\"bfc-bar\" id=\"bfc-bar-3\" style=\"height: 78%\"></div>\n <div class=\"bfc-bar\" id=\"bfc-bar-4\" style=\"height: 52%\"></div>\n <div class=\"bfc-bar\" id=\"bfc-bar-5\" style=\"height: 88%\"></div>\n <div class=\"bfc-bar\" id=\"bfc-bar-6\" style=\"height: 46%\"></div>\n <div class=\"bfc-bar\" id=\"bfc-bar-7\" style=\"height: 64%\"></div>\n <div class=\"bfc-bar\" id=\"bfc-bar-8\" style=\"height: 36%\"></div>\n <div class=\"bfc-bar\" id=\"bfc-bar-9\" style=\"height: 72%\"></div>\n <div class=\"bfc-bar\" id=\"bfc-bar-10\" style=\"height: 48%\"></div>\n <div class=\"bfc-bar\" id=\"bfc-bar-11\" style=\"height: 92%\"></div>\n </div>\n <div id=\"bfc-a-meta\">\n <span id=\"bfc-a-kicker\">MUSIC PROMO</span>\n <span id=\"bfc-a-pill\">LIVE</span>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Shot B -->\n <div id=\"bfc-shot-b\">\n <div id=\"bfc-b-layout\">\n <div id=\"bfc-b-left\">\n <div id=\"bfc-b-eyebrow\">Next shot</div>\n <div id=\"bfc-b-title\">HARD<br />CUT</div>\n <div id=\"bfc-b-accent-bar\" aria-hidden=\"true\"></div>\n <div id=\"bfc-b-sub\">\n Beat-locked freeze, then cut. Built for music-led promos and montages.\n </div>\n </div>\n <div id=\"bfc-b-right\">\n <div class=\"bfc-b-card\" id=\"bfc-b-card-0\">\n <span class=\"bfc-b-card-label\">Ramp</span>\n <span class=\"bfc-b-card-value accent\">1.35→2.2</span>\n </div>\n <div class=\"bfc-b-card\" id=\"bfc-b-card-1\">\n <span class=\"bfc-b-card-label\">Freeze</span>\n <span class=\"bfc-b-card-value\">0.8s</span>\n </div>\n <div class=\"bfc-b-card\" id=\"bfc-b-card-2\">\n <span class=\"bfc-b-card-label\">Cut</span>\n <span class=\"bfc-b-card-value accent\">3.00</span>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <div id=\"bfc-vignette\" aria-hidden=\"true\"></div>\n\n <div id=\"bfc-intro\" aria-hidden=\"true\">\n <span id=\"bfc-intro-label\">ON THE BEAT</span>\n </div>\n\n <div id=\"bfc-hit-type\" aria-hidden=\"true\">\n <span id=\"bfc-hit-type-text\">DROP</span>\n </div>\n\n <div id=\"bfc-freeze\" aria-hidden=\"true\">\n <div id=\"bfc-freeze-flash\"></div>\n <div id=\"bfc-freeze-contour\"></div>\n <div id=\"bfc-freeze-outline\"></div>\n <div id=\"bfc-freeze-badge\">FREEZE</div>\n </div>\n\n <div id=\"bfc-cut\" aria-hidden=\"true\"></div>\n\n <div id=\"bfc-driver\" class=\"clip\" data-start=\"0\" data-duration=\"6\" data-track-index=\"0\"></div>\n </div>\n\n <script>\n (function () {\n window.__timelines = window.__timelines || {};\n\n // ── EDIT ME (not Studio params — see HTML comment) ──────────────\n var CONFIG = {\n primaryLabel: \"DROP\",\n secondaryLabel: \"ON THE BEAT\",\n beat1: 0.7,\n beat2: 2.2,\n freezeTime: 2.2,\n intensity: 1.0,\n };\n // ────────────────────────────────────────────────────────────────\n\n var DURATION = 6;\n var intensity = Math.max(0.6, Math.min(1.4, Number(CONFIG.intensity) || 1));\n var beat1 = Number(CONFIG.beat1) || 0.7;\n var beat2 = Number(CONFIG.beat2) || 2.2;\n var freezeAt = Number(CONFIG.freezeTime) || beat2;\n var cutAt = 3.0;\n var beat3 = 4.7;\n var holdAt = 5.2;\n\n var root = document.getElementById(\"bfc-root\");\n var stage = document.getElementById(\"bfc-stage\");\n var shotA = document.getElementById(\"bfc-shot-a\");\n var shotB = document.getElementById(\"bfc-shot-b\");\n var aCrop = document.getElementById(\"bfc-a-crop\");\n var aCard = document.getElementById(\"bfc-a-card\");\n var intro = document.getElementById(\"bfc-intro\");\n var introLabel = document.getElementById(\"bfc-intro-label\");\n var hitType = document.getElementById(\"bfc-hit-type\");\n var hitText = document.getElementById(\"bfc-hit-type-text\");\n var freeze = document.getElementById(\"bfc-freeze\");\n var freezeOutline = document.getElementById(\"bfc-freeze-outline\");\n var freezeFlash = document.getElementById(\"bfc-freeze-flash\");\n var freezeContour = document.getElementById(\"bfc-freeze-contour\");\n var freezeBadge = document.getElementById(\"bfc-freeze-badge\");\n var cutFx = document.getElementById(\"bfc-cut\");\n var bLeft = document.getElementById(\"bfc-b-left\");\n var bCards = [\n document.getElementById(\"bfc-b-card-0\"),\n document.getElementById(\"bfc-b-card-1\"),\n document.getElementById(\"bfc-b-card-2\"),\n ];\n var bars = [];\n for (var bi = 0; bi < 12; bi++) {\n bars.push(document.getElementById(\"bfc-bar-\" + bi));\n }\n\n // Apply editable labels\n if (hitText) hitText.textContent = CONFIG.primaryLabel || \"DROP\";\n if (introLabel) introLabel.textContent = CONFIG.secondaryLabel || \"ON THE BEAT\";\n\n // Accent color from CSS var → freeze/glow that use hardcoded rgba fallbacks\n // stay coherent because stroke/fill already use var(--accent-color).\n\n var tl = gsap.timeline({ paused: true });\n\n // Initial states (no CSS transform conflicts on animated props)\n gsap.set(stage, { scale: 1, x: 0, y: 0, filter: \"blur(0px) brightness(1)\" });\n gsap.set(aCrop, { scale: 1, x: 0, y: 0 });\n // Card visible at t=0 so catalog snapshots are never a black frame\n gsap.set(aCard, { scale: 0.9, y: 20, opacity: 1 });\n gsap.set(shotA, { opacity: 1, visibility: \"visible\" });\n gsap.set(shotB, { opacity: 0, visibility: \"hidden\" });\n gsap.set(intro, { opacity: 0, visibility: \"hidden\", y: -12 });\n gsap.set(hitType, { opacity: 0, visibility: \"hidden\", scale: 1.15 });\n gsap.set(freeze, { opacity: 0, visibility: \"hidden\" });\n gsap.set([freezeOutline, freezeFlash, freezeContour, freezeBadge], { opacity: 0 });\n gsap.set(cutFx, { opacity: 0, visibility: \"hidden\", scaleX: 0.4, x: -200 });\n gsap.set(bLeft, { opacity: 0, x: -48 });\n gsap.set(bCards, { opacity: 0, x: 40 });\n gsap.set(bars, { scaleY: 0.55 });\n\n // Full duration anchor (seek-safe length)\n tl.to({}, { duration: DURATION, ease: \"none\" }, 0);\n\n // ── 0.000.70: intro push-in + secondary type ──\n tl.to(aCard, { scale: 1, y: 0, duration: 0.55, ease: \"power3.out\" }, 0);\n tl.to(stage, { scale: 1 + 0.04 * intensity, duration: 0.7, ease: \"power1.out\" }, 0);\n tl.to(bars, { scaleY: 1, duration: 0.5, ease: \"power2.out\", stagger: 0.02 }, 0.12);\n tl.set(intro, { visibility: \"visible\" }, 0.12);\n tl.to(intro, { opacity: 1, y: 0, duration: 0.28, ease: \"power2.out\" }, 0.12);\n tl.to(intro, { opacity: 0, y: -8, duration: 0.18, ease: \"power2.in\" }, 0.52);\n tl.set(intro, { visibility: \"hidden\", opacity: 0 }, 0.7);\n\n // ── 0.701.35: beat 1 — hard reframe ──\n tl.to(\n aCrop,\n {\n scale: 1 + 0.18 * intensity,\n x: -80 * intensity,\n y: 24 * intensity,\n duration: 0.22,\n ease: \"power4.out\",\n },\n beat1,\n );\n tl.to(\n stage,\n {\n scale: 1.08 * intensity > 1.2 ? 1.2 : 1 + 0.08 * intensity,\n duration: 0.28,\n ease: \"expo.out\",\n },\n beat1,\n );\n tl.to(\n bars,\n {\n scaleY: function (i) {\n // Deterministic bar pattern (no Math.random)\n var pattern = [0.55, 1.15, 0.7, 1.35, 0.9, 1.4, 0.65, 1.1, 0.5, 1.25, 0.8, 1.45];\n return pattern[i % pattern.length];\n },\n duration: 0.18,\n ease: \"power3.out\",\n stagger: 0.01,\n },\n beat1,\n );\n\n // ── 1.352.20: speed ramp into freeze ──\n var rampStart = 1.35;\n var rampDur = Math.max(0.2, freezeAt - rampStart);\n tl.to(\n aCrop,\n {\n scale: 1.28 * (0.9 + 0.1 * intensity),\n x: 28 * intensity,\n y: -12 * intensity,\n duration: rampDur,\n ease: \"power3.in\",\n },\n rampStart,\n );\n tl.to(\n stage,\n {\n scale: 1.12,\n filter: \"blur(0px) brightness(1.08)\",\n duration: rampDur,\n ease: \"power2.in\",\n },\n rampStart,\n );\n tl.to(\n bars,\n {\n scaleY: 1.55,\n duration: rampDur * 0.85,\n ease: \"power2.in\",\n stagger: { each: 0.012, from: \"center\" },\n },\n rampStart,\n );\n\n // ── 2.203.00: freeze hit (snap-on for seek/snapshot stability) ──\n tl.set(freeze, { visibility: \"visible\", opacity: 1 }, freezeAt);\n tl.set(freezeOutline, { opacity: 1 }, freezeAt);\n tl.set(freezeContour, { opacity: 0.85 }, freezeAt);\n tl.set(freezeBadge, { opacity: 1 }, freezeAt);\n tl.set(hitType, { visibility: \"visible\", opacity: 1, scale: 1 }, freezeAt);\n // Hold crop/stage where ramp ended\n tl.to(aCrop, { duration: 0.01, ease: \"none\" }, freezeAt);\n tl.to(\n stage,\n {\n scale: 1.12,\n filter: \"blur(0px) brightness(1.15)\",\n duration: 0.05,\n ease: \"power4.out\",\n },\n freezeAt,\n );\n // Single exposure flash (not a strobe sequence)\n tl.fromTo(\n freezeFlash,\n { opacity: 0.55 * intensity },\n { opacity: 0, duration: 0.28, ease: \"power2.out\" },\n freezeAt,\n );\n tl.to(\n hitType,\n { opacity: 0, scale: 0.96, duration: 0.2, ease: \"power2.in\" },\n freezeAt + 0.55,\n );\n tl.set(hitType, { visibility: \"hidden\", opacity: 0 }, freezeAt + 0.78);\n // Soft settle of freeze dressing before cut\n tl.to(\n [freezeOutline, freezeContour, freezeBadge],\n { opacity: 0.75, duration: 0.35, ease: \"sine.out\" },\n freezeAt + 0.35,\n );\n\n // ── 3.003.35: hard cut connector ──\n tl.set(cutFx, { visibility: \"visible\" }, cutAt);\n tl.fromTo(\n cutFx,\n { opacity: 0, scaleX: 0.35, x: -280 },\n {\n opacity: 0.95,\n scaleX: 1.6,\n x: 120,\n duration: 0.12,\n ease: \"power4.in\",\n },\n cutAt,\n );\n tl.to(\n stage,\n {\n filter: \"blur(14px) brightness(1.35)\",\n scaleX: 1.35,\n scaleY: 0.92,\n duration: 0.12,\n ease: \"power3.in\",\n },\n cutAt,\n );\n // Switch shots mid-cut — shot B content lands immediately (no black gap)\n var switchAt = cutAt + 0.14;\n tl.set(shotA, { opacity: 0, visibility: \"hidden\" }, switchAt);\n tl.set(shotB, { opacity: 1, visibility: \"visible\" }, switchAt);\n tl.set(hitType, { opacity: 0, visibility: \"hidden\" }, switchAt);\n tl.set(\n [freeze, freezeOutline, freezeFlash, freezeContour, freezeBadge],\n { opacity: 0, visibility: \"hidden\" },\n switchAt,\n );\n tl.set(freeze, { visibility: \"hidden\" }, switchAt);\n // Shot B content visible on switch (offset settles after)\n tl.set(bLeft, { opacity: 1, x: -24 }, switchAt);\n tl.set(bCards, { opacity: 1, x: 24 }, switchAt);\n tl.to(\n stage,\n {\n filter: \"blur(0px) brightness(1)\",\n scale: 1,\n scaleX: 1,\n scaleY: 1,\n x: 0,\n y: 0,\n duration: 0.18,\n ease: \"power3.out\",\n },\n switchAt,\n );\n tl.to(cutFx, { opacity: 0, x: 360, duration: 0.16, ease: \"power2.out\" }, cutAt + 0.16);\n tl.set(cutFx, { visibility: \"hidden\", opacity: 0 }, cutAt + 0.34);\n\n // ── after switch: shot B settle (no scale overlap with recover) ──\n tl.to(bLeft, { x: 0, duration: 0.4, ease: \"power3.out\" }, switchAt);\n tl.to(\n bCards,\n {\n x: 0,\n duration: 0.38,\n ease: \"power3.out\",\n stagger: 0.06,\n },\n switchAt + 0.04,\n );\n tl.to(stage, { scale: 1.02, duration: 1.17, ease: \"sine.out\" }, switchAt + 0.18);\n\n // ── 4.705.20: final beat punch-in ──\n tl.to(\n stage,\n {\n scale: 1.08 * (0.9 + 0.1 * intensity),\n duration: 0.14,\n ease: \"power4.out\",\n },\n beat3,\n );\n tl.to(\n bCards[1],\n {\n scale: 1.04,\n duration: 0.12,\n ease: \"power3.out\",\n },\n beat3,\n );\n tl.to(\n bCards[1],\n {\n scale: 1,\n duration: 0.22,\n ease: \"power2.out\",\n },\n beat3 + 0.14,\n );\n // Micro freeze feel: brief hold of scale\n tl.to(\n stage,\n { scale: 1.08 * (0.9 + 0.1 * intensity), duration: 0.2, ease: \"none\" },\n beat3 + 0.14,\n );\n\n // ── 5.206.00: hold readable end card (≥0.8s) ──\n tl.to(stage, { scale: 1.03, duration: 0.35, ease: \"power2.out\" }, holdAt);\n // Ensure transient layers stay dead through end\n tl.set(intro, { opacity: 0, visibility: \"hidden\" }, holdAt);\n tl.set(hitType, { opacity: 0, visibility: \"hidden\" }, holdAt);\n tl.set(freeze, { opacity: 0, visibility: \"hidden\" }, holdAt);\n tl.set(cutFx, { opacity: 0, visibility: \"hidden\" }, holdAt);\n\n window.__timelines[\"beat-freeze-cut\"] = tl;\n })();\n </script>\n </body>\n</html>\n"}