Streaming — Browse Home
A cinematic dark-first browse home for the fictional Nebula streaming service, built in HTML, CSS, and vanilla JS. A full-width hero billboard rotates featured titles with gradient-scrim poster art, match scores, quality badges, and Play and More Info actions. Below sit horizontally scrollable rows — Continue Watching with progress bars, a ranked Trending Top 10, New Releases, and genre rails — each with hover-scale posters that expand into a quick-action preview card. A top nav condenses on scroll and a toast confirms every illustrative click.
MCP
Code
:root {
--bg: #0b0b0f;
--surface: #15151c;
--surface-2: #1e1e27;
--ink: #f4f4f7;
--ink-2: #b6b7c3;
--muted: #83859a;
--brand: #e50914;
--accent: #ffffff;
--line: rgba(255, 255, 255, 0.1);
--line-2: rgba(255, 255, 255, 0.16);
--green: #46d369;
--r-sm: 8px;
--r-md: 12px;
--r-lg: 18px;
--shadow: 0 18px 50px rgba(0, 0, 0, 0.6);
--glow: 0 0 0 2px var(--accent), 0 24px 60px rgba(0, 0, 0, 0.7);
--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;
overflow-x: hidden;
}
img { display: block; max-width: 100%; }
button { font-family: inherit; cursor: pointer; }
a { color: inherit; }
.skip-link {
position: absolute;
left: 12px;
top: -48px;
z-index: 200;
background: var(--ink);
color: var(--bg);
padding: 10px 14px;
border-radius: var(--r-sm);
font-weight: 600;
transition: top 0.18s ease;
}
.skip-link:focus { top: 12px; }
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: 4px;
}
/* ---------- Top nav ---------- */
.topnav {
position: fixed;
inset: 0 0 auto 0;
z-index: 100;
height: var(--nav-h);
display: flex;
align-items: center;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.85) 0%, rgba(0, 0, 0, 0) 100%);
transition: background 0.3s ease, height 0.25s ease, box-shadow 0.3s ease;
}
.topnav.is-condensed {
height: 56px;
background: rgba(11, 11, 15, 0.94);
backdrop-filter: blur(10px);
box-shadow: 0 1px 0 var(--line);
}
.topnav__inner {
width: 100%;
max-width: 1500px;
margin: 0 auto;
padding: 0 clamp(16px, 4vw, 48px);
display: flex;
align-items: center;
gap: 28px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 9px;
text-decoration: none;
font-weight: 800;
letter-spacing: 0.14em;
}
.brand__mark {
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 7px;
background: var(--brand);
color: #fff;
font-size: 18px;
font-weight: 800;
box-shadow: 0 6px 18px rgba(229, 9, 20, 0.5);
}
.brand__word { font-size: 17px; color: var(--ink); }
.topnav__links {
display: flex;
gap: 22px;
margin-right: auto;
font-size: 14px;
}
.topnav__links a {
text-decoration: none;
color: var(--ink-2);
font-weight: 500;
transition: color 0.15s ease;
}
.topnav__links a:hover { color: var(--ink); }
.topnav__links a.is-active { color: var(--ink); font-weight: 600; }
.topnav__right {
display: flex;
align-items: center;
gap: 10px;
margin-left: auto;
}
.iconbtn {
display: grid;
place-items: center;
width: 38px;
height: 38px;
border-radius: 50%;
border: 1px solid transparent;
background: transparent;
color: var(--ink);
transition: background 0.15s ease, border-color 0.15s ease;
}
.iconbtn:hover { background: var(--surface-2); border-color: var(--line); }
.avatar {
width: 34px;
height: 34px;
border-radius: 8px;
border: none;
background: linear-gradient(135deg, #6d28d9, #db2777);
color: #fff;
font-weight: 700;
font-size: 14px;
}
/* ---------- Hero ---------- */
.hero {
position: relative;
min-height: clamp(540px, 78vh, 760px);
display: flex;
align-items: flex-end;
padding: 0 clamp(16px, 4vw, 56px) clamp(40px, 7vw, 96px);
isolation: isolate;
overflow: hidden;
}
.hero__art {
position: absolute;
inset: 0;
z-index: -2;
transition: opacity 0.7s ease;
}
.hero__scrim {
position: absolute;
inset: 0;
z-index: -1;
background:
linear-gradient(90deg, rgba(11, 11, 15, 0.94) 0%, rgba(11, 11, 15, 0.6) 38%, rgba(11, 11, 15, 0) 70%),
linear-gradient(0deg, var(--bg) 2%, rgba(11, 11, 15, 0.2) 32%, rgba(11, 11, 15, 0) 60%);
}
.hero__content { max-width: 620px; }
.hero__kicker {
margin: 0 0 12px;
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--ink-2);
}
.hero__kicker::before {
content: "";
width: 22px;
height: 4px;
border-radius: 3px;
background: var(--brand);
}
.hero__title {
margin: 0 0 16px;
font-size: clamp(34px, 6.2vw, 68px);
font-weight: 800;
line-height: 1.02;
letter-spacing: -0.02em;
text-shadow: 0 4px 30px rgba(0, 0, 0, 0.6);
}
.hero__meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
margin-bottom: 16px;
font-size: 14px;
color: var(--ink-2);
font-weight: 500;
}
.badge {
display: inline-flex;
align-items: center;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
padding: 3px 8px;
border-radius: 5px;
line-height: 1;
}
.badge--match { color: var(--green); background: transparent; padding-left: 0; font-size: 14px; }
.badge--rated { border: 1px solid var(--line-2); color: var(--ink-2); }
.badge--hd { background: rgba(255, 255, 255, 0.14); color: var(--ink); }
.hero__desc {
margin: 0 0 26px;
font-size: clamp(15px, 1.6vw, 18px);
color: var(--ink);
text-shadow: 0 2px 16px rgba(0, 0, 0, 0.7);
}
.hero__actions { display: flex; gap: 12px; flex-wrap: wrap; }
.btn {
display: inline-flex;
align-items: center;
gap: 9px;
border: none;
border-radius: var(--r-sm);
padding: 12px 26px;
font-size: 16px;
font-weight: 700;
transition: transform 0.12s ease, background 0.18s ease, opacity 0.18s ease;
}
.btn:active { transform: scale(0.97); }
.btn--play { background: var(--accent); color: #0b0b0f; }
.btn--play:hover { background: rgba(255, 255, 255, 0.82); }
.btn--info { background: rgba(120, 120, 140, 0.42); color: var(--ink); }
.btn--info:hover { background: rgba(120, 120, 140, 0.62); }
.hero__dots {
position: absolute;
right: clamp(16px, 4vw, 56px);
bottom: clamp(40px, 7vw, 96px);
display: flex;
gap: 8px;
}
.hero__dots button {
width: 10px;
height: 10px;
padding: 0;
border-radius: 50%;
border: 1px solid var(--line-2);
background: rgba(255, 255, 255, 0.18);
transition: background 0.2s ease, transform 0.2s ease;
}
.hero__dots button[aria-selected="true"] {
background: var(--accent);
transform: scale(1.15);
}
/* ---------- Rows ---------- */
.rows {
position: relative;
z-index: 2;
margin-top: clamp(-60px, -5vw, -40px);
padding-bottom: 64px;
display: flex;
flex-direction: column;
gap: clamp(26px, 3vw, 42px);
}
.row { padding-left: clamp(16px, 4vw, 56px); }
.row__head {
display: flex;
align-items: baseline;
gap: 12px;
margin-bottom: 12px;
padding-right: clamp(16px, 4vw, 56px);
}
.row__title {
margin: 0;
font-size: clamp(16px, 2vw, 21px);
font-weight: 700;
letter-spacing: -0.01em;
}
.row__explore {
margin-left: auto;
font-size: 12px;
font-weight: 600;
color: var(--muted);
opacity: 0;
transform: translateX(-6px);
transition: opacity 0.2s ease, transform 0.2s ease, color 0.2s ease;
background: none;
border: none;
}
.row:hover .row__explore { opacity: 1; transform: translateX(0); }
.row__explore:hover { color: var(--ink); }
.row__viewport { position: relative; }
.row__track {
display: flex;
gap: 10px;
overflow-x: auto;
scroll-behavior: smooth;
padding: 26px clamp(16px, 4vw, 56px) 26px 0;
margin: -26px 0;
scrollbar-width: none;
}
.row__track::-webkit-scrollbar { display: none; }
.arrow {
position: absolute;
top: 0;
bottom: 0;
z-index: 5;
width: clamp(40px, 4vw, 56px);
border: none;
display: grid;
place-items: center;
color: var(--ink);
background: linear-gradient(90deg, rgba(11, 11, 15, 0.9), rgba(11, 11, 15, 0));
opacity: 0;
transition: opacity 0.2s ease;
}
.arrow--right {
right: 0;
background: linear-gradient(270deg, rgba(11, 11, 15, 0.9), rgba(11, 11, 15, 0));
}
.arrow--left { left: 0; }
.row__viewport:hover .arrow:not([disabled]) { opacity: 1; }
.arrow:hover svg { transform: scale(1.3); }
.arrow svg { transition: transform 0.15s ease; }
.arrow[disabled] { opacity: 0 !important; pointer-events: none; }
/* ---------- Cards ---------- */
.card {
position: relative;
flex: 0 0 auto;
width: clamp(150px, 17vw, 232px);
border-radius: var(--r-md);
overflow: visible;
background: transparent;
border: none;
padding: 0;
text-align: left;
transition: transform 0.22s cubic-bezier(0.2, 0.7, 0.3, 1);
}
.card__poster {
position: relative;
aspect-ratio: 16 / 10;
border-radius: var(--r-md);
overflow: hidden;
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.45);
transition: box-shadow 0.22s ease;
}
.card__poster::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 45%, rgba(0, 0, 0, 0.78) 100%);
}
.card__label {
position: absolute;
z-index: 2;
left: 12px;
bottom: 10px;
right: 12px;
font-size: 13px;
font-weight: 700;
line-height: 1.2;
text-shadow: 0 1px 6px rgba(0, 0, 0, 0.8);
}
.card__top {
position: absolute;
z-index: 2;
top: 8px;
left: 8px;
right: 8px;
display: flex;
gap: 6px;
}
.card__tag {
font-size: 10px;
font-weight: 800;
letter-spacing: 0.06em;
padding: 3px 6px;
border-radius: 4px;
text-transform: uppercase;
backdrop-filter: blur(4px);
}
.card__tag--new { background: var(--brand); color: #fff; }
.card__tag--hd { background: rgba(0, 0, 0, 0.55); color: var(--ink); border: 1px solid var(--line-2); margin-left: auto; }
.card__tag--top { background: var(--brand); color: #fff; }
.card__rank {
position: absolute;
z-index: 3;
left: -6px;
bottom: -4px;
font-size: clamp(54px, 7vw, 92px);
font-weight: 800;
line-height: 0.8;
color: #0b0b0f;
-webkit-text-stroke: 3px var(--muted);
letter-spacing: -0.06em;
pointer-events: none;
}
.card--ranked { width: clamp(190px, 21vw, 280px); padding-left: 46px; }
.card__progress {
position: absolute;
z-index: 2;
left: 0;
right: 0;
bottom: 0;
height: 4px;
background: rgba(255, 255, 255, 0.22);
}
.card__progress span {
display: block;
height: 100%;
background: var(--brand);
}
/* hover preview */
.card__hover {
position: absolute;
z-index: 4;
left: 0;
top: 100%;
width: 100%;
margin-top: 6px;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 12px 13px 14px;
box-shadow: var(--shadow);
opacity: 0;
visibility: hidden;
transform: translateY(-6px);
transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s;
pointer-events: none;
}
.card__actions { display: flex; gap: 8px; margin-bottom: 9px; }
.card__circ {
width: 32px;
height: 32px;
border-radius: 50%;
display: grid;
place-items: center;
border: 1.5px solid var(--line-2);
background: var(--surface);
color: var(--ink);
transition: border-color 0.15s ease, background 0.15s ease, transform 0.1s ease;
}
.card__circ:active { transform: scale(0.9); }
.card__circ--play { background: var(--accent); color: #0b0b0f; border-color: var(--accent); }
.card__circ:hover { border-color: var(--accent); }
.card__hmeta {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--ink-2);
}
.card__hmeta .match { color: var(--green); font-weight: 700; }
.card__chip {
border: 1px solid var(--line-2);
border-radius: 4px;
padding: 1px 5px;
font-size: 10px;
font-weight: 700;
}
.card__genres {
margin-top: 7px;
font-size: 11.5px;
color: var(--muted);
}
@media (hover: hover) and (min-width: 700px) {
.card:hover { transform: scale(1.22); z-index: 10; }
.card:hover .card__poster { box-shadow: var(--glow); }
.card:hover .card__hover { opacity: 1; visibility: visible; transform: translateY(0); pointer-events: auto; }
.card:first-child:hover { transform: scale(1.22) translateX(8%); }
.row__track > .card:last-child:hover { transform: scale(1.22) translateX(-8%); }
}
.card:focus-visible { transform: scale(1.1); z-index: 10; }
.card:focus-within .card__hover { opacity: 1; visibility: visible; transform: translateY(0); pointer-events: auto; }
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 24px);
z-index: 300;
background: var(--surface-2);
color: var(--ink);
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 11px 20px;
font-size: 14px;
font-weight: 500;
box-shadow: var(--shadow);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
}
.toast.is-on { opacity: 1; transform: translate(-50%, 0); }
@media (max-width: 860px) {
.topnav__links { display: none; }
}
@media (max-width: 520px) {
:root { --nav-h: 56px; }
.hero { min-height: 70vh; align-items: flex-end; }
.hero__title { font-size: 36px; }
.hero__desc { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.hero__dots { display: none; }
.btn { padding: 11px 20px; font-size: 15px; }
.card { width: 132px; }
.card--ranked { width: 168px; }
.brand__word { display: none; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
scroll-behavior: auto !important;
}
}(function () {
"use strict";
/* ---------- Toast helper ---------- */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("is-on");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-on");
}, 2200);
}
/* Generic data-toast wiring */
document.addEventListener("click", function (e) {
var t = e.target.closest("[data-toast]");
if (t) toast(t.getAttribute("data-toast"));
});
/* ---------- Poster art generator (CSS gradients, deterministic) ---------- */
function poster(seed, a, b, c) {
return (
"radial-gradient(120% 90% at 78% 12%, " + a + "33 0%, transparent 55%)," +
"radial-gradient(90% 80% at 20% 90%, " + c + "44 0%, transparent 60%)," +
"linear-gradient(135deg, " + a + " 0%, " + b + " 52%, " + c + " 100%)"
);
}
/* ---------- Featured hero titles ---------- */
var HERO = [
{
kicker: "Nebula Original Series",
title: "The Glass Meridian",
match: "97% Match", year: "2026", rated: "TV-MA", extra: "3 Seasons",
desc: "A disgraced cartographer is recruited to map a city that rewrites itself every night — and the deeper she charts it, the more it charts her.",
art: poster(1, "#3b0764", "#7c1d6f", "#0b0b0f")
},
{
kicker: "Nebula Original Film",
title: "Saltwater Engine",
match: "94% Match", year: "2025", rated: "PG-13", extra: "2h 11m",
desc: "Two estranged sisters restore their late father's fishing trawler and discover his last voyage was never finished.",
art: poster(2, "#0c4a6e", "#0e7490", "#0b0b0f")
},
{
kicker: "New Limited Series",
title: "Hollow Crown Lane",
match: "91% Match", year: "2026", rated: "TV-14", extra: "Limited Series",
desc: "On a cul-de-sac where every house keeps a secret, a new neighbor's arrival sets off a quietly devastating chain reaction.",
art: poster(3, "#7c2d12", "#b91c1c", "#0b0b0f")
},
{
kicker: "Nebula Original Series",
title: "Quantum Pastoral",
match: "96% Match", year: "2026", rated: "TV-MA", extra: "1 Season",
desc: "A physicist retreats to a remote farm to grieve — and finds the orchard is leaking time.",
art: poster(4, "#14532d", "#15803d", "#0b0b0f")
}
];
var heroIdx = 0;
var els = {
art: document.getElementById("heroArt"),
kicker: document.getElementById("heroKicker"),
title: document.getElementById("heroTitle"),
match: document.getElementById("heroMatch"),
year: document.getElementById("heroYear"),
rated: null,
seasons: document.getElementById("heroSeasons"),
desc: document.getElementById("heroDesc")
};
var dotsWrap = document.getElementById("heroDots");
function renderHero(i, focusDot) {
var h = HERO[i];
if (els.art) {
els.art.style.opacity = "0";
setTimeout(function () {
els.art.style.backgroundImage = h.art;
els.art.style.backgroundSize = "cover";
els.art.style.opacity = "1";
}, 200);
}
els.kicker.textContent = h.kicker;
els.title.textContent = h.title;
els.match.textContent = h.match;
els.year.textContent = h.year;
els.seasons.textContent = h.extra;
els.desc.textContent = h.desc;
var rated = document.querySelector(".badge--rated");
if (rated) rated.textContent = h.rated;
Array.prototype.forEach.call(dotsWrap.children, function (d, di) {
d.setAttribute("aria-selected", di === i ? "true" : "false");
});
if (focusDot && dotsWrap.children[i]) dotsWrap.children[i].focus();
}
HERO.forEach(function (h, i) {
var b = document.createElement("button");
b.type = "button";
b.setAttribute("role", "tab");
b.setAttribute("aria-label", "Show " + h.title);
b.addEventListener("click", function () {
heroIdx = i;
renderHero(heroIdx);
restartRotate();
});
dotsWrap.appendChild(b);
});
renderHero(0);
var rotateTimer;
function rotate() {
heroIdx = (heroIdx + 1) % HERO.length;
renderHero(heroIdx);
}
var reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
function restartRotate() {
clearInterval(rotateTimer);
if (!reduceMotion) rotateTimer = setInterval(rotate, 7000);
}
restartRotate();
document.getElementById("heroPlay").addEventListener("click", function () {
toast("Playing “" + HERO[heroIdx].title + "”");
});
document.getElementById("heroInfo").addEventListener("click", function () {
toast("More info: " + HERO[heroIdx].title);
});
/* ---------- Row data ---------- */
var PALETTES = [
["#3b0764", "#7c1d6f", "#0b0b0f"], ["#0c4a6e", "#0e7490", "#0b0b0f"],
["#7c2d12", "#b91c1c", "#0b0b0f"], ["#14532d", "#15803d", "#0b0b0f"],
["#1e1b4b", "#4338ca", "#0b0b0f"], ["#831843", "#db2777", "#0b0b0f"],
["#713f12", "#ca8a04", "#0b0b0f"], ["#134e4a", "#0d9488", "#0b0b0f"],
["#3f1d38", "#9333ea", "#0b0b0f"], ["#1c1917", "#57534e", "#0b0b0f"]
];
var GENRE_POOL = ["Suspense", "Drama", "Sci-Fi", "Thriller", "Mystery", "Crime", "Romance", "Dark Comedy", "Adventure"];
function mk(title, genres, opts) {
opts = opts || {};
return {
title: title,
genres: genres,
progress: opts.progress,
tag: opts.tag,
match: opts.match || (88 + Math.floor(Math.random() * 11)) + "% Match",
rated: opts.rated || "TV-MA",
dur: opts.dur || "1 Season"
};
}
var ROWS = [
{
title: "Continue Watching for Priya",
items: [
mk("Midnight Archive", ["Mystery", "Drama"], { progress: 72, dur: "S2:E4" }),
mk("Ferrous", ["Sci-Fi"], { progress: 41, dur: "S1:E8" }),
mk("The Long Quiet", ["Drama"], { progress: 18, dur: "1h 54m" }),
mk("Paper Tigers", ["Crime", "Thriller"], { progress: 88, dur: "S3:E2" }),
mk("Driftwood County", ["Mystery"], { progress: 55, dur: "S1:E6" }),
mk("Echo Bay", ["Thriller"], { progress: 9, dur: "S2:E1" })
]
},
{
title: "Trending Now",
ranked: true,
items: [
mk("The Glass Meridian", ["Suspense", "Sci-Fi"], { tag: "top" }),
mk("Saltwater Engine", ["Drama"], { tag: "top" }),
mk("Hollow Crown Lane", ["Mystery"], { tag: "top" }),
mk("Quantum Pastoral", ["Sci-Fi", "Drama"], { tag: "top" }),
mk("Static Garden", ["Thriller"], { tag: "top" }),
mk("Nine Below", ["Crime"], { tag: "top" }),
mk("The Understudy", ["Dark Comedy"], { tag: "top" }),
mk("Cobalt Hour", ["Sci-Fi"], { tag: "top" }),
mk("Marrow", ["Suspense"], { tag: "top" }),
mk("Lantern Street", ["Drama"], { tag: "top" })
]
},
{
title: "New Releases",
items: [
mk("Petrichor", ["Romance", "Drama"], { tag: "new", dur: "1h 47m" }),
mk("Foxglove", ["Thriller"], { tag: "new" }),
mk("The Mapmaker's Daughter", ["Adventure"], { tag: "new" }),
mk("Slow Burn Avenue", ["Crime"], { tag: "new" }),
mk("Aurora Service", ["Sci-Fi"], { tag: "new" }),
mk("Tidewater", ["Drama"], { tag: "new" }),
mk("Glasshouse", ["Mystery"], { tag: "new" }),
mk("Velvet Static", ["Dark Comedy"], { tag: "new" })
]
},
{
title: "Sci-Fi & Beyond",
items: [
mk("Orbital Decay", ["Sci-Fi"]), mk("The Pale Engine", ["Sci-Fi", "Thriller"]),
mk("Heliotrope", ["Sci-Fi"]), mk("Null Sector", ["Sci-Fi", "Adventure"]),
mk("Carbon Lullaby", ["Sci-Fi"]), mk("Far Meridian", ["Sci-Fi"]),
mk("The Quiet Machine", ["Sci-Fi", "Drama"]), mk("Strata", ["Sci-Fi"]),
mk("Lightfall", ["Sci-Fi", "Adventure"])
]
},
{
title: "Critically Acclaimed Dramas",
items: [
mk("The Understudy", ["Drama"]), mk("Bone & Brass", ["Drama", "Crime"]),
mk("Marigold Down", ["Drama"]), mk("A Smaller Sky", ["Drama", "Romance"]),
mk("The Tenant", ["Drama", "Mystery"]), mk("Salt of the Coast", ["Drama"]),
mk("Embers", ["Drama"]), mk("Thin Ice County", ["Drama", "Thriller"])
]
}
];
function svgIcon(name) {
if (name === "play") return '<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path d="M8 5v14l11-7z" fill="currentColor"/></svg>';
if (name === "plus") return '<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>';
if (name === "like") return '<svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><path d="M7 11v8H4v-8h3zm3 8c-.4 0-.7-.2-1-.5V10l3-7c1.2 0 2 .9 2 2v3h4.5c1 0 1.8 1 1.5 2l-1.6 6c-.2.8-1 1.4-1.8 1.4H10z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg>';
return '<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>';
}
var rowsWrap = document.getElementById("rows");
function buildCard(item, ranked, rank) {
var pal = PALETTES[(item.title.length + (rank || 0)) % PALETTES.length];
var card = document.createElement("button");
card.type = "button";
card.className = "card" + (ranked ? " card--ranked" : "");
card.setAttribute("aria-label", item.title + ", " + item.match);
var rankMarkup = ranked ? '<span class="card__rank">' + rank + "</span>" : "";
var topTags = "";
if (item.tag === "new") topTags = '<span class="card__tag card__tag--new">New</span>';
else if (item.tag === "top") topTags = '<span class="card__tag card__tag--top">Top 10</span>';
topTags += '<span class="card__tag card__tag--hd">HD</span>';
var progressMarkup = item.progress != null
? '<div class="card__progress"><span style="width:' + item.progress + '%"></span></div>'
: "";
card.innerHTML =
rankMarkup +
'<div class="card__poster" style="background-image:' + poster(0, pal[0], pal[1], pal[2]) + ';background-size:cover">' +
'<div class="card__top">' + topTags + "</div>" +
'<div class="card__label">' + item.title + "</div>" +
progressMarkup +
"</div>" +
'<div class="card__hover">' +
'<div class="card__actions">' +
'<span class="card__circ card__circ--play" aria-hidden="true">' + svgIcon("play") + "</span>" +
'<span class="card__circ" data-act="list" aria-hidden="true">' + svgIcon("plus") + "</span>" +
'<span class="card__circ" data-act="like" aria-hidden="true">' + svgIcon("like") + "</span>" +
"</div>" +
'<div class="card__hmeta"><span class="match">' + item.match + '</span><span class="card__chip">' + item.rated + "</span><span>" + item.dur + "</span></div>" +
'<div class="card__genres">' + item.genres.join(" · ") + "</div>" +
"</div>";
card.addEventListener("click", function (e) {
var act = e.target.closest("[data-act]");
if (act) {
var a = act.getAttribute("data-act");
toast(a === "list" ? "Added “" + item.title + "” to My List" : "You liked “" + item.title + "”");
return;
}
toast("Playing “" + item.title + "”");
});
return card;
}
function buildRow(rowData) {
var section = document.createElement("section");
section.className = "row";
section.setAttribute("aria-label", rowData.title);
var head = document.createElement("div");
head.className = "row__head";
head.innerHTML =
'<h2 class="row__title">' + rowData.title + "</h2>" +
'<button class="row__explore" type="button">Explore all ' + svgIcon("chev") + "</button>";
head.querySelector(".row__explore").addEventListener("click", function () {
toast("Explore: " + rowData.title);
});
section.appendChild(head);
var viewport = document.createElement("div");
viewport.className = "row__viewport";
var leftBtn = document.createElement("button");
leftBtn.className = "arrow arrow--left";
leftBtn.type = "button";
leftBtn.setAttribute("aria-label", "Scroll " + rowData.title + " left");
leftBtn.innerHTML = '<svg viewBox="0 0 24 24" width="26" height="26" aria-hidden="true"><path d="M15 5l-7 7 7 7" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>';
var rightBtn = document.createElement("button");
rightBtn.className = "arrow arrow--right";
rightBtn.type = "button";
rightBtn.setAttribute("aria-label", "Scroll " + rowData.title + " right");
rightBtn.innerHTML = '<svg viewBox="0 0 24 24" width="26" height="26" aria-hidden="true"><path d="M9 5l7 7-7 7" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>';
var track = document.createElement("div");
track.className = "row__track";
rowData.items.forEach(function (item, i) {
track.appendChild(buildCard(item, rowData.ranked, i + 1));
});
function updateArrows() {
var max = track.scrollWidth - track.clientWidth - 2;
leftBtn.disabled = track.scrollLeft <= 2;
rightBtn.disabled = track.scrollLeft >= max;
}
function page(dir) {
track.scrollBy({ left: dir * Math.round(track.clientWidth * 0.85), behavior: "smooth" });
}
leftBtn.addEventListener("click", function () { page(-1); });
rightBtn.addEventListener("click", function () { page(1); });
track.addEventListener("scroll", updateArrows, { passive: true });
window.addEventListener("resize", updateArrows);
viewport.appendChild(leftBtn);
viewport.appendChild(track);
viewport.appendChild(rightBtn);
section.appendChild(viewport);
rowsWrap.appendChild(section);
requestAnimationFrame(updateArrows);
}
ROWS.forEach(buildRow);
/* ---------- Nav condense on scroll ---------- */
var nav = document.getElementById("topnav");
function onScroll() {
nav.classList.toggle("is-condensed", window.scrollY > 40);
}
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Nebula — Browse</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="Nebula home">
<span class="brand__mark" aria-hidden="true">N</span>
<span class="brand__word">NEBULA</span>
</a>
<nav class="topnav__links" aria-label="Primary">
<a href="#" class="is-active" aria-current="page">Home</a>
<a href="#">Series</a>
<a href="#">Films</a>
<a href="#">New & Popular</a>
<a href="#">My List</a>
</nav>
<div class="topnav__right">
<button class="iconbtn" type="button" aria-label="Search" data-toast="Search is illustrative only.">
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><circle cx="11" cy="11" r="7" fill="none" stroke="currentColor" stroke-width="2"/><path d="M20 20l-3.2-3.2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
</button>
<button class="iconbtn" type="button" aria-label="Notifications" data-toast="No new notifications.">
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><path d="M12 3a5 5 0 0 0-5 5v3l-1.5 2.5h13L17 11V8a5 5 0 0 0-5-5z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M10 18a2 2 0 0 0 4 0" fill="none" stroke="currentColor" stroke-width="2"/></svg>
</button>
<button class="avatar" type="button" aria-label="Account: Priya" data-toast="Viewing Priya's profile.">P</button>
</div>
</div>
</header>
<main id="main">
<section class="hero" id="hero" aria-label="Featured title">
<div class="hero__art" id="heroArt" aria-hidden="true"></div>
<div class="hero__scrim"></div>
<div class="hero__content">
<p class="hero__kicker" id="heroKicker">Nebula Original Series</p>
<h1 class="hero__title" id="heroTitle">The Glass Meridian</h1>
<div class="hero__meta">
<span class="badge badge--match" id="heroMatch">97% Match</span>
<span class="badge badge--rated">TV-MA</span>
<span id="heroYear">2026</span>
<span class="badge badge--hd">4K</span>
<span id="heroSeasons">3 Seasons</span>
</div>
<p class="hero__desc" id="heroDesc">A disgraced cartographer is recruited to map a city that rewrites itself every night — and the deeper she charts it, the more it charts her.</p>
<div class="hero__actions">
<button class="btn btn--play" type="button" id="heroPlay">
<svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><path d="M8 5v14l11-7z" fill="currentColor"/></svg>
<span>Play</span>
</button>
<button class="btn btn--info" type="button" id="heroInfo">
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="2"/><path d="M12 11v5M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<span>More Info</span>
</button>
</div>
</div>
<div class="hero__dots" id="heroDots" role="tablist" aria-label="Featured selector"></div>
</section>
<div class="rows" id="rows"></div>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Browse Home
A streaming browse home for Nebula, a fictional service. The page opens on a full-width hero billboard whose gradient-scrim poster art, kicker, title, match score, rating and quality badges, synopsis, and Play / More Info buttons rotate through four featured titles every few seconds. A row of dots lets you jump to any feature, and the rotation restarts when you do. The top navigation starts transparent over the billboard and condenses into a solid, blurred bar as you scroll.
Beneath the hero, content is organized into horizontally scrollable rows generated entirely in JavaScript: Continue Watching cards carry live progress bars, Trending Now is a ranked Top 10 with oversized outline numerals, and New Releases plus two genre rails round it out. Each poster scales up on hover and reveals a preview card with circular Play / Add-to-list / Like actions, a match percentage, rating chip, runtime, and genres. Per-row arrows page through the rail and disable at the ends.
Everything is vanilla JS with no dependencies: poster art is built from deterministic CSS gradients, rows scroll smoothly with auto-toggling arrow buttons, cards and preview actions fire a shared toast() helper, the hero auto-advances on a timer, and the whole layout reflows down to ~360px. Interactive elements are real <button>s with aria labels, visible focus rings, and keyboard-reachable previews, and all motion is disabled under prefers-reduced-motion.
Illustrative UI only — fictional titles, not a real streaming service.