Music — Album Page (tracklist · credits)
A dark, album-art-driven detail page for a fictional record. A large CSS-drawn cover glows its theme accent across the header beside the album title, artist, year, track count and a computed total runtime. A numbered tracklist shows featured artists, play counts, per-track like toggles and an animated equalizer on the active row. Simulated playback drives a glassy now-playing bar with a draggable, keyboard-seekable scrubber, plus credits, a producer notes block and a More by this artist album row.
MCP
Kod
: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 18px 50px rgba(0, 0, 0, 0.45);
--display: "Space Grotesk", system-ui, sans-serif;
--body: "Inter", system-ui, sans-serif;
/* cover-derived theme accent */
--cover-a: #ff3d71;
--cover-b: #8b5cf6;
}
* { 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;
-moz-osx-font-smoothing: grayscale;
padding-bottom: 96px;
min-height: 100vh;
}
h1, h2, h3 { margin: 0; font-weight: 700; }
button { font-family: inherit; cursor: pointer; color: inherit; }
a { color: inherit; }
.page {
max-width: 1080px;
margin: 0 auto;
padding: 28px 22px 48px;
}
/* ---------------- ALBUM HEADER ---------------- */
.album-head {
position: relative;
display: grid;
grid-template-columns: 280px 1fr;
gap: 32px;
align-items: end;
padding: 36px 28px;
border-radius: var(--r-lg);
overflow: hidden;
background:
radial-gradient(120% 120% at 0% 0%, color-mix(in srgb, var(--cover-a) 26%, transparent), transparent 55%),
radial-gradient(120% 120% at 100% 100%, color-mix(in srgb, var(--cover-b) 22%, transparent), transparent 60%),
linear-gradient(180deg, var(--bg-2), var(--surface));
border: 1px solid var(--line);
}
.cover-wrap { perspective: 800px; }
.cover {
position: relative;
width: 280px;
height: 280px;
border-radius: var(--r-md);
overflow: hidden;
background: linear-gradient(145deg, var(--cover-a), var(--cover-b));
box-shadow: var(--shadow), 0 0 60px -10px color-mix(in srgb, var(--cover-a) 60%, transparent);
transition: transform 0.5s ease;
}
.cover:hover { transform: translateY(-4px) rotate(-0.6deg); }
.cover-shape {
position: absolute;
border-radius: 50%;
filter: blur(2px);
mix-blend-mode: screen;
}
.cover-shape.s1 { width: 180px; height: 180px; top: -40px; left: -30px; background: radial-gradient(circle, #fff7, transparent 70%); }
.cover-shape.s2 { width: 150px; height: 150px; bottom: -30px; right: -20px; background: radial-gradient(circle, color-mix(in srgb, var(--accent) 70%, transparent), transparent 70%); }
.cover-shape.s3 { width: 90px; height: 90px; top: 55%; left: 40%; background: radial-gradient(circle, #fff5, transparent 70%); }
.cover-grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(255,255,255,0.07) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.07) 1px, transparent 1px);
background-size: 28px 28px;
mask: linear-gradient(180deg, transparent, #000 60%);
}
.cover-text {
position: absolute;
left: 22px;
bottom: 20px;
right: 22px;
display: flex;
flex-direction: column;
text-shadow: 0 2px 14px rgba(0,0,0,0.5);
}
.cover-text em {
font-style: normal;
font-family: var(--body);
font-weight: 700;
font-size: 0.72rem;
letter-spacing: 0.22em;
text-transform: uppercase;
opacity: 0.85;
}
.cover-text strong {
font-family: var(--display);
font-weight: 700;
font-size: 1.9rem;
line-height: 1.05;
margin-top: 4px;
}
.head-meta { min-width: 0; }
.eyebrow {
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--muted);
}
.album-title {
font-family: var(--display);
font-size: clamp(2.2rem, 6vw, 3.6rem);
line-height: 1.02;
margin: 6px 0 12px;
letter-spacing: -0.01em;
}
.artist-line {
display: flex;
align-items: center;
gap: 10px;
margin: 0 0 6px;
}
.artist-avatar {
width: 30px; height: 30px;
border-radius: var(--r-full);
background: conic-gradient(from 120deg, var(--cover-a), var(--cover-b), var(--accent), var(--cover-a));
flex: none;
}
.artist-name { font-weight: 700; text-decoration: none; }
.artist-name:hover { text-decoration: underline; }
.album-sub {
color: var(--muted);
font-size: 0.92rem;
margin: 0 0 22px;
font-weight: 500;
}
.album-sub .dot { margin: 0 7px; opacity: 0.6; }
.head-actions {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.play-album {
display: inline-flex;
align-items: center;
gap: 10px;
border: none;
border-radius: var(--r-full);
padding: 13px 26px;
font-weight: 700;
font-size: 1rem;
color: #06140b;
background: var(--accent);
box-shadow: 0 10px 26px -8px color-mix(in srgb, var(--accent) 80%, transparent);
transition: transform 0.15s ease, box-shadow 0.2s ease;
}
.play-album:hover { transform: scale(1.04); }
.play-album:active { transform: scale(0.98); }
.pa-icon {
width: 0; height: 0;
border-left: 13px solid #06140b;
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
margin-left: 2px;
}
.play-album[aria-pressed="true"] .pa-icon {
width: 13px; height: 14px;
border: none;
margin-left: 0;
background:
linear-gradient(#06140b, #06140b) left / 4px 100% no-repeat,
linear-gradient(#06140b, #06140b) right / 4px 100% no-repeat;
}
.icon-btn {
width: 46px; height: 46px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--r-full);
background: var(--surface-2);
border: 1px solid var(--line);
color: var(--muted);
transition: color 0.18s, border-color 0.18s, transform 0.12s, background 0.18s;
}
.icon-btn:hover { color: var(--text); border-color: var(--line-2); transform: translateY(-2px); }
.icon-btn svg { width: 21px; height: 21px; }
.ic-heart { fill: none; stroke: currentColor; stroke-width: 1.8; }
.icon-btn[aria-pressed="true"] { color: var(--accent-3); border-color: color-mix(in srgb, var(--accent-3) 50%, transparent); }
.icon-btn[aria-pressed="true"] .ic-heart { fill: var(--accent-3); stroke: var(--accent-3); animation: pop 0.32s ease; }
@keyframes pop { 0% { transform: scale(0.7); } 55% { transform: scale(1.25); } 100% { transform: scale(1); } }
.more-wrap { position: relative; }
.more-menu {
position: absolute;
top: calc(100% + 8px);
right: 0;
min-width: 196px;
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: var(--r-md);
box-shadow: var(--shadow);
padding: 6px;
display: none;
z-index: 30;
}
.more-menu.open { display: block; animation: fade 0.14s ease; }
@keyframes fade { from { opacity: 0; transform: translateY(-4px); } }
.more-menu button {
display: block;
width: 100%;
text-align: left;
background: none;
border: none;
padding: 10px 12px;
border-radius: var(--r-sm);
font-size: 0.9rem;
color: var(--text);
}
.more-menu button:hover { background: var(--surface-2); }
/* ---------------- TRACKLIST ---------------- */
.tracklist-sec { margin-top: 34px; }
.tl-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
padding: 0 6px;
}
.tl-bar h2 { font-size: 1.25rem; }
.sort-toggle {
display: inline-flex;
align-items: center;
gap: 8px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-full);
padding: 8px 14px;
font-size: 0.84rem;
font-weight: 600;
color: var(--muted);
transition: color 0.18s, border-color 0.18s;
}
.sort-toggle:hover { color: var(--text); border-color: var(--line-2); }
.st-dot {
width: 8px; height: 8px;
border-radius: 50%;
background: var(--muted);
transition: background 0.18s;
}
.sort-toggle[aria-pressed="true"] { color: var(--accent-2); border-color: color-mix(in srgb, var(--accent-2) 45%, transparent); }
.sort-toggle[aria-pressed="true"] .st-dot { background: var(--accent-2); }
.tracklist { list-style: none; margin: 0; padding: 6px; }
.track {
display: grid;
grid-template-columns: 30px 1fr auto auto 56px;
align-items: center;
gap: 14px;
padding: 10px 14px;
border-radius: var(--r-md);
transition: background 0.15s;
}
.track:hover { background: var(--surface); }
.track.playing { background: color-mix(in srgb, var(--accent) 10%, var(--surface)); }
.t-num {
position: relative;
color: var(--muted);
font-variant-numeric: tabular-nums;
font-size: 0.92rem;
font-weight: 600;
text-align: center;
}
.track:hover .t-num-text { opacity: 0; }
.t-play {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
border: none;
background: none;
color: var(--text);
opacity: 0;
transition: opacity 0.15s;
}
.track:hover .t-play { opacity: 1; }
.track.playing .t-num-text { display: none; }
.t-play .pic { width: 0; height: 0; border-left: 10px solid currentColor; border-top: 6px solid transparent; border-bottom: 6px solid transparent; }
.track.playing .t-play { opacity: 1; }
.track.playing .t-play .pic {
width: 10px; height: 11px; border: none;
background:
linear-gradient(currentColor, currentColor) left / 3px 100% no-repeat,
linear-gradient(currentColor, currentColor) right / 3px 100% no-repeat;
}
.t-info { min-width: 0; }
.t-title {
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.track.playing .t-title { color: var(--accent); }
.t-feat {
display: block;
font-size: 0.82rem;
color: var(--muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.t-plays {
color: var(--muted);
font-size: 0.84rem;
font-variant-numeric: tabular-nums;
}
.t-like {
border: none;
background: none;
color: var(--muted);
padding: 4px;
display: inline-flex;
opacity: 0;
transition: opacity 0.15s, color 0.15s, transform 0.12s;
}
.track:hover .t-like { opacity: 1; }
.t-like svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.8; }
.t-like:hover { color: var(--text); }
.t-like[aria-pressed="true"] { opacity: 1; color: var(--accent-3); }
.t-like[aria-pressed="true"] svg { fill: var(--accent-3); stroke: var(--accent-3); animation: pop 0.32s ease; }
.t-dur {
color: var(--muted);
font-size: 0.9rem;
font-variant-numeric: tabular-nums;
text-align: right;
position: relative;
}
/* equalizer on active row */
.eq {
display: none;
gap: 2px;
align-items: flex-end;
height: 14px;
}
.track.playing .t-dur .dur-text { display: none; }
.track.playing .eq { display: inline-flex; }
.eq i {
width: 3px;
background: var(--accent);
border-radius: 2px;
animation: eqbar 0.9s ease-in-out infinite;
}
.eq i:nth-child(1) { animation-delay: -0.2s; }
.eq i:nth-child(2) { animation-delay: -0.5s; }
.eq i:nth-child(3) { animation-delay: -0.1s; }
.eq i:nth-child(4) { animation-delay: -0.7s; }
@keyframes eqbar { 0%, 100% { height: 4px; } 50% { height: 14px; } }
/* ---------------- CREDITS ---------------- */
.credits-sec { margin-top: 42px; padding: 0 6px; }
.credits-sec h2 { font-size: 1.25rem; margin-bottom: 18px; }
.credits-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
.credit-block {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 18px 20px;
}
.credit-block h3 {
font-size: 0.74rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--accent-2);
margin-bottom: 12px;
}
.credit-block p { margin: 0 0 9px; font-size: 0.92rem; }
.credit-block p:last-child { margin-bottom: 0; }
.credit-block p span { display: block; color: var(--muted); font-size: 0.76rem; margin-bottom: 1px; }
.p-line {
color: var(--muted);
font-size: 0.8rem;
margin: 20px 0 0;
}
/* ---------------- MORE BY ARTIST ---------------- */
.more-sec { margin-top: 42px; padding: 0 6px; }
.more-sec h2 { font-size: 1.25rem; margin-bottom: 18px; }
.album-row {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 18px;
}
.mini {
background: none;
border: none;
text-align: left;
padding: 0;
}
.mini-cover {
width: 100%;
aspect-ratio: 1;
border-radius: var(--r-md);
margin-bottom: 10px;
position: relative;
overflow: hidden;
box-shadow: 0 14px 30px -14px rgba(0,0,0,0.7);
transition: transform 0.2s;
}
.mini:hover .mini-cover { transform: translateY(-4px); }
.mini-cover::after {
content: "";
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(255,255,255,0.06) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.06) 1px, transparent 1px);
background-size: 22px 22px;
mask: linear-gradient(180deg, transparent, #000);
}
.mini-title { font-weight: 600; font-size: 0.94rem; }
.mini-year { color: var(--muted); font-size: 0.82rem; }
/* ---------------- NOW PLAYING BAR ---------------- */
.now-bar {
position: fixed;
left: 50%;
transform: translateX(-50%);
bottom: 16px;
width: min(960px, calc(100% - 32px));
display: grid;
grid-template-columns: 46px minmax(120px, 200px) auto 1fr 44px;
align-items: center;
gap: 14px;
padding: 10px 16px;
background: color-mix(in srgb, var(--surface) 86%, transparent);
backdrop-filter: blur(16px);
border: 1px solid var(--line-2);
border-radius: var(--r-lg);
box-shadow: var(--shadow);
z-index: 50;
animation: slideUp 0.3s ease;
}
@keyframes slideUp { from { transform: translate(-50%, 30px); opacity: 0; } to { transform: translate(-50%, 0); opacity: 1; } }
.nb-cover {
width: 46px; height: 46px;
border-radius: var(--r-sm);
background: linear-gradient(145deg, var(--cover-a), var(--cover-b));
}
.nb-meta { min-width: 0; }
.nb-meta strong { display: block; font-size: 0.92rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.nb-meta span { color: var(--muted); font-size: 0.8rem; }
.nb-eq { display: inline-flex; gap: 2px; align-items: flex-end; height: 16px; }
.nb-eq i { width: 3px; background: var(--accent); border-radius: 2px; animation: eqbar 0.9s ease-in-out infinite; }
.nb-eq i:nth-child(2) { animation-delay: -0.4s; }
.nb-eq i:nth-child(3) { animation-delay: -0.15s; }
.nb-eq i:nth-child(4) { animation-delay: -0.6s; }
.now-bar.paused .nb-eq i { animation-play-state: paused; }
.nb-scrub {
display: flex;
align-items: center;
gap: 10px;
}
.nb-scrub span { color: var(--muted); font-size: 0.76rem; font-variant-numeric: tabular-nums; width: 34px; }
.nb-scrub span:last-child { text-align: right; }
.scrub {
position: relative;
flex: 1;
height: 16px;
display: flex;
align-items: center;
cursor: pointer;
}
.scrub::before {
content: "";
position: absolute;
left: 0; right: 0;
height: 5px;
border-radius: var(--r-full);
background: var(--surface-2);
}
.scrub-fill {
position: absolute;
left: 0;
height: 5px;
width: 0%;
border-radius: var(--r-full);
background: linear-gradient(90deg, var(--accent), color-mix(in srgb, var(--accent) 60%, var(--accent-2)));
}
.scrub-knob {
position: absolute;
left: 0;
width: 13px; height: 13px;
border-radius: 50%;
background: #fff;
box-shadow: 0 2px 6px rgba(0,0,0,0.5);
transform: translateX(-50%);
opacity: 0;
transition: opacity 0.15s;
}
.scrub:hover .scrub-knob,
.scrub:focus-visible .scrub-knob { opacity: 1; }
.scrub:focus-visible { outline: none; }
.scrub:focus-visible::before { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 40%, transparent); }
.nb-play {
width: 44px; height: 44px;
border-radius: 50%;
border: none;
background: var(--accent);
display: inline-flex;
align-items: center;
justify-content: center;
color: #06140b;
transition: transform 0.12s;
}
.nb-play:hover { transform: scale(1.06); }
.nb-ic {
width: 11px; height: 13px;
background:
linear-gradient(#06140b, #06140b) left / 4px 100% no-repeat,
linear-gradient(#06140b, #06140b) right / 4px 100% no-repeat;
}
.now-bar.paused .nb-play .nb-ic {
width: 0; height: 0;
background: none;
border-left: 12px solid #06140b;
border-top: 7px solid transparent;
border-bottom: 7px solid transparent;
margin-left: 3px;
}
/* ---------------- TOAST ---------------- */
.toast-wrap {
position: fixed;
bottom: 96px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
gap: 8px;
z-index: 80;
pointer-events: none;
}
.toast {
background: var(--surface-2);
border: 1px solid var(--line-2);
color: var(--text);
padding: 11px 18px;
border-radius: var(--r-full);
font-size: 0.86rem;
font-weight: 500;
box-shadow: var(--shadow);
animation: toastIn 0.25s ease, toastOut 0.3s ease 2.4s forwards;
}
@keyframes toastIn { from { opacity: 0; transform: translateY(12px); } }
@keyframes toastOut { to { opacity: 0; transform: translateY(12px); } }
/* ---------------- RESPONSIVE ---------------- */
@media (max-width: 760px) {
.album-head {
grid-template-columns: 1fr;
text-align: center;
padding: 28px 20px;
}
.cover-wrap { justify-self: center; }
.head-meta { display: flex; flex-direction: column; align-items: center; }
.artist-line, .head-actions { justify-content: center; }
}
@media (max-width: 520px) {
.page { padding: 18px 14px 40px; }
.cover, .cover-wrap { width: 220px; }
.cover { height: 220px; }
.cover-text strong { font-size: 1.5rem; }
.track {
grid-template-columns: 26px 1fr 50px;
gap: 10px;
}
.t-plays { display: none; }
.t-like { display: none; }
.head-actions { gap: 10px; }
.play-album { padding: 12px 22px; }
.now-bar {
grid-template-columns: 40px 1fr 40px;
grid-template-rows: auto auto;
gap: 8px 12px;
padding: 10px 14px;
}
.nb-cover { width: 40px; height: 40px; }
.nb-eq { display: none; }
.nb-scrub { grid-column: 1 / -1; }
}
/* Visibility guard: honor the [hidden] attribute over base display */
.now-bar[hidden] {
display: none;
}(() => {
"use strict";
/* ---------- data (fictional) ---------- */
const TRACKS = [
{ title: "Paper Lanterns", feat: "", sec: 224, plays: 4820113, liked: false },
{ title: "Midnight Reservoir", feat: "", sec: 252, plays: 8104552, liked: true },
{ title: "Velvet Static", feat: "feat. Lake Mercer", sec: 198, plays: 3290881, liked: false },
{ title: "Harbor Lights", feat: "", sec: 241, plays: 2740019, liked: false },
{ title: "Glass Avenue", feat: "feat. The Hollow Quartet", sec: 277, plays: 1980442, liked: false },
{ title: "Saltwater Telegraph", feat: "", sec: 213, plays: 1556730, liked: false },
{ title: "Low Tide Confessions", feat: "feat. J. Wilder", sec: 268, plays: 2204117, liked: true },
{ title: "Neon Undertow", feat: "", sec: 231, plays: 3071008, liked: false },
{ title: "Driftwood Radio", feat: "", sec: 256, plays: 1342990, liked: false },
{ title: "Reservoir (Reprise)", feat: "", sec: 162, plays: 998421, liked: false }
];
const MORE_ALBUMS = [
{ title: "Saltwater EP", year: 2024, a: "#1db954", b: "#0a5c8a" },
{ title: "Half-Light", year: 2023, a: "#8b5cf6", b: "#ff3d71" },
{ title: "Coastline Tapes", year: 2022, a: "#f59e0b", b: "#ff3d71" },
{ title: "First Frequencies", year: 2021, a: "#06b6d4", b: "#8b5cf6" }
];
/* ---------- helpers ---------- */
const $ = (s, r = document) => r.querySelector(s);
const fmt = (s) => `${Math.floor(s / 60)}:${String(Math.round(s % 60)).padStart(2, "0")}`;
const compact = (n) =>
n >= 1e6 ? (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M"
: n >= 1e3 ? (n / 1e3).toFixed(1).replace(/\.0$/, "") + "K"
: String(n);
const toastWrap = $("#toastWrap");
function toast(msg) {
const el = document.createElement("div");
el.className = "toast";
el.textContent = msg;
toastWrap.appendChild(el);
setTimeout(() => el.remove(), 2750);
}
/* ---------- build tracklist ---------- */
const tracklist = $("#tracklist");
let order = TRACKS.map((_, i) => i); // index order, mutated by sort
function renderTracklist() {
tracklist.innerHTML = "";
order.forEach((idx, pos) => {
const t = TRACKS[idx];
const li = document.createElement("li");
li.className = "track";
li.dataset.idx = String(idx);
if (idx === playing.idx) li.classList.add("playing");
li.innerHTML = `
<span class="t-num">
<span class="t-num-text">${pos + 1}</span>
<button class="t-play" aria-label="Play ${t.title}"><span class="pic"></span></button>
</span>
<span class="t-info">
<span class="t-title">${t.title}</span>
${t.feat ? `<span class="t-feat">${t.feat}</span>` : ""}
</span>
<span class="t-plays">${compact(t.plays)}</span>
<button class="t-like" aria-pressed="${t.liked}" aria-label="Like ${t.title}">
<svg viewBox="0 0 24 24"><path d="M12 21s-7.5-4.6-10-9.2C.4 8.5 1.8 5 5.2 5c2 0 3.3 1.2 3.8 2.2C9.5 6.2 10.8 5 12.8 5 16.2 5 17.6 8.5 16 11.8 13.5 16.4 12 21 12 21z"/></svg>
</button>
<span class="t-dur">
<span class="dur-text">${fmt(t.sec)}</span>
<span class="eq"><i></i><i></i><i></i><i></i></span>
</span>`;
tracklist.appendChild(li);
});
}
/* ---------- runtime sum ---------- */
const totalSec = TRACKS.reduce((a, t) => a + t.sec, 0);
$("#totalRuntime").textContent = `${Math.round(totalSec / 60)} min`;
$("#trackCount").textContent = `${TRACKS.length} tracks`;
/* ---------- playback simulation ---------- */
const nowBar = $("#nowBar");
const scrubFill = $("#scrubFill");
const scrubKnob = $("#scrubKnob");
const scrub = $("#scrub");
const playing = { idx: -1, elapsed: 0, timer: null, paused: false };
function setNowBar(idx) {
const t = TRACKS[idx];
$("#nbTitle").textContent = t.title;
$("#nbArtist").textContent = t.feat ? "Neon Tides " + t.feat : "Neon Tides";
$("#nbDur").textContent = fmt(t.sec);
nowBar.hidden = false;
}
function updateScrub() {
const t = TRACKS[playing.idx];
if (!t) return;
const pct = Math.min(100, (playing.elapsed / t.sec) * 100);
scrubFill.style.width = pct + "%";
scrubKnob.style.left = pct + "%";
scrub.setAttribute("aria-valuenow", Math.round(pct));
$("#nbCur").textContent = fmt(playing.elapsed);
}
function tick() {
if (playing.paused) return;
const t = TRACKS[playing.idx];
playing.elapsed += 1;
if (playing.elapsed >= t.sec) {
// advance to next in current visual order
const curPos = order.indexOf(playing.idx);
if (curPos < order.length - 1) {
startTrack(order[curPos + 1]);
return;
}
stopAll();
toast("Album finished");
return;
}
updateScrub();
}
function startTrack(idx) {
clearInterval(playing.timer);
playing.idx = idx;
playing.elapsed = 0;
playing.paused = false;
nowBar.classList.remove("paused");
setNowBar(idx);
updateScrub();
syncRows();
syncAlbumBtn();
$("#nbPlay").setAttribute("aria-pressed", "true");
$("#nbPlay").setAttribute("aria-label", "Pause");
playing.timer = setInterval(tick, 1000);
}
function togglePause() {
if (playing.idx < 0) return;
playing.paused = !playing.paused;
nowBar.classList.toggle("paused", playing.paused);
const pressed = !playing.paused;
$("#nbPlay").setAttribute("aria-pressed", String(pressed));
$("#nbPlay").setAttribute("aria-label", pressed ? "Pause" : "Play");
syncRows();
syncAlbumBtn();
}
function stopAll() {
clearInterval(playing.timer);
playing.idx = -1;
playing.paused = false;
syncRows();
syncAlbumBtn();
}
function syncRows() {
tracklist.querySelectorAll(".track").forEach((row) => {
const isCur = Number(row.dataset.idx) === playing.idx;
row.classList.toggle("playing", isCur && !playing.paused ? true : isCur);
// keep row marked when paused but show static state via class only when current
if (!isCur) row.classList.remove("playing");
else row.classList.add("playing");
});
}
function syncAlbumBtn() {
const active = playing.idx >= 0 && !playing.paused;
const btn = $("#playAlbum");
btn.setAttribute("aria-pressed", String(active));
$(".pa-label", btn).textContent = active ? "Pause" : "Play";
}
/* ---------- tracklist interactions (delegated) ---------- */
tracklist.addEventListener("click", (e) => {
const row = e.target.closest(".track");
if (!row) return;
const idx = Number(row.dataset.idx);
const like = e.target.closest(".t-like");
if (like) {
TRACKS[idx].liked = !TRACKS[idx].liked;
like.setAttribute("aria-pressed", String(TRACKS[idx].liked));
toast(TRACKS[idx].liked ? "Added to Liked Songs" : "Removed from Liked Songs");
return;
}
// play/pause this track
if (idx === playing.idx) {
togglePause();
} else {
startTrack(idx);
}
});
/* ---------- album-level play ---------- */
$("#playAlbum").addEventListener("click", () => {
if (playing.idx < 0) {
startTrack(order[0]);
} else {
togglePause();
}
});
/* ---------- now-bar play button ---------- */
$("#nbPlay").addEventListener("click", togglePause);
/* ---------- scrubber: click, drag, keyboard ---------- */
function seekFromEvent(clientX) {
if (playing.idx < 0) return;
const r = scrub.getBoundingClientRect();
const pct = Math.min(1, Math.max(0, (clientX - r.left) / r.width));
playing.elapsed = Math.round(pct * TRACKS[playing.idx].sec);
updateScrub();
}
scrub.addEventListener("pointerdown", (e) => {
scrub.setPointerCapture(e.pointerId);
seekFromEvent(e.clientX);
const move = (ev) => seekFromEvent(ev.clientX);
const up = () => {
scrub.removeEventListener("pointermove", move);
scrub.removeEventListener("pointerup", up);
};
scrub.addEventListener("pointermove", move);
scrub.addEventListener("pointerup", up);
});
scrub.addEventListener("keydown", (e) => {
if (playing.idx < 0) return;
const step = e.key === "ArrowRight" ? 5 : e.key === "ArrowLeft" ? -5 : 0;
if (!step) return;
e.preventDefault();
playing.elapsed = Math.min(TRACKS[playing.idx].sec, Math.max(0, playing.elapsed + step));
updateScrub();
});
/* ---------- album like / add / download / menu ---------- */
$("#likeAlbum").addEventListener("click", (e) => {
const b = e.currentTarget;
const on = b.getAttribute("aria-pressed") !== "true";
b.setAttribute("aria-pressed", String(on));
toast(on ? "Saved Midnight Reservoir to your library" : "Removed from your library");
});
$("#addPlaylist").addEventListener("click", () => toast("Add to playlist"));
$("#downloadBtn").addEventListener("click", () => toast("Downloading album (simulated)"));
const moreBtn = $("#moreBtn");
const moreMenu = $("#moreMenu");
moreBtn.addEventListener("click", (e) => {
e.stopPropagation();
const open = moreMenu.classList.toggle("open");
moreBtn.setAttribute("aria-expanded", String(open));
});
moreMenu.addEventListener("click", (e) => {
const b = e.target.closest("button");
if (!b) return;
moreMenu.classList.remove("open");
moreBtn.setAttribute("aria-expanded", "false");
toast(b.dataset.toast);
});
document.addEventListener("click", () => {
moreMenu.classList.remove("open");
moreBtn.setAttribute("aria-expanded", "false");
});
/* ---------- sort toggle ---------- */
const sortToggle = $("#sortToggle");
let sorted = false;
sortToggle.addEventListener("click", () => {
sorted = !sorted;
sortToggle.setAttribute("aria-pressed", String(sorted));
$("#sortLabel").textContent = sorted ? "Most played" : "Album order";
if (sorted) {
order = TRACKS.map((_, i) => i).sort((a, b) => TRACKS[b].plays - TRACKS[a].plays);
} else {
order = TRACKS.map((_, i) => i);
}
renderTracklist();
toast(sorted ? "Sorted by play count" : "Restored album order");
});
/* ---------- more by artist ---------- */
const albumRow = $("#albumRow");
MORE_ALBUMS.forEach((al) => {
const card = document.createElement("button");
card.className = "mini";
card.innerHTML = `
<span class="mini-cover" style="background:linear-gradient(145deg,${al.a},${al.b})"></span>
<span class="mini-title">${al.title}</span><br />
<span class="mini-year">${al.year}</span>`;
card.addEventListener("click", () => toast(`Opening “${al.title}” (${al.year})`));
albumRow.appendChild(card);
});
/* ---------- init ---------- */
renderTracklist();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Midnight Reservoir — Neon Tides</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>
<main class="page">
<!-- ALBUM HEADER -->
<header class="album-head" id="albumHead">
<div class="cover-wrap">
<div class="cover" id="cover" aria-hidden="true">
<span class="cover-shape s1"></span>
<span class="cover-shape s2"></span>
<span class="cover-shape s3"></span>
<span class="cover-grid"></span>
<span class="cover-text">
<em>Neon Tides</em>
<strong>Midnight<br />Reservoir</strong>
</span>
</div>
</div>
<div class="head-meta">
<span class="eyebrow">Album</span>
<h1 class="album-title">Midnight Reservoir</h1>
<p class="artist-line">
<span class="artist-avatar" aria-hidden="true"></span>
<a href="#" class="artist-name">Neon Tides</a>
</p>
<p class="album-sub">
<span>2026</span>
<span class="dot">·</span>
<span id="trackCount">10 tracks</span>
<span class="dot">·</span>
<span id="totalRuntime">42 min</span>
</p>
<div class="head-actions">
<button class="play-album" id="playAlbum" aria-pressed="false" aria-label="Play album">
<span class="pa-icon" aria-hidden="true"></span>
<span class="pa-label">Play</span>
</button>
<button class="icon-btn" id="likeAlbum" aria-pressed="false" aria-label="Save album to library" title="Save">
<svg viewBox="0 0 24 24" class="ic-heart" aria-hidden="true"><path d="M12 21s-7.5-4.6-10-9.2C.4 8.5 1.8 5 5.2 5c2 0 3.3 1.2 3.8 2.2C9.5 6.2 10.8 5 12.8 5 16.2 5 17.6 8.5 16 11.8 13.5 16.4 12 21 12 21z"/></svg>
</button>
<button class="icon-btn" id="addPlaylist" aria-label="Add to playlist" title="Add to playlist">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 7h12M3 12h8M3 17h8M17 12v8M13 16h8" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
</button>
<button class="icon-btn" id="downloadBtn" aria-label="Download album" title="Download">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v11m0 0l-4-4m4 4l4-4M5 19h14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="more-wrap">
<button class="icon-btn" id="moreBtn" aria-haspopup="true" aria-expanded="false" aria-label="More options" title="More">
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/></svg>
</button>
<div class="more-menu" id="moreMenu" role="menu">
<button role="menuitem" data-toast="Link copied to clipboard">Copy album link</button>
<button role="menuitem" data-toast="Sharing sheet opened">Share</button>
<button role="menuitem" data-toast="Added to queue">Add to queue</button>
<button role="menuitem" data-toast="Artist radio started">Go to artist radio</button>
</div>
</div>
</div>
</div>
</header>
<!-- TRACKLIST -->
<section class="tracklist-sec" aria-labelledby="tlHeading">
<div class="tl-bar">
<h2 id="tlHeading">Tracklist</h2>
<button class="sort-toggle" id="sortToggle" aria-pressed="false">
<span class="st-dot"></span>
<span id="sortLabel">Album order</span>
</button>
</div>
<ol class="tracklist" id="tracklist"></ol>
</section>
<!-- CREDITS -->
<section class="credits-sec" aria-labelledby="crHeading">
<h2 id="crHeading">Credits & notes</h2>
<div class="credits-grid">
<div class="credit-block">
<h3>Production</h3>
<p><span>Produced by</span> Neon Tides, Mara Solenne</p>
<p><span>Mixing</span> Theo Vance</p>
<p><span>Mastering</span> Halcyon Audio, Berlin</p>
</div>
<div class="credit-block">
<h3>Writing</h3>
<p><span>Songwriting</span> Neon Tides, J. Wilder</p>
<p><span>Additional vocals</span> Velvet Static, Lake Mercer</p>
<p><span>Strings arranged by</span> The Hollow Quartet</p>
</div>
<div class="credit-block">
<h3>Release</h3>
<p><span>Label</span> Driftwave Records</p>
<p><span>Catalogue</span> DWR-0142</p>
<p><span>Format</span> Digital · Vinyl · Cassette</p>
</div>
</div>
<p class="p-line">℗ 2026 Driftwave Records, under exclusive license. All fictional, illustrative use only.</p>
</section>
<!-- MORE BY ARTIST -->
<section class="more-sec" aria-labelledby="mbHeading">
<h2 id="mbHeading">More by Neon Tides</h2>
<div class="album-row" id="albumRow"></div>
</section>
</main>
<!-- NOW PLAYING BAR -->
<div class="now-bar" id="nowBar" aria-live="polite" hidden>
<div class="nb-cover" aria-hidden="true"></div>
<div class="nb-meta">
<strong id="nbTitle">—</strong>
<span id="nbArtist">Neon Tides</span>
</div>
<div class="nb-eq" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
<div class="nb-scrub">
<span id="nbCur">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 id="nbDur">0:00</span>
</div>
<button class="nb-play" id="nbPlay" aria-pressed="true" aria-label="Pause"><span class="nb-ic" aria-hidden="true"></span></button>
</div>
<div class="toast-wrap" id="toastWrap" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Album Page (tracklist · credits)
A full album detail page for the fictional record Midnight Reservoir by Neon Tides. The header pairs a large, fully CSS-drawn cover — layered gradients, blurred shapes and a fading grid — with the album title, artist line, release year, track count and a runtime that is summed from the tracklist at load. The cover’s two accent colors theme the page: a soft glow bleeds behind the header and into the now-playing bar. A primary Play button sits alongside save (heart), add-to-playlist, download and an overflow ”…” menu.
The numbered tracklist lists each title with its featured artists, a compact play count, a like toggle and a duration. Clicking a row plays it; the active row swaps its number for a pause control and animates an equalizer in place of the timestamp. Album-level Play starts the first track and morphs to Pause, and a sort toggle flips the list between album order and most-played. Playback is fully simulated with timers — a glassy now-playing bar tracks elapsed time and auto-advances between tracks.
The scrubber supports click-to-seek, pointer dragging and ArrowLeft/ArrowRight keyboard seeking, and exposes role="slider" with live aria-valuenow. Play and like controls use aria-pressed, the overflow menu uses aria-expanded, and a small toast() helper confirms actions like saving, downloading or sharing. Below the player sit a three-column credits grid (production, writing, release, plus a ℗ line) and a More by this artist row of additional CSS-drawn covers.
Illustrative UI only — fictional artists, albums, tracks, and data. No real audio playback.