All checks were successful
Deploy to Production / deploy (push) Successful in 1m6s
Ships the long-form (71.5 s) hero video to the marketing /flow section
along with the iteration trail of architectural visual fixes the owner
worked through over the last sprint.
## Video composition (remotion/)
Eight phases driven by the 71.47 s voice-over in `audio.mp3` plus the
`Sub-bass Lullaby.wav` background music (ducked to 0.16 with fade in /
fade out). Every scene was rebuilt for v10 with concrete fixes:
- **HookScene** (12 s) — adds FloatingChaos overlay: a docker-compose
excerpt, an oauth_callback.ts snippet, an .env file with a yellow
squiggle warning ("in git history since v0.3.1"), and a live-ticking
502 retry toast. Tangle now reads as a developer's desktop right
before they give up, not as four icons drifting.
- **PromptScene** (12.2 s) — 6.5 s post-typing dead-zone replaced with
the parse beat: three sequential highlights on the prompt text
(MCP server / searches / Notion workspace), three chips below the
input (intent / tool / secret → vault), three-stat summary panel
(tools · 2, secrets · 1, targets · 3). At local frame 250 (≈ 21 s
global, on the voice line "the prompt path and the secret path
never cross") a mini two-rail diagram with an explicit X-marker
ring lands, visualising the architectural promise the moment it's
spoken.
- **SecretsScene** (15.2 s) — kept the arrow-fork + AES-256 stamp +
env-var injection beats; added the lock-snap flash at frame 66,
pinned the vault at full opacity throughout, and added a dashed
vault → container connector so the secret's provenance is visible.
The "what the AI sees" panel is now 680 px wide with an eye icon,
four corner viewfinder brackets around the prompt text, and three
explicit denied lines (no secrets / no environment variables / no
tokens).
- **BuildScene** (7.2 s) — unchanged beats: streaming log, server
card emerges with code + 🔒 NOTION_API_KEY slot pills, isolated-
container caption, <60s countdown.
- **IsolationScene** (14 s) — completely restructured. Orbit-and-dock
chips that collided with the card and with the tokens-only badge
are replaced by a clean vertical chip column at x=760: read-only
filesystem · dropped capabilities · no new privileges · 512 MB
memory cap · 0.5 CPU limit · ✓ your token only (last in green).
A vault graphic now sits below the server card with a dashed arrow
up into its env slot so the architecture story is complete in one
frame. PKCE jargon removed: "OAuth 2.1 · PKCE" → "only your token
gets in" with a small "oauth 2.1 · proof-key flow" subtitle for
the curious. Handshake stages simplified to your client → verified
→ scoped token. Final settlement arrow in success-green curves
from the scoped-token pill back into the card.
- **LibraryScene** (7 s) — cards enlarged from 340×180 to 400×220
with 36 px gaps. The "templates carry code, not credentials"
sub-caption was pulled (felt on-the-nose; the detached lock and
empty NOTION_API_KEY=? slot carry the story visually).
- **DiscoveryScene** (3 s) — the most-iterated scene. Earlier
versions had a fake "1,200+ developers building" fork counter
(pulled — solo-founder, hadn't earned). Replaced with a two-lane
architecture diagram that visualises "no paths cross" literally:
top lane prompt → AI → code, bottom lane vault → encrypted →
env, both converging at the server box on the right. v10
refinements: all seven boxes visible from frame 0 (no late
server arrival), a parallel glow tour walks across both lanes
simultaneously, a dashed vertical divider with a "no shared
node" chip pinned in the middle, and the closing line "One
sentence in. Live server out." slides down from above and lands
centred while the diagram fades to 0.12 opacity behind it —
no overlap.
- **LogoLockup** (1.7 s) — wordmark + fade-to-black for a clean
loop seam.
The Subtitle / CAPTIONS layer added in v7 was pulled wholesale —
owner found the kinetic-typography overlay aggressive and noted
that technical terms (PKCE etc.) created friction with no payoff.
Scene visuals and voice now carry the whole story; the Subtitle
component file is retained for possible future use.
Render pipeline (`render:mp4` / `render:webm` / `render:poster` in
remotion/package.json) is unchanged. The MP4 is post-processed to
H.264 Main / yuv420p / TV-range with faststart + AAC audio. The
WebM is re-encoded at VP9 CRF 38 / Opus 64k to stay under the 3 MB
budget. Final artefacts in apps/web/public/videos/: 2.59 MB mp4,
2.99 MB webm, 62 KB poster.
## Web integration (apps/web/components/hero-video.tsx)
New client component wraps the <video> element and pins a frosted-
glass mute toggle bottom-right of the player. Why not native
`controls`: the browser chrome fights the section's design vocabulary
and we only need one affordance — unmute — so we render exactly
that. The toggle's icon flips between VolumeX (currently muted) and
Volume2 (currently unmuted), accent colour switches indigo when sound
is on. Initial state is muted so autoplay still fires; on unmute we
call .play() defensively because mobile Safari pauses on
muted-property changes mid-playback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
432 lines
12 KiB
TypeScript
432 lines
12 KiB
TypeScript
import { interpolate } from 'remotion';
|
||
import { C } from '../lib/colors';
|
||
import { springIn, softSpring, clampLerp } from '../lib/easings';
|
||
|
||
// Phase 2 (frames 159–261 global → localFrame 0..102): build log streams in
|
||
// line-by-line, then a server card emerges. Two pills slot into the card:
|
||
// • `code` arrives from the left (the LLM side)
|
||
// • `🔒 NOTION_API_KEY` arrives from the right (the vault side)
|
||
// This visualizes the architectural moment: code and credentials are
|
||
// injected at runtime from separate paths.
|
||
//
|
||
// Then a subtle caption appears below the card:
|
||
// "your isolated container · only you can reach it"
|
||
//
|
||
// Log lines stagger ~10 frames apart starting at localFrame 4.
|
||
// Server card emerges at localFrame ~58.
|
||
// Slot pills fly in at localFrame ~78.
|
||
|
||
const LOG_LINES = [
|
||
{ label: 'Generating spec', detail: '2 tools detected' },
|
||
{ label: 'Static checks', detail: 'passed' },
|
||
{ label: 'Building image', detail: '17.2s' },
|
||
{ label: 'Deploying', detail: 'live' },
|
||
];
|
||
|
||
const LINE_STAGGER = 18;
|
||
const LINE_START = 8;
|
||
const CARD_START = 110;
|
||
const SLOTS_START = 140;
|
||
const CAPTION_START = 160;
|
||
const SCENE_LEN = 216;
|
||
// Countdown displays "< NN s" in the corner while build is running.
|
||
// We tick from 58 → 12 between frames 8 → CARD_START.
|
||
const COUNTDOWN_START_VAL = 58;
|
||
const COUNTDOWN_END_VAL = 12;
|
||
|
||
export function BuildScene({ localFrame, fps }: { localFrame: number; fps: number }) {
|
||
const panelIn = springIn(localFrame, fps, 0);
|
||
const panelOpacity = clampLerp(localFrame, 0, 12);
|
||
|
||
// Card emerges late in phase.
|
||
const cardIn = softSpring(localFrame, fps, CARD_START, 24);
|
||
// Once the card is up, the log panel slides up to make room.
|
||
const panelShift = interpolate(cardIn, [0, 1], [0, -160]);
|
||
|
||
// Slot pills fly in after card is settled.
|
||
const codeSlotIn = softSpring(localFrame, fps, SLOTS_START, 18);
|
||
const secretSlotIn = softSpring(localFrame, fps, SLOTS_START + 4, 18);
|
||
|
||
// Caption appears last.
|
||
const captionIn = clampLerp(localFrame, CAPTION_START, CAPTION_START + 10);
|
||
|
||
// Countdown: ticks from 58 → 12 as the log lines stream.
|
||
const countT = clampLerp(localFrame, 8, CARD_START);
|
||
const countVal = Math.round(
|
||
interpolate(countT, [0, 1], [COUNTDOWN_START_VAL, COUNTDOWN_END_VAL]),
|
||
);
|
||
const countShown = localFrame >= 4 && localFrame <= CARD_START + 24;
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
position: 'absolute',
|
||
inset: 0,
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
}}
|
||
>
|
||
{/* Build log panel */}
|
||
<div
|
||
style={{
|
||
width: 720,
|
||
backgroundColor: C.bgElevated,
|
||
border: `1px solid ${C.border}`,
|
||
borderRadius: 14,
|
||
padding: '24px 28px',
|
||
boxShadow: '0 20px 60px rgba(0,0,0,0.5)',
|
||
opacity: panelOpacity,
|
||
transform: `translateY(${interpolate(panelIn, [0, 1], [20, 0]) + panelShift}px) scale(${interpolate(panelIn, [0, 1], [0.96, 1])})`,
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
gap: 14,
|
||
}}
|
||
>
|
||
{/* Panel header — tiny status row */}
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
paddingBottom: 12,
|
||
borderBottom: `1px solid ${C.border}`,
|
||
fontFamily: 'ui-monospace, SF Mono, Menlo, monospace',
|
||
fontSize: 14,
|
||
color: C.fgSubtle,
|
||
letterSpacing: 1,
|
||
textTransform: 'uppercase',
|
||
}}
|
||
>
|
||
<span>build · notion-search</span>
|
||
<span style={{ color: C.fgMuted, display: 'flex', alignItems: 'center', gap: 14 }}>
|
||
{countShown && (
|
||
<span style={{ color: C.accent, fontVariantNumeric: 'tabular-nums' }}>
|
||
{'< '}{countVal}{' s'}
|
||
</span>
|
||
)}
|
||
<span>● running</span>
|
||
</span>
|
||
</div>
|
||
|
||
{LOG_LINES.map((line, i) => (
|
||
<LogLine
|
||
key={i}
|
||
label={line.label}
|
||
detail={line.detail}
|
||
localFrame={localFrame}
|
||
startFrame={LINE_START + i * LINE_STAGGER}
|
||
fps={fps}
|
||
/>
|
||
))}
|
||
</div>
|
||
|
||
{/* Server card (emerges in second half) */}
|
||
{cardIn > 0.01 && (
|
||
<ServerCard
|
||
progress={cardIn}
|
||
localFrame={localFrame}
|
||
codeSlotIn={codeSlotIn}
|
||
secretSlotIn={secretSlotIn}
|
||
/>
|
||
)}
|
||
|
||
{/* Isolated container caption */}
|
||
{captionIn > 0.01 && (
|
||
<div
|
||
style={{
|
||
position: 'absolute',
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 180,
|
||
textAlign: 'center',
|
||
fontFamily: 'ui-monospace, SF Mono, Menlo, monospace',
|
||
fontSize: 13,
|
||
letterSpacing: 3,
|
||
textTransform: 'uppercase',
|
||
color: C.fgSubtle,
|
||
opacity: captionIn,
|
||
transform: `translateY(${interpolate(captionIn, [0, 1], [6, 0])}px)`,
|
||
}}
|
||
>
|
||
your isolated container · only you can reach it
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function LogLine({
|
||
label,
|
||
detail,
|
||
localFrame,
|
||
startFrame,
|
||
fps,
|
||
}: {
|
||
label: string;
|
||
detail: string;
|
||
localFrame: number;
|
||
startFrame: number;
|
||
fps: number;
|
||
}) {
|
||
const spring = springIn(localFrame, fps, startFrame);
|
||
const opacity = clampLerp(localFrame, startFrame, startFrame + 10);
|
||
const x = interpolate(spring, [0, 1], [-30, 0]);
|
||
// Check fills in slightly after the line slides in.
|
||
const checkFill = clampLerp(localFrame, startFrame + 6, startFrame + 14);
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 14,
|
||
opacity,
|
||
transform: `translateX(${x}px)`,
|
||
fontFamily: 'ui-monospace, SF Mono, Menlo, monospace',
|
||
fontSize: 18,
|
||
}}
|
||
>
|
||
{/* Checkmark circle */}
|
||
<div
|
||
style={{
|
||
width: 22,
|
||
height: 22,
|
||
borderRadius: 11,
|
||
backgroundColor: `rgba(34, 197, 94, ${checkFill * 0.18})`,
|
||
border: `1.5px solid ${C.success}`,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
flexShrink: 0,
|
||
}}
|
||
>
|
||
<svg width="12" height="12" viewBox="0 0 12 12">
|
||
<path
|
||
d="M 2.5 6.5 L 5 9 L 9.5 3.5"
|
||
fill="none"
|
||
stroke={C.success}
|
||
strokeWidth={2}
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeDasharray={12}
|
||
strokeDashoffset={(1 - checkFill) * 12}
|
||
/>
|
||
</svg>
|
||
</div>
|
||
|
||
<span style={{ color: C.fg, minWidth: 220 }}>{label}</span>
|
||
<span style={{ color: C.fgMuted, flex: 1 }}>{detail}</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function ServerCard({
|
||
progress,
|
||
localFrame,
|
||
codeSlotIn,
|
||
secretSlotIn,
|
||
}: {
|
||
progress: number;
|
||
localFrame: number;
|
||
codeSlotIn: number;
|
||
secretSlotIn: number;
|
||
}) {
|
||
const scale = interpolate(progress, [0, 1], [0.85, 1]);
|
||
const y = interpolate(progress, [0, 1], [40, 180]);
|
||
// Live dot pulses once the slots have arrived.
|
||
const liveOn = secretSlotIn > 0.6;
|
||
const pulsePhase = (localFrame - (SLOTS_START + 18)) / 30;
|
||
const livePulse = liveOn
|
||
? 0.6 + 0.4 * Math.sin(pulsePhase * Math.PI * 2)
|
||
: 0;
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
position: 'absolute',
|
||
left: '50%',
|
||
top: '50%',
|
||
transform: `translate(-50%, calc(-50% + ${y}px)) scale(${scale})`,
|
||
opacity: progress,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
width: 540,
|
||
backgroundColor: C.bgElevated,
|
||
border: `1.5px solid ${C.accent}`,
|
||
borderRadius: 16,
|
||
padding: '24px 28px',
|
||
boxShadow: `0 0 0 5px ${C.accentGlow}, 0 24px 70px rgba(0,0,0,0.6)`,
|
||
position: 'relative',
|
||
}}
|
||
>
|
||
{/* Header row: title + live dot */}
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
marginBottom: 18,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
fontFamily: 'ui-monospace, SF Mono, Menlo, monospace',
|
||
fontSize: 20,
|
||
color: C.fg,
|
||
letterSpacing: 0.2,
|
||
}}
|
||
>
|
||
notion-search
|
||
</div>
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 8,
|
||
fontFamily: 'ui-monospace, SF Mono, Menlo, monospace',
|
||
fontSize: 12,
|
||
color: liveOn ? C.success : C.fgSubtle,
|
||
letterSpacing: 1.5,
|
||
textTransform: 'uppercase',
|
||
opacity: 0.4 + 0.6 * Math.min(1, secretSlotIn),
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
width: 8,
|
||
height: 8,
|
||
borderRadius: 4,
|
||
backgroundColor: liveOn ? C.success : C.fgSubtle,
|
||
boxShadow: liveOn ? `0 0 ${10 * livePulse}px ${C.success}` : 'none',
|
||
opacity: liveOn ? 0.5 + 0.5 * livePulse : 0.5,
|
||
}}
|
||
/>
|
||
{liveOn ? 'live' : 'starting'}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Slot pills row */}
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
gap: 12,
|
||
marginBottom: 18,
|
||
position: 'relative',
|
||
minHeight: 38,
|
||
}}
|
||
>
|
||
{/* code slot — arrives from the left */}
|
||
<SlotPill
|
||
label="code"
|
||
kind="code"
|
||
in={codeSlotIn}
|
||
fromX={-200}
|
||
/>
|
||
{/* secret slot — arrives from the right */}
|
||
<SlotPill
|
||
label="NOTION_API_KEY"
|
||
kind="secret"
|
||
in={secretSlotIn}
|
||
fromX={200}
|
||
/>
|
||
</div>
|
||
|
||
{/* Tool rows */}
|
||
<ToolRow name="search_pages" desc="full-text query" />
|
||
<div style={{ height: 8 }} />
|
||
<ToolRow name="get_page_content" desc="fetch by id" />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function SlotPill({
|
||
label,
|
||
kind,
|
||
in: progress,
|
||
fromX,
|
||
}: {
|
||
label: string;
|
||
kind: 'code' | 'secret';
|
||
in: number;
|
||
fromX: number;
|
||
}) {
|
||
const x = interpolate(progress, [0, 1], [fromX, 0]);
|
||
const opacity = clampLerp(progress, 0.05, 0.6);
|
||
const isSecret = kind === 'secret';
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 8,
|
||
padding: '8px 14px',
|
||
backgroundColor: isSecret ? 'rgba(99,102,241,0.10)' : C.bgSubtle,
|
||
border: `1px solid ${isSecret ? C.accentDim : C.borderStrong}`,
|
||
borderRadius: 999,
|
||
fontFamily: 'ui-monospace, SF Mono, Menlo, monospace',
|
||
fontSize: 13,
|
||
color: isSecret ? C.accent : C.fg,
|
||
letterSpacing: 0.5,
|
||
opacity,
|
||
transform: `translateX(${x}px)`,
|
||
}}
|
||
>
|
||
{isSecret ? (
|
||
<MiniLockIcon />
|
||
) : (
|
||
<span
|
||
style={{
|
||
display: 'inline-flex',
|
||
width: 14,
|
||
height: 14,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
color: C.fgMuted,
|
||
fontSize: 13,
|
||
}}
|
||
>
|
||
{'<>'}
|
||
</span>
|
||
)}
|
||
<span>{label}</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function MiniLockIcon() {
|
||
return (
|
||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none">
|
||
<path d="M 5 8 L 5 6 A 3 3 0 0 1 11 6 L 11 8" stroke={C.accent} strokeWidth={1.4} fill="none" />
|
||
<rect x="3.5" y="7.5" width="9" height="7" rx="1.5" fill="rgba(99,102,241,0.20)" stroke={C.accent} strokeWidth={1.4} />
|
||
<circle cx="8" cy="11" r="1" fill={C.accent} />
|
||
</svg>
|
||
);
|
||
}
|
||
|
||
function ToolRow({ name, desc }: { name: string; desc: string }) {
|
||
return (
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 12,
|
||
padding: '10px 14px',
|
||
backgroundColor: C.bgSubtle,
|
||
border: `1px solid ${C.border}`,
|
||
borderRadius: 10,
|
||
fontFamily: 'ui-monospace, SF Mono, Menlo, monospace',
|
||
}}
|
||
>
|
||
<div style={{ width: 6, height: 6, borderRadius: 3, backgroundColor: C.accent }} />
|
||
<span style={{ color: C.fg, fontSize: 16, minWidth: 200 }}>{name}</span>
|
||
<span style={{ color: C.fgSubtle, fontSize: 14, flex: 1 }}>{desc}</span>
|
||
</div>
|
||
);
|
||
}
|