Podcast — Show Hub
A dark, audio-first hub for browsing a network of podcasts. Shows land in a responsive grid of glowing cover cards, each carrying host, category, episode count and a pulsing play button. Live search filters by title or host as you type, category chips narrow the roster, and a sort control reorders by newest, most episodes or alphabetical. Empty states, result counts and toast feedback keep the whole browsing loop feeling responsive and alive.
MCP
Code
:root {
--bg: #0c0c10;
--surface: #15151c;
--surface-2: #1d1d27;
--violet: #8b5cf6;
--violet-d: #7c3aed;
--cyan: #22d3ee;
--pink: #f472b6;
--ink: #f4f4f8;
--muted: #9a9aab;
--line: rgba(255, 255, 255, 0.10);
--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;
min-height: 100vh;
background: 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;
position: relative;
overflow-x: hidden;
}
.glow {
position: fixed;
width: 44vw;
height: 44vw;
max-width: 620px;
max-height: 620px;
border-radius: 50%;
filter: blur(120px);
opacity: 0.32;
pointer-events: none;
z-index: 0;
}
.glow-a { top: -14vw; left: -8vw; background: radial-gradient(circle, var(--violet), transparent 70%); }
.glow-b { bottom: -18vw; right: -10vw; background: radial-gradient(circle, var(--cyan), transparent 70%); }
/* ---------- Topbar ---------- */
.topbar {
position: sticky;
top: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px clamp(16px, 4vw, 40px);
background: rgba(12, 12, 16, 0.72);
backdrop-filter: blur(14px);
border-bottom: 1px solid var(--line);
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark {
display: flex;
align-items: flex-end;
gap: 3px;
height: 26px;
padding: 6px;
border-radius: var(--r-sm);
background: linear-gradient(135deg, rgba(139, 92, 246, 0.22), rgba(34, 211, 238, 0.18));
border: 1px solid var(--line);
}
.brand-mark span {
width: 3px;
border-radius: 3px;
background: linear-gradient(180deg, var(--cyan), var(--violet));
animation: eq 1.1s ease-in-out infinite;
}
.brand-mark span:nth-child(1) { height: 40%; animation-delay: 0s; }
.brand-mark span:nth-child(2) { height: 80%; animation-delay: .15s; }
.brand-mark span:nth-child(3) { height: 55%; animation-delay: .3s; }
.brand-mark span:nth-child(4) { height: 95%; animation-delay: .45s; }
.brand-mark span:nth-child(5) { height: 50%; animation-delay: .6s; }
@keyframes eq { 0%, 100% { transform: scaleY(.55); } 50% { transform: scaleY(1); } }
.brand-text { display: flex; flex-direction: column; line-height: 1.15; }
.brand-text strong { font-size: 15px; font-weight: 800; letter-spacing: -0.01em; }
.brand-text small { font-size: 11px; color: var(--muted); }
.topnav { display: flex; align-items: center; gap: 6px; }
.topnav a {
color: var(--muted);
text-decoration: none;
font-size: 14px;
font-weight: 600;
padding: 8px 12px;
border-radius: var(--r-sm);
transition: color .15s, background .15s;
}
.topnav a:hover { color: var(--ink); background: var(--surface-2); }
.topnav a.active { color: var(--ink); }
.btn-ghost {
margin-left: 6px;
font: inherit;
font-size: 13px;
font-weight: 600;
color: var(--ink);
background: var(--surface-2);
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 8px 16px;
cursor: pointer;
transition: border-color .15s, transform .1s;
}
.btn-ghost:hover { border-color: var(--violet); }
.btn-ghost:active { transform: translateY(1px); }
/* ---------- Layout ---------- */
.wrap {
position: relative;
z-index: 1;
max-width: 1180px;
margin: 0 auto;
padding: clamp(28px, 5vw, 56px) clamp(16px, 4vw, 40px) 80px;
}
.hero { max-width: 640px; margin-bottom: 32px; }
.eyebrow {
display: inline-block;
margin: 0 0 12px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--cyan);
}
.hero h1 {
margin: 0 0 12px;
font-size: clamp(30px, 5.5vw, 46px);
font-weight: 800;
letter-spacing: -0.03em;
line-height: 1.05;
}
.grad {
background: linear-gradient(100deg, var(--violet), var(--cyan));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.lede { margin: 0; color: var(--muted); font-size: 16px; }
/* ---------- Controls ---------- */
.controls {
display: flex;
gap: 12px;
align-items: center;
margin-bottom: 18px;
}
.search {
position: relative;
flex: 1;
display: flex;
align-items: center;
}
.search svg {
position: absolute;
left: 14px;
width: 18px;
height: 18px;
fill: none;
stroke: var(--muted);
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
pointer-events: none;
}
.search input {
width: 100%;
font: inherit;
font-size: 15px;
color: var(--ink);
background: var(--surface);
border: 1px solid var(--line);
border-radius: 999px;
padding: 13px 44px 13px 42px;
transition: border-color .15s, box-shadow .15s;
}
.search input::placeholder { color: var(--muted); }
.search input:focus {
outline: none;
border-color: var(--violet);
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.28);
}
.clear-search {
position: absolute;
right: 8px;
width: 28px;
height: 28px;
display: grid;
place-items: center;
font-size: 20px;
line-height: 1;
color: var(--muted);
background: var(--surface-2);
border: none;
border-radius: 50%;
cursor: pointer;
transition: color .15s, background .15s;
}
.clear-search:hover { color: var(--ink); background: var(--violet-d); }
.sort { display: flex; align-items: center; gap: 8px; }
.sort label { font-size: 13px; color: var(--muted); font-weight: 600; }
.sort select {
font: inherit;
font-size: 14px;
font-weight: 600;
color: var(--ink);
background: var(--surface);
border: 1px solid var(--line);
border-radius: 999px;
padding: 11px 34px 11px 14px;
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%239a9aab' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M2 4l4 4 4-4'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 14px center;
transition: border-color .15s;
}
.sort select:focus { outline: none; border-color: var(--violet); box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.28); }
/* ---------- Chips ---------- */
.chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 22px;
}
.chip {
font: inherit;
font-size: 13px;
font-weight: 600;
color: var(--muted);
background: var(--surface);
border: 1px solid var(--line);
border-radius: 999px;
padding: 8px 15px;
cursor: pointer;
transition: color .15s, border-color .15s, background .15s, transform .1s;
}
.chip:hover { color: var(--ink); border-color: var(--line-2); }
.chip:active { transform: translateY(1px); }
.chip .count {
margin-left: 6px;
font-size: 11px;
color: var(--muted);
opacity: .8;
}
.chip:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.4); }
.chip.active {
color: var(--white);
background: linear-gradient(120deg, var(--violet-d), var(--violet));
border-color: transparent;
}
.chip.active .count { color: rgba(255, 255, 255, 0.82); }
/* ---------- Meta row ---------- */
.meta-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px;
}
.result-count { margin: 0; font-size: 13px; color: var(--muted); }
.result-count b { color: var(--ink); }
.reset-inline {
font: inherit;
font-size: 13px;
font-weight: 600;
color: var(--cyan);
background: none;
border: none;
cursor: pointer;
padding: 4px;
}
.reset-inline:hover { text-decoration: underline; }
/* ---------- Grid ---------- */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 18px;
}
.card {
display: flex;
flex-direction: column;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
transition: transform .18s ease, border-color .18s, box-shadow .18s;
animation: rise .4s ease both;
}
.card:hover {
transform: translateY(-4px);
border-color: var(--line-2);
box-shadow: 0 18px 40px -18px rgba(0, 0, 0, 0.7);
}
@keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.cover {
position: relative;
aspect-ratio: 16 / 10;
display: flex;
align-items: flex-end;
padding: 14px;
color: var(--white);
}
.cover::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg, transparent 30%, rgba(0, 0, 0, 0.55));
}
.cover-mono {
position: relative;
z-index: 1;
font-size: 34px;
font-weight: 800;
letter-spacing: -0.03em;
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
}
.wave {
position: absolute;
z-index: 1;
left: 14px;
right: 14px;
bottom: 14px;
display: flex;
align-items: flex-end;
gap: 3px;
height: 26px;
opacity: .9;
}
.wave i {
flex: 1;
border-radius: 3px;
background: linear-gradient(180deg, var(--cyan), var(--violet));
}
.play {
position: absolute;
z-index: 2;
top: 14px;
right: 14px;
width: 44px;
height: 44px;
display: grid;
place-items: center;
color: var(--white);
background: linear-gradient(135deg, var(--violet), var(--violet-d));
border: none;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 0 0 0 rgba(139, 92, 246, 0.55);
transition: transform .15s, box-shadow .25s;
}
.play svg { width: 18px; height: 18px; margin-left: 2px; fill: currentColor; }
.play:hover { transform: scale(1.08); box-shadow: 0 0 22px 4px rgba(139, 92, 246, 0.55); }
.play:active { transform: scale(0.96); }
.play:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.7); }
.card.playing .play {
animation: pulse 1.4s ease-out infinite;
background: linear-gradient(135deg, var(--cyan), var(--violet));
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(34, 211, 238, 0.5); }
100% { box-shadow: 0 0 0 16px rgba(34, 211, 238, 0); }
}
.card-body { padding: 14px 16px 16px; display: flex; flex-direction: column; gap: 8px; flex: 1; }
.card-top { display: flex; align-items: center; gap: 8px; }
.badge {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.03em;
text-transform: uppercase;
color: var(--ink);
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: 999px;
padding: 3px 9px;
}
.badge.new {
color: var(--cyan);
border-color: rgba(34, 211, 238, 0.35);
background: rgba(34, 211, 238, 0.10);
}
.card h3 { margin: 0; font-size: 16px; font-weight: 700; letter-spacing: -0.01em; }
.host { margin: 0; font-size: 13px; color: var(--muted); }
.host b { color: var(--ink); font-weight: 600; }
.card-foot {
margin-top: auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding-top: 4px;
}
.eps { font-size: 12px; color: var(--muted); display: flex; align-items: center; gap: 6px; }
.eps::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--pink); }
.follow {
font: inherit;
font-size: 12px;
font-weight: 700;
color: var(--ink);
background: var(--surface-2);
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 6px 13px;
cursor: pointer;
transition: border-color .15s, color .15s, background .15s;
}
.follow:hover { border-color: var(--violet); }
.follow.on { color: var(--white); background: linear-gradient(120deg, var(--violet-d), var(--violet)); border-color: transparent; }
.follow:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.4); }
/* ---------- Empty ---------- */
.empty {
text-align: center;
padding: 60px 20px;
border: 1px dashed var(--line-2);
border-radius: var(--r-lg);
background: var(--surface);
}
.empty-icon { font-size: 40px; margin-bottom: 8px; }
.empty h2 { margin: 0 0 6px; font-size: 20px; font-weight: 700; }
.empty p { margin: 0 0 18px; color: var(--muted); }
.btn-primary {
font: inherit;
font-size: 14px;
font-weight: 700;
color: var(--white);
background: linear-gradient(120deg, var(--violet-d), var(--violet));
border: none;
border-radius: 999px;
padding: 11px 22px;
cursor: pointer;
transition: transform .1s, box-shadow .2s;
}
.btn-primary:hover { box-shadow: 0 0 20px rgba(139, 92, 246, 0.5); }
.btn-primary:active { transform: translateY(1px); }
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 20px);
z-index: 60;
max-width: calc(100vw - 32px);
padding: 12px 18px;
font-size: 14px;
font-weight: 600;
color: var(--ink);
background: var(--surface-2);
border: 1px solid var(--line-2);
border-radius: var(--r-md);
box-shadow: 0 16px 40px -12px rgba(0, 0, 0, 0.8);
opacity: 0;
pointer-events: none;
transition: opacity .25s, transform .25s;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
.topnav a:not(.active) { display: none; }
.controls { flex-direction: column; align-items: stretch; }
.sort { justify-content: space-between; }
.grid { grid-template-columns: 1fr; }
.hero h1 { font-size: 30px; }
}
@media (prefers-reduced-motion: reduce) {
* { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; }
}(function () {
"use strict";
// --- Data (fictional Waveform network) ---
var SHOWS = [
{ id: "s1", title: "Signal & Noise", host: "Priya Raman", category: "Technology", eps: 214, days: 2, g: ["#7c3aed", "#22d3ee"] },
{ id: "s2", title: "Cold Cases West", host: "Dana Whitlock", category: "True Crime", eps: 98, days: 1, g: ["#4c1d95", "#f472b6"] },
{ id: "s3", title: "Second Serving", host: "Marco Feliz", category: "Culture", eps: 156, days: 9, g: ["#0e7490", "#22d3ee"] },
{ id: "s4", title: "The Long Green", host: "Anaya Brooks", category: "Business", eps: 340, days: 4, g: ["#166534", "#22d3ee"] },
{ id: "s5", title: "Static Hours", host: "Leo Nakamura", category: "Music", eps: 72, days: 0, g: ["#831843", "#8b5cf6"] },
{ id: "s6", title: "Field Notes", host: "Imani Okafor", category: "Science", eps: 188, days: 6, g: ["#1e3a8a", "#22d3ee"] },
{ id: "s7", title: "Off the Clock", host: "Sam Delgado", category: "Comedy", eps: 261, days: 3, g: ["#9a3412", "#f472b6"] },
{ id: "s8", title: "Deep End", host: "Rowan Price", category: "Technology", eps: 45, days: 0, g: ["#5b21b6", "#22d3ee"] },
{ id: "s9", title: "Midnight Ledger", host: "Talia Voss", category: "True Crime", eps: 130, days: 12, g: ["#3b0764", "#f472b6"] },
{ id: "s10", title: "Small Batch", host: "Hugo Estévez", category: "Culture", eps: 84, days: 5, g: ["#155e75", "#8b5cf6"] },
{ id: "s11", title: "Ground Floor", host: "Nadia Karim", category: "Business", eps: 112, days: 8, g: ["#065f46", "#22d3ee"] },
{ id: "s12", title: "Wavelength", host: "Beckett Yu", category: "Science", eps: 203, days: 1, g: ["#1e40af", "#8b5cf6"] }
];
var NEW_DAYS = 3;
// --- State ---
var state = { q: "", category: "All", sort: "featured" };
var followed = {};
var currentPlaying = null;
// --- Elements ---
var $grid = document.getElementById("grid");
var $chips = document.getElementById("chips");
var $search = document.getElementById("search");
var $clearSearch = document.getElementById("clearSearch");
var $sort = document.getElementById("sort");
var $empty = document.getElementById("empty");
var $count = document.getElementById("resultCount");
var $resetInline = document.getElementById("resetInline");
var $toast = document.getElementById("toast");
// --- Toast helper ---
var toastTimer;
function toast(msg) {
$toast.textContent = msg;
$toast.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { $toast.classList.remove("show"); }, 2200);
}
function esc(s) {
return String(s).replace(/[&<>"]/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """ }[c];
});
}
function initials(title) {
return title.split(/\s+/).slice(0, 2).map(function (w) { return w[0]; }).join("").toUpperCase();
}
// --- Build category chips ---
function buildChips() {
var counts = { All: SHOWS.length };
SHOWS.forEach(function (s) { counts[s.category] = (counts[s.category] || 0) + 1; });
var cats = ["All"].concat(Object.keys(counts).filter(function (c) { return c !== "All"; }).sort());
$chips.innerHTML = cats.map(function (c) {
return '<button type="button" class="chip' + (c === state.category ? " active" : "") +
'" data-cat="' + esc(c) + '" aria-pressed="' + (c === state.category) + '">' +
esc(c) + '<span class="count">' + counts[c] + '</span></button>';
}).join("");
}
// --- Filtering + sorting ---
function getVisible() {
var q = state.q.trim().toLowerCase();
var list = SHOWS.filter(function (s) {
if (state.category !== "All" && s.category !== state.category) return false;
if (q && s.title.toLowerCase().indexOf(q) < 0 && s.host.toLowerCase().indexOf(q) < 0) return false;
return true;
});
var order = { featured: 0, newest: 0, episodes: 0, az: 0 };
if (order[state.sort] === undefined) state.sort = "featured";
if (state.sort === "newest") list.sort(function (a, b) { return a.days - b.days; });
else if (state.sort === "episodes") list.sort(function (a, b) { return b.eps - a.eps; });
else if (state.sort === "az") list.sort(function (a, b) { return a.title.localeCompare(b.title); });
return list;
}
function waveBars() {
var h = [55, 90, 40, 75, 60, 95, 35, 70, 50, 85, 45, 65];
return h.map(function (v) { return '<i style="height:' + v + '%"></i>'; }).join("");
}
function agoLabel(days) {
if (days === 0) return "today";
if (days === 1) return "1 day ago";
return days + " days ago";
}
function cardHTML(s) {
var isNew = s.days <= NEW_DAYS;
var isFollowed = !!followed[s.id];
return '<article class="card" data-id="' + s.id + '">' +
'<div class="cover" style="background:linear-gradient(140deg,' + s.g[0] + ',' + s.g[1] + ')">' +
'<span class="cover-mono" aria-hidden="true">' + esc(initials(s.title)) + '</span>' +
'<button class="play" type="button" data-play aria-label="Play latest episode of ' + esc(s.title) + '">' +
'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>' +
'</button>' +
'<div class="wave" aria-hidden="true">' + waveBars() + '</div>' +
'</div>' +
'<div class="card-body">' +
'<div class="card-top">' +
'<span class="badge">' + esc(s.category) + '</span>' +
(isNew ? '<span class="badge new">New</span>' : '') +
'</div>' +
'<h3>' + esc(s.title) + '</h3>' +
'<p class="host">with <b>' + esc(s.host) + '</b> · updated ' + agoLabel(s.days) + '</p>' +
'<div class="card-foot">' +
'<span class="eps">' + s.eps + ' episodes</span>' +
'<button class="follow' + (isFollowed ? " on" : "") + '" type="button" data-follow aria-pressed="' + isFollowed + '">' +
(isFollowed ? "Following" : "Follow") + '</button>' +
'</div>' +
'</div>' +
'</article>';
}
// --- Render ---
function render() {
var list = getVisible();
if (list.length === 0) {
$grid.innerHTML = "";
$grid.hidden = true;
$empty.hidden = false;
} else {
$grid.hidden = false;
$empty.hidden = true;
$grid.innerHTML = list.map(cardHTML).join("");
}
var filtered = state.q.trim() !== "" || state.category !== "All";
$count.innerHTML = "Showing <b>" + list.length + "</b> of " + SHOWS.length + " shows" +
(state.category !== "All" ? " in " + esc(state.category) : "");
$resetInline.hidden = !filtered;
$clearSearch.hidden = state.q === "";
}
// --- Reset ---
function reset() {
state.q = "";
state.category = "All";
$search.value = "";
buildChips();
render();
toast("Filters cleared");
}
// --- Events ---
$search.addEventListener("input", function () {
state.q = $search.value;
render();
});
$clearSearch.addEventListener("click", function () {
state.q = "";
$search.value = "";
$search.focus();
render();
});
$sort.addEventListener("change", function () {
state.sort = $sort.value;
render();
toast("Sorted by " + $sort.options[$sort.selectedIndex].text.toLowerCase());
});
$chips.addEventListener("click", function (e) {
var chip = e.target.closest(".chip");
if (!chip) return;
state.category = chip.getAttribute("data-cat");
buildChips();
render();
});
$resetInline.addEventListener("click", reset);
document.getElementById("resetEmpty").addEventListener("click", reset);
document.getElementById("submitShow").addEventListener("click", function () {
toast("Show submissions open in the fall — thanks for your interest!");
});
// Grid delegation: play + follow
$grid.addEventListener("click", function (e) {
var card = e.target.closest(".card");
if (!card) return;
var id = card.getAttribute("data-id");
var show = SHOWS.find(function (s) { return s.id === id; });
if (!show) return;
if (e.target.closest("[data-play]")) {
var btn = e.target.closest("[data-play]");
if (currentPlaying === id) {
card.classList.remove("playing");
btn.setAttribute("aria-label", "Play latest episode of " + show.title);
currentPlaying = null;
toast("Paused " + show.title);
} else {
var prev = $grid.querySelector(".card.playing");
if (prev) prev.classList.remove("playing");
card.classList.add("playing");
btn.setAttribute("aria-label", "Pause " + show.title);
currentPlaying = id;
toast("Now playing · " + show.title);
}
return;
}
if (e.target.closest("[data-follow]")) {
var fbtn = e.target.closest("[data-follow]");
followed[id] = !followed[id];
fbtn.classList.toggle("on", followed[id]);
fbtn.textContent = followed[id] ? "Following" : "Follow";
fbtn.setAttribute("aria-pressed", String(!!followed[id]));
toast(followed[id] ? "Following " + show.title : "Unfollowed " + show.title);
}
});
// --- Init ---
buildChips();
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Waveform — Podcast Show Hub</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="glow glow-a" aria-hidden="true"></div>
<div class="glow glow-b" aria-hidden="true"></div>
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<span></span><span></span><span></span><span></span><span></span>
</span>
<div class="brand-text">
<strong>Waveform</strong>
<small>Podcast Network</small>
</div>
</div>
<nav class="topnav" aria-label="Primary">
<a href="#" class="active">Shows</a>
<a href="#">Charts</a>
<a href="#">Live</a>
<button class="btn-ghost" type="button" id="submitShow">Submit a show</button>
</nav>
</header>
<main class="wrap">
<section class="hero">
<p class="eyebrow">Now streaming</p>
<h1>Find your next <span class="grad">obsession</span>.</h1>
<p class="lede">Browse the full Waveform roster — culture, tech, true crime and more. Search a title, tap a category, tune in.</p>
</section>
<section class="controls" aria-label="Filter shows">
<div class="search">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M21 21l-4.3-4.3M11 19a8 8 0 1 1 0-16 8 8 0 0 1 0 16Z"/></svg>
<input id="search" type="search" placeholder="Search shows or hosts…" aria-label="Search shows or hosts" autocomplete="off" />
<button class="clear-search" id="clearSearch" type="button" aria-label="Clear search" hidden>×</button>
</div>
<div class="sort">
<label for="sort">Sort</label>
<select id="sort" aria-label="Sort shows">
<option value="featured">Featured</option>
<option value="newest">Newest</option>
<option value="episodes">Most episodes</option>
<option value="az">A–Z</option>
</select>
</div>
</section>
<div class="chips" id="chips" role="group" aria-label="Filter by category"></div>
<div class="meta-row">
<p id="resultCount" class="result-count" aria-live="polite"></p>
<button class="reset-inline" id="resetInline" type="button" hidden>Reset filters</button>
</div>
<section class="grid" id="grid" aria-label="Podcast shows"></section>
<div class="empty" id="empty" hidden>
<div class="empty-icon" aria-hidden="true">🎧</div>
<h2>No shows match that</h2>
<p>Try a different search or clear your filters to see the whole network.</p>
<button class="btn-primary" id="resetEmpty" type="button">Reset filters</button>
</div>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>The Show Hub is a landing surface for a fictional podcast network, built around a responsive grid of cover cards. Each card pairs a gradient artwork block with the show title, host name, a category badge and an episode count, plus a glowing violet play button that pulses on hover. The header holds a live search field and a row of category chips, so listeners can jump straight to the shows they want without a page reload.
Interactions are all vanilla JS. Typing in the search box filters shows by title or host in real time; clicking a category chip toggles a filter and updates the active state; and a sort dropdown reorders the grid by newest, episode count or name. A result counter reflects the current view, and when nothing matches, a friendly empty state appears with a one-tap reset. Play and follow actions raise a small toast so the UI always confirms what happened.
The layout is dark-first and audio-forward: vibrant violet-to-cyan waveform accents, deep surface cards and accessible focus rings. It collapses gracefully to a single column below 520px, keeps controls keyboard-usable, and maintains WCAG AA contrast throughout so the hub reads clearly on any screen.