Coach — Exercise Library
An energetic dark exercise library where clients search a movement bank, filter by muscle group and equipment, and tap any card to open a detail modal with coaching cues, step-by-step form, and target muscles.
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.1);
--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;
background:
radial-gradient(1200px 600px at 12% -8%, rgba(194, 245, 66, 0.08), transparent 60%),
radial-gradient(900px 500px at 100% 0%, rgba(255, 107, 53, 0.08), transparent 55%),
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;
min-height: 100vh;
}
.page {
max-width: 1120px;
margin: 0 auto;
padding: 40px 22px 72px;
}
/* Masthead */
.masthead {
margin-bottom: 26px;
}
.brand {
display: flex;
align-items: center;
gap: 14px;
}
.brand__mark {
display: grid;
place-items: center;
width: 52px;
height: 52px;
border-radius: var(--r-md);
background: linear-gradient(135deg, var(--lime), var(--lime-d));
color: #10130a;
font-size: 26px;
box-shadow: 0 0 26px rgba(194, 245, 66, 0.4);
}
.brand__kicker {
display: block;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--lime);
}
.brand__title {
margin: 2px 0 0;
font-size: clamp(30px, 6vw, 46px);
font-weight: 800;
letter-spacing: -0.01em;
text-transform: uppercase;
line-height: 1.02;
}
.masthead__lede {
margin: 14px 0 0;
max-width: 56ch;
color: var(--muted);
font-size: 15px;
}
/* Command bar */
.command {
position: sticky;
top: 12px;
z-index: 20;
background: rgba(23, 26, 33, 0.86);
backdrop-filter: blur(12px);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 18px;
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.35);
}
.search {
position: relative;
display: flex;
align-items: center;
}
.search__icon {
position: absolute;
left: 16px;
width: 20px;
height: 20px;
color: var(--muted);
pointer-events: none;
}
.search__input {
width: 100%;
padding: 14px 44px 14px 46px;
border-radius: var(--r-md);
border: 1px solid var(--line-2);
background: var(--surface-2);
color: var(--ink);
font: inherit;
font-size: 15px;
}
.search__input::placeholder {
color: var(--muted);
}
.search__input:focus-visible {
outline: none;
border-color: var(--lime);
box-shadow: 0 0 0 3px rgba(194, 245, 66, 0.28);
}
.search__clear {
position: absolute;
right: 12px;
width: 26px;
height: 26px;
border: none;
border-radius: 50%;
background: var(--line-2);
color: var(--ink);
font-size: 18px;
line-height: 1;
cursor: pointer;
}
.search__clear:hover {
background: var(--orange);
color: var(--white);
}
.filters {
display: grid;
gap: 12px;
margin-top: 16px;
}
.filters__group {
display: flex;
align-items: flex-start;
gap: 12px;
flex-wrap: wrap;
}
.filters__label {
flex: 0 0 auto;
min-width: 74px;
padding-top: 6px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.chip {
padding: 7px 13px;
border-radius: 999px;
border: 1px solid var(--line-2);
background: transparent;
color: var(--muted);
font: inherit;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: transform 0.12s ease, background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.chip:hover {
color: var(--ink);
border-color: var(--lime);
transform: translateY(-1px);
}
.chip:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(194, 245, 66, 0.3);
}
.chip[aria-pressed="true"] {
background: var(--lime);
border-color: var(--lime);
color: #10130a;
box-shadow: 0 0 18px rgba(194, 245, 66, 0.4);
}
.command__meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 16px;
padding-top: 14px;
border-top: 1px solid var(--line);
}
.count {
font-size: 13px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--lime);
}
.reset {
border: none;
background: none;
color: var(--muted);
font: inherit;
font-size: 13px;
font-weight: 600;
cursor: pointer;
text-decoration: underline;
text-underline-offset: 3px;
}
.reset:hover {
color: var(--orange);
}
/* Grid */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 18px;
margin-top: 26px;
}
.card {
display: flex;
flex-direction: column;
text-align: left;
border: 1px solid var(--line);
border-radius: var(--r-lg);
background: var(--surface);
overflow: hidden;
cursor: pointer;
font: inherit;
color: inherit;
padding: 0;
transition: transform 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease;
}
.card:hover {
transform: translateY(-4px);
border-color: var(--lime);
box-shadow: 0 18px 34px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(194, 245, 66, 0.25);
}
.card:focus-visible {
outline: none;
border-color: var(--lime);
box-shadow: 0 0 0 3px rgba(194, 245, 66, 0.35);
}
.card__thumb {
position: relative;
aspect-ratio: 16 / 10;
display: grid;
place-items: center;
}
.card__glyph {
font-size: 46px;
filter: drop-shadow(0 6px 14px rgba(0, 0, 0, 0.45));
transition: transform 0.2s ease;
}
.card:hover .card__glyph {
transform: scale(1.12) rotate(-4deg);
}
.card__diff {
position: absolute;
top: 10px;
left: 10px;
padding: 4px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
background: rgba(15, 17, 21, 0.72);
backdrop-filter: blur(4px);
color: var(--ink);
}
.card__diff[data-diff="beginner"] { color: var(--lime); }
.card__diff[data-diff="intermediate"] { color: #ffd166; }
.card__diff[data-diff="advanced"] { color: var(--orange); }
.card__body {
padding: 14px 15px 16px;
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
}
.card__name {
margin: 0;
font-size: 16px;
font-weight: 700;
letter-spacing: -0.01em;
}
.card__primary {
font-size: 12.5px;
color: var(--muted);
}
.card__primary strong {
color: var(--lime);
font-weight: 700;
}
.card__tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: auto;
}
.tag {
padding: 3px 9px;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
background: var(--surface-2);
border: 1px solid var(--line);
color: var(--muted);
}
.tag--equip {
color: var(--ink);
border-color: var(--line-2);
}
/* Empty state */
.empty {
margin-top: 40px;
text-align: center;
padding: 48px 20px;
border: 1px dashed var(--line-2);
border-radius: var(--r-lg);
background: var(--surface);
}
.empty__mark {
font-size: 40px;
}
.empty__title {
margin: 12px 0 4px;
font-size: 20px;
font-weight: 800;
text-transform: uppercase;
}
.empty__hint {
margin: 0 0 18px;
color: var(--muted);
}
/* Buttons */
.btn {
border: none;
border-radius: var(--r-md);
padding: 12px 18px;
font: inherit;
font-weight: 700;
cursor: pointer;
transition: transform 0.12s ease, box-shadow 0.15s ease, background 0.15s ease;
}
.btn:active {
transform: translateY(1px);
}
.btn--lime {
background: var(--lime);
color: #10130a;
box-shadow: 0 0 20px rgba(194, 245, 66, 0.35);
}
.btn--lime:hover {
background: var(--lime-d);
box-shadow: 0 0 28px rgba(194, 245, 66, 0.5);
}
.btn--lime:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(194, 245, 66, 0.4);
}
/* Modal */
.modal {
position: fixed;
inset: 0;
z-index: 50;
display: grid;
place-items: center;
padding: 20px;
}
.modal[hidden] {
display: none;
}
.modal__backdrop {
position: absolute;
inset: 0;
background: rgba(6, 8, 11, 0.72);
backdrop-filter: blur(4px);
animation: fade 0.2s ease;
}
.modal__panel {
position: relative;
width: min(720px, 100%);
max-height: 90vh;
overflow-y: auto;
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: var(--r-lg);
box-shadow: 0 30px 70px rgba(0, 0, 0, 0.6);
animation: pop 0.22s cubic-bezier(0.2, 0.9, 0.3, 1.3);
}
.modal__close {
position: absolute;
top: 14px;
right: 14px;
z-index: 2;
width: 36px;
height: 36px;
border: none;
border-radius: 50%;
background: rgba(15, 17, 21, 0.7);
color: var(--ink);
font-size: 22px;
line-height: 1;
cursor: pointer;
}
.modal__close:hover {
background: var(--orange);
color: var(--white);
}
.modal__close:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(194, 245, 66, 0.5);
}
.modal__thumb {
height: 170px;
display: grid;
place-items: center;
font-size: 64px;
border-radius: var(--r-lg) var(--r-lg) 0 0;
}
.modal__body {
padding: 22px 24px 26px;
}
.modal__badges {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 10px;
}
.modal__title {
margin: 0 0 8px;
font-size: clamp(22px, 4vw, 30px);
font-weight: 800;
text-transform: uppercase;
letter-spacing: -0.01em;
}
.modal__summary {
margin: 0 0 20px;
color: var(--muted);
font-size: 15px;
}
.modal__grid {
display: grid;
grid-template-columns: 1.4fr 1fr;
gap: 20px;
}
.block__title {
margin: 0 0 12px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--lime);
}
.steps {
margin: 0;
padding: 0;
list-style: none;
counter-reset: step;
display: grid;
gap: 12px;
}
.steps li {
counter-increment: step;
position: relative;
padding-left: 40px;
font-size: 14px;
}
.steps li::before {
content: counter(step);
position: absolute;
left: 0;
top: -2px;
width: 27px;
height: 27px;
display: grid;
place-items: center;
border-radius: 50%;
background: var(--surface-2);
border: 1px solid var(--lime);
color: var(--lime);
font-size: 12px;
font-weight: 700;
}
.block--side {
display: grid;
gap: 16px;
align-content: start;
}
.rx {
padding: 14px;
border-radius: var(--r-md);
background: linear-gradient(135deg, rgba(255, 107, 53, 0.16), rgba(194, 245, 66, 0.08));
border: 1px solid var(--line-2);
}
.rx__label {
display: block;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
}
.rx__value {
font-size: 18px;
font-weight: 800;
color: var(--ink);
}
.muscles__row {
display: flex;
align-items: baseline;
gap: 8px;
font-size: 14px;
margin-bottom: 8px;
}
.muscles__tag {
flex: 0 0 auto;
padding: 3px 9px;
border-radius: 999px;
font-size: 10.5px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
background: var(--lime);
color: #10130a;
}
.muscles__tag--sec {
background: var(--surface-2);
color: var(--muted);
border: 1px solid var(--line-2);
}
.block__cta {
width: 100%;
}
/* Toast */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translateX(-50%) translateY(16px);
z-index: 80;
padding: 12px 20px;
border-radius: 999px;
background: var(--lime);
color: #10130a;
font-weight: 700;
font-size: 14px;
box-shadow: 0 14px 34px rgba(0, 0, 0, 0.5), 0 0 24px rgba(194, 245, 66, 0.4);
opacity: 0;
transition: opacity 0.25s ease, transform 0.25s ease;
}
.toast.is-show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
@keyframes fade {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes pop {
from { opacity: 0; transform: translateY(14px) scale(0.97); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@media (max-width: 520px) {
.page {
padding: 28px 16px 60px;
}
.command {
top: 8px;
padding: 14px;
}
.filters__group {
flex-direction: column;
gap: 8px;
}
.filters__label {
padding-top: 0;
}
.grid {
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.card__glyph {
font-size: 38px;
}
.modal__grid {
grid-template-columns: 1fr;
gap: 18px;
}
.modal__body {
padding: 18px 16px 22px;
}
}
@media (max-width: 380px) {
.grid {
grid-template-columns: 1fr;
}
}
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
}
}/* Coach — Exercise Library
Searchable/filterable movement grid + detail modal. Vanilla JS only. */
(function () {
"use strict";
const GRADIENTS = {
lime: "linear-gradient(135deg, #1f232c 0%, #2a3410 60%, #3f4d12 100%)",
orange: "linear-gradient(135deg, #241118 0%, #3a1a10 60%, #4d2410 100%)",
steel: "linear-gradient(135deg, #171a21 0%, #232a36 60%, #2c3a4d 100%)",
violet: "linear-gradient(135deg, #1b1626 0%, #2a2140 60%, #3a2c55 100%)",
};
const EXERCISES = [
{
name: "Back Squat", glyph: "🏋️", grad: "lime", diff: "intermediate",
muscle: "Legs", equip: "Barbell", rx: "4 × 6 reps",
primary: ["Quadriceps", "Glutes"], secondary: ["Hamstrings", "Core"],
summary: "The king of lower-body strength. Load the bar across your traps and drive through the floor with intent.",
steps: ["Set the bar on your rear delts, hands just outside shoulders.", "Brace your core and unrack, taking two steps back.", "Sit down and back, knees tracking over toes.", "Descend until hip crease passes the knee.", "Drive through mid-foot and stand tall to lockout."],
},
{
name: "Deadlift", glyph: "💪", grad: "orange", diff: "advanced",
muscle: "Back", equip: "Barbell", rx: "5 × 3 reps",
primary: ["Erectors", "Glutes"], secondary: ["Hamstrings", "Lats", "Traps"],
summary: "A full-body hinge that builds raw pulling power from the floor.",
steps: ["Stand mid-foot under the bar, shins an inch away.", "Hinge and grip just outside your knees.", "Set a flat back, chest proud, lats tight.", "Push the floor away and drag the bar up your legs.", "Lock out hips and knees together, then control down."],
},
{
name: "Bench Press", glyph: "🛋️", grad: "steel", diff: "intermediate",
muscle: "Chest", equip: "Barbell", rx: "4 × 8 reps",
primary: ["Pectorals"], secondary: ["Triceps", "Front Delts"],
summary: "The benchmark upper-body press for chest and pushing strength.",
steps: ["Set feet flat, shoulder blades pinched and down.", "Grip slightly wider than shoulder width.", "Unrack and stack the bar over your shoulders.", "Lower to mid-chest with elbows at 45 degrees.", "Press up and slightly back to lockout."],
},
{
name: "Pull-Up", glyph: "🧗", grad: "violet", diff: "advanced",
muscle: "Back", equip: "Bodyweight", rx: "4 × AMRAP",
primary: ["Lats"], secondary: ["Biceps", "Rear Delts", "Core"],
summary: "The purest test of relative upper-body pulling strength.",
steps: ["Grip the bar just outside shoulder width, palms away.", "Start from a full dead hang, shoulders active.", "Drive elbows down and pull chest toward the bar.", "Clear your chin over the bar at the top.", "Lower under control to a full hang."],
},
{
name: "Overhead Press", glyph: "🙆", grad: "lime", diff: "intermediate",
muscle: "Shoulders", equip: "Barbell", rx: "5 × 5 reps",
primary: ["Deltoids"], secondary: ["Triceps", "Upper Chest", "Core"],
summary: "A strict vertical press that builds bulletproof shoulders.",
steps: ["Rack the bar on your front delts, grip shoulder width.", "Brace hard and squeeze your glutes.", "Press straight up, moving your head back slightly.", "Push through and shrug at lockout overhead.", "Lower under control back to the shelf."],
},
{
name: "Kettlebell Swing", glyph: "🔔", grad: "orange", diff: "beginner",
muscle: "Glutes", equip: "Kettlebell", rx: "5 × 15 reps",
primary: ["Glutes", "Hamstrings"], secondary: ["Core", "Shoulders"],
summary: "An explosive hip hinge for power, conditioning, and posterior chain.",
steps: ["Stand tall with the bell a foot in front of you.", "Hinge and hike the bell back between your legs.", "Snap your hips forward to float the bell up.", "Let it rise to chest height on momentum only.", "Guide it back down and repeat the hinge."],
},
{
name: "Dumbbell Row", glyph: "🚣", grad: "steel", diff: "beginner",
muscle: "Back", equip: "Dumbbell", rx: "3 × 12 reps",
primary: ["Lats", "Mid-Back"], secondary: ["Biceps", "Rear Delts"],
summary: "A single-arm row that carves back thickness and fixes imbalances.",
steps: ["Plant one knee and hand on a flat bench.", "Let the dumbbell hang with a long arm.", "Pull the bell to your hip, elbow tight.", "Squeeze your shoulder blade at the top.", "Lower slowly to a full stretch."],
},
{
name: "Goblet Squat", glyph: "🥤", grad: "lime", diff: "beginner",
muscle: "Legs", equip: "Kettlebell", rx: "3 × 12 reps",
primary: ["Quadriceps"], secondary: ["Glutes", "Core"],
summary: "A friendly loaded squat that grooves clean pattern and depth.",
steps: ["Hold a bell at chest height, elbows tucked.", "Set feet shoulder width, toes slightly out.", "Sit straight down between your hips.", "Keep the chest tall and heels planted.", "Drive up and stand fully upright."],
},
{
name: "Plank Hold", glyph: "🧱", grad: "violet", diff: "beginner",
muscle: "Core", equip: "Bodyweight", rx: "3 × 45 sec",
primary: ["Abdominals"], secondary: ["Glutes", "Shoulders"],
summary: "An anti-extension hold that teaches full-body bracing.",
steps: ["Set forearms under shoulders, elbows bent 90 degrees.", "Extend legs back, feet hip width apart.", "Squeeze glutes and brace your abs hard.", "Keep a straight line from head to heels.", "Breathe steadily and hold the position."],
},
{
name: "Romanian Deadlift", glyph: "🦵", grad: "orange", diff: "intermediate",
muscle: "Hamstrings", equip: "Barbell", rx: "4 × 8 reps",
primary: ["Hamstrings", "Glutes"], secondary: ["Erectors"],
summary: "A hip hinge with soft knees that lights up the hamstrings.",
steps: ["Hold the bar at your hips, soft knees.", "Push your hips back and slide the bar down.", "Feel a stretch as the bar passes your knees.", "Keep the bar close and back flat.", "Drive hips forward to stand tall."],
},
{
name: "Push-Up", glyph: "🤸", grad: "steel", diff: "beginner",
muscle: "Chest", equip: "Bodyweight", rx: "3 × 15 reps",
primary: ["Pectorals"], secondary: ["Triceps", "Core"],
summary: "The everywhere press that builds a strong pushing base.",
steps: ["Set hands just wider than shoulders.", "Form a straight plank from head to heels.", "Lower your chest to just above the floor.", "Keep elbows around 45 degrees to the torso.", "Press back up to full lockout."],
},
{
name: "Cable Face Pull", glyph: "🪢", grad: "violet", diff: "beginner",
muscle: "Shoulders", equip: "Cable", rx: "3 × 15 reps",
primary: ["Rear Delts"], secondary: ["Traps", "Rotator Cuff"],
summary: "A shoulder-health staple for posture and rear-delt detail.",
steps: ["Set a rope at upper-chest height.", "Grip both ends with thumbs back.", "Pull the rope toward your forehead.", "Flare elbows high and squeeze the rear delts.", "Return slowly with control."],
},
];
// ---- State ----
const state = { query: "", muscle: "All", equip: "All" };
const grid = document.getElementById("grid");
const empty = document.getElementById("empty");
const searchInput = document.getElementById("search");
const clearSearch = document.getElementById("clearSearch");
const resultCount = document.getElementById("resultCount");
const muscleChips = document.getElementById("muscleChips");
const equipChips = document.getElementById("equipChips");
const modal = document.getElementById("modal");
const toastEl = document.getElementById("toast");
let lastFocused = null;
let toastTimer = null;
// ---- Toast ----
function toast(msg) {
toastEl.textContent = msg;
toastEl.hidden = false;
requestAnimationFrame(() => toastEl.classList.add("is-show"));
clearTimeout(toastTimer);
toastTimer = setTimeout(() => {
toastEl.classList.remove("is-show");
setTimeout(() => (toastEl.hidden = true), 260);
}, 2200);
}
// ---- Build filter chips ----
function unique(key) {
return ["All", ...Array.from(new Set(EXERCISES.map((e) => e[key]))).sort()];
}
function buildChips(container, values, group) {
container.innerHTML = "";
values.forEach((val) => {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "chip";
btn.textContent = val;
btn.setAttribute("aria-pressed", String(state[group] === val));
btn.addEventListener("click", () => {
state[group] = val;
syncChips(container, group);
render();
});
container.appendChild(btn);
});
}
function syncChips(container, group) {
container.querySelectorAll(".chip").forEach((c) => {
c.setAttribute("aria-pressed", String(c.textContent === state[group]));
});
}
// ---- Filtering ----
function matches(ex) {
const q = state.query.trim().toLowerCase();
if (q && !(ex.name.toLowerCase().includes(q) || ex.muscle.toLowerCase().includes(q) || ex.equip.toLowerCase().includes(q))) {
return false;
}
if (state.muscle !== "All" && ex.muscle !== state.muscle) return false;
if (state.equip !== "All" && ex.equip !== state.equip) return false;
return true;
}
// ---- Render grid ----
function render() {
const list = EXERCISES.filter(matches);
grid.innerHTML = "";
list.forEach((ex) => {
const card = document.createElement("button");
card.type = "button";
card.className = "card";
card.setAttribute("role", "listitem");
card.setAttribute("aria-label", ex.name + ", " + ex.diff + " " + ex.muscle + " movement");
card.innerHTML =
'<span class="card__thumb" style="background:' + GRADIENTS[ex.grad] + '">' +
'<span class="card__diff" data-diff="' + ex.diff + '">' + ex.diff + "</span>" +
'<span class="card__glyph">' + ex.glyph + "</span>" +
"</span>" +
'<span class="card__body">' +
'<span class="card__name">' + ex.name + "</span>" +
'<span class="card__primary">Targets <strong>' + ex.primary.join(", ") + "</strong></span>" +
'<span class="card__tags">' +
'<span class="tag">' + ex.muscle + "</span>" +
'<span class="tag tag--equip">' + ex.equip + "</span>" +
"</span>" +
"</span>";
card.addEventListener("click", () => openModal(ex, card));
grid.appendChild(card);
});
const n = list.length;
resultCount.textContent = n + (n === 1 ? " movement" : " movements");
empty.hidden = n !== 0;
grid.hidden = n === 0;
}
// ---- Modal ----
function chipTags(container, values, secondary) {
container.textContent = "";
values.forEach((v, i) => {
if (i) container.appendChild(document.createTextNode(", "));
const span = document.createElement("strong");
span.textContent = v;
span.style.color = secondary ? "var(--muted)" : "var(--ink)";
span.style.fontWeight = "600";
container.appendChild(span);
});
}
function openModal(ex, trigger) {
lastFocused = trigger || document.activeElement;
document.getElementById("modalThumb").style.background = GRADIENTS[ex.grad];
document.getElementById("modalThumb").textContent = ex.glyph;
document.getElementById("modalTitle").textContent = ex.name;
document.getElementById("modalSummary").textContent = ex.summary;
document.getElementById("modalRx").textContent = ex.rx;
const badges = document.getElementById("modalBadges");
badges.innerHTML =
'<span class="tag tag--equip" style="color:var(--lime);border-color:var(--lime)">' + ex.muscle + "</span>" +
'<span class="tag tag--equip">' + ex.equip + "</span>" +
'<span class="card__diff" data-diff="' + ex.diff + '" style="position:static">' + ex.diff + "</span>";
const steps = document.getElementById("modalSteps");
steps.innerHTML = "";
ex.steps.forEach((s) => {
const li = document.createElement("li");
li.textContent = s;
steps.appendChild(li);
});
chipTags(document.getElementById("modalPrimary"), ex.primary, false);
chipTags(document.getElementById("modalSecondary"), ex.secondary, true);
const addBtn = document.getElementById("addToPlan");
addBtn.onclick = () => {
toast(ex.name + " added to today's plan ⚡");
closeModal();
};
modal.hidden = false;
document.body.style.overflow = "hidden";
modal.querySelector(".modal__close").focus();
document.addEventListener("keydown", onKeydown);
}
function closeModal() {
if (modal.hidden) return;
modal.hidden = true;
document.body.style.overflow = "";
document.removeEventListener("keydown", onKeydown);
if (lastFocused && typeof lastFocused.focus === "function") lastFocused.focus();
}
function onKeydown(e) {
if (e.key === "Escape") {
closeModal();
return;
}
if (e.key === "Tab") {
const focusables = modal.querySelectorAll('button, [href], input, [tabindex]:not([tabindex="-1"])');
if (!focusables.length) return;
const first = focusables[0];
const last = focusables[focusables.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
}
modal.querySelectorAll("[data-close]").forEach((el) => el.addEventListener("click", closeModal));
// ---- Search ----
searchInput.addEventListener("input", () => {
state.query = searchInput.value;
clearSearch.hidden = !searchInput.value;
render();
});
clearSearch.addEventListener("click", () => {
searchInput.value = "";
state.query = "";
clearSearch.hidden = true;
searchInput.focus();
render();
});
function resetAll() {
state.query = "";
state.muscle = "All";
state.equip = "All";
searchInput.value = "";
clearSearch.hidden = true;
syncChips(muscleChips, "muscle");
syncChips(equipChips, "equip");
render();
toast("Library reset");
}
document.getElementById("resetFilters").addEventListener("click", resetAll);
document.getElementById("emptyReset").addEventListener("click", resetAll);
// ---- Init ----
buildChips(muscleChips, unique("muscle"), "muscle");
buildChips(equipChips, unique("equip"), "equip");
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Coach — Exercise Library</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>
<div class="page">
<header class="masthead">
<div class="brand">
<span class="brand__mark" aria-hidden="true">⚡</span>
<div class="brand__text">
<span class="brand__kicker">Vaultstrength Coaching</span>
<h1 class="brand__title">Exercise Library</h1>
</div>
</div>
<p class="masthead__lede">
Search the movement bank, filter by muscle and gear, then tap any card
for coaching cues and target muscles.
</p>
</header>
<section class="command" aria-label="Search and filter exercises">
<div class="search">
<svg class="search__icon" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="11" cy="11" r="7" fill="none" stroke="currentColor" stroke-width="2" />
<line x1="16.5" y1="16.5" x2="21" y2="21" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
<input
id="search"
class="search__input"
type="search"
placeholder="Search movements — squat, press, hinge…"
aria-label="Search exercises by name"
autocomplete="off"
/>
<button id="clearSearch" class="search__clear" type="button" aria-label="Clear search" hidden>×</button>
</div>
<div class="filters">
<div class="filters__group" role="group" aria-label="Filter by muscle group">
<span class="filters__label">Muscle</span>
<div class="chips" id="muscleChips"></div>
</div>
<div class="filters__group" role="group" aria-label="Filter by equipment">
<span class="filters__label">Equipment</span>
<div class="chips" id="equipChips"></div>
</div>
</div>
<div class="command__meta">
<span id="resultCount" class="count" aria-live="polite">12 movements</span>
<button id="resetFilters" class="reset" type="button">Reset filters</button>
</div>
</section>
<main>
<div id="grid" class="grid" role="list" aria-label="Exercise cards"></div>
<div id="empty" class="empty" hidden>
<span class="empty__mark" aria-hidden="true">🔍</span>
<p class="empty__title">No movements match</p>
<p class="empty__hint">Try clearing a filter or searching a different term.</p>
<button id="emptyReset" class="btn btn--lime" type="button">Reset library</button>
</div>
</main>
</div>
<div id="modal" class="modal" hidden>
<div class="modal__backdrop" data-close></div>
<div class="modal__panel" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
<button class="modal__close" type="button" aria-label="Close detail" data-close>×</button>
<div class="modal__thumb" id="modalThumb" aria-hidden="true"></div>
<div class="modal__body">
<div class="modal__badges" id="modalBadges"></div>
<h2 class="modal__title" id="modalTitle">Movement</h2>
<p class="modal__summary" id="modalSummary"></p>
<div class="modal__grid">
<section class="block block--steps">
<h3 class="block__title">Form steps</h3>
<ol class="steps" id="modalSteps"></ol>
</section>
<aside class="block block--side">
<div class="rx">
<span class="rx__label">Prescription</span>
<strong class="rx__value" id="modalRx"></strong>
</div>
<div class="muscles">
<h3 class="block__title">Target muscles</h3>
<div class="muscles__row">
<span class="muscles__tag">Primary</span>
<span id="modalPrimary"></span>
</div>
<div class="muscles__row muscles__row--sec">
<span class="muscles__tag muscles__tag--sec">Secondary</span>
<span id="modalSecondary"></span>
</div>
</div>
<button class="btn btn--lime block__cta" id="addToPlan" type="button">Add to today's plan</button>
</aside>
</div>
</div>
</div>
</div>
<div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
<script src="script.js"></script>
</body>
</html>A high-energy exercise library built for coaching apps. A sticky command bar pairs a live search box with two rows of filter chips — one for muscle groups, one for equipment — that instantly narrow a responsive grid of movement cards. Each card carries a gradient thumbnail, a difficulty badge, muscle and equipment tags, and a primary-muscle line so athletes can scan the catalog at a glance.
Clicking or pressing Enter on a card launches a detail modal that renders the movement name, a coaching summary, numbered step-by-step form cues, a set-and-rep prescription, and a breakdown of primary and secondary target muscles. The modal traps focus, closes on Escape or backdrop click, and returns focus to the originating card.
The whole interface leans into the fitness-coach aesthetic: bold uppercase display headings, an electric-lime accent that glows on hover, and orange highlights for intensity. It is fully keyboard-navigable, responsive down to 360px, and driven entirely by vanilla JavaScript with a small toast helper for lightweight feedback.