scroll-world
Produces a landing page where scroll drives a camera: it dives from outside a scene
into its interior, then flies out and into the next scene, continuously, with no visible
cuts. The visuals are AI-generated (Higgsfield); the page just scrubs pre-rendered video
by scroll position. This is the same technique behind Apple's scroll-through product
pages — the camera genuinely moves, scroll only drives time.
What you generate: N scene stills → N "dive-in" camera clips → N-1 "connector" clips
that join consecutive scenes seamlessly → a portable scrub engine that plays the whole
chain as one flight.
The one rule that makes or breaks it: seams must be frame-identical. Read
The seamless chain before generating any
connector. Getting this wrong is the single most common failure and produces a visible
"pop" between scenes.
Do not assume a frontend framework. The scrub engine in references/scrub-engine.js is
self-contained vanilla JS (it builds its own DOM + injects its own CSS into a container
you give it), so it drops into plain HTML, Next.js, Vue, a Python-served page, anything.
The value of this skill is the Higgsfield pipeline, the prompts, and the seam method —
not the framework.
Step 0 — Bootstrap
- Higgsfield CLI. If
higgsfield is not on $PATH, install per the
higgsfield-generate skill. If higgsfield workspace list fails auth, ask the user
to run higgsfield auth login (interactive OAuth — you cannot run it) and, if needed,
higgsfield workspace set <id>. Confirm there are enough credits: a full run is
roughly N image gens + (2N-1) video gens.
- ffmpeg / ffprobe on
$PATH (frame extraction + encoding).
- An image tool for background knockout if you want floating scenes: PIL
(
python3 -c "import PIL"), or cwebp/sips. Optional — see Step 3.
- (Optional) Codex CLI — if
codex is on $PATH (≥ 0.125) and
codex login status reports a ChatGPT login, the scene stills can be generated
through Codex's built-in image_gen (the same gpt-image-2 model) billed to the
user's ChatGPT subscription instead of Higgsfield credits — offer it at
Step 1.6, command in Step 2. Absence just removes the option.
- Caveats: macOS ships bash 3.2 (no
declare -A); don't use associative arrays in
scripts. Higgsfield generations take 3–8 min each — always run them detached
(background) and poll, never a foreground blocking call. Reference-by-job-UUID is
rejected by media flags — pass local file paths to --image/--start-image/--end-image.
Video models differ in accepted params (e.g. Kling has no --resolution) and in whether
they support start/end-image conditioning at all — before batching, confirm the chosen
model's schema with higgsfield model get <job_type> and see the Step 4 model table.
Step 1 — Interview the user
The subject is the user's to state — ask it as an open question in plain prose, never a
fabricated multiple-choice. A made-up list of industries biases them and reads as you
deciding their business for them; let them answer in their own words (their real business,
a client's, or any idea). Reserve structured multiple-choice (AskUserQuestion in Claude
Code; a plain either/or question elsewhere) for the genuinely
enumerable, lower-stakes choices below — art direction and brand-kit approach — and even
there, signal they can go their own way ("Other"). Ask only what you can't sensibly
default. Cover:
Subject (ask openly, not multiple-choice) — "What should this world be about? Your
business, a client's, or any idea — a word or a sentence is fine." Capture the
industry/product + a one-line pitch (e.g. "a bubble tea company, from leaf to last
sip"), and a brand name if they have one; otherwise you'll propose one below.
Brand kit — offer three paths, pick one:
- Import from a URL:
higgsfield marketing-studio brand-kits fetch --url <site> --wait
(pulls name, colours, tone). Then read it back with brand-kits list --json.
- The user hands you palette + name + tone directly.
- You propose a palette + name and let them approve.
Capture 4–6 named hex values, a display name, and a tone word or two.
Art direction — default is "soft matte low-poly clay diorama, isometric,
tilt-shift miniature, warm light." Offer alternatives (flat papercraft, glossy toy,
claymation, neon night). Whatever is chosen becomes the shared style preamble
reused verbatim in every scene prompt (this is what makes the world cohesive).
The journey (sections) — the ordered scenes the camera flies through. Propose a
set derived from the subject's own value chain and let the user edit. 5–7 works well.
Boba example: farms → pearl kitchen → flagship shop → delivery → community plaza →
the hero product. Each section needs: a short subject description (what's IN the
diorama), an eyebrow, a headline, one line of body, and 0–3 tag pills. The last
section is usually the hero product + the CTA.
Mobile version — ALWAYS ask this; never silently generate both. Ask as a
two-option choice (AskUserQuestion in Claude Code; a plain question elsewhere):
"Want a mobile-optimized version too? The mobile version is a second camera chain
rendered natively in 9:16 portrait — composed for phones, not a crop of the
landscape film — which roughly doubles the Higgsfield credit spend (state the
estimated number)."
Options: "Desktop only" / "Desktop + mobile (native 9:16 — ~2× credits)". The
credit cost must be stated to the user, not just implied.
What the answer gates:
- Yes → render the parallel 9:16 portrait chain and ship it as the mobile variants
(Step 6 / pipeline.md §6b): portrait start canvases → 9:16 dives + connectors
frame-locked against their own renders → 720-wide
-m.mp4 encodes → stillMobile
portrait posters. Wire clipMobile/connectorsMobile/stillMobile (Step 7); run
the full mobile QA (Step 8). Budget ~2N-1 extra video gens + NSFW re-rolls.
Never ship the centre-crop as the mobile version by default — if credits can't
cover the portrait chain, say so and offer the crop encodes (pipeline.md §6) as an
explicitly-labelled stopgap the user must approve.
- No → skip the mobile encodes and wiring entirely. The engine's phone hardening
(seek-coalescing, iOS priming, safe-area CSS) is always on regardless — that's not
a "mobile version," it's just the page not breaking when a phone visits — so a
desktop-only build still degrades gracefully.
Budget — engines shown by cost, decided before anything renders. Present the
render tiers (AskUserQuestion), then compute and state the estimated total for
the user's N scenes — N stills + (2N−1) videos [videos ×2 if mobile] + ~15% re-roll headroom — and get a go before generating.
Video tier (roster only — every option frame-locks seams, Step 4):
| Tier |
Model |
Rough cost |
| Draft / previz |
seedance_2_0_mini (720p) |
~¼ of Standard |
| Standard (default) |
seedance_2_0 (1080p) |
baseline |
| Alternate |
kling3_0 (720p native) |
≈ Standard; different look + content filter |
Draft doubles as the previz path: run the whole chain cheap, approve the
journey, re-render final legs on Standard (pipeline.md Notes) — suggest it
unprompted when the balance reads tight.
Stills source (only offer if the Codex CLI is present, Step 0.4):
Higgsfield gpt_image_2 (spends credits) vs Codex image_gen — the same
gpt-image-2 model billed to the ChatGPT subscription (zero credits; counts
toward Codex usage limits; 1536×1024 output — exactly 3:2, slightly under
Higgsfield's 2k). Stills are plain PNGs handed to --start-image, so the
video chain is indifferent to their source. Command in Step 2. One source
for all N stills of a build — the two render with slightly different
character (verified: Codex runs warmer/lighter), and mixing sources across
scenes reads as style drift, same reason the video chain uses one model.
Calibrate costs, don't guess. The CLI exposes no pricing and plans differ.
Run ONE still and ONE video first, diff higgsfield workspace list before/
after, extrapolate to the full run, and warn the user whenever the estimate
exceeds ~70% of the balance. (Observed on a plus plan, 2026-07: Standard
video ≈ 40–55 credits, still ≈ 15.) A real not_enough_credits mid-run is
recoverable (finished clips survive; resume after top-up) but ugly — the
whole point of this step is that the user decides before the spend.
If the user names a video model outside the roster, honor it only if it can
frame-lock seams (Step 4). This skill only ships seamless output, so a model that
can't frame-lock is declined with a one-line why, not substituted in — use a roster
model instead.
Keep the scroll mechanic fixed (continuous fly-through) — that's the point of the skill.
See references/prompts.md for the intake checklist and copy structure.
Step 2 — Generate the scene stills
One image per section, all sharing the same style preamble for cohesion. Default
model gpt_image_2 (crisp, great at isometric illustration; returns a solid/white
background which is perfect for floating diorama "islands"). Use nano_banana_2 only if
the brief is character/cartoon-heavy (note: nano_banana_2 is a CLI alias — it resolves
to nano_banana_pro; it won't appear under that name in higgsfield model list).
Prompt shape (full templates in references/prompts.md):
<STYLE PREAMBLE, identical every time>. On a plain solid <bg> background with a soft
contact shadow. <PALETTE hexes>. No text, no letters, no logos, centered, 3:2.
Subject: <what is in THIS diorama>.
Run all N concurrently, detached. Command per scene:
higgsfield generate create gpt_image_2 --prompt "$(cat scene_i.txt)" --aspect_ratio 3:2 --resolution 2k --quality high --wait --wait-timeout 15m --json > scene_i.json 2>scene_i.err
Result URL is .[]0.result_url in the --wait --json output. curl it down.
Codex stills variant (if chosen at Step 1.6 — subscription-billed, zero
credits): same prompt files, same byte-identical preamble, generated by Codex's
built-in image_gen:
codex exec -C "$WORK" -s workspace-write --skip-git-repo-check \
'Use the image generation tool ($imagegen) to generate: '"$(cat "$WORK/still_i.txt")"' Wide 3:2 landscape, high resolution. Save it as ./still_i.png. Do not do anything else.'
Single-quote the $imagegen segment (the shell must not expand it); if editing
with reference images, the prompt goes BEFORE any -i flag (it's variadic).
~1–3 min per image; run a few in parallel, not all N at once. Output lands at
1536×1024 (3:2) — fine for --start-image and posters. Everything downstream
(cohesion review, knockout, dives) is unchanged.
A generation may fail transiently (HTTP 503) — re-roll that one individually; don't
restart the batch.
Review the stills before continuing. They must read as one cohesive world (same
angle, palette, light). If one is off-style, regenerate it, optionally passing an
approved scene as --image to lock style.
See references/pipeline.md for the exact batch script.
Step 3 — (Optional) Float the scenes
If you want the dioramas to float over an atmospheric background instead of sitting in a
solid box, knock out the flat background to transparency with
references/knockout.py (border-connected flood fill — preserves interior colour that
matches the bg, e.g. cream walls). Then encode to webp. If you'd rather keep it simple,
just make the page background the same colour as the scene background and skip this.
These stills double as video posters and lazy-load fallbacks, so keep them.
Step 4 — Camera architecture (pick one — this makes or breaks the feel)
How the camera moves between scenes is the single biggest quality lever. Two shapes;
pick by aesthetic.
Video model — pick ONE for the whole chain
This skill only ships seamless output, so the only usable models are ones that can
frame-lock a seam: every chained clip must accept --start-image, and connectors also
need --end-image. That capability — not preference — is the selection rule. Check any
model with higgsfield model get <job_type> and skip anything whose media inputs are
reference-only (no start/end image): it can only condition a generation, not
continue a shot, so it physically can't hold a seam. Schemas below were confirmed
against the CLI:
| Model |
start/end image |
Notes |
seedance_2_0 (default) |
✓ / ✓ |
Full chain (legs + connectors). --mode std --resolution 1080p. Its NSFW filter is the touchy one (see Gotchas). |
kling3_0 |
✓ / ✓ |
Full chain — tested: --mode std --sound off --duration 5 with start+end images accepted, seams frame-lock cleanly. No --resolution param (don't pass one; --mode std returns 720p native — encode what ffprobe reports, never upscale). Sound defaults on → --sound off. --duration default 5, try 10 for legs. Different content filter than Seedance — the sanctioned NSFW fallback. |
seedance_2_0_mini |
✓ / ✓ |
Cheap draft tier that keeps frame-locking (720p). The previz tier: run the whole chain here first, then re-render final legs on the full model — still seamless, so it translates directly. |
Those three are the roster — all do both architectures. (kling3_0_turbo also frame-locks
via --start-image, but has no --end-image, so it's architecture-A-only and can't make
connectors; it also takes a different flag set — no --mode, has --resolution — so it
doesn't drop into the pipeline as-is. It's not in the default roster; only reach for it, and
wire it by hand, if architecture A's sequential render time is a proven bottleneck and you've
benchmarked it as actually faster.)
Rules:
- One model for all chained clips. Each renderer has its own motion/color/grain
character; mixing models mid-chain keeps position continuity (frames still hand off)
but the render-character shift reads as a subtle pop. The one sanctioned exception is
the NSFW fallback for a single stubborn clip (Gotchas) — a slight character shift on
one 5s connector beats a missing connector.
- Default to
seedance_2_0; honor a user's stated preference only if the model
qualifies (frame-locking). If it doesn't, say so and use a supported model — never
ship a non-seamless build to satisfy a model request.
- The pipeline scripts take the model as
$VMODEL with per-model flags already cased
out (references/pipeline.md).
A) Continuous forward take — RECOMMENDED for grounded / realistic / walkthrough
One camera that only ever glides forward, first scene through last, as a single take.
Generate the legs sequentially: leg 0 from scene-0's still (glide forward into it);
then each leg's --start-image = the previous leg's ACTUAL last frame (extract with
ffmpeg), prompt "continue gliding smoothly FORWARD into [scene i], never pulling back"
(or an expressive mid-leg move under the motion-handoff contract — see Camera grammar
below), and no --end-image — an end-image of a wide establishing shot forces the
camera to pull back, which is the #1 cause of stutter. Extract each leg's last frame to feed the
next. Result: every seam is frame-identical and the camera never reverses. There are
no connectors (skip Step 5) — the legs ARE the journey. Wire each leg as a section
clip with connectors: [] and a small crossfade (~0.08). Even without an --end-image
the legs still arrive at distinct rooms (the prompt steers the content). Cost: strictly
sequential (can't parallelize) and slower; interiors trip the NSFW filter, so build in
re-rolls (3 attempts/leg).
B) Dive-in + aerial connector — only for diorama / miniature / god's-eye worlds
A "dive into each scene" clip + a connector that pulls up and out and flies over to the
next scene (Step 5). The pull-out reverses camera direction at every seam (forward dive
→ backward pull-out). In a miniature/diorama world that reads as an intentional "zoom out
to the map, fly to the next island"; in a grounded first-person walkthrough it reads as a
jarring rewind/stutter. Use B only for the map-like aesthetic. When in doubt, use A.
Camera grammar — the move should fit the concept (A is NOT "forward only")
"Forward only" is the seam rule, not the leg rule. The physics of the chain:
- Position continuity at a seam comes from the frame handoff (next leg starts from the
previous leg's actual last frame).
- Velocity continuity at a seam means the camera must never reverse across a seam —
that's the rewind stutter.
- Inside a single leg the camera is free. One leg is one continuous render — there is
no seam to break mid-leg, so orbits, crane-ups, lateral tracking, even a push-in that
eases back out are all safe within the clip. Reversals are only fatal across seams.
So give each leg an expressive move chosen from the scene's own logic, under a motion
handoff contract: every leg ends by settling into a slow, steady forward drift toward
the next destination (final ~1 s), and every leg begins by continuing that same drift.
Keep both clauses in the prompts verbatim (templates in references/prompts.md).
Pick the grammar from the concept:
| Concept / tone |
Mid-leg move |
| Product / luxury retail |
slow half-orbit around the hero object, then continue past it |
| Real estate / hospitality |
steadicam glide through doorways; gentle crane-up in atria |
| Industrial / process / logistics |
low lateral track alongside the line, foreground parallax |
| Travel / outdoors / campus |
drone-style rise-and-reveal, then a descending swoop |
| Food / craft / detail-driven |
push in close to the craft moment, ease back, carry on |
| Playful miniature (arch. B) |
dives + aerial hops — the connector IS the grammar |
Honest costs: expressive mid-leg moves raise re-roll odds — the model can end a fancy move
in a state that isn't a clean forward drift. Mitigations: keep the final-second settle
clause verbatim; eyeball each leg's last frame before chaining the next (it should look
like a frame from a gentle forward glide — if not, re-roll before wasting the next leg);
budget ~1 extra re-roll per expressive leg. A plain forward glide stays the zero-risk
default — use it for legs where the scene itself is the show.
Two related pacing knobs live in the engine (Step 7): per-section scroll (more scroll
distance = longer dwell in that scene) and linger (the camera settles mid-scene exactly
while the copy peaks, then picks up speed toward the seam). Prefer expressive motion in the
clip and restraint in the scrub mapping — they compound.
And remember scroll is a scrubber: visitors can scroll up, so every move also plays in
reverse. That's free and expected — no extra work — but it's another reason seam velocity
must be consistent in both directions (a seam that reads fine forward reads as a stutter
backward too if velocity flips).
For B, one camera flight per scene: starts high/outside, descends into the interior,
structure opens. Model: the chain model you picked above (default seedance_2_0),
--start-image = the scene still.
- Use the solid-background still (not the knocked-out transparent one) as the
start image, so the video has a full frame.
- Prompt: "Single continuous cinematic camera move, no cuts. Begin high and far looking
at the whole from outside … descend and fly inside toward … the
roof/walls gently open to reveal the interior. , smooth graceful slow motion.
No text." (Template in
references/prompts.md.)
- Params (seedance):
--mode std --resolution 1080p --aspect_ratio 16:9 --duration 8.
For Kling: drop --resolution (no such param), add --sound off, --duration 10.
Do not pass --generate-audio (it errors on seedance; audio is wasted anyway —
you'll mute).
- Run concurrently, detached, then download each
.result_url. Re-roll individual
failures. Keep the raw 1080p sources — you need their frames next.
Step 5 — Connectors (architecture B only)
Skip this whole step for architecture A — the forward take has no connectors; its legs
already chain seamlessly. This step applies to B (diorama/miniature), and note the
reversal caveat from Step 4.
The connector clips are what make the world feel connected instead of cut. A connector
flies from the end of scene i out and into the start of scene i+1. Both of its
endpoints must be the ACTUAL RENDERED FRAMES of the neighbouring clips — never the
original diorama still.
Why: every Higgsfield generation renders slightly differently. If a connector ends on
a fresh render of "the kitchen diorama," but the next dive clip starts on its own
different render of that same diorama, the two won't match and you get a pop at the seam.
The fix is to hand off the exact pixels:
For each connector between dive_i and dive_{i+1}:
start-image = the LAST frame extracted from dive_i's rendered video
end-image = the FIRST frame extracted from dive_{i+1}'s rendered video
Now every seam is frame-identical on both sides:
dive_i.end == connector.start and connector.end == dive_{i+1}.start.
Extract the boundary frames from the rendered dives (not the stills):
ffmpeg -sseof -0.15 -i dive_i.mp4 -frames:v 1 -q:v 2 dive_i_last.png # interior of i
ffmpeg -ss 0 -i dive_{i+1}.mp4 -frames:v 1 -q:v 2 dive_next_first.png # establishing of i+1
Generate the connector (--duration 5 is plenty). Connectors need --end-image, so the
model must accept it — any roster model does (seedance_2_0, seedance_2_0_mini,
kling3_0):
higgsfield generate create "$VMODEL" \
--prompt "$(cat connector_i.txt)" \
--start-image dive_i_last.png --end-image dive_next_first.png \
$VOPTS --aspect_ratio 16:9 --duration 5 --wait --json
# seedance: VOPTS="--mode std --resolution 1080p"; kling3_0: VOPTS="--mode std --sound off"
Connector prompt: "Single continuous camera move, no cuts. Pull up and back out of
, rise into the sky, glide across the connected miniature world, and arrive
above <scene i+1>, beginning to descend toward it. Seamless flowing aerial transition.
1---2name: scroll-world3description: >4 Build an immersive scroll-scrubbed "fly through the world" landing page for any5 industry or brand using Higgsfield. As the visitor scrolls, a pre-rendered camera6 flies from outside each scene into its interior, then flows on to the next scene7 with NO cuts — one continuous connected flight (Emons-style isometric diorama world,8 or any art direction you pick). The skill interviews the user for the topic, the9 story beats/sections, and brand kit, then generates cohesive scenes + seamless camera10 clips with Higgsfield and wires a portable, framework-agnostic scroll-scrub engine.11 Use when the user wants a "3D world" / "browse-through-the-industry" hero, a scroll12 cinematic, a diorama landing, or to turn a business into a scrollable world.13allowed-tools: Bash, Read, Write, Edit, AskUserQuestion, Skill14---1516# scroll-world1718Produces a landing page where **scroll drives a camera**: it dives from outside a scene19into its interior, then flies out and into the next scene, continuously, with no visible20cuts. The visuals are AI-generated (Higgsfield); the page just scrubs pre-rendered video21by scroll position. This is the same technique behind Apple's scroll-through product22pages — the camera genuinely moves, scroll only drives time.2324**What you generate:** N scene stills → N "dive-in" camera clips → N-1 "connector" clips25that join consecutive scenes seamlessly → a portable scrub engine that plays the whole26chain as one flight.2728**The one rule that makes or breaks it:** seams must be *frame-identical*. Read29[The seamless chain](#step-5--the-seamless-chain-the-critical-part) before generating any30connector. Getting this wrong is the single most common failure and produces a visible31"pop" between scenes.3233Do not assume a frontend framework. The scrub engine in `references/scrub-engine.js` is34self-contained vanilla JS (it builds its own DOM + injects its own CSS into a container35you give it), so it drops into plain HTML, Next.js, Vue, a Python-served page, anything.36The value of this skill is the Higgsfield pipeline, the prompts, and the seam method —37not the framework.3839---4041## Step 0 — Bootstrap42431. **Higgsfield CLI.** If `higgsfield` is not on `$PATH`, install per the44 `higgsfield-generate` skill. If `higgsfield workspace list` fails auth, ask the user45 to run `higgsfield auth login` (interactive OAuth — you cannot run it) and, if needed,46 `higgsfield workspace set <id>`. Confirm there are enough credits: a full run is47 roughly `N` image gens + `(2N-1)` video gens.482. **ffmpeg / ffprobe** on `$PATH` (frame extraction + encoding).493. **An image tool** for background knockout if you want floating scenes: PIL50 (`python3 -c "import PIL"`), or `cwebp`/`sips`. Optional — see Step 3.514. **(Optional) Codex CLI** — if `codex` is on `$PATH` (≥ 0.125) and52 `codex login status` reports a ChatGPT login, the scene stills can be generated53 through Codex's built-in `image_gen` (the same gpt-image-2 model) billed to the54 user's ChatGPT subscription instead of Higgsfield credits — offer it at55 Step 1.6, command in Step 2. Absence just removes the option.565. Caveats: macOS ships **bash 3.2** (no `declare -A`); don't use associative arrays in57 scripts. Higgsfield generations take **3–8 min each** — always run them detached58 (background) and poll, never a foreground blocking call. Reference-by-job-UUID is59 rejected by media flags — pass **local file paths** to `--image/--start-image/--end-image`.60 Video models differ in accepted params (e.g. Kling has no `--resolution`) and in whether61 they support start/end-image conditioning at all — before batching, confirm the chosen62 model's schema with `higgsfield model get <job_type>` and see the Step 4 model table.6364---6566## Step 1 — Interview the user6768The **subject is the user's to state — ask it as an open question in plain prose**, never a69fabricated multiple-choice. A made-up list of industries biases them and reads as you70deciding their business for them; let them answer in their own words (their real business,71a client's, or any idea). Reserve structured multiple-choice (`AskUserQuestion` in Claude72Code; a plain either/or question elsewhere) for the genuinely73enumerable, lower-stakes choices below — art direction and brand-kit approach — and even74there, signal they can go their own way ("Other"). Ask only what you can't sensibly75default. Cover:76771. **Subject** (ask openly, not multiple-choice) — "What should this world be about? Your78 business, a client's, or any idea — a word or a sentence is fine." Capture the79 industry/product + a one-line pitch (e.g. "a bubble tea company, from leaf to last80 sip"), and a brand name if they have one; otherwise you'll propose one below.812. **Brand kit** — offer three paths, pick one:82 - Import from a URL: `higgsfield marketing-studio brand-kits fetch --url <site> --wait`83 (pulls name, colours, tone). Then read it back with `brand-kits list --json`.84 - The user hands you palette + name + tone directly.85 - You propose a palette + name and let them approve.86 Capture **4–6 named hex values**, a display name, and a tone word or two.873. **Art direction** — default is "soft matte low-poly **clay diorama**, isometric,88 tilt-shift miniature, warm light." Offer alternatives (flat papercraft, glossy toy,89 claymation, neon night). Whatever is chosen becomes the shared **style preamble**90 reused verbatim in every scene prompt (this is what makes the world cohesive).914. **The journey (sections)** — the ordered scenes the camera flies through. Propose a92 set derived from the subject's own value chain and let the user edit. 5–7 works well.93 Boba example: farms → pearl kitchen → flagship shop → delivery → community plaza →94 the hero product. Each section needs: a short subject description (what's IN the95 diorama), an eyebrow, a headline, one line of body, and 0–3 tag pills. The last96 section is usually the hero product + the CTA.975. **Mobile version — ALWAYS ask this; never silently generate both.** Ask as a98 two-option choice (`AskUserQuestion` in Claude Code; a plain question elsewhere):99 *"Want a mobile-optimized version too? The mobile version is a second camera chain100 rendered natively in **9:16 portrait** — composed for phones, not a crop of the101 landscape film — which roughly doubles the Higgsfield credit spend (state the102 estimated number)."*103 Options: "Desktop only" / "Desktop + mobile (native 9:16 — ~2× credits)". The104 credit cost must be stated to the user, not just implied.105 What the answer gates:106 - **Yes** → render the parallel 9:16 portrait chain and ship it as the mobile variants107 (Step 6 / pipeline.md §6b): portrait start canvases → 9:16 dives + connectors108 frame-locked against their own renders → 720-wide `-m.mp4` encodes → `stillMobile`109 portrait posters. Wire `clipMobile`/`connectorsMobile`/`stillMobile` (Step 7); run110 the full mobile QA (Step 8). Budget ~2N-1 extra video gens + NSFW re-rolls.111 **Never ship the centre-crop as the mobile version by default** — if credits can't112 cover the portrait chain, say so and offer the crop encodes (pipeline.md §6) as an113 explicitly-labelled stopgap the user must approve.114 - **No** → skip the mobile encodes and wiring entirely. The engine's phone hardening115 (seek-coalescing, iOS priming, safe-area CSS) is always on regardless — that's not116 a "mobile version," it's just the page not breaking when a phone visits — so a117 desktop-only build still degrades gracefully.1181196. **Budget — engines shown by cost, decided before anything renders.** Present the120 render tiers (`AskUserQuestion`), then compute and state the estimated total for121 the user's N scenes — `N stills + (2N−1) videos [videos ×2 if mobile] + ~15%122 re-roll headroom` — and get a go before generating.123 - **Video tier** (roster only — every option frame-locks seams, Step 4):124125 | Tier | Model | Rough cost |126 |---|---|---|127 | Draft / previz | `seedance_2_0_mini` (720p) | ~¼ of Standard |128 | Standard (default) | `seedance_2_0` (1080p) | baseline |129 | Alternate | `kling3_0` (720p native) | ≈ Standard; different look + content filter |130131 Draft doubles as the previz path: run the whole chain cheap, approve the132 journey, re-render final legs on Standard (pipeline.md Notes) — suggest it133 unprompted when the balance reads tight.134 - **Stills source** (only offer if the Codex CLI is present, Step 0.4):135 Higgsfield `gpt_image_2` (spends credits) vs **Codex `image_gen`** — the same136 gpt-image-2 model billed to the ChatGPT subscription (zero credits; counts137 toward Codex usage limits; 1536×1024 output — exactly 3:2, slightly under138 Higgsfield's 2k). Stills are plain PNGs handed to `--start-image`, so the139 video chain is indifferent to their source. Command in Step 2. **One source140 for all N stills of a build** — the two render with slightly different141 character (verified: Codex runs warmer/lighter), and mixing sources across142 scenes reads as style drift, same reason the video chain uses one model.143 - **Calibrate costs, don't guess.** The CLI exposes no pricing and plans differ.144 Run ONE still and ONE video first, diff `higgsfield workspace list` before/145 after, extrapolate to the full run, and warn the user whenever the estimate146 exceeds ~70% of the balance. (Observed on a plus plan, 2026-07: Standard147 video ≈ 40–55 credits, still ≈ 15.) A real `not_enough_credits` mid-run is148 recoverable (finished clips survive; resume after top-up) but ugly — the149 whole point of this step is that the user decides *before* the spend.150151If the user names a video model outside the roster, honor it **only if it can152frame-lock seams** (Step 4). This skill only ships seamless output, so a model that153can't frame-lock is declined with a one-line why, not substituted in — use a roster154model instead.155156Keep the scroll mechanic fixed (continuous fly-through) — that's the point of the skill.157See `references/prompts.md` for the intake checklist and copy structure.158159---160161## Step 2 — Generate the scene stills162163One image per section, **all sharing the same style preamble** for cohesion. Default164model **`gpt_image_2`** (crisp, great at isometric illustration; returns a solid/white165background which is perfect for floating diorama "islands"). Use `nano_banana_2` only if166the brief is character/cartoon-heavy (note: `nano_banana_2` is a CLI alias — it resolves167to `nano_banana_pro`; it won't appear under that name in `higgsfield model list`).168169Prompt shape (full templates in `references/prompts.md`):170171```172<STYLE PREAMBLE, identical every time>. On a plain solid <bg> background with a soft173contact shadow. <PALETTE hexes>. No text, no letters, no logos, centered, 3:2.174Subject: <what is in THIS diorama>.175```176177- Run all N concurrently, detached. Command per scene:178 `higgsfield generate create gpt_image_2 --prompt "$(cat scene_i.txt)" --aspect_ratio 3:2 --resolution 2k --quality high --wait --wait-timeout 15m --json > scene_i.json 2>scene_i.err`179- Result URL is `.[]0.result_url` in the `--wait --json` output. `curl` it down.180- **Codex stills variant** (if chosen at Step 1.6 — subscription-billed, zero181 credits): same prompt files, same byte-identical preamble, generated by Codex's182 built-in `image_gen`:183184 ```bash185 codex exec -C "$WORK" -s workspace-write --skip-git-repo-check \186 'Use the image generation tool ($imagegen) to generate: '"$(cat "$WORK/still_i.txt")"' Wide 3:2 landscape, high resolution. Save it as ./still_i.png. Do not do anything else.'187 ```188189 Single-quote the `$imagegen` segment (the shell must not expand it); if editing190 with reference images, the prompt goes BEFORE any `-i` flag (it's variadic).191 ~1–3 min per image; run a few in parallel, not all N at once. Output lands at192 1536×1024 (3:2) — fine for `--start-image` and posters. Everything downstream193 (cohesion review, knockout, dives) is unchanged.194- A generation may fail transiently (HTTP 503) — re-roll that one individually; don't195 restart the batch.196- **Review the stills before continuing.** They must read as one cohesive world (same197 angle, palette, light). If one is off-style, regenerate it, optionally passing an198 approved scene as `--image` to lock style.199200See `references/pipeline.md` for the exact batch script.201202---203204## Step 3 — (Optional) Float the scenes205206If you want the dioramas to float over an atmospheric background instead of sitting in a207solid box, knock out the flat background to transparency with208`references/knockout.py` (border-connected flood fill — preserves interior colour that209matches the bg, e.g. cream walls). Then encode to webp. If you'd rather keep it simple,210just make the page background the same colour as the scene background and skip this.211212These stills double as **video posters and lazy-load fallbacks**, so keep them.213214---215216## Step 4 — Camera architecture (pick one — this makes or breaks the feel)217218How the camera moves *between* scenes is the single biggest quality lever. Two shapes;219pick by aesthetic.220221### Video model — pick ONE for the whole chain222223**This skill only ships seamless output**, so the only usable models are ones that can224frame-lock a seam: every chained clip must accept `--start-image`, and connectors also225need `--end-image`. That capability — not preference — is the selection rule. Check any226model with `higgsfield model get <job_type>` and **skip anything whose media inputs are227reference-only** (no start/end image): it can only *condition* a generation, not228*continue* a shot, so it physically can't hold a seam. Schemas below were confirmed229against the CLI:230231| Model | start/end image | Notes |232|---|---|---|233| `seedance_2_0` (default) | ✓ / ✓ | Full chain (legs + connectors). `--mode std --resolution 1080p`. Its NSFW filter is the touchy one (see Gotchas). |234| `kling3_0` | ✓ / ✓ | Full chain — tested: `--mode std --sound off --duration 5` with start+end images accepted, seams frame-lock cleanly. **No `--resolution` param** (don't pass one; `--mode std` returns **720p native** — encode what ffprobe reports, never upscale). Sound defaults **on** → `--sound off`. `--duration` default 5, try 10 for legs. Different content filter than Seedance — the sanctioned NSFW fallback. |235| `seedance_2_0_mini` | ✓ / ✓ | Cheap draft tier that keeps frame-locking (720p). The previz tier: run the whole chain here first, then re-render final legs on the full model — still seamless, so it translates directly. |236237Those three are the roster — all do both architectures. (`kling3_0_turbo` also frame-locks238via `--start-image`, but has no `--end-image`, so it's architecture-A-only and can't make239connectors; it also takes a different flag set — no `--mode`, has `--resolution` — so it240doesn't drop into the pipeline as-is. It's not in the default roster; only reach for it, and241wire it by hand, if architecture A's sequential render time is a proven bottleneck and you've242benchmarked it as actually faster.)243244Rules:245- **One model for all chained clips.** Each renderer has its own motion/color/grain246 character; mixing models mid-chain keeps *position* continuity (frames still hand off)247 but the render-character shift reads as a subtle pop. The one sanctioned exception is248 the NSFW fallback for a single stubborn clip (Gotchas) — a slight character shift on249 one 5s connector beats a missing connector.250- Default to `seedance_2_0`; honor a user's stated preference **only if the model251 qualifies** (frame-locking). If it doesn't, say so and use a supported model — never252 ship a non-seamless build to satisfy a model request.253- The pipeline scripts take the model as `$VMODEL` with per-model flags already cased254 out (`references/pipeline.md`).255256### A) Continuous forward take — RECOMMENDED for grounded / realistic / walkthrough257One camera that only ever glides **forward**, first scene through last, as a single take.258Generate the legs **sequentially**: leg 0 from scene-0's still (glide forward into it);259then each leg's `--start-image` = the **previous leg's ACTUAL last frame** (extract with260ffmpeg), prompt *"continue gliding smoothly FORWARD into [scene i], never pulling back"*261(or an expressive mid-leg move under the motion-handoff contract — see **Camera grammar**262below), and **no `--end-image`** — an end-image of a wide establishing shot forces the263camera to pull back, which is the #1 cause of stutter. Extract each leg's last frame to feed the264next. Result: every seam is frame-identical **and** the camera never reverses. There are265**no connectors** (skip Step 5) — the legs ARE the journey. Wire each leg as a section266clip with `connectors: []` and a small `crossfade` (~0.08). Even without an `--end-image`267the legs still arrive at distinct rooms (the prompt steers the content). Cost: strictly268**sequential** (can't parallelize) and slower; interiors trip the NSFW filter, so build in269re-rolls (3 attempts/leg).270271### B) Dive-in + aerial connector — only for diorama / miniature / god's-eye worlds272A "dive into each scene" clip + a connector that pulls **up and out** and flies over to the273next scene (Step 5). The pull-out **reverses camera direction at every seam** (forward dive274→ backward pull-out). In a miniature/diorama world that reads as an intentional "zoom out275to the map, fly to the next island"; in a grounded first-person walkthrough it reads as a276jarring **rewind/stutter**. Use B only for the map-like aesthetic. When in doubt, use A.277278### Camera grammar — the move should fit the concept (A is NOT "forward only")279280"Forward only" is the *seam* rule, not the *leg* rule. The physics of the chain:281282- **Position continuity** at a seam comes from the frame handoff (next leg starts from the283 previous leg's actual last frame).284- **Velocity continuity** at a seam means the camera must never *reverse across a seam* —285 that's the rewind stutter.286- **Inside a single leg the camera is free.** One leg is one continuous render — there is287 no seam to break mid-leg, so orbits, crane-ups, lateral tracking, even a push-in that288 eases back out are all safe *within* the clip. Reversals are only fatal *across* seams.289290So give each leg an expressive move chosen from the scene's own logic, under a **motion291handoff contract**: every leg **ends by settling into a slow, steady forward drift** toward292the next destination (final ~1 s), and every leg **begins by continuing that same drift**.293Keep both clauses in the prompts verbatim (templates in `references/prompts.md`).294295Pick the grammar from the concept:296297| Concept / tone | Mid-leg move |298|---|---|299| Product / luxury retail | slow half-orbit around the hero object, then continue past it |300| Real estate / hospitality | steadicam glide through doorways; gentle crane-up in atria |301| Industrial / process / logistics | low lateral track alongside the line, foreground parallax |302| Travel / outdoors / campus | drone-style rise-and-reveal, then a descending swoop |303| Food / craft / detail-driven | push in close to the craft moment, ease back, carry on |304| Playful miniature (arch. B) | dives + aerial hops — the connector IS the grammar |305306Honest costs: expressive mid-leg moves raise re-roll odds — the model can end a fancy move307in a state that isn't a clean forward drift. Mitigations: keep the final-second settle308clause verbatim; **eyeball each leg's last frame before chaining the next** (it should look309like a frame from a gentle forward glide — if not, re-roll before wasting the next leg);310budget ~1 extra re-roll per expressive leg. A plain forward glide stays the zero-risk311default — use it for legs where the scene itself is the show.312313Two related pacing knobs live in the engine (Step 7): per-section `scroll` (more scroll314distance = longer dwell in that scene) and `linger` (the camera settles mid-scene exactly315while the copy peaks, then picks up speed toward the seam). Prefer expressive motion in the316*clip* and restraint in the *scrub mapping* — they compound.317318And remember scroll is a scrubber: visitors can scroll **up**, so every move also plays in319reverse. That's free and expected — no extra work — but it's another reason seam velocity320must be consistent in both directions (a seam that reads fine forward reads as a stutter321backward too if velocity flips).322323**For B**, one camera flight per scene: starts high/outside, descends into the interior,324structure opens. Model: the chain model you picked above (default **`seedance_2_0`**),325`--start-image = the scene still`.326327- Use the **solid-background still** (not the knocked-out transparent one) as the328 start image, so the video has a full frame.329- Prompt: "Single continuous cinematic camera move, no cuts. Begin high and far looking330 at the whole <scene> from outside … descend and fly inside toward <focal point> … the331 roof/walls gently open to reveal the interior. <style>, smooth graceful slow motion.332 No text." (Template in `references/prompts.md`.)333- Params (seedance): `--mode std --resolution 1080p --aspect_ratio 16:9 --duration 8`.334 For Kling: drop `--resolution` (no such param), add `--sound off`, `--duration 10`.335 Do **not** pass `--generate-audio` (it errors on seedance; audio is wasted anyway —336 you'll mute).337- Run concurrently, detached, then download each `.result_url`. Re-roll individual338 failures. Keep the raw 1080p sources — you need their frames next.339340---341342## Step 5 — Connectors (architecture B only)343344Skip this whole step for architecture **A** — the forward take has no connectors; its legs345already chain seamlessly. This step applies to **B** (diorama/miniature), and note the346reversal caveat from Step 4.347348The connector clips are what make the world feel *connected* instead of cut. A connector349flies from the end of scene i out and into the start of scene i+1. **Both of its350endpoints must be the ACTUAL RENDERED FRAMES of the neighbouring clips — never the351original diorama still.**352353Why: every Higgsfield generation renders slightly differently. If a connector *ends* on354a fresh render of "the kitchen diorama," but the next dive clip *starts* on its own355different render of that same diorama, the two won't match and you get a pop at the seam.356The fix is to hand off the exact pixels:357358```359For each connector between dive_i and dive_{i+1}:360 start-image = the LAST frame extracted from dive_i's rendered video361 end-image = the FIRST frame extracted from dive_{i+1}'s rendered video362```363364Now every seam is frame-identical on *both* sides:365`dive_i.end == connector.start` and `connector.end == dive_{i+1}.start`.366367Extract the boundary frames from the rendered dives (not the stills):368369```bash370ffmpeg -sseof -0.15 -i dive_i.mp4 -frames:v 1 -q:v 2 dive_i_last.png # interior of i371ffmpeg -ss 0 -i dive_{i+1}.mp4 -frames:v 1 -q:v 2 dive_next_first.png # establishing of i+1372```373374Generate the connector (`--duration 5` is plenty). Connectors need `--end-image`, so the375model must accept it — any roster model does (`seedance_2_0`, `seedance_2_0_mini`,376`kling3_0`):377378```bash379higgsfield generate create "$VMODEL" \380 --prompt "$(cat connector_i.txt)" \381 --start-image dive_i_last.png --end-image dive_next_first.png \382 $VOPTS --aspect_ratio 16:9 --duration 5 --wait --json383# seedance: VOPTS="--mode std --resolution 1080p"; kling3_0: VOPTS="--mode std --sound off"384```385386Connector prompt: "Single continuous camera move, no cuts. Pull up and back out of387<scene i>, rise into the sky, glide across the connected miniature world, and arrive388above <scene i+1>, beginning to descend toward it. Seamless flowing aerial transition.389<style>. No text." (Template in `references/prompts.md`.)390391Insurance: Seedance lands *close* to the end-image but not always pixel-perfect, so the392engine still applies a **short crossfade** (a few frames) at each seam. Frame-matched393endpoints + a small crossfade = no visible cut. Never skip the actual-frame handoff and394rely on the crossfade alone; a big content jump can't be hidden by a crossfade.395396---397398## Step 6 — Encode for smooth scrubbing399400Scrubbing = setting `video.currentTime` from scroll. Two things matter, and they are401often gotten wrong:4024031. **Seekability, not keyframe density, is what makes scrubbing work.** Many static404 hosts (and `python -m http.server`) don't serve HTTP byte-range requests, which pins405 `video.seekable` to `[0,0]` and clamps *every* seek to frame 0 — the video looks406 frozen. The robust fix is to **fetch each clip as a `Blob` and play it from an407 in-memory object URL** (blobs are always fully seekable). The engine does this.408 Because of it, you do **not** need all-intra video.4092. **Don't shrink quality to get smooth seeks.** Encode at the **native resolution**410 (1080p from Seedance — don't downscale), `crf ~20`, a **small GOP** (`-g 8`) rather411 than all-intra (all-intra bloats an 8s clip to ~25 MB; GOP 8 is ~8 MB and scrubs412 fine via blob). Strip audio, add faststart, and a light `unsharp` counters video413 softness:414415```bash416ffmpeg -i src.mp4 -an -vf "unsharp=5:5:0.8:5:5:0.0" \417 -c:v libx264 -preset slow -crf 20 -pix_fmt yuv420p \418 -g 8 -keyint_min 8 -sc_threshold 0 -movflags +faststart out.mp4419```420421Encode all 2N-1 clips (dives + connectors) with the same settings for uniform quality.422423**Mobile encodes (only if the user opted in at Step 1.5).** The mobile version is424the **native 9:16 portrait chain** (pipeline.md §6b): portrait renders of every dive and425connector, encoded **720 wide (`scale=720:-2`), `-g 4`** (more keyframes = cheaper seeks —426phone decoders' seek cost scales with GOP length), crf 23 — wired as `clipMobile` /427`connectorsMobile`, with each portrait dive's first frame extracted as the section's428`stillMobile` poster (Step 7). The engine serves them automatically on phones and falls429back to the desktop clip when absent. The 16:9 centre-crop `encm()` encodes430(pipeline.md §6) are a **fallback only** — for when credits can't cover the portrait431chain — and shipping them must be called out to the user, never silent. If the user chose432desktop-only, skip this — the engine still hardens phone scrubbing regardless433(seek-coalescing, iOS priming), so the page degrades gracefully rather than breaking.434435---436437## Step 7 — Assemble the page438439Copy `references/scrub-engine.js` (and, if you want a fully standalone page, the tiny440`references/index-template.html`) into the user's project — or adapt into their441framework. It's config-driven and self-contained:442443```js444mountScrollWorld(document.getElementById('world'), {445 brand: { name: 'Pearl & Co.' },446 diveScroll: 1.3, connScroll: 0.9, // viewport-heights of scroll per clip447 sections: [448 { id:'farm', label:'The Farms', still:'assets/farm.webp',449 clip:'assets/vid/farm.mp4',450 clipMobile:'assets/vid/farm-m.mp4', // mobile opt-in only: native 9:16 render451 stillMobile:'assets/farm-m.webp', // its first frame as the portrait poster452 scroll: 1.6, linger: 0.45, // optional pacing: longer dwell + camera settles mid-scene453 accent:'#8FB98A', eyebrow:'From leaf to last sip', title:'It starts in the hills.',454 body:'…', tags:['Single-origin','Hand-picked'] },455 // …one per section; last may carry a `cta`456 ],457 connectors: ['assets/vid/conn1.mp4','assets/vid/conn2.mp4', /* … length = sections-1 */],458 connectorsMobile: ['assets/vid/conn1-m.mp4','assets/vid/conn2-m.mp4' /* … same length; mobile opt-in only */],459});460```461462The engine handles: the ordered dive/connector chain, scroll→currentTime with rAF463smoothing, blob loading, lazy prefetch of nearby clips, frame-matched crossfades, pinned464per-section copy (first section greets on landing, last holds its CTA), a route rail,465`prefers-reduced-motion`, and mobile. **Pacing per section:** `scroll` overrides466`diveScroll` for that scene (more scroll = longer dwell) and `linger` (0–1, keep ≤ 0.6)467remaps time so the camera settles mid-scene — exactly while the copy peaks — then speeds468up toward the seam; seam frames are untouched (f(0)=0, f(1)=1). Give the hero and finale469scenes a higher `scroll` + some `linger`; keep transit scenes brisk. Theme it with CSS variables (`--accent`,470`--sw-bg`, `--sw-ink`, …) — the visual identity comes from the generated clips, so the471chrome stays quiet. See the header of `scrub-engine.js` for the full config + CSS vars.472473**On phones the engine adapts automatically** (coarse pointer or ≤860px): it serves474`clipMobile` / `connectorsMobile` when present, **coalesces seeks** (never queues a new475`currentTime` while the decoder is still seeking — this is what stops a fast flick from476freezing the clip), **keeps the still as a poster until the clip paints its first frame**477and **primes each video on first touch** (fixes iOS's blank-until-played video), drops the478drifting particles, ignores URL-bar-only resizes (no scroll jump), and uses safe-area479insets so copy clears the notch/home indicator. All of this hardening is on by default —480no config needed. The `clipMobile`/`connectorsMobile` encodes are the opt-in part481(Step 1.5): only wire them when the user asked for the mobile version.482483For non-JS backends (Python/Rails/etc.): serve the assets and drop the engine `<script>`484into the rendered HTML; nothing about it is framework-specific.485486---487488## Step 8 — QA the seams (don't skip)489490Drive the page in a headless browser and **verify frame continuity at the seams**, which491is the thing most likely to be wrong:492493- Screenshot at scroll positions just before and just after each seam. The two frames494 must be near-identical (the dive's last frame == the connector's first frame). If they495 pop, you used the diorama still instead of the actual rendered frame (redo Step 5), or496 the crossfade band is too short.497- Check the console for errors, confirm `video.seekable.end(0) > 0` (blob working), and498 that `currentTime` tracks scroll across each clip's band.499- **Mobile — full checklist only if the user opted into the mobile version (Step 1.5).**500 For a desktop-only build, just sanity-check a phone viewport once: page loads, still501 posters show, nothing overlaps — the engine's hardening covers graceful degradation.502 For the mobile build (do this on a real phone or an emulated one, portrait + landscape):503 - Emulate a phone viewport **with CPU throttled 4–6×** and scroll fast — the clip should504 track without freezing (the seek-coalescing + `-m.mp4` encodes are what make this hold).505 - Confirm the first scene shows immediately (its still is the poster) and the video takes506 over the instant you scroll — no blank/black scene (the iOS priming fix). Test iOS Safari507 specifically; it's the one that goes blank if this regresses.508 - Verify the `-m.mp4` variant is actually served on mobile (Network panel), and the509 heavy 1080p master on desktop. The mobile clips must be **natively portrait**510 (`videoWidth < videoHeight` — not a downscaled 16:9 file), and the `stillMobile`511 posters must be served and match each portrait clip's first frame (no512 landscape→portrait flash when the video paints).513 - Slowly scroll so the URL bar collapses — the page must **not jump** (height-only resizes514 are ignored on touch). Rotate the device — layout should recompose cleanly.515 - Only if the crop **fallback** shipped (no credits for the portrait chain): portrait516 crops a 16:9 clip to its centre — confirm the focal subject still reads, and remind517 the user this is the stopgap, not the mobile version.518- Check reduced-motion (should fall back to the stills, no video, no particles).519520---521522## Gotchas (hard-won)523524- **Seam pop** → connector endpoints were the diorama stills, not the neighbouring525 clips' actual frames. Always extract real frames (Step 5).526- **Seam stutter / camera "jumps backward"** → even with frame-matched seams, if the527 camera *velocity reverses* (forward dive, then a connector that pulls back out) it528 reads as a rewind. This is inherent to architecture B. For any grounded walkthrough use529 architecture A (one continuous forward take — legs chained from actual last frames, no530 pull-back, no `--end-image`); see Step 4.531- **Frozen video / stuck at frame 0** → `seekable=[0,0]`; the host isn't serving byte532 ranges. Use blob URLs (engine does).533- **Huge files** → you used all-intra. Use `-g 8` + blob instead.534- **Soft / low quality** → you downscaled or over-compressed. Encode native 1080p,535 crf ≤ 20, add `unsharp`. Video is inherently softer than the stills — keep the stills536 as the lite fallback for max fidelity.537- **Concurrent gens 503 / "not_enough_credits" race** → transient when many launch at538 once; re-roll the individual failure, it's not really out of credits (verify with539 `higgsfield workspace list`).540- **NSFW false-positives (Seedance `status "nsfw"`)** → the video content filter flags541 perfectly innocuous clips, especially **bedroom, pool, spa/wellness** contexts and542 trigger words like "bed", "pool", "waterfall", "wine", "swim". It's partly the prompt543 wording and partly the reference frames. Fixes, in order: (1) re-roll — it's often544 non-deterministic and passes on the 2nd–3rd try; (2) strip trigger words and add545 "empty, unoccupied, no people, no figures, architectural, tasteful"; (3) regenerate546 just that clip on **`kling3_0`** with the same start/end frames — a different547 provider's filter often passes what Seedance blocks. Expect a slight render-character548 shift on that one clip (each model has its own grain/motion feel); for a 5s connector549 behind a crossfade that usually beats option (4): set the connector slot to `null` —550 the engine crossfades that seam directly (optional connectors), so the page still551 completes. Budget extra credits/time for these re-rolls on interiors/real-estate content.552- **Dark / custom theme** → the engine wraps its default tokens in `@layer sw`, so a553 page-level `:root` / `.sw-root { --sw-bg; --sw-ink; --sw-accent; --sw-font-* }` block554 wins cleanly (no specificity hacks). `--sw-ink` is your primary **text/heading** colour;555 the **accent** fills the primary button and active nav. For a dark theme, set `--sw-bg`556 dark and `--sw-ink` light — the copy scrim and title shadow follow `--sw-bg` automatically.557- **Phone scrub stutters / freezes on a fast flick** → the 1080p master is too heavy for a558 phone decoder and seeks pile up. Ship the `-m.mp4` mobile encodes (720p, `-g 4`) and wire559 `clipMobile`/`connectorsMobile` (Step 6/7). The engine already coalesces seeks; the lighter560 encode is the other half. Still choppy on a low-end device? Tighten GOP (`-g 2` / all-intra).561- **Blank / black scene on iOS (desktop was fine)** → an iOS Safari quirk: a muted video that562 was never played won't paint a seeked frame. The engine fixes this by keeping the still as a563 poster until the clip paints and priming each video on first touch — so **don't** hide the564 still on `loadedmetadata` or strip the `playsinline`/`muted` attributes if you adapt the565 engine into a framework.566- **Page jumps while scrolling on mobile** → something is re-running layout on the URL-bar567 show/hide `resize`. The engine ignores height-only resizes on touch; if you ported it, gate568 your resize handler on a width change (keep the `orientationchange` path for rotation).569- **Copy hidden behind the URL bar / notch on mobile** → use the engine's safe-area-aware570 bottom offset (`env(safe-area-inset-bottom)` + `dvh`); make sure the page's571 `<meta viewport>` includes `viewport-fit=cover` (the template does).572- **Portrait crops the scene** → a 16:9 clip on a tall phone shows only its centre — which573 is why the mobile version is the native 9:16 chain (§6b), never the crop. If you're seeing574 this on a mobile build, either the crop fallback shipped (call it out to the user) or the575 9:16 encodes aren't actually being served (check `videoWidth < videoHeight`). Keeping each576 scene's focal subject centred (prompts.md) still matters for the desktop film itself.577- **`--generate-audio` errors on seedance** → omit it; mute in HTML and `-an` on encode.578- **Kling rejects your flags** → `kling3_0` has **no `--resolution` param** (don't pass579 one; encode at whatever native res ffprobe reports) and **sound defaults on** — pass580 `--sound off`. Duration default is 5; legs/dives want 10.581- **Seam pop only where you "saved credits"** → you swapped models mid-chain, or used a582 start-image-only model where a connector needs an `--end-image`. One model for the whole583 chain; the only cheap tier is `seedance_2_0_mini`, which keeps frame-locking so it stays584 seamless. (Any model with reference-only inputs can't hold a seam at all — Step 4.)585- **White-box scenes** → `gpt_image_2` returns a solid bg; either match the page bg to it586 or knock it out (Step 3).587- **bash 3.2** on macOS → no associative arrays in scripts.588- **Connector grabs the wrong scene's frames** (or errors on a frame that doesn't exist589 yet) → the array loop ran in **zsh** (macOS default interactive shell), where arrays are590 1-indexed, not bash's 0-indexed. Keep every array-driven chain step in a `#!/bin/bash`591 script run via `bash script.sh` — never inline array loops in the interactive shell.592593## References594595- `references/prompts.md` — the intake checklist, style-preamble pattern, and every596 prompt template (scene still, dive, connector) with fill-in slots.597- `references/pipeline.md` — copy-paste batch scripts for the whole run (generate →598 extract frames → connectors → encode → mobile encode), bash-3.2-safe.599- `references/scrub-engine.js` — the portable, config-driven scrub engine (builds DOM +600 injects CSS; blob-seek, lazy load, seam crossfade, copy, route rail, reduced-motion, and601 phone hardening: mobile encodes, seek-coalescing, iOS priming, safe-area, no-jump resize).602- `references/index-template.html` — a minimal standalone page that mounts the engine.603- `references/knockout.py` — border-connected background knockout for floating scenes.