Gym — Client Progress View
A trainer-facing progress dashboard for a single gym client, with an avatar header, goal and since-date, and an animated SVG adherence ring. A metric tab switcher re-renders self-drawn line charts for body weight, bench and squat over twelve weeks, alongside a KPI strip, a measurements table with chest, waist and arm deltas, a session-attendance heat strip and a coaching-notes timeline. A Send-program button and an inline note composer append entries live, all in dependency-free vanilla JS.
MCP
コード
: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;
--sh-1: 0 1px 0 rgba(255, 255, 255, 0.03) inset, 0 8px 24px rgba(0, 0, 0, 0.4);
--sh-2: 0 12px 40px rgba(0, 0, 0, 0.55);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
background:
radial-gradient(1100px 520px at 85% -10%, rgba(198, 255, 58, 0.07), transparent 60%),
radial-gradient(900px 500px at -5% 0%, rgba(255, 106, 43, 0.06), transparent 55%),
var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
.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;
}
.shell {
max-width: 1080px;
margin: 0 auto;
padding: 28px 20px 56px;
}
.eyebrow {
margin: 0 0 6px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
}
h1, h2 { margin: 0; letter-spacing: -0.02em; }
h1 { font-size: 30px; font-weight: 900; }
h2 { font-size: 18px; font-weight: 800; }
/* ---------- Buttons ---------- */
.btn {
appearance: none;
border: 1px solid var(--line-2);
background: var(--surface-2);
color: var(--ink);
font: inherit;
font-weight: 700;
padding: 12px 18px;
border-radius: var(--r-md);
cursor: pointer;
transition: transform .12s ease, background .15s ease, border-color .15s ease, box-shadow .15s ease;
}
.btn:hover { transform: translateY(-1px); border-color: var(--line-2); }
.btn:active { transform: translateY(0); }
.btn:focus-visible { outline: 3px solid var(--neon); outline-offset: 2px; }
.btn--neon {
background: var(--neon);
border-color: var(--neon-d);
color: #0c1300;
box-shadow: 0 8px 20px rgba(198, 255, 58, 0.22);
}
.btn--neon:hover { background: #d3ff5c; }
.btn--ghost { background: transparent; border-color: var(--line-2); color: var(--ink-2); }
.btn--ghost:hover { color: var(--ink); background: var(--surface-2); }
.btn--sm { padding: 8px 14px; font-size: 13px; border-radius: var(--r-sm); }
/* ---------- Client header ---------- */
.client {
display: grid;
grid-template-columns: 1fr auto auto;
gap: 22px;
align-items: center;
background: linear-gradient(180deg, var(--surface), var(--surface));
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 22px 24px;
box-shadow: var(--sh-1);
}
.client__id { display: flex; gap: 18px; align-items: center; min-width: 0; }
.avatar {
flex: none;
width: 72px; height: 72px;
border-radius: 18px;
display: grid; place-items: center;
font-weight: 900; font-size: 24px;
color: #0c1300;
background: linear-gradient(135deg, var(--neon), var(--neon-d));
box-shadow: 0 8px 22px rgba(198, 255, 58, 0.25);
}
.client__sub { margin: 6px 0 10px; color: var(--ink-2); font-size: 14px; }
.client__sub strong { color: var(--ink); }
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
font-size: 12px; font-weight: 600;
padding: 5px 11px;
border-radius: 999px;
background: var(--surface-2);
border: 1px solid var(--line);
color: var(--ink-2);
}
.chip--neon { background: var(--neon-50); border-color: rgba(198, 255, 58, 0.3); color: var(--neon); }
.adherence { position: relative; width: 120px; height: 120px; }
.ring { width: 120px; height: 120px; transform: rotate(-90deg); }
.ring__track { fill: none; stroke: var(--surface-2); stroke-width: 11; }
.ring__fill {
fill: none;
stroke: var(--neon);
stroke-width: 11;
stroke-linecap: round;
stroke-dasharray: 326.7;
stroke-dashoffset: 326.7;
filter: drop-shadow(0 0 6px rgba(198, 255, 58, 0.5));
transition: stroke-dashoffset 1s cubic-bezier(.2, .8, .2, 1);
}
.adherence__txt {
position: absolute; inset: 0;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
}
.adherence__pct { font-size: 26px; font-weight: 900; letter-spacing: -0.02em; }
.adherence__lbl { font-size: 10px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); }
.header-actions { display: flex; flex-direction: column; gap: 10px; }
/* ---------- KPIs ---------- */
.kpis {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin: 18px 0;
}
.kpi {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 16px 18px;
display: flex; flex-direction: column; gap: 6px;
}
.kpi__lbl { font-size: 11px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); }
.kpi__val { font-size: 28px; font-weight: 900; letter-spacing: -0.03em; }
.kpi__val small { font-size: 14px; font-weight: 700; color: var(--muted); margin-left: 2px; }
.delta {
align-self: flex-start;
font-size: 12px; font-weight: 700;
padding: 3px 9px; border-radius: 999px;
}
.delta--up { color: var(--neon); background: var(--neon-50); }
.delta--down { color: var(--ok); background: rgba(52, 211, 153, 0.14); }
/* ---------- Grid / cards ---------- */
.grid {
display: grid;
grid-template-columns: 1.55fr 1fr;
gap: 18px;
}
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 20px 22px;
box-shadow: var(--sh-1);
}
.card--chart { grid-column: 1; grid-row: 1 / span 2; }
.card--notes { grid-column: 1 / -1; }
.card__head {
display: flex; align-items: flex-start; justify-content: space-between;
gap: 16px; margin-bottom: 16px;
}
/* ---------- Tabs ---------- */
.tabs {
display: inline-flex;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: 999px;
padding: 4px;
gap: 2px;
}
.tab {
appearance: none; border: 0; background: transparent;
color: var(--ink-2); font: inherit; font-weight: 700; font-size: 13px;
padding: 7px 16px; border-radius: 999px; cursor: pointer;
transition: color .15s ease, background .15s ease;
}
.tab:hover { color: var(--ink); }
.tab.is-active { background: var(--neon); color: #0c1300; }
.tab:focus-visible { outline: 3px solid var(--neon); outline-offset: 2px; }
/* ---------- Chart ---------- */
.chart-wrap {
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 14px;
}
.chart { width: 100%; height: 240px; display: block; }
.chart-grid { stroke: rgba(255, 255, 255, 0.06); stroke-width: 1; }
.chart-area { transition: opacity .3s ease; }
.chart-line { fill: none; stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; }
.chart-dot { transition: r .15s ease; }
.chart-dot:hover { r: 6; }
.chart-lbl { fill: var(--muted); font-size: 11px; font-weight: 600; font-family: "Inter", sans-serif; }
.chart-foot {
display: flex; gap: 18px; flex-wrap: wrap;
margin-top: 12px; font-size: 13px; color: var(--ink-2);
}
.chart-foot b { color: var(--ink); }
/* ---------- Measurements ---------- */
.measure { width: 100%; border-collapse: collapse; font-size: 14px; }
.measure th, .measure td { text-align: left; padding: 11px 8px; }
.measure thead th {
font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
color: var(--muted); border-bottom: 1px solid var(--line);
}
.measure tbody th { font-weight: 700; }
.measure tbody td { color: var(--ink-2); }
.measure tbody tr { border-bottom: 1px solid var(--line); }
.measure tbody tr:last-child { border-bottom: 0; }
.measure tbody tr:hover { background: var(--surface-2); }
.measure td:last-child, .measure th:last-child { text-align: right; }
/* ---------- Heat strip ---------- */
.heat-key { display: flex; align-items: center; gap: 5px; font-size: 11px; color: var(--muted); font-weight: 600; }
.hk { width: 12px; height: 12px; border-radius: 3px; border: 1px solid var(--line); }
.hk-0 { background: var(--surface-2); }
.hk-1 { background: rgba(198, 255, 58, 0.3); }
.hk-2 { background: rgba(198, 255, 58, 0.6); }
.hk-3 { background: var(--neon); }
.heat {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 6px;
}
.heat .cell {
aspect-ratio: 1 / 1;
border-radius: 6px;
border: 1px solid var(--line);
transition: transform .12s ease, box-shadow .12s ease;
cursor: default;
}
.heat .cell:hover { transform: scale(1.12); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); }
.heat .l0 { background: var(--surface-2); }
.heat .l1 { background: rgba(198, 255, 58, 0.3); }
.heat .l2 { background: rgba(198, 255, 58, 0.6); }
.heat .l3 { background: var(--neon); }
/* ---------- Notes / composer / timeline ---------- */
.composer {
background: var(--surface-2);
border: 1px solid var(--line-2);
border-radius: var(--r-md);
padding: 14px;
margin-bottom: 16px;
animation: pop .2s ease;
}
@keyframes pop { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
.composer textarea {
width: 100%; resize: vertical;
background: var(--bg); color: var(--ink);
border: 1px solid var(--line); border-radius: var(--r-sm);
padding: 10px 12px; font: inherit; font-size: 14px;
}
.composer textarea:focus-visible { outline: 2px solid var(--neon); outline-offset: 1px; border-color: transparent; }
.composer__row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 10px; }
.composer select {
background: var(--bg); color: var(--ink);
border: 1px solid var(--line); border-radius: var(--r-sm);
padding: 8px 12px; font: inherit; font-weight: 600; font-size: 13px;
}
.composer__btns { display: flex; gap: 8px; }
.timeline { list-style: none; margin: 0; padding: 0; }
.note {
position: relative;
padding: 0 0 18px 24px;
border-left: 2px solid var(--line);
}
.note:last-child { padding-bottom: 0; border-left-color: transparent; }
.note::before {
content: ""; position: absolute; left: -7px; top: 4px;
width: 12px; height: 12px; border-radius: 50%;
background: var(--neon); box-shadow: 0 0 0 4px var(--surface);
}
.note.is-new { animation: pop .25s ease; }
.note__top { display: flex; align-items: center; gap: 10px; margin-bottom: 4px; }
.note__tag {
font-size: 10px; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase;
padding: 3px 8px; border-radius: 999px;
background: var(--surface-2); border: 1px solid var(--line); color: var(--ink-2);
}
.note__tag.t-training { color: var(--neon); border-color: rgba(198, 255, 58, 0.3); background: var(--neon-50); }
.note__tag.t-nutrition { color: var(--orange); border-color: rgba(255, 106, 43, 0.3); background: var(--orange-soft); }
.note__tag.t-recovery { color: #7dd3fc; border-color: rgba(125, 211, 252, 0.3); background: rgba(125, 211, 252, 0.12); }
.note__tag.t-flag { color: var(--warn); border-color: rgba(251, 191, 36, 0.3); background: rgba(251, 191, 36, 0.12); }
.note__time { font-size: 12px; color: var(--muted); font-weight: 600; }
.note__body { color: var(--ink-2); font-size: 14px; margin: 0; }
.footnote { margin: 28px 2px 0; color: var(--muted); font-size: 12px; }
/* ---------- Toast ---------- */
.toast {
position: fixed; left: 50%; bottom: 26px;
transform: translate(-50%, 30px);
background: var(--elevated);
border: 1px solid var(--line-2);
color: var(--ink);
font-weight: 700; font-size: 14px;
padding: 12px 20px; border-radius: var(--r-md);
box-shadow: var(--sh-2);
opacity: 0; pointer-events: none;
transition: opacity .2s ease, transform .2s ease;
z-index: 50;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
.toast::before { content: "✓ "; color: var(--neon); }
/* ---------- Responsive ---------- */
@media (max-width: 900px) {
.grid { grid-template-columns: 1fr; }
.card--chart { grid-row: auto; }
.client { grid-template-columns: 1fr auto; }
.header-actions { grid-column: 1 / -1; flex-direction: row; }
.header-actions .btn { flex: 1; }
}
@media (max-width: 520px) {
.shell { padding: 18px 14px 44px; }
h1 { font-size: 24px; }
.client { grid-template-columns: 1fr; padding: 18px; }
.client__id { gap: 14px; }
.avatar { width: 58px; height: 58px; font-size: 20px; border-radius: 14px; }
.adherence { justify-self: start; }
.kpis { grid-template-columns: repeat(2, 1fr); }
.card__head { flex-direction: column; align-items: flex-start; }
.tabs { width: 100%; justify-content: space-between; }
.tab { flex: 1; text-align: center; padding: 8px 6px; }
.heat { grid-template-columns: repeat(6, 1fr); }
.composer__row { flex-direction: column; align-items: stretch; }
.composer__btns { justify-content: flex-end; }
}(function () {
"use strict";
/* ---------- Toast helper ---------- */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2400);
}
/* ---------- Adherence ring ---------- */
(function ring() {
var fill = document.getElementById("ringFill");
var pct = 87;
var C = 2 * Math.PI * 52; // ~326.7
requestAnimationFrame(function () {
fill.style.strokeDashoffset = String(C * (1 - pct / 100));
});
document.getElementById("ringPct").textContent = pct + "%";
})();
/* ---------- Chart data (12 weekly points) ---------- */
var SERIES = {
weight: {
title: "Body weight",
unit: "kg",
color: "#c6ff3a",
data: [86.8, 86.1, 85.4, 85.0, 84.3, 83.9, 83.4, 83.0, 82.7, 82.5, 82.3, 82.1],
foot: function (d) {
return [
["Start", d[0] + " kg"],
["Now", d[d.length - 1] + " kg"],
["Change", (d[d.length - 1] - d[0]).toFixed(1) + " kg"]
];
}
},
bench: {
title: "Bench press 1RM",
unit: "kg",
color: "#ff6a2b",
data: [93, 94, 95, 95, 96, 98, 99, 99, 100, 101, 101, 102],
foot: function (d) {
return [
["Start", d[0] + " kg"],
["Now", d[d.length - 1] + " kg"],
["Gain", "+" + (d[d.length - 1] - d[0]) + " kg"]
];
}
},
squat: {
title: "Back squat 1RM",
unit: "kg",
color: "#34d399",
data: [125, 127, 128, 130, 132, 133, 135, 138, 140, 141, 143, 145],
foot: function (d) {
return [
["Start", d[0] + " kg"],
["Now", d[d.length - 1] + " kg"],
["Gain", "+" + (d[d.length - 1] - d[0]) + " kg"]
];
}
}
};
var W = 640, H = 260, PAD_L = 44, PAD_R = 14, PAD_T = 16, PAD_B = 28;
var svg = document.getElementById("chart");
var titleEl = document.getElementById("chartTitle");
var footEl = document.getElementById("chartFoot");
var SVGNS = "http://www.w3.org/2000/svg";
function el(name, attrs) {
var n = document.createElementNS(SVGNS, name);
for (var k in attrs) n.setAttribute(k, attrs[k]);
return n;
}
function renderChart(key) {
var s = SERIES[key];
var d = s.data;
var min = Math.min.apply(null, d);
var max = Math.max.apply(null, d);
var span = max - min || 1;
// pad domain a touch
var lo = min - span * 0.15;
var hi = max + span * 0.15;
var range = hi - lo;
var innerW = W - PAD_L - PAD_R;
var innerH = H - PAD_T - PAD_B;
function x(i) { return PAD_L + (i / (d.length - 1)) * innerW; }
function y(v) { return PAD_T + (1 - (v - lo) / range) * innerH; }
svg.textContent = "";
// horizontal gridlines + y labels
var rows = 4;
for (var r = 0; r <= rows; r++) {
var gv = lo + (range * r) / rows;
var gy = y(gv);
svg.appendChild(el("line", { class: "chart-grid", x1: PAD_L, y1: gy, x2: W - PAD_R, y2: gy }));
var lbl = el("text", { class: "chart-lbl", x: PAD_L - 8, y: gy + 4, "text-anchor": "end" });
lbl.textContent = Math.round(gv);
svg.appendChild(lbl);
}
// x labels (weeks)
for (var i = 0; i < d.length; i++) {
if (i % 2 !== 0 && i !== d.length - 1) continue;
var t = el("text", { class: "chart-lbl", x: x(i), y: H - 8, "text-anchor": "middle" });
t.textContent = "W" + (i + 1);
svg.appendChild(t);
}
// build path
var line = "";
for (var j = 0; j < d.length; j++) {
line += (j === 0 ? "M" : "L") + x(j).toFixed(1) + " " + y(d[j]).toFixed(1) + " ";
}
var area = line + "L" + x(d.length - 1).toFixed(1) + " " + (H - PAD_B) + " L" + PAD_L + " " + (H - PAD_B) + " Z";
// gradient fill
var defs = el("defs", {});
var grad = el("linearGradient", { id: "g_" + key, x1: 0, y1: 0, x2: 0, y2: 1 });
grad.appendChild(el("stop", { offset: "0%", "stop-color": s.color, "stop-opacity": "0.28" }));
grad.appendChild(el("stop", { offset: "100%", "stop-color": s.color, "stop-opacity": "0" }));
defs.appendChild(grad);
svg.appendChild(defs);
svg.appendChild(el("path", { class: "chart-area", d: area, fill: "url(#g_" + key + ")" }));
svg.appendChild(el("path", { class: "chart-line", d: line, stroke: s.color }));
// dots
for (var p = 0; p < d.length; p++) {
var dot = el("circle", { class: "chart-dot", cx: x(p), cy: y(d[p]), r: 3.5, fill: s.color });
var tt = el("title", {});
tt.textContent = "Week " + (p + 1) + ": " + d[p] + " " + s.unit;
dot.appendChild(tt);
svg.appendChild(dot);
}
titleEl.textContent = s.title;
svg.setAttribute("aria-label", s.title + " over 12 weeks");
// footer stats
footEl.textContent = "";
s.foot(d).forEach(function (pair) {
var span2 = document.createElement("span");
span2.innerHTML = pair[0] + " <b>" + pair[1] + "</b>";
footEl.appendChild(span2);
});
}
var tabs = Array.prototype.slice.call(document.querySelectorAll(".tab"));
tabs.forEach(function (tab) {
tab.addEventListener("click", function () {
tabs.forEach(function (t) {
t.classList.remove("is-active");
t.setAttribute("aria-selected", "false");
});
tab.classList.add("is-active");
tab.setAttribute("aria-selected", "true");
renderChart(tab.dataset.metric);
});
});
renderChart("weight");
/* ---------- Attendance heat strip (12 weeks × 3 sessions) ---------- */
(function heat() {
var grid = document.getElementById("heat");
// value 0-3 sessions attended that week
var weeks = [3, 3, 2, 3, 3, 3, 1, 3, 3, 2, 3, 3];
var labels = ["No sessions", "1 of 3 sessions", "2 of 3 sessions", "Full week — 3/3"];
weeks.forEach(function (v, idx) {
var c = document.createElement("div");
c.className = "cell l" + v;
c.setAttribute("role", "img");
c.setAttribute("title", "Week " + (idx + 1) + ": " + labels[v]);
c.setAttribute("aria-label", "Week " + (idx + 1) + ", " + labels[v]);
grid.appendChild(c);
});
})();
/* ---------- Notes timeline ---------- */
var timeline = document.getElementById("timeline");
var seed = [
{ tag: "training", time: "2 days ago", body: "Bumped bench to 3×6 @ 92.5 kg — bar speed strong, added a back-off set." },
{ tag: "nutrition", time: "5 days ago", body: "Protein dialed to 180g/day. Reports better gym energy on training days." },
{ tag: "recovery", time: "1 week ago", body: "Right shoulder a little cranky — swapped flat for slight incline, cued scap retraction." },
{ tag: "flag", time: "2 weeks ago", body: "Missed Wed session (travel). Adjusted week to 2 sessions, kept volume." }
];
function tagLabel(t) {
return { training: "Training", nutrition: "Nutrition", recovery: "Recovery", flag: "Flag" }[t] || t;
}
function makeNote(n, isNew) {
var li = document.createElement("li");
li.className = "note" + (isNew ? " is-new" : "");
var top = document.createElement("div");
top.className = "note__top";
var tag = document.createElement("span");
tag.className = "note__tag t-" + n.tag;
tag.textContent = tagLabel(n.tag);
var time = document.createElement("span");
time.className = "note__time";
time.textContent = n.time;
top.appendChild(tag);
top.appendChild(time);
var body = document.createElement("p");
body.className = "note__body";
body.textContent = n.body;
li.appendChild(top);
li.appendChild(body);
return li;
}
seed.forEach(function (n) { timeline.appendChild(makeNote(n, false)); });
/* ---------- Composer ---------- */
var composer = document.getElementById("composer");
var noteInput = document.getElementById("noteInput");
var noteTag = document.getElementById("noteTag");
function showComposer() {
composer.hidden = false;
noteInput.focus();
}
function hideComposer() {
composer.hidden = true;
noteInput.value = "";
noteTag.value = "training";
}
document.getElementById("openNote").addEventListener("click", showComposer);
document.getElementById("openNote2").addEventListener("click", showComposer);
document.getElementById("cancelNote").addEventListener("click", hideComposer);
composer.addEventListener("submit", function (e) {
e.preventDefault();
var text = noteInput.value.trim();
if (!text) {
noteInput.focus();
toast("Write something first");
return;
}
var li = makeNote({ tag: noteTag.value, time: "Just now", body: text }, true);
timeline.insertBefore(li, timeline.firstChild);
hideComposer();
toast("Note added to timeline");
});
/* ---------- Send program ---------- */
document.getElementById("sendProgram").addEventListener("click", function () {
var btn = this;
var orig = btn.textContent;
btn.disabled = true;
btn.textContent = "Sending…";
setTimeout(function () {
btn.textContent = "Sent ✓";
toast("Week 13 program sent to Marcus");
setTimeout(function () {
btn.textContent = orig;
btn.disabled = false;
}, 1600);
}, 700);
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Gym — Client Progress View</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>
<main class="shell">
<!-- Client header -->
<header class="client" aria-label="Client overview">
<div class="client__id">
<div class="avatar" aria-hidden="true">MR</div>
<div class="client__meta">
<p class="eyebrow">Client · Strength & Cut</p>
<h1>Marcus Reyes</h1>
<p class="client__sub">Goal: <strong>Drop 6 kg, +bench</strong> · Member since <strong>Jan 2026</strong></p>
<div class="chips">
<span class="chip chip--neon">Hypertrophy block</span>
<span class="chip">3×/week</span>
<span class="chip">Coach: Dana V.</span>
</div>
</div>
</div>
<div class="adherence" role="group" aria-label="Program adherence">
<svg class="ring" viewBox="0 0 120 120" aria-hidden="true">
<circle class="ring__track" cx="60" cy="60" r="52" />
<circle class="ring__fill" cx="60" cy="60" r="52" id="ringFill" />
</svg>
<div class="adherence__txt">
<span class="adherence__pct" id="ringPct">87%</span>
<span class="adherence__lbl">Adherence</span>
</div>
</div>
<div class="header-actions">
<button class="btn btn--neon" id="sendProgram" type="button">Send program</button>
<button class="btn btn--ghost" id="openNote" type="button">Add note</button>
</div>
</header>
<!-- KPI strip -->
<section class="kpis" aria-label="Key metrics">
<div class="kpi">
<span class="kpi__lbl">Weight</span>
<span class="kpi__val">82.1<small>kg</small></span>
<span class="delta delta--down">−4.7 kg</span>
</div>
<div class="kpi">
<span class="kpi__lbl">Bench 1RM</span>
<span class="kpi__val">102<small>kg</small></span>
<span class="delta delta--up">+9 kg</span>
</div>
<div class="kpi">
<span class="kpi__lbl">Body fat</span>
<span class="kpi__val">17.4<small>%</small></span>
<span class="delta delta--down">−3.1 pt</span>
</div>
<div class="kpi">
<span class="kpi__lbl">Sessions</span>
<span class="kpi__val">42<small>/48</small></span>
<span class="delta delta--up">on track</span>
</div>
</section>
<div class="grid">
<!-- Chart card -->
<section class="card card--chart" aria-label="Progress chart">
<div class="card__head">
<div>
<p class="eyebrow">Trend · 12 weeks</p>
<h2 id="chartTitle">Body weight</h2>
</div>
<div class="tabs" role="tablist" aria-label="Metric">
<button class="tab is-active" role="tab" aria-selected="true" data-metric="weight" type="button">Weight</button>
<button class="tab" role="tab" aria-selected="false" data-metric="bench" type="button">Bench</button>
<button class="tab" role="tab" aria-selected="false" data-metric="squat" type="button">Squat</button>
</div>
</div>
<div class="chart-wrap">
<svg id="chart" class="chart" viewBox="0 0 640 260" preserveAspectRatio="none" role="img" aria-label="Metric over time"></svg>
</div>
<div class="chart-foot" id="chartFoot"></div>
</section>
<!-- Measurements -->
<section class="card" aria-label="Body measurements">
<div class="card__head">
<div>
<p class="eyebrow">Tape · last 8 weeks</p>
<h2>Measurements</h2>
</div>
</div>
<table class="measure">
<thead>
<tr><th scope="col">Site</th><th scope="col">Start</th><th scope="col">Now</th><th scope="col">Δ</th></tr>
</thead>
<tbody>
<tr><th scope="row">Chest</th><td>104 cm</td><td>107 cm</td><td><span class="delta delta--up">+3.0</span></td></tr>
<tr><th scope="row">Waist</th><td>96 cm</td><td>89 cm</td><td><span class="delta delta--down">−7.0</span></td></tr>
<tr><th scope="row">Arms</th><td>37.5 cm</td><td>39.2 cm</td><td><span class="delta delta--up">+1.7</span></td></tr>
<tr><th scope="row">Thigh</th><td>59 cm</td><td>61 cm</td><td><span class="delta delta--up">+2.0</span></td></tr>
<tr><th scope="row">Hips</th><td>101 cm</td><td>98 cm</td><td><span class="delta delta--down">−3.0</span></td></tr>
</tbody>
</table>
</section>
<!-- Attendance heat strip -->
<section class="card" aria-label="Session attendance">
<div class="card__head">
<div>
<p class="eyebrow">Attendance · 12 weeks</p>
<h2>Session heat</h2>
</div>
<div class="heat-key" aria-hidden="true">
<span>Missed</span>
<i class="hk hk-0"></i><i class="hk hk-1"></i><i class="hk hk-2"></i><i class="hk hk-3"></i>
<span>Full</span>
</div>
</div>
<div class="heat" id="heat" aria-label="Weekly attendance grid"></div>
</section>
<!-- Notes timeline -->
<section class="card card--notes" aria-label="Coaching notes">
<div class="card__head">
<div>
<p class="eyebrow">Log</p>
<h2>Recent notes</h2>
</div>
<button class="btn btn--ghost btn--sm" id="openNote2" type="button">+ Note</button>
</div>
<form class="composer" id="composer" hidden>
<label class="sr-only" for="noteInput">New note</label>
<textarea id="noteInput" rows="2" placeholder="e.g. Bumped working sets to 3×6 on bench, form solid…"></textarea>
<div class="composer__row">
<select id="noteTag" aria-label="Note tag">
<option value="training">Training</option>
<option value="nutrition">Nutrition</option>
<option value="recovery">Recovery</option>
<option value="flag">Flag</option>
</select>
<div class="composer__btns">
<button class="btn btn--ghost btn--sm" type="button" id="cancelNote">Cancel</button>
<button class="btn btn--neon btn--sm" type="submit">Save note</button>
</div>
</div>
</form>
<ol class="timeline" id="timeline"></ol>
</section>
</div>
<p class="footnote">Illustrative trainer UI — fictional data, not medical or coaching advice.</p>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Client Progress View
A focused, high-energy panel a coach opens to review one member at a glance. The header pairs a neon avatar with the client’s goal, membership start date and an animated SVG adherence ring, while a KPI strip surfaces weight, bench 1RM, body-fat and session counts with up / down deltas. The whole layout is dark, athletic and tap-friendly, and reflows cleanly down to small phones.
The centrepiece is a self-drawn SVG chart — no charting library — with a tab switcher for Weight, Bench and Squat. Each tab recomputes the domain, gridlines, gradient fill, trend line and hoverable data points, then updates the footer stats. Below it sit a measurements table with per-site deltas, a twelve-week session-attendance heat strip, and a coaching-notes timeline.
The Send program button shows a sending / sent micro-state and toast, while Add note opens
an inline composer: pick a tag (Training, Nutrition, Recovery, Flag), write a line, and it prepends
to the timeline with a subtle entrance animation. Everything is vanilla JS with a small toast()
helper.
Illustrative trainer UI — fictional data, not medical or coaching advice.