{"project":{"id":"05e08qD","userId":"davidyarham@gmail.com","username":null,"userPicture":null,"name":"Blnq Launch Video","thumbnail":"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","visible":true,"contributors":"","githubRepo":null,"forkedFrom":null,"isTemplate":false,"tags":"","files":{"folder":"","files":[{"name":"CUESHEET.md","content":"# Cue Sheet — 56.0s @ 30fps (1680 frames)\n\nHard music anchors: **3** (drop / crash / hit). Everything else rides the grid.\n\n| TC | Frame | Event |\n|------|-------|-------|\n| 00:00.0 | 0 | Black. Blinking caret. \"Want to build a web page?\" |\n| 00:01.5 | 45 | Terminal 1 spawns — `npx create-everything@latest` |\n| 00:03.0 | 90 | Terminal 2 — `npm install` spinner, \"added 1,427 packages in 2m 41s\" |\n| 00:04.5 | 135 | Config cascade — webpack/babel/ts/eslint/postcss/vite… cards rain in |\n| 00:06.5 | 195 | Terminal 3 — red: `ERESOLVE peer dependency conflict`, `Cannot find module` |\n| 00:08.0 | 240 | Copy: \"Two hours. Zero pixels.\" Windows multiply, jitter builds |\n| 00:09.0 | 270 | Musical riser starts; visual shake ramps |\n| 00:10.85 | 325 | Hard blackout. 150ms dead silence (pre-drop gap) |\n| **00:11.0** | **330** | **ANCHOR 1 — THE DROP.** Chaos collapses into one browser-tab outline |\n| 00:12.0 | 360 | Tab reads `blnq.studio` — \"Or: just open a tab.\" |\n| 00:13.5 | 405 | Stamp: \"No install. No build step. No excuses.\" |\n| 00:15.0 | 450 | Editor chrome assembles from the tab (same frame geometry) |\n| 00:15.5–21.5 | 465–645 | `index.html` + CSS type in; preview pane renders live, element by element |\n| 00:23.0 | 690 | Preview splits into 5 viewports — iPhone · iPad · Pixel · Galaxy · Desktop |\n| 00:26.0 | 780 | Synced scroll sweeps all five simultaneously |\n| 00:28.0 | 840 | Synced click — ripple lands in every frame at once |\n| 00:30.0 | 900 | Audit sidebar slides in; WCAG scan runs |\n| 00:32.0 | 960 | RED FLAG: \"Contrast 3.2:1 — fails AA (needs 4.5:1)\" |\n| 00:33.5 | 1005 | Cursor hits \"Fix\" — code line patches `#6e6e6e 0 #e9e9e9` |\n| 00:34.5 | 1035 | Badge flips: \"AA pass — 12.1:1\". Yellow tick |\n| 00:36.5 | 1095 | Vision sim: Deuteranopia — filter hits all five frames at once |\n| 00:38.0 | 1140 | Vision sim: Cataracts |\n| 00:39.5 | 1185 | Filters clear |\n| 00:40.5 | 1215 | Ship menu opens: GitHub · Netlify · Vercel |\n| 00:41.5 | 1245 | Netlify selected; build log ticks; riser 2 starts |\n| **00:45.0** | **1350** | **ANCHOR 2 — THE CRASH.** URL chip slams in: `hoffs-bar.netlify.app` ● Live |\n| 00:46.0 | 1380 | Copy: \"Shipped. From a tab.\" Beat thins out |\n| **00:48.0** | **1440** | **ANCHOR 3 — FINAL HIT.** Cut to black; `blnq.` stamps |\n| 00:49.5 | 1485 | \"Code, Preview & Ship the Web.\" |\n| 00:51.5 | 1545 | CTA: \"Start building → blnq.studio\" |\n| 00:54.0–56.0 | 1620–1680 | Fade to silence/black |\n\n## Score brief\n- 0–11: tense ticking pulse, detuned low drone, glitch stabs; riser 9.0→10.85; **silence 10.85–11.0**\n- 11.0: drop — dark four-on-floor (120bpm), sub bass, offbeat hats\n- 41.5–45.0: riser 2; **45.0: the only crash** (noise burst + low boom)\n- 45–48: beat thins to pulse + pad\n- **48.0: final hit** (boom + tonal stab); pad decays to nothing by 56.0\n- Engineering gate: RMS step ≥ 20dB within ±0.05s at 11.0; crash peak at 45.0 ±0.05s; hit at 48.0 ±0.05s\n"},{"name":"DIRECTION.md","content":"# Blnq Launch Film — Direction\n\n## Extracted palette (from live `site.bundle.css` — verbatim, not invented)\n\n| Token | Value | Source |\n|---|---|---|\n| Accent (blnq yellow) | `#ffd300` | `--color-bg-accent` |\n| Text on accent | `#1e1e1e` | `--color-text-accent` |\n| Surface dark | `#0e0f12` | `--color-bg-secondary` |\n| Background | `#000000` (bg-primary = bg-secondary mixed 55% black ≈ `#08080a`) | `--color-bg-primary` |\n| Slate hue (borders/tertiary) | `#4a5568` | `--color-hue` |\n| Danger red | `#ff4455` | `--site-danger: #f45` |\n| Blue (dark theme links) | `#438efe` | `--ui-blue` |\n| Warning amber | `#f59e0b` | toast styles |\n| Light neutral | `#f0f1f3` | light theme bg |\n| Type | Inter / system-ui; code: ui-monospace, Menlo, Monaco, Consolas | `--site-font` + code blocks |\n| Radius | 15px | `--ui-radius` |\n| Ease | cubic-bezier(.42, 0, .58, 1) | `--transition-ease` |\n\nOne accent. Yellow is earned, not sprayed: it appears only when blnq acts — the tab, the fix, the live URL, the logo.\n\n## Brand canon (every line of copy must pass this)\n\nBlnq speaks in short declaratives with the swagger of someone who already shipped. Terse, confident, faintly cheeky — \"No install. No build step. No excuses.\" The villain is the modern front-end toolchain — installs, build steps, config sprawl, framework lock-in — never the viewer; we mock the paradigm, not the person. The thesis is one tab: code, five synced viewports, WCAG audits, vision and sensory simulation, optional bring-your-own-key AI, and one-click deploy all live where the page loads. Every claim on screen must be literally true today: 5 synced viewports (iPhone, iPad, Pixel, Galaxy, desktop) with synced clicks/scrolls; built-in WCAG + contrast + keyboard checks; vision sim (colour blindness, glaucoma, cataracts) and sensory filters; AI with your key (Claude, GPT, Gemini) in an agentic loop with rewind; MCP for Claude Code/Cursor/Windsurf; DESIGN.md tokens; push to GitHub, deploy to Netlify or Vercel; free to start. Never show or imply: the Chrome extension, visual design mode, backend/data integration, Cloudflare deploys, or subscription AI. If a line could appear on a competitor's site, cut it; if it sounds like marketing, shorten it until it sounds like fact.\n\n## Story — three acts (56.0s, 1680 frames @30fps)\n\n**ACT 1 — THE PAIN (0:00–0:11).** Black. \"Want to build a web page?\" Then the toolchain answers: terminals stack and overlap, `npm install` spins, config files cascade, peer-dep errors bleed red. Two hours. Zero pixels. The clutter accelerates into a riser — hard blackout.\n\n**ACT 2 — THE TURN (0:11–0:15).** THE DROP. All that chaos collapses into one clean browser tab: `blnq.studio`. \"Or: just open a tab.\" Stamp: \"No install. No build step. No excuses.\"\n\n**ACT 3 — THE PROOF (0:15–0:48).** One journey, end to end, inside the tab: HTML/CSS types itself and renders live → the single preview splits into **five synced viewports** (the act-1 clutter reborn as order — same scattered-windows composition, now aligned) → a scroll syncs across all five → the WCAG audit flags a 3.2:1 contrast failure → one-click fix, badge flips to pass → vision sim sweeps deuteranopia and cataracts across every frame at once → Deploy → Netlify → build log → **live URL slams in** (THE CRASH). \"Shipped. From a tab.\"\n\n**CLOSE (0:48–0:56).** Cut to black. Logo stamps (FINAL HIT). \"Code, Preview & Ship the Web.\" → \"Start building → blnq.studio\".\n\n## Rhyme\nAct 1's scattered overlapping windows and Act 3's five-up grid share the same anchor coordinates (see `js/config.js` LAYOUT) — chaos and order are the same shot, recomposed.\n"},{"name":"index.html","content":"<!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>Blnq — Launch Film</title>\n  <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n  <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\n  <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap\" rel=\"stylesheet\" />\n  <link rel=\"stylesheet\" href=\"/css/styles.css\" />\n  <link rel=\"stylesheet\" href=\"/css/scenes.css\" />\n</head>\n<body>\n  <main class=\"film\" aria-label=\"Blnq Studio launch film player\">\n    <div class=\"film__frame\" id=\"frame\">\n      <div class=\"stage\" id=\"stage\"></div>\n      <button class=\"film__poster\" id=\"poster\" aria-label=\"Play the launch film\">\n        <span class=\"film__poster-mark\">blnq<span class=\"film__poster-dot\">.</span></span>\n        <span class=\"film__poster-play\">▶ Play film — 56s, with sound</span>\n      </button>\n    </div>\n    <div class=\"film__bar\" id=\"bar\">\n      <button class=\"film__btn\" id=\"playBtn\" aria-label=\"Play or pause\">▶</button>\n      <span class=\"film__tc\" id=\"tc\">00:00.0</span>\n      <div class=\"film__scrub\" id=\"scrub\" role=\"slider\" aria-label=\"Timeline\" aria-valuemin=\"0\" aria-valuemax=\"56\" aria-valuenow=\"0\" tabindex=\"0\">\n        <div class=\"film__scrub-fill\" id=\"scrubFill\"></div>\n        <div class=\"film__scrub-anchor\" style=\"left:19.64%\" title=\"Drop 11.0s\"></div>\n        <div class=\"film__scrub-anchor\" style=\"left:80.36%\" title=\"Crash 45.0s\"></div>\n        <div class=\"film__scrub-anchor\" style=\"left:85.71%\" title=\"Hit 48.0s\"></div>\n      </div>\n      <span class=\"film__tc film__tc--end\">00:56.0</span>\n      <button class=\"film__btn film__btn--mute\" id=\"muteBtn\" aria-label=\"Mute or unmute\">♪</button>\n    </div>\n  </main>\n  <script type=\"module\" src=\"/js/main.js\"></script>\n</body>\n</html>\n"},{"name":"README.md","content":"# Blnq Project\n"}],"folders":[{"folder":"css","files":[{"name":"scenes.css","content":"/* ============ Scene styling — everything lives on the 1920x1080 stage ============ */\n\n.scene { position: absolute; inset: 0; }\n\n/* ---------- shared: big centered supers ---------- */\n.super {\n  position: absolute;\n  left: 0; right: 0;\n  text-align: center;\n  font-weight: 800;\n  letter-spacing: -0.03em;\n  color: var(--white);\n}\n\n/* ---------- ACT 1 — toolchain hell ---------- */\n.pain__ask {\n  top: 300px;\n  font-size: 76px;\n}\n\n.pain__caret {\n  display: inline-block;\n  width: 8px; height: 64px;\n  margin-left: 10px;\n  vertical-align: -6px;\n  background: var(--white);\n}\n\n.pain__zero {\n  top: 120px;\n  font-size: 64px;\n  color: var(--danger);\n  z-index: 30;\n  text-shadow: 0 4px 40px #000;\n}\n\n.term {\n  position: absolute;\n  width: 560px;\n  border-radius: 10px;\n  background: var(--surface);\n  box-shadow: 0 0 0 1px #23262d, 0 30px 70px rgba(0,0,0,.8);\n  overflow: hidden;\n  font-family: var(--mono);\n  font-size: 19px;\n  line-height: 1.65;\n}\n\n.term__bar {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  padding: 10px 14px;\n  background: #15171c;\n  font-family: var(--font);\n  font-size: 13px;\n  color: #6b7280;\n}\n\n.term__dot { width: 11px; height: 11px; border-radius: 50%; background: #2d3138; }\n\n.term__body { padding: 14px 18px 18px; color: #aeb6c2; min-height: 120px; }\n.term__body .ln { display: block; white-space: pre; }\n.term__body .ln--dim { color: #5b6470; }\n.term__body .ln--err { color: var(--danger); }\n.term__body .ln--warn { color: var(--warn); }\n.term__body .ln--ok { color: #7d8694; }\n\n.cfg {\n  position: absolute;\n  padding: 12px 20px;\n  border-radius: 8px;\n  background: #121419;\n  box-shadow: 0 0 0 1px #262a32, 0 20px 50px rgba(0,0,0,.7);\n  font-family: var(--mono);\n  font-size: 18px;\n  color: #8a93a1;\n  white-space: nowrap;\n}\n\n/* ---------- ACT 2 — the turn ---------- */\n.turn__tab {\n  position: absolute;\n  left: 50%; top: 50%;\n  width: 1100px; height: 640px;\n  transform: translate(-50%, -50%);\n  border-radius: 14px;\n  background: var(--bg-soft, #08080a);\n  box-shadow: 0 0 0 1.5px #2a2d34, 0 0 120px rgba(255, 211, 0, 0.07), 0 50px 120px #000;\n  overflow: hidden;\n}\n\n.turn__bar {\n  display: flex;\n  align-items: center;\n  gap: 14px;\n  height: 64px;\n  padding: 0 22px;\n  background: var(--surface);\n  border-bottom: 1px solid #1d2026;\n}\n\n.turn__dot { width: 13px; height: 13px; border-radius: 50%; background: #2d3138; }\n\n.turn__url {\n  flex: 1;\n  max-width: 460px;\n  margin-left: 14px;\n  padding: 9px 18px;\n  border-radius: 100px;\n  background: #15171c;\n  font-family: var(--mono);\n  font-size: 19px;\n  color: #cfd4dc;\n}\n\n.turn__url b { color: var(--accent); font-weight: 600; }\n\n.turn__line1 {\n  top: 300px;\n  font-size: 50px;\n  font-weight: 700;\n  color: #cfd4dc;\n}\n\n.turn__stamp {\n  top: 300px;\n  font-size: 62px;\n  font-weight: 900;\n  letter-spacing: -0.035em;\n}\n\n.turn__stamp em { font-style: normal; color: var(--accent); }\n\n/* ---------- ACT 3 — editor ---------- */\n.ed {\n  position: absolute;\n  left: 60px; top: 60px;\n  width: 1800px; height: 960px;\n  border-radius: 14px;\n  background: var(--bg-soft, #08080a);\n  box-shadow: 0 0 0 1.5px #2a2d34, 0 50px 120px #000;\n  overflow: hidden;\n}\n\n.ed__bar {\n  display: flex;\n  align-items: center;\n  gap: 14px;\n  height: 56px;\n  padding: 0 22px;\n  background: var(--surface);\n  border-bottom: 1px solid #1d2026;\n}\n\n.ed__dot { width: 12px; height: 12px; border-radius: 50%; background: #2d3138; }\n\n.ed__url {\n  padding: 7px 16px;\n  margin-left: 10px;\n  border-radius: 100px;\n  background: #15171c;\n  font-family: var(--mono);\n  font-size: 16px;\n  color: #cfd4dc;\n}\n\n.ed__url b { color: var(--accent); font-weight: 600; }\n\n.ed__actions {\n  margin-left: auto;\n  display: flex;\n  gap: 10px;\n  align-items: center;\n}\n\n.ed__chip {\n  padding: 7px 16px;\n  border-radius: 100px;\n  font-size: 15px;\n  font-weight: 600;\n  color: #9aa1ad;\n  background: #15171c;\n  border: 1px solid #23262d;\n}\n\n.ed__chip--accent { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }\n\n.ed__main { position: relative; display: flex; height: calc(100% - 56px); }\n\n/* code pane */\n.code {\n  position: relative;\n  width: 720px;\n  flex: none;\n  background: var(--bg-soft, #08080a);\n  border-right: 1px solid #1d2026;\n  font-family: var(--mono);\n  font-size: 21px;\n  line-height: 1.75;\n  overflow: hidden;\n  transition: none;\n}\n\n.code__tabs {\n  display: flex;\n  gap: 4px;\n  padding: 10px 14px 0;\n  font-family: var(--font);\n  font-size: 14.5px;\n  color: #79818d;\n}\n\n.code__tab { padding: 8px 16px; border-radius: 8px 8px 0 0; }\n.code__tab--on { background: #15171c; color: var(--light); }\n\n.code__body { display: flex; padding: 14px 0 0; }\n\n.code__lns {\n  width: 56px;\n  flex: none;\n  text-align: right;\n  padding-right: 16px;\n  color: #3a3f48;\n  user-select: none;\n  white-space: pre;\n}\n\n.code__txt { white-space: pre; color: #c6ccd6; }\n\n.code__caret {\n  display: inline-block;\n  width: 2.5px; height: 24px;\n  vertical-align: -4px;\n  background: var(--accent);\n}\n\n.tk-tag { color: var(--blue); }\n.tk-attr { color: #8fd0ff; }\n.tk-str { color: #d2b56a; }\n.tk-txt { color: #c6ccd6; }\n.tk-sel { color: var(--accent); }\n.tk-prop { color: #8fd0ff; }\n.tk-val { color: #d2b56a; }\n.tk-pun { color: #5b6470; }\n.tk-fix { color: var(--accent); font-weight: 700; }\n\n/* preview area (single, then five-up) */\n.view { position: relative; flex: 1; background: #050507; overflow: hidden; }\n\n.dev {\n  position: absolute;\n  border-radius: 14px;\n  background: #111;\n  box-shadow: 0 0 0 1.5px #272b33, 0 24px 60px rgba(0,0,0,.75);\n}\n\n.dev__label {\n  position: absolute;\n  left: 50%;\n  bottom: -38px;\n  transform: translateX(-50%);\n  font-size: 16px;\n  font-weight: 600;\n  letter-spacing: .1em;\n  text-transform: uppercase;\n  color: #79818d;\n  white-space: nowrap;\n}\n\n.dev__screen { position: absolute; inset: 0; overflow: hidden; border-radius: 14px; }\n.dev__inner { position: absolute; left: 0; top: 0; width: 100%; }\n\n/* the little site being built — Hoff's Bar */\n.msite {\n  --tagc: #666;\n  background: #111;\n  color: #f4f4f4;\n  font-family: var(--font);\n  text-align: center;\n  min-height: 100%;\n}\n\n.msite__hero { padding: 64px 28px 56px; }\n.msite__h1 { font-size: 52px; font-weight: 800; letter-spacing: -0.02em; color: var(--accent); }\n.msite__tag { margin-top: 12px; font-size: 21px; color: var(--tagc); }\n.msite__btn {\n  display: inline-block;\n  margin-top: 26px;\n  padding: 13px 30px;\n  border-radius: 100px;\n  background: var(--accent);\n  color: var(--on-accent);\n  font-weight: 700;\n  font-size: 18px;\n}\n.msite__menu { padding: 8px 28px 48px; }\n.msite__item {\n  display: flex;\n  justify-content: space-between;\n  padding: 16px 4px;\n  border-bottom: 1px solid #262626;\n  font-size: 18px;\n  color: #cfcfcf;\n}\n\n/* sync click ripple */\n.ripple {\n  position: absolute;\n  width: 26px; height: 26px;\n  border-radius: 50%;\n  border: 3px solid var(--accent);\n  transform: translate(-50%, -50%);\n  pointer-events: none;\n}\n\n/* vision chip */\n.vision {\n  position: absolute;\n  top: 18px; left: 50%;\n  transform: translateX(-50%);\n  display: flex;\n  align-items: center;\n  gap: 10px;\n  padding: 9px 20px;\n  border-radius: 100px;\n  background: #15171c;\n  border: 1px solid #2a2d34;\n  font-size: 16px;\n  font-weight: 600;\n  color: var(--light);\n  z-index: 20;\n}\n\n.vision b { color: var(--accent); }\n\n/* audit panel */\n.audit {\n  position: absolute;\n  top: 0; right: 0; bottom: 0;\n  width: 470px;\n  background: var(--surface);\n  border-left: 1px solid #1d2026;\n  padding: 26px 28px;\n  z-index: 15;\n}\n\n.audit__head {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  font-size: 19px;\n  font-weight: 700;\n  color: var(--light);\n}\n\n.audit__badge {\n  font-size: 14px;\n  font-weight: 700;\n  padding: 5px 13px;\n  border-radius: 100px;\n  background: #15171c;\n  color: #79818d;\n}\n\n.audit__badge--fail { background: rgba(255, 68, 85, .14); color: var(--danger); }\n.audit__badge--pass { background: rgba(255, 211, 0, .16); color: var(--accent); }\n\n.audit__scan {\n  margin-top: 18px;\n  height: 4px;\n  border-radius: 2px;\n  background: #1c1e23;\n  overflow: hidden;\n}\n\n.audit__scan-fill { height: 100%; width: 0%; background: var(--accent); }\n\n.audit__item {\n  display: flex;\n  align-items: center;\n  gap: 12px;\n  padding: 15px 2px;\n  border-bottom: 1px solid #1d2026;\n  font-size: 17px;\n  color: #aeb6c2;\n}\n\n.audit__tick { width: 22px; flex: none; text-align: center; color: #79818d; }\n.audit__item--ok .audit__tick { color: var(--accent); }\n\n.audit__flag {\n  margin-top: 16px;\n  border-radius: 12px;\n  border: 1px solid rgba(255, 68, 85, .4);\n  background: rgba(255, 68, 85, .08);\n  padding: 18px;\n}\n\n.audit__flag--fixed { border-color: rgba(255, 211, 0, .45); background: rgba(255, 211, 0, .07); }\n\n.audit__flag-title { font-size: 17px; font-weight: 700; color: var(--danger); }\n.audit__flag--fixed .audit__flag-title { color: var(--accent); }\n\n.audit__flag-meta { margin-top: 6px; font-size: 15.5px; color: #9aa1ad; font-family: var(--mono); }\n\n.audit__fix {\n  margin-top: 14px;\n  display: inline-block;\n  padding: 10px 22px;\n  border-radius: 100px;\n  background: var(--accent);\n  color: var(--on-accent);\n  font-size: 15.5px;\n  font-weight: 700;\n}\n\n/* ship */\n.ship__menu {\n  position: absolute;\n  top: 64px; right: 24px;\n  width: 320px;\n  border-radius: 14px;\n  background: var(--surface);\n  border: 1px solid #2a2d34;\n  box-shadow: 0 20px 60px #000;\n  padding: 10px;\n  z-index: 25;\n}\n\n.ship__opt {\n  display: flex;\n  align-items: center;\n  gap: 12px;\n  padding: 13px 14px;\n  border-radius: 9px;\n  font-size: 17px;\n  font-weight: 600;\n  color: var(--light);\n}\n\n.ship__opt--on { background: var(--accent); color: var(--on-accent); }\n.ship__opt span:last-child { margin-left: auto; font-size: 14px; font-weight: 500; opacity: .65; }\n\n.ship__log {\n  position: absolute;\n  right: 24px; top: 240px;\n  width: 320px;\n  border-radius: 12px;\n  background: #0a0b0e;\n  border: 1px solid #23262d;\n  padding: 14px 16px;\n  font-family: var(--mono);\n  font-size: 14.5px;\n  line-height: 1.8;\n  color: #8a93a1;\n  z-index: 25;\n}\n\n.ship__log .ok { color: var(--accent); }\n\n.url {\n  position: absolute;\n  left: 50%; top: 50%;\n  transform: translate(-50%, -50%);\n  display: flex;\n  align-items: center;\n  gap: 18px;\n  padding: 26px 44px;\n  border-radius: 100px;\n  background: var(--surface);\n  border: 2px solid var(--accent);\n  box-shadow: 0 0 90px rgba(255, 211, 0, .25), 0 40px 100px #000;\n  font-family: var(--mono);\n  font-size: 36px;\n  color: var(--white);\n  z-index: 40;\n  white-space: nowrap;\n}\n\n.url__dot { width: 14px; height: 14px; border-radius: 50%; background: var(--accent); flex: none; }\n.url__live { font-family: var(--font); font-size: 19px; font-weight: 700; letter-spacing: .12em; color: var(--accent); }\n\n.ship__copy {\n  top: 850px;\n  font-size: 48px;\n  font-weight: 700;\n  color: #cfd4dc;\n  z-index: 40;\n}\n\n/* flash on the crash */\n.flash {\n  position: absolute;\n  inset: 0;\n  background: var(--accent);\n  pointer-events: none;\n  z-index: 45;\n}\n\n/* fake cursor */\n.fcur {\n  position: absolute;\n  width: 22px; height: 22px;\n  border-radius: 50%;\n  background: rgba(255, 211, 0, .9);\n  box-shadow: 0 0 0 5px rgba(255, 211, 0, .22);\n  transform: translate(-50%, -50%);\n  z-index: 60;\n}\n\n/* ---------- CLOSE ---------- */\n.close__logo {\n  top: 420px;\n  font-size: 170px;\n  font-weight: 900;\n  letter-spacing: -0.05em;\n}\n\n.close__logo i { font-style: normal; color: var(--accent); }\n\n.close__tagline {\n  top: 640px;\n  font-size: 46px;\n  font-weight: 600;\n  color: #cfd4dc;\n  letter-spacing: -0.01em;\n}\n\n.close__cta {\n  position: absolute;\n  top: 740px;\n  left: 50%;\n  transform: translateX(-50%);\n  padding: 18px 40px;\n  border-radius: 100px;\n  background: var(--accent);\n  color: var(--on-accent);\n  font-size: 26px;\n  font-weight: 800;\n}\n"},{"name":"styles.css","content":"/* ============ Blnq Launch Film — player chrome ============\n   Palette extracted from live blnq.studio CSS — see DIRECTION.md */\n:root {\n  color-scheme: dark;\n  --accent: #ffd300;          /* --color-bg-accent */\n  --on-accent: #1e1e1e;       /* --color-text-accent */\n  --surface: #0e0f12;         /* --color-bg-secondary */\n  --bg: #000000;\n  --bg-soft: #08080a;         /* computed bg-primary */\n  --slate: #4a5568;           /* --color-hue */\n  --danger: #ff4455;          /* --site-danger */\n  --blue: #438efe;            /* --ui-blue */\n  --warn: #f59e0b;\n  --light: #f0f1f3;\n  --white: #ffffff;\n  --font: \"Inter\", system-ui, -apple-system, sans-serif;\n  --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;\n  --ease: cubic-bezier(.42, 0, .58, 1);\n  --radius: 15px;\n}\n\n* { box-sizing: border-box; margin: 0; }\n\nhtml, body { height: 100%; }\n\nbody {\n  background: var(--bg);\n  color: var(--white);\n  font-family: var(--font);\n  overflow: hidden;\n}\n\n.film {\n  height: 100vh;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  gap: 14px;\n  padding: 18px;\n}\n\n/* The 16:9 frame — stage is a fixed 1920x1080 canvas scaled to fit */\n.film__frame {\n  position: relative;\n  width: min(100%, calc((100vh - 110px) * 16 / 9));\n  aspect-ratio: 16 / 9;\n  background: var(--bg);\n  border-radius: 8px;\n  overflow: hidden;\n  box-shadow: 0 0 0 1px #1d1f24, 0 30px 80px #000;\n}\n\n.stage {\n  position: absolute;\n  top: 0; left: 0;\n  width: 1920px;\n  height: 1080px;\n  transform-origin: top left;\n  background: var(--bg);\n  overflow: hidden;\n}\n\n/* ---- poster / play gate (audio needs a gesture) ---- */\n.film__poster {\n  position: absolute;\n  inset: 0;\n  z-index: 50;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  gap: 22px;\n  background: var(--bg);\n  border: 0;\n  cursor: pointer;\n  font-family: var(--font);\n}\n\n.film__poster-mark {\n  font-size: clamp(48px, 9vw, 120px);\n  font-weight: 900;\n  letter-spacing: -0.04em;\n  color: var(--white);\n}\n\n.film__poster-dot { color: var(--accent); }\n\n.film__poster-play {\n  font-size: 15px;\n  font-weight: 600;\n  letter-spacing: .14em;\n  text-transform: uppercase;\n  color: var(--on-accent);\n  background: var(--accent);\n  padding: 13px 26px;\n  border-radius: 100px;\n  transition: transform .15s var(--ease);\n}\n\n.film__poster:hover .film__poster-play { transform: scale(1.05); }\n\n.film__poster--hidden { display: none; }\n\n/* ---- transport bar ---- */\n.film__bar {\n  width: min(100%, calc((100vh - 110px) * 16 / 9));\n  display: flex;\n  align-items: center;\n  gap: 14px;\n}\n\n.film__btn {\n  width: 40px;\n  height: 40px;\n  border-radius: 50%;\n  border: 1px solid #24262c;\n  background: var(--surface);\n  color: var(--white);\n  font-size: 14px;\n  cursor: pointer;\n  transition: border-color .15s var(--ease), color .15s var(--ease);\n}\n\n.film__btn:hover { border-color: var(--accent); color: var(--accent); }\n.film__btn--muted { color: var(--slate); text-decoration: line-through; }\n\n.film__tc {\n  font-family: var(--mono);\n  font-size: 13px;\n  color: #9aa1ad;\n  min-width: 62px;\n}\n\n.film__tc--end { text-align: right; }\n\n.film__scrub {\n  position: relative;\n  flex: 1;\n  height: 28px;\n  cursor: pointer;\n}\n\n.film__scrub::before {\n  content: \"\";\n  position: absolute;\n  top: 12px;\n  left: 0; right: 0;\n  height: 4px;\n  border-radius: 2px;\n  background: #1c1e23;\n}\n\n.film__scrub-fill {\n  position: absolute;\n  top: 12px;\n  left: 0;\n  height: 4px;\n  width: 0%;\n  border-radius: 2px;\n  background: var(--accent);\n}\n\n.film__scrub-anchor {\n  position: absolute;\n  top: 9px;\n  width: 2px;\n  height: 10px;\n  background: var(--slate);\n}\n\n.film__scrub:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }\n"}],"folders":[]},{"folder":"js","files":[{"name":"audio.js","content":"// ============ Score — synthesized in Web Audio, anchored to the cue sheet ============\n// Anchors: DROP 11.0 (≥20dB step), CRASH 45.0 (the only crash), HIT 48.0 (logo stamp).\n// verifyScore() renders offline and probes the RMS envelope at each anchor.\nimport { T, DURATION } from './config.js';\n\nconst BPM = 120;\nconst BEAT = 60 / BPM; // 0.5s\n\nfunction noiseBuffer(ctx, secs = 2) {\n  const buf = ctx.createBuffer(1, ctx.sampleRate * secs, ctx.sampleRate);\n  const d = buf.getChannelData(0);\n  // deterministic pseudo-noise (no Math.random needed for verification parity)\n  let x = 1234567;\n  for (let i = 0; i < d.length; i++) {\n    x = (x * 1103515245 + 12345) & 0x7fffffff;\n    d[i] = (x / 0x3fffffff) - 1;\n  }\n  return buf;\n}\n\n// ---- tiny synth helpers (all times absolute, ctx-clock seconds) ----\nfunction env(ctx, dest, t0, a, peak, dur) {\n  const g = ctx.createGain();\n  g.gain.setValueAtTime(0.0001, t0);\n  g.gain.exponentialRampToValueAtTime(Math.max(peak, 0.0002), t0 + a);\n  g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);\n  g.connect(dest);\n  return g;\n}\n\nfunction kick(ctx, dest, t0, vol = 0.85) {\n  const o = ctx.createOscillator();\n  o.frequency.setValueAtTime(150, t0);\n  o.frequency.exponentialRampToValueAtTime(42, t0 + 0.12);\n  o.connect(env(ctx, dest, t0, 0.004, vol, 0.32));\n  o.start(t0); o.stop(t0 + 0.35);\n}\n\nfunction boom(ctx, dest, t0, vol = 1.0) {\n  const o = ctx.createOscillator();\n  o.frequency.setValueAtTime(85, t0);\n  o.frequency.exponentialRampToValueAtTime(26, t0 + 0.7);\n  o.connect(env(ctx, dest, t0, 0.005, vol, 1.4));\n  o.start(t0); o.stop(t0 + 1.5);\n}\n\nfunction hat(ctx, noise, dest, t0, vol = 0.12) {\n  const s = ctx.createBufferSource();\n  s.buffer = noise;\n  const f = ctx.createBiquadFilter();\n  f.type = 'highpass'; f.frequency.value = 8000;\n  s.connect(f);\n  f.connect(env(ctx, dest, t0, 0.002, vol, 0.06));\n  s.start(t0, (t0 * 7.3) % 1, 0.08);\n}\n\nfunction tick(ctx, noise, dest, t0, vol = 0.07) {\n  const s = ctx.createBufferSource();\n  s.buffer = noise;\n  const f = ctx.createBiquadFilter();\n  f.type = 'bandpass'; f.frequency.value = 2400; f.Q.value = 6;\n  s.connect(f);\n  f.connect(env(ctx, dest, t0, 0.002, vol, 0.05));\n  s.start(t0, (t0 * 3.7) % 1, 0.06);\n}\n\nfunction stab(ctx, dest, t0, freqs, vol = 0.1, dur = 0.5) {\n  for (const fr of freqs) {\n    const o = ctx.createOscillator();\n    o.type = 'sawtooth';\n    o.frequency.value = fr;\n    const lp = ctx.createBiquadFilter();\n    lp.type = 'lowpass'; lp.frequency.value = 1100;\n    o.connect(lp);\n    lp.connect(env(ctx, dest, t0, 0.01, vol / freqs.length, dur));\n    o.start(t0); o.stop(t0 + dur + 0.1);\n  }\n}\n\nfunction bassNote(ctx, dest, t0, fr, vol, dur) {\n  const o = ctx.createOscillator();\n  o.type = 'square';\n  o.frequency.value = fr;\n  const lp = ctx.createBiquadFilter();\n  lp.type = 'lowpass';\n  lp.frequency.setValueAtTime(900, t0);\n  lp.frequency.exponentialRampToValueAtTime(180, t0 + dur);\n  o.connect(lp);\n  lp.connect(env(ctx, dest, t0, 0.008, vol, dur));\n  o.start(t0); o.stop(t0 + dur + 0.1);\n}\n\nfunction riser(ctx, noise, dest, t0, t1, vol = 0.3) {\n  const s = ctx.createBufferSource();\n  s.buffer = noise; s.loop = true;\n  const f = ctx.createBiquadFilter();\n  f.type = 'bandpass'; f.Q.value = 1.5;\n  f.frequency.setValueAtTime(220, t0);\n  f.frequency.exponentialRampToValueAtTime(7500, t1);\n  const g = ctx.createGain();\n  g.gain.setValueAtTime(0.0001, t0);\n  g.gain.exponentialRampToValueAtTime(vol, t1);\n  g.gain.setValueAtTime(0.0001, t1 + 0.001); // hard stop — keeps the gap clean\n  s.connect(f); f.connect(g); g.connect(dest);\n  s.start(t0); s.stop(t1 + 0.01);\n}\n\nfunction crash(ctx, noise, dest, t0, vol = 0.9) {\n  const s = ctx.createBufferSource();\n  s.buffer = noise; s.loop = true;\n  const f = ctx.createBiquadFilter();\n  f.type = 'lowpass';\n  f.frequency.setValueAtTime(9000, t0);\n  f.frequency.exponentialRampToValueAtTime(300, t0 + 1.6);\n  s.connect(f);\n  f.connect(env(ctx, dest, t0, 0.004, vol, 1.8));\n  s.start(t0); s.stop(t0 + 1.9);\n}\n\n// ---- the full 56.0s arrangement ----\nexport function schedule(ctx, dest, t0 = 0) {\n  const noise = noiseBuffer(ctx);\n  const at = (t) => t0 + t;\n\n  // ACT 1 (0 – 10.85): drone + accelerating ticks + glitch stabs + riser\n  {\n    const g = ctx.createGain();\n    g.gain.setValueAtTime(0.0001, at(0));\n    g.gain.exponentialRampToValueAtTime(0.07, at(1.2));\n    g.gain.exponentialRampToValueAtTime(0.13, at(T.riser));\n    g.gain.setValueAtTime(0.0001, at(T.blackout)); // dead silence before the drop\n    const lp = ctx.createBiquadFilter();\n    lp.type = 'lowpass'; lp.frequency.value = 320;\n    for (const fr of [55, 55.7, 110.4]) {\n      const o = ctx.createOscillator();\n      o.type = 'sawtooth'; o.frequency.value = fr;\n      o.connect(lp);\n      o.start(at(0)); o.stop(at(T.blackout));\n    }\n    lp.connect(g); g.connect(dest);\n  }\n  // ticks accelerate: gap shrinks 0.5 → 0.18\n  for (let tt = 0.5; tt < T.riser + 1.2 && tt < T.blackout - 0.05; ) {\n    tick(ctx, noise, dest, at(tt), 0.05 + 0.05 * (tt / 10));\n    tt += Math.max(0.18, 0.5 - tt * 0.032);\n  }\n  // glitch stabs on the window spawns\n  for (const gt of [T.term1, T.term2, T.configRain, T.term3, T.zeroPixels]) {\n    stab(ctx, dest, at(gt), [110, 116.5], 0.14, 0.18);\n  }\n  riser(ctx, noise, dest, at(T.riser), at(T.blackout), 0.34);\n  // 10.85 – 11.0: SILENCE (the pre-drop gap)\n\n  // ACT 2+3 (11.0 – 45.0): the drop — dark four-on-floor\n  boom(ctx, dest, at(T.DROP), 1.0);\n  const beatEnd = T.CRASH;\n  for (let b = 0; T.DROP + b * BEAT < beatEnd - 0.01; b++) {\n    const bt = T.DROP + b * BEAT;\n    kick(ctx, dest, at(bt), 0.8);\n    if (bt + BEAT / 2 < beatEnd) hat(ctx, noise, dest, at(bt + BEAT / 2), 0.09);\n    // offbeat eighth bass (A1 / G1 alternating bars)\n    const bar = Math.floor(b / 4);\n    const fr = bar % 2 === 0 ? 55 : 49;\n    if (bt + BEAT * 0.5 < beatEnd) bassNote(ctx, dest, at(bt + BEAT * 0.25), fr, 0.22, 0.2);\n  }\n  // sparse minor stabs through the proof act\n  for (let st = T.editorIn; st < 41.5; st += 4) {\n    stab(ctx, dest, at(st), [220, 261.6, 329.6], 0.09, 1.2);\n  }\n  // moment accents: five-up split, audit pass\n  stab(ctx, dest, at(T.fiveUp), [440, 523.2, 659.2], 0.12, 0.7);\n  stab(ctx, dest, at(T.auditPass), [523.2, 659.2, 784], 0.12, 0.6);\n  // riser 2 into the crash\n  riser(ctx, noise, dest, at(T.shipNetlify), at(T.CRASH), 0.3);\n\n  // ANCHOR 2 — THE CRASH (45.0): the only crash in the film\n  crash(ctx, noise, dest, at(T.CRASH), 0.42);\n  boom(ctx, dest, at(T.CRASH), 0.4);\n  // thin pulse under the held URL\n  for (let bt = T.CRASH + 1.0; bt < T.HIT - 0.4; bt += 1.0) {\n    kick(ctx, dest, at(bt), 0.25);\n  }\n\n  // ANCHOR 3 — THE HIT (48.0): logo stamp\n  boom(ctx, dest, at(T.HIT), 1.0);\n  stab(ctx, dest, at(T.HIT), [110, 220, 330, 440], 0.3, 2.2);\n  // outro pad fades to nothing by 55.5\n  {\n    const g = ctx.createGain();\n    g.gain.setValueAtTime(0.0001, at(T.HIT + 0.3));\n    g.gain.exponentialRampToValueAtTime(0.07, at(T.HIT + 1.4));\n    g.gain.exponentialRampToValueAtTime(0.0001, at(55.5));\n    const lp = ctx.createBiquadFilter();\n    lp.type = 'lowpass'; lp.frequency.value = 700;\n    for (const fr of [110, 164.8, 220, 261.6]) {\n      const o = ctx.createOscillator();\n      o.type = 'triangle'; o.frequency.value = fr;\n      o.connect(lp);\n      o.start(at(T.HIT + 0.3)); o.stop(at(55.6));\n    }\n    lp.connect(g); g.connect(dest);\n  }\n}\n\n// ---- live playback ----\nexport class Score {\n  constructor() {\n    this.ctx = null;\n    this.master = null;\n    this.muted = false;\n  }\n\n  // start playback at film-time `filmT`\n  start(filmT = 0) {\n    this.stop();\n    this.ctx = new (window.AudioContext || window.webkitAudioContext)();\n    this.master = this.ctx.createGain();\n    this.master.gain.value = this.muted ? 0 : 0.9;\n    const comp = this.ctx.createDynamicsCompressor();\n    comp.threshold.value = -14;\n    comp.ratio.value = 6;\n    this.master.connect(comp);\n    comp.connect(this.ctx.destination);\n    // schedule the whole film shifted so that \"now\" == filmT\n    schedule(this.ctx, this.master, this.ctx.currentTime + 0.05 - filmT);\n  }\n\n  stop() {\n    if (this.ctx) { this.ctx.close(); this.ctx = null; }\n  }\n\n  setMuted(m) {\n    this.muted = m;\n    if (this.master) this.master.gain.value = m ? 0 : 0.9;\n  }\n}\n\n// ---- engineering gate: render offline, probe RMS at the anchors ----\nexport async function verifyScore() {\n  const sr = 44100;\n  const ctx = new OfflineAudioContext(1, Math.ceil(sr * DURATION), sr);\n  const comp = ctx.createDynamicsCompressor();\n  comp.threshold.value = -14; comp.ratio.value = 6;\n  comp.connect(ctx.destination);\n  schedule(ctx, comp, 0);\n  const buf = await ctx.startRendering();\n  const d = buf.getChannelData(0);\n  const win = Math.floor(sr * 0.025); // 25ms RMS windows\n  const rmsDb = (centerT) => {\n    const c = Math.floor(centerT * sr);\n    let s = 0, n = 0;\n    for (let i = Math.max(0, c - win); i < Math.min(d.length, c + win); i++) { s += d[i] * d[i]; n++; }\n    const r = Math.sqrt(s / Math.max(n, 1));\n    return 20 * Math.log10(Math.max(r, 1e-7));\n  };\n  const peakNear = (t, span = 0.4) => {\n    let best = -1, bt = 0;\n    const a = Math.floor((t - span) * sr), b = Math.floor((t + span) * sr);\n    for (let i = Math.max(0, a); i < Math.min(d.length, b); i++) {\n      const v = Math.abs(d[i]);\n      if (v > best) { best = v; bt = i / sr; }\n    }\n    return { peak: best, at: bt };\n  };\n  const pre = rmsDb(T.DROP - 0.06);\n  const post = rmsDb(T.DROP + 0.05);\n  return {\n    drop: { preDb: +pre.toFixed(1), postDb: +post.toFixed(1), stepDb: +(post - pre).toFixed(1), pass: post - pre >= 20 },\n    crash: { ...peakNear(T.CRASH), offset: +(peakNear(T.CRASH).at - T.CRASH).toFixed(3) },\n    hit: { ...peakNear(T.HIT), offset: +(peakNear(T.HIT).at - T.HIT).toFixed(3) },\n    tailDb: +rmsDb(55.8).toFixed(1),\n  };\n}\n"},{"name":"config.js","content":"// ============ Shared constants — every scene reads from here ============\n// Palette extracted from live blnq.studio CSS (see DIRECTION.md)\n\nexport const PAL = {\n  accent: '#ffd300',\n  onAccent: '#1e1e1e',\n  surface: '#0e0f12',\n  bg: '#000000',\n  bgSoft: '#08080a',\n  slate: '#4a5568',\n  danger: '#ff4455',\n  blue: '#438efe',\n  warn: '#f59e0b',\n  light: '#f0f1f3',\n  white: '#ffffff',\n};\n\nexport const FPS = 30;\nexport const DURATION = 56.0; // seconds — 1680 frames\n\n// ---- Timecodes (seconds). Three hard music anchors marked. ----\nexport const T = {\n  // ACT 1 — pain\n  caret: 0.0,\n  term1: 1.5,\n  term2: 3.0,\n  configRain: 4.5,\n  term3: 6.5,\n  zeroPixels: 8.0,\n  riser: 9.0,\n  blackout: 10.85,\n  // ACT 2 — turn\n  DROP: 11.0, // ANCHOR 1\n  tabLabel: 12.0,\n  stamp: 13.5,\n  // ACT 3 — proof\n  editorIn: 15.0,\n  typeHtml: 15.5,\n  typeCss: 19.0,\n  typeDone: 21.5,\n  fiveUp: 23.0,\n  syncScroll: 26.0,\n  syncClick: 28.0,\n  auditIn: 30.0,\n  auditFlag: 32.0,\n  auditFix: 33.5,\n  auditPass: 34.5,\n  visionDeuter: 36.5,\n  visionCataract: 38.0,\n  visionClear: 39.5,\n  shipMenu: 40.5,\n  shipNetlify: 41.5,\n  CRASH: 45.0, // ANCHOR 2 — live URL\n  shippedCopy: 46.0,\n  // CLOSE\n  HIT: 48.0, // ANCHOR 3 — logo stamp\n  tagline: 49.5,\n  cta: 51.5,\n  fadeOut: 54.0,\n  end: 56.0,\n};\n\n// ---- Stage geometry (1920x1080) ----\n// Five device frames: real device CSS sizes, scaled. The Act-1 chaos\n// windows sit on the SAME five x-centers — chaos and order rhyme.\nexport const DEVICES = [\n  { name: 'iPhone', w: 393, h: 852, s: 0.55 },\n  { name: 'Pixel', w: 412, h: 915, s: 0.55 },\n  { name: 'Galaxy', w: 360, h: 780, s: 0.55 },\n  { name: 'iPad', w: 820, h: 1180, s: 0.42 },\n  { name: 'Desktop', w: 1440, h: 900, s: 0.34 },\n];\n\nconst MARGIN = 80;\nconst widths = DEVICES.map((d) => d.w * d.s);\nconst totalW = widths.reduce((a, b) => a + b, 0);\nconst GAP = (1920 - MARGIN * 2 - totalW) / (DEVICES.length - 1);\n\nlet runX = MARGIN;\nexport const FIVE_UP = DEVICES.map((d, i) => {\n  const w = widths[i];\n  const x = runX;\n  runX += w + GAP;\n  return { ...d, x, w, h: d.h * d.s, cx: x + w / 2, y: 568 - (d.h * d.s) / 2 };\n});\n\n// Act-1 window anchors — same x-centers as FIVE_UP, scattered y/rotation\nexport const CHAOS = FIVE_UP.map((f, i) => ({\n  cx: f.cx,\n  cy: [430, 620, 360, 660, 480][i],\n  rot: [-4, 3, -2, 5, -3][i],\n}));\n\n// ---- helpers ----\nexport const clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v);\nexport const prog = (t, a, b) => clamp01((t - a) / (b - a));\nexport const lerp = (a, b, p) => a + (b - a) * p;\nexport const easeOut = (p) => 1 - Math.pow(1 - p, 3);\nexport const easeInOut = (p) => (p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2);\nexport const stepAt = (t, at) => (t >= at ? 1 : 0);\n\nexport function fmtTC(t) {\n  const m = Math.floor(t / 60);\n  const s = t - m * 60;\n  return `${String(m).padStart(2, '0')}:${s.toFixed(1).padStart(4, '0')}`;\n}\n"},{"name":"main.js","content":"// ============ Boot: stage scaling, transport, scenes, score ============\nimport { DURATION, fmtTC } from './config.js';\nimport { Timeline } from './timeline.js';\nimport { painScene } from './scenes/pain.js';\nimport { turnScene } from './scenes/turn.js';\nimport { proofScene } from './scenes/proof.js';\nimport { closeScene } from './scenes/close.js';\nimport { Score, verifyScore } from './audio.js';\n\nconst stage = document.getElementById('stage');\nconst frame = document.getElementById('frame');\nconst poster = document.getElementById('poster');\nconst playBtn = document.getElementById('playBtn');\nconst muteBtn = document.getElementById('muteBtn');\nconst tc = document.getElementById('tc');\nconst scrub = document.getElementById('scrub');\nconst scrubFill = document.getElementById('scrubFill');\n\n// scale the fixed 1920x1080 stage into the responsive frame\nfunction fit() {\n  const s = frame.clientWidth / 1920;\n  stage.style.transform = `scale(${s})`;\n}\nnew ResizeObserver(fit).observe(frame);\nfit();\n\nconst tl = new Timeline(stage, [painScene, turnScene, proofScene, closeScene]);\nconst score = new Score();\n\ntl.onTick = (t) => {\n  tc.textContent = fmtTC(t);\n  scrubFill.style.width = (t / DURATION) * 100 + '%';\n  scrub.setAttribute('aria-valuenow', t.toFixed(1));\n};\n\ntl.onEnd = () => {\n  score.stop();\n  playBtn.textContent = '↺';\n};\n\nfunction play() {\n  poster.classList.add('film__poster--hidden');\n  score.start(tl.t >= DURATION - 0.04 ? 0 : tl.t);\n  tl.play();\n  playBtn.textContent = '❚❚';\n}\n\nfunction pause() {\n  tl.pause();\n  score.stop();\n  playBtn.textContent = '▶';\n}\n\nposter.addEventListener('click', play);\nplayBtn.addEventListener('click', () => (tl.playing ? pause() : play()));\n\nmuteBtn.addEventListener('click', () => {\n  score.setMuted(!score.muted);\n  muteBtn.classList.toggle('film__btn--muted', score.muted);\n});\n\n// scrub: click / drag / keyboard\nfunction scrubTo(clientX) {\n  const r = scrub.getBoundingClientRect();\n  const p = Math.min(Math.max((clientX - r.left) / r.width, 0), 1);\n  const wasPlaying = tl.playing;\n  pause();\n  tl.seek(p * DURATION);\n  if (wasPlaying) play();\n}\nscrub.addEventListener('pointerdown', (e) => {\n  scrub.setPointerCapture(e.pointerId);\n  scrubTo(e.clientX);\n});\nscrub.addEventListener('pointermove', (e) => {\n  if (e.buttons) scrubTo(e.clientX);\n});\nscrub.addEventListener('keydown', (e) => {\n  if (e.key === 'ArrowRight') { pause(); tl.seek(tl.t + 1); }\n  if (e.key === 'ArrowLeft') { pause(); tl.seek(tl.t - 1); }\n  if (e.key === ' ') { e.preventDefault(); tl.playing ? pause() : play(); }\n});\n\n// draw first frame behind the poster (or boot at the QA beat)\nimport('./qa.js').then(({ QA_T }) => {\n  if (typeof QA_T === 'number') {\n    poster.classList.add('film__poster--hidden');\n    tl.seek(QA_T);\n  } else {\n    tl.seek(0);\n  }\n});\n\n// ---- director/engineer hooks (used during QA, harmless in production) ----\nwindow.__seek = (t) => { poster.classList.add('film__poster--hidden'); pause(); tl.seek(t); return fmtTC(tl.t); };\nwindow.__verifyScore = verifyScore;\n"},{"name":"qa.js","content":"// Director QA: when QA_T is a number, the film boots paused at that time\n// with the poster hidden — used to capture stills at exact beats.\n// MUST be null for the real experience.\nexport const QA_T = null;\n"},{"name":"timeline.js","content":"// ============ Deterministic timeline driver — 30fps, scrub-safe ============\n// Every scene renders purely from `t`. Seeking to any frame reproduces it.\n\nimport { FPS, DURATION } from './config.js';\n\nexport class Timeline {\n  constructor(stage, scenes) {\n    this.stage = stage;\n    this.scenes = scenes.map((s) => ({ ...s, el: null }));\n    this.t = 0;\n    this.playing = false;\n    this._origin = 0; // performance.now() at t=0\n    this._raf = 0;\n    this.onTick = null; // (t) => void\n    this.onEnd = null;\n  }\n\n  // Quantize to the frame grid so playback and stills are identical\n  static q(t) {\n    return Math.round(t * FPS) / FPS;\n  }\n\n  seek(rawT) {\n    const t = Math.min(Math.max(Timeline.q(rawT), 0), DURATION);\n    this.t = t;\n    for (const s of this.scenes) {\n      const active = t >= s.start && t < s.end;\n      if (active && !s.el) {\n        s.el = document.createElement('div');\n        s.el.className = `scene scene--${s.name}`;\n        s.el.dataset.scene = s.name;\n        this.stage.appendChild(s.el);\n        s.mount(s.el);\n      } else if (!active && s.el) {\n        s.el.remove();\n        s.el = null;\n      }\n      if (s.el) s.update(s.el, t);\n    }\n    if (this.onTick) this.onTick(t);\n  }\n\n  play() {\n    if (this.playing) return;\n    if (this.t >= DURATION) this.t = 0;\n    this.playing = true;\n    this._origin = performance.now() - this.t * 1000;\n    const loop = () => {\n      if (!this.playing) return;\n      const t = (performance.now() - this._origin) / 1000;\n      if (t >= DURATION) {\n        this.seek(DURATION - 1 / FPS);\n        this.pause();\n        if (this.onEnd) this.onEnd();\n        return;\n      }\n      this.seek(t);\n      this._raf = requestAnimationFrame(loop);\n    };\n    this._raf = requestAnimationFrame(loop);\n  }\n\n  pause() {\n    this.playing = false;\n    cancelAnimationFrame(this._raf);\n  }\n}\n"}],"folders":[{"folder":"scenes","files":[{"name":"audit.js","content":"// ============ Proof sub-scene: WCAG audit + one-click fix + vision sim (30 – 40) ============\nimport { T, prog, easeOut, easeInOut, lerp } from '../config.js';\n\nfunction el(tag, cls, html) {\n  const e = document.createElement(tag);\n  if (cls) e.className = cls;\n  if (html !== undefined) e.innerHTML = html;\n  return e;\n}\n\nexport function mountAudit(view) {\n  const panel = el('div', 'audit');\n  panel.innerHTML = `\n    <div class=\"audit__head\">Accessibility <span class=\"audit__badge\" id=\"aBadge\">scanning…</span></div>\n    <div class=\"audit__scan\"><div class=\"audit__scan-fill\" id=\"aScan\"></div></div>\n    <div id=\"aItems\">\n      <div class=\"audit__item\" data-at=\"0\"><span class=\"audit__tick\">✓</span> Landmarks &amp; headings</div>\n      <div class=\"audit__item\" data-at=\"1\"><span class=\"audit__tick\">✓</span> Alt text on images</div>\n      <div class=\"audit__item\" data-at=\"2\"><span class=\"audit__tick\">✓</span> Keyboard navigation</div>\n      <div class=\"audit__item\" data-at=\"3\"><span class=\"audit__tick\">✓</span> Focus order</div>\n    </div>\n    <div class=\"audit__flag\" id=\"aFlag\">\n      <div class=\"audit__flag-title\" id=\"aTitle\">Text contrast — .tag</div>\n      <div class=\"audit__flag-meta\" id=\"aMeta\">#666 on #111 · 3.3:1 — fails AA (needs 4.5:1)</div>\n      <div class=\"audit__flag-meta\" id=\"aDiff\">color: <s>#666</s> → <span class=\"tk-fix\">#c9c9c9</span></div>\n      <span class=\"audit__fix\" id=\"aFix\">Fix it</span>\n    </div>`;\n  view.appendChild(panel);\n\n  // vision chip\n  const vision = el('div', 'vision');\n  vision.innerHTML = `Vision: <b id=\"vMode\">—</b>`;\n  view.appendChild(vision);\n\n  return {\n    panel, vision,\n    badge: panel.querySelector('#aBadge'),\n    scan: panel.querySelector('#aScan'),\n    items: [...panel.querySelectorAll('.audit__item')],\n    flag: panel.querySelector('#aFlag'),\n    title: panel.querySelector('#aTitle'),\n    meta: panel.querySelector('#aMeta'),\n    diff: panel.querySelector('#aDiff'),\n    fix: panel.querySelector('#aFix'),\n    vMode: vision.querySelector('#vMode'),\n  };\n}\n\nexport function updateAudit(a, t, scene) {\n  // panel slides in at 30.0, slides out at 36.2 (vision takes the stage)\n  const inP = easeOut(prog(t, T.auditIn, T.auditIn + 0.5));\n  const outP = easeInOut(prog(t, 36.0, 36.4));\n  a.panel.style.transform = `translateX(${lerp(490, 0, inP) + outP * 490}px)`;\n  a.panel.style.opacity = inP > 0 ? 1 : 0;\n\n  // scan bar 30.5 → 32.0\n  const scanP = prog(t, T.auditIn + 0.5, T.auditFlag);\n  a.scan.style.width = scanP * 100 + '%';\n\n  // checklist ticks land during the scan\n  a.items.forEach((item, i) => {\n    const at = T.auditIn + 0.7 + i * 0.3;\n    item.style.opacity = t >= at ? 1 : 0.25;\n    item.classList.toggle('audit__item--ok', t >= at);\n  });\n\n  const fixed = t >= T.auditPass;\n\n  // flag card pops at 32.0\n  const fp = easeOut(prog(t, T.auditFlag, T.auditFlag + 0.35));\n  a.flag.style.opacity = fp;\n  a.flag.style.transform = `translateY(${lerp(18, 0, fp)}px)`;\n  a.flag.classList.toggle('audit__flag--fixed', fixed);\n\n  if (fixed) {\n    a.title.textContent = 'Text contrast — .tag';\n    a.meta.textContent = '#c9c9c9 on #111 · 11.4:1 — AA pass';\n    a.badge.textContent = 'AA pass';\n    a.badge.className = 'audit__badge audit__badge--pass';\n    a.fix.style.opacity = 0.35;\n    a.fix.textContent = 'Fixed ✓';\n  } else if (t >= T.auditFlag) {\n    a.badge.textContent = '1 issue';\n    a.badge.className = 'audit__badge audit__badge--fail';\n    a.fix.style.opacity = 1;\n    a.fix.textContent = 'Fix it';\n  } else {\n    a.badge.textContent = 'scanning…';\n    a.badge.className = 'audit__badge';\n  }\n\n  // diff line reveals just before the fix\n  a.diff.style.opacity = t >= T.auditFix - 0.3 ? 1 : 0;\n\n  // fix click pulse at 33.5\n  const clickP = prog(t, T.auditFix, T.auditFix + 0.25);\n  a.fix.style.transform = clickP > 0 && clickP < 1 ? `scale(${1 - 0.12 * Math.sin(clickP * Math.PI)})` : '';\n\n  // the fix lands EVERYWHERE at once — flip --tagc on every msite\n  const tagColor = fixed ? '#c9c9c9' : '#666';\n  scene.ed.querySelectorAll('.msite').forEach((m) => m.style.setProperty('--tagc', tagColor));\n\n  // ---- vision simulation (36.5 / 38.0 / clear 39.5) ----\n  let filter = 'none';\n  let mode = '—';\n  let on = false;\n  if (t >= T.visionDeuter && t < T.visionCataract) {\n    filter = 'saturate(0.45) sepia(0.18) hue-rotate(-18deg)';\n    mode = 'Deuteranopia';\n    on = true;\n  } else if (t >= T.visionCataract && t < T.visionClear) {\n    filter = 'blur(2.5px) brightness(1.12) contrast(0.82)';\n    mode = 'Cataracts';\n    on = true;\n  } else if (t >= T.visionClear && t < T.visionClear + 0.8) {\n    mode = 'Clear';\n    on = true;\n  }\n  scene.row.style.filter = filter; // hits all five viewports at once\n  a.vMode.textContent = mode;\n  const vIn = easeOut(prog(t, T.visionDeuter - 0.4, T.visionDeuter - 0.1));\n  a.vision.style.opacity = on || (t >= T.visionDeuter - 0.4 && t < T.visionClear + 0.8) ? vIn : 0;\n  a.vision.style.transform = `translateX(-50%) translateY(${lerp(-16, 0, vIn)}px)`;\n}\n"},{"name":"close.js","content":"// ============ CLOSE (48.0 – 56.0): logo stamp (THE HIT) → tagline → CTA ============\nimport { T, prog, easeOut, lerp } from '../config.js';\n\nfunction el(tag, cls, html) {\n  const e = document.createElement(tag);\n  if (cls) e.className = cls;\n  if (html !== undefined) e.innerHTML = html;\n  return e;\n}\n\nexport const closeScene = {\n  name: 'close',\n  start: 48.0,\n  end: 56.0,\n\n  mount(root) {\n    this.logo = el('div', 'super close__logo', 'blnq<i>.</i>');\n    this.tagline = el('div', 'super close__tagline', 'Code, Preview &amp; Ship the Web.');\n    this.cta = el('div', 'close__cta', 'Start building → blnq.studio');\n    root.append(this.logo, this.tagline, this.cta);\n\n    this.fade = el('div');\n    this.fade.style.cssText = 'position:absolute;inset:0;background:#000;z-index:99;pointer-events:none;';\n    root.appendChild(this.fade);\n  },\n\n  update(root, t) {\n    // logo STAMPS on the hit — big to settled in 4 frames, tiny recoil\n    const sp = prog(t, T.HIT, T.HIT + 0.14);\n    const settle = easeOut(prog(t, T.HIT + 0.14, T.HIT + 0.5));\n    this.logo.style.opacity = sp > 0 ? 1 : 0;\n    this.logo.style.transform = `scale(${lerp(2.4, 1.04, easeOut(sp)) - 0.04 * settle})`;\n\n    const tp = easeOut(prog(t, T.tagline, T.tagline + 0.5));\n    this.tagline.style.opacity = tp;\n    this.tagline.style.transform = `translateY(${lerp(22, 0, tp)}px)`;\n\n    const cp = easeOut(prog(t, T.cta, T.cta + 0.4));\n    this.cta.style.opacity = cp;\n    this.cta.style.transform = `translateX(-50%) scale(${lerp(0.9, 1, cp)})`;\n\n    // fade to black 54 → 56\n    this.fade.style.opacity = prog(t, T.fadeOut, T.end - 0.3);\n  },\n};\n"},{"name":"pain.js","content":"// ============ ACT 1 — toolchain hell (0.0 – 11.0) ============\nimport { T, CHAOS, prog, easeOut, lerp } from '../config.js';\n\nconst TERMS = [\n  {\n    anchor: 1, at: 1.5, title: 'zsh — ~/my-site',\n    lines: [\n      ['$ npx create-everything@latest', '', 0.0],\n      ['Need to install 312 packages…', 'ln--dim', 0.5],\n      ['⠧ resolving dependency tree', 'ln--dim', 0.9],\n      ['added 1,427 packages in 2m 41s', 'ln--ok', 1.3],\n    ],\n  },\n  {\n    anchor: 3, at: 3.0, title: 'zsh — npm install',\n    lines: [\n      ['$ npm install', '', 0.0],\n      ['⠹ idealTree: timing 38s', 'ln--dim', 0.4],\n      ['npm WARN deprecated querystring@0.2.0', 'ln--warn', 0.8],\n      ['npm WARN deprecated left-pad@1.3.0', 'ln--warn', 1.1],\n      ['node_modules: 1.2 GB', '', 1.5],\n    ],\n  },\n  {\n    anchor: 0, at: 6.5, title: 'zsh — build',\n    lines: [\n      ['$ npm run dev', '', 0.0],\n      ['ERR! ERESOLVE peer dependency conflict', 'ln--err', 0.3],\n      [\"Error: Cannot find module 'left-pad'\", 'ln--err', 0.6],\n      ['error during build — exit code 1', 'ln--err', 0.9],\n    ],\n  },\n];\n\nconst CFGS = [\n  ['webpack.config.js', 2, 4.5, 250],\n  ['babel.config.js', 4, 4.8, 215],\n  ['tsconfig.json', 1, 5.1, 790],\n  ['.eslintrc.cjs', 3, 5.4, 420],\n  ['postcss.config.js', 0, 5.7, 660],\n  ['vite.config.ts', 2, 6.0, 560],\n  ['rollup.config.mjs', 4, 6.2, 700],\n  ['.browserslistrc', 0, 6.9, 880],\n  ['turbo.json', 3, 7.2, 860],\n  ['jest.config.js', 1, 7.5, 330],\n];\n\nfunction el(tag, cls, html) {\n  const e = document.createElement(tag);\n  if (cls) e.className = cls;\n  if (html !== undefined) e.innerHTML = html;\n  return e;\n}\n\nexport const painScene = {\n  name: 'pain',\n  start: 0,\n  end: 11.0,\n\n  mount(root) {\n    // opening question\n    const ask = el('div', 'super pain__ask');\n    ask.innerHTML = 'Want to build a web page?<span class=\"pain__caret\"></span>';\n    root.appendChild(ask);\n    this.ask = ask;\n\n    // terminals\n    this.terms = TERMS.map((cfg) => {\n      const a = CHAOS[cfg.anchor];\n      const w = el('div', 'term');\n      w.style.left = a.cx - 280 + 'px';\n      w.style.top = a.cy - 140 + 'px';\n      w.style.setProperty('--rot', cfg.rot ?? a.rot);\n      const bar = el('div', 'term__bar');\n      bar.append(el('span', 'term__dot'), el('span', 'term__dot'), el('span', 'term__dot'), el('span', '', cfg.title));\n      const body = el('div', 'term__body');\n      cfg.lineEls = cfg.lines.map(([txt, cls]) => {\n        const ln = el('span', 'ln ' + cls, txt);\n        body.appendChild(ln);\n        return ln;\n      });\n      w.append(bar, body);\n      root.appendChild(w);\n      return { cfg, w, a };\n    });\n\n    // config-file chips\n    this.cfgs = CFGS.map(([name, anchor, at, dy]) => {\n      const a = CHAOS[anchor];\n      const c = el('div', 'cfg', name);\n      c.style.left = a.cx - 110 + (anchor * 37 % 90) - 45 + 'px';\n      c.style.top = dy + 'px';\n      root.appendChild(c);\n      return { c, at, a };\n    });\n\n    // \"Two hours. Zero pixels.\"\n    const zero = el('div', 'super pain__zero', 'Two hours. Zero pixels.');\n    root.appendChild(zero);\n    this.zero = zero;\n\n    // blackout cover\n    const black = el('div');\n    black.style.cssText = 'position:absolute;inset:0;background:#000;z-index:90;';\n    root.appendChild(black);\n    this.black = black;\n  },\n\n  update(root, t) {\n    // opening line types in 0–1.2, fades 2.2–3.0\n    const askIn = prog(t, 0.15, 1.0);\n    this.ask.style.opacity = t < 2.2 ? askIn : 1 - prog(t, 2.2, 3.0);\n    this.ask.style.transform = `translateY(${lerp(0, -40, prog(t, 2.2, 3.0))}px)`;\n    this.ask.querySelector('.pain__caret').style.opacity = Math.floor(t * 2.5) % 2 === 0 ? 1 : 0;\n\n    // jitter ramps with the riser\n    const shakeAmp = lerp(0, 9, prog(t, T.riser, T.blackout));\n\n    this.terms.forEach(({ cfg, w, a }, i) => {\n      const p = easeOut(prog(t, cfg.at, cfg.at + 0.45));\n      const jx = Math.sin(t * 51 + i * 7) * shakeAmp;\n      const jy = Math.cos(t * 47 + i * 13) * shakeAmp;\n      w.style.opacity = p;\n      w.style.transform = `translate(${jx}px,${lerp(60, 0, p) + jy}px) rotate(${a.rot}deg) scale(${lerp(0.92, 1, p)})`;\n      cfg.lines.forEach(([, , dt], li) => {\n        cfg.lineEls[li].style.opacity = t >= cfg.at + 0.35 + dt ? 1 : 0;\n      });\n    });\n\n    this.cfgs.forEach(({ c, at, a }, i) => {\n      const p = easeOut(prog(t, at, at + 0.4));\n      const jx = Math.sin(t * 55 + i * 3) * shakeAmp;\n      const jy = Math.cos(t * 49 + i * 5) * shakeAmp;\n      c.style.opacity = p;\n      c.style.transform = `translate(${jx}px,${lerp(-40, 0, p) + jy}px) rotate(${a.rot * 0.6}deg)`;\n    });\n\n    // \"Two hours. Zero pixels.\"\n    const zp = prog(t, T.zeroPixels, T.zeroPixels + 0.4);\n    this.zero.style.opacity = zp;\n    this.zero.style.transform = `scale(${lerp(1.15, 1, easeOut(zp))})`;\n\n    // hard blackout at 10.85\n    this.black.style.opacity = t >= T.blackout ? 1 : 0;\n  },\n};\n"},{"name":"proof.js","content":"// ============ ACT 3 — the proof (15.0 – 48.0) ============\n// One journey: type → live render → 5 synced viewports → audit → vision → ship.\nimport { T, FIVE_UP, prog, easeOut, easeInOut, lerp } from '../config.js';\nimport { mountAudit, updateAudit } from './audit.js';\nimport { mountShip, updateShip } from './ship.js';\n\nfunction el(tag, cls, html) {\n  const e = document.createElement(tag);\n  if (cls) e.className = cls;\n  if (html !== undefined) e.innerHTML = html;\n  return e;\n}\n\n// ---- the code being \"typed\" (tokenized: [text, tokenClass]) ----\nconst HTML_SEGS = [\n  ['<', 'tk-pun'], ['header', 'tk-tag'], [' ', ''], ['class', 'tk-attr'], ['=', 'tk-pun'], ['\"hero\"', 'tk-str'], ['>', 'tk-pun'], ['\\n  ', ''],\n  ['<', 'tk-pun'], ['h1', 'tk-tag'], ['>', 'tk-pun'], [\"Hoff's Bar\", 'tk-txt'], ['</', 'tk-pun'], ['h1', 'tk-tag'], ['>', 'tk-pun'], ['\\n  ', ''],\n  ['<', 'tk-pun'], ['p', 'tk-tag'], [' ', ''], ['class', 'tk-attr'], ['=', 'tk-pun'], ['\"tag\"', 'tk-str'], ['>', 'tk-pun'], ['Cold drinks. Warm neon.', 'tk-txt'], ['</', 'tk-pun'], ['p', 'tk-tag'], ['>', 'tk-pun'], ['\\n  ', ''],\n  ['<', 'tk-pun'], ['a', 'tk-tag'], [' ', ''], ['class', 'tk-attr'], ['=', 'tk-pun'], ['\"btn\"', 'tk-str'], ['>', 'tk-pun'], ['See the menu', 'tk-txt'], ['</', 'tk-pun'], ['a', 'tk-tag'], ['>', 'tk-pun'], ['\\n', ''],\n  ['</', 'tk-pun'], ['header', 'tk-tag'], ['>', 'tk-pun'], ['\\n', ''],\n  ['<', 'tk-pun'], ['ul', 'tk-tag'], [' ', ''], ['class', 'tk-attr'], ['=', 'tk-pun'], ['\"menu\"', 'tk-str'], ['>', 'tk-pun'], ['…', 'tk-pun'], ['</', 'tk-pun'], ['ul', 'tk-tag'], ['>', 'tk-pun'],\n];\n\nconst CSS_SEGS = [\n  ['body', 'tk-sel'], [' { ', 'tk-pun'], ['background', 'tk-prop'], [': ', 'tk-pun'], ['#111', 'tk-val'], ['; ', 'tk-pun'], ['color', 'tk-prop'], [': ', 'tk-pun'], ['#f4f4f4', 'tk-val'], [' }', 'tk-pun'], ['\\n', ''],\n  ['.hero', 'tk-sel'], [' { ', 'tk-pun'], ['padding', 'tk-prop'], [': ', 'tk-pun'], ['4rem 2rem', 'tk-val'], ['; ', 'tk-pun'], ['text-align', 'tk-prop'], [': ', 'tk-pun'], ['center', 'tk-val'], [' }', 'tk-pun'], ['\\n', ''],\n  ['h1', 'tk-sel'], [' { ', 'tk-pun'], ['color', 'tk-prop'], [': ', 'tk-pun'], ['#ffd300', 'tk-val'], ['; ', 'tk-pun'], ['font-size', 'tk-prop'], [': ', 'tk-pun'], ['3rem', 'tk-val'], [' }', 'tk-pun'], ['\\n', ''],\n  ['.tag', 'tk-sel'], [' { ', 'tk-pun'], ['color', 'tk-prop'], [': ', 'tk-pun'], ['#666', 'tk-val'], [' }', 'tk-pun'], ['\\n', ''],\n  ['.btn', 'tk-sel'], [' { ', 'tk-pun'], ['background', 'tk-prop'], [': ', 'tk-pun'], ['#ffd300', 'tk-val'], ['; ', 'tk-pun'], ['color', 'tk-prop'], [': ', 'tk-pun'], ['#1e1e1e', 'tk-val'], [' }', 'tk-pun'],\n];\n\nfunction flatten(segs) {\n  const chars = [];\n  for (const [txt, cls] of segs) for (const ch of txt) chars.push([ch, cls]);\n  return chars;\n}\nconst HTML_CHARS = flatten(HTML_SEGS);\nconst CSS_CHARS = flatten(CSS_SEGS);\n\nfunction renderTyped(chars, n, withCaret) {\n  let html = '';\n  let cur = null;\n  for (let i = 0; i < Math.min(n, chars.length); i++) {\n    const [ch, cls] = chars[i];\n    if (cls !== cur) {\n      if (cur !== null) html += '</span>';\n      html += `<span class=\"${cls}\">`;\n      cur = cls;\n    }\n    html += ch === '<' ? '&lt;' : ch === '>' ? '&gt;' : ch === '&' ? '&amp;' : ch;\n  }\n  if (cur !== null) html += '</span>';\n  if (withCaret) html += '<span class=\"code__caret\"></span>';\n  return html;\n}\n\n// ---- the mini site shown in preview + every device ----\nexport function msiteHTML() {\n  return `<div class=\"msite\">\n    <div class=\"msite__hero\">\n      <div class=\"msite__h1 ms-h1\">Hoff's Bar</div>\n      <div class=\"msite__tag ms-tag\">Cold drinks. Warm neon.</div>\n      <span class=\"msite__btn ms-btn\">See the menu</span>\n    </div>\n    <div class=\"msite__menu ms-menu\">\n      <div class=\"msite__item\"><span>Neon Spritz</span><span>9</span></div>\n      <div class=\"msite__item\"><span>Midnight Stout</span><span>7</span></div>\n      <div class=\"msite__item\"><span>Static Sour</span><span>8</span></div>\n      <div class=\"msite__item\"><span>House Lager</span><span>6</span></div>\n    </div>\n  </div>`;\n}\n\nexport const proofScene = {\n  name: 'proof',\n  start: 15.0,\n  end: 48.0,\n\n  mount(root) {\n    const ed = el('div', 'ed');\n    const bar = el('div', 'ed__bar');\n    const url = el('div', 'ed__url');\n    url.innerHTML = '<b>blnq</b>.studio/e';\n    const actions = el('div', 'ed__actions');\n    actions.innerHTML = `<span class=\"ed__chip\">5 viewports</span>\n      <span class=\"ed__chip\">A11y</span>\n      <span class=\"ed__chip ed__chip--accent\" id=\"shipChip\">Ship ▸</span>`;\n    bar.append(el('span', 'ed__dot'), el('span', 'ed__dot'), el('span', 'ed__dot'), url, actions);\n\n    const main = el('div', 'ed__main');\n\n    // code pane\n    const code = el('div', 'code');\n    const tabs = el('div', 'code__tabs');\n    tabs.innerHTML = `<span class=\"code__tab\" data-f=\"html\">index.html</span>\n      <span class=\"code__tab\" data-f=\"css\">styles.css</span>`;\n    const body = el('div', 'code__body');\n    const lns = el('div', 'code__lns', '1\\n2\\n3\\n4\\n5\\n6\\n7');\n    const txt = el('div', 'code__txt');\n    body.append(lns, txt);\n    code.append(tabs, body);\n\n    // view area\n    const view = el('div', 'view');\n\n    // single big preview (pre-split)\n    const single = el('div', 'dev');\n    single.style.cssText = 'left:40px;top:40px;right:40px;bottom:40px;border-radius:10px;';\n    const sScreen = el('div', 'dev__screen');\n    sScreen.innerHTML = msiteHTML();\n    single.appendChild(sScreen);\n    view.appendChild(single);\n\n    // five devices (post-split) — row container so audit/vision can transform all at once\n    const row = el('div');\n    row.style.cssText = 'position:absolute;inset:0;transform-origin:40px center;';\n    this.devs = FIVE_UP.map((f) => {\n      const d = el('div', 'dev');\n      d.style.cssText = `left:${f.x - 60}px;top:${f.y - 116}px;width:${f.w}px;height:${f.h}px;`;\n      const screen = el('div', 'dev__screen');\n      const inner = el('div', 'dev__inner');\n      inner.style.cssText = `width:${f.w / f.s * f.s === f.w ? f.w / f.s : f.w / f.s}px;transform:scale(${f.s});transform-origin:top left;`;\n      inner.style.width = f.w / f.s + 'px';\n      inner.innerHTML = msiteHTML();\n      screen.appendChild(inner);\n      const label = el('div', 'dev__label', f.name);\n      d.append(screen, label);\n      const ripple = el('div', 'ripple');\n      ripple.style.cssText += `left:${f.w / 2}px;top:${210 * f.s}px;opacity:0;`;\n      d.appendChild(ripple);\n      row.appendChild(d);\n      return { d, inner, ripple, f };\n    });\n    view.appendChild(row);\n\n    main.append(code, view);\n    ed.append(bar, main);\n    root.appendChild(ed);\n\n    this.ed = ed;\n    this.code = code;\n    this.txt = txt;\n    this.tabs = tabs;\n    this.view = view;\n    this.single = single;\n    this.sScreen = sScreen;\n    this.row = row;\n\n    // audit + ship sub-mounts (own overlays inside view / root)\n    this.audit = mountAudit(view, root);\n    this.ship = mountShip(view, root);\n  },\n\n  update(root, t) {\n    // --- editor frame grows out of the act-2 tab (1100x640 centred → full) ---\n    const grow = easeInOut(prog(t, T.editorIn, T.editorIn + 0.6));\n    const s = lerp(0.611, 1, grow);\n    const ix = lerp(350, 0, grow); // (1920-1100)/2 - 60 ≈ 350\n    const iy = lerp(160, 0, grow); // (1080-640)/2 - 60 ≈ 160\n    this.ed.style.transform = `translate(${ix}px,${iy}px) scale(${s})`;\n    this.ed.style.transformOrigin = 'top left';\n\n    // --- typing ---\n    const isCss = t >= T.typeCss;\n    const nHtml = Math.floor(easeInOut(prog(t, T.typeHtml, T.typeCss - 0.3)) * HTML_CHARS.length);\n    const nCss = Math.floor(easeInOut(prog(t, T.typeCss + 0.2, T.typeDone)) * CSS_CHARS.length);\n    const typingDone = t >= T.typeDone;\n    this.txt.innerHTML = isCss\n      ? renderTyped(CSS_CHARS, nCss, !typingDone)\n      : renderTyped(HTML_CHARS, nHtml, true);\n    this.tabs.querySelectorAll('.code__tab').forEach((tab) => {\n      tab.classList.toggle('code__tab--on', (tab.dataset.f === 'css') === isCss);\n    });\n\n    // code pane collapses for five-up\n    const collapse = easeInOut(prog(t, T.fiveUp - 0.4, T.fiveUp + 0.2));\n    this.code.style.width = lerp(720, 0, collapse) + 'px';\n    this.code.style.opacity = 1 - collapse;\n\n    // --- single preview: elements reveal as their markup lands ---\n    const hp = nHtml / HTML_CHARS.length;\n    const rev = (sel, at) => {\n      this.sScreen.querySelectorAll(sel).forEach((n) => {\n        const p = at <= hp ? 1 : 0;\n        n.style.opacity = p;\n      });\n    };\n    rev('.ms-h1', 0.18);\n    rev('.ms-tag', 0.45);\n    rev('.ms-btn', 0.68);\n    rev('.ms-menu', 0.97);\n    // pre-CSS the site is unstyled-ish: fade palette in with CSS typing\n    const cssP = isCss ? nCss / CSS_CHARS.length : 0;\n    this.sScreen.querySelector('.msite').style.filter = `grayscale(${1 - cssP})`;\n    this.single.style.opacity = (t < T.fiveUp ? 1 : 1 - prog(t, T.fiveUp, T.fiveUp + 0.35)) * prog(t, T.editorIn + 0.4, T.editorIn + 0.7);\n\n    // --- five-up split ---\n    this.devs.forEach(({ d, inner, ripple }, i) => {\n      const p = easeOut(prog(t, T.fiveUp + i * 0.12, T.fiveUp + 0.5 + i * 0.12));\n      d.style.opacity = p;\n      d.style.transform = `translateY(${lerp(40, 0, p)}px) scale(${lerp(0.85, 1, p)})`;\n      // synced scroll (26.0–27.6): same logical pixels in every frame\n      const sc = easeInOut(prog(t, T.syncScroll, T.syncScroll + 1.6));\n      const back = easeInOut(prog(t, T.syncScroll + 2.4, T.syncScroll + 3.4));\n      inner.style.transform = `scale(${this.devs[i].f.s}) translateY(${-(sc - back) * 170}px)`;\n      // synced click ripple at 28.0\n      const rp = prog(t, T.syncClick, T.syncClick + 0.55);\n      ripple.style.opacity = rp > 0 && rp < 1 ? 1 - rp : 0;\n      ripple.style.transform = `translate(-50%,-50%) scale(${lerp(0.4, 2.6, easeOut(rp))})`;\n    });\n\n    // row compresses left while audit panel / ship column is open\n    const squeeze = easeInOut(prog(t, T.auditIn - 0.4, T.auditIn + 0.3));\n    const release = 0; // stays compressed through ship — URL takes over at 45\n    this.row.style.transform = `scale(${lerp(1, 0.76, squeeze - release)})`;\n\n    updateAudit(this.audit, t, this);\n    updateShip(this.ship, t, this);\n  },\n};\n"},{"name":"ship.js","content":"// ============ Proof sub-scene: ship it (40.0 – 48.0) — deploy → live URL (THE CRASH) ============\nimport { T, prog, easeOut, lerp } from '../config.js';\n\nfunction el(tag, cls, html) {\n  const e = document.createElement(tag);\n  if (cls) e.className = cls;\n  if (html !== undefined) e.innerHTML = html;\n  return e;\n}\n\nconst LOG_LINES = [\n  ['→ pushing to GitHub', 42.0],\n  ['→ netlify build started', 42.7],\n  ['→ optimizing 2 files', 43.4],\n  ['→ uploading assets', 44.0],\n  ['✓ done in 2.8s', 44.6, 'ok'],\n];\n\nexport function mountShip(view, root) {\n  const menu = el('div', 'ship__menu');\n  menu.innerHTML = `\n    <div class=\"ship__opt\" data-o=\"gh\">GitHub <span>push</span></div>\n    <div class=\"ship__opt\" data-o=\"nl\">Netlify <span>deploy</span></div>\n    <div class=\"ship__opt\" data-o=\"vc\">Vercel <span>deploy</span></div>`;\n  view.appendChild(menu);\n\n  const log = el('div', 'ship__log');\n  log.innerHTML = LOG_LINES.map(([txt, , cls]) => `<div class=\"${cls || ''}\">${txt}</div>`).join('');\n  view.appendChild(log);\n\n  const url = el('div', 'url');\n  url.innerHTML = `<span class=\"url__dot\"></span>hoffs-bar.netlify.app<span class=\"url__live\">LIVE</span>`;\n  root.appendChild(url);\n\n  const copy = el('div', 'super ship__copy', 'Shipped. From a tab.');\n  root.appendChild(copy);\n\n  const flash = el('div', 'flash');\n  root.appendChild(flash);\n\n  const cur = el('div', 'fcur');\n  root.appendChild(cur);\n\n  return {\n    menu, log, url, copy, flash, cur,\n    opts: [...menu.querySelectorAll('.ship__opt')],\n    logLines: [...log.children],\n  };\n}\n\n// fake-cursor waypoints: [time, x, y] on the 1920x1080 stage\nconst CUR_PATH = [\n  [32.6, 1180, 700],\n  [33.5, 1462, 562],  // audit \"Fix it\" button\n  [34.2, 1462, 562],\n  [40.5, 1745, 100],  // \"Ship ▸\" chip in the editor bar\n  [41.0, 1745, 100],\n  [41.5, 1660, 270],  // Netlify row in the menu\n  [42.2, 1660, 270],\n];\n\nfunction cursorPos(t) {\n  if (t <= CUR_PATH[0][0]) return CUR_PATH[0];\n  for (let i = 0; i < CUR_PATH.length - 1; i++) {\n    const [t0, x0, y0] = CUR_PATH[i];\n    const [t1, x1, y1] = CUR_PATH[i + 1];\n    if (t >= t0 && t < t1) {\n      const p = easeOut((t - t0) / (t1 - t0));\n      return [t, lerp(x0, x1, p), lerp(y0, y1, p)];\n    }\n  }\n  return CUR_PATH[CUR_PATH.length - 1];\n}\n\nexport function updateShip(s, t, scene) {\n  // fake cursor lives 32.6 → 42.4 (fix click → ship click → netlify click)\n  const curOn = t >= 32.6 && t < 42.4;\n  s.cur.style.opacity = curOn ? 1 : 0;\n  if (curOn) {\n    const [, x, y] = cursorPos(t);\n    // click pulses at 33.5 (fix), 40.5 (ship), 41.5 (netlify)\n    let sc = 1;\n    for (const ct of [T.auditFix, T.shipMenu, T.shipNetlify]) {\n      const cp = prog(t, ct, ct + 0.22);\n      if (cp > 0 && cp < 1) sc = 1 - 0.35 * Math.sin(cp * Math.PI);\n    }\n    s.cur.style.transform = `translate(-50%,-50%) scale(${sc})`;\n    s.cur.style.left = x - 60 + 'px'; // stage → scene-root coords (scene root = stage)\n    s.cur.style.left = x + 'px';\n    s.cur.style.top = y + 'px';\n  }\n\n  // menu opens at 40.5\n  const mp = easeOut(prog(t, T.shipMenu, T.shipMenu + 0.3));\n  s.menu.style.opacity = mp * (t < T.CRASH ? 1 : 1 - prog(t, T.CRASH, T.CRASH + 0.2));\n  s.menu.style.transform = `translateY(${lerp(-14, 0, mp)}px)`;\n\n  // netlify highlights at 41.5\n  s.opts.forEach((o) => o.classList.toggle('ship__opt--on', o.dataset.o === 'nl' && t >= T.shipNetlify));\n\n  // build log ticks 42.0 → 44.6\n  const lp = prog(t, T.shipNetlify + 0.3, T.shipNetlify + 0.6);\n  s.log.style.opacity = lp * (t < T.CRASH ? 1 : 1 - prog(t, T.CRASH, T.CRASH + 0.2));\n  s.logLines.forEach((ln, i) => {\n    ln.style.opacity = t >= LOG_LINES[i][1] ? 1 : 0.18;\n  });\n\n  // ---- THE CRASH (45.0): live URL slams in ----\n  const up = easeOut(prog(t, T.CRASH, T.CRASH + 0.35));\n  s.url.style.opacity = up;\n  s.url.style.transform = `translate(-50%,-50%) scale(${lerp(1.7, 1, up)})`;\n\n  // yellow flash, gone in 300ms\n  const fl = prog(t, T.CRASH, T.CRASH + 0.3);\n  s.flash.style.opacity = fl > 0 && fl < 1 ? 0.85 * (1 - easeOut(fl)) : 0;\n\n  // closing copy\n  const cp = easeOut(prog(t, T.shippedCopy, T.shippedCopy + 0.4));\n  s.copy.style.opacity = cp;\n  s.copy.style.transform = `translateY(${lerp(24, 0, cp)}px)`;\n}\n"},{"name":"turn.js","content":"// ============ ACT 2 — the turn (11.0 – 15.0): chaos collapses into one tab ============\nimport { T, CHAOS, prog, easeOut, lerp } from '../config.js';\n\nfunction el(tag, cls, html) {\n  const e = document.createElement(tag);\n  if (cls) e.className = cls;\n  if (html !== undefined) e.innerHTML = html;\n  return e;\n}\n\nexport const turnScene = {\n  name: 'turn',\n  start: 11.0,\n  end: 15.0,\n\n  mount(root) {\n    // ghost fragments of the act-1 chaos, flying INTO the tab\n    this.frags = CHAOS.map((a) => {\n      const f = el('div');\n      f.style.cssText = `position:absolute;width:200px;height:130px;border-radius:8px;\n        background:#121419;box-shadow:0 0 0 1px #262a32;left:${a.cx - 100}px;top:${a.cy - 65}px;`;\n      root.appendChild(f);\n      return { f, a };\n    });\n\n    // the one tab\n    const tab = el('div', 'turn__tab');\n    const bar = el('div', 'turn__bar');\n    const url = el('div', 'turn__url');\n    url.innerHTML = '<b>blnq</b>.studio';\n    bar.append(el('span', 'turn__dot'), el('span', 'turn__dot'), el('span', 'turn__dot'), url);\n    tab.appendChild(bar);\n\n    const line1 = el('div', 'super turn__line1', 'Or: just open a tab.');\n    const stamp = el('div', 'super turn__stamp',\n      'No install. No build step. <em>No excuses.</em>');\n    tab.append(line1, stamp);\n    root.appendChild(tab);\n\n    this.tab = tab;\n    this.url = url;\n    this.line1 = line1;\n    this.stamp = stamp;\n  },\n\n  update(root, t) {\n    // fragments race to centre and die (11.0 – 11.55)\n    this.frags.forEach(({ f, a }, i) => {\n      const p = easeOut(prog(t, T.DROP + i * 0.04, T.DROP + 0.45 + i * 0.04));\n      const dx = (960 - a.cx) * p;\n      const dy = (540 - a.cy) * p;\n      f.style.transform = `translate(${dx}px,${dy}px) scale(${lerp(1, 0.05, p)}) rotate(${a.rot * (1 - p)}deg)`;\n      f.style.opacity = 1 - prog(t, T.DROP + 0.35, T.DROP + 0.55);\n    });\n\n    // tab punches in ON the drop\n    const pop = easeOut(prog(t, T.DROP, T.DROP + 0.4));\n    const overshoot = 1 + 0.05 * Math.sin(Math.min(1, pop) * Math.PI);\n    this.tab.style.opacity = pop;\n    this.tab.style.transform = `translate(-50%,-50%) scale(${lerp(0.6, 1, pop) * overshoot})`;\n\n    // url types in\n    const full = 'blnq.studio';\n    const n = Math.floor(prog(t, T.DROP + 0.5, T.tabLabel) * full.length);\n    const shown = full.slice(0, n);\n    this.url.innerHTML = `<b>${shown.slice(0, 4)}</b>${shown.slice(4)}<span style=\"color:#ffd300\">${n < full.length ? '▎' : ''}</span>`;\n\n    // copy\n    this.line1.style.opacity = prog(t, T.tabLabel, T.tabLabel + 0.4) - prog(t, T.stamp - 0.2, T.stamp);\n    this.line1.style.transform = `translateY(${lerp(20, 0, easeOut(prog(t, T.tabLabel, T.tabLabel + 0.4)))}px)`;\n\n    const sp = prog(t, T.stamp, T.stamp + 0.35);\n    this.stamp.style.opacity = sp;\n    this.stamp.style.transform = `scale(${lerp(1.18, 1, easeOut(sp))})`;\n  },\n};\n"}],"folders":[]}]},{"folder":"other","files":[{"name":"main.sketch","content":""}],"folders":[]}]},"variants":null,"createdAt":"2026-06-11T12:37:42.325Z","updatedAt":"2026-06-11T12:37:54.342Z"}}