Music — Playlist Page (cover · tracks · share)
A dark, album-art-driven playlist detail page for a fictional collection called Neon Hours. A CSS-drawn four-tile mosaic cover themes the glassy header beside the title, curator, description and a live track-count, total runtime and likes line. A play, shuffle, like, share and overflow row sits above a searchable, sortable track table with per-row covers, play counts, like toggles and an active-row equalizer. Simulated playback drives a now-playing bar with a draggable, keyboard-seekable scrubber, a share popover with copy-link, and an inline-editable title.
MCP
Code
: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);
/* themed by hero cover */
--theme: #8b5cf6;
--theme-2: #ff3d71;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
min-height: 100vh;
padding-bottom: 104px;
}
.page {
max-width: 980px;
margin: 0 auto;
padding: 0 20px 40px;
}
/* ===================== HERO ===================== */
.hero {
position: relative;
display: flex;
gap: 26px;
align-items: flex-end;
padding: 56px 24px 28px;
margin: 0 -20px 8px;
border-radius: 0 0 var(--r-lg) var(--r-lg);
background:
radial-gradient(1100px 380px at 18% -10%, color-mix(in srgb, var(--theme) 40%, transparent), transparent 70%),
linear-gradient(180deg, color-mix(in srgb, var(--theme) 22%, var(--bg-2)) 0%, var(--bg) 92%);
overflow: hidden;
}
.hero-cover {
position: relative;
flex: 0 0 auto;
width: 220px;
height: 220px;
border-radius: var(--r-md);
box-shadow: var(--shadow);
overflow: hidden;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
background: #000;
}
.tile { position: relative; }
.tile.t1 { background: linear-gradient(135deg, #8b5cf6, #3b1d8a); }
.tile.t2 { background: linear-gradient(135deg, #ff3d71, #7a1338); }
.tile.t3 { background: linear-gradient(135deg, #1db954, #0d5a2c); }
.tile.t4 { background: linear-gradient(135deg, #38bdf8, #134e6b); }
.tile::after {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(60% 60% at 30% 25%, rgba(255, 255, 255, 0.32), transparent 70%);
mix-blend-mode: screen;
}
.cover-shine {
position: absolute;
inset: 0;
background: linear-gradient(115deg, transparent 42%, rgba(255, 255, 255, 0.20) 50%, transparent 58%);
pointer-events: none;
}
.hero-meta { min-width: 0; padding-bottom: 4px; }
.eyebrow {
margin: 0 0 6px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
.title {
font-family: "Space Grotesk", sans-serif;
font-weight: 700;
margin: 0 0 12px;
font-size: clamp(34px, 7vw, 66px);
line-height: 1.02;
letter-spacing: -0.02em;
cursor: text;
outline: none;
border-radius: 6px;
}
.title:focus-visible { box-shadow: 0 0 0 3px color-mix(in srgb, var(--theme) 60%, transparent); }
.title.editing {
background: rgba(0, 0, 0, 0.25);
box-shadow: 0 0 0 2px var(--line-2);
padding: 0 6px;
}
.desc {
margin: 0 0 14px;
color: var(--muted);
max-width: 60ch;
font-size: 14px;
}
.byline {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
font-size: 14px;
}
.byline b { color: var(--text); }
.byline .stat { color: var(--muted); }
.avatar {
width: 26px;
height: 26px;
border-radius: var(--r-full);
display: grid;
place-items: center;
font-size: 11px;
font-weight: 800;
background: linear-gradient(135deg, var(--theme), var(--theme-2));
color: #fff;
}
.creator { font-weight: 700; }
.dot { color: var(--line-2); }
/* ===================== CONTROLS ===================== */
.controls {
display: flex;
align-items: center;
gap: 14px;
margin-top: 22px;
flex-wrap: wrap;
}
.btn-play {
display: inline-flex;
align-items: center;
gap: 10px;
border: none;
cursor: pointer;
padding: 13px 24px 13px 20px;
border-radius: var(--r-full);
background: var(--accent);
color: #04140a;
font-family: inherit;
font-weight: 800;
font-size: 15px;
box-shadow: 0 10px 28px rgba(29, 185, 84, 0.35);
transition: transform 0.12s ease, filter 0.15s ease, box-shadow 0.15s ease;
}
.btn-play:hover { transform: scale(1.04); filter: brightness(1.05); }
.btn-play:active { transform: scale(0.98); }
.btn-play .ic-play,
.btn-play .ic-pause { background: #04140a; }
.btn-play[aria-pressed="true"] .ic-play { display: none; }
.btn-play[aria-pressed="false"] .ic-pause { display: none; }
.ic-play {
width: 0;
height: 0;
border-style: solid;
border-width: 8px 0 8px 13px;
border-color: transparent transparent transparent currentColor;
background: transparent !important;
}
.ic-pause {
width: 13px;
height: 16px;
border-left: 4px solid currentColor;
border-right: 4px solid currentColor;
background: transparent !important;
}
.ctl {
width: 44px;
height: 44px;
display: grid;
place-items: center;
border-radius: var(--r-full);
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.04);
color: var(--muted);
cursor: pointer;
transition: color 0.15s, border-color 0.15s, background 0.15s, transform 0.1s;
}
.ctl:hover { color: var(--text); border-color: var(--line-2); background: rgba(255, 255, 255, 0.08); }
.ctl:active { transform: scale(0.94); }
.ctl svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.ctl[aria-pressed="true"] { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 55%, transparent); }
#btnLikeAll[aria-pressed="true"] { color: var(--accent-3); border-color: color-mix(in srgb, var(--accent-3) 55%, transparent); }
#btnLikeAll[aria-pressed="true"] svg { fill: var(--accent-3); }
/* ===================== POPOVERS ===================== */
.share-wrap { position: relative; }
.popover {
position: absolute;
top: 54px;
left: 0;
z-index: 40;
width: 268px;
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: var(--r-md);
box-shadow: var(--shadow);
padding: 14px;
animation: pop 0.16s ease;
}
.popover.menu { width: 210px; padding: 6px; }
@keyframes pop { from { opacity: 0; transform: translateY(-6px); } }
.pop-head { margin: 0 0 10px; font-size: 13px; font-weight: 700; color: var(--muted); }
.share-row { display: flex; gap: 8px; margin-bottom: 12px; }
.share-chip {
flex: 1;
padding: 9px 6px;
border-radius: var(--r-sm);
border: 1px solid var(--line);
background: var(--surface-2);
color: var(--text);
font-family: inherit;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: border-color 0.15s, background 0.15s;
}
.share-chip:hover { border-color: var(--theme); background: color-mix(in srgb, var(--theme) 16%, var(--surface-2)); }
.copy-row { display: flex; gap: 6px; }
.copy-input {
flex: 1;
min-width: 0;
border: 1px solid var(--line);
background: var(--bg-2);
color: var(--muted);
border-radius: var(--r-sm);
padding: 9px 10px;
font-size: 12px;
font-family: inherit;
}
.copy-btn {
border: none;
cursor: pointer;
padding: 0 14px;
border-radius: var(--r-sm);
background: var(--accent);
color: #04140a;
font-family: inherit;
font-weight: 700;
font-size: 12px;
white-space: nowrap;
transition: filter 0.15s;
}
.copy-btn:hover { filter: brightness(1.08); }
.copy-btn.done { background: var(--accent-2); color: #fff; }
.menu-item {
display: block;
width: 100%;
text-align: left;
border: none;
background: transparent;
color: var(--text);
font-family: inherit;
font-size: 13px;
padding: 10px 12px;
border-radius: var(--r-sm);
cursor: pointer;
}
.menu-item:hover { background: var(--surface-2); }
.menu-item.danger { color: var(--accent-3); }
/* ===================== TOOLBAR ===================== */
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
margin: 22px 0 10px;
flex-wrap: wrap;
}
.search {
position: relative;
display: flex;
align-items: center;
flex: 1;
min-width: 200px;
max-width: 340px;
}
.search svg {
position: absolute;
left: 12px;
width: 17px;
height: 17px;
fill: none;
stroke: var(--muted);
stroke-width: 2;
stroke-linecap: round;
}
.search input {
width: 100%;
border: 1px solid var(--line);
background: var(--surface);
color: var(--text);
border-radius: var(--r-full);
padding: 11px 16px 11px 38px;
font-family: inherit;
font-size: 14px;
outline: none;
transition: border-color 0.15s;
}
.search input:focus { border-color: var(--line-2); }
.sort {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--muted);
}
.sort select {
border: 1px solid var(--line);
background: var(--surface);
color: var(--text);
border-radius: var(--r-sm);
padding: 9px 12px;
font-family: inherit;
font-size: 13px;
cursor: pointer;
outline: none;
}
/* ===================== TRACK TABLE ===================== */
.tracks { margin-top: 4px; }
.thead {
display: grid;
grid-template-columns: 30px 2.4fr 1.5fr 1fr 70px 36px 56px;
gap: 12px;
align-items: center;
padding: 8px 14px;
border-bottom: 1px solid var(--line);
color: var(--muted);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.03em;
text-transform: uppercase;
}
.thead .c-idx { text-align: center; }
.thead .c-plays { text-align: right; }
.thead .c-dur { display: grid; place-items: end; }
.thead .c-dur svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; }
.tbody { list-style: none; margin: 8px 0 0; padding: 0; }
.row {
display: grid;
grid-template-columns: 30px 2.4fr 1.5fr 1fr 70px 36px 56px;
gap: 12px;
align-items: center;
padding: 8px 14px;
border-radius: var(--r-sm);
cursor: pointer;
transition: background 0.14s;
}
.row:hover { background: rgba(255, 255, 255, 0.05); }
.row:hover .idx-num { display: none; }
.row:hover .idx-play { display: grid; }
.row.active { background: color-mix(in srgb, var(--theme) 14%, transparent); }
.c-idx { position: relative; height: 24px; }
.idx-num {
display: grid;
place-items: center;
height: 100%;
color: var(--muted);
font-size: 14px;
font-variant-numeric: tabular-nums;
}
.row.active .idx-num { display: none; }
.idx-play {
display: none;
place-items: center;
height: 100%;
color: var(--text);
}
.idx-play .ic-play { background: transparent !important; }
.row.active .idx-play { display: grid; }
.row.active .idx-play .play-tri { display: none; }
.row.active .idx-eq { display: flex; }
.idx-eq { display: none; gap: 2px; align-items: flex-end; height: 16px; }
.idx-eq i {
width: 3px;
background: var(--accent);
border-radius: 2px;
animation: eq 0.8s ease-in-out infinite;
}
.idx-eq i:nth-child(1) { height: 7px; animation-delay: -0.1s; }
.idx-eq i:nth-child(2) { height: 14px; animation-delay: -0.4s; }
.idx-eq i:nth-child(3) { height: 9px; animation-delay: -0.7s; }
.idx-eq i:nth-child(4) { height: 12px; animation-delay: -0.2s; }
.row.paused .idx-eq i { animation-play-state: paused; }
@keyframes eq { 0%, 100% { transform: scaleY(0.35); } 50% { transform: scaleY(1); } }
.c-title { display: flex; align-items: center; gap: 12px; min-width: 0; }
.tk-cover {
flex: 0 0 auto;
width: 40px;
height: 40px;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
}
.tk-txt { min-width: 0; }
.tk-name {
font-weight: 600;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.row.active .tk-name { color: var(--accent); }
.tk-artist {
font-size: 12.5px;
color: var(--muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.c-album, .c-added {
font-size: 13px;
color: var(--muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.c-plays {
text-align: right;
font-size: 13px;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.like-btn {
width: 30px;
height: 30px;
display: grid;
place-items: center;
border: none;
background: transparent;
cursor: pointer;
border-radius: var(--r-full);
opacity: 0;
transition: opacity 0.14s, transform 0.1s;
}
.row:hover .like-btn,
.like-btn[aria-pressed="true"] { opacity: 1; }
.like-btn svg { width: 17px; height: 17px; fill: none; stroke: var(--muted); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.like-btn:hover svg { stroke: var(--text); }
.like-btn:active { transform: scale(0.85); }
.like-btn[aria-pressed="true"] svg { fill: var(--accent-3); stroke: var(--accent-3); }
.c-dur {
text-align: right;
font-size: 13px;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.empty { text-align: center; color: var(--muted); padding: 40px 0; font-size: 14px; }
.foot { margin: 26px 4px 0; font-size: 12px; color: var(--muted); }
/* ===================== NOW PLAYING BAR ===================== */
.nowbar {
position: fixed;
left: 0; right: 0; bottom: 0;
z-index: 60;
display: grid;
grid-template-columns: 1fr 1.4fr 1fr;
align-items: center;
gap: 16px;
padding: 12px 20px;
background: color-mix(in srgb, var(--surface) 88%, transparent);
backdrop-filter: blur(18px) saturate(1.2);
border-top: 1px solid var(--line);
box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.5);
animation: slideUp 0.3s ease;
}
@keyframes slideUp { from { transform: translateY(100%); } }
.np-left { display: flex; align-items: center; gap: 12px; min-width: 0; }
.np-cover { width: 48px; height: 48px; border-radius: 8px; flex: 0 0 auto; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.4); }
.np-text { min-width: 0; }
.np-title { display: block; font-weight: 600; font-size: 13.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.np-artist { display: block; font-size: 12px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.np-like { width: 32px; height: 32px; border: none; background: transparent; cursor: pointer; display: grid; place-items: center; border-radius: var(--r-full); }
.np-like svg { width: 17px; height: 17px; fill: none; stroke: var(--muted); stroke-width: 2; }
.np-like[aria-pressed="true"] svg { fill: var(--accent-3); stroke: var(--accent-3); }
.np-mid { display: flex; flex-direction: column; gap: 6px; }
.np-btns { display: flex; align-items: center; justify-content: center; gap: 16px; }
.np-ctl { width: 34px; height: 34px; border: none; background: transparent; color: var(--muted); cursor: pointer; display: grid; place-items: center; border-radius: var(--r-full); }
.np-ctl:hover { color: var(--text); }
.np-ctl svg { width: 20px; height: 20px; fill: currentColor; stroke: none; }
.np-main {
width: 40px;
height: 40px;
border: none;
border-radius: var(--r-full);
background: var(--text);
color: var(--bg);
cursor: pointer;
display: grid;
place-items: center;
transition: transform 0.1s;
}
.np-main:hover { transform: scale(1.06); }
.np-main .ic-play, .np-main .ic-pause { background: transparent !important; }
.np-main[aria-pressed="true"] .ic-play { display: none; }
.np-main[aria-pressed="false"] .ic-pause { display: none; }
.np-scrub { display: flex; align-items: center; gap: 10px; }
.np-time { font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; min-width: 34px; }
.np-time:last-child { text-align: left; }
.scrub {
position: relative;
flex: 1;
height: 16px;
display: flex;
align-items: center;
cursor: pointer;
touch-action: none;
}
.scrub::before {
content: "";
position: absolute;
left: 0; right: 0;
height: 4px;
border-radius: var(--r-full);
background: var(--line-2);
}
.scrub-fill {
position: absolute;
left: 0;
height: 4px;
width: 0%;
border-radius: var(--r-full);
background: linear-gradient(90deg, var(--accent), color-mix(in srgb, var(--accent) 60%, var(--theme)));
}
.scrub-knob {
position: absolute;
left: 0%;
width: 12px;
height: 12px;
border-radius: var(--r-full);
background: #fff;
transform: translateX(-50%);
opacity: 0;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
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); }
.np-right { display: flex; align-items: center; justify-content: flex-end; gap: 14px; }
.eq.mini { display: flex; align-items: flex-end; gap: 2px; height: 18px; }
.eq.mini i {
width: 3px;
background: var(--accent);
border-radius: 2px;
animation: eq 0.8s ease-in-out infinite;
}
.eq.mini i:nth-child(1) { height: 8px; animation-delay: -0.2s; }
.eq.mini i:nth-child(2) { height: 16px; animation-delay: -0.5s; }
.eq.mini i:nth-child(3) { height: 11px; animation-delay: -0.1s; }
.eq.mini i:nth-child(4) { height: 14px; animation-delay: -0.7s; }
.nowbar.paused .eq.mini i { animation-play-state: paused; }
.np-vol svg { width: 19px; height: 19px; fill: none; stroke: var(--muted); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
/* ===================== TOASTS ===================== */
.toast-stack {
position: fixed;
left: 50%;
bottom: 120px;
transform: translateX(-50%);
z-index: 80;
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
pointer-events: none;
}
.toast {
background: var(--surface-2);
color: var(--text);
border: 1px solid var(--line-2);
border-radius: var(--r-full);
padding: 10px 18px;
font-size: 13px;
font-weight: 600;
box-shadow: var(--shadow);
animation: toastIn 0.25s ease, toastOut 0.3s ease 2.4s forwards;
}
@keyframes toastIn { from { opacity: 0; transform: translateY(10px); } }
@keyframes toastOut { to { opacity: 0; transform: translateY(-8px); } }
/* ===================== RESPONSIVE ===================== */
@media (max-width: 760px) {
.thead .c-album, .row .c-album { display: none; }
.thead, .row { grid-template-columns: 30px 2.4fr 1fr 70px 36px 50px; }
}
@media (max-width: 520px) {
body { padding-bottom: 150px; }
.page { padding: 0 14px 30px; }
.hero {
flex-direction: column;
align-items: flex-start;
gap: 18px;
padding: 40px 16px 22px;
margin: 0 -14px 8px;
}
.hero-cover { width: 150px; height: 150px; }
.title { font-size: clamp(30px, 11vw, 44px); }
.desc { font-size: 13px; }
.controls { gap: 10px; }
.thead .c-added, .row .c-added,
.thead .c-plays, .row .c-plays { display: none; }
.thead, .row { grid-template-columns: 26px 1fr 34px 46px; }
.toolbar { gap: 10px; }
.search { max-width: none; }
.nowbar {
grid-template-columns: 1fr;
gap: 8px;
padding: 10px 14px 12px;
}
.np-left { order: 1; }
.np-mid { order: 3; }
.np-right { display: none; }
.np-btns { gap: 22px; }
}
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; }
.idx-eq i, .eq.mini i { transform: scaleY(0.7); }
}
/* Visibility guard: honor the [hidden] attribute over base display */
.nowbar[hidden] {
display: none;
}(function () {
"use strict";
/* ---------- Data: fictional tracks ---------- */
var TRACKS = [
{ title: "Paper Lanterns", artist: "Neon Tides", album: "Midnight Reservoir", added: "3 days ago", addedTs: 3, plays: 1842203, dur: 222, liked: true, g1: "#8b5cf6", g2: "#3b1d8a" },
{ title: "Velvet Static", artist: "Aurora Vale", album: "Glass Cathedral", added: "1 week ago", addedTs: 7, plays: 984551, dur: 198, liked: false, g1: "#ff3d71", g2: "#7a1338" },
{ title: "Slow Tide", artist: "Neon Tides", album: "Midnight Reservoir", added: "1 week ago", addedTs: 7, plays: 2310887, dur: 241, liked: true, g1: "#1db954", g2: "#0d5a2c" },
{ title: "Cobalt Rooms", artist: "Halcyon Drift", album: "Low Orbit", added: "2 weeks ago", addedTs: 14, plays: 612034, dur: 176, liked: false, g1: "#38bdf8", g2: "#134e6b" },
{ title: "Rainfall Theory", artist: "Mara Keene", album: "Quiet Engine", added: "3 weeks ago", addedTs: 21, plays: 1455920, dur: 263, liked: false, g1: "#f59e0b", g2: "#7c4a06" },
{ title: "Ghost Lights", artist: "Aurora Vale", album: "Glass Cathedral", added: "1 month ago", addedTs: 30, plays: 728410, dur: 209, liked: true, g1: "#ec4899", g2: "#6d1640" },
{ title: "Undertow", artist: "Halcyon Drift", album: "Low Orbit", added: "1 month ago", addedTs: 30, plays: 1990066, dur: 187, liked: false, g1: "#22d3ee", g2: "#0e5563" },
{ title: "Last Train Home", artist: "Mara Keene", album: "Quiet Engine", added: "2 months ago", addedTs: 60, plays: 3045112, dur: 254, liked: false, g1: "#a78bfa", g2: "#4c2a8a" }
];
TRACKS.forEach(function (t, i) { t.id = i; t.order = i; });
/* ---------- Helpers ---------- */
function $(s, r) { return (r || document).querySelector(s); }
function fmt(sec) {
var m = Math.floor(sec / 60), s = Math.floor(sec % 60);
return m + ":" + (s < 10 ? "0" : "") + s;
}
function plays(n) { return n.toLocaleString("en-US"); }
function grad(t) { return "linear-gradient(135deg, " + t.g1 + ", " + t.g2 + ")"; }
var toasts = $("#toasts");
function toast(msg) {
var el = document.createElement("div");
el.className = "toast";
el.textContent = msg;
toasts.appendChild(el);
setTimeout(function () { el.remove(); }, 2800);
}
/* ---------- Build rows ---------- */
var tbody = $("#tbody");
var search = $("#search");
var sortSel = $("#sort");
var empty = $("#empty");
function buildRow(t) {
var li = document.createElement("li");
li.className = "row";
li.dataset.id = t.id;
li.innerHTML =
'<span class="c-idx">' +
'<span class="idx-num">' + (t.order + 1) + '</span>' +
'<span class="idx-play">' +
'<span class="play-tri ic-play"></span>' +
'<span class="idx-eq"><i></i><i></i><i></i><i></i></span>' +
'</span>' +
'</span>' +
'<span class="c-title">' +
'<span class="tk-cover" style="background:' + grad(t) + '"></span>' +
'<span class="tk-txt">' +
'<span class="tk-name">' + t.title + '</span>' +
'<span class="tk-artist">' + t.artist + '</span>' +
'</span>' +
'</span>' +
'<span class="c-album">' + t.album + '</span>' +
'<span class="c-added">' + t.added + '</span>' +
'<span class="c-plays">' + plays(t.plays) + '</span>' +
'<button class="like-btn" aria-pressed="' + t.liked + '" aria-label="Like ' + t.title + '">' +
'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21s-7.5-4.6-10-9.3C.7 8.9 2.1 5.5 5.3 5.1 7.2 4.9 9 6 12 9c3-3 4.8-4.1 6.7-3.9 3.2.4 4.6 3.8 3.3 6.6C19.5 16.4 12 21 12 21z"/></svg>' +
'</button>' +
'<span class="c-dur">' + fmt(t.dur) + '</span>';
return li;
}
function render() {
var q = search.value.trim().toLowerCase();
var list = TRACKS.slice();
var sort = sortSel.value;
var by = {
custom: function (a, b) { return a.order - b.order; },
title: function (a, b) { return a.title.localeCompare(b.title); },
artist: function (a, b) { return a.artist.localeCompare(b.artist); },
added: function (a, b) { return a.addedTs - b.addedTs; },
plays: function (a, b) { return b.plays - a.plays; },
duration: function (a, b) { return a.dur - b.dur; }
}[sort];
list.sort(by);
if (q) {
list = list.filter(function (t) {
return (t.title + " " + t.artist + " " + t.album).toLowerCase().indexOf(q) > -1;
});
}
tbody.innerHTML = "";
list.forEach(function (t) { tbody.appendChild(buildRow(t)); });
empty.hidden = list.length > 0;
syncActiveRow();
}
/* ---------- Like toggles ---------- */
function syncLikeCount() {
var base = 12489;
var extra = TRACKS.filter(function (t) { return t.liked; }).length;
$("#likeCount").textContent = (base + extra).toLocaleString("en-US");
}
tbody.addEventListener("click", function (e) {
var likeBtn = e.target.closest(".like-btn");
if (likeBtn) {
e.stopPropagation();
var row = likeBtn.closest(".row");
var t = TRACKS[+row.dataset.id];
t.liked = !t.liked;
likeBtn.setAttribute("aria-pressed", String(t.liked));
if (current && current.id === t.id) npLike.setAttribute("aria-pressed", String(t.liked));
syncLikeCount();
toast(t.liked ? "Added to Liked Songs" : "Removed from Liked Songs");
return;
}
var row2 = e.target.closest(".row");
if (row2) playTrack(TRACKS[+row2.dataset.id], true);
});
/* ---------- Player engine ---------- */
var nowbar = $("#nowbar");
var npCover = $("#npCover"), npTitle = $("#npTitle"), npArtist = $("#npArtist");
var npLike = $("#npLike"), npPlay = $("#npPlay");
var npCur = $("#npCur"), npTot = $("#npTot");
var scrub = $("#scrub"), scrubFill = $("#scrubFill"), scrubKnob = $("#scrubKnob");
var btnPlay = $("#btnPlay");
var current = null;
var playing = false;
var elapsed = 0;
var shuffle = false;
var timer = null;
function playOrder() {
// current visible/sorted order of ids
return Array.prototype.map.call(tbody.children, function (r) { return +r.dataset.id; });
}
function syncActiveRow() {
Array.prototype.forEach.call(tbody.children, function (r) {
var on = current && +r.dataset.id === current.id;
r.classList.toggle("active", on);
r.classList.toggle("paused", on && !playing);
});
}
function setScrub(pct) {
scrubFill.style.width = pct + "%";
scrubKnob.style.left = pct + "%";
scrub.setAttribute("aria-valuenow", Math.round(pct));
}
function tick() {
if (!playing || !current) return;
elapsed += 1;
if (elapsed >= current.dur) { next(); return; }
npCur.textContent = fmt(elapsed);
setScrub((elapsed / current.dur) * 100);
}
function startTimer() { clearInterval(timer); timer = setInterval(tick, 1000); }
function setPlaying(on) {
playing = on;
nowbar.classList.toggle("paused", !on);
npPlay.setAttribute("aria-pressed", String(on));
npPlay.setAttribute("aria-label", on ? "Pause" : "Play");
btnPlay.setAttribute("aria-pressed", String(on));
$(".lbl", btnPlay).textContent = on ? "Pause" : "Play";
syncActiveRow();
if (on) startTimer(); else clearInterval(timer);
}
function loadTrack(t, resetTime) {
current = t;
if (resetTime) elapsed = 0;
nowbar.hidden = false;
npCover.style.background = grad(t);
npTitle.textContent = t.title;
npArtist.textContent = t.artist;
npLike.setAttribute("aria-pressed", String(t.liked));
npTot.textContent = fmt(t.dur);
npCur.textContent = fmt(elapsed);
setScrub((elapsed / t.dur) * 100);
document.body.style.setProperty("--theme", t.g1);
syncActiveRow();
}
function playTrack(t, resetTime) {
var same = current && current.id === t.id;
loadTrack(t, resetTime && !same);
if (same) { setPlaying(!playing); }
else { setPlaying(true); }
}
function firstInView() {
var order = playOrder();
return order.length ? TRACKS[order[0]] : TRACKS[0];
}
function next() {
var order = playOrder();
if (!order.length) return;
if (shuffle) {
var pool = order.filter(function (id) { return !current || id !== current.id; });
var pick = pool.length ? pool[Math.floor(Math.random() * pool.length)] : order[0];
playTrack(TRACKS[pick], true);
return;
}
var idx = current ? order.indexOf(current.id) : -1;
var nextId = order[(idx + 1) % order.length];
playTrack(TRACKS[nextId], true);
}
function prev() {
if (elapsed > 3) { elapsed = 0; loadTrack(current, false); return; }
var order = playOrder();
if (!order.length) return;
var idx = current ? order.indexOf(current.id) : 0;
var prevId = order[(idx - 1 + order.length) % order.length];
playTrack(TRACKS[prevId], true);
}
/* ---------- Top + bar controls ---------- */
btnPlay.addEventListener("click", function () {
if (!current) playTrack(firstInView(), true);
else setPlaying(!playing);
});
npPlay.addEventListener("click", function () {
if (!current) playTrack(firstInView(), true);
else setPlaying(!playing);
});
$("#npNext").addEventListener("click", next);
$("#npPrev").addEventListener("click", prev);
npLike.addEventListener("click", function () {
if (!current) return;
current.liked = !current.liked;
npLike.setAttribute("aria-pressed", String(current.liked));
var row = tbody.querySelector('.row[data-id="' + current.id + '"] .like-btn');
if (row) row.setAttribute("aria-pressed", String(current.liked));
syncLikeCount();
toast(current.liked ? "Added to Liked Songs" : "Removed from Liked Songs");
});
var btnShuffle = $("#btnShuffle");
btnShuffle.addEventListener("click", function () {
shuffle = !shuffle;
btnShuffle.setAttribute("aria-pressed", String(shuffle));
toast(shuffle ? "Shuffle on" : "Shuffle off");
if (shuffle && !playing) playTrack(firstInView(), true);
});
var btnLikeAll = $("#btnLikeAll");
btnLikeAll.addEventListener("click", function () {
var on = btnLikeAll.getAttribute("aria-pressed") !== "true";
btnLikeAll.setAttribute("aria-pressed", String(on));
toast(on ? "Saved to Your Library" : "Removed from Your Library");
});
/* ---------- Scrubber: click, drag, keyboard ---------- */
function seekFromX(clientX) {
if (!current) return;
var rect = scrub.getBoundingClientRect();
var pct = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
elapsed = Math.round(pct * current.dur);
npCur.textContent = fmt(elapsed);
setScrub(pct * 100);
}
var dragging = false;
scrub.addEventListener("pointerdown", function (e) {
if (!current) return;
dragging = true;
scrub.setPointerCapture(e.pointerId);
seekFromX(e.clientX);
});
scrub.addEventListener("pointermove", function (e) { if (dragging) seekFromX(e.clientX); });
scrub.addEventListener("pointerup", function () { dragging = false; });
scrub.addEventListener("keydown", function (e) {
if (!current) return;
if (e.key === "ArrowLeft") { elapsed = Math.max(0, elapsed - 5); }
else if (e.key === "ArrowRight") { elapsed = Math.min(current.dur, elapsed + 5); }
else if (e.key === "Home") { elapsed = 0; }
else if (e.key === "End") { elapsed = current.dur - 1; }
else return;
e.preventDefault();
npCur.textContent = fmt(elapsed);
setScrub((elapsed / current.dur) * 100);
});
/* ---------- Search + sort ---------- */
search.addEventListener("input", render);
sortSel.addEventListener("change", function () {
render();
toast("Sorted by " + sortSel.options[sortSel.selectedIndex].text.toLowerCase());
});
/* ---------- Popovers (share + more) ---------- */
function wirePopover(btn, pop) {
btn.addEventListener("click", function (e) {
e.stopPropagation();
var open = !pop.hidden;
closeAllPops();
if (!open) { pop.hidden = false; btn.setAttribute("aria-expanded", "true"); }
});
}
function closeAllPops() {
[["#btnShare", "#sharePop"], ["#btnMore", "#morePop"]].forEach(function (p) {
$(p[1]).hidden = true;
$(p[0]).setAttribute("aria-expanded", "false");
});
}
wirePopover($("#btnShare"), $("#sharePop"));
wirePopover($("#btnMore"), $("#morePop"));
document.addEventListener("click", closeAllPops);
document.addEventListener("keydown", function (e) { if (e.key === "Escape") closeAllPops(); });
$("#sharePop").addEventListener("click", function (e) { e.stopPropagation(); });
Array.prototype.forEach.call(document.querySelectorAll(".share-chip"), function (c) {
c.addEventListener("click", function () { closeAllPops(); toast("Shared via " + c.dataset.share); });
});
var copyBtn = $("#copyBtn"), copyInput = $("#copyInput");
copyBtn.addEventListener("click", function () {
var done = function () {
copyBtn.textContent = "Copied!";
copyBtn.classList.add("done");
toast("Link copied to clipboard");
setTimeout(function () { copyBtn.textContent = "Copy link"; copyBtn.classList.remove("done"); }, 1800);
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(copyInput.value).then(done, function () {
copyInput.select(); document.execCommand && document.execCommand("copy"); done();
});
} else {
copyInput.select(); document.execCommand && document.execCommand("copy"); done();
}
});
$("#morePop").addEventListener("click", function (e) {
var item = e.target.closest(".menu-item");
if (item) { closeAllPops(); toast(item.dataset.act); }
});
/* ---------- Editable title ---------- */
var plTitle = $("#plTitle");
plTitle.addEventListener("click", function () { startEdit(); });
plTitle.addEventListener("keydown", function (e) {
if (!plTitle.isContentEditable && (e.key === "Enter" || e.key === " ")) { e.preventDefault(); startEdit(); }
});
function startEdit() {
plTitle.contentEditable = "true";
plTitle.classList.add("editing");
plTitle.focus();
var r = document.createRange();
r.selectNodeContents(plTitle);
var sel = window.getSelection();
sel.removeAllRanges(); sel.addRange(r);
}
function endEdit(save) {
plTitle.contentEditable = "false";
plTitle.classList.remove("editing");
var v = plTitle.textContent.trim();
if (!v) { plTitle.textContent = "Untitled playlist"; }
if (save) { document.title = plTitle.textContent + " — Playlist"; toast("Playlist renamed"); }
}
plTitle.addEventListener("keydown", function (e) {
if (!plTitle.isContentEditable) return;
if (e.key === "Enter") { e.preventDefault(); plTitle.blur(); }
if (e.key === "Escape") { plTitle.blur(); }
});
plTitle.addEventListener("blur", function () { if (plTitle.isContentEditable) endEdit(true); });
/* ---------- Footer stats ---------- */
function initStats() {
$("#trackCount").textContent = TRACKS.length;
var total = TRACKS.reduce(function (s, t) { return s + t.dur; }, 0);
var min = Math.round(total / 60);
$("#totalDur").textContent = min + " min";
syncLikeCount();
}
/* ---------- Init ---------- */
initStats();
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Neon Hours — Playlist</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">
<!-- ===== Header ===== -->
<header class="hero" id="hero">
<div class="hero-cover" aria-hidden="true">
<span class="tile t1"></span>
<span class="tile t2"></span>
<span class="tile t3"></span>
<span class="tile t4"></span>
<span class="cover-shine"></span>
</div>
<div class="hero-meta">
<p class="eyebrow">Public playlist</p>
<h1 class="title" id="plTitle" tabindex="0" role="textbox" aria-label="Playlist title, click to edit" title="Click to rename">Neon Hours</h1>
<p class="desc">Late-night synth, slow-burn dream pop and rainy-window ballads. Curated for the drive home after midnight.</p>
<div class="byline">
<span class="avatar" aria-hidden="true">VS</span>
<span class="creator">Velvet Static</span>
<span class="dot">·</span>
<span class="stat"><b id="trackCount">8</b> tracks</span>
<span class="dot">·</span>
<span class="stat" id="totalDur">31 min</span>
<span class="dot">·</span>
<span class="stat"><b id="likeCount">12,489</b> likes</span>
</div>
<div class="controls">
<button class="btn-play" id="btnPlay" aria-pressed="false" aria-label="Play playlist">
<span class="ic-play" aria-hidden="true"></span>
<span class="ic-pause" aria-hidden="true"></span>
<span class="lbl">Play</span>
</button>
<button class="ctl" id="btnShuffle" aria-pressed="false" aria-label="Shuffle" title="Shuffle">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M16 3h5v5M4 20 21 3M21 16v5h-5M15 15l6 6M4 4l5 5"/></svg>
</button>
<button class="ctl" id="btnLikeAll" aria-pressed="true" aria-label="Like playlist" title="Like">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21s-7.5-4.6-10-9.3C.7 8.9 2.1 5.5 5.3 5.1 7.2 4.9 9 6 12 9c3-3 4.8-4.1 6.7-3.9 3.2.4 4.6 3.8 3.3 6.6C19.5 16.4 12 21 12 21z"/></svg>
</button>
<div class="share-wrap">
<button class="ctl" id="btnShare" aria-haspopup="true" aria-expanded="false" aria-label="Share" title="Share">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M18 8a3 3 0 1 0-2.8-4M6 12a3 3 0 1 0 0 .1M18 16a3 3 0 1 0-2.8 4M8.6 13.5l6.8 4M15.4 6.5l-6.8 4"/></svg>
</button>
<div class="popover" id="sharePop" role="menu" aria-label="Share options" hidden>
<p class="pop-head">Share this playlist</p>
<div class="share-row">
<button class="share-chip" data-share="Twitter">𝕏 Post</button>
<button class="share-chip" data-share="WhatsApp">Message</button>
<button class="share-chip" data-share="Embed">Embed</button>
</div>
<div class="copy-row">
<input id="copyInput" class="copy-input" readonly value="https://neon.fm/p/neon-hours" aria-label="Playlist link" />
<button class="copy-btn" id="copyBtn">Copy link</button>
</div>
</div>
</div>
<button class="ctl" id="btnMore" 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="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg>
</button>
<div class="popover menu" id="morePop" role="menu" aria-label="More options" hidden>
<button class="menu-item" data-act="Added to queue">Add to queue</button>
<button class="menu-item" data-act="Download started">Download</button>
<button class="menu-item" data-act="Editing details">Edit details</button>
<button class="menu-item danger" data-act="Removed from library">Remove from library</button>
</div>
</div>
</div>
</header>
<!-- ===== Toolbar ===== -->
<section class="toolbar">
<div class="search">
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input type="search" id="search" placeholder="Search in playlist" aria-label="Search in playlist" />
</div>
<label class="sort">
<span>Sort</span>
<select id="sort" aria-label="Sort tracks">
<option value="custom">Custom order</option>
<option value="title">Title</option>
<option value="artist">Artist</option>
<option value="added">Date added</option>
<option value="plays">Most played</option>
<option value="duration">Duration</option>
</select>
</label>
</section>
<!-- ===== Track table ===== -->
<section class="tracks" aria-label="Track list">
<div class="thead" role="row">
<span class="c-idx">#</span>
<span class="c-title">Title</span>
<span class="c-album">Album</span>
<span class="c-added">Date added</span>
<span class="c-plays">Plays</span>
<span class="c-like"></span>
<span class="c-dur">
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>
</span>
</div>
<ol class="tbody" id="tbody"></ol>
<p class="empty" id="empty" hidden>No tracks match your search.</p>
</section>
<p class="foot">Made by Velvet Static · Updated 2 days ago</p>
</main>
<!-- ===== Now playing bar ===== -->
<div class="nowbar" id="nowbar" hidden>
<div class="np-left">
<div class="np-cover" id="npCover" aria-hidden="true"></div>
<div class="np-text">
<span class="np-title" id="npTitle">—</span>
<span class="np-artist" id="npArtist">—</span>
</div>
<button class="np-like" id="npLike" aria-pressed="false" aria-label="Like current track">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21s-7.5-4.6-10-9.3C.7 8.9 2.1 5.5 5.3 5.1 7.2 4.9 9 6 12 9c3-3 4.8-4.1 6.7-3.9 3.2.4 4.6 3.8 3.3 6.6C19.5 16.4 12 21 12 21z"/></svg>
</button>
</div>
<div class="np-mid">
<div class="np-btns">
<button class="np-ctl" id="npPrev" aria-label="Previous"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M19 5v14L8 12zM5 5v14"/></svg></button>
<button class="np-main" id="npPlay" aria-pressed="true" aria-label="Pause">
<span class="ic-play" aria-hidden="true"></span>
<span class="ic-pause" aria-hidden="true"></span>
</button>
<button class="np-ctl" id="npNext" aria-label="Next"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 5v14l11-7zM19 5v14"/></svg></button>
</div>
<div class="np-scrub">
<span class="np-time" id="npCur">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="np-time" id="npTot">0:00</span>
</div>
</div>
<div class="np-right">
<div class="eq mini" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
<span class="np-vol">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M11 5 6 9H2v6h4l5 4zM15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13"/></svg>
</span>
</div>
</div>
<div class="toast-stack" id="toasts" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Playlist Page (cover · tracks · share)
A full playlist detail page for the fictional collection Neon Hours by curator Velvet Static. The header pairs a large, fully CSS-drawn mosaic cover — four gradient tiles with a sweeping shine — against a glassy, theme-tinted backdrop. Beside it sit the playlist title, curator byline, description and a meta line that computes the track count, total runtime and like count from the data at load. A big green Play button leads a control row with shuffle, like, share and an overflow ”…” menu.
The track table lists each song with its own CSS-drawn cover, title, artist, album, date added, play count, a like toggle and a duration. Hovering a row swaps the index for a play triangle; the active track shows an animated equalizer and tints to the accent. A search box filters across title, artist and album in real time, and a sort dropdown reorders by title, artist, date added, most played or duration. Playback is fully simulated with timers — a glassy now-playing bar tracks elapsed time, auto-advances, and honors shuffle.
The scrubber supports click-to-seek, pointer dragging and ArrowLeft/ArrowRight keyboard seeking, exposing role="slider" with live aria-valuenow. Play, like and shuffle controls use aria-pressed, the share and overflow buttons use aria-expanded, and a small toast() helper confirms actions. The share popover carries quick-share chips and a copy-link field with clipboard fallback, and the playlist title is editable inline on click.
Illustrative UI only — fictional artists, albums, tracks, and data. No real audio playback.