/* === THEME: TERMINAL WARM ===
   Dark brown #1a1008, amber #c87941, Space Mono throughout.
   Derived from mockup-3f-terminal-ascii. */

/* === Reset === */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

/* === Variables === */
:root {
  --sidebar-width: 270px;

  /* Type scale. Replaces 19 ad-hoc rem values that included 0.62 / 0.63 / 0.65 -- sizes
     nobody can tell apart but every one of which had to be maintained. Six steps, named
     by role, on whole pixels so a monospace grid stays crisp. */
  --fs-2xs: 0.625rem;   /* 10px — micro labels only, never running text */
  --fs-xs:  0.6875rem;  /* 11px — venue line, chips, buttons */
  --fs-sm:  0.75rem;    /* 12px — utility text */
  --fs-md:  0.8125rem;  /* 13px — event titles, body */
  --fs-lg:  0.9375rem;  /* 15px — toolbar title, modal headings */
  --fs-xl:  1.125rem;   /* 18px — modal title */

  /* Two font stacks with different jobs: Space Mono carries the identity, IBM Plex Mono
     carries the data. Plex holds its counters at 11-13px where Space Mono's fill in. */
  --font-identity: 'Space Mono', ui-monospace, monospace;
  --font-data:     'IBM Plex Mono', 'Space Mono', ui-monospace, monospace;
  --font-pixel:    'Silkscreen', 'Space Mono', ui-monospace, monospace;


  /* Hearted shows in the equalizer. Deliberately off-palette -- it is the one thing in
     the strip that is not "how busy is that night", so it should not be a shade of the
     accent. Matches the heart on an event tile (#ff8fa8), which is where the association
     is learned. Overridden below for wisteria, where pink on pink says nothing, and
     darkened for light mode, where the bright pink measures 2.0:1 on warm paper and
     graphics want 3:1. */
  --eq-fav:       #ff8fa8;
  --eq-fav-hover: #ffb3c4;
  --bg:           #1a1008;
  --surface:      #241609;
  --surface2:     #2e1d0c;
  --border:       #3d2a12;
  --text:         #e8d5b0;
  --muted:        #9a7a50;
  --dim:          #5a4020;
  --accent:       #c87941;
  --accent-hover: #e09050;
  --accent-bg:    rgba(200,121,65,0.10);
  --today-bg:     rgba(200,121,65,0.05);
}

/* === Palette overrides (driven by data-palette attribute on <html>) === */
html[data-palette="phosphor"] {
  --bg:           #060d07;
  --surface:      #0c1a0d;
  --surface2:     #122014;
  --border:       #1e3820;
  --text:         #a8d8ac;
  --muted:        #4e8858;
  --dim:          #28502e;
  --accent:       #44c754;
  --accent-hover: #68e078;
  --accent-bg:    rgba(68,199,84,0.10);
  --today-bg:     rgba(68,199,84,0.05);
}

html[data-palette="midnight"] {
  --bg:           #050810;
  --surface:      #0a0f20;
  --surface2:     #101828;
  --border:       #1a2640;
  --text:         #c0cce0;
  --muted:        #4868a0;
  --dim:          #1a2e50;
  --accent:       #4888e8;
  --accent-hover: #68a8ff;
  --accent-bg:    rgba(72,136,232,0.12);
  --today-bg:     rgba(72,136,232,0.05);
}

html[data-palette="wisteria"] {
  --bg:           #0e0810;
  --surface:      #180e1c;
  --surface2:     #201428;
  --border:       #361e40;
  --text:         #e0c8e8;
  --muted:        #906898;
  --dim:          #4a2858;
  --accent:       #c060d0;
  --accent-hover: #d880e8;
  --accent-bg:    rgba(192,96,208,0.12);
  --today-bg:     rgba(192,96,208,0.05);
}

/* Durham Bulls: #003E7A navy bg, #B15E27 orange accent */
html[data-palette="durham"] {
  --bg:           #030d22;
  --surface:      #081a30;
  --surface2:     #0e2340;
  --border:       #1c3a6a;
  --text:         #d8e4f4;
  --muted:        #4878b8;
  --dim:          #1c3868;
  --accent:       #bf6a28;
  --accent-hover: #f0a050;
  --accent-bg:    rgba(191,106,40,0.14);
  --today-bg:     rgba(191,106,40,0.06);
}

/* === Light mode palette overrides ===
   data-mode="light" triggers warm-paper versions of each accent family.
   Compound selectors [data-mode="light"][data-palette="..."] have higher
   specificity and win over the single-attr dark overrides above. */

html[data-mode="light"] {
  --bg:           #fdf6e8;
  --surface:      #f7ead4;
  --surface2:     #f0ddbb;
  --border:       #dcc898;
  --text:         #2c1a06;
  --muted:        #7a5520;
  --dim:          #c4a068;
  --accent:       #b86020;
  --accent-hover: #8a4010;
  --accent-bg:    rgba(184,96,32,0.10);
  --today-bg:     rgba(184,96,32,0.06);
}

html[data-mode="light"][data-palette="phosphor"] {
  --bg:           #f0f8f1;
  --surface:      #e4f2e6;
  --surface2:     #d4ebd8;
  --border:       #b0d8b8;
  --text:         #0a2010;
  --muted:        #2a6838;
  --dim:          #88b898;
  --accent:       #1a8830;
  --accent-hover: #106820;
  --accent-bg:    rgba(26,136,48,0.10);
  --today-bg:     rgba(26,136,48,0.06);
}

html[data-mode="light"][data-palette="midnight"] {
  --bg:           #f0f4fc;
  --surface:      #e4ecf8;
  --surface2:     #d4e0f4;
  --border:       #aabce0;
  --text:         #08102a;
  --muted:        #284888;
  --dim:          #88a0c8;
  --accent:       #2050c8;
  --accent-hover: #103898;
  --accent-bg:    rgba(32,80,200,0.10);
  --today-bg:     rgba(32,80,200,0.06);
}

html[data-mode="light"][data-palette="wisteria"] {
  --bg:           #f8f0fc;
  --surface:      #f0e4f8;
  --surface2:     #e4d0f0;
  --border:       #c8a8d8;
  --text:         #1a0828;
  --muted:        #603878;
  --dim:          #b090c8;
  --accent:       #8828a8;
  --accent-hover: #6a1888;
  --accent-bg:    rgba(136,40,168,0.10);
  --today-bg:     rgba(136,40,168,0.06);
}

/* === Equalizer favorite color =============================================
   Order matters and the specificities are deliberate. The two single-attribute rules
   are both (0,1,1), so wisteria is written second to win in dark mode; the compound
   rule is (0,2,1) and so still wins for wisteria + light. */

/* Light mode: #ff8fa8 measures 2.0:1 against the warm-paper background, under the 3:1
   graphics floor. This one is 3.66:1 and still reads as the same pink. */
html[data-mode="light"] {
  --eq-fav:       #d94f74;
  --eq-fav-hover: #b83a5c;
}

/* Wisteria is a pink-purple palette, so a pink overlay would say nothing. Soft sky blue
   is the opposite side of the wheel and stays gentle rather than alarming. */
html[data-palette="wisteria"] {
  --eq-fav:       #8ad5f0;
  --eq-fav-hover: #b0e6f8;
}

html[data-mode="light"][data-palette="wisteria"] {
  --eq-fav:       #1f7fa8;
  --eq-fav-hover: #12617f;
}

/* The "strong" favourite colour: same hue family as --eq-fav, taken darker.
   Used by the favourites-only button's pressed background and by a hearted heart icon
   on an event tile. Both need something --eq-fav cannot give them, for the same reason:
   --eq-fav is tuned as a *graphic* sitting on --bg, because it paints equalizer bars.
   Against a tile (--surface2) or behind button text it is too light — measured at
   3.37-4.05 behind the button label (needs 4.5) and 2.64-2.96 as a heart on a light
   tile (needs 3). These values clear both floors everywhere: worst 4.73 for the button
   label and 3.70 for the heart, both on durham/light. */
:root {
  --fav-strong:       #ff8fa8;
  --fav-strong-hover: #ffb3c4;
}
html[data-mode="light"] {
  --fav-strong:       #b83a5c;
  --fav-strong-hover: #9c2a48;
}
html[data-palette="wisteria"] {
  --fav-strong:       #8ad5f0;
  --fav-strong-hover: #b0e6f8;
}
html[data-mode="light"][data-palette="wisteria"] {
  --fav-strong:       #12617f;
  --fav-strong-hover: #0d4d66;
}

/* Durham Bulls light: warm desaturated brown bg, navy text */
html[data-mode="light"][data-palette="durham"] {
  --bg:           #f5ece0;
  --surface:      #eee0d0;
  --surface2:     #e4d0bc;
  --border:       #cdb898;
  --text:         #002858;
  --muted:        #2a4e82;
  --dim:          #b89e80;
  --accent:       #003E7A;
  --accent-hover: #0060c0;
  --accent-bg:    rgba(0,62,122,0.10);
  --today-bg:     rgba(0,62,122,0.06);
}

/* === Base === */
html, body {
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: 'Space Mono', monospace;
  font-size: 16px;
  line-height: 1.5;
}

a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent-hover); text-decoration: underline; }

/* === Layout === */
.layout {
  display: grid;
  grid-template-columns: var(--sidebar-width) 1fr;
  grid-template-rows: auto 1fr;
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
}

/* === Header === */
.app-header {
  grid-column: 1 / -1;
  background: var(--surface);
  border-bottom: 2px solid var(--border);
  padding: 0.6rem 1rem 0.5rem;
  overflow-x: auto;
  position: relative;
}

.ascii-title {
  font-family: 'Space Mono', monospace;
  font-size: 14px; /* fitAsciiTitle() steps this down dynamically if needed */
  line-height: 1.22;
  white-space: pre;
  color: var(--accent);
  user-select: none;
}

.site-title {
  display: none;
  font-family: 'Orbitron', monospace;
  font-size: clamp(1.2rem, 3vw, 2rem);
  font-weight: 900;
  font-style: italic;
  letter-spacing: 0.03em;
  color: var(--accent);
  user-select: none;
  line-height: 1;
}

.site-subtitle {
  font-family: 'Space Mono', monospace;
  font-size: 0.65rem;
  font-style: italic;
  color: var(--accent);
  white-space: nowrap;
  margin-top: 0.2rem;
}

.cursor {
  display: inline-block;
  width: 0.5em;
  height: 0.85em;
  background: var(--accent);
  vertical-align: text-bottom;
  margin-left: 3px;
  animation: blink 1.1s step-end infinite;
}
@keyframes blink { 50% { opacity: 0; } }

/* ── Loading screen ─────────────────────────────────────────────────────── */
#loading-screen {
  position: fixed;
  inset: 0;
  z-index: 2000;
  background: color-mix(in srgb, var(--bg) 50%, transparent);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: opacity 1s ease;
}
#loading-screen.fade-out {
  opacity: 0;
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  #loading-screen { transition: none; }
}
.ls-block {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}
.ls-line {
  display: flex;
  align-items: baseline;
  font-family: 'Space Mono', monospace;
  font-size: 0.9rem;
  white-space: nowrap;
}
.ls-prompt {
  color: var(--dim);
  margin-right: 0.5em;
}
.ls-text {
  color: var(--accent);
}
.ls-cursor {
  display: inline-block;
  width: 0.5em;
  height: 0.85em;
  background: var(--accent);
  vertical-align: text-bottom;
  margin-left: 2px;
  animation: blink 1.1s step-end infinite;
}
.ls-bar-line {
  display: flex;
  align-items: baseline;
  gap: 0.5em;
  font-family: 'Space Mono', monospace;
  font-size: 0.78rem;
  white-space: nowrap;
}
.ls-bar-bracket { color: var(--dim); }
.ls-bar-track   { color: var(--dim); letter-spacing: -0.02em; }
.ls-bar-filled  { color: var(--accent); }
.ls-bar-pct     { color: var(--dim); font-size: 0.68rem; min-width: 2.8em; }

/* ── Per-day hidden-shows chip (month) & row (list) ─────────────────────── */
.day-hidden-chip {
  display: inline-block;
  font-size: 0.6rem;
  color: var(--dim);
  cursor: pointer;
  padding: 1px 4px;
  margin-top: 2px;
  border-radius: 1px;
  border: 1px solid var(--border);
  transition: color 0.12s, border-color 0.12s;
  white-space: nowrap;
  user-select: none;
  line-height: 1.6;
}
.day-hidden-chip:hover { color: var(--accent); border-color: var(--accent); }

.fc-list-hidden-row .fc-list-hidden-cell { padding: 0; }
.list-hidden-btn {
  display: block;
  width: 100%;
  padding: 0.28rem 0.75rem;
  font-family: 'Space Mono', monospace;
  font-size: 0.6rem;
  color: var(--dim);
  background: none;
  border: none;
  cursor: pointer;
  text-align: left;
  transition: color 0.12s, background 0.12s;
  user-select: none;
}
.list-hidden-btn:hover { color: var(--accent); background: var(--accent-bg); }

.header-right {
  position: absolute;
  top: 50%;
  right: 1rem;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.btn-filter-toggle {
  display: none;
  font-family: 'Space Mono', monospace;
  font-size: 0.6rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  padding: 0.25rem 0.6rem;
  background: var(--surface2);
  border: 1px solid var(--border);
  color: var(--muted);
  cursor: pointer;
  border-radius: 1px;
}
.btn-filter-toggle:hover { border-color: var(--accent); color: var(--accent); }

/* Sidebar backdrop — hidden on desktop, toggled on mobile */
.sidebar-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  z-index: 899;
  cursor: pointer; /* required for onclick to fire on iOS Safari */
}
.sidebar-backdrop.active { display: block; }

/* === Sidebar === */
.sidebar {
  background: var(--surface);
  border-right: 1px solid var(--border);
  padding: 1.1rem 1rem;
  overflow-y: auto;
  height: 100%;
  display: flex;
  flex-direction: column;
}

.sidebar::-webkit-scrollbar { width: 4px; }
.sidebar::-webkit-scrollbar-track { background: var(--bg); }
.sidebar::-webkit-scrollbar-thumb { background: var(--dim); border-radius: 2px; }

.filter-section { margin-bottom: 1.4rem; }

/* Section labels — terminal # prefix */
.filter-label {
  font-size: 0.65rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--muted);
  margin-bottom: 0.45rem;
}
.filter-label::before { content: "# "; color: var(--accent); }

/* Search */
.search-wrapper { margin-bottom: 1.25rem; }

.search-wrapper input,
#search-input {
  width: 100%;
  font-family: 'Space Mono', monospace;
  font-size: 0.75rem;
  background: var(--surface2);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 0.4rem 0.6rem;
  outline: none;
  border-radius: 1px;
}
#search-input::placeholder { color: var(--dim); }
#search-input:focus { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-bg); }

/* Chips (city, size, event type — all rendered by JS) */
.chip-group { display: flex; flex-wrap: wrap; gap: 0.3rem; }

.chip {
  font-family: 'Space Mono', monospace;
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  padding: 0.18rem 0.5rem;
  border: 1px solid var(--dim);
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  border-radius: 1px;
  transition: all 0.12s;
}
.chip:hover { border-color: var(--accent); color: var(--accent); }
.chip.active {
  background: var(--accent-bg);
  border-color: var(--accent);
  color: var(--accent-hover);
}

/* Palette switcher */
.palette-group { gap: 0.45rem; }
.palette-btn {
  width: 18px;
  height: 18px;
  border-radius: 1px;
  border: 2px solid transparent;
  cursor: pointer;
  padding: 0;
  flex-shrink: 0;
  transition: border-color 0.12s, transform 0.1s;
}
.palette-btn:hover  { border-color: var(--muted); transform: scale(1.15); }
.palette-btn.active { border-color: var(--text);  transform: scale(1.15); }

/* City chips use per-city color from --chip-border */
.city-chip {
  border-color: var(--chip-border, var(--dim));
  color: var(--chip-border, var(--muted));
  opacity: 0.65;
}
.city-chip:hover {
  border-color: var(--chip-border, var(--accent));
  color: var(--chip-border, var(--accent));
  opacity: 1;
}
.city-chip.active {
  background: var(--chip-active-bg, var(--accent-bg));
  border-color: var(--chip-border, var(--accent));
  color: var(--chip-border, var(--accent-hover));
  opacity: 1;
}

/* Venue hidden via individual or city toggle */
.venue-checkbox:has(input:not(:checked)) .venue-label {
  opacity: 0.45;
}

/* ── Cities & venues ───────────────────────────────────────────────────────────
   Venues nest under a collapsible city header. Replaces the former flat .venue-list
   and the separate city chip row. */

.city-groups { display: flex; flex-direction: column; gap: 0.6rem; }

/* Row for non-city chips (Spotify's "★ for you"). Takes no space while empty, so an
   unconnected visitor sees no gap. */
.quick-filters:not(:empty) { margin-bottom: 0.6rem; }

.city-group-head { display: flex; align-items: center; gap: 0.3rem; }
/* The chip fills the row so the whole width is a city filter target, with the caret a
   separate leading hit area — one row, two distinct actions that can't steal each
   other's clicks. */
.city-group-head .city-chip { flex: 1 1 auto; text-align: left; }

/* Disclosure caret, leading the row like a tree view. */
.city-collapse {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 1px solid transparent;
  border-radius: 1px;
  cursor: pointer;
  /* --muted, not --dim. This is the control the whole feature hangs on, and --dim
     measures 1.83:1 against --surface in dark mode — under the 3:1 an interactive
     element needs. --dim is fine for inert decoration, not for this. */
  color: var(--muted);
  /* 0.6rem was legible only if you already knew it was there. */
  font-size: 0.95rem;
  line-height: 1;
  /* Square, and at the 24px hit-target minimum. */
  width: 24px;
  min-height: 24px;
  padding: 0;
  transition: color 0.12s, border-color 0.12s;
}
.city-collapse:hover { color: var(--accent); border-color: var(--border); }
.city-collapse:focus-visible { outline: 1px solid var(--accent); outline-offset: 1px; }

/* Venue count — a readout, not a control, so it sits outside both buttons. */
.city-count {
  flex-shrink: 0;
  font-family: 'Space Mono', monospace;
  font-size: 0.6rem;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  opacity: 0.75;
  min-width: 1.4em;
  text-align: right;
}

.city-caret { display: inline-block; transition: transform 0.15s ease; }
.city-group.collapsed .city-caret { transform: rotate(-90deg); }

/* Venues indented under their city. The rule is positioned to descend from the middle
   of the 24px caret, so it reads as a tree line rather than an arbitrary margin. */
.city-venues {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  padding: 0.4rem 0 0.1rem 0.8rem;
  margin-left: 11px;
  border-left: 1px solid var(--border);
}

/* Collapse uses display:none deliberately, and the inputs must stay in the DOM.
   _getFilteredEvents() builds its venue map with
   querySelectorAll(".venue-checkbox input[type=checkbox]"), so unmounting a collapsed
   city would silently drop its venues from the filter — the calendar would disagree
   with a sidebar the visitor cannot see. display:none keeps them queryable while
   removing them from layout and from the tab order, which is the same thing
   `.venue-checkbox input { display: none }` already relies on further down. */
.city-group.collapsed .city-venues { display: none; }

@media (prefers-reduced-motion: reduce) {
  .city-caret { transition: none; }
}

.venue-checkbox {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.75rem;
  color: var(--muted);
  cursor: pointer;
}
.venue-checkbox::before { content: "> "; color: var(--dim); font-family: 'Space Mono', monospace; }
.venue-checkbox:hover .venue-label { color: var(--accent); }

.venue-hide-btn {
  margin-left: auto;
  flex-shrink: 0;
  background: none;
  border: none;
  cursor: pointer;
  font-size: 0.6rem;
  line-height: 1;
  color: var(--dim);
  padding: 1px 3px;
  opacity: 0;
  transition: opacity 0.15s, color 0.12s;
  user-select: none;
}
.venue-checkbox:hover .venue-hide-btn { opacity: 1; }
.venue-hide-btn:hover { color: var(--accent); }

.venue-restore-btn {
  font-family: 'Space Mono', monospace;
  font-size: 0.65rem;
  color: var(--muted);
  background: none;
  border: none;
  cursor: pointer;
  padding: 0.35rem 0 0 0;
  text-align: left;
  opacity: 0.6;
  transition: color 0.15s, opacity 0.15s;
  width: 100%;
}
.venue-restore-btn:hover { color: var(--accent); opacity: 1; }

.venue-checkbox input { display: none; }

.venue-dot {
  width: 6px;
  height: 6px;
  border-radius: 0;   /* square */
  flex-shrink: 0;
}

/* Unchecked venue: strikethrough */
.venue-checkbox input:not(:checked) ~ .venue-label {
  opacity: 0.35;
  text-decoration: line-through;
}
.venue-checkbox input:not(:checked) ~ .venue-dot { opacity: 0.25; }

/* === Friends & Feeds === */
.feeds-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.feeds-list a {
  font-size: 0.65rem;
  color: var(--muted);
  display: flex;
  align-items: baseline;
  gap: 0.35rem;
  transition: color 0.12s;
  text-decoration: none;
}
.feeds-list a::before { content: '›'; color: var(--accent); font-size: 0.85rem; }
.feeds-list a:hover { color: var(--accent); text-decoration: none; }

/* === Ko-fi === */
.kofi-sidebar { display: none; }

.kofi-wrap {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.2rem;
}
.kofi-pitch {
  font-size: 0.55rem;
  font-style: italic;
  color: var(--muted);
}
.kofi-btn {
  display: inline-block;
  font-family: 'Space Mono', monospace;
  font-size: 0.58rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 0.3rem 0.7rem;
  background: var(--accent);
  color: var(--bg);
  border-radius: 1px;
  text-decoration: none;
  transition: background 0.15s;
}
.kofi-btn:hover { background: var(--accent-hover); color: var(--bg); text-decoration: none; }

/* === Calendar === */
.calendar-container {
  /* Top spacing is a margin on .fc rather than padding here. A sticky element pins to the
     padding box, so with padding-top the toolbar stopped 1.1rem below the container's top
     edge and scrolling rows showed through the strip above it. As a margin the same gap
     scrolls away with the content, and the toolbar pins flush. */
  padding: 0 1.25rem 1.1rem;
  background: var(--bg);
  overflow-y: auto;
  height: 100%;
}
.calendar-container > .fc { margin-top: 1.1rem; }

/* FullCalendar overrides */
.fc {
  --fc-border-color:              var(--border);
  --fc-button-bg-color:           var(--surface2);
  --fc-button-border-color:       var(--border);
  --fc-button-hover-bg-color:     var(--accent-bg);
  --fc-button-hover-border-color: var(--accent);
  --fc-button-active-bg-color:    var(--accent-bg);
  --fc-button-active-border-color:var(--accent);
  --fc-today-bg-color:            var(--today-bg);
  --fc-page-bg-color:             var(--surface);
  --fc-list-event-hover-bg-color: var(--surface2);
  font-family: 'Space Mono', monospace;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 2px;
}

.fc .fc-toolbar-title {
  font-size: 1rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--accent);
}
.fc .fc-toolbar-title::before { content: "$ "; opacity: 0.4; }

.fc .fc-button {
  font-family: 'Space Mono', monospace !important;
  font-size: 0.6rem !important;
  text-transform: uppercase !important;
  letter-spacing: 0.08em !important;
  border-radius: 1px !important;
  color: var(--text) !important;
  box-shadow: none !important;
}
.fc .fc-button:hover        { color: var(--accent)       !important; }
.fc .fc-button-active,
.fc .fc-button:focus        { color: var(--accent-hover) !important; box-shadow: none !important; }

.fc .fc-col-header-cell-cushion {
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
  text-decoration: none;
}

.fc .fc-daygrid-day-number {
  font-size: 0.75rem;
  color: var(--muted);
  text-decoration: none;
  padding: 4px 6px;
}
.fc .fc-day-today .fc-daygrid-day-number { color: var(--accent); font-weight: 700; }

.fc .fc-day-other .fc-daygrid-day-number { color: var(--dim); }

.fc .fc-daygrid-event {
  border-radius: 1px !important;
  border: none !important;
  margin-bottom: 2px !important;
  cursor: pointer;
}

.fc .fc-daygrid-more-link {
  font-family: 'Space Mono', monospace;
  font-size: 0.58rem;
  color: var(--muted);
}
.fc .fc-daygrid-more-link:hover { color: var(--accent); }

.fc .fc-list-day-cushion { background: var(--surface2) !important; }
.fc .fc-list-day-text,
.fc .fc-list-day-side-text {
  color: var(--accent);
  font-size: 0.7rem;
  text-transform: uppercase;
  text-decoration: none;
}
.fc .fc-list-event { cursor: pointer; }
.fc .fc-list-event:hover td { background: var(--surface2) !important; }

/* List view: hide the time column. table-layout: auto (the default) lets the
   graphic cell shrink to its content and the hidden time cell take 0 space.
   eventDidMount in app.js also sets these via inline style as belt-and-suspenders. */
.fc .fc-list-event-time { display: none !important; width: 0 !important; padding: 0 !important; }
.fc .fc-list-event-graphic { width: 22px !important; max-width: 22px !important; padding: 2px 4px !important; vertical-align: middle !important; text-align: center !important; }
.fc .fc-list-event-title { overflow: hidden; max-width: 0; }
.fc .fc-list-event-title .ev { min-width: 0; overflow: hidden; }

.fc .fc-popover {
  background: var(--surface) !important;
  border: 1px solid var(--accent) !important;
  border-radius: 2px !important;
}
.fc .fc-popover-header {
  background: var(--surface2) !important;
  color: var(--text) !important;
  font-family: 'Space Mono', monospace !important;
  font-size: 0.65rem !important;
}

/* Event tiles */
.ev {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: 2px 44px 2px 4px; /* right padding reserves space for heart + hide */
  overflow: hidden;
}

/* Only reserve room for the icons while they are actually on screen.
   The icons are absolutely positioned, so changing this padding re-ellipsizes the title
   without resizing the tile — nothing around it moves, the text just gets shorter. That
   buys back roughly a character and a half in every month cell for the 99% of the time
   the pointer is elsewhere.
   Gated on (hover: hover) rather than a width: on touch devices the icons are permanently
   visible, so the reserve has to be permanent too, and there is no hover to restore it.
   Selector carries .fc to reach (0,2,0), so it wins over the width-based .ev override in
   the responsive block below regardless of source order. */
@media (hover: hover) {
  .fc .ev          { padding-right: 6px; }
  .fc .ev:hover    { padding-right: 44px; }
}
/* Event title.
   Was Space Mono 700 at 12px, which is the readability complaint that started this branch.
   Bold works against a monospace at this size: Space Mono's counters are tight to begin
   with, and at 12px on a saturated field they close up, so the letterforms lose the shapes
   that tell them apart. The fix is the opposite of the instinct -- less weight, slightly
   more size, and a little tracking to open the rhythm. */
.ev-title {
  font-family: var(--font-data);
  font-size: var(--fs-md);       /* 13px, up from 12 */
  font-weight: 500;              /* down from 700 */
  letter-spacing: 0.005em;
  line-height: 1.35;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--text);
}
/* Venue line.
   Was 10.1px at 78% alpha. Two problems: 10px is under the floor for a mono, and
   compositing white over 22 different saturated hues shifted the result differently on
   every tile. Now 11px in a solid mixed ink, so it reads the same everywhere. */
.ev-venue {
  font-family: var(--font-data);
  font-size: var(--fs-xs);       /* 11px, up from ~10.1 */
  font-weight: 400;
  letter-spacing: 0.015em;
  line-height: 1.3;
  color: color-mix(in srgb, var(--muted) 60%, var(--text));
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Heart / favorite button — revealed when the tile is hovered.
   Colors are palette tokens, not the hardcoded rgba(255,255,255,…) they used to be.
   That white was correct while tiles were saturated dark fills; once the gutter
   treatment moved tile text onto --surface2 it became near-invisible in light mode. */
.ev-heart {
  position: absolute;
  top: 50%;
  right: 24px;
  transform: translateY(-50%);
  background: none;
  border: none;
  cursor: pointer;
  font-size: 0.95rem;
  line-height: 1;
  color: var(--muted);
  padding: 2px 3px;
  opacity: 0;
  transition: opacity 0.15s, color 0.12s;
  z-index: 1;
  user-select: none;
}
.ev:hover .ev-heart        { opacity: 1; }
/* --fav-strong rather than a literal #ff8fa8, and rather than --eq-fav: the token
   carries the light-mode and wisteria variants, so a hearted show stays visible on warm
   paper and stops clashing on the pink palette. --eq-fav itself measures only 2.64-2.96
   against a light tile, because it is tuned to sit on --bg rather than on --surface2. */
.ev-heart.hearted          { opacity: 1 !important; color: var(--fav-strong); }
.ev-heart:hover            { color: var(--accent); }
.ev-heart.hearted:hover    { color: var(--fav-strong-hover); }

/* Hide button — dismiss event until page refresh */
.ev-hide {
  position: absolute;
  top: 50%;
  right: 3px;
  transform: translateY(-50%);
  background: none;
  border: none;
  cursor: pointer;
  /* Was 0.62rem — roughly 10px, and the smallest interactive thing on the page. */
  font-size: 0.78rem;
  line-height: 1;
  color: var(--muted);
  padding: 2px 3px;
  opacity: 0;
  transition: opacity 0.15s, color 0.12s;
  z-index: 1;
  user-select: none;
}
.ev:hover .ev-hide  { opacity: 1; }
.ev-hide:hover      { color: var(--accent); }

/* Past events — greyed out */
.fc-event.ev-past { opacity: 0.45; }

/* Sold-out events */
.fc-event.ev-sold-out { opacity: 0.6; }
.fc-event.ev-sold-out .ev-title { text-decoration: line-through; }

/* Hearted events — accent outline */
.fc-event.ev-hearted {
  outline: 2px solid var(--accent) !important;
  outline-offset: -1px;
}

/* === Modal === */
.modal-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.7);
  z-index: 1000;
  backdrop-filter: blur(2px);
}
.modal-overlay.active { display: block; }

.event-modal {
  display: none;
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: var(--surface);
  border: 1px solid var(--accent);
  border-radius: 2px;
  box-shadow: 0 8px 32px rgba(0,0,0,0.6);
  z-index: 1001;
  max-width: 500px;
  width: 90%;
  max-height: 85vh;
  overflow-y: auto;
  font-family: 'Space Mono', monospace;
}
.event-modal.active { display: block; }

.modal-close {
  position: absolute;
  top: 0.6rem;
  right: 0.6rem;
  background: var(--dim);
  color: var(--text);
  border: 1px solid var(--border);
  width: 28px;
  height: 28px;
  border-radius: 1px;
  cursor: pointer;
  font-size: 1rem;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 2;
  font-family: 'Space Mono', monospace;
}
.modal-close:hover { background: var(--accent); color: var(--bg); }

.modal-image {
  width: 100%;
  height: 180px;
  object-fit: cover;
  border-radius: 0;
}

.modal-body {
  padding: 1.1rem 1.25rem 1.25rem;
}

.modal-header-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.75rem;
}

.modal-body h2 {
  font-size: 1.1rem;
  font-weight: 700;
  line-height: 1.25;
  color: var(--accent);
}

.modal-event-name {
  color: var(--muted);
  font-size: 0.82rem;
  margin-top: 0.25rem;
}

.modal-support {
  color: var(--muted);
  font-size: 0.82rem;
  margin-top: 0.2rem;
  font-style: italic;
}

.modal-venue {
  font-weight: 700;
  font-size: 0.85rem;
  margin-top: 0.65rem;
}

.modal-date {
  font-size: 0.8rem;
  margin-top: 0.2rem;
  color: var(--muted);
}

.modal-times {
  display: flex;
  gap: 1rem;
  margin-top: 0.4rem;
  font-size: 0.78rem;
  color: var(--muted);
}

.modal-price {
  margin-top: 0.65rem;
  font-size: 1rem;
  font-weight: 700;
  color: var(--accent);
}

.modal-genre,
.modal-age {
  margin-top: 0.2rem;
  font-size: 0.75rem;
  color: var(--muted);
}

.modal-description {
  margin-top: 0.65rem;
  font-size: 0.8rem;
  color: var(--muted);
  line-height: 1.5;
  /* Descriptions are plain text with blank lines between paragraphs (see
     clean_html_text in the backend) — pre-line renders those breaks instead of
     collapsing every description into one run-on line, while still collapsing
     ordinary runs of spaces the way normal HTML text does. */
  white-space: pre-line;
}

/* Badges */
.badge {
  display: inline-block;
  padding: 0.15rem 0.5rem;
  border-radius: 1px;
  font-size: 0.62rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  white-space: nowrap;
}
.badge-sold-out  { background: rgba(220,38,38,0.2);  color: #f87171; border: 1px solid #f87171; }
.badge-cancelled { background: rgba(202,138,4,0.2);  color: #fbbf24; border: 1px solid #fbbf24; }
.badge-free      { background: rgba(22,163,74,0.2);  color: #4ade80; border: 1px solid #4ade80; }

.btn-tickets {
  display: inline-block;
  margin-top: 0.85rem;
  padding: 0.4rem 1.1rem;
  background: var(--accent);
  color: var(--bg);
  text-decoration: none;
  border-radius: 1px;
  font-weight: 700;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  transition: background 0.15s;
}
.btn-tickets:hover { background: var(--accent-hover); color: var(--bg); text-decoration: none; }

/* === Mode toggle === */
.mode-toggle-wrap { margin-top: 0.5rem; }

.mode-toggle {
  font-family: 'Space Mono', monospace;
  font-size: 0.6rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  padding: 0.22rem 0.55rem;
  background: var(--surface2);
  border: 1px solid var(--border);
  color: var(--muted);
  cursor: pointer;
  border-radius: 1px;
  transition: all 0.12s;
}
.mode-toggle:hover { border-color: var(--accent); color: var(--accent); }

/* === Bottom-left utility bar (favorites + restore hidden) === */
.favorites-bar {
  position: fixed;
  bottom: 1.1rem;
  left: calc(var(--sidebar-width) + 1rem);
  z-index: 500;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.35rem;
  pointer-events: none;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 0.35s ease, transform 0.45s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.favorites-bar.visible {
  pointer-events: auto;
  opacity: 1;
  transform: translateY(0);
}

.btn-favorites-only,
.btn-download-shows,
.btn-restore-hidden {
  display: inline-block;
  padding: 0.38rem 0.85rem;
  font-family: 'Space Mono', monospace;
  font-size: 0.62rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  background: transparent;
  border: 1px solid var(--accent);
  color: var(--accent);
  cursor: pointer;
  border-radius: 1px;
  transition: background 0.15s, color 0.15s;
  opacity: 0.72;
}
.btn-favorites-only:hover,
.btn-download-shows:hover,
.btn-restore-hidden:hover {
  background: var(--accent);
  color: var(--bg);
  opacity: 1;
}
/* Restore button: slightly subdued by default */
.btn-restore-hidden { opacity: 0.55; }

/* Favourites-only toggle.
   The other two buttons are actions — press and something happens. This one is a mode,
   and the pressed state has to be unmistakable: a filtered calendar that looks
   unfiltered is the failure worth designing against. So the active state is a filled
   button in the heart pink rather than another accent outline, which also stops it
   reading as "hovered". */
.btn-favorites-only.active {
  background: var(--fav-strong);
  border-color: var(--fav-strong);
  color: var(--bg);
  opacity: 1;
}
.btn-favorites-only.active:hover {
  background: var(--fav-strong-hover);
  border-color: var(--fav-strong-hover);
  color: var(--bg);
}
.btn-favorites-only:focus-visible,
.btn-download-shows:focus-visible,
.btn-restore-hidden:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* First-appear bounce for the buttons */
.favorites-bar.visible .btn-download-shows,
.favorites-bar.visible .btn-restore-hidden {
  animation: favBounceIn 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
@keyframes favBounceIn {
  from { transform: scale(0.82) translateY(8px); opacity: 0; }
  to   { transform: scale(1)    translateY(0);   opacity: 1; }
}

/* === Desktop: sticky calendar toolbar === */
@media (min-width: 769px) {
  .fc .fc-header-toolbar,
  .fc .fc-toolbar {
    position: sticky;
    top: 0;
    z-index: 5;
    background: var(--surface);
    padding-top: 0.5rem;
    padding-bottom: 0.25rem;
    border-bottom: 1px solid var(--border);
  }
}

/* === Responsive === */
@media (max-width: 768px) {
  .layout {
    grid-template-columns: 1fr;
    grid-template-rows: auto 1fr;
    height: auto;
    min-height: 100vh;
    min-height: 100dvh;
    overflow: visible;
  }

  .btn-filter-toggle { display: block; }

  /* Swap title: hide ASCII art, show Orbitron text + subtitle below */
  .ascii-title { display: none; }
  .site-title  { display: inline; }

  .site-subtitle {
    position: static;
    transform: none;
    display: block;
    font-size: 0.65rem;
    margin-top: 0.25rem;
    white-space: normal;
    max-width: calc(100% - 5rem); /* clear the filters button */
  }

  /* Prevent iOS auto-zoom on input focus */
  #search-input { font-size: 16px; }

  .sidebar {
    position: fixed;
    top: 0;
    left: -100%;
    width: 85%;
    max-width: 300px;
    height: 100vh;
    height: 100dvh;
    z-index: 900;
    transition: left 0.25s ease;
    box-shadow: 4px 0 20px rgba(0,0,0,0.6);
  }
  .sidebar.open { left: 0; }

  .calendar-container {
    padding: 0.75rem;
    height: auto;
    overflow-x: hidden;
    overflow-y: visible;
  }

  /* List view: clip overflow on mobile; title fills all available width */
  .fc { overflow: hidden; }
  /* List view keeps the reserve: the icons are permanently visible here (no hover to
     reveal them), and titles wrap rather than truncate, so the reserve costs a little
     line width and never hides text. 20px used to be under the icons' own width. */
  .fc-list-event .ev { padding-right: 40px; }

  /* Ko-fi: hide from header, show at bottom of sidebar */
  .app-header .kofi-wrap { display: none; }
  .kofi-sidebar {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.2rem;
    margin-top: 1rem;
    padding-top: 1rem;
    border-top: 1px solid var(--border);
  }

  .fc .fc-toolbar { flex-direction: column; gap: 0.5rem; }

  .event-modal { width: 95%; max-height: 90dvh; }

  /* Bigger touch targets */
  .chip { padding: 0.5rem 0.75rem; font-size: 0.72rem; }
  .venue-checkbox { min-height: 2.75rem; }
  .palette-btn { width: 26px; height: 26px; }

  /* Heart + hide: always slightly visible on mobile (no real hover) */
  .ev-heart { opacity: 0.75; font-size: 0.95rem; }
  .ev-hide  { opacity: 0.6;  font-size: 0.78rem; }
  .venue-hide-btn { opacity: 0.3; }

  /* Bottom bar: left edge on mobile (sidebar is hidden) */
  .favorites-bar { left: 0.75rem; }
}

/* Month view on a phone: drop the icons entirely.
   A day cell is ~52px there and a tile ~47px, so two permanently-visible controls plus
   a title does not fit — measured, the title was allotted 0px and the tile rendered as
   nothing but a heart and a cross. The month grid on a phone is a density overview;
   hearting and hiding belong to the list view, which is the default at that width
   anyway. Also gated on (hover: none) so a merely narrow desktop window keeps its
   hover-revealed icons instead of losing them. */
@media (max-width: 768px) and (hover: none) {
  .fc-daygrid-event .ev-heart,
  .fc-daygrid-event .ev-hide { display: none; }
  .fc-daygrid-event .ev { padding-right: 4px; }
}

/* Light mode: darken the modal overlay slightly less */
html[data-mode="light"] .modal-overlay {
  background: rgba(0,0,0,0.45);
}

/* List view: allow title to wrap — full text is more useful here than truncation */
.fc-list-event .ev-title { white-space: normal; }

/* Light mode: list view needs dark text (events have no colored bg in list view) */
html[data-mode="light"] .fc-list-event .ev-title { color: var(--text); }
html[data-mode="light"] .fc-list-event .ev-venue { color: var(--muted); }
/* The .ev-heart / .ev-hide light-mode overrides that were here are gone: they existed
   only to undo a hardcoded white base, and the base is now --muted, which is already
   palette- and mode-correct. */
html[data-mode="light"] .venue-hide-btn          { color: var(--muted); }

/* Light mode needs nothing special for daygrid events now. The block that used to live
   here put them on --surface2 and colored the title with the venue color, to rescue the
   filled tile in light mode. The gutter treatment already does that in both modes, and
   these rules were (0,3,1) -- high enough to override it. */

/* === Spotify === */
.spotify-connect-wrap { margin-top: 0.55rem; }

.spotify-connect-link {
  font-family: 'Space Mono', monospace;
  font-size: 0.6rem;
  color: var(--dim);
  text-decoration: none;
  letter-spacing: 0.05em;
  transition: color 0.12s;
}
.spotify-connect-link:hover { color: var(--accent); text-decoration: none; }

/* ★ for you chip — sits alongside city chips, uses accent color */
.for-you-chip {
  border-color: var(--accent);
  color: var(--accent);
  opacity: 0.7;
}
.for-you-chip:hover { opacity: 1; }
.for-you-chip.active {
  background: var(--accent-bg);
  border-color: var(--accent);
  color: var(--accent-hover);
  opacity: 1;
}

/* === Multi-show group modal === */
.modal-group-list {
  margin-top: 0.85rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.modal-group-event {
  border-top: 1px solid var(--border);
  padding-top: 0.65rem;
}

.modal-group-event-header {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.modal-group-event-title {
  font-size: 0.92rem;
  font-weight: 700;
  color: var(--text);
}

.modal-group-support {
  font-size: 0.75rem;
  font-style: italic;
  color: var(--muted);
  margin-top: 0.15rem;
}

.modal-group-meta {
  font-size: 0.72rem;
  color: var(--muted);
  margin-top: 0.2rem;
}

.btn-tickets-sm {
  font-size: 0.62rem;
  padding: 0.25rem 0.7rem;
  margin-top: 0.45rem;
}

/* === iCal subscribe button === */
.btn-subscribe-venues {
  display: block;
  width: 100%;
  margin-top: 0.6rem;
  padding: 0.28rem 0.5rem;
  font-family: 'Space Mono', monospace;
  font-size: 0.6rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  background: transparent;
  border: 1px solid var(--dim);
  color: var(--muted);
  cursor: pointer;
  border-radius: 1px;
  text-align: left;
  transition: border-color 0.12s, color 0.12s;
}
.btn-subscribe-venues:hover { border-color: var(--accent); color: var(--accent); }

.subscribe-hint {
  font-size: 0.58rem;
  font-style: italic;
  color: var(--dim);
  margin-top: 0.3rem;
  letter-spacing: 0.02em;
}

/* === Attribution footer === */
.site-credit {
  font-size: 0.65rem;
  font-style: italic;
  color: var(--muted);
  text-align: center;
  margin-top: 1in;
  padding-bottom: 0.5rem;
}
.site-credit a { color: var(--accent); text-decoration: none; }
.site-credit a:hover { color: var(--accent-hover); }


/* ═══════════════════════════════════════════════════════════════════════════
   VISUAL REFRESH — feature/visual-refresh
   Tile treatments, CRT texture, and the night-density equalizer.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Tile treatment: gutter ──────────────────────────────────────────────────
   Alternative to the filled tile. The venue color moves to a 3px left rule and the
   tile sits on the surface, so event text is never on a saturated field and contrast
   is identical for all 22 venues. Reads like a log gutter or a diff marker, which
   suits the terminal frame.

   Specificity note: the light-mode rules further up are (0,3,1). These carry .fc as
   well to reach (0,4,1) so they win in both modes rather than only in dark.
   background-color needs !important because FullCalendar writes the venue color to
   the element as an inline style. */

.fc .fc-daygrid-event {
  background-color: var(--surface2) !important;
  border: none !important;
}
.fc .fc-daygrid-event .ev,
.fc .fc-list-event .ev {
  border-left: 3px solid var(--venue-rule-dark, var(--venue-color, var(--accent)));
  padding-left: 6px;
}
html[data-mode="light"] .fc .fc-daygrid-event .ev,
html[data-mode="light"] .fc .fc-list-event .ev {
  border-left-color: var(--venue-rule-light, var(--venue-color, var(--accent)));
}
.fc .fc-daygrid-event .ev-title { color: var(--text); }
/* Not plain --muted: measured at 4.07:1 on --surface2, which is under the 4.5 floor for
   11px text. Lifted toward --text until it clears, in every palette and both modes. */
.fc .fc-daygrid-event .ev-venue {
  color: color-mix(in srgb, var(--muted) 60%, var(--text));
}
.fc .fc-daygrid-event:hover {
  background-color: color-mix(in srgb, var(--venue-color, var(--accent)) 14%, var(--surface2)) !important;
}
/* No daygrid-specific colour for the icons any more. This block set them to --dim,
   which measures 1.83:1 against --surface2 in dark mode -- under the 3:1 an interactive
   control needs, and the reason they were hard to read on hover. The base .ev-heart /
   .ev-hide rules now use --muted, which works in both modes. */

/* The list view's own dot is drawn by FullCalendar from the event's border color, which
   is the unbrightened seed value -- same invisibility problem on a dark surface. */
.fc .fc-list-event-dot { border-color: var(--venue-rule-dark, var(--venue-color)) !important; }
html[data-mode="light"] .fc .fc-list-event-dot {
  border-color: var(--venue-rule-light, var(--venue-color)) !important;
}


/* ── CRT texture: phosphor glow ──────────────────────────────────────────────
   Accent-colored bloom on identity text only. Never applied to event text, where it
   would undo the readability work above. */

/* Four layers rather than two: a tight hot core, a bright inner bloom, then two wide
   falloffs. Real phosphor bloom is a steep core with a long tail, and stacking the radii
   gets closer to that than turning one shadow up would -- a single wide shadow at high
   opacity just goes muddy. */
html[data-glow="on"] .ascii-title {
  text-shadow: 0 0 2px  color-mix(in srgb, var(--accent) 51%, transparent),
               0 0 9px  color-mix(in srgb, var(--accent) 44%, transparent),
               0 0 22px color-mix(in srgb, var(--accent) 25%, transparent),
               0 0 42px color-mix(in srgb, var(--accent) 11%, transparent);
}
html[data-glow="on"] .site-title,
html[data-glow="on"] .site-subtitle,
html[data-glow="on"] .fc .fc-toolbar-title,
html[data-glow="on"] .filter-label,
html[data-glow="on"] .ls-text {
  text-shadow: 0 0 2px  color-mix(in srgb, var(--accent) 34%, transparent),
               0 0 8px  color-mix(in srgb, var(--accent) 34%, transparent),
               0 0 18px color-mix(in srgb, var(--accent) 16%, transparent);
}
html[data-glow="on"] .cursor,
html[data-glow="on"] .ls-cursor {
  box-shadow: 0 0 4px  color-mix(in srgb, var(--accent) 52%, transparent),
              0 0 12px color-mix(in srgb, var(--accent) 30%, transparent);
}

/* Calendar chrome: weekday headers, day numbers, and the list view's date headers.
   These glow in currentColor rather than --accent, because unlike the identity text they
   are not all accent-colored -- day numbers are --muted, today's is --accent, and the
   list headers are --accent. Phosphor emits the color it is already showing, so
   currentColor is both more accurate and self-maintaining. Radii stay tight: these sit
   at 11-12px, and a wide falloff at that size costs legibility. */
html[data-glow="on"] .fc .fc-col-header-cell-cushion,
html[data-glow="on"] .fc .fc-daygrid-day-number,
html[data-glow="on"] .fc .fc-list-day-text,
html[data-glow="on"] .fc .fc-list-day-side-text {
  text-shadow: 0 0 4px  color-mix(in srgb, currentColor 30%, transparent),
               0 0 10px color-mix(in srgb, currentColor 14%, transparent);
}

/* Today's number is the one number that is meant to catch the eye, so it gets a little
   more than its neighbours rather than the same amount. */
html[data-glow="on"] .fc .fc-day-today .fc-daygrid-day-number {
  text-shadow: 0 0 3px  color-mix(in srgb, var(--accent) 44%, transparent),
               0 0 11px color-mix(in srgb, var(--accent) 22%, transparent);
}

/* Footer. Radius is kept tight here on purpose: bloom has to scale with type size, and
   the wide falloff that reads as phosphor on the 14px ASCII art smears 10px italic into
   mush. Same effect, sized for the text it sits on. */
html[data-glow="on"] .site-credit,
html[data-glow="on"] .site-credit a {
  text-shadow: 0 0 3px color-mix(in srgb, var(--accent) 33%, transparent),
               0 0 9px color-mix(in srgb, var(--accent) 15%, transparent);
}

/* Light mode stays much weaker: bloom on a paper-white ground reads as a printing error
   rather than as phosphor, because there is no dark surround for light to spill into. */
html[data-glow="on"][data-mode="light"] .ascii-title {
  text-shadow: 0 0 4px color-mix(in srgb, var(--accent) 15%, transparent);
}
html[data-glow="on"][data-mode="light"] .site-credit,
html[data-glow="on"][data-mode="light"] .site-credit a {
  text-shadow: 0 0 3px color-mix(in srgb, var(--accent) 10%, transparent);
}


/* Scanlines were tried here and cut: a 1-in-3 duty cycle lands on the same scale as the
   ASCII art's own strokes and the 11px sidebar text, so it read as interference rather
   than as texture. The glow above carries the CRT reference on its own. */


/* ── Sticky calendar chrome ──────────────────────────────────────────────────
   The toolbar and the weekday header are both sticky, and FullCalendar pins both at
   top: 0 of the scroll container. The toolbar wins on z-index (5 against 3) and has an
   opaque background, so it sat directly on top of SUN/MON/TUE and hid it completely.
   Invisible until now, because nothing scrolled the month grid far enough to notice --
   opening the view at today is what surfaced it.

   Offsetting the header by the toolbar's height stacks them instead. The height is
   measured rather than hardcoded: the toolbar wraps to two rows at narrow widths, and a
   fixed value would leave a gap there or overlap again. app.js keeps --fc-toolbar-h
   current with a ResizeObserver; the fallback covers the frame before it first runs.

   The selector carries .fc-scrollgrid-section-sticky only to win the cascade. FullCalendar
   sets this with `.fc .fc-scrollgrid-section-header.fc-scrollgrid-section-sticky > *`,
   which is (0,3,0); the obvious `.fc .fc-scrollgrid-section-header > th` is (0,2,1) and
   loses, because three classes outrank two classes and an element. It also injects its CSS
   into a runtime <style> tag, so source order is not reliably ours either. */
.fc .fc-scrollgrid-section-header.fc-scrollgrid-section-sticky > th {
  top: var(--fc-toolbar-h, 40px);
}


/* ── Night-density equalizer ─────────────────────────────────────────────────
   Fourteen bars, one per night, height = number of shows. Sits above the filters it
   answers to, so changing a filter and watching the bars redraw is a single glance.
   Built from discrete cells rather than a scaled height so the pixel grid is real. */

.eq-section { padding-bottom: 0.15rem; }

.eq-strip {
  border: 1px solid var(--border);
  border-radius: 2px;
  background: var(--bg);
  padding: 7px 7px 5px;
}

.equalizer {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 54px;           /* 8 cells × 5px + 7 gaps × 2px */
}

.eq-bar {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 0;
  background: none;
  border: none;
  cursor: pointer;
  height: 100%;
}

/* Sunday gets a hairline so a fortnight reads as two weeks rather than fourteen days. */
.eq-bar.eq-week-start { box-shadow: -2px 0 0 -1px var(--border); }

.eq-cell {
  flex: 1 1 0;
  border-radius: 0;                        /* square cells — this is a pixel grid */
  background: var(--surface2);
  transition: background-color 0.22s ease, opacity 0.22s ease;
  /* Bars fill left to right, cells bottom to top, so a redraw sweeps rather than snaps. */
  transition-delay: calc(var(--bar-index) * 18ms + (7 - var(--cell-index)) * 12ms);
}
.eq-cell.on  { background: var(--accent); }
.eq-cell.cap { background: var(--accent-hover); }

/* Hearted shows fill the base of each bar in --eq-fav, so a bar reads bottom-up as
   "this many are mine, this many more are on". Declared after .cap so a bar that is
   entirely hearted stays pink rather than taking the accent cap color.

   The inset rim is not decoration. Pink and the phosphor palette's green sit at almost
   identical luminance (1.02:1), which is the red-green colorblind failure case exactly --
   they would flatten to the same grey. The rim makes hearted cells legible as a texture
   rather than only as a hue. Position carries it too: hearted cells are always the
   contiguous base of a bar, and the readout and aria-label both say "N hearted" in
   words, so the color is the third cue rather than the only one. */
.eq-cell.fav,
.eq-cell.fav.cap {
  background: var(--eq-fav);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, white 45%, transparent);
}

.eq-bar:hover  .eq-cell.on,
.eq-bar:focus-visible .eq-cell.on,
.eq-bar.is-active .eq-cell.on { background: var(--accent-hover); }

.eq-bar:hover .eq-cell,
.eq-bar.is-active .eq-cell { background: color-mix(in srgb, var(--accent) 22%, var(--surface2)); }

/* Must come after the .on hover rules above. Both are (0,4,0), so order decides, and
   written any earlier the hearted cells would flip to the accent color on hover. */
.eq-bar:hover .eq-cell.fav,
.eq-bar:focus-visible .eq-cell.fav,
.eq-bar.is-active .eq-cell.fav { background: var(--eq-fav-hover); }

.eq-bar:focus-visible { outline: 1px solid var(--accent); outline-offset: 1px; }

/* Idle shimmer on the top lit cell only — a slow phosphor flicker, not a bounce. The
   per-bar delay keeps the fourteen out of lockstep. */
.eq-cell.cap {
  animation: eqFlicker 3.4s ease-in-out infinite;
  animation-delay: calc(var(--bar-index) * 240ms);
}
@keyframes eqFlicker {
  0%, 84%, 100% { opacity: 1; }
  90%           { opacity: 0.55; }
}

.eq-ruler {
  display: flex;
  justify-content: space-between;
  margin-top: 5px;
  font-family: var(--font-pixel);
  font-size: 6px;
  letter-spacing: 0.06em;
  color: var(--dim);
  text-transform: lowercase;
  user-select: none;
}

.eq-readout {
  font-family: var(--font-data);
  font-size: var(--fs-2xs);
  color: var(--muted);
  margin-top: 0.35rem;
  min-height: 1.2em;
  letter-spacing: 0.02em;
}


/* ── Reduced motion ──────────────────────────────────────────────────────────
   The equalizer's meaning is in the bar heights, which survive intact. Only the
   sweep and the flicker are dropped. */

@media (prefers-reduced-motion: reduce) {
  .eq-cell {
    transition-delay: 0ms !important;
    transition-duration: 0ms !important;
    animation: none !important;
  }
  .cursor, .ls-cursor { animation: none !important; }
}
