Music — Tour / Live Dates (list + tickets)
A dark, music-styled tour and live-dates component for the fictional act Neon Tides, in plain HTML, CSS, and vanilla JS. A themed header pairs a CSS-drawn cover and animated map pin with a live show count and a Get notified toggle, above a region filter (All, North America, Europe, Asia). Twelve show rows carry a date block, city, venue, country flag, an On sale, Few left, Sold out, or Just announced badge, and a status-aware Tickets or Notify me button. Rows expand for doors, set time, support act, and pricing.
MCP
Codice
:root {
--bg: #0b0b0f;
--bg-2: #13131a;
--surface: #1a1a22;
--surface-2: #22222c;
--text: #f4f4f7;
--muted: #a0a0ad;
--line: rgba(255, 255, 255, 0.1);
--line-2: rgba(255, 255, 255, 0.18);
--accent: #1db954;
--accent-2: #8b5cf6;
--accent-3: #ff3d71;
--amber: #ffb547;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--r-full: 999px;
--shadow: 0 18px 50px -22px rgba(0, 0, 0, 0.8);
}
* {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
min-height: 100vh;
padding: clamp(16px, 4vw, 48px);
display: flex;
justify-content: center;
align-items: flex-start;
font-family: "Inter", system-ui, sans-serif;
line-height: 1.5;
color: var(--text);
background:
radial-gradient(1100px 620px at 78% -8%, rgba(139, 92, 246, 0.16), transparent 60%),
radial-gradient(900px 520px at 8% 12%, rgba(29, 185, 84, 0.12), transparent 55%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.tour {
width: 100%;
max-width: 760px;
}
/* ===== Header ===== */
.tour__head {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: clamp(14px, 3vw, 22px);
padding: clamp(16px, 3vw, 22px);
background: linear-gradient(180deg, var(--surface), var(--bg-2));
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow);
}
/* CSS-drawn album cover */
.cover {
position: relative;
width: clamp(72px, 16vw, 104px);
aspect-ratio: 1;
border-radius: var(--r-md);
overflow: hidden;
background:
radial-gradient(120% 120% at 20% 15%, #2b1a4d, transparent 60%),
linear-gradient(135deg, #120c24, #0c0c12 70%);
border: 1px solid var(--line-2);
flex: none;
}
.cover__shape {
position: absolute;
border-radius: var(--r-full);
filter: blur(2px);
}
.cover__shape--a {
width: 70%;
height: 70%;
left: -12%;
top: -10%;
background: radial-gradient(circle, var(--accent-2), transparent 68%);
opacity: 0.85;
}
.cover__shape--b {
width: 62%;
height: 62%;
right: -8%;
bottom: -10%;
background: radial-gradient(circle, var(--accent), transparent 70%);
opacity: 0.7;
}
.cover__disc {
position: absolute;
inset: 28% 28%;
border-radius: var(--r-full);
background:
repeating-radial-gradient(circle, rgba(0, 0, 0, 0.55) 0 2px, rgba(255, 255, 255, 0.05) 2px 4px);
box-shadow: inset 0 0 0 3px rgba(0, 0, 0, 0.6);
animation: spin 7s linear infinite;
}
.cover__disc::after {
content: "";
position: absolute;
inset: 42%;
border-radius: var(--r-full);
background: var(--accent-3);
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.head__eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
margin: 0 0 6px;
font-size: 0.74rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--accent);
}
.pin {
width: 12px;
height: 12px;
border-radius: var(--r-full) var(--r-full) var(--r-full) 1px;
background: var(--accent);
transform: rotate(45deg);
box-shadow: 0 0 0 3px rgba(29, 185, 84, 0.22);
animation: drop 2.4s ease-in-out infinite;
}
@keyframes drop {
0%, 100% { transform: rotate(45deg) translateY(0); }
50% { transform: rotate(45deg) translateY(-3px); }
}
.head__name {
margin: 0;
font-family: "Space Grotesk", "Inter", sans-serif;
font-weight: 700;
font-size: clamp(1.6rem, 6vw, 2.6rem);
line-height: 1.05;
letter-spacing: -0.02em;
}
.head__sub {
margin: 6px 0 0;
color: var(--muted);
font-size: 0.92rem;
}
.head__sub strong,
.head__sub em {
color: var(--text);
}
.count {
font-variant-numeric: tabular-nums;
font-weight: 800;
color: var(--accent-2);
}
/* Follow button */
.follow {
display: inline-flex;
align-items: center;
gap: 9px;
padding: 11px 18px;
font: inherit;
font-weight: 700;
color: var(--text);
background: var(--surface-2);
border: 1px solid var(--line-2);
border-radius: var(--r-full);
cursor: pointer;
white-space: nowrap;
transition: transform 0.12s ease, background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}
.follow:hover {
transform: translateY(-1px);
border-color: var(--accent-2);
}
.follow:active {
transform: translateY(0);
}
.follow:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
}
.follow__bell {
width: 14px;
height: 14px;
border-radius: 50% 50% 4px 4px;
border: 2px solid currentColor;
border-bottom: none;
position: relative;
}
.follow__bell::after {
content: "";
position: absolute;
left: -3px;
right: -3px;
bottom: -3px;
height: 2px;
border-radius: 2px;
background: currentColor;
}
.follow[aria-pressed="true"] {
background: linear-gradient(135deg, var(--accent-2), #6d28d9);
border-color: transparent;
box-shadow: 0 10px 26px -12px rgba(139, 92, 246, 0.8);
}
/* ===== Marquee ===== */
.marquee {
overflow: hidden;
margin: 18px 0 14px;
padding: 9px 0;
border-top: 1px solid var(--line);
border-bottom: 1px solid var(--line);
-webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.marquee span {
display: inline-block;
white-space: nowrap;
font-family: "Space Grotesk", sans-serif;
font-weight: 600;
font-size: 0.82rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--muted);
animation: marquee 22s linear infinite;
}
@keyframes marquee {
to {
transform: translateX(-50%);
}
}
/* ===== Filters ===== */
.filters {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
}
.chip {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 8px 14px;
font: inherit;
font-size: 0.86rem;
font-weight: 600;
color: var(--muted);
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-full);
cursor: pointer;
transition: color 0.18s ease, background 0.18s ease, border-color 0.18s ease;
}
.chip:hover {
color: var(--text);
border-color: var(--line-2);
}
.chip:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.chip.is-active {
color: #06210f;
background: var(--accent);
border-color: transparent;
}
.chip__n {
font-size: 0.74rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
padding: 1px 7px;
border-radius: var(--r-full);
background: rgba(255, 255, 255, 0.14);
color: inherit;
}
.chip:not(.is-active) .chip__n {
background: var(--surface-2);
color: var(--text);
}
/* ===== Shows ===== */
.shows {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 12px;
}
.show {
background: linear-gradient(180deg, var(--surface), var(--bg-2));
border: 1px solid var(--line);
border-radius: var(--r-md);
overflow: hidden;
transition: border-color 0.2s ease, transform 0.12s ease, box-shadow 0.2s ease;
}
.show:hover {
border-color: var(--line-2);
transform: translateY(-1px);
box-shadow: 0 14px 34px -22px rgba(0, 0, 0, 0.9);
}
.show.is-soldout {
opacity: 0.74;
}
.show__main {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: clamp(12px, 3vw, 18px);
padding: 14px clamp(14px, 3vw, 18px);
}
/* Date block */
.date {
display: grid;
place-items: center;
width: 60px;
padding: 9px 0;
border-radius: var(--r-sm);
background: var(--bg);
border: 1px solid var(--line-2);
text-align: center;
flex: none;
}
.date__mo {
font-size: 0.66rem;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--accent-3);
}
.date__day {
font-family: "Space Grotesk", sans-serif;
font-weight: 700;
font-size: 1.5rem;
line-height: 1;
}
.date__yr {
font-size: 0.62rem;
font-weight: 600;
color: var(--muted);
}
/* Venue info */
.info {
min-width: 0;
}
.info__city {
margin: 0;
font-weight: 700;
font-size: 1.02rem;
display: flex;
align-items: center;
gap: 8px;
}
.flag {
font-size: 1rem;
line-height: 1;
}
.info__venue {
margin: 3px 0 0;
color: var(--muted);
font-size: 0.88rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.region-tag {
display: inline-block;
margin-top: 7px;
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted);
padding: 3px 9px;
border: 1px solid var(--line);
border-radius: var(--r-full);
}
/* Right column: status + cta */
.right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 9px;
flex: none;
}
.status {
font-size: 0.68rem;
font-weight: 800;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 4px 10px;
border-radius: var(--r-full);
white-space: nowrap;
}
.status--onsale {
color: var(--accent);
background: rgba(29, 185, 84, 0.14);
}
.status--few {
color: var(--amber);
background: rgba(255, 181, 71, 0.15);
}
.status--soldout {
color: var(--muted);
background: rgba(255, 255, 255, 0.06);
}
.status--announced {
color: var(--accent-2);
background: rgba(139, 92, 246, 0.16);
}
.cta {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 9px 16px;
font: inherit;
font-weight: 700;
font-size: 0.85rem;
border-radius: var(--r-full);
border: 1px solid transparent;
cursor: pointer;
transition: transform 0.12s ease, filter 0.2s ease, background 0.2s ease;
}
.cta:hover {
transform: translateY(-1px);
}
.cta:active {
transform: translateY(0);
}
.cta:focus-visible {
outline: 2px solid var(--text);
outline-offset: 2px;
}
.cta--tickets {
color: #06210f;
background: var(--accent);
}
.cta--tickets:hover {
filter: brightness(1.08);
}
.cta--notify {
color: var(--text);
background: var(--surface-2);
border-color: var(--line-2);
}
.cta--notify:hover {
border-color: var(--accent-2);
}
.cta[disabled] {
cursor: not-allowed;
color: var(--muted);
background: var(--surface);
border-color: var(--line);
transform: none;
}
.cta[disabled]:hover {
filter: none;
}
/* Expand toggle */
.expand {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
padding: 9px clamp(14px, 3vw, 18px);
font: inherit;
font-size: 0.78rem;
font-weight: 600;
color: var(--muted);
background: transparent;
border: none;
border-top: 1px dashed var(--line);
cursor: pointer;
transition: color 0.18s ease;
}
.expand:hover {
color: var(--text);
}
.expand:focus-visible {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
.expand__chev {
width: 7px;
height: 7px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
transition: transform 0.2s ease;
margin-left: 2px;
}
.show.is-open .expand__chev {
transform: rotate(-135deg);
}
.details {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.28s ease;
}
.show.is-open .details {
grid-template-rows: 1fr;
}
.details__inner {
overflow: hidden;
}
.details__grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
padding: 4px clamp(14px, 3vw, 18px) 16px;
}
.detail {
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 10px 12px;
}
.detail__k {
margin: 0 0 3px;
font-size: 0.66rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
.detail__v {
margin: 0;
font-size: 0.9rem;
font-weight: 600;
}
/* Empty state */
.empty {
margin: 24px 0;
padding: 28px;
text-align: center;
color: var(--muted);
background: var(--surface);
border: 1px dashed var(--line-2);
border-radius: var(--r-md);
}
.empty em {
color: var(--accent);
font-style: normal;
font-weight: 700;
}
/* Toast */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 24px);
padding: 12px 18px;
max-width: min(90vw, 420px);
font-weight: 600;
font-size: 0.9rem;
color: var(--text);
background: var(--surface-2);
border: 1px solid var(--line-2);
border-radius: var(--r-full);
box-shadow: var(--shadow);
opacity: 0;
pointer-events: none;
transition: transform 0.3s cubic-bezier(0.2, 0.9, 0.3, 1), opacity 0.3s ease;
z-index: 50;
}
.toast.is-show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ===== Responsive ===== */
@media (max-width: 520px) {
.tour__head {
grid-template-columns: auto 1fr;
}
.follow {
grid-column: 1 / -1;
justify-content: center;
}
.show__main {
grid-template-columns: auto 1fr;
row-gap: 12px;
}
.right {
grid-column: 1 / -1;
flex-direction: row;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
}
.info__venue {
white-space: normal;
}
}
@media (prefers-reduced-motion: reduce) {
.cover__disc,
.pin,
.marquee span {
animation: none;
}
.toast,
.details {
transition: none;
}
}(() => {
"use strict";
// ---- Data (fictional) -------------------------------------------------
const SHOWS = [
{
id: "phx",
month: "JUN", day: "14", year: "2026",
city: "Phoenix, AZ", venue: "Desert Mirage Amphitheatre",
region: "north-america", flag: "🇺🇸", regionLabel: "North America",
status: "onsale",
doors: "6:30 PM", start: "8:00 PM", support: "Velvet Static",
price: "from $48",
},
{
id: "la",
month: "JUN", day: "21", year: "2026",
city: "Los Angeles, CA", venue: "The Glass Atrium",
region: "north-america", flag: "🇺🇸", regionLabel: "North America",
status: "few",
doors: "7:00 PM", start: "8:30 PM", support: "Paper Lanterns",
price: "from $62",
},
{
id: "tor",
month: "JUL", day: "02", year: "2026",
city: "Toronto, ON", venue: "Harbourline Hall",
region: "north-america", flag: "🇨🇦", regionLabel: "North America",
status: "soldout",
doors: "6:45 PM", start: "8:15 PM", support: "Slow Marquee",
price: "Sold out",
},
{
id: "nyc",
month: "JUL", day: "09", year: "2026",
city: "New York, NY", venue: "Reservoir Stage",
region: "north-america", flag: "🇺🇸", regionLabel: "North America",
status: "onsale",
doors: "7:00 PM", start: "8:45 PM", support: "Velvet Static",
price: "from $58",
},
{
id: "ldn",
month: "JUL", day: "24", year: "2026",
city: "London, UK", venue: "Camden Tide Arena",
region: "europe", flag: "🇬🇧", regionLabel: "Europe",
status: "few",
doors: "6:30 PM", start: "8:00 PM", support: "Paper Lanterns",
price: "from £45",
},
{
id: "par",
month: "JUL", day: "29", year: "2026",
city: "Paris, FR", venue: "Salle Néon",
region: "europe", flag: "🇫🇷", regionLabel: "Europe",
status: "onsale",
doors: "7:00 PM", start: "8:30 PM", support: "Cobalt Hour",
price: "from €52",
},
{
id: "ber",
month: "AUG", day: "05", year: "2026",
city: "Berlin, DE", venue: "Stahlwerk Halle",
region: "europe", flag: "🇩🇪", regionLabel: "Europe",
status: "announced",
doors: "8:00 PM", start: "9:30 PM", support: "TBA",
price: "Presale Fri",
},
{
id: "ams",
month: "AUG", day: "09", year: "2026",
city: "Amsterdam, NL", venue: "Kanaal Pavilion",
region: "europe", flag: "🇳🇱", regionLabel: "Europe",
status: "soldout",
doors: "7:30 PM", start: "9:00 PM", support: "Cobalt Hour",
price: "Sold out",
},
{
id: "tyo",
month: "SEP", day: "03", year: "2026",
city: "Tokyo, JP", venue: "Akari Dome",
region: "asia", flag: "🇯🇵", regionLabel: "Asia",
status: "onsale",
doors: "5:30 PM", start: "7:00 PM", support: "Mono Garden",
price: "from ¥7,800",
},
{
id: "sel",
month: "SEP", day: "08", year: "2026",
city: "Seoul, KR", venue: "Hangang Skybox",
region: "asia", flag: "🇰🇷", regionLabel: "Asia",
status: "few",
doors: "6:00 PM", start: "7:30 PM", support: "Mono Garden",
price: "from ₩72,000",
},
{
id: "sgp",
month: "SEP", day: "13", year: "2026",
city: "Singapore, SG", venue: "Marina Static Hall",
region: "asia", flag: "🇸🇬", regionLabel: "Asia",
status: "announced",
doors: "7:00 PM", start: "8:30 PM", support: "TBA",
price: "Presale soon",
},
{
id: "syd",
month: "SEP", day: "20", year: "2026",
city: "Sydney, AU", venue: "Harbour Neon Theatre",
region: "asia", flag: "🇦🇺", regionLabel: "Asia-Pacific",
status: "onsale",
doors: "6:30 PM", start: "8:00 PM", support: "Slow Marquee",
price: "from A$74",
},
];
const STATUS = {
onsale: { cls: "onsale", label: "On sale", cta: "Tickets", ctaCls: "tickets", disabled: false },
few: { cls: "few", label: "Few left", cta: "Tickets", ctaCls: "tickets", disabled: false },
soldout: { cls: "soldout", label: "Sold out", cta: "Sold out", ctaCls: "notify", disabled: true },
announced: { cls: "announced", label: "Just announced", cta: "Notify me", ctaCls: "notify", disabled: false },
};
// ---- Helpers ----------------------------------------------------------
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => [...root.querySelectorAll(sel)];
let toastTimer;
function toast(msg) {
const el = $("#toast");
el.textContent = msg;
el.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(() => el.classList.remove("is-show"), 2600);
}
const esc = (s) =>
String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
let following = false;
let currentRegion = "all";
// ---- Render -----------------------------------------------------------
const list = $("#show-list");
function rowHTML(s) {
const st = STATUS[s.status];
const ctaInner = st.disabled
? `<span aria-hidden="true">✓</span> ${st.cta}`
: (st.ctaCls === "tickets"
? `<span aria-hidden="true">🎟</span> ${st.cta}`
: `${st.cta}`);
return `
<li class="show ${st.disabled ? "is-soldout" : ""}" data-region="${s.region}" data-id="${s.id}">
<div class="show__main">
<time class="date" datetime="${s.year}-${s.month}-${s.day}">
<span class="date__mo">${s.month}</span>
<span class="date__day">${s.day}</span>
<span class="date__yr">${s.year}</span>
</time>
<div class="info">
<p class="info__city"><span class="flag" aria-hidden="true">${s.flag}</span>${esc(s.city)}</p>
<p class="info__venue">${esc(s.venue)}</p>
<span class="region-tag">${esc(s.regionLabel)}</span>
</div>
<div class="right">
<span class="status status--${st.cls}">${st.label}</span>
<button
class="cta cta--${st.ctaCls}"
type="button"
data-action="cta"
data-status="${s.status}"
${st.disabled ? "disabled aria-disabled=\"true\"" : ""}
>${ctaInner}</button>
</div>
</div>
<button class="expand" type="button" data-action="expand" aria-expanded="false" aria-controls="det-${s.id}">
Showtime & support
<span class="expand__chev" aria-hidden="true"></span>
</button>
<div class="details" id="det-${s.id}">
<div class="details__inner">
<div class="details__grid">
<div class="detail"><p class="detail__k">Doors</p><p class="detail__v">${esc(s.doors)}</p></div>
<div class="detail"><p class="detail__k">Set time</p><p class="detail__v">${esc(s.start)}</p></div>
<div class="detail"><p class="detail__k">Support</p><p class="detail__v">${esc(s.support)}</p></div>
<div class="detail"><p class="detail__k">Tickets</p><p class="detail__v">${esc(s.price)}</p></div>
</div>
</div>
</div>
</li>`;
}
function render() {
const visible = SHOWS.filter((s) => currentRegion === "all" || s.region === currentRegion);
list.innerHTML = visible.map(rowHTML).join("");
$("#empty").hidden = visible.length > 0;
$("#show-count").textContent = visible.length;
}
// ---- Filter counts ----------------------------------------------------
function paintCounts() {
$$("[data-count-for]").forEach((el) => {
const r = el.getAttribute("data-count-for");
el.textContent = r === "all"
? SHOWS.length
: SHOWS.filter((s) => s.region === r).length;
});
}
// ---- Events -----------------------------------------------------------
// Region filter
$(".filters").addEventListener("click", (e) => {
const chip = e.target.closest(".chip");
if (!chip) return;
currentRegion = chip.dataset.region;
$$(".chip").forEach((c) => {
const on = c === chip;
c.classList.toggle("is-active", on);
c.setAttribute("aria-selected", String(on));
});
render();
const name = chip.textContent.trim().replace(/\s+\d+$/, "");
toast(`Showing ${$("#show-count").textContent} ${name === "All" ? "shows" : name + " shows"}`);
});
// List delegation: cta + expand
list.addEventListener("click", (e) => {
const cta = e.target.closest('[data-action="cta"]');
if (cta) {
if (cta.disabled) return;
const row = cta.closest(".show");
const city = $(".info__city", row).textContent.trim();
if (cta.dataset.status === "announced") {
toast(`We'll alert you when ${city} tickets drop ⏰`);
} else {
toast(`Opening tickets for ${city} 🎟`);
}
return;
}
const exp = e.target.closest('[data-action="expand"]');
if (exp) {
const row = exp.closest(".show");
const open = row.classList.toggle("is-open");
exp.setAttribute("aria-expanded", String(open));
return;
}
});
// Follow / notify toggle
const followBtn = $("#follow-btn");
followBtn.addEventListener("click", () => {
following = !following;
followBtn.setAttribute("aria-pressed", String(following));
$(".follow__label", followBtn).textContent = following ? "Notifying" : "Get notified";
toast(following
? "You're on the list — we'll ping you about new Neon Tides dates 🔔"
: "Notifications off.");
});
// ---- Init -------------------------------------------------------------
paintCounts();
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Neon Tides — Tour Dates</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="tour" aria-labelledby="tour-title">
<!-- ===== Header ===== -->
<header class="tour__head">
<div class="cover" aria-hidden="true">
<span class="cover__shape cover__shape--a"></span>
<span class="cover__shape cover__shape--b"></span>
<span class="cover__disc"></span>
</div>
<div class="head__meta">
<p class="head__eyebrow">
<span class="pin" aria-hidden="true"></span>
On Tour · 2026 World Run
</p>
<h1 id="tour-title" class="head__name">Neon Tides</h1>
<p class="head__sub">
<span id="show-count" class="count">12</span> upcoming shows
across <strong>9 countries</strong> · supporting the album
<em>Midnight Reservoir</em>
</p>
</div>
<button id="follow-btn" class="follow" type="button" aria-pressed="false">
<span class="follow__bell" aria-hidden="true"></span>
<span class="follow__label">Get notified</span>
</button>
</header>
<!-- ===== Filter marquee ===== -->
<div class="marquee" aria-hidden="true">
<span>★ World Tour 2026 ★ Midnight Reservoir ★ Neon Tides Live ★ World Tour 2026 ★ Midnight Reservoir ★ Neon Tides Live</span>
</div>
<!-- ===== Region filter ===== -->
<div class="filters" role="tablist" aria-label="Filter shows by region">
<button class="chip is-active" role="tab" aria-selected="true" data-region="all">All <span class="chip__n" data-count-for="all"></span></button>
<button class="chip" role="tab" aria-selected="false" data-region="north-america">North America <span class="chip__n" data-count-for="north-america"></span></button>
<button class="chip" role="tab" aria-selected="false" data-region="europe">Europe <span class="chip__n" data-count-for="europe"></span></button>
<button class="chip" role="tab" aria-selected="false" data-region="asia">Asia <span class="chip__n" data-count-for="asia"></span></button>
</div>
<!-- ===== Shows list (rendered by JS) ===== -->
<ul id="show-list" class="shows" aria-live="polite"></ul>
<p id="empty" class="empty" hidden>No shows in this region yet — hit <em>Get notified</em> and we'll let you know.</p>
</main>
<!-- Toast -->
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Tour / Live Dates (list + tickets)
A self-contained tour schedule for the fictional synth-pop act Neon Tides, rendered entirely in CSS — no images. The themed header sets a CSS-drawn spinning-disc cover next to an animated map pin, the artist’s display-font name, a live count of upcoming shows, and a circular Get notified follow button. A scrolling marquee and a row of region chips (All / North America / Europe / Asia) each show how many dates sit in that region, and selecting one filters the list instantly.
Twelve realistic shows are rendered from data: every row pairs a month-and-day date block with city, venue, a country flag, and a region tag, plus a status badge — On sale, Few left, Sold out, or Just announced — that drives the call to action. Live and few-left shows get a green Tickets button, just-announced shows get Notify me, and sold-out shows are visibly dimmed with a disabled, checked button so they can’t be clicked.
Interactions are all vanilla JS. The region chips re-render the list and update the upcoming-show count, each row expands with a smooth grid-rows animation to reveal doors time, set time, support act, and pricing, the header follow button toggles its aria-pressed state and label, and a small toast() helper confirms filtering, ticket clicks, notify sign-ups, and follows. Buttons are keyboard-usable, the filter is a labelled tablist, and the layout reflows cleanly down to ~360px while respecting prefers-reduced-motion.
Illustrative UI only — fictional artists, albums, tracks, and data. No real audio playback.