LMS — Course Card
A reusable e-learning course card rendered as a responsive grid of six real-feeling variants — free, paid, in-progress and completed. Each card pairs a gradient thumbnail with a level pill, course title, instructor avatar, half-step star rating, duration and lesson counts, and either a price block or an animated progress bar. Cards lift on hover, quick-enroll inline, filter by type, and a study mode swaps the calm light theme for a focused dark palette.
MCP
Code
:root {
--brand: #5b5bd6;
--brand-d: #4444c2;
--brand-50: #eeeefc;
--accent: #13b981;
--amber: #f59e0b;
--ink: #1a1a2e;
--ink-2: #44465f;
--muted: #6b6e87;
--bg: #f7f7fb;
--surface: #ffffff;
--line: rgba(26, 26, 46, 0.1);
--ok: #13b981;
--danger: #e05656;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--shadow-1: 0 1px 2px rgba(26, 26, 46, 0.06), 0 1px 3px rgba(26, 26, 46, 0.05);
--shadow-2: 0 6px 16px rgba(26, 26, 46, 0.1), 0 2px 6px rgba(26, 26, 46, 0.06);
--shadow-3: 0 18px 40px rgba(68, 68, 194, 0.16), 0 6px 14px rgba(26, 26, 46, 0.1);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(1100px 520px at 12% -10%, rgba(91, 91, 214, 0.08), transparent 60%),
radial-gradient(900px 520px at 100% 0%, rgba(19, 185, 129, 0.07), transparent 55%),
var(--bg);
min-height: 100vh;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transition: background-color 0.3s ease, color 0.3s ease;
}
.page {
max-width: 1120px;
margin: 0 auto;
padding: 28px 22px 56px;
}
/* ---------- masthead ---------- */
.masthead {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
flex-wrap: wrap;
margin-bottom: 26px;
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand__mark {
display: grid;
place-items: center;
width: 44px;
height: 44px;
border-radius: var(--r-md);
background: linear-gradient(135deg, var(--brand), var(--brand-d));
color: #fff;
font-size: 22px;
box-shadow: var(--shadow-2);
}
.brand__name { margin: 0; font-size: 1.18rem; font-weight: 800; letter-spacing: -0.02em; }
.brand__sub { margin: 0; font-size: 0.82rem; color: var(--muted); font-weight: 500; }
.masthead__actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.seg {
display: inline-flex;
padding: 4px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 999px;
box-shadow: var(--shadow-1);
}
.seg__btn {
appearance: none;
border: 0;
background: transparent;
color: var(--ink-2);
font: inherit;
font-weight: 600;
font-size: 0.85rem;
padding: 7px 14px;
border-radius: 999px;
cursor: pointer;
transition: color 0.18s ease, background 0.18s ease, box-shadow 0.18s ease;
}
.seg__btn:hover { color: var(--ink); }
.seg__btn.is-active {
background: var(--brand);
color: #fff;
box-shadow: 0 4px 12px rgba(91, 91, 214, 0.35);
}
.seg__btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.theme-toggle {
display: inline-flex;
align-items: center;
gap: 8px;
border: 1px solid var(--line);
background: var(--surface);
color: var(--ink-2);
font: inherit;
font-weight: 600;
font-size: 0.83rem;
padding: 8px 12px;
border-radius: 999px;
cursor: pointer;
box-shadow: var(--shadow-1);
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.theme-toggle:hover { transform: translateY(-1px); box-shadow: var(--shadow-2); }
.theme-toggle:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.theme-toggle__dot {
width: 14px; height: 14px; border-radius: 50%;
background: radial-gradient(circle at 35% 35%, #fde68a, var(--amber));
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.5);
}
/* ---------- grid ---------- */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.empty {
text-align: center;
color: var(--muted);
font-weight: 500;
padding: 48px 0;
}
/* ---------- card ---------- */
.card {
position: relative;
display: flex;
flex-direction: column;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: var(--shadow-1);
transition: transform 0.22s cubic-bezier(0.2, 0.7, 0.2, 1), box-shadow 0.22s ease, border-color 0.22s ease;
animation: rise 0.4s both;
}
@keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.card:hover {
transform: translateY(-6px);
box-shadow: var(--shadow-3);
border-color: rgba(91, 91, 214, 0.3);
}
.card__thumb {
position: relative;
height: 150px;
display: grid;
place-items: center;
overflow: hidden;
}
.card__thumb::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg, transparent 40%, rgba(0, 0, 0, 0.16));
}
.card__glyph {
font-size: 52px;
filter: drop-shadow(0 6px 12px rgba(0, 0, 0, 0.18));
transition: transform 0.3s ease;
z-index: 1;
}
.card:hover .card__glyph { transform: scale(1.08) rotate(-3deg); }
.level {
position: absolute;
top: 12px;
left: 12px;
z-index: 2;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.02em;
padding: 4px 10px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.92);
color: var(--ink);
backdrop-filter: blur(4px);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
}
.level--beginner { color: var(--accent); }
.level--intermediate { color: var(--brand-d); }
.level--advanced { color: var(--danger); }
.ribbon {
position: absolute;
top: 12px;
right: 12px;
z-index: 2;
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.03em;
text-transform: uppercase;
padding: 4px 9px;
border-radius: 999px;
color: #fff;
}
.ribbon--free { background: var(--accent); }
.ribbon--bestseller { background: var(--amber); color: #4a2c00; }
.ribbon--done { background: var(--brand-d); }
.card__body { padding: 14px 16px 16px; display: flex; flex-direction: column; gap: 10px; flex: 1; }
.card__cat {
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--brand);
}
.card__title {
margin: 0;
font-size: 1.04rem;
font-weight: 700;
letter-spacing: -0.01em;
line-height: 1.28;
color: var(--ink);
}
.instr { display: flex; align-items: center; gap: 8px; }
.avatar {
width: 26px; height: 26px;
border-radius: 50%;
display: grid; place-items: center;
font-size: 0.72rem; font-weight: 700; color: #fff;
flex: none;
box-shadow: 0 0 0 2px var(--surface), 0 0 0 3px var(--line);
}
.instr__name { font-size: 0.85rem; color: var(--ink-2); font-weight: 500; }
.meta {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
font-size: 0.82rem;
color: var(--muted);
}
.meta__item { display: inline-flex; align-items: center; gap: 5px; }
.meta__item svg { width: 15px; height: 15px; stroke: currentColor; }
.rating { display: inline-flex; align-items: center; gap: 6px; font-weight: 600; color: var(--ink-2); }
.stars { display: inline-flex; gap: 1px; }
.star { width: 14px; height: 14px; color: var(--line); }
.star.is-on { color: var(--amber); }
.star.is-half { position: relative; color: var(--line); }
.star.is-half::before {
content: "";
position: absolute; inset: 0;
width: 50%;
overflow: hidden;
color: var(--amber);
}
.rating__count { color: var(--muted); font-weight: 500; font-size: 0.78rem; }
/* progress */
.progress { display: flex; flex-direction: column; gap: 6px; }
.progress__top {
display: flex; justify-content: space-between;
font-size: 0.78rem; font-weight: 600; color: var(--ink-2);
}
.progress__top .pct { color: var(--accent); }
.bar {
height: 8px;
border-radius: 999px;
background: var(--brand-50);
overflow: hidden;
}
.bar__fill {
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, var(--accent), #34d399);
width: 0;
transition: width 0.9s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.card.is-complete .bar__fill { background: linear-gradient(90deg, var(--brand), var(--brand-d)); }
/* footer */
.card__foot {
margin-top: auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding-top: 12px;
border-top: 1px solid var(--line);
}
.price { display: flex; align-items: baseline; gap: 7px; }
.price__now { font-size: 1.1rem; font-weight: 800; color: var(--ink); letter-spacing: -0.02em; }
.price__now.is-free { color: var(--accent); }
.price__was { font-size: 0.82rem; color: var(--muted); text-decoration: line-through; }
.btn {
appearance: none;
border: 0;
font: inherit;
font-weight: 700;
font-size: 0.84rem;
padding: 9px 16px;
border-radius: var(--r-sm);
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
}
.btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.btn:active { transform: scale(0.96); }
.btn--enroll {
background: var(--brand);
color: #fff;
box-shadow: 0 4px 12px rgba(91, 91, 214, 0.3);
}
.btn--enroll:hover { background: var(--brand-d); box-shadow: 0 7px 18px rgba(91, 91, 214, 0.42); }
.btn--resume {
background: var(--accent);
color: #fff;
box-shadow: 0 4px 12px rgba(19, 185, 129, 0.3);
}
.btn--resume:hover { background: #0fa674; }
.btn--ghost {
background: var(--brand-50);
color: var(--brand-d);
}
.btn--ghost:hover { background: #e3e3fb; }
.btn.is-enrolled {
background: var(--brand-50);
color: var(--brand-d);
box-shadow: none;
cursor: default;
}
/* ---------- study (dark) mode ---------- */
body.study {
--ink: #eef0ff;
--ink-2: #c3c6e6;
--muted: #9094bd;
--bg: #14152a;
--surface: #1f2140;
--line: rgba(255, 255, 255, 0.1);
--brand-50: rgba(91, 91, 214, 0.2);
background:
radial-gradient(1100px 520px at 12% -10%, rgba(91, 91, 214, 0.22), transparent 60%),
radial-gradient(900px 520px at 100% 0%, rgba(19, 185, 129, 0.14), transparent 55%),
var(--bg);
}
body.study .level { background: rgba(20, 21, 42, 0.85); color: #fff; }
body.study .seg, body.study .theme-toggle { box-shadow: none; }
/* ---------- responsive ---------- */
@media (max-width: 520px) {
.page { padding: 20px 14px 44px; }
.masthead { gap: 12px; }
.masthead__actions { width: 100%; justify-content: space-between; }
.seg { flex: 1; overflow-x: auto; }
.seg__btn { flex: 1; white-space: nowrap; }
.grid { grid-template-columns: 1fr; gap: 16px; }
.theme-toggle__label { display: none; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
/* ---------- toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 24px);
background: var(--ink);
color: #fff;
font-weight: 600;
font-size: 0.88rem;
padding: 11px 18px;
border-radius: 999px;
box-shadow: var(--shadow-3);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
z-index: 50;
max-width: calc(100% - 32px);
}
.toast.is-show { opacity: 1; transform: translate(-50%, 0); }(function () {
"use strict";
/* ---------- fictional data ---------- */
var COURSES = [
{
id: "react-foundations",
cat: "Web Development",
title: "React Foundations: From Zero to Components",
instructor: "Dev Patel",
glyph: "⚛️",
grad: ["#6366f1", "#8b5cf6"],
level: "Beginner",
rating: 4.8,
reviews: 2143,
hours: 9.5,
lessons: 58,
kind: "paid",
price: 49,
was: 89
},
{
id: "css-grid-master",
cat: "Design Engineering",
title: "Modern CSS Layout — Grid & Flexbox in Depth",
instructor: "Lena Hoffmann",
glyph: "🎨",
grad: ["#13b981", "#34d399"],
level: "Intermediate",
rating: 4.6,
reviews: 884,
hours: 6,
lessons: 41,
kind: "free"
},
{
id: "ts-systems",
cat: "Programming",
title: "TypeScript for Large Teams & Type-Safe APIs",
instructor: "Marco Silva",
glyph: "🧩",
grad: ["#0ea5e9", "#2563eb"],
level: "Advanced",
rating: 4.9,
reviews: 3402,
hours: 12.5,
lessons: 73,
kind: "paid",
price: 64,
was: 119,
badge: "bestseller"
},
{
id: "ux-research",
cat: "Product",
title: "Practical UX Research & Usability Testing",
instructor: "Aisha Khan",
glyph: "🔍",
grad: ["#f59e0b", "#f97316"],
level: "Intermediate",
rating: 4.5,
reviews: 612,
hours: 5.5,
lessons: 34,
kind: "progress",
progress: 62,
price: 39
},
{
id: "py-data",
cat: "Data Science",
title: "Data Analysis with Python & Pandas",
instructor: "Noah Becker",
glyph: "🐍",
grad: ["#10b981", "#059669"],
level: "Beginner",
rating: 4.7,
reviews: 1789,
hours: 8,
lessons: 49,
kind: "progress",
progress: 24,
price: 0
},
{
id: "design-systems",
cat: "Design Engineering",
title: "Building Scalable Design Systems",
instructor: "Priya Nair",
glyph: "🧱",
grad: ["#5b5bd6", "#4444c2"],
level: "Advanced",
rating: 5.0,
reviews: 941,
hours: 11,
lessons: 66,
kind: "complete",
progress: 100,
price: 79
}
];
var grid = document.getElementById("grid");
var emptyEl = document.getElementById("empty");
var enrolled = Object.create(null);
/* ---------- helpers ---------- */
function el(tag, cls, html) {
var n = document.createElement(tag);
if (cls) n.className = cls;
if (html != null) n.innerHTML = html;
return n;
}
function initials(name) {
return name.split(/\s+/).map(function (p) { return p[0]; }).join("").slice(0, 2).toUpperCase();
}
function avatarColor(name) {
var h = 0;
for (var i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) % 360;
return "linear-gradient(135deg, hsl(" + h + " 62% 52%), hsl(" + ((h + 40) % 360) + " 60% 44%))";
}
function starsMarkup(rating) {
var out = "";
for (var i = 1; i <= 5; i++) {
var cls = "star";
if (rating >= i) cls += " is-on";
else if (rating >= i - 0.5) cls += " is-half";
out += '<svg class="' + cls + '" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">' +
'<path d="M12 2l2.9 6.3 6.9.7-5.1 4.6 1.4 6.8L12 17.8 5.9 20.4l1.4-6.8L2.2 9l6.9-.7z"/></svg>';
}
return out;
}
var ICONS = {
clock: '<svg viewBox="0 0 24 24" fill="none" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>',
play: '<svg viewBox="0 0 24 24" fill="none" stroke-width="2" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M10 9l5 3-5 3z" fill="currentColor" stroke="none"/></svg>'
};
/* ---------- card ---------- */
function buildCard(c) {
var card = el("article", "card");
card.dataset.id = c.id;
card.dataset.kind = c.kind;
if (c.kind === "complete") card.classList.add("is-complete");
var levelMod = "level--" + c.level.toLowerCase();
var ribbon = "";
if (c.kind === "free" || c.price === 0) ribbon = '<span class="ribbon ribbon--free">Free</span>';
else if (c.kind === "complete") ribbon = '<span class="ribbon ribbon--done">✓ Done</span>';
else if (c.badge === "bestseller") ribbon = '<span class="ribbon ribbon--bestseller">Bestseller</span>';
var thumb = el("div", "card__thumb");
thumb.style.background = "linear-gradient(135deg, " + c.grad[0] + ", " + c.grad[1] + ")";
thumb.innerHTML =
'<span class="level ' + levelMod + '">' + c.level + "</span>" +
ribbon +
'<span class="card__glyph" aria-hidden="true">' + c.glyph + "</span>";
var body = el("div", "card__body");
// header
body.appendChild(el("span", "card__cat", c.cat));
var h3 = el("h3", "card__title", c.title);
body.appendChild(h3);
// instructor
var instr = el("div", "instr");
var av = el("span", "avatar", initials(c.instructor));
av.style.background = avatarColor(c.instructor);
instr.appendChild(av);
instr.appendChild(el("span", "instr__name", c.instructor));
body.appendChild(instr);
// rating + meta
var meta = el("div", "meta");
meta.innerHTML =
'<span class="rating"><span class="stars" aria-hidden="true">' + starsMarkup(c.rating) + "</span>" +
"<span>" + c.rating.toFixed(1) + "</span>" +
'<span class="rating__count">(' + c.reviews.toLocaleString() + ")</span></span>";
meta.setAttribute("aria-label", c.rating.toFixed(1) + " stars from " + c.reviews + " reviews");
var hours = el("span", "meta__item", ICONS.clock + "<span>" + c.hours + "h</span>");
var lessons = el("span", "meta__item", ICONS.play + "<span>" + c.lessons + " lessons</span>");
meta.appendChild(hours);
meta.appendChild(lessons);
body.appendChild(meta);
// progress (for in-progress / complete)
if (c.kind === "progress" || c.kind === "complete") {
var prog = el("div", "progress");
var label = c.kind === "complete" ? "Completed" : "In progress";
prog.innerHTML =
'<div class="progress__top"><span>' + label + '</span><span class="pct">' + c.progress + "%</span></div>" +
'<div class="bar"><div class="bar__fill" data-w="' + c.progress + '"></div></div>';
body.appendChild(prog);
}
// footer
var foot = el("div", "card__foot");
var priceWrap = el("div", "price");
if (c.kind === "progress" || c.kind === "complete") {
// show lessons-left summary instead of price for owned courses
if (c.kind === "complete") {
priceWrap.innerHTML = '<span class="price__now is-free">Certificate ✓</span>';
} else {
var left = Math.round(c.lessons * (1 - c.progress / 100));
priceWrap.innerHTML = '<span class="price__now" style="font-size:.92rem">' + left + " lessons left</span>";
}
} else if (c.kind === "free" || c.price === 0) {
priceWrap.innerHTML = '<span class="price__now is-free">Free</span>';
} else {
priceWrap.innerHTML =
'<span class="price__now">$' + c.price + "</span>" +
(c.was ? '<span class="price__was">$' + c.was + "</span>" : "");
}
foot.appendChild(priceWrap);
var btn = el("button", "btn");
btn.type = "button";
if (c.kind === "progress") {
btn.className += " btn--resume";
btn.textContent = "Resume";
} else if (c.kind === "complete") {
btn.className += " btn--ghost";
btn.textContent = "Review";
} else {
btn.className += " btn--enroll";
btn.textContent = "Enroll";
}
btn.setAttribute("aria-label", btn.textContent + " — " + c.title);
btn.addEventListener("click", function () { onAction(c, btn); });
foot.appendChild(btn);
body.appendChild(foot);
card.appendChild(thumb);
card.appendChild(body);
return card;
}
function onAction(c, btn) {
if (c.kind === "complete") {
toast("Reviewing “" + shortTitle(c.title) + "”");
return;
}
if (c.kind === "progress") {
toast("Resuming at lesson " + (Math.round(c.lessons * c.progress / 100) + 1));
return;
}
if (enrolled[c.id]) return;
enrolled[c.id] = true;
btn.classList.add("is-enrolled");
btn.textContent = "✓ Enrolled";
btn.setAttribute("aria-disabled", "true");
toast(
(c.kind === "free" || c.price === 0)
? "Enrolled in “" + shortTitle(c.title) + "” — free!"
: "Added “" + shortTitle(c.title) + "” to your courses"
);
}
function shortTitle(t) {
return t.length > 34 ? t.slice(0, 32).trim() + "…" : t;
}
/* ---------- render + filter ---------- */
function matches(c, f) {
if (f === "all") return true;
if (f === "free") return c.kind === "free" || c.price === 0;
if (f === "paid") return c.kind === "paid" || (c.price > 0 && c.kind !== "progress" && c.kind !== "complete");
if (f === "active") return c.kind === "progress" || c.kind === "complete";
return true;
}
function render(filter) {
grid.innerHTML = "";
var shown = 0;
COURSES.forEach(function (c, i) {
if (!matches(c, filter)) return;
var card = buildCard(c);
card.style.animationDelay = (i * 55) + "ms";
grid.appendChild(card);
shown++;
});
emptyEl.hidden = shown > 0;
// animate progress bars after layout
requestAnimationFrame(function () {
requestAnimationFrame(function () {
var fills = grid.querySelectorAll(".bar__fill");
for (var i = 0; i < fills.length; i++) {
fills[i].style.width = fills[i].dataset.w + "%";
}
});
});
}
/* ---------- filters ---------- */
var segBtns = document.querySelectorAll(".seg__btn");
segBtns.forEach(function (b) {
b.addEventListener("click", function () {
segBtns.forEach(function (x) {
x.classList.remove("is-active");
x.setAttribute("aria-selected", "false");
});
b.classList.add("is-active");
b.setAttribute("aria-selected", "true");
render(b.dataset.filter);
});
});
/* ---------- study mode ---------- */
var toggle = document.getElementById("themeToggle");
toggle.addEventListener("click", function () {
var on = document.body.classList.toggle("study");
toggle.setAttribute("aria-pressed", String(on));
toast(on ? "Study mode on" : "Study mode off");
});
/* ---------- toast ---------- */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 2200);
}
render("all");
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>LMS — Course Cards</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>
<h1 class="brand__name">Brightpath</h1>
<p class="brand__sub">Course catalog</p>
</div>
</div>
<div class="masthead__actions">
<div class="seg" role="tablist" aria-label="Filter courses">
<button class="seg__btn is-active" role="tab" aria-selected="true" data-filter="all">All</button>
<button class="seg__btn" role="tab" aria-selected="false" data-filter="free">Free</button>
<button class="seg__btn" role="tab" aria-selected="false" data-filter="paid">Paid</button>
<button class="seg__btn" role="tab" aria-selected="false" data-filter="active">In progress</button>
</div>
<button class="theme-toggle" id="themeToggle" type="button" aria-pressed="false" title="Toggle study mode">
<span class="theme-toggle__dot" aria-hidden="true"></span>
<span class="theme-toggle__label">Study mode</span>
</button>
</div>
</header>
<main>
<div class="grid" id="grid" aria-live="polite"><!-- cards injected by script.js --></div>
<p class="empty" id="empty" hidden>No courses match this filter.</p>
</main>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Course Card
A drop-in course card for any learning platform, shown here as a responsive catalog grid of six variants. Every card leads with a gradient thumbnail carrying a difficulty pill (Beginner / Intermediate / Advanced) and a contextual ribbon — Free, Bestseller, or a Done check. Below it sit the category eyebrow, title, an auto-colored instructor avatar, a half-step star rating with review count, and compact duration and lesson meta. The footer adapts to the card’s state: paid courses show a price with strikethrough, free courses read Free, and owned courses swap in an animated progress bar plus a lessons-left summary.
Interactions are all vanilla JS. Hovering lifts the card and nudges its glyph; the Enroll button flips to an enrolled state inline and fires a toast, while in-progress cards offer Resume and completed cards offer Review. The segmented control filters the grid by All, Free, Paid, or In progress, re-running the entry animation and re-filling progress bars on each pass. A Study mode toggle recolors the whole surface to a dark, focused reading theme.
The component is self-contained — data lives in a single COURSES array, ratings and avatar colors are derived from the data, and the layout reflows cleanly from a multi-column desktop grid down to a single mobile-first column at ~360px. Reduced-motion preferences are respected throughout.
Illustrative UI only — fictional courses, not a real learning platform.