Guided Growth Remotion build-out

Everything below was rendered on 2026-07-27 and verified by driving a real browser or by measuring the file, not by inspection. Scope is website animations, the orb pipeline and the social clip factory. App Store assets and the narrated onboarding video are out of scope.

The site link is the real 11-section page with the animation layer in it. Scroll into "It sees how it all connects" and "One sentence becomes part of a long arc" and scrub up and down. The percentage badge in the corner is the live scroll progress.

01bSaid, heard, card, weekly

The data-flow story, built for the siri section whose headline is already "One sentence becomes part of a long arc". Four beats, 430 frames, scrubbed by scroll. One spoken sentence in the gym becomes a habit card, and that card takes its place in the week. Nothing cuts: the card physically travels into the grid and the cell it fills stays marked.

VoiceToWeek. Said, heard, card, weekly.
siri scroll strip
Scrubbed on the page, 0 / 25 / 50 / 75 / 100 percent scroll.
BeatWhat it doesBuilt from
1 SaidHer line as a live waveformreal speech envelope, 398 frames from a Cartesia clip
2 HeardWaveform resolves into wordsword reveal timed off the envelope, not hand-tuned
3 CardHabit card forms, green check drawsnew HabitLogCard component
4 WeeklyCard shrinks, a line carries it into the grid, Wednesday lands greenweek grid + connector with a travelling head

Copy is lifted verbatim from the live page's own beatstrip in that section, so this is an animated version of already-approved content rather than new copy. On the page the animation replaces those four static cards, but only once the player is actually live, so with JavaScript off or reduced motion the original cards stay and the section still reads.

One thing to decide: the card shows "4 x 8 @ 185 lb" because the live page already says that. The-source models a habit as name, days, time and status, with no quantity field anywhere, so the page is already ahead of the data model on sets and reps.

01Website animations

Three sections are pinned scroll-scrub driven by a Remotion Player through seekTo(). Six animate once on entry. One is deliberately left still. The whole integration is one stylesheet and one script added to the page, nothing else changed.

Section to treatment map

SectionHeadlineTreatment
heroA coach in your pocketlive orb loop replaces the static PNG
visionGoals that keep getting pushed asideanimate once, slow push
problemWe looked at 95 appsanimate once
newwayA privacy-first conversational AI coachanimate once
how-it-worksTalk it through, or just tappinned scrub, habit card forming
carHands free, on the drive homeanimate once, slow push
morningIt sees how it all connectspinned scrub, insight panel
siriOne sentence becomes part of a long arcpinned scrub, week grid
privacyWhat you say stays yoursleft alone on purpose, motion undercuts trust copy
coachBuilt by a real coachanimate once
ctaOpening soonanimate once, form works without JS

Three pinned sections is the ceiling before the page feels like it is fighting the reader. If it reads as too much, how-it-works is the one to drop first.

Scroll strips, captured from the live page

morning scroll strip

morning, "It sees how it all connects", at 0 / 25 / 50 / 75 / 100 percent scroll.

siri scroll strip

siri, "One sentence becomes part of a long arc", same five positions.

how-it-works scroll strip

how-it-works, "Talk it through, or just tap", same five positions.

The product moments on their own

Insight panel. Signals connect into one read. Connection is earned, not asserted.
Week grid. Fills cell by cell. Two misses still lands as a good week.
Habit card forming. A spoken sentence resolving into structure.
Three coaching styles. One fact, three deliveries. Not three characters.

Also rendered as transparent WebM for overlay use: media/web/*.webm. The section transition exists as transparent WebM and as ProRes 4444 for Premiere (the ProRes is 34MB so it is kept local, not deployed).

Measured weight

FileRawGzipWho pays it
gg-anim.css4.9 kB1.9 kBeveryone
gg-anim.js5.9 kB2.1 kBeveryone
gg-player.js479 kB154 kBwide screens only

The Player is 97 percent of the weight, so it is loaded conditionally: skipped under 900px wide, skipped on prefers-reduced-motion, skipped on Save-Data. Phones get the 4 kB path and the sections still scrub through CSS. The orb loop is also skipped on Save-Data.

Verification 18 of 18 passed

Run with node scripts/verify-site.mjs against a real headless browser.

CheckResult
Scrub tracks scrolling down[0, 0.2496, 0.4996, 0.7496, 0.9996]
Scrub tracks scrolling back up[0.7496, 0.4996, 0.2496, 0]
Players attached and receiving seekTo3 of 3
Console and page errors0
Reduced motion: all sections in finished stateyes, pinning collapsed, orb video not injected
JavaScript disabled4 of 4 headlines present, signup surface intact

03Orb pipeline

A real port into React Three Fiber, not a wrapper: the lab is plain three.js with no package.json. Every value is a pure function of the frame, particle seeds come from a fixed PRNG, so frame N is identical on every render pass.

Hero loop. Transparent VP9, 150 frames = exactly one breath cycle, so it loops seamlessly.
Talking state. Driven by the real Cartesia clip, not a live mic. Unmute it.

Variant contact sheet

Eight variants, each at frames 0 / 6 / 12 / 18 / 24 across one breath cycle, because stills lie about motion. Rows top to bottom:

orb variant contact sheet
RowVariantRowVariant
1Locked preset5Sealed, gap 0.02
2Dense, 60k particles6Coach talking, amp 0.8
3Sparse, 12k particles7User talking, amp 0.8
4Wide gap 0.38Both halves off (idle grey)

Talking curves, ported back as data

The lab bug is that all 16 voice-routing amounts default to 0, so the envelope drives almost nothing. These are the values the orb should see, derived from real audio through the locked mapping, written as plain JSON so they can go back into the app.

FieldValue
Frames398 at 30fps, 13.28 s
Speaking / silent391 / 7
Peak amp1.0
Mappingem = 1.25 * (0.6 + amp*0.55), scale = 0.5 + amp*0.9, membrane = 0.7 + amp*0.8
Filetalking-curves.json

Source is coach_greeting.mp3, the only onboarding clip that exists as mp3. The other 27 declared clips are wav-only. Nothing was written back to the-source.

02Social clip factory

One parameterised composition, one config array, three aspect ratios, rendered in a single pass. 6 clips x 3 ratios = 18 files. Format is the locked one: mundane footage, no talking, one headline, message lives in the caption.

placeholder copy Every headline is marked [PLACEHOLDER] on purpose. No approved marketing, no claims, no numbers, no results.

commute, 9:16, 8 s, slow push
morning, 1:1, 9 s, drift up
reflection, 16:9, 10 s, drift left
gym, 9:16, 7 s, slow pull
week, 9:16, 8 s, slow push
carry, 1:1, 11 s, slow pull

Safe margins measured rather than eyeballed: on 9:16 the headline occupies y 0.129 to 0.258 of frame height, inside the 0.12 to 0.82 box. The bottom 18 percent is left clear because the platform UI covers it. All 18 files are in media/social/.

--Open items

ItemDetail
Orb look is not finalThe contact sheet exists so it can be picked by eye. The gap currently reads as a hard seam, and the particle field is on the speckled side.
Section colours flaggednewway uses orb-gold.png against the coach-blue / user-orange rule. The preset names that value userPrimary #dc9612, so it looks like naming rather than a wrong asset. Nothing was recoloured.
Live site has no reduced-motion handlinggrep -c prefers-reduced-motion returns 0 on the deployed page. The integration layer adds it. The gap on the live site is separate from this work.
Nothing merged backgg-website-mock was never written to. Merging is two tags plus the asset files, see site-integrated/DROP-IN.md.