← back to Rentv Promos
auto-save: 2026-07-22T14:14:43 (21 files) — recap-v2/index.html recap-v2/SCRIPT.md recap-v2/media/captures/admin-analytics.png recap-v2/media/captures/admin-command.png recap-v2/media/captures/admin-index.png
3787f47b4a8518a0767dfbd5eeb79748d9ce0a39 · 2026-07-22 14:14:48 -0700 · Steve Abrams
Files touched
A recap-v2/SCRIPT.mdM recap-v2/index.htmlA recap-v2/media/captures/admin-analytics.pngA recap-v2/media/captures/admin-command.pngA recap-v2/media/captures/admin-index.pngA recap-v2/media/captures/admin-kanban.pngA recap-v2/media/captures/blog.pngA recap-v2/media/captures/cretalk.pngA recap-v2/media/captures/publish.pngA recap-v2/media/captures/theme-midnight.pngA recap-v2/media/captures/theme-salmon.pngA recap-v2/media/captures/v-broadsheet.pngA recap-v2/media/captures/v-dashboard.pngA recap-v2/media/captures/v-magazine.pngA recap-v2/media/captures/v-terminal.pngA recap-v2/media/captures/v-wire.pngA recap-v2/media/captures/versions.pngA recap-v2/media/narration.wavA recap-v2/media/vo/01-intro.wavA recap-v2/media/vo/02-frontpage.wavA recap-v2/media/vo/03-reader.wavA recap-v2/media/vo/04-review.wavA recap-v2/media/vo/05-markets.wavA recap-v2/media/vo/06-cretalk.wavA recap-v2/media/vo/07-versions.wavA recap-v2/media/vo/08-themes.wavA recap-v2/media/vo/09-admin.wavA recap-v2/media/vo/10-publish.wavA recap-v2/media/vo/11-close.wavA recap-v2/media/vo/_sil-gap.wavA recap-v2/media/vo/_sil-lead.wavA recap-v2/media/vo/_sil-tail.wavA recap-v2/media/vo/concat.txtA recap-v2/media/vo/durations.jsonA recap-v2/scripts/build-composition.mjsA recap-v2/scripts/tts.mjs
Diff
commit 3787f47b4a8518a0767dfbd5eeb79748d9ce0a39
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jul 22 14:14:48 2026 -0700
auto-save: 2026-07-22T14:14:43 (21 files) — recap-v2/index.html recap-v2/SCRIPT.md recap-v2/media/captures/admin-analytics.png recap-v2/media/captures/admin-command.png recap-v2/media/captures/admin-index.png
---
recap-v2/SCRIPT.md | 47 +++++
recap-v2/index.html | 317 +++++++++++++++++++++++-----
recap-v2/media/captures/admin-analytics.png | Bin 0 -> 116181 bytes
recap-v2/media/captures/admin-command.png | Bin 0 -> 120561 bytes
recap-v2/media/captures/admin-index.png | Bin 0 -> 115243 bytes
recap-v2/media/captures/admin-kanban.png | Bin 0 -> 163206 bytes
recap-v2/media/captures/blog.png | Bin 0 -> 82221 bytes
recap-v2/media/captures/cretalk.png | Bin 0 -> 660945 bytes
recap-v2/media/captures/publish.png | Bin 0 -> 77780 bytes
recap-v2/media/captures/theme-midnight.png | Bin 0 -> 572170 bytes
recap-v2/media/captures/theme-salmon.png | Bin 0 -> 602366 bytes
recap-v2/media/captures/v-broadsheet.png | Bin 0 -> 1006422 bytes
recap-v2/media/captures/v-dashboard.png | Bin 0 -> 227684 bytes
recap-v2/media/captures/v-magazine.png | Bin 0 -> 822894 bytes
recap-v2/media/captures/v-terminal.png | Bin 0 -> 195715 bytes
recap-v2/media/captures/v-wire.png | Bin 0 -> 947054 bytes
recap-v2/media/captures/versions.png | Bin 0 -> 133902 bytes
recap-v2/media/narration.wav | Bin 0 -> 26910218 bytes
recap-v2/media/vo/01-intro.wav | Bin 0 -> 2453582 bytes
recap-v2/media/vo/02-frontpage.wav | Bin 0 -> 3059790 bytes
recap-v2/media/vo/03-reader.wav | Bin 0 -> 2162766 bytes
recap-v2/media/vo/04-review.wav | Bin 0 -> 1208398 bytes
recap-v2/media/vo/05-markets.wav | Bin 0 -> 2113614 bytes
recap-v2/media/vo/06-cretalk.wav | Bin 0 -> 864334 bytes
recap-v2/media/vo/07-versions.wav | Bin 0 -> 4968526 bytes
recap-v2/media/vo/08-themes.wav | Bin 0 -> 1986638 bytes
recap-v2/media/vo/09-admin.wav | Bin 0 -> 2646094 bytes
recap-v2/media/vo/10-publish.wav | Bin 0 -> 1835086 bytes
recap-v2/media/vo/11-close.wav | Bin 0 -> 2527310 bytes
recap-v2/media/vo/_sil-gap.wav | Bin 0 -> 70638 bytes
recap-v2/media/vo/_sil-lead.wav | Bin 0 -> 70638 bytes
recap-v2/media/vo/_sil-tail.wav | Bin 0 -> 308778 bytes
recap-v2/media/vo/concat.txt | 23 ++
recap-v2/media/vo/durations.json | 13 ++
recap-v2/scripts/build-composition.mjs | 217 +++++++++++++++++++
recap-v2/scripts/tts.mjs | 32 +++
36 files changed, 596 insertions(+), 53 deletions(-)
diff --git a/recap-v2/SCRIPT.md b/recap-v2/SCRIPT.md
new file mode 100644
index 0000000..3fe116a
--- /dev/null
+++ b/recap-v2/SCRIPT.md
@@ -0,0 +1,47 @@
+# RENTV Recap v2 — narration script (Steve's voice)
+
+Target ~5:00. One block per scene; each block is one TTS call (< 1024 chars).
+
+## 01-intro [capture: home (top)]
+
+Boomer — Steve here. Before anything else: what you've built with RENTV is remarkable. For decades it's been the place the West Coast commercial real estate world goes to find out what actually closed, who financed it, and what it traded for. That reputation is the hard part, and you already own it. So everything you're about to see is just a set of suggestions — ideas we sketched on top of your live feed, running on your real stories, built in a single day. Take what's useful, ignore the rest.
+
+## 02-frontpage [capture: home (pan down)]
+
+Here's one idea for the front page. Your reporting stays the star — but we put RENTV Originals right up top, leading into the live wire. Every headline below it is your actual feed, pulled live from rentv dot com — The Swig Company's refi in San Francisco, Tailwind's Sacramento buy — real stories, updating all day. Your full section map is right there in the navigation — Sales, Leases, Financing, Development, Multifamily, Retail, Industrial, L A Commercial — so a broker who only cares about one lane can get straight to it. The suggestion is simple: let the brand lead, and let the wire run underneath it.
+
+## 03-reader [capture: article]
+
+When a reader clicks a headline, the story opens right inside the site — same masthead, same navigation, no jumping away. The photo's there, the category tag is there, and related stories wait at the bottom, so the natural next click is another RENTV story. It's a small touch, but it keeps people in your world, reading one more piece, and then another. Your writing already earns that time; this just makes it easy to spend.
+
+## 04-review [capture: review]
+
+The REview deserves its own stage, so we gave it one. Face to face with the dealmakers — your interview franchise, presented like the flagship it is. This is content nobody else in the space has, and we think it's worth putting a spotlight on it.
+
+## 05-markets [capture: markets]
+
+Here's a live Markets page — treasury rates, the REIT board, the numbers a commercial real estate reader checks every single morning. It's built on public data feeds, so it refreshes on its own and there's nothing for your team to maintain — no data entry, no upkeep, it just runs. A page like this gives people a reason to open RENTV before their first phone call of the day, even on a morning when they're not chasing a specific story.
+
+## 06-cretalk [capture: cretalk]
+
+And CRE Talk — your video conversations, collected in one clean place, easy to browse and easy to play. The interviews are already great; this just gives them a proper home.
+
+## 07-versions [captures: versions, v-wire, v-broadsheet, v-terminal, v-magazine, v-dashboard]
+
+Now the fun part. Instead of asking you to imagine a redesign, we built ten complete versions of the site — whole directions, not mockups. The Wire is the modern newsroom: a lead story, a three-column river, a right rail. Broadsheet reads like a printed paper on the web — dense serif columns, small photos. Terminal is a dark, data-first deal desk. Magazine goes big and editorial, one oversized feature up top. Then there's Daily Digest, a numbered headline list that reads like the morning email. Photo Grid, image-forward, every story equal. Split, with that salmon-paper feel. Dashboard, a midnight command view. Minimal, quiet and typographic. And Feed, a dark modern river. Every single one is running your live stories right now — you can click through them side by side and just point at the one that feels like RENTV.
+
+## 08-themes [captures: theme-midnight, theme-salmon]
+
+On top of the versions, ten professional themes. One click in the toolbar restyles the entire site — RENTV Classic, Broadsheet, F T Salmon, Bloomberg Terminal, Midnight Capital, The Transcript, Western Dispatch, Gunmetal Report, Legal Pad, Institutional White. Same content, different suit — real financial-publication palettes and typography, and the site remembers each reader's choice.
+
+## 09-admin [captures: admin-index, admin-command, admin-kanban, admin-analytics]
+
+For your team, we sketched ten admin dashboards. A command center that puts every surface on one screen — subscribers, stories, videos, the feed. A kanban board for moving work through stages. Analytics, a classic layout, a sidebar layout, a split view, a console, cards, a clean table, a minimal desk. Same live data in every one of them; the suggestion is that you pick the workflow that matches how you and your people actually like to work, rather than adapting to whatever a vendor shipped.
+
+## 10-publish [captures: blog, publish]
+
+And a simple publishing desk. Write a post, hit publish, and it's live on the blog — no plugins, no fighting a content system. A subscriber list builds alongside it, so the newsletter audience you've earned over the years has a place to keep growing. Nothing complicated. Just a quiet backend that stays out of the way of the journalism, which is the whole point.
+
+## 11-close [capture: home top → title card]
+
+So that's the tour — a rebuilt front page, an in-shell reader, The REview, live markets, CRE Talk, ten site versions, ten themes, ten dashboards, and a publishing desk, all running on your live data. Boomer, none of this is a critique — it's a fan letter with working code attached. These are suggestions, and every call is yours. It's all live at rentv dash v one dot agentabrams dot com — logins are on screen. Take a look when you have a minute. Talk soon.
diff --git a/recap-v2/index.html b/recap-v2/index.html
index cfed975..592079d 100644
--- a/recap-v2/index.html
+++ b/recap-v2/index.html
@@ -1,59 +1,270 @@
<!doctype html>
<html lang="en">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=1920, height=1080" />
- <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
- <style>
- * {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }
- html,
- body {
- margin: 0;
- width: 1920px;
- height: 1080px;
- overflow: hidden;
- background: #000;
- }
- body {
- font-family: "Inter", sans-serif;
- }
- code,
- pre,
- .monospace {
- font-family: "JetBrains Mono", monospace;
- }
- </style>
- </head>
- <body>
- <div
- id="root"
- data-composition-id="main"
- data-start="0"
- data-duration="10"
- data-width="1920"
- data-height="1080"
- >
-
-
+<head>
+<meta charset="UTF-8"/>
+<meta name="viewport" content="width=1920, height=1080"/>
+<title>RENTV Recap for Boomer</title>
+<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
+<style>
+ html,body{margin:0;background:#0c0f13}
+ #root{position:relative;width:1920px;height:1080px;overflow:hidden;background:#0c0f13;
+ font-family:'Inter','Helvetica Neue',system-ui,sans-serif;color:#fff}
+ .clip{position:absolute}
+ .shot{inset:0;overflow:hidden;background:#0c0f13;opacity:0}
+ .shot img{display:block;width:1920px;height:auto;will-change:transform}
+ .card{inset:0;display:grid;place-items:center;opacity:0;background:
+ radial-gradient(1200px 700px at 50% 38%, #171d26 0%, #0c0f13 70%)}
+ .cardin{text-align:center;max-width:1400px}
+ .kicker{font-size:26px;letter-spacing:.42em;color:#c8102e;font-weight:800;margin-bottom:34px}
+ h1{font-family:Georgia,'Times New Roman',serif;font-size:170px;margin:0;font-weight:700;letter-spacing:.01em}
+ h2{font-family:Georgia,'Times New Roman',serif;font-size:96px;margin:0;font-weight:700}
+ .red{color:#c8102e}
+ .sub{font-size:34px;line-height:1.55;color:#aeb6c2;margin-top:38px;font-weight:300}
+ .sub2{font-size:28px;color:#aeb6c2;margin-top:26px;font-weight:300}
+ .logins{font-size:36px;margin-top:44px;color:#e8ecf2}
+ .logins b{color:#fff;background:#1a212c;border:1px solid #2c3644;border-radius:8px;padding:8px 18px}
+ .chip{left:80px;bottom:76px;display:flex;align-items:stretch;background:rgba(10,13,17,.82);
+ border:1px solid rgba(255,255,255,.14);border-radius:10px;overflow:hidden;
+ box-shadow:0 12px 40px rgba(0,0,0,.45)}
+ .chipin{display:flex;align-items:stretch}
+ .chip .bar{width:10px;background:#c8102e}
+ .chip .txt{padding:20px 30px}
+ .chip b{display:block;font-family:Georgia,serif;font-size:40px;font-weight:700}
+ .chip span{display:block;font-size:23px;color:#aeb6c2;margin-top:6px}
+ .sublabel{right:80px;top:64px}
+ .sublabel span{background:#c8102e;color:#fff;font-size:24px;font-weight:800;letter-spacing:.08em;
+ padding:12px 22px;border-radius:8px;box-shadow:0 10px 30px rgba(0,0,0,.4)}
+</style>
+</head>
+<body>
+<div id="root" data-composition-id="main" data-start="0" data-width="1920" data-height="1080" data-duration="305.1">
- <!--
- Add your clips here. Example:
- <div id="title" class="clip" data-start="0" data-duration="5" data-track-index="1"
- style="font-size: 64px; color: #fff; padding: 40px">
- Hello World
- </div>
- -->
+ <section id="sc01" class="clip card" data-start="0" data-duration="28.62" data-track-index="1">
+ <div class="cardin" id="sc01in">
+ <div class="kicker">A BUILD RECAP · WITH ADMIRATION</div>
+ <h1>RENTV<span class="red">.com</span></h1>
+ <p class="sub">For Boomer — the new build, on your live feed.<br/>Ten versions · ten themes · ten dashboards · built in a day.</p>
</div>
+ </section>
+ <section id="s02i0" class="clip shot" data-start="28.62" data-duration="35.49" data-track-index="1">
+ <img id="s02i0m" src="media/captures/home.png" alt=""/>
+ </section>
+ <div class="clip chip" id="ch02" data-start="29.32" data-duration="7.00" data-track-index="4">
+ <div class="chipin" id="ch02in"><div class="bar"></div><div class="txt"><b>The Front Page</b><span>RENTV Originals · leading the live wire</span></div></div>
+ </div>
+ <section id="s03i0" class="clip shot" data-start="64.11" data-duration="25.32" data-track-index="1">
+ <img id="s03i0m" src="media/captures/article.png" alt=""/>
+ </section>
+ <div class="clip chip" id="ch03" data-start="64.81" data-duration="7.00" data-track-index="4">
+ <div class="chipin" id="ch03in"><div class="bar"></div><div class="txt"><b>The Article Reader</b><span>stories open in-shell — readers stay on RENTV</span></div></div>
+ </div>
+ <section id="s04i0" class="clip shot" data-start="89.43" data-duration="14.50" data-track-index="1">
+ <img id="s04i0m" src="media/captures/review.png" alt=""/>
+ </section>
+ <div class="clip chip" id="ch04" data-start="90.13" data-duration="7.00" data-track-index="4">
+ <div class="chipin" id="ch04in"><div class="bar"></div><div class="txt"><b>The REview</b><span>face to face with the dealmakers</span></div></div>
+ </div>
+ <section id="s05i0" class="clip shot" data-start="103.93" data-duration="24.76" data-track-index="1">
+ <img id="s05i0m" src="media/captures/markets.png" alt=""/>
+ </section>
+ <div class="clip chip" id="ch05" data-start="104.63" data-duration="7.00" data-track-index="4">
+ <div class="chipin" id="ch05in"><div class="bar"></div><div class="txt"><b>Markets — Live</b><span>rates + REITs · maintains itself</span></div></div>
+ </div>
+ <section id="s06i0" class="clip shot" data-start="128.69" data-duration="10.60" data-track-index="1">
+ <img id="s06i0m" src="media/captures/cretalk.png" alt=""/>
+ </section>
+ <div class="clip chip" id="ch06" data-start="129.39" data-duration="7.00" data-track-index="4">
+ <div class="chipin" id="ch06in"><div class="bar"></div><div class="txt"><b>CRE Talk</b><span>the video conversations, in one place</span></div></div>
+ </div>
+ <section id="s07i0" class="clip shot" data-start="139.29" data-duration="9.52" data-track-index="1">
+ <img id="s07i0m" src="media/captures/versions.png" alt=""/>
+ </section>
+ <div class="clip sublabel" id="s07i0l" data-start="139.59" data-duration="4.50" data-track-index="3"><span>The Versions Picker</span></div>
+ <section id="s07i1" class="clip shot" data-start="148.81" data-duration="9.52" data-track-index="1">
+ <img id="s07i1m" src="media/captures/v-wire.png" alt=""/>
+ </section>
+ <div class="clip sublabel" id="s07i1l" data-start="149.11" data-duration="4.50" data-track-index="3"><span>The Wire</span></div>
+ <section id="s07i2" class="clip shot" data-start="158.33" data-duration="9.53" data-track-index="1">
+ <img id="s07i2m" src="media/captures/v-broadsheet.png" alt=""/>
+ </section>
+ <div class="clip sublabel" id="s07i2l" data-start="158.63" data-duration="4.50" data-track-index="3"><span>Broadsheet</span></div>
+ <section id="s07i3" class="clip shot" data-start="167.86" data-duration="9.52" data-track-index="1">
+ <img id="s07i3m" src="media/captures/v-terminal.png" alt=""/>
+ </section>
+ <div class="clip sublabel" id="s07i3l" data-start="168.16" data-duration="4.50" data-track-index="3"><span>Terminal</span></div>
+ <section id="s07i4" class="clip shot" data-start="177.38" data-duration="9.52" data-track-index="1">
+ <img id="s07i4m" src="media/captures/v-magazine.png" alt=""/>
+ </section>
+ <div class="clip sublabel" id="s07i4l" data-start="177.68" data-duration="4.50" data-track-index="3"><span>Magazine</span></div>
+ <section id="s07i5" class="clip shot" data-start="186.90" data-duration="9.52" data-track-index="1">
+ <img id="s07i5m" src="media/captures/v-dashboard.png" alt=""/>
+ </section>
+ <div class="clip sublabel" id="s07i5l" data-start="187.20" data-duration="4.50" data-track-index="3"><span>Dashboard</span></div>
+ <div class="clip chip" id="ch07" data-start="139.99" data-duration="7.00" data-track-index="4">
+ <div class="chipin" id="ch07in"><div class="bar"></div><div class="txt"><b>Ten Complete Site Versions</b><span>whole directions — all on live data</span></div></div>
+ </div>
+ <section id="s08i0" class="clip shot" data-start="196.42" data-duration="11.66" data-track-index="1">
+ <img id="s08i0m" src="media/captures/theme-midnight.png" alt=""/>
+ </section>
+ <div class="clip sublabel" id="s08i0l" data-start="196.72" data-duration="4.50" data-track-index="3"><span>Theme · Midnight Capital</span></div>
+ <section id="s08i1" class="clip shot" data-start="208.08" data-duration="11.66" data-track-index="1">
+ <img id="s08i1m" src="media/captures/theme-salmon.png" alt=""/>
+ </section>
+ <div class="clip sublabel" id="s08i1l" data-start="208.38" data-duration="4.50" data-track-index="3"><span>Theme · FT Salmon</span></div>
+ <div class="clip chip" id="ch08" data-start="197.12" data-duration="7.00" data-track-index="4">
+ <div class="chipin" id="ch08in"><div class="bar"></div><div class="txt"><b>Ten Professional Themes</b><span>one click restyles the entire site</span></div></div>
+ </div>
+ <section id="s09i0" class="clip shot" data-start="219.74" data-duration="7.70" data-track-index="1">
+ <img id="s09i0m" src="media/captures/admin-index.png" alt=""/>
+ </section>
+ <div class="clip sublabel" id="s09i0l" data-start="220.04" data-duration="4.50" data-track-index="3"><span>All 10 Dashboards</span></div>
+ <section id="s09i1" class="clip shot" data-start="227.44" data-duration="7.70" data-track-index="1">
+ <img id="s09i1m" src="media/captures/admin-command.png" alt=""/>
+ </section>
+ <div class="clip sublabel" id="s09i1l" data-start="227.74" data-duration="4.50" data-track-index="3"><span>Command Center</span></div>
+ <section id="s09i2" class="clip shot" data-start="235.14" data-duration="7.70" data-track-index="1">
+ <img id="s09i2m" src="media/captures/admin-kanban.png" alt=""/>
+ </section>
+ <div class="clip sublabel" id="s09i2l" data-start="235.44" data-duration="4.50" data-track-index="3"><span>Kanban</span></div>
+ <section id="s09i3" class="clip shot" data-start="242.84" data-duration="7.70" data-track-index="1">
+ <img id="s09i3m" src="media/captures/admin-analytics.png" alt=""/>
+ </section>
+ <div class="clip sublabel" id="s09i3l" data-start="243.14" data-duration="4.50" data-track-index="3"><span>Analytics</span></div>
+ <div class="clip chip" id="ch09" data-start="220.44" data-duration="7.00" data-track-index="4">
+ <div class="chipin" id="ch09in"><div class="bar"></div><div class="txt"><b>Ten Admin Dashboards</b><span>same live data · pick your workflow</span></div></div>
+ </div>
+ <section id="s10i0" class="clip shot" data-start="250.54" data-duration="10.80" data-track-index="1">
+ <img id="s10i0m" src="media/captures/blog.png" alt=""/>
+ </section>
+ <div class="clip sublabel" id="s10i0l" data-start="250.84" data-duration="4.50" data-track-index="3"><span>The Blog</span></div>
+ <section id="s10i1" class="clip shot" data-start="261.34" data-duration="10.81" data-track-index="1">
+ <img id="s10i1m" src="media/captures/publish.png" alt=""/>
+ </section>
+ <div class="clip sublabel" id="s10i1l" data-start="261.64" data-duration="4.50" data-track-index="3"><span>The Publishing Desk</span></div>
+ <div class="clip chip" id="ch10" data-start="251.24" data-duration="7.00" data-track-index="4">
+ <div class="chipin" id="ch10in"><div class="bar"></div><div class="txt"><b>The Publishing Desk</b><span>write → publish → live, plus subscribers</span></div></div>
+ </div>
+ <section id="s11a" class="clip shot" data-start="272.15" data-duration="7.50" data-track-index="1">
+ <img id="s11am" src="media/captures/home.png" alt=""/>
+ </section>
+ <section id="s11b" class="clip card" data-start="279.65" data-duration="25.45" data-track-index="1">
+ <div class="cardin" id="s11bin">
+ <div class="kicker">THANK YOU, BOOMER — THESE ARE JUST SUGGESTIONS</div>
+ <h2>rentv-v1<span class="red">.agentabrams.com</span></h2>
+ <p class="logins">login <b>admin / DW2024!</b> or <b>Boomer / Rentv2024!</b></p>
+ <p class="sub2">Every headline you saw is live rentv.com data · built in a day · your call, always.</p>
+ </div>
+ </section>
+</div>
+<audio id="vo" src="media/narration.wav" data-start="0" data-duration="305.1" data-track-index="10" data-volume="1"></audio>
+<script>
+ window.__timelines = window.__timelines || {};
+ const tl = gsap.timeline({ paused: true });
- <script>
- window.__timelines = window.__timelines || {};
- const tl = gsap.timeline({ paused: true });
- // Example: tl.from("#title", { opacity: 0, y: -50, duration: 1 }, 0);
- window.__timelines["main"] = tl;
- </script>
- </body>
+ tl.to("#sc01", { opacity: 1, duration: 0.35 }, 0);
+ tl.from("#sc01in", { opacity: 0, y: 40, duration: 1.2, ease: "power3.out" }, 0.30);
+ tl.fromTo("#sc01in", { scale: 1 }, { scale: 1.045, duration: 27.62, ease: "none" }, 0.50);
+ tl.to("#sc01in", { opacity: 0, duration: 0.6, ease: "power2.in" }, 28.02);
+ tl.set("#sc01in", { opacity: 0 }, 28.62);
+ tl.to("#s02i0", { opacity: 1, duration: 0.45, ease: "power2.out" }, 28.62);
+ tl.fromTo("#s02i0m", { y: 0 }, { y: -1513, duration: 35.29, ease: "sine.inOut" }, 28.72);
+ tl.from("#ch02", { opacity: 0, y: 26, duration: 0.55, ease: "power3.out" }, 29.32);
+ tl.to("#ch02in", { opacity: 0, duration: 0.4 }, 35.92);
+ tl.set("#ch02in", { opacity: 0 }, 36.32);
+ tl.to("#s03i0", { opacity: 1, duration: 0.45, ease: "power2.out" }, 64.11);
+ tl.fromTo("#s03i0m", { y: 0 }, { y: -1162, duration: 25.12, ease: "sine.inOut" }, 64.21);
+ tl.from("#ch03", { opacity: 0, y: 26, duration: 0.55, ease: "power3.out" }, 64.81);
+ tl.to("#ch03in", { opacity: 0, duration: 0.4 }, 71.41);
+ tl.set("#ch03in", { opacity: 0 }, 71.81);
+ tl.to("#s04i0", { opacity: 1, duration: 0.45, ease: "power2.out" }, 89.43);
+ tl.fromTo("#s04i0m", { y: 0 }, { y: -1131, duration: 14.30, ease: "sine.inOut" }, 89.53);
+ tl.from("#ch04", { opacity: 0, y: 26, duration: 0.55, ease: "power3.out" }, 90.13);
+ tl.to("#ch04in", { opacity: 0, duration: 0.4 }, 96.73);
+ tl.set("#ch04in", { opacity: 0 }, 97.13);
+ tl.to("#s05i0", { opacity: 1, duration: 0.45, ease: "power2.out" }, 103.93);
+ tl.fromTo("#s05i0m", { y: 0 }, { y: -465, duration: 24.56, ease: "sine.inOut" }, 104.03);
+ tl.from("#ch05", { opacity: 0, y: 26, duration: 0.55, ease: "power3.out" }, 104.63);
+ tl.to("#ch05in", { opacity: 0, duration: 0.4 }, 111.23);
+ tl.set("#ch05in", { opacity: 0 }, 111.63);
+ tl.to("#s06i0", { opacity: 1, duration: 0.45, ease: "power2.out" }, 128.69);
+ tl.fromTo("#s06i0m", { y: 0 }, { y: -827, duration: 10.40, ease: "sine.inOut" }, 128.79);
+ tl.from("#ch06", { opacity: 0, y: 26, duration: 0.55, ease: "power3.out" }, 129.39);
+ tl.to("#ch06in", { opacity: 0, duration: 0.4 }, 135.99);
+ tl.set("#ch06in", { opacity: 0 }, 136.39);
+ tl.to("#s07i0", { opacity: 1, duration: 0.45, ease: "power2.out" }, 139.29);
+ tl.fromTo("#s07i0m", { y: 0 }, { y: -106, duration: 9.32, ease: "sine.inOut" }, 139.39);
+ tl.from("#s07i0l", { opacity: 0, x: -18, duration: 0.4, ease: "power2.out" }, 139.59);
+ tl.to("#s07i0l", { opacity: 0, duration: 0.35 }, 143.74);
+ tl.to("#s07i1", { opacity: 1, duration: 0.45, ease: "power2.out" }, 148.81);
+ tl.fromTo("#s07i1m", { scale: 1.07, transformOrigin: "50% 30%" }, { scale: 1, duration: 9.52, ease: "none" }, 148.81);
+ tl.from("#s07i1l", { opacity: 0, x: -18, duration: 0.4, ease: "power2.out" }, 149.11);
+ tl.to("#s07i1l", { opacity: 0, duration: 0.35 }, 153.26);
+ tl.to("#s07i2", { opacity: 1, duration: 0.45, ease: "power2.out" }, 158.33);
+ tl.fromTo("#s07i2m", { scale: 1, transformOrigin: "50% 30%" }, { scale: 1.07, duration: 9.53, ease: "none" }, 158.33);
+ tl.from("#s07i2l", { opacity: 0, x: -18, duration: 0.4, ease: "power2.out" }, 158.63);
+ tl.to("#s07i2l", { opacity: 0, duration: 0.35 }, 162.78);
+ tl.to("#s07i3", { opacity: 1, duration: 0.45, ease: "power2.out" }, 167.86);
+ tl.fromTo("#s07i3m", { scale: 1.07, transformOrigin: "50% 30%" }, { scale: 1, duration: 9.52, ease: "none" }, 167.86);
+ tl.from("#s07i3l", { opacity: 0, x: -18, duration: 0.4, ease: "power2.out" }, 168.16);
+ tl.to("#s07i3l", { opacity: 0, duration: 0.35 }, 172.31);
+ tl.to("#s07i4", { opacity: 1, duration: 0.45, ease: "power2.out" }, 177.38);
+ tl.fromTo("#s07i4m", { scale: 1, transformOrigin: "50% 30%" }, { scale: 1.07, duration: 9.52, ease: "none" }, 177.38);
+ tl.from("#s07i4l", { opacity: 0, x: -18, duration: 0.4, ease: "power2.out" }, 177.68);
+ tl.to("#s07i4l", { opacity: 0, duration: 0.35 }, 181.83);
+ tl.to("#s07i5", { opacity: 1, duration: 0.45, ease: "power2.out" }, 186.90);
+ tl.fromTo("#s07i5m", { scale: 1.07, transformOrigin: "50% 30%" }, { scale: 1, duration: 9.52, ease: "none" }, 186.90);
+ tl.from("#s07i5l", { opacity: 0, x: -18, duration: 0.4, ease: "power2.out" }, 187.20);
+ tl.to("#s07i5l", { opacity: 0, duration: 0.35 }, 191.35);
+ tl.from("#ch07", { opacity: 0, y: 26, duration: 0.55, ease: "power3.out" }, 139.99);
+ tl.to("#ch07in", { opacity: 0, duration: 0.4 }, 146.59);
+ tl.set("#ch07in", { opacity: 0 }, 146.99);
+ tl.to("#s08i0", { opacity: 1, duration: 0.45, ease: "power2.out" }, 196.42);
+ tl.fromTo("#s08i0m", { scale: 1, transformOrigin: "50% 30%" }, { scale: 1.07, duration: 11.66, ease: "none" }, 196.42);
+ tl.from("#s08i0l", { opacity: 0, x: -18, duration: 0.4, ease: "power2.out" }, 196.72);
+ tl.to("#s08i0l", { opacity: 0, duration: 0.35 }, 200.87);
+ tl.to("#s08i1", { opacity: 1, duration: 0.45, ease: "power2.out" }, 208.08);
+ tl.fromTo("#s08i1m", { scale: 1.07, transformOrigin: "50% 30%" }, { scale: 1, duration: 11.66, ease: "none" }, 208.08);
+ tl.from("#s08i1l", { opacity: 0, x: -18, duration: 0.4, ease: "power2.out" }, 208.38);
+ tl.to("#s08i1l", { opacity: 0, duration: 0.35 }, 212.53);
+ tl.from("#ch08", { opacity: 0, y: 26, duration: 0.55, ease: "power3.out" }, 197.12);
+ tl.to("#ch08in", { opacity: 0, duration: 0.4 }, 203.72);
+ tl.set("#ch08in", { opacity: 0 }, 204.12);
+ tl.to("#s09i0", { opacity: 1, duration: 0.45, ease: "power2.out" }, 219.74);
+ tl.fromTo("#s09i0m", { scale: 1, transformOrigin: "50% 30%" }, { scale: 1.07, duration: 7.70, ease: "none" }, 219.74);
+ tl.from("#s09i0l", { opacity: 0, x: -18, duration: 0.4, ease: "power2.out" }, 220.04);
+ tl.to("#s09i0l", { opacity: 0, duration: 0.35 }, 224.19);
+ tl.to("#s09i1", { opacity: 1, duration: 0.45, ease: "power2.out" }, 227.44);
+ tl.fromTo("#s09i1m", { scale: 1.07, transformOrigin: "50% 30%" }, { scale: 1, duration: 7.70, ease: "none" }, 227.44);
+ tl.from("#s09i1l", { opacity: 0, x: -18, duration: 0.4, ease: "power2.out" }, 227.74);
+ tl.to("#s09i1l", { opacity: 0, duration: 0.35 }, 231.89);
+ tl.to("#s09i2", { opacity: 1, duration: 0.45, ease: "power2.out" }, 235.14);
+ tl.fromTo("#s09i2m", { scale: 1, transformOrigin: "50% 30%" }, { scale: 1.07, duration: 7.70, ease: "none" }, 235.14);
+ tl.from("#s09i2l", { opacity: 0, x: -18, duration: 0.4, ease: "power2.out" }, 235.44);
+ tl.to("#s09i2l", { opacity: 0, duration: 0.35 }, 239.59);
+ tl.to("#s09i3", { opacity: 1, duration: 0.45, ease: "power2.out" }, 242.84);
+ tl.fromTo("#s09i3m", { scale: 1.07, transformOrigin: "50% 30%" }, { scale: 1, duration: 7.70, ease: "none" }, 242.84);
+ tl.from("#s09i3l", { opacity: 0, x: -18, duration: 0.4, ease: "power2.out" }, 243.14);
+ tl.to("#s09i3l", { opacity: 0, duration: 0.35 }, 247.29);
+ tl.from("#ch09", { opacity: 0, y: 26, duration: 0.55, ease: "power3.out" }, 220.44);
+ tl.to("#ch09in", { opacity: 0, duration: 0.4 }, 227.04);
+ tl.set("#ch09in", { opacity: 0 }, 227.44);
+ tl.to("#s10i0", { opacity: 1, duration: 0.45, ease: "power2.out" }, 250.54);
+ tl.fromTo("#s10i0m", { scale: 1, transformOrigin: "50% 30%" }, { scale: 1.07, duration: 10.80, ease: "none" }, 250.54);
+ tl.from("#s10i0l", { opacity: 0, x: -18, duration: 0.4, ease: "power2.out" }, 250.84);
+ tl.to("#s10i0l", { opacity: 0, duration: 0.35 }, 254.99);
+ tl.to("#s10i1", { opacity: 1, duration: 0.45, ease: "power2.out" }, 261.34);
+ tl.fromTo("#s10i1m", { scale: 1.07, transformOrigin: "50% 30%" }, { scale: 1, duration: 10.81, ease: "none" }, 261.34);
+ tl.from("#s10i1l", { opacity: 0, x: -18, duration: 0.4, ease: "power2.out" }, 261.64);
+ tl.to("#s10i1l", { opacity: 0, duration: 0.35 }, 265.79);
+ tl.from("#ch10", { opacity: 0, y: 26, duration: 0.55, ease: "power3.out" }, 251.24);
+ tl.to("#ch10in", { opacity: 0, duration: 0.4 }, 257.84);
+ tl.set("#ch10in", { opacity: 0 }, 258.24);
+ tl.to("#s11a", { opacity: 1, duration: 0.5 }, 272.15);
+ tl.fromTo("#s11am", { y: 0 }, { y: -420, duration: 7.30, ease: "sine.inOut" }, 272.25);
+ tl.to("#s11b", { opacity: 1, duration: 0.5 }, 279.65);
+ tl.from("#s11bin", { opacity: 0, y: 36, duration: 1, ease: "power3.out" }, 279.75);
+ tl.fromTo("#s11bin", { scale: 1 }, { scale: 1.035, duration: 24.45, ease: "none" }, 280.05);
+ window.__timelines["main"] = tl;
+</script>
+</body>
</html>
diff --git a/recap-v2/media/captures/admin-analytics.png b/recap-v2/media/captures/admin-analytics.png
new file mode 100644
index 0000000..354ab11
Binary files /dev/null and b/recap-v2/media/captures/admin-analytics.png differ
diff --git a/recap-v2/media/captures/admin-command.png b/recap-v2/media/captures/admin-command.png
new file mode 100644
index 0000000..2ba2e5f
Binary files /dev/null and b/recap-v2/media/captures/admin-command.png differ
diff --git a/recap-v2/media/captures/admin-index.png b/recap-v2/media/captures/admin-index.png
new file mode 100644
index 0000000..57d0978
Binary files /dev/null and b/recap-v2/media/captures/admin-index.png differ
diff --git a/recap-v2/media/captures/admin-kanban.png b/recap-v2/media/captures/admin-kanban.png
new file mode 100644
index 0000000..3267584
Binary files /dev/null and b/recap-v2/media/captures/admin-kanban.png differ
diff --git a/recap-v2/media/captures/blog.png b/recap-v2/media/captures/blog.png
new file mode 100644
index 0000000..5157f00
Binary files /dev/null and b/recap-v2/media/captures/blog.png differ
diff --git a/recap-v2/media/captures/cretalk.png b/recap-v2/media/captures/cretalk.png
new file mode 100644
index 0000000..890cc75
Binary files /dev/null and b/recap-v2/media/captures/cretalk.png differ
diff --git a/recap-v2/media/captures/publish.png b/recap-v2/media/captures/publish.png
new file mode 100644
index 0000000..ef5b110
Binary files /dev/null and b/recap-v2/media/captures/publish.png differ
diff --git a/recap-v2/media/captures/theme-midnight.png b/recap-v2/media/captures/theme-midnight.png
new file mode 100644
index 0000000..96e0806
Binary files /dev/null and b/recap-v2/media/captures/theme-midnight.png differ
diff --git a/recap-v2/media/captures/theme-salmon.png b/recap-v2/media/captures/theme-salmon.png
new file mode 100644
index 0000000..929efd7
Binary files /dev/null and b/recap-v2/media/captures/theme-salmon.png differ
diff --git a/recap-v2/media/captures/v-broadsheet.png b/recap-v2/media/captures/v-broadsheet.png
new file mode 100644
index 0000000..14e64aa
Binary files /dev/null and b/recap-v2/media/captures/v-broadsheet.png differ
diff --git a/recap-v2/media/captures/v-dashboard.png b/recap-v2/media/captures/v-dashboard.png
new file mode 100644
index 0000000..c74c45d
Binary files /dev/null and b/recap-v2/media/captures/v-dashboard.png differ
diff --git a/recap-v2/media/captures/v-magazine.png b/recap-v2/media/captures/v-magazine.png
new file mode 100644
index 0000000..59170ba
Binary files /dev/null and b/recap-v2/media/captures/v-magazine.png differ
diff --git a/recap-v2/media/captures/v-terminal.png b/recap-v2/media/captures/v-terminal.png
new file mode 100644
index 0000000..2c71fd5
Binary files /dev/null and b/recap-v2/media/captures/v-terminal.png differ
diff --git a/recap-v2/media/captures/v-wire.png b/recap-v2/media/captures/v-wire.png
new file mode 100644
index 0000000..85aec85
Binary files /dev/null and b/recap-v2/media/captures/v-wire.png differ
diff --git a/recap-v2/media/captures/versions.png b/recap-v2/media/captures/versions.png
new file mode 100644
index 0000000..7db5cae
Binary files /dev/null and b/recap-v2/media/captures/versions.png differ
diff --git a/recap-v2/media/narration.wav b/recap-v2/media/narration.wav
new file mode 100644
index 0000000..6b420fe
Binary files /dev/null and b/recap-v2/media/narration.wav differ
diff --git a/recap-v2/media/vo/01-intro.wav b/recap-v2/media/vo/01-intro.wav
new file mode 100644
index 0000000..d408e9e
Binary files /dev/null and b/recap-v2/media/vo/01-intro.wav differ
diff --git a/recap-v2/media/vo/02-frontpage.wav b/recap-v2/media/vo/02-frontpage.wav
new file mode 100644
index 0000000..b3f8528
Binary files /dev/null and b/recap-v2/media/vo/02-frontpage.wav differ
diff --git a/recap-v2/media/vo/03-reader.wav b/recap-v2/media/vo/03-reader.wav
new file mode 100644
index 0000000..21d0f1c
Binary files /dev/null and b/recap-v2/media/vo/03-reader.wav differ
diff --git a/recap-v2/media/vo/04-review.wav b/recap-v2/media/vo/04-review.wav
new file mode 100644
index 0000000..f4003e0
Binary files /dev/null and b/recap-v2/media/vo/04-review.wav differ
diff --git a/recap-v2/media/vo/05-markets.wav b/recap-v2/media/vo/05-markets.wav
new file mode 100644
index 0000000..0108acd
Binary files /dev/null and b/recap-v2/media/vo/05-markets.wav differ
diff --git a/recap-v2/media/vo/06-cretalk.wav b/recap-v2/media/vo/06-cretalk.wav
new file mode 100644
index 0000000..5021bc7
Binary files /dev/null and b/recap-v2/media/vo/06-cretalk.wav differ
diff --git a/recap-v2/media/vo/07-versions.wav b/recap-v2/media/vo/07-versions.wav
new file mode 100644
index 0000000..2f2c3c6
Binary files /dev/null and b/recap-v2/media/vo/07-versions.wav differ
diff --git a/recap-v2/media/vo/08-themes.wav b/recap-v2/media/vo/08-themes.wav
new file mode 100644
index 0000000..069a8b2
Binary files /dev/null and b/recap-v2/media/vo/08-themes.wav differ
diff --git a/recap-v2/media/vo/09-admin.wav b/recap-v2/media/vo/09-admin.wav
new file mode 100644
index 0000000..c36f1a5
Binary files /dev/null and b/recap-v2/media/vo/09-admin.wav differ
diff --git a/recap-v2/media/vo/10-publish.wav b/recap-v2/media/vo/10-publish.wav
new file mode 100644
index 0000000..e3e8979
Binary files /dev/null and b/recap-v2/media/vo/10-publish.wav differ
diff --git a/recap-v2/media/vo/11-close.wav b/recap-v2/media/vo/11-close.wav
new file mode 100644
index 0000000..443e996
Binary files /dev/null and b/recap-v2/media/vo/11-close.wav differ
diff --git a/recap-v2/media/vo/_sil-gap.wav b/recap-v2/media/vo/_sil-gap.wav
new file mode 100644
index 0000000..cda43f7
Binary files /dev/null and b/recap-v2/media/vo/_sil-gap.wav differ
diff --git a/recap-v2/media/vo/_sil-lead.wav b/recap-v2/media/vo/_sil-lead.wav
new file mode 100644
index 0000000..cda43f7
Binary files /dev/null and b/recap-v2/media/vo/_sil-lead.wav differ
diff --git a/recap-v2/media/vo/_sil-tail.wav b/recap-v2/media/vo/_sil-tail.wav
new file mode 100644
index 0000000..2ebce11
Binary files /dev/null and b/recap-v2/media/vo/_sil-tail.wav differ
diff --git a/recap-v2/media/vo/concat.txt b/recap-v2/media/vo/concat.txt
new file mode 100644
index 0000000..04a8b19
--- /dev/null
+++ b/recap-v2/media/vo/concat.txt
@@ -0,0 +1,23 @@
+file '_sil-lead.wav'
+file '01-intro.wav'
+file '_sil-gap.wav'
+file '02-frontpage.wav'
+file '_sil-gap.wav'
+file '03-reader.wav'
+file '_sil-gap.wav'
+file '04-review.wav'
+file '_sil-gap.wav'
+file '05-markets.wav'
+file '_sil-gap.wav'
+file '06-cretalk.wav'
+file '_sil-gap.wav'
+file '07-versions.wav'
+file '_sil-gap.wav'
+file '08-themes.wav'
+file '_sil-gap.wav'
+file '09-admin.wav'
+file '_sil-gap.wav'
+file '10-publish.wav'
+file '_sil-gap.wav'
+file '11-close.wav'
+file '_sil-tail.wav'
\ No newline at end of file
diff --git a/recap-v2/media/vo/durations.json b/recap-v2/media/vo/durations.json
new file mode 100644
index 0000000..8a07bbf
--- /dev/null
+++ b/recap-v2/media/vo/durations.json
@@ -0,0 +1,13 @@
+{
+ "01-intro": 27.82,
+ "02-frontpage": 34.69,
+ "03-reader": 24.52,
+ "04-review": 13.7,
+ "05-markets": 23.96,
+ "06-cretalk": 9.8,
+ "07-versions": 56.33,
+ "08-themes": 22.52,
+ "09-admin": 30,
+ "10-publish": 20.81,
+ "11-close": 28.65
+}
\ No newline at end of file
diff --git a/recap-v2/scripts/build-composition.mjs b/recap-v2/scripts/build-composition.mjs
new file mode 100644
index 0000000..cc3d55d
--- /dev/null
+++ b/recap-v2/scripts/build-composition.mjs
@@ -0,0 +1,217 @@
+#!/usr/bin/env node
+/* Build master narration track + generate index.html for the RENTV recap.
+ Deterministic: all timing baked statically from media/vo/durations.json. */
+import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
+import { execFileSync } from 'node:child_process';
+
+const ROOT = new URL('..', import.meta.url).pathname;
+const FF = '/opt/homebrew/bin/ffmpeg';
+const durs = JSON.parse(readFileSync(ROOT + 'media/vo/durations.json', 'utf8'));
+
+const ORDER = ['01-intro','02-frontpage','03-reader','04-review','05-markets','06-cretalk',
+ '07-versions','08-themes','09-admin','10-publish','11-close'];
+const LEAD = 0.8, GAP = 0.8, TAIL = 3.5;
+
+// ---- timeline math: scene i visuals start when its VO block starts minus LEAD-ish pad
+const r2 = (x) => Math.round(x * 100) / 100;
+let t = LEAD;
+const vo = {}; // vo start per scene
+for (const id of ORDER) { vo[id] = t; t += durs[id] + GAP; }
+const TOTAL = Math.ceil((t - GAP + TAIL) * 10) / 10;
+const sceneStart = {};
+ORDER.forEach((id, i) => { sceneStart[id] = i === 0 ? 0 : r2(vo[id] - GAP); });
+const sceneEnd = {};
+ORDER.forEach((id, i) => { sceneEnd[id] = i === ORDER.length - 1 ? TOTAL : sceneStart[ORDER[i + 1]]; });
+
+// ---- master.wav: silence LEAD + vo + GAP ... + TAIL (all 44.1k mono pcm)
+mkdirSync(ROOT + 'media', { recursive: true });
+const mk = (f, secs) => execFileSync(FF, ['-y','-loglevel','error','-f','lavfi','-i',
+ `anullsrc=r=44100:cl=mono`, '-t', String(secs), '-c:a','pcm_s16le', f]);
+mk(ROOT + 'media/vo/_sil-lead.wav', LEAD);
+mk(ROOT + 'media/vo/_sil-gap.wav', GAP);
+mk(ROOT + 'media/vo/_sil-tail.wav', TAIL);
+const list = [];
+list.push('_sil-lead.wav');
+ORDER.forEach((id, i) => { list.push(id + '.wav'); if (i < ORDER.length - 1) list.push('_sil-gap.wav'); });
+list.push('_sil-tail.wav');
+writeFileSync(ROOT + 'media/vo/concat.txt', list.map(f => `file '${f}'`).join('\n'));
+execFileSync(FF, ['-y','-loglevel','error','-f','concat','-safe','0','-i', ROOT + 'media/vo/concat.txt',
+ '-ar','44100','-ac','1','-c:a','pcm_s16le', ROOT + 'media/narration.wav']);
+
+// ---- shot plan. Each shot: [img, h(px), label?] laid sequentially inside its scene.
+const IMG = (n) => `media/captures/${n}.png`;
+const shotsByScene = {
+ '02-frontpage': [['home', 2593]],
+ '03-reader': [['article', 2242]],
+ '04-review': [['review', 3328]],
+ '05-markets': [['markets', 1545]],
+ '06-cretalk': [['cretalk', 2760]],
+ '07-versions': [['versions', 1186, 'The Versions Picker'], ['v-wire', 1080, 'The Wire'],
+ ['v-broadsheet', 1080, 'Broadsheet'], ['v-terminal', 1080, 'Terminal'],
+ ['v-magazine', 1080, 'Magazine'], ['v-dashboard', 1080, 'Dashboard']],
+ '08-themes': [['theme-midnight', 1080, 'Theme · Midnight Capital'], ['theme-salmon', 1080, 'Theme · FT Salmon']],
+ '09-admin': [['admin-index', 1080, 'All 10 Dashboards'], ['admin-command', 1080, 'Command Center'],
+ ['admin-kanban', 1080, 'Kanban'], ['admin-analytics', 1080, 'Analytics']],
+ '10-publish': [['blog', 1080, 'The Blog'], ['publish', 1080, 'The Publishing Desk']],
+};
+const chips = {
+ '02-frontpage': ['The Front Page', 'RENTV Originals · leading the live wire'],
+ '03-reader': ['The Article Reader', 'stories open in-shell — readers stay on RENTV'],
+ '04-review': ['The REview', 'face to face with the dealmakers'],
+ '05-markets': ['Markets — Live', 'rates + REITs · maintains itself'],
+ '06-cretalk': ['CRE Talk', 'the video conversations, in one place'],
+ '07-versions': ['Ten Complete Site Versions', 'whole directions — all on live data'],
+ '08-themes': ['Ten Professional Themes', 'one click restyles the entire site'],
+ '09-admin': ['Ten Admin Dashboards', 'same live data · pick your workflow'],
+ '10-publish': ['The Publishing Desk', 'write → publish → live, plus subscribers'],
+};
+
+let clips = '', tw = '';
+const esc = (s) => s.replace(/&/g,'&').replace(/</g,'<');
+
+// scene 01: title card
+{
+ const s = 0, d = sceneEnd['01-intro'];
+ clips += `
+ <section id="sc01" class="clip card" data-start="0" data-duration="${d.toFixed(2)}" data-track-index="1">
+ <div class="cardin" id="sc01in">
+ <div class="kicker">A BUILD RECAP · WITH ADMIRATION</div>
+ <h1>RENTV<span class="red">.com</span></h1>
+ <p class="sub">For Boomer — the new build, on your live feed.<br/>Ten versions · ten themes · ten dashboards · built in a day.</p>
+ </div>
+ </section>`;
+ tw += `
+ tl.to("#sc01", { opacity: 1, duration: 0.35 }, 0);
+ tl.from("#sc01in", { opacity: 0, y: 40, duration: 1.2, ease: "power3.out" }, ${(s + 0.3).toFixed(2)});
+ tl.fromTo("#sc01in", { scale: 1 }, { scale: 1.045, duration: ${(d - 1).toFixed(2)}, ease: "none" }, ${(s + 0.5).toFixed(2)});
+ tl.to("#sc01in", { opacity: 0, duration: 0.6, ease: "power2.in" }, ${(d - 0.6).toFixed(2)});
+ tl.set("#sc01in", { opacity: 0 }, ${d.toFixed(2)});`;
+}
+
+// scenes 02..10: image shots
+for (const id of ORDER.slice(1, 10)) {
+ const s0 = sceneStart[id], s1 = sceneEnd[id], dur = s1 - s0;
+ const shots = shotsByScene[id];
+ const per = dur / shots.length;
+ // rounded shot boundaries so adjacent clips never overlap by float dust
+ const bounds = shots.map((_, i) => r2(s0 + i * per)).concat([s1]);
+ shots.forEach((sh, i) => {
+ const [name, h, label] = sh;
+ const cs = bounds[i], cd = r2(bounds[i + 1] - bounds[i]);
+ const cid = `s${id.slice(0, 2)}i${i}`;
+ clips += `
+ <section id="${cid}" class="clip shot" data-start="${cs.toFixed(2)}" data-duration="${cd.toFixed(2)}" data-track-index="1">
+ <img id="${cid}m" src="${IMG(name)}" alt=""/>
+ </section>`;
+ tw += `
+ tl.to("#${cid}", { opacity: 1, duration: 0.45, ease: "power2.out" }, ${cs.toFixed(2)});`;
+ if (h > 1120) {
+ const travel = Math.min(h - 1080, Math.max(60, 78 * cd));
+ tw += `
+ tl.fromTo("#${cid}m", { y: 0 }, { y: ${-Math.round(travel)}, duration: ${(cd - 0.2).toFixed(2)}, ease: "sine.inOut" }, ${(cs + 0.1).toFixed(2)});`;
+ } else {
+ const zin = i % 2 === 0;
+ tw += `
+ tl.fromTo("#${cid}m", { scale: ${zin ? 1 : 1.07}, transformOrigin: "50% 30%" }, { scale: ${zin ? 1.07 : 1}, duration: ${cd.toFixed(2)}, ease: "none" }, ${cs.toFixed(2)});`;
+ }
+ if (label) {
+ const ld = Math.min(4.5, cd - 0.4);
+ clips += `
+ <div class="clip sublabel" id="${cid}l" data-start="${(cs + 0.3).toFixed(2)}" data-duration="${ld.toFixed(2)}" data-track-index="3"><span>${esc(label)}</span></div>`;
+ tw += `
+ tl.from("#${cid}l", { opacity: 0, x: -18, duration: 0.4, ease: "power2.out" }, ${(cs + 0.3).toFixed(2)});
+ tl.to("#${cid}l", { opacity: 0, duration: 0.35 }, ${(cs + 0.3 + ld - 0.35).toFixed(2)});`;
+ }
+ });
+ // scene chip (lower-third)
+ const c = chips[id];
+ const chd = Math.min(7, dur - 1);
+ clips += `
+ <div class="clip chip" id="ch${id.slice(0, 2)}" data-start="${(s0 + 0.7).toFixed(2)}" data-duration="${chd.toFixed(2)}" data-track-index="4">
+ <div class="chipin" id="ch${id.slice(0, 2)}in"><div class="bar"></div><div class="txt"><b>${esc(c[0])}</b><span>${esc(c[1])}</span></div></div>
+ </div>`;
+ tw += `
+ tl.from("#ch${id.slice(0, 2)}", { opacity: 0, y: 26, duration: 0.55, ease: "power3.out" }, ${(s0 + 0.7).toFixed(2)});
+ tl.to("#ch${id.slice(0, 2)}in", { opacity: 0, duration: 0.4 }, ${(s0 + 0.7 + chd - 0.4).toFixed(2)});
+ tl.set("#ch${id.slice(0, 2)}in", { opacity: 0 }, ${(s0 + 0.7 + chd).toFixed(2)});`;
+}
+
+// scene 11: home glimpse then close card
+{
+ const s0 = sceneStart['11-close'];
+ const glimpse = 7.5;
+ clips += `
+ <section id="s11a" class="clip shot" data-start="${s0.toFixed(2)}" data-duration="${glimpse.toFixed(2)}" data-track-index="1">
+ <img id="s11am" src="${IMG('home')}" alt=""/>
+ </section>
+ <section id="s11b" class="clip card" data-start="${(s0 + glimpse).toFixed(2)}" data-duration="${(TOTAL - s0 - glimpse).toFixed(2)}" data-track-index="1">
+ <div class="cardin" id="s11bin">
+ <div class="kicker">THANK YOU, BOOMER — THESE ARE JUST SUGGESTIONS</div>
+ <h2>rentv-v1<span class="red">.agentabrams.com</span></h2>
+ <p class="logins">login <b>admin / DW2024!</b> or <b>Boomer / Rentv2024!</b></p>
+ <p class="sub2">Every headline you saw is live rentv.com data · built in a day · your call, always.</p>
+ </div>
+ </section>`;
+ tw += `
+ tl.to("#s11a", { opacity: 1, duration: 0.5 }, ${s0.toFixed(2)});
+ tl.fromTo("#s11am", { y: 0 }, { y: -420, duration: ${(glimpse - 0.2).toFixed(2)}, ease: "sine.inOut" }, ${(s0 + 0.1).toFixed(2)});
+ tl.to("#s11b", { opacity: 1, duration: 0.5 }, ${(s0 + glimpse).toFixed(2)});
+ tl.from("#s11bin", { opacity: 0, y: 36, duration: 1, ease: "power3.out" }, ${(s0 + glimpse + 0.1).toFixed(2)});
+ tl.fromTo("#s11bin", { scale: 1 }, { scale: 1.035, duration: ${(TOTAL - s0 - glimpse - 1).toFixed(2)}, ease: "none" }, ${(s0 + glimpse + 0.4).toFixed(2)});`;
+}
+
+const html = `<!doctype html>
+<html lang="en">
+<head>
+<meta charset="UTF-8"/>
+<meta name="viewport" content="width=1920, height=1080"/>
+<title>RENTV Recap for Boomer</title>
+<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
+<style>
+ html,body{margin:0;background:#0c0f13}
+ #root{position:relative;width:1920px;height:1080px;overflow:hidden;background:#0c0f13;
+ font-family:'Inter','Helvetica Neue',system-ui,sans-serif;color:#fff}
+ .clip{position:absolute}
+ .shot{inset:0;overflow:hidden;background:#0c0f13;opacity:0}
+ .shot img{display:block;width:1920px;height:auto;will-change:transform}
+ .card{inset:0;display:grid;place-items:center;opacity:0;background:
+ radial-gradient(1200px 700px at 50% 38%, #171d26 0%, #0c0f13 70%)}
+ .cardin{text-align:center;max-width:1400px}
+ .kicker{font-size:26px;letter-spacing:.42em;color:#c8102e;font-weight:800;margin-bottom:34px}
+ h1{font-family:Georgia,'Times New Roman',serif;font-size:170px;margin:0;font-weight:700;letter-spacing:.01em}
+ h2{font-family:Georgia,'Times New Roman',serif;font-size:96px;margin:0;font-weight:700}
+ .red{color:#c8102e}
+ .sub{font-size:34px;line-height:1.55;color:#aeb6c2;margin-top:38px;font-weight:300}
+ .sub2{font-size:28px;color:#aeb6c2;margin-top:26px;font-weight:300}
+ .logins{font-size:36px;margin-top:44px;color:#e8ecf2}
+ .logins b{color:#fff;background:#1a212c;border:1px solid #2c3644;border-radius:8px;padding:8px 18px}
+ .chip{left:80px;bottom:76px;display:flex;align-items:stretch;background:rgba(10,13,17,.82);
+ border:1px solid rgba(255,255,255,.14);border-radius:10px;overflow:hidden;
+ box-shadow:0 12px 40px rgba(0,0,0,.45)}
+ .chipin{display:flex;align-items:stretch}
+ .chip .bar{width:10px;background:#c8102e}
+ .chip .txt{padding:20px 30px}
+ .chip b{display:block;font-family:Georgia,serif;font-size:40px;font-weight:700}
+ .chip span{display:block;font-size:23px;color:#aeb6c2;margin-top:6px}
+ .sublabel{right:80px;top:64px}
+ .sublabel span{background:#c8102e;color:#fff;font-size:24px;font-weight:800;letter-spacing:.08em;
+ padding:12px 22px;border-radius:8px;box-shadow:0 10px 30px rgba(0,0,0,.4)}
+</style>
+</head>
+<body>
+<div id="root" data-composition-id="main" data-start="0" data-width="1920" data-height="1080" data-duration="${TOTAL}">
+${clips}
+</div>
+<audio id="vo" src="media/narration.wav" data-start="0" data-duration="${TOTAL}" data-track-index="10" data-volume="1"></audio>
+<script>
+ window.__timelines = window.__timelines || {};
+ const tl = gsap.timeline({ paused: true });
+${tw}
+ window.__timelines["main"] = tl;
+</script>
+</body>
+</html>
+`;
+writeFileSync(ROOT + 'index.html', html);
+console.log('TOTAL duration', TOTAL + 's');
+console.log(ORDER.map(id => `${id} @ ${sceneStart[id].toFixed(1)}s`).join('\n'));
diff --git a/recap-v2/scripts/tts.mjs b/recap-v2/scripts/tts.mjs
new file mode 100644
index 0000000..3947002
--- /dev/null
+++ b/recap-v2/scripts/tts.mjs
@@ -0,0 +1,32 @@
+#!/usr/bin/env node
+/* Parse SCRIPT.md blocks → ElevenLabs (Steve clone) via clone-voice say.sh →
+ media/vo/NN-name.wav + media/vo/durations.json */
+import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
+import { execFileSync } from 'node:child_process';
+
+const ROOT = new URL('..', import.meta.url).pathname;
+const OUT = ROOT + 'media/vo/';
+mkdirSync(OUT, { recursive: true });
+
+const md = readFileSync(ROOT + 'SCRIPT.md', 'utf8');
+const blocks = [...md.matchAll(/^## (\d\d-[a-z]+)[^\n]*\n\n([\s\S]*?)(?=\n## |\n*$)/gm)]
+ .map(m => [m[1], m[2].trim().replace(/\s+/g, ' ')]);
+
+const SAY = process.env.HOME + '/.claude/skills/clone-voice/scripts/say.sh';
+const durations = {};
+let chars = 0;
+for (const [id, text] of blocks) {
+ chars += text.length;
+ const wav = OUT + id + '.wav';
+ execFileSync('bash', [SAY, text, wav, 'elevenlabs'], {
+ stdio: ['ignore', 'inherit', 'inherit'],
+ env: { ...process.env, PATH: '/opt/homebrew/bin:' + process.env.PATH },
+ });
+ const d = parseFloat(execFileSync('/opt/homebrew/bin/ffprobe',
+ ['-v', 'error', '-show_entries', 'format=duration', '-of', 'csv=p=0', wav]).toString());
+ durations[id] = Math.round(d * 100) / 100;
+ console.log(id, durations[id] + 's', text.length + 'ch');
+}
+writeFileSync(OUT + 'durations.json', JSON.stringify(durations, null, 2));
+const total = Object.values(durations).reduce((a, b) => a + b, 0);
+console.log('TOTAL', Math.round(total) + 's', chars + ' chars');
← 263c1a0 auto-save: 2026-07-22T13:44:34 (1 files) — recap-v2/
·
back to Rentv Promos
·
(newest)