Music — Artist Page (hero · top tracks · about)
A dark, immersive streaming-style artist profile for the fictional act Neon Tides, built in plain HTML, CSS, and vanilla JS. A full-bleed hero with a CSS-drawn animated banner pulls its accent from the artist color, framing a verified name, monthly-listener count, and a Play, Follow, shuffle, and more actions row. A Popular top-tracks list shows rank, cover, play counts, and durations with hover-play and a live equalizer on the active track, backed by a discography strip, an About card, a Fans also like row, and a now-playing bar with a working scrubber.
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);
--artist: #1db954;
--artist-2: #0e7a3a;
}
* { 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;
padding-bottom: 92px;
}
h1, h2 { margin: 0; }
button { font-family: inherit; cursor: pointer; }
.page { max-width: 1040px; margin: 0 auto; }
/* ===== HERO ===== */
.hero {
position: relative;
min-height: 380px;
display: flex;
align-items: flex-end;
padding: 28px clamp(18px, 4vw, 40px) 30px;
overflow: hidden;
isolation: isolate;
}
.hero-art { position: absolute; inset: 0; z-index: -2; background: var(--bg-2); }
.hero-art::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(11,11,15,0) 35%, var(--bg) 96%),
linear-gradient(180deg, rgba(11,11,15,0.18), rgba(11,11,15,0.55));
}
.blob {
position: absolute;
border-radius: 50%;
filter: blur(46px);
opacity: 0.9;
}
.hero-art .b1 {
width: 460px; height: 460px;
top: -140px; left: -80px;
background: radial-gradient(circle at 30% 30%, color-mix(in srgb, var(--artist) 92%, white), var(--artist));
animation: drift 14s ease-in-out infinite;
}
.hero-art .b2 {
width: 380px; height: 380px;
top: -60px; right: 8%;
background: radial-gradient(circle at 60% 40%, var(--accent-2), color-mix(in srgb, var(--artist) 60%, #000));
animation: drift 18s ease-in-out infinite reverse;
}
.hero-art .b3 {
width: 320px; height: 320px;
bottom: -120px; left: 38%;
background: radial-gradient(circle at 50% 50%, var(--artist-2), transparent 70%);
animation: drift 20s ease-in-out infinite;
}
.grain {
position: absolute; inset: 0;
background-image: radial-gradient(rgba(255,255,255,0.06) 1px, transparent 1px);
background-size: 4px 4px;
mix-blend-mode: overlay;
}
@keyframes drift {
0%, 100% { transform: translate3d(0,0,0) scale(1); }
50% { transform: translate3d(20px, 26px, 0) scale(1.08); }
}
.hero-inner { position: relative; width: 100%; }
.verified {
display: inline-flex; align-items: center; gap: 7px;
color: #fff; font-size: 13px; font-weight: 600;
text-shadow: 0 1px 8px rgba(0,0,0,0.5);
}
.verified svg { color: #3ea6ff; }
.artist-name {
font-family: "Space Grotesk", sans-serif;
font-weight: 700;
font-size: clamp(48px, 12vw, 110px);
line-height: 0.94;
letter-spacing: -0.02em;
margin: 6px 0 14px;
text-shadow: 0 2px 30px rgba(0,0,0,0.4);
}
.listeners { margin: 0 0 22px; color: #e7e7ee; font-size: 15px; }
.listeners strong { font-weight: 700; }
/* actions */
.actions { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.btn-play {
width: 58px; height: 58px;
border: none; border-radius: var(--r-full);
background: var(--artist);
color: #061206;
display: grid; place-items: center;
box-shadow: 0 10px 26px color-mix(in srgb, var(--artist) 45%, transparent);
transition: transform .15s ease, filter .15s ease;
}
.btn-play:hover { transform: scale(1.06); filter: brightness(1.08); }
.btn-play:active { transform: scale(0.97); }
.btn-play .ic-pause { display: none; }
.btn-play[aria-pressed="true"] .ic-play { display: none; }
.btn-play[aria-pressed="true"] .ic-pause { display: block; }
.btn-follow {
padding: 11px 24px;
border: 1px solid var(--line-2);
border-radius: var(--r-full);
background: transparent;
color: var(--text);
font-weight: 700; font-size: 14px;
letter-spacing: 0.02em;
transition: border-color .15s ease, background .15s ease, transform .12s ease;
}
.btn-follow:hover { border-color: #fff; transform: scale(1.03); }
.btn-follow[aria-pressed="true"] {
background: color-mix(in srgb, var(--artist) 20%, transparent);
border-color: var(--artist);
}
.btn-ghost.icon {
width: 44px; height: 44px;
border-radius: var(--r-full);
border: none; background: transparent;
color: var(--muted);
display: grid; place-items: center;
transition: color .15s ease, background .15s ease;
}
.btn-ghost.icon:hover { color: var(--text); background: var(--surface); }
/* ===== SECTIONS ===== */
.section { padding: 18px clamp(18px, 4vw, 40px) 8px; }
.sec-head { display: flex; align-items: baseline; justify-content: space-between; }
.sec-h {
font-family: "Space Grotesk", sans-serif;
font-weight: 700; font-size: 22px;
margin-bottom: 14px;
}
.see-all { color: var(--muted); font-size: 12px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; cursor: pointer; }
.see-all:hover { color: var(--text); }
/* ===== TRACKS ===== */
.tracks { list-style: none; margin: 0; padding: 0; }
.track {
display: grid;
grid-template-columns: 28px 44px 1fr auto auto;
align-items: center;
gap: 14px;
padding: 8px 12px;
border-radius: var(--r-sm);
transition: background .15s ease;
}
.track.hidden { display: none; }
.track:hover { background: var(--surface); }
.track.playing { background: color-mix(in srgb, var(--artist) 14%, var(--surface)); }
.t-rank { position: relative; color: var(--muted); font-variant-numeric: tabular-nums; font-size: 15px; text-align: center; }
.track:hover .t-rank-num, .track.playing .t-rank-num { opacity: 0; }
.t-hit { position: absolute; inset: 0; display: grid; place-items: center; opacity: 0; transition: opacity .15s ease; }
.track:hover .t-hit, .track.playing .t-hit { opacity: 1; }
.t-hit svg { color: var(--text); }
.t-mini-play { background: none; border: none; padding: 0; color: var(--text); display: grid; place-items: center; }
.track.playing .t-mini { display: none; }
/* equalizer (shown on playing track) */
.eq { display: none; gap: 2px; align-items: flex-end; height: 16px; }
.track.playing .t-hit .eq { display: flex; }
.track.playing .t-hit .t-mini-play { display: none; }
.eq span {
width: 3px; background: var(--artist); border-radius: 2px;
animation: eq 0.9s ease-in-out infinite;
}
.eq span:nth-child(1) { height: 40%; animation-delay: -0.2s; }
.eq span:nth-child(2) { height: 90%; animation-delay: -0.5s; }
.eq span:nth-child(3) { height: 60%; animation-delay: -0.1s; }
.eq span:nth-child(4) { height: 100%; animation-delay: -0.7s; }
@keyframes eq { 0%,100% { transform: scaleY(0.35); } 50% { transform: scaleY(1); } }
.t-cover {
width: 44px; height: 44px;
border-radius: 6px;
background: linear-gradient(135deg, var(--c1), var(--c2));
box-shadow: inset 0 0 0 1px var(--line);
}
.t-main { min-width: 0; }
.t-title { font-weight: 600; font-size: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.track.playing .t-title { color: var(--artist); }
.t-plays { color: var(--muted); font-size: 13px; font-variant-numeric: tabular-nums; text-align: right; }
.t-like {
width: 34px; height: 34px; border: none; background: none;
color: var(--muted); display: grid; place-items: center;
border-radius: var(--r-full); opacity: 0;
transition: opacity .15s ease, color .15s ease, transform .12s ease;
}
.track:hover .t-like, .t-like[aria-pressed="true"] { opacity: 1; }
.t-like:hover { color: var(--text); }
.t-like[aria-pressed="true"] { color: var(--artist); }
.t-like[aria-pressed="true"] svg { transform: scale(1.1); }
.t-dur { color: var(--muted); font-size: 13px; font-variant-numeric: tabular-nums; min-width: 36px; text-align: right; }
.show-more {
margin: 14px 12px 0;
background: none; border: none;
color: var(--muted); font-weight: 700; font-size: 13px;
letter-spacing: 0.04em; text-transform: uppercase;
}
.show-more:hover { color: var(--text); }
/* ===== ROWS / CARDS ===== */
.row {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 168px;
gap: 16px;
overflow-x: auto;
padding-bottom: 12px;
scroll-snap-type: x proximity;
scrollbar-width: thin;
}
.row::-webkit-scrollbar { height: 8px; }
.row::-webkit-scrollbar-thumb { background: var(--surface-2); border-radius: var(--r-full); }
.album {
scroll-snap-align: start;
background: var(--surface);
border-radius: var(--r-md);
padding: 12px;
transition: background .2s ease, transform .2s ease;
cursor: pointer;
}
.album:hover { background: var(--surface-2); transform: translateY(-2px); }
.album-art {
position: relative;
aspect-ratio: 1;
border-radius: var(--r-sm);
overflow: hidden;
margin-bottom: 12px;
background: linear-gradient(150deg, var(--c1), var(--c2));
box-shadow: var(--shadow);
}
.album-art::before {
content: "";
position: absolute; inset: 0;
background: radial-gradient(circle at 72% 26%, rgba(255,255,255,0.35), transparent 42%),
conic-gradient(from 200deg at 30% 70%, transparent, rgba(0,0,0,0.25), transparent);
}
.album-art .a-disc {
position: absolute; right: -22px; bottom: -22px;
width: 86px; height: 86px; border-radius: 50%;
background: repeating-radial-gradient(circle, rgba(0,0,0,0.35) 0 2px, rgba(0,0,0,0.15) 2px 4px);
box-shadow: inset 0 0 0 3px rgba(255,255,255,0.12);
}
.album-art .a-disc::after {
content: ""; position: absolute; inset: 38%;
border-radius: 50%; background: var(--c1);
}
.album .a-mini {
position: absolute; right: 10px; bottom: 10px;
width: 42px; height: 42px; border-radius: 50%;
background: var(--artist); color: #061206;
border: none; display: grid; place-items: center;
box-shadow: 0 8px 18px rgba(0,0,0,0.4);
opacity: 0; transform: translateY(8px);
transition: opacity .2s ease, transform .2s ease;
}
.album:hover .a-mini { opacity: 1; transform: translateY(0); }
.a-title { font-weight: 600; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.a-sub { color: var(--muted); font-size: 13px; margin-top: 2px; }
/* artist cards */
.row.artists { grid-auto-columns: 158px; }
.fan { text-align: center; }
.fan-art {
aspect-ratio: 1; border-radius: 50%;
background: linear-gradient(150deg, var(--c1), var(--c2));
box-shadow: var(--shadow);
margin-bottom: 10px;
position: relative; overflow: hidden;
}
.fan-art::after {
content: ""; position: absolute; inset: 0;
background: radial-gradient(circle at 35% 30%, rgba(255,255,255,0.3), transparent 45%);
}
.fan .a-title { white-space: normal; }
/* ===== ABOUT ===== */
.about {
display: grid;
grid-template-columns: 300px 1fr;
gap: 22px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
}
.about-art {
position: relative; min-height: 240px;
background: var(--bg-2); overflow: hidden;
}
.about-art .b1 {
width: 240px; height: 240px; top: -40px; left: -40px;
background: radial-gradient(circle, var(--artist), transparent 70%);
}
.about-art .b2 {
width: 200px; height: 200px; bottom: -50px; right: -30px;
background: radial-gradient(circle, var(--accent-2), transparent 70%);
}
.about-body { padding: 24px 26px 24px 0; }
.about-bio { margin: 0 0 22px; color: #d7d7df; font-size: 15px; max-width: 56ch; }
.about-bio em { color: var(--text); font-style: italic; }
.about-stats { display: flex; gap: 30px; flex-wrap: wrap; }
.stat { display: flex; flex-direction: column; gap: 2px; }
.stat-n { font-family: "Space Grotesk", sans-serif; font-weight: 700; font-size: 26px; }
.stat-l { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; }
/* ===== NOW PLAYING BAR ===== */
.nowbar {
position: fixed; left: 0; right: 0; bottom: 0;
z-index: 30;
display: grid;
grid-template-columns: 52px minmax(120px, 220px) 44px 1fr;
align-items: center; gap: 14px;
padding: 12px clamp(14px, 4vw, 28px);
background: color-mix(in srgb, var(--surface) 88%, #000);
border-top: 1px solid var(--line);
backdrop-filter: blur(14px);
}
.np-cover {
width: 52px; height: 52px; border-radius: 8px;
background: linear-gradient(135deg, var(--c1, var(--artist)), var(--c2, var(--accent-2)));
box-shadow: inset 0 0 0 1px var(--line);
}
.np-meta { min-width: 0; display: flex; flex-direction: column; }
.np-title { font-weight: 600; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.np-meta .np-artist { color: var(--muted); font-size: 12px; }
.np-toggle {
width: 42px; height: 42px; border-radius: 50%;
border: none; background: var(--text); color: #0b0b0f;
display: grid; place-items: center;
transition: transform .12s ease;
}
.np-toggle:hover { transform: scale(1.06); }
.np-toggle .ic-play { display: none; }
.np-toggle[aria-pressed="false"] .ic-play { display: block; }
.np-toggle[aria-pressed="false"] .ic-pause { display: none; }
.np-scrub { display: flex; align-items: center; gap: 10px; }
.np-time { color: var(--muted); font-size: 11px; font-variant-numeric: tabular-nums; min-width: 32px; }
.np-time:last-child { text-align: right; }
.scrub {
position: relative; flex: 1; height: 14px;
display: flex; align-items: center; cursor: pointer;
}
.scrub::before {
content: ""; position: absolute; left: 0; right: 0; height: 4px;
background: var(--line-2); border-radius: var(--r-full);
}
.scrub-fill {
position: absolute; left: 0; height: 4px; width: 0%;
background: var(--text); border-radius: var(--r-full);
}
.scrub:hover .scrub-fill { background: var(--artist); }
.scrub-knob {
position: absolute; left: 0; width: 12px; height: 12px;
border-radius: 50%; background: var(--text);
transform: translateX(-50%); opacity: 0;
transition: opacity .15s ease;
}
.scrub:hover .scrub-knob, .scrub:focus-visible .scrub-knob { opacity: 1; }
/* ===== TOAST ===== */
.toast {
position: fixed; left: 50%; bottom: 104px;
transform: translate(-50%, 14px);
background: var(--surface-2);
border: 1px solid var(--line-2);
color: var(--text);
padding: 11px 18px; border-radius: var(--r-full);
font-size: 14px; font-weight: 600;
box-shadow: var(--shadow);
opacity: 0; pointer-events: none;
transition: opacity .2s ease, transform .2s ease;
z-index: 40;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
:focus-visible { outline: 2px solid var(--artist); outline-offset: 2px; border-radius: 4px; }
/* ===== RESPONSIVE ===== */
@media (max-width: 520px) {
body { padding-bottom: 132px; }
.hero { min-height: 320px; padding-bottom: 24px; }
.artist-name { font-size: clamp(40px, 16vw, 64px); }
.track {
grid-template-columns: 24px 40px 1fr auto;
gap: 10px;
}
.t-plays { display: none; }
.about { grid-template-columns: 1fr; }
.about-art { min-height: 140px; }
.about-body { padding: 0 18px 22px; }
.about-stats { gap: 20px; }
.nowbar {
grid-template-columns: 46px 1fr 40px;
grid-template-areas: "cover meta toggle" "scrub scrub scrub";
row-gap: 8px;
}
.np-cover { grid-area: cover; }
.np-meta { grid-area: meta; }
.np-toggle { grid-area: toggle; }
.np-scrub { grid-area: scrub; }
}
@media (prefers-reduced-motion: reduce) {
.blob { animation: none !important; }
.eq span { animation: none; }
* { transition-duration: 0.01ms !important; }
}
/* Visibility guard: honor the [hidden] attribute over base display */
.nowbar[hidden] {
display: none;
}(function () {
"use strict";
// ---- Data (fictional) ----
var TRACKS = [
{ id: "t1", title: "Paper Lanterns", plays: "182,440,118", dur: 222, c1: "#1db954", c2: "#0e7a3a", liked: true },
{ id: "t2", title: "Midnight Reservoir", plays: "146,902,331", dur: 256, c1: "#8b5cf6", c2: "#3b1d7a", liked: false },
{ id: "t3", title: "Velvet Static", plays: "121,773,540", dur: 198, c1: "#ff3d71", c2: "#7a1d36", liked: false },
{ id: "t4", title: "Coastal Glow", plays: "98,210,664", dur: 241, c1: "#22d3ee", c2: "#0e5a6a", liked: false },
{ id: "t5", title: "Saltwater Neon", plays: "76,558,902", dur: 207, c1: "#f59e0b", c2: "#7a5210", liked: false },
{ id: "t6", title: "Halcyon Drift", plays: "61,330,210", dur: 274, c1: "#ec4899", c2: "#7a2150", liked: true },
{ id: "t7", title: "Low Tide Lights", plays: "44,118,775", dur: 189, c1: "#34d399", c2: "#0f6b4d", liked: false },
{ id: "t8", title: "Marble Skies", plays: "39,902,114", dur: 233, c1: "#60a5fa", c2: "#1e3a8a", liked: false }
];
var ALBUMS = [
{ title: "Midnight Reservoir", year: "2026 · Album", c1: "#8b5cf6", c2: "#1db954" },
{ title: "Coastal Static", year: "2025 · Album", c1: "#22d3ee", c2: "#ff3d71" },
{ title: "Paper Lanterns", year: "2024 · Single", c1: "#1db954", c2: "#0e7a3a" },
{ title: "Neon Demos", year: "2023 · EP", c1: "#f59e0b", c2: "#ec4899" },
{ title: "Tidewater", year: "2022 · Album", c1: "#60a5fa", c2: "#8b5cf6" }
];
var FANS = [
{ name: "Velvet Static", sub: "Artist", c1: "#ff3d71", c2: "#7a1d36" },
{ name: "Glasshouse", sub: "Artist", c1: "#22d3ee", c2: "#1e3a8a" },
{ name: "Mara Vel", sub: "Artist", c1: "#8b5cf6", c2: "#3b1d7a" },
{ name: "Coral Hours", sub: "Artist", c1: "#34d399", c2: "#0f6b4d" },
{ name: "Lumen Ave", sub: "Artist", c1: "#f59e0b", c2: "#7a5210" },
{ name: "Northbound", sub: "Artist", c1: "#60a5fa", c2: "#1e3a8a" }
];
var $ = function (s, r) { return (r || document).querySelector(s); };
var $$ = function (s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); };
function fmt(sec) {
var m = Math.floor(sec / 60);
var s = Math.floor(sec % 60);
return m + ":" + (s < 10 ? "0" : "") + s;
}
// ---- Toast ----
var toastEl = $("#toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2200);
}
// ---- Render top tracks ----
var trackList = $("#trackList");
TRACKS.forEach(function (t, i) {
var li = document.createElement("li");
li.className = "track" + (i >= 5 ? " hidden extra" : "");
li.dataset.id = t.id;
li.style.setProperty("--c1", t.c1);
li.style.setProperty("--c2", t.c2);
li.innerHTML =
'<div class="t-rank">' +
'<span class="t-rank-num">' + (i + 1) + '</span>' +
'<span class="t-hit">' +
'<button class="t-mini-play t-mini" type="button" aria-label="Play ' + t.title + '">' +
'<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M8 5v14l11-7z"/></svg>' +
'</button>' +
'<span class="eq" aria-hidden="true"><span></span><span></span><span></span><span></span></span>' +
'</span>' +
'</div>' +
'<div class="t-cover" aria-hidden="true"></div>' +
'<div class="t-main"><span class="t-title">' + t.title + '</span></div>' +
'<span class="t-plays">' + t.plays + '</span>' +
'<button class="t-like" type="button" aria-pressed="' + (t.liked ? "true" : "false") +
'" aria-label="Like ' + t.title + '">' +
'<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">' +
(t.liked
? '<path fill="currentColor" d="M12 21s-7.5-4.9-10-9.3C.4 8.7 2 5 5.4 5c2 0 3.3 1.1 4.1 2.2C10.3 6.1 11.6 5 13.6 5 17 5 18.6 8.7 17 11.7 14.5 16.1 12 21 12 21z"/>'
: '<path fill="none" stroke="currentColor" stroke-width="1.7" d="M12 20s-6.8-4.4-9.1-8.5C1.3 8.4 2.7 6 5.4 6c1.9 0 3 1 3.9 2.1L12 9.6l.7-1.5C13.6 7 14.7 6 16.6 6c2.7 0 4.1 2.4 2.5 5.5C16.8 15.6 12 20 12 20z"/>') +
'</svg>' +
'</button>' +
'<span class="t-dur">' + fmt(t.dur) + '</span>';
trackList.appendChild(li);
});
// ---- Render discography ----
var discoRow = $("#discoRow");
ALBUMS.forEach(function (a) {
var d = document.createElement("article");
d.className = "album";
d.tabIndex = 0;
d.style.setProperty("--c1", a.c1);
d.style.setProperty("--c2", a.c2);
d.innerHTML =
'<div class="album-art"><span class="a-disc"></span>' +
'<button class="a-mini" type="button" aria-label="Play ' + a.title + '">' +
'<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M8 5v14l11-7z"/></svg>' +
'</button>' +
'</div>' +
'<div class="a-title">' + a.title + '</div>' +
'<div class="a-sub">' + a.year + '</div>';
d.querySelector(".a-mini").addEventListener("click", function (e) {
e.stopPropagation();
toast("Playing " + a.title);
});
d.addEventListener("click", function () { toast("Opening " + a.title); });
discoRow.appendChild(d);
});
// ---- Render fans also like ----
var fansRow = $("#fansRow");
FANS.forEach(function (f) {
var el = document.createElement("article");
el.className = "fan";
el.tabIndex = 0;
el.style.setProperty("--c1", f.c1);
el.style.setProperty("--c2", f.c2);
el.innerHTML =
'<div class="fan-art" aria-hidden="true"></div>' +
'<div class="a-title">' + f.name + '</div>' +
'<div class="a-sub">' + f.sub + '</div>';
el.addEventListener("click", function () { toast("Opening " + f.name); });
fansRow.appendChild(el);
});
// ---- Playback engine ----
var heroPlay = $("#heroPlay");
var nowbar = $("#nowbar");
var npCover = $("#npCover");
var npTitle = $("#npTitle");
var npToggle = $("#npToggle");
var npCur = $("#npCur");
var npDur = $("#npDur");
var scrub = $("#scrub");
var scrubFill = $("#scrubFill");
var scrubKnob = $("#scrubKnob");
var state = { id: null, playing: false, pos: 0, dur: 0, timer: null };
function trackById(id) {
for (var i = 0; i < TRACKS.length; i++) if (TRACKS[i].id === id) return TRACKS[i];
return null;
}
function tick() {
if (!state.playing) return;
state.pos += 0.5;
if (state.pos >= state.dur) { nextTrack(); return; }
renderProgress();
}
function startTimer() {
stopTimer();
state.timer = setInterval(tick, 500);
}
function stopTimer() {
if (state.timer) { clearInterval(state.timer); state.timer = null; }
}
function renderProgress() {
var pct = state.dur ? (state.pos / state.dur) * 100 : 0;
scrubFill.style.width = pct + "%";
scrubKnob.style.left = pct + "%";
scrub.setAttribute("aria-valuenow", Math.round(pct));
npCur.textContent = fmt(state.pos);
}
function syncUI() {
$$(".track").forEach(function (li) {
var on = li.dataset.id === state.id && state.playing;
li.classList.toggle("playing", li.dataset.id === state.id);
var btn = li.querySelector(".t-mini-play");
if (btn) {
btn.setAttribute("aria-label",
(li.dataset.id === state.id && state.playing ? "Pause " : "Play ") +
li.querySelector(".t-title").textContent);
}
// toggle play/pause icon on currently selected, non-playing track handled by .playing class via eq
void on;
});
heroPlay.setAttribute("aria-pressed", state.playing ? "true" : "false");
npToggle.setAttribute("aria-pressed", state.playing ? "true" : "false");
npToggle.setAttribute("aria-label", state.playing ? "Pause" : "Play");
}
function loadTrack(id, autoplay) {
var t = trackById(id);
if (!t) return;
state.id = id;
state.dur = t.dur;
state.pos = 0;
npTitle.textContent = t.title;
npCover.style.setProperty("--c1", t.c1);
npCover.style.setProperty("--c2", t.c2);
npDur.textContent = fmt(t.dur);
nowbar.hidden = false;
document.body.style.setProperty("--c1", t.c1);
renderProgress();
if (autoplay) play(); else { state.playing = false; stopTimer(); syncUI(); }
}
function play() {
if (!state.id) { loadTrack(TRACKS[0].id, false); }
state.playing = true;
startTimer();
syncUI();
}
function pause() {
state.playing = false;
stopTimer();
syncUI();
}
function toggle() { state.playing ? pause() : play(); }
function nextTrack() {
var idx = 0;
for (var i = 0; i < TRACKS.length; i++) if (TRACKS[i].id === state.id) idx = i;
var next = TRACKS[(idx + 1) % TRACKS.length];
loadTrack(next.id, true);
}
// hero play
heroPlay.addEventListener("click", function () {
if (!state.id) { loadTrack(TRACKS[0].id, true); }
else { toggle(); }
});
// shuffle
$("#shuffleBtn").addEventListener("click", function () {
var r = TRACKS[Math.floor(Math.random() * TRACKS.length)];
loadTrack(r.id, true);
toast("Shuffling Neon Tides");
});
$("#moreBtn").addEventListener("click", function () { toast("More options"); });
// now bar toggle
npToggle.addEventListener("click", toggle);
// track row clicks (delegated)
trackList.addEventListener("click", function (e) {
var likeBtn = e.target.closest(".t-like");
if (likeBtn) {
var pressed = likeBtn.getAttribute("aria-pressed") === "true";
likeBtn.setAttribute("aria-pressed", pressed ? "false" : "true");
likeBtn.querySelector("svg").innerHTML = pressed
? '<path fill="none" stroke="currentColor" stroke-width="1.7" d="M12 20s-6.8-4.4-9.1-8.5C1.3 8.4 2.7 6 5.4 6c1.9 0 3 1 3.9 2.1L12 9.6l.7-1.5C13.6 7 14.7 6 16.6 6c2.7 0 4.1 2.4 2.5 5.5C16.8 15.6 12 20 12 20z"/>'
: '<path fill="currentColor" d="M12 21s-7.5-4.9-10-9.3C.4 8.7 2 5 5.4 5c2 0 3.3 1.1 4.1 2.2C10.3 6.1 11.6 5 13.6 5 17 5 18.6 8.7 17 11.7 14.5 16.1 12 21 12 21z"/>';
toast(pressed ? "Removed from Liked Songs" : "Added to Liked Songs");
return;
}
var row = e.target.closest(".track");
if (!row) return;
var id = row.dataset.id;
if (id === state.id) { toggle(); }
else { loadTrack(id, true); }
});
// ---- Follow toggle ----
var followBtn = $("#followBtn");
followBtn.addEventListener("click", function () {
var following = followBtn.getAttribute("aria-pressed") === "true";
followBtn.setAttribute("aria-pressed", following ? "false" : "true");
followBtn.textContent = following ? "Follow" : "Following";
var el = $("#listeners");
var n = parseInt(el.textContent.replace(/,/g, ""), 10) + (following ? -1 : 1);
el.textContent = n.toLocaleString("en-US");
toast(following ? "Unfollowed Neon Tides" : "Following Neon Tides");
});
// ---- Show more ----
var showMore = $("#showMore");
showMore.addEventListener("click", function () {
var expanded = showMore.getAttribute("aria-expanded") === "true";
$$(".track.extra").forEach(function (el) { el.classList.toggle("hidden", expanded); });
showMore.setAttribute("aria-expanded", expanded ? "false" : "true");
showMore.textContent = expanded ? "Show more" : "Show less";
});
// ---- Scrubber ----
function seekFromEvent(clientX) {
var rect = scrub.getBoundingClientRect();
var pct = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
state.pos = pct * state.dur;
renderProgress();
}
var dragging = false;
scrub.addEventListener("pointerdown", function (e) {
if (!state.id) return;
dragging = true;
scrub.setPointerCapture(e.pointerId);
seekFromEvent(e.clientX);
});
scrub.addEventListener("pointermove", function (e) {
if (dragging) seekFromEvent(e.clientX);
});
scrub.addEventListener("pointerup", function () { dragging = false; });
scrub.addEventListener("keydown", function (e) {
if (!state.id) return;
var step = 5;
if (e.key === "ArrowRight") { state.pos = Math.min(state.dur, state.pos + step); renderProgress(); e.preventDefault(); }
else if (e.key === "ArrowLeft") { state.pos = Math.max(0, state.pos - step); renderProgress(); e.preventDefault(); }
else if (e.key === "Home") { state.pos = 0; renderProgress(); e.preventDefault(); }
else if (e.key === "End") { state.pos = state.dur; renderProgress(); e.preventDefault(); }
});
// keyboard activation for album/fan cards
document.addEventListener("keydown", function (e) {
if ((e.key === "Enter" || e.key === " ") && document.activeElement &&
(document.activeElement.classList.contains("album") || document.activeElement.classList.contains("fan"))) {
e.preventDefault();
document.activeElement.click();
}
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Neon Tides — Artist</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" id="page" style="--artist:#1db954; --artist-2:#0e7a3a;">
<!-- HERO -->
<header class="hero" aria-label="Artist header">
<div class="hero-art" aria-hidden="true">
<span class="blob b1"></span>
<span class="blob b2"></span>
<span class="blob b3"></span>
<span class="grain"></span>
</div>
<div class="hero-inner">
<div class="verified">
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><path fill="currentColor" d="M12 2l2.4 1.8 3 .1 1 2.8 2.4 1.7-.9 2.9.9 2.9-2.4 1.7-1 2.8-3 .1L12 22l-2.4-1.8-3-.1-1-2.8L3.2 15l.9-2.9L3.2 9.2l2.4-1.7 1-2.8 3-.1L12 2z"/><path fill="#0b0b0f" d="M10.6 14.6l-2.2-2.2 1.1-1.1 1.1 1.1 3.3-3.3 1.1 1.1-4.4 4.4z"/></svg>
<span>Verified Artist</span>
</div>
<h1 class="artist-name" id="artistName">Neon Tides</h1>
<p class="listeners"><strong id="listeners">4,182,940</strong> monthly listeners</p>
<div class="actions">
<button class="btn-play" id="heroPlay" type="button" aria-pressed="false" aria-label="Play Neon Tides">
<svg class="ic-play" viewBox="0 0 24 24" width="26" height="26" aria-hidden="true"><path fill="currentColor" d="M8 5v14l11-7z"/></svg>
<svg class="ic-pause" viewBox="0 0 24 24" width="26" height="26" aria-hidden="true"><path fill="currentColor" d="M6 5h4v14H6zM14 5h4v14h-4z"/></svg>
</button>
<button class="btn-follow" id="followBtn" type="button" aria-pressed="false">Follow</button>
<button class="btn-ghost icon" id="shuffleBtn" type="button" title="Shuffle" aria-label="Shuffle play">
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><path fill="currentColor" d="M17 3h4v4h-2V5.4l-3.3 3.3-1.4-1.4L17.6 4H17V3zm4 14v4h-4v-2h.6l-3.3-3.3 1.4-1.4L19 17.6V17h2zM3 5h3.2c1 0 1.9.5 2.5 1.3l8.6 11.4c.2.3.5.4.8.4H21v2h-3.9c-1 0-1.9-.5-2.5-1.3L6 7.4c-.2-.3-.5-.4-.8-.4H3V5zm0 12h3.2c.3 0 .6-.1.8-.4l2-2.6 1.3 1.7-1.7 2.2C8 20.5 7.1 21 6.2 21H3v-4z"/></svg>
</button>
<button class="btn-ghost icon" id="moreBtn" type="button" title="More" aria-label="More options">
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><circle cx="5" cy="12" r="2" fill="currentColor"/><circle cx="12" cy="12" r="2" fill="currentColor"/><circle cx="19" cy="12" r="2" fill="currentColor"/></svg>
</button>
</div>
</div>
</header>
<!-- POPULAR -->
<section class="section" aria-labelledby="popular-h">
<h2 class="sec-h" id="popular-h">Popular</h2>
<ol class="tracks" id="trackList"></ol>
<button class="show-more" id="showMore" type="button" aria-expanded="false">Show more</button>
</section>
<!-- DISCOGRAPHY -->
<section class="section" aria-labelledby="disco-h">
<div class="sec-head">
<h2 class="sec-h" id="disco-h">Discography</h2>
<span class="see-all">See all</span>
</div>
<div class="row" id="discoRow"></div>
</section>
<!-- ABOUT -->
<section class="section" aria-labelledby="about-h">
<h2 class="sec-h" id="about-h">About</h2>
<div class="about">
<div class="about-art" aria-hidden="true"><span class="blob b1"></span><span class="blob b2"></span></div>
<div class="about-body">
<p class="about-bio">
Neon Tides is the synth-pop project of fictional songwriter Mara Vel, trading in widescreen
choruses, glassy arpeggios, and late-night reverb. Since the breakout single
<em>Paper Lanterns</em>, the act has grown from bedroom demos into sold-out neon-soaked rooms,
scoring its sound somewhere between coastal nostalgia and city static.
</p>
<div class="about-stats">
<div class="stat"><span class="stat-n">4.1M</span><span class="stat-l">Monthly listeners</span></div>
<div class="stat"><span class="stat-n">#214</span><span class="stat-l">In the world</span></div>
<div class="stat"><span class="stat-n">912K</span><span class="stat-l">Followers</span></div>
</div>
</div>
</div>
</section>
<!-- FANS ALSO LIKE -->
<section class="section" aria-labelledby="fans-h">
<div class="sec-head">
<h2 class="sec-h" id="fans-h">Fans also like</h2>
<span class="see-all">See all</span>
</div>
<div class="row artists" id="fansRow"></div>
</section>
</main>
<!-- NOW PLAYING BAR -->
<div class="nowbar" id="nowbar" aria-live="polite" hidden>
<div class="np-cover" id="npCover" aria-hidden="true"></div>
<div class="np-meta">
<span class="np-title" id="npTitle">—</span>
<span class="np-artist">Neon Tides</span>
</div>
<button class="np-toggle" id="npToggle" type="button" aria-pressed="true" aria-label="Pause">
<svg class="ic-play" viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><path fill="currentColor" d="M8 5v14l11-7z"/></svg>
<svg class="ic-pause" viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><path fill="currentColor" d="M6 5h4v14H6zM14 5h4v14h-4z"/></svg>
</button>
<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="npDur">0:00</span>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Artist Page (hero · top tracks · about)
A full streaming-style artist profile for the fictional synth-pop act Neon Tides, rendered entirely in CSS — no images. The hero is a full-bleed banner of drifting gradient blobs and a halftone grain that fade into the page background, with the accent color pulled from the artist theme so the Play button, equalizer, and active states all feel coordinated. Above the giant display-font name sit a verified badge and a live monthly-listeners count, with a circular Play, a Follow toggle, and shuffle / more actions beneath.
The Popular section is a numbered top-tracks list: each row shows a CSS cover, title, formatted play count, like heart, and duration. Hovering a row swaps the rank number for a mini play button, and the currently playing track flips to a four-bar animated equalizer in the artist accent. Show more expands the list from five to eight tracks. Below, a horizontally scrollable Discography strip of album cards (with a hover reveal play button and a spinning-disc motif), an About card with bio and stat blocks, and a Fans also like artist row round out the page.
Interactions are all vanilla JS: clicking a track or the hero Play starts a simulated playback engine driven by a timer, advancing a fixed now-playing bar at the bottom with a keyboard-accessible role="slider" scrubber (drag, arrow keys, Home/End), auto-advancing to the next track at the end. Play/pause, follow (which nudges the listener count), per-track like hearts, shuffle, and show-more all toggle their aria state, and a small toast() helper confirms each action. Motion respects prefers-reduced-motion, and the layout reflows cleanly down to ~360px.
Illustrative UI only — fictional artists, albums, tracks, and data. No real audio playback.