Podcast — Show Detail
A dark, audio-first podcast show page anchored by a glowing cover-art header with host byline, subscribe and follow controls, and animated listener stats. An expandable about section pairs with a rich episode list where each row carries a gradient waveform, a glowing play toggle, duration and date badges, and season filters. Platform link chips route to Apple, Spotify, and RSS, all wired with keyboard-friendly interactions and toast feedback.
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;
--grad: linear-gradient(90deg, var(--violet), var(--cyan));
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background:
radial-gradient(1100px 620px at 82% -8%, rgba(139, 92, 246, 0.16), transparent 60%),
radial-gradient(900px 520px at 5% 0%, rgba(34, 211, 238, 0.10), 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;
padding-bottom: 96px;
}
.page {
max-width: 940px;
margin: 0 auto;
padding: 40px 20px 20px;
}
h1, h2 { margin: 0; letter-spacing: -0.02em; }
/* ===== Header ===== */
.show {
display: grid;
grid-template-columns: 208px 1fr;
gap: 28px;
align-items: center;
}
.art {
position: relative;
width: 208px;
height: 208px;
border-radius: var(--r-lg);
background:
radial-gradient(120% 120% at 20% 15%, rgba(255, 255, 255, 0.20), transparent 45%),
linear-gradient(150deg, var(--violet-d), #3b1d6e 55%, #0e1b2e);
border: 1px solid var(--line-2);
box-shadow: 0 24px 60px rgba(124, 58, 237, 0.35), inset 0 0 0 1px rgba(255, 255, 255, 0.05);
display: grid;
place-items: center;
overflow: hidden;
}
.art-glyph {
font-size: 88px;
color: rgba(255, 255, 255, 0.92);
text-shadow: 0 6px 30px rgba(34, 211, 238, 0.5);
transform: translateY(-8px);
}
.art-eq {
position: absolute;
bottom: 22px;
display: flex;
gap: 6px;
align-items: flex-end;
height: 34px;
}
.art-eq i {
width: 6px;
border-radius: 3px;
background: var(--grad);
animation: eq 1.1s ease-in-out infinite;
}
.art-eq i:nth-child(1) { height: 40%; animation-delay: 0s; }
.art-eq i:nth-child(2) { height: 90%; animation-delay: .18s; }
.art-eq i:nth-child(3) { height: 60%; animation-delay: .36s; }
.art-eq i:nth-child(4) { height: 100%; animation-delay: .12s; }
.art-eq i:nth-child(5) { height: 50%; animation-delay: .28s; }
@keyframes eq { 0%, 100% { transform: scaleY(.4); } 50% { transform: scaleY(1); } }
.show-meta { min-width: 0; }
.badges { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
.badge {
font-size: 12px;
font-weight: 600;
padding: 5px 10px;
border-radius: 999px;
background: var(--surface-2);
border: 1px solid var(--line);
color: var(--muted);
}
.badge-live {
color: var(--ink);
border-color: rgba(139, 92, 246, 0.5);
background: rgba(139, 92, 246, 0.14);
display: inline-flex;
align-items: center;
gap: 6px;
}
.badge-live .dot {
width: 7px; height: 7px; border-radius: 50%;
background: var(--cyan);
box-shadow: 0 0 0 0 rgba(34, 211, 238, 0.6);
animation: pulse 1.8s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(34, 211, 238, 0.55); }
70% { box-shadow: 0 0 0 8px rgba(34, 211, 238, 0); }
100% { box-shadow: 0 0 0 0 rgba(34, 211, 238, 0); }
}
h1 { font-size: 40px; font-weight: 800; line-height: 1.05; }
.byline { margin: 8px 0 4px; color: var(--muted); font-size: 15px; }
.byline strong { color: var(--ink); font-weight: 600; }
.tagline { margin: 0 0 18px; color: var(--muted); max-width: 46ch; }
.stats {
list-style: none;
display: flex;
gap: 26px;
padding: 0;
margin: 0 0 20px;
}
.stats li { display: flex; flex-direction: column; }
.stat-num {
font-size: 22px;
font-weight: 800;
background: var(--grad);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-variant-numeric: tabular-nums;
}
.stat-label { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; }
.actions { display: flex; flex-wrap: wrap; gap: 10px; }
.btn {
font: inherit;
font-weight: 600;
cursor: pointer;
border-radius: 999px;
padding: 11px 20px;
border: 1px solid var(--line-2);
background: var(--surface-2);
color: var(--ink);
display: inline-flex;
align-items: center;
gap: 8px;
transition: transform .12s ease, box-shadow .2s ease, background .2s ease, border-color .2s ease;
}
.btn:hover { transform: translateY(-2px); }
.btn:active { transform: translateY(0); }
.btn-ico { font-size: 15px; line-height: 1; }
.btn-primary {
border: none;
background: var(--grad);
color: #0b0b12;
font-weight: 700;
box-shadow: 0 10px 30px rgba(139, 92, 246, 0.4);
}
.btn-primary:hover { box-shadow: 0 14px 40px rgba(34, 211, 238, 0.45); }
.btn-primary.is-on {
background: var(--surface-2);
color: var(--ink);
box-shadow: none;
border: 1px solid rgba(34, 211, 238, 0.55);
}
.btn-ghost.is-on {
color: var(--pink);
border-color: rgba(244, 114, 182, 0.5);
background: rgba(244, 114, 182, 0.12);
}
.btn-icon { padding: 11px 15px; }
:focus-visible {
outline: 2px solid var(--cyan);
outline-offset: 2px;
}
/* ===== About ===== */
.about {
margin-top: 34px;
padding: 24px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
}
.about h2 { font-size: 18px; margin-bottom: 12px; }
.about-body { color: var(--muted); font-size: 15px; }
.about-body p { margin: 0 0 12px; }
.about-body p:last-child { margin-bottom: 0; }
.about-body.is-clamped { max-height: 3.2em; overflow: hidden; -webkit-mask-image: linear-gradient(#000 40%, transparent); mask-image: linear-gradient(#000 40%, transparent); }
.link-btn {
margin-top: 4px;
background: none;
border: none;
color: var(--cyan);
font: inherit;
font-weight: 600;
cursor: pointer;
padding: 4px 0;
}
.link-btn:hover { text-decoration: underline; }
.platforms { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 20px; }
.chip {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 9px 15px;
border-radius: 999px;
background: var(--surface-2);
border: 1px solid var(--line);
color: var(--ink);
text-decoration: none;
font-size: 14px;
font-weight: 600;
transition: transform .12s ease, border-color .2s ease, background .2s ease;
}
.chip:hover { transform: translateY(-2px); border-color: var(--line-2); background: #23232f; }
.chip-ico {
width: 16px; height: 16px; border-radius: 4px;
background: var(--grad);
flex: none;
}
/* ===== Episodes ===== */
.episodes { margin-top: 34px; }
.eps-head {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 14px;
margin-bottom: 18px;
}
.eps-head h2 { font-size: 22px; }
.eps-controls { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; }
.filters { display: flex; gap: 6px; flex-wrap: wrap; }
.pill {
font: inherit;
font-size: 13px;
font-weight: 600;
cursor: pointer;
padding: 7px 13px;
border-radius: 999px;
border: 1px solid var(--line);
background: var(--surface);
color: var(--muted);
transition: color .18s ease, border-color .18s ease, background .18s ease;
}
.pill:hover { color: var(--ink); border-color: var(--line-2); }
.pill.is-active {
color: #0b0b12;
background: var(--grad);
border-color: transparent;
}
.pill-sort { color: var(--ink); }
.ep-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; }
.ep {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 16px;
align-items: center;
padding: 16px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
transition: border-color .2s ease, transform .15s ease, background .2s ease;
}
.ep:hover { border-color: var(--line-2); background: #191922; transform: translateY(-1px); }
.ep.is-playing { border-color: rgba(139, 92, 246, 0.6); box-shadow: 0 0 0 1px rgba(139, 92, 246, 0.3), 0 12px 30px rgba(124, 58, 237, 0.22); }
.play {
position: relative;
width: 52px; height: 52px;
border-radius: 50%;
border: none;
cursor: pointer;
flex: none;
background: var(--surface-2);
color: var(--ink);
font-size: 16px;
display: grid;
place-items: center;
transition: transform .12s ease, box-shadow .25s ease, background .2s ease;
}
.play::before { content: "▶"; margin-left: 2px; }
.play:hover { transform: scale(1.06); }
.ep.is-playing .play {
background: var(--grad);
color: #0b0b12;
box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.18), 0 8px 24px rgba(34, 211, 238, 0.45);
}
.ep.is-playing .play::before { content: "❚❚"; margin-left: 0; letter-spacing: -1px; }
.ep-body { min-width: 0; }
.ep-season { font-size: 11px; font-weight: 700; color: var(--violet); text-transform: uppercase; letter-spacing: .07em; }
.ep-title {
margin: 3px 0 4px;
font-size: 16px;
font-weight: 700;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ep-teaser {
margin: 0 0 10px;
font-size: 13.5px;
color: var(--muted);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.wave { display: flex; align-items: flex-end; gap: 3px; height: 22px; margin-bottom: 10px; }
.wave i {
width: 3px;
border-radius: 2px;
background: linear-gradient(180deg, var(--violet), var(--cyan));
opacity: .55;
transition: opacity .2s ease;
}
.ep.is-playing .wave i { opacity: 1; animation: bar 1s ease-in-out infinite; }
@keyframes bar { 0%, 100% { transform: scaleY(.5); } 50% { transform: scaleY(1.15); } }
.ep-meta { display: flex; flex-wrap: wrap; gap: 8px; }
.tag {
font-size: 11.5px;
font-weight: 600;
padding: 3px 9px;
border-radius: 999px;
background: var(--surface-2);
border: 1px solid var(--line);
color: var(--muted);
}
.ep-side { display: flex; flex-direction: column; align-items: flex-end; gap: 10px; }
.ep-dur { font-size: 13px; font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }
.ep-save {
background: none;
border: 1px solid var(--line);
color: var(--muted);
width: 34px; height: 34px;
border-radius: 50%;
cursor: pointer;
font-size: 14px;
transition: color .2s ease, border-color .2s ease, transform .12s ease;
}
.ep-save:hover { transform: scale(1.08); border-color: var(--line-2); }
.ep-save.is-saved { color: var(--pink); border-color: rgba(244, 114, 182, 0.5); }
.ep-empty { text-align: center; color: var(--muted); padding: 40px 0; }
/* ===== Now playing ===== */
.now-playing {
position: fixed;
left: 50%;
bottom: 18px;
transform: translateX(-50%);
width: min(760px, calc(100% - 32px));
display: flex;
align-items: center;
gap: 14px;
padding: 12px 16px;
background: rgba(29, 29, 39, 0.85);
backdrop-filter: blur(14px);
border: 1px solid var(--line-2);
border-radius: var(--r-lg);
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.5);
z-index: 40;
animation: rise .3s ease;
}
@keyframes rise { from { transform: translate(-50%, 20px); opacity: 0; } }
.np-play {
width: 44px; height: 44px;
border-radius: 50%;
border: none;
cursor: pointer;
flex: none;
background: var(--grad);
color: #0b0b12;
font-size: 14px;
box-shadow: 0 6px 20px rgba(139, 92, 246, 0.5);
}
.np-info { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.np-title { font-weight: 700; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.np-sub { font-size: 12px; color: var(--muted); }
.np-wave { display: flex; align-items: center; gap: 3px; height: 26px; }
.np-wave i {
width: 3px; height: 100%;
border-radius: 2px;
background: var(--grad);
animation: bar 1s ease-in-out infinite;
}
.np-wave i:nth-child(odd) { animation-delay: .2s; }
.np-wave i:nth-child(3n) { animation-delay: .4s; }
.now-playing.is-paused .np-wave i { animation-play-state: paused; }
.np-close {
background: none; border: none; color: var(--muted);
cursor: pointer; font-size: 15px; padding: 6px;
}
.np-close:hover { color: var(--ink); }
/* ===== Toast ===== */
.toast {
position: fixed;
bottom: 96px;
left: 50%;
transform: translate(-50%, 20px);
background: var(--surface-2);
border: 1px solid var(--line-2);
color: var(--ink);
padding: 11px 18px;
border-radius: 999px;
font-size: 14px;
font-weight: 600;
opacity: 0;
pointer-events: none;
transition: opacity .25s ease, transform .25s ease;
z-index: 60;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.4);
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ===== Responsive ===== */
@media (max-width: 720px) {
.show { grid-template-columns: 1fr; text-align: left; }
.art { width: 148px; height: 148px; }
.art-glyph { font-size: 62px; }
h1 { font-size: 30px; }
}
@media (max-width: 520px) {
.page { padding: 26px 14px 16px; }
.stats { gap: 18px; }
.stat-num { font-size: 19px; }
.ep { grid-template-columns: auto 1fr; }
.ep-side { flex-direction: row; align-items: center; grid-column: 1 / -1; justify-content: space-between; }
.play { width: 46px; height: 46px; }
.actions { width: 100%; }
.btn-primary, .btn-ghost { flex: 1; justify-content: center; }
.np-wave { display: none; }
.now-playing { bottom: 10px; }
}
@media (prefers-reduced-motion: reduce) {
* { animation: none !important; transition: none !important; }
}"use strict";
/* ---------- Data ---------- */
const EPISODES = [
{
id: 248, season: 3, title: "The Sound of Almost Human",
teaser: "Neural voice engineer Priya Nandakumar on why the last 2% of realism is the hardest — and the ethics of crossing it.",
duration: "58:12", mins: 58, date: "Jun 26, 2026",
},
{
id: 247, season: 3, title: "Rooms That Don't Exist",
teaser: "Building convincing spatial audio for places nobody has ever stood in, with acoustics researcher Theo Marsh.",
duration: "44:05", mins: 44, date: "Jun 19, 2026",
},
{
id: 246, season: 3, title: "A Synth in Every Pocket",
teaser: "How generative music models went from lab curiosity to shipping in mobile apps, featuring founder Lena Ortiz.",
duration: "51:37", mins: 51, date: "Jun 12, 2026",
},
{
id: 231, season: 2, title: "The Loudness Wars, Revisited",
teaser: "Mastering engineer Kwame Bediako on why everything sounds the same — and the quiet rebellion pushing back.",
duration: "39:48", mins: 39, date: "Feb 20, 2026",
},
{
id: 230, season: 2, title: "Latency Is a Feeling",
teaser: "Why 12 milliseconds decides whether a musician trusts the software in their hands. With DSP hacker Sun-woo Park.",
duration: "47:19", mins: 47, date: "Feb 13, 2026",
},
{
id: 212, season: 1, title: "Where Signal Meets Static",
teaser: "The pilot. Mara and Dev on why they left their day jobs to make a show about sound, software, and the messy middle.",
duration: "33:02", mins: 33, date: "Sep 05, 2025",
},
];
/* ---------- Toast ---------- */
let toastTimer;
const toastEl = document.getElementById("toast");
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(() => toastEl.classList.remove("show"), 2200);
}
/* ---------- Waveform generator ---------- */
function waveBars(seed) {
let s = seed;
const rnd = () => {
s = (s * 9301 + 49297) % 233280;
return s / 233280;
};
let html = "";
for (let i = 0; i < 40; i++) {
const h = 20 + Math.round(rnd() * 80);
html += `<i style="height:${h}%"></i>`;
}
return html;
}
/* ---------- Render episodes ---------- */
const listEl = document.getElementById("epList");
const emptyEl = document.getElementById("epEmpty");
let currentSeason = "all";
let order = "newest";
let playingId = null;
function visibleEpisodes() {
let eps = EPISODES.filter((e) => currentSeason === "all" || String(e.season) === currentSeason);
eps = eps.slice().sort((a, b) => (order === "newest" ? b.id - a.id : a.id - b.id));
return eps;
}
function render() {
const eps = visibleEpisodes();
listEl.innerHTML = "";
emptyEl.hidden = eps.length > 0;
eps.forEach((ep) => {
const li = document.createElement("li");
li.className = "ep" + (ep.id === playingId ? " is-playing" : "");
li.dataset.id = ep.id;
li.innerHTML = `
<button class="play" data-play="${ep.id}" aria-label="Play ${escapeHtml(ep.title)}" aria-pressed="${ep.id === playingId}"></button>
<div class="ep-body">
<span class="ep-season">S${ep.season} · Ep ${ep.id}</span>
<h3 class="ep-title">${escapeHtml(ep.title)}</h3>
<p class="ep-teaser">${escapeHtml(ep.teaser)}</p>
<div class="wave" aria-hidden="true">${waveBars(ep.id)}</div>
<div class="ep-meta">
<span class="tag">🎧 ${ep.date}</span>
<span class="tag">Full episode</span>
</div>
</div>
<div class="ep-side">
<span class="ep-dur">${ep.duration}</span>
<button class="ep-save" data-save="${ep.id}" aria-label="Save ${escapeHtml(ep.title)} for later" aria-pressed="false">+</button>
</div>`;
listEl.appendChild(li);
});
}
function escapeHtml(str) {
return str.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
}
/* ---------- Playback ---------- */
const nowPlaying = document.getElementById("nowPlaying");
const npTitle = document.getElementById("npTitle");
const npToggle = document.getElementById("npToggle");
let paused = false;
function playEpisode(id) {
const ep = EPISODES.find((e) => e.id === id);
if (!ep) return;
if (playingId === id && !paused) {
// toggle pause
paused = true;
setPausedState(true);
toast("Paused");
} else {
playingId = id;
paused = false;
setPausedState(false);
npTitle.textContent = ep.title;
nowPlaying.hidden = false;
toast(`Now playing · ${ep.title}`);
}
render();
syncPlayIcons();
}
function setPausedState(isPaused) {
paused = isPaused;
nowPlaying.classList.toggle("is-paused", isPaused);
npToggle.textContent = isPaused ? "▶" : "❚❚";
npToggle.setAttribute("aria-label", isPaused ? "Play" : "Pause");
// reflect pause on active row
document.querySelectorAll(".ep.is-playing").forEach((el) => {
el.classList.toggle("is-paused", isPaused);
});
}
function syncPlayIcons() {
document.querySelectorAll(".ep").forEach((el) => {
const isActive = Number(el.dataset.id) === playingId && !paused;
el.classList.toggle("is-playing", Number(el.dataset.id) === playingId);
});
}
listEl.addEventListener("click", (e) => {
const playBtn = e.target.closest("[data-play]");
if (playBtn) {
playEpisode(Number(playBtn.dataset.play));
return;
}
const saveBtn = e.target.closest("[data-save]");
if (saveBtn) {
const on = saveBtn.classList.toggle("is-saved");
saveBtn.textContent = on ? "✓" : "+";
saveBtn.setAttribute("aria-pressed", String(on));
toast(on ? "Saved to your queue" : "Removed from queue");
}
});
npToggle.addEventListener("click", () => {
if (!playingId) return;
setPausedState(!paused);
render();
toast(paused ? "Paused" : "Playing");
});
document.getElementById("npClose").addEventListener("click", () => {
nowPlaying.hidden = true;
playingId = null;
paused = false;
render();
toast("Player closed");
});
/* ---------- Filters & sort ---------- */
document.querySelector(".filters").addEventListener("click", (e) => {
const btn = e.target.closest(".pill");
if (!btn) return;
document.querySelectorAll(".filters .pill").forEach((p) => p.classList.remove("is-active"));
btn.classList.add("is-active");
currentSeason = btn.dataset.season;
render();
});
const sortBtn = document.getElementById("sortBtn");
sortBtn.addEventListener("click", () => {
order = order === "newest" ? "oldest" : "newest";
sortBtn.dataset.order = order;
sortBtn.textContent = order === "newest" ? "Newest ↓" : "Oldest ↑";
render();
});
/* ---------- Subscribe / follow / share ---------- */
const subBtn = document.getElementById("subscribeBtn");
subBtn.addEventListener("click", () => {
const on = subBtn.classList.toggle("is-on");
subBtn.setAttribute("aria-pressed", String(on));
subBtn.querySelector(".btn-label").textContent = on ? "Subscribed" : "Subscribe";
subBtn.querySelector(".btn-ico").textContent = on ? "✓" : "+";
toast(on ? "Subscribed to Signal & Static" : "Unsubscribed");
});
const followBtn = document.getElementById("followBtn");
followBtn.addEventListener("click", () => {
const on = followBtn.classList.toggle("is-on");
followBtn.setAttribute("aria-pressed", String(on));
followBtn.querySelector(".btn-ico").textContent = on ? "♥" : "♡";
followBtn.querySelector(".btn-label").textContent = on ? "Following" : "Follow";
toast(on ? "You'll get new episode alerts" : "Unfollowed");
});
document.getElementById("shareBtn").addEventListener("click", () => {
toast("Share link copied to clipboard");
});
/* ---------- Platform chips ---------- */
document.querySelectorAll(".chip").forEach((chip) => {
chip.addEventListener("click", (e) => {
e.preventDefault();
toast(`Opening in ${chip.dataset.platform}…`);
});
});
/* ---------- About toggle ---------- */
const aboutBody = document.getElementById("aboutBody");
const aboutToggle = document.getElementById("aboutToggle");
aboutToggle.addEventListener("click", () => {
const clamped = aboutBody.classList.toggle("is-clamped");
aboutToggle.setAttribute("aria-expanded", String(!clamped));
aboutToggle.textContent = clamped ? "Read more" : "Read less";
});
/* ---------- Animated stat counters ---------- */
function formatNum(el, val) {
const decimals = Number(el.dataset.decimals || 0);
const suffix = el.dataset.suffix || "";
if (el.dataset.format === "compact") {
if (val >= 1_000_000) return (val / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M" + suffix;
if (val >= 1000) return Math.round(val / 1000) + "K" + suffix;
return Math.round(val) + suffix;
}
return val.toLocaleString("en-US", { minimumFractionDigits: decimals, maximumFractionDigits: decimals }) + suffix;
}
function animateCounters() {
document.querySelectorAll(".stat-num").forEach((el) => {
const target = Number(el.dataset.count);
const dur = 1100;
const start = performance.now();
function step(now) {
const p = Math.min((now - start) / dur, 1);
const eased = 1 - Math.pow(1 - p, 3);
el.textContent = formatNum(el, target * eased);
if (p < 1) requestAnimationFrame(step);
else el.textContent = formatNum(el, target);
}
requestAnimationFrame(step);
});
}
/* ---------- Init ---------- */
render();
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
document.querySelectorAll(".stat-num").forEach((el) => (el.textContent = formatNum(el, Number(el.dataset.count))));
} else {
animateCounters();
}<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Signal & Static — Podcast Show</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>
<main class="page">
<!-- ===== Show header ===== -->
<header class="show" aria-labelledby="show-title">
<div class="art" role="img" aria-label="Signal and Static cover art">
<span class="art-glyph">◐</span>
<span class="art-eq" aria-hidden="true">
<i></i><i></i><i></i><i></i><i></i>
</span>
</div>
<div class="show-meta">
<div class="badges">
<span class="badge badge-live"><span class="dot"></span> New episode</span>
<span class="badge">Technology</span>
<span class="badge">Interview</span>
</div>
<h1 id="show-title">Signal & Static</h1>
<p class="byline">Hosted by <strong>Mara Voss</strong> & <strong>Dev Okafor</strong></p>
<p class="tagline">Weekly conversations at the edge of sound, software, and the people building both.</p>
<ul class="stats" aria-label="Show statistics">
<li><span class="stat-num" data-count="248">0</span><span class="stat-label">Episodes</span></li>
<li><span class="stat-num" data-count="1900000" data-suffix="+" data-format="compact">0</span><span class="stat-label">Listeners</span></li>
<li><span class="stat-num" data-count="4.9" data-decimals="1">0</span><span class="stat-label">Rating</span></li>
</ul>
<div class="actions">
<button id="subscribeBtn" class="btn btn-primary" aria-pressed="false">
<span class="btn-ico" aria-hidden="true">+</span>
<span class="btn-label">Subscribe</span>
</button>
<button id="followBtn" class="btn btn-ghost" aria-pressed="false">
<span class="btn-ico" aria-hidden="true">♡</span>
<span class="btn-label">Follow</span>
</button>
<button id="shareBtn" class="btn btn-icon" aria-label="Share show">↗</button>
</div>
</div>
</header>
<!-- ===== About ===== -->
<section class="about" aria-labelledby="about-h">
<h2 id="about-h">About the show</h2>
<div id="aboutBody" class="about-body is-clamped">
<p>Every Thursday, <strong>Signal & Static</strong> pulls apart one idea from the frontier of audio
technology — from spatial sound and generative music to the tiny engineering decisions that make a
voice feel human. Mara and Dev sit down with the researchers, founders, and tinkerers actually
shipping this work.</p>
<p>Expect long-form, unhurried conversations: no hype cycles, no press-release readouts. Just curious
people asking better questions. Recorded live in Lisbon and streamed to a community of nearly two
million weekly listeners across the globe.</p>
</div>
<button id="aboutToggle" class="link-btn" aria-expanded="false" aria-controls="aboutBody">Read more</button>
<div class="platforms" aria-label="Listen on other platforms">
<a class="chip" href="#" data-platform="Apple Podcasts"><span class="chip-ico"></span> Apple Podcasts</a>
<a class="chip" href="#" data-platform="Spotify"><span class="chip-ico"></span> Spotify</a>
<a class="chip" href="#" data-platform="Overcast"><span class="chip-ico"></span> Overcast</a>
<a class="chip" href="#" data-platform="RSS feed"><span class="chip-ico"></span> RSS</a>
</div>
</section>
<!-- ===== Episodes ===== -->
<section class="episodes" aria-labelledby="eps-h">
<div class="eps-head">
<h2 id="eps-h">Episodes</h2>
<div class="eps-controls">
<div class="filters" role="group" aria-label="Filter by season">
<button class="pill is-active" data-season="all">All</button>
<button class="pill" data-season="3">Season 3</button>
<button class="pill" data-season="2">Season 2</button>
<button class="pill" data-season="1">Season 1</button>
</div>
<button id="sortBtn" class="pill pill-sort" data-order="newest">Newest ↓</button>
</div>
</div>
<ol id="epList" class="ep-list" aria-label="Episode list"></ol>
<p id="epEmpty" class="ep-empty" hidden>No episodes in this season yet.</p>
</section>
</main>
<!-- ===== Now playing bar ===== -->
<div id="nowPlaying" class="now-playing" hidden aria-live="polite">
<button id="npToggle" class="np-play" aria-label="Pause">❚❚</button>
<div class="np-info">
<span class="np-title" id="npTitle">—</span>
<span class="np-sub" id="npSub">Signal & Static</span>
</div>
<div class="np-wave" aria-hidden="true">
<i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i>
</div>
<button id="npClose" class="np-close" aria-label="Close player">✕</button>
</div>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A complete show detail page for a fictional podcast, Signal & Static, built as a single self-contained static UI. The header stacks glowing cover art beside the show title, host byline, category badges, and a pair of primary actions — a Subscribe button that toggles state and a Follow button — while animated listener and rating stats count up on load. Below, an About panel expands and collapses to reveal the full description, and a row of platform chips links out to Apple Podcasts, Spotify, and an RSS feed.
The core of the page is the episode list. Each episode row shows a numbered season badge, title, teaser copy, a gradient violet-to-cyan waveform, and duration and publish-date badges. A glowing circular play button toggles between play and pause states, animating the waveform bars and surfacing a now-playing bar pinned to the bottom. Season filter pills narrow the visible list, and a sort control flips between newest and oldest ordering.
All interactions are vanilla JS: play toggling, subscribe state, about expansion, filtering, sorting, and a small toast helper for feedback. Controls are keyboard-usable with visible focus rings, the layout collapses cleanly to a single column below 520px, and colours meet WCAG AA contrast against the near-black background.