Gym — Class Detail
A bold, athletic class detail card for a single gym session. Shows a hero strip with the class name and intensity badge, trainer avatar and rating, schedule, studio, an estimated calorie burn, a difficulty meter, a what-to-bring checklist, a live spots progress bar with a roster preview of attendees, and a prominent Book CTA. Booking updates the bar and roster, while a full class flips the button into a waitlist flow with toast feedback.
MCP
Code
: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 2px 8px rgba(0, 0, 0, 0.4), 0 16px 40px rgba(0, 0, 0, 0.45);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
background: var(--bg);
color: var(--ink);
min-height: 100vh;
display: grid;
place-items: center;
padding: 32px 16px;
background-image:
radial-gradient(900px 500px at 12% -10%, rgba(198, 255, 58, 0.07), transparent 60%),
radial-gradient(800px 480px at 110% 10%, rgba(255, 106, 43, 0.08), transparent 55%);
}
.demo-root {
width: 100%;
display: grid;
place-items: center;
}
/* ---- CARD ---- */
.class-card {
width: 100%;
max-width: 460px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: var(--shadow);
}
button {
font-family: inherit;
cursor: pointer;
}
:focus-visible {
outline: 2px solid var(--neon);
outline-offset: 2px;
border-radius: var(--r-sm);
}
.eyebrow,
.section-label {
font-size: 11px;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
}
/* ---- HERO ---- */
.hero {
position: relative;
padding: 18px;
min-height: 220px;
display: flex;
flex-direction: column;
justify-content: space-between;
background:
linear-gradient(135deg, rgba(255, 106, 43, 0.35), transparent 55%),
linear-gradient(220deg, rgba(198, 255, 58, 0.22), transparent 50%),
var(--surface-2);
isolation: isolate;
}
.hero__overlay {
position: absolute;
inset: 0;
z-index: -1;
background:
radial-gradient(120% 90% at 80% 0%, rgba(255, 106, 43, 0.4), transparent 60%),
repeating-linear-gradient(
115deg,
rgba(255, 255, 255, 0.03) 0 2px,
transparent 2px 14px
);
}
.hero__top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
}
.eyebrow {
color: var(--neon);
background: rgba(13, 15, 18, 0.5);
padding: 6px 10px;
border-radius: 999px;
backdrop-filter: blur(6px);
}
.intensity {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: 11px;
font-weight: 800;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 6px 11px;
border-radius: 999px;
background: rgba(13, 15, 18, 0.6);
border: 1px solid var(--line-2);
backdrop-filter: blur(6px);
}
.intensity__dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--orange);
box-shadow: 0 0 0 0 rgba(255, 106, 43, 0.6);
animation: pulse 1.8s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(255, 106, 43, 0.6); }
70% { box-shadow: 0 0 0 8px rgba(255, 106, 43, 0); }
100% { box-shadow: 0 0 0 0 rgba(255, 106, 43, 0); }
}
.intensity--high {
color: var(--orange);
}
.hero__title {
font-size: 38px;
font-weight: 900;
line-height: 1.02;
letter-spacing: -0.02em;
text-transform: uppercase;
}
.hero__sub {
margin-top: 8px;
font-size: 13.5px;
color: var(--ink-2);
max-width: 34ch;
}
/* ---- BODY ---- */
.body {
padding: 18px;
display: flex;
flex-direction: column;
gap: 20px;
}
/* trainer */
.trainer {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-md);
}
.trainer__avatar {
width: 46px;
height: 46px;
flex: 0 0 auto;
border-radius: 50%;
display: grid;
place-items: center;
font-weight: 800;
font-size: 15px;
color: #0d0f12;
background: linear-gradient(140deg, var(--neon), var(--neon-d));
}
.trainer__meta {
display: flex;
flex-direction: column;
line-height: 1.25;
flex: 1;
min-width: 0;
}
.trainer__label {
font-size: 10.5px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
}
.trainer__name {
font-size: 16px;
font-weight: 700;
}
.trainer__rating {
font-size: 12px;
color: var(--warn);
}
.btn-ghost {
flex: 0 0 auto;
font-size: 12.5px;
font-weight: 700;
color: var(--ink);
background: transparent;
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 8px 14px;
transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.btn-ghost:hover {
background: var(--elevated);
border-color: var(--line-2);
}
.btn-ghost.is-following {
color: #0d0f12;
background: var(--neon);
border-color: var(--neon);
}
/* meta grid */
.meta-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.meta {
display: flex;
flex-direction: column;
gap: 3px;
padding: 12px;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-md);
}
.meta__label {
font-size: 10.5px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
.meta__value {
font-size: 14.5px;
font-weight: 700;
}
/* difficulty */
.difficulty__head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.difficulty__tag {
font-size: 11px;
font-weight: 800;
letter-spacing: 0.05em;
text-transform: uppercase;
color: var(--orange);
background: var(--orange-soft);
padding: 4px 10px;
border-radius: 999px;
}
.meter {
display: flex;
gap: 6px;
}
.meter__bar {
flex: 1;
height: 10px;
border-radius: 999px;
background: var(--elevated);
transition: background 0.2s;
}
.meter__bar.is-on {
background: linear-gradient(90deg, var(--orange), var(--neon));
}
/* bring */
.bring__list {
list-style: none;
margin-top: 10px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px 12px;
}
.bring__list li {
display: flex;
align-items: center;
gap: 9px;
font-size: 13.5px;
font-weight: 500;
color: var(--ink-2);
}
.bring__ic {
width: 28px;
height: 28px;
flex: 0 0 auto;
display: grid;
place-items: center;
font-size: 14px;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-sm);
}
/* spots */
.spots__head {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 10px;
}
.spots__count {
font-size: 13px;
color: var(--ink-2);
}
.spots__count strong {
color: var(--ink);
font-weight: 800;
}
.progress {
height: 12px;
border-radius: 999px;
background: var(--elevated);
overflow: hidden;
}
.progress__fill {
display: block;
height: 100%;
width: 70%;
border-radius: 999px;
background: linear-gradient(90deg, var(--neon-d), var(--neon));
transition: width 0.45s cubic-bezier(0.22, 1, 0.36, 1), background 0.3s;
}
.progress.is-full .progress__fill {
background: linear-gradient(90deg, var(--orange), var(--danger));
}
.spots__hint {
margin-top: 8px;
font-size: 12.5px;
font-weight: 600;
color: var(--neon);
}
.spots__hint.is-warn {
color: var(--warn);
}
.spots__hint.is-full {
color: var(--orange);
}
/* roster */
.roster__row {
margin-top: 10px;
display: flex;
align-items: center;
gap: 12px;
}
.avatars {
list-style: none;
display: flex;
flex: 0 0 auto;
}
.avatar {
width: 34px;
height: 34px;
border-radius: 50%;
display: grid;
place-items: center;
font-size: 11.5px;
font-weight: 800;
color: #0d0f12;
background: var(--clr, var(--muted));
background: color-mix(in srgb, var(--clr, #888) 85%, white);
border: 2px solid var(--surface);
margin-left: calc(var(--i) == 0 ? 0 : -10px);
margin-left: -10px;
}
.avatar:first-child {
margin-left: 0;
}
.avatar--more {
color: var(--ink);
background: var(--elevated);
font-weight: 700;
font-size: 11px;
}
.roster__names {
font-size: 12.5px;
color: var(--muted);
min-width: 0;
}
/* ---- CTA ---- */
.cta {
display: flex;
align-items: center;
gap: 14px;
padding: 16px 18px calc(16px + env(safe-area-inset-bottom));
background: var(--surface-2);
border-top: 1px solid var(--line);
}
.cta__price {
display: flex;
flex-direction: column;
line-height: 1.2;
flex: 0 0 auto;
}
.cta__amount {
font-size: 16px;
font-weight: 800;
}
.cta__note {
font-size: 11.5px;
color: var(--muted);
}
.btn-book {
flex: 1;
border: none;
border-radius: var(--r-md);
padding: 15px 18px;
font-size: 15px;
font-weight: 800;
letter-spacing: 0.02em;
color: #0d0f12;
background: var(--neon);
transition: transform 0.12s, background 0.18s, box-shadow 0.18s;
box-shadow: 0 8px 22px rgba(198, 255, 58, 0.22);
}
.btn-book:hover {
background: var(--neon-d);
}
.btn-book:active {
transform: translateY(1px) scale(0.99);
}
.btn-book.is-booked {
color: var(--ink);
background: var(--elevated);
box-shadow: none;
border: 1px solid var(--line-2);
}
.btn-book.is-booked:hover {
background: rgba(248, 113, 113, 0.14);
border-color: rgba(248, 113, 113, 0.4);
color: var(--danger);
}
.btn-book.is-waitlist {
color: #0d0f12;
background: var(--orange);
box-shadow: 0 8px 22px rgba(255, 106, 43, 0.25);
}
.btn-book.is-waitlist:hover {
background: #ff7e47;
}
/* ---- TOAST ---- */
.toast-wrap {
position: fixed;
left: 50%;
bottom: 22px;
transform: translateX(-50%);
display: flex;
flex-direction: column;
gap: 8px;
z-index: 50;
width: max-content;
max-width: 90vw;
}
.toast {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
background: var(--elevated);
border: 1px solid var(--line-2);
border-left: 3px solid var(--neon);
border-radius: var(--r-md);
color: var(--ink);
font-size: 13.5px;
font-weight: 600;
box-shadow: var(--shadow);
animation: toast-in 0.28s cubic-bezier(0.22, 1, 0.36, 1);
}
.toast.is-out {
animation: toast-out 0.25s forwards ease-in;
}
.toast--warn {
border-left-color: var(--orange);
}
@keyframes toast-in {
from { opacity: 0; transform: translateY(14px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes toast-out {
to { opacity: 0; transform: translateY(10px); }
}
/* ---- RESPONSIVE ---- */
@media (max-width: 520px) {
body {
padding: 0;
place-items: stretch;
}
.class-card {
max-width: none;
min-height: 100vh;
border: none;
border-radius: 0;
box-shadow: none;
display: flex;
flex-direction: column;
}
.body {
flex: 1;
}
.hero__title {
font-size: 32px;
}
.meta-grid {
grid-template-columns: 1fr 1fr;
}
}
@media (max-width: 360px) {
.bring__list {
grid-template-columns: 1fr;
}
.cta {
flex-wrap: wrap;
}
.cta__price {
flex: 1;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
}
}(function () {
"use strict";
// ---- State ----
var TOTAL = 20;
var state = {
booked: 14,
total: TOTAL,
isBookedByMe: false,
isWaitlisted: false,
following: false,
};
// ---- Elements ----
var $ = function (sel) { return document.querySelector(sel); };
var els = {
book: $("[data-book]"),
bookLabel: $(".btn-book__label"),
progress: $("[data-progress]"),
fill: $("[data-fill]"),
bookedEl: $("[data-booked]"),
totalEl: $("[data-total]"),
hint: $("[data-hint]"),
follow: $("[data-follow]"),
toastWrap: $("[data-toast-wrap]"),
avatars: $("[data-avatars]"),
more: $("[data-more]"),
names: $("[data-names]"),
};
// ---- Toast helper ----
function toast(msg, kind) {
if (!els.toastWrap) return;
var t = document.createElement("div");
t.className = "toast" + (kind === "warn" ? " toast--warn" : "");
t.textContent = msg;
els.toastWrap.appendChild(t);
setTimeout(function () {
t.classList.add("is-out");
t.addEventListener("animationend", function () { t.remove(); });
}, 2600);
}
// ---- Render ----
function spotsLeft() {
return Math.max(0, state.total - state.booked);
}
function isFull() {
return state.booked >= state.total;
}
function render() {
var left = spotsLeft();
var pct = Math.min(100, (state.booked / state.total) * 100);
els.fill.style.width = pct + "%";
els.bookedEl.textContent = String(state.booked);
els.totalEl.textContent = String(state.total);
els.progress.setAttribute("aria-valuenow", String(state.booked));
els.progress.classList.toggle("is-full", isFull());
// hint
els.hint.classList.remove("is-warn", "is-full");
if (isFull()) {
if (state.isWaitlisted) {
els.hint.textContent = "Class is full — you're on the waitlist.";
} else {
els.hint.textContent = "Class is full — join the waitlist.";
}
els.hint.classList.add("is-full");
} else if (left <= 6) {
els.hint.textContent = left + " spots left — fills up fast.";
els.hint.classList.add("is-warn");
} else {
els.hint.textContent = left + " spots available.";
}
// CTA
els.book.classList.remove("is-booked", "is-waitlist");
if (state.isBookedByMe) {
els.book.classList.add("is-booked");
els.bookLabel.textContent = "Booked ✓ — Cancel?";
els.book.setAttribute("aria-pressed", "true");
} else if (state.isWaitlisted) {
els.book.classList.add("is-waitlist");
els.bookLabel.textContent = "On waitlist — Leave?";
els.book.setAttribute("aria-pressed", "true");
} else if (isFull()) {
els.book.classList.add("is-waitlist");
els.bookLabel.textContent = "Join waitlist";
els.book.setAttribute("aria-pressed", "false");
} else {
els.bookLabel.textContent = "Book this class";
els.book.setAttribute("aria-pressed", "false");
}
}
// ---- Roster ----
var myAvatar = null;
function addMyAvatar() {
if (myAvatar) return;
myAvatar = document.createElement("li");
myAvatar.className = "avatar";
myAvatar.textContent = "ME";
myAvatar.style.setProperty("--clr", "#c6ff3a");
myAvatar.style.marginLeft = "-10px";
els.avatars.insertBefore(myAvatar, els.more);
bumpMore(1);
}
function removeMyAvatar() {
if (!myAvatar) return;
myAvatar.remove();
myAvatar = null;
bumpMore(-1);
}
function bumpMore(delta) {
var cur = parseInt((els.more.textContent || "+0").replace("+", ""), 10) || 0;
els.more.textContent = "+" + Math.max(0, cur + delta);
}
// ---- Actions ----
function onBook() {
if (state.isBookedByMe) {
// cancel booking
state.isBookedByMe = false;
state.booked = Math.max(0, state.booked - 1);
removeMyAvatar();
toast("Booking cancelled. Your credit is back.", "warn");
} else if (state.isWaitlisted) {
// leave waitlist
state.isWaitlisted = false;
toast("Left the waitlist.", "warn");
} else if (isFull()) {
// join waitlist
state.isWaitlisted = true;
toast("You're on the waitlist — we'll text you if a spot opens.");
} else {
// book
state.isBookedByMe = true;
state.booked += 1;
addMyAvatar();
toast("You're in! HIIT Burn 45 · Mon 6:30 AM.");
}
render();
}
function onFollow() {
state.following = !state.following;
els.follow.classList.toggle("is-following", state.following);
els.follow.textContent = state.following ? "Following" : "Follow";
toast(
state.following
? "Following coach Mara Reyes."
: "Unfollowed Mara Reyes.",
state.following ? undefined : "warn"
);
}
// ---- Wire up ----
els.book.addEventListener("click", onBook);
els.follow.addEventListener("click", onFollow);
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<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" />
<title>Gym — Class Detail</title>
</head>
<body>
<main class="demo-root">
<article class="class-card" aria-labelledby="class-title">
<!-- HERO -->
<header class="hero">
<div class="hero__overlay"></div>
<div class="hero__top">
<span class="eyebrow">Strength & Conditioning</span>
<span class="intensity intensity--high" aria-label="Intensity: High">
<span class="intensity__dot"></span> High Intensity
</span>
</div>
<div class="hero__bottom">
<h1 id="class-title" class="hero__title">HIIT Burn 45</h1>
<p class="hero__sub">
45 minutes of all-out interval work designed to torch calories and
build engine.
</p>
</div>
</header>
<div class="body">
<!-- TRAINER -->
<section class="trainer" aria-label="Trainer">
<div class="trainer__avatar" aria-hidden="true">MR</div>
<div class="trainer__meta">
<span class="trainer__label">Your coach</span>
<span class="trainer__name">Mara Reyes</span>
<span class="trainer__rating" aria-label="Rated 4.9 out of 5">
★ 4.9 · 312 sessions
</span>
</div>
<button class="btn-ghost" type="button" data-follow>Follow</button>
</section>
<!-- META GRID -->
<section class="meta-grid" aria-label="Class details">
<div class="meta">
<span class="meta__label">Day & Time</span>
<span class="meta__value">Mon · 6:30 AM</span>
</div>
<div class="meta">
<span class="meta__label">Duration</span>
<span class="meta__value">45 min</span>
</div>
<div class="meta">
<span class="meta__label">Studio</span>
<span class="meta__value">Studio B · Heat Room</span>
</div>
<div class="meta">
<span class="meta__label">Est. burn</span>
<span class="meta__value">~520 kcal</span>
</div>
</section>
<!-- DIFFICULTY -->
<section class="difficulty" aria-label="Difficulty">
<div class="difficulty__head">
<span class="section-label">Difficulty</span>
<span class="difficulty__tag">Advanced</span>
</div>
<div
class="meter"
role="img"
aria-label="Difficulty 4 out of 5"
>
<span class="meter__bar is-on"></span>
<span class="meter__bar is-on"></span>
<span class="meter__bar is-on"></span>
<span class="meter__bar is-on"></span>
<span class="meter__bar"></span>
</div>
</section>
<!-- WHAT TO BRING -->
<section class="bring" aria-label="What to bring">
<span class="section-label">What to bring</span>
<ul class="bring__list">
<li><span class="bring__ic">💧</span> Water bottle</li>
<li><span class="bring__ic">🧤</span> Lifting gloves</li>
<li><span class="bring__ic">👟</span> Cross-trainers</li>
<li><span class="bring__ic">🧴</span> Sweat towel</li>
</ul>
</section>
<!-- SPOTS -->
<section class="spots" aria-label="Availability">
<div class="spots__head">
<span class="section-label">Spots</span>
<span class="spots__count" data-count>
<strong data-booked>14</strong>/<span data-total>20</span> booked
</span>
</div>
<div
class="progress"
role="progressbar"
aria-valuemin="0"
aria-valuemax="20"
aria-valuenow="14"
data-progress
>
<span class="progress__fill" data-fill></span>
</div>
<p class="spots__hint" data-hint>6 spots left — fills up fast.</p>
</section>
<!-- ROSTER -->
<section class="roster" aria-label="Who's going">
<span class="section-label">Who's going</span>
<div class="roster__row">
<ul class="avatars" data-avatars aria-hidden="true">
<li class="avatar" style="--i:0" data-clr="#ff6a2b">JD</li>
<li class="avatar" style="--i:1" data-clr="#c6ff3a">AL</li>
<li class="avatar" style="--i:2" data-clr="#34d399">SK</li>
<li class="avatar" style="--i:3" data-clr="#fbbf24">TM</li>
<li class="avatar" style="--i:4" data-clr="#60a5fa">RP</li>
<li class="avatar avatar--more" style="--i:5" data-more>+9</li>
</ul>
<span class="roster__names" data-names>
Jordan, Aimee, Sora & 11 others
</span>
</div>
</section>
</div>
<!-- CTA FOOTER -->
<footer class="cta">
<div class="cta__price">
<span class="cta__amount">1 credit</span>
<span class="cta__note">or $18 drop-in</span>
</div>
<button class="btn-book" type="button" data-book>
<span class="btn-book__label">Book this class</span>
</button>
</footer>
</article>
<div class="toast-wrap" data-toast-wrap aria-live="polite"></div>
</main>
<script src="script.js"></script>
</body>
</html>Class Detail
A self-contained, dark-themed class detail panel for a boutique gym, built for the “book a class” moment. The hero strip leads with the class name (HIIT Burn 45), a pulsing high-intensity badge and a one-line pitch, then hands off to the coach block with avatar, rating and a Follow toggle. A four-cell meta grid covers day and time, duration, studio and estimated calorie burn, followed by a five-bar difficulty meter, a what-to-bring checklist and an availability section.
The availability section is the interactive core: a live progress bar tracks 14/20 booked, with a hint line that warns when spots are running low. Pressing Book this class increments the count, animates the bar, drops a “ME” avatar into the roster preview and confirms with a toast. The button then becomes a Cancel state that reverses everything. When the class is full the CTA switches to a Join waitlist flow, and the progress bar shifts to a hot orange-to-red fill to signal the sell-out.
Everything is vanilla HTML, CSS and JavaScript — no frameworks or build step. State is
held in a small object and re-rendered on each action, a reusable toast() helper
handles feedback, and the layout is responsive down to ~360px (collapsing to a full-screen
sheet on phones). The card uses ARIA roles on the progress bar and buttons, keyboard-usable
controls, visible focus rings and a reduced-motion fallback.