Streaming — Title Detail
A cinematic, dark-first streaming title-detail page for a fictional sci-fi series. A full-bleed backdrop hero carries the title, match score, rating and quality badges alongside play, my-list and like controls, plus an expandable synopsis. Below it, season tabs swap a rich episode list with hover previews and per-episode list toggles, an expandable cast grid reveals the full crew, and a scrollable more-like-this row surfaces poster recommendations. Built with semantic HTML, CSS variables and vanilla JS.
MCP
Code
:root {
--bg: #0b0b0f;
--surface: #15151c;
--surface-2: #1e1e27;
--ink: #f4f4f7;
--ink-2: #b6b7c3;
--muted: #83859a;
--brand: #6c5ce7; /* override: violet for Nightfall Atlas */
--brand-2: #00d4ff;
--accent: #ffffff;
--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 12px 36px rgba(108, 92, 231, 0.32);
--nav-h: 64px;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
img { display: block; max-width: 100%; }
ol, ul { list-style: none; margin: 0; padding: 0; }
button { font: inherit; cursor: pointer; }
.skip-link {
position: absolute;
left: -999px;
top: 8px;
z-index: 200;
background: var(--brand);
color: #fff;
padding: 10px 16px;
border-radius: var(--r-sm);
}
.skip-link:focus { left: 12px; }
:focus-visible {
outline: 2px solid var(--brand-2);
outline-offset: 2px;
border-radius: var(--r-sm);
}
/* ---------------- TOP NAV ---------------- */
.topnav {
position: fixed;
inset: 0 0 auto 0;
height: var(--nav-h);
z-index: 100;
background: linear-gradient(180deg, rgba(11, 11, 15, 0.92) 0%, rgba(11, 11, 15, 0) 100%);
transition: background 0.3s ease;
}
.topnav.is-solid {
background: rgba(11, 11, 15, 0.96);
border-bottom: 1px solid var(--line);
backdrop-filter: blur(10px);
}
.topnav__inner {
height: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 clamp(16px, 4vw, 44px);
display: flex;
align-items: center;
gap: 28px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 9px;
text-decoration: none;
color: var(--ink);
font-weight: 800;
letter-spacing: 0.16em;
font-size: 15px;
}
.brand__mark {
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 8px;
background: linear-gradient(135deg, var(--brand), var(--brand-2));
color: #fff;
font-weight: 800;
}
.topnav__links {
display: flex;
gap: 22px;
margin-right: auto;
}
.topnav__links a {
color: var(--ink-2);
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: color 0.18s ease;
}
.topnav__links a:hover { color: var(--ink); }
.avatar {
width: 36px;
height: 36px;
border-radius: var(--r-sm);
border: 1px solid var(--line-2);
background: var(--surface-2);
color: var(--ink);
font-weight: 700;
font-size: 12px;
}
/* ---------------- HERO ---------------- */
.hero {
position: relative;
min-height: 88vh;
display: flex;
align-items: flex-end;
overflow: hidden;
}
.hero__backdrop {
position: absolute;
inset: 0;
background:
radial-gradient(120% 90% at 78% 18%, rgba(108, 92, 231, 0.5), transparent 60%),
radial-gradient(90% 80% at 92% 80%, rgba(0, 212, 255, 0.28), transparent 55%),
linear-gradient(115deg, #11101d 0%, #1a1230 42%, #06141f 100%);
}
.hero__backdrop::after {
content: "";
position: absolute;
inset: 0;
background-image:
radial-gradient(1.4px 1.4px at 20% 30%, rgba(255,255,255,0.7), transparent),
radial-gradient(1.2px 1.2px at 65% 22%, rgba(255,255,255,0.5), transparent),
radial-gradient(1.6px 1.6px at 82% 55%, rgba(255,255,255,0.65), transparent),
radial-gradient(1.1px 1.1px at 40% 70%, rgba(255,255,255,0.4), transparent),
radial-gradient(1.3px 1.3px at 88% 38%, rgba(255,255,255,0.55), transparent);
opacity: 0.8;
}
.hero__scrim {
position: absolute;
inset: 0;
background:
linear-gradient(180deg, rgba(11,11,15,0.2) 0%, rgba(11,11,15,0) 30%, rgba(11,11,15,0.85) 82%, var(--bg) 100%),
linear-gradient(90deg, rgba(11,11,15,0.9) 0%, rgba(11,11,15,0.4) 45%, transparent 75%);
}
.hero__content {
position: relative;
z-index: 2;
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 clamp(16px, 4vw, 44px) clamp(40px, 7vw, 76px);
}
.hero__kicker {
margin: 0 0 8px;
color: var(--brand-2);
font-weight: 700;
font-size: 12px;
letter-spacing: 0.28em;
text-transform: uppercase;
}
.hero__title {
margin: 0 0 16px;
font-size: clamp(2.6rem, 8vw, 5.4rem);
font-weight: 800;
line-height: 1.02;
letter-spacing: -0.02em;
max-width: 14ch;
text-shadow: 0 6px 30px rgba(0, 0, 0, 0.6);
}
.hero__meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
.meta-item {
color: var(--ink-2);
font-size: 14px;
font-weight: 600;
}
.badge {
display: inline-flex;
align-items: center;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
padding: 3px 8px;
border-radius: 6px;
border: 1px solid var(--line-2);
color: var(--ink-2);
}
.badge--match { color: #3ddc84; border-color: rgba(61, 220, 132, 0.4); }
.badge--rating { color: var(--ink); }
.badge--quality { color: var(--ink); background: rgba(255,255,255,0.06); }
.hero__tags {
margin: 0 0 16px;
color: var(--ink-2);
font-size: 14px;
font-weight: 500;
}
.hero__synopsis {
margin: 0;
max-width: 56ch;
color: var(--ink);
font-size: clamp(15px, 1.6vw, 17px);
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.hero__synopsis.is-open {
-webkit-line-clamp: unset;
overflow: visible;
}
.link-btn {
margin-top: 6px;
background: none;
border: none;
color: var(--brand-2);
font-weight: 600;
font-size: 14px;
padding: 4px 0;
}
.link-btn:hover { text-decoration: underline; }
.hero__actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 24px;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 9px;
border: 1px solid transparent;
border-radius: var(--r-md);
padding: 12px 22px;
font-weight: 700;
font-size: 15px;
color: var(--ink);
background: var(--surface-2);
transition: transform 0.12s ease, background 0.18s ease, box-shadow 0.18s ease;
}
.btn:hover { transform: translateY(-1px); }
.btn:active { transform: translateY(0); }
.btn--play {
background: var(--accent);
color: #0b0b0f;
}
.btn--play:hover { box-shadow: 0 10px 30px rgba(255,255,255,0.18); }
.btn--ghost {
background: rgba(255,255,255,0.1);
border-color: var(--line-2);
}
.btn--ghost.is-active {
background: linear-gradient(135deg, var(--brand), var(--brand-2));
border-color: transparent;
box-shadow: var(--glow);
}
.btn--icon {
width: 48px;
height: 48px;
padding: 0;
border-radius: 50%;
background: rgba(255,255,255,0.08);
border: 1px solid var(--line-2);
}
.btn--icon.is-active {
background: linear-gradient(135deg, var(--brand), var(--brand-2));
border-color: transparent;
box-shadow: var(--glow);
}
/* simple CSS icons */
.ico-play {
width: 0; height: 0;
border-left: 11px solid currentColor;
border-top: 7px solid transparent;
border-bottom: 7px solid transparent;
}
.ico-plus, .ico-like, .ico-share {
position: relative;
width: 16px; height: 16px;
display: inline-block;
}
.ico-plus::before, .ico-plus::after {
content: ""; position: absolute; background: currentColor; border-radius: 2px;
}
.ico-plus::before { left: 7px; top: 1px; width: 2px; height: 14px; }
.ico-plus::after { top: 7px; left: 1px; height: 2px; width: 14px; }
.ico-check {
position: relative;
width: 16px; height: 16px;
display: inline-block;
}
.ico-check::before {
content: "";
position: absolute;
left: 2px; top: 1px;
width: 6px; height: 11px;
border: solid currentColor;
border-width: 0 2px 2px 0;
transform: rotate(40deg);
}
.ico-like {
border: 2px solid currentColor;
border-radius: 2px 6px 6px 6px;
transform: rotate(0);
}
.ico-like::before {
content: ""; position: absolute; top: -6px; left: 2px;
width: 7px; height: 7px; background: currentColor;
border-radius: 2px 2px 0 0;
}
.ico-share {
border: 2px solid currentColor;
border-radius: 4px;
width: 15px; height: 12px;
}
.ico-share::before {
content: ""; position: absolute; top: -7px; left: 4px;
width: 0; height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-bottom: 6px solid currentColor;
}
/* ---------------- PAGE BODY ---------------- */
.page {
max-width: 1200px;
margin: 0 auto;
padding: 0 clamp(16px, 4vw, 44px) 60px;
}
.block { margin-top: clamp(32px, 5vw, 52px); }
.block__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 18px;
}
.block__title {
margin: 0;
font-size: clamp(1.2rem, 2.6vw, 1.55rem);
font-weight: 700;
letter-spacing: -0.01em;
}
/* ---------------- SEASON TABS ---------------- */
.seasons {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.season-tab {
background: var(--surface);
border: 1px solid var(--line);
color: var(--ink-2);
border-radius: 999px;
padding: 7px 16px;
font-size: 13px;
font-weight: 600;
transition: all 0.18s ease;
}
.season-tab:hover { color: var(--ink); border-color: var(--line-2); }
.season-tab[aria-selected="true"] {
background: var(--ink);
color: #0b0b0f;
border-color: var(--ink);
}
/* ---------------- EPISODES ---------------- */
.episodes { display: grid; gap: 10px; }
.episode {
display: grid;
grid-template-columns: 40px 168px 1fr auto;
align-items: center;
gap: 18px;
padding: 14px;
border: 1px solid var(--line);
border-radius: var(--r-md);
background: var(--surface);
transition: background 0.18s ease, border-color 0.18s ease, transform 0.12s ease;
}
.episode:hover {
background: var(--surface-2);
border-color: var(--line-2);
transform: translateY(-1px);
}
.episode__num {
font-size: 22px;
font-weight: 800;
color: var(--muted);
text-align: center;
}
.episode__thumb {
position: relative;
width: 168px;
aspect-ratio: 16 / 9;
border-radius: var(--r-sm);
overflow: hidden;
background: linear-gradient(135deg, var(--surface-2), #0c1622);
border: 1px solid var(--line);
}
.episode__thumb span {
position: absolute;
inset: 0;
display: grid;
place-items: center;
}
.episode__play {
width: 38px; height: 38px;
border-radius: 50%;
background: rgba(0,0,0,0.55);
border: 1px solid rgba(255,255,255,0.5);
display: grid; place-items: center;
opacity: 0;
transform: scale(0.8);
transition: opacity 0.18s ease, transform 0.18s ease;
}
.episode:hover .episode__play,
.episode:focus-within .episode__play { opacity: 1; transform: scale(1); }
.episode__play .ico-play { border-left-color: #fff; }
.episode__dur {
position: absolute;
right: 6px; bottom: 6px;
font-size: 11px;
font-weight: 700;
padding: 2px 6px;
border-radius: 4px;
background: rgba(0,0,0,0.7);
color: #fff;
}
.episode__body { min-width: 0; }
.episode__title {
margin: 0 0 4px;
font-size: 15px;
font-weight: 700;
}
.episode__desc {
margin: 0;
color: var(--ink-2);
font-size: 13.5px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.episode__air {
display: block;
margin-top: 6px;
color: var(--muted);
font-size: 12px;
}
.episode__add {
width: 40px; height: 40px;
border-radius: 50%;
background: rgba(255,255,255,0.06);
border: 1px solid var(--line-2);
color: var(--ink-2);
display: grid; place-items: center;
transition: all 0.18s ease;
}
.episode__add:hover { color: var(--ink); border-color: var(--ink); }
.episode__add.is-active {
color: #3ddc84;
border-color: rgba(61,220,132,0.5);
background: rgba(61,220,132,0.12);
}
/* ---------------- CAST ---------------- */
.cast {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 14px;
}
.cast__item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px;
border: 1px solid var(--line);
border-radius: var(--r-md);
background: var(--surface);
transition: background 0.18s ease, border-color 0.18s ease;
}
.cast__item:hover { background: var(--surface-2); border-color: var(--line-2); }
.cast__item.is-hidden { display: none; }
.cast__avatar {
width: 46px; height: 46px;
border-radius: 50%;
flex: 0 0 auto;
display: grid;
place-items: center;
font-weight: 700;
font-size: 15px;
color: #fff;
}
.cast__name { margin: 0; font-size: 14px; font-weight: 600; }
.cast__role { margin: 0; font-size: 12.5px; color: var(--muted); }
/* ---------------- SIMILAR ROW ---------------- */
.row { position: relative; }
.row__track {
display: flex;
gap: 14px;
overflow-x: auto;
padding: 4px 2px 14px;
scroll-behavior: smooth;
scrollbar-width: thin;
scrollbar-color: var(--line-2) transparent;
}
.row__track::-webkit-scrollbar { height: 8px; }
.row__track::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 8px; }
.poster {
flex: 0 0 200px;
border-radius: var(--r-md);
overflow: hidden;
background: var(--surface);
border: 1px solid var(--line);
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.poster:hover {
transform: translateY(-4px) scale(1.03);
box-shadow: var(--shadow);
border-color: var(--line-2);
z-index: 2;
}
.poster__art {
aspect-ratio: 16 / 10;
position: relative;
}
.poster__art .badge--quality {
position: absolute;
top: 8px; right: 8px;
}
.poster__match {
position: absolute;
left: 8px; bottom: 8px;
font-size: 11px;
font-weight: 800;
color: #3ddc84;
}
.poster__body { padding: 10px 12px 12px; }
.poster__title { margin: 0; font-size: 14px; font-weight: 700; }
.poster__meta { margin: 3px 0 0; font-size: 12px; color: var(--muted); }
.row__nav {
position: absolute;
top: 0;
bottom: 18px;
width: 44px;
border: none;
background: linear-gradient(90deg, rgba(11,11,15,0.92), rgba(11,11,15,0));
color: var(--ink);
font-size: 30px;
line-height: 1;
z-index: 4;
opacity: 0;
transition: opacity 0.2s ease;
}
.row__nav--next {
right: 0;
background: linear-gradient(270deg, rgba(11,11,15,0.92), rgba(11,11,15,0));
}
.row:hover .row__nav { opacity: 1; }
.row__nav:hover { color: var(--brand-2); }
/* ---------------- FOOTER ---------------- */
.foot {
margin-top: 56px;
padding-top: 20px;
border-top: 1px solid var(--line);
color: var(--muted);
font-size: 13px;
}
/* ---------------- TOAST ---------------- */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 24px);
background: var(--surface-2);
border: 1px solid var(--line-2);
color: var(--ink);
padding: 12px 20px;
border-radius: var(--r-md);
font-size: 14px;
font-weight: 600;
box-shadow: var(--shadow);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
z-index: 300;
}
.toast.is-show { opacity: 1; transform: translate(-50%, 0); }
/* ---------------- RESPONSIVE ---------------- */
@media (max-width: 760px) {
.episode {
grid-template-columns: 36px 116px 1fr;
gap: 12px;
}
.episode__thumb { width: 116px; }
.episode__add { display: none; }
}
@media (max-width: 520px) {
.topnav__links { display: none; }
.hero { min-height: 82vh; }
.hero__synopsis { -webkit-line-clamp: 4; }
.btn--ghost .btn__label { display: none; }
.btn--ghost { width: 48px; height: 48px; padding: 0; border-radius: 50%; }
.episode {
grid-template-columns: 1fr;
gap: 10px;
}
.episode__num { display: none; }
.episode__thumb { width: 100%; }
.poster { flex-basis: 150px; }
}(function () {
"use strict";
/* ---------------- DATA ---------------- */
var SEASONS = [
{
label: "Season 1",
episodes: [
{ n: 1, title: "The Drowned Meridian", dur: "58m", air: "Mar 7, 2024",
desc: "Mara Vey intercepts a signal from a satellite presumed dead for forty years — and the first map it sends back has no coastline." },
{ n: 2, title: "Cartographer's Dilemma", dur: "52m", air: "Mar 14, 2024",
desc: "A colleague vanishes from every photograph in the archive. Only Mara remembers he existed." },
{ n: 3, title: "Below the Waterline", dur: "55m", air: "Mar 21, 2024",
desc: "The atlas predicts a street that does not exist. Mara walks it anyway." },
{ n: 4, title: "Hollow North", dur: "49m", air: "Mar 28, 2024",
desc: "An expedition to the satellite's last known position reveals it was never in orbit at all." },
{ n: 5, title: "The Second Sea", dur: "61m", air: "Apr 4, 2024",
desc: "What lies beneath the drowned city begins to chart Mara in return." }
]
},
{
label: "Season 2",
episodes: [
{ n: 1, title: "Echo Survey", dur: "54m", air: "Feb 12, 2025",
desc: "One year on, the maps have started leaking into the waking world — and so have the people erased by them." },
{ n: 2, title: "The Quiet Latitude", dur: "50m", air: "Feb 19, 2025",
desc: "Mara recruits a disgraced surveyor who claims to have walked the second sea and returned." },
{ n: 3, title: "Reckoning Tide", dur: "57m", air: "Feb 26, 2025",
desc: "The agency that funded the satellite resurfaces with a very different account of who launched it." },
{ n: 4, title: "Phantom Mile", dur: "53m", air: "Mar 5, 2025",
desc: "A mile of road appears overnight where the harbor used to be. Anyone who drives it does not come back the same." },
{ n: 5, title: "Salt and Static", dur: "59m", air: "Mar 12, 2025",
desc: "Mara decodes the satellite's true purpose hidden in the noise between transmissions." },
{ n: 6, title: "The Atlas Opens", dur: "63m", air: "Mar 19, 2025",
desc: "Season finale. To save the people the map has taken, Mara must let it finish drawing her." }
]
},
{
label: "Season 3",
episodes: [
{ n: 1, title: "New Cartography", dur: "56m", air: "Jan 9, 2026",
desc: "Premiere. The drowned city has a sky now, and Mara is the only one who knows which way is up." },
{ n: 2, title: "Tideborn", dur: "52m", air: "Jan 16, 2026",
desc: "A child appears in the archive footage — present in every season, named by no one." },
{ n: 3, title: "The Unmapped", dur: "60m", air: "Jan 23, 2026",
desc: "Mara confronts the surveyor who has been redrawing the world one disappearance at a time." }
]
}
];
var CAST = [
{ name: "Lena Okonkwo", role: "Mara Vey", c: "#6c5ce7" },
{ name: "Idris Vance", role: "Director Hale", c: "#00b894" },
{ name: "Sora Mizuki", role: "Surveyor Quill", c: "#e17055" },
{ name: "Tomas Reyes", role: "Archivist Bell", c: "#0984e3" },
{ name: "Priya Anand", role: "Dr. Calder", c: "#fd79a8" },
{ name: "Niamh Doyle", role: "The Child", c: "#fdcb6e" },
{ name: "Marcus Yune", role: "Captain Frey", c: "#a29bfe" },
{ name: "Ada Bauer", role: "Voice of the Atlas", c: "#55efc4" },
{ name: "Jonah Petr", role: "Created by", c: "#636e72" },
{ name: "Cleo Marsh", role: "Composer", c: "#ff7675" }
];
var SIMILAR = [
{ title: "Tidal Ledger", meta: "2025 · Series", q: "4K", match: 96, c1: "#0c2340", c2: "#1b6ca8" },
{ title: "The Glass Meridian", meta: "2023 · Series", q: "HDR", match: 91, c1: "#2d1b46", c2: "#6c5ce7" },
{ title: "Cold Orbit", meta: "2024 · Film", q: "4K", match: 88, c1: "#11201f", c2: "#00b894" },
{ title: "Paper Coastline", meta: "2022 · Series", q: "HD", match: 84, c1: "#3a1f12", c2: "#e17055" },
{ title: "Signal Decay", meta: "2026 · Series", q: "4K", match: 93, c1: "#1a1030", c2: "#a29bfe" },
{ title: "The Last Survey", meta: "2021 · Film", q: "HDR", match: 79, c1: "#102a3a", c2: "#0984e3" },
{ title: "Undertow Atlas", meta: "2025 · Series", q: "4K", match: 95, c1: "#2b1530", c2: "#fd79a8" }
];
/* ---------------- HELPERS ---------------- */
function el(id) { return document.getElementById(id); }
var toastTimer;
function toast(msg) {
var t = el("toast");
t.textContent = msg;
t.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { t.classList.remove("is-show"); }, 2200);
}
/* ---------------- TOP NAV solidify on scroll ---------------- */
var nav = el("topnav");
function onScroll() {
nav.classList.toggle("is-solid", window.scrollY > 40);
}
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
/* ---------------- HERO actions ---------------- */
el("playBtn").addEventListener("click", function () {
toast("▶ Playing “Nightfall Atlas” — S1 E1");
});
var listBtn = el("listBtn");
listBtn.addEventListener("click", function () {
var on = listBtn.getAttribute("aria-pressed") === "true";
on = !on;
listBtn.setAttribute("aria-pressed", String(on));
listBtn.classList.toggle("is-active", on);
listBtn.querySelector(".ico-plus").className = on ? "ico-check" : "ico-plus";
var label = listBtn.querySelector(".btn__label");
if (label) label.textContent = on ? "Added" : "My List";
toast(on ? "Added to My List" : "Removed from My List");
});
var likeBtn = el("likeBtn");
likeBtn.addEventListener("click", function () {
var on = likeBtn.getAttribute("aria-pressed") === "true";
on = !on;
likeBtn.setAttribute("aria-pressed", String(on));
likeBtn.classList.toggle("is-active", on);
toast(on ? "Glad you like it — we'll find more like this" : "Rating removed");
});
el("shareBtn").addEventListener("click", function () {
toast("Link copied to clipboard");
});
/* ---------------- Synopsis read more ---------------- */
var readMore = el("readMore");
var synopsis = el("synopsis");
readMore.addEventListener("click", function () {
var open = synopsis.classList.toggle("is-open");
readMore.setAttribute("aria-expanded", String(open));
readMore.textContent = open ? "Read less" : "Read more";
});
/* ---------------- SEASON TABS + EPISODES ---------------- */
var tabsWrap = el("seasonTabs");
var listWrap = el("episodeList");
var current = SEASONS.length - 1; // default to latest season
function renderTabs() {
tabsWrap.innerHTML = "";
SEASONS.forEach(function (s, i) {
var b = document.createElement("button");
b.className = "season-tab";
b.type = "button";
b.setAttribute("role", "tab");
b.textContent = s.label;
b.setAttribute("aria-selected", String(i === current));
b.addEventListener("click", function () {
if (i === current) return;
current = i;
renderTabs();
renderEpisodes();
});
tabsWrap.appendChild(b);
});
}
function renderEpisodes() {
var eps = SEASONS[current].episodes;
listWrap.innerHTML = "";
eps.forEach(function (ep) {
var li = document.createElement("li");
li.className = "episode";
li.innerHTML =
'<div class="episode__num">' + ep.n + '</div>' +
'<div class="episode__thumb">' +
'<span><span class="episode__play"><span class="ico-play"></span></span></span>' +
'<span class="episode__dur">' + ep.dur + '</span>' +
'</div>' +
'<div class="episode__body">' +
'<h3 class="episode__title">' + ep.title + '</h3>' +
'<p class="episode__desc">' + ep.desc + '</p>' +
'<span class="episode__air">' + ep.air + '</span>' +
'</div>' +
'<button class="episode__add" type="button" aria-pressed="false" ' +
'aria-label="Add episode ' + ep.n + ' to My List">+</button>';
li.querySelector(".episode__thumb").addEventListener("click", function () {
toast("▶ " + SEASONS[current].label + " · E" + ep.n + " — " + ep.title);
});
var add = li.querySelector(".episode__add");
add.addEventListener("click", function (e) {
e.stopPropagation();
var on = add.getAttribute("aria-pressed") === "true";
on = !on;
add.setAttribute("aria-pressed", String(on));
add.classList.toggle("is-active", on);
add.textContent = on ? "✓" : "+";
toast(on ? "Episode added to My List" : "Episode removed");
});
listWrap.appendChild(li);
});
}
renderTabs();
renderEpisodes();
/* ---------------- CAST ---------------- */
var castList = el("castList");
var castToggle = el("castToggle");
var CAST_VISIBLE = 6;
function renderCast() {
castList.innerHTML = "";
CAST.forEach(function (p, i) {
var li = document.createElement("li");
li.className = "cast__item" + (i >= CAST_VISIBLE ? " is-hidden" : "");
var initials = p.name.split(" ").map(function (w) { return w[0]; }).join("").slice(0, 2);
li.innerHTML =
'<span class="cast__avatar" style="background:' + p.c + '">' + initials + '</span>' +
'<div><p class="cast__name">' + p.name + '</p>' +
'<p class="cast__role">' + p.role + '</p></div>';
castList.appendChild(li);
});
}
renderCast();
var castOpen = false;
castToggle.addEventListener("click", function () {
castOpen = !castOpen;
var hidden = castList.querySelectorAll(".cast__item");
hidden.forEach(function (item, i) {
if (i >= CAST_VISIBLE) item.classList.toggle("is-hidden", !castOpen);
});
castToggle.setAttribute("aria-expanded", String(castOpen));
castToggle.textContent = castOpen ? "Show less" : "Show all";
});
/* ---------------- SIMILAR ROW ---------------- */
var track = el("similarTrack");
SIMILAR.forEach(function (m) {
var li = document.createElement("li");
li.className = "poster";
li.tabIndex = 0;
li.setAttribute("role", "button");
li.setAttribute("aria-label", m.title + " — " + m.match + "% match");
li.innerHTML =
'<div class="poster__art" style="background:linear-gradient(135deg,' + m.c1 + ',' + m.c2 + ')">' +
'<span class="badge badge--quality">' + m.q + '</span>' +
'<span class="poster__match">' + m.match + '% Match</span>' +
'</div>' +
'<div class="poster__body">' +
'<p class="poster__title">' + m.title + '</p>' +
'<p class="poster__meta">' + m.meta + '</p>' +
'</div>';
function open() { toast("Opening “" + m.title + "”"); }
li.addEventListener("click", open);
li.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); open(); }
});
track.appendChild(li);
});
var simRow = el("similarRow");
simRow.querySelectorAll(".row__nav").forEach(function (btn) {
btn.addEventListener("click", function () {
var dir = parseInt(btn.getAttribute("data-dir"), 10);
track.scrollBy({ left: dir * Math.min(track.clientWidth * 0.85, 640), behavior: "smooth" });
});
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Nightfall Atlas — Title Detail</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>
<a class="skip-link" href="#main">Skip to content</a>
<header class="topnav" id="topnav">
<div class="topnav__inner">
<a class="brand" href="#" aria-label="Streamion home">
<span class="brand__mark">S</span><span class="brand__word">STREAMION</span>
</a>
<nav class="topnav__links" aria-label="Primary">
<a href="#">Home</a>
<a href="#">Series</a>
<a href="#">Films</a>
<a href="#">My List</a>
</nav>
<button class="avatar" type="button" aria-label="Account menu">AV</button>
</div>
</header>
<main id="main">
<!-- HERO / BILLBOARD -->
<section class="hero" aria-labelledby="hero-title">
<div class="hero__backdrop" role="img" aria-label="Nightfall Atlas backdrop art"></div>
<div class="hero__scrim"></div>
<div class="hero__content">
<p class="hero__kicker">Streamion Original Series</p>
<h1 class="hero__title" id="hero-title">Nightfall Atlas</h1>
<div class="hero__meta">
<span class="badge badge--match">98% Match</span>
<span class="meta-item">2026</span>
<span class="badge badge--rating">TV-MA</span>
<span class="meta-item" id="season-count">3 Seasons</span>
<span class="badge badge--quality">4K</span>
<span class="badge badge--quality">HDR</span>
<span class="badge badge--quality">5.1</span>
</div>
<p class="hero__tags">Sci-Fi · Mystery · Thriller</p>
<p class="hero__synopsis" id="synopsis">
When a derelict mapping satellite reawakens over a drowned city, cartographer
Mara Vey discovers the charts it draws are not of our world — they are of the
one waiting underneath. Each episode peels back another layer of an impossible
atlas while the people she trusts vanish from the record entirely.
</p>
<button class="link-btn" id="readMore" type="button" aria-expanded="false">Read more</button>
<div class="hero__actions">
<button class="btn btn--play" id="playBtn" type="button">
<span class="ico-play" aria-hidden="true"></span> Play
</button>
<button class="btn btn--ghost" id="listBtn" type="button" aria-pressed="false">
<span class="ico-plus" aria-hidden="true"></span> <span class="btn__label">My List</span>
</button>
<button class="btn btn--icon" id="likeBtn" type="button" aria-pressed="false" aria-label="Like this title">
<span class="ico-like" aria-hidden="true"></span>
</button>
<button class="btn btn--icon" id="shareBtn" type="button" aria-label="Share this title">
<span class="ico-share" aria-hidden="true"></span>
</button>
</div>
</div>
</section>
<div class="page">
<!-- EPISODES -->
<section class="block" aria-labelledby="ep-heading">
<div class="block__head">
<h2 class="block__title" id="ep-heading">Episodes</h2>
<div class="seasons" role="tablist" aria-label="Select season" id="seasonTabs"></div>
</div>
<ol class="episodes" id="episodeList"></ol>
</section>
<!-- CAST -->
<section class="block" aria-labelledby="cast-heading">
<div class="block__head">
<h2 class="block__title" id="cast-heading">Cast & Crew</h2>
<button class="link-btn" id="castToggle" type="button" aria-expanded="false">Show all</button>
</div>
<ul class="cast" id="castList"></ul>
</section>
<!-- SIMILAR -->
<section class="block" aria-labelledby="sim-heading">
<div class="block__head">
<h2 class="block__title" id="sim-heading">More Like This</h2>
</div>
<div class="row" id="similarRow">
<button class="row__nav row__nav--prev" type="button" aria-label="Scroll left" data-dir="-1">‹</button>
<ul class="row__track" id="similarTrack"></ul>
<button class="row__nav row__nav--next" type="button" aria-label="Scroll right" data-dir="1">›</button>
</div>
</section>
<footer class="foot">
<p>Streamion — illustrative UI. All titles, people and ratings are fictional.</p>
</footer>
</div>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Title Detail
A streaming title-detail page for the fictional original series Nightfall Atlas. A full-height billboard hero pairs a layered, starfield-gradient backdrop with the show title, a 98% match score, rating and 4K/HDR/5.1 quality badges, and the primary action cluster — Play, an add-to-My-List toggle that morphs from a plus into a check, a like button, and share. The synopsis clamps to three lines with a Read more / Read less control, and a minimal top nav fades into a solid bar as you scroll.
Below the hero, season tabs (defaulting to the latest season) swap a detailed episode list rendered from data: each row shows an episode number, a 16:9 thumbnail with a hover-reveal play affordance and duration chip, title, two-line description, air date, and its own list toggle. The cast section renders as an avatar grid that starts collapsed and expands with Show all, and a More Like This row presents poster recommendations with quality badges, match scores, gradient-overlay hover scaling, keyboard support, and edge scroll buttons.
Every interaction is driven by dependency-free vanilla JS — season switching, the My List and like toggles, synopsis expansion, per-episode add buttons, cast expansion, and horizontal row scrolling — with a small toast() helper giving lightweight feedback. The layout is responsive from wide desktop down to ~360px, collapsing the episode grid and condensing the action bar on small screens.
Illustrative UI only — fictional titles, not a real streaming service.