Coach — Video Lesson
An energetic dark-mode video lesson player for fitness coaching, pairing a 16:9 video placeholder with a live progress bar, chapter cues, and a synced step-by-step instruction list. Track sets and reps with a working counter, tick off cues as you master them, mark the whole exercise complete with a glowing lime CTA, and jump straight to the next move in the program without leaving the screen.
MCP
Code
:root {
--bg: #0f1115;
--surface: #171a21;
--surface-2: #1f232c;
--lime: #c2f542;
--lime-d: #a3d62f;
--orange: #ff6b35;
--ink: #f2f4f0;
--muted: #9aa0a6;
--line: rgba(255, 255, 255, 0.10);
--line-2: rgba(255, 255, 255, 0.18);
--white: #fff;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
min-height: 100vh;
font-family: Inter, system-ui, sans-serif;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(900px 500px at 15% -10%, rgba(194, 245, 66, 0.10), transparent 60%),
radial-gradient(800px 500px at 100% 0%, rgba(255, 107, 53, 0.10), transparent 55%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.wrap {
max-width: 1080px;
margin: 0 auto;
padding: 28px 20px 48px;
}
/* ---------- Header ---------- */
.crumbs {
display: flex;
gap: 8px;
align-items: center;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--muted);
}
.crumbs .program { color: var(--lime); }
.crumbs .sep { opacity: 0.4; }
.lesson-title-row {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 16px;
margin-top: 10px;
}
.eyebrow {
margin: 0 0 4px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--orange);
}
.lesson-title-row h1 {
margin: 0;
font-size: clamp(26px, 5vw, 40px);
font-weight: 800;
letter-spacing: -0.01em;
text-transform: uppercase;
line-height: 1.05;
}
.badges { display: flex; gap: 8px; flex-shrink: 0; }
.badge {
padding: 6px 12px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.03em;
white-space: nowrap;
}
.badge-focus {
background: rgba(194, 245, 66, 0.14);
color: var(--lime);
border: 1px solid rgba(194, 245, 66, 0.35);
}
.badge-diff {
background: var(--surface-2);
color: var(--ink);
border: 1px solid var(--line-2);
}
/* ---------- Grid ---------- */
.grid {
display: grid;
grid-template-columns: 1fr 300px;
gap: 18px;
margin-top: 22px;
}
/* ---------- Video stage ---------- */
.stage {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 12px;
}
.video {
position: relative;
aspect-ratio: 16 / 9;
border-radius: var(--r-md);
overflow: hidden;
background:
linear-gradient(135deg, #232833 0%, #12151b 55%, #0c0e12 100%);
display: grid;
place-items: center;
}
.video::before {
content: "";
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px);
background-size: 42px 42px;
mask-image: radial-gradient(circle at 50% 45%, #000 20%, transparent 75%);
}
.video-glow {
position: absolute;
width: 55%;
aspect-ratio: 1;
border-radius: 50%;
background: radial-gradient(circle, rgba(194, 245, 66, 0.30), transparent 65%);
filter: blur(24px);
transition: opacity 0.4s ease, transform 0.6s ease;
}
.video.playing .video-glow { opacity: 0.75; transform: scale(1.15); }
.video:not(.playing) .video-glow { opacity: 0.35; }
.video-meta {
position: absolute;
top: 12px;
left: 12px;
display: flex;
align-items: center;
gap: 7px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.12em;
color: rgba(255, 255, 255, 0.72);
z-index: 2;
}
.rec-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--orange);
box-shadow: 0 0 0 0 rgba(255, 107, 53, 0.6);
}
.video.playing .rec-dot { animation: pulse 1.4s infinite; }
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(255, 107, 53, 0.55); }
70% { box-shadow: 0 0 0 9px rgba(255, 107, 53, 0); }
100% { box-shadow: 0 0 0 0 rgba(255, 107, 53, 0); }
}
.play-toggle {
position: relative;
z-index: 2;
width: 76px;
height: 76px;
border-radius: 50%;
border: none;
cursor: pointer;
display: grid;
place-items: center;
background: var(--lime);
color: #0c0e12;
box-shadow: 0 12px 34px rgba(194, 245, 66, 0.4);
transition: transform 0.18s ease, box-shadow 0.18s ease;
}
.play-toggle svg { width: 34px; height: 34px; fill: currentColor; }
.play-toggle:hover { transform: scale(1.06); box-shadow: 0 16px 40px rgba(194, 245, 66, 0.55); }
.play-toggle:active { transform: scale(0.97); }
.ico-pause { display: none; }
.video.playing .play-toggle .ico-play,
.playing .ctrl .ico-play { display: none; }
.video.playing .play-toggle .ico-pause,
.playing .ctrl .ico-pause { display: block; }
.chapter-flag {
position: absolute;
bottom: 12px;
left: 12px;
z-index: 2;
padding: 7px 13px;
border-radius: 999px;
background: rgba(12, 14, 18, 0.72);
border: 1px solid var(--line-2);
backdrop-filter: blur(6px);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.03em;
color: var(--lime);
}
/* ---------- Controls ---------- */
.controls {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 6px 6px;
}
.ctrl {
flex-shrink: 0;
width: 38px;
height: 38px;
border-radius: 50%;
border: 1px solid var(--line-2);
background: var(--surface-2);
color: var(--ink);
cursor: pointer;
display: grid;
place-items: center;
transition: border-color 0.15s ease, background 0.15s ease;
}
.ctrl svg { width: 18px; height: 18px; fill: currentColor; display: block; }
.ctrl:hover { border-color: var(--lime); color: var(--lime); }
.ctrl-speed {
width: auto;
padding: 0 12px;
border-radius: 999px;
font-weight: 700;
font-size: 13px;
font-family: inherit;
}
.time {
font-variant-numeric: tabular-nums;
font-size: 13px;
font-weight: 600;
color: var(--muted);
min-width: 38px;
}
.time-dur { text-align: right; }
.track {
position: relative;
flex: 1;
height: 8px;
border-radius: 999px;
background: var(--surface-2);
cursor: pointer;
outline: none;
}
.track-fill {
position: absolute;
inset: 0 auto 0 0;
width: 0%;
border-radius: 999px;
background: linear-gradient(90deg, var(--lime-d), var(--lime));
}
.track-thumb {
position: absolute;
top: 50%;
left: 0%;
width: 15px;
height: 15px;
border-radius: 50%;
background: var(--white);
border: 3px solid var(--lime-d);
transform: translate(-50%, -50%);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
transition: transform 0.12s ease;
}
.track:hover .track-thumb { transform: translate(-50%, -50%) scale(1.15); }
.track:focus-visible { box-shadow: 0 0 0 3px rgba(194, 245, 66, 0.4); }
.track-marks { position: absolute; inset: 0; pointer-events: none; }
.track-mark {
position: absolute;
top: 50%;
width: 3px;
height: 3px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.55);
transform: translate(-50%, -50%);
}
/* ---------- Side rail ---------- */
.rail { display: flex; flex-direction: column; gap: 16px; }
.coach-card {
display: flex;
align-items: center;
gap: 12px;
padding: 14px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
}
.coach-avatar {
width: 44px;
height: 44px;
border-radius: 50%;
display: grid;
place-items: center;
font-weight: 800;
font-size: 15px;
color: #0c0e12;
background: linear-gradient(135deg, var(--lime), var(--lime-d));
flex-shrink: 0;
}
.coach-name { margin: 0; font-weight: 700; font-size: 14px; }
.coach-role { margin: 0; font-size: 12px; color: var(--muted); }
.live-pill {
margin-left: auto;
align-self: flex-start;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.08em;
color: var(--orange);
border: 1px solid rgba(255, 107, 53, 0.4);
border-radius: 999px;
padding: 3px 8px;
}
.setwrap,
.progress-card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 16px;
}
.rail-label {
margin: 0 0 12px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
}
.setrow { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.setbox {
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 10px 6px;
text-align: center;
}
.setbox-num { display: block; font-size: 22px; font-weight: 800; }
.setbox-lab {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
.stepper {
margin-top: 14px;
padding-top: 14px;
border-top: 1px dashed var(--line-2);
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.stepper-lab { font-size: 12px; font-weight: 600; color: var(--muted); }
.stepper-ctrls { display: flex; align-items: center; gap: 8px; }
.round-btn {
width: 30px;
height: 30px;
border-radius: 50%;
border: 1px solid var(--line-2);
background: var(--surface-2);
color: var(--ink);
font-size: 18px;
font-weight: 700;
cursor: pointer;
line-height: 1;
transition: transform 0.12s ease, border-color 0.15s ease, color 0.15s ease;
}
.round-btn:hover { border-color: var(--lime); color: var(--lime); }
.round-btn:active { transform: scale(0.9); }
.stepper-count { font-size: 14px; min-width: 42px; text-align: center; }
.stepper-count b { font-size: 18px; font-weight: 800; color: var(--lime); }
.stepper-count i { font-style: normal; color: var(--muted); font-size: 12px; }
/* ---------- Progress ring ---------- */
.progress-card {
position: relative;
display: grid;
place-items: center;
min-height: 148px;
}
.ring { width: 128px; height: 128px; transform: rotate(-90deg); }
.ring circle { fill: none; stroke-width: 10; }
.ring-bg { stroke: var(--surface-2); }
.ring-fg {
stroke: var(--lime);
stroke-linecap: round;
stroke-dasharray: 326.7;
stroke-dashoffset: 326.7;
transition: stroke-dashoffset 0.5s cubic-bezier(0.4, 0, 0.2, 1);
filter: drop-shadow(0 0 6px rgba(194, 245, 66, 0.5));
}
.ring-center {
position: absolute;
text-align: center;
}
.ring-center b { display: block; font-size: 28px; font-weight: 800; }
.ring-center span {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted);
}
/* ---------- Steps ---------- */
.steps {
margin-top: 22px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 20px;
}
.steps-head { margin-bottom: 14px; }
.steps-head h2 {
margin: 0;
font-size: 18px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.02em;
}
.steps-sub { margin: 4px 0 0; font-size: 13px; color: var(--muted); }
.step-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.step {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 14px;
padding: 14px 16px;
border: 1px solid var(--line);
border-radius: var(--r-md);
background: var(--surface-2);
cursor: pointer;
transition: border-color 0.18s ease, transform 0.18s ease, background 0.18s ease;
}
.step:hover { border-color: var(--line-2); transform: translateX(3px); }
.step:focus-visible { outline: 2px solid var(--lime); outline-offset: 2px; }
.step.active {
border-color: var(--lime);
background: linear-gradient(90deg, rgba(194, 245, 66, 0.10), transparent 70%);
box-shadow: inset 3px 0 0 var(--lime);
}
.step.done { opacity: 0.85; }
.step.done .step-text { text-decoration: line-through; color: var(--muted); }
.step-time {
font-variant-numeric: tabular-nums;
font-size: 12px;
font-weight: 700;
color: var(--muted);
background: rgba(0, 0, 0, 0.25);
border-radius: var(--r-sm);
padding: 4px 8px;
}
.step.active .step-time { color: var(--lime); }
.step-body { min-width: 0; }
.step-text { font-weight: 600; font-size: 14.5px; }
.step-hint { font-size: 12.5px; color: var(--muted); margin-top: 2px; }
.step-check {
width: 26px;
height: 26px;
border-radius: 50%;
border: 2px solid var(--line-2);
display: grid;
place-items: center;
flex-shrink: 0;
transition: background 0.18s ease, border-color 0.18s ease;
}
.step-check svg { width: 14px; height: 14px; stroke: #0c0e12; stroke-width: 3; fill: none; opacity: 0; }
.step.done .step-check { background: var(--lime); border-color: var(--lime); }
.step.done .step-check svg { opacity: 1; }
/* ---------- Footer ---------- */
.lesson-foot {
margin-top: 22px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 16px 20px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
}
.foot-lab {
display: block;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
}
.foot-title { font-size: 17px; font-weight: 800; }
.cta {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 14px 22px;
border: none;
border-radius: 999px;
background: var(--lime);
color: #0c0e12;
font-family: inherit;
font-size: 14px;
font-weight: 800;
letter-spacing: 0.03em;
text-transform: uppercase;
cursor: pointer;
box-shadow: 0 10px 28px rgba(194, 245, 66, 0.35);
transition: transform 0.16s ease, box-shadow 0.16s ease, background 0.16s ease;
}
.cta svg { width: 18px; height: 18px; stroke: currentColor; stroke-width: 2.6; fill: none; }
.cta:hover { transform: translateY(-2px); box-shadow: 0 16px 38px rgba(194, 245, 66, 0.55); }
.cta:active { transform: translateY(0); }
.cta:focus-visible { outline: 3px solid rgba(194, 245, 66, 0.5); outline-offset: 3px; }
.cta.is-complete { background: var(--orange); color: var(--white); box-shadow: 0 10px 28px rgba(255, 107, 53, 0.4); }
.cta.is-complete:hover { box-shadow: 0 16px 38px rgba(255, 107, 53, 0.6); }
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 24px);
background: var(--surface-2);
border: 1px solid var(--line-2);
color: var(--ink);
padding: 12px 18px;
border-radius: 999px;
font-size: 14px;
font-weight: 600;
box-shadow: 0 14px 40px rgba(0, 0, 0, 0.5);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
z-index: 50;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
.toast.lime { border-color: var(--lime); }
/* ---------- Responsive ---------- */
@media (max-width: 820px) {
.grid { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
.wrap { padding: 20px 14px 40px; }
.lesson-title-row { flex-direction: column; align-items: flex-start; }
.badges { flex-wrap: wrap; }
.controls { flex-wrap: wrap; gap: 10px; }
.track { order: 3; flex-basis: 100%; }
.lesson-foot { flex-direction: column; align-items: stretch; }
.cta { justify-content: center; }
.step { grid-template-columns: auto 1fr auto; gap: 10px; padding: 12px; }
.step-time { font-size: 11px; padding: 3px 6px; }
}(function () {
"use strict";
var DURATION = 200; // seconds (3:20)
// Coaching cues, each pinned to a timestamp in the mock playback.
var CUES = [
{ t: 0, chapter: "Set-up & brace", text: "Bar on upper traps, grip just outside shoulders", hint: "Squeeze the bar to lock the shelf" },
{ t: 24, chapter: "Unrack & stance", text: "Unrack, walk back two steps, feet shoulder-width", hint: "Toes turned out ~15°" },
{ t: 58, chapter: "The descent", text: "Big breath into the belly, then break at the hips", hint: "Brace like you're about to be punched" },
{ t: 96, chapter: "Hitting depth", text: "Sit between your knees until hip crease is below knee", hint: "Keep knees tracking over toes" },
{ t: 132, chapter: "The drive", text: "Drive the floor away, hips and chest rise together", hint: "Don't let the chest cave forward" },
{ t: 168, chapter: "Lockout & reset", text: "Stand tall, glutes tight, exhale then reset the breath", hint: "One clean rep — no bouncing" }
];
var TOTAL_SETS = 4;
// ---- element refs ----
var $ = function (id) { return document.getElementById(id); };
var video = $("video");
var playToggle = $("playToggle");
var ctrlPlay = $("ctrlPlay");
var speedBtn = $("speedBtn");
var track = $("track");
var trackFill = $("trackFill");
var trackThumb = $("trackThumb");
var trackMarks = $("trackMarks");
var curTime = $("curTime");
var chapterFlag = $("chapterFlag");
var stepList = $("stepList");
var ringFg = $("ringFg");
var ringPct = $("ringPct");
var setsDoneEl = $("setsDone");
var completeBtn = $("completeBtn");
var nextTitle = $("nextTitle");
var RING_LEN = 326.7;
var state = {
time: 0,
playing: false,
speed: 1,
speeds: [1, 1.25, 1.5, 0.75],
speedIdx: 0,
activeCue: -1,
done: CUES.map(function () { return false; }),
setsDone: 0,
completed: false,
timer: null
};
// ---- toast helper ----
var toastEl = $("toast");
var toastTimer = null;
function toast(msg, lime) {
toastEl.textContent = msg;
toastEl.classList.toggle("lime", !!lime);
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2200);
}
// ---- build step list + timeline marks ----
function fmt(s) {
s = Math.max(0, Math.round(s));
var m = Math.floor(s / 60);
var r = s % 60;
return m + ":" + (r < 10 ? "0" : "") + r;
}
CUES.forEach(function (cue, i) {
var li = document.createElement("li");
li.className = "step";
li.tabIndex = 0;
li.dataset.i = i;
li.setAttribute("role", "button");
li.innerHTML =
'<span class="step-time">' + fmt(cue.t) + '</span>' +
'<span class="step-body">' +
'<span class="step-text">' + cue.text + '</span>' +
'<span class="step-hint">' + cue.hint + '</span>' +
'</span>' +
'<span class="step-check" aria-hidden="true">' +
'<svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>' +
'</span>';
// click on the check toggles done; click elsewhere seeks
li.querySelector(".step-check").addEventListener("click", function (e) {
e.stopPropagation();
toggleDone(i);
});
li.addEventListener("click", function () { seekTo(cue.t); });
li.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); seekTo(cue.t); }
if (e.key.toLowerCase() === "x") { e.preventDefault(); toggleDone(i); }
});
stepList.appendChild(li);
// timeline tick
var mark = document.createElement("span");
mark.className = "track-mark";
mark.style.left = (cue.t / DURATION * 100) + "%";
trackMarks.appendChild(mark);
});
var stepEls = Array.prototype.slice.call(stepList.children);
// ---- render loop ----
function activeCueFor(t) {
var idx = 0;
for (var i = 0; i < CUES.length; i++) {
if (t >= CUES[i].t) idx = i;
}
return idx;
}
function render() {
var pct = state.time / DURATION * 100;
trackFill.style.width = pct + "%";
trackThumb.style.left = pct + "%";
curTime.textContent = fmt(state.time);
track.setAttribute("aria-valuenow", Math.round(pct));
var ai = activeCueFor(state.time);
if (ai !== state.activeCue) {
state.activeCue = ai;
stepEls.forEach(function (el, i) { el.classList.toggle("active", i === ai); });
chapterFlag.textContent = CUES[ai].chapter;
}
}
function renderProgress() {
var count = state.done.filter(Boolean).length;
var frac = count / CUES.length;
ringFg.style.strokeDashoffset = String(RING_LEN * (1 - frac));
ringPct.textContent = Math.round(frac * 100) + "%";
var allDone = count === CUES.length;
if (allDone && !completeBtn.classList.contains("ready")) {
completeBtn.classList.add("ready");
} else if (!allDone) {
completeBtn.classList.remove("ready");
}
}
// ---- playback ----
function setPlaying(on) {
state.playing = on;
video.classList.toggle("playing", on);
document.body.classList.toggle("playing", on);
playToggle.setAttribute("aria-pressed", String(on));
playToggle.setAttribute("aria-label", on ? "Pause lesson" : "Play lesson");
if (on) {
if (state.time >= DURATION) { state.time = 0; }
last = performance.now();
tick();
} else if (state.timer) {
cancelAnimationFrame(state.timer);
state.timer = null;
}
}
function togglePlay() { setPlaying(!state.playing); }
var last = 0;
function tick() {
var now = performance.now();
var dt = (now - last) / 1000;
last = now;
state.time += dt * state.speed;
if (state.time >= DURATION) {
state.time = DURATION;
render();
setPlaying(false);
toast("Lesson watched — log your sets and mark it complete", true);
return;
}
render();
state.timer = requestAnimationFrame(tick);
}
function seekTo(t) {
state.time = Math.max(0, Math.min(DURATION, t));
if (state.timer) last = performance.now();
render();
}
// ---- cue completion ----
function toggleDone(i) {
state.done[i] = !state.done[i];
stepEls[i].classList.toggle("done", state.done[i]);
renderProgress();
if (state.done[i]) {
var remaining = state.done.filter(function (d) { return !d; }).length;
toast(remaining === 0 ? "All cues nailed — nice work!" : "Cue locked in · " + remaining + " to go", remaining === 0);
}
}
// ---- sets stepper ----
function bumpSets(delta) {
state.setsDone = Math.max(0, Math.min(TOTAL_SETS, state.setsDone + delta));
setsDoneEl.textContent = String(state.setsDone);
if (delta > 0 && state.setsDone === TOTAL_SETS) {
toast("All " + TOTAL_SETS + " sets logged. Beast.", true);
}
}
// ---- complete / next ----
function markComplete() {
if (!state.completed) {
var undone = state.done.filter(function (d) { return !d; }).length;
if (undone > 0) {
toast("Check off all " + CUES.length + " cues first (" + undone + " left)");
return;
}
state.completed = true;
completeBtn.classList.add("is-complete");
completeBtn.querySelector(".cta-label").textContent = "Next exercise";
completeBtn.querySelector("svg").innerHTML = '<path d="M5 12h14M13 6l6 6-6 6"/>';
toast("Squat complete — up next: " + nextTitle.textContent, true);
} else {
// advance to the next exercise
toast("Loading " + nextTitle.textContent + "…");
setTimeout(function () {
document.querySelector(".lesson-title-row h1").textContent = nextTitle.textContent.toUpperCase();
nextTitle.textContent = "Walking Lunge";
// reset lesson state
state.completed = false;
state.done = CUES.map(function () { return false; });
state.setsDone = 0;
state.time = 0;
setsDoneEl.textContent = "0";
stepEls.forEach(function (el) { el.classList.remove("done"); });
completeBtn.classList.remove("is-complete", "ready");
completeBtn.querySelector(".cta-label").textContent = "Mark complete";
completeBtn.querySelector("svg").innerHTML = '<path d="M20 6 9 17l-5-5"/>';
setPlaying(false);
render();
renderProgress();
toast("New exercise loaded — press play", true);
}, 650);
}
}
// ---- track seek (pointer + keyboard) ----
function seekFromEvent(e) {
var rect = track.getBoundingClientRect();
var x = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left;
var frac = Math.max(0, Math.min(1, x / rect.width));
seekTo(frac * DURATION);
}
var dragging = false;
track.addEventListener("pointerdown", function (e) {
dragging = true;
track.setPointerCapture(e.pointerId);
seekFromEvent(e);
});
track.addEventListener("pointermove", function (e) { if (dragging) seekFromEvent(e); });
track.addEventListener("pointerup", function () { dragging = false; });
track.addEventListener("keydown", function (e) {
if (e.key === "ArrowRight") { e.preventDefault(); seekTo(state.time + 5); }
else if (e.key === "ArrowLeft") { e.preventDefault(); seekTo(state.time - 5); }
else if (e.key === "Home") { e.preventDefault(); seekTo(0); }
else if (e.key === "End") { e.preventDefault(); seekTo(DURATION); }
});
// ---- wire controls ----
playToggle.addEventListener("click", togglePlay);
ctrlPlay.addEventListener("click", togglePlay);
speedBtn.addEventListener("click", function () {
state.speedIdx = (state.speedIdx + 1) % state.speeds.length;
state.speed = state.speeds[state.speedIdx];
speedBtn.textContent = state.speed + "×";
toast("Speed · " + state.speed + "×");
});
$("setPlus").addEventListener("click", function () { bumpSets(1); });
$("setMinus").addEventListener("click", function () { bumpSets(-1); });
completeBtn.addEventListener("click", markComplete);
// spacebar toggles play when not focused on a control
document.addEventListener("keydown", function (e) {
if (e.code === "Space" && !/^(BUTTON|INPUT|TEXTAREA)$/.test(e.target.tagName) && !e.target.classList.contains("step")) {
e.preventDefault();
togglePlay();
}
});
// ---- init ----
$("setsTotal").textContent = String(TOTAL_SETS);
render();
renderProgress();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Coach — Video Lesson</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=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="wrap">
<header class="lesson-head">
<div class="crumbs">
<span class="program">Push · Pull · Legs</span>
<span class="sep">/</span>
<span class="week">Week 3 · Day 2</span>
</div>
<div class="lesson-title-row">
<div>
<p class="eyebrow">Exercise 4 of 7</p>
<h1 id="lessonTitle">Barbell Back Squat</h1>
</div>
<div class="badges">
<span class="badge badge-focus">Legs · Glutes</span>
<span class="badge badge-diff">Intermediate</span>
</div>
</div>
</header>
<div class="grid">
<!-- ===== Video stage ===== -->
<section class="stage" aria-label="Video player">
<div class="video" id="video">
<div class="video-glow" aria-hidden="true"></div>
<div class="video-meta">
<span class="rec-dot" aria-hidden="true"></span>
<span>COACH CAM · 4K</span>
</div>
<button class="play-toggle" id="playToggle" aria-label="Play lesson" aria-pressed="false">
<svg class="ico-play" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>
<svg class="ico-pause" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 5h4v14H6zM14 5h4v14h-4z"/></svg>
</button>
<div class="chapter-flag" id="chapterFlag">Set-up & brace</div>
</div>
<div class="controls">
<button class="ctrl" id="ctrlPlay" aria-label="Play or pause">
<svg class="ico-play" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>
<svg class="ico-pause" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 5h4v14H6zM14 5h4v14h-4z"/></svg>
</button>
<span class="time" id="curTime">0:00</span>
<div class="track" id="track" role="slider" tabindex="0"
aria-label="Seek" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
<div class="track-fill" id="trackFill"></div>
<div class="track-thumb" id="trackThumb"></div>
<div class="track-marks" id="trackMarks"></div>
</div>
<span class="time time-dur" id="durTime">3:20</span>
<button class="ctrl ctrl-speed" id="speedBtn" aria-label="Playback speed">1×</button>
</div>
</section>
<!-- ===== Side rail ===== -->
<aside class="rail" aria-label="Lesson details">
<div class="coach-card">
<div class="coach-avatar" aria-hidden="true">MJ</div>
<div>
<p class="coach-name">Coach Mara Jensen</p>
<p class="coach-role">CSCS · Strength Lead</p>
</div>
<span class="live-pill">LIVE PLAN</span>
</div>
<div class="setwrap">
<p class="rail-label">Prescription</p>
<div class="setrow">
<div class="setbox">
<span class="setbox-num" id="setsVal">4</span>
<span class="setbox-lab">Sets</span>
</div>
<div class="setbox">
<span class="setbox-num" id="repsVal">8</span>
<span class="setbox-lab">Reps</span>
</div>
<div class="setbox">
<span class="setbox-num">2:00</span>
<span class="setbox-lab">Rest</span>
</div>
</div>
<div class="stepper" aria-label="Adjust sets completed">
<span class="stepper-lab">Sets logged</span>
<div class="stepper-ctrls">
<button class="round-btn" id="setMinus" aria-label="Remove a logged set">−</button>
<span class="stepper-count"><b id="setsDone">0</b><i>/ <span id="setsTotal">4</span></i></span>
<button class="round-btn" id="setPlus" aria-label="Log a set">+</button>
</div>
</div>
</div>
<div class="progress-card">
<svg class="ring" viewBox="0 0 120 120" aria-hidden="true">
<circle class="ring-bg" cx="60" cy="60" r="52"></circle>
<circle class="ring-fg" id="ringFg" cx="60" cy="60" r="52"></circle>
</svg>
<div class="ring-center">
<b id="ringPct">0%</b>
<span>cues nailed</span>
</div>
</div>
</aside>
</div>
<!-- ===== Synced instructions ===== -->
<section class="steps" aria-label="Step-by-step instructions">
<div class="steps-head">
<h2>Coaching cues</h2>
<p class="steps-sub">Tap a cue to jump the video · check it off once you own it</p>
</div>
<ol class="step-list" id="stepList"></ol>
</section>
<!-- ===== Footer action ===== -->
<footer class="lesson-foot">
<div class="foot-next">
<span class="foot-lab">Up next</span>
<span class="foot-title" id="nextTitle">Romanian Deadlift</span>
</div>
<button class="cta" id="completeBtn">
<span class="cta-label">Mark complete</span>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>
</button>
</footer>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A single-lesson training screen built for a fitness coaching app. The stage holds a 16:9 video placeholder with a play/pause overlay, a fake timeline that scrubs as the mock playback runs, and chapter markers that advance the currently active instruction. Alongside it sits a metadata rail showing the coach, muscle focus, and difficulty badges, plus a live sets and reps tracker with plus and minus steppers.
The synced instruction list is the core interaction: each step lights up as playback reaches its timestamp, and learners can tap a cue to seek to that moment or check it off once nailed. A circular completion ring fills as cues are ticked, and the electric-lime Mark complete button glows once every step is done. Finishing the lesson swaps the CTA into a Next exercise action that loads the following move in the program.
All state is vanilla JS with a small toast helper for feedback. Controls are keyboard reachable with visible focus rings, the timeline is an ARIA slider, and the layout collapses from a two-column split into a single stacked flow below 520px so it stays usable on a phone at the gym.