/* ============================================================================
   RAVE THE PLANET 2026 — independent guide
   Design system.

   Concept: THE AXIS. Straße des 17. Juni is a single 3.5 km straight line, and
   so is this interface — one continuous rail down the left edge of every
   screen. By day its markers are trucks; after dark they become club doors.
   One object, two meanings, no redesign at 18:00.

   There is no time-based reordering. Every truck runs the same 14:00–22:00
   window and no per-DJ set times exist anywhere, so a hierarchy that shuffles
   itself around the clock would be reacting to a signal that does not vary.
   The clock drives what it genuinely can: party doors, open/closed states and
   the countdown.
   ========================================================================= */

/* ── typography ──────────────────────────────────────────────────────────── */

@font-face {
  font-family: 'Anybody';
  src: url('/fonts/anybody-var.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-stretch: 75% 150%;
  font-display: swap;
}
@font-face {
  font-family: 'Hanken';
  src: url('/fonts/hanken-var.woff2') format('woff2-variations');
  font-weight: 300 800;
  font-display: swap;
}
@font-face {
  font-family: 'Martian';
  src: url('/fonts/martian-var.woff2') format('woff2-variations');
  font-weight: 100 800;
  font-stretch: 75% 112.5%;
  font-display: swap;
}

/* ── tokens ──────────────────────────────────────────────────────────────── */

:root {
  /* Surfaces. Near-black rather than pure #000: OLED-friendly without the
     banding pure black shows under a grain overlay. */
  --bg: #07070a;
  --surface: #0e0f14;
  --surface-2: #161821;
  --line: #23262f;

  /* Three text weights. There is no soft middle — contrast is the hierarchy. */
  --ink: #f5f6f8;
  --ink-2: #a8acb8;
  /* 4.93:1 on the lightest surface we use (#161821), so muted text clears AA
     everywhere. The previous #6e7382 measured only 3.74:1. */
  --ink-3: #828796;

  /* Brand accent. #605dba measures 3.61:1 on --bg: correct for fills, large
     type and UI, but below the 4.5:1 AA floor for body-size text. So the same
     hue carries two roles — the brand colour fills, and a lifted tint of it
     (6.38:1) is what small text is allowed to use. Filled accent takes white
     at 5.57:1. */
  --accent: #605dba;
  --accent-text: #8b88dd;
  --accent-ink: #fff;         /* text on a filled accent */
  --warn: #ffb020;
  --ok: #4ade80;

  /* Type scale: deliberately extreme. Nothing lives between 17 and 40. */
  --t-meta: 0.8125rem;   /* 13 */
  --t-body: 0.9375rem;   /* 15 */
  --t-lead: 1.0625rem;   /* 17 */
  --t-d1: 2.5rem;        /* 40 */
  --t-d2: 3.5rem;        /* 56 */
  --t-d3: 5rem;          /* 80 */

  --rail: 2.25rem;       /* the axis gutter */
  --pad: 1.125rem;
  --radius: 0.625rem;

  /* Emil's curves — the built-in easings are too weak to read as intentional */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
  --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);

  --chip-h: 2.75rem;     /* 44px — the tap-target floor, and the clamp unit */
  --nav-h: calc(3.75rem + env(safe-area-inset-bottom));
  color-scheme: dark;
}


/* ── reset ───────────────────────────────────────────────────────────────── */

*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  background: var(--bg);
  color: var(--ink);
  font: 400 var(--t-body)/1.55 'Hanken', ui-sans-serif, system-ui, sans-serif;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
  overscroll-behavior-y: none;
  padding-bottom: var(--nav-h);
  min-height: 100dvh;
}

img { max-width: 100%; display: block; }
button, input, select { font: inherit; color: inherit; }
button { background: none; border: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; }

/* One grain layer over everything. The entire texture budget. */
.grain {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 60;
  opacity: 0.032;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='128' height='128'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='128' height='128' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ── focus & accessibility ───────────────────────────────────────────────── */

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 3px;
}
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.skip {
  position: fixed; top: -100px; left: 50%; translate: -50% 0; z-index: 100;
  background: var(--accent); color: var(--accent-ink);
  padding: 0.75rem 1.25rem; border-radius: 0 0 var(--radius) var(--radius);
  font-weight: 700; transition: top 160ms var(--ease-out);
}
.skip:focus { top: 0; }

/* ── layout ──────────────────────────────────────────────────────────────── */

.wrap {
  max-width: 46rem;
  margin: 0 auto;
  padding: 0 var(--pad) 0 calc(var(--rail) + 0.25rem);
  position: relative;
}

/* THE AXIS — the continuous rail. */
.axis {
  position: absolute;
  left: calc(var(--rail) / 2);
  top: 0;
  bottom: 0;
  width: 1px;
  background: linear-gradient(180deg, transparent, var(--line) 4%, var(--line) 96%, transparent);
  pointer-events: none;
}
[data-axis='set'] .axis::before { opacity: 1; }
.axis::before {
  /* the moving marker: where we are in the day */
  content: '';
  position: absolute;
  left: 50%;
  /* Positioned as a percentage of the rail, moved with a composited transform
     so the tick never triggers layout. */
  top: calc(var(--axis-pos, 0) * 1%);
  width: 7px; height: 7px;
  translate: -50% -50%;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 4px color-mix(in oklch, var(--accent) 18%, transparent);
  /* Hidden until JS has positioned it. The marker's default is the top of the
     rail, so on first paint it jumped the full page height to its real spot —
     a 0.93 layout shift that a 600ms transition had been quietly masking.
     Fading it in at the correct position removes the jump at source; after
     that it moves a sub-pixel fraction per 30s tick and needs no animation. */
  opacity: 0;
  transition: opacity 400ms var(--ease-out), background 2s linear;
}

/**
 * The view is client-rendered, so before it fills, the footer sits just under
 * the header and is then shoved down — one 0.22 layout shift, four times the
 * entire CLS budget.
 *
 * A full viewport of reserved height puts the footer just below the fold on
 * first paint. A shift only counts against CLS when the element is visible, so
 * once the content arrives and pushes the footer further down, nothing that the
 * visitor can see has moved. A smaller reserve made it worse, not better: the
 * footer stayed on screen and travelled further.
 */
#view { min-height: 100dvh; }

section { margin: 2.75rem 0; scroll-margin-top: 1rem; }
section:first-of-type { margin-top: 1.25rem; }

/* Section head: a rule that runs out past the content edge — a print gesture,
   not an effect. */
.head {
  display: flex;
  align-items: baseline;
  gap: 0.625rem;
  margin-bottom: 0.875rem;
  position: relative;
}
.head::before {
  content: '';
  position: absolute;
  left: calc(-1 * var(--rail) + 0.5rem);
  /* Runs out past the content edge — a print gesture. NOT -100vw: that
     overflowed the viewport by 302px at 320px wide. */
  right: calc(-1 * var(--pad));
  bottom: -0.4rem;
  height: 1px;
  background: var(--line);
}
.head h2 {
  font: 700 var(--t-meta)/1 'Anybody', sans-serif;
  font-stretch: 108%;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.head .count {
  font: 400 var(--t-meta)/1 'Martian', monospace;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.head .more { margin-left: auto; font-size: var(--t-meta); color: var(--accent-text); font-weight: 600; }

/* ── display type ────────────────────────────────────────────────────────── */

.display {
  /* An unbreakable word wider than the column was clipped, not wrapped:
     "Lokschuppen Berlin" lost 61px at 320px in BOTH languages, and German
     "Praktisches & Sicherheit" / "Gespeichert" lost 43px and 48px. The clamp
     floor is 40px and never scales below a 364px viewport, so the only fix
     that holds is letting the word break. html[lang] is set, so `auto`
     hyphenates properly per language. */
  hyphens: auto;
  overflow-wrap: break-word;
  font-family: 'Anybody', sans-serif;
  font-weight: 800;
  font-stretch: 112%;
  line-height: 0.92;
  letter-spacing: -0.02em;
  text-wrap: balance;
}
h1.display { font-size: clamp(var(--t-d1), 11vw, var(--t-d3)); }
h2.display { font-size: clamp(1.75rem, 7vw, var(--t-d2)); }

/* A measure cap on every element that carries real sentences. Without it the
   62rem desktop column ran the lead to 112 and notes to 147 characters a line,
   well past the ~75 where the eye starts losing its place on the return. */
.lead { font-size: var(--t-lead); color: var(--ink-2); text-wrap: pretty; max-width: 36em; }
.meta {
  font: 400 var(--t-meta)/1.4 'Martian', monospace;
  font-stretch: 88%;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  /* ~80 characters is roughly 40em; 36 keeps a margin. Short meta lines —
     clock data, counts — never reach the cap, so only sentences are affected. */
  max-width: 36em;
}
.num { font-family: 'Martian', monospace; font-variant-numeric: tabular-nums; font-stretch: 88%; }

/* ── badges — three families, so an opinion never dresses as a fact ──────── */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
  font: 700 0.6875rem/1.3 'Anybody', sans-serif;
  font-stretch: 110%;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  padding: 0.34em 0.54em;
  border-radius: 4px;
  white-space: nowrap;
}
/* our judgement — the only solid-accent element on a card */
.badge--judgement { background: var(--accent); color: var(--accent-ink); }
/* published fact */
.badge--fact { border: 1px solid var(--line); color: var(--ink-2); }
/* caution — never filled; a warning that shouts becomes wallpaper */
.badge--caution { border: 1px solid var(--warn); color: var(--warn); }
button.badge { cursor: pointer; }
button.badge:active { transform: scale(0.97); }

/* ── cards & rows ────────────────────────────────────────────────────────── */

.card {
  position: relative;
  display: block;
  width: 100%;
  text-align: left;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.875rem 1rem;
  transition: transform 140ms var(--ease-out), border-color 200ms ease, background 200ms ease;
}
.card + .card { margin-top: 0.5rem; }
.card:active { transform: scale(0.985); }
@media (hover: hover) and (pointer: fine) {
  .card:hover { border-color: color-mix(in oklch, var(--accent) 40%, var(--line)); }
}

/* A card's tick on the axis, so every item is physically on the route. */
.card::before {
  content: '';
  position: absolute;
  left: calc(-1 * var(--rail) / 2 - 1px);
  top: 1.35rem;
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--line);
  transition: background 200ms ease, box-shadow 200ms ease;
}
.card[data-live='1']::before {
  background: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in oklch, var(--accent) 20%, transparent);
}
/**
 * De-emphasis by colour, never by opacity.
 *
 * `opacity: 0.52` dimmed the whole subtree including its text, which measured
 * 1.0–1.9:1 against the page — a WCAG AA failure invisible to any test that
 * reads computed colour, because the colour value itself still says
 * rgb(168,172,184). Recede the surface and the border; keep every glyph above
 * the contrast floor.
 */
.card[data-done='1'] {
  background: color-mix(in oklch, var(--surface) 70%, var(--bg));
  border-color: color-mix(in oklch, var(--line) 60%, var(--bg));
}
.card[data-done='1'] .card__title { color: var(--ink-2); }
.card[data-done='1'] .card__sub,
.card[data-done='1'] .meta { color: var(--ink-3); }

.card__top { display: flex; align-items: flex-start; gap: 0.625rem; flex-wrap: wrap; }
/* Two badges must never squeeze a long name into a mangled wrap ("Bam / Mobil").
   The title keeps at least 55% of the row; badges drop to their own line. */
.card__title { flex: 1 1 55%; }
.card__title {
  font: 700 1.0625rem/1.3 'Anybody', sans-serif;
  font-stretch: 104%;
  letter-spacing: -0.01em;
  flex: 1;
  min-width: 0;
  /* Safety net for a title no shortening rule anticipated: cap the visual
     height without truncating the text for screen readers or copy-paste. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}
.card__badges { display: flex; gap: 0.3rem; flex-wrap: wrap; margin-left: auto; justify-content: flex-end; }
/* Two badges beside a title left "Trance Baby Trance" rendering as "Trance
   Baby…" — the title had 55% of a 390px card. Past one badge the row drops to
   its own line and the title gets the full width. */
.card__top:has(.card__badges > .badge:nth-child(2)) > .card__badges {
  flex-basis: 100%;
  margin-left: 0;
  justify-content: flex-start;
}
.card__meta { margin-top: 0.3rem; display: flex; flex-wrap: wrap; gap: 0.25rem 0.625rem; align-items: center; }
.card__sub { color: var(--ink-2); font-size: var(--t-meta); margin-top: 0.35rem; }

.dot { color: var(--ink-3); }

/* Sheet action stack — the install and follow prompts offer a real choice, so
   the buttons are full-width and stacked rather than crammed into a row. */
.sheet__actions { display: flex; flex-direction: column; gap: 0.5rem; margin-top: 1.15rem; }
.sheet__actions .btn { width: 100%; gap: 0.5rem; }
.sheet__actions svg { width: 1.05rem; height: 1.05rem; flex-shrink: 0; }

/* iOS has no install API, so the instruction has to carry the actual glyph the
   visitor is looking for. */
.steps { margin: 1rem 0 0; padding: 0; list-style: none; display: grid; gap: 0.6rem; }
.steps li {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  color: var(--ink);
  font-size: var(--t-body);
}
.steps__icon {
  width: 2rem;
  height: 2rem;
  flex-shrink: 0;
  display: grid;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: 0.5rem;
  background: var(--surface-2);
  color: var(--accent-text);
  font-weight: 700;
}
.steps__icon svg { width: 1.1rem; height: 1.1rem; }
/* The how-it-works steps are a sequence, so they get a little more air than the
   two-step iOS instruction. */
.steps--numbered li { align-items: flex-start; }
.steps--numbered .steps__icon { margin-top: -0.15rem; }

/* ── berliner.party's own channels ───────────────────────────────────────────
   Deliberately quieter than a card: a bordered panel, not a filled accent
   billboard. It appears at the end of a page, so it must read as an offer
   rather than as something competing with the content above it. */

.follow {
  margin: 2rem 0 0;
  padding: 1.1rem 1.1rem 1.2rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background:
    radial-gradient(120% 100% at 0% 0%, color-mix(in oklch, var(--accent) 9%, transparent), transparent 60%),
    var(--surface);
}
.follow__kicker {
  font: 600 0.6875rem/1 'Martian', monospace;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.follow__title {
  margin-top: 0.5rem;
  font: 800 1.25rem/1.2 'Anybody', sans-serif;
  font-stretch: 104%;
  letter-spacing: -0.01em;
}
.follow__body {
  margin-top: 0.4rem;
  color: var(--ink-2);
  font-size: var(--t-body);
  line-height: 1.55;
}
.follow__links { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.9rem; }
.follow__links .btn { flex: 1 1 9rem; gap: 0.5rem; }
.follow__links svg { width: 1.05rem; height: 1.05rem; flex-shrink: 0; }

/* Footer strip — one line, no panel. */
/* Nothing inside #view has a bottom margin that survives into the footer, so
   whatever renders last ends flush against it. Padding here rather than a
   margin on .follow, so this holds for any future last element. */
footer.wrap { padding-top: 1.75rem; }

.follow-inline { display: flex; flex-wrap: wrap; gap: 0.25rem 0.6rem; align-items: baseline; }
.follow-inline a { color: var(--accent-text); font-weight: 600; }

/* ── door policy ─────────────────────────────────────────────────────────────
   Three things a visitor needs separately: what to wear, what happens at the
   door, and the full text. Previously one paragraph with a bare URL in it. */

.dress {
  margin: 1.25rem 0;
  padding: 1rem 1.05rem 1.15rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  /* No accent stripe down the left edge. THE AXIS already owns that gesture —
     one continuous accent rail per screen — and a second one on a card reads as
     a competing rail rather than as emphasis. The enforcement badge carries the
     accent instead, and the panel gets the same faint wash as .follow. */
  background:
    radial-gradient(120% 100% at 0% 0%, color-mix(in oklch, var(--accent) 8%, transparent), transparent 60%),
    var(--surface);
}
.dress__head { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.dress__title {
  font: 700 var(--t-meta)/1.3 'Anybody', sans-serif;
  font-stretch: 108%;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.dress__text { margin-top: 0.6rem; color: var(--ink-2); font-size: var(--t-body); line-height: 1.55; }
.dress__rules {
  margin: 0.85rem 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 0.4rem;
}
.dress__rules li {
  position: relative;
  padding-left: 1.1rem;
  color: var(--ink);
  font-size: var(--t-meta);
  line-height: 1.5;
}
.dress__rules li::before {
  content: '';
  position: absolute;
  left: 0.15rem;
  top: 0.55em;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--accent-text);
}

/* ── buttons ─────────────────────────────────────────────────────────────── */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.45rem;
  min-height: 2.75rem;
  padding: 0 1.05rem;
  border-radius: var(--radius);
  font-weight: 650;
  font-size: var(--t-body);
  border: 1px solid var(--line);
  color: var(--ink);
  transition: transform 140ms var(--ease-out), background 180ms ease, border-color 180ms ease;
}
.btn:active { transform: scale(0.97); }
.btn--primary { background: var(--accent); color: var(--accent-ink); border-color: transparent; font-weight: 750; }
.btn--ghost { background: transparent; }
.btn--block { width: 100%; }
.btn[aria-pressed='true'] { border-color: var(--accent); color: var(--accent-text); }

/* ── chips / filters ─────────────────────────────────────────────────────── */

.chips {
  display: flex;
  gap: 0.4rem;
  overflow-x: auto;
  scrollbar-width: none;
  padding: 0.15rem 0 0.4rem;
  margin-inline: calc(-1 * var(--pad));
  padding-inline: var(--pad);
  scroll-snap-type: x proximity;
}
.chips::-webkit-scrollbar { display: none; }
.chip {
  flex: 0 0 auto;
  scroll-snap-align: start;
  /* Uniform height: a label that wrapped would make its row taller than the
     clamp expects and reintroduce the slicing. */
  white-space: nowrap;
  min-height: var(--chip-h);
  padding: 0.55rem 0.8rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: var(--t-meta);
  font-weight: 600;
  color: var(--ink-2);
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  transition: transform 140ms var(--ease-out), color 180ms ease, border-color 180ms ease, background 180ms ease;
}
.chip:active { transform: scale(0.96); }
.chip[aria-pressed='true'] { background: var(--accent); color: var(--accent-ink); border-color: transparent; }

/* ── header ──────────────────────────────────────────────────────────────── */

.topbar {
  position: sticky;
  top: 0;
  z-index: 40;
  background: color-mix(in oklch, var(--bg) 88%, transparent);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line);
  padding-top: env(safe-area-inset-top);
}
.topbar__in {
  max-width: 46rem;
  margin: 0 auto;
  padding: 0.5rem var(--pad);
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 3rem;
}
.brand {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 0.15rem;
  min-height: 44px;
  text-transform: uppercase;
}
/* Reads before the name, and is never the thing that gets truncated: at 320px
   the single-line version could not fit "Unofficial guide to Rave The Planet"
   beside the search and language buttons. */
.brand__kicker {
  font: 600 0.5625rem/1 'Martian', monospace;
  letter-spacing: 0.12em;
  color: var(--ink-3);
}
.brand__name {
  font: 800 0.8125rem/1 'Anybody', sans-serif;
  font-stretch: 118%;
  letter-spacing: 0.02em;
}
/* With four header controls the wordmark wraps at 320-360px, and the year was
   being orphaned onto a line of its own. Bound to the word before it, so it
   breaks as "RAVE THE / PLANET 26" instead. */
.brand__tail { white-space: nowrap; }
.brand__yr { color: var(--accent); }
.topbar__spacer { flex: 1; }
.iconbtn {
  width: 2.5rem; height: 2.5rem;
  display: grid; place-items: center;
  border-radius: var(--radius);
  color: var(--ink-2);
  transition: transform 140ms var(--ease-out), color 160ms ease;
}
.iconbtn:active { transform: scale(0.94); }
.iconbtn[aria-pressed='true'] { color: var(--accent-text); }


/* ── bottom nav — never moves, all day ───────────────────────────────────── */

.nav {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 50;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  background: color-mix(in oklch, var(--bg) 92%, transparent);
  backdrop-filter: blur(16px);
  border-top: 1px solid var(--line);
  padding-bottom: env(safe-area-inset-bottom);
}
.nav a {
  display: grid;
  place-items: center;
  gap: 0.15rem;
  padding: 0.5rem 0.25rem;
  min-height: 3.5rem;
  color: var(--ink-3);
  font: 600 0.6875rem/1.3 'Hanken', sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  transition: color 160ms ease;
  position: relative;
}
.nav a[aria-current='page'] { color: var(--accent-text); }
.nav a[aria-current='page']::after {
  content: '';
  position: absolute;
  top: 0; left: 50%; translate: -50% 0;
  width: 1.5rem; height: 2px;
  background: var(--accent);
}
.nav svg { width: 21px; height: 21px; }
.nav .pip {
  position: absolute;
  top: 0.45rem; right: 50%; margin-right: -0.85rem;
  min-width: 1rem; height: 1rem; padding: 0 0.2rem;
  border-radius: 999px;
  background: var(--accent); color: var(--accent-ink);
  font: 700 0.5625rem/1rem 'Martian', monospace;
  text-align: center;
}


/* ── countdown ───────────────────────────────────────────────────────────── */

.countdown { display: flex; align-items: baseline; gap: 0.5rem; margin-top: 0.5rem; }
.countdown b {
  font: 800 clamp(2rem, 9vw, 3.25rem)/0.9 'Martian', monospace;
  font-stretch: 100%;
  font-variant-numeric: tabular-nums;
  color: var(--accent-text);
  letter-spacing: -0.03em;
}
.countdown span { font-size: var(--t-meta); color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.1em; }

/* ── notes: conflicts, unconfirmed, provenance ───────────────────────────── */

.note {
  display: block;
  width: 100%;
  max-width: 36em;
  text-align: left;
  margin-top: 0.55rem;
  padding: 0.5rem 0.65rem;
  border-left: 2px solid var(--warn);
  background: color-mix(in oklch, var(--warn) 7%, transparent);
  color: var(--ink-2);
  font-size: var(--t-meta);
  border-radius: 0 4px 4px 0;
}
.note b { color: var(--warn); }
.note--quiet { border-left-color: var(--line); background: var(--surface-2); }
.note--quiet b { color: var(--ink-2); }

/* ── lists & empty states ────────────────────────────────────────────────── */

.stack > * + * { margin-top: 0.5rem; }
.empty {
  padding: 2rem 1rem;
  text-align: center;
  color: var(--ink-3);
  border: 1px dashed var(--line);
  border-radius: var(--radius);
}
.empty b { display: block; color: var(--ink-2); margin-bottom: 0.35rem; font-size: var(--t-lead); }

/* ── entry motion: staggered, short, and only on first paint ─────────────── */

@media (prefers-reduced-motion: no-preference) {
  .reveal {
    opacity: 1;
    transform: none;
    transition: opacity 220ms var(--ease-out), transform 220ms var(--ease-out);
    transition-delay: var(--d, 0ms);
  }
  @starting-style {
    .reveal { opacity: 0; transform: translateY(8px); }
  }
}

/* ── utilities ───────────────────────────────────────────────────────────── */

.row { display: flex; align-items: center; gap: 0.5rem; }
.row--wrap { flex-wrap: wrap; }
.grow { flex: 1; min-width: 0; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.accent { color: var(--accent-text); }
.dim { color: var(--ink-3); }
.hidden { display: none !important; }

/* ── bottom sheets ───────────────────────────────────────────────────────────
   Drawer curve from Ionic via Emil. Sheets scale/slide from the bottom because
   they are thumb-reachable; they are not anchored to a trigger, so the origin
   stays centred like a modal. */

.sheet-backdrop {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  background: rgba(0, 0, 0, 0);
  transition: background 240ms var(--ease-out);
}
.sheet-backdrop.is-open { background: rgba(0, 0, 0, 0.62); }

.sheet {
  width: min(46rem, 100%);
  max-height: 82dvh;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--surface);
  border: 1px solid var(--line);
  border-bottom: 0;
  border-radius: 1rem 1rem 0 0;
  padding: 0.75rem var(--pad) calc(1rem + env(safe-area-inset-bottom));
  transform: translateY(100%);
  transition: transform 260ms var(--ease-drawer);
}
.sheet-backdrop.is-open .sheet { transform: translateY(0); }

.sheet__grip {
  width: 2.25rem; height: 4px;
  margin: 0 auto 0.85rem;
  border-radius: 999px;
  background: var(--line);
}
.sheet__title {
  font: 700 var(--t-lead)/1.2 'Anybody', sans-serif;
  font-stretch: 106%;
  margin-bottom: 0.75rem;
}
.sheet__body { color: var(--ink-2); font-size: var(--t-body); }
.sheet__body .card { margin-top: 0.5rem; }
.sheet__body a { color: var(--accent-text); text-decoration: underline; text-underline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .sheet { transition: none; transform: none; }
  .sheet-backdrop { transition: none; }
}

/* ── detail pages ────────────────────────────────────────────────────────── */

.hero { margin: 1rem 0 1.5rem; }
.hero h1 { margin-bottom: 0.5rem; }

.factgrid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  margin: 1rem 0;
}
.factgrid div { background: var(--surface); padding: 0.7rem 0.8rem; }
.factgrid dt {
  font: 600 0.6875rem/1.3 'Anybody', sans-serif;
  font-stretch: 108%;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: 0.35rem;
}
.factgrid dd { font-size: var(--t-body); font-weight: 600; }

.actions { display: flex; gap: 0.5rem; flex-wrap: wrap; margin: 1rem 0; }
.actions .btn { flex: 1 1 8rem; }

.lineup-item {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding: 0.6rem 0;
  border-bottom: 1px solid var(--line);
}
.lineup-item:last-child { border-bottom: 0; }
.lineup-item .grow { min-width: 0; }

/* Set-time rows carry three columns: time, name, score. At 320px the time
   (100px) and the score chip (114px) leave the name 28px of a 262px row, so
   it overflowed across the chip. Below 380px the time takes its own line. */
@media (max-width: 380px) {
  .lineup-item { flex-wrap: wrap; row-gap: 0.15rem; }
  .lineup-item time { flex-basis: 100%; }
}
.lineup-item__name { font-weight: 650; }
.lineup-item__sub { font-size: var(--t-meta); color: var(--ink-3); }
.lineup-item time {
  font-family: 'Martian', monospace;
  font-variant-numeric: tabular-nums;
  font-size: var(--t-meta);
  color: var(--accent-text);
  flex-shrink: 0;
}

/* 127 characters per line at 1280px measured; the readable range is 45–75.
   A measure cap is the single highest-value typographic rule on any page that
   carries real prose. */
/* Full width, matching the fact grid and actions above it. A 34em measure
   left the description as the only narrow block on the page. */
.prose { color: var(--ink-2); font-size: var(--t-body); line-height: 1.65; }
.prose p + p { margin-top: 0.75rem; }

/* Search.
 *
 * Not sticky. It was `position: sticky; top: 3.5rem; z-index: 30`, but a
 * sticky element can only travel inside its containing block — here the
 * <section> holding the title, filters and sort, about 321px tall. So it
 * pinned for ~200px, dragged its opaque background across the filter and sort
 * rows (hiding them entirely), then unpinned for good. On /artists, 18,000px
 * of list, the field was unreachable from ~450px down.
 *
 * The header carries a permanent global search button, so nothing is lost by
 * letting this scroll with its own list. */
.searchbar {
  padding: 0.5rem 0 0.75rem;
}
.searchbar input {
  width: 100%;
  min-height: 2.75rem;
  padding: 0 0.9rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--ink);
  font-size: var(--t-body);
}
.searchbar input::placeholder { color: var(--ink-3); }
/* `outline: none` here removed the only focus indicator on the one field in
   the app — a colour-only border change is not a visible focus indicator. */
.searchbar input:focus-visible {
  border-color: var(--accent);
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.searchbar input:focus { border-color: var(--accent); }

/* ── tap targets ─────────────────────────────────────────────────────────────
   Some interactive elements must stay visually small — a badge the size of a
   button stops reading as a badge. They get an invisible expanded hit area
   instead, so the thumb target is comfortable without changing the design. */

.badge,
.brand,
.head .more,
.meta a,
.prose a,
.chips-more {
  position: relative;
}
/* Invisible hit-area expansion for controls that are deliberately small.
 *
 * `.meta a` used to be in this list and must not be: those are links inside a
 * running sentence, which WCAG 2.5.8 exempts explicitly ("Inline"), and a 48px
 * box on a 16px line reaches 14px onto the lines above and below. It covered
 * 86% of the hero date line and 61% of the footer tagline, so tapping either
 * followed a link the visitor never aimed at.
 *
 * `.chips-more` and `.brand` are already min-height 44px, so their box adds
 * about 2px — harmless, kept for the case where that changes. */
.chips-more::after,
.brand::after,
.head .more::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 48px;
  translate: 0 -50%;
  /* Wider than the glyphs but never wider than the row, so neighbouring
     targets cannot overlap. */
}

/* The badge sits above an <h1> with only 9.6px of gap, and a 48px box spilled
   13px onto the title — tapping the top of the heading opened the explainer.
   24px clears WCAG 2.5.8 AA (the badge is ~80px wide) and fits the gap. */
button.badge::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 24px;
  translate: 0 -50%;
}
/* The header carries the wordmark plus up to four controls (install, socials,
   search, language). At 320px that overflowed by 10px at 44px each. Height is
   what the tap-target floor is about here — a horizontal row of icons is
   separated by its own edges — so only the box narrows. */
@media (max-width: 420px) {
  .topbar__in { gap: 0.1rem; }
  .iconbtn { width: 2.5rem; min-width: 2.5rem; }
  /* One step down keeps the wordmark on a single line at 390px alongside four
     controls. Below ~360px it wraps to "RAVE THE / PLANET 26", which is why
     the year is bound to the word before it. */
  .brand__name { font-size: 0.75rem; }
}

.nav a, .btn, .chip, .card, .lineup-item { min-height: 44px; }
.lineup-item { min-height: 48px; }
.iconbtn { min-width: 44px; min-height: 44px; }

/* ── popularity score ────────────────────────────────────────────────────────
   Public, unlike the truck and party scores. It sits beside a DJ's name and
   promotes to HEADLINER at 8. Tabular so a column of them lines up. */

.score {
  display: inline-flex;
  align-items: baseline;
  gap: 0.15em;
  line-height: 1.3;
  font-family: 'Martian', monospace;
  font-variant-numeric: tabular-nums;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--ink-2);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 0.28em 0.42em;
  white-space: nowrap;
}
.score b { color: var(--accent-text); font-weight: 700; font-size: 0.875rem; }
.score span { color: var(--ink-3); font-size: 0.6875rem; }
.score--high { border-color: color-mix(in oklch, var(--accent) 55%, var(--line)); }

.score-big {
  display: flex;
  align-items: baseline;
  gap: 0.35rem;
  margin-top: 0.75rem;
}
.score-big b {
  font: 800 2.25rem/1 'Martian', monospace;
  font-variant-numeric: tabular-nums;
  color: var(--accent-text);
  letter-spacing: -0.03em;
}
.score-big span {
  font-size: var(--t-meta);
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.1em;
}

/* Inline place links on an artist row — get from a name to the truck or the
   club in one tap, without opening the artist first. */
.pill {
  display: inline-flex;
  align-items: center;
  min-height: 2rem;
  padding: 0.25rem 0.55rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: var(--t-meta);
  color: var(--ink-2);
  transition: border-color 160ms ease, color 160ms ease;
}
.pill + .pill { margin-left: 0.3rem; }
@media (hover: hover) and (pointer: fine) {
  .pill:hover { border-color: var(--accent); color: var(--accent-text); }
}
.pill:active { transform: scale(0.97); }

/* Events with no parade connection: present and openable, never promoted. */
/* Present and openable, never promoted — and still readable (see above). */
.card--muted {
  background: color-mix(in oklch, var(--surface) 70%, var(--bg));
  border-style: dashed;
  border-color: color-mix(in oklch, var(--line) 75%, var(--bg));
}
.card--muted .card__title { color: var(--ink-2); font-weight: 600; }
.card--muted .card__sub { color: var(--ink-3); }
@media (hover: hover) and (pointer: fine) {
  .card--muted:hover { border-style: solid; border-color: var(--line); }
}

/* ── B5: chips must not hide two thirds of themselves ────────────────────────
   The style row held 20 chips in a 368px viewport — 2,252px of scroll width
   with no scrollbar and no fade, so it read as though only seven styles
   existed. Wrap instead of scroll, clamped to two rows until expanded. */

.chips--wrap {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  overflow: hidden;
  margin-inline: 0;
  /* Zero on every side, not just inline. `.chips` carries block padding, and
     with border-box that padding came out of the clamp's content area — so
     "two rows" only ever fitted one and a sliver of the next. */
  padding: 0;
  /* Exactly two rows, derived from --chip-h so it cannot drift. The first
     version hard-coded 2.25rem while the tap-target fix had already grown
     chips to 44px, so the clamp sliced row two through the text. */
  max-height: calc(2 * var(--chip-h) + 0.4rem);
  /* No transition. Animating max-height reflows the whole page every frame,
     and there are ~30 cards below this row. It is also the wrong call on its
     own terms: you tap "All styles" because you want the styles now, and an
     instant expand is the fastest-feeling response there is. */
}
/* `none`, not a magic 40rem — a taller list would have been clipped. */
.chips--wrap[data-expanded='1'] { max-height: none; }

.stylegroup { margin-bottom: 0.4rem; }
.stylegroup .chips--wrap { margin-bottom: 0.4rem; }

.chips-more {
  display: inline-flex;
  align-items: center;
  min-height: var(--chip-h);
  padding: 0.55rem 0.8rem;
  border: 1px dashed var(--line);
  border-radius: 999px;
  font-size: var(--t-meta);
  font-weight: 600;
  color: var(--accent-text);
}
.chips-more:active { transform: scale(0.96); }

/* Horizontal rows that stay horizontal get an edge fade so the overflow reads. */
.chips--scroll { position: relative; }
.chips--scroll::after {
  content: '';
  position: sticky;
  right: 0;
  flex: 0 0 2rem;
  align-self: stretch;
  margin-left: -2rem;
  background: linear-gradient(90deg, transparent, var(--bg));
  pointer-events: none;
}

/* ── B7: display type scales with title length ───────────────────────────── */
h1.display[data-len='mid']   { font-size: clamp(1.75rem, 7.5vw, 3rem); }
h1.display[data-len='long']  { font-size: clamp(1.5rem, 6vw, 2.25rem); line-height: 1.05; }

/* ── B8: desktop gets a real layout instead of a phone column ────────────── */
@media (min-width: 64rem) {
  .wrap { max-width: 62rem; padding-left: var(--pad); }

  /* Header and nav must share the content width, or the logo floats out of
     alignment with everything below it. */
  .topbar__in { max-width: 62rem; }
  .nav { max-width: 62rem; margin: 0 auto; border-inline: 1px solid var(--line); }

  /* THE AXIS is a single-column device. In a two-column grid its ticks have
     nothing to line up with, and the rail is left as a stray vertical rule —
     so it goes, rather than being kept as decoration that means nothing. */
  .axis { display: none; }
  .card::before { display: none; }
  .head::before { left: calc(-1 * var(--pad)); }

  /* stretch, not start: paired cards match height so the grid reads as rows
     rather than a ragged masonry. */
  .stack { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; align-items: stretch; }
  /* `.card + .card` is (0,2,0) and beat the old `.stack > * + *` (0,1,0)
     regardless of source order, so the stacking margin survived into the grid:
     every second card sat 8px low and its row grew by 8px. Match the
     specificity or the grid never wins. */
  .stack > * + * { margin-top: 0; }
  .stack > .card + .card { margin-top: 0; }
  h1.display { font-size: clamp(2.5rem, 5vw, 4rem); }
  /* Four columns was hard-coded for the party grid. The truck grid has three
     since the "NOW" cell went, and the fourth track stayed as an empty box. */
  .factgrid { grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr)); }
}

/* "No rating" reads as an absence, not a low mark. */
.score--none {
  transition: border-color 160ms ease, color 160ms ease;
  border-style: dashed;
  color: var(--ink-3);
  font-size: 0.6875rem;
  letter-spacing: 0.02em;
}

/* ── whole-card click targets ────────────────────────────────────────────────
   Rows that contain their own links (an artist row links to the truck AND the
   club) cannot be wrapped in an <a> — nesting links is invalid and ambiguous.
   The stretched-link pattern gives the card a full-size hit area from its
   title link, while the inner links stay above it and keep working. */

.card--linked { position: relative; }
.card--linked .card__title::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
}
/* Inner links sit above the stretched overlay. */
.card--linked .pill,
.card--linked a:not(.card__title),
.card--linked button {
  position: relative;
  z-index: 2;
}
.card--linked:active { transform: scale(0.985); }
@media (hover: hover) and (pointer: fine) {
  .card--linked:hover { border-color: color-mix(in oklch, var(--accent) 40%, var(--line)); }
}
/* Containers sit above the overlay so their links work, but they must not
   swallow clicks on empty space — only the interactive children do. */
.card--linked .card__sub,
.card--linked .card__meta {
  position: relative;
  z-index: 2;
  pointer-events: none;
}
.card--linked .card__meta a,
.card--linked .card__sub a,
.card--linked .pill { pointer-events: auto; }

/* Desktop has room: chip rows wrap freely and the expand control is redundant.
   This lives at the end of the file deliberately — the .chips-more base rule is
   declared later than the token block, and an earlier override lost to it. */
@media (min-width: 64rem) {
  .chips--wrap { max-height: none; }
  .chips-more { display: none; }
}

/* Transport line numbers read as the badges they are on a station sign. */
.line {
  display: inline-flex;
  align-items: center;
  min-height: 1.5rem;
  padding: 0 0.4rem;
  margin-right: 0.3rem;
  border-radius: 3px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  font: 600 0.75rem/1 'Martian', monospace;
  font-stretch: 85%;
  color: var(--ink-2);
}

/* The route schematic must never be taller than it is useful. */
/* The diagram is the point of this page, so give it room and a surface to sit
   on rather than floating loose in the column. */
.routemap {
  display: block;
  width: 100%;
  height: auto;
  /* Capped, or a 320-unit viewBox scales to 2.9× on desktop and the labels
     balloon to 27px. */
  max-width: 30rem;
  margin: 0 auto;
  color: var(--ink-2);
}
.routefig {
  margin: 1.5rem 0;
  padding: 1.25rem 1rem 1rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

/* ── filters vs sort ─────────────────────────────────────────────────────────
   Three identical pill rows made a sort control look like a third filter, with
   "Recommended" and "All" both filled accent and nothing saying which did what.
   Filter groups now carry a label; sort is a joined segmented control sitting
   directly above the results it orders. */

.filtergroup { margin-bottom: 0.7rem; }
.filtergroup__label,
.sortbar__label {
  display: block;
  /* 11px floor for functional text; these are navigational, not decorative. */
  font: 700 0.6875rem/1.3 'Anybody', sans-serif;
  font-stretch: 108%;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: 0.4rem;
}

.sortbar {
  flex-wrap: wrap;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 1rem 0 0.25rem;
  padding-top: 0.85rem;
  border-top: 1px solid var(--line);
}
.sortbar__label { margin-bottom: 0; }

.segmented {
  display: inline-flex;
  border: 1px solid var(--line);
  border-radius: 8px;
  overflow: hidden;
  /* Children were flush against the border with no inset. */
  padding: 4px;
  gap: 4px;
}
.segment {
  min-height: 2.25rem;
  padding: 0.45rem 0.85rem;
  font-size: var(--t-meta);
  font-weight: 600;
  color: var(--ink-2);
  transition: background 160ms ease, color 160ms ease;
}
.segment { border-radius: 6px; }
.segment[aria-pressed='true'] { background: var(--surface-2); color: var(--ink); box-shadow: inset 0 -2px 0 var(--accent); }
.segment:active { background: var(--line); }
@media (hover: hover) and (pointer: fine) {
  .segment:hover { color: var(--ink); }
}

/* KINKY is a published fact about the night, not our opinion — outlined like
   the other facts, but in the accent so it leads the badge row. */
.badge--kinky {
  /* Accent outline, full-strength ink. The first attempt tinted the background
     and used --accent-text, which measured 2.37:1. */
  border: 1px solid var(--accent);
  color: var(--ink);
  background: transparent;
}

/* Press feedback on the remaining pressable elements. Every control should
   confirm it heard the tap. */
button.score--none:active { border-color: var(--accent); color: var(--ink-2); }
button.note:active { background: color-mix(in oklch, var(--warn) 12%, transparent); }
button.note--quiet:active { background: var(--line); }

/* ── typographic hero ────────────────────────────────────────────────────────
   Truck and party pages were built around a crew photo. With the photos gone
   for rights reasons, the name and the sound carry the page: the name at full
   display scale, the primary style as a second display line beneath it. No
   substitute imagery, no invented ornament. */

.hero__sound {
  font-family: 'Anybody', sans-serif;
  font-weight: 800;
  font-stretch: 112%;
  font-size: clamp(1.35rem, 5.5vw, 2.25rem);
  line-height: 1;
  letter-spacing: -0.015em;
  color: var(--ink-3);
  margin-top: 0.3rem;
  text-wrap: balance;
}
.hero__meta {
  margin-top: 0.85rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem 0.6rem;
  align-items: baseline;
}
/* Separators, or the items read as one run-on string. */
.hero__meta > * + *::before {
  content: '·';
  color: var(--ink-3);
  margin-right: 0.6rem;
}
