Music — Discography Grid (albums · singles)
A dark, cover-forward discography page for the fictional artist Neon Tides. A glowing header pairs the artist name with monthly listeners, a shuffle-play button and a follow toggle, then release-type tabs (All, Albums, Singles & EPs, Compilations) sit beside a Newest/Oldest/Most-played sort and a grid/list view switch. A responsive grid of fully CSS-drawn covers shows title, year, type badge, runtime and play counts, with a hover play overlay that themes each card to its cover accent and animates an equalizer on the active release. Playback is fully simulated.
MCP
Код
:root {
--bg: #0b0b0f;
--bg-2: #13131a;
--surface: #1a1a22;
--surface-2: #22222c;
--text: #f4f4f7;
--muted: #a0a0ad;
--line: rgba(255, 255, 255, 0.1);
--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 18px 40px -18px rgba(0, 0, 0, 0.7);
--shadow-sm: 0 6px 18px -10px rgba(0, 0, 0, 0.6);
--display: "Space Grotesk", "Inter", system-ui, sans-serif;
--body: "Inter", system-ui, -apple-system, sans-serif;
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
}
body {
background:
radial-gradient(1100px 600px at 85% -10%, rgba(139, 92, 246, 0.18), transparent 60%),
radial-gradient(900px 500px at -5% 0%, rgba(29, 185, 84, 0.12), transparent 55%),
var(--bg);
color: var(--text);
font-family: var(--body);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
.page {
max-width: 1120px;
margin: 0 auto;
padding: 28px 22px 140px;
}
.ic {
width: 18px;
height: 18px;
fill: currentColor;
}
/* ---------- Hero ---------- */
.hero {
position: relative;
display: flex;
align-items: flex-end;
gap: 26px;
padding: 38px;
border-radius: var(--r-lg);
overflow: hidden;
background: linear-gradient(180deg, var(--surface), var(--bg-2));
border: 1px solid var(--line);
box-shadow: var(--shadow);
min-height: 240px;
}
.hero__art {
position: absolute;
inset: 0;
z-index: 0;
filter: blur(2px);
opacity: 0.85;
}
.hero__blob {
position: absolute;
border-radius: 50%;
mix-blend-mode: screen;
animation: float 14s ease-in-out infinite;
}
.hero__blob--a {
width: 360px;
height: 360px;
top: -120px;
right: -60px;
background: radial-gradient(circle at 30% 30%, #ff3d71, transparent 70%);
}
.hero__blob--b {
width: 320px;
height: 320px;
bottom: -140px;
left: 10%;
background: radial-gradient(circle at 50% 50%, #8b5cf6, transparent 70%);
animation-delay: -4s;
}
.hero__blob--c {
width: 280px;
height: 280px;
top: 20%;
left: 45%;
background: radial-gradient(circle at 50% 50%, #1db954, transparent 70%);
animation-delay: -8s;
}
@keyframes float {
0%,
100% {
transform: translate3d(0, 0, 0) scale(1);
}
50% {
transform: translate3d(0, -22px, 0) scale(1.08);
}
}
.hero__body {
position: relative;
z-index: 1;
}
.hero__eyebrow {
margin: 0 0 8px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: #d7c8ff;
}
.hero__name {
margin: 0;
font-family: var(--display);
font-weight: 700;
font-size: clamp(40px, 9vw, 84px);
line-height: 0.96;
letter-spacing: -0.02em;
text-shadow: 0 8px 30px rgba(0, 0, 0, 0.45);
}
.hero__meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin: 14px 0 0;
color: var(--muted);
font-size: 14px;
font-weight: 500;
}
.hero__meta strong {
color: var(--text);
}
.dot {
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--muted);
}
.hero__actions {
display: flex;
gap: 12px;
margin-top: 22px;
}
/* ---------- Buttons ---------- */
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
border: 1px solid transparent;
border-radius: var(--r-full);
padding: 11px 20px;
font-family: var(--body);
font-size: 14px;
font-weight: 700;
cursor: pointer;
transition: transform 0.12s ease, background 0.18s ease, box-shadow 0.18s ease;
}
.btn:active {
transform: scale(0.97);
}
.btn--accent {
background: var(--accent);
color: #04130a;
box-shadow: 0 10px 26px -10px rgba(29, 185, 84, 0.7);
}
.btn--accent:hover {
background: #1ed760;
}
.btn--ghost {
background: rgba(255, 255, 255, 0.04);
border-color: var(--line-2);
color: var(--text);
}
.btn--ghost:hover {
background: rgba(255, 255, 255, 0.09);
}
.btn--ghost[aria-pressed="true"] {
background: var(--text);
color: #0b0b0f;
border-color: var(--text);
}
/* ---------- Controls ---------- */
.controls {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 16px;
margin: 26px 0 18px;
}
.tabs {
display: flex;
gap: 6px;
flex-wrap: wrap;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-full);
padding: 5px;
}
.tab {
border: 0;
background: transparent;
color: var(--muted);
font-family: var(--body);
font-size: 13.5px;
font-weight: 600;
padding: 8px 16px;
border-radius: var(--r-full);
cursor: pointer;
transition: color 0.15s ease, background 0.15s ease;
white-space: nowrap;
}
.tab:hover {
color: var(--text);
}
.tab.is-active {
background: var(--text);
color: #0b0b0f;
}
.tab__count {
display: inline-block;
margin-left: 4px;
font-size: 11px;
font-weight: 700;
opacity: 0.65;
}
.controls__right {
display: flex;
align-items: center;
gap: 12px;
}
.sort {
display: inline-flex;
align-items: center;
gap: 8px;
}
.sort__label {
font-size: 12px;
font-weight: 600;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.08em;
}
#sortSelect {
appearance: none;
background: var(--surface) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23a0a0ad' stroke-width='3' stroke-linecap='round'><path d='M6 9l6 6 6-6'/></svg>")
no-repeat right 12px center;
border: 1px solid var(--line-2);
color: var(--text);
font-family: var(--body);
font-size: 13.5px;
font-weight: 600;
padding: 9px 34px 9px 14px;
border-radius: var(--r-md);
cursor: pointer;
}
#sortSelect:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
}
.viewtoggle {
display: inline-flex;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 4px;
gap: 3px;
}
.viewtoggle__btn {
display: grid;
place-items: center;
width: 34px;
height: 32px;
border: 0;
border-radius: var(--r-sm);
background: transparent;
color: var(--muted);
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
}
.viewtoggle__btn .ic {
fill: currentColor;
}
.viewtoggle__btn:hover {
color: var(--text);
}
.viewtoggle__btn.is-active {
background: var(--surface-2);
color: var(--text);
}
/* ---------- Grid ---------- */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
}
.empty {
text-align: center;
color: var(--muted);
padding: 60px 0;
font-weight: 500;
}
/* ---------- Card ---------- */
.card {
position: relative;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 14px;
cursor: pointer;
transition: transform 0.18s ease, background 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
}
.card:hover {
transform: translateY(-4px);
background: var(--surface-2);
box-shadow: var(--shadow-sm);
border-color: var(--line-2);
}
.card.is-playing {
border-color: var(--card-accent, var(--accent));
box-shadow: 0 0 0 1px var(--card-accent, var(--accent)), var(--shadow-sm);
}
.cover {
position: relative;
aspect-ratio: 1 / 1;
border-radius: var(--r-sm);
overflow: hidden;
background: var(--cover-bg, linear-gradient(135deg, #333, #111));
}
.cover__shapes {
position: absolute;
inset: 0;
}
.cover__shapes::before,
.cover__shapes::after {
content: "";
position: absolute;
border-radius: 50%;
mix-blend-mode: screen;
opacity: 0.85;
}
.cover__shapes::before {
width: 70%;
height: 70%;
top: -16%;
right: -10%;
background: radial-gradient(circle, var(--cover-a, #fff4) 0%, transparent 68%);
}
.cover__shapes::after {
width: 80%;
height: 80%;
bottom: -22%;
left: -16%;
background: radial-gradient(circle, var(--cover-b, #fff3) 0%, transparent 70%);
}
.cover__bars {
position: absolute;
inset: 0;
display: flex;
align-items: flex-end;
justify-content: center;
gap: 6%;
padding: 22%;
opacity: 0.28;
}
.cover__bars i {
flex: 1;
background: rgba(255, 255, 255, 0.85);
border-radius: 2px;
height: 30%;
}
.cover__bars i:nth-child(1) { height: 55%; }
.cover__bars i:nth-child(2) { height: 90%; }
.cover__bars i:nth-child(3) { height: 40%; }
.cover__bars i:nth-child(4) { height: 70%; }
.cover__bars i:nth-child(5) { height: 50%; }
.badge {
position: absolute;
top: 10px;
left: 10px;
z-index: 2;
font-size: 10.5px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 4px 9px;
border-radius: var(--r-full);
background: rgba(8, 8, 12, 0.6);
backdrop-filter: blur(6px);
border: 1px solid var(--line-2);
color: var(--text);
}
/* play overlay */
.playbtn {
position: absolute;
bottom: 10px;
right: 10px;
z-index: 3;
width: 46px;
height: 46px;
display: grid;
place-items: center;
border: 0;
border-radius: 50%;
background: var(--card-accent, var(--accent));
color: #04130a;
cursor: pointer;
opacity: 0;
transform: translateY(8px) scale(0.85);
transition: opacity 0.18s ease, transform 0.18s ease, background 0.18s ease;
box-shadow: 0 10px 24px -8px rgba(0, 0, 0, 0.7);
}
.card:hover .playbtn,
.card:focus-within .playbtn,
.card.is-playing .playbtn {
opacity: 1;
transform: translateY(0) scale(1);
}
.playbtn:hover {
transform: translateY(0) scale(1.08);
}
.icon-play,
.icon-pause {
display: block;
}
.icon-play {
width: 0;
height: 0;
border-style: solid;
border-width: 8px 0 8px 14px;
border-color: transparent transparent transparent currentColor;
margin-left: 3px;
}
.icon-pause {
width: 14px;
height: 16px;
position: relative;
}
.icon-pause::before,
.icon-pause::after {
content: "";
position: absolute;
top: 0;
width: 4.5px;
height: 16px;
border-radius: 2px;
background: currentColor;
}
.icon-pause::before { left: 1px; }
.icon-pause::after { right: 1px; }
/* equalizer shown on playing card */
.eq {
position: absolute;
bottom: 10px;
left: 10px;
z-index: 3;
display: none;
align-items: flex-end;
gap: 3px;
height: 18px;
padding: 5px 7px;
border-radius: var(--r-sm);
background: rgba(8, 8, 12, 0.55);
backdrop-filter: blur(6px);
}
.card.is-playing .eq {
display: flex;
}
.eq span {
width: 3px;
border-radius: 2px;
background: var(--card-accent, var(--accent));
animation: eq 0.9s ease-in-out infinite;
}
.eq span:nth-child(1) { animation-delay: -0.2s; }
.eq span:nth-child(2) { animation-delay: -0.5s; }
.eq span:nth-child(3) { animation-delay: -0.1s; }
.eq span:nth-child(4) { animation-delay: -0.7s; }
@keyframes eq {
0%,
100% { height: 5px; }
50% { height: 16px; }
}
.card__meta {
margin-top: 12px;
}
.card__title {
margin: 0;
font-family: var(--display);
font-weight: 600;
font-size: 15.5px;
letter-spacing: -0.01em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card__sub {
margin: 5px 0 0;
font-size: 12.5px;
color: var(--muted);
display: flex;
align-items: center;
gap: 7px;
}
.card__sub .dot {
flex: 0 0 auto;
}
/* ---------- List view ---------- */
.grid.is-list {
grid-template-columns: 1fr;
gap: 8px;
}
.grid.is-list .card {
display: grid;
grid-template-columns: 56px 1fr auto;
align-items: center;
gap: 16px;
padding: 10px 14px;
}
.grid.is-list .cover {
width: 56px;
height: 56px;
aspect-ratio: 1 / 1;
}
.grid.is-list .cover__bars {
display: none;
}
.grid.is-list .badge {
position: static;
display: inline-block;
}
.grid.is-list .playbtn {
position: static;
width: 40px;
height: 40px;
order: 3;
}
.grid.is-list .eq {
position: static;
background: transparent;
backdrop-filter: none;
padding: 0;
}
.grid.is-list .card__meta {
margin-top: 0;
min-width: 0;
}
.grid.is-list .card__sub {
flex-wrap: wrap;
}
.grid.is-list .card:hover {
transform: none;
}
.grid.is-list .card__tail {
display: flex;
align-items: center;
gap: 14px;
}
.card__plays {
font-size: 12px;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.grid:not(.is-list) .card__tail .card__plays,
.grid:not(.is-list) .badge.badge--inline {
display: none;
}
.grid:not(.is-list) .card__tail {
display: contents;
}
/* ---------- Now bar ---------- */
.nowbar {
position: fixed;
left: 50%;
bottom: 18px;
transform: translateX(-50%);
z-index: 50;
width: min(720px, calc(100% - 36px));
display: flex;
align-items: center;
gap: 14px;
padding: 12px 14px;
border-radius: var(--r-lg);
background: rgba(20, 20, 26, 0.86);
backdrop-filter: blur(16px);
border: 1px solid var(--line-2);
box-shadow: var(--shadow);
animation: rise 0.28s ease;
}
@keyframes rise {
from {
opacity: 0;
transform: translate(-50%, 16px);
}
to {
opacity: 1;
transform: translate(-50%, 0);
}
}
.nowbar__cover {
width: 48px;
height: 48px;
border-radius: var(--r-sm);
flex: 0 0 auto;
background: var(--cover-bg, #333);
}
.nowbar__info {
flex: 0 0 auto;
min-width: 0;
width: 140px;
}
.nowbar__title {
margin: 0;
font-weight: 700;
font-size: 13.5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.nowbar__sub {
margin: 2px 0 0;
font-size: 12px;
color: var(--muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.nowbar__player {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
min-width: 0;
}
.nowbar__play {
width: 40px;
height: 40px;
flex: 0 0 auto;
display: grid;
place-items: center;
border: 0;
border-radius: 50%;
background: var(--text);
color: #0b0b0f;
cursor: pointer;
transition: transform 0.12s ease;
}
.nowbar__play:active {
transform: scale(0.92);
}
.scrubrow {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
min-width: 0;
}
.time {
font-size: 11.5px;
color: var(--muted);
font-variant-numeric: tabular-nums;
flex: 0 0 auto;
}
.scrub {
position: relative;
flex: 1;
height: 6px;
border-radius: var(--r-full);
background: rgba(255, 255, 255, 0.14);
cursor: pointer;
}
.scrub:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 4px;
}
.scrub__fill {
position: absolute;
inset: 0 auto 0 0;
width: 0;
border-radius: var(--r-full);
background: linear-gradient(90deg, var(--accent-2), var(--accent));
}
.scrub__knob {
position: absolute;
top: 50%;
left: 0;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--text);
transform: translate(-50%, -50%);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
opacity: 0;
transition: opacity 0.15s ease;
}
.scrub:hover .scrub__knob,
.scrub:focus-visible .scrub__knob {
opacity: 1;
}
.nowbar__close {
flex: 0 0 auto;
width: 30px;
height: 30px;
border: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.06);
color: var(--muted);
font-size: 20px;
line-height: 1;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
}
.nowbar__close:hover {
background: rgba(255, 255, 255, 0.12);
color: var(--text);
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 92px;
transform: translate(-50%, 12px);
z-index: 60;
background: var(--surface-2);
border: 1px solid var(--line-2);
color: var(--text);
font-size: 13px;
font-weight: 600;
padding: 10px 16px;
border-radius: var(--r-full);
box-shadow: var(--shadow);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
}
.toast.is-show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---------- Focus ---------- */
button:focus-visible,
[role="slider"]:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
}
/* ---------- Responsive ---------- */
@media (max-width: 760px) {
.controls {
align-items: stretch;
}
.tabs {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.nowbar__info {
width: 110px;
}
}
@media (max-width: 520px) {
.page {
padding: 18px 14px 130px;
}
.hero {
padding: 24px;
min-height: 180px;
}
.grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 14px;
}
.controls__right {
justify-content: space-between;
width: 100%;
}
.sort__label {
display: none;
}
.nowbar {
gap: 10px;
padding: 10px;
}
.nowbar__info {
width: 92px;
}
.time {
display: none;
}
.grid.is-list .card {
grid-template-columns: 48px 1fr auto;
gap: 12px;
}
.grid.is-list .cover {
width: 48px;
height: 48px;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
}
/* Visibility guard: honor the [hidden] attribute over base display */
.nowbar[hidden] {
display: none;
}(function () {
"use strict";
/* ---------- Fictional discography data ---------- */
// Each release: id, title, year, type, plays (raw), duration (sec),
// and a CSS-drawn cover defined by two accent colors + a base gradient.
var RELEASES = [
{
id: "midnight-reservoir",
title: "Midnight Reservoir",
year: 2026,
type: "album",
plays: 48200000,
duration: 222,
accent: "#8b5cf6",
bg: "linear-gradient(150deg,#3a1d63,#0e0a1f)",
a: "rgba(255,61,113,0.85)",
b: "rgba(139,92,246,0.8)",
},
{
id: "paper-lanterns",
title: "Paper Lanterns",
year: 2025,
type: "single",
plays: 19800000,
duration: 198,
accent: "#ff3d71",
bg: "linear-gradient(150deg,#5c1530,#160810)",
a: "rgba(255,184,108,0.85)",
b: "rgba(255,61,113,0.8)",
},
{
id: "velvet-static",
title: "Velvet Static",
year: 2024,
type: "album",
plays: 63400000,
duration: 241,
accent: "#1db954",
bg: "linear-gradient(150deg,#0c4d34,#06140f)",
a: "rgba(108,255,196,0.8)",
b: "rgba(29,185,84,0.85)",
},
{
id: "harbor-lights-ep",
title: "Harbor Lights EP",
year: 2024,
type: "single",
plays: 8700000,
duration: 176,
accent: "#38bdf8",
bg: "linear-gradient(150deg,#0b3a5c,#06121c)",
a: "rgba(125,211,252,0.85)",
b: "rgba(56,189,248,0.7)",
},
{
id: "low-orbit",
title: "Low Orbit",
year: 2023,
type: "album",
plays: 31100000,
duration: 263,
accent: "#f59e0b",
bg: "linear-gradient(150deg,#5a3a08,#1a1206)",
a: "rgba(253,224,71,0.85)",
b: "rgba(245,158,11,0.75)",
},
{
id: "ghost-frequency",
title: "Ghost Frequency",
year: 2023,
type: "single",
plays: 12400000,
duration: 205,
accent: "#a78bfa",
bg: "linear-gradient(150deg,#2b2150,#0c0a18)",
a: "rgba(199,210,254,0.8)",
b: "rgba(167,139,250,0.8)",
},
{
id: "tidal-archives",
title: "Tidal Archives",
year: 2022,
type: "compilation",
plays: 27600000,
duration: 312,
accent: "#22d3ee",
bg: "linear-gradient(150deg,#0a4a52,#06151a)",
a: "rgba(165,243,252,0.8)",
b: "rgba(34,211,238,0.75)",
},
{
id: "saltwater-bloom",
title: "Saltwater Bloom",
year: 2022,
type: "album",
plays: 41900000,
duration: 228,
accent: "#fb7185",
bg: "linear-gradient(150deg,#5a1f33,#180a10)",
a: "rgba(254,205,211,0.85)",
b: "rgba(251,113,133,0.75)",
},
{
id: "cassette-summer",
title: "Cassette Summer",
year: 2021,
type: "single",
plays: 9200000,
duration: 187,
accent: "#34d399",
bg: "linear-gradient(150deg,#0d4a3a,#07150f)",
a: "rgba(167,243,208,0.8)",
b: "rgba(52,211,153,0.75)",
},
{
id: "first-light-anthology",
title: "First Light: The Anthology",
year: 2020,
type: "compilation",
plays: 53800000,
duration: 358,
accent: "#c084fc",
bg: "linear-gradient(150deg,#3a1a55,#0e0818)",
a: "rgba(233,213,255,0.8)",
b: "rgba(192,132,252,0.75)",
},
{
id: "neon-tides-debut",
title: "Neon Tides",
year: 2019,
type: "album",
plays: 72500000,
duration: 214,
accent: "#ec4899",
bg: "linear-gradient(150deg,#5c1545,#170818)",
a: "rgba(251,207,232,0.85)",
b: "rgba(236,72,153,0.75)",
},
{
id: "static-tide-remixes",
title: "Static Tide (Remixes)",
year: 2019,
type: "single",
plays: 6100000,
duration: 232,
accent: "#60a5fa",
bg: "linear-gradient(150deg,#1a3a66,#080f1c)",
a: "rgba(191,219,254,0.8)",
b: "rgba(96,165,250,0.7)",
},
];
var TYPE_LABEL = {
album: "Album",
single: "Single / EP",
compilation: "Compilation",
};
/* ---------- DOM refs ---------- */
var grid = document.getElementById("grid");
var empty = document.getElementById("empty");
var tabs = Array.prototype.slice.call(document.querySelectorAll(".tab"));
var sortSelect = document.getElementById("sortSelect");
var gridViewBtn = document.getElementById("gridViewBtn");
var listViewBtn = document.getElementById("listViewBtn");
var toastEl = document.getElementById("toast");
var nowbar = document.getElementById("nowbar");
var nowCover = document.getElementById("nowCover");
var nowTitle = document.getElementById("nowTitle");
var nowSub = document.getElementById("nowSub");
var nowPlay = document.getElementById("nowPlay");
var nowCur = document.getElementById("nowCur");
var nowDur = document.getElementById("nowDur");
var scrub = document.getElementById("scrub");
var scrubFill = document.getElementById("scrubFill");
var scrubKnob = document.getElementById("scrubKnob");
/* ---------- State ---------- */
var state = {
filter: "all",
sort: "newest",
view: "grid",
playingId: null,
isPlaying: false,
progress: 0, // seconds
duration: 0,
timer: null,
};
/* ---------- Helpers ---------- */
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 2200);
}
function fmtTime(sec) {
sec = Math.max(0, Math.floor(sec));
var m = Math.floor(sec / 60);
var s = sec % 60;
return m + ":" + (s < 10 ? "0" : "") + s;
}
function fmtPlays(n) {
if (n >= 1e9) return (n / 1e9).toFixed(1).replace(/\.0$/, "") + "B";
if (n >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M";
if (n >= 1e3) return (n / 1e3).toFixed(0) + "K";
return String(n);
}
function byId(id) {
for (var i = 0; i < RELEASES.length; i++) {
if (RELEASES[i].id === id) return RELEASES[i];
}
return null;
}
/* ---------- Tab counts ---------- */
function setCounts() {
var counts = { all: RELEASES.length, album: 0, single: 0, compilation: 0 };
RELEASES.forEach(function (r) {
counts[r.type]++;
});
document.querySelectorAll(".tab__count").forEach(function (el) {
el.textContent = counts[el.getAttribute("data-count")];
});
}
/* ---------- Filter + sort ---------- */
function getVisible() {
var list = RELEASES.filter(function (r) {
return state.filter === "all" || r.type === state.filter;
});
list.sort(function (a, b) {
if (state.sort === "oldest") return a.year - b.year;
if (state.sort === "played") return b.plays - a.plays;
return b.year - a.year; // newest
});
return list;
}
/* ---------- Render ---------- */
function render() {
var list = getVisible();
grid.innerHTML = "";
if (!list.length) {
empty.hidden = false;
return;
}
empty.hidden = true;
list.forEach(function (r) {
var card = document.createElement("article");
card.className = "card";
card.setAttribute("data-id", r.id);
card.style.setProperty("--card-accent", r.accent);
if (state.playingId === r.id && state.isPlaying) {
card.classList.add("is-playing");
}
var coverVars =
"--cover-bg:" + r.bg + ";--cover-a:" + r.a + ";--cover-b:" + r.b + ";";
card.innerHTML =
'<div class="cover" style="' + coverVars + '">' +
'<span class="badge">' + TYPE_LABEL[r.type] + "</span>" +
'<div class="cover__shapes"></div>' +
'<div class="cover__bars"><i></i><i></i><i></i><i></i><i></i></div>' +
'<div class="eq" aria-hidden="true"><span></span><span></span><span></span><span></span></div>' +
'<button class="playbtn" type="button" aria-pressed="false" aria-label="Play ' +
r.title + '"><span class="icon-play"></span></button>' +
"</div>" +
'<div class="card__meta">' +
'<h3 class="card__title">' + r.title + "</h3>" +
'<div class="card__sub">' +
'<span class="badge badge--inline">' + TYPE_LABEL[r.type] + "</span>" +
"<span>" + r.year + "</span>" +
'<span class="dot" aria-hidden="true"></span>' +
"<span>" + fmtTime(r.duration) + "</span>" +
'<span class="card__tail">' +
'<span class="dot" aria-hidden="true"></span>' +
'<span class="card__plays">' + fmtPlays(r.plays) + " plays</span>" +
"</span>" +
"</div>" +
"</div>";
grid.appendChild(card);
});
syncPlayButtons();
}
function syncPlayButtons() {
document.querySelectorAll(".card").forEach(function (card) {
var id = card.getAttribute("data-id");
var btn = card.querySelector(".playbtn");
var isThis = id === state.playingId && state.isPlaying;
btn.setAttribute("aria-pressed", isThis ? "true" : "false");
btn.innerHTML = isThis
? '<span class="icon-pause"></span>'
: '<span class="icon-play"></span>';
btn.setAttribute("aria-label", (isThis ? "Pause " : "Play ") + byId(id).title);
card.classList.toggle("is-playing", isThis);
});
}
/* ---------- Playback simulation ---------- */
function startTimer() {
stopTimer();
state.timer = setInterval(function () {
if (!state.isPlaying) return;
state.progress += 1;
if (state.progress >= state.duration) {
state.progress = state.duration;
updateScrub();
pause();
toast("Track finished");
return;
}
updateScrub();
}, 1000);
}
function stopTimer() {
if (state.timer) {
clearInterval(state.timer);
state.timer = null;
}
}
function updateScrub() {
var pct = state.duration ? (state.progress / state.duration) * 100 : 0;
scrubFill.style.width = pct + "%";
scrubKnob.style.left = pct + "%";
scrub.setAttribute("aria-valuenow", Math.round(pct));
scrub.setAttribute(
"aria-valuetext",
fmtTime(state.progress) + " of " + fmtTime(state.duration)
);
nowCur.textContent = fmtTime(state.progress);
}
function playRelease(id) {
var r = byId(id);
if (!r) return;
if (state.playingId === id) {
// toggle on same track
togglePlay();
return;
}
state.playingId = id;
state.duration = r.duration;
state.progress = 0;
state.isPlaying = true;
nowbar.hidden = false;
nowCover.style.background = r.bg;
nowTitle.textContent = r.title;
nowSub.textContent = "Neon Tides · " + r.year + " · " + TYPE_LABEL[r.type];
nowDur.textContent = fmtTime(r.duration);
scrub.setAttribute("aria-valuemax", r.duration);
document.documentElement.style.setProperty("--accent-now", r.accent);
setPlayIcon(true);
updateScrub();
startTimer();
syncPlayButtons();
toast("Now playing — " + r.title);
}
function togglePlay() {
if (!state.playingId) return;
if (state.isPlaying) pause();
else resume();
}
function pause() {
state.isPlaying = false;
setPlayIcon(false);
syncPlayButtons();
}
function resume() {
if (state.progress >= state.duration) state.progress = 0;
state.isPlaying = true;
setPlayIcon(true);
startTimer();
syncPlayButtons();
}
function stopPlayback() {
stopTimer();
state.playingId = null;
state.isPlaying = false;
state.progress = 0;
nowbar.hidden = true;
syncPlayButtons();
}
function setPlayIcon(playing) {
nowPlay.innerHTML = playing
? '<span class="icon-pause"></span>'
: '<span class="icon-play"></span>';
nowPlay.setAttribute("aria-pressed", playing ? "true" : "false");
nowPlay.setAttribute("aria-label", playing ? "Pause" : "Play");
}
/* ---------- Scrub seek ---------- */
function seekFromClientX(clientX) {
var rect = scrub.getBoundingClientRect();
var ratio = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
state.progress = Math.round(ratio * state.duration);
updateScrub();
}
var dragging = false;
scrub.addEventListener("pointerdown", function (e) {
if (!state.playingId) return;
dragging = true;
scrub.setPointerCapture(e.pointerId);
seekFromClientX(e.clientX);
});
scrub.addEventListener("pointermove", function (e) {
if (dragging) seekFromClientX(e.clientX);
});
scrub.addEventListener("pointerup", function (e) {
dragging = false;
try { scrub.releasePointerCapture(e.pointerId); } catch (_) {}
});
scrub.addEventListener("keydown", function (e) {
if (!state.playingId) return;
var step = e.shiftKey ? 15 : 5;
if (e.key === "ArrowRight" || e.key === "ArrowUp") {
state.progress = Math.min(state.duration, state.progress + step);
updateScrub();
e.preventDefault();
} else if (e.key === "ArrowLeft" || e.key === "ArrowDown") {
state.progress = Math.max(0, state.progress - step);
updateScrub();
e.preventDefault();
} else if (e.key === "Home") {
state.progress = 0;
updateScrub();
e.preventDefault();
} else if (e.key === "End") {
state.progress = state.duration;
updateScrub();
e.preventDefault();
}
});
/* ---------- Event wiring ---------- */
grid.addEventListener("click", function (e) {
var playBtn = e.target.closest(".playbtn");
var card = e.target.closest(".card");
if (!card) return;
var id = card.getAttribute("data-id");
if (playBtn) {
e.stopPropagation();
playRelease(id);
} else {
// clicking the card body opens / plays the release
playRelease(id);
}
});
tabs.forEach(function (tab) {
tab.addEventListener("click", function () {
tabs.forEach(function (t) {
t.classList.remove("is-active");
t.setAttribute("aria-selected", "false");
});
tab.classList.add("is-active");
tab.setAttribute("aria-selected", "true");
state.filter = tab.getAttribute("data-filter");
render();
});
});
sortSelect.addEventListener("change", function () {
state.sort = sortSelect.value;
render();
toast("Sorted by " + sortSelect.options[sortSelect.selectedIndex].text.toLowerCase());
});
function setView(view) {
state.view = view;
var isList = view === "list";
grid.classList.toggle("is-list", isList);
gridViewBtn.classList.toggle("is-active", !isList);
listViewBtn.classList.toggle("is-active", isList);
gridViewBtn.setAttribute("aria-pressed", String(!isList));
listViewBtn.setAttribute("aria-pressed", String(isList));
}
gridViewBtn.addEventListener("click", function () { setView("grid"); });
listViewBtn.addEventListener("click", function () { setView("list"); });
nowPlay.addEventListener("click", togglePlay);
document.getElementById("nowClose").addEventListener("click", function () {
stopPlayback();
toast("Playback stopped");
});
document.getElementById("shuffleBtn").addEventListener("click", function () {
var list = getVisible();
if (!list.length) return;
var pick = list[Math.floor(Math.random() * list.length)];
playRelease(pick.id);
});
document.getElementById("followBtn").addEventListener("click", function (e) {
var btn = e.currentTarget;
var on = btn.getAttribute("aria-pressed") === "true";
btn.setAttribute("aria-pressed", String(!on));
btn.textContent = on ? "Follow" : "Following";
toast(on ? "Unfollowed Neon Tides" : "Following Neon Tides");
});
/* ---------- Init ---------- */
setCounts();
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Neon Tides — Discography</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="page">
<header class="hero">
<div class="hero__art" aria-hidden="true">
<span class="hero__blob hero__blob--a"></span>
<span class="hero__blob hero__blob--b"></span>
<span class="hero__blob hero__blob--c"></span>
</div>
<div class="hero__body">
<p class="hero__eyebrow">Verified Artist</p>
<h1 class="hero__name">Neon Tides</h1>
<p class="hero__meta">
<span>Synthwave · Dream Pop</span>
<span class="dot" aria-hidden="true"></span>
<span><strong id="monthlyListeners">2,481,903</strong> monthly listeners</span>
</p>
<div class="hero__actions">
<button class="btn btn--accent" id="shuffleBtn" type="button">
<svg viewBox="0 0 24 24" class="ic" aria-hidden="true"><path d="M16 3h5v5M21 3l-7 7M4 20l5-5M16 21h5v-5M4 4l16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
Shuffle play
</button>
<button class="btn btn--ghost" id="followBtn" type="button" aria-pressed="false">Follow</button>
</div>
</div>
</header>
<section class="controls" aria-label="Discography controls">
<div class="tabs" role="tablist" aria-label="Filter releases by type">
<button class="tab is-active" role="tab" aria-selected="true" data-filter="all" type="button">All <span class="tab__count" data-count="all"></span></button>
<button class="tab" role="tab" aria-selected="false" data-filter="album" type="button">Albums <span class="tab__count" data-count="album"></span></button>
<button class="tab" role="tab" aria-selected="false" data-filter="single" type="button">Singles & EPs <span class="tab__count" data-count="single"></span></button>
<button class="tab" role="tab" aria-selected="false" data-filter="compilation" type="button">Compilations <span class="tab__count" data-count="compilation"></span></button>
</div>
<div class="controls__right">
<label class="sort">
<span class="sort__label">Sort</span>
<select id="sortSelect" aria-label="Sort releases">
<option value="newest">Newest</option>
<option value="oldest">Oldest</option>
<option value="played">Most played</option>
</select>
</label>
<div class="viewtoggle" role="group" aria-label="View mode">
<button class="viewtoggle__btn is-active" id="gridViewBtn" type="button" aria-pressed="true" title="Grid view">
<svg viewBox="0 0 24 24" class="ic" aria-hidden="true"><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/></svg>
</button>
<button class="viewtoggle__btn" id="listViewBtn" type="button" aria-pressed="false" title="List view">
<svg viewBox="0 0 24 24" class="ic" aria-hidden="true"><rect x="3" y="4" width="18" height="3" rx="1.5"/><rect x="3" y="10.5" width="18" height="3" rx="1.5"/><rect x="3" y="17" width="18" height="3" rx="1.5"/></svg>
</button>
</div>
</div>
</section>
<main>
<div class="grid" id="grid" aria-live="polite"></div>
<p class="empty" id="empty" hidden>No releases in this category yet.</p>
</main>
<footer class="nowbar" id="nowbar" hidden aria-label="Now playing">
<div class="nowbar__cover" id="nowCover" aria-hidden="true"></div>
<div class="nowbar__info">
<p class="nowbar__title" id="nowTitle">—</p>
<p class="nowbar__sub" id="nowSub">—</p>
</div>
<div class="nowbar__player">
<button class="nowbar__play" id="nowPlay" type="button" aria-pressed="true" aria-label="Pause">
<span class="icon-pause"></span>
</button>
<div class="scrubrow">
<span class="time" id="nowCur">0:00</span>
<div class="scrub" id="scrub" role="slider" tabindex="0" aria-label="Seek" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
<div class="scrub__fill" id="scrubFill"></div>
<div class="scrub__knob" id="scrubKnob"></div>
</div>
<span class="time" id="nowDur">0:00</span>
</div>
</div>
<button class="nowbar__close" id="nowClose" type="button" aria-label="Stop playback">×</button>
</footer>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
</div>
<script src="script.js"></script>
</body>
</html>Discography Grid (albums · singles)
A complete discography page for the fictional artist Neon Tides. The header floats animated, blurred color blobs behind the artist name, monthly listener count, genre line, a shuffle-play button and a follow toggle. Below it, a control row pairs release-type tabs — All, Albums, Singles & EPs and Compilations, each showing a live count — with a Newest / Oldest / Most-played sort dropdown and a grid/list view switch.
Every release is a fully CSS-drawn cover (layered gradients, blurred shapes and faint equalizer bars — no images), themed by an accent color pulled from the artwork. Cards show the title, year, type badge, runtime and play count, and reveal a circular play button on hover or focus. Activating a card or its play button starts simulated playback: the card swaps in an animated equalizer, a glassy now-playing bar rises with the cover, title and metadata, and a draggable, keyboard-seekable scrubber tracks elapsed time and auto-stops at the end.
Tab clicks filter the grid, the sort dropdown reorders it, and the view switch toggles between a cover-forward grid and a compact list layout that surfaces play counts inline. Play and follow controls expose aria-pressed, the scrubber uses role="slider" with live aria-valuenow/aria-valuetext and Arrow/Home/End seeking, and a small toast() helper confirms actions like shuffling, sorting and following. Everything works with vanilla JS and scales down to roughly 360px.
Illustrative UI only — fictional artists, albums, tracks, and data. No real audio playback.