← back to Prestige Car Wash Video
Narrated story video: Steve-voice VO, all 8 options w/ real clips, FCFS/6-lines, live lot cam, tip cards + admin act; parallel segmented render
d99d4955d1b33c96bc3533f69ef9fbb0b2f57811 · 2026-07-12 08:56:51 -0700 · Steve Abrams
Files touched
A narration/audio/admin.wavA narration/audio/intro.wavA narration/audio/livecam.wavA narration/audio/model.wavA narration/audio/opt-ceramic-coating.wavA narration/audio/opt-complete-detail.wavA narration/audio/opt-express-wash.wavA narration/audio/opt-full-service.wavA narration/audio/opt-hand-wash.wavA narration/audio/opt-headlight.wavA narration/audio/opt-interior-detail.wavA narration/audio/opt-wax-seal.wavA narration/audio/options.wavA narration/audio/outro.wavA narration/audio/tips.wavA narration/lines.jsonA public/audio/admin.wavA public/audio/intro.wavA public/audio/livecam.wavA public/audio/model.wavA public/audio/opt-ceramic-coating.wavA public/audio/opt-complete-detail.wavA public/audio/opt-express-wash.wavA public/audio/opt-full-service.wavA public/audio/opt-hand-wash.wavA public/audio/opt-headlight.wavA public/audio/opt-interior-detail.wavA public/audio/opt-wax-seal.wavA public/audio/options.wavA public/audio/outro.wavA public/audio/tips.wavA public/cards/andre.pngA public/cards/diego.pngA public/cards/kevin.pngA public/cards/luis.pngA public/cards/marco.pngA public/cards/qr-marco.pngA public/cards/sam.pngM src/Root.tsxA src/StoryVideo.tsxA src/durations.jsonA src/scenes/CamScene.tsxA src/scenes/FcfsScene.tsxA src/scenes/OptionScene.tsxA src/scenes/TipsScene.tsxA src/story.ts
Diff
commit d99d4955d1b33c96bc3533f69ef9fbb0b2f57811
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Jul 12 08:56:51 2026 -0700
Narrated story video: Steve-voice VO, all 8 options w/ real clips, FCFS/6-lines, live lot cam, tip cards + admin act; parallel segmented render
---
narration/audio/admin.wav | Bin 0 -> 614478 bytes
narration/audio/intro.wav | Bin 0 -> 417870 bytes
narration/audio/livecam.wav | Bin 0 -> 647246 bytes
narration/audio/model.wav | Bin 0 -> 1015886 bytes
narration/audio/opt-ceramic-coating.wav | Bin 0 -> 856142 bytes
narration/audio/opt-complete-detail.wav | Bin 0 -> 716878 bytes
narration/audio/opt-express-wash.wav | Bin 0 -> 593998 bytes
narration/audio/opt-full-service.wav | Bin 0 -> 733262 bytes
narration/audio/opt-hand-wash.wav | Bin 0 -> 770126 bytes
narration/audio/opt-headlight.wav | Bin 0 -> 680014 bytes
narration/audio/opt-interior-detail.wav | Bin 0 -> 1003598 bytes
narration/audio/opt-wax-seal.wav | Bin 0 -> 782414 bytes
narration/audio/options.wav | Bin 0 -> 262222 bytes
narration/audio/outro.wav | Bin 0 -> 319566 bytes
narration/audio/tips.wav | Bin 0 -> 852046 bytes
narration/lines.json | 19 +++++++
public/audio/admin.wav | Bin 0 -> 614478 bytes
public/audio/intro.wav | Bin 0 -> 417870 bytes
public/audio/livecam.wav | Bin 0 -> 647246 bytes
public/audio/model.wav | Bin 0 -> 1015886 bytes
public/audio/opt-ceramic-coating.wav | Bin 0 -> 856142 bytes
public/audio/opt-complete-detail.wav | Bin 0 -> 716878 bytes
public/audio/opt-express-wash.wav | Bin 0 -> 593998 bytes
public/audio/opt-full-service.wav | Bin 0 -> 733262 bytes
public/audio/opt-hand-wash.wav | Bin 0 -> 770126 bytes
public/audio/opt-headlight.wav | Bin 0 -> 680014 bytes
public/audio/opt-interior-detail.wav | Bin 0 -> 1003598 bytes
public/audio/opt-wax-seal.wav | Bin 0 -> 782414 bytes
public/audio/options.wav | Bin 0 -> 262222 bytes
public/audio/outro.wav | Bin 0 -> 319566 bytes
public/audio/tips.wav | Bin 0 -> 852046 bytes
public/cards/andre.png | Bin 0 -> 1821942 bytes
public/cards/diego.png | Bin 0 -> 1857474 bytes
public/cards/kevin.png | Bin 0 -> 1841676 bytes
public/cards/luis.png | Bin 0 -> 1861193 bytes
public/cards/marco.png | Bin 0 -> 1848876 bytes
public/cards/qr-marco.png | Bin 0 -> 884 bytes
public/cards/sam.png | Bin 0 -> 1825139 bytes
src/Root.tsx | 28 ++++++++---
src/StoryVideo.tsx | 72 +++++++++++++++++++++++++++
src/durations.json | 17 +++++++
src/scenes/CamScene.tsx | 52 +++++++++++++++++++
src/scenes/FcfsScene.tsx | 63 +++++++++++++++++++++++
src/scenes/OptionScene.tsx | 42 ++++++++++++++++
src/scenes/TipsScene.tsx | 50 +++++++++++++++++++
src/story.ts | 85 ++++++++++++++++++++++++++++++++
46 files changed, 420 insertions(+), 8 deletions(-)
diff --git a/narration/audio/admin.wav b/narration/audio/admin.wav
new file mode 100644
index 0000000..0a84569
Binary files /dev/null and b/narration/audio/admin.wav differ
diff --git a/narration/audio/intro.wav b/narration/audio/intro.wav
new file mode 100644
index 0000000..ff0ddf0
Binary files /dev/null and b/narration/audio/intro.wav differ
diff --git a/narration/audio/livecam.wav b/narration/audio/livecam.wav
new file mode 100644
index 0000000..4096b1f
Binary files /dev/null and b/narration/audio/livecam.wav differ
diff --git a/narration/audio/model.wav b/narration/audio/model.wav
new file mode 100644
index 0000000..c5e0efb
Binary files /dev/null and b/narration/audio/model.wav differ
diff --git a/narration/audio/opt-ceramic-coating.wav b/narration/audio/opt-ceramic-coating.wav
new file mode 100644
index 0000000..c0fc0db
Binary files /dev/null and b/narration/audio/opt-ceramic-coating.wav differ
diff --git a/narration/audio/opt-complete-detail.wav b/narration/audio/opt-complete-detail.wav
new file mode 100644
index 0000000..9520cf1
Binary files /dev/null and b/narration/audio/opt-complete-detail.wav differ
diff --git a/narration/audio/opt-express-wash.wav b/narration/audio/opt-express-wash.wav
new file mode 100644
index 0000000..0fb63d6
Binary files /dev/null and b/narration/audio/opt-express-wash.wav differ
diff --git a/narration/audio/opt-full-service.wav b/narration/audio/opt-full-service.wav
new file mode 100644
index 0000000..36502d2
Binary files /dev/null and b/narration/audio/opt-full-service.wav differ
diff --git a/narration/audio/opt-hand-wash.wav b/narration/audio/opt-hand-wash.wav
new file mode 100644
index 0000000..cb8cd61
Binary files /dev/null and b/narration/audio/opt-hand-wash.wav differ
diff --git a/narration/audio/opt-headlight.wav b/narration/audio/opt-headlight.wav
new file mode 100644
index 0000000..e2a986e
Binary files /dev/null and b/narration/audio/opt-headlight.wav differ
diff --git a/narration/audio/opt-interior-detail.wav b/narration/audio/opt-interior-detail.wav
new file mode 100644
index 0000000..9b73bf5
Binary files /dev/null and b/narration/audio/opt-interior-detail.wav differ
diff --git a/narration/audio/opt-wax-seal.wav b/narration/audio/opt-wax-seal.wav
new file mode 100644
index 0000000..0c69a25
Binary files /dev/null and b/narration/audio/opt-wax-seal.wav differ
diff --git a/narration/audio/options.wav b/narration/audio/options.wav
new file mode 100644
index 0000000..79889f1
Binary files /dev/null and b/narration/audio/options.wav differ
diff --git a/narration/audio/outro.wav b/narration/audio/outro.wav
new file mode 100644
index 0000000..ea118de
Binary files /dev/null and b/narration/audio/outro.wav differ
diff --git a/narration/audio/tips.wav b/narration/audio/tips.wav
new file mode 100644
index 0000000..d03ea3d
Binary files /dev/null and b/narration/audio/tips.wav differ
diff --git a/narration/lines.json b/narration/lines.json
new file mode 100644
index 0000000..8d92eed
--- /dev/null
+++ b/narration/lines.json
@@ -0,0 +1,19 @@
+{
+ "segments": [
+ { "id": "intro", "text": "Welcome to Prestige Car Wash, in the heart of the San Fernando Valley. Come take the tour." },
+ { "id": "model", "text": "Here's the first thing to know. We don't take appointments. It's first come, first served. With a big crew and six full lines running, you just pull up and you're moving. No booking a slot, no waiting around. You drive in, and we take it from there." },
+ { "id": "options", "text": "Now let's walk through everything we offer. Take your time." },
+ { "id": "opt-express-wash", "text": "The Express Exterior Wash. Just nineteen dollars. A foam bath, a spot-free rinse, and a hand dry. In and out, with no upsell pressure." },
+ { "id": "opt-full-service", "text": "The Full-Service Wash. Thirty-nine dollars. Exterior hand wash, interior vacuum, dash wipe, windows, and tire dressing. Every seat taken care of." },
+ { "id": "opt-hand-wash", "text": "The Premium Hand Wash. Seventy-nine dollars. A two-bucket, foam-cannon hand wash. No tunnel, no swirl marks. Safe for ceramic, matte, and wraps." },
+ { "id": "opt-wax-seal", "text": "Wax and Ceramic Seal. Forty-nine dollars. A triple-foam polish, plus a hydrophobic ceramic seal that shields your clear coat from the sun and the road." },
+ { "id": "opt-interior-detail", "text": "Interior Express Detail. A hundred and twenty. A deep clean inside — seats, carpets, dash, vents, and door jambs, with pet-hair extraction and shampoo." },
+ { "id": "opt-complete-detail", "text": "The Complete Detail. Four ninety-nine. A full inside-and-out restoration — wash, clay bar, polish, wax, interior shampoo, even the engine bay." },
+ { "id": "opt-ceramic-coating", "text": "Ceramic Coating. From nine ninety-nine. Iron decontamination, clay bar, a one-step polish, and a nine-H nano-ceramic coating — one to three years of protection." },
+ { "id": "opt-headlight", "text": "And Headlight Restoration. Eighty-nine dollars. We sand, polish, and seal foggy, yellowed headlights back to clear, for better night visibility." },
+ { "id": "livecam", "text": "Want to see how busy we are before you drive over? Our live lot cam shows all six lines in real time. See a short wait? Come on down." },
+ { "id": "tips", "text": "And our crew works hard for you. Every team member has their own tip card. Just scan the code and tip them directly — a hundred percent goes to the person who took care of your car." },
+ { "id": "admin", "text": "And behind the scenes, we run a growth center that keeps the business sharp — competitors, campaigns, ads, and leads, all in one place." },
+ { "id": "outro", "text": "That's Prestige Car Wash. First come, first served. Come see us." }
+ ]
+}
diff --git a/public/audio/admin.wav b/public/audio/admin.wav
new file mode 100644
index 0000000..0a84569
Binary files /dev/null and b/public/audio/admin.wav differ
diff --git a/public/audio/intro.wav b/public/audio/intro.wav
new file mode 100644
index 0000000..ff0ddf0
Binary files /dev/null and b/public/audio/intro.wav differ
diff --git a/public/audio/livecam.wav b/public/audio/livecam.wav
new file mode 100644
index 0000000..4096b1f
Binary files /dev/null and b/public/audio/livecam.wav differ
diff --git a/public/audio/model.wav b/public/audio/model.wav
new file mode 100644
index 0000000..c5e0efb
Binary files /dev/null and b/public/audio/model.wav differ
diff --git a/public/audio/opt-ceramic-coating.wav b/public/audio/opt-ceramic-coating.wav
new file mode 100644
index 0000000..c0fc0db
Binary files /dev/null and b/public/audio/opt-ceramic-coating.wav differ
diff --git a/public/audio/opt-complete-detail.wav b/public/audio/opt-complete-detail.wav
new file mode 100644
index 0000000..9520cf1
Binary files /dev/null and b/public/audio/opt-complete-detail.wav differ
diff --git a/public/audio/opt-express-wash.wav b/public/audio/opt-express-wash.wav
new file mode 100644
index 0000000..0fb63d6
Binary files /dev/null and b/public/audio/opt-express-wash.wav differ
diff --git a/public/audio/opt-full-service.wav b/public/audio/opt-full-service.wav
new file mode 100644
index 0000000..36502d2
Binary files /dev/null and b/public/audio/opt-full-service.wav differ
diff --git a/public/audio/opt-hand-wash.wav b/public/audio/opt-hand-wash.wav
new file mode 100644
index 0000000..cb8cd61
Binary files /dev/null and b/public/audio/opt-hand-wash.wav differ
diff --git a/public/audio/opt-headlight.wav b/public/audio/opt-headlight.wav
new file mode 100644
index 0000000..e2a986e
Binary files /dev/null and b/public/audio/opt-headlight.wav differ
diff --git a/public/audio/opt-interior-detail.wav b/public/audio/opt-interior-detail.wav
new file mode 100644
index 0000000..9b73bf5
Binary files /dev/null and b/public/audio/opt-interior-detail.wav differ
diff --git a/public/audio/opt-wax-seal.wav b/public/audio/opt-wax-seal.wav
new file mode 100644
index 0000000..0c69a25
Binary files /dev/null and b/public/audio/opt-wax-seal.wav differ
diff --git a/public/audio/options.wav b/public/audio/options.wav
new file mode 100644
index 0000000..79889f1
Binary files /dev/null and b/public/audio/options.wav differ
diff --git a/public/audio/outro.wav b/public/audio/outro.wav
new file mode 100644
index 0000000..ea118de
Binary files /dev/null and b/public/audio/outro.wav differ
diff --git a/public/audio/tips.wav b/public/audio/tips.wav
new file mode 100644
index 0000000..d03ea3d
Binary files /dev/null and b/public/audio/tips.wav differ
diff --git a/public/cards/andre.png b/public/cards/andre.png
new file mode 100644
index 0000000..3c760b3
Binary files /dev/null and b/public/cards/andre.png differ
diff --git a/public/cards/diego.png b/public/cards/diego.png
new file mode 100644
index 0000000..97135b8
Binary files /dev/null and b/public/cards/diego.png differ
diff --git a/public/cards/kevin.png b/public/cards/kevin.png
new file mode 100644
index 0000000..4c90965
Binary files /dev/null and b/public/cards/kevin.png differ
diff --git a/public/cards/luis.png b/public/cards/luis.png
new file mode 100644
index 0000000..fd0f9fd
Binary files /dev/null and b/public/cards/luis.png differ
diff --git a/public/cards/marco.png b/public/cards/marco.png
new file mode 100644
index 0000000..ddfc442
Binary files /dev/null and b/public/cards/marco.png differ
diff --git a/public/cards/qr-marco.png b/public/cards/qr-marco.png
new file mode 100644
index 0000000..cfbf345
Binary files /dev/null and b/public/cards/qr-marco.png differ
diff --git a/public/cards/sam.png b/public/cards/sam.png
new file mode 100644
index 0000000..09d5349
Binary files /dev/null and b/public/cards/sam.png differ
diff --git a/src/Root.tsx b/src/Root.tsx
index f7f1281..ffc4935 100644
--- a/src/Root.tsx
+++ b/src/Root.tsx
@@ -1,16 +1,28 @@
import { Composition } from 'remotion';
import { PrestigeShowcase } from './Video';
+import { StoryVideo } from './StoryVideo';
import { FPS, WIDTH, HEIGHT, TOTAL_FRAMES } from './sections';
+import { TOTAL as STORY_TOTAL } from './story';
export const RemotionRoot: React.FC = () => {
return (
- <Composition
- id="PrestigeShowcase"
- component={PrestigeShowcase}
- durationInFrames={TOTAL_FRAMES}
- fps={FPS}
- width={WIDTH}
- height={HEIGHT}
- />
+ <>
+ <Composition
+ id="PrestigeStory"
+ component={StoryVideo}
+ durationInFrames={STORY_TOTAL}
+ fps={FPS}
+ width={WIDTH}
+ height={HEIGHT}
+ />
+ <Composition
+ id="PrestigeShowcase"
+ component={PrestigeShowcase}
+ durationInFrames={TOTAL_FRAMES}
+ fps={FPS}
+ width={WIDTH}
+ height={HEIGHT}
+ />
+ </>
);
};
diff --git a/src/StoryVideo.tsx b/src/StoryVideo.tsx
new file mode 100644
index 0000000..4ddd3e8
--- /dev/null
+++ b/src/StoryVideo.tsx
@@ -0,0 +1,72 @@
+import { AbsoluteFill, Sequence, Audio, staticFile, useCurrentFrame, useVideoConfig, interpolate } from 'remotion';
+import { SCENES } from './story';
+import { Background } from './Background';
+import { BrowserShot } from './BrowserShot';
+import { Caption } from './Caption';
+import { Card } from './Card';
+import { OptionScene } from './scenes/OptionScene';
+import { FcfsScene } from './scenes/FcfsScene';
+import { CamScene } from './scenes/CamScene';
+import { TipsScene } from './scenes/TipsScene';
+
+const FONT = 'Inter, "SF Pro Display", system-ui, -apple-system, sans-serif';
+
+const FadeWrap: React.FC<{ dur: number; children: React.ReactNode }> = ({ dur, children }) => {
+ const f = useCurrentFrame();
+ const o = interpolate(f, [0, 10, dur - 9, dur], [0, 1, 1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
+ return <AbsoluteFill style={{ opacity: o }}>{children}</AbsoluteFill>;
+};
+
+export const StoryVideo: React.FC = () => {
+ const frame = useCurrentFrame();
+ const { durationInFrames } = useVideoConfig();
+ const offsets: number[] = [];
+ let acc = 0;
+ for (const s of SCENES) { offsets.push(acc); acc += s.frames; }
+ let active = 0;
+ for (let i = 0; i < SCENES.length; i++) if (frame >= offsets[i]) active = i;
+ const accent = SCENES[active].accent;
+
+ return (
+ <AbsoluteFill style={{ backgroundColor: '#05090f' }}>
+ <Background accent={accent} />
+
+ {SCENES.map((s, i) => (
+ <Sequence key={s.id} from={offsets[i]} durationInFrames={s.frames} name={s.title || s.name || s.id}>
+ <FadeWrap dur={s.frames}>
+ {s.type === 'clip' && <OptionScene s={s} />}
+ {s.type === 'fcfs' && <FcfsScene s={s} />}
+ {s.type === 'cam' && <CamScene s={s} />}
+ {s.type === 'tips' && <TipsScene s={s} />}
+ {s.type === 'shot' && (
+ <>
+ <BrowserShot s={s as any} />
+ <Caption s={s as any} index={i} total={SCENES.length} />
+ </>
+ )}
+ {s.type === 'card' && (
+ <>
+ <Card s={s as any} />
+ {s.narr && <Audio src={staticFile(`audio/${s.narr}.wav`)} />}
+ </>
+ )}
+ </FadeWrap>
+ </Sequence>
+ ))}
+
+ {/* progress bar */}
+ <div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 6, background: 'rgba(255,255,255,0.08)' }}>
+ <div style={{ height: 6, width: `${(frame / durationInFrames) * 100}%`, background: accent, boxShadow: `0 0 16px ${accent}` }} />
+ </div>
+
+ {/* persistent wordmark */}
+ <div style={{ position: 'absolute', top: 30, left: 54, display: 'flex', alignItems: 'center', gap: 12,
+ fontFamily: FONT, fontWeight: 800, fontSize: 24, color: '#e8f1ff', letterSpacing: 0.5,
+ textShadow: '0 2px 12px rgba(0,0,0,0.6)' }}>
+ <span style={{ display: 'inline-flex', width: 34, height: 34, borderRadius: 9,
+ background: 'linear-gradient(135deg,#38bdf8,#2563eb)', alignItems: 'center', justifyContent: 'center', fontSize: 18 }}>🚗</span>
+ Prestige<span style={{ opacity: 0.5, fontWeight: 600 }}>Car Wash</span>
+ </div>
+ </AbsoluteFill>
+ );
+};
diff --git a/src/durations.json b/src/durations.json
new file mode 100644
index 0000000..f9640da
--- /dev/null
+++ b/src/durations.json
@@ -0,0 +1,17 @@
+{
+ "admin": 6.966,
+ "intro": 4.737,
+ "livecam": 7.338,
+ "model": 11.517,
+ "opt-ceramic-coating": 9.706,
+ "opt-complete-detail": 8.127,
+ "opt-express-wash": 6.734,
+ "opt-full-service": 8.313,
+ "opt-hand-wash": 8.731,
+ "opt-headlight": 7.709,
+ "opt-interior-detail": 11.378,
+ "opt-wax-seal": 8.87,
+ "options": 2.972,
+ "outro": 3.622,
+ "tips": 9.66
+}
\ No newline at end of file
diff --git a/src/scenes/CamScene.tsx b/src/scenes/CamScene.tsx
new file mode 100644
index 0000000..cfa4824
--- /dev/null
+++ b/src/scenes/CamScene.tsx
@@ -0,0 +1,52 @@
+import { AbsoluteFill, OffthreadVideo, Audio, staticFile, useCurrentFrame, interpolate } from 'remotion';
+import type { Scene } from '../story';
+
+const FONT = 'Inter, "SF Pro Display", system-ui, -apple-system, sans-serif';
+const MONO = '"SF Mono", ui-monospace, Menlo, monospace';
+
+export const CamScene: React.FC<{ s: Scene }> = ({ s }) => {
+ const f = useCurrentFrame();
+ const blink = Math.floor(f / 15) % 2 === 0;
+ const secs = 8 + Math.floor(f / 30); // fake ticking clock
+ const clock = `10:${String(24 + Math.floor(secs / 60)).padStart(2, '0')}:${String(secs % 60).padStart(2, '0')} AM`;
+ const inO = interpolate(f, [0, 14], [0, 1], { extrapolateRight: 'clamp' });
+
+ return (
+ <AbsoluteFill style={{ backgroundColor: '#05090f', alignItems: 'center', justifyContent: 'center' }}>
+ {s.narr && <Audio src={staticFile(`audio/${s.narr}.wav`)} />}
+ {/* webcam bezel */}
+ <div style={{ width: 1580, height: 890, borderRadius: 18, overflow: 'hidden', position: 'relative',
+ border: '3px solid #1b2b44', boxShadow: '0 40px 120px rgba(0,0,0,0.6)', opacity: inO }}>
+ <OffthreadVideo src={staticFile('media/real-lot-tour.mp4')} muted loop
+ style={{ width: '100%', height: '100%', objectFit: 'cover', filter: 'saturate(1.05) contrast(1.03)' }} />
+ {/* subtle scanlines */}
+ <AbsoluteFill style={{ background: 'repeating-linear-gradient(0deg, rgba(0,0,0,0.10) 0px, rgba(0,0,0,0.10) 1px, transparent 2px, transparent 4px)' }} />
+ <AbsoluteFill style={{ background: 'radial-gradient(120% 120% at 50% 50%, transparent 60%, rgba(0,0,0,0.4) 100%)' }} />
+
+ {/* LIVE badge */}
+ <div style={{ position: 'absolute', top: 24, left: 26, display: 'flex', alignItems: 'center', gap: 12,
+ background: 'rgba(4,8,15,0.6)', backdropFilter: 'blur(4px)', padding: '10px 18px', borderRadius: 10 }}>
+ <span style={{ width: 16, height: 16, borderRadius: 16, background: blink ? '#ff3b3b' : '#7a1414', boxShadow: blink ? '0 0 18px #ff3b3b' : 'none' }} />
+ <span style={{ fontFamily: FONT, fontWeight: 900, fontSize: 26, color: '#fff', letterSpacing: 3 }}>LIVE</span>
+ </div>
+ {/* label + clock */}
+ <div style={{ position: 'absolute', top: 26, right: 28, fontFamily: MONO, fontSize: 26, color: '#dbe7f6', textShadow: '0 2px 8px #000' }}>
+ {clock}
+ </div>
+ <div style={{ position: 'absolute', bottom: 26, left: 28, fontFamily: FONT, fontWeight: 800, fontSize: 30, color: '#fff', textShadow: '0 2px 12px #000' }}>
+ 🚗 Prestige Car Wash — Lot Cam
+ </div>
+ {/* lines-open indicator */}
+ <div style={{ position: 'absolute', bottom: 26, right: 28, display: 'flex', alignItems: 'center', gap: 12,
+ background: 'rgba(4,8,15,0.6)', backdropFilter: 'blur(4px)', padding: '10px 18px', borderRadius: 10,
+ fontFamily: FONT, fontWeight: 800, fontSize: 26, color: s.accent }}>
+ <span style={{ width: 11, height: 11, borderRadius: 11, background: s.accent, boxShadow: `0 0 14px ${s.accent}` }} />
+ 6 LINES OPEN · SHORT WAIT
+ </div>
+ </div>
+ <div style={{ marginTop: 30, fontFamily: FONT, fontWeight: 700, fontSize: 34, color: '#c2d2e8', opacity: inO }}>
+ See the wait before you drive over.
+ </div>
+ </AbsoluteFill>
+ );
+};
diff --git a/src/scenes/FcfsScene.tsx b/src/scenes/FcfsScene.tsx
new file mode 100644
index 0000000..c1641d9
--- /dev/null
+++ b/src/scenes/FcfsScene.tsx
@@ -0,0 +1,63 @@
+import { AbsoluteFill, Audio, staticFile, useCurrentFrame, interpolate, Easing, useVideoConfig, spring } from 'remotion';
+import type { Scene } from '../story';
+
+const FONT = 'Inter, "SF Pro Display", system-ui, -apple-system, sans-serif';
+
+const Lane: React.FC<{ i: number; total: number; accent: string; frames: number }> = ({ i, total, frames, accent }) => {
+ const f = useCurrentFrame();
+ const start = 20 + i * 5;
+ // a car glides up each of the 6 lanes, staggered, then loops
+ const t = ((f - start) % 70) / 70;
+ const carY = interpolate(t < 0 ? 0 : t, [0, 1], [560, -80]);
+ const reveal = interpolate(f, [10 + i * 4, 30 + i * 4], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
+ return (
+ <div style={{ position: 'relative', width: 150, height: 560, borderRadius: 16, overflow: 'hidden',
+ background: 'linear-gradient(180deg,#0f1c30,#0a1220)', border: '1px solid rgba(255,255,255,0.06)', opacity: reveal }}>
+ {/* lane dashes */}
+ {[...Array(6)].map((_, k) => (
+ <div key={k} style={{ position: 'absolute', left: '50%', top: 20 + k * 96, width: 8, height: 52, marginLeft: -4, borderRadius: 4, background: 'rgba(255,255,255,0.10)' }} />
+ ))}
+ {f > start && <div style={{ position: 'absolute', left: 0, right: 0, top: carY, textAlign: 'center', fontSize: 62 }}>🚗</div>}
+ <div style={{ position: 'absolute', bottom: 12, left: 0, right: 0, textAlign: 'center', fontFamily: FONT, fontWeight: 800, fontSize: 22, color: accent }}>{i + 1}</div>
+ </div>
+ );
+};
+
+export const FcfsScene: React.FC<{ s: Scene }> = ({ s }) => {
+ const f = useCurrentFrame();
+ const { fps } = useVideoConfig();
+ const pop = spring({ frame: f, fps, config: { damping: 200 } });
+ const subO = interpolate(f, [16, 34], [0, 1], { extrapolateRight: 'clamp', extrapolateLeft: 'clamp' });
+ const strike = interpolate(f, [26, 46], [0, 1], { easing: Easing.out(Easing.cubic), extrapolateRight: 'clamp', extrapolateLeft: 'clamp' });
+
+ return (
+ <AbsoluteFill style={{ alignItems: 'center', justifyContent: 'center' }}>
+ {s.narr && <Audio src={staticFile(`audio/${s.narr}.wav`)} />}
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
+ <div style={{ position: 'relative', opacity: pop, transform: `translateY(${interpolate(pop, [0, 1], [24, 0])}px)` }}>
+ <div style={{ fontFamily: FONT, fontWeight: 900, fontSize: 92, color: '#f4f9ff', letterSpacing: 1 }}>
+ No <span style={{ position: 'relative' }}>Appointments
+ <span style={{ position: 'absolute', left: 0, top: '54%', height: 8, width: `${strike * 100}%`, background: '#fb7185', borderRadius: 6 }} />
+ </span>
+ </div>
+ </div>
+ <div style={{ opacity: subO, marginTop: 14, fontFamily: FONT, fontWeight: 800, fontSize: 46, color: s.accent, letterSpacing: 2 }}>
+ First Come · First Served
+ </div>
+
+ <div style={{ display: 'flex', gap: 22, marginTop: 44 }}>
+ {[...Array(6)].map((_, i) => <Lane key={i} i={i} total={6} frames={s.frames} accent={s.accent} />)}
+ </div>
+
+ <div style={{ marginTop: 40, display: 'flex', gap: 46, opacity: subO, fontFamily: FONT }}>
+ {[['6', 'full lines'], ['Large', 'crew on deck'], ['0', 'wait to book']].map(([n, l]) => (
+ <div key={l} style={{ textAlign: 'center' }}>
+ <div style={{ fontWeight: 900, fontSize: 56, color: '#eaf2ff' }}>{n}</div>
+ <div style={{ fontWeight: 600, fontSize: 24, color: '#9fb3cd', letterSpacing: 1 }}>{l}</div>
+ </div>
+ ))}
+ </div>
+ </div>
+ </AbsoluteFill>
+ );
+};
diff --git a/src/scenes/OptionScene.tsx b/src/scenes/OptionScene.tsx
new file mode 100644
index 0000000..b7e5b67
--- /dev/null
+++ b/src/scenes/OptionScene.tsx
@@ -0,0 +1,42 @@
+import { AbsoluteFill, OffthreadVideo, Audio, staticFile, useCurrentFrame, interpolate, Easing } from 'remotion';
+import type { Scene } from '../story';
+
+const FONT = 'Inter, "SF Pro Display", system-ui, -apple-system, sans-serif';
+
+export const OptionScene: React.FC<{ s: Scene }> = ({ s }) => {
+ const f = useCurrentFrame();
+ const zoom = interpolate(f, [0, s.frames], [1.06, 1.14], { extrapolateRight: 'clamp' });
+ const panelIn = interpolate(f, [8, 26], [80, 0], { easing: Easing.out(Easing.cubic), extrapolateRight: 'clamp', extrapolateLeft: 'clamp' });
+ const panelO = interpolate(f, [8, 26, s.frames - 10, s.frames], [0, 1, 1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
+ const priceScale = interpolate(f, [16, 34], [0.7, 1], { easing: Easing.out(Easing.back(1.6)), extrapolateRight: 'clamp', extrapolateLeft: 'clamp' });
+
+ return (
+ <AbsoluteFill style={{ backgroundColor: '#05090f' }}>
+ <AbsoluteFill style={{ transform: `scale(${zoom})` }}>
+ <OffthreadVideo src={staticFile(s.clip!)} muted loop playbackRate={1}
+ style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
+ </AbsoluteFill>
+ {/* cinematic scrims */}
+ <AbsoluteFill style={{ background: 'linear-gradient(90deg, rgba(4,8,15,0.92) 0%, rgba(4,8,15,0.5) 42%, rgba(4,8,15,0) 68%)' }} />
+ <AbsoluteFill style={{ background: 'linear-gradient(0deg, rgba(4,8,15,0.6) 0%, transparent 30%)' }} />
+ {s.narr && <Audio src={staticFile(`audio/${s.narr}.wav`)} />}
+
+ <div style={{ position: 'absolute', left: 90, top: 300, maxWidth: 820, transform: `translateY(${panelIn}px)`, opacity: panelO }}>
+ <div style={{ display: 'inline-flex', alignItems: 'center', gap: 10, padding: '8px 16px', borderRadius: 999,
+ background: `${s.accent}26`, border: `1px solid ${s.accent}66`, color: s.accent, fontFamily: FONT,
+ fontWeight: 800, fontSize: 20, letterSpacing: 3, textTransform: 'uppercase' }}>
+ <span style={{ width: 9, height: 9, borderRadius: 9, background: s.accent }} /> {s.category}
+ </div>
+ <div style={{ marginTop: 18, fontFamily: FONT, fontWeight: 900, fontSize: 78, lineHeight: 1.0, color: '#f4f9ff', textShadow: '0 8px 40px rgba(0,0,0,0.7)' }}>
+ {s.name}
+ </div>
+ <div style={{ marginTop: 22, display: 'flex', alignItems: 'flex-end', gap: 16, transform: `scale(${priceScale})`, transformOrigin: 'left bottom' }}>
+ <span style={{ fontFamily: FONT, fontWeight: 900, fontSize: 96, lineHeight: 0.9, color: s.accent, textShadow: `0 0 40px ${s.accent}55` }}>{s.price}</span>
+ </div>
+ <div style={{ marginTop: 22, fontFamily: FONT, fontWeight: 500, fontSize: 30, lineHeight: 1.35, color: '#d6e2f2', maxWidth: 680, textShadow: '0 3px 18px rgba(0,0,0,0.8)' }}>
+ {s.blurb}
+ </div>
+ </div>
+ </AbsoluteFill>
+ );
+};
diff --git a/src/scenes/TipsScene.tsx b/src/scenes/TipsScene.tsx
new file mode 100644
index 0000000..f699208
--- /dev/null
+++ b/src/scenes/TipsScene.tsx
@@ -0,0 +1,50 @@
+import { AbsoluteFill, Img, Audio, staticFile, useCurrentFrame, interpolate, Easing } from 'remotion';
+import type { Scene } from '../story';
+
+const FONT = 'Inter, "SF Pro Display", system-ui, -apple-system, sans-serif';
+const CARDS = ['marco', 'diego', 'andre', 'luis', 'kevin', 'sam'];
+
+export const TipsScene: React.FC<{ s: Scene }> = ({ s }) => {
+ const f = useCurrentFrame();
+ const headO = interpolate(f, [4, 20], [0, 1], { extrapolateRight: 'clamp', extrapolateLeft: 'clamp' });
+ // hero card (marco) slides in and its QR pulses; supporting cards fan behind
+ return (
+ <AbsoluteFill style={{ alignItems: 'center', justifyContent: 'center' }}>
+ {s.narr && <Audio src={staticFile(`audio/${s.narr}.wav`)} />}
+
+ <div style={{ position: 'absolute', top: 88, textAlign: 'center', opacity: headO }}>
+ <div style={{ fontFamily: FONT, fontWeight: 800, fontSize: 22, letterSpacing: 6, textTransform: 'uppercase', color: s.accent }}>Tip the crew directly</div>
+ <div style={{ marginTop: 12, fontFamily: FONT, fontWeight: 900, fontSize: 66, color: '#f4f9ff' }}>Every wash is done by a person</div>
+ <div style={{ marginTop: 10, fontFamily: FONT, fontWeight: 500, fontSize: 30, color: '#b8c9e0' }}>Scan their card — 100% goes straight to them.</div>
+ </div>
+
+ {/* fanned supporting cards */}
+ {CARDS.slice(1).map((c, i) => {
+ const n = CARDS.length - 1;
+ const spread = (i - (n - 1) / 2);
+ const rot = spread * 7;
+ const x = spread * 150;
+ const rise = interpolate(f, [10 + i * 3, 34 + i * 3], [120, 0], { easing: Easing.out(Easing.cubic), extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
+ const o = interpolate(f, [10 + i * 3, 30 + i * 3], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
+ return (
+ <Img key={c} src={staticFile(`cards/${c}.png`)}
+ style={{ position: 'absolute', top: 300 + rise, width: 300, borderRadius: 20,
+ transform: `translateX(${x}px) rotate(${rot}deg)`, opacity: o * 0.9,
+ boxShadow: '0 30px 80px rgba(0,0,0,0.6)' }} />
+ );
+ })}
+
+ {/* hero card front-and-center */}
+ {(() => {
+ const rise = interpolate(f, [22, 46], [140, 0], { easing: Easing.out(Easing.cubic), extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
+ const o = interpolate(f, [22, 40], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
+ const pulse = 1 + 0.02 * Math.sin(f / 6);
+ return (
+ <Img src={staticFile('cards/marco.png')}
+ style={{ position: 'absolute', top: 250 + rise, width: 372, borderRadius: 24, opacity: o,
+ transform: `scale(${pulse})`, boxShadow: '0 40px 110px rgba(0,0,0,0.75)', border: '1px solid rgba(255,255,255,0.08)' }} />
+ );
+ })()}
+ </AbsoluteFill>
+ );
+};
diff --git a/src/story.ts b/src/story.ts
new file mode 100644
index 0000000..7f3bfdb
--- /dev/null
+++ b/src/story.ts
@@ -0,0 +1,85 @@
+import durations from './durations.json';
+import dims from './dims.json';
+
+export const FPS = 30;
+export const WIDTH = 1920;
+export const HEIGHT = 1080;
+
+const CYAN = '#38bdf8';
+const AMBER = '#f5b301';
+const GREEN = '#34d399';
+const VIOLET = '#a78bfa';
+
+const D: Record<string, number> = durations as any;
+// frames for a narrated scene = (voice length + pad), floored to a minimum
+const nf = (id: string, pad = 1.1, min = 2.2) =>
+ Math.max(Math.round(((D[id] || 0) + pad) * FPS), Math.round(min * FPS));
+
+export interface Scene {
+ type: 'card' | 'clip' | 'fcfs' | 'cam' | 'tips' | 'shot';
+ id: string;
+ narr?: string;
+ frames: number;
+ accent: string;
+ // card
+ act?: string; title?: string; sub?: string;
+ // clip / option
+ name?: string; price?: string; category?: string; blurb?: string; clip?: string;
+ // shot
+ file?: string; w?: number; h?: number; url?: string;
+}
+
+const OPTIONS: Array<[string, string, string, string, string]> = [
+ // name, price, category, blurb, clipfile(without ext maps to narr opt-<x>)
+ ['Express Exterior Wash', '$19', 'Wash', 'Foam bath, spot-free rinse, and a hand dry. In and out — no upsell pressure.', 'express-wash'],
+ ['Full-Service Wash', '$39', 'Wash', 'Exterior hand wash, interior vacuum, dash wipe, windows, and tire dressing.', 'full-service'],
+ ['Premium Hand Wash', '$79', 'Wash', 'Two-bucket, foam-cannon hand wash — no tunnel, no swirl marks. Safe for ceramic, matte & wraps.', 'hand-wash'],
+ ['Wax & Ceramic Seal', '$49', 'Protection', 'Triple-foam polish plus a hydrophobic ceramic seal that shields clear coat from UV & road.', 'wax-seal'],
+ ['Interior Express Detail', '$120', 'Detail', 'Deep interior: seats, carpets, dash, vents & door jambs. Pet-hair extraction and shampoo.', 'interior-detail'],
+ ['Complete Detail', '$499', 'Detail', 'Full inside-and-out restoration: wash, clay bar, polish, wax, interior shampoo & engine bay.', 'complete-detail'],
+ ['Ceramic Coating · 9H', 'from $999', 'Protection', 'Iron decon, clay bar, one-step polish, and a 9H nano-ceramic coating — 1–3 years of protection.', 'ceramic-coating'],
+ ['Headlight Restoration', '$89', 'Restore', 'Sand, polish, and seal foggy, yellowed headlights back to clear — better night visibility.', 'headlight'],
+];
+
+const ADMIN: Array<[string, string]> = [
+ ['overview', 'Overview'], ['competitors', 'Competitors'], ['best-post', 'Best Times to Post'],
+ ['best-market', 'Best Times to Market'], ['holidays', 'Holiday-Driven'], ['services', 'Services'],
+ ['suggestions', 'Grow the Business'], ['ads', 'Online Ads'], ['directories', 'Directories'],
+ ['google', 'Google Business'], ['leads', 'Leads'], ['credentials', 'Credentials'],
+];
+
+const catAccent: Record<string, string> = { Wash: CYAN, Protection: GREEN, Detail: AMBER, Restore: VIOLET };
+
+export const SCENES: Scene[] = [
+ { type: 'card', id: 'intro', narr: 'intro', frames: nf('intro', 1.2), accent: CYAN,
+ act: 'San Fernando Valley', title: 'Prestige Car Wash', sub: 'Hand wash · Detail · Ceramic' },
+
+ { type: 'fcfs', id: 'model', narr: 'model', frames: nf('model', 1.0), accent: CYAN },
+
+ { type: 'card', id: 'options', narr: 'options', frames: nf('options', 1.2, 3), accent: AMBER,
+ act: 'The Menu', title: 'Every Option', sub: 'Flat pricing · no upsell pressure' },
+
+ ...OPTIONS.map(([name, price, category, blurb, key]): Scene => ({
+ type: 'clip', id: `opt-${key}`, narr: `opt-${key}`, frames: nf(`opt-${key}`, 1.0),
+ accent: catAccent[category] || CYAN, name, price, category, blurb, clip: `media/svc-${key}.mp4`,
+ })),
+
+ { type: 'cam', id: 'livecam', narr: 'livecam', frames: nf('livecam', 1.0), accent: GREEN },
+
+ { type: 'tips', id: 'tips', narr: 'tips', frames: nf('tips', 1.2), accent: AMBER },
+
+ { type: 'card', id: 'act2', narr: 'admin', frames: nf('admin', 1.0), accent: AMBER,
+ act: 'Behind the scenes', title: 'Growth Center', sub: 'How we keep the business sharp' },
+
+ ...ADMIN.map(([key, title]): Scene => {
+ const dk = 'admin-' + key;
+ return { type: 'shot', id: dk, frames: Math.round(3.3 * FPS), accent: AMBER,
+ file: `shots/${dk}.png`, w: (dims as any)[dk]?.w, h: (dims as any)[dk]?.h,
+ title, url: 'prestigecarwash.com/admin', act: 'Admin · Growth Center', sub: title };
+ }),
+
+ { type: 'card', id: 'outro', narr: 'outro', frames: nf('outro', 1.4, 4), accent: GREEN,
+ act: 'Prestige Car Wash', title: 'First Come, First Served', sub: 'No appointment · 6 lines · come see us' },
+];
+
+export const TOTAL = SCENES.reduce((a, s) => a + s.frames, 0);
← 7985b9d auto-save: 2026-07-12T08:47:12 (1 files) — public/
·
back to Prestige Car Wash Video
·
chore: lint, refactor (dead-code), v1.0.1 (session close) 12ff853 →