Podcast — Latest Feed
A dark, audio-first feed of the latest podcast episodes rendered as tappable rows with cover art, show name, publish date, duration and a glowing gradient play button. New drops carry a pulsing badge, filter chips scope the list by show, and a mini now-playing bar with an animated waveform tracks whatever you press play on.
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(135deg, var(--violet) 0%, var(--cyan) 100%);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(1100px 480px at 82% -8%, rgba(139, 92, 246, 0.18), transparent 60%),
radial-gradient(900px 420px at 8% 0%, rgba(34, 211, 238, 0.12), 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;
padding: 40px 20px 140px;
}
.wrap {
max-width: 720px;
margin: 0 auto;
}
/* ---------- header ---------- */
.head-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 22px;
}
.brand { display: flex; align-items: center; gap: 14px; }
.brand-mark {
display: grid;
place-items: center;
width: 46px;
height: 46px;
border-radius: var(--r-md);
background: var(--grad);
color: var(--white);
box-shadow: 0 8px 24px rgba(139, 92, 246, 0.4);
flex: none;
}
h1 {
margin: 0;
font-size: 1.55rem;
font-weight: 800;
letter-spacing: -0.02em;
}
.sub { margin: 2px 0 0; color: var(--muted); font-size: 0.85rem; font-weight: 500; }
.sub #count { color: var(--cyan); font-weight: 700; }
.ghost-btn {
font: inherit;
font-size: 0.82rem;
font-weight: 600;
color: var(--ink);
background: var(--surface);
border: 1px solid var(--line);
border-radius: 999px;
padding: 9px 16px;
cursor: pointer;
transition: border-color .18s, background .18s, transform .12s;
white-space: nowrap;
}
.ghost-btn:hover { border-color: var(--line-2); background: var(--surface-2); }
.ghost-btn:active { transform: translateY(1px); }
/* ---------- chips ---------- */
.chips {
display: flex;
gap: 8px;
overflow-x: auto;
padding-bottom: 4px;
margin-bottom: 22px;
scrollbar-width: none;
}
.chips::-webkit-scrollbar { display: none; }
.chip {
font: inherit;
font-size: 0.82rem;
font-weight: 600;
color: var(--muted);
background: var(--surface);
border: 1px solid var(--line);
border-radius: 999px;
padding: 8px 15px;
cursor: pointer;
white-space: nowrap;
flex: none;
transition: color .18s, border-color .18s, background .18s;
}
.chip:hover { color: var(--ink); border-color: var(--line-2); }
.chip.is-active {
color: var(--white);
background: var(--surface-2);
border-color: transparent;
box-shadow: inset 0 0 0 1.5px transparent;
position: relative;
}
.chip.is-active::before {
content: "";
position: absolute;
inset: -1px;
border-radius: 999px;
padding: 1.5px;
background: var(--grad);
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}
/* ---------- feed ---------- */
.feed { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.row {
display: grid;
grid-template-columns: 60px 1fr auto;
align-items: center;
gap: 14px;
padding: 12px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
transition: border-color .2s, background .2s, transform .12s;
animation: rowIn .4s ease both;
}
.row:hover { border-color: var(--line-2); background: var(--surface-2); }
.row.is-playing { border-color: transparent; box-shadow: 0 0 0 1.5px var(--violet), 0 12px 30px rgba(139, 92, 246, 0.22); }
.row.is-played { opacity: 0.55; }
@keyframes rowIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.art {
width: 60px;
height: 60px;
border-radius: var(--r-md);
flex: none;
position: relative;
overflow: hidden;
display: grid;
place-items: center;
font-weight: 800;
font-size: 1.2rem;
color: rgba(255, 255, 255, 0.92);
text-shadow: 0 1px 6px rgba(0, 0, 0, 0.35);
}
.art::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg, transparent 40%, rgba(0, 0, 0, 0.28));
}
.info { min-width: 0; }
.eyebrow { display: flex; align-items: center; gap: 8px; margin-bottom: 3px; }
.show {
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--cyan);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.badge {
font-size: 0.62rem;
font-weight: 800;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--white);
background: var(--pink);
padding: 2px 7px;
border-radius: 999px;
flex: none;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(244, 114, 182, 0.5); }
50% { box-shadow: 0 0 0 5px rgba(244, 114, 182, 0); }
}
.title {
margin: 0;
font-size: 0.98rem;
font-weight: 700;
letter-spacing: -0.01em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.meta { margin: 4px 0 0; display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 0.78rem; }
.dot { width: 3px; height: 3px; border-radius: 50%; background: var(--muted); flex: none; }
.dur {
font-variant-numeric: tabular-nums;
font-weight: 600;
color: var(--ink);
background: rgba(255, 255, 255, 0.06);
border: 1px solid var(--line);
padding: 1px 8px;
border-radius: 999px;
font-size: 0.72rem;
}
.play {
width: 44px;
height: 44px;
border-radius: 50%;
flex: none;
display: grid;
place-items: center;
cursor: pointer;
color: var(--white);
background: var(--grad);
border: none;
box-shadow: 0 6px 18px rgba(139, 92, 246, 0.42);
transition: transform .14s, box-shadow .2s, filter .2s;
}
.play svg { transition: opacity .15s; }
.play .i-pause { display: none; }
.play:hover { transform: scale(1.08); box-shadow: 0 8px 26px rgba(34, 211, 238, 0.5); }
.play:active { transform: scale(0.96); }
.row.is-playing .play .i-play { display: none; }
.row.is-playing .play .i-pause { display: block; }
.empty { text-align: center; color: var(--muted); padding: 40px 0; font-weight: 500; }
/* ---------- focus ---------- */
:where(button):focus-visible {
outline: 2px solid var(--cyan);
outline-offset: 2px;
}
/* ---------- now playing bar ---------- */
.nowbar {
position: fixed;
left: 50%;
bottom: 18px;
transform: translateX(-50%);
width: min(680px, calc(100% - 32px));
display: grid;
grid-template-columns: 48px 1fr auto auto;
align-items: center;
gap: 14px;
padding: 12px 14px;
background: rgba(29, 29, 39, 0.86);
backdrop-filter: blur(16px);
border: 1px solid var(--line-2);
border-radius: var(--r-lg);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
z-index: 30;
animation: barIn .35s cubic-bezier(.2, .9, .3, 1.2) both;
}
@keyframes barIn { from { opacity: 0; transform: translate(-50%, 20px); } to { opacity: 1; transform: translate(-50%, 0); } }
.np-art { width: 48px; height: 48px; border-radius: var(--r-md); flex: none; }
.np-meta { min-width: 0; }
.np-title { margin: 0; font-weight: 700; font-size: 0.9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.np-show { margin: 2px 0 6px; color: var(--cyan); font-size: 0.72rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; }
.np-progress { height: 4px; border-radius: 999px; background: rgba(255, 255, 255, 0.12); overflow: hidden; }
.np-progress span { display: block; height: 100%; width: 0; background: var(--grad); border-radius: 999px; transition: width .25s linear; }
.waveform { display: flex; align-items: flex-end; gap: 3px; height: 30px; }
.waveform span {
width: 3px;
border-radius: 999px;
background: var(--grad);
height: 30%;
}
.nowbar.playing .waveform span { animation: bar .9s ease-in-out infinite; }
.waveform span:nth-child(1) { animation-delay: -.1s; }
.waveform span:nth-child(2) { animation-delay: -.4s; }
.waveform span:nth-child(3) { animation-delay: -.7s; }
.waveform span:nth-child(4) { animation-delay: -.2s; }
.waveform span:nth-child(5) { animation-delay: -.5s; }
.waveform span:nth-child(6) { animation-delay: -.8s; }
.waveform span:nth-child(7) { animation-delay: -.3s; }
@keyframes bar { 0%, 100% { height: 25%; } 50% { height: 100%; } }
.np-play {
width: 42px;
height: 42px;
border-radius: 50%;
border: none;
cursor: pointer;
color: var(--white);
background: var(--grad);
display: grid;
place-items: center;
flex: none;
box-shadow: 0 6px 18px rgba(139, 92, 246, 0.42);
transition: transform .14s;
}
.np-play:hover { transform: scale(1.08); }
.np-play:active { transform: scale(0.95); }
/* ---------- toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 96px;
transform: translate(-50%, 20px);
background: var(--surface-2);
color: var(--ink);
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 10px 18px;
font-size: 0.85rem;
font-weight: 600;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.45);
opacity: 0;
pointer-events: none;
transition: opacity .25s, transform .25s;
z-index: 40;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- responsive ---------- */
@media (max-width: 520px) {
body { padding: 26px 14px 150px; }
h1 { font-size: 1.3rem; }
.ghost-btn { padding: 8px 12px; font-size: 0.76rem; }
.row { grid-template-columns: 52px 1fr auto; gap: 11px; padding: 10px; }
.art { width: 52px; height: 52px; font-size: 1rem; }
.title { font-size: 0.9rem; }
.play { width: 40px; height: 40px; }
.nowbar { grid-template-columns: 44px 1fr auto; gap: 11px; bottom: 12px; }
.waveform { display: none; }
}(function () {
"use strict";
var EPISODES = [
{
id: "e1",
title: "The Quiet Collapse of Group Chats",
show: "Signal & Noise",
date: "2h ago",
duration: "42:18",
isNew: true,
grad: "linear-gradient(135deg,#8b5cf6,#22d3ee)",
initials: "SN"
},
{
id: "e2",
title: "Deep Work in a World That Won't Shut Up",
show: "Deep Focus",
date: "5h ago",
duration: "58:04",
isNew: true,
grad: "linear-gradient(135deg,#f472b6,#7c3aed)",
initials: "DF"
},
{
id: "e3",
title: "Field Recording: 3AM in the Server Room",
show: "Night Static",
date: "Yesterday",
duration: "31:47",
isNew: true,
grad: "linear-gradient(135deg,#22d3ee,#7c3aed)",
initials: "NS"
},
{
id: "e4",
title: "How We Almost Sold the Company Twice",
show: "Founders Raw",
date: "Yesterday",
duration: "1:12:09",
isNew: false,
grad: "linear-gradient(135deg,#7c3aed,#f472b6)",
initials: "FR"
},
{
id: "e5",
title: "Everything Is a Recommendation Engine Now",
show: "Signal & Noise",
date: "2 days ago",
duration: "47:33",
isNew: false,
grad: "linear-gradient(135deg,#8b5cf6,#f472b6)",
initials: "SN"
},
{
id: "e6",
title: "The 90-Minute Ritual That Rewired My Week",
show: "Deep Focus",
date: "3 days ago",
duration: "39:52",
isNew: false,
grad: "linear-gradient(135deg,#22d3ee,#8b5cf6)",
initials: "DF"
}
];
var feed = document.getElementById("feed");
var empty = document.getElementById("empty");
var countEl = document.getElementById("count");
var toastEl = document.getElementById("toast");
var chips = Array.prototype.slice.call(document.querySelectorAll(".chip"));
var nowbar = document.getElementById("nowbar");
var npArt = document.getElementById("npArt");
var npTitle = document.getElementById("npTitle");
var npShow = document.getElementById("npShow");
var npPlay = document.getElementById("npPlay");
var npFill = document.getElementById("npFill");
var activeFilter = "all";
var currentId = null;
var isPlaying = false;
var progress = 0;
var timer = null;
var played = Object.create(null);
var toastTimer = null;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2000);
}
function render() {
var list = EPISODES.filter(function (ep) {
return activeFilter === "all" || ep.show === activeFilter;
});
feed.innerHTML = "";
empty.hidden = list.length > 0;
var newCount = 0;
list.forEach(function (ep) {
if (ep.isNew) newCount++;
var li = document.createElement("li");
li.className = "row";
if (ep.id === currentId) li.classList.add("is-playing");
if (played[ep.id]) li.classList.add("is-played");
li.dataset.id = ep.id;
var badge = ep.isNew
? '<span class="badge">New</span>'
: "";
li.innerHTML =
'<div class="art" style="background:' + ep.grad + '">' + ep.initials + "</div>" +
'<div class="info">' +
'<div class="eyebrow"><span class="show">' + ep.show + "</span>" + badge + "</div>" +
'<h3 class="title">' + ep.title + "</h3>" +
'<p class="meta"><span>' + ep.date + '</span><span class="dot"></span><span class="dur">' + ep.duration + "</span></p>" +
"</div>" +
'<button class="play" type="button" aria-label="Play ' + ep.title.replace(/"/g, "") + '">' +
'<svg class="i-play" viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M8 5v14l11-7Z"/></svg>' +
'<svg class="i-pause" viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></svg>' +
"</button>";
li.querySelector(".play").addEventListener("click", function () {
onPlay(ep);
});
feed.appendChild(li);
});
countEl.textContent = String(newCount);
}
function getEp(id) {
for (var i = 0; i < EPISODES.length; i++) {
if (EPISODES[i].id === id) return EPISODES[i];
}
return null;
}
function onPlay(ep) {
if (ep.id === currentId) {
togglePlay();
return;
}
currentId = ep.id;
played[ep.id] = true;
progress = 0;
startPlayback(ep);
render();
}
function startPlayback(ep) {
nowbar.hidden = false;
npArt.style.background = ep.grad;
npTitle.textContent = ep.title;
npShow.textContent = ep.show;
setPlaying(true);
toast("Now playing · " + ep.show);
}
function setPlaying(state) {
isPlaying = state;
nowbar.classList.toggle("playing", state);
var iPlay = npPlay.querySelector(".i-play");
var iPause = npPlay.querySelector(".i-pause");
iPlay.hidden = state;
iPause.hidden = !state;
npPlay.setAttribute("aria-label", state ? "Pause" : "Play");
var row = feed.querySelector('.row[data-id="' + currentId + '"]');
if (row) row.classList.toggle("is-playing", state);
clearInterval(timer);
if (state) {
timer = setInterval(tick, 500);
}
}
function togglePlay() {
setPlaying(!isPlaying);
}
function tick() {
progress += 1.4;
if (progress >= 100) {
progress = 100;
npFill.style.width = "100%";
setPlaying(false);
toast("Episode finished");
return;
}
npFill.style.width = progress + "%";
}
npPlay.addEventListener("click", function () {
if (!currentId) return;
togglePlay();
});
chips.forEach(function (chip) {
chip.addEventListener("click", function () {
chips.forEach(function (c) {
c.classList.remove("is-active");
c.setAttribute("aria-pressed", "false");
});
chip.classList.add("is-active");
chip.setAttribute("aria-pressed", "true");
activeFilter = chip.dataset.show;
render();
});
});
document.getElementById("markBtn").addEventListener("click", function () {
EPISODES.forEach(function (ep) {
played[ep.id] = true;
ep.isNew = false;
});
render();
toast("Marked all as played");
});
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Podcast — Latest Feed</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="wrap">
<header class="head">
<div class="head-top">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2a7 7 0 0 0-7 7v4a7 7 0 0 0 14 0V9a7 7 0 0 0-7-7Z"/>
<path d="M12 19v3M8 22h8"/>
</svg>
</span>
<div>
<h1>Latest Episodes</h1>
<p class="sub"><span id="count">6</span> new drops this week</p>
</div>
</div>
<button class="ghost-btn" id="markBtn" type="button">Mark all played</button>
</div>
<nav class="chips" aria-label="Filter by show">
<button class="chip is-active" type="button" data-show="all" aria-pressed="true">All shows</button>
<button class="chip" type="button" data-show="Signal & Noise" aria-pressed="false">Signal & Noise</button>
<button class="chip" type="button" data-show="Deep Focus" aria-pressed="false">Deep Focus</button>
<button class="chip" type="button" data-show="Night Static" aria-pressed="false">Night Static</button>
<button class="chip" type="button" data-show="Founders Raw" aria-pressed="false">Founders Raw</button>
</nav>
</header>
<ul class="feed" id="feed" aria-label="Episode list"></ul>
<p class="empty" id="empty" hidden>No episodes from this show yet.</p>
</main>
<div class="nowbar" id="nowbar" aria-live="polite" hidden>
<div class="np-art" id="npArt" aria-hidden="true"></div>
<div class="np-meta">
<p class="np-title" id="npTitle">—</p>
<p class="np-show" id="npShow">—</p>
<div class="np-progress"><span id="npFill"></span></div>
</div>
<div class="waveform" id="wave" aria-hidden="true">
<span></span><span></span><span></span><span></span><span></span><span></span><span></span>
</div>
<button class="np-play" id="npPlay" type="button" aria-label="Pause">
<svg class="i-pause" viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></svg>
<svg class="i-play" viewBox="0 0 24 24" width="20" height="20" fill="currentColor" hidden><path d="M8 5v14l11-7Z"/></svg>
</button>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A latest-episodes feed built for audio-first products. Each row pairs a gradient cover tile with the episode title, its parent show, a relative publish date and a duration pill, plus a glowing violet-to-cyan play button that sits front and centre. Freshly published episodes are marked with a pulsing “New” badge so returning listeners can spot what dropped since their last visit.
Filter chips across the top scope the feed to a single show or back to everything, updating a live count as they toggle. Pressing play on any row promotes it to a sticky now-playing bar at the bottom, where an animated bar-graph waveform pulses in the accent gradient while a play/pause control and a scrubbing progress line stand in for real playback state.
Everything is vanilla HTML, CSS and JavaScript with no dependencies. Controls are real buttons with visible focus rings, aria-pressed state on toggles, and the layout collapses gracefully to a single-column, thumb-friendly list below 520px.