Gym — Workout Tracker
A high-energy active workout tracker for the gym floor — an ordered list of exercises like Back Squat, Bench Press and Deadlift, each with editable weight-by-reps set rows, a one-tap check-off, an add-set button and per-exercise rest. A big neon rest timer counts down with start, pause and reset plus a beep-free flash at zero, while live totals track volume, sets done and elapsed workout time. Finishing surfaces a summary toast. All state in vanilla JS.
MCP
Codice
:root {
--bg: #0d0f12;
--surface: #15181d;
--surface-2: #1d2127;
--elevated: #23282f;
--ink: #f4f6f8;
--ink-2: #c2c8d0;
--muted: #8b929c;
--neon: #c6ff3a;
--neon-d: #a6e016;
--neon-50: rgba(198, 255, 58, 0.12);
--orange: #ff6a2b;
--orange-soft: rgba(255, 106, 43, 0.14);
--line: rgba(255, 255, 255, 0.08);
--line-2: rgba(255, 255, 255, 0.16);
--ok: #34d399;
--warn: #fbbf24;
--danger: #f87171;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--shadow: 0 10px 30px rgba(0, 0, 0, 0.45), 0 2px 8px rgba(0, 0, 0, 0.35);
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
}
body {
background: var(--bg);
color: var(--ink);
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-image:
radial-gradient(900px 500px at 100% -10%, rgba(198, 255, 58, 0.06), transparent 60%),
radial-gradient(700px 400px at -10% 110%, rgba(255, 106, 43, 0.07), transparent 55%);
min-height: 100vh;
}
.app {
max-width: 720px;
margin: 0 auto;
padding: 24px 20px 120px;
}
.eyebrow {
margin: 0;
text-transform: uppercase;
letter-spacing: 0.14em;
font-size: 11px;
font-weight: 700;
color: var(--muted);
}
/* Header */
.top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
}
.brand { display: flex; align-items: center; gap: 14px; }
.logo {
display: grid;
place-items: center;
width: 48px;
height: 48px;
border-radius: var(--r-md);
background: var(--neon-50);
border: 1px solid var(--line-2);
font-size: 24px;
}
.top h1 {
margin: 2px 0 0;
font-size: 24px;
font-weight: 900;
letter-spacing: -0.02em;
}
.clock {
text-align: right;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 8px 14px;
box-shadow: var(--shadow);
}
.clock-label {
display: block;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--muted);
font-weight: 700;
}
.clock-time {
font-variant-numeric: tabular-nums;
font-size: 22px;
font-weight: 800;
color: var(--neon);
}
/* Stats */
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 18px;
}
.stat {
background: linear-gradient(180deg, var(--surface-2), var(--surface));
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 14px;
box-shadow: var(--shadow);
}
.stat-label {
display: block;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--muted);
font-weight: 700;
margin-bottom: 6px;
}
.stat-value {
font-size: 14px;
color: var(--ink-2);
font-variant-numeric: tabular-nums;
}
.stat-value b {
font-size: 22px;
font-weight: 900;
color: var(--ink);
}
/* Timer */
.timer-card {
background: radial-gradient(120% 140% at 50% -20%, rgba(198, 255, 58, 0.1), transparent 60%), var(--surface);
border: 1px solid var(--line-2);
border-radius: var(--r-lg);
padding: 18px;
margin-bottom: 22px;
box-shadow: var(--shadow);
text-align: center;
transition: box-shadow 0.2s ease, border-color 0.2s ease;
}
.timer-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.timer-presets { display: flex; gap: 6px; }
.chip {
font: inherit;
font-size: 12px;
font-weight: 700;
color: var(--ink-2);
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: 999px;
padding: 5px 11px;
cursor: pointer;
transition: all 0.15s ease;
}
.chip:hover { border-color: var(--line-2); color: var(--ink); }
.chip.active {
background: var(--neon-50);
border-color: var(--neon-d);
color: var(--neon);
}
.timer-display {
font-variant-numeric: tabular-nums;
font-size: clamp(56px, 18vw, 86px);
font-weight: 900;
letter-spacing: -0.03em;
line-height: 1;
margin: 14px 0 12px;
color: var(--ink);
}
.timer-display.running { color: var(--neon); }
.timer-display.flash {
animation: flash 0.4s ease 3;
color: var(--orange);
}
@keyframes flash {
0%, 100% { opacity: 1; }
50% { opacity: 0.25; }
}
.timer-card.alarm {
border-color: var(--orange);
box-shadow: 0 0 0 3px var(--orange-soft), var(--shadow);
}
.timer-bar {
height: 6px;
border-radius: 999px;
background: var(--surface-2);
overflow: hidden;
margin-bottom: 16px;
}
.timer-bar span {
display: block;
height: 100%;
width: 100%;
background: linear-gradient(90deg, var(--neon-d), var(--neon));
transform-origin: left;
transition: width 0.25s linear;
}
.timer-controls { display: flex; gap: 10px; justify-content: center; }
/* Buttons */
.btn {
font: inherit;
font-weight: 800;
border: 1px solid transparent;
border-radius: var(--r-md);
padding: 13px 22px;
cursor: pointer;
transition: transform 0.08s ease, background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
letter-spacing: 0.01em;
}
.btn:active { transform: translateY(1px) scale(0.99); }
.btn:focus-visible {
outline: 3px solid var(--neon);
outline-offset: 2px;
}
.btn-neon { background: var(--neon); color: #0d0f12; }
.btn-neon:hover { background: var(--neon-d); }
.btn-orange { background: var(--orange); color: #1a0d05; }
.btn-orange:hover { filter: brightness(1.06); }
.btn-ghost {
background: var(--surface-2);
color: var(--ink-2);
border-color: var(--line);
}
.btn-ghost:hover { color: var(--ink); border-color: var(--line-2); }
.btn-sm { padding: 9px 14px; font-size: 13px; border-radius: var(--r-sm); }
.btn-block { width: 100%; padding: 16px; font-size: 16px; }
.btn-paused { background: var(--warn); color: #1a1305; }
/* Exercise cards */
.exercises { display: flex; flex-direction: column; gap: 16px; }
.ex-card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 18px;
box-shadow: var(--shadow);
}
.ex-card.complete {
border-color: var(--neon-d);
box-shadow: 0 0 0 1px var(--neon-50), var(--shadow);
}
.ex-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 14px;
}
.ex-title { display: flex; align-items: center; gap: 12px; }
.ex-index {
display: grid;
place-items: center;
width: 34px;
height: 34px;
border-radius: var(--r-sm);
background: var(--surface-2);
border: 1px solid var(--line);
font-weight: 900;
font-size: 15px;
color: var(--neon);
flex: none;
}
.ex-name { margin: 0; font-size: 17px; font-weight: 800; letter-spacing: -0.01em; }
.ex-meta { margin: 1px 0 0; font-size: 12px; color: var(--muted); }
.ex-badge {
font-size: 12px;
font-weight: 800;
font-variant-numeric: tabular-nums;
color: var(--ink-2);
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: 999px;
padding: 5px 12px;
}
.ex-card.complete .ex-badge {
background: var(--neon-50);
border-color: var(--neon-d);
color: var(--neon);
}
.set-head, .set-row {
display: grid;
grid-template-columns: 42px 1fr 1fr 48px;
align-items: center;
gap: 10px;
}
.set-head {
padding: 0 4px 8px;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.1em;
font-weight: 700;
color: var(--muted);
}
.set-rows { display: flex; flex-direction: column; gap: 8px; }
.set-row {
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 8px 10px;
transition: background 0.15s ease, border-color 0.15s ease, opacity 0.15s ease;
}
.set-row.done {
background: var(--neon-50);
border-color: var(--neon-d);
}
.set-num {
font-weight: 800;
font-variant-numeric: tabular-nums;
color: var(--ink-2);
text-align: center;
}
.set-row.done .set-num { color: var(--neon); }
.set-row input {
font: inherit;
font-weight: 700;
font-variant-numeric: tabular-nums;
width: 100%;
text-align: center;
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-sm);
color: var(--ink);
padding: 8px 6px;
-moz-appearance: textfield;
appearance: textfield;
}
.set-row input::-webkit-outer-spin-button,
.set-row input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.set-row input:focus-visible {
outline: none;
border-color: var(--neon);
box-shadow: 0 0 0 2px var(--neon-50);
}
.set-row.done input { opacity: 0.7; }
.check {
width: 32px;
height: 32px;
margin: 0 auto;
display: grid;
place-items: center;
border-radius: 50%;
border: 2px solid var(--line-2);
background: transparent;
color: transparent;
cursor: pointer;
font-size: 16px;
font-weight: 900;
transition: all 0.15s ease;
}
.check:hover { border-color: var(--neon); }
.check:focus-visible { outline: 3px solid var(--neon); outline-offset: 2px; }
.set-row.done .check {
background: var(--neon);
border-color: var(--neon);
color: #0d0f12;
}
.set-del {
background: none;
border: none;
color: var(--muted);
cursor: pointer;
font-size: 18px;
line-height: 1;
padding: 4px;
display: none;
}
.ex-actions {
display: flex;
gap: 10px;
margin-top: 14px;
flex-wrap: wrap;
}
.rest-set { color: var(--ink-2); }
/* Finish bar */
.finish-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 14px 20px calc(14px + env(safe-area-inset-bottom));
background: linear-gradient(180deg, transparent, var(--bg) 35%);
}
.finish-bar .btn { max-width: 720px; margin: 0 auto; display: block; }
/* Toast */
.toast-host {
position: fixed;
left: 0;
right: 0;
bottom: 90px;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
pointer-events: none;
z-index: 50;
padding: 0 16px;
}
.toast {
pointer-events: auto;
max-width: 480px;
width: 100%;
background: var(--elevated);
border: 1px solid var(--line-2);
border-left: 4px solid var(--neon);
border-radius: var(--r-md);
padding: 14px 16px;
box-shadow: var(--shadow);
color: var(--ink);
font-size: 14px;
font-weight: 600;
transform: translateY(16px);
opacity: 0;
transition: transform 0.25s ease, opacity 0.25s ease;
}
.toast.show { transform: translateY(0); opacity: 1; }
.toast.summary { border-left-color: var(--orange); }
.toast b { font-weight: 800; }
.toast .toast-line { display: block; font-size: 13px; font-weight: 500; color: var(--ink-2); margin-top: 4px; }
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap; border: 0;
}
/* Responsive */
@media (max-width: 520px) {
.app { padding: 18px 14px 110px; }
.top h1 { font-size: 20px; }
.stats { gap: 8px; }
.stat { padding: 11px; }
.stat-value b { font-size: 18px; }
.clock { padding: 6px 10px; }
.clock-time { font-size: 18px; }
.timer-presets .chip { padding: 5px 9px; }
.set-head, .set-row { grid-template-columns: 34px 1fr 1fr 44px; gap: 7px; }
.ex-actions .btn { flex: 1; }
}(function () {
"use strict";
/* ---------- Seed data (fictional Push Day A) ---------- */
const workout = [
{
name: "Back Squat",
meta: "Barbell · Legs · 3 min rest",
rest: 180,
sets: [
{ weight: 100, reps: 5, done: false },
{ weight: 100, reps: 5, done: false },
{ weight: 100, reps: 5, done: false },
],
},
{
name: "Bench Press",
meta: "Barbell · Chest · 2 min rest",
rest: 120,
sets: [
{ weight: 80, reps: 8, done: false },
{ weight: 80, reps: 8, done: false },
{ weight: 75, reps: 10, done: false },
],
},
{
name: "Deadlift",
meta: "Barbell · Posterior · 3 min rest",
rest: 180,
sets: [
{ weight: 140, reps: 3, done: false },
{ weight: 140, reps: 3, done: false },
],
},
{
name: "Overhead Press",
meta: "Barbell · Shoulders · 90s rest",
rest: 90,
sets: [
{ weight: 45, reps: 8, done: false },
{ weight: 45, reps: 8, done: false },
],
},
];
/* ---------- Toast helper ---------- */
const toastHost = document.getElementById("toastHost");
function toast(msg, opts) {
opts = opts || {};
const el = document.createElement("div");
el.className = "toast" + (opts.summary ? " summary" : "");
el.innerHTML = msg;
toastHost.appendChild(el);
requestAnimationFrame(() => el.classList.add("show"));
const life = opts.life || 3200;
setTimeout(() => {
el.classList.remove("show");
setTimeout(() => el.remove(), 300);
}, life);
}
/* ---------- Render exercises ---------- */
const root = document.getElementById("exercises");
const tpl = document.getElementById("exTpl");
function render() {
root.innerHTML = "";
workout.forEach((ex, ei) => {
const node = tpl.content.cloneNode(true);
const card = node.querySelector(".ex-card");
card.dataset.ex = ei;
node.querySelector(".ex-index").textContent = ei + 1;
node.querySelector(".ex-name").textContent = ex.name;
node.querySelector(".ex-meta").textContent = ex.meta;
node.querySelector(".rest-secs").textContent = ex.rest;
node.querySelector(".rest-set").dataset.secs = ex.rest;
const rows = node.querySelector(".set-rows");
ex.sets.forEach((set, si) => rows.appendChild(buildRow(ei, si, set)));
updateCard(card, ex);
root.appendChild(node);
// re-grab the live card (appendChild moves the fragment children)
const live = root.querySelector('.ex-card[data-ex="' + ei + '"]');
updateCard(live, ex);
});
recalc();
}
function buildRow(ei, si, set) {
const row = document.createElement("div");
row.className = "set-row" + (set.done ? " done" : "");
row.dataset.ex = ei;
row.dataset.set = si;
row.innerHTML =
'<span class="set-num">' + (si + 1) + "</span>" +
'<input type="number" inputmode="decimal" min="0" step="2.5" class="in-weight" value="' + set.weight + '" aria-label="Weight in kilograms" />' +
'<input type="number" inputmode="numeric" min="0" step="1" class="in-reps" value="' + set.reps + '" aria-label="Repetitions" />' +
'<button class="check" aria-label="Mark set ' + (si + 1) + ' done" aria-pressed="' + set.done + '">✓</button>';
return row;
}
function updateCard(card, ex) {
const done = ex.sets.filter((s) => s.done).length;
card.querySelector(".ex-badge").textContent = done + "/" + ex.sets.length;
card.classList.toggle("complete", done === ex.sets.length && ex.sets.length > 0);
}
/* ---------- Event delegation ---------- */
root.addEventListener("click", (e) => {
const card = e.target.closest(".ex-card");
if (!card) return;
const ei = +card.dataset.ex;
const ex = workout[ei];
if (e.target.classList.contains("check")) {
const row = e.target.closest(".set-row");
const si = +row.dataset.set;
ex.sets[si].done = !ex.sets[si].done;
row.classList.toggle("done", ex.sets[si].done);
e.target.setAttribute("aria-pressed", String(ex.sets[si].done));
updateCard(card, ex);
recalc();
if (ex.sets[si].done) startTimer(ex.rest, true);
return;
}
if (e.target.classList.contains("add-set")) {
const last = ex.sets[ex.sets.length - 1] || { weight: 20, reps: 8 };
ex.sets.push({ weight: last.weight, reps: last.reps, done: false });
const rows = card.querySelector(".set-rows");
rows.appendChild(buildRow(ei, ex.sets.length - 1, ex.sets[ex.sets.length - 1]));
updateCard(card, ex);
recalc();
toast("Added set to <b>" + ex.name + "</b>");
return;
}
if (e.target.classList.contains("rest-set")) {
startTimer(+e.target.dataset.secs, true);
return;
}
});
root.addEventListener("input", (e) => {
const row = e.target.closest(".set-row");
if (!row) return;
const ei = +row.dataset.ex;
const si = +row.dataset.set;
const set = workout[ei].sets[si];
if (e.target.classList.contains("in-weight")) set.weight = parseFloat(e.target.value) || 0;
if (e.target.classList.contains("in-reps")) set.reps = parseInt(e.target.value, 10) || 0;
recalc();
});
/* ---------- Running totals ---------- */
const elTotalVolume = document.getElementById("totalVolume");
const elSetsDone = document.getElementById("setsDone");
const elSetsTotal = document.getElementById("setsTotal");
const elExDone = document.getElementById("exDone");
const elExTotal = document.getElementById("exTotal");
function totals() {
let volume = 0, setsDone = 0, setsTotal = 0, exDone = 0;
workout.forEach((ex) => {
let allDone = ex.sets.length > 0;
ex.sets.forEach((s) => {
setsTotal++;
if (s.done) {
setsDone++;
volume += s.weight * s.reps;
} else {
allDone = false;
}
});
if (allDone) exDone++;
});
return { volume, setsDone, setsTotal, exDone, exTotal: workout.length };
}
function recalc() {
const t = totals();
elTotalVolume.textContent = t.volume.toLocaleString();
elSetsDone.textContent = t.setsDone;
elSetsTotal.textContent = t.setsTotal;
elExDone.textContent = t.exDone;
elExTotal.textContent = t.exTotal;
}
/* ---------- Rest timer ---------- */
const display = document.getElementById("timerDisplay");
const bar = document.getElementById("timerBar");
const card = document.getElementById("timerCard");
const startBtn = document.getElementById("timerStart");
const resetBtn = document.getElementById("timerReset");
const chips = Array.from(document.querySelectorAll(".chip"));
let total = 120; // selected preset
let remaining = 120; // seconds left
let running = false;
let endAt = 0;
let raf = null;
function fmt(s) {
s = Math.max(0, Math.ceil(s));
const m = Math.floor(s / 60);
const r = s % 60;
return String(m).padStart(2, "0") + ":" + String(r).padStart(2, "0");
}
function paintTimer() {
display.textContent = fmt(remaining);
bar.style.width = total > 0 ? (Math.max(0, remaining) / total) * 100 + "%" : "0%";
}
function tick() {
remaining = (endAt - Date.now()) / 1000;
if (remaining <= 0) {
remaining = 0;
paintTimer();
finishTimer();
return;
}
paintTimer();
raf = requestAnimationFrame(tick);
}
function setStartLabel() {
if (running) {
startBtn.textContent = "Pause";
startBtn.classList.add("btn-paused");
startBtn.classList.remove("btn-neon");
} else {
startBtn.textContent = remaining < total && remaining > 0 ? "Resume" : "Start";
startBtn.classList.add("btn-neon");
startBtn.classList.remove("btn-paused");
}
}
function startTimer(secs, fresh) {
card.classList.remove("alarm");
display.classList.remove("flash");
if (typeof secs === "number" && fresh) {
total = secs;
remaining = secs;
selectChip(secs);
}
if (remaining <= 0) remaining = total;
endAt = Date.now() + remaining * 1000;
running = true;
display.classList.add("running");
setStartLabel();
cancelAnimationFrame(raf);
raf = requestAnimationFrame(tick);
}
function pauseTimer() {
running = false;
cancelAnimationFrame(raf);
display.classList.remove("running");
setStartLabel();
}
function resetTimer() {
pauseTimer();
remaining = total;
card.classList.remove("alarm");
display.classList.remove("flash");
paintTimer();
setStartLabel();
}
function finishTimer() {
running = false;
cancelAnimationFrame(raf);
display.classList.remove("running");
// beep-free visual flash
card.classList.add("alarm");
display.classList.remove("flash");
void display.offsetWidth; // restart animation
display.classList.add("flash");
setStartLabel();
toast("Rest done — back to work! 💪");
}
function selectChip(secs) {
chips.forEach((c) => c.classList.toggle("active", +c.dataset.secs === secs));
}
startBtn.addEventListener("click", () => {
if (running) pauseTimer();
else startTimer();
});
resetBtn.addEventListener("click", resetTimer);
chips.forEach((c) =>
c.addEventListener("click", () => startTimer(+c.dataset.secs, true))
);
selectChip(120);
paintTimer();
setStartLabel();
/* ---------- Elapsed workout clock ---------- */
const elElapsed = document.getElementById("elapsed");
const workoutStart = Date.now();
function tickElapsed() {
const s = Math.floor((Date.now() - workoutStart) / 1000);
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
const r = s % 60;
elElapsed.textContent =
(h > 0 ? String(h) + ":" + String(m).padStart(2, "0") : String(m).padStart(2, "0")) +
":" + String(r).padStart(2, "0");
}
tickElapsed();
setInterval(tickElapsed, 1000);
/* ---------- Finish workout ---------- */
document.getElementById("finishBtn").addEventListener("click", () => {
const t = totals();
const elapsed = elElapsed.textContent;
if (t.setsDone === 0) {
toast("Log at least one set before finishing.", { life: 2600 });
return;
}
toast(
"🏁 <b>Workout complete!</b>" +
'<span class="toast-line">' +
t.setsDone + " sets · " + t.volume.toLocaleString() + " kg volume · " +
t.exDone + "/" + t.exTotal + " exercises · " + elapsed + " elapsed" +
"</span>",
{ summary: true, life: 6000 }
);
});
/* ---------- Boot ---------- */
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Iron Forge — Workout Tracker</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;900&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="app">
<header class="top">
<div class="brand">
<span class="logo" aria-hidden="true">⚡</span>
<div>
<p class="eyebrow">Iron Forge · Push Day A</p>
<h1>Workout Tracker</h1>
</div>
</div>
<div class="clock" aria-live="polite">
<span class="clock-label">Elapsed</span>
<span class="clock-time" id="elapsed">00:00</span>
</div>
</header>
<section class="stats" aria-label="Workout totals">
<div class="stat">
<span class="stat-label">Total Volume</span>
<span class="stat-value"><b id="totalVolume">0</b> kg</span>
</div>
<div class="stat">
<span class="stat-label">Sets Done</span>
<span class="stat-value"><b id="setsDone">0</b> / <span id="setsTotal">0</span></span>
</div>
<div class="stat">
<span class="stat-label">Exercises</span>
<span class="stat-value"><b id="exDone">0</b> / <span id="exTotal">0</span></span>
</div>
</section>
<section class="timer-card" id="timerCard" aria-label="Rest timer">
<div class="timer-head">
<span class="eyebrow">Rest Timer</span>
<div class="timer-presets" role="group" aria-label="Rest presets">
<button class="chip" data-secs="60">60s</button>
<button class="chip" data-secs="90">90s</button>
<button class="chip" data-secs="120">2m</button>
<button class="chip" data-secs="180">3m</button>
</div>
</div>
<div class="timer-display" id="timerDisplay" aria-live="polite">02:00</div>
<div class="timer-bar"><span id="timerBar"></span></div>
<div class="timer-controls">
<button class="btn btn-neon" id="timerStart">Start</button>
<button class="btn btn-ghost" id="timerReset">Reset</button>
</div>
</section>
<main class="exercises" id="exercises" aria-label="Exercises"></main>
<footer class="finish-bar">
<button class="btn btn-orange btn-block" id="finishBtn">Finish Workout</button>
</footer>
</div>
<div class="toast-host" id="toastHost" aria-live="assertive" aria-atomic="true"></div>
<template id="exTpl">
<article class="ex-card">
<div class="ex-head">
<div class="ex-title">
<span class="ex-index"></span>
<div>
<h2 class="ex-name"></h2>
<p class="ex-meta"></p>
</div>
</div>
<span class="ex-badge">0/0</span>
</div>
<div class="set-head">
<span>Set</span><span>Weight (kg)</span><span>Reps</span><span class="sr-only">Done</span>
</div>
<div class="set-rows"></div>
<div class="ex-actions">
<button class="btn btn-ghost btn-sm add-set">+ Add set</button>
<button class="btn btn-ghost btn-sm rest-set">Rest <span class="rest-secs"></span>s</button>
</div>
</article>
</template>
<script src="script.js"></script>
</body>
</html>Workout Tracker
A dark, athletic logging screen built for tracking lifts mid-session. The header carries a live elapsed clock and three running totals — total volume, sets done versus planned, and exercises completed — that recompute the instant you tap a set done or edit a weight. Below it, a prominent rest timer dominates with a giant neon countdown, quick presets (60s, 90s, 2m, 3m) and start / pause / reset controls; checking off a set auto-starts that exercise’s rest, and at zero the display does a beep-free visual flash while the card frames itself in orange.
Each exercise is its own card — Back Squat, Bench Press, Deadlift, Overhead Press — with realistic seed loads laid out in tidy set rows of weight by reps. Every row has editable number inputs and a round check button that fills neon when done; a card outlines itself once all its sets are logged. “Add set” clones the last row’s load so you can push another working set, and a per-exercise rest chip kicks the timer straight to that lift’s prescribed interval.
Hitting Finish Workout rolls everything into a summary toast — sets logged, total volume, exercises completed and elapsed time. The whole thing is vanilla JS with session-held state, large tap-friendly targets, visible focus rings and a layout that holds together down to ~360px.