diff --git a/demo/index.html b/demo/index.html
index 7be1a85f8..159f8352e 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -426,9 +426,9 @@
.story-cta .primary { background: #10b981; color: #000; }
.story-cta .ghost { border: 1px solid rgb(var(--c-dark-700)); color: rgb(var(--c-fg)); background: rgb(var(--c-dark-900) / 0.6); }
/* Scripted mode: the pin track is tall and the stage sticks while scroll drives the steps. */
- .js-story .story-intro { display: block; height: 150vh; padding: 0; }
+ .js-story .story-intro { display: block; height: 120vh; padding: 0; }
.js-story .story-intro > div { position: sticky; top: 0; height: 100svh; display: flex; flex-direction: column; justify-content: center; align-items: flex-start; max-width: 58rem; margin: 0 auto; padding: 5rem 1.5rem 2rem; }
- .js-story .story-pin { height: 340vh; }
+ .js-story .story-pin { height: 250vh; }
.js-story .story-stage { position: sticky; top: 0; height: 100svh; min-height: 0; padding-top: 5.5rem; padding-bottom: 2rem; }
.story-hint { display: none; }
.js-story .story-hint { display: flex; align-items: center; gap: 0.5rem; margin: 2rem 0 0; font: 500 0.8rem/1 'JetBrains Mono', 'JetBrains Fallback', monospace; letter-spacing: 0.08em; text-transform: uppercase; color: rgb(var(--c-slate-400)); transition: opacity 0.4s; }
@@ -3117,7 +3117,7 @@
${res.title}
var rows = story.querySelectorAll('.story-row');
var tagged = story.querySelectorAll('[data-for]');
var hint = document.getElementById('story-hint');
- var step = -1, ticking = false, timed = 0;
+ var step = -1, ticking = false, timed = 0, auto = -1, autoTimer = 0;
// The line also lights on a timer, so a fast scroll cannot skip it and nobody has to scroll to read it.
var timer = setTimeout(function tick() {
timed++;
@@ -3137,6 +3137,17 @@ ${res.title}
var pr = pin.getBoundingClientRect();
var t = Math.min(0.9999, Math.max(0, -pr.top / (pr.height - vh)));
var n = pr.top > vh * 0.4 ? -1 : Math.floor(t * 5);
+ // Once the stage is on screen the rows also play on a timer, so nobody has to keep scrolling to see the whole runbook.
+ // Scrolling still wins whenever the visitor is ahead of the timer.
+ if (pr.top > vh * 0.9) auto = -1;
+ if (pr.top <= vh * 0.4 && pr.bottom >= vh * 0.6) {
+ if (auto < 4 && !autoTimer) autoTimer = setTimeout(function () { autoTimer = 0; auto++; frame(); }, auto < 0 ? 700 : 1100);
+ n = Math.max(n, auto);
+ } else if (autoTimer) {
+ // Off the stage: drop a pending tick so it cannot advance the rows while nobody is looking at them.
+ clearTimeout(autoTimer);
+ autoTimer = 0;
+ }
if (n === step) return;
step = n;
rows.forEach(function (row, i) { row.classList.toggle('on', i <= n); });
diff --git a/tests/homepage.spec.ts b/tests/homepage.spec.ts
index 5f4362799..68525b005 100644
--- a/tests/homepage.spec.ts
+++ b/tests/homepage.spec.ts
@@ -363,6 +363,15 @@ test.describe('Homepage', () => {
await expect(page.locator('.story-verdict .v-block')).not.toHaveClass(/show/);
});
+ test('the runbook rows play on their own once the stage is on screen', async ({ page }) => {
+ await page.goto('/', { waitUntil: 'domcontentloaded' });
+ const [top, vh] = await page.locator('.story-pin').evaluate((el) => [el.getBoundingClientRect().top + window.scrollY, window.innerHeight]);
+ // Park just inside the stage and do not scroll again.
+ await page.evaluate((y) => window.scrollTo(0, y), top - vh * 0.3);
+ await expect(page.locator('.story-row.on')).toHaveCount(5, { timeout: 8_000 });
+ await expect(page.locator('.story-verdict .v-allow')).toHaveClass(/show/);
+ });
+
test('a scroll hint shows at the top and goes away once the visitor scrolls', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded' });
const hint = page.locator('#story-hint');