THE WORLD IS NOT STANDING STILL.RSS
BIG CHANGE.
BIG CHANGE, one tap away
BIG CHANGE, one tap away

Make room for the big picture.

Add BIG CHANGE to your home screen or desktop. It opens in its own window, with the latest stories when you’re online.

iPhone & iPad
Open bigchange.ai in Safari. Tap Share, then Add to Home Screen. Turn on Open as Web App if shown, then tap Add.
Android
Open bigchange.ai in Chrome. In the browser menu, choose Install app or Add to Home screen.
Desktop
In Chrome or Edge, use the install icon in the address bar or the browser’s app menu. In Safari on Mac, choose File → Add to Dock.

Markdown edition

# Build a landing page with a playable AI video and real 3D

> Generate and inspect a Flow clip, add it to a copyable Vite and Three.js page, then check full playback and motion behavior at the hosted URL.

By BIGCHANGE.AI Editorial

Published: 2026-10-27T04:41:39.151Z
Updated: 2026-10-27T04:41:39.151Z
Canonical: https://bigchange.ai/blog/flow-video-threejs-3d-landing-page-guide

![Conceptual charcoal illustration of one angled computer display showing a paused abstract video player beside a shaded orange 3D knot on the same webpage.](https://bigchange.ai/api/media/file/flow-video-live-3d-page-hero-v2.png)
AI-generated conceptual illustration by BIG CHANGE.

A generated video can be the centerpiece of a web page, but a video file alone cannot supply the live 3D movement around it. This guide joins a clip you make and approve in Google Flow to a small Vite site whose Three.js shape turns as the visitor scrolls. You will publish only after checking the actual clip, the source changes and the destination. The result to verify is one hosted page where the whole film plays on desktop and a real phone, while motion controls and readable fallbacks work.

The supplied page and code were prepared and tested locally as a starter. We did not use a Flow account, generate a clip, run Claude Code, open the page in a browser, test a GPU or phone, or deploy a site. Your video and release checks are essential parts of the procedure.

## The big change

- **What changed:** A creator can combine a generated, downloadable film with live WebGL geometry in one page. The film remains a video element; the shape is rendered by Three.js and responds to scroll on eligible screens.
- **Why it matters:** A visitor can choose when to play the film and stop decorative motion. The same page still has readable copy when the browser or device cannot run the 3D effect.
- **What to watch:** The decisive checks happen on the published URL. An MP4 header and a successful build do not establish that the hosted clip decodes, that a phone can play it, or that the destination is appropriate for the intended audience.

## 1. Check access, rights and cost before making the clip

You need a Google account eligible for [Flow](https://support.google.com/flow/answer/16353333?hl=en) in your [region](https://support.google.com/flow/answer/16353544?hl=en), Claude Code access, a current supported Node.js/npm installation, Git, a GitHub repository you control and a Vercel account. Flow's access and [credit](https://support.google.com/flow/answer/16526234?hl=en) pages disagree about free eligibility. Check your account's [model](https://support.google.com/flow/answer/16352836?hl=en), balance and displayed charge before each generation. Stop if the account, age, regional or charge gate is unclear. Record the balance before and after. There is no reliable fixed total for this project; retries, Claude use and hosting can change it.

Choose a subject and any input material you can publish. Avoid another person's likeness, logos or protected designs without permission. [Google's terms](https://policies.google.com/terms) say Google does not claim ownership of original generated content, while users remain responsible for applicable rights. That statement does not settle every copyright or third-party claim. Review the final frames and sound for accidental marks or misleading material, and preserve any watermark. Do not put account credentials, private prompts or provider keys in the repository.

## 2. Make and inspect a real MP4

In a [Flow project](https://support.google.com/flow/answer/16353334?hl=en), choose Video and a model that supports Text to Video. If Veo 3.1 Lite, 16:9, one output and 4, 6 or 8 seconds appear at an acceptable charge, select them. Here is an original abstract prompt you can change: “A translucent teal glass ring turns slowly around an amber sphere in a dark blue studio. Fixed camera, clean silhouette, soft reflections, no people, lettering or logos. Wide frame.” Confirm the displayed settings before Generate.

Play the entire result. Inspect picture, motion and audio; reject an unusable or rights-problematic result before proceeding. Flow documents [More → Download](https://support.google.com/flow/answer/16935308?hl=en). Keep the original download and charge record privately. Confirm the downloaded file plays in Chrome, and note its duration, dimensions, codec and size. If the offered file is not a Chrome-playable MP4, convert your own authorized source with a tool you trust, then replay the entire derivative and retain the original. This starter includes no converter and no generated media. Copy the approved file to `public/assets/scene.mp4` in the project root. Replace `public/assets/poster.svg` only if you have an approved still.

## 3. Put the page together and review Claude Code's changes

Copy every file in the appendix into an otherwise empty directory, preserving names and subdirectories, including the hidden `.gitignore` and `package-lock.json`. Add your inspected MP4 at the path above. The lockfile and package file pin the tested dependency graph, including Three.js 0.186.1 and Vite 8.3.5. From the directory root, run:

```powershell
npm ci
npm test
npm run check:asset
npm run build
npm run dev
```

Open the local address printed by Vite in Chrome. The page uses a real torus-knot mesh, lights and a WebGL renderer. On wide screens with motion allowed, scrolling changes the mesh rotation and reveals the lower cards. The video has controls, starts only on a visitor's action and keeps its stage size while waiting for media. The visible **Pause decorative motion** button, system reduced-motion preference, narrow layout and a supported browser `saveData` setting each hold the decoration still. The Network Information preference is [not available in every browser](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/saveData); an absent value is unknown, so use the manual button if you want a still page. If WebGL 2 cannot start, the site keeps readable text and shows a fallback message.

[Claude Code's quickstart](https://code.claude.com/docs/en/quickstart) describes how to run it in a local project. Give it only your approved copy and facts. For example: “Inspect this Vite project and its tests. Replace the sample Field Notes copy with my approved brief and links. Keep the Three.js mesh, scroll behavior, native video controls, motion pause, reduced-motion and narrow-screen behavior, and fallback text. Show the diff, run local checks and list any browser or device checks you could not perform.” Read the changed files and staged diff yourself. Remove invented claims, unauthorized dependencies, unlicensed assets and accidental keys. Rerun the commands after changes.

The asset script checks file presence, size and an MP4 header. It cannot prove codec support, full playback, ownership or suitability. The prepared kit passed `npm ci --ignore-scripts`, two Node logic tests, JavaScript syntax checks and a Vite build on Node 24.19.0/npm 11.17.0. The missing-MP4 asset check failed as expected because no clip was supplied. Vite reported a 532.26 kB minified JavaScript chunk (132.79 kB gzip) and a size warning; check the shipped bundle against your own performance budget. These are offline checks, not evidence of a working hosted page.

## 4. Test the local experience

Run `npm run preview` and inspect its local URL after the build. In desktop Chrome, scroll through the hero and watch the actual shape turn. Press the pause button and check that it stops; resume and check that it responds again. Confirm the film starts stopped, then play its full duration and check picture and sound. Use portrait and landscape emulation to find layout errors. Reload with system reduced motion enabled; the shape and reveal animation should stay still while text and player remain usable. Try a browser or environment without WebGL 2 and check the visible fallback. Where Chrome exposes data saving, test it; otherwise record that case as unknown and use manual pause.

Inspect the video request in the Network panel. If it is missing or blank, check `/assets/scene.mp4`, response status, content type and the browser's media error; re-export or convert the owned source and replay it. For missing geometry, inspect WebGL 2 availability, console errors and context loss. If scroll motion is absent, confirm the viewport is wider than 640px, motion preferences permit it and there is space to scroll. Correct any overflowing element identified during mobile layout inspection. Local desktop emulation cannot establish real phone decoding or GPU behavior.

## 5. Review the first production destination, then deploy

Before the first Vercel Deploy, write down and review the exact GitHub owner, repository and visibility, Vercel account or team and project, production branch `main`, root `/`, build command `npm run build`, output `dist`, production URL or approved domain, and whether the audience will be public or protected. Check the full page copy, licenses, media rights, GitHub file-size limit, secrets scan, `.gitignore`, source diff and MP4 before the push as well. The [first Git import into Vercel](https://vercel.com/docs/git) can publish production immediately; do not count on an earlier preview of that first import. Complete this owner review before clicking Deploy.

Create an empty GitHub repository with no starter files. In the project root, substitute its real HTTPS owner and repository in the remote URL. Stage exactly the files you reviewed:

```powershell
git init -b main
git add .gitignore index.html package.json package-lock.json public/assets/poster.svg public/assets/scene.mp4 src/main.js src/motion.js src/style.css scripts/check-assets.mjs test/motion.test.mjs
git status --short
git diff --cached --stat
git diff --cached --check
git commit -m "Add reviewed video and 3D page"
git remote add origin https://github.com/OWNER/REPO.git
git remote -v
git push -u origin main
git rev-parse HEAD
git ls-remote origin refs/heads/main
```

Compare local `HEAD` with remote `main` and inspect the GitHub file list and MP4. In the chosen Vercel account, import that repository, confirm the production branch and audience controls, then use the [Vite static build settings](https://vite.dev/guide/static-deploy) above and deploy. A protected staging audience can help review, but only a site accessible to your intended final viewers meets the final result. Future branch deployments can be previews; the first import may already be live production.

Open the actual production root and `/assets/scene.mp4` as an intended viewer. Play the whole film in desktop Chrome and on a real intended phone running Chrome. Check portrait and landscape, keyboard player controls, the manual motion button, reduced motion, narrow layout and a WebGL-failure fallback. Record the URL, commit, deployment ID, MP4 hash, phone model, OS and browser version, plus what you actually saw and heard. If there is a build failure or a 404, inspect the build log, selected repository, root/output settings and asset path; fix the source, rerun local checks and make a new reviewed deployment. For an access mistake or harmful first release, use the project's available protection controls while correcting it. A first release has no earlier approved version to restore. On later releases, keep the last approved deployment available as a rollback target.

The appendix is a complete, copyable starter for this workflow. Its sample page text is deliberately generic. Replace it with verified facts about your own work before publication. The generated MP4 cannot be provided here because it must come from your eligible account and rights review.

## Copyable reader kit

Save each block at its stated path relative to your new project root. Preserve the lockfile byte for byte. The MP4 at `public/assets/scene.mp4` is the sole reader-created file, and the asset check will remain red until it exists.

### `.gitignore`

```text
node_modules/
dist/
.vercel/
.env*
*.log
```

### `package.json`

```json
{
  "name": "owned-video-3d-landing-page",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite --host 127.0.0.1",
    "build": "vite build",
    "preview": "vite preview --host 127.0.0.1",
    "test": "node --test test/*.test.mjs",
    "check:asset": "node scripts/check-assets.mjs"
  },
  "dependencies": {
    "three": "0.186.1"
  },
  "devDependencies": {
    "vite": "8.3.5"
  }
}
```

### `package-lock.json`

```json
{
  "name": "owned-video-3d-landing-page",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "owned-video-3d-landing-page",
      "version": "1.0.0",
      "dependencies": {
        "three": "0.186.1"
      },
      "devDependencies": {
        "vite": "8.3.5"
      }
    },
    "node_modules/@oxc-project/types": {
      "version": "0.155.0",
      "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.155.0.tgz",
      "integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==",
      "dev": true,
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/oxc-project"
      }
    },
    "node_modules/@rolldown/binding-android-arm-eabi": {
      "version": "1.2.15",
      "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz",
      "integrity": "sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==",
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": "^20.19.0 || >=22.12.0"
      }
    },
    "node_modules/@rolldown/binding-android-arm64": {
      "version": "1.2.15",
      "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.15.tgz",
      "integrity": "sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": "^20.19.0 || >=22.12.0"
      }
    },
    "node_modules/@rolldown/binding-darwin-arm64": {
      "version": "1.2.15",
      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.15.tgz",
      "integrity": "sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": "^20.19.0 || >=22.12.0"
      }
    },
    "node_modules/@rolldown/binding-darwin-x64": {
      "version": "1.2.15",
      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.15.tgz",
      "integrity": "sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": "^20.19.0 || >=22.12.0"
      }
    },
    "node_modules/@rolldown/binding-freebsd-x64": {
      "version": "1.2.15",
      "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.15.tgz",
      "integrity": "sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "freebsd"
      ],
      "engines": {
        "node": "^20.19.0 || >=22.12.0"
      }
    },
    "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
      "version": "1.2.15",
      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.15.tgz",
      "integrity": "sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==",
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": "^20.19.0 || >=22.12.0"
      }
    },
    "node_modules/@rolldown/binding-linux-arm64-gnu": {
      "version": "1.2.15",
      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.15.tgz",
      "integrity": "sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "libc": [
        "glibc"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": "^20.19.0 || >=22.12.0"
      }
    },
    "node_modules/@rolldown/binding-linux-arm64-musl": {
      "version": "1.2.15",
      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.15.tgz",
      "integrity": "sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "libc": [
        "musl"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": "^20.19.0 || >=22.12.0"
      }
    },
    "node_modules/@rolldown/binding-linux-ppc64-gnu": {
      "version": "1.2.15",
      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.15.tgz",
      "integrity": "sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==",
      "cpu": [
        "ppc64"
      ],
      "dev": true,
      "libc": [
        "glibc"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": "^20.19.0 || >=22.12.0"
      }
    },
    "node_modules/@rolldown/binding-linux-s390x-gnu": {
      "version": "1.2.15",
      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.15.tgz",
      "integrity": "sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==",
      "cpu": [
        "s390x"
      ],
      "dev": true,
      "libc": [
        "glibc"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": "^20.19.0 || >=22.12.0"
      }
    },
    "node_modules/@rolldown/binding-linux-x64-gnu": {
      "version": "1.2.15",
      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.15.tgz",
      "integrity": "sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "libc": [
        "glibc"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": "^20.19.0 || >=22.12.0"
      }
    },
    "node_modules/@rolldown/binding-linux-x64-musl": {
      "version": "1.2.15",
      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.15.tgz",
      "integrity": "sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "libc": [
        "musl"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": "^20.19.0 || >=22.12.0"
      }
    },
    "node_modules/@rolldown/binding-openharmony-arm64": {
      "version": "1.2.15",
      "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.15.tgz",
      "integrity": "sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "openharmony"
      ],
      "engines": {
        "node": "^20.19.0 || >=22.12.0"
      }
    },
    "node_modules/@rolldown/binding-win32-arm64-msvc": {
      "version": "1.2.15",
      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.15.tgz",
      "integrity": "sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": "^20.19.0 || >=22.12.0"
      }
    },
    "node_modules/@rolldown/binding-win32-x64-msvc": {
      "version": "1.2.15",
      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.15.tgz",
      "integrity": "sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": "^20.19.0 || >=22.12.0"
      }
    },
    "node_modules/@rolldown/pluginutils": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
      "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/detect-libc": {
      "version": "2.1.2",
      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
      "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
      "dev": true,
      "license": "Apache-2.0",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/fdir": {
      "version": "6.5.0",
      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=12.0.0"
      },
      "peerDependencies": {
        "picomatch": "^3 || ^4"
      },
      "peerDependenciesMeta": {
        "picomatch": {
          "optional": true
        }
      }
    },
    "node_modules/fsevents": {
      "version": "2.3.3",
      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
      "dev": true,
      "hasInstallScript": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
      }
    },
    "node_modules/lightningcss": {
      "version": "1.33.0",
      "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
      "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
      "dev": true,
      "license": "MPL-2.0",
      "dependencies": {
        "detect-libc": "^2.0.3"
      },
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      },
      "optionalDependencies": {
        "lightningcss-android-arm64": "1.33.0",
        "lightningcss-darwin-arm64": "1.33.0",
        "lightningcss-darwin-x64": "1.33.0",
        "lightningcss-freebsd-x64": "1.33.0",
        "lightningcss-linux-arm-gnueabihf": "1.33.0",
        "lightningcss-linux-arm64-gnu": "1.33.0",
        "lightningcss-linux-arm64-musl": "1.33.0",
        "lightningcss-linux-x64-gnu": "1.33.0",
        "lightningcss-linux-x64-musl": "1.33.0",
        "lightningcss-win32-arm64-msvc": "1.33.0",
        "lightningcss-win32-x64-msvc": "1.33.0"
      }
    },
    "node_modules/lightningcss-android-arm64": {
      "version": "1.33.0",
      "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
      "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-darwin-arm64": {
      "version": "1.33.0",
      "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
      "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-darwin-x64": {
      "version": "1.33.0",
      "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
      "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-freebsd-x64": {
      "version": "1.33.0",
      "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
      "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "freebsd"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-linux-arm-gnueabihf": {
      "version": "1.33.0",
      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
      "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-linux-arm64-gnu": {
      "version": "1.33.0",
      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
      "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "libc": [
        "glibc"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-linux-arm64-musl": {
      "version": "1.33.0",
      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
      "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "libc": [
        "musl"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-linux-x64-gnu": {
      "version": "1.33.0",
      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
      "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "libc": [
        "glibc"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-linux-x64-musl": {
      "version": "1.33.0",
      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
      "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "libc": [
        "musl"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-win32-arm64-msvc": {
      "version": "1.33.0",
      "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
      "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-win32-x64-msvc": {
      "version": "1.33.0",
      "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
      "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/nanoid": {
      "version": "3.3.20",
      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz",
      "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==",
      "dev": true,
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/ai"
        }
      ],
      "license": "MIT",
      "bin": {
        "nanoid": "bin/nanoid.cjs"
      },
      "engines": {
        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
      }
    },
    "node_modules/picocolors": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
      "dev": true,
      "license": "ISC"
    },
    "node_modules/picomatch": {
      "version": "4.0.7",
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
      "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/sponsors/jonschlinkert"
      }
    },
    "node_modules/postcss": {
      "version": "8.5.32",
      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz",
      "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==",
      "dev": true,
      "funding": [
        {
          "type": "opencollective",
          "url": "https://opencollective.com/postcss/"
        },
        {
          "type": "tidelift",
          "url": "https://tidelift.com/funding/github/npm/postcss"
        },
        {
          "type": "github",
          "url": "https://github.com/sponsors/ai"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "nanoid": "^3.3.20",
        "picocolors": "^1.1.1",
        "source-map-js": "^1.2.2"
      },
      "engines": {
        "node": "^10 || ^12 || >=14"
      }
    },
    "node_modules/rolldown": {
      "version": "1.2.15",
      "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.15.tgz",
      "integrity": "sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@oxc-project/types": "=0.155.0",
        "@rolldown/pluginutils": "^1.0.0"
      },
      "bin": {
        "rolldown": "bin/cli.mjs"
      },
      "engines": {
        "node": "^20.19.0 || >=22.12.0"
      },
      "optionalDependencies": {
        "@rolldown/binding-android-arm-eabi": "1.2.15",
        "@rolldown/binding-android-arm64": "1.2.15",
        "@rolldown/binding-darwin-arm64": "1.2.15",
        "@rolldown/binding-darwin-x64": "1.2.15",
        "@rolldown/binding-freebsd-x64": "1.2.15",
        "@rolldown/binding-linux-arm-gnueabihf": "1.2.15",
        "@rolldown/binding-linux-arm64-gnu": "1.2.15",
        "@rolldown/binding-linux-arm64-musl": "1.2.15",
        "@rolldown/binding-linux-ppc64-gnu": "1.2.15",
        "@rolldown/binding-linux-s390x-gnu": "1.2.15",
        "@rolldown/binding-linux-x64-gnu": "1.2.15",
        "@rolldown/binding-linux-x64-musl": "1.2.15",
        "@rolldown/binding-openharmony-arm64": "1.2.15",
        "@rolldown/binding-win32-arm64-msvc": "1.2.15",
        "@rolldown/binding-win32-x64-msvc": "1.2.15"
      }
    },
    "node_modules/source-map-js": {
      "version": "1.2.2",
      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
      "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
      "dev": true,
      "license": "BSD-3-Clause",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/three": {
      "version": "0.186.1",
      "resolved": "https://registry.npmjs.org/three/-/three-0.186.1.tgz",
      "integrity": "sha512-blFeqb49wRCSGUGj7gtpfnSGHy2lwDk94RhUmS1c/hTby70kvChbWpkJ4Pm1390LqzzvTmzgXKHPEafJwCb8jA==",
      "license": "MIT"
    },
    "node_modules/tinyglobby": {
      "version": "0.2.17",
      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
      "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "fdir": "^6.5.0",
        "picomatch": "^4.0.4"
      },
      "engines": {
        "node": ">=12.0.0"
      },
      "funding": {
        "url": "https://github.com/sponsors/SuperchupuDev"
      }
    },
    "node_modules/vite": {
      "version": "8.3.5",
      "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz",
      "integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "lightningcss": "^1.33.0",
        "picomatch": "^4.0.7",
        "postcss": "^8.5.30",
        "rolldown": "~1.2.14",
        "tinyglobby": "^0.2.17"
      },
      "bin": {
        "vite": "bin/vite.js"
      },
      "engines": {
        "node": "^20.19.0 || >=22.12.0"
      },
      "funding": {
        "url": "https://github.com/vitejs/vite?sponsor=1"
      },
      "optionalDependencies": {
        "fsevents": "~2.3.3"
      },
      "peerDependencies": {
        "@types/node": "^20.19.0 || >=22.12.0",
        "@vitejs/devtools": "^0.7.1 || ^0.8.0",
        "esbuild": "^0.27.0 || ^0.28.0",
        "jiti": ">=1.21.0",
        "less": "^4.0.0",
        "sass": "^1.70.0",
        "sass-embedded": "^1.70.0",
        "stylus": ">=0.54.8",
        "sugarss": "^5.0.0",
        "terser": "^5.16.0",
        "tsx": "^4.8.1",
        "yaml": "^2.4.2"
      },
      "peerDependenciesMeta": {
        "@types/node": {
          "optional": true
        },
        "@vitejs/devtools": {
          "optional": true
        },
        "esbuild": {
          "optional": true
        },
        "jiti": {
          "optional": true
        },
        "less": {
          "optional": true
        },
        "sass": {
          "optional": true
        },
        "sass-embedded": {
          "optional": true
        },
        "stylus": {
          "optional": true
        },
        "sugarss": {
          "optional": true
        },
        "terser": {
          "optional": true
        },
        "tsx": {
          "optional": true
        },
        "yaml": {
          "optional": true
        }
      }
    }
  }
}
```

### `index.html`

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="description" content="An original demonstration page combining an owned video clip, scroll motion, and a live 3D object." />
    <title>Field Notes — a motion study</title>
  </head>
  <body>
    <a class="skip" href="#main">Skip to content</a>
    <main id="main">
      <section class="hero" aria-labelledby="hero-title">
        <div class="hero-copy">
          <p class="eyebrow">FIELD NOTES / 01</p>
          <h1 id="hero-title">A page with motion you can control.</h1>
          <p class="lede">A short film, a live 3D form and a clear path through the story. Replace this demonstration copy with your own checked claims.</p>
          <a class="button" href="#film">Watch the film</a>
          <button id="motion-toggle" class="motion-toggle" type="button" aria-pressed="false" hidden>Pause decorative motion</button>
          <p id="motion-status" class="motion-status" role="status"></p>
        </div>
        <div class="scene-wrap" aria-label="Rotating three-dimensional ring sculpture">
          <canvas id="scene" aria-hidden="true"></canvas>
          <p id="scene-fallback" class="scene-fallback" hidden>3D is unavailable here. The page and film still work.</p>
        </div>
      </section>

      <section id="film" class="film-section" aria-labelledby="film-title">
        <div class="section-head reveal">
          <p class="eyebrow">01 / FILM</p>
          <h2 id="film-title">A clip made for this page</h2>
          <p>Play when you choose. A still frame holds the layout until the clip is ready.</p>
        </div>
        <div class="film-frame reveal">
          <p class="film-label">Your reviewed clip plays here</p>
          <video id="hero-video" src="/assets/scene.mp4" controls playsinline preload="none" poster="/assets/poster.svg" width="1280" height="720" aria-label="Original generated abstract film">
            <p>Your browser cannot play this video. <a href="/assets/scene.mp4">Download the clip</a>.</p>
          </video>
          <p id="video-note" class="video-note">Place your reviewed Flow MP4 at public/assets/scene.mp4 before using this page.</p>
        </div>
      </section>

      <section class="story" aria-labelledby="story-title">
        <div class="section-head reveal">
          <p class="eyebrow">02 / DEPTH</p>
          <h2 id="story-title">Real geometry, modest movement</h2>
          <p>The ring above is rendered in WebGL. Cards below move into view only when motion is allowed.</p>
        </div>
        <div class="cards">
          <article class="card reveal"><span>01</span><h3>Owned source</h3><p>Use a clip and references you have permission to publish.</p></article>
          <article class="card reveal"><span>02</span><h3>Human review</h3><p>Check every frame, visual claim and the page on a phone.</p></article>
          <article class="card reveal"><span>03</span><h3>Public readback</h3><p>Open the final URL and play the actual hosted asset.</p></article>
        </div>
      </section>
      <footer><p>Field Notes — sample page copy and geometry.</p><a href="#main">Back to top</a></footer>
    </main>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>
```

### `public/assets/poster.svg`

```xml
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720" role="img" aria-label="Abstract blue and orange rings on a dark field">
  <rect width="1280" height="720" fill="#111d29"/>
  <circle cx="640" cy="360" r="240" fill="none" stroke="#77ced3" stroke-width="24" opacity=".8"/>
  <ellipse cx="640" cy="360" rx="370" ry="120" fill="none" stroke="#e8a267" stroke-width="16" transform="rotate(-24 640 360)"/>
  <circle cx="640" cy="360" r="58" fill="#f1d6ac"/>
</svg>
```

### `src/main.js`

```javascript
import './style.css';
import { WebGLRenderer, PerspectiveCamera, Scene, TorusKnotGeometry, MeshStandardMaterial, Mesh, HemisphereLight, DirectionalLight } from 'three';
import { motionAllowed, progressAt } from './motion.js';

const reduced = matchMedia('(prefers-reduced-motion: reduce)');
const narrow = matchMedia('(max-width: 640px)');
const connection = navigator.connection;
const saveData = () => connection?.saveData === true;
let manualStill = false;
const motionToggle = document.querySelector('#motion-toggle');
const motionStatus = document.querySelector('#motion-status');
const canvas = document.querySelector('#scene');
const fallback = document.querySelector('#scene-fallback');
const hero = document.querySelector('.hero');
const video = document.querySelector('#hero-video');
const videoNote = document.querySelector('#video-note');
let renderer;
let geometry;
let material;
let frame = 0;
let queueDraw = () => {};
motionToggle.hidden = false;

function syncMotionChoice() {
  const allowed = motionAllowed(reduced, narrow, saveData(), manualStill);
  document.documentElement.classList.toggle('motion-ready', allowed);
  motionToggle.setAttribute('aria-pressed', String(manualStill));
  motionToggle.textContent = manualStill ? 'Resume decorative motion' : 'Pause decorative motion';
  motionStatus.textContent = reduced.matches ? 'Device reduced-motion setting keeps decoration still.'
    : narrow.matches ? 'Decoration is still on narrow screens.'
    : saveData() ? 'Browser data-saving setting keeps decoration still.'
    : manualStill ? 'Decoration paused.'
    : 'Decoration follows scrolling.';
  queueDraw();
}
motionToggle.addEventListener('click', () => { manualStill = !manualStill; syncMotionChoice(); });

// The page stays readable when WebGL 2 is unavailable or graphics context is lost.
try {
  if (!canvas.getContext('webgl2')) throw new Error('WebGL 2 unavailable');
  renderer = new WebGLRenderer({ canvas, antialias: true, alpha: true, powerPreference: 'low-power' });
  renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
  const scene = new Scene();
  const camera = new PerspectiveCamera(45, 1, 0.1, 100);
  camera.position.z = 5;
  geometry = new TorusKnotGeometry(1.15, 0.3, 128, 12, 2, 3);
  material = new MeshStandardMaterial({ color: 0x79d4d6, metalness: 0.25, roughness: 0.3 });
  const form = new Mesh(geometry, material);
  scene.add(form);
  scene.add(new HemisphereLight(0xffffff, 0x133044, 2.8));
  const key = new DirectionalLight(0xffc991, 2.5);
  key.position.set(3, 4, 4);
  scene.add(key);
  const resize = () => {
    const width = Math.max(1, canvas.clientWidth);
    const height = Math.max(1, canvas.clientHeight);
    renderer.setSize(width, height, false);
    camera.aspect = width / height;
    camera.updateProjectionMatrix();
    renderer.render(scene, camera);
  };
  const draw = () => {
    frame = 0;
    const moving = motionAllowed(reduced, narrow, saveData(), manualStill);
    const start = hero.offsetTop;
    const end = start + Math.max(1, hero.offsetHeight - innerHeight);
    const progress = progressAt(scrollY, start, end);
    form.rotation.y = moving ? progress * Math.PI * 1.2 + 0.25 : 0.25;
    form.rotation.x = moving ? progress * 0.32 - 0.15 : -0.15;
    renderer.render(scene, camera);
  };
  queueDraw = () => { if (!frame) frame = requestAnimationFrame(draw); };
  addEventListener('scroll', queueDraw, { passive: true });
  addEventListener('resize', () => { resize(); queueDraw(); });
  reduced.addEventListener('change', syncMotionChoice);
  narrow.addEventListener('change', syncMotionChoice);
  connection?.addEventListener?.('change', syncMotionChoice);
  canvas.addEventListener('webglcontextlost', (event) => {
    event.preventDefault();
    canvas.hidden = true;
    fallback.hidden = false;
  });
  resize();
  syncMotionChoice();
} catch (error) {
  canvas.hidden = true;
  fallback.hidden = false;
  console.warn('3D fallback:', error);
  renderer?.dispose();
  geometry?.dispose();
  material?.dispose();
}
syncMotionChoice();

// IntersectionObserver reveals content only when the visitor permits motion.
const reveals = [...document.querySelectorAll('.reveal')];
if ('IntersectionObserver' in window && !reduced.matches) {
  const observer = new IntersectionObserver((entries) => {
    for (const entry of entries) {
      if (!entry.isIntersecting) continue;
      entry.target.classList.add('visible');
      observer.unobserve(entry.target);
    }
  }, { threshold: 0.08 });
  reveals.forEach((element) => observer.observe(element));
  reduced.addEventListener('change', () => {
    if (reduced.matches) {
      observer.disconnect();
      reveals.forEach((element) => element.classList.add('visible'));
    }
  });
  motionToggle.addEventListener('click', () => {
    if (manualStill) {
      observer.disconnect();
      reveals.forEach((element) => element.classList.add('visible'));
    }
  });
} else {
  reveals.forEach((element) => element.classList.add('visible'));
}

video.addEventListener('error', () => {
  videoNote.textContent = 'The video did not load. Check /assets/scene.mp4 on this host and confirm the file plays locally.';
});
video.addEventListener('loadedmetadata', () => {
  videoNote.textContent = `Ready to play: ${Math.round(video.duration)} seconds. Playback starts only when you press Play.`;
});
```

### `src/motion.js`

```javascript
export function progressAt(scrollY, start, end) {
  if (end <= start) return 0;
  return Math.min(1, Math.max(0, (scrollY - start) / (end - start)));
}

export function motionAllowed(query, narrowScreen, saveData, manualStill = false) {
  return !query.matches && !narrowScreen.matches && !saveData && !manualStill;
}
```

### `src/style.css`

```css
:root { font-family: system-ui, sans-serif; color: #f4f0e8; background: #0b1420; scroll-behavior: smooth; }
* { box-sizing: border-box; }
[hidden] { display: none !important; }
body { margin: 0; }
a { color: inherit; }
button, a { touch-action: manipulation; }
:focus-visible { outline: 3px solid #ffcd82; outline-offset: 4px; }
.skip { position: absolute; top: -100px; left: 1rem; z-index: 10; padding: .6rem 1rem; background: #f4f0e8; color: #0b1420; }
.skip:focus { top: 1rem; }
.hero { min-height: 145vh; display: grid; grid-template-columns: 1.05fr .95fr; gap: 4vw; padding: 9vh 6vw 20vh; background: radial-gradient(circle at 75% 30%, #285163 0, #0b1420 48%); }
.hero-copy { position: sticky; top: 18vh; height: fit-content; z-index: 1; }
.eyebrow { color: #8ed9d8; font-size: .78rem; font-weight: 700; letter-spacing: .22em; }
h1, h2, h3, p { margin-top: 0; }
h1 { max-width: 10ch; font-size: clamp(3rem, 6vw, 7rem); line-height: .99; letter-spacing: -.055em; }
h2 { font-size: clamp(2.2rem, 5vw, 5rem); line-height: 1.03; letter-spacing: -.04em; }
.lede { max-width: 42ch; color: #c9d5d6; font-size: clamp(1.1rem, 1.8vw, 1.45rem); line-height: 1.55; }
.button { display: inline-block; padding: .85rem 1.2rem; border-radius: 999px; background: #ecb176; color: #0b1420; font-weight: 750; text-decoration: none; }
.motion-toggle { display: block; margin-top: 1rem; padding: .5rem .7rem; border: 1px solid #b6ced1; border-radius: .6rem; color: #f4f0e8; background: #143746; font: inherit; cursor: pointer; }
.motion-status { min-height: 1.5em; margin-top: .5rem; color: #c9d5d6; font-size: .88rem; }
.scene-wrap { position: sticky; top: 8vh; height: 74vh; min-height: 360px; border: 1px solid #528793; border-radius: 2rem; background: linear-gradient(145deg, #1b3b4b, #12202f); overflow: hidden; }
#scene { width: 100%; height: 100%; display: block; }
.scene-fallback { margin: 0; padding: 2rem; position: absolute; inset: auto 0 0; background: #132c3b; }
.film-section, .story { padding: 8rem 6vw; }
.section-head { max-width: 48rem; margin-bottom: 3rem; }
.section-head p:not(.eyebrow) { color: #c7d1d5; font-size: 1.13rem; line-height: 1.6; }
.film-frame { position: relative; max-width: 1100px; margin: auto; }
.film-label { position: absolute; top: 1rem; left: 1rem; z-index: 1; max-width: calc(100% - 2rem); padding: .4rem .7rem; border-radius: .4rem; background: #06121ec9; color: white; pointer-events: none; }
video { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; background: #152a3a; border-radius: 1.5rem; border: 1px solid #4e7782; }
.video-note { padding-top: 1rem; color: #bfced1; }
.story { background: #172b38; }
.cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.25rem; }
.card { min-height: 16rem; padding: 2rem; border: 1px solid #537984; border-radius: 1.5rem; background: #1c3945; box-shadow: 0 1.5rem 3rem #06121e40; }
.card span { color: #ecb176; font-weight: 700; }
.card h3 { margin-top: 3rem; font-size: 1.5rem; }
.card p { color: #c7d1d5; line-height: 1.5; }
footer { display: flex; justify-content: space-between; gap: 2rem; padding: 3rem 6vw; color: #c7d1d5; }
@media (min-width: 641px) and (prefers-reduced-motion: no-preference) {
  .motion-ready .reveal { opacity: 0; transform: translateY(22px); transition: opacity .55s ease, transform .55s ease; }
  .motion-ready .reveal.visible { opacity: 1; transform: none; }
}
@media (max-width: 640px) {
  .hero { display: block; min-height: auto; padding: 6rem 1.25rem 3rem; }
  .hero-copy, .scene-wrap { position: relative; top: auto; }
  .hero-copy { margin-bottom: 3rem; }
  .scene-wrap { height: 340px; min-height: 0; border-radius: 1.25rem; }
  .film-section, .story { padding: 5rem 1.25rem; }
  .cards { grid-template-columns: 1fr; }
  .card { min-height: 0; }
  .card h3 { margin-top: 2rem; }
  footer { padding: 2rem 1.25rem; }
}
@media (prefers-reduced-motion: reduce) {
  :root { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}
```

### `test/motion.test.mjs`

```javascript
import test from 'node:test';
import assert from 'node:assert/strict';
import { progressAt, motionAllowed } from '../src/motion.js';

test('scroll progress is clamped and handles a collapsed range', () => {
  assert.equal(progressAt(0, 100, 300), 0);
  assert.equal(progressAt(200, 100, 300), .5);
  assert.equal(progressAt(400, 100, 300), 1);
  assert.equal(progressAt(200, 300, 300), 0);
});

test('reduced motion, narrow screen and data saving each stop decorative motion', () => {
  assert.equal(motionAllowed({ matches: false }, { matches: false }, false), true);
  assert.equal(motionAllowed({ matches: true }, { matches: false }, false), false);
  assert.equal(motionAllowed({ matches: false }, { matches: true }, false), false);
  assert.equal(motionAllowed({ matches: false }, { matches: false }, true), false);
  assert.equal(motionAllowed({ matches: false }, { matches: false }, false, true), false);
});
```

### `scripts/check-assets.mjs`

```javascript
import { open, stat } from 'node:fs/promises';
import { join } from 'node:path';

const path = join(process.cwd(), 'public', 'assets', 'scene.mp4');
try {
  const info = await stat(path);
  const handle = await open(path, 'r');
  const header = Buffer.alloc(8);
  try {
    await handle.read(header, 0, 8, 0);
  } finally {
    await handle.close();
  }
  if (info.size < 1024 || header.toString('ascii', 4, 8) !== 'ftyp') {
    throw new Error('The selected file is not a plausible MP4.');
  }
  console.log(`MP4 is present (${info.size} bytes). Play and inspect every frame in a browser before deployment.`);
} catch (error) {
  console.error(`Asset check failed at ${path}: ${error.message}`);
  process.exitCode = 1;
}
```

## Sources

- [Google Flow Help: video workflow](https://support.google.com/flow/answer/16353334?hl=en) — Companion pages linked in the article document creation, models, access, credits and downloads. Access and credit guidance conflicts on free eligibility; inspect your account.
- [Google Terms of Service](https://policies.google.com/terms) — Ownership clause and user responsibility; does not establish exclusive rights to a particular output.
- [Anthropic: Claude Code quickstart](https://code.claude.com/docs/en/quickstart) — Local Claude Code installation/use; the bounded project prompt is original and was not run.
- [Three.js: installation](https://threejs.org/manual/pages/installation.html) — Package setup; the reader kit pins Three.js 0.186.1.
- [Three.js: WebGLRenderer](https://threejs.org/docs/pages/WebGLRenderer) — Renderer API contract; no real browser GPU test was run.
- [MDN: HTML video element](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/video) — Native controls, poster, playsinline and preload contract; codec and full playback require real tests.
- [MDN: browser motion and graphics APIs](https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext) — WebGL 2 availability and failure boundary.
- [MDN: saveData](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/saveData) — Limited availability; unknown when unsupported.
- [Vite: static deployment](https://vite.dev/guide/static-deploy) — Build command and dist output; preview is local.
- [Vercel: Git deployment](https://vercel.com/docs/git) — Production branch and preview behavior; first import of main may be production.
- [BIG CHANGE: Claude brand kit, page and video](https://bigchange.ai/blog/claude-brand-kit-landing-page-launch-video-guide) — Related prior task with separate page and exported video.
- [BIG CHANGE: Claude web project to GitHub and Vercel](https://bigchange.ai/blog/claude-web-project-github-vercel-deployment-guide) — Related deployment lifecycle for an existing project.