Streaming — Episode Picker
A cinematic, dark-first episode picker for a fictional original series. A compact billboard hero carries the show title, match score and quality badges, while a season dropdown and tabbed switcher swap a rich episode list rendered from data. Each row pairs a 16:9 thumbnail with a hover play affordance, duration chip and continue-watching progress bar against an episode number, title, air date, downloaded badge and an expandable synopsis. Now-playing rows are highlighted, and every interaction runs on dependency-free vanilla JS.
MCP
Code
:root {
--bg: #0b0b0f;
--surface: #15151c;
--surface-2: #1e1e27;
--ink: #f4f4f7;
--ink-2: #b6b7c3;
--muted: #83859a;
--brand: #16b8a3;
--accent: #ffffff;
--gold: #f5c451;
--line: rgba(255, 255, 255, 0.1);
--line-2: rgba(255, 255, 255, 0.16);
--r-sm: 8px;
--r-md: 12px;
--r-lg: 18px;
--shadow: 0 18px 50px rgba(0, 0, 0, 0.55);
--glow: 0 0 0 1px var(--line-2), 0 14px 40px rgba(22, 184, 163, 0.18);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background:
radial-gradient(1200px 600px at 80% -10%, rgba(22, 184, 163, 0.12), transparent 60%),
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;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0);
white-space: nowrap; border: 0;
}
/* ---------- topbar ---------- */
.topbar {
position: sticky;
top: 0;
z-index: 20;
display: flex;
align-items: center;
gap: 24px;
padding: 16px clamp(16px, 4vw, 48px);
background: linear-gradient(180deg, rgba(11, 11, 15, 0.92), rgba(11, 11, 15, 0));
backdrop-filter: blur(6px);
}
.brand {
display: inline-flex;
align-items: center;
gap: 9px;
font-weight: 800;
letter-spacing: 0.18em;
font-size: 18px;
color: var(--ink);
text-decoration: none;
}
.brand__mark {
width: 16px; height: 16px;
border-radius: 5px;
background: conic-gradient(from 140deg, var(--brand), #4be0cb, var(--brand));
box-shadow: 0 0 18px rgba(22, 184, 163, 0.6);
}
.topbar__nav {
display: flex;
gap: 22px;
margin-left: 8px;
}
.topbar__nav a {
color: var(--ink-2);
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: color 0.15s;
}
.topbar__nav a:hover { color: var(--ink); }
.topbar__nav a[aria-current="page"] { color: var(--ink); }
.topbar__avatar {
margin-left: auto;
width: 34px; height: 34px;
border-radius: 8px;
display: grid;
place-items: center;
font-weight: 700;
font-size: 14px;
background: linear-gradient(135deg, #7a5cff, var(--brand));
color: #0b0b0f;
}
/* ---------- layout ---------- */
.show {
max-width: 1160px;
margin: 0 auto;
padding: 8px clamp(16px, 4vw, 48px) 80px;
}
/* ---------- hero ---------- */
.hero {
display: grid;
grid-template-columns: 230px 1fr;
gap: 32px;
align-items: center;
padding: 28px 0 40px;
}
.hero__poster {
position: relative;
aspect-ratio: 2 / 3;
border-radius: var(--r-lg);
overflow: hidden;
background:
radial-gradient(120% 80% at 50% 0%, #1d3a3d, transparent 55%),
linear-gradient(160deg, #0e2a2e 0%, #0a1418 60%, #06090c 100%);
border: 1px solid var(--line);
box-shadow: var(--shadow);
display: grid;
place-items: center;
}
.hero__poster-glow {
position: absolute;
inset: -40% 30% auto -10%;
height: 70%;
background: radial-gradient(closest-side, rgba(22, 184, 163, 0.55), transparent);
filter: blur(8px);
}
.hero__poster-title {
position: relative;
font-weight: 800;
font-size: 26px;
letter-spacing: 0.06em;
line-height: 1.05;
text-align: center;
color: #eafffb;
text-shadow: 0 2px 24px rgba(0, 0, 0, 0.6);
}
.hero__eyebrow {
margin: 0 0 8px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--brand);
}
.hero__title {
margin: 0 0 14px;
font-size: clamp(28px, 5vw, 44px);
font-weight: 800;
letter-spacing: -0.02em;
line-height: 1.05;
}
.hero__badges {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
}
.badge {
font-size: 12px;
font-weight: 600;
padding: 3px 9px;
border-radius: 999px;
background: var(--surface-2);
border: 1px solid var(--line);
color: var(--ink-2);
}
.badge--match { color: #4be0cb; border-color: rgba(75, 224, 203, 0.4); }
.badge--age { color: var(--ink); }
.badge--q {
border-radius: 5px;
border-color: var(--line-2);
letter-spacing: 0.04em;
}
.hero__synopsis {
margin: 0 0 22px;
max-width: 60ch;
color: var(--ink-2);
font-size: 15px;
}
.hero__actions { display: flex; flex-wrap: wrap; gap: 12px; }
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
font: inherit;
font-weight: 600;
font-size: 15px;
padding: 11px 20px;
border-radius: var(--r-sm);
border: 1px solid transparent;
cursor: pointer;
transition: transform 0.12s, background 0.15s, border-color 0.15s, color 0.15s;
}
.btn:active { transform: scale(0.97); }
.btn--play { background: var(--accent); color: #0b0b0f; }
.btn--play:hover { background: #e7e7ee; }
.btn--ghost {
background: rgba(255, 255, 255, 0.06);
color: var(--ink);
border-color: var(--line-2);
}
.btn--ghost:hover { background: rgba(255, 255, 255, 0.12); }
.btn--ghost[aria-pressed="true"] {
border-color: var(--brand);
color: #4be0cb;
}
.btn--ghost[aria-pressed="true"] .btn__plus { transform: rotate(45deg); }
.btn__plus { transition: transform 0.2s; display: inline-block; font-size: 18px; line-height: 1; }
/* ---------- episodes header ---------- */
.episodes__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 14px;
}
.episodes__head h2 {
margin: 0;
font-size: 22px;
font-weight: 700;
letter-spacing: -0.01em;
}
.select-wrap { position: relative; }
.select-wrap select {
appearance: none;
-webkit-appearance: none;
font: inherit;
font-weight: 600;
font-size: 14px;
color: var(--ink);
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 9px 38px 9px 14px;
cursor: pointer;
}
.select-wrap select:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.select-wrap__chev {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
color: var(--ink-2);
pointer-events: none;
}
/* tabs */
.season-tabs {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin-bottom: 22px;
border-bottom: 1px solid var(--line);
padding-bottom: 2px;
}
.season-tab {
font: inherit;
font-weight: 600;
font-size: 14px;
color: var(--muted);
background: none;
border: 0;
border-bottom: 2px solid transparent;
padding: 8px 12px;
cursor: pointer;
transition: color 0.15s, border-color 0.15s;
}
.season-tab:hover { color: var(--ink-2); }
.season-tab[aria-selected="true"] {
color: var(--ink);
border-bottom-color: var(--brand);
}
.season-tab:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 4px; }
/* ---------- episode list ---------- */
.ep-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.ep {
display: grid;
grid-template-columns: 38px 168px 1fr;
gap: 18px;
align-items: start;
padding: 14px;
border-radius: var(--r-md);
border: 1px solid var(--line);
background: var(--surface);
cursor: pointer;
transition: background 0.15s, border-color 0.15s, transform 0.12s;
}
.ep:hover { background: var(--surface-2); border-color: var(--line-2); }
.ep:focus-within { border-color: var(--line-2); }
.ep.is-playing {
border-color: rgba(22, 184, 163, 0.5);
box-shadow: var(--glow);
background: linear-gradient(180deg, rgba(22, 184, 163, 0.08), var(--surface));
}
.ep__num {
font-size: 22px;
font-weight: 700;
color: var(--muted);
text-align: center;
padding-top: 28px;
}
.ep.is-playing .ep__num { color: var(--brand); }
.ep__thumb {
position: relative;
aspect-ratio: 16 / 9;
border-radius: var(--r-sm);
overflow: hidden;
background: var(--surface-2);
border: 1px solid var(--line);
}
.ep__thumb-art {
position: absolute;
inset: 0;
display: grid;
place-items: center;
font-weight: 800;
font-size: 13px;
letter-spacing: 0.12em;
color: rgba(255, 255, 255, 0.5);
}
.ep__play {
position: absolute;
inset: 0;
display: grid;
place-items: center;
background: rgba(0, 0, 0, 0.35);
opacity: 0;
transition: opacity 0.18s;
}
.ep:hover .ep__play, .ep.is-playing .ep__play { opacity: 1; }
.ep__play svg {
width: 34px; height: 34px;
color: #fff;
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.6));
}
.ep__dur {
position: absolute;
right: 6px; bottom: 6px;
font-size: 11px;
font-weight: 600;
padding: 2px 6px;
border-radius: 5px;
background: rgba(0, 0, 0, 0.7);
color: var(--ink);
}
.ep__progress {
position: absolute;
left: 0; right: 0; bottom: 0;
height: 4px;
background: rgba(255, 255, 255, 0.16);
}
.ep__progress span {
display: block;
height: 100%;
background: var(--brand);
border-radius: 0 2px 2px 0;
}
.ep__body { min-width: 0; }
.ep__topline {
display: flex;
align-items: baseline;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 4px;
}
.ep__title {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.ep.is-playing .ep__title { color: var(--brand); }
.ep__nowtag {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #0b0b0f;
background: var(--brand);
padding: 2px 7px;
border-radius: 999px;
}
.ep__chips { display: flex; gap: 8px; margin-left: auto; align-items: center; }
.chip-dl {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11px;
font-weight: 600;
color: #4be0cb;
}
.chip-dl svg { width: 14px; height: 14px; }
.ep__air { font-size: 12px; color: var(--muted); }
.ep__syn {
margin: 0;
color: var(--ink-2);
font-size: 14px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.ep.is-open .ep__syn { -webkit-line-clamp: unset; overflow: visible; }
.ep__more {
margin-top: 6px;
font: inherit;
font-size: 13px;
font-weight: 600;
color: var(--ink);
background: none;
border: 0;
padding: 0;
cursor: pointer;
}
.ep__more:hover { color: var(--brand); }
.ep__more:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 4px; }
/* ---------- toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 24px);
background: var(--surface-2);
border: 1px solid var(--line-2);
color: var(--ink);
font-size: 14px;
font-weight: 500;
padding: 11px 18px;
border-radius: var(--r-md);
box-shadow: var(--shadow);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s, transform 0.2s;
z-index: 40;
}
.toast.is-on { opacity: 1; transform: translate(-50%, 0); }
/* ---------- responsive ---------- */
@media (max-width: 760px) {
.hero { grid-template-columns: 130px 1fr; gap: 20px; align-items: start; }
.hero__poster-title { font-size: 17px; }
}
@media (max-width: 520px) {
.topbar__nav { display: none; }
.hero { grid-template-columns: 1fr; }
.hero__poster { max-width: 150px; }
.ep {
grid-template-columns: 1fr;
gap: 12px;
}
.ep__num { display: none; }
.ep__thumb { width: 100%; max-width: none; }
.ep__chips { width: 100%; margin-left: 0; }
.hero__actions .btn { flex: 1; justify-content: center; }
}(function () {
"use strict";
/* ---------- data ---------- */
var SEASONS = [
{
n: 1,
year: 2024,
label: "Season 1",
episodes: [
{ n: 1, t: "Carrier Wave", dur: "52m", air: "Mar 8, 2024", code: "S1E1", progress: 100, downloaded: true,
syn: "A decommissioned listening post in northern Norway transmits a forty-year-old distress call. Cryptographer Iris Calder is dragged out of early retirement to confirm what everyone hopes is a hoax." },
{ n: 2, t: "Static Bloom", dur: "49m", air: "Mar 8, 2024", code: "S1E2", progress: 100, downloaded: true,
syn: "Iris isolates a second layer buried inside the signal. The agency wants it buried with it. A storm strands the team on the island for the night." },
{ n: 3, t: "The Long Echo", dur: "55m", air: "Mar 15, 2024", code: "S1E3", progress: 0, downloaded: false,
syn: "An old colleague resurfaces with a tape that should not exist. Iris begins to suspect the voice on the wire is reading from her own case files." },
{ n: 4, t: "Dead Air", dur: "47m", air: "Mar 22, 2024", code: "S1E4", progress: 0, downloaded: false,
syn: "The investigation splinters as the team disagrees about who is really listening. A power cut leaves them deciphering by hand for thirty-six hours." }
]
},
{
n: 2,
year: 2025,
label: "Season 2",
episodes: [
{ n: 1, t: "Cold Open", dur: "58m", air: "Jan 10, 2025", code: "S2E1", progress: 100, downloaded: true,
syn: "One year on, Iris hears the same frequency leaking from a children's radio in Lisbon. What was supposed to be a single anomaly has started to spread across the map." },
{ n: 2, t: "Ground Loop", dur: "51m", air: "Jan 17, 2025", code: "S2E2", progress: 100, downloaded: false,
syn: "A rival analyst claims the signal is man-made and points the finger at Iris's own department. Trust on the team frays to a single thread." },
{ n: 3, t: "Sidebands", dur: "53m", air: "Jan 24, 2025", code: "S2E3", progress: 100, downloaded: true,
syn: "Decoding the Lisbon recording reveals coordinates that lead somewhere impossible. Iris makes a call she can never take back." },
{ n: 4, t: "The Quiet Hour", dur: "61m", air: "Jan 31, 2025", code: "S2E4", progress: 38, downloaded: true, nowPlaying: true,
syn: "Iris and Marlowe go off the books to reach the coordinates before the agency does. For sixty silent minutes, the only thing transmitting is them." },
{ n: 5, t: "Harmonics", dur: "49m", air: "Feb 7, 2025", code: "S2E5", progress: 0, downloaded: false,
syn: "The consequences of the quiet hour ripple outward. A familiar voice offers Iris a deal that sounds exactly like surrender." },
{ n: 6, t: "Null Point", dur: "57m", air: "Feb 14, 2025", code: "S2E6", progress: 0, downloaded: false,
syn: "Season finale. Everything Iris believed about the source of the signal collapses into a single, devastating frequency." }
]
},
{
n: 3,
year: 2026,
label: "Season 3",
episodes: [
{ n: 1, t: "Open Channel", dur: "54m", air: "Mar 6, 2026", code: "S3E1", progress: 0, downloaded: false,
syn: "A new season begins with Iris on the wrong side of an inquiry. The signal has gone silent — and its absence is somehow louder than it ever was." },
{ n: 2, t: "Phantom Power", dur: "50m", air: "Mar 13, 2026", code: "S3E2", progress: 0, downloaded: false,
syn: "A junior tech stumbles onto a pattern in the silence. Iris must decide whether to mentor her or warn her away from the wire entirely." },
{ n: 3, t: "Standing Wave", dur: "56m", air: "Mar 20, 2026", code: "S3E3", progress: 0, downloaded: false,
syn: "Old allies become liabilities as the inquiry closes in. The frequency returns, and this time it is asking for Iris by name." }
]
}
];
/* ---------- elements ---------- */
var select = document.getElementById("season");
var tabsEl = document.querySelector(".season-tabs");
var listEl = document.getElementById("ep-list");
var toastEl = document.getElementById("toast");
var defaultSeasonIndex = 1; // Season 2 (latest with in-progress)
var current = defaultSeasonIndex;
/* ---------- toast ---------- */
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-on");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-on");
}, 2200);
}
/* ---------- build season controls ---------- */
SEASONS.forEach(function (s, i) {
var opt = document.createElement("option");
opt.value = String(i);
opt.textContent = s.label + " · " + s.year;
select.appendChild(opt);
var tab = document.createElement("button");
tab.className = "season-tab";
tab.type = "button";
tab.setAttribute("role", "tab");
tab.dataset.index = String(i);
tab.textContent = s.label;
tabsEl.appendChild(tab);
});
var PLAY_ICON = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z" fill="currentColor"/></svg>';
var DL_ICON = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v10m0 0l-4-4m4 4l4-4M5 19h14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
/* ---------- render ---------- */
function render(index) {
current = index;
var season = SEASONS[index];
listEl.innerHTML = "";
season.episodes.forEach(function (ep) {
var li = document.createElement("li");
li.className = "ep";
if (ep.nowPlaying) li.classList.add("is-playing");
li.tabIndex = 0;
li.setAttribute("role", "button");
li.setAttribute("aria-label", "Play " + season.label + " episode " + ep.n + ", " + ep.t);
var nowTag = ep.nowPlaying ? '<span class="ep__nowtag">Now playing</span>' : "";
var dl = ep.downloaded
? '<span class="chip-dl">' + DL_ICON + "Downloaded</span>"
: "";
var prog = ep.progress > 0
? '<div class="ep__progress"><span style="width:' + ep.progress + '%"></span></div>'
: "";
li.innerHTML =
'<div class="ep__num">' + ep.n + "</div>" +
'<div class="ep__thumb">' +
'<div class="ep__thumb-art">' + ep.code + "</div>" +
'<div class="ep__play">' + PLAY_ICON + "</div>" +
'<span class="ep__dur">' + ep.dur + "</span>" +
prog +
"</div>" +
'<div class="ep__body">' +
'<div class="ep__topline">' +
'<h3 class="ep__title">' + ep.t + "</h3>" +
nowTag +
'<div class="ep__chips">' + dl + "</div>" +
"</div>" +
'<p class="ep__air">' + ep.air + "</p>" +
'<p class="ep__syn">' + ep.syn + "</p>" +
'<button class="ep__more" type="button" aria-expanded="false">Read more</button>' +
"</div>";
// expand synopsis
var more = li.querySelector(".ep__more");
more.addEventListener("click", function (e) {
e.stopPropagation();
var open = li.classList.toggle("is-open");
more.textContent = open ? "Read less" : "Read more";
more.setAttribute("aria-expanded", String(open));
});
// select / play episode
function pickEpisode() {
listEl.querySelectorAll(".ep").forEach(function (el) {
el.classList.remove("is-playing");
var tag = el.querySelector(".ep__nowtag");
if (tag) tag.remove();
});
li.classList.add("is-playing");
var topline = li.querySelector(".ep__topline");
if (!topline.querySelector(".ep__nowtag")) {
var tag = document.createElement("span");
tag.className = "ep__nowtag";
tag.textContent = "Now playing";
topline.querySelector(".ep__title").insertAdjacentElement("afterend", tag);
}
toast("Playing " + season.label.replace("Season ", "S") + " · E" + ep.n + " — " + ep.t);
}
li.addEventListener("click", pickEpisode);
li.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
pickEpisode();
}
});
listEl.appendChild(li);
});
// sync controls
select.value = String(index);
tabsEl.querySelectorAll(".season-tab").forEach(function (t) {
var on = Number(t.dataset.index) === index;
t.setAttribute("aria-selected", String(on));
t.tabIndex = on ? 0 : -1;
});
}
/* ---------- events ---------- */
select.addEventListener("change", function () {
render(Number(select.value));
});
tabsEl.addEventListener("click", function (e) {
var tab = e.target.closest(".season-tab");
if (!tab) return;
render(Number(tab.dataset.index));
});
// keyboard nav across tabs
tabsEl.addEventListener("keydown", function (e) {
if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
var next = current + (e.key === "ArrowRight" ? 1 : -1);
if (next < 0 || next >= SEASONS.length) return;
render(next);
tabsEl.querySelectorAll(".season-tab")[next].focus();
});
// hero actions
document.querySelectorAll("[data-action]").forEach(function (btn) {
btn.addEventListener("click", function () {
var action = btn.dataset.action;
if (action === "resume") {
render(1);
toast("Resuming The Hollow Signal — S2 · E4");
} else if (action === "mylist") {
var on = btn.getAttribute("aria-pressed") === "true";
btn.setAttribute("aria-pressed", String(!on));
toast(on ? "Removed from My List" : "Added to My List");
}
});
});
render(defaultSeasonIndex);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Streaming — Episode Picker</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>
<header class="topbar">
<a class="brand" href="#" aria-label="Lumen home">
<span class="brand__mark" aria-hidden="true"></span>
LUMEN
</a>
<nav class="topbar__nav" aria-label="Primary">
<a href="#" aria-current="page">Series</a>
<a href="#">Films</a>
<a href="#">My List</a>
</nav>
<div class="topbar__avatar" aria-hidden="true">A</div>
</header>
<main class="show" id="app">
<section class="hero" aria-labelledby="show-title">
<div class="hero__poster" aria-hidden="true">
<span class="hero__poster-glow"></span>
<span class="hero__poster-title">THE<br />HOLLOW<br />SIGNAL</span>
</div>
<div class="hero__meta">
<p class="hero__eyebrow">Lumen Original Series</p>
<h1 class="hero__title" id="show-title">The Hollow Signal</h1>
<div class="hero__badges">
<span class="badge badge--match">97% Match</span>
<span class="badge">2026</span>
<span class="badge badge--age">TV-MA</span>
<span class="badge badge--q">4K</span>
<span class="badge badge--q">HDR</span>
<span class="badge">3 Seasons</span>
</div>
<p class="hero__synopsis">
When a derelict listening station off the coast of Tromsø starts broadcasting
a voice that hasn't existed for forty years, a burned-out cryptographer is
pulled back into a case the agency swore was closed. A slow-burn thriller about
frequencies, grief, and the things we'd rather not hear answer back.
</p>
<div class="hero__actions">
<button class="btn btn--play" type="button" data-action="resume">
<svg viewBox="0 0 24 24" aria-hidden="true" width="20" height="20"><path d="M8 5v14l11-7z" fill="currentColor"/></svg>
Resume S2 · E4
</button>
<button class="btn btn--ghost" type="button" data-action="mylist" aria-pressed="false">
<span class="btn__plus" aria-hidden="true">+</span> My List
</button>
</div>
</div>
</section>
<section class="episodes" aria-labelledby="episodes-heading">
<div class="episodes__head">
<h2 id="episodes-heading">Episodes</h2>
<div class="season-select">
<label class="sr-only" for="season">Choose a season</label>
<div class="select-wrap">
<select id="season" aria-label="Choose a season"></select>
<svg class="select-wrap__chev" viewBox="0 0 24 24" aria-hidden="true" width="18" height="18"><path d="M7 10l5 5 5-5z" fill="currentColor"/></svg>
</div>
</div>
</div>
<div class="season-tabs" role="tablist" aria-label="Seasons"></div>
<ol class="ep-list" id="ep-list" aria-live="polite"></ol>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Episode Picker
An episode picker for the fictional Lumen original series The Hollow Signal. A compact billboard hero anchors the page with a poster, a 97% match score, rating and 4K/HDR quality badges, a synopsis and a Resume control. Below it, the episodes panel offers two synchronized ways to move between seasons: a styled season dropdown and a row of underline tabs, both defaulting to the latest in-progress season.
Each episode is rendered from data as a list row pairing a 16:9 thumbnail — with a hover-reveal play button, a duration chip and a continue-watching progress bar — against a large episode number, title, air date, an optional Downloaded badge and a two-line synopsis. The currently playing episode is highlighted with a glow, a Now playing pill and an accent-tinted number. A Read more control expands each synopsis in place.
Every interaction is dependency-free vanilla JS: switching seasons via the dropdown, tabs or arrow keys re-renders the list; clicking or pressing Enter on a row selects it, moves the now-playing highlight and fires a small toast() confirmation; the My List button toggles its pressed state; and synopses expand independently. The layout is responsive from desktop down to ~360px, stacking the thumbnail above the metadata and collapsing chrome on small screens.
Illustrative UI only — fictional titles, not a real streaming service.