+
+## Beats
+| Time | Beat | Real footage |
+|---|---|---|
+| 0–3s | *What's this site built with?* | stripe.com's real HTML source, blurred and scrolling |
+| 3–5s | Reveal | Logo, **WhatStack**, *What's under this page?* |
+| 5–9s | *It reads the stack while you browse.* | nextjs.org → notion.com → stripe.com, each with its real toolbar badge (4, 4, 4) |
+| 9–13s | *One click. The whole stack.* | The icon is clicked, and the real stripe.com popup shows *Next.js 14.2.32 · React · Webpack* |
+| 13–16s | *Confidence, not guesses.* | HIGH and MEDIUM badges light up, then GitHub's real *Lower confidence* section |
+| 16–19s | *See why it matched.* | The real Next.js evidence (scripts, globals and DOM) |
+| 19–23s | *Any site. Instantly.* | Real popups for notion.com, nextjs.org, react.dev, netflix.com, airbnb.com and vuejs.org |
+| 23–26s | *Copy as text, Markdown or JSON.* | The real MD and JSON output from the Copy buttons |
+| 26–28s | *Runs entirely on your device.* | The real *Local only* pill. Sizes are from the store listing (49 KB) |
+| 28–32s | CTA | The real Chrome Web Store header (logged out), clicking *Add to Chrome*, then the end card |
+
+## How it's made
+It's rendered frame by frame from code, with no motion design app.
+
+- [`source/capture.mjs`](source/capture.mjs) loads the **unpacked extension (v1.7.2)** into Chromium with Playwright, visits each site, reads the real badge, and opens the real `popup.html` against the real service worker and a real deep scan at 3× resolution. The only change is that `chrome.tabs.query` is pointed at the site's tab, because a popup opened as a tab would otherwise scan itself. Copy output is recorded from the real Copy / MD / JSON buttons.
+- [`source/timeline.json`](source/timeline.json) sets every scene and cue in **beats** (120 BPM). The picture and the sound both read from it.
+- [`source/engine.js`](source/engine.js) is a small animation engine built on [skia-canvas](https://skia-canvas.org). Every element is a pure function of time. The browser window is drawn as a 3D plane with true perspective, and each output frame averages **10 renders** for motion blur, which never spans a hard cut.
+- [`source/render.js`](source/render.js) splits the frames across 8 parallel workers and pipes them into FFmpeg. [`source/score.py`](source/score.py) generates the music and UI sounds from the same timeline.
+
+## Notes
+- The browser window is a simple generic frame, not Chrome's real toolbar. It shows only the real WhatStack icon and badge.
+- **Badge vs popup count:** the badge comes from the light scan when the page loads. The popup runs a deep scan, which can find more: on stripe.com the badge shows 4, but the popup shows "5 solid". Once the popup opens, the badge updates to 5. Both numbers are filmed exactly as the extension shows them.
+- Linear is not included, because it reported Next.js "v1.0.0-beta.9", which looks like a detection bug.
+- No technology count or user count is claimed.
diff --git a/launch-video/preview-vertical.gif b/launch-video/preview-vertical.gif
new file mode 100644
index 0000000..3f9206e
Binary files /dev/null and b/launch-video/preview-vertical.gif differ
diff --git a/launch-video/preview.gif b/launch-video/preview.gif
new file mode 100644
index 0000000..249c03f
Binary files /dev/null and b/launch-video/preview.gif differ
diff --git a/launch-video/source/cap/meta.json b/launch-video/source/cap/meta.json
new file mode 100644
index 0000000..9ece851
--- /dev/null
+++ b/launch-video/source/cap/meta.json
@@ -0,0 +1,1469 @@
+{
+ "stripe": {
+ "url": "https://stripe.com/",
+ "light": {
+ "badge": "4",
+ "title": "WhatStack: Next.js · 4 signals"
+ },
+ "deep": {
+ "badge": "5",
+ "title": "WhatStack: Next.js · 5 signals"
+ },
+ "popup": {
+ "nat": 560,
+ "status": "5 solid · 5 total · deep · Next.js",
+ "headline": {
+ "x": 10,
+ "y": 116.015625,
+ "w": 340,
+ "h": 58.6875
+ },
+ "headlineText": "CORE STACK\n\nNext.js 14.2.32 · React · Webpack",
+ "sections": [
+ {
+ "title": "FRAMEWORKS",
+ "x": 10,
+ "y": 184.703125,
+ "w": 340,
+ "h": 111.9375
+ },
+ {
+ "title": "BUILD",
+ "x": 10,
+ "y": 304.640625,
+ "w": 340,
+ "h": 72.9375
+ },
+ {
+ "title": "OBSERVABILITY",
+ "x": 10,
+ "y": 385.578125,
+ "w": 340,
+ "h": 72.9375
+ },
+ {
+ "title": "ANALYTICS & PRODUCT",
+ "x": 10,
+ "y": 466.515625,
+ "w": 340,
+ "h": 72.9375
+ }
+ ],
+ "hits": [
+ {
+ "text": "Next.js v14.2.32 HIGH",
+ "x": 11,
+ "y": 218.640625,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 227.390625,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ },
+ {
+ "text": "React HIGH",
+ "x": 11,
+ "y": 257.640625,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 266.390625,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ },
+ {
+ "text": "Webpack HIGH",
+ "x": 11,
+ "y": 338.578125,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 347.328125,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ },
+ {
+ "text": "Sentry HIGH",
+ "x": 11,
+ "y": 419.515625,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 428.265625,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ },
+ {
+ "text": "Google Analytics MEDIUM",
+ "x": 11,
+ "y": 500.453125,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 279.28125,
+ "y": 509.203125,
+ "w": 59.71875,
+ "h": 20.5
+ }
+ }
+ ],
+ "low": null,
+ "evidence": [],
+ "buttons": {
+ "Rescan": {
+ "x": 282.609375,
+ "y": 15.34375,
+ "w": 63.390625,
+ "h": 31
+ },
+ "Next.js\nv14.2.32\nHIGH": {
+ "x": 11,
+ "y": 218.640625,
+ "w": 338,
+ "h": 38
+ },
+ "React\nHIGH": {
+ "x": 11,
+ "y": 257.640625,
+ "w": 338,
+ "h": 38
+ },
+ "Webpack\nHIGH": {
+ "x": 11,
+ "y": 338.578125,
+ "w": 338,
+ "h": 38
+ },
+ "Sentry\nHIGH": {
+ "x": 11,
+ "y": 419.515625,
+ "w": 338,
+ "h": 38
+ },
+ "Google Analytics\nMEDIUM": {
+ "x": 11,
+ "y": 500.453125,
+ "w": 338,
+ "h": 38
+ },
+ "Copy": {
+ "x": 14,
+ "y": 570.453125,
+ "w": 50.828125,
+ "h": 31
+ },
+ "MD": {
+ "x": 70.828125,
+ "y": 570.453125,
+ "w": 41.984375,
+ "h": 31
+ },
+ "JSON": {
+ "x": 118.8125,
+ "y": 570.453125,
+ "w": 51.3125,
+ "h": 31
+ },
+ "Wrong?": {
+ "x": 178.125,
+ "y": 570.453125,
+ "w": 66.46875,
+ "h": 31
+ }
+ },
+ "local": {
+ "x": 281.203125,
+ "y": 574.703125,
+ "w": 64.796875,
+ "h": 22.5
+ },
+ "url": "https://stripe.com/",
+ "H": 613,
+ "expanded": {
+ "status": "5 solid · 5 total · deep · Next.js",
+ "headline": {
+ "x": 10,
+ "y": 116.015625,
+ "w": 340,
+ "h": 58.6875
+ },
+ "headlineText": "CORE STACK\n\nNext.js 14.2.32 · React · Webpack",
+ "sections": [
+ {
+ "title": "FRAMEWORKS",
+ "x": 10,
+ "y": 184.703125,
+ "w": 340,
+ "h": 299.375
+ },
+ {
+ "title": "BUILD",
+ "x": 10,
+ "y": 492.078125,
+ "w": 340,
+ "h": 72.9375
+ },
+ {
+ "title": "OBSERVABILITY",
+ "x": 10,
+ "y": 573.015625,
+ "w": 340,
+ "h": 72.9375
+ },
+ {
+ "title": "ANALYTICS & PRODUCT",
+ "x": 10,
+ "y": 653.953125,
+ "w": 340,
+ "h": 72.9375
+ }
+ ],
+ "hits": [
+ {
+ "text": "Next.js v14.2.32 HIGH",
+ "x": 11,
+ "y": 218.640625,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 227.390625,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ },
+ {
+ "text": "React HIGH",
+ "x": 11,
+ "y": 445.078125,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 453.828125,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ },
+ {
+ "text": "Webpack HIGH",
+ "x": 11,
+ "y": 526.015625,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 534.765625,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ },
+ {
+ "text": "Sentry HIGH",
+ "x": 11,
+ "y": 606.953125,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 615.703125,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ },
+ {
+ "text": "Google Analytics MEDIUM",
+ "x": 11,
+ "y": 687.890625,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 279.28125,
+ "y": 696.640625,
+ "w": 59.71875,
+ "h": 20.5
+ }
+ }
+ ],
+ "low": null,
+ "evidence": [
+ {
+ "x": 11,
+ "y": 267.640625,
+ "w": 338,
+ "h": 165.4375
+ }
+ ],
+ "buttons": {
+ "Rescan": {
+ "x": 282.609375,
+ "y": 15.34375,
+ "w": 63.390625,
+ "h": 31
+ },
+ "Next.js\nv14.2.32\nHIGH": {
+ "x": 11,
+ "y": 218.640625,
+ "w": 338,
+ "h": 38
+ },
+ "React\nHIGH": {
+ "x": 11,
+ "y": 445.078125,
+ "w": 338,
+ "h": 38
+ },
+ "Webpack\nHIGH": {
+ "x": 11,
+ "y": 526.015625,
+ "w": 338,
+ "h": 38
+ },
+ "Sentry\nHIGH": {
+ "x": 11,
+ "y": 606.953125,
+ "w": 338,
+ "h": 38
+ },
+ "Google Analytics\nMEDIUM": {
+ "x": 11,
+ "y": 687.890625,
+ "w": 338,
+ "h": 38
+ },
+ "Copy": {
+ "x": 14,
+ "y": 757.890625,
+ "w": 50.828125,
+ "h": 31
+ },
+ "MD": {
+ "x": 70.828125,
+ "y": 757.890625,
+ "w": 41.984375,
+ "h": 31
+ },
+ "JSON": {
+ "x": 118.8125,
+ "y": 757.890625,
+ "w": 51.3125,
+ "h": 31
+ },
+ "Wrong?": {
+ "x": 178.125,
+ "y": 757.890625,
+ "w": 66.46875,
+ "h": 31
+ }
+ },
+ "local": {
+ "x": 281.203125,
+ "y": 762.140625,
+ "w": 64.796875,
+ "h": 22.5
+ },
+ "url": "https://stripe.com/",
+ "H": 801,
+ "evidenceText": [
+ "scripthttps://b.stripecdn.com/mkt-ssr-statics/assets/_next/static/chunks/polyfills-42372ed130431b0a.js",
+ "domscript#__NEXT_DATA__",
+ "globalwindow.__NEXT_DATA__",
+ "globalwindow.webpackChunk_N_E",
+ "globalwindow.next v14.2.32",
+ "domnext-route-announcer",
+ "dom#__next"
+ ]
+ },
+ "exports": {
+ "text": "WhatStack for https://stripe.com/:\nStack: Next.js 14.2.32 · React · Webpack\nFrameworks: Next.js@14.2.32 (high), React (high)\nBuild: Webpack (high)\nObservability: Sentry (high)\nAnalytics & product: Google Analytics (medium)",
+ "textStatus": "Copied text summary",
+ "md": "## WhatStack\n**URL:** https://stripe.com/\n**Stack:** Next.js 14.2.32 · React · Webpack\n| Tech | Category | Confidence | Version |\n| --- | --- | --- | --- |\n| Next.js | Frameworks | high | 14.2.32 |\n| React | Frameworks | high | — |\n| Webpack | Build | high | — |\n| Sentry | Observability | high | — |\n| Google Analytics | Analytics & product | medium | — |",
+ "mdStatus": "Copied Markdown",
+ "json": "{\n \"tool\": \"WhatStack\",\n \"url\": \"https://stripe.com/\",\n \"pass\": \"deep\",\n \"headline\": \"Next.js 14.2.32 · React · Webpack\",\n \"primary\": {\n \"id\": \"nextjs\",\n \"name\": \"Next.js\"\n },\n \"hits\": [\n {\n \"id\": \"nextjs\",\n \"name\": \"Next.js\",\n \"category\": \"framework\",\n \"confidence\": \"high\",\n \"version\": \"14.2.32\",\n \"evidence\": [\n {\n \"type\": \"script\",\n \"snippet\": \"https://b.stripecdn.com/mkt-ssr-statics/assets/_next/static/chunks/polyfills-42372ed130431b0a.js\",\n \"weight\": 5\n },\n {\n \"type\": \"dom\",\n \"snippet\": \"script#__NEXT_DATA__\",\n \"weight\": 5\n },\n {\n \"type\": \"global\",\n \"snippet\": \"window.__NEXT_DATA__\",\n \"weight\": 5\n },\n {\n \"type\": \"global\",\n \"snippet\": \"window.webpackChunk_N_E\",\n \"weight\": 4\n },\n {\n \"type\": \"global\",\n \"snippet\": \"window.next v14.2.32\",\n \"weight\": 2\n },\n {\n \"type\": \"dom\",\n \"snippet\": \"next-route-announcer\",\n \"weight\": 4\n },\n {\n \"type\": \"dom\",\n \"snippet\": \"#__next\",\n \"weight\": 2\n }\n ]\n },\n {\n \"id\": \"react\",\n \"name\": \"React\",\n \"category\": \"framework\",\n \"confidence\": \"high\",\n \"version\": null,\n \"evidence\": [\n {\n \"type\": \"global\",\n \"snippet\": \"window.__reactContainer\",\n \"weight\": 5\n },\n {\n \"type\": \"dom\",\n \"snippet\": \"confirmed via Next.js stack\",\n \"weight\": 3\n }\n ]\n },\n {\n \"id\": \"webpack\",\n \"name\": \"Webpack\",\n \"category\": \"build\",\n \"confidence\": \"high\",\n \"version\": null,\n \"evidence\": [\n {\n \"type\": \"global\",\n \"snippet\": \"window.webpackChunk\",\n \"weight\": 4\n },\n {\n \"type\": \"script\",\n \"snippet\": \"https://b.stripecdn.com/mkt-ssr-statics/assets/_next/static/chunks/webpack-f85e96e588c53278.js\",\n \"weight\": 3\n }\n ]\n },\n {\n \"id\": \"sentry\",\n \"name\": \"Sentry\",\n \"category\": \"observability\",\n \"confidence\": \"high\",\n \"version\": null,\n \"evidence\": [\n {\n \"type\": \"global\",\n \"snippet\": \"window.__SENTRY__\",\n \"weight\": 5\n }\n ]\n },\n {\n \"id\": \"google-analytics\",\n \"name\": \"Google Analytics\",\n \"category\": \"analytics\",\n \"confidence\": \"medium\",\n \"version\": null,\n \"evidence\": [\n {\n \"type\": \"cookie\",\n \"snippet\": \"_ga\",\n \"weight\": 2\n }\n ]\n }\n ]\n}",
+ "jsonStatus": "Copied JSON"
+ }
+ }
+ },
+ "notion": {
+ "url": "https://www.notion.com/",
+ "light": {
+ "badge": "4",
+ "title": "WhatStack: Next.js · 4 signals"
+ },
+ "deep": {
+ "badge": "5",
+ "title": "WhatStack: Next.js · 5 signals"
+ },
+ "popup": {
+ "nat": 560,
+ "status": "5 solid · 5 total · deep · Next.js",
+ "headline": {
+ "x": 10,
+ "y": 116.015625,
+ "w": 340,
+ "h": 58.6875
+ },
+ "headlineText": "CORE STACK\n\nNext.js 16.3.2 · React · Turbopack",
+ "sections": [
+ {
+ "title": "FRAMEWORKS",
+ "x": 10,
+ "y": 184.703125,
+ "w": 340,
+ "h": 111.9375
+ },
+ {
+ "title": "BUILD",
+ "x": 10,
+ "y": 304.640625,
+ "w": 340,
+ "h": 72.9375
+ },
+ {
+ "title": "OBSERVABILITY",
+ "x": 10,
+ "y": 385.578125,
+ "w": 340,
+ "h": 72.9375
+ },
+ {
+ "title": "ANALYTICS & PRODUCT",
+ "x": 10,
+ "y": 466.515625,
+ "w": 340,
+ "h": 72.9375
+ }
+ ],
+ "hits": [
+ {
+ "text": "Next.js v16.3.2 HIGH",
+ "x": 11,
+ "y": 218.640625,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 227.390625,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ },
+ {
+ "text": "React HIGH",
+ "x": 11,
+ "y": 257.640625,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 266.390625,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ },
+ {
+ "text": "Turbo Turbopack HIGH",
+ "x": 11,
+ "y": 338.578125,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 347.328125,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ },
+ {
+ "text": "Sentry HIGH",
+ "x": 11,
+ "y": 419.515625,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 428.265625,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ },
+ {
+ "text": "Google Analytics HIGH",
+ "x": 11,
+ "y": 500.453125,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 509.203125,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ }
+ ],
+ "low": null,
+ "evidence": [],
+ "buttons": {
+ "Rescan": {
+ "x": 282.609375,
+ "y": 15.34375,
+ "w": 63.390625,
+ "h": 31
+ },
+ "Next.js\nv16.3.2\nHIGH": {
+ "x": 11,
+ "y": 218.640625,
+ "w": 338,
+ "h": 38
+ },
+ "React\nHIGH": {
+ "x": 11,
+ "y": 257.640625,
+ "w": 338,
+ "h": 38
+ },
+ "Turbo\nTurbopack\nHIGH": {
+ "x": 11,
+ "y": 338.578125,
+ "w": 338,
+ "h": 38
+ },
+ "Sentry\nHIGH": {
+ "x": 11,
+ "y": 419.515625,
+ "w": 338,
+ "h": 38
+ },
+ "Google Analytics\nHIGH": {
+ "x": 11,
+ "y": 500.453125,
+ "w": 338,
+ "h": 38
+ },
+ "Copy": {
+ "x": 14,
+ "y": 570.453125,
+ "w": 50.828125,
+ "h": 31
+ },
+ "MD": {
+ "x": 70.828125,
+ "y": 570.453125,
+ "w": 41.984375,
+ "h": 31
+ },
+ "JSON": {
+ "x": 118.8125,
+ "y": 570.453125,
+ "w": 51.3125,
+ "h": 31
+ },
+ "Wrong?": {
+ "x": 178.125,
+ "y": 570.453125,
+ "w": 66.46875,
+ "h": 31
+ }
+ },
+ "local": {
+ "x": 281.203125,
+ "y": 574.703125,
+ "w": 64.796875,
+ "h": 22.5
+ },
+ "url": "https://www.notion.com/",
+ "H": 613
+ }
+ },
+ "nextjs": {
+ "url": "https://nextjs.org/",
+ "light": {
+ "badge": "4",
+ "title": "WhatStack: Next.js · 4 signals"
+ },
+ "deep": {
+ "badge": "4",
+ "title": "WhatStack: Next.js · 4 signals"
+ },
+ "popup": {
+ "nat": 533,
+ "status": "4 solid · 4 total · deep · Next.js",
+ "headline": {
+ "x": 10,
+ "y": 116.015625,
+ "w": 340,
+ "h": 58.6875
+ },
+ "headlineText": "CORE STACK\n\nNext.js 16.4.0-canary.38 · React · Turbopack",
+ "sections": [
+ {
+ "title": "FRAMEWORKS",
+ "x": 10,
+ "y": 184.703125,
+ "w": 340,
+ "h": 111.9375
+ },
+ {
+ "title": "BUILD",
+ "x": 10,
+ "y": 304.640625,
+ "w": 340,
+ "h": 72.9375
+ },
+ {
+ "title": "UI / CSS",
+ "x": 10,
+ "y": 385.578125,
+ "w": 340,
+ "h": 72.9375
+ }
+ ],
+ "hits": [
+ {
+ "text": "Next.js v16.4.0-canary.38 HIGH",
+ "x": 11,
+ "y": 218.640625,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 227.390625,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ },
+ {
+ "text": "React HIGH",
+ "x": 11,
+ "y": 257.640625,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 266.390625,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ },
+ {
+ "text": "Turbo Turbopack HIGH",
+ "x": 11,
+ "y": 338.578125,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 347.328125,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ },
+ {
+ "text": "Tailwind CSS MEDIUM",
+ "x": 11,
+ "y": 419.515625,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 279.28125,
+ "y": 428.265625,
+ "w": 59.71875,
+ "h": 20.5
+ }
+ }
+ ],
+ "low": null,
+ "evidence": [],
+ "buttons": {
+ "Rescan": {
+ "x": 282.609375,
+ "y": 15.34375,
+ "w": 63.390625,
+ "h": 31
+ },
+ "Next.js\nv16.4.0-canary.38\nHIGH": {
+ "x": 11,
+ "y": 218.640625,
+ "w": 338,
+ "h": 38
+ },
+ "React\nHIGH": {
+ "x": 11,
+ "y": 257.640625,
+ "w": 338,
+ "h": 38
+ },
+ "Turbo\nTurbopack\nHIGH": {
+ "x": 11,
+ "y": 338.578125,
+ "w": 338,
+ "h": 38
+ },
+ "Tailwind CSS\nMEDIUM": {
+ "x": 11,
+ "y": 419.515625,
+ "w": 338,
+ "h": 38
+ },
+ "Copy": {
+ "x": 14,
+ "y": 489.515625,
+ "w": 50.828125,
+ "h": 31
+ },
+ "MD": {
+ "x": 70.828125,
+ "y": 489.515625,
+ "w": 41.984375,
+ "h": 31
+ },
+ "JSON": {
+ "x": 118.8125,
+ "y": 489.515625,
+ "w": 51.3125,
+ "h": 31
+ },
+ "Wrong?": {
+ "x": 178.125,
+ "y": 489.515625,
+ "w": 66.46875,
+ "h": 31
+ }
+ },
+ "local": {
+ "x": 281.203125,
+ "y": 493.765625,
+ "w": 64.796875,
+ "h": 22.5
+ },
+ "url": "https://nextjs.org/",
+ "H": 533
+ }
+ },
+ "netflix": {
+ "url": "https://www.netflix.com/",
+ "light": {
+ "badge": "1",
+ "title": "WhatStack: 1 stack signals"
+ },
+ "deep": {
+ "badge": "3",
+ "title": "WhatStack: React · 3 signals"
+ },
+ "popup": {
+ "nat": 494,
+ "status": "3 solid · 3 total · deep · React",
+ "headline": {
+ "x": 10,
+ "y": 116.015625,
+ "w": 340,
+ "h": 58.6875
+ },
+ "headlineText": "CORE STACK\n\nReact · Webpack",
+ "sections": [
+ {
+ "title": "FRAMEWORKS",
+ "x": 10,
+ "y": 184.703125,
+ "w": 340,
+ "h": 72.9375
+ },
+ {
+ "title": "BUILD",
+ "x": 10,
+ "y": 265.640625,
+ "w": 340,
+ "h": 72.9375
+ },
+ {
+ "title": "UI / CSS",
+ "x": 10,
+ "y": 346.578125,
+ "w": 340,
+ "h": 72.9375
+ }
+ ],
+ "hits": [
+ {
+ "text": "React HIGH",
+ "x": 11,
+ "y": 218.640625,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 227.390625,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ },
+ {
+ "text": "Webpack HIGH",
+ "x": 11,
+ "y": 299.578125,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 308.328125,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ },
+ {
+ "text": "💅 Emotion HIGH",
+ "x": 11,
+ "y": 380.515625,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 389.265625,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ }
+ ],
+ "low": null,
+ "evidence": [],
+ "buttons": {
+ "Rescan": {
+ "x": 282.609375,
+ "y": 15.34375,
+ "w": 63.390625,
+ "h": 31
+ },
+ "React\nHIGH": {
+ "x": 11,
+ "y": 218.640625,
+ "w": 338,
+ "h": 38
+ },
+ "Webpack\nHIGH": {
+ "x": 11,
+ "y": 299.578125,
+ "w": 338,
+ "h": 38
+ },
+ "💅\nEmotion\nHIGH": {
+ "x": 11,
+ "y": 380.515625,
+ "w": 338,
+ "h": 38
+ },
+ "Copy": {
+ "x": 14,
+ "y": 450.515625,
+ "w": 50.828125,
+ "h": 31
+ },
+ "MD": {
+ "x": 70.828125,
+ "y": 450.515625,
+ "w": 41.984375,
+ "h": 31
+ },
+ "JSON": {
+ "x": 118.8125,
+ "y": 450.515625,
+ "w": 51.3125,
+ "h": 31
+ },
+ "Wrong?": {
+ "x": 178.125,
+ "y": 450.515625,
+ "w": 66.46875,
+ "h": 31
+ }
+ },
+ "local": {
+ "x": 281.203125,
+ "y": 454.765625,
+ "w": 64.796875,
+ "h": 22.5
+ },
+ "url": "https://www.netflix.com/",
+ "H": 494
+ }
+ },
+ "react": {
+ "url": "https://react.dev/",
+ "light": {
+ "badge": "5",
+ "title": "WhatStack: Next.js · 5 signals"
+ },
+ "deep": {
+ "badge": "5",
+ "title": "WhatStack: Next.js · 5 signals"
+ },
+ "popup": {
+ "nat": 560,
+ "status": "5 solid · 5 total · deep · Next.js",
+ "headline": {
+ "x": 10,
+ "y": 116.015625,
+ "w": 340,
+ "h": 58.6875
+ },
+ "headlineText": "CORE STACK\n\nNext.js 15.1.12 · React · Webpack",
+ "sections": [
+ {
+ "title": "FRAMEWORKS",
+ "x": 10,
+ "y": 184.703125,
+ "w": 340,
+ "h": 111.9375
+ },
+ {
+ "title": "BUILD",
+ "x": 10,
+ "y": 304.640625,
+ "w": 340,
+ "h": 72.9375
+ },
+ {
+ "title": "UI / CSS",
+ "x": 10,
+ "y": 385.578125,
+ "w": 340,
+ "h": 72.9375
+ },
+ {
+ "title": "ANALYTICS & PRODUCT",
+ "x": 10,
+ "y": 466.515625,
+ "w": 340,
+ "h": 72.9375
+ }
+ ],
+ "hits": [
+ {
+ "text": "Next.js v15.1.12 HIGH",
+ "x": 11,
+ "y": 218.640625,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 227.390625,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ },
+ {
+ "text": "React HIGH",
+ "x": 11,
+ "y": 257.640625,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 266.390625,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ },
+ {
+ "text": "Webpack HIGH",
+ "x": 11,
+ "y": 338.578125,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 347.328125,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ },
+ {
+ "text": "Tailwind CSS MEDIUM",
+ "x": 11,
+ "y": 419.515625,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 279.28125,
+ "y": 428.265625,
+ "w": 59.71875,
+ "h": 20.5
+ }
+ },
+ {
+ "text": "Google Analytics HIGH",
+ "x": 11,
+ "y": 500.453125,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 509.203125,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ }
+ ],
+ "low": null,
+ "evidence": [],
+ "buttons": {
+ "Rescan": {
+ "x": 282.609375,
+ "y": 15.34375,
+ "w": 63.390625,
+ "h": 31
+ },
+ "Next.js\nv15.1.12\nHIGH": {
+ "x": 11,
+ "y": 218.640625,
+ "w": 338,
+ "h": 38
+ },
+ "React\nHIGH": {
+ "x": 11,
+ "y": 257.640625,
+ "w": 338,
+ "h": 38
+ },
+ "Webpack\nHIGH": {
+ "x": 11,
+ "y": 338.578125,
+ "w": 338,
+ "h": 38
+ },
+ "Tailwind CSS\nMEDIUM": {
+ "x": 11,
+ "y": 419.515625,
+ "w": 338,
+ "h": 38
+ },
+ "Google Analytics\nHIGH": {
+ "x": 11,
+ "y": 500.453125,
+ "w": 338,
+ "h": 38
+ },
+ "Copy": {
+ "x": 14,
+ "y": 570.453125,
+ "w": 50.828125,
+ "h": 31
+ },
+ "MD": {
+ "x": 70.828125,
+ "y": 570.453125,
+ "w": 41.984375,
+ "h": 31
+ },
+ "JSON": {
+ "x": 118.8125,
+ "y": 570.453125,
+ "w": 51.3125,
+ "h": 31
+ },
+ "Wrong?": {
+ "x": 178.125,
+ "y": 570.453125,
+ "w": 66.46875,
+ "h": 31
+ }
+ },
+ "local": {
+ "x": 281.203125,
+ "y": 574.703125,
+ "w": 64.796875,
+ "h": 22.5
+ },
+ "url": "https://react.dev/",
+ "H": 613
+ }
+ },
+ "vuejs": {
+ "url": "https://vuejs.org/",
+ "light": {
+ "badge": "",
+ "title": "WhatStack: no high/medium signals"
+ },
+ "deep": {
+ "badge": "1",
+ "title": "WhatStack: Vue · 1 signals"
+ },
+ "popup": {
+ "nat": 332,
+ "status": "1 solid · 1 total · deep · Vue",
+ "headline": {
+ "x": 10,
+ "y": 116.015625,
+ "w": 340,
+ "h": 58.6875
+ },
+ "headlineText": "CORE STACK\n\nVue 3.5.42",
+ "sections": [
+ {
+ "title": "FRAMEWORKS",
+ "x": 10,
+ "y": 184.703125,
+ "w": 340,
+ "h": 72.9375
+ }
+ ],
+ "hits": [
+ {
+ "text": "Vue v3.5.42 HIGH",
+ "x": 11,
+ "y": 218.640625,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 227.390625,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ }
+ ],
+ "low": null,
+ "evidence": [],
+ "buttons": {
+ "Rescan": {
+ "x": 282.609375,
+ "y": 15.34375,
+ "w": 63.390625,
+ "h": 31
+ },
+ "Vue\nv3.5.42\nHIGH": {
+ "x": 11,
+ "y": 218.640625,
+ "w": 338,
+ "h": 38
+ },
+ "Copy": {
+ "x": 14,
+ "y": 288.640625,
+ "w": 50.828125,
+ "h": 31
+ },
+ "MD": {
+ "x": 70.828125,
+ "y": 288.640625,
+ "w": 41.984375,
+ "h": 31
+ },
+ "JSON": {
+ "x": 118.8125,
+ "y": 288.640625,
+ "w": 51.3125,
+ "h": 31
+ },
+ "Wrong?": {
+ "x": 178.125,
+ "y": 288.640625,
+ "w": 66.46875,
+ "h": 31
+ }
+ },
+ "local": {
+ "x": 281.203125,
+ "y": 292.890625,
+ "w": 64.796875,
+ "h": 22.5
+ },
+ "url": "https://vuejs.org/",
+ "H": 332
+ }
+ },
+ "airbnb": {
+ "url": "https://www.airbnb.com/",
+ "light": {
+ "badge": "1",
+ "title": "WhatStack: 1 stack signals"
+ },
+ "deep": {
+ "badge": "3",
+ "title": "WhatStack: React · 3 signals"
+ },
+ "popup": {
+ "nat": 494,
+ "status": "3 solid · 3 total · deep · React",
+ "headline": {
+ "x": 10,
+ "y": 116.015625,
+ "w": 340,
+ "h": 58.6875
+ },
+ "headlineText": "CORE STACK\n\nReact",
+ "sections": [
+ {
+ "title": "FRAMEWORKS",
+ "x": 10,
+ "y": 184.703125,
+ "w": 340,
+ "h": 72.9375
+ },
+ {
+ "title": "DATA",
+ "x": 10,
+ "y": 265.640625,
+ "w": 340,
+ "h": 72.9375
+ },
+ {
+ "title": "ANALYTICS & PRODUCT",
+ "x": 10,
+ "y": 346.578125,
+ "w": 340,
+ "h": 72.9375
+ }
+ ],
+ "hits": [
+ {
+ "text": "React HIGH",
+ "x": 11,
+ "y": 218.640625,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 227.390625,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ },
+ {
+ "text": "React Router HIGH",
+ "x": 11,
+ "y": 299.578125,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 308.328125,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ },
+ {
+ "text": "Google Analytics MEDIUM",
+ "x": 11,
+ "y": 380.515625,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 279.28125,
+ "y": 389.265625,
+ "w": 59.71875,
+ "h": 20.5
+ }
+ }
+ ],
+ "low": null,
+ "evidence": [],
+ "buttons": {
+ "Rescan": {
+ "x": 282.609375,
+ "y": 15.34375,
+ "w": 63.390625,
+ "h": 31
+ },
+ "React\nHIGH": {
+ "x": 11,
+ "y": 218.640625,
+ "w": 338,
+ "h": 38
+ },
+ "React Router\nHIGH": {
+ "x": 11,
+ "y": 299.578125,
+ "w": 338,
+ "h": 38
+ },
+ "Google Analytics\nMEDIUM": {
+ "x": 11,
+ "y": 380.515625,
+ "w": 338,
+ "h": 38
+ },
+ "Copy": {
+ "x": 14,
+ "y": 450.515625,
+ "w": 50.828125,
+ "h": 31
+ },
+ "MD": {
+ "x": 70.828125,
+ "y": 450.515625,
+ "w": 41.984375,
+ "h": 31
+ },
+ "JSON": {
+ "x": 118.8125,
+ "y": 450.515625,
+ "w": 51.3125,
+ "h": 31
+ },
+ "Wrong?": {
+ "x": 178.125,
+ "y": 450.515625,
+ "w": 66.46875,
+ "h": 31
+ }
+ },
+ "local": {
+ "x": 281.203125,
+ "y": 454.765625,
+ "w": 64.796875,
+ "h": 22.5
+ },
+ "url": "https://www.airbnb.com/",
+ "H": 494
+ }
+ },
+ "github": {
+ "url": "https://github.com/",
+ "light": {
+ "badge": "",
+ "title": "WhatStack: no high/medium signals"
+ },
+ "deep": {
+ "badge": "1",
+ "title": "WhatStack: 1 stack signals"
+ },
+ "popup": {
+ "nat": 367,
+ "status": "1 solid · 2 total · deep",
+ "headline": {
+ "x": 0,
+ "y": 0,
+ "w": 0,
+ "h": 0
+ },
+ "headlineText": "\n Core stack\n \n ",
+ "sections": [
+ {
+ "title": "BUILD",
+ "x": 10,
+ "y": 116.015625,
+ "w": 340,
+ "h": 96.4375
+ },
+ {
+ "title": "DATA",
+ "x": 10,
+ "y": 220.453125,
+ "w": 340,
+ "h": 72.9375
+ }
+ ],
+ "hits": [
+ {
+ "text": "Import Maps LOW",
+ "x": 11,
+ "y": 173.453125,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 300.8125,
+ "y": 182.203125,
+ "w": 38.1875,
+ "h": 20.5
+ }
+ },
+ {
+ "text": "React Router v8.3.0 HIGH",
+ "x": 11,
+ "y": 254.390625,
+ "w": 338,
+ "h": 38,
+ "badge": {
+ "x": 296.96875,
+ "y": 263.140625,
+ "w": 42.03125,
+ "h": 20.5
+ }
+ }
+ ],
+ "low": {
+ "x": 11,
+ "y": 149.953125,
+ "w": 338,
+ "h": 23.5
+ },
+ "evidence": [],
+ "buttons": {
+ "Rescan": {
+ "x": 282.609375,
+ "y": 15.34375,
+ "w": 63.390625,
+ "h": 31
+ },
+ "Import Maps\nLOW": {
+ "x": 11,
+ "y": 173.453125,
+ "w": 338,
+ "h": 38
+ },
+ "React Router\nv8.3.0\nHIGH": {
+ "x": 11,
+ "y": 254.390625,
+ "w": 338,
+ "h": 38
+ },
+ "Copy": {
+ "x": 14,
+ "y": 324.390625,
+ "w": 50.828125,
+ "h": 31
+ },
+ "MD": {
+ "x": 70.828125,
+ "y": 324.390625,
+ "w": 41.984375,
+ "h": 31
+ },
+ "JSON": {
+ "x": 118.8125,
+ "y": 324.390625,
+ "w": 51.3125,
+ "h": 31
+ },
+ "Wrong?": {
+ "x": 178.125,
+ "y": 324.390625,
+ "w": 66.46875,
+ "h": 31
+ }
+ },
+ "local": {
+ "x": 281.203125,
+ "y": 328.640625,
+ "w": 64.796875,
+ "h": 22.5
+ },
+ "url": "https://github.com/",
+ "H": 367
+ }
+ },
+ "store": {
+ "add": {
+ "x": 1109,
+ "y": 262,
+ "w": 147,
+ "h": 40
+ },
+ "addText": "Add to Chrome",
+ "title": {
+ "x": 260,
+ "y": 261.5,
+ "w": 724,
+ "h": 41
+ }
+ }
+}
\ No newline at end of file
diff --git a/launch-video/source/capture.mjs b/launch-video/source/capture.mjs
new file mode 100644
index 0000000..c9cffb2
--- /dev/null
+++ b/launch-video/source/capture.mjs
@@ -0,0 +1,78 @@
+// Real captures of the unpacked WhatStack extension (v1.7.2) on real sites.
+// Popup: the real popup.html + real service worker + real deep scan. Only chrome.tabs.query is pointed at the target tab
+// (a popup opened as a tab would otherwise scan itself). Clipboard writes are recorded so exports are the real output.
+import {chromium} from '/projects/sandbox/tools/node_modules/playwright/index.mjs';
+import fs from 'fs';import os from 'os';import path from 'path';
+const EXT='/projects/sandbox/whatstack';const OUT='/projects/sandbox/whatstack-video/cap/';fs.mkdirSync(OUT,{recursive:true});
+const only=process.argv[2]?process.argv[2].split(','):null;
+const ctx=await chromium.launchPersistentContext(fs.mkdtempSync(path.join(os.tmpdir(),'ws-')),{channel:'chromium',headless:true,
+ viewport:{width:1440,height:900},deviceScaleFactor:3,locale:'en-US',colorScheme:'dark',
+ args:[`--disable-extensions-except=${EXT}`,`--load-extension=${EXT}`,'--disable-features=DisableLoadExtensionCommandLineSwitch','--no-sandbox','--no-first-run']});
+const sw=ctx.serviceWorkers()[0]||await ctx.waitForEvent('serviceworker',{timeout:20000});
+const id=new URL(sw.url()).host;
+const meta=fs.existsSync(OUT+'meta.json')?JSON.parse(fs.readFileSync(OUT+'meta.json')):{};
+const R=b=>b&&{x:+b.x.toFixed(1),y:+b.y.toFixed(1),w:+b.width.toFixed(1),h:+b.height.toFixed(1)};
+async function dismiss(page){
+ for(const re of [/^Accept all( cookies)?$/i,/^Accept$/i,/^Allow all( cookies)?$/i,/^I agree$/i,/^Got it$/i,/^OK$/i,/^Accept All Cookies$/i,/^Only allow essential cookies$/i,/^Reject all$/i,/^Close$/i]){
+ const b=page.getByRole('button',{name:re}).first();if(await b.isVisible().catch(()=>false)){await b.click({timeout:2000}).catch(()=>{});await page.waitForTimeout(500);}}
+}
+async function popupFor(tab,name,{expand=null,exports=false}={}){
+ const pop=await ctx.newPage();await pop.setViewportSize({width:360,height:600});
+ await pop.addInitScript(t=>{const q=chrome.tabs.query.bind(chrome.tabs);chrome.tabs.query=async o=>(o&&o.active)?[{id:t.id,url:t.url,active:true}]:q(o);
+ window.__copied=[];const w=navigator.clipboard&&navigator.clipboard.writeText.bind(navigator.clipboard);
+ navigator.clipboard.writeText=async s=>{window.__copied.push(s);try{await w(s)}catch(e){}};},tab);
+ await pop.goto(`chrome-extension://${id}/popup/popup.html`);
+ await pop.waitForFunction(()=>!document.querySelector('#results')?.hidden||!document.querySelector('#empty')?.hidden,null,{timeout:30000}).catch(()=>{});
+ await pop.waitForTimeout(800);
+ // natural (capped, like the real popup) + full-height versions
+ const natH=await pop.evaluate(()=>Math.min(document.body.scrollHeight,560));await pop.setViewportSize({width:360,height:natH});
+ await pop.screenshot({path:OUT+`pop-${name}-nat.png`,scale:'device'});
+ await pop.addStyleTag({content:'body{max-height:none!important;overflow:visible!important}.header,.footer{position:static!important}'});
+ const boxes=async()=>pop.evaluate(()=>{const R=e=>{const b=e.getBoundingClientRect();return {x:b.x,y:b.y,w:b.width,h:b.height}};
+ return {status:document.querySelector('#status')?.innerText,headline:R(document.querySelector('#headline')),headlineText:document.querySelector('#headline')?.innerText,
+ sections:[...document.querySelectorAll('.section')].map(s=>({title:s.querySelector('.section-title')?.innerText,...R(s)})),
+ hits:[...document.querySelectorAll('.hit-row')].map(h=>({text:h.innerText.replace(/\s+/g,' '),...R(h),badge:R(h.querySelector('.badge,[class*=badge],[class*=conf]')||h.lastElementChild)})),
+ low:(()=>{const e=[...document.querySelectorAll('*')].find(e=>e.children.length===0&&/Lower confidence/i.test(e.textContent));return e?R(e):null})(),
+ evidence:[...document.querySelectorAll('.hit.open .evidence')].map(R),
+ buttons:Object.fromEntries([...document.querySelectorAll('button')].map(b=>[b.innerText.trim()||b.title,R(b)])),
+ local:(()=>{const e=[...document.querySelectorAll('*')].find(e=>e.children.length===0&&/^Local only$/.test(e.textContent.trim()));return e?R(e):null})(),
+ url:document.querySelector('#page-url')?.innerText,H:document.body.scrollHeight}});
+ let h=await pop.evaluate(()=>document.body.scrollHeight);await pop.setViewportSize({width:360,height:h});await pop.waitForTimeout(200);
+ const m={nat:natH,...await boxes()};await pop.screenshot({path:OUT+`pop-${name}.png`,scale:'device'});
+ if(expand){const row=pop.locator('.hit-row',{hasText:expand}).first();await row.click();await pop.waitForTimeout(400);
+ h=await pop.evaluate(()=>document.body.scrollHeight);await pop.setViewportSize({width:360,height:h});await pop.waitForTimeout(200);
+ m.expanded=await boxes();m.expanded.evidenceText=await pop.evaluate(()=>[...document.querySelectorAll('.hit.open .evidence li')].map(l=>l.innerText));
+ await pop.screenshot({path:OUT+`pop-${name}-open.png`,scale:'device'});await row.click();await pop.waitForTimeout(300);}
+ if(exports){m.exports={};for(const [label,key] of [['Copy','text'],['MD','md'],['JSON','json']]){
+ await pop.getByRole('button',{name:label,exact:true}).first().click().catch(e=>console.log('btn',label,e.message.slice(0,80)));await pop.waitForTimeout(400);
+ m.exports[key]=await pop.evaluate(()=>window.__copied[window.__copied.length-1]||null);m.exports[key+'Status']=await pop.evaluate(()=>document.querySelector('#status')?.innerText);}}
+ await pop.close();return m;
+}
+const sites=[['stripe','https://stripe.com/'],['notion','https://www.notion.com/'],['nextjs','https://nextjs.org/'],['netflix','https://www.netflix.com/'],
+ ['react','https://react.dev/'],['vuejs','https://vuejs.org/'],['airbnb','https://www.airbnb.com/'],['github','https://github.com/']];
+for(const [name,url] of sites){ if(only&&!only.includes(name))continue;
+ const page=await ctx.newPage();
+ try{await page.goto(url,{waitUntil:'domcontentloaded',timeout:60000});}catch(e){console.log(name,'nav fail');continue;}
+ await page.waitForTimeout(4000);await dismiss(page);await page.waitForTimeout(2500);await page.mouse.move(2,2);
+ await page.screenshot({path:OUT+`page-${name}.jpg`,type:'jpeg',quality:90});
+ const host=new URL(page.url()).host;
+ const tab=await sw.evaluate(async h=>{const t=(await chrome.tabs.query({})).find(t=>t.url&&new URL(t.url).host===h);return {id:t.id,url:t.url}},host);
+ const light=await sw.evaluate(async i=>({badge:await chrome.action.getBadgeText({tabId:i}),title:await chrome.action.getTitle({tabId:i})}),tab.id);
+ const pm=await popupFor(tab,name,{expand:name==='stripe'?'Next.js':null,exports:name==='stripe'});
+ const deep=await sw.evaluate(async i=>({badge:await chrome.action.getBadgeText({tabId:i}),title:await chrome.action.getTitle({tabId:i})}),tab.id);
+ meta[name]={url:page.url(),light,deep,popup:pm};
+ console.log(name,'| light',light.badge,'| deep',deep.badge,'|',pm.status,'|',(pm.headlineText||'').replace(/\s+/g,' '));
+ if(name==='stripe'){const r=await ctx.request.get(url,{headers:{'user-agent':'Mozilla/5.0'}});fs.writeFileSync(OUT+'src-stripe.html',await r.text());}
+ await page.close();
+}
+// Chrome Web Store listing (logged out) — top of page only
+if(!only||only.includes('store')){
+ const st=await ctx.newPage();
+ await st.goto('https://chromewebstore.google.com/detail/whatstack/kpmbanlddakoocgimdenfeppfaidmcgk?hl=en',{waitUntil:'networkidle',timeout:60000}).catch(()=>{});
+ await st.waitForTimeout(3000);await st.getByRole('button',{name:/No thanks/i}).first().click({timeout:3000}).catch(()=>{});await st.waitForTimeout(600);await dismiss(st);await st.mouse.move(2,2);
+ const add=st.getByRole('button',{name:/Add to Chrome|Remove from Chrome/}).first();
+ meta.store={add:R(await add.boundingBox().catch(()=>null)),addText:await add.innerText().catch(()=>null),
+ title:R(await st.getByRole('heading',{name:'WhatStack'}).first().boundingBox().catch(()=>null))};
+ await st.screenshot({path:OUT+'store.jpg',type:'jpeg',quality:90});console.log('store',meta.store);await st.close();
+}
+fs.writeFileSync(OUT+'meta.json',JSON.stringify(meta,null,1));await ctx.close();
diff --git a/launch-video/source/engine.js b/launch-video/source/engine.js
new file mode 100644
index 0000000..d8609c2
--- /dev/null
+++ b/launch-video/source/engine.js
@@ -0,0 +1,297 @@
+// WhatStack launch film — beat-timed animation engine on Skia (skia-canvas).
+// Every element is a pure function of b (time in beats). timeline.json drives picture and score.
+// FMT=vertical renders 1080×1920; default 1920×1080.
+const {Canvas,FontLibrary,loadImage,Path2D}=require('/projects/sandbox/tools/node_modules/skia-canvas');
+const fs=require('fs');
+const ROOT=__dirname+'/',CAP=ROOT+'cap/',ICONS='/projects/sandbox/whatstack/icons/';
+const TL=JSON.parse(fs.readFileSync(ROOT+'timeline.json'));const S=TL.scenes,C=TL.cues;
+const META=JSON.parse(fs.readFileSync(CAP+'meta.json'));
+const V=process.env.FMT==='vertical';
+const W=V?1080:1920,H=V?1920:1080,CX=W/2,CY=H/2;const pick=(h,v)=>V?v:h;
+FontLibrary.use('Inter',[ROOT+'fonts/Inter.ttf']);FontLibrary.use('JBMono',[ROOT+'fonts/JetBrainsMono.ttf']);
+
+// ---------- math
+const clamp=(x,a=0,b=1)=>Math.min(b,Math.max(a,x)),seg=(t,a,b)=>clamp((t-a)/(b-a)),lerp=(a,b,x)=>a+(b-a)*x;
+const eo=x=>1-Math.pow(1-x,3),eo5=x=>1-Math.pow(1-x,5),eio=x=>x<.5?4*x*x*x:1-Math.pow(-2*x+2,3)/2;
+const eback=x=>{const c=1.4;return 1+(c+1)*Math.pow(x-1,3)+c*Math.pow(x-1,2)};
+const inS=(b,[a,z])=>b>=a&&b=0){x.save();x.globalAlpha*=(1-rip)*.9*a;x.strokeStyle='rgba(165,180,252,1)';x.lineWidth=2.5;x.beginPath();x.arc(sx,sy,12+rip*32,0,7);x.stroke();x.restore();}
+ x.save();x.globalAlpha*=a;x.translate(sx-5,sy-4);const s=scale*(1-.16*press);x.scale(s,s);x.shadowColor='rgba(0,0,0,.55)';x.shadowBlur=8;x.shadowOffsetY=3;x.fillStyle='#fff';x.fill(CURSOR);
+ x.shadowColor='transparent';x.strokeStyle='#05070b';x.lineWidth=1.3;x.lineJoin='round';x.stroke(CURSOR);x.restore();}
+function band(x,h,stop=.82,sa=1){const g=x.createLinearGradient(0,0,0,h);g.addColorStop(0,`rgba(12,15,20,1)`);g.addColorStop(stop,`rgba(12,15,20,${sa})`);g.addColorStop(1,'rgba(12,15,20,0)');x.fillStyle=g;x.fillRect(0,0,W,h);}
+function bandLeft(x,w){const g=x.createLinearGradient(0,0,w,0);g.addColorStop(0,'rgba(12,15,20,1)');g.addColorStop(.72,'rgba(12,15,20,1)');g.addColorStop(1,'rgba(12,15,20,0)');x.fillStyle=g;x.fillRect(0,0,w,H);}
+function sceneAlpha(b,[a,z],fin=.5,fout=.5){if(b=z)return 0;return Math.min(fin?eo(seg(b,a,a+fin)):1,fout?1-eio(seg(b,z-fout,z)):1);}
+function ring(x,bx,col,e,pad=5,r=null){if(e<=0)return;const s=lerp(1.4,1,eo(e));x.save();x.globalAlpha*=clamp(e*1.5);const cx=bx.x+bx.w/2,cy=bx.y+bx.h/2,w=(bx.w+pad*2)*s,h=(bx.h+pad*2)*s;
+ rr(x,cx-w/2,cy-h/2,w,h,r??h/2);x.shadowColor=col;x.shadowBlur=16;x.strokeStyle=col;x.lineWidth=2;x.stroke();x.restore();}
+
+// ---------- the browser window (a simple generic frame; not Chrome's real UI) with the real extension icon + badge
+const TB=52,PX=976,PY=58,PWID=360;const ICX=1290,ICY=26; // icon centre in window coords
+const HOSTS={stripe:'stripe.com',notion:'notion.com',nextjs:'nextjs.org',netflix:'netflix.com',react:'react.dev',vuejs:'vuejs.org',airbnb:'airbnb.com',github:'github.com'};
+function toolbar(x,site,badge,badgePop=1,iconPress=0){
+ x.fillStyle='#1b1f27';x.fillRect(0,0,1440,TB);x.fillStyle='rgba(255,255,255,.06)';x.fillRect(0,TB-1,1440,1);
+ x.strokeStyle='#8b93a3';x.lineWidth=2;x.lineCap='round';x.lineJoin='round';
+ const arrow=(cx,d)=>{x.beginPath();x.moveTo(cx+6*d,ICY);x.lineTo(cx-6*d,ICY);x.moveTo(cx-1*d,ICY-5);x.lineTo(cx-6*d,ICY);x.lineTo(cx-1*d,ICY+5);x.stroke();};
+ arrow(28,1);x.globalAlpha*=.45;arrow(62,-1);x.globalAlpha/=.45;x.beginPath();x.arc(96,ICY,6.5,-.2*Math.PI,1.55*Math.PI);x.stroke();
+ rr(x,124,9,1100,34,17);x.fillStyle='#262b35';x.fill();
+ x.strokeStyle='#8b93a3';x.lineWidth=1.6;rr(x,142,20,10,8,2);x.stroke();x.beginPath();x.arc(147,20,3.5,Math.PI,0);x.stroke();
+ x.font='500 15px Inter';x.letterSpacing='0px';x.fillStyle='#e6e9ef';x.fillText(HOSTS[site]||site,166,31);
+ // extension slot: the real WhatStack icon + blue badge
+ if(iconPress>0){x.fillStyle=`rgba(255,255,255,${.12*iconPress})`;x.beginPath();x.arc(ICX,ICY,17,0,7);x.fill();}
+ x.drawImage(A.icon,ICX-11,ICY-11,22,22);
+ if(badge){const s=lerp(.3,1,eback(clamp(badgePop)));x.save();x.globalAlpha*=clamp(badgePop*3);x.translate(ICX+9,ICY+9);x.scale(s,s);
+ x.font='700 10px Inter';const tw=Math.max(14,x.measureText(badge).width+8);rr(x,-tw/2,-7,tw,14,7);x.fillStyle=BADGE;x.fill();x.strokeStyle='#1b1f27';x.lineWidth=1.5;x.stroke();
+ x.fillStyle='#fff';x.textAlign='center';x.fillText(badge,0,3.6);x.restore();}
+ // generic menu dots
+ x.fillStyle='#8b93a3';[0,1,2].forEach(i=>{x.beginPath();x.arc(1410,ICY-6+i*6,1.7,0,7);x.fill();});
+}
+function popupH(site,open=false){return open?A['pop-stripe-open'].height/3:A['pop-'+site].height/3;}
+function drawPopup(x,site,k=1,{open=false,overlay=null}={}){ // popup anchored under the icon, opens from its top-right
+ if(k<=0)return;const img=open?A['pop-stripe-open']:A['pop-'+site];const h=img.height/3;
+ x.save();const e=eo5(clamp(k));x.globalAlpha*=clamp(k*2.2);x.translate(PX+PWID,PY);x.scale(lerp(.94,1,e),lerp(.94,1,e));x.translate(-(PX+PWID),-PY);
+ x.save();x.shadowColor='rgba(0,0,0,.6)';x.shadowBlur=40;x.shadowOffsetY=18;rr(x,PX,PY,PWID,h,10);x.fillStyle=BG;x.fill();x.restore();
+ x.save();rr(x,PX,PY,PWID,h,10);x.clip();x.drawImage(img,PX,PY,PWID,h);if(overlay)overlay(x);x.restore();
+ x.strokeStyle='rgba(255,255,255,.10)';x.lineWidth=1;rr(x,PX+.5,PY+.5,PWID-1,h-1,10);x.stroke();x.restore();}
+const P=(bx)=>({x:PX+bx.x,y:PY+bx.y,w:bx.w,h:bx.h}); // popup box -> window coords
+function windowPlane(x,cam,site,{badge=null,badgePop=1,popup=0,open=false,popOverlay=null,iconPress=0,alpha=1,dim=0}={}){
+ plane(x,cam,null,{w:1440,h:952,alpha,overlay:(p)=>{p.drawImage(A['page-'+site],0,TB,1440,900);if(dim>0){p.fillStyle=`rgba(12,15,20,${dim})`;p.fillRect(0,TB,1440,900);}toolbar(p,site,badge,badgePop,iconPress);drawPopup(p,site,popup,{open,overlay:popOverlay});}});}
+
+// ---------- ambient
+function ambient(x,b,level=1){const t=b*.5;
+ const blob=(cx,cy,r,col,a)=>{const g=x.createRadialGradient(cx,cy,0,cx,cy,r);g.addColorStop(0,`rgba(${col},${a})`);g.addColorStop(1,`rgba(${col},0)`);x.fillStyle=g;x.fillRect(cx-r,cy-r,2*r,2*r);};
+ blob(W*.12+Math.sin(t*.25)*90,H*.08+Math.cos(t*.2)*60,760,'45,212,191',.08*level);
+ blob(W*.88+Math.cos(t*.22)*100,H*.95+Math.sin(t*.3)*60,880,'99,102,241',.13*level);
+ blob(W*.9+Math.sin(t*.18)*60,H*.1,520,'192,132,252',.05*level);}
+function finish(x,b,fi){const g=x.createRadialGradient(CX,CY-40,pick(720,640),CX,CY,1300);g.addColorStop(0,'rgba(12,15,20,0)');g.addColorStop(1,'rgba(12,15,20,.7)');x.fillStyle=g;x.fillRect(0,0,W,H);
+ x.save();x.globalAlpha=.03;const p=x.createPattern(A.grain[fi%A.grain.length],'repeat');x.fillStyle=p;x.translate((fi*37)%256,(fi*91)%256);x.fillRect(-256,-256,W+512,H+512);x.restore();}
+
+// ================= SCENES
+function hook(x,b){const [a,z]=S.hook;if(!inS(b,[a,z]))return;x.save();
+ const cut=z-.4;if(b