Podcast — Search Episodes
An audio-first episode finder with a live search box, show, duration and date filters that narrow a dark card list in real time. Results update instantly with a running count, animated waveform accents and glowing play buttons, plus a graceful empty state and a one-tap reset. Fully keyboard-navigable, responsive down to 360px and built with vanilla JS — no dependencies, no build step, ready to drop into any podcast site.
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;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(1100px 600px at 80% -10%, rgba(139, 92, 246, 0.18), transparent 60%),
radial-gradient(900px 500px at -10% 10%, rgba(34, 211, 238, 0.12), transparent 55%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
.wrap {
max-width: 860px;
margin: 0 auto;
padding: clamp(20px, 4vw, 40px) clamp(16px, 4vw, 28px) 64px;
}
/* ---- Masthead ---- */
.masthead {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 24px;
}
.brand { display: flex; align-items: center; gap: 14px; }
.brand-mark {
display: inline-flex;
align-items: flex-end;
gap: 3px;
width: 44px;
height: 44px;
padding: 9px;
border-radius: var(--r-md);
background: linear-gradient(135deg, var(--violet-d), var(--cyan));
box-shadow: 0 8px 24px rgba(139, 92, 246, 0.4);
}
.brand-mark .bar {
flex: 1;
border-radius: 2px;
background: rgba(255, 255, 255, 0.92);
animation: eq 1.1s ease-in-out infinite;
}
.brand-mark .bar:nth-child(1) { height: 40%; animation-delay: -0.2s; }
.brand-mark .bar:nth-child(2) { height: 90%; animation-delay: -0.5s; }
.brand-mark .bar:nth-child(3) { height: 60%; animation-delay: -0.1s; }
.brand-mark .bar:nth-child(4) { height: 75%; animation-delay: -0.35s; }
@keyframes eq {
0%, 100% { transform: scaleY(0.5); }
50% { transform: scaleY(1); }
}
.brand h1 {
margin: 0;
font-size: 1.5rem;
font-weight: 800;
letter-spacing: -0.02em;
}
.tagline { margin: 2px 0 0; font-size: 0.8rem; color: var(--muted); }
.live-dot {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
padding: 7px 12px;
border: 1px solid var(--line);
border-radius: 999px;
background: var(--surface);
}
.live-dot i {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--pink);
box-shadow: 0 0 0 0 rgba(244, 114, 182, 0.6);
animation: pulse 1.8s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(244, 114, 182, 0.55); }
70% { box-shadow: 0 0 0 7px rgba(244, 114, 182, 0); }
100% { box-shadow: 0 0 0 0 rgba(244, 114, 182, 0); }
}
/* ---- Search panel ---- */
.search-panel {
background: linear-gradient(180deg, var(--surface-2), var(--surface));
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 16px;
box-shadow: 0 20px 50px -30px rgba(0, 0, 0, 0.9);
}
.search-field {
position: relative;
display: flex;
align-items: center;
}
.search-icon {
position: absolute;
left: 15px;
width: 20px;
height: 20px;
fill: var(--muted);
pointer-events: none;
}
#search {
width: 100%;
padding: 14px 44px 14px 44px;
font: inherit;
font-size: 1rem;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--line-2);
border-radius: var(--r-md);
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
#search::placeholder { color: var(--muted); }
#search:focus {
border-color: var(--violet);
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.3);
}
#search::-webkit-search-decoration,
#search::-webkit-search-cancel-button { -webkit-appearance: none; }
.clear-search {
position: absolute;
right: 10px;
width: 26px;
height: 26px;
display: grid;
place-items: center;
font-size: 1.2rem;
line-height: 1;
color: var(--muted);
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: 50%;
cursor: pointer;
transition: color 0.15s, background 0.15s;
}
.clear-search:hover { color: var(--ink); background: var(--violet-d); }
.clear-search:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; }
.filters {
display: flex;
gap: 10px;
margin-top: 14px;
flex-wrap: wrap;
}
.ctrl {
display: flex;
flex-direction: column;
gap: 5px;
flex: 1;
min-width: 130px;
}
.ctrl > span {
font-size: 0.68rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--muted);
padding-left: 2px;
}
.ctrl select {
font: inherit;
font-size: 0.9rem;
color: var(--ink);
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 10px 12px;
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%239a9aab'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
padding-right: 30px;
transition: border-color 0.2s, box-shadow 0.2s;
}
.ctrl select:hover { border-color: var(--line-2); }
.ctrl select:focus-visible {
outline: none;
border-color: var(--violet);
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.3);
}
.reset-btn {
align-self: flex-end;
font: inherit;
font-size: 0.85rem;
font-weight: 600;
color: var(--ink);
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 10px 16px;
cursor: pointer;
transition: background 0.15s, border-color 0.15s, transform 0.1s;
}
.reset-btn:hover { background: var(--surface-2); border-color: var(--violet); }
.reset-btn:active { transform: translateY(1px); }
.reset-btn:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; }
.reset-btn.solid {
background: linear-gradient(135deg, var(--violet-d), var(--violet));
border-color: transparent;
color: var(--white);
box-shadow: 0 8px 22px -8px rgba(139, 92, 246, 0.7);
}
.reset-btn.solid:hover { filter: brightness(1.08); }
/* ---- Results head ---- */
.results-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin: 26px 4px 14px;
}
.results-head h2 { margin: 0; font-size: 0.95rem; font-weight: 600; color: var(--muted); }
.results-head h2 strong { color: var(--ink); font-weight: 700; }
.sort { display: flex; align-items: center; gap: 8px; }
.sort label { font-size: 0.75rem; color: var(--muted); }
.sort select {
font: inherit;
font-size: 0.82rem;
color: var(--ink);
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 7px 10px;
cursor: pointer;
}
.sort select:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; }
/* ---- Results ---- */
.results { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
.card {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
transition: transform 0.18s ease, border-color 0.18s ease, background 0.18s ease;
animation: rise 0.35s ease both;
}
@keyframes rise {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.card:hover {
transform: translateY(-2px);
border-color: var(--line-2);
background: var(--surface-2);
}
.play {
flex: none;
width: 52px;
height: 52px;
display: grid;
place-items: center;
border-radius: 50%;
border: none;
cursor: pointer;
color: var(--white);
background: linear-gradient(135deg, var(--violet-d), var(--violet));
box-shadow: 0 8px 22px -6px rgba(139, 92, 246, 0.75);
transition: transform 0.15s, box-shadow 0.2s, filter 0.15s;
}
.play svg { width: 20px; height: 20px; fill: currentColor; }
.play:hover { transform: scale(1.07); filter: brightness(1.1); }
.play:active { transform: scale(0.96); }
.play:focus-visible { outline: 2px solid var(--cyan); outline-offset: 3px; }
.play.playing {
background: linear-gradient(135deg, var(--cyan), var(--violet));
box-shadow: 0 0 0 0 rgba(34, 211, 238, 0.5);
animation: glow 1.6s infinite;
}
@keyframes glow {
0% { box-shadow: 0 0 0 0 rgba(34, 211, 238, 0.5); }
70% { box-shadow: 0 0 0 10px rgba(34, 211, 238, 0); }
100% { box-shadow: 0 0 0 0 rgba(34, 211, 238, 0); }
}
.card-body { flex: 1; min-width: 0; }
.card-title {
margin: 0;
font-size: 1rem;
font-weight: 700;
letter-spacing: -0.01em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-title mark {
background: rgba(139, 92, 246, 0.32);
color: var(--ink);
border-radius: 3px;
padding: 0 1px;
}
.card-meta {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
margin-top: 8px;
}
.badge {
font-size: 0.7rem;
font-weight: 600;
padding: 3px 9px;
border-radius: 999px;
border: 1px solid var(--line-2);
color: var(--ink);
background: var(--surface-2);
}
.badge.show { border-color: rgba(139, 92, 246, 0.5); color: #c9b6ff; }
.badge.dur { border-color: rgba(34, 211, 238, 0.45); color: #a5eef8; }
.card-meta .dot { color: var(--muted); font-size: 0.7rem; }
.card-meta time { font-size: 0.76rem; color: var(--muted); }
.wave {
flex: none;
display: none;
align-items: flex-end;
gap: 2px;
width: 46px;
height: 28px;
}
.wave span {
flex: 1;
border-radius: 2px;
background: linear-gradient(180deg, var(--cyan), var(--violet));
animation: eq 0.9s ease-in-out infinite;
}
.wave span:nth-child(2) { animation-delay: -0.2s; }
.wave span:nth-child(3) { animation-delay: -0.45s; }
.wave span:nth-child(4) { animation-delay: -0.1s; }
.wave span:nth-child(5) { animation-delay: -0.3s; }
.card.active .wave { display: flex; }
/* ---- Empty state ---- */
.empty {
text-align: center;
padding: 48px 20px;
border: 1px dashed var(--line-2);
border-radius: var(--r-lg);
background: var(--surface);
margin-top: 4px;
}
.empty-glyph {
display: inline-flex;
align-items: flex-end;
gap: 4px;
height: 40px;
margin-bottom: 14px;
opacity: 0.55;
}
.empty-glyph .bar {
width: 5px;
border-radius: 3px;
background: linear-gradient(180deg, var(--violet), var(--cyan));
}
.empty-glyph .bar:nth-child(1) { height: 40%; }
.empty-glyph .bar:nth-child(2) { height: 75%; }
.empty-glyph .bar:nth-child(3) { height: 100%; }
.empty-glyph .bar:nth-child(4) { height: 60%; }
.empty-glyph .bar:nth-child(5) { height: 30%; }
.empty h3 { margin: 0 0 6px; font-size: 1.15rem; font-weight: 700; }
.empty p { margin: 0 0 18px; color: var(--muted); font-size: 0.9rem; }
/* ---- Toast ---- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 20px);
background: var(--surface-2);
color: var(--ink);
border: 1px solid var(--line-2);
padding: 11px 18px;
border-radius: 999px;
font-size: 0.85rem;
font-weight: 500;
box-shadow: 0 16px 40px -12px rgba(0, 0, 0, 0.8);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s, transform 0.25s;
z-index: 40;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ---- Responsive ---- */
@media (max-width: 520px) {
.masthead { flex-direction: column; align-items: flex-start; gap: 12px; }
.filters { flex-direction: column; }
.ctrl { min-width: 0; }
.reset-btn { align-self: stretch; text-align: center; }
.results-head { flex-direction: column; align-items: flex-start; gap: 10px; }
.card { gap: 12px; padding: 14px; }
.play { width: 46px; height: 46px; }
.card-title { white-space: normal; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; }
}(function () {
"use strict";
// ---- Fictional but realistic episode catalog ----
var DAY = 86400000;
var now = Date.now();
var EPISODES = [
{ id: 1, title: "The Quiet Economics of Attention", show: "Signal & Noise", guest: "Dr. Maya Okonkwo", minutes: 47, days: 3 },
{ id: 2, title: "Mapping the Deep Ocean Floor", show: "Deep Field", guest: "Ravi Chandra", minutes: 62, days: 9 },
{ id: 3, title: "Why We Sleep Badly in Cities", show: "Night Frequencies", guest: "Lena Vasquez", minutes: 21, days: 14 },
{ id: 4, title: "A Director's Cut, Twenty Years Later", show: "The Long Cut", guest: "Theo Marsh", minutes: 74, days: 20 },
{ id: 5, title: "Signals from the Edge of Data", show: "Signal & Noise", guest: "Priya Nair", minutes: 38, days: 27 },
{ id: 6, title: "Fieldwork in the Atacama Desert", show: "Deep Field", guest: "Sofia Bianchi", minutes: 55, days: 41 },
{ id: 7, title: "Insomnia and the Midnight Hour", show: "Night Frequencies", guest: "Jonah Reed", minutes: 18, days: 58 },
{ id: 8, title: "Editing the Unfilmable Novel", show: "The Long Cut", guest: "Amara Diallo", minutes: 68, days: 74 },
{ id: 9, title: "The Noise Floor of Modern Markets", show: "Signal & Noise", guest: "Dr. Maya Okonkwo", minutes: 44, days: 96 },
{ id: 10, title: "Glaciers, Memory and Slow Change", show: "Deep Field", guest: "Ingrid Halvorsen", minutes: 51, days: 120 },
{ id: 11, title: "Dreams We Can Almost Remember", show: "Night Frequencies", guest: "Lena Vasquez", minutes: 24, days: 155 },
{ id: 12, title: "The Three-Hour Interview, Uncut", show: "The Long Cut", guest: "Theo Marsh", minutes: 92, days: 210 },
{ id: 13, title: "Reading the Silence Between Words", show: "Signal & Noise", guest: "Kwame Boateng", minutes: 33, days: 260 },
{ id: 14, title: "Beneath the Antarctic Ice Shelf", show: "Deep Field", guest: "Ravi Chandra", minutes: 59, days: 330 },
{ id: 15, title: "Late-Night Radio, A History", show: "Night Frequencies", guest: "Amara Diallo", minutes: 46, days: 400 }
];
// ---- Elements ----
var $ = function (s) { return document.querySelector(s); };
var searchEl = $("#search");
var clearBtn = $("#clearSearch");
var showEl = $("#filterShow");
var durEl = $("#filterDuration");
var dateEl = $("#filterDate");
var sortEl = $("#sortBy");
var resultsEl = $("#results");
var countEl = $("#resultCount");
var emptyEl = $("#empty");
var toastEl = $("#toast");
var playingId = null;
var toastTimer;
// ---- Helpers ----
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2200);
}
function fmtDuration(m) {
if (m < 60) return m + " min";
var h = Math.floor(m / 60);
var rem = m % 60;
return h + "h" + (rem ? " " + rem + "m" : "");
}
function durationBucket(m) {
if (m < 25) return "short";
if (m <= 50) return "medium";
return "long";
}
function relDate(days) {
if (days === 0) return "Today";
if (days === 1) return "Yesterday";
if (days < 7) return days + " days ago";
if (days < 30) return Math.round(days / 7) + " weeks ago";
if (days < 365) return Math.round(days / 30) + " months ago";
return Math.round(days / 365) + " year" + (days >= 730 ? "s" : "") + " ago";
}
function escapeHtml(s) {
return s.replace(/[&<>"]/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """ }[c];
});
}
function highlight(text, term) {
var safe = escapeHtml(text);
if (!term) return safe;
var idx = text.toLowerCase().indexOf(term);
if (idx === -1) return safe;
var esc = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return safe.replace(new RegExp("(" + esc + ")", "ig"), "<mark>$1</mark>");
}
// ---- Core filter ----
function currentFilters() {
return {
q: searchEl.value.trim().toLowerCase(),
show: showEl.value,
dur: durEl.value,
date: dateEl.value,
sort: sortEl.value
};
}
function matches(ep, f) {
if (f.q) {
var hay = (ep.title + " " + ep.show + " " + ep.guest).toLowerCase();
if (hay.indexOf(f.q) === -1) return false;
}
if (f.show && ep.show !== f.show) return false;
if (f.dur && durationBucket(ep.minutes) !== f.dur) return false;
if (f.date && ep.days > parseInt(f.date, 10)) return false;
return true;
}
function sortList(list, mode) {
var copy = list.slice();
copy.sort(function (a, b) {
switch (mode) {
case "oldest": return b.days - a.days;
case "longest": return b.minutes - a.minutes;
case "shortest": return a.minutes - b.minutes;
default: return a.days - b.days; // newest
}
});
return copy;
}
function cardHtml(ep, term) {
var active = ep.id === playingId;
return (
'<li class="card' + (active ? " active" : "") + '" data-id="' + ep.id + '">' +
'<button class="play' + (active ? " playing" : "") + '" type="button" ' +
'aria-pressed="' + active + '" aria-label="' + (active ? "Pause" : "Play") + " " + escapeHtml(ep.title) + '">' +
(active
? '<svg viewBox="0 0 24 24"><path d="M6 5h4v14H6zM14 5h4v14h-4z"/></svg>'
: '<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>') +
"</button>" +
'<div class="card-body">' +
'<h3 class="card-title">' + highlight(ep.title, term) + "</h3>" +
'<div class="card-meta">' +
'<span class="badge show">' + escapeHtml(ep.show) + "</span>" +
'<span class="badge dur">' + fmtDuration(ep.minutes) + "</span>" +
'<span class="dot">·</span>' +
"<time>" + relDate(ep.days) + "</time>" +
'<span class="dot">·</span>' +
'<span class="card-meta-guest" style="font-size:.76rem;color:var(--muted)">' + highlight(ep.guest, term) + "</span>" +
"</div>" +
"</div>" +
'<span class="wave" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span></span>' +
"</li>"
);
}
function render() {
var f = currentFilters();
clearBtn.hidden = !searchEl.value;
var list = EPISODES.filter(function (ep) { return matches(ep, f); });
list = sortList(list, f.sort);
if (list.length === 0) {
resultsEl.innerHTML = "";
resultsEl.hidden = true;
emptyEl.hidden = false;
countEl.innerHTML = "No matching episodes";
return;
}
emptyEl.hidden = true;
resultsEl.hidden = false;
countEl.innerHTML = "<strong>" + list.length + "</strong> of " + EPISODES.length + " episode" + (list.length === 1 ? "" : "s");
resultsEl.innerHTML = list.map(function (ep) { return cardHtml(ep, f.q); }).join("");
}
// ---- Events ----
var debounce;
searchEl.addEventListener("input", function () {
clearTimeout(debounce);
debounce = setTimeout(render, 90);
});
clearBtn.addEventListener("click", function () {
searchEl.value = "";
searchEl.focus();
render();
});
[showEl, durEl, dateEl, sortEl].forEach(function (el) {
el.addEventListener("change", render);
});
function resetAll() {
searchEl.value = "";
showEl.value = "";
durEl.value = "";
dateEl.value = "";
sortEl.value = "newest";
playingId = null;
render();
toast("Filters reset");
}
$("#resetTop").addEventListener("click", resetAll);
$("#resetEmpty").addEventListener("click", resetAll);
// Play / pause via event delegation
resultsEl.addEventListener("click", function (e) {
var btn = e.target.closest(".play");
if (!btn) return;
var li = btn.closest(".card");
var id = parseInt(li.getAttribute("data-id"), 10);
var ep = EPISODES.filter(function (x) { return x.id === id; })[0];
if (playingId === id) {
playingId = null;
toast("Paused");
} else {
playingId = id;
toast("Now playing · " + ep.title);
}
render();
});
// Initial paint
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Podcast — Search Episodes</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="masthead">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<span class="bar"></span><span class="bar"></span><span class="bar"></span><span class="bar"></span>
</span>
<div>
<h1>Frequency</h1>
<p class="tagline">Search the archive · 248 episodes</p>
</div>
</div>
<span class="live-dot" aria-hidden="true"><i></i> On air</span>
</header>
<section class="search-panel" aria-label="Search and filter episodes">
<div class="search-field">
<svg class="search-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M10 4a6 6 0 104.47 10.03l4.25 4.25 1.41-1.41-4.25-4.25A6 6 0 0010 4zm0 2a4 4 0 110 8 4 4 0 010-8z"/></svg>
<input id="search" type="search" autocomplete="off" placeholder="Search titles, shows or guests…" aria-label="Search episodes" />
<button id="clearSearch" class="clear-search" type="button" aria-label="Clear search" hidden>×</button>
</div>
<div class="filters">
<label class="ctrl">
<span>Show</span>
<select id="filterShow" aria-label="Filter by show">
<option value="">All shows</option>
<option value="Signal & Noise">Signal & Noise</option>
<option value="Deep Field">Deep Field</option>
<option value="The Long Cut">The Long Cut</option>
<option value="Night Frequencies">Night Frequencies</option>
</select>
</label>
<label class="ctrl">
<span>Duration</span>
<select id="filterDuration" aria-label="Filter by duration">
<option value="">Any length</option>
<option value="short">Under 25 min</option>
<option value="medium">25–50 min</option>
<option value="long">Over 50 min</option>
</select>
</label>
<label class="ctrl">
<span>Published</span>
<select id="filterDate" aria-label="Filter by publish date">
<option value="">Any time</option>
<option value="30">Last 30 days</option>
<option value="90">Last 3 months</option>
<option value="365">Last year</option>
</select>
</label>
<button id="resetTop" class="reset-btn" type="button">Reset</button>
</div>
</section>
<div class="results-head">
<h2 id="resultCount" aria-live="polite">Loading…</h2>
<div class="sort">
<label for="sortBy">Sort</label>
<select id="sortBy" aria-label="Sort episodes">
<option value="newest">Newest first</option>
<option value="oldest">Oldest first</option>
<option value="longest">Longest</option>
<option value="shortest">Shortest</option>
</select>
</div>
</div>
<ul id="results" class="results" aria-label="Episode results"></ul>
<div id="empty" class="empty" hidden>
<span class="empty-glyph" aria-hidden="true">
<span class="bar"></span><span class="bar"></span><span class="bar"></span><span class="bar"></span><span class="bar"></span>
</span>
<h3>No episodes found</h3>
<p>Try a different search term or loosen your filters.</p>
<button id="resetEmpty" class="reset-btn solid" type="button">Reset all filters</button>
</div>
</main>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A focused, audio-first search experience for browsing a podcast back catalog. A prominent search field filters episodes by title, show and guest as you type, while a row of controls lets you scope results to a specific show, a duration bucket (short, medium or long) and a publish window. Every keystroke and filter change re-runs the query, updates a live result count in the results header, and animates the matching cards into view.
Each result is a dark card with a gradient waveform accent, a glowing violet play button, show and duration badges, and a relative publish date. Hover lifts the card and brightens its border; the play button pulses and toggles between play and pause states with an inline toast confirming the action. When no episodes match, a dedicated empty state appears with a clear reset button that restores every filter at once.
The whole component is keyboard-usable with visible focus rings, uses aria-live to announce result counts to screen readers, and collapses its filter row into a single column below 520px. It ships as three plain files — no framework, no build tooling — so it can be embedded directly into any static or server-rendered podcast site.