Coach — Progress Tracker
An energetic dark-mode progress dashboard for fitness coaching clients: an animated inline-SVG weight and strength line chart, a quick log-entry form that redraws the trend live, a fiery workout streak counter, a personal-records leaderboard, and a body-measurements grid. Bold uppercase headings, glowing electric-lime CTAs, and smooth micro-interactions turn dry numbers into motivation.
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, body {
margin: 0;
background:
radial-gradient(1100px 520px at 90% -10%, rgba(194, 245, 66, 0.10), transparent 60%),
radial-gradient(900px 500px at -10% 110%, rgba(255, 107, 53, 0.08), transparent 55%),
var(--bg);
color: var(--ink);
font-family: "Inter", system-ui, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.wrap {
max-width: 1080px;
margin: 0 auto;
padding: 28px 20px 64px;
}
/* Masthead */
.masthead {
display: flex;
justify-content: space-between;
align-items: center;
gap: 18px;
flex-wrap: wrap;
margin-bottom: 22px;
}
.brand { display: flex; align-items: center; gap: 14px; }
.brand-mark {
font-size: 26px;
color: var(--lime);
filter: drop-shadow(0 0 12px rgba(194, 245, 66, 0.5));
}
.eyebrow {
margin: 0;
font-size: 11px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--muted);
font-weight: 600;
}
.masthead h1 {
margin: 2px 0 0;
font-size: clamp(24px, 4vw, 34px);
font-weight: 800;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.client-chip {
display: flex;
align-items: center;
gap: 12px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 999px;
padding: 8px 16px 8px 8px;
}
.client-chip strong { display: block; font-size: 14px; }
.avatar {
width: 40px; height: 40px;
display: grid; place-items: center;
border-radius: 50%;
font-weight: 800;
font-size: 13px;
color: var(--bg);
background: linear-gradient(135deg, var(--lime), var(--lime-d));
}
.muted { color: var(--muted); font-size: 12px; }
/* Stat strip */
.stat-strip {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-bottom: 18px;
}
.stat {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 16px;
position: relative;
overflow: hidden;
transition: transform 0.18s ease, border-color 0.18s ease;
}
.stat:hover { transform: translateY(-3px); border-color: var(--line-2); }
.stat-label {
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
font-weight: 600;
}
.stat-value {
display: block;
font-size: 26px;
font-weight: 800;
margin: 6px 0 4px;
letter-spacing: 0.01em;
}
.stat-value em { font-size: 12px; font-style: normal; color: var(--muted); font-weight: 600; margin-left: 4px; }
.delta { font-size: 12px; font-weight: 600; }
.delta.up { color: var(--lime); }
.delta.down { color: var(--orange); }
.delta.neutral { color: var(--muted); }
.stat.streak::after {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(120px 60px at 90% 10%, rgba(255, 107, 53, 0.22), transparent 70%);
pointer-events: none;
}
.flame { filter: drop-shadow(0 0 6px rgba(255, 107, 53, 0.7)); }
/* Grid */
.grid {
display: grid;
grid-template-columns: 1.6fr 1fr;
gap: 16px;
}
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 18px;
}
.chart-card { grid-column: 1; grid-row: 1; }
.form-card { grid-column: 2; grid-row: 1 / span 2; }
.pr-card { grid-column: 1; grid-row: 2; }
.meas-card { grid-column: 1 / -1; grid-row: 3; }
.card-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
gap: 10px;
}
.card-head h2 {
margin: 0;
font-size: 15px;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
}
/* Segmented control */
.seg {
display: inline-flex;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: 999px;
padding: 3px;
}
.seg-btn {
border: 0;
background: transparent;
color: var(--muted);
font: inherit;
font-weight: 600;
font-size: 12px;
padding: 6px 14px;
border-radius: 999px;
cursor: pointer;
transition: color 0.15s, background 0.15s;
}
.seg-btn.active {
background: var(--lime);
color: var(--bg);
box-shadow: 0 0 14px rgba(194, 245, 66, 0.45);
}
.seg-btn:focus-visible { outline: 2px solid var(--lime); outline-offset: 2px; }
/* Chart */
.chart-shell { position: relative; }
#chart { width: 100%; height: 260px; display: block; }
#gridlines line { stroke: var(--line); stroke-width: 1; }
#linePath {
filter: drop-shadow(0 4px 10px rgba(194, 245, 66, 0.3));
stroke-dasharray: var(--len, 1600);
stroke-dashoffset: var(--len, 1600);
animation: draw 1.1s ease forwards;
}
@keyframes draw { to { stroke-dashoffset: 0; } }
#areaPath { opacity: 0; animation: fadeArea 0.9s ease 0.4s forwards; }
@keyframes fadeArea { to { opacity: 1; } }
.dot {
fill: var(--bg);
stroke: var(--lime);
stroke-width: 3;
cursor: pointer;
transition: r 0.15s ease;
}
.dot:hover, .dot:focus-visible { r: 7; stroke: var(--white); outline: none; }
.tooltip {
position: absolute;
transform: translate(-50%, -120%);
background: var(--surface-2);
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 6px 10px;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
pointer-events: none;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
}
.tooltip b { color: var(--lime); }
.chart-foot {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
font-size: 12px;
}
.legend { display: inline-flex; align-items: center; gap: 8px; color: var(--muted); font-weight: 600; }
.swatch { width: 14px; height: 4px; border-radius: 2px; background: var(--lime); display: inline-block; }
/* Form */
.field { display: block; margin-bottom: 14px; }
.field > span {
display: block;
font-size: 11px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
font-weight: 600;
margin-bottom: 6px;
}
input {
width: 100%;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-sm);
color: var(--ink);
font: inherit;
padding: 10px 12px;
transition: border-color 0.15s, box-shadow 0.15s;
}
input::placeholder { color: #6b7078; }
input:focus {
outline: none;
border-color: var(--lime);
box-shadow: 0 0 0 3px rgba(194, 245, 66, 0.2);
}
input:invalid:not(:placeholder-shown) { border-color: var(--orange); }
.pair { display: flex; align-items: center; gap: 8px; }
.times { color: var(--muted); font-weight: 700; }
.cta {
width: 100%;
border: 0;
border-radius: var(--r-sm);
background: var(--lime);
color: var(--bg);
font: inherit;
font-weight: 800;
letter-spacing: 0.06em;
text-transform: uppercase;
font-size: 14px;
padding: 12px;
margin-top: 4px;
cursor: pointer;
box-shadow: 0 0 0 rgba(194, 245, 66, 0);
transition: transform 0.12s, box-shadow 0.2s, background 0.15s;
}
.cta:hover { box-shadow: 0 6px 26px rgba(194, 245, 66, 0.5); background: var(--lime-d); }
.cta:active { transform: translateY(1px) scale(0.99); }
.cta:focus-visible { outline: 2px solid var(--white); outline-offset: 3px; }
.hint { font-size: 12px; color: var(--muted); margin: 10px 0 0; }
.hint.err { color: var(--orange); }
/* PR list */
.badge-count {
min-width: 24px;
height: 24px;
padding: 0 8px;
display: inline-grid;
place-items: center;
border-radius: 999px;
background: var(--surface-2);
border: 1px solid var(--line);
font-size: 12px;
font-weight: 700;
color: var(--lime);
}
.pr-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.pr-list li {
display: flex;
align-items: center;
gap: 12px;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 12px 14px;
transition: transform 0.15s, border-color 0.15s;
}
.pr-list li:hover { transform: translateX(3px); border-color: var(--line-2); }
.pr-lift { font-weight: 600; flex: 1; }
.pr-val { font-weight: 800; font-size: 16px; }
.pr-val em { font-style: normal; font-size: 11px; color: var(--muted); margin-left: 3px; }
.pr-tag {
font-size: 11px;
font-weight: 700;
color: var(--lime);
background: rgba(194, 245, 66, 0.12);
border-radius: 999px;
padding: 3px 8px;
}
.pr-tag.new {
color: var(--bg);
background: var(--orange);
animation: pop 0.5s ease;
}
@keyframes pop { 0% { transform: scale(0.6); } 60% { transform: scale(1.15); } 100% { transform: scale(1); } }
/* Measurements */
.meas-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 12px;
}
.meas {
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 14px 12px;
text-align: center;
transition: transform 0.15s, border-color 0.15s;
}
.meas:hover { transform: translateY(-3px); border-color: var(--line-2); }
.meas-label {
display: block;
font-size: 11px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
font-weight: 600;
margin-bottom: 6px;
}
.meas strong { font-size: 20px; font-weight: 800; display: block; }
.meas-delta { font-size: 12px; font-weight: 700; }
.meas-delta.up { color: var(--lime); }
.meas-delta.down { color: var(--orange); }
/* Toast */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 30px);
background: var(--lime);
color: var(--bg);
font-weight: 700;
font-size: 14px;
padding: 12px 20px;
border-radius: 999px;
box-shadow: 0 12px 40px rgba(194, 245, 66, 0.5);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s, transform 0.25s;
z-index: 40;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* Responsive */
@media (max-width: 820px) {
.grid { grid-template-columns: 1fr; }
.chart-card, .form-card, .pr-card, .meas-card {
grid-column: 1;
grid-row: auto;
}
.meas-grid { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 520px) {
.wrap { padding: 20px 14px 48px; }
.stat-strip { grid-template-columns: repeat(2, 1fr); }
.masthead { flex-direction: column; align-items: flex-start; }
.client-chip { width: 100%; }
.meas-grid { grid-template-columns: repeat(2, 1fr); }
.stat-value { font-size: 22px; }
}(function () {
"use strict";
// ---- Data model -------------------------------------------------------
// Each session: date, bodyweight (kg), and top set (load kg x reps).
// Strength index = Epley estimated 1RM = load * (1 + reps/30).
var sessions = [
{ date: "2026-05-04", weight: 70.5, load: 100, reps: 5 },
{ date: "2026-05-11", weight: 70.1, load: 102, reps: 5 },
{ date: "2026-05-18", weight: 69.6, load: 105, reps: 4 },
{ date: "2026-05-25", weight: 69.4, load: 108, reps: 5 },
{ date: "2026-06-01", weight: 69.0, load: 110, reps: 4 },
{ date: "2026-06-08", weight: 68.7, load: 112, reps: 5 },
{ date: "2026-06-15", weight: 68.6, load: 116, reps: 4 },
{ date: "2026-06-22", weight: 68.5, load: 118, reps: 5 },
{ date: "2026-06-29", weight: 68.4, load: 122, reps: 4 }
];
var metric = "weight"; // "weight" | "strength"
var streak = 14;
var W = 640, H = 260, PAD_L = 44, PAD_R = 16, PAD_T = 20, PAD_B = 30;
// ---- Element refs ------------------------------------------------------
var svg = document.getElementById("chart");
var gridG = document.getElementById("gridlines");
var linePath = document.getElementById("linePath");
var areaPath = document.getElementById("areaPath");
var dotsG = document.getElementById("dots");
var tooltip = document.getElementById("tooltip");
var legendLabel = document.getElementById("legendLabel");
var rangeLabel = document.getElementById("rangeLabel");
var toastEl = document.getElementById("toast");
var toastTimer;
var SVGNS = "http://www.w3.org/2000/svg";
function strengthIndex(s) { return Math.round(s.load * (1 + s.reps / 30)); }
function metricValue(s) { return metric === "weight" ? s.weight : strengthIndex(s); }
// ---- Toast -------------------------------------------------------------
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2600);
}
// ---- Chart rendering ---------------------------------------------------
function buildScales() {
var vals = sessions.map(metricValue);
var min = Math.min.apply(null, vals);
var max = Math.max.apply(null, vals);
var span = max - min || 1;
// pad the domain so the line doesn't hug the edges
min -= span * 0.18;
max += span * 0.18;
var n = sessions.length;
return {
x: function (i) { return PAD_L + (i / Math.max(n - 1, 1)) * (W - PAD_L - PAD_R); },
y: function (v) { return PAD_T + (1 - (v - min) / (max - min)) * (H - PAD_T - PAD_B); },
min: min, max: max
};
}
function clearNode(node) { while (node.firstChild) node.removeChild(node.firstChild); }
function renderGrid(sc) {
clearNode(gridG);
for (var g = 0; g <= 4; g++) {
var y = PAD_T + (g / 4) * (H - PAD_T - PAD_B);
var ln = document.createElementNS(SVGNS, "line");
ln.setAttribute("x1", PAD_L); ln.setAttribute("x2", W - PAD_R);
ln.setAttribute("y1", y); ln.setAttribute("y2", y);
gridG.appendChild(ln);
var val = sc.max - (g / 4) * (sc.max - sc.min);
var t = document.createElementNS(SVGNS, "text");
t.setAttribute("x", PAD_L - 8); t.setAttribute("y", y + 4);
t.setAttribute("text-anchor", "end");
t.setAttribute("fill", "#6b7078");
t.setAttribute("font-size", "11");
t.textContent = Math.round(val);
gridG.appendChild(t);
}
}
function renderDots(sc) {
clearNode(dotsG);
sessions.forEach(function (s, i) {
var cx = sc.x(i), cy = sc.y(metricValue(s));
var c = document.createElementNS(SVGNS, "circle");
c.setAttribute("class", "dot");
c.setAttribute("cx", cx); c.setAttribute("cy", cy); c.setAttribute("r", 4.5);
c.setAttribute("tabindex", "0");
var unit = metric === "weight" ? " kg" : " pts";
var label = metric === "weight" ? "Bodyweight" : "Strength";
c.setAttribute("aria-label", label + " " + metricValue(s) + unit + " on " + fmtDate(s.date));
c.addEventListener("mouseenter", function () { showTip(cx, cy, s); });
c.addEventListener("mouseleave", hideTip);
c.addEventListener("focus", function () { showTip(cx, cy, s); });
c.addEventListener("blur", hideTip);
dotsG.appendChild(c);
});
}
function pathFromPoints(sc, closeArea) {
var d = "";
sessions.forEach(function (s, i) {
d += (i === 0 ? "M" : "L") + sc.x(i).toFixed(1) + " " + sc.y(metricValue(s)).toFixed(1) + " ";
});
if (closeArea) {
var lastX = sc.x(sessions.length - 1), firstX = sc.x(0), baseY = H - PAD_B;
d += "L" + lastX.toFixed(1) + " " + baseY + " L" + firstX.toFixed(1) + " " + baseY + " Z";
}
return d.trim();
}
function renderChart(animate) {
var sc = buildScales();
renderGrid(sc);
var line = pathFromPoints(sc, false);
linePath.setAttribute("d", line);
areaPath.setAttribute("d", pathFromPoints(sc, true));
renderDots(sc);
// re-trigger the draw animation
var len = linePath.getTotalLength ? linePath.getTotalLength() : 1600;
linePath.style.setProperty("--len", len);
if (animate) {
linePath.style.animation = "none";
// force reflow
void linePath.getBBox();
linePath.style.animation = "draw 1.1s ease forwards";
}
var unit = metric === "weight" ? "(kg)" : "(pts)";
legendLabel.textContent = (metric === "weight" ? "Bodyweight " : "Strength index ") + unit;
rangeLabel.textContent = "Last " + sessions.length + " sessions";
}
// ---- Tooltip -----------------------------------------------------------
function showTip(cx, cy, s) {
var rect = svg.getBoundingClientRect();
var px = (cx / W) * rect.width;
var py = (cy / H) * rect.height;
var unit = metric === "weight" ? " kg" : " pts";
tooltip.innerHTML = fmtDate(s.date) + " · <b>" + metricValue(s) + unit + "</b>";
tooltip.hidden = false;
tooltip.style.left = px + "px";
tooltip.style.top = py + "px";
}
function hideTip() { tooltip.hidden = true; }
// ---- Stats -------------------------------------------------------------
function fmtDate(iso) {
var d = new Date(iso + "T00:00:00");
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}
function refreshStats() {
var first = sessions[0], last = sessions[sessions.length - 1];
var wEl = document.getElementById("statWeight");
var sEl = document.getElementById("statStrength");
var sessEl = document.getElementById("statSessions");
wEl.innerHTML = last.weight.toFixed(1) + "<em>kg</em>";
sEl.innerHTML = strengthIndex(last) + "<em>pts</em>";
sessEl.innerHTML = sessions.length + "<em>logged</em>";
var wDelta = last.weight - first.weight;
var wd = document.getElementById("statWeightDelta");
wd.textContent = (wDelta <= 0 ? "▼ " : "▲ ") + Math.abs(wDelta).toFixed(1) + " kg";
wd.className = "delta " + (wDelta <= 0 ? "down" : "up");
var sDelta = strengthIndex(last) - strengthIndex(first);
var sd = document.getElementById("statStrengthDelta");
sd.textContent = (sDelta >= 0 ? "▲ " : "▼ ") + Math.abs(sDelta) + " pts";
sd.className = "delta " + (sDelta >= 0 ? "up" : "down");
}
// ---- Metric toggle -----------------------------------------------------
Array.prototype.forEach.call(document.querySelectorAll(".seg-btn"), function (btn) {
btn.addEventListener("click", function () {
if (btn.classList.contains("active")) return;
document.querySelectorAll(".seg-btn").forEach(function (b) {
b.classList.remove("active");
b.setAttribute("aria-selected", "false");
});
btn.classList.add("active");
btn.setAttribute("aria-selected", "true");
metric = btn.getAttribute("data-metric");
hideTip();
renderChart(true);
});
});
// ---- Log form ----------------------------------------------------------
var form = document.getElementById("logForm");
var hint = document.getElementById("formHint");
var fDate = document.getElementById("fDate");
fDate.value = new Date().toISOString().slice(0, 10);
form.addEventListener("submit", function (e) {
e.preventDefault();
var weight = parseFloat(document.getElementById("fWeight").value);
var load = parseInt(document.getElementById("fLoad").value, 10);
var reps = parseInt(document.getElementById("fReps").value, 10);
var date = fDate.value;
if (!date || isNaN(weight) || isNaN(load) || isNaN(reps)) {
hint.textContent = "Fill in date, bodyweight and your top set.";
hint.classList.add("err");
return;
}
hint.classList.remove("err");
hint.textContent = "Estimated 1RM auto-computes from load × reps.";
var prev = sessions[sessions.length - 1];
sessions.push({ date: date, weight: weight, load: load, reps: reps });
if (sessions.length > 12) sessions.shift();
streak += 1;
document.getElementById("streakNum").textContent = streak;
refreshStats();
renderChart(true);
var newIndex = strengthIndex({ load: load, reps: reps });
if (prev && newIndex > strengthIndex(prev)) {
addPR(load, reps, newIndex);
toast("New strength PR logged — 🔥 " + streak + "-day streak!");
} else {
toast("Session saved — 🔥 " + streak + "-day streak!");
}
form.reset();
fDate.value = new Date().toISOString().slice(0, 10);
document.getElementById("fWeight").focus();
});
// ---- PR list -----------------------------------------------------------
function addPR(load, reps, index) {
var list = document.getElementById("prList");
var li = document.createElement("li");
li.innerHTML =
'<span class="pr-lift">Squat e1RM</span>' +
'<span class="pr-val">' + index + ' <em>kg</em></span>' +
'<span class="pr-tag new">PR</span>';
list.insertBefore(li, list.firstChild);
while (list.children.length > 5) list.removeChild(list.lastChild);
var countEl = document.getElementById("prCount");
countEl.textContent = Math.min(parseInt(countEl.textContent, 10) + 1, 99);
}
// ---- Init --------------------------------------------------------------
refreshStats();
renderChart(true);
window.addEventListener("resize", function () { hideTip(); });
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Coach — Progress 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&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="wrap">
<header class="masthead">
<div class="brand">
<span class="brand-mark" aria-hidden="true">◢◤</span>
<div>
<p class="eyebrow">Ironhouse Coaching</p>
<h1>Progress Tracker</h1>
</div>
</div>
<div class="client-chip">
<span class="avatar" aria-hidden="true">MR</span>
<div>
<strong>Maya Ruiz</strong>
<span class="muted">12-week strength block · Wk 7</span>
</div>
</div>
</header>
<section class="stat-strip" aria-label="Key stats">
<div class="stat">
<span class="stat-label">Bodyweight</span>
<strong class="stat-value" id="statWeight">68.4<em>kg</em></strong>
<span class="delta down" id="statWeightDelta">▼ 2.1 kg</span>
</div>
<div class="stat">
<span class="stat-label">Strength Index</span>
<strong class="stat-value" id="statStrength">312<em>pts</em></strong>
<span class="delta up" id="statStrengthDelta">▲ 46 pts</span>
</div>
<div class="stat">
<span class="stat-label">Sessions</span>
<strong class="stat-value" id="statSessions">9<em>logged</em></strong>
<span class="delta neutral">this block</span>
</div>
<div class="stat streak" id="streakStat">
<span class="stat-label">Streak</span>
<strong class="stat-value"><span id="streakNum">14</span><em>days</em> <span class="flame" aria-hidden="true">🔥</span></strong>
<span class="delta up">keep it lit</span>
</div>
</section>
<div class="grid">
<section class="card chart-card" aria-labelledby="chartTitle">
<div class="card-head">
<h2 id="chartTitle">Trend</h2>
<div class="seg" role="tablist" aria-label="Choose metric">
<button class="seg-btn active" role="tab" aria-selected="true" data-metric="weight">Weight</button>
<button class="seg-btn" role="tab" aria-selected="false" data-metric="strength">Strength</button>
</div>
</div>
<div class="chart-shell">
<svg id="chart" viewBox="0 0 640 260" preserveAspectRatio="none" role="img" aria-label="Progress line chart">
<defs>
<linearGradient id="area" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#c2f542" stop-opacity="0.35" />
<stop offset="100%" stop-color="#c2f542" stop-opacity="0" />
</linearGradient>
</defs>
<g id="gridlines"></g>
<path id="areaPath" fill="url(#area)" d="" />
<path id="linePath" fill="none" stroke="#c2f542" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" d="" />
<g id="dots"></g>
</svg>
<div class="tooltip" id="tooltip" hidden></div>
</div>
<div class="chart-foot">
<span class="legend"><i class="swatch"></i> <span id="legendLabel">Bodyweight (kg)</span></span>
<span class="muted" id="rangeLabel">Last 9 sessions</span>
</div>
</section>
<section class="card form-card" aria-labelledby="logTitle">
<div class="card-head"><h2 id="logTitle">Log Entry</h2></div>
<form id="logForm" novalidate>
<label class="field">
<span>Date</span>
<input type="date" id="fDate" required />
</label>
<label class="field">
<span>Bodyweight (kg)</span>
<input type="number" id="fWeight" step="0.1" min="30" max="250" placeholder="68.0" required />
</label>
<label class="field">
<span>Top set — Squat (kg × reps)</span>
<div class="pair">
<input type="number" id="fLoad" min="0" max="500" placeholder="110" required />
<span class="times">×</span>
<input type="number" id="fReps" min="1" max="30" placeholder="5" required />
</div>
</label>
<button type="submit" class="cta">Save Session</button>
<p class="hint" id="formHint">Estimated 1RM auto-computes from load × reps.</p>
</form>
</section>
<section class="card pr-card" aria-labelledby="prTitle">
<div class="card-head">
<h2 id="prTitle">Personal Records</h2>
<span class="badge-count" id="prCount">4</span>
</div>
<ul class="pr-list" id="prList">
<li><span class="pr-lift">Back Squat</span><span class="pr-val">140 <em>kg</em></span><span class="pr-tag">+5</span></li>
<li><span class="pr-lift">Deadlift</span><span class="pr-val">172 <em>kg</em></span><span class="pr-tag">+7</span></li>
<li><span class="pr-lift">Bench Press</span><span class="pr-val">82 <em>kg</em></span><span class="pr-tag new">PR</span></li>
<li><span class="pr-lift">Strict Press</span><span class="pr-val">50 <em>kg</em></span><span class="pr-tag">+2</span></li>
</ul>
</section>
<section class="card meas-card" aria-labelledby="measTitle">
<div class="card-head">
<h2 id="measTitle">Measurements</h2>
<span class="muted">cm · updated Wk 7</span>
</div>
<div class="meas-grid">
<div class="meas"><span class="meas-label">Chest</span><strong>102.0</strong><span class="meas-delta up">+1.5</span></div>
<div class="meas"><span class="meas-label">Waist</span><strong>74.5</strong><span class="meas-delta down">-3.0</span></div>
<div class="meas"><span class="meas-label">Hips</span><strong>96.0</strong><span class="meas-delta down">-1.2</span></div>
<div class="meas"><span class="meas-label">Arm</span><strong>34.8</strong><span class="meas-delta up">+0.9</span></div>
<div class="meas"><span class="meas-label">Thigh</span><strong>58.2</strong><span class="meas-delta up">+1.1</span></div>
<div class="meas"><span class="meas-label">Calf</span><strong>37.4</strong><span class="meas-delta up">+0.4</span></div>
</div>
</section>
</div>
</main>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A client-facing progress tracker built for online fitness coaches. The centerpiece is a hand-drawn inline-SVG line chart that plots bodyweight against a strength index across recent sessions; the path strokes itself on load and animates again every time new data arrives. A toggle swaps the visible metric, an area gradient fills beneath the trend, and hovering a data point surfaces a labelled tooltip so clients can read exact figures without leaving the card.
Below the chart, a compact log-entry form captures date, bodyweight, and top set. Submitting it prepends the entry, rescales the chart, bumps the streak counter, and fires a motivational toast — all in vanilla JS with no dependencies. The streak card burns brighter as the number climbs, and a personal-records list celebrates fresh PRs with an animated badge.
The layout is responsive down to roughly 360px, collapsing the two-column grid into a single stack while keeping the measurements grid legible. Every control is keyboard-usable with visible focus rings, controls carry ARIA labels, and the palette holds WCAG AA contrast against the near-black background for readable, energetic reporting.