/* Google Fonts (Inter + Space Grotesk) are loaded from base.html's
   <head> via <link rel="preconnect">/<link rel="stylesheet"> -- NOT a
   second @import here. @import inside a stylesheet on the critical
   render path blocks that stylesheet's own parsing until the imported
   CSS is fetched, and can't be discovered by the browser's preload
   scanner nearly as early as a <link> in <head> can (confirmed as
   Lighthouse's top render-blocking-requests flag). This @import also
   requested a DIFFERENT, incomplete weight set than what's actually
   used below (500;700, missing 600 and 800 entirely) -- base.html's
   <link> now requests the real set every weight below actually needs
   (400;700 Inter, 600;700;800 Space Grotesk), so removing this
   redundant, wrong-weight duplicate is a pure improvement either way. */

:root {
  --c-base: #080808;
  --c-surface: #111111;
  --c-elevated: #1C1C1C;
  --c-border: #2A2A2A;
  --c-border-mid: #3D3D3D;
  --c-accent: #F5A623;
  --c-text: #F0F0F0;
  --c-muted: #888888;
  --c-faint: #444444;

  /* Neon nightlife accent pair, layered with the existing warm
     --c-accent for a three-stop brand gradient. Defined once here so
     every other rule references them via var(--c-*) -- see
     test_style_css_responsive.py's approved-palette check, which
     forbids raw hex literals anywhere outside a --c-* definition
     line like this one. */
  --c-accent-pink: #FF2E7A;
  --c-accent-violet: #8B5CF6;

  /* Translucent glow/glass variants -- rgba(), not hex, so these sit
     outside the hex-literal check entirely while still giving every
     other rule a single named source of truth to reference. */
  --c-glow-pink: rgba(255, 46, 122, 0.45);
  --c-glow-violet: rgba(139, 92, 246, 0.4);
  --c-glow-amber: rgba(245, 166, 35, 0.3);
  --c-surface-glass: rgba(17, 17, 17, 0.62);
  --c-elevated-glass: rgba(28, 28, 28, 0.72);

  --gradient-brand: linear-gradient(135deg, var(--c-accent-pink), var(--c-accent-violet));
  --gradient-brand-wide: linear-gradient(120deg, var(--c-accent-pink) 0%, var(--c-accent-violet) 55%, var(--c-accent) 100%);

  /* Reward-card-only palette -- deliberately a different, hotter hue
     order (orange -> red -> purple) than --gradient-brand's
     pink -> violet, so the card doesn't blend in as "just more of the
     same UI". --c-* prefix (not e.g. --reward-*) purely to stay
     inside test_style_css_responsive.py's approved-palette check,
     which only exempts hex literals inside a --c-*: definition line. */
  --c-reward-orange: #FFB347;
  --c-reward-red: #FD1D1D;
  --c-reward-purple: #833AB4;
  --c-reward-glow-red: rgba(253, 29, 29, 0.15);
  --c-reward-glow-purple: rgba(131, 58, 180, 0.35);
  --gradient-reward: linear-gradient(135deg, var(--c-reward-orange), var(--c-reward-red) 55%, var(--c-reward-purple));

  --font-display: 'Space Grotesk', sans-serif;
  --font-body: 'Inter', sans-serif;
  --font-mono: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;

  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --radius-full: 999px;
}

* {
  box-sizing: border-box;
}

/* Fills the margin area outside body's own 1940px cap (below) on very
   wide screens -- body::before's glow background is `position: fixed`
   (viewport-relative, covers the full screen regardless of body's own
   width), but withOUT this, the bare strip of html itself beyond
   body's edges would show the browser's default background instead
   of the dark theme. */
html {
  background-color: var(--c-base);
}

body {
  margin: 0 auto;
  max-width: 1940px;
  background-color: var(--c-base);
  color: var(--c-text);
  font-family: var(--font-body);
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  position: relative;
  overflow-x: hidden;
}

/* Atmospheric "club lighting" glow, fixed behind all content. Pure
   decoration -- no template markup needed, so it can't break the
   index.html/style.css class-reconciliation test. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(circle at 15% -10%, var(--c-glow-pink), transparent 40%),
    radial-gradient(circle at 90% 5%, var(--c-glow-violet), transparent 38%),
    radial-gradient(circle at 50% 110%, var(--c-glow-amber), transparent 45%);
  filter: blur(60px);
  opacity: 0.8;
}

.search-form,
.results-main,
.sort-control,
.pagination {
  position: relative;
  z-index: 1;
}

.site-header {
  /* Sticky, not relative -- kept on screen while the results list
     scrolls underneath it. The existing translucent glass background
     + backdrop-filter blur below already reads well with content
     passing behind it, so no separate "scrolled" style is needed. A
     higher z-index than the other positioned elements above (all at
     1) is required, not optional -- without it, .results-main (later
     in DOM order, same stacking context) would paint over the sticky
     header instead of under it once they overlap during scroll. */
  position: sticky;
  top: 0;
  z-index: 10;
  padding: 24px 32px;
  border-bottom: 1px solid var(--c-border);
  background-color: var(--c-surface-glass);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 24px;
}

.tagline {
  margin: 0;
  color: var(--c-muted);
  font-family: var(--font-body);
  font-size: 0.85rem;
}

.site-nav {
  margin-left: auto;
}

.site-nav a {
  color: var(--c-muted);
  text-decoration: none;
  font-size: 0.85rem;
  transition: color 0.2s ease;
}

.site-nav a:hover {
  color: var(--c-text);
}

.site-footer {
  padding: 16px 32px;
  text-align: center;
  color: var(--c-faint);
  font-size: 0.75rem;
}

.site-footer p {
  margin: 0;
}

.brand {
  display: block;
  font-family: var(--font-display);
  background: var(--gradient-brand-wide);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  text-decoration: none;
  margin: 0;
  font-size: 1.75rem;
  letter-spacing: 0.02em;
  width: fit-content;
  transition: opacity 0.2s ease;
}

.brand:hover {
  opacity: 0.85;
}

select.form-input {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  background-color: var(--c-surface);
  border: 1px solid var(--c-border-mid);
  border-radius: var(--radius-md);
  color: var(--c-text);
  font-family: var(--font-body);
  padding: 12px 16px;
  width: 100%;
  cursor: pointer;
  background-image: linear-gradient(45deg, transparent 50%, var(--c-muted) 50%), linear-gradient(135deg, var(--c-muted) 50%, transparent 50%);
  background-position: calc(100% - 20px) calc(1em + 2px), calc(100% - 15px) calc(1em + 2px);
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

select.form-input:focus {
  outline: none;
  border-color: var(--c-accent-violet);
  box-shadow: 0 0 0 3px var(--c-glow-violet);
}

select.form-input option {
  background-color: var(--c-surface);
  color: var(--c-text);
}

select.form-input option:disabled {
  color: var(--c-faint);
}

.form-input {
  background-color: var(--c-surface);
  border: 1px solid var(--c-border-mid);
  border-radius: var(--radius-md);
  color: var(--c-text);
  font-family: var(--font-body);
  padding: 12px 16px;
  width: 100%;
  min-width: 0;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

.form-input:focus {
  outline: none;
  border-color: var(--c-accent-violet);
  box-shadow: 0 0 0 3px var(--c-glow-violet);
}

.search-form {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  align-items: flex-end;
  padding: 24px 32px;
  border-bottom: 1px solid var(--c-border);
  background-color: var(--c-surface-glass);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
}

.search-form .form-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
  flex: 1 1 200px;
}

.search-form label {
  font-family: var(--font-body);
  color: var(--c-muted);
  font-size: 0.85rem;
}

.button {
  background: var(--gradient-brand);
  color: var(--c-text);
  border: none;
  border-radius: var(--radius-full);
  font-family: var(--font-display);
  font-weight: 700;
  padding: 12px 24px;
  cursor: pointer;
  box-shadow: 0 8px 24px -8px var(--c-glow-pink);
  transition: transform 0.2s ease, box-shadow 0.2s ease, filter 0.2s ease;
}

.button:hover {
  transform: translateY(-2px) scale(1.02);
  box-shadow: 0 12px 32px -8px var(--c-glow-violet);
  filter: brightness(1.08);
}

.button:active {
  transform: translateY(0) scale(0.99);
}

.quick-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* Quick filters share .button's pill shape/sizing/hover-lift affordance
   (per Sprint 14 Subtask 5: "reuse that class rather than inventing a
   new visual style"), but the vivid gradient fill itself is reserved
   for marking the ACTIVE preset -- an inactive quick filter overrides
   .button's background/shadow down to the same muted, outlined
   treatment .genre-pill and .pagination a already use, so there is
   real visual contrast for .pagination .current-page's own pattern
   (filled --gradient-brand) to reuse on the active one. */
.quick-filter {
  display: inline-block;
  text-decoration: none;
  font-size: 0.85rem;
  padding: 8px 16px;
  background: transparent;
  color: var(--c-muted);
  border: 1px solid var(--c-border-mid);
  box-shadow: none;
}

.quick-filter:hover {
  border-color: var(--c-accent-violet);
  color: var(--c-text);
  transform: none;
  box-shadow: none;
}

.quick-filter-active {
  background: var(--gradient-brand);
  color: var(--c-text);
  border-color: transparent;
}

.quick-filter-active:hover {
  border-color: transparent;
}

/* Sprint 16, subtask 1: share button + copy/native-share confirmation.
   Same outlined-pill shape and violet glow-on-hover language as
   .quick-filter above -- this is a secondary action next to the
   results, not a primary CTA, so it deliberately does NOT use the
   solid --gradient-brand fill .quick-filter-active reserves for
   "currently selected" state. */
.share-button {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-body);
  font-size: 0.85rem;
  padding: 8px 16px;
  background: transparent;
  color: var(--c-muted);
  border: 1px solid var(--c-border-mid);
  border-radius: var(--radius-sm, 4px);
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s, box-shadow 0.15s;
}

.share-button:hover {
  border-color: var(--c-accent-violet);
  color: var(--c-text);
  box-shadow: 0 0 0 3px var(--c-glow-violet);
}

.share-button-icon {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
}

.share-button-label {
  font-family: var(--font-body);
}

.share-confirmation {
  display: inline-block;
  margin-left: 8px;
  font-family: var(--font-body);
  font-size: 0.8125rem;
  color: var(--c-accent-pink);
  opacity: 0;
  transition: opacity 0.15s;
}

.share-confirmation.is-visible {
  opacity: 1;
}

.date-range-toggle {
  color: var(--c-muted);
  font-family: var(--font-body);
  font-size: 0.85rem;
  flex: 1 1 200px;
}

.date-range-toggle summary {
  cursor: pointer;
  color: var(--c-muted);
  padding: 8px 0;
}

/* Filter drawer: on wide viewports .filter-drawer-panel/.search-form
   render exactly as they did before this wrapper was introduced --
   .filter-fab (the mobile pill) is hidden and the checkbox driving
   it is irrelevant. On narrow viewports (see the max-width media
   query below) .filter-fab becomes a sticky pill that toggles a
   full-screen overlay via #filter-toggle:checked, a JS-free
   checkbox-driven disclosure (plain CSS sibling selectors, no
   dependency on the browser's own <details> open/closed rendering,
   which -- unlike a plain element -- cannot be reliably forced open
   again with author CSS once collapsed). */
.filter-toggle-checkbox {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Desktop: collapsed, the drawer is just the compact toggle bar, so
   sticking it to the bottom of the (already sticky) header costs
   almost no permanent viewport space and keeps "☰ Search" reachable
   while scrolling. Expanded, the panel is tall (city, quick filters,
   genre/venue pills, custom range, Search) -- staying sticky then
   would permanently occupy a large, fixed chunk of the viewport, so
   this drops back to normal document flow (scrolls away with the rest
   of the page) the moment it's opened. top: 85px matches
   .site-header's own rendered height (24px 32px padding + its content)
   -- see that rule's own sticky/z-index comment. Mobile's OWN sticky
   treatment (inside the max-width media query below) already differs
   intentionally -- collapsed sticks at the very top (no header offset
   needed there), expanded becomes a full-screen overlay instead of
   losing sticky outright -- so this is scoped to NOT apply there. */
@media (min-width: 769px) {
  .filter-drawer {
    position: sticky;
    top: 85px;
    z-index: 5;
  }

  /* Expanded: still normal document flow (scrolls away, not sticky --
     position: relative behaves identically to static for that
     purpose), but ALSO now the positioning context for .filter-fab
     below, which becomes the panel's own embedded close control
     rather than a separate row above it. top: 0 is required, not
     decorative -- the base .filter-drawer rule above sets top: 85px
     for the sticky COLLAPSED state, which position: static ignores
     entirely but position: relative does NOT: left as inherited, it
     shifted the whole expanded panel 85px down from its natural
     position, opening a dead gap under the header. */
  #filter-toggle:checked ~ .filter-drawer {
    position: relative;
    top: 0;
  }

  /* Expanded: "× Close" no longer occupies its own full-width bar --
     it's absolutely positioned into the top-right corner of the panel
     below, reading as part of that one surface rather than a second
     stacked box. .search-form's own padding-top (below) is widened to
     reserve clear space for it, so it never overlaps the city select/
     quick filters/Search button that already sit in that same corner. */
  #filter-toggle:checked ~ .filter-drawer .filter-fab {
    position: absolute;
    top: 18px;
    right: 32px;
    width: auto;
    min-height: 0;
    padding: 4px 10px;
    background: none;
    border: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    z-index: 2;
  }

  #filter-toggle:checked ~ .filter-drawer .search-form {
    padding-top: 56px;
  }
}

/* Sprint 17: collapsed-by-default filter drawer on the RESULTS page
   (fits more of the page above the fold without scrolling), open by
   default on the plain search form (nothing to hide before a first
   search -- the intro text literally says "pick a city below").
   templates/index.html sets #filter-toggle's initial checked state
   from show_results; the checkbox-driven sibling-selector mechanism
   below is otherwise identical to the mobile-only version the max-
   width media query further down already had (that block now adds
   ONLY the full-screen-overlay treatment on top of this, for narrow
   viewports specifically). */
/* Full-width bar, not a shrink-to-content pill -- a small isolated
   rectangle floating at the left reads as an orphaned leftover
   control, not an intentional toolbar. Matching the header's own
   surface-glass treatment (border-bottom + translucent background)
   makes it read as a natural extension of the page chrome instead of
   a randomly-styled button dropped on top of it. */
.filter-fab {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-height: 48px;
  padding: 12px 20px;
  background-color: var(--c-surface-glass);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-md);
  font-family: var(--font-display);
  font-weight: 600;
  color: var(--c-text);
  cursor: pointer;
  box-sizing: border-box;
}

.filter-fab-label {
  white-space: nowrap;
}

.filter-fab-label-open {
  display: none;
}


.filter-drawer-panel {
  display: none;
}

#filter-toggle:checked ~ .filter-drawer .filter-drawer-panel {
  display: block;
  margin-top: 16px;
}

/* Desktop: no gap needed above the panel -- .filter-fab no longer
   occupies its own row there (see the min-width: 769px block above),
   so the 16px breathing room the base rule above still needs for
   mobile's separate sticky-bar treatment would otherwise show up as a
   dead strip between the header and the panel here. Placed AFTER the
   base rule on purpose: identical selector specificity means source
   order decides the winner, and a copy of this override inside the
   earlier min-width media query lost to this rule every time despite
   matching -- confirmed live as the actual cause of that gap. */
@media (min-width: 769px) {
  #filter-toggle:checked ~ .filter-drawer .filter-drawer-panel {
    margin-top: 0;
  }
}

#filter-toggle:checked ~ .filter-drawer .filter-fab-label-closed {
  display: none;
}

/* Close sits at the trailing edge, not stacked at the same leading
   position "☰ Filters" occupied -- margin-left: auto on the sole
   visible label pushes it there regardless of the bar's own
   justify-content, so this one rule covers both mobile and desktop
   without needing separate per-breakpoint alignment overrides. */
#filter-toggle:checked ~ .filter-drawer .filter-fab-label-open {
  display: inline;
  margin-left: auto;
}

/* The chip's whole purpose is summarizing the current search WHILE
   COLLAPSED -- once expanded, the form fields themselves already show
   those values, and leaving the chip visible competes with
   .filter-fab-label-open's auto-margin for the same trailing space
   (confirmed: on a narrow viewport this left "Close" only partially
   pushed right instead of reaching the true edge, since the chip
   after it still needed room). */
#filter-toggle:checked ~ .filter-drawer .filter-fab-chip {
  display: none;
}

.filter-fab-chip {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 0.8rem;
  color: var(--c-muted);
  background-color: var(--c-elevated);
  border: 1px solid var(--c-border-mid);
  border-radius: var(--radius-full);
  padding: 4px 12px;
}

.intro {
  padding: 32px;
  max-width: 640px;
}

.intro-lede {
  font-family: var(--font-display);
  color: var(--c-text);
  font-size: 1.5rem;
  margin: 0 0 8px;
}

.intro-body {
  font-family: var(--font-body);
  color: var(--c-muted);
  font-size: 1rem;
  line-height: 1.6;
  margin: 0;
}

.results-main {
  flex: 1;
  padding: 32px;
}

.results-title {
  font-family: var(--font-display);
  color: var(--c-text);
  font-size: 1.5rem;
  margin: 0 0 16px;
}

.contact-page {
  max-width: 560px;
  padding: 32px;
}

.contact-page > h1 {
  font-family: var(--font-display);
  color: var(--c-text);
  font-size: 1.75rem;
  margin: 0 0 24px;
}

.contact-section {
  background-color: var(--c-surface);
  border: 1px solid var(--c-border-mid);
  border-radius: var(--radius-lg);
  padding: 24px;
  margin-bottom: 24px;
}

.contact-section h2 {
  font-family: var(--font-display);
  color: var(--c-text);
  font-size: 1.15rem;
  margin: 0 0 8px;
}

.contact-section > p {
  color: var(--c-muted);
  font-size: 0.9rem;
  line-height: 1.5;
  margin: 0 0 16px;
}

.contact-form {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.contact-form label {
  font-family: var(--font-body);
  color: var(--c-muted);
  font-size: 0.85rem;
  margin-bottom: -6px;
}

.contact-form textarea.form-input {
  resize: vertical;
  font-family: var(--font-body);
}

.contact-form .button {
  align-self: flex-start;
}

.form-success {
  color: var(--c-text);
  background-color: var(--c-elevated);
  border: 1px solid var(--c-accent-violet);
  border-radius: var(--radius-md);
  padding: 10px 14px;
  font-size: 0.9rem;
  margin: 0 0 16px;
}

.form-error {
  color: var(--c-text);
  background-color: var(--c-elevated);
  border: 1px solid var(--c-accent-pink);
  border-radius: var(--radius-md);
  padding: 10px 14px;
  font-size: 0.9rem;
  margin: 0 0 16px;
}

.results-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  margin-bottom: 16px;
}

.results-count {
  color: var(--c-muted);
  font-family: var(--font-body);
  font-size: 0.85rem;
  margin: 0;
}

.results {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 16px;
  list-style: none;
  margin: 0;
  padding: 0;
}

.event-card {
  background-color: var(--c-surface);
  border: 1px solid var(--c-border-mid);
  border-radius: var(--radius-lg);
  /* Sprint 18: nudged from 20px -> 24px so the card grid's own visual
     density better matches the airier header/search-pane above it. */
  padding: 24px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  position: relative;
  overflow: hidden;
  /* Fixed height, not just a min-height -- a long title/venue name
     (see .event-name/.event-venue's own line-clamp below) used to
     stretch some cards taller than their grid row neighbours, making
     an otherwise even grid look ragged. Clamping both title and venue
     to a bounded number of lines makes every card's content provably
     fit within this height regardless of text length, so the fixed
     height never clips real content. 262px (not 230px) -- tall enough
     for .event-name's 3-line clamp, one line more than a plain 2-line
     title needs, so a long event name reads more fully before it
     truncates; raised from 254px to 262px alongside the padding bump
     above, preserving the exact same usable content height. */
  height: 262px;
  touch-action: pan-y;
  user-select: none;
  cursor: grab;
  transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow 0.25s ease, border-color 0.25s ease, background-color 0.25s ease,
    opacity 0.25s ease;
}

/* While a swipe drag is in progress, static/app.js drives transform
   and opacity directly on every pointer move -- the eased transition
   above would lag a finger/cursor by ~250ms, so it's suspended for
   the duration of the drag and restored once the card snaps back or
   the page reloads. */
.event-card-dragging {
  transition: none;
  cursor: grabbing;
}

.event-card[data-favorite="true"] {
  border-color: var(--c-accent);
}

.event-card[data-hidden="true"] {
  opacity: 0.5;
}

.event-card::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: var(--gradient-brand-wide);
  opacity: 0.85;
}

.event-card:hover {
  background-color: var(--c-elevated);
  border-color: var(--c-accent-violet);
  transform: translateY(-4px);
  box-shadow: 0 16px 40px -16px var(--c-glow-violet), 0 8px 20px -12px var(--c-glow-pink);
}

.event-card-header {
  display: flex;
  align-items: flex-start;
  gap: 12px;
}

.event-card-header .event-name {
  flex: 1 1 auto;
  min-width: 0;
}

.event-card .event-name {
  font-family: var(--font-display);
  color: var(--c-text);
  font-size: 1.1rem;
  line-height: 1.3;
  margin: 0;
  /* Truncate past 3 lines rather than letting a long title stretch
     the card taller than its grid neighbours -- 3 lines lets a long
     title read almost in full before truncating (see .event-card's
     height, sized for this), while still leaving room for
     venue/time/genre below it, which matter just as much for scanning
     a results grid. The full, untruncated title is still reachable
     via the title="" attribute's native tooltip. */
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.event-card .event-name a {
  color: var(--c-accent);
  text-decoration: none;
  transition: color 0.2s ease;
}

.event-card .event-name a:hover {
  color: var(--c-accent-pink);
  text-decoration: underline;
}

.event-card .event-venue {
  color: var(--c-muted);
  font-size: 0.9rem;
  line-height: 1.3;
  margin: 0;
  /* Same reasoning as .event-name's clamp above -- a long venue name
     (e.g. two names joined with "/") must not be able to grow the
     card past its fixed height either. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.event-card .event-start {
  color: var(--c-faint);
  font-size: 0.85rem;
  margin: 0;
}

.event-genre {
  display: inline-block;
  align-self: flex-start;
  background-color: var(--c-elevated);
  border: 1px solid var(--c-border-mid);
  color: var(--c-muted);
  font-family: var(--font-body);
  font-size: 0.75rem;
  padding: 4px 12px;
  margin-top: 4px;
  border-radius: var(--radius-full);
}

.event-card-actions {
  /* Bottom-right corner of the card: last in DOM order among
     .event-card's flex-column children, margin-top: auto pushes it
     down to consume any remaining vertical space (rather than sitting
     wherever the genre pill happens to end), and align-self: flex-end
     pulls it to the right -- real reserved space in normal flow, not
     an absolutely-positioned overlay that could sit on top of a long
     title/venue/description. */
  flex: 0 0 auto;
  margin-top: auto;
  align-self: flex-end;
  display: flex;
  gap: 6px;
}

.event-badge {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border-radius: var(--radius-full);
  border: none;
  background-color: var(--c-surface);
  cursor: pointer;
  transition: background-color 0.2s ease, transform 0.15s ease;
}

.event-badge:hover {
  background-color: var(--c-elevated);
  transform: scale(1.08);
}

.event-badge-icon {
  width: 18px;
  height: 18px;
}

/* Default state: grey outline, unfilled. */
.event-badge-favorite .event-badge-icon {
  fill: none;
  stroke: var(--c-muted);
  stroke-width: 1.6;
}

.event-badge-hidden .event-badge-icon .icon-eye-shape {
  fill: none;
}

.event-badge-hidden .event-badge-icon {
  stroke: var(--c-muted);
  stroke-width: 1.6;
}

/* Active state: filled -- brand pink heart, brand violet crossed-out
   eye (the palette has no literal red/blue token; these are the
   closest approved --c-accent-* tones, reused rather than introducing
   new raw colours -- see test_no_hardcoded_hex_colours_outside_approved_palette). */
.event-badge-favorite[aria-pressed="true"] .event-badge-icon {
  fill: var(--c-accent-pink);
  stroke: var(--c-accent-pink);
}

.event-badge-hidden[aria-pressed="true"] .event-badge-icon {
  stroke: var(--c-accent-violet);
}

.event-badge-hidden[aria-pressed="true"] .event-badge-icon .icon-eye-shape {
  fill: var(--c-accent-violet);
}

/* Unconfirmed badge: a static indicator (not a button -- nothing to
   toggle), shown when a previously-stored event was carried over
   because the latest scrape didn't return it (see db.py's
   merge_and_save_events). Amber/--c-accent to read as "needs a
   second look", distinct from the favorite/hidden badges' pink/violet. */
.event-badge-unconfirmed {
  cursor: help;
}

.event-badge-unconfirmed:hover {
  transform: none;
}

.event-badge-unconfirmed .event-badge-icon {
  fill: none;
  stroke: var(--c-accent);
  stroke-width: 1.6;
}

/* Sponsored partner-offer card, rendered as one of the .results list
   items (see templates/index.html) so it sits inline with the event
   cards it interrupts. Deliberately styled to stand apart -- a
   different gradient direction/hue than the rest of the page, a
   tinted outer glow instead of a flat shadow, and a spring-eased
   pop-in -- rather than blending in as "just another card". */
.reward-card {
  /* span 1 here is the safe default -- it can NEVER force CSS Grid to
     create an extra implicit column. static/app.js's sizeRewardCard()
     overrides this inline to "span 2" once it knows the real column
     count (see there for why: confirmed live that grid-column: span N
     for an N the grid doesn't actually have at a given width does NOT
     clamp down gracefully -- it forces an extra implicit column sized
     by grid-auto-columns (default "auto"), which corrupted EVERY
     row's column widths, not just this card's row, since column
     tracks are shared across the whole grid). */
  grid-column: span 1;
  /* Same fixed height as .event-card (see that rule) so this card
     never sticks up taller than its row neighbours -- every child
     element below is sized/spaced to comfortably fit within it at
     both 1-column (mobile) and 2-column widths, with a line-clamp on
     the description as a safety net rather than letting overflow:
     hidden clip it mid-sentence. */
  height: 262px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;
  border-radius: var(--radius-lg);
  padding: 24px;
  background: var(--gradient-reward);
  color: var(--c-text);
  box-shadow: 0 0 0 4px var(--c-reward-glow-red), 0 12px 30px var(--c-reward-glow-purple);
  animation: reward-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

@keyframes reward-pop {
  0%   { opacity: 0; transform: scale(0.85) translateY(8px); }
  100% { opacity: 1; transform: scale(1) translateY(0); }
}

.reward-eyebrow {
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.65rem;
  font-weight: 700;
  opacity: 0.9;
  margin-bottom: 4px;
}

.reward-percentage {
  font-family: var(--font-display);
  font-size: 2.1rem;
  font-weight: 800;
  line-height: 1;
}

.reward-service {
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 700;
  margin: 4px 0 6px;
}

.reward-code {
  display: inline-block;
  background-color: var(--c-elevated-glass);
  border: 2px dashed var(--c-text);
  border-radius: var(--radius-md);
  padding: 3px 10px;
  font-family: var(--font-mono);
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.05em;
}

.reward-description {
  font-size: 0.7rem;
  line-height: 1.35;
  opacity: 0.95;
  margin-top: 6px;
  /* Safety net, not the primary fit strategy -- the sizes/spacing
     above are already tuned to fit the full text at both 1- and
     2-column widths, but a line-clamp here means a narrower viewport
     than tested (or a future edit to this copy) degrades to a clean
     ellipsis instead of the card's own overflow: hidden clipping the
     text mid-sentence. */
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Shown by static/app.js the moment a search/filter navigation to
   /events is triggered (form submit or a pagination/filter link
   click), and hidden again by default on every fresh page load --
   the natural request/response cycle clears it, no JS teardown call
   needed on the arriving page. */
.search-loading-overlay {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 24px;
  text-align: center;
  background-color: var(--c-surface-glass);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
}

.search-loading-overlay.is-visible {
  display: flex;
}

.search-loading-spinner {
  width: 40px;
  height: 40px;
  border-radius: var(--radius-full);
  border: 4px solid var(--c-border-mid);
  border-top-color: var(--c-accent-pink);
  animation: search-loading-spin 0.8s linear infinite;
}

@keyframes search-loading-spin {
  to { transform: rotate(360deg); }
}

.search-loading-message {
  max-width: 320px;
  color: var(--c-text);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1rem;
  margin: 0;
}

.genre-filters,
.venue-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  /* Full-width row of their own within .search-form's flex layout,
     rather than squeezing in alongside the city select/quick filters
     at whatever leftover inline space remains. */
  flex: 1 1 100%;
}

.genre-pill {
  display: inline-block;
  text-decoration: none;
  color: var(--c-muted);
  font-family: var(--font-body);
  font-size: 0.8rem;
  padding: 8px 16px;
  border: 1px solid var(--c-border-mid);
  border-radius: var(--radius-full);
  transition: border-color 0.2s ease, color 0.2s ease;
}

.genre-pill:hover {
  border-color: var(--c-accent-violet);
  color: var(--c-text);
}

.genre-pill-active {
  background: var(--gradient-brand);
  color: var(--c-text);
  border-color: transparent;
}

.genre-pill-reset {
  padding: 8px 12px;
  color: var(--c-muted);
  font-weight: 600;
}

.genre-pill-reset:hover {
  border-color: var(--c-accent-pink);
  color: var(--c-accent-pink);
}

.empty-state {
  color: var(--c-muted);
  font-family: var(--font-body);
  text-align: center;
  padding: 40px;
  border: 1px dashed var(--c-border-mid);
  border-radius: var(--radius-lg);
}

/* Sprint 18: sits directly above the results grid, not inside
   .results-header -- the venue/sort controls act on the LIST right
   below them, so they read more naturally anchored there than tucked
   next to the count+share row above. Selects are reskinned pills
   (same glass surface + custom chevron language as select.form-input
   above) rather than the old boxy <select> default. */
.sort-control {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
  padding: 0;
  margin-bottom: 20px;
}

.sort-control label {
  color: var(--c-muted);
  font-size: 0.85rem;
  font-family: var(--font-body);
}

.sort-control select {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  background-color: var(--c-surface-glass);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border: 1px solid var(--c-border-mid);
  border-radius: var(--radius-full);
  color: var(--c-text);
  font-family: var(--font-body);
  font-size: 0.85rem;
  padding: 8px 32px 8px 16px;
  cursor: pointer;
  background-image: linear-gradient(45deg, transparent 50%, var(--c-muted) 50%), linear-gradient(135deg, var(--c-muted) 50%, transparent 50%);
  background-position: calc(100% - 18px) calc(1em + 0px), calc(100% - 13px) calc(1em + 0px);
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

.sort-control select:hover {
  border-color: var(--c-accent-violet);
}

.sort-control select:focus {
  outline: none;
  border-color: var(--c-accent-violet);
  box-shadow: 0 0 0 3px var(--c-glow-violet);
}

.sort-control select option {
  background-color: var(--c-surface);
  color: var(--c-text);
}

/* Sprint 14 Subtask 5: moved from centered to bottom-right of the
   results section. */
.pagination {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 12px;
  padding: 24px 0 0;
}

.pagination a,
.pagination span {
  font-family: var(--font-body);
  color: var(--c-text);
  text-decoration: none;
  padding: 8px 16px;
  border: 1px solid var(--c-border-mid);
  border-radius: var(--radius-full);
  transition: border-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
}

.pagination a:hover {
  border-color: var(--c-accent-pink);
  color: var(--c-accent-pink);
  box-shadow: 0 4px 16px -6px var(--c-glow-pink);
}

.pagination .current-page {
  color: var(--c-base);
  background: var(--gradient-brand);
  border-color: transparent;
}

@media (max-width: 768px) {
  .site-header {
    flex-direction: column;
    text-align: center;
  }

  .brand {
    margin: 0 auto;
  }

  .site-nav {
    margin-left: 0;
  }

  /* Sticky filter pill: collapsed state sits at the top of the
     scroll so it stays reachable with a thumb without permanently
     eating vertical space the way the old always-expanded form did. */
  .filter-drawer {
    position: sticky;
    top: 0;
    z-index: 5;
  }

  .filter-fab {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    min-height: 48px;
    padding: 12px 16px;
    background-color: var(--c-surface-glass);
    backdrop-filter: blur(16px);
    -webkit-backdrop-filter: blur(16px);
    border-bottom: 1px solid var(--c-border);
    font-family: var(--font-display);
    font-weight: 700;
    color: var(--c-text);
  }

  /* Collapsed-by-default is now a base-level rule (see the non-media-
     query .filter-drawer-panel rule above) -- mobile only adds its
     own padding on top of that shared behavior. */
  .filter-drawer-panel {
    padding: 16px;
  }

  /* Opened: the whole drawer becomes a full-screen overlay (our
     equivalent of a bottom-sheet/modal, built from the same
     surface/blur tokens the header already uses -- there is no
     pre-existing modal component in this app to reuse) and the
     pill turns into its sticky close bar. The panel's display:block
     on check is already a base-level rule; only the fullscreen
     positioning below is mobile-specific. */
  #filter-toggle:checked ~ .filter-drawer {
    position: fixed;
    inset: 0;
    z-index: 20;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    background-color: var(--c-base);
  }

  #filter-toggle:checked ~ .filter-drawer .filter-fab {
    position: sticky;
    top: 0;
    z-index: 1;
  }

  .search-form {
    flex-direction: column;
    align-items: stretch;
    padding: 0;
    border-bottom: none;
    background: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }

  .search-form .form-group {
    flex: 1 1 auto;
  }

  .search-form .button {
    width: 100%;
  }

  .intro {
    padding: 16px;
  }

  .results {
    grid-template-columns: 1fr;
  }

  .results-main {
    padding: 16px;
  }

  .results-header {
    flex-direction: column;
    align-items: stretch;
  }

  .sort-control {
    flex-direction: column;
    align-items: stretch;
  }

  .sort-control select {
    width: 100%;
  }

  /* Every remaining tappable control gets a real 48x48 hit area --
     the event card link, quick/genre filter pills, and pagination
     links previously relied on text-sized padding alone.
     display: block (not flex) deliberately -- .event-name's own
     -webkit-line-clamp (see that rule) requires its content to stay
     in normal block/inline flow to count line boxes correctly; a flex
     child broke that, letting a long title escape the 2-line clamp
     specifically on mobile. min-height alone still guarantees the
     48px tap target without a flex context -- no negative margin to
     compensate for it: this is the very first element inside the
     card (right after its own top padding), and a negative top margin
     here pulled the title up far enough to clip its first line
     against the card's own overflow: hidden edge, confirmed live on a
     real mobile viewport. A short single-line title just gets a
     little extra breathing room around it instead. */
  .event-card .event-name a {
    display: block;
    min-height: 48px;
  }

  .quick-filter,
  .genre-pill {
    min-height: 48px;
    display: inline-flex;
    align-items: center;
    box-sizing: border-box;
  }

  .form-input,
  select.form-input {
    min-height: 48px;
    padding: 14px 16px;
    box-sizing: border-box;
  }

  .sort-control select {
    min-height: 48px;
    padding: 14px 32px 14px 16px;
    box-sizing: border-box;
  }

  .button {
    min-height: 48px;
    padding: 14px 24px;
    box-sizing: border-box;
  }

  /* Pagination becomes a full-width stack of large thumb-friendly
     buttons instead of small inline links -- this app renders pages
     server-side (no client-side fetch/append), so a page change is a
     normal navigation and never causes the jumpy-footer reflow an
     appended "Load More" list can cause. */
  .pagination {
    flex-direction: column;
    align-items: stretch;
    padding: 16px 0 0;
  }

  .pagination a,
  .pagination span {
    min-height: 48px;
    display: flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    width: 100%;
  }

  /* Blur is comparatively expensive on lower-powered mobile GPUs --
     keep the glow atmosphere but cut its cost on small screens. */
  body::before {
    filter: blur(36px);
  }
}
