Gym — Class Leaderboard
A live spin/HIIT class leaderboard built on the dark performance-gym theme with zero libraries. Ranked rows pair member avatars with an animated metric bar, a color-coded HR zone pill from Z1 to Z5 and a live effort percentage, while the top three earn dedicated podium cards. A segmented toggle re-ranks the field by calories, total output or Z4-plus zone minutes, and a simulated live tick nudges every rider and re-sorts rows with a smooth FLIP reorder animation, club-screen style.
MCP
Kod
: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;
--z1: #38bdf8;
--z2: #34d399;
--z3: #fbbf24;
--z4: #ff6a2b;
--z5: #f87171;
--shadow: 0 1px 0 rgba(255, 255, 255, 0.04) inset, 0 10px 30px rgba(0, 0, 0, 0.45);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background:
radial-gradient(1100px 540px at 85% -10%, rgba(198, 255, 58, 0.08), transparent 60%),
radial-gradient(900px 500px at -5% 110%, rgba(255, 106, 43, 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;
}
button { font-family: inherit; }
:focus-visible {
outline: 2px solid var(--neon);
outline-offset: 2px;
border-radius: 4px;
}
.screen {
max-width: 980px;
margin: 0 auto;
padding: clamp(18px, 4vw, 40px) clamp(14px, 3vw, 28px) 56px;
}
/* ---------- Header ---------- */
.board-head {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 20px;
flex-wrap: wrap;
margin-bottom: 22px;
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
text-transform: uppercase;
letter-spacing: 0.14em;
font-size: 12px;
font-weight: 700;
color: var(--neon);
}
.live-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--neon);
box-shadow: 0 0 0 0 rgba(198, 255, 58, 0.6);
animation: ping 1.6s ease-out infinite;
}
@keyframes ping {
0% { box-shadow: 0 0 0 0 rgba(198, 255, 58, 0.55); }
70%, 100% { box-shadow: 0 0 0 8px rgba(198, 255, 58, 0); }
}
#board-title {
margin: 8px 0 4px;
font-size: clamp(28px, 6vw, 46px);
font-weight: 900;
letter-spacing: -0.02em;
line-height: 1.04;
}
.head-sub {
margin: 0;
color: var(--muted);
font-size: 14px;
font-weight: 500;
}
.head-right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 12px;
}
.seg {
display: inline-flex;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 999px;
padding: 4px;
box-shadow: var(--shadow);
}
.seg-btn {
border: 0;
background: transparent;
color: var(--ink-2);
font-weight: 700;
font-size: 13px;
padding: 9px 16px;
border-radius: 999px;
cursor: pointer;
transition: color 0.18s, background 0.18s, transform 0.12s;
}
.seg-btn:hover { color: var(--ink); }
.seg-btn:active { transform: scale(0.96); }
.seg-btn.is-active {
background: var(--neon);
color: #10130a;
box-shadow: 0 6px 16px rgba(198, 255, 58, 0.28);
}
.ghost-btn {
display: inline-flex;
align-items: center;
gap: 9px;
background: var(--surface-2);
border: 1px solid var(--line);
color: var(--ink-2);
font-weight: 600;
font-size: 13px;
padding: 9px 15px;
border-radius: 999px;
cursor: pointer;
transition: border-color 0.18s, color 0.18s, transform 0.12s;
}
.ghost-btn:hover { border-color: var(--line-2); color: var(--ink); }
.ghost-btn:active { transform: scale(0.97); }
.pulse-ico {
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--orange);
animation: pulseio 1.1s ease-in-out infinite;
}
.ghost-btn[aria-pressed="true"] .pulse-ico {
background: var(--muted);
animation: none;
}
@keyframes pulseio {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.5); opacity: 0.55; }
}
/* ---------- Podium ---------- */
.podium {
display: grid;
grid-template-columns: 1fr 1.12fr 1fr;
gap: 12px;
align-items: end;
margin-bottom: 18px;
}
.pod-card {
position: relative;
background: linear-gradient(180deg, var(--surface-2), var(--surface));
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 18px 14px 16px;
text-align: center;
box-shadow: var(--shadow);
transition: transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 0.3s;
}
.pod-card.r1 {
border-color: rgba(198, 255, 58, 0.45);
background: linear-gradient(180deg, rgba(198, 255, 58, 0.16), var(--surface));
box-shadow: 0 14px 40px rgba(198, 255, 58, 0.16), var(--shadow);
transform: translateY(-8px);
}
.pod-card.r2 { border-color: rgba(255, 255, 255, 0.22); }
.pod-card.r3 { border-color: rgba(255, 106, 43, 0.32); }
.pod-rank {
font-size: 13px;
font-weight: 900;
letter-spacing: 0.08em;
color: var(--muted);
}
.pod-card.r1 .pod-rank { color: var(--neon); }
.pod-card.r3 .pod-rank { color: var(--orange); }
.pod-medal {
font-size: 22px;
line-height: 1;
margin-bottom: 2px;
}
.pod-avatar {
width: 54px;
height: 54px;
border-radius: 50%;
margin: 6px auto 8px;
display: grid;
place-items: center;
font-weight: 800;
font-size: 18px;
color: #0d0f12;
border: 2px solid rgba(255, 255, 255, 0.18);
}
.pod-card.r1 .pod-avatar {
width: 64px;
height: 64px;
border-color: var(--neon);
box-shadow: 0 0 0 4px rgba(198, 255, 58, 0.12);
}
.pod-name {
font-weight: 800;
font-size: 15px;
margin: 0 0 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pod-metric {
font-size: 26px;
font-weight: 900;
letter-spacing: -0.02em;
line-height: 1.1;
}
.pod-card.r1 .pod-metric { color: var(--neon); }
.pod-metric span {
font-size: 12px;
font-weight: 600;
color: var(--muted);
letter-spacing: 0;
}
.pod-foot {
display: inline-flex;
align-items: center;
gap: 6px;
margin-top: 8px;
}
/* ---------- Legend ---------- */
.legend {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
padding: 0 4px;
margin: 18px 0 12px;
}
.legend-label {
text-transform: uppercase;
letter-spacing: 0.1em;
font-size: 11px;
font-weight: 700;
color: var(--muted);
}
.legend-metric {
margin-left: auto;
font-size: 12px;
font-weight: 600;
color: var(--muted);
}
/* ---------- Zone pills ---------- */
.zpill {
font-size: 11px;
font-weight: 800;
letter-spacing: 0.04em;
padding: 3px 9px;
border-radius: 999px;
color: #0d0f12;
white-space: nowrap;
}
.zpill.z1 { background: var(--z1); }
.zpill.z2 { background: var(--z2); }
.zpill.z3 { background: var(--z3); }
.zpill.z4 { background: var(--z4); color: #fff; }
.zpill.z5 { background: var(--z5); color: #fff; }
/* ---------- Board rows ---------- */
.board {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.row {
display: grid;
grid-template-columns: 38px 44px 1fr auto;
align-items: center;
gap: 14px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 12px 16px;
box-shadow: var(--shadow);
cursor: pointer;
transition: border-color 0.18s, background 0.18s, transform 0.12s;
will-change: transform;
}
.row:hover { border-color: var(--line-2); background: var(--surface-2); }
.row:active { transform: scale(0.995); }
.row.is-leader {
border-color: rgba(198, 255, 58, 0.4);
background: linear-gradient(90deg, var(--neon-50), var(--surface) 55%);
}
.row.flash {
animation: flash 0.7s ease-out;
}
@keyframes flash {
0% { box-shadow: 0 0 0 0 rgba(198, 255, 58, 0.45), var(--shadow); }
100% { box-shadow: 0 0 0 12px rgba(198, 255, 58, 0), var(--shadow); }
}
.row-pos {
font-size: 19px;
font-weight: 900;
color: var(--muted);
text-align: center;
font-variant-numeric: tabular-nums;
}
.row.is-leader .row-pos { color: var(--neon); }
.row-mover {
display: inline-block;
font-size: 10px;
font-weight: 800;
margin-left: 1px;
}
.mv-up { color: var(--ok); }
.mv-down { color: var(--danger); }
.row-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
display: grid;
place-items: center;
font-weight: 800;
font-size: 14px;
color: #0d0f12;
border: 2px solid rgba(255, 255, 255, 0.14);
}
.row-main { min-width: 0; }
.row-name {
font-weight: 700;
font-size: 15px;
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.row-sub {
display: flex;
align-items: center;
gap: 10px;
margin-top: 6px;
}
.bar-track {
flex: 1;
height: 7px;
min-width: 60px;
background: rgba(255, 255, 255, 0.07);
border-radius: 999px;
overflow: hidden;
}
.bar-fill {
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, var(--neon-d), var(--neon));
transition: width 0.6s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.row-effort {
font-size: 12px;
font-weight: 700;
color: var(--ink-2);
font-variant-numeric: tabular-nums;
min-width: 38px;
text-align: right;
}
.row-right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 6px;
}
.row-metric {
font-size: 20px;
font-weight: 900;
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
line-height: 1;
}
.row.is-leader .row-metric { color: var(--neon); }
.row-metric small {
font-size: 11px;
font-weight: 600;
color: var(--muted);
letter-spacing: 0;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translate(-50%, 130%);
background: var(--elevated);
color: var(--ink);
border: 1px solid var(--line-2);
border-left: 3px solid var(--neon);
padding: 12px 18px;
border-radius: var(--r-md);
font-size: 14px;
font-weight: 600;
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.55);
opacity: 0;
transition: transform 0.32s cubic-bezier(0.2, 0.9, 0.2, 1), opacity 0.32s;
z-index: 50;
max-width: 88vw;
}
.toast.show { transform: translate(-50%, 0); opacity: 1; }
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
.board-head { flex-direction: column; align-items: stretch; }
.head-right { align-items: stretch; }
.seg { width: 100%; justify-content: space-between; }
.seg-btn { flex: 1; padding: 9px 4px; }
.ghost-btn { justify-content: center; }
.podium { grid-template-columns: 1fr; gap: 8px; }
.pod-card, .pod-card.r1 { transform: none; }
.pod-card.r1 { order: -1; }
.row {
grid-template-columns: 30px 36px 1fr;
gap: 10px;
padding: 11px 13px;
}
.row-right {
grid-column: 2 / -1;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-top: 8px;
}
.row-metric { font-size: 18px; }
.legend-metric { margin-left: 0; width: 100%; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
}
}(function () {
"use strict";
/* ---------- Data ---------- */
var AVATAR_COLORS = [
"#c6ff3a", "#ff6a2b", "#38bdf8", "#34d399", "#fbbf24",
"#f87171", "#a78bfa", "#f472b6", "#22d3ee", "#fb923c"
];
var riders = [
{ name: "Dana Okafor", seat: 7 },
{ name: "Marco Bellini", seat: 12 },
{ name: "Priya Raman", seat: 3 },
{ name: "Jules Lefevre", seat: 19 },
{ name: "Sofia Marquez", seat: 1 },
{ name: "Theo Andersson", seat: 24 },
{ name: "Naomi Clarke", seat: 8 },
{ name: "Wesley Brooks", seat: 15 },
{ name: "Aisha Karim", seat: 5 },
{ name: "Diego Ferraro", seat: 21 },
{ name: "Lena Hoffmann", seat: 11 },
{ name: "Kwame Mensah", seat: 17 }
];
// seed metrics
riders.forEach(function (r, i) {
r.id = "r" + i;
r.color = AVATAR_COLORS[i % AVATAR_COLORS.length];
r.calories = 180 + Math.round(Math.random() * 240); // kcal so far
r.output = 220 + Math.round(Math.random() * 360); // total output (kJ-ish)
r.zone = 1 + Math.round(Math.random() * 4); // current zone 1-5
r.zoneTime = 4 + Math.round(Math.random() * 26); // minutes in Z4+Z5
r.effort = 55 + Math.round(Math.random() * 44); // effort %
r.prevPos = i;
});
var METRICS = {
calories: { key: "calories", label: "Ranking by total calories", unit: "kcal", suffix: " cal" },
output: { key: "output", label: "Ranking by total output", unit: "kJ", suffix: " out" },
zone: { key: "zoneTime", label: "Ranking by Z4+ zone minutes", unit: "min", suffix: " min" }
};
var ZONE_NAME = { 1: "Z1", 2: "Z2", 3: "Z3", 4: "Z4", 5: "Z5" };
var state = { metric: "calories", paused: false };
/* ---------- Helpers ---------- */
var boardEl = document.getElementById("board");
var podiumEl = document.getElementById("podium");
var toastEl = document.getElementById("toast");
var legendMetricEl = document.getElementById("legendMetric");
var nodeMap = {}; // riderId -> row element
function initials(name) {
return name.split(" ").map(function (p) { return p[0]; }).join("").slice(0, 2).toUpperCase();
}
function metricValue(r) {
return r[METRICS[state.metric].key];
}
function sorted() {
return riders.slice().sort(function (a, b) {
var d = metricValue(b) - metricValue(a);
if (d !== 0) return d;
return a.name.localeCompare(b.name);
});
}
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2400);
}
/* ---------- Row creation ---------- */
function buildRow(r) {
var li = document.createElement("li");
li.className = "row";
li.tabIndex = 0;
li.setAttribute("role", "button");
li.dataset.id = r.id;
li.innerHTML =
'<div class="row-pos"><span class="pos-num"></span><span class="row-mover" aria-hidden="true"></span></div>' +
'<div class="row-avatar"></div>' +
'<div class="row-main">' +
'<div class="row-name"><span class="nm"></span><span class="zpill"></span></div>' +
'<div class="row-sub">' +
'<div class="bar-track"><div class="bar-fill"></div></div>' +
'<span class="row-effort"></span>' +
'</div>' +
'</div>' +
'<div class="row-right">' +
'<div class="row-metric"><span class="mval"></span> <small></small></div>' +
'</div>';
var av = li.querySelector(".row-avatar");
av.textContent = initials(r.name);
av.style.background = r.color;
li.querySelector(".nm").textContent = r.name;
function activate() { showDetail(r); }
li.addEventListener("click", activate);
li.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); activate(); }
});
nodeMap[r.id] = li;
return li;
}
function updateRow(li, r, pos) {
li.querySelector(".pos-num").textContent = pos + 1;
// movement indicator
var mover = li.querySelector(".row-mover");
var delta = r.prevPos - pos;
if (delta > 0) { mover.textContent = "▲" + delta; mover.className = "row-mover mv-up"; }
else if (delta < 0) { mover.textContent = "▼" + (-delta); mover.className = "row-mover mv-down"; }
else { mover.textContent = ""; mover.className = "row-mover"; }
li.classList.toggle("is-leader", pos === 0);
// zone pill
var zp = li.querySelector(".zpill");
zp.className = "zpill z" + r.zone;
zp.textContent = ZONE_NAME[r.zone];
// bar (relative to current leader's metric value)
var max = metricValue(sortedCache[0]) || 1;
var pct = Math.max(6, Math.round((metricValue(r) / max) * 100));
li.querySelector(".bar-fill").style.width = pct + "%";
li.querySelector(".row-effort").textContent = r.effort + "%";
li.querySelector(".mval").textContent = metricValue(r).toLocaleString();
li.querySelector(".row-metric small").textContent = METRICS[state.metric].unit;
}
/* ---------- Podium ---------- */
var MEDAL = ["🥇", "🥈", "🥉"];
function renderPodium(top3) {
podiumEl.innerHTML = "";
var order = [1, 0, 2]; // visual: 2nd, 1st, 3rd
order.forEach(function (idx) {
var r = top3[idx];
if (!r) return;
var card = document.createElement("article");
card.className = "pod-card r" + (idx + 1);
card.innerHTML =
'<div class="pod-medal" aria-hidden="true">' + MEDAL[idx] + '</div>' +
'<div class="pod-rank">#' + (idx + 1) + '</div>' +
'<div class="pod-avatar"></div>' +
'<p class="pod-name"></p>' +
'<div class="pod-metric"><span class="pv"></span> <span></span></div>' +
'<div class="pod-foot"><span class="zpill z' + r.zone + '">' + ZONE_NAME[r.zone] + '</span></div>';
var av = card.querySelector(".pod-avatar");
av.textContent = initials(r.name);
av.style.background = r.color;
card.querySelector(".pod-name").textContent = r.name;
card.querySelector(".pv").textContent = metricValue(r).toLocaleString();
card.querySelector(".pod-metric span:last-child").textContent = METRICS[state.metric].unit;
podiumEl.appendChild(card);
});
}
/* ---------- Render + FLIP reorder ---------- */
var sortedCache = [];
var reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
function render(animate) {
sortedCache = sorted();
// FLIP: record First positions
var firstTops = {};
if (animate && !reduced) {
Object.keys(nodeMap).forEach(function (id) {
firstTops[id] = nodeMap[id].getBoundingClientRect().top;
});
}
// ensure rows exist + reorder DOM
sortedCache.forEach(function (r, pos) {
var li = nodeMap[r.id] || buildRow(r);
boardEl.appendChild(li); // append in sorted order
updateRow(li, r, pos);
});
// FLIP: Last -> Invert -> Play
if (animate && !reduced) {
Object.keys(nodeMap).forEach(function (id) {
var li = nodeMap[id];
var lastTop = li.getBoundingClientRect().top;
var dy = (firstTops[id] || lastTop) - lastTop;
if (dy) {
li.style.transform = "translateY(" + dy + "px)";
li.style.transition = "none";
requestAnimationFrame(function () {
li.style.transition = "transform 0.55s cubic-bezier(0.2, 0.8, 0.2, 1)";
li.style.transform = "";
});
}
});
}
renderPodium(sortedCache.slice(0, 3));
legendMetricEl.textContent = METRICS[state.metric].label;
}
function commitPrevPositions() {
sortedCache.forEach(function (r, pos) { r.prevPos = pos; });
}
/* ---------- Live tick ---------- */
function tick() {
if (state.paused) return;
commitPrevPositions();
riders.forEach(function (r) {
// calories + output keep climbing, weighted by current effort
var drive = r.effort / 100;
r.calories += Math.round((1 + Math.random() * 4) * (0.5 + drive));
r.output += Math.round((2 + Math.random() * 6) * (0.5 + drive));
// effort drifts
r.effort = Math.min(100, Math.max(40, r.effort + Math.round((Math.random() - 0.5) * 9)));
// zone follows effort with some noise
var target = r.effort >= 92 ? 5 : r.effort >= 82 ? 4 : r.effort >= 68 ? 3 : r.effort >= 55 ? 2 : 1;
if (Math.random() < 0.55) r.zone = target;
if (r.zone >= 4) r.zoneTime += Math.random() < 0.5 ? 1 : 0;
});
render(true);
}
/* ---------- Detail toast ---------- */
function showDetail(r) {
var rank = sortedCache.findIndex(function (x) { return x.id === r.id; }) + 1;
toast(
"#" + rank + " " + r.name + " · Seat " + r.seat + " · " +
r.calories + " cal · " + r.output + " out · " +
ZONE_NAME[r.zone] + " @ " + r.effort + "% effort"
);
}
/* ---------- Controls ---------- */
var tabs = Array.prototype.slice.call(document.querySelectorAll(".seg-btn"));
tabs.forEach(function (btn) {
btn.addEventListener("click", function () {
if (state.metric === btn.dataset.metric) return;
state.metric = btn.dataset.metric;
tabs.forEach(function (b) {
var on = b === btn;
b.classList.toggle("is-active", on);
b.setAttribute("aria-selected", on ? "true" : "false");
});
commitPrevPositions();
render(true);
toast("Ranking by " + btn.textContent.toLowerCase());
});
});
var pauseBtn = document.getElementById("pauseBtn");
var pauseLabel = document.getElementById("pauseLabel");
pauseBtn.addEventListener("click", function () {
state.paused = !state.paused;
pauseBtn.setAttribute("aria-pressed", state.paused ? "true" : "false");
pauseLabel.textContent = state.paused ? "Resume live" : "Pause live";
toast(state.paused ? "Live updates paused" : "Live updates resumed");
});
/* ---------- Boot ---------- */
render(false);
commitPrevPositions();
setInterval(tick, 2200);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Class Leaderboard — Live</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="screen" aria-labelledby="board-title">
<header class="board-head">
<div class="head-left">
<span class="eyebrow">
<span class="live-dot" aria-hidden="true"></span> Live · Studio 3
</span>
<h1 id="board-title">Power Spin 45</h1>
<p class="head-sub">Coach Mara Quintero · 24 riders · 31:12 elapsed</p>
</div>
<div class="head-right">
<div class="seg" role="tablist" aria-label="Ranking metric">
<button class="seg-btn is-active" role="tab" aria-selected="true" data-metric="calories" id="tab-cal">Calories</button>
<button class="seg-btn" role="tab" aria-selected="false" data-metric="output" id="tab-out">Output</button>
<button class="seg-btn" role="tab" aria-selected="false" data-metric="zone" id="tab-zone">Zone Time</button>
</div>
<button class="ghost-btn" id="pauseBtn" aria-pressed="false">
<span class="pulse-ico" aria-hidden="true"></span>
<span id="pauseLabel">Pause live</span>
</button>
</div>
</header>
<section class="podium" id="podium" aria-label="Top three riders"></section>
<section class="legend" aria-hidden="true">
<span class="legend-label">HR Zones</span>
<span class="zpill z1">Z1</span>
<span class="zpill z2">Z2</span>
<span class="zpill z3">Z3</span>
<span class="zpill z4">Z4</span>
<span class="zpill z5">Z5</span>
<span class="legend-metric" id="legendMetric">Ranking by total calories</span>
</section>
<ol class="board" id="board" aria-live="polite" aria-label="Class leaderboard, ranked"></ol>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Class Leaderboard
A high-energy, club-screen leaderboard for a live spin or HIIT class on the dark performance-gym theme. Each ranked row shows the rider’s position with a tiny up/down mover badge, a colored avatar, their name, a neon metric bar scaled against the current leader, a live effort percentage and a color-coded heart-rate zone pill running Z1 (blue, easy) through Z5 (red, max). The top three riders are lifted out into dedicated podium cards, with the leader getting a glowing neon treatment.
A segmented Calories / Output / Zone Time toggle re-ranks the entire field on the fly — the bars, podium and headline metric all switch units together. A simulated live tick runs every couple of seconds, nudging each rider’s calories, output, effort and HR zone, then re-sorts the board. Rows glide to their new positions with a smooth FLIP-style reorder animation so overtakes read clearly, and a Pause control freezes the feed.
Tap or keyboard-activate any row to fire a toast with that rider’s full detail (rank, seat, calories,
output, zone and effort). Names, seats and data are realistic but fictional, the layout collapses to a
single column down to ~360px, focus rings stay visible for keyboard use, and motion respects
prefers-reduced-motion. Everything is vanilla JS — no frameworks, no build step.