Music — Browse / Genres / New Releases
A dark, cover-rich music discovery home for a fictional streaming app. A time-aware greeting sits above a New Releases scroller of CSS-drawn album cards with hover-play overlays, a vibrant gradient grid of genre and mood tiles, a Made for you mixes row, and a Top 50 charts preview with movement arrows, play counts and durations. Scrollers support drag and arrow navigation, a Music or Podcasts tab swaps the entire feed, and simulated playback drives a glassy now-playing bar with an animated equalizer and a draggable, keyboard-seekable scrubber.
MCP
代码
:root {
--bg: #0b0b0f;
--bg-2: #13131a;
--surface: #1a1a22;
--surface-2: #22222c;
--text: #f4f4f7;
--muted: #a0a0ad;
--line: rgba(255, 255, 255, 0.10);
--line-2: rgba(255, 255, 255, 0.18);
--accent: #1db954;
--accent-2: #8b5cf6;
--accent-3: #ff3d71;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--r-full: 999px;
--shadow: 0 12px 34px rgba(0, 0, 0, 0.45);
--shadow-sm: 0 6px 18px rgba(0, 0, 0, 0.35);
--display: "Space Grotesk", system-ui, sans-serif;
--body: "Inter", system-ui, sans-serif;
}
* { box-sizing: border-box; }
html, body { margin: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: var(--body);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
padding-bottom: 92px;
}
/* ---------- Top bar ---------- */
.topbar {
position: sticky;
top: 0;
z-index: 30;
display: flex;
align-items: center;
gap: 18px;
padding: 14px clamp(16px, 4vw, 40px);
background: linear-gradient(180deg, rgba(11,11,15,0.96), rgba(11,11,15,0.78));
backdrop-filter: blur(14px);
border-bottom: 1px solid var(--line);
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand-mark {
display: inline-flex;
align-items: flex-end;
gap: 2px;
height: 22px;
padding: 0 2px;
}
.brand-mark i {
width: 3px;
border-radius: 2px;
background: linear-gradient(180deg, var(--accent), var(--accent-2));
animation: brandbar 1.1s ease-in-out infinite;
}
.brand-mark i:nth-child(1) { height: 10px; animation-delay: 0s; }
.brand-mark i:nth-child(2) { height: 20px; animation-delay: .18s; }
.brand-mark i:nth-child(3) { height: 14px; animation-delay: .36s; }
.brand-mark i:nth-child(4) { height: 8px; animation-delay: .54s; }
@keyframes brandbar { 0%,100% { transform: scaleY(.45); } 50% { transform: scaleY(1); } }
.brand-name {
font-family: var(--display);
font-weight: 700;
font-size: 19px;
letter-spacing: -0.01em;
}
.tabs {
display: flex;
gap: 6px;
margin-left: 8px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-full);
padding: 4px;
}
.tab {
border: 0;
background: transparent;
color: var(--muted);
font: 600 13px var(--body);
padding: 7px 16px;
border-radius: var(--r-full);
cursor: pointer;
transition: color .18s, background .18s;
}
.tab:hover { color: var(--text); }
.tab.is-active {
color: #06120b;
background: var(--accent);
}
.topbar-end {
margin-left: auto;
display: flex;
align-items: center;
gap: 12px;
}
.icon-btn {
width: 38px; height: 38px;
display: grid; place-items: center;
border-radius: var(--r-full);
background: var(--surface);
border: 1px solid var(--line);
color: var(--muted);
cursor: pointer;
transition: color .18s, border-color .18s, transform .18s;
}
.icon-btn:hover { color: var(--text); border-color: var(--line-2); transform: translateY(-1px); }
.avatar {
width: 38px; height: 38px;
border-radius: var(--r-full);
border: 0;
background: linear-gradient(135deg, var(--accent-2), var(--accent-3));
color: #fff;
font: 700 13px var(--body);
cursor: pointer;
transition: transform .18s, box-shadow .18s;
}
.avatar:hover { transform: translateY(-1px); box-shadow: var(--shadow-sm); }
/* ---------- Feed ---------- */
.feed {
flex: 1;
width: 100%;
max-width: 1180px;
margin: 0 auto;
padding: clamp(20px, 3vw, 34px) clamp(16px, 4vw, 40px) 60px;
}
.greeting { margin-bottom: 26px; }
.greeting h1 {
font-family: var(--display);
font-weight: 700;
font-size: clamp(26px, 5vw, 40px);
letter-spacing: -0.02em;
margin: 0 0 4px;
}
.greeting-sub { color: var(--muted); margin: 0; font-size: 15px; }
.row { margin-bottom: 38px; }
.row-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px;
}
.row-title {
font-family: var(--display);
font-weight: 700;
font-size: clamp(18px, 2.6vw, 23px);
letter-spacing: -0.01em;
margin: 0;
display: flex;
align-items: center;
gap: 10px;
}
.row-flag {
font: 600 11px var(--body);
color: var(--accent);
background: rgba(29,185,84,0.12);
border: 1px solid rgba(29,185,84,0.3);
padding: 3px 9px;
border-radius: var(--r-full);
letter-spacing: .02em;
}
.row-ctrls { display: flex; gap: 8px; }
.scroll-btn {
width: 34px; height: 34px;
border-radius: var(--r-full);
border: 1px solid var(--line);
background: var(--surface);
color: var(--text);
font-size: 18px;
line-height: 1;
cursor: pointer;
transition: background .18s, transform .18s, opacity .18s;
}
.scroll-btn:hover { background: var(--surface-2); transform: scale(1.06); }
.scroll-btn:disabled { opacity: .3; cursor: default; transform: none; }
.text-btn {
border: 0; background: none;
color: var(--muted);
font: 600 13px var(--body);
cursor: pointer;
letter-spacing: .04em;
text-transform: uppercase;
}
.text-btn:hover { color: var(--text); }
/* ---------- Scrollers ---------- */
.scroller {
display: flex;
gap: 16px;
overflow-x: auto;
scroll-behavior: smooth;
padding: 4px 2px 12px;
scrollbar-width: none;
cursor: grab;
outline: none;
}
.scroller::-webkit-scrollbar { display: none; }
.scroller.dragging { cursor: grabbing; scroll-behavior: auto; }
.scroller.dragging * { pointer-events: none; }
.scroller:focus-visible { box-shadow: 0 0 0 2px var(--accent-2); border-radius: var(--r-md); }
/* ---------- Album / mix card ---------- */
.card {
flex: 0 0 auto;
width: 180px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 12px;
cursor: pointer;
transition: background .2s, transform .2s, border-color .2s;
}
.card:hover { background: var(--surface-2); transform: translateY(-4px); border-color: var(--line-2); }
.cover {
position: relative;
width: 100%;
aspect-ratio: 1;
border-radius: var(--r-sm);
overflow: hidden;
margin-bottom: 12px;
box-shadow: var(--shadow-sm);
}
.cover::before {
content: "";
position: absolute; inset: 0;
background:
radial-gradient(120% 90% at 20% 12%, var(--c1), transparent 60%),
radial-gradient(120% 120% at 85% 90%, var(--c2), transparent 62%),
linear-gradient(135deg, var(--c1), var(--c2));
}
.cover::after {
content: "";
position: absolute; inset: 0;
background-image:
linear-gradient(rgba(255,255,255,.06) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,.06) 1px, transparent 1px);
background-size: 26px 26px;
mask: radial-gradient(120% 90% at 60% 30%, #000, transparent 75%);
opacity: .5;
}
.cover .cover-shape {
position: absolute;
width: 64%; height: 64%;
left: 18%; top: 18%;
border-radius: 50%;
border: 2px solid rgba(255,255,255,.32);
box-shadow: inset 0 0 0 14px rgba(255,255,255,.07);
}
.cover .cover-shape::after {
content: ""; position: absolute;
inset: 38%;
border-radius: 50%;
background: rgba(0,0,0,.55);
box-shadow: 0 0 0 4px rgba(255,255,255,.18);
}
.mix .cover .cover-shape { border-radius: var(--r-sm); }
.play-overlay {
position: absolute;
right: 10px; bottom: 10px;
width: 46px; height: 46px;
border-radius: var(--r-full);
border: 0;
background: var(--accent);
color: #06120b;
display: grid; place-items: center;
cursor: pointer;
opacity: 0;
transform: translateY(8px) scale(.9);
transition: opacity .2s, transform .2s, box-shadow .2s;
box-shadow: 0 8px 20px rgba(29,185,84,.4);
}
.card:hover .play-overlay,
.card:focus-within .play-overlay { opacity: 1; transform: translateY(0) scale(1); }
.play-overlay:hover { transform: scale(1.08); }
.play-overlay .tri {
width: 0; height: 0;
border-left: 13px solid currentColor;
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
margin-left: 3px;
}
.card.is-playing .play-overlay { opacity: 1; transform: none; }
.card.is-playing .play-overlay .tri { display: none; }
.card.is-playing .play-overlay::after {
content: "";
width: 14px; height: 14px;
border-left: 4px solid currentColor;
border-right: 4px solid currentColor;
box-sizing: content-box;
}
.card-title {
font-weight: 700;
font-size: 14px;
margin: 0 0 3px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-sub {
font-size: 12.5px;
color: var(--muted);
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.card-tag {
display: inline-block;
margin-top: 8px;
font: 600 10.5px var(--body);
letter-spacing: .04em;
text-transform: uppercase;
color: var(--accent-2);
background: rgba(139,92,246,.13);
border: 1px solid rgba(139,92,246,.28);
padding: 2px 8px;
border-radius: var(--r-full);
}
/* ---------- Genre grid ---------- */
.genre-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
gap: 14px;
}
.genre {
position: relative;
aspect-ratio: 16 / 10;
border-radius: var(--r-md);
border: 0;
overflow: hidden;
cursor: pointer;
text-align: left;
padding: 14px;
color: #fff;
font-family: var(--display);
font-weight: 700;
font-size: 17px;
letter-spacing: -0.01em;
background: linear-gradient(140deg, var(--c1), var(--c2));
transition: transform .2s, box-shadow .2s;
isolation: isolate;
}
.genre:hover { transform: translateY(-5px) scale(1.02); box-shadow: var(--shadow); }
.genre::after {
content: "";
position: absolute;
right: -18px; bottom: -22px;
width: 92px; height: 92px;
border-radius: 12px;
background: rgba(0,0,0,.35);
transform: rotate(28deg);
box-shadow: -8px -8px 0 rgba(255,255,255,.12);
z-index: -1;
transition: transform .25s;
}
.genre:hover::after { transform: rotate(18deg) translate(-4px, -4px); }
.genre span { position: relative; z-index: 1; text-shadow: 0 1px 8px rgba(0,0,0,.35); }
/* ---------- Charts ---------- */
.chart {
list-style: none;
margin: 0; padding: 0;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
overflow: hidden;
}
.chart-row {
display: grid;
grid-template-columns: 34px 44px 1fr auto auto;
align-items: center;
gap: 14px;
padding: 10px 16px;
cursor: pointer;
border-bottom: 1px solid var(--line);
transition: background .16s;
}
.chart-row:last-child { border-bottom: 0; }
.chart-row:hover { background: var(--surface-2); }
.ch-rank {
font-family: var(--display);
font-weight: 700;
font-size: 17px;
color: var(--muted);
text-align: center;
}
.chart-row.is-playing .ch-rank { color: var(--accent); }
.ch-move {
font-size: 10px;
display: block;
margin-top: 1px;
letter-spacing: .02em;
}
.ch-up { color: var(--accent); }
.ch-down { color: var(--accent-3); }
.ch-flat { color: var(--muted); }
.ch-cover {
width: 44px; height: 44px;
border-radius: var(--r-sm);
position: relative; overflow: hidden;
}
.ch-cover::before {
content: ""; position: absolute; inset: 0;
background: linear-gradient(135deg, var(--c1), var(--c2));
}
.ch-info { min-width: 0; }
.ch-title {
font-weight: 600; font-size: 14.5px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
display: flex; align-items: center; gap: 8px;
}
.ch-artist {
font-size: 12.5px; color: var(--muted);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ch-plays {
font-size: 12.5px; color: var(--muted);
font-variant-numeric: tabular-nums;
}
.ch-dur {
font-size: 12.5px; color: var(--muted);
font-variant-numeric: tabular-nums;
}
/* equalizer (active chart row + nowbar) */
.eq {
display: inline-flex;
align-items: flex-end;
gap: 2px;
height: 13px;
}
.eq i {
width: 3px;
background: var(--accent);
border-radius: 2px;
animation: eq 0.9s ease-in-out infinite;
}
.eq i:nth-child(1) { height: 40%; animation-delay: 0s; }
.eq i:nth-child(2) { height: 90%; animation-delay: .15s; }
.eq i:nth-child(3) { height: 60%; animation-delay: .3s; }
.eq i:nth-child(4) { height: 100%; animation-delay: .45s; }
@keyframes eq { 0%,100% { transform: scaleY(.35); } 50% { transform: scaleY(1); } }
/* ---------- Now playing bar ---------- */
.nowbar {
position: fixed;
left: 0; right: 0; bottom: 0;
z-index: 40;
display: grid;
grid-template-columns: 56px minmax(0, 1.4fr) auto auto auto 2.2fr;
align-items: center;
gap: 14px;
padding: 12px clamp(16px, 4vw, 40px);
background: linear-gradient(180deg, rgba(20,20,28,0.86), rgba(13,13,18,0.98));
backdrop-filter: blur(18px);
border-top: 1px solid var(--line);
box-shadow: 0 -10px 30px rgba(0,0,0,.4);
}
.nb-art {
width: 52px; height: 52px;
border-radius: var(--r-sm);
position: relative; overflow: hidden;
box-shadow: var(--shadow-sm);
}
.nb-art::before {
content: ""; position: absolute; inset: 0;
background: linear-gradient(135deg, var(--c1, var(--accent)), var(--c2, var(--accent-2)));
}
.nb-meta { min-width: 0; display: flex; flex-direction: column; }
.nb-title { font-weight: 700; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.nb-artist { font-size: 12.5px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.nb-eq { display: inline-flex; align-items: flex-end; gap: 3px; height: 18px; }
.nb-eq i {
width: 3px; border-radius: 2px;
background: linear-gradient(180deg, var(--accent), var(--accent-2));
animation: eq .85s ease-in-out infinite;
}
.nb-eq i:nth-child(1){ height: 8px; animation-delay: 0s;}
.nb-eq i:nth-child(2){ height: 18px; animation-delay: .14s;}
.nb-eq i:nth-child(3){ height: 12px; animation-delay: .28s;}
.nb-eq i:nth-child(4){ height: 16px; animation-delay: .42s;}
.nowbar.paused .nb-eq i { animation-play-state: paused; }
.nb-like {
width: 38px; height: 38px;
border-radius: var(--r-full);
border: 1px solid var(--line);
background: var(--surface);
color: var(--muted);
cursor: pointer;
display: grid; place-items: center;
transition: color .18s, transform .18s, border-color .18s;
}
.nb-like:hover { color: var(--text); transform: scale(1.06); }
.nb-like[aria-pressed="true"] { color: var(--accent-3); border-color: rgba(255,61,113,.4); }
.nb-like[aria-pressed="true"] svg path { fill: var(--accent-3); }
.nb-play {
width: 46px; height: 46px;
border-radius: var(--r-full);
border: 0;
background: var(--text);
color: #0b0b0f;
cursor: pointer;
display: grid; place-items: center;
transition: transform .18s, box-shadow .18s;
}
.nb-play:hover { transform: scale(1.07); box-shadow: 0 8px 22px rgba(255,255,255,.18); }
.ic-play {
width: 0; height: 0;
border-left: 14px solid currentColor;
border-top: 9px solid transparent;
border-bottom: 9px solid transparent;
margin-left: 3px;
}
.ic-pause { display: none; gap: 4px; }
.ic-pause i { width: 4px; height: 16px; background: currentColor; border-radius: 1px; }
.nb-play[aria-pressed="true"] .ic-play { display: none; }
.nb-play[aria-pressed="true"] .ic-pause { display: flex; }
.nb-scrub {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.nb-time {
font-size: 11.5px; color: var(--muted);
font-variant-numeric: tabular-nums;
min-width: 34px; text-align: center;
}
.scrub {
position: relative;
flex: 1;
height: 6px;
border-radius: var(--r-full);
background: var(--surface-2);
cursor: pointer;
outline: none;
}
.scrub:focus-visible { box-shadow: 0 0 0 3px rgba(139,92,246,.5); }
.scrub-fill {
position: absolute; left: 0; top: 0; bottom: 0;
width: 0%;
border-radius: var(--r-full);
background: linear-gradient(90deg, var(--accent), var(--accent-2));
}
.scrub-knob {
position: absolute; top: 50%;
left: 0%;
width: 13px; height: 13px;
border-radius: 50%;
background: #fff;
transform: translate(-50%, -50%);
box-shadow: 0 2px 8px rgba(0,0,0,.5);
opacity: 0;
transition: opacity .15s;
}
.scrub:hover .scrub-knob,
.scrub:focus-visible .scrub-knob { opacity: 1; }
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%; bottom: 108px;
transform: translate(-50%, 16px);
background: var(--surface-2);
border: 1px solid var(--line-2);
color: var(--text);
font-size: 13.5px;
font-weight: 500;
padding: 11px 18px;
border-radius: var(--r-full);
box-shadow: var(--shadow);
opacity: 0;
pointer-events: none;
transition: opacity .25s, transform .25s;
z-index: 60;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- Responsive ---------- */
@media (max-width: 760px) {
.nowbar { grid-template-columns: 52px minmax(0,1fr) auto auto; }
.nb-eq, .nb-scrub { display: none; }
}
@media (max-width: 520px) {
.topbar { gap: 10px; padding: 12px 16px; }
.brand-name { display: none; }
.tabs { margin-left: 0; }
.tab { padding: 6px 12px; font-size: 12.5px; }
.feed { padding: 18px 16px 50px; }
.card { width: 150px; }
.genre-grid { grid-template-columns: 1fr 1fr; gap: 10px; }
.genre { font-size: 15px; }
.chart-row {
grid-template-columns: 26px 40px 1fr auto;
gap: 10px; padding: 9px 12px;
}
.ch-plays { display: none; }
.nowbar { gap: 10px; padding: 10px 14px; }
.toast { bottom: 96px; }
}
@media (prefers-reduced-motion: reduce) {
*, .scroller { scroll-behavior: auto !important; animation: none !important; transition-duration: .01ms !important; }
.eq i, .nb-eq i, .brand-mark i { animation: none !important; }
}
/* Visibility guard: honor the [hidden] attribute over base display */
.nowbar[hidden] {
display: none;
}(function () {
"use strict";
/* ----------------------------------------------------------------
* Fictional data
* ---------------------------------------------------------------- */
var PALETTES = {
sunset: ["#ff7a59", "#8b5cf6"],
deep: ["#1db954", "#0e7c5a"],
violet: ["#8b5cf6", "#22d3ee"],
blush: ["#ff3d71", "#ffb86b"],
ocean: ["#2563eb", "#22d3ee"],
ember: ["#f97316", "#ff3d71"],
mint: ["#10b981", "#84cc16"],
dusk: ["#6366f1", "#ec4899"],
gold: ["#facc15", "#f97316"],
night: ["#334155", "#0ea5e9"]
};
var MUSIC = {
new: [
{ title: "Midnight Reservoir", sub: "Neon Tides", dur: 222, pal: "sunset" },
{ title: "Velvet Static", sub: "The Paper Lanterns", dur: 198, pal: "violet" },
{ title: "Glass Harbor", sub: "Mara Quill", dur: 244, pal: "ocean" },
{ title: "Slow Comet", sub: "Halcyon Drift", dur: 211, pal: "blush" },
{ title: "Amber Frequency", sub: "Lou Verano", dur: 187, pal: "gold" },
{ title: "Northern Ghosts", sub: "Coastline", dur: 256, pal: "night" },
{ title: "Cassette Heaven", sub: "Bloom & Static", dur: 203, pal: "dusk" },
{ title: "Saltwater Radio", sub: "Wren Avila", dur: 229, pal: "mint" }
],
mixes: [
{ title: "Daily Mix 1", sub: "Neon Tides, Coastline, Mara Quill +", dur: 0, pal: "deep", tag: "Mix" },
{ title: "Night Drive", sub: "Made for Riva — synth & dream pop", dur: 0, pal: "dusk", tag: "Mix" },
{ title: "Deep Focus", sub: "Beatless textures to stay locked in", dur: 0, pal: "ocean", tag: "Mix" },
{ title: "On Repeat", sub: "The songs you can't stop playing", dur: 0, pal: "ember", tag: "Mix" },
{ title: "Time Capsule", sub: "Throwbacks picked for you", dur: 0, pal: "gold", tag: "Mix" },
{ title: "Discover Weekly", sub: "Fresh finds, every Monday", dur: 0, pal: "violet", tag: "Mix" }
],
charts: [
{ title: "Paper Lanterns", artist: "Neon Tides", plays: "48.2M", dur: 201, move: "up", pal: "sunset" },
{ title: "Velvet Static", artist: "The Paper Lanterns", plays: "41.9M", dur: 198, move: "flat", pal: "violet" },
{ title: "Glass Harbor", artist: "Mara Quill", plays: "37.4M", dur: 244, move: "up", pal: "ocean" },
{ title: "Slow Comet", artist: "Halcyon Drift", plays: "33.1M", dur: 211, move: "down", pal: "blush" },
{ title: "Saltwater Radio", artist: "Wren Avila", plays: "29.8M", dur: 229, move: "up", pal: "mint" },
{ title: "Amber Frequency", artist: "Lou Verano", plays: "26.0M", dur: 187, move: "down", pal: "gold" },
{ title: "Northern Ghosts", artist: "Coastline", plays: "24.5M", dur: 256, move: "flat", pal: "night" },
{ title: "Cassette Heaven", artist: "Bloom & Static", plays: "22.7M", dur: 203, move: "up", pal: "dusk" }
]
};
var PODCASTS = {
new: [
{ title: "Signal & Noise", sub: "Ep. 142 — The synth that ate pop", dur: 2940, pal: "ocean" },
{ title: "Quiet Riot Hours", sub: "Late-night talk for night owls", dur: 3360, pal: "dusk" },
{ title: "Field Notes", sub: "Ep. 58 — Recording the Arctic", dur: 2580, pal: "mint" },
{ title: "The Long Cut", sub: "Filmmakers, unscripted", dur: 4020, pal: "ember" },
{ title: "Stack Overflow Coffee", sub: "Dev stories before standup", dur: 1860, pal: "deep" },
{ title: "Ghost Frequencies", sub: "Audio mysteries, retold", dur: 3120, pal: "night" }
],
mixes: [
{ title: "Your Daily Drive", sub: "News + the shows you follow", dur: 0, pal: "violet", tag: "Playlist" },
{ title: "Wind Down", sub: "Calm voices for the evening", dur: 0, pal: "blush", tag: "Playlist" },
{ title: "Commute Boost", sub: "Short episodes under 25 min", dur: 0, pal: "gold", tag: "Playlist" },
{ title: "Deep Dives", sub: "Long-form investigations", dur: 0, pal: "ocean", tag: "Playlist" },
{ title: "Comedy Queue", sub: "Picked to make you laugh", dur: 0, pal: "ember", tag: "Playlist" }
],
charts: [
{ title: "The synth that ate pop", artist: "Signal & Noise", plays: "1.2M", dur: 2940, move: "up", pal: "ocean" },
{ title: "Recording the Arctic", artist: "Field Notes", plays: "980K", dur: 2580, move: "up", pal: "mint" },
{ title: "Filmmakers, unscripted", artist: "The Long Cut", plays: "874K", dur: 4020, move: "flat", pal: "ember" },
{ title: "Audio mysteries, retold", artist: "Ghost Frequencies", plays: "812K", dur: 3120, move: "down", pal: "night" },
{ title: "Dev stories before standup", artist: "Stack Overflow Coffee", plays: "760K", dur: 1860, move: "up", pal: "deep" },
{ title: "Late-night talk", artist: "Quiet Riot Hours", plays: "688K", dur: 3360, move: "flat", pal: "dusk" }
]
};
var GENRES = [
{ name: "Pop", pal: "sunset" },
{ name: "Hip-Hop", pal: "ember" },
{ name: "Chill", pal: "ocean" },
{ name: "Workout", pal: "blush" },
{ name: "Focus", pal: "deep" },
{ name: "Party", pal: "dusk" },
{ name: "Indie", pal: "mint" },
{ name: "R&B", pal: "violet" },
{ name: "Electronic", pal: "night" },
{ name: "Jazz", pal: "gold" }
];
/* ----------------------------------------------------------------
* Helpers
* ---------------------------------------------------------------- */
function fmt(sec) {
sec = Math.max(0, Math.round(sec));
var m = Math.floor(sec / 60);
var s = sec % 60;
return m + ":" + (s < 10 ? "0" : "") + s;
}
function pal(name) { return PALETTES[name] || PALETTES.deep; }
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2200);
}
function coverVars(p) {
var c = pal(p);
return "--c1:" + c[0] + ";--c2:" + c[1] + ";";
}
/* ----------------------------------------------------------------
* Card builders
* ---------------------------------------------------------------- */
function buildCard(item, kind) {
var card = document.createElement("article");
card.className = "card" + (kind === "mix" ? " mix" : "");
card.dataset.title = item.title;
card.dataset.sub = item.sub;
card.dataset.pal = item.pal;
var cover = document.createElement("div");
cover.className = "cover";
cover.setAttribute("style", coverVars(item.pal));
cover.innerHTML = '<span class="cover-shape"></span>' +
'<button class="play-overlay" aria-label="Play ' + item.title + '" aria-pressed="false"><span class="tri"></span></button>';
card.appendChild(cover);
var title = document.createElement("p");
title.className = "card-title";
title.textContent = item.title;
card.appendChild(title);
var sub = document.createElement("p");
sub.className = "card-sub";
sub.textContent = item.sub;
card.appendChild(sub);
if (item.tag) {
var tag = document.createElement("span");
tag.className = "card-tag";
tag.textContent = item.tag;
card.appendChild(tag);
}
var play = cover.querySelector(".play-overlay");
play.addEventListener("click", function (e) {
e.stopPropagation();
playTrack({ title: item.title, artist: item.sub, dur: item.dur || 210, pal: item.pal }, card);
});
card.addEventListener("click", function () {
toast("Opening " + item.title);
});
return card;
}
function buildGenre(g) {
var btn = document.createElement("button");
btn.className = "genre";
btn.setAttribute("style", coverVars(g.pal));
btn.innerHTML = "<span>" + g.name + "</span>";
btn.addEventListener("click", function () { toast("Browsing " + g.name); });
return btn;
}
function buildChartRow(item, rank) {
var li = document.createElement("li");
li.className = "chart-row";
li.dataset.title = item.title;
var moveSym = item.move === "up" ? "▲" : item.move === "down" ? "▼" : "—";
var moveCls = item.move === "up" ? "ch-up" : item.move === "down" ? "ch-down" : "ch-flat";
li.innerHTML =
'<div class="ch-rank">' + rank +
'<span class="ch-move ' + moveCls + '">' + moveSym + '</span></div>' +
'<div class="ch-cover" style="' + coverVars(item.pal) + '"></div>' +
'<div class="ch-info">' +
'<div class="ch-title"><span class="ch-name"></span></div>' +
'<div class="ch-artist"></div>' +
'</div>' +
'<div class="ch-plays">' + item.plays + '</div>' +
'<div class="ch-dur">' + fmt(item.dur) + '</div>';
li.querySelector(".ch-name").textContent = item.title;
li.querySelector(".ch-artist").textContent = item.artist;
li.addEventListener("click", function () {
playTrack({ title: item.title, artist: item.artist, dur: item.dur, pal: item.pal }, null);
markChartPlaying(li);
});
return li;
}
/* ----------------------------------------------------------------
* Render a feed (music | podcasts)
* ---------------------------------------------------------------- */
var scrollerEls = document.querySelectorAll('[data-row="new"] .scroller, [data-row="mixes"] .scroller');
function renderFeed(data) {
var newScroller = document.querySelector('[data-row="new"] .scroller');
var mixScroller = document.querySelector('[data-row="mixes"] .scroller');
var chartList = document.getElementById("chartList");
newScroller.innerHTML = "";
data.new.forEach(function (it) { newScroller.appendChild(buildCard(it, "album")); });
mixScroller.innerHTML = "";
data.mixes.forEach(function (it) { mixScroller.appendChild(buildCard(it, "mix")); });
chartList.innerHTML = "";
data.charts.forEach(function (it, i) { chartList.appendChild(buildChartRow(it, i + 1)); });
updateArrows();
}
function markChartPlaying(row) {
document.querySelectorAll(".chart-row.is-playing").forEach(function (r) {
r.classList.remove("is-playing");
var eq = r.querySelector(".eq");
if (eq) eq.remove();
var dur = r.querySelector(".ch-dur");
if (dur) dur.style.display = "";
});
if (row) {
row.classList.add("is-playing");
var title = row.querySelector(".ch-title");
if (!title.querySelector(".eq")) {
var eq = document.createElement("span");
eq.className = "eq";
eq.innerHTML = "<i></i><i></i><i></i><i></i>";
title.appendChild(eq);
}
}
}
/* Build genres once */
var genreGrid = document.getElementById("genreGrid");
GENRES.forEach(function (g) { genreGrid.appendChild(buildGenre(g)); });
/* ----------------------------------------------------------------
* Tab switching
* ---------------------------------------------------------------- */
var greetSub = document.getElementById("greetSub");
document.querySelectorAll(".tab").forEach(function (tab) {
tab.addEventListener("click", function () {
if (tab.classList.contains("is-active")) return;
document.querySelectorAll(".tab").forEach(function (t) {
t.classList.remove("is-active");
t.setAttribute("aria-selected", "false");
});
tab.classList.add("is-active");
tab.setAttribute("aria-selected", "true");
var feed = tab.dataset.feed;
renderFeed(feed === "podcasts" ? PODCASTS : MUSIC);
greetSub.textContent = feed === "podcasts"
? "Shows and episodes picked for you."
: "Picked for your night drive.";
var fe = document.getElementById("feed");
fe.animate(
[{ opacity: 0, transform: "translateY(8px)" }, { opacity: 1, transform: "none" }],
{ duration: 280, easing: "ease" }
);
});
});
/* ----------------------------------------------------------------
* Horizontal scrollers — arrows + drag
* ---------------------------------------------------------------- */
function updateArrows() {
document.querySelectorAll('[data-row="new"], [data-row="mixes"]').forEach(function (row) {
var sc = row.querySelector(".scroller");
if (!sc) return;
var btns = row.querySelectorAll(".scroll-btn");
if (!btns.length) return;
var maxScroll = sc.scrollWidth - sc.clientWidth - 2;
btns.forEach(function (b) {
if (b.dataset.dir === "-1") b.disabled = sc.scrollLeft <= 2;
else b.disabled = sc.scrollLeft >= maxScroll;
});
});
}
document.querySelectorAll(".scroll-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
var sc = btn.closest(".row").querySelector(".scroller");
var amount = (sc.clientWidth * 0.8) * parseInt(btn.dataset.dir, 10);
sc.scrollBy({ left: amount, behavior: "smooth" });
});
});
document.querySelectorAll('[data-scroller]').forEach(function (sc) {
sc.addEventListener("scroll", updateArrows, { passive: true });
var down = false, startX = 0, startScroll = 0, moved = 0;
sc.addEventListener("pointerdown", function (e) {
if (e.button !== 0) return;
down = true; moved = 0;
startX = e.clientX;
startScroll = sc.scrollLeft;
sc.setPointerCapture(e.pointerId);
});
sc.addEventListener("pointermove", function (e) {
if (!down) return;
var dx = e.clientX - startX;
if (Math.abs(dx) > 4) sc.classList.add("dragging");
moved += Math.abs(dx);
sc.scrollLeft = startScroll - dx;
});
function end(e) {
if (!down) return;
down = false;
sc.classList.remove("dragging");
try { sc.releasePointerCapture(e.pointerId); } catch (_) {}
}
sc.addEventListener("pointerup", end);
sc.addEventListener("pointercancel", end);
/* keyboard scroll */
sc.addEventListener("keydown", function (e) {
if (e.key === "ArrowRight") { sc.scrollBy({ left: 200, behavior: "smooth" }); e.preventDefault(); }
if (e.key === "ArrowLeft") { sc.scrollBy({ left: -200, behavior: "smooth" }); e.preventDefault(); }
});
});
window.addEventListener("resize", updateArrows);
/* ----------------------------------------------------------------
* Simulated playback + now-playing bar
* ---------------------------------------------------------------- */
var nowbar = document.getElementById("nowbar");
var nbArt = document.getElementById("nbArt");
var nbTitle = document.getElementById("nbTitle");
var nbArtist = document.getElementById("nbArtist");
var nbPlay = document.getElementById("nbPlay");
var nbLike = document.getElementById("nbLike");
var nbCur = document.getElementById("nbCur");
var nbDur = document.getElementById("nbDur");
var nbScrub = document.getElementById("nbScrub");
var nbFill = document.getElementById("nbFill");
var nbKnob = document.getElementById("nbKnob");
var current = null; // { title, artist, dur, pal }
var elapsed = 0;
var playing = false;
var timer = null;
var activeCard = null;
function renderProgress() {
var d = current ? current.dur : 1;
var pct = Math.min(100, (elapsed / d) * 100);
nbFill.style.width = pct + "%";
nbKnob.style.left = pct + "%";
nbCur.textContent = fmt(elapsed);
nbScrub.setAttribute("aria-valuenow", Math.round(pct));
nbScrub.setAttribute("aria-valuetext", fmt(elapsed) + " of " + fmt(d));
}
function tick() {
if (!playing || !current) return;
elapsed += 1;
if (elapsed >= current.dur) {
elapsed = current.dur;
renderProgress();
pause();
toast("Track finished");
return;
}
renderProgress();
}
function startTimer() {
clearInterval(timer);
timer = setInterval(tick, 1000);
}
function setActiveCard(card) {
if (activeCard && activeCard !== card) {
activeCard.classList.remove("is-playing");
var ov = activeCard.querySelector(".play-overlay");
if (ov) ov.setAttribute("aria-pressed", "false");
}
activeCard = card || null;
if (activeCard) {
activeCard.classList.add("is-playing");
var ov2 = activeCard.querySelector(".play-overlay");
if (ov2) ov2.setAttribute("aria-pressed", "true");
}
}
function playTrack(track, card) {
current = track;
elapsed = 0;
nowbar.hidden = false;
var c = pal(track.pal);
nbArt.setAttribute("style", "--c1:" + c[0] + ";--c2:" + c[1] + ";");
nbTitle.textContent = track.title;
nbArtist.textContent = track.artist;
nbDur.textContent = fmt(track.dur);
nbLike.setAttribute("aria-pressed", "false");
setActiveCard(card);
play();
renderProgress();
toast("Now playing — " + track.title);
}
function play() {
if (!current) return;
playing = true;
nbPlay.setAttribute("aria-pressed", "true");
nbPlay.setAttribute("aria-label", "Pause");
nowbar.classList.remove("paused");
startTimer();
}
function pause() {
playing = false;
nbPlay.setAttribute("aria-pressed", "false");
nbPlay.setAttribute("aria-label", "Play");
nowbar.classList.add("paused");
clearInterval(timer);
}
nbPlay.addEventListener("click", function () {
if (!current) return;
if (elapsed >= current.dur) elapsed = 0;
playing ? pause() : play();
});
nbLike.addEventListener("click", function () {
var on = nbLike.getAttribute("aria-pressed") === "true";
nbLike.setAttribute("aria-pressed", String(!on));
toast(on ? "Removed from your library" : "Saved to your library");
});
/* Scrubber: click + drag + keyboard */
function seekFromClientX(clientX) {
if (!current) return;
var rect = nbScrub.getBoundingClientRect();
var ratio = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
elapsed = Math.round(ratio * current.dur);
renderProgress();
}
var scrubbing = false;
nbScrub.addEventListener("pointerdown", function (e) {
if (!current) return;
scrubbing = true;
nbScrub.setPointerCapture(e.pointerId);
seekFromClientX(e.clientX);
});
nbScrub.addEventListener("pointermove", function (e) {
if (scrubbing) seekFromClientX(e.clientX);
});
nbScrub.addEventListener("pointerup", function (e) {
scrubbing = false;
try { nbScrub.releasePointerCapture(e.pointerId); } catch (_) {}
});
nbScrub.addEventListener("keydown", function (e) {
if (!current) return;
var step = 5;
if (e.key === "ArrowRight") { elapsed = Math.min(current.dur, elapsed + step); renderProgress(); e.preventDefault(); }
else if (e.key === "ArrowLeft") { elapsed = Math.max(0, elapsed - step); renderProgress(); e.preventDefault(); }
else if (e.key === "Home") { elapsed = 0; renderProgress(); e.preventDefault(); }
else if (e.key === "End") { elapsed = current.dur; renderProgress(); e.preventDefault(); }
});
/* ----------------------------------------------------------------
* Misc top-bar actions
* ---------------------------------------------------------------- */
document.getElementById("searchBtn").addEventListener("click", function () { toast("Search coming soon"); });
document.getElementById("avatarBtn").addEventListener("click", function () { toast("Signed in as Riva V."); });
document.getElementById("chartsMore").addEventListener("click", function () { toast("Opening full Top 50"); });
/* ----------------------------------------------------------------
* Greeting by time of day
* ---------------------------------------------------------------- */
(function () {
var h = new Date().getHours();
var g = h < 12 ? "Good morning" : h < 18 ? "Good afternoon" : "Good evening";
document.getElementById("greetTitle").textContent = g;
})();
/* Boot */
renderFeed(MUSIC);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Music — 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=Space+Grotesk:wght@500;600;700&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="app">
<!-- Top bar -->
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<i></i><i></i><i></i><i></i>
</span>
<span class="brand-name">Resonant</span>
</div>
<div class="tabs" role="tablist" aria-label="Feed type">
<button class="tab is-active" role="tab" aria-selected="true" data-feed="music">Music</button>
<button class="tab" role="tab" aria-selected="false" data-feed="podcasts">Podcasts</button>
</div>
<div class="topbar-end">
<button class="icon-btn" id="searchBtn" aria-label="Search">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><circle cx="11" cy="11" r="7" fill="none" stroke="currentColor" stroke-width="2"/><line x1="16.5" y1="16.5" x2="21" y2="21" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
</button>
<button class="avatar" id="avatarBtn" aria-label="Your profile">RV</button>
</div>
</header>
<main class="feed" id="feed">
<!-- Greeting -->
<section class="greeting">
<h1 id="greetTitle">Good evening</h1>
<p class="greeting-sub" id="greetSub">Picked for your night drive.</p>
</section>
<!-- New Releases -->
<section class="row" data-row="new">
<div class="row-head">
<h2 class="row-title">New Releases</h2>
<div class="row-ctrls">
<button class="scroll-btn" data-dir="-1" aria-label="Scroll left">‹</button>
<button class="scroll-btn" data-dir="1" aria-label="Scroll right">›</button>
</div>
</div>
<div class="scroller" data-scroller tabindex="0" aria-label="New releases, drag to scroll">
<!-- album cards injected -->
</div>
</section>
<!-- Genres / moods -->
<section class="row" data-row="genres">
<div class="row-head">
<h2 class="row-title">Genres & moods</h2>
</div>
<div class="genre-grid" id="genreGrid"><!-- tiles injected --></div>
</section>
<!-- Made for you -->
<section class="row" data-row="mixes">
<div class="row-head">
<h2 class="row-title">Made for you</h2>
<div class="row-ctrls">
<button class="scroll-btn" data-dir="-1" aria-label="Scroll left">‹</button>
<button class="scroll-btn" data-dir="1" aria-label="Scroll right">›</button>
</div>
</div>
<div class="scroller" data-scroller tabindex="0" aria-label="Made for you mixes, drag to scroll">
<!-- mix cards injected -->
</div>
</section>
<!-- Charts -->
<section class="row" data-row="charts">
<div class="row-head">
<h2 class="row-title">Charts · Top 50 <span class="row-flag">Global</span></h2>
<button class="text-btn" id="chartsMore">See all</button>
</div>
<ol class="chart" id="chartList"><!-- chart rows injected --></ol>
</section>
</main>
<!-- Now playing bar -->
<footer class="nowbar" id="nowbar" hidden>
<div class="nb-art" id="nbArt"></div>
<div class="nb-meta">
<span class="nb-title" id="nbTitle">—</span>
<span class="nb-artist" id="nbArtist">—</span>
</div>
<div class="nb-eq" id="nbEq" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
<button class="nb-like" id="nbLike" aria-pressed="false" aria-label="Save to your library">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M12 21s-7.5-4.6-10-9.2C.4 8.4 1.9 5 5.2 5c2 0 3.3 1.2 3.8 2.2C9.5 6.2 10.8 5 12.8 5 16.1 5 17.6 8.4 16 11.8 13.5 16.4 12 21 12 21z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg>
</button>
<button class="nb-play" id="nbPlay" aria-pressed="false" aria-label="Play">
<span class="ic-play"></span>
<span class="ic-pause"><i></i><i></i></span>
</button>
<div class="nb-scrub">
<span class="nb-time" id="nbCur">0:00</span>
<div class="scrub" id="nbScrub" role="slider" tabindex="0" aria-label="Seek" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
<div class="scrub-fill" id="nbFill"></div>
<div class="scrub-knob" id="nbKnob"></div>
</div>
<span class="nb-time" id="nbDur">0:00</span>
</div>
</footer>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
</div>
<script src="script.js"></script>
</body>
</html>Browse / Genres / New Releases
A Spotify-home style discover page for the fictional app Resonant. A time-aware greeting opens the feed, followed by a horizontal New Releases scroller of album cards whose artwork is drawn entirely in CSS — layered gradients, a vinyl-style shape and a fading grid — themed from a per-card accent palette so each cover feels distinct. Hovering a card lifts it and reveals a green play overlay; clicking play starts simulated playback and marks the card as the active, now-playing tile.
Below the releases sits a vibrant Genres & moods grid of gradient tiles (Pop, Hip-Hop, Chill, Workout, Focus, Party, Indie, R&B, Electronic, Jazz) that lift and tilt on hover, a Made for you mixes row, and a Charts · Top 50 preview list with rank movement arrows, play counts and durations. Selecting a chart row plays it and animates an equalizer beside the title.
Every scroller works by dragging with the pointer or with the prev/next arrows, which auto-disable at each end, and is keyboard-scrollable. A Music / Podcasts tab swaps the entire feed — releases, mixes and charts — with a soft fade. Playback is fully simulated with timers: a glassy now-playing bar tracks elapsed time, exposes a draggable role="slider" scrubber with ArrowLeft/ArrowRight, Home and End seeking, and offers play/pause and like toggles that use aria-pressed. A small toast() helper confirms actions throughout.
Illustrative UI only — fictional artists, albums, tracks, and data. No real audio playback.