Cookbook — Video Recipe layout (steps synced)
An editorial cook-along page that pairs a large faux video player with a synced list of recipe steps. A simulated playhead advances a timecode, highlights the matching chapter in real time, and animates rising steam over a warm gradient poster. Click any step to seek the player to its timestamp, drag the scrubber across chapter markers, change playback speed, and watch completed steps mark themselves done as the braise progresses from spice to finish.
MCP
Code
:root {
--cream: #faf6ef;
--paper: #fffdf8;
--ink: #2b2622;
--ink-2: #5c534a;
--muted: #8a7f73;
--tomato: #d6452b;
--tomato-d: #b8351e;
--saffron: #e8a33d;
--sage: #7c8a6b;
--clay: #c8775a;
--line: rgba(43, 38, 34, 0.12);
--line-2: rgba(43, 38, 34, 0.2);
--ok: #3f8f5f;
--warn: #d98a2b;
--danger: #c8412b;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 22px;
--sh-sm: 0 1px 2px rgba(43, 38, 34, 0.1);
--sh-lg: 0 10px 30px rgba(43, 38, 34, 0.1);
--serif: "Fraunces", Georgia, serif;
--sans: "Inter", system-ui, -apple-system, sans-serif;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
body {
margin: 0;
font-family: var(--sans);
line-height: 1.6;
color: var(--ink);
background: var(--cream);
background-image: radial-gradient(
1200px 600px at 100% -10%,
rgba(232, 163, 61, 0.08),
transparent 60%
),
radial-gradient(900px 500px at -10% 110%, rgba(214, 69, 43, 0.06), transparent 60%);
min-height: 100vh;
}
/* ---------- Topbar ---------- */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem clamp(1rem, 4vw, 2.5rem);
border-bottom: 1px solid var(--line);
background: rgba(255, 253, 248, 0.8);
backdrop-filter: blur(8px);
position: sticky;
top: 0;
z-index: 20;
}
.brand {
display: flex;
align-items: center;
gap: 0.55rem;
}
.brand-mark {
font-size: 1.35rem;
filter: saturate(1.1);
}
.brand-name {
font-family: var(--serif);
font-weight: 600;
font-size: 1.15rem;
letter-spacing: 0.01em;
}
.topnav {
display: flex;
gap: 0.35rem;
}
.topnav-link {
text-decoration: none;
color: var(--ink-2);
font-weight: 500;
font-size: 0.92rem;
padding: 0.4rem 0.7rem;
border-radius: var(--r-sm);
transition: background 0.18s, color 0.18s;
}
.topnav-link:hover {
background: rgba(43, 38, 34, 0.05);
color: var(--ink);
}
.topnav-link.is-active {
color: var(--tomato-d);
background: rgba(214, 69, 43, 0.08);
}
/* ---------- Layout ---------- */
.layout {
max-width: 1200px;
margin: 0 auto;
padding: clamp(1.25rem, 4vw, 2.75rem) clamp(1rem, 4vw, 2.5rem) 3rem;
display: grid;
grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
gap: clamp(1.5rem, 3vw, 2.75rem);
align-items: start;
}
/* ---------- Player column ---------- */
.kicker {
margin: 0 0 0.4rem;
text-transform: uppercase;
letter-spacing: 0.16em;
font-size: 0.72rem;
font-weight: 600;
color: var(--tomato);
}
.title {
font-family: var(--serif);
font-weight: 600;
font-size: clamp(1.9rem, 4.5vw, 2.9rem);
line-height: 1.08;
margin: 0 0 0.5rem;
letter-spacing: -0.01em;
}
.byline {
margin: 0 0 1.4rem;
color: var(--ink-2);
max-width: 52ch;
}
.dot {
color: var(--muted);
margin: 0 0.15rem;
}
/* ---------- Player ---------- */
.player {
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 0.55rem;
box-shadow: var(--sh-lg);
}
.stage {
position: relative;
aspect-ratio: 16 / 9;
border-radius: var(--r-md);
overflow: hidden;
background: #2a1f18;
cursor: pointer;
isolation: isolate;
}
.poster {
position: absolute;
inset: 0;
background: linear-gradient(135deg, #7a2516 0%, #b8351e 42%, #e8a33d 100%);
}
.poster::after {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(
120% 90% at 30% 20%,
rgba(255, 240, 210, 0.35),
transparent 55%
),
radial-gradient(80% 70% at 85% 95%, rgba(43, 16, 8, 0.6), transparent 60%);
}
.poster-blob {
position: absolute;
border-radius: 50%;
filter: blur(2px);
mix-blend-mode: screen;
opacity: 0.85;
}
.blob-a {
width: 46%;
height: 60%;
left: 8%;
top: 22%;
background: radial-gradient(circle at 35% 30%, #ffcf6e, #d6452b 70%, transparent 72%);
}
.blob-b {
width: 30%;
height: 38%;
right: 12%;
top: 14%;
background: radial-gradient(circle at 40% 40%, #ff8a5b, #b8351e 72%, transparent 74%);
}
.blob-c {
width: 28%;
height: 34%;
right: 22%;
bottom: 10%;
background: radial-gradient(circle at 50% 40%, #f7d27a, #c8775a 70%, transparent 73%);
}
.poster-emoji {
position: absolute;
font-size: clamp(1.6rem, 4vw, 2.6rem);
filter: drop-shadow(0 4px 8px rgba(43, 16, 8, 0.45));
z-index: 2;
}
.poster-emoji.e1 {
left: 18%;
top: 40%;
font-size: clamp(2.4rem, 7vw, 4.5rem);
}
.poster-emoji.e2 {
right: 20%;
top: 24%;
transform: rotate(-12deg);
}
.poster-emoji.e3 {
right: 30%;
bottom: 18%;
}
/* steam — animates when playing */
.poster-steam {
position: absolute;
bottom: 38%;
left: 24%;
width: 8px;
height: 60px;
border-radius: 8px;
background: linear-gradient(to top, rgba(255, 255, 255, 0.45), transparent);
opacity: 0;
filter: blur(3px);
z-index: 2;
}
.poster-steam.s2 {
left: 30%;
height: 80px;
}
.poster-steam.s3 {
left: 36%;
height: 50px;
}
.stage[data-state="playing"] .poster-steam {
animation: steam 2.4s ease-in-out infinite;
}
.stage[data-state="playing"] .poster-steam.s2 {
animation-delay: 0.5s;
}
.stage[data-state="playing"] .poster-steam.s3 {
animation-delay: 1s;
}
@keyframes steam {
0% {
opacity: 0;
transform: translateY(10px) scaleY(0.6);
}
40% {
opacity: 0.7;
}
100% {
opacity: 0;
transform: translateY(-40px) scaleY(1.2);
}
}
.stage-chapter {
position: absolute;
left: 1rem;
top: 1rem;
z-index: 3;
display: flex;
align-items: baseline;
gap: 0.5rem;
padding: 0.45rem 0.75rem;
background: rgba(43, 16, 8, 0.55);
backdrop-filter: blur(6px);
border-radius: 999px;
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.18);
max-width: 70%;
}
.stage-chapter-no {
font-family: var(--serif);
font-weight: 600;
color: var(--saffron);
}
.stage-chapter-label {
font-size: 0.85rem;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bigplay {
position: absolute;
inset: 0;
margin: auto;
width: 84px;
height: 84px;
border-radius: 50%;
border: none;
background: rgba(255, 253, 248, 0.92);
color: var(--tomato-d);
font-size: 1.8rem;
cursor: pointer;
z-index: 4;
display: grid;
place-items: center;
box-shadow: 0 10px 30px rgba(43, 16, 8, 0.4);
transition: transform 0.18s, opacity 0.25s, background 0.18s;
}
.bigplay:hover {
transform: scale(1.06);
background: #fff;
}
.bigplay-icon {
margin-left: 4px;
}
.stage[data-state="playing"] .bigplay {
opacity: 0;
pointer-events: none;
transform: scale(0.7);
}
.livecode {
position: absolute;
right: 0.9rem;
bottom: 0.9rem;
z-index: 3;
font-variant-numeric: tabular-nums;
font-weight: 600;
font-size: 0.8rem;
color: #fff;
background: rgba(43, 16, 8, 0.55);
padding: 0.25rem 0.55rem;
border-radius: var(--r-sm);
backdrop-filter: blur(6px);
}
/* ---------- Controls ---------- */
.controls {
display: flex;
align-items: center;
gap: 0.7rem;
padding: 0.7rem 0.5rem 0.35rem;
}
.ctrl-btn {
flex: none;
width: 40px;
height: 40px;
border-radius: 50%;
border: 1px solid var(--line);
background: var(--cream);
color: var(--ink);
cursor: pointer;
font-size: 0.85rem;
display: grid;
place-items: center;
transition: background 0.16s, border-color 0.16s, transform 0.12s;
}
.ctrl-btn:hover {
background: rgba(214, 69, 43, 0.08);
border-color: var(--line-2);
}
.ctrl-btn:active {
transform: scale(0.94);
}
.ctrl-rate {
width: auto;
padding: 0 0.7rem;
border-radius: 999px;
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.ico-pause {
display: none;
font-size: 0.7rem;
letter-spacing: 1px;
}
.stage[data-state="playing"] ~ .controls .ico-play,
[data-playing="true"] .ico-play {
display: none;
}
[data-playing="true"] .ico-pause {
display: inline;
}
.time {
font-variant-numeric: tabular-nums;
font-size: 0.82rem;
font-weight: 600;
color: var(--ink-2);
flex: none;
min-width: 3ch;
}
.time-total {
color: var(--muted);
}
/* scrubber */
.scrub {
flex: 1;
padding: 0.6rem 0;
cursor: pointer;
outline: none;
}
.scrub-track {
position: relative;
height: 6px;
border-radius: 999px;
background: var(--line);
}
.scrub-buffer {
position: absolute;
inset: 0;
width: 80%;
border-radius: 999px;
background: rgba(43, 38, 34, 0.16);
}
.scrub-fill {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 0%;
border-radius: 999px;
background: linear-gradient(90deg, var(--saffron), var(--tomato));
}
.scrub-markers {
position: absolute;
inset: 0;
}
.chapter-mark {
position: absolute;
top: 50%;
width: 3px;
height: 12px;
transform: translate(-50%, -50%);
background: var(--paper);
border: 1px solid var(--line-2);
border-radius: 2px;
}
.scrub-thumb {
position: absolute;
top: 50%;
left: 0%;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--tomato);
border: 3px solid var(--paper);
box-shadow: var(--sh-sm);
transform: translate(-50%, -50%);
transition: transform 0.12s;
}
.scrub:hover .scrub-thumb,
.scrub:focus-visible .scrub-thumb {
transform: translate(-50%, -50%) scale(1.18);
}
.scrub:focus-visible .scrub-track {
outline: 2px solid var(--tomato);
outline-offset: 4px;
}
/* player meta chips */
.player-meta {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1.1rem;
}
.chip {
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-size: 0.82rem;
font-weight: 500;
color: var(--ink-2);
background: var(--paper);
border: 1px solid var(--line);
border-radius: 999px;
padding: 0.35rem 0.7rem;
}
/* ---------- Steps column ---------- */
.steps-col {
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 1.25rem;
box-shadow: var(--sh-sm);
position: sticky;
top: 84px;
}
.steps-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 0.5rem;
padding-bottom: 0.75rem;
margin-bottom: 0.5rem;
border-bottom: 1px solid var(--line);
}
.steps-title {
font-family: var(--serif);
font-weight: 600;
font-size: 1.25rem;
margin: 0;
}
.steps-hint {
font-size: 0.72rem;
color: var(--muted);
font-weight: 500;
}
.steps {
list-style: none;
margin: 0;
padding: 0;
counter-reset: none;
max-height: 60vh;
overflow-y: auto;
scroll-behavior: smooth;
}
.step {
margin: 0;
}
.step + .step {
border-top: 1px solid var(--line);
}
.step-btn {
display: grid;
grid-template-columns: auto auto 1fr;
align-items: start;
gap: 0.6rem 0.75rem;
width: 100%;
text-align: left;
background: transparent;
border: none;
border-left: 3px solid transparent;
padding: 0.85rem 0.5rem 0.85rem 0.7rem;
cursor: pointer;
border-radius: 0 var(--r-sm) var(--r-sm) 0;
transition: background 0.16s, border-color 0.16s;
font-family: inherit;
}
.step-btn:hover {
background: rgba(232, 163, 61, 0.08);
}
.step-btn:focus-visible {
outline: 2px solid var(--tomato);
outline-offset: -2px;
}
.step-time {
font-variant-numeric: tabular-nums;
font-size: 0.74rem;
font-weight: 600;
color: var(--muted);
padding-top: 0.15rem;
min-width: 3ch;
}
.step-no {
font-family: var(--serif);
font-weight: 600;
font-size: 0.95rem;
color: var(--ink-2);
width: 1.9rem;
height: 1.9rem;
display: grid;
place-items: center;
border-radius: 50%;
border: 1px solid var(--line);
background: var(--cream);
transition: background 0.16s, color 0.16s, border-color 0.16s;
}
.step-body {
display: block;
min-width: 0;
}
.step-name {
display: block;
font-weight: 600;
font-size: 0.95rem;
color: var(--ink);
line-height: 1.35;
}
.step-text {
display: block;
font-size: 0.86rem;
color: var(--ink-2);
margin-top: 0.15rem;
}
/* active step */
.step.is-active .step-btn {
background: rgba(214, 69, 43, 0.08);
border-left-color: var(--tomato);
}
.step.is-active .step-no {
background: var(--tomato);
color: #fff;
border-color: var(--tomato);
}
.step.is-active .step-time {
color: var(--tomato-d);
}
.step.is-done .step-no {
background: var(--sage);
color: #fff;
border-color: var(--sage);
}
.step.is-done .step-name {
color: var(--ink-2);
}
.legal {
margin: 1rem 0 0;
padding-top: 0.85rem;
border-top: 1px dashed var(--line-2);
font-size: 0.74rem;
color: var(--muted);
font-style: italic;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 1.5rem;
transform: translate(-50%, 1.5rem);
background: var(--ink);
color: var(--paper);
padding: 0.7rem 1.1rem;
border-radius: 999px;
font-size: 0.88rem;
font-weight: 500;
box-shadow: var(--sh-lg);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s, transform 0.22s;
z-index: 50;
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---------- Responsive ---------- */
@media (max-width: 880px) {
.layout {
grid-template-columns: 1fr;
}
.steps-col {
position: static;
}
.steps {
max-height: none;
}
}
@media (max-width: 560px) {
.topnav {
display: none;
}
.stage-chapter {
max-width: 60%;
}
.controls {
gap: 0.45rem;
}
.ctrl-rate {
padding: 0 0.5rem;
}
}
@media (prefers-reduced-motion: reduce) {
.poster-steam,
.bigplay,
.scrub-thumb {
animation: none !important;
transition: none !important;
}
}(function () {
"use strict";
var DURATION = 560; // total seconds (9:20)
var current = 0; // current playback time, seconds
var playing = false;
var rates = [1, 1.25, 1.5, 2];
var rateIndex = 0;
var timer = null;
var lastTick = 0;
var stage = document.getElementById("stage");
var bigPlay = document.getElementById("bigPlay");
var playToggle = document.getElementById("playToggle");
var rateBtn = document.getElementById("rateBtn");
var scrub = document.getElementById("scrub");
var scrubFill = document.getElementById("scrubFill");
var scrubThumb = document.getElementById("scrubThumb");
var scrubMarkers = document.getElementById("scrubMarkers");
var timeCurrent = document.getElementById("timeCurrent");
var liveTime = document.getElementById("liveTime");
var stageChapter = document.getElementById("stageChapter");
var stageChapterNo = stageChapter.querySelector(".stage-chapter-no");
var stageChapterLabel = stageChapter.querySelector(".stage-chapter-label");
var toastEl = document.getElementById("toast");
var stepEls = Array.prototype.slice.call(
document.querySelectorAll("#steps .step")
);
var steps = stepEls.map(function (el) {
return {
el: el,
btn: el.querySelector(".step-btn"),
start: parseInt(el.getAttribute("data-start"), 10),
end: parseInt(el.getAttribute("data-end"), 10),
name: el.querySelector(".step-name").textContent.trim(),
no: el.querySelector(".step-no").textContent.trim(),
};
});
var activeIndex = -1;
var toastTimer = null;
function fmt(sec) {
sec = Math.max(0, Math.round(sec));
var m = Math.floor(sec / 60);
var s = sec % 60;
return m + ":" + (s < 10 ? "0" : "") + s;
}
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 1800);
}
// Build chapter markers on the scrubber.
steps.forEach(function (step) {
var mark = document.createElement("span");
mark.className = "chapter-mark";
mark.style.left = (step.start / DURATION) * 100 + "%";
mark.title = step.name;
scrubMarkers.appendChild(mark);
});
function stepIndexAt(t) {
for (var i = steps.length - 1; i >= 0; i--) {
if (t >= steps[i].start) return i;
}
return 0;
}
function render() {
var pct = (current / DURATION) * 100;
scrubFill.style.width = pct + "%";
scrubThumb.style.left = pct + "%";
timeCurrent.textContent = fmt(current);
liveTime.textContent = fmt(current);
scrub.setAttribute("aria-valuenow", Math.round(pct));
scrub.setAttribute(
"aria-valuetext",
fmt(current) + " of " + fmt(DURATION)
);
var idx = stepIndexAt(current);
if (idx !== activeIndex) {
setActive(idx);
}
}
function setActive(idx) {
activeIndex = idx;
steps.forEach(function (step, i) {
var on = i === idx;
step.el.classList.toggle("is-active", on);
step.el.classList.toggle("is-done", i < idx);
if (on) {
step.btn.setAttribute("aria-current", "step");
// keep active step in view within the scroll container
if (typeof step.el.scrollIntoView === "function") {
step.el.scrollIntoView({ block: "nearest" });
}
} else {
step.btn.removeAttribute("aria-current");
}
});
var s = steps[idx];
stageChapterNo.textContent = s.no;
stageChapterLabel.textContent = s.name;
}
function play() {
if (playing) return;
if (current >= DURATION) current = 0;
playing = true;
stage.setAttribute("data-state", "playing");
playToggle.setAttribute("aria-pressed", "true");
playToggle.setAttribute("aria-label", "Pause");
playToggle.setAttribute("data-playing", "true");
liveTime.setAttribute("aria-live", "polite");
lastTick = performance.now();
loop();
}
function pause() {
if (!playing) return;
playing = false;
stage.setAttribute("data-state", "paused");
playToggle.setAttribute("aria-pressed", "false");
playToggle.setAttribute("aria-label", "Play");
playToggle.removeAttribute("data-playing");
liveTime.setAttribute("aria-live", "off");
if (timer) cancelAnimationFrame(timer);
}
function toggle() {
playing ? pause() : play();
}
function loop() {
timer = requestAnimationFrame(function (now) {
var dt = ((now - lastTick) / 1000) * rates[rateIndex];
lastTick = now;
current += dt;
if (current >= DURATION) {
current = DURATION;
render();
pause();
toast("Recipe complete — buen provecho! 🍽️");
return;
}
render();
loop();
});
}
function seek(t) {
current = Math.min(DURATION, Math.max(0, t));
render();
}
function seekFromClientX(clientX) {
var rect = scrub.getBoundingClientRect();
var ratio = (clientX - rect.left) / rect.width;
seek(ratio * DURATION);
}
// --- Wire up controls ---
bigPlay.addEventListener("click", function (e) {
e.stopPropagation();
play();
});
stage.addEventListener("click", toggle);
playToggle.addEventListener("click", toggle);
rateBtn.addEventListener("click", function () {
rateIndex = (rateIndex + 1) % rates.length;
var r = rates[rateIndex];
rateBtn.textContent = r + "×";
rateBtn.setAttribute("aria-label", "Playback speed, currently " + r + "x");
toast("Speed " + r + "×");
});
// Scrubber: click + drag
var dragging = false;
scrub.addEventListener("pointerdown", function (e) {
dragging = true;
scrub.setPointerCapture(e.pointerId);
seekFromClientX(e.clientX);
});
scrub.addEventListener("pointermove", function (e) {
if (dragging) seekFromClientX(e.clientX);
});
scrub.addEventListener("pointerup", function (e) {
dragging = false;
try {
scrub.releasePointerCapture(e.pointerId);
} catch (err) {}
});
// Scrubber keyboard
scrub.addEventListener("keydown", function (e) {
var step = e.shiftKey ? 30 : 5;
if (e.key === "ArrowRight" || e.key === "ArrowUp") {
seek(current + step);
e.preventDefault();
} else if (e.key === "ArrowLeft" || e.key === "ArrowDown") {
seek(current - step);
e.preventDefault();
} else if (e.key === "Home") {
seek(0);
e.preventDefault();
} else if (e.key === "End") {
seek(DURATION);
e.preventDefault();
} else if (e.key === " " || e.key === "Enter") {
toggle();
e.preventDefault();
}
});
// Click a step to seek
steps.forEach(function (step) {
step.btn.addEventListener("click", function () {
seek(step.start);
toast("Jumped to " + fmt(step.start) + " · " + step.name);
});
});
// Keyboard shortcuts (space anywhere not in a control)
document.addEventListener("keydown", function (e) {
if (e.key === " " && e.target === document.body) {
toggle();
e.preventDefault();
}
});
// initial paint
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Saffron Butter Chicken — Video Recipe</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,[email protected],500;9..144,600;9..144,700&family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<header class="topbar" role="banner">
<div class="brand">
<span class="brand-mark" aria-hidden="true">🍅</span>
<span class="brand-name">The Slow Table</span>
</div>
<nav class="topnav" aria-label="Primary">
<a href="#" class="topnav-link">Recipes</a>
<a href="#" class="topnav-link">Technique</a>
<a href="#" class="topnav-link is-active" aria-current="page">Watch</a>
</nav>
</header>
<main class="layout" role="main">
<article class="player-col">
<p class="kicker">Watch & Cook · Episode 14</p>
<h1 class="title">Saffron Butter Chicken</h1>
<p class="byline">
A weeknight braise with toasted spice, blistered tomato and a slow
finish of cultured butter. <span class="dot">·</span> 42 min
<span class="dot">·</span> Serves 4
</p>
<!-- Faux video player -->
<section
class="player"
aria-label="Recipe video player"
aria-roledescription="video player"
>
<div class="stage" id="stage" data-state="paused">
<div class="poster" aria-hidden="true">
<span class="poster-blob blob-a"></span>
<span class="poster-blob blob-b"></span>
<span class="poster-blob blob-c"></span>
<span class="poster-emoji e1">🍗</span>
<span class="poster-emoji e2">🌿</span>
<span class="poster-emoji e3">🍅</span>
<span class="poster-steam s1"></span>
<span class="poster-steam s2"></span>
<span class="poster-steam s3"></span>
</div>
<div class="stage-chapter" id="stageChapter" aria-hidden="true">
<span class="stage-chapter-no">01</span>
<span class="stage-chapter-label">Toast the spices</span>
</div>
<button
class="bigplay"
id="bigPlay"
type="button"
aria-label="Play video"
>
<span class="bigplay-icon" aria-hidden="true">▶</span>
</button>
<div class="livecode" id="liveTime" aria-live="off">0:00</div>
</div>
<!-- Controls -->
<div class="controls">
<button
class="ctrl-btn"
id="playToggle"
type="button"
aria-label="Play"
aria-pressed="false"
>
<span class="ico-play" aria-hidden="true">▶</span>
<span class="ico-pause" aria-hidden="true">❚❚</span>
</button>
<span class="time time-current" id="timeCurrent">0:00</span>
<div
class="scrub"
id="scrub"
role="slider"
tabindex="0"
aria-label="Seek video"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="0"
aria-valuetext="0 seconds of 9 minutes 20 seconds"
>
<div class="scrub-track">
<div class="scrub-buffer"></div>
<div class="scrub-fill" id="scrubFill"></div>
<div class="scrub-markers" id="scrubMarkers" aria-hidden="true"></div>
<div class="scrub-thumb" id="scrubThumb"></div>
</div>
</div>
<span class="time time-total" id="timeTotal">9:20</span>
<button
class="ctrl-btn ctrl-rate"
id="rateBtn"
type="button"
aria-label="Playback speed, currently 1x"
>
1×
</button>
</div>
</section>
<div class="player-meta">
<span class="chip"><span aria-hidden="true">🔥</span> Stovetop</span>
<span class="chip"><span aria-hidden="true">🧈</span> Cultured butter</span>
<span class="chip"><span aria-hidden="true">🌶️</span> Mild heat</span>
<span class="chip"><span aria-hidden="true">⏱️</span> 9 chapters</span>
</div>
</article>
<!-- Synced steps -->
<aside class="steps-col" aria-label="Recipe steps">
<div class="steps-head">
<h2 class="steps-title">Chapters & Steps</h2>
<span class="steps-hint">Click a step to jump there</span>
</div>
<ol class="steps" id="steps">
<li class="step" data-start="0" data-end="55">
<button class="step-btn" type="button">
<span class="step-time">0:00</span>
<span class="step-no">01</span>
<span class="step-body">
<span class="step-name">Toast the whole spices</span>
<span class="step-text"
>Dry-toast cumin, coriander and 4 green cardamom pods until
fragrant, about 90 seconds.</span
>
</span>
</button>
</li>
<li class="step" data-start="55" data-end="135">
<button class="step-btn" type="button">
<span class="step-time">0:55</span>
<span class="step-no">02</span>
<span class="step-body">
<span class="step-name">Bloom the saffron</span>
<span class="step-text"
>Steep a generous pinch of saffron in 3 tbsp warm milk; set
aside to turn deep amber.</span
>
</span>
</button>
</li>
<li class="step" data-start="135" data-end="215">
<button class="step-btn" type="button">
<span class="step-time">2:15</span>
<span class="step-no">03</span>
<span class="step-body">
<span class="step-name">Sear the chicken thighs</span>
<span class="step-text"
>Brown 800 g bone-in thighs in ghee, skin down, until deeply
golden. Rest on a plate.</span
>
</span>
</button>
</li>
<li class="step" data-start="215" data-end="300">
<button class="step-btn" type="button">
<span class="step-time">3:35</span>
<span class="step-no">04</span>
<span class="step-body">
<span class="step-name">Soften onion & garlic</span>
<span class="step-text"
>Cook 2 sliced onions low and slow, then add grated garlic and
ginger until jammy.</span
>
</span>
</button>
</li>
<li class="step" data-start="300" data-end="380">
<button class="step-btn" type="button">
<span class="step-time">5:00</span>
<span class="step-no">05</span>
<span class="step-body">
<span class="step-name">Blister the tomatoes</span>
<span class="step-text"
>Add 6 halved tomatoes and the toasted spices; cook until they
collapse into a rough sauce.</span
>
</span>
</button>
</li>
<li class="step" data-start="380" data-end="445">
<button class="step-btn" type="button">
<span class="step-time">6:20</span>
<span class="step-no">06</span>
<span class="step-body">
<span class="step-name">Braise & reduce</span>
<span class="step-text"
>Return the chicken, pour in 200 ml stock, cover and simmer 20
minutes until tender.</span
>
</span>
</button>
</li>
<li class="step" data-start="445" data-end="510">
<button class="step-btn" type="button">
<span class="step-time">7:25</span>
<span class="step-no">07</span>
<span class="step-body">
<span class="step-name">Finish with butter & saffron</span>
<span class="step-text"
>Swirl in 60 g cold cultured butter and the saffron milk;
season and let it emulsify.</span
>
</span>
</button>
</li>
<li class="step" data-start="510" data-end="560">
<button class="step-btn" type="button">
<span class="step-time">8:30</span>
<span class="step-no">08</span>
<span class="step-body">
<span class="step-name">Garnish & serve</span>
<span class="step-text"
>Scatter torn coriander and a squeeze of lemon; serve with warm
flatbread.</span
>
</span>
</button>
</li>
</ol>
<p class="legal">
Illustrative UI only — recipe & nutrition data are fictional, not
dietary advice.
</p>
</aside>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Video Recipe layout (steps synced)
A two-column cook-along built for watching and cooking at once. On the left, a large faux video player shows a gradient “food photography” poster with blurred radial blobs, food emoji and animated steam that rises only while playback is running. A play overlay, scrubber with chapter markers, live timecode and a cycling speed control round out the player chrome — all rendered in the warm cookbook palette with an editorial serif/sans pairing.
On the right, the chapters-and-steps rail stays synced to the simulated playhead. As the timecode advances, the current step highlights with aria-current="step", earlier steps mark themselves done in sage green, and the active chapter scrolls into view. The player badge mirrors the active chapter number and title so you always know where you are in the dish.
Every control works in vanilla JS: play / pause advances a real timecode via requestAnimationFrame, clicking a step seeks the player to its timestamp, the scrubber supports click, drag and keyboard seeking (arrows, Home / End, space to toggle), and the speed button cycles 1× → 2×. Chapter markers are computed from each step’s start time and laid over the scrub track.
Illustrative UI only — recipes & nutrition data are fictional, not dietary advice.