[object Object]

← 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

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 →