/* ---- modern_v2/app.css ---- */
/* Nittany Nights site CSS — source of truth lives at
 * nittany_nights/themes/modern_v2/app.css; the renderer copies it to the
 * site root and cache-busts via ?v=<content-hash> in index.html.
 *
 * Design decisions do NOT live here. Every color, corner radius and font
 * stack is a token defined in the sibling tokens.css, referenced below via
 * var(). Keep this file free of raw hex colors, color functions, literal
 * border-radius values (other than 0) and literal font-family stacks:
 * tests/test_theme_tokens.py enforces that. To reskin, edit tokens.css. */
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
/* Persistent bottom bar height (since 2026-09-07), excluding the safe-area
   inset the bar itself adds in its own bottom padding: every element that
   must clear the bar adds env(safe-area-inset-bottom) again on top of this,
   matching the bar's own padding formula. A size, not a design decision, so
   it lives here rather than in tokens.css (test_theme_tokens.py only bans
   raw colors, radii and font stacks in app.css). */
:root {
  --fab-bar-h: 3.8rem;
}
body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 15px;
  min-height: 100vh;
  overflow-x: hidden;
  line-height: 1.5;
  /* Clear the gesture nav bar when running edge-to-edge (app/PWA). 0 in browsers. */
  padding-bottom: env(safe-area-inset-bottom);
}
/* Opaque fill behind the transparent edge-to-edge status bar so scrolled
   content never shows through behind the system clock/wifi/battery icons.
   (body::before is the noise texture, so the scrim lives on ::after.) */
body::after {
  content: ''; position: fixed; top: 0; left: 0; right: 0;
  height: env(safe-area-inset-top);
  background: var(--bg); z-index: 30; pointer-events: none;
}
body::before {
  content: ''; position: fixed; inset: 0; pointer-events: none; z-index: 0;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.035'/%3E%3C/svg%3E");
  opacity: 0.4;
}
.glow { position: fixed; border-radius: var(--radius-circle); filter: blur(130px); opacity: 0.16; pointer-events: none; z-index: 0; }
.glow-1 { width: 600px; height: 600px; background: var(--glow-orb-1); top: -220px; left: -180px;
  animation: drift1 18s ease-in-out infinite alternate; }
.glow-2 { width: 500px; height: 500px; background: var(--glow-orb-2); bottom: -100px; right: -150px; opacity: 0.09;
  animation: drift2 14s ease-in-out infinite alternate; }
.glow-3 { width: 380px; height: 380px; background: var(--glow-orb-3); top: 30%; left: 45%; opacity: 0.06;
  animation: drift3 22s ease-in-out infinite alternate; }

@keyframes drift1 { from { transform: translate(0,0); } to { transform: translate(40px,30px); } }
@keyframes drift2 { from { transform: translate(0,0); } to { transform: translate(-30px,-20px); } }
@keyframes drift3 { from { transform: translate(0,0); } to { transform: translate(20px,-30px); } }

header {
  position: relative; z-index: 10;
  background: var(--scrim-header);
  backdrop-filter: blur(20px);
  border-bottom: 1px solid var(--border);
  /* Push header content below the status bar when running edge-to-edge. */
  padding-top: env(safe-area-inset-top);
}
.header-inner {
  max-width: 820px; margin: 0 auto;
  padding: 0.55rem calc(1.25rem + env(safe-area-inset-right)) 0.55rem calc(1.25rem + env(safe-area-inset-left));
  display: flex; align-items: center; justify-content: space-between;
  gap: 0.75rem;
}
.brand { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.brand-row { display: flex; align-items: baseline; gap: 0.55rem; min-width: 0; }
.wordmark { font-family: var(--font-display); font-size: 1.85rem; letter-spacing: 0.04em; color: var(--ink-bright); line-height: 1; }
.wordmark .accent {
  color: var(--wordmark-accent);
  text-shadow:
    0 0 4px var(--wordmark-glow-strong),
    0 0 10px var(--wordmark-glow-mid),
    0 0 22px var(--wordmark-glow-soft);
}
/* Wordmark tube glyphs (2026-09-02). When the renderer puts a letter's
   neon tube inline (themes/glyphs.py: nightprint's N, indy's I), the
   .accent span becomes a box sized from the display face's cap height
   (--wm-cap, per theme: 1em is not the cap height) by the glyph's own
   aspect (--glyph-aspect, set inline on the span), sitting on the
   baseline so cap line and baseline match the letters beside it. The
   font's letter stays in the DOM for copy and screen readers, painted
   transparent. Glow is a drop-shadow stack on the span (a real bloom:
   tight bright core out to a wide soft halo); the tube is the path,
   stroked at the renderer's tube width with round joins. */
.wordmark .accent:has(.neon-n) {
  position: relative;
  display: inline-block;
  height: var(--wm-cap);
  width: calc(var(--wm-cap) * var(--glyph-aspect));
  vertical-align: baseline;
  /* overflow other than visible makes an inline-block's baseline its
     bottom edge instead of the hidden letter's text baseline, so the box
     sits exactly on the line regardless of the face's ascent metrics
     (Bebas put it 1 to 2 px high). The glow is a filter on this element
     and is not clipped by its own overflow. */
  overflow: hidden;
  margin-right: var(--wm-glyph-gap);
  color: transparent;
  text-shadow: none;
  filter:
    drop-shadow(0 0 1px var(--wordmark-glow-strong))
    drop-shadow(0 0 3px var(--wordmark-glow-mid))
    drop-shadow(0 0 7px var(--wordmark-glow-soft));
}
.wordmark .accent .neon-n {
  position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible;
}
.wordmark .accent .neon-n path {
  fill: none; stroke: var(--wordmark-accent); stroke-width: 5.25;
  stroke-linejoin: round; stroke-linecap: round;
}
/* Neon strike: app.js adds .strike on boot (once the fonts are in) and
   every 27 s. The gas runs along the glass from one electrode to the
   other (dash offset over the tube's length, --tube-len from the svg),
   the tube stutters out, then holds. Each glyph gets its own start delay
   (--strike-delay, set inline per strike) and one of three stutter
   patterns (.v1 .v2 .v3, picked at random per strike), so a sign with
   several tubes lights up unevenly like real neon. Steady state is
   untouched: the animation only ever subtracts from it. */
/* Boot: the template stamps html[data-nn-neon="dark"] before first paint
   and app.js removes it as it adds the first .strike, so the tube is
   never seen lit before it strikes (same dark state as nn-run's start). */
html[data-nn-neon="dark"] .wordmark .accent .neon-n path {
  stroke-dasharray: var(--tube-len);
  stroke-dashoffset: var(--tube-len);
}
.wordmark .accent .neon-n.strike path {
  stroke-dasharray: var(--tube-len);
  animation: nn-run 0.9s cubic-bezier(0.25, 0.6, 0.3, 1) both;
  animation-delay: var(--strike-delay);
}
.wordmark .accent .neon-n.strike { animation: nn-stutter-1 2.4s steps(1, end) both; animation-delay: var(--strike-delay); }
.wordmark .accent .neon-n.strike.v2 { animation-name: nn-stutter-2; }
.wordmark .accent .neon-n.strike.v3 { animation-name: nn-stutter-3; }
@keyframes nn-run { from { stroke-dashoffset: var(--tube-len); } to { stroke-dashoffset: 0; } }
@keyframes nn-stutter-1 {
  0%, 40%   { opacity: 1; }
  41%, 45%  { opacity: 0.06; }
  46%, 52%  { opacity: 1; }
  53%, 55%  { opacity: 0.06; }
  56%, 100% { opacity: 1; }
}
@keyframes nn-stutter-2 {
  0%, 38%   { opacity: 1; }
  39%, 41%  { opacity: 0.06; }
  42%, 47%  { opacity: 1; }
  48%, 49%  { opacity: 0.06; }
  50%, 60%  { opacity: 1; }
  61%, 66%  { opacity: 0.06; }
  67%, 100% { opacity: 1; }
}
@keyframes nn-stutter-3 {
  0%, 44%   { opacity: 1; }
  45%, 53%  { opacity: 0.06; }
  54%, 57%  { opacity: 0.4; }
  58%, 100% { opacity: 1; }
}
.tagline {
  font-family: var(--font-mono);
  font-size: 0.56rem; color: var(--accent2);
  letter-spacing: 0.16em; text-transform: uppercase; white-space: nowrap;
}
.logo-seo {
  /* The page's H1 (2026-09-06): the one-line description under the logo.
     Styled exactly as the div it replaced; weight and margin reset the
     browser's heading defaults so nothing moves. */
  font-family: var(--font-body-simple);
  font-size: 0.72rem; color: var(--text-dim);
  font-weight: 400; margin: 0;
  opacity: 0.85;
  line-height: 1.1;
}
.header-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 0.5rem; flex-shrink: 0; }
.header-meta-row { display: flex; align-items: center; gap: 0.65rem; }
/* Header store badge (second header-meta row): one platform-matched badge,
   picked by the data-nn-store attribute stamped in the head. Both anchors
   ship in the markup; only the matching one displays. */
.header-store-badge a { display: none; line-height: 0; }
.header-store-badge img { height: 30px; width: auto; display: block; }
html[data-nn-store="ios"] .header-store-badge .store-badge-ios { display: block; }
html[data-nn-store="play"] .header-store-badge .store-badge-play { display: block; }
.weather-pill {
  display: flex; align-items: center; gap: 0.35rem;
  background: var(--card); border: 1px solid var(--border);
  border-radius: var(--radius-pill); padding: 0.3rem 0.7rem;
  font-size: 0.78rem; color: var(--muted);
  text-decoration: none;
}
.weather-pill:hover { text-decoration: none; }
.weather-pill span { color: var(--text); font-weight: 600; }
.header-icon {
  color: var(--muted);
  width: 1.9rem; height: 1.9rem;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--radius-pill); text-decoration: none;
  font-size: 0.95rem; transition: all 0.15s;
}
.header-icon:hover { color: var(--text); background: var(--card); }
.header-icon svg { width: 1rem; height: 1rem; fill: currentColor; }
.header-icon-ig svg path { fill: url(#ig-grad); }
.header-icon-ig:hover { background: var(--card); }
/* SHARE (2026-10-02): a labelled header button that opens the share sheet
   (store QR codes). Ships hidden; app.js unhides it. The button resets sit
   at one class so a theme's .header-meta-row .header-icon box still wins. */
.header-share { background: none; border: 0; font: inherit; cursor: pointer; }
.header-share[hidden] { display: none; }
.header-icon.header-share {
  width: auto; gap: 0.3rem; padding: 0 0.55rem;
  color: var(--text); font-size: 0.72rem; font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase;
}
.header-icon.header-share svg { width: 0.9rem; height: 0.9rem; }

.ts-bar {
  text-align: center;
  font-family: var(--font-mono);
  font-size: 0.62rem; color: var(--dimmer);
  letter-spacing: 0.14em;
  padding: 0.3rem 0.5rem;
  border-bottom: 1px solid var(--border);
  background: var(--scrim-ts-bar);
  position: relative; z-index: 10;
  text-transform: uppercase;
}
.ts-bar a, .ts-bar a:link, .ts-bar a:visited {
  color: inherit; text-decoration: underline;
}
/* Status rotation in the updated bar (see app.js). .ts-fade is an inner
   wrapper injected only for Android/iOS visitors; fading it keeps the
   bar's border/background solid during the content swap. (The app promo
   state and its .ts-promo/.promo-* rules were removed 2026-09-30.) */
.ts-fade { transition: opacity 0.3s ease; }
.live-dot {
  display: inline-block; width: 6px; height: 6px;
  background: var(--green); border-radius: var(--radius-circle); margin-right: 6px;
  animation: pulse 2s ease-in-out infinite;
  vertical-align: middle; position: relative; top: -1px;
  box-shadow: 0 0 8px var(--state-live-glow);
}
@keyframes pulse { 0%,100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.4; transform: scale(0.75); } }

/* ANNOUNCE — slim one-line ticker, styled to match .ts-bar.
   Purpose: context for why town is busy tonight (game day, farmers market, etc.)
   Auto-scrolls horizontally (stock-ticker style); pauses on hover. */
.announce {
  position: relative; z-index: 10;
  max-width: 820px; margin: 0 auto;
  font-family: var(--font-mono);
  font-size: 0.62rem; color: var(--muted);
  letter-spacing: 0.12em; text-transform: uppercase;
  border-bottom: 1px solid var(--border);
  background: var(--scrim-announce);
  display: flex; align-items: stretch;
  height: 26px;
}
.announce-lead {
  flex-shrink: 0;
  display: flex; align-items: center;
  color: var(--announce-lead-ink);
  font-weight: 600;
  padding: 0 0.5rem 0 1.25rem;
  border-right: 1px solid var(--border);
  background: var(--announce-lead-bg);
  z-index: 2;
}
.announce-mask {
  flex: 1;
  overflow: hidden;
  position: relative;
  display: flex; align-items: center;
  padding-left: 0.8rem;
}
/* DESIRED BEHAVIOR — DO NOT "FIX" THIS:
   The ticker MUST start with items just off the right edge of the mask
   (mobile-first: items slide in from the right, traverse the bar, exit left).
   This is achieved via `padding-left: 100%` on the track, which shifts the
   first item to start at x = mask_width (off-screen right). The animation
   then translates the entire track by -100% of its own width so every item
   eventually exits off the left.

   Side effect on wide desktop: because items (~337px each, banner-dependent)
   are narrower than the desktop mask (~733px), there are stretches of the
   cycle where the bar appears empty. That is acceptable — the slide-in-from-
   right entrance is the priority. Do NOT switch to a seamless `translateX(-50%)`
   marquee or remove the `padding-left: 100%`; doing so eliminates the
   off-screen-right entrance the user explicitly wants.

   If desktop emptiness ever needs addressing, the right lever is to add MORE
   banners (so the duplicated stream fills the wider mask), not to change
   this animation. */
.announce-track {
  display: inline-flex;
  white-space: nowrap;
  padding-left: 100%;
  animation: announce-scroll 120s linear infinite;
  will-change: transform;
}
/* Pause-on-hover is applied via JS (app.js), NOT CSS. A CSS `:hover` rule —
   even gated behind `@media (hover: hover)` — still fires on touch in the
   Android WebView (it reports hover:hover), where a tap leaves a sticky
   :hover that froze the ticker until you tapped elsewhere. JS gates the
   pause on `pointerType === 'mouse'`, so touch taps can never freeze it. */
.announce-track.is-paused { animation-play-state: paused; }
.announce-card {
  display: inline-block;
  color: var(--text-dim);
  padding-right: 2.2rem; /* gap between items; sep rendered via ::after */
  position: relative;
}
.announce-card::after {
  content: '·';
  position: absolute;
  right: 1rem;
  color: var(--dimmer);
}
.announce-card:last-child::after { content: ''; }
.announce-card a { color: var(--neon); text-decoration: none; font-weight: 600; }
.announce-card a:hover { text-decoration: underline; }
@keyframes announce-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-100%); }
}
/* Intentionally NOT gated on `prefers-reduced-motion: reduce`. On Windows,
   Chrome reports `reduce` whenever the OS "Animation effects" toggle is off
   (Settings > Accessibility > Visual effects), a perf/preference switch many
   users flip for reasons unrelated to motion sensitivity (and commonly off on
   VMs, RDP, corporate images). The old `.announce-track { animation: none }`
   rule froze the ticker for those users and, because the track rests at
   `padding-left: 100%`, parked every card off-screen right so the bar rendered
   EMPTY, not merely static. The ticker is decorative and already pauses on
   hover/tap (WCAG 2.2.2 Pause/Stop/Hide), so by design it scrolls everywhere.
   See lessons.md: "Windows Chrome froze the ticker via prefers-reduced-motion." */

.day-nav-wrapper {
  position: sticky; top: env(safe-area-inset-top); z-index: 20;
  background: var(--scrim-day-nav); backdrop-filter: blur(20px);
  border-bottom: 1px solid var(--border);
}
.day-nav {
  max-width: 820px; margin: 0 auto;
  display: flex; gap: 0.625rem;
  padding: 0.4rem calc(1.25rem + env(safe-area-inset-right)) 0.4rem calc(1.25rem + env(safe-area-inset-left));
  overflow-x: auto; scrollbar-width: none;
  scroll-snap-type: x proximity;
  scroll-padding-inline-start: 1.25rem;
}
.day-nav::-webkit-scrollbar { display: none; }
.day-tab {
  flex-shrink: 0; padding: 0.3rem 0.9rem;
  min-width: 72px; text-align: center;
  border-radius: var(--radius-pill);
  font-size: 0.78rem; font-weight: 500;
  cursor: pointer; border: 1px solid var(--border);
  background: transparent; color: var(--muted);
  transition: all 0.15s; white-space: nowrap;
  font-family: inherit;
  scroll-snap-align: start;
}
.day-tab:hover { color: var(--text); border-color: var(--border-strong); }
.day-tab.today { border-color: var(--accent2); color: var(--neon); }
.day-tab.active { background: var(--accent2); border-color: var(--accent2); color: var(--ink-bright); font-weight: 600; }

.filters {
  max-width: 820px; margin: 0 auto;
  display: flex; gap: 0.625rem;
  padding: 0.45rem 1.25rem 0.1rem;
  flex-wrap: wrap;
  justify-content: flex-start;
}
.filter-pill {
  display: inline-flex; align-items: center; gap: 0.4rem;
  padding: 0.55rem 0.9rem;
  min-height: 40px;
  border-radius: var(--radius-pill);
  font-size: 0.8rem; font-weight: 500;
  cursor: pointer; border: 1px solid var(--border);
  background: var(--card); color: var(--muted);
  transition: all 0.15s; font-family: inherit;
}
.filter-pill:hover { color: var(--text); border-color: var(--border-strong); }
.filter-pill.active { color: var(--text); border-color: var(--border-strong); background: var(--card-hover); }
.dot { width: 7px; height: 7px; border-radius: var(--radius-circle); display: inline-block; }
.dot.all { background: var(--dot-all-bg); box-shadow: 0 0 5px var(--dot-all-glow); }
.dot.music { background: var(--music); box-shadow: 0 0 6px var(--music-glow); }
.dot.drinks { background: var(--drinks); box-shadow: 0 0 6px var(--drinks-glow); }
.dot.food { background: var(--food); box-shadow: 0 0 6px var(--food-glow); }
.dot.sports { background: var(--sports); box-shadow: 0 0 6px var(--sports-glow); }
.dot.specials { background: var(--drinks); box-shadow: 0 0 6px var(--drinks-glow); }
.dot.events { background: var(--events); box-shadow: 0 0 6px var(--events-glow); }

main {
  position: relative; z-index: 5;
  max-width: 820px; margin: 0 auto;
  padding: 0.6rem 1.25rem 3rem;
}
.day-section { display: none; }
.day-section.visible { display: block; }

.day-header {
  display: flex; align-items: flex-end; gap: 1rem;
  margin: 0.2rem 0 calc(0.3rem + 4px);
  flex-wrap: wrap;
}
.day-label {
  font-family: var(--font-display);
  font-size: 2.3rem; letter-spacing: 0.04em;
  color: var(--ink-bright); line-height: 0.9;
}
.day-weather {
  margin-left: auto;
  align-self: flex-end;
  display: flex; align-items: center; gap: 0.45rem;
  color: var(--text-dim); font-size: 0.82rem;
  padding: 0.4rem 0.7rem;
  background: var(--surface-tint-weather);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-lg);
}
.day-weather .day-weather-icon { font-size: 1.1rem; }
.day-weather .day-weather-info { display: flex; flex-direction: column; line-height: 1.25; }
.day-weather .day-weather-top { font-size: 0.72rem; }
.day-weather .day-weather-temps { font-family: var(--font-mono); font-size: 0.62rem; color: var(--dimmer); }

/* TONIGHT HOURLY FORECAST — slim, below venues, before pricing */
.tonight-forecast-wrap {
  position: relative; z-index: 5;
  max-width: 820px; margin: 0 auto;
  padding: 0 1.25rem;
}
.tonight-forecast {
  background: var(--surface-tint-faint);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-xl);
  padding: 0.4rem 0.65rem 0.45rem;
  margin-bottom: 0.55rem;
  scroll-margin-top: calc(120px + env(safe-area-inset-top)); /* clear the sticky day-nav when scrolled to via #tonight-forecast */
}
html { scroll-behavior: smooth; }
.tonight-label {
  font-family: var(--font-mono);
  font-size: 0.58rem; letter-spacing: 0.18em;
  color: var(--dimmer); text-transform: uppercase;
  margin-bottom: 0.4rem;
}
.tonight-hours {
  display: flex; gap: 0.2rem;
  overflow-x: auto; scrollbar-width: none;
  justify-content: space-between;
}
.tonight-hours::-webkit-scrollbar { display: none; }
.th-hour {
  flex: 1; min-width: 40px;
  text-align: center;
  display: flex; flex-direction: column;
  align-items: center; gap: 0.12rem;
}
.th-time { font-family: var(--font-mono); font-size: 0.58rem; color: var(--dimmer); text-transform: uppercase; }
.th-emoji { font-size: 1rem; line-height: 1.1; }
.th-temp { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text); font-weight: 500; }
.th-precip { font-family: var(--font-mono); font-size: 0.54rem; color: var(--accent2); }

/* VENUE CARD */
.venue-card {
  position: relative;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-2xl);
  padding: 0.65rem 0.9rem 0.65rem;
  margin-bottom: 0.75rem;
  transition: background 0.18s, border-color 0.18s, transform 0.18s, box-shadow 0.18s;
  /* Cascade-in: fade + slide up on load */
  opacity: 0;
  transform: translateY(8px);
  animation: card-in 0.45s ease-out forwards;
  animation-delay: var(--card-delay, 0s);
}
@keyframes card-in {
  to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .venue-card { animation: none; opacity: 1; transform: none; }
}
/* Moved by Near me (app.js slide): no card-in replay on re-insert. Before
   :hover so the lift still applies. */
.venue-card.nn-placed { animation: none; opacity: 1; transform: none; }
.venue-card:hover {
  background: var(--card-hover);
  border-color: var(--border-strong);
  transform: translateY(-1px);
  box-shadow: 0 10px 40px var(--shadow-card);
}
/* LIVE NOW — green-tinted border, subtle green glow */
.venue-card.live-now {
  border-color: var(--state-live-border);
  box-shadow: 0 0 0 1px var(--state-live-ring), 0 8px 28px var(--state-live-halo);
}
.venue-card.live-now:hover {
  border-color: var(--state-live-border-hover);
  box-shadow: 0 0 0 1px var(--state-live-ring-hover), 0 10px 36px var(--state-live-halo-hover);
}

.venue-head {
  display: flex; align-items: baseline;
  column-gap: 0.6rem; row-gap: 0.3rem;
  flex-wrap: wrap;
  margin-bottom: 0.35rem;
}
.venue-name {
  font-family: var(--font-body-simple);
  font-size: 1.18rem; font-weight: 700;
  color: var(--ink-bright); letter-spacing: -0.01em;
  line-height: 1.15; text-decoration: none;
}
.venue-name:hover { color: var(--neon); }
.venue-arrow {
  display: inline-block; color: var(--dimmer);
  transition: all 0.15s;
  font-size: 0.8em; margin-left: 0.2em;
}
.venue-name:hover .venue-arrow { color: var(--neon); transform: translateX(3px); }
.venue-meta {
  display: flex; align-items: center; gap: 0.5rem;
  font-size: 0.78rem; color: var(--muted);
  font-family: var(--font-mono); letter-spacing: 0.05em;
}
.venue-meta .sep { opacity: 0.5; }
.age-badge {
  font-family: var(--font-mono);
  font-size: 0.56rem; font-weight: 600;
  letter-spacing: 0.08em;
  padding: 0.08rem 0.35rem;
  border-radius: var(--radius-xs);
  border: 1px solid var(--border-strong);
  color: var(--muted);
  text-transform: uppercase;
}
.downtown-badge {
  font-family: var(--font-mono);
  font-size: 0.56rem; font-weight: 600;
  letter-spacing: 0.06em;
  padding: 0.08rem 0.4rem;
  border-radius: var(--radius-xs);
  border: 1px solid var(--accent-cyan-edge);
  background: var(--accent-cyan-wash);
  color: var(--accent-cyan);
  white-space: nowrap;
}

/* LIVE / UPCOMING pills — injected into .status-slot by JS */
.status-slot { display: inline-flex; }
.live-badge, .upcoming-badge {
  display: inline-flex; align-items: center; gap: 0.35rem;
  font-family: var(--font-mono);
  font-size: 0.58rem; font-weight: 600;
  letter-spacing: 0.1em; text-transform: uppercase;
  padding: 0.15rem 0.45rem;
  border-radius: var(--radius-sm);
  line-height: 1;
}
.live-badge {
  color: var(--green);
  border: 1px solid var(--state-live-badge-border);
  background: var(--state-live-badge-bg);
}
.live-badge::before {
  content: ''; width: 5px; height: 5px; border-radius: var(--radius-circle);
  background: var(--green);
  box-shadow: 0 0 6px var(--state-live-dot-glow);
  animation: pulse 1.6s ease-in-out infinite;
}
.upcoming-badge {
  color: var(--state-upcoming-ink);
  border: 1px solid var(--state-upcoming-border);
  background: var(--state-upcoming-bg);
  animation: up-blink 2.4s ease-in-out infinite;
}
.upcoming-badge::before {
  content: ''; width: 5px; height: 5px; border-radius: var(--radius-circle);
  background: var(--state-upcoming-dot);
  box-shadow: 0 0 8px var(--state-upcoming-dot-glow);
}
@keyframes up-blink {
  0%,100% { opacity: 1; }
  50%     { opacity: 0.55; }
}

/* Premier + live-now: a restrained light sweep across the LIVE pill.
   Cycle: 8s with a long dormant tail so it reads as periodic, not strobe. */
@keyframes premier-live-shimmer {
  0%        { transform: translateX(-150%) skewX(-18deg); }
  45%, 100% { transform: translateX(380%)  skewX(-18deg); }
}
/* Premier cards keep their gold frame even when live — the LIVE pill,
   green dot, and shimmer carry the live signal. Avoids a green/gold
   border clash on premier+live-now cards. */
.venue-card.premier.live-now,
.venue-card.premier.live-now:hover {
  border-color: var(--gold);
}
.venue-card.premier.live-now .live-badge {
  position: relative; overflow: hidden;
}
.venue-card.premier.live-now .live-badge::after {
  content: ''; position: absolute; inset: 0; left: 0;
  width: 35%;
  background: linear-gradient(90deg,
    var(--sheen-edge)    0%,
    var(--sheen-mid) 50%,
    var(--sheen-edge)    100%);
  animation: premier-live-shimmer 8s ease-in-out infinite;
  pointer-events: none;
}

/* BUSYNESS */
.busyness-strip {
  display: grid;
  grid-template-rows: auto auto;
  grid-template-columns: auto 1fr;
  grid-template-areas:
    "bars label"
    "ticks label";
  gap: 0 0.6rem;
  align-items: end;
  margin: 0.1rem 0 0.4rem;
  padding: 0.35rem 0.55rem 0.3rem;
  background: var(--surface-tint-strip);
  border: 1px solid var(--border-strip);
  border-radius: var(--radius-md);
}
.bs-bars { grid-area: bars; display: flex; align-items: flex-end; gap: 3px; height: 22px; }
.bs-bar {
  width: 7px;
  height: max(calc(var(--v, 0) * 22px), 3px);
  background: linear-gradient(to top, var(--busy-bar-from), var(--busy-bar-to));
  border-radius: var(--radius-bar-top);
  opacity: 0.55;
  position: relative;
}
.bs-bar.zero {
  opacity: 0.22;
  background: linear-gradient(to top, var(--busy-zero-from), var(--busy-zero-to));
}
.bs-bar.peak {
  background: linear-gradient(to top, var(--busy-peak-from), var(--busy-peak-to));
  opacity: 0.9;
}
.bs-bar.now {
  opacity: 1;
  background: linear-gradient(to top, var(--busy-now-from), var(--busy-now-to));
  box-shadow: 0 0 8px var(--busy-now-glow);
  outline: 1px solid var(--busy-now-outline);
}
.bs-bar.now::after {
  content: ''; position: absolute; top: -6px; left: 50%;
  transform: translateX(-50%);
  width: 4px; height: 4px; border-radius: var(--radius-circle);
  background: var(--busy-now-marker); box-shadow: 0 0 6px var(--busy-now-marker-glow);
  animation: bs-pulse 2s ease-in-out infinite;
}
.bs-bar.now.peak {
  background: linear-gradient(to top, var(--busy-now-peak-from), var(--busy-now-peak-to));
  box-shadow: 0 0 10px var(--busy-now-peak-glow);
}
@keyframes bs-pulse {
  0%,100% { opacity: 1; transform: translateX(-50%) scale(1); }
  50%     { opacity: 0.5; transform: translateX(-50%) scale(0.6); }
}
.bs-ticks { grid-area: ticks; display: flex; gap: 3px; margin-top: 3px; }
.bs-tick {
  width: 7px; text-align: center;
  font-family: var(--font-mono);
  font-size: 0.52rem; color: var(--dimmer);
  line-height: 1; white-space: nowrap; overflow: visible;
}
.bs-label { grid-area: label; align-self: center; font-size: 0.72rem; color: var(--text-dim); line-height: 1.3; }
.bs-label b { color: var(--text); font-weight: 600; }

/* EVENT ROWS */
.events { display: flex; flex-direction: column; gap: 0; }
.event {
  display: flex; align-items: flex-start; gap: 0.7rem;
  padding: 0.55rem 0;
  border-top: 1px solid var(--border-event-rule);
  transition: opacity 0.2s;
}
.event:first-child { border-top: none; padding-top: 0.4rem; }

/* Event states (toggled by JS for today's venues) */
.event.past {
  opacity: 0.38;
}
.event.past .event-type { filter: saturate(0.2); }
.event.live .event-title { color: var(--ink-bright); font-weight: 600; }
.event.live .event-type {
  box-shadow: -3px 0 0 var(--state-live-glow);
}
.event.live .event-time .time-text {
  color: var(--green);
  font-weight: 600;
}

.event-type {
  flex-shrink: 0;
  width: 54px;
  font-family: var(--font-mono);
  font-size: 0.58rem; font-weight: 600;
  letter-spacing: 0.1em; text-transform: uppercase;
  padding: 0.12rem 0 0.12rem 0.55rem;
  border-left: 2px solid;
  margin-top: 0.1rem;
}
.event-type.music { color: var(--music); border-color: var(--music); }
.event-type.drinks { color: var(--drinks); border-color: var(--drinks); }
.event-type.food { color: var(--food); border-color: var(--food); }
.event-type.sports { color: var(--sports); border-color: var(--sports); }
.event-type.events { color: var(--events); border-color: var(--events); }
.event-type.info { color: var(--info); border-color: var(--info); }

.event-body { flex: 1; min-width: 0; }
.event-title { font-size: 0.9rem; font-weight: 500; color: var(--text); line-height: 1.35; }
.event-time {
  flex-shrink: 0;
  font-family: var(--font-mono);
  font-size: 0.7rem; color: var(--muted);
  padding-top: 0.22rem;
  white-space: nowrap;
  text-align: right;
  display: flex; flex-direction: column; align-items: flex-end; gap: 0.1rem;
}
.ends-soon {
  font-size: 0.62rem;
  color: var(--amber);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  display: none;
}
.ends-soon.visible { display: inline; }

/* PILLS */
.venue-pills {
  display: flex; gap: 0.35rem; flex-wrap: wrap;
  align-items: center;
  margin-top: 0.5rem;
  padding-top: 0.4rem;
  border-top: 1px dashed var(--border-hairline);
}
/* Borderless since 2026-08-26 (the ring chrome read as redundant next to
   the icons): bare icon/text buttons that keep the 40px touch target via
   padding + min-height. border-radius stays so the hover/active background
   feedback is still a soft bubble, but at rest nothing is drawn. */
.pill {
  display: inline-flex; align-items: center; gap: 0.4rem;
  font-size: 0.82rem; font-weight: 500;
  padding: 0.55rem 0.95rem;
  min-height: 40px;
  border-radius: var(--radius-pill);
  border: none;
  background: transparent;
  color: var(--muted);
  text-decoration: none;
  transition: all 0.15s;
}
.pill:hover { color: var(--text); background: var(--card-hover); }
.pill.lineleap { color: var(--pill-lineleap-ink); }
.pill.lineleap:hover { color: var(--pill-lineleap-ink-hover); background: var(--pill-lineleap-bg-hover); }
.pill.ig { color: var(--pill-ig-ink); }
.pill.ig:hover { color: var(--pill-ig-ink-hover); background: var(--pill-ig-bg-hover); }
/* Icon-only links (labels dropped 2026-07-27 to keep the row on one
   line). IG is exactly the header icon: same path, same shared #ig-grad
   gradient (fill lives on the path attribute; no fill here or it would
   override the gradient). LineLeap is its double-L monogram in the pill's
   blue accent. */
.pill.ig, .pill.lineleap { padding: 0.55rem 0.75rem; }
.pill.ig svg, .pill.lineleap svg { width: 1.3rem; height: 1.3rem; }
.pill.lineleap svg { fill: currentColor; }

/* SPONSORED — Premier
   Framed editorial treatment: 3px gold left spine + 1px gold hairline on
   the remaining three sides + a 1px inner hairline (inset shadow) for a
   double-frame "luxury catalog plate" look. Elevation shadow is DARK
   (depth), not gold (no haze). Reads as "this slot was paid for" without
   the Vegas marquee. */
.venue-card.premier {
  position: relative;
  background: var(--card);
  border: 1px solid var(--gold);
  border-left: 3px solid var(--gold);
  box-shadow:
    inset 0 0 0 1px var(--premier-inset),   /* inner hairline frame */
    inset 0 1px 0 var(--premier-edge),     /* top bevel highlight */
    0 0 0 1px var(--premier-ring),         /* tight halo hugging the edge */
    0 0 12px -2px var(--premier-edge),     /* short controlled bloom */
    0 6px 22px -10px var(--shadow-premier);        /* lift */
  padding-top: 1rem;
  transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
}
.venue-card.premier:hover {
  transform: translateY(-2px);
  box-shadow:
    inset 0 0 0 1px var(--premier-edge),
    inset 0 1px 0 var(--premier-inset-hover),
    0 0 0 2px var(--premier-ring-hover),
    0 0 24px 0 var(--premier-glow-hover),
    0 12px 32px -8px var(--shadow-premier-hover);
}
.venue-card.featured {
  border: 1px solid var(--featured-border);
  border-left: 3px solid var(--accent2);
  background: linear-gradient(135deg, var(--featured-wash) 0%, var(--card) 55%);
}
.badge {
  position: absolute; top: -0.55rem; left: 1rem;
  font-family: var(--font-mono);
  font-size: 0.58rem; font-weight: 600;
  letter-spacing: 0.12em; text-transform: uppercase;
  border-radius: var(--radius-sm);
  padding: 0.12rem 0.45rem;
  background: var(--card); z-index: 1;
}
.badge.premier {
  color: var(--premier-badge-ink);
  border: 1px solid var(--premier-badge-border);
  letter-spacing: 0.18em;
  padding: 0.14rem 0.55rem;
}
.badge.featured { color: var(--accent2); border: 1px solid var(--featured-badge-border); }

/* SPONSOR LOGO — top-right of premier card header.
   Reserves padding on the venue-head so the venue name never runs under
   the logo on narrow widths. Per-sponsor data-URI rules are generated at
   compile time from assets/sponsors/<venue-id>.png and injected below. */
.sponsor-logo {
  position: absolute;
  top: 0.7rem;
  right: 0.9rem;
  height: 32px;
  width: 80px;
  background-repeat: no-repeat;
  background-position: right center;
  background-size: contain;
  opacity: 0.92;
  pointer-events: none;
  z-index: 1;
}
.venue-card.premier:has(> .sponsor-logo) .venue-head {
  padding-right: 92px;
}
@media (max-width: 480px) {
  .sponsor-logo { height: 26px; width: 64px; top: 0.6rem; right: 0.8rem; }
  .venue-card.premier:has(> .sponsor-logo) .venue-head { padding-right: 72px; }
}


/* NO RESULTS */
.no-results {
  display: none;
  text-align: center;
  padding: 2rem 1rem;
  color: var(--muted);
  font-size: 0.88rem;
  border: 1px dashed var(--border);
  border-radius: var(--radius-xl);
  margin-top: 1rem;
}
.no-results.visible { display: block; }

/* QUIET TONIGHT */
.quiet-tonight {
  margin-top: 1.3rem;
  background: var(--surface-tint-quiet);
  border: 1px dashed var(--border-dashed-quiet);
  border-radius: var(--radius-2xl);
  overflow: hidden;
}
.quiet-tonight > summary {
  list-style: none;
  cursor: pointer;
  padding: 0.8rem 1rem;
  display: flex; align-items: center; gap: 0.6rem;
  font-size: 0.82rem; color: var(--text-dim);
  transition: background 0.15s;
}
.quiet-tonight > summary::-webkit-details-marker { display: none; }
.quiet-tonight > summary:hover { background: var(--surface-tint-faint); }
.qt-chev {
  display: inline-block;
  color: var(--dimmer);
  font-size: 0.7rem;
  transition: transform 0.2s;
}
.quiet-tonight[open] > summary .qt-chev { transform: rotate(90deg); }
.qt-head {
  display: inline-flex; align-items: center; gap: 0.35rem;
  font-family: var(--font-display);
  font-size: 1.1rem; letter-spacing: 0.05em;
  color: var(--text);
}
.qt-head .moon { font-size: 0.9rem; }
.qt-sub {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 0.66rem; color: var(--muted);
  letter-spacing: 0.05em;
}
.qt-grid {
  display: flex; flex-direction: column; gap: 0.3rem;
  padding: 0 0.7rem 0.7rem;
}
.qt-venue {
  display: block;
  padding: 0.5rem 0.7rem;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  font-size: 0.82rem;
}
.qt-venue.has-week {
  padding: 0;
}
.qt-venue.has-week > summary {
  list-style: none; cursor: pointer;
  padding: 0.5rem 0.7rem;
}
.qt-row {
  display: flex; align-items: center; gap: 0.6rem;
}
.qt-venue .busyness-strip {
  margin: 0.4rem 0 0;
  padding: 0.25rem 0.45rem;
}
.qt-venue .bs-bars { height: 16px; }
.qt-venue .bs-bar {
  width: 6px;
  height: max(calc(var(--v, 0) * 16px), 2px);
}
.qt-venue .bs-tick { width: 6px; }
.qt-venue.has-week > summary::-webkit-details-marker { display: none; }
.qt-venue-name { color: var(--text); font-weight: 500; }
.qt-venue.empty .qt-venue-name a { color: var(--text-dim); text-decoration: none; }
.qt-venue.empty .qt-venue-name a:hover { color: var(--text); }
.qt-count {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  color: var(--muted);
}
.qt-venue.empty .qt-count { color: var(--dimmer); }
.qt-caret {
  color: var(--dimmer); font-size: 0.7rem;
  transition: transform 0.2s;
}
.qt-venue[open] .qt-caret { transform: rotate(180deg); }
/* Near me (2026-10-02): today's quiet tiles join the distance list as slim
   cards and the Quiet Tonight block hides (app.js mergeQuiet). In the list
   they space like cards, say why they are quiet on a second line, show the
   distance, and follow the filters: each filter tags every tile, but only a
   tile in the list is hidden by it. */
details.quiet-tonight.nn-merged { display: none; }
.qt-venue.in-list { margin-bottom: 0.75rem; border-radius: var(--radius-2xl); }
.qt-venue.in-list .qt-row { flex-wrap: wrap; row-gap: 0.1rem; }
.qt-venue.in-list .qt-count { order: 5; flex-basis: 100%; margin-left: 0; }
.qt-venue.in-list .qt-count::before { content: 'Nothing listed tonight \00b7  '; }
.qt-venue.in-list .qt-caret { margin-left: auto; }
.qt-dist {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  color: var(--muted);
}
.qt-venue.in-list.cat-hidden,
.qt-venue.in-list.hood-hidden,
.qt-venue.in-list.saved-hidden { display: none !important; }
.qt-venue-body {
  padding: 0.25rem 0.7rem 0.7rem;
  border-top: 1px dashed var(--border-hairline);
}
.qt-links {
  display: flex; gap: 0.6rem;
  font-size: 0.72rem;
  padding: 0.35rem 0 0.5rem;
}
.qt-site { color: var(--accent2); text-decoration: none; }
.qt-site:hover { color: var(--neon); }
.qt-map { color: var(--dimmer); text-decoration: none; }
.qt-map:hover { color: var(--text); }
.qt-week {
  display: flex; flex-direction: column;
  gap: 0.15rem;
  font-size: 0.78rem;
}
.qt-event {
  display: grid;
  grid-template-columns: 48px auto 1fr auto;
  align-items: center;
  gap: 0.5rem;
  padding: 0.25rem 0;
  border-top: 1px solid var(--border-hairline-faint);
}
.qt-event:first-child { border-top: none; }
.qt-date {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.qt-dot { display: inline-flex; align-items: center; }
.qt-title { color: var(--text-dim); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.qt-time {
  font-family: var(--font-mono);
  font-size: 0.68rem; color: var(--muted);
  white-space: nowrap;
}

/* Venue week preview: "N ▾" toggle at the bottom right of each venue
   card, expanding to the venue's other events this week using the .qt-*
   row styles above. Toggle AND expansion work on every surface
   (2026-08-26; before that the toggle showed everywhere but expansion
   was app-exclusive, with an it's-an-app-feature teaser toast on web).
   display:contents so the toggle button and the panel participate in the
   pills flex row as independent items: the pill-shaped button holds its
   spot in the row while the hidden panel, when revealed, wraps to its
   own full-width line (flex-basis: 100%) below. This is why the toggle
   is a <button> + sibling div rather than <details>/<summary> — a
   details element cannot keep its summary on one flex line while its
   content spans another. */
.venue-week { display: contents; }
/* Toggle matches .pill geometry (40px touch target, borderless like the
   pills since 2026-08-26) and keeps the muted week-preview palette so it
   stays secondary to the branded link icons. The .share-btn before it owns
   the pills row's right-alignment (margin-left:auto below is a fallback
   for a row with no share button). */
.vw-toggle {
  display: flex; align-items: center;
  width: fit-content; margin-left: auto;
  gap: 0.3rem; cursor: pointer;
  padding: 0.55rem 0.95rem; min-height: 40px;
  border-radius: var(--radius-pill);
  border: none;
  background: none; font: inherit; font-size: 0.8rem;
  margin-top: 0.35rem;
  transition: all 0.15s;
}
.venue-pills .vw-toggle { margin-top: 0; }
.vw-toggle:active { background: var(--card-hover); }
.vw-count {
  font-family: var(--font-mono);
  font-size: 0.85rem; color: var(--muted);
}
.vw-toggle[aria-expanded="true"] .vw-count { color: var(--text-dim); }
.vw-caret {
  color: var(--dimmer); font-size: 0.78rem;
  transition: transform 0.2s;
}
.vw-toggle[aria-expanded="true"] .vw-caret { transform: rotate(180deg); }
.vw-panel { flex-basis: 100%; }
.venue-week .qt-week { margin-top: 0.25rem; }

/* Venue share button, ALL surfaces (web + app, 2026-07-27; app-only at
   birth): right cluster of the pills row, after the link pills and before
   the week toggle so the toggle's full-width .vw-panel stays the row's
   last flex item. app.js owns the click: native share sheet when
   available, else copy to clipboard + toast. Cyan accent matches the app
   fabs: an action, not an outbound link pill. */
.share-btn {
  display: inline-flex;
  margin-left: auto;
  padding: 0.55rem 0.75rem;  /* icon-only */
  cursor: pointer;
  font: inherit;
  color: var(--accent-cyan);
}
.share-btn:hover, .share-btn:active { background: var(--card-hover); color: var(--accent-cyan-bright); }
.share-btn svg { width: 1.3rem; height: 1.3rem; fill: currentColor; }
/* Share owns the row's right-alignment; a week toggle after it sits flush. */
.share-btn ~ .venue-week .vw-toggle { margin-left: 0; }
/* In-view nudge (2026-09-02, idle trigger since 2026-09-03): 5 s after
   scrolling stops, and every 5 s while the page stays still, app.js adds
   .nudge to one visible card's share button (icon wiggles) and, 0.3 s
   later, to its collapsed week toggle (caret bobs twice), taking turns
   through the visible cards. The class comes off on animationend.
   Picked from a five-and-five mockup (S3, W1). */
.share-btn.nudge svg { animation: nudge-wiggle 0.6s ease-in-out; }
@keyframes nudge-wiggle {
  0%, 100% { transform: rotate(0); }
  25%  { transform: rotate(-12deg); }
  50%  { transform: rotate(10deg); }
  75%  { transform: rotate(-6deg); }
}
.vw-toggle.nudge .vw-caret { animation: nudge-bob 0.8s ease-in-out; }
@keyframes nudge-bob {
  0%, 50%, 100% { transform: translateY(0); }
  25%, 75% { transform: translateY(4px); }
}

.no-events {
  text-align: center; color: var(--muted);
  padding: 3rem 1rem; font-size: 0.92rem;
}

/* PRICING */
.pricing-section {
  position: relative; z-index: 5;
  max-width: 820px; margin: 2.5rem auto 0;
  padding: 2rem 1.25rem 2.5rem;
  border-top: 1px solid var(--border);
}
.pricing-eyebrow {
  text-align: center;
  font-family: var(--font-mono);
  font-size: 0.62rem; letter-spacing: 0.2em;
  color: var(--dimmer); text-transform: uppercase;
  margin-bottom: 0.4rem;
}
.pricing-header h2 {
  text-align: center;
  font-family: var(--font-display);
  font-size: 1.6rem; letter-spacing: 0.04em;
  color: var(--text); font-weight: 400;
  margin-bottom: 0.4rem;
}
.pricing-header p {
  text-align: center;
  font-size: 0.82rem; color: var(--muted);
  max-width: 480px; margin: 0 auto 1.5rem;
}
.pricing-grid {
  display: grid; grid-template-columns: repeat(2,1fr); gap: 0.7rem;
  max-width: 520px; margin: 0 auto 1.2rem;
}
.pricing-card {
  background: var(--card); border: 1px solid var(--border);
  border-radius: var(--radius-xl); padding: 1.1rem 1rem;
  text-align: center;
}
.pricing-card.premier-pricing { border-color: var(--pricing-premier-border); }
.pricing-card.featured-pricing { border-color: var(--pricing-featured-border); }
.pricing-tier {
  font-family: var(--font-mono);
  font-size: 0.6rem; letter-spacing: 0.16em;
  text-transform: uppercase; margin-bottom: 0.5rem;
}
.pricing-card.premier-pricing .pricing-tier { color: var(--gold); }
.pricing-card.featured-pricing .pricing-tier { color: var(--accent2); }
.pricing-price { font-family: var(--font-display); font-size: 1.9rem; color: var(--ink-bright); line-height: 1; }
.pricing-period { font-family: var(--font-mono); font-size: 0.58rem; color: var(--dimmer); letter-spacing: 0.1em; margin-bottom: 0.7rem; }
.pricing-features { list-style: none; font-size: 0.74rem; color: var(--muted); text-align: left; line-height: 1.75; }
.pricing-features li::before { content: '→ '; color: var(--dimmer); }
.pricing-slots {
  margin-top: 0.75rem;
  font-family: var(--font-mono);
  font-size: 0.6rem;
  letter-spacing: 0.1em;
  padding: 0.3rem 0.6rem;
  border-radius: var(--radius-pill);
  display: inline-block;
}
.pricing-card.premier-pricing .pricing-slots {
  color: var(--gold);
  border: 1px solid var(--pricing-premier-slot-border);
  background: var(--pricing-premier-slot-bg);
}
.pricing-card.featured-pricing .pricing-slots {
  color: var(--accent2);
  border: 1px solid var(--pricing-featured-slot-border);
  background: var(--featured-wash);
}
.pricing-cta-wrap { text-align: center; }
.pricing-cta {
  display: inline-block;
  padding: 0.55rem 1.6rem;
  border-radius: var(--radius-pill);
  font-size: 0.82rem; font-weight: 600;
  text-decoration: none; color: var(--ink-bright);
  background: var(--accent); border: 1px solid var(--pricing-cta-border);
  transition: all 0.15s;
}
.pricing-cta:hover { background: var(--accent2); transform: translateY(-1px); }
.pricing-note { text-align: center; font-size: 0.72rem; color: var(--dimmer); margin-top: 0.8rem; line-height: 1.5; }

footer {
  text-align: center; color: var(--dimmer);
  font-size: 0.72rem;
  padding: 1.5rem 1rem calc(2.5rem + var(--fab-bar-h) + env(safe-area-inset-bottom));
  font-family: var(--font-mono); letter-spacing: 0.08em;
}
footer a { color: var(--muted); text-decoration: none; }
footer a:hover { color: var(--text); }

.staging-banner {
  /* bottom-left so it never overlaps the bottom-right .settings-fab in-app */
  position: fixed; bottom: 1rem; left: 1rem;
  background: var(--staging-bg);
  border: 1px solid var(--staging-border);
  color: var(--staging-ink);
  font-family: var(--font-mono);
  font-size: 0.68rem; letter-spacing: 0.14em;
  padding: 0.45rem 0.8rem; border-radius: var(--radius-pill);
  text-transform: uppercase;
  z-index: 100;
  backdrop-filter: blur(6px);
}

@media (max-width: 640px) {
  .header-inner { padding: 0.5rem 1rem; }
  .wordmark { font-size: 1.55rem; }
  .tagline { display: none; }
  .logo-seo { font-size: 0.62rem; max-width: 230px; }
  .header-store-badge img { height: 26px; }
  .day-label { font-size: 1.9rem; }
  .event-type { width: 48px; font-size: 0.54rem; }
  .event-title { font-size: 0.86rem; }
  .event-time { font-size: 0.66rem; }
  .qt-sub { display: none; }

  /* Day header: redundant weather box hidden on mobile (already in top bar) */
  .day-weather { display: none; }

  /* Unified left/right rail = 16px across header, day-nav, filters, main, announce strip */
  main { padding: 0.6rem 1rem 3rem; }
  .day-nav { padding: 0.4rem 1rem; scroll-padding-inline-start: 1rem; }
  .announce-lead { padding: 0 0.5rem 0 1rem; }

  /* Filter pills: fit all 5 on one row at 360w, distributed across width */
  .filters { gap: 0.25rem; padding: 0.4rem 1rem 0.2rem; justify-content: space-between; }
  .filter-pill {
    min-height: 36px;
    padding: 0.35rem 0.55rem;
    font-size: 0.7rem;
    gap: 0.3rem;
  }

  /* Venue-card action pills — slightly smaller but still tappable */
  .pill {
    min-height: 36px;
    padding: 0.4rem 0.7rem;
    font-size: 0.74rem;
  }
  /* Week-preview toggle and icon-only buttons (IG, share) shrink with the
     pills so a LineLeap + IG + Map + share + count row stays on one line
     on phone widths. Extra specificity so these beat the base .pill.ig /
     app-gated .share-btn padding. */
  .vw-toggle {
    min-height: 36px;
    padding: 0.4rem 0.7rem;
  }
  .vw-toggle .vw-count { font-size: 0.7rem; }
  .pill.pill.ig, .pill.pill.lineleap, .pill.pill.share-btn { padding: 0.4rem 0.55rem; }
}
/* Narrow phones (360dp Galaxys etc.): the full 5-item row (LineLeap + IG +
   Map + share + week toggle) needs a few more px than the ≤640 sizes give.
   Same touch-target min-heights, tighter chrome. */
@media (max-width: 400px) {
  .venue-pills { gap: 0.25rem; }
  .pill { padding: 0.35rem 0.5rem; font-size: 0.7rem; }
  .pill.pill.ig, .pill.pill.lineleap, .pill.pill.share-btn { padding: 0.35rem 0.45rem; }
  .vw-toggle { padding: 0.35rem 0.5rem; }
}
@keyframes iosArrowBounce {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(10px); }
}

/* PRESENTER SPONSOR — day-header "Brought to you by" + mid-list strip.
   Only emitted when an active row exists in sponsorships.md ## Presenter Sponsors. */
.day-header.has-presenter {
  /* No !important: .day-header.has-presenter already out-ranks
     .day-header's own `display: flex`, and an !important here beat the
     grid-on hide rules below, which left the headline on screen in Grid
     view whenever a presenter sponsorship was active (2026-09-12). */
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  column-gap: 1rem;
  margin: 0.2rem 0 calc(0.5rem + 4px);
}
.day-presenter {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  line-height: 1.15;
  min-width: 0;
}
.dp-eyebrow {
  font-family: var(--font-mono);
  font-size: 0.5rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--dimmer);
  margin-bottom: 2px;
}
.dp-name {
  font-family: var(--font-serif);
  font-style: italic;
  font-weight: 400;
  font-size: 1.2rem;
  color: var(--dp-name-ink);
  text-decoration: none;
  letter-spacing: 0.005em;
  transition: color 0.15s;
}
.dp-name:hover { color: var(--whiskey-ink); text-decoration: underline; }
@media (max-width: 560px) {
  .day-header.has-presenter { column-gap: 0.5rem; grid-template-columns: auto 1fr; }
  .day-header.has-presenter .day-weather { display: none; }
  .dp-eyebrow { font-size: 0.4rem; letter-spacing: 0.14em; }
  .dp-name { font-size: 0.95rem; line-height: 1.15; }
}
@media (max-width: 380px) {
  .dp-eyebrow { font-size: 0.36rem; letter-spacing: 0.12em; }
  .dp-name { font-size: 0.85rem; }
}

.whiskey-sponsor {
  position: relative;
  display: block;
  margin: 0.4rem 0 1.4rem;
  padding: 0.75rem 1rem 0.85rem;
  border-radius: var(--radius-2xl);
  background: var(--whiskey-surface);
  border: 1px solid var(--premier-edge);
  box-shadow:
    inset 0 1px 0 var(--whiskey-inset),
    0 6px 22px var(--shadow-card);
  text-decoration: none;
  color: var(--text);
  overflow: visible;
  transition: border-color 0.25s, box-shadow 0.25s, transform 0.25s;
}
.whiskey-sponsor::before {
  content: "";
  position: absolute; top: 0; left: 12px; right: 12px;
  height: 1px;
  background: linear-gradient(90deg,
    transparent 0%, var(--premier-sheen-fade) 4%,
    var(--premier-badge-border) 30%, var(--whiskey-gold-bright) 50%,
    var(--premier-badge-border) 70%, var(--premier-sheen-fade) 96%, transparent 100%);
  opacity: 0.9;
}
.whiskey-sponsor:hover {
  border-color: var(--whiskey-border-hover);
  box-shadow:
    inset 0 1px 0 var(--whiskey-inset-hover),
    0 10px 36px var(--premier-ring),
    0 6px 22px var(--shadow-floating);
  transform: translateY(-1px);
}
.whiskey-tag-corner {
  position: absolute;
  bottom: -7px;
  left: 50%;
  transform: translateX(-50%);
  font-family: var(--font-mono);
  background: var(--bg);
  padding: 2px 10px;
  letter-spacing: 0.28em;
  font-size: 0.44rem;
  text-transform: uppercase;
  border: 1px solid var(--whiskey-tag-corner-border);
  border-radius: var(--radius-xs);
  color: var(--whiskey-tag-corner-ink);
  pointer-events: none;
  z-index: 3;
}
.whiskey-row { display: flex; align-items: center; gap: 0.95rem; }
.whiskey-logo {
  flex: 0 0 auto;
  height: 46px;
  width: auto;
  filter: drop-shadow(0 1px 6px var(--premier-ring));
}
.whiskey-text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 0.1rem; }
.whiskey-tag {
  font-family: var(--font-serif-alt);
  font-style: italic;
  font-weight: 400;
  font-size: 1.18rem;
  line-height: 1.1;
  letter-spacing: -0.005em;
  color: var(--whiskey-ink);
  text-shadow: 0 1px 0 var(--shadow-text-emboss);
}
.whiskey-sub {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--whiskey-sub-ink);
}
.whiskey-cta {
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-size: 0.66rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--whiskey-ink);
  padding: 0.42rem 0.7rem;
  border: 1px solid var(--whiskey-cta-border);
  border-radius: var(--radius-pill-full);
  white-space: nowrap;
  transition: background 0.2s, border-color 0.2s, color 0.2s;
}
.whiskey-sponsor:hover .whiskey-cta {
  background: var(--whiskey-cta-bg-hover);
  border-color: var(--whiskey-gold-bright);
  color: var(--whiskey-ink-hover);
}
@media (max-width: 480px) {
  .whiskey-row { gap: 0.7rem; }
  .whiskey-logo { height: 36px; }
  .whiskey-tag { font-size: 1.02rem; }
  .whiskey-sub { font-size: 0.56rem; letter-spacing: 0.14em; }
  .whiskey-cta { font-size: 0.58rem; padding: 0.36rem 0.6rem; }
}
/* end presenter sponsor */

.app-only { display: none; }
html[data-nn-app="true"] .app-only { display: inline; }
@media (display-mode: standalone) { .app-only { display: inline; } }

/* Inverse of .app-only: visible in ordinary browser tabs, hidden inside the
   native app and installed PWAs (where advertising the app is pointless).
   !important so the gate wins over any element-specific display rule. */
html[data-nn-app="true"] .browser-only { display: none !important; }
@media (display-mode: standalone) { .browser-only { display: none !important; } }

/* PWA sunset gate blackout: the gate attribute is stamped synchronously in
   the <head>, so this hides ALL page content (venue data included) from
   first paint on a gated launch — a retired PWA install must see nothing
   but the store gate, with no flash before the overlay builds on
   DOMContentLoaded and no content exposed if that script ever fails. */
html[data-nn-pwa-gate="true"] body > :not(#nn-pwa-gate) { display: none !important; }

/* Footer store badges (browser-only): the official "Download on the App
   Store" and "Get it on Google Play" badge art (data-URI SVGs in the
   template; sources in tools/badges/). Same aspect ratio, so equal heights
   render as a uniform pair; the row wraps on narrow phones. */
.play-badge-row {
  margin: 14px 0 0;
  display: flex; justify-content: center; align-items: flex-start;
  gap: 10px 16px; flex-wrap: wrap;
}
.play-badge-row a { display: block; line-height: 0; }
.play-badge-row img { height: 44px; width: auto; display: block; }
/* Each badge with its QR code under it (2026-10-02): a column per store.
   The QR sits on a white tile so it scans in dark mode; the svg carries a
   one-module quiet zone and the tile's padding adds to it. */
.store-col { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.store-qr, .nn-sheet-qr {
  display: block; box-sizing: border-box; width: 112px; padding: 6px;
  background: var(--qr-paper); border-radius: var(--radius-md); line-height: 0;
}
.store-qr svg, .nn-sheet-qr svg { width: 100%; height: auto; display: block; }

/* Persistent bottom bar (since 2026-09-07, replacing three floating
   circles): fixed, full width, the buttons laid out in a row and spread
   evenly. On the website only .near-fab is ever display:flex (the other
   two stay app-only-gated), so a single child is centered automatically by
   justify-content: space-evenly; in the app all three show, spread evenly.
   Same z-index layer the floating buttons used.
   The background spans the viewport like the header and day-nav bars, but
   the buttons stay inside the same 820px column their content uses: the
   side padding eats everything past the column, so on a phone it is 0 and
   on a desktop the buttons never float outside the listing (2026-09-10). */
.fab-bar {
  position: fixed; left: 0; right: 0; bottom: 0;
  z-index: 90;
  display: flex;
  justify-content: space-evenly;
  align-items: stretch;
  padding: 0.35rem max(0px, calc((100% - 820px) / 2)) calc(0.35rem + env(safe-area-inset-bottom));
  background: var(--scrim-day-nav);
  backdrop-filter: blur(20px);
  border-top: 1px solid var(--border);
}
/* Shared by every bar button: a vertical icon-over-label column, hidden
   by default (each turns on with its own gating selector below, unchanged
   from the old floating-button rules). */
.fab-bar > .settings-fab, .fab-bar > .find-fab, .fab-bar > .near-fab, .fab-bar > .saved-fab, .fab-bar > .grid-fab, .fab-bar > .map-fab {
  display: none;
  position: relative;
  flex: 1 1 0;
  max-width: 9rem;
  min-height: 48px;
  flex-direction: column;
  align-items: center; justify-content: center;
  gap: 0.2rem;
  background: none; border: 0; padding: 0;
  color: var(--accent-cyan);
  text-decoration: none;
  cursor: pointer;
  transition: color 0.15s, opacity 0.15s;
}
.fab-bar > .settings-fab:hover, .fab-bar > .settings-fab:active,
.fab-bar > .find-fab:hover, .fab-bar > .find-fab:active,
.fab-bar > .saved-fab:hover, .fab-bar > .saved-fab:active,
.fab-bar > .near-fab:hover, .fab-bar > .near-fab:active,
.fab-bar > .grid-fab:hover, .fab-bar > .grid-fab:active,
.fab-bar > .map-fab:hover, .fab-bar > .map-fab:active {
  opacity: 0.75;
}
.fab-label {
  font-family: var(--font-mono);
  font-size: 0.58rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

/* App-only Settings button. Hidden in normal browser tabs (where
   push/settings aren't relevant), shown in the installed app / standalone PWA:
   mirrors the .app-only gating above but needs display:flex for centering. */
.settings-fab svg { width: 1.4rem; height: 1.4rem; fill: currentColor; }
html[data-nn-app="true"] .settings-fab { display: flex; }
@media (display-mode: standalone) { .settings-fab { display: flex; } }

/* Transient toast (share button's clipboard fallback; week-preview app
   teaser). Bottom-center, above the fab z-index. Non-interactive by
   default; .tappable (set by app.js when the content carries a link or
   button) re-enables pointer events while the toast shows, so a faded
   toast never covers what is under it. */
.nn-toast {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(var(--fab-bar-h) + env(safe-area-inset-bottom));
  background: var(--card);
  color: var(--text);
  border: 1px solid var(--accent-cyan-soft);
  border-radius: var(--radius-pill);
  padding: 0.6rem 1.1rem;
  width: 90%;
  text-align: center;
  font-size: 0.85rem;
  box-shadow: 0 4px 16px var(--shadow-floating);
  z-index: 95;
  opacity: 0;
  transition: opacity 0.25s;
  pointer-events: none;
}
.nn-toast.show { opacity: 1; }
.nn-toast.tappable.show { pointer-events: auto; }
.nn-toast a { color: var(--accent-cyan); font-weight: 600; }

/* Bottom sheets: dimmed backdrop + slide-up card, built by app.js. Two use
   it: the dwell app sheet (mobile browsers only) and the missing-venue ask
   sheet (2026-09-21, cities with features.missing_venue_ask; no icon, same
   box). Only one shows per visit. Sits above the toast (z 95) but below
   nothing else interactive. */
.nn-sheet-backdrop {
  position: fixed; inset: 0;
  background: var(--scrim-sheet-backdrop);
  opacity: 0; transition: opacity 0.3s ease;
  z-index: 96;
}
.nn-sheet-backdrop.show { opacity: 1; }
.nn-sheet {
  /* bottom: clears the persistent fab bar (2026-09-07) instead of the
     viewport edge directly; the bar's own padding already accounts for the
     safe area, so this sheet's own padding no longer needs to add it too. */
  position: fixed; left: 0; right: 0;
  bottom: calc(var(--fab-bar-h) + env(safe-area-inset-bottom));
  max-width: 480px; margin: 0 auto;
  background: var(--card);
  border: 1px solid var(--border-strong); border-bottom: none;
  border-radius: var(--radius-sheet-top);
  padding: 1.4rem;
  text-align: center;
  transform: translateY(105%); transition: transform 0.3s ease;
  z-index: 97;
  box-shadow: 0 -8px 32px var(--shadow-sheet);
}
.nn-sheet.show { transform: translateY(0); }
.nn-sheet-close {
  position: absolute; top: 0.4rem; right: 0.6rem;
  background: none; border: none; color: var(--dimmer);
  font-size: 1.7rem; line-height: 1; padding: 0.3rem 0.5rem; cursor: pointer;
}
.nn-sheet-icon { border-radius: var(--radius-xl); margin-bottom: 0.6rem; }
.nn-sheet-title { font-size: 1.05rem; font-weight: 700; color: var(--text); text-align: center; }
.nn-sheet-copy { font-size: 0.85rem; color: var(--text-dim); margin: 0.35rem 0 1rem; text-align: center; }
.nn-sheet-cta {
  display: inline-block; background: var(--accent-cyan); color: var(--ink-on-highlight);
  font-weight: 700; font-size: 0.9rem;
  padding: 0.75rem 1.6rem; border-radius: var(--radius-pill-full); text-decoration: none;
}
button.nn-sheet-cta { border: 0; width: 100%; cursor: pointer; font: inherit; font-weight: 700; }
/* Two-button row (game-day share sheet): equal widths, stacks if a label wraps. */
.nn-sheet-row { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.nn-sheet-row .nn-sheet-cta { flex: 1 1 0; min-width: 0; width: auto; text-align: center; white-space: nowrap; padding-left: 0.8rem; padding-right: 0.8rem; }
.nn-sheet-cta.nn-sheet-alt { background: transparent; color: var(--accent-cyan); box-shadow: inset 0 0 0 2px var(--accent-cyan); }
/* Store-badge CTA (dwell app sheet, 2026-09-22): the official Apple and
   Google lockups replace the themed pill, so every pill style is stripped
   back off and the art is centred at its own aspect ratio. Both badges carry
   their own clear space, so no extra padding. 48px keeps the App Store badge
   above Apple's 40px minimum height on a 412px-wide phone. */
.nn-sheet-cta.nn-sheet-badge {
  background: none; padding: 0; border-radius: 0; line-height: 0;
  box-shadow: none;
}
.nn-sheet-cta.nn-sheet-badge img { height: 48px; width: auto; display: block; }
/* Share sheet (2026-10-02): the store codes side by side, each under its
   badge art. Each badge and code is one link to its store (2026-10-06). */
.nn-share-codes { display: flex; justify-content: center; gap: 1rem; flex-wrap: wrap; }
.nn-share-code { display: flex; flex-direction: column; align-items: center; gap: 0.6rem;
  color: inherit; text-decoration: none; }
.nn-share-code img { height: 40px; width: auto; display: block; }
.nn-share-code .nn-sheet-qr { margin: 0; }
/* The badge's QR code under it (2026-10-02), cloned from the footer, for
   showing a friend. */
.nn-sheet-qr { margin: 0.9rem auto 0; width: 128px; }
a.nn-sheet-qr { display: block; }  /* the app sheet's code is a store link (2026-10-06) */
.nn-sheet-qr-note { font-size: 0.75rem; color: var(--text-dim); margin-top: 0.4rem; text-align: center; }
@media (prefers-reduced-motion: reduce) {
  .nn-sheet-backdrop, .nn-sheet { transition: none; }
}

/* Find-in-page button in the bar (app, standalone, and cities that opt the
   website in via features.find_on_web). Gated on data-nn-find, which
   app.js sets only when CSS.highlights is available, in addition to the
   same app/standalone gating as the settings button. */
.find-fab svg { width: 1.4rem; height: 1.4rem; fill: currentColor; }
/* Idle fidget (2026-09-02): 10 s after load, then every 13 s (30 s at first), while the tab is visible and the
   find bar is closed, app.js adds .fidget to each circular button for one
   cycle (staggered 0.7 s apart) and removes it on animationend. The gear
   pulls back, whips round once, overshoots and settles; the magnifier wags
   its handle around the lens. Picked from a five-and-five mockup (G3 and
   M4). Not gated on prefers-reduced-motion, same reasoning as the ticker
   note near the top of this file; the JS already skips hidden tabs. */
.settings-fab.fidget svg { animation: fab-windup 1.3s; }
@keyframes fab-windup {
  0%   { transform: rotate(0); }
  20%  { transform: rotate(-40deg); }
  70%  { transform: rotate(380deg); }
  85%  { transform: rotate(350deg); }
  100% { transform: rotate(360deg); }
}
.find-fab.fidget svg { transform-origin: 45% 45%; animation: fab-wave 1.2s ease-in-out; }
@keyframes fab-wave {
  0%, 100% { transform: rotate(0); }
  20%  { transform: rotate(-18deg); }
  40%  { transform: rotate(16deg); }
  60%  { transform: rotate(-10deg); }
  80%  { transform: rotate(6deg); }
}
html[data-nn-app="true"][data-nn-find="true"] .find-fab { display: flex; }
/* Cities with features.find_on_web (nittany since 2026-09-07) show it on the
   website as well; the template stamps data-nn-find-web before first paint. */
html[data-nn-find-web="true"][data-nn-find="true"] .find-fab { display: flex; }
@media (display-mode: standalone) { html[data-nn-find="true"] .find-fab { display: flex; } }

/* Website-only nearest-first sort button (near_me arrow) in the bar. Gated
   on data-nn-near, which app.js sets only on the web when geolocation
   exists and permission is not denied. .active = distance order is on
   (accent color on both icon and label). */
/* Off: the arrow is an outline. On (.active): filled, in the accent. */
.near-fab svg { width: 1.4rem; height: 1.4rem; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linejoin: round; }
/* On: the arrow is filled in the Live Now green (--green, the same token the
   live-now card border uses) with a white outline (--ink-bright). Off keeps
   the plain outline above. */
.near-fab.active svg { fill: var(--green); stroke: var(--ink-bright); stroke-width: 1.4; }
.near-ico { position: relative; display: inline-flex; }
/* The arrow's centroid sits low-left in its 24x24 box; nudge it to read centered. */
.near-fab svg { transform: translate(-1px, 1px); }
.near-fab.active { color: var(--accent-cyan); }
.near-fab.active .fab-label { color: var(--accent-cyan); }
/* Locating: the choice is on, no fix yet. A satellite (shaded sphere)
   orbits the arrow itself on a tilted ellipse (major axis 10:30 to 4:30,
   perpendicular to the arrow's heading), passing in front of the glyph on
   the down-left half and behind it on the up-right half. The 48x48 scene
   is centered on the icon (.near-ico), not the button, and drawn at 80%
   of the old size so it clears the label (2026-09-07: the disc that used to sit under the glyph is gone;
   the glyph is filled in the accent while locating). Stops the instant
   a fix lands. */
.near-orbit { display: none; }
/* turnOn() sets .active before .locating (app.js), so the glyph is already
   filled in the accent; it rides above the track and the far half of the orbit. */
/* The glyph sits between the far half of the orbit (z 0) and the near half
   (z 3 track-front, z 4 satellite). For that interleave to work nothing
   between them may open a stacking context: .near-orbit is positioned with
   left/top only, never transform (a transform on it put the whole scene
   behind the glyph on 2026-09-07). */
.near-fab.locating svg { position: relative; z-index: 2; }
.near-fab.locating .near-orbit {
  display: block;
  position: absolute; top: calc(50% - 24px); left: calc(50% - 24px);
  width: 48px; height: 48px;
  pointer-events: none;
}
.near-orbit i { position: absolute; display: block; border-radius: var(--radius-circle); }
.near-track {
  /* a=14, b=7 about (24,24), the true center of the 48px scene and of the
     icon (the arrow's centroid offset and its 1px nudge cancel out): 2a+1
     by 2b+1, top-left at center minus half. Tight around the glyph, with a
     short minor axis so the loop reads as a ring seen at an angle. */
  left: 9.5px; top: 16.5px; width: 29px; height: 15px;
  /* darker blue than the disc, so the front half stays visible across it */
  border: 1px solid color-mix(in srgb, var(--accent-cyan) 60%, var(--bg));
  transform: rotate(45deg); z-index: 0;
}
/* The near half of the track (lower-left, where the satellite passes in
   front) is drawn again above the disc and glyph, so the line reads as
   crossing the face. clip-path is in the element's own frame, before the
   rotation, so "bottom half" is the near half of the tilted ellipse. */
.near-track-front { clip-path: inset(50% 0 0 0); z-index: 3; }
.near-sat {
  left: 0; top: 0; width: 8px; height: 8px;
  offset-path: path('M 33.9,33.9 L 33.4,34.3 L 32.9,34.6 L 32.3,34.8 L 31.6,35.0 L 30.9,35.1 L 30.1,35.0 L 29.3,34.9 L 28.4,34.8 L 27.5,34.5 L 26.6,34.2 L 25.6,33.7 L 24.7,33.2 L 23.7,32.7 L 22.7,32.0 L 21.8,31.3 L 20.8,30.6 L 19.9,29.8 L 19.1,28.9 L 18.2,28.1 L 17.4,27.2 L 16.7,26.2 L 16.0,25.3 L 15.3,24.3 L 14.8,23.3 L 14.3,22.4 L 13.8,21.4 L 13.5,20.5 L 13.2,19.6 L 13.1,18.7 L 13.0,17.9 L 12.9,17.1 L 13.0,16.4 L 13.2,15.7 L 13.4,15.1 L 13.7,14.6 L 14.1,14.1 L 14.6,13.7 L 15.1,13.4 L 15.7,13.2 L 16.4,13.0 L 17.1,12.9 L 17.9,13.0 L 18.7,13.1 L 19.6,13.2 L 20.5,13.5 L 21.4,13.8 L 22.4,14.3 L 23.3,14.8 L 24.3,15.3 L 25.3,16.0 L 26.2,16.7 L 27.2,17.4 L 28.1,18.2 L 28.9,19.1 L 29.8,19.9 L 30.6,20.8 L 31.3,21.8 L 32.0,22.7 L 32.7,23.7 L 33.2,24.7 L 33.7,25.6 L 34.2,26.6 L 34.5,27.5 L 34.8,28.4 L 34.9,29.3 L 35.0,30.1 L 35.1,30.9 L 35.0,31.6 L 34.8,32.3 L 34.6,32.9 L 34.3,33.4 L 33.9,33.9 Z');
  offset-rotate: 0deg;
  /* highlight near the center: an off-center highlight made the sphere read
     as sitting beside the line even though its center is on it. The path
     is the track's stroke centerline (a=14, b=7 about (24,24), long axis
     at +45deg so it runs 10:30 to 4:30, perpendicular to the arrow's
     heading; starts at the 4:30 end and runs the near, down-left half
     first), regenerated 2026-09-07, 5deg steps. */
  background: radial-gradient(circle at 45% 45%, var(--text) 0, var(--accent-cyan-bright) 35%, var(--accent-cyan) 70%, var(--accent-cyan-strong) 100%);
  box-shadow: 0 0 3px var(--accent-cyan);
  /* reverse: the keyframes are written for a clockwise ride (front pass
     lower-left travelling down-and-left first); played backwards the
     satellite goes the other way round, and the z-index swaps still land
     at the same two points on the track. Flipped 2026-09-02 evening to compare. */
  animation: near-ride 2.6s linear infinite reverse;
}
@keyframes near-ride {
  /* Scale eases continuously: largest at the near point (25%), smallest at
     the far point (75%), unity at the two ends of the major axis, where the
     z-index swaps. The satellite clears the disc by 2px there, so the swap
     itself is invisible. No translate: the path is the stroke centerline. */
  0%    { offset-distance: 0%;    z-index: 4; transform: scale(1); }
  25%   { offset-distance: 25%;   z-index: 4; transform: scale(1.15); }
  49.9% { offset-distance: 49.9%; z-index: 4; transform: scale(1.003); }
  50%   { offset-distance: 50%;   z-index: 0; transform: scale(1); }
  75%   { offset-distance: 75%;   z-index: 0; transform: scale(0.85); }
  99.9% { offset-distance: 99.9%; z-index: 0; transform: scale(0.997); }
  100%  { offset-distance: 100%;  z-index: 4; transform: scale(1); }
}
.near-fab.shake { animation: near-shake 0.4s; }
@keyframes near-shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-4px); }
  75% { transform: translateX(4px); }
}
/* No data-nn-app qualifier here: demo cities set that marker on the plain
   website. The head stamps data-nn-near before first paint (see website.md
   "Pre-paint environment stamp"); app.js only removes it on a denied
   result. .in-app (added on Android) no longer needs its own offset: the
   bar's flex layout, not per-button bottom math, is what stacks the three
   buttons now. */
html[data-nn-near="true"] .near-fab { display: flex; }
/* Distance label app.js drops into each card's map pill while distance
   order is on ("0.3 mi"); removed when the order is toggled off. */
.pill.map .dist { font-size: 0.78rem; font-variant-numeric: tabular-nums; }

/* First-visit hint for .near-fab: a small callout with a tail pointing at
   the button, positioned by app.js from the button's rect (the button
   stacks differently in-app). Fades in; .near-fab.pulse is inert since 2026-09-07
   twice with the hover glow so eye and bubble land on the same spot. */
.near-hint {
  position: fixed;
  z-index: 91;
  max-width: 220px;
  padding: 0.7rem 2rem 0.7rem 0.9rem;
  background: var(--card);
  border: 1px solid var(--accent-cyan-soft);
  border-radius: var(--radius-xl);
  color: var(--text);
  box-shadow: 0 4px 16px var(--shadow-floating), 0 0 12px var(--accent-cyan-wash);
  cursor: pointer;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 0.35s ease, transform 0.35s ease;
  font-size: 0.9rem;
  line-height: 1.35;
}
.near-hint.show { opacity: 1; transform: none; }
.near-hint strong { display: block; color: var(--accent-cyan); font-size: 0.95rem; margin-bottom: 0.15rem; }
.near-hint::after {
  /* tail toward the button, bottom-right corner */
  content: "";
  position: absolute;
  right: -7px;
  bottom: 14px;
  width: 12px; height: 12px;
  background: var(--card);
  border-right: 1px solid var(--accent-cyan-soft);
  border-top: 1px solid var(--accent-cyan-soft);
  transform: rotate(45deg);
}
.near-hint-close {
  position: absolute; top: 4px; right: 4px;
  width: 1.6rem; height: 1.6rem;
  border: 0; background: transparent; color: var(--muted);
  padding: 0; cursor: pointer; border-radius: var(--radius-pill);
}
.near-hint-close svg { width: 1rem; height: 1rem; fill: currentColor; }
.near-hint-close:hover { color: var(--text); background: var(--card-hover); }
/* The ring pings for as long as distance order is off (the invitation);
   the filled .active button holds still. .pulse (the hint) just runs it
   faster for a moment. */
/* Inside the bottom bar the button is a rounded tile. The sonar ring that
   pinged here while distance order was off (2026-09-02 to 2026-09-07) is
   gone; app.js still toggles .pulse for the hint and it is inert. */
.fab-bar .near-fab { border-radius: var(--radius-lg); }
/* Wind-up spin (2026-09-04, "D" from the second round of mockups; the
   synced-ring spin held the slot for an hour): every 7.8 s while distance
   order is off, app.js adds .spin. The arrow pulls back 28deg like a
   run-up, whips one full turn, blows 36deg past north, and wobbles into
   place like it went too far, cartoon style. The period was three
   cycles of the old sonar ring, gone since 2026-09-07. The arrow's 1px centering offset is composed into every
   keyframe. Only the glyph moves; disc, border and glow hold still. */
.near-fab.spin svg { animation: near-spin 1.7s linear both; }
@keyframes near-spin {
  0%   { transform: translate(-1px, 1px) rotate(0deg);   animation-timing-function: ease-out; }
  16%  { transform: translate(-1px, 1px) rotate(-28deg); animation-timing-function: linear; }
  24%  { transform: translate(-1px, 1px) rotate(-28deg); animation-timing-function: cubic-bezier(0.6, 0, 0.5, 1); }
  60%  { transform: translate(-1px, 1px) rotate(396deg); animation-timing-function: ease-in-out; }
  72%  { transform: translate(-1px, 1px) rotate(342deg); animation-timing-function: ease-in-out; }
  82%  { transform: translate(-1px, 1px) rotate(371deg); animation-timing-function: ease-in-out; }
  90%  { transform: translate(-1px, 1px) rotate(354deg); animation-timing-function: ease-in-out; }
  96%  { transform: translate(-1px, 1px) rotate(363deg); animation-timing-function: ease-in-out; }
  100% { transform: translate(-1px, 1px) rotate(360deg); }
}
  100% { box-shadow: 0 4px 16px var(--shadow-floating), 0 0 0 18px transparent; }
}
.near-hint.denied { cursor: default; }
.near-hint.denied strong { color: var(--text); }
@media (prefers-reduced-motion: reduce) {
  .near-hint { transition: none; transform: none; }
  .near-fab.spin svg { animation: none; }
  .near-sat { animation: none; }
}

/* Chrome-style find bar, pinned top center. Above the sticky day-nav (20)
   and the fabs (90), below the pull-to-refresh spinner. */
.find-bar {
  display: none;
  position: fixed;
  top: calc(0.5rem + env(safe-area-inset-top));
  left: 50%;
  transform: translateX(-50%);
  width: min(26rem, calc(100vw - 1rem));
  flex-wrap: wrap;
  align-items: center;
  gap: 0.15rem;
  padding: 0.35rem 0.5rem;
  border-radius: var(--radius-xl);
  background: var(--card);
  border: 1px solid var(--accent-cyan-soft);
  box-shadow: 0 4px 16px var(--shadow-floating), 0 0 12px var(--accent-cyan-wash);
  backdrop-filter: blur(6px);
  z-index: 95;
}
.find-bar.open { display: flex; }
.find-input {
  flex: 1;
  min-width: 0;
  background: transparent;
  border: 0;
  outline: none;
  color: var(--text);
  /* 16px floor: smaller inputs make iOS zoom the page on focus */
  font-size: 16px;
  padding: 0.25rem 0.35rem;
}
.find-input::placeholder { color: var(--dimmer); }
/* Hide the WebKit/Blink built-in clear button; the bar has its own ✕ */
.find-input::-webkit-search-cancel-button { display: none; }
.find-bar > button {
  display: flex; align-items: center; justify-content: center;
  width: 2.1rem; height: 2.1rem;
  background: transparent;
  border: 0;
  border-radius: var(--radius-md);
  color: var(--accent-cyan);
  cursor: pointer;
  padding: 0;
}
.find-bar > button svg { width: 1.3rem; height: 1.3rem; fill: currentColor; }

/* Results panel: full width under the input row, scrolls inside itself. */
.find-results {
  flex: 1 1 100%;
  max-height: var(--find-results-max-h);
  overflow-y: auto;
  overflow-x: hidden;
  padding: var(--find-results-pad);
  display: flex;
  flex-direction: column;
  gap: var(--find-results-gap);
  overscroll-behavior: contain;
}
.find-results[hidden] { display: none; }
.find-summary { font-size: var(--find-summary-size); color: var(--find-summary-ink); padding: 0 0.15rem; }
.find-group-head {
  font-size: var(--find-group-head-size);
  font-weight: 700;
  color: var(--find-group-head-ink);
  margin: 0.25rem 0.15rem 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A venue hit is a grouped card (2026-09-12). The head is a filled bar with
   a CTA pill so it reads as a control before it reads as a heading: hover
   was the only cue before, and hover does not exist on a phone. The venue's
   nights sit inside the same box, hanging off a spine, so they read as
   belonging to it. Day groups under "Also in events" get no .venue-group
   class and stay plain divs with loose rows under them. */
.find-group.venue-group {
  /* The panel is a column flexbox that overflows; `overflow: hidden` here
     (needed so the rows respect the card's rounded corners) drops this
     card's automatic minimum size to zero, and a long result list would
     squash it to a sliver. Opt out of shrinking. */
  flex-shrink: 0;
  border: 1px solid var(--find-group-card-border);
  border-radius: var(--find-group-card-radius);
  background: var(--find-group-card-bg);
  overflow: hidden;
}
/* Sponsor tier moves from the head to the card edge: inside the card the
   old inset rule would collide with the row spine. */
.find-group.venue-group.tier-premier { border-left: var(--find-row-rule-w) solid var(--find-row-rule-premier); }
.find-group.venue-group.tier-featured { border-left: var(--find-row-rule-w) solid var(--find-row-rule-featured); }

button.find-group-head {
  display: flex; align-items: center; gap: var(--find-group-head-gap);
  width: 100%; min-width: 0;
  font-family: inherit; text-align: left;
  margin: 0;
  padding: var(--find-group-head-pad);
  background: var(--find-group-head-bg);
  border: 0; border-bottom: 1px solid var(--find-group-head-border);
  color: var(--find-group-head-ink);
  cursor: pointer;
  overflow: visible; white-space: normal;
}
button.find-group-head:hover, button.find-group-head:focus-visible {
  background: var(--find-group-head-bg-hover);
}
.fgh-txt { flex: 1; min-width: 0; }
.fgh-name {
  display: block;
  font-size: var(--find-group-head-size);
  font-weight: 700; line-height: 1.2;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fgh-sub {
  display: block;
  font-size: var(--find-group-sub-size);
  font-weight: 400;
  color: var(--find-summary-ink);
  margin-top: var(--find-group-sub-gap);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fgh-cta {
  flex-shrink: 0;
  font-size: var(--find-group-cta-size); font-weight: 600;
  color: var(--find-group-cta-ink);
  background: var(--find-group-cta-bg);
  border: 1px solid var(--find-group-cta-border);
  border-radius: var(--radius-pill);
  padding: var(--find-group-cta-pad);
  white-space: nowrap;
}
/* Landing cue for a tapped venue head: a ring that fades out on its own. */
.find-landed {
  animation: find-landed var(--find-landed-fade) ease-out 1;
  border-radius: var(--find-landed-radius);
}
@keyframes find-landed {
  from { box-shadow: 0 0 0 var(--find-landed-ring-w) var(--find-landed-ring); }
  to { box-shadow: 0 0 0 var(--find-landed-ring-w) transparent; }
}
.find-also { font-size: var(--find-summary-size); color: var(--find-also-ink); font-weight: 600; margin: 0.5rem 0.15rem 0; }
.find-row {
  position: relative;
  display: flex; flex-direction: column; align-items: flex-start; gap: 0.1rem;
  width: 100%; min-width: 0;
  text-align: left;
  padding: var(--find-row-pad);
  border: 0; border-radius: var(--find-row-radius);
  background: var(--find-row-bg);
  color: var(--text);
  cursor: pointer;
  border-left: var(--find-row-rule-w) solid transparent;
}
.find-row:hover, .find-row:focus-visible { background: var(--find-row-bg-hover); }
.find-row.tier-premier { border-left-color: var(--find-row-rule-premier); }
.find-row.tier-featured { border-left-color: var(--find-row-rule-featured); }
/* Category dot, in the same hues the venue cards use for their event type
   stripes. .cat-* comes from the row's .event-type class, so a row with no
   category falls back to the neutral --find-row-dot. */
.find-row::after {
  content: ''; position: absolute;
  left: var(--find-row-dot-x); top: var(--find-row-dot-y);
  width: var(--find-row-dot-size); height: var(--find-row-dot-size);
  border-radius: var(--radius-circle);
  background: var(--find-row-dot);
}
.find-row.cat-music { --find-row-dot: var(--music); }
.find-row.cat-drinks { --find-row-dot: var(--drinks); }
.find-row.cat-food { --find-row-dot: var(--food); }
.find-row.cat-sports { --find-row-dot: var(--sports); }
.find-row.cat-events { --find-row-dot: var(--events); }
.find-row.cat-info { --find-row-dot: var(--info); }

/* Rows inside a venue card: no pill of their own, separated by a hairline
   and strung on a spine that runs behind their dots. */
.venue-group .find-row {
  background: none;
  border-radius: 0;
  border-left: 0;
  border-top: 1px solid var(--find-spine-rule);
}
.find-group-head + .find-row { border-top: 0; }
.venue-group .find-row:hover, .venue-group .find-row:focus-visible {
  background: var(--find-row-bg-hover);
}
.venue-group .find-row::before {
  content: ''; position: absolute;
  left: var(--find-spine-x); top: 0; bottom: 0;
  width: 1px;
  background: var(--find-spine-line);
}
/* The ring punches the dot out of the spine line it sits on. */
.venue-group .find-row::after { box-shadow: 0 0 0 3px var(--find-row-dot-ring); }
.find-row-when { font-size: var(--find-row-when-size); color: var(--find-row-when-ink); font-variant-numeric: tabular-nums; }
.find-row-what {
  font-size: var(--find-row-what-size);
  max-width: 100%;
  overflow-wrap: anywhere;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.find-row-what mark { background: var(--find-mark-bg); color: inherit; border-radius: var(--find-mark-radius); padding: 0 1px; }
.find-empty { font-size: var(--find-row-what-size); color: var(--find-empty-ink); padding: 0.4rem 0.15rem; }
.find-empty em { font-style: normal; font-weight: 600; color: var(--text); }

/* Match highlights (CSS Custom Highlight API): all matches translucent cyan,
   the current one gold with dark text — Chrome's yellow/orange pattern in
   site colors. */
::highlight(nn-find) { background-color: var(--accent-cyan-soft); }
::highlight(nn-find-current) { background-color: var(--find-current-bg); color: var(--ink-on-highlight); }

/* --- Fixed-event guide card (Arts Fest, etc.) --- */
.venue-card.guide {
  border: 1px solid var(--accent-cyan-strong);
  box-shadow: 0 0 0 1px var(--accent-cyan-ring), 0 8px 30px var(--accent-cyan-wash);
  background: linear-gradient(180deg, var(--accent-cyan-veil), var(--guide-card-wash-fade));
}
/* When live, guide cards keep their cyan border (this .venue-card.guide rule
   out-orders .venue-card.live-now); only the Live Now pill and the row-level
   green time/title states apply. Intentional — do not add a green override. */
/* Cyan tab on the card's top border line — same treatment as the sponsor
   badges on venue cards (inherits the absolute positioning, size, and spacing
   from the base .badge), so the title sits with normal venue spacing below. */
.badge.guide {
  color: var(--accent-cyan);
  border: 1px solid var(--accent-cyan-half);
}
.guide-blurb {
  margin: 2px 0 10px;
  color: var(--guide-blurb-ink);
  font-size: 0.9rem;
}
.guide-teaser {
  margin-top: 4px;
  color: var(--guide-teaser-ink);
  font-size: 0.95rem;
  line-height: 1.4;
}
/* Small "Map" bubble that sits next to the guide title in the header. */
.guide-map-link {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  align-self: center;
  margin-left: auto;   /* right-justify the bubble in the header row */
  padding: 2px 9px;
  border-radius: var(--radius-pill-full);
  background: var(--accent-cyan-fill);
  border: 1px solid var(--accent-cyan-line);
  color: var(--guide-map-link-ink);
  font-size: 0.72rem;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
  line-height: 1.4;
}
.guide-map-link:hover { background: var(--accent-cyan-fill-hover); }
/* Inline guide image (Stripe Out seating map). Full card width, capped so a
   tall map can't push the day's listings off the first screen; the tap target
   is the whole image, opening the full-resolution file in a new tab. */
.guide-image-link {
  display: block;
  margin: 8px 0 4px;
  line-height: 0;            /* no descender gap under the image */
}
.guide-image {
  display: block;
  width: 100%;
  height: auto;
  max-height: 62vh;
  object-fit: contain;
  border-radius: var(--radius-lg);
  border: 1px solid var(--accent-cyan-line);
}

/* Compact guide strip (guides.md `strip: 1`, 2026-09-03): the same card
   chrome, tightened to badge + title row + one line of text, so it takes
   about a fifth of a phone screen and the cards below it peek above the
   fold. Only the line for the current time of day is shown (app.js). */
.venue-card.guide.guide-strip { padding-bottom: 0.7rem; }
.guide-strip .venue-head { margin-bottom: 0.2rem; }
.guide-strip-lines { margin-top: 0.15rem; }
.guide-strip-line {
  font-size: 0.9rem;
  line-height: 1.4;
  color: var(--guide-teaser-ink);
}
.guide-strip-line[hidden] { display: none; }

/* Location sits on its own line under the event title, left-aligned and
   wrapping — never clipped, unlike the right-hand time column. */
.guide-loc {
  display: block;
  margin-top: 0.12rem;
  font-size: 0.72rem;
  line-height: 1.3;
  color: var(--guide-loc-ink);
  white-space: normal;
}

/* Penn State home games (2026-09-09): team cards sit after the venues. */
.venue-card.team .venue-name { letter-spacing: 0.01em; }

/* Neighborhood badge filter (colors are inline per label, set server-side) */
.hood-badge { cursor: pointer; user-select: none; -webkit-user-select: none; }
.hood-badge.hood-active { box-shadow: 0 0 0 1.5px currentColor; }
.venue-card.hood-hidden { display: none !important; }

/* ===== Light theme (cities with features.light_theme) =====
   The sun/moon toggle renders in the weather pill's header slot; a head
   script applies the saved .light-theme class before first paint. The
   :root.light-theme selector outranks the per-city :root accent override
   injected via CITY_ACCENT_CSS, so these values win in light mode while
   the city's accent/neon hues carry through untouched. */
.theme-toggle { border: 0; background: transparent; cursor: pointer; padding: 0; }
.theme-toggle svg { fill: none; }
.theme-toggle .icon-sun { display: none; }
:root.light-theme .theme-toggle .icon-sun { display: block; }
:root.light-theme .theme-toggle .icon-moon { display: none; }

/* The fixed noise texture and neon glow blobs are tuned for a dark bg;
   fade them way down instead of removing so the page keeps some depth. */
:root.light-theme body::before { opacity: 0.12; }
:root.light-theme .glow { opacity: 0.07; }
:root.light-theme .glow-2 { opacity: 0.04; }
:root.light-theme .glow-3 { opacity: 0.03; }
/* Surfaces and text that hardcode dark-theme colors instead of variables;
   each gets a light counterpart here. Keep this list in sync when adding
   new hardcoded colors above (or better: use variables). */
:root.light-theme header { background: var(--light-scrim-header); }
:root.light-theme .ts-bar { background: var(--light-scrim-ts-bar); }
:root.light-theme .announce { background: var(--light-announce-bg); }
:root.light-theme .announce-lead { background: var(--light-announce-lead-bg); color: var(--light-amber-ink); }
:root.light-theme .day-nav-wrapper { background: var(--light-scrim-day-nav); }
/* The bottom bar shares the day-nav scrim, so it needs the same light swap
   (2026-09-08: it stayed navy in light mode on both cities). */
:root.light-theme .fab-bar { background: var(--light-scrim-day-nav); }
:root.light-theme .wordmark,
:root.light-theme .day-label,
:root.light-theme .venue-name,
:root.light-theme .spotlight-line-venue,
:root.light-theme .loc-card-title,
:root.light-theme .event.live .event-title,
:root.light-theme .pricing-price { color: var(--text); }
:root.light-theme .day-weather,
:root.light-theme .tonight-forecast,
:root.light-theme .busyness-strip,
:root.light-theme .quiet-tonight {
  background: var(--light-surface-quiet);
  border-color: var(--light-border-rule);
}
:root.light-theme .quiet-tonight > summary:hover { background: var(--light-surface-quiet-hover); }
:root.light-theme .event { border-top-color: var(--light-border-rule); }
:root.light-theme .venue-pills,
:root.light-theme .qt-venue-body { border-top-color: var(--light-border-rule); }
:root.light-theme .qt-event { border-top-color: var(--light-border-rule-faint); }
:root.light-theme .whiskey-sponsor:hover .whiskey-cta { color: var(--light-amber-ink); }

/* ===== Tonight downtown plan card (spec 2026-09-07, v4 2026-09-08) =====
   Sits between the filter row and <main>, once per page, on every date
   tab. Quiet chrome on purpose: no sponsor gold, no busyness rail. It is
   not a .venue-card, so the category and hood filters never touch it.
   Body is a list of up to three "Venue: what, clock" lines and one
   full-width Text tonight pill. [hidden] is set by app.js on a stale page
   (client date past the compiled today), because "tonight" would be wrong. */
.spotlight-wrap {
  max-width: 820px;
  margin: 0 auto;
  padding: 0.75rem 1.25rem 0;
  box-sizing: border-box;
}
@media (max-width: 640px) {
  /* Same rail as main's mobile rule: 1rem left/right, no safe-area terms. */
  .spotlight-wrap { padding: 0.75rem 1rem 0; }
}
.spotlight-card {
  margin: 0;
  padding: 0.65rem 0.9rem 0.6rem;
  box-sizing: border-box;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-2xl);
}
.spotlight-card[hidden] { display: none; }
.spotlight-wrap[hidden] { display: none; }
.spotlight-kicker {
  font-family: var(--font-mono);
  font-size: 0.6rem; font-weight: 600;
  letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 0.35rem;
}
.spotlight-lines {
  list-style: none;
  margin: 0 0 0.6rem; padding: 0;
  display: flex; flex-direction: column; gap: 0.55rem;
}
/* One line = venue and clock on the first row, the act on the second,
   with a category dot (the filter pills' colors) in place of a bullet, so
   three lines are always six tidy rows and never a ragged wrap. */
.spotlight-line {
  position: relative;
  padding-left: 0.95rem;
  display: grid; grid-template-columns: 1fr auto; column-gap: 0.6rem;
  color: var(--text);
  font-size: 0.95rem; line-height: 1.4;
  overflow-wrap: anywhere;
}
.spotlight-line::before {
  content: "";
  position: absolute; left: 0; top: 0.42rem;
  width: 7px; height: 7px; border-radius: var(--radius-circle);
  background: var(--events);
}
.spotlight-line[data-cat="music"]::before { background: var(--music); }
/* A Premier/Featured sponsor with a real listing leads the card in gold
   (2026-09-08); the listing below carries the badge, so no label here. */
.spotlight-line[data-sponsor]::before { background: var(--gold); }
.spotlight-line[data-sponsor] .spotlight-line-venue { color: var(--gold); }
.spotlight-line-star { font-size: 0.8em; }
.spotlight-line-star:first-child { margin-right: 0.3em; }
.spotlight-line-star:last-child { margin-left: 0.3em; }
.spotlight-line-venue {
  grid-column: 1;
  font-family: var(--font-body-simple);
  font-weight: 700;
  color: var(--ink-bright);
}
.spotlight-line-time {
  grid-column: 2; grid-row: 1;
  align-self: baseline;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  color: var(--muted);
  white-space: nowrap;
}
.spotlight-line-day {
  font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;
}
.spotlight-line-day::after { content: "\00b7"; margin: 0 0.4em; letter-spacing: 0; }
/* Neighborhood label (radius cities, e.g. Indy): same voice as the weekday.
   In the clock column it leads the time; after the act it trails it. */
.spotlight-line-hood {
  font-family: var(--font-mono);
  font-size: 0.72rem; font-weight: 600;
  letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--muted);
}
.spotlight-line-time .spotlight-line-hood::after { content: "\00b7"; margin: 0 0.4em; letter-spacing: 0; }
.spotlight-line-what .spotlight-line-hood::before { content: "\00b7"; margin: 0 0.4em; letter-spacing: 0; }
.spotlight-line-what {
  grid-column: 1 / -1;
  color: var(--text-dim);
  font-size: 0.92rem; line-height: 1.35;
}
.spotlight-actions {
  display: flex; gap: 0.5rem;
  padding-top: 0.4rem;
  border-top: 1px dashed var(--border-hairline);
}
.spotlight-actions .pill {
  flex: 1 1 0;
  justify-content: center;
  border: 1px solid var(--border);
  color: var(--text);
  font-family: inherit;
  cursor: pointer;
}
.spotlight-ico {
  width: 1rem; height: 1rem;
  fill: currentColor;
  flex: none;
}

/* ===== Web location card (2026-09-29, features.location_card) =====
   One per day section, after the pinned block. The head stamp sets
   data-nn-loc-card (ask | fix | off) and data-nn-loc-help before the first
   paint; without the attribute (feature off) nothing shows. Hidden while
   Grid, Map or Find is showing. Spec 2026-09-29-web-location-card-design.md. */
.loc-card {
  display: none;
  margin: 0.9rem 0;
  padding: 0.65rem 0.9rem 0.7rem;
  box-sizing: border-box;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-2xl);
  overflow-wrap: anywhere;
}
html[data-nn-loc-card="ask"] .loc-card,
html[data-nn-loc-card="fix"] .loc-card { display: block; }
.loc-card-ask, .loc-card-fix { display: none; }
html[data-nn-loc-card="ask"] .loc-card-ask,
html[data-nn-loc-card="fix"] .loc-card-fix { display: block; }
html[data-nn-grid-on="true"] .loc-card,
html[data-nn-map-on="true"] .loc-card { display: none; }
html:has(.find-bar.open) .loc-card { display: none; }
.loc-card-kicker {
  font-family: var(--font-mono);
  font-size: 0.6rem; font-weight: 600;
  letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 0.35rem;
}
.loc-card-title {
  margin: 0 0 0.25rem;
  font-family: var(--font-body-simple);
  font-weight: 700; font-size: 1rem;
  color: var(--ink-bright);
}
.loc-card-body, .loc-card-help, .loc-card-note, .loc-card-app {
  margin: 0 0 0.6rem;
  color: var(--text-dim);
  font-size: 0.92rem; line-height: 1.4;
}
.loc-card-note { color: var(--text); }
.loc-card-note[hidden] { display: none; }
.loc-card-help [data-help] { display: none; }
html[data-nn-loc-help="ios-safari"] .loc-card-help [data-help="ios-safari"],
html[data-nn-loc-help="ios-other"] .loc-card-help [data-help="ios-other"],
html[data-nn-loc-help="android"] .loc-card-help [data-help="android"],
html[data-nn-loc-help="desktop"] .loc-card-help [data-help="desktop"],
html[data-nn-loc-help="app-denied-ios"] .loc-card-help [data-help="app-denied-ios"],
html[data-nn-loc-help="app-denied-android"] .loc-card-help [data-help="app-denied-android"],
html[data-nn-loc-help="app-services-ios"] .loc-card-help [data-help="app-services-ios"],
html[data-nn-loc-help="app-services-android"] .loc-card-help [data-help="app-services-android"] { display: inline; }
/* App fix face (2026-09-29): native title and Open Settings instead of the
   browser's Try again and store line. iOS allows no link to Location
   Services, so its services-off face has no button. */
.loc-card-app-only { display: none; }
html[data-nn-loc-help^="app-"] .loc-card-web-only { display: none; }
html[data-nn-loc-help^="app-"] p.loc-card-app-only { display: block; }
html[data-nn-loc-help^="app-"] .loc-card-actions .loc-card-app-only { display: inline-flex; }
html[data-nn-loc-help="app-services-ios"] .loc-card-actions .loc-card-settings { display: none; }
.loc-card-app a { color: var(--accent-cyan); }
/* No store app on a desktop; stamped before paint, so it never flashes. */
html[data-nn-loc-help="desktop"] .loc-card-app { display: none; }
.loc-card-actions { display: flex; gap: 0.5rem; align-items: center; }
.loc-card-actions .pill { justify-content: center; font-family: inherit; cursor: pointer; }
.loc-card-on {
  flex: 1 1 auto;
  background: var(--accent-cyan); color: var(--ink-on-highlight);
  border: 1px solid var(--accent-cyan);
  font-weight: 700;
}
.loc-card-on[aria-busy="true"] { opacity: 0.7; cursor: progress; }
.loc-card-later {
  flex: none;
  background: none; border: 1px solid var(--border);
  color: var(--muted);
}
.loc-card.collapsing {
  overflow: hidden;
  transition: height 0.3s ease, opacity 0.3s ease, margin 0.3s ease, padding 0.3s ease;
}
@media (prefers-reduced-motion: reduce) {
  .loc-card.collapsing { transition: none; }
}

/* ===== Saved venues (2026-09-07, features.saved_venues) =====
   A bookmark (rectangle with a notch cut from its bottom) on every card and
   a Saved button in the bottom bar. Both hidden unless the template stamps
   data-nn-saved. Off: outline in the current color; saved: filled gold.
   The Saved bar button brings saved venues to the top of every day (the
   rest of the list stays below); nothing on a card changes but the
   bookmark. */
.save-btn { display: none; }
html[data-nn-saved="true"] .save-btn { display: inline-flex; }
/* Same compact footprint as the other icon pills (.ig, .lineleap): the
   default .pill padding made it 8 to 11px wider and pushed the app's week
   toggle onto a second row on Android with a larger font scale. */
.pill.save-btn { padding: 0.55rem 0.7rem; }
/* The dance (2026-09-07, idle since 2026-09-08): every so often one unsaved
   bookmark on screen has its two bottom points walk to the left, then to
   the right, then the whole mark hops back to center. An invitation; a
   saved, gold bookmark never dances. Rotation about a point near the top
   edge so the bottom swings and the top barely moves. Added by app.js as
   .dance, removed on animationend. */
.save-btn svg { transform-origin: 50% 15%; }
.save-btn.dance:not(.on) svg { animation: save-dance 0.9s ease-in-out both; }
@keyframes save-dance {
  0%   { transform: rotate(0deg) translateY(0); }
  22%  { transform: rotate(16deg) translateY(0); }
  48%  { transform: rotate(-14deg) translateY(0); }
  66%  { transform: rotate(5deg) translateY(0); }
  78%  { transform: rotate(0deg) translateY(-3px); }
  90%  { transform: rotate(0deg) translateY(1px); }
  100% { transform: rotate(0deg) translateY(0); }
}
/* The Saved bar button does the same dance as an idle fidget (app.js adds
   .fidget on the gear/magnifier cadence), only while it is not active. */
.saved-fab svg { transform-origin: 50% 15%; }
.saved-fab.fidget:not(.active) svg { animation: save-dance 0.9s ease-in-out both; }
@media (prefers-reduced-motion: reduce) {
  .save-btn.dance svg, .saved-fab.fidget:not(.active) svg { animation: none; }
}
.save-btn svg { width: 1.15rem; height: 1.15rem; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linejoin: round; }
.save-btn.on { color: var(--gold); }
.save-btn.on svg { fill: var(--gold); stroke: var(--gold); }
html[data-nn-saved="true"] .fab-bar > .saved-fab { display: flex; }

/* ===== Grid view (2026-09-08, features.grid_view) =====
   The Grid bar button swaps the visible day's card list for a TV-guide
   timeline built by app.js from the cards that are currently shown: one
   flex row per venue (a sticky name column, then a lane of absolutely
   positioned blocks), under a sticky hour axis. Flex rows, not CSS grid: a
   sticky element inside a CSS grid can only slide within its own cell, so
   a pinned name column does not work there. The box scrolls in both axes
   inside itself with its bars hidden the way the day tabs hide theirs
   (website.md rule 1). Sizes shared with app.js: name column 104px, hour
   96px, lane 44px, block 36px at top 4px, axis 34px. */
.grid-fab svg { width: 1.4rem; height: 1.4rem; fill: currentColor; }
html[data-nn-grid="true"] .fab-bar > .grid-fab { display: flex; }
html[data-nn-map="true"] .fab-bar > .map-fab { display: flex; }
.map-fab svg { width: 1.4rem; height: 1.4rem; fill: currentColor; }
/* Icon A (2026-09-29, option A of docs/mockups/2026-09-29-map-icon-lineup.html):
   a folded street map with one dot in the Live Now green (--map-live, the
   same token as a live dot on the map), so the button previews what it
   opens. While the map is on the icon is the list and the label List. */
.map-fab .map-i-dot { fill: var(--map-live); }
.map-fab .map-i-list { display: none; }
.map-fab.active .map-i-map { display: none; }
.map-fab.active .map-i-list { display: inline; }
/* Its idle dance: on the fidget cadence (app.js, 2.8 s after the gear) the
   sheet folds in and snaps open while the dot blinks twice, like a live
   venue. Only while the map is closed; still under reduced motion. The
   svg's animationend (bubbled from the parts) takes .fidget off. */
.map-fab .map-i-sheet { transform-origin: 12px 12px; }
.map-fab .map-i-dot { transform-box: view-box; transform-origin: 12px 11px; }
.map-fab.fidget:not(.active) .map-i-sheet { animation: map-unfold 1.3s ease-in-out both; }
.map-fab.fidget:not(.active) .map-i-dot { animation: map-blink 1.3s ease-in-out both; }
@keyframes map-unfold {
  0% { transform: scaleX(1); }
  18% { transform: scaleX(0.55); }
  42% { transform: scaleX(1.08); }
  56%, 100% { transform: scaleX(1); }
}
@keyframes map-blink {
  0%, 50% { opacity: 1; transform: scale(1); }
  60% { opacity: 0.25; transform: scale(1.7); }
  70% { opacity: 1; transform: scale(1); }
  80% { opacity: 0.25; transform: scale(1.7); }
  90%, 100% { opacity: 1; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .map-fab.fidget:not(.active) .map-i-sheet, .map-fab.fidget:not(.active) .map-i-dot { animation: none; }
}
/* Its List-state dance (2026-09-29, option 4 of
   docs/mockups/2026-09-29-list-button-lineup.html): while the map is open,
   on the same fidget cadence, the bottom bar lights the map's live green
   (--map-live, the same token the live dots use) and swaps places with the
   top bar, then both settle back. Only while the map is open (.active); the
   map-closed dance above and this one use exact opposite selectors so they
   never fire together. Same animationend/fallback teardown in app.js. */
.map-fab .map-i-row1, .map-fab .map-i-row3 { transform-box: view-box; }
.map-fab.active.fidget .map-i-row3 { animation: map-list-up 1.4s ease-in-out both; }
.map-fab.active.fidget .map-i-row1 { animation: map-list-down 1.4s ease-in-out both; }
.map-fab.active.fidget .map-i-row3 rect { animation: map-list-lit 1.4s both; }
@keyframes map-list-up {
  0%, 10% { transform: translateY(0); }
  45%, 70% { transform: translateY(-11px); }
  100% { transform: translateY(0); }
}
@keyframes map-list-down {
  0%, 10% { transform: translateY(0); }
  45%, 70% { transform: translateY(11px); }
  100% { transform: translateY(0); }
}
@keyframes map-list-lit {
  0%, 15% { fill: currentColor; }
  35%, 75% { fill: var(--map-live); }
  100% { fill: currentColor; }
}
@media (prefers-reduced-motion: reduce) {
  .map-fab.active.fidget .map-i-row3, .map-fab.active.fidget .map-i-row1, .map-fab.active.fidget .map-i-row3 rect { animation: none; }
}
/* Green, not gold (2026-09-09): matches the Near me on-state and the grid's
   own now-line token. */
.fab-bar > .grid-fab.active { color: var(--green); }
.fab-bar > .grid-fab.active .fab-label { color: var(--green); }
/* Grid button sweep (2026-09-09, the "now-line sweep" pick from a lineup of
   five, docs/mockups/2026-09-09-grid-fidget-lineup.html holds the other
   four): on the idle fidget cadence app.js adds .fidget for one cycle, only
   while the button is not active. The green now rule (.g-now, the grid's
   own .tv-now token) crosses the glyph left to right and each block flashes
   to the text color as the rule reaches its centre (the percentages are
   each block's centre x over the rule's 19-unit run). Same reduced-motion
   stance as the other fidgets. */
.grid-fab .g-now { fill: var(--green); opacity: 0; }
.grid-fab.fidget:not(.active) .g-now { animation: grid-sweep 1.2s linear both; }
@keyframes grid-sweep {
  0%   { transform: translateX(0); opacity: 0; }
  6%   { opacity: 1; }
  94%  { opacity: 1; }
  100% { transform: translateX(19px); opacity: 0; }
}
.grid-fab.fidget:not(.active) .g-blk { animation-duration: 1.2s; animation-timing-function: linear; animation-fill-mode: both; }
.grid-fab.fidget:not(.active) .g-r3a { animation-name: grid-lit-16; }
.grid-fab.fidget:not(.active) .g-r2a { animation-name: grid-lit-21; }
.grid-fab.fidget:not(.active) .g-r4a { animation-name: grid-lit-29; }
.grid-fab.fidget:not(.active) .g-r3b { animation-name: grid-lit-68; }
.grid-fab.fidget:not(.active) .g-r2b { animation-name: grid-lit-74; }
.grid-fab.fidget:not(.active) .g-r4b { animation-name: grid-lit-82; }
@keyframes grid-lit-16 { 0%, 8%  { fill: currentColor; } 16% { fill: var(--text); } 34%  { fill: currentColor; } }
@keyframes grid-lit-21 { 0%, 13% { fill: currentColor; } 21% { fill: var(--text); } 39%  { fill: currentColor; } }
@keyframes grid-lit-29 { 0%, 21% { fill: currentColor; } 29% { fill: var(--text); } 47%  { fill: currentColor; } }
@keyframes grid-lit-68 { 0%, 60% { fill: currentColor; } 68% { fill: var(--text); } 86%  { fill: currentColor; } }
@keyframes grid-lit-74 { 0%, 66% { fill: currentColor; } 74% { fill: var(--text); } 92%  { fill: currentColor; } }
@keyframes grid-lit-82 { 0%, 74% { fill: currentColor; } 82% { fill: var(--text); } 100% { fill: currentColor; } }
@media (prefers-reduced-motion: reduce) {
  .grid-fab.fidget:not(.active) .g-now, .grid-fab.fidget:not(.active) .g-blk { animation: none; }
}
.day-section.grid-on > .venue-card, .day-section.grid-on > .qt-venue.in-list, .day-section.grid-on > .saved-empty { display: none !important; }
/* The big date line (and its weather) is redundant while the timeline is
   up: the tabs and the grid's corner cell say which day it is. !important
   like its .venue-card twin above: this has to win over any display a
   header variant sets (.has-presenter is a grid). */
.day-section.grid-on > .day-header { display: none !important; }
/* Root-keyed twin of the two rules above (2026-09-09): a day switch flips
   .visible to a section that does not have .grid-on yet (render() adds it a
   moment later), so without this a newly visible section's list would paint
   for a frame before the grid replaces it. Keyed on the root instead of the
   section, this hides every section's list the instant Grid turns on, no
   matter which section is visible when. See app.js gridOn/gridOff. */
html[data-nn-grid-on="true"] .day-section > .venue-card, html[data-nn-grid-on="true"] .day-section > .qt-venue.in-list,
html[data-nn-grid-on="true"] .day-section > .saved-empty { display: none !important; }
html[data-nn-grid-on="true"] .day-section > .day-header { display: none !important; }

.tv-grid {
  position: relative; overflow: auto;
  scrollbar-width: none; -ms-overflow-style: none;
  overscroll-behavior: contain;
  border-top: 1px solid var(--border);
}
.tv-grid::-webkit-scrollbar { width: 0; height: 0; display: none; }
.tv-inner { position: relative; width: max-content; }
.tv-row, .tv-head { display: flex; align-items: stretch; }
.tv-head { position: sticky; top: 0; z-index: 6; background: var(--bg); }
.tv-corner, .tv-name {
  position: sticky; left: 0; flex: 0 0 104px; box-sizing: border-box;
  background: var(--bg); border-right: 1px solid var(--border-strong);
}
.tv-corner {
  z-index: 7; border-bottom: 1px solid var(--border-strong);
  font-family: var(--font-mono); font-size: 0.55rem; color: var(--muted);
  text-transform: uppercase; letter-spacing: 0.06em;
  padding: 8px 10px; display: flex; align-items: flex-end;
  cursor: pointer;
}
/* The corner cell doubles as a jump-to-now target (2026-09-09): a small dot
   after its text while the line is off screen, cleared by syncJump in
   app.js once the line is back in view or there is no line at all. */
.tv-corner.off::after {
  content: ""; display: inline-block; width: 6px; height: 6px;
  border-radius: var(--radius-circle); background: var(--green); margin-left: 6px; margin-bottom: 2px;
}
.tv-axis { position: relative; flex: 0 0 auto; height: 34px; border-bottom: 1px solid var(--border-strong); }
.tv-hour {
  position: absolute; bottom: 6px; transform: translateX(-50%);
  font-family: var(--font-mono); font-size: 0.58rem; color: var(--muted); white-space: nowrap;
}
.tv-hour::after { content: ""; position: absolute; left: 50%; bottom: -7px; width: 1px; height: 5px; background: var(--border-strong); }
.tv-name {
  z-index: 4; border-bottom: 1px solid var(--border-event-rule);
  padding: 6px 8px 6px 10px; display: flex; flex-direction: column; justify-content: center;
  min-width: 0; cursor: pointer;
}
.tv-name .tv-n { font-size: 0.74rem; font-weight: 600; line-height: 1.15; overflow-wrap: anywhere; color: var(--text); }
.tv-name.premier .tv-n, .tv-name.featured .tv-n { color: var(--gold); }
.tv-name .tv-tier { font-family: var(--font-mono); font-size: 0.46rem; letter-spacing: 0.08em; color: var(--gold); text-transform: uppercase; margin-bottom: 2px; }
.tv-name .tv-un { font-family: var(--font-mono); font-size: 0.5rem; color: var(--muted); margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tv-name .tv-un i { font-style: normal; color: var(--dimmer); }
.tv-lane {
  position: relative; flex: 0 0 auto; border-bottom: 1px solid var(--border-event-rule);
  background-image: linear-gradient(to right, var(--border-event-rule) 1px, transparent 1px);
  background-size: 96px 100%;
}
/* Each block carries its category as a class; the class maps it to the
   category ink, which paints the left rule and a 16% tint. */
.tv-blk {
  --blk-ink: var(--events);
  position: absolute; top: 4px; height: 36px; box-sizing: border-box;
  padding: 0 8px; border-radius: var(--radius-sm);
  display: flex; align-items: center; white-space: nowrap; cursor: pointer;
  /* The pair matters: overflow:clip trims the label without making the block
     a scroll container, so the sticky .tv-lbl inside sticks to the grid's
     scrollport. Older WebKit ignores clip and keeps the hidden behaviour. */
  overflow: hidden; overflow: clip;
  border-left: 3px solid var(--blk-ink);
  background: color-mix(in srgb, var(--blk-ink) 16%, transparent);
  color: var(--text); font-size: 0.7rem; font-weight: 500;
}
.tv-blk.music { --blk-ink: var(--music); }
.tv-blk.drinks { --blk-ink: var(--drinks); }
.tv-blk.food { --blk-ink: var(--food); }
.tv-blk.sports { --blk-ink: var(--sports); }
.tv-blk.events { --blk-ink: var(--events); }
.tv-blk.info { --blk-ink: var(--info); }
/* A block that starts before the visible window would read as empty, so its
   label rides along and parks against the name column (104px) plus the
   block's own 8px padding. */
.tv-lbl {
  position: sticky; left: 112px;
  display: inline-flex; align-items: center;
  min-width: 0; max-width: 100%;
}
.tv-lbl > span:first-child { overflow: hidden; text-overflow: ellipsis; }
.tv-blk .tv-t { font-family: var(--font-mono); font-size: 0.52rem; color: var(--muted); margin-left: 6px; flex-shrink: 0; }
.tv-blk.live { outline: 1.5px solid var(--green); outline-offset: -1.5px; }
.tv-blk.past { opacity: 0.45; }
.tv-now { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--green); z-index: 3; pointer-events: none; }
/* Non-today days (2026-09-09): same time-of-day, but dashed instead of
   solid, since it reads as "this time of day" rather than "live now". The
   NOW label is unchanged: it centers on the line either way, and a
   zero-width dashed line still needs a left edge to sit on. */
.tv-now.ghost { width: 0; background: transparent; border-left: 2px dashed var(--green); }
.tv-now::before {
  content: "NOW"; position: absolute; top: 2px; left: -14px;
  font-family: var(--font-mono); font-size: 0.48rem; letter-spacing: 0.06em;
  color: var(--bg); background: var(--green); padding: 1px 4px; border-radius: var(--radius-xs);
}
/* The Now chip (2026-09-09): a wrapper around the box, not inside its
   scrolled content, so the button stays put over the box while .tv-inner
   scrolls both ways underneath it. app.js toggles .show via syncJump. */
.tv-wrap { position: relative; }
.tv-jump {
  display: none; position: absolute; right: 10px; top: 44px; z-index: 8;
  align-items: center; justify-content: center;
  min-width: 44px; min-height: 44px; box-sizing: border-box;
  background: var(--green); color: var(--bg); font-family: var(--font-mono);
  font-size: 0.6rem; letter-spacing: 0.06em; text-transform: uppercase;
  padding: 4px 10px; border-radius: var(--radius-pill); border: 0;
  box-shadow: 0 2px 8px var(--shadow-card); cursor: pointer;
}
.tv-jump.show { display: flex; }
.tv-empty { padding: 2rem 1rem; text-align: center; font-family: var(--font-mono); font-size: 0.72rem; color: var(--muted); }
/* The card a grid tap landed on: a short gold ring, cleared by app.js. */
.venue-card.grid-landed { outline: 2px solid var(--gold); outline-offset: 2px; }

/* ===== Map view (2026-09-28, features.map_view) =====
   The Map bar button swaps the visible day's card list for a Leaflet map
   (app.js builds .map-wrap above the first day section). Dots: filled gold
   for live now, a blinking gold ring for starting within 20 minutes, grey
   for listed but not on now, a smaller plain grey dot for nothing listed;
   a category symbol inside, a count badge when venues share a building,
   the short name underneath (grey dots' names from zoom 16 only). */
html[data-nn-map-on="true"] .day-section { display: none !important; }
/* Open map (2026-10-02, Isaac on Android): a drag that started on the
   bottom bar scrolled the whole page, map and all. The bar takes no touch
   panning while the map is open (taps still work), and the sticky day nav
   only pans sideways (its tab row scrolls horizontally; set on every
   descendant too, since a scroller does not inherit the wrapper's value). The page itself
   keeps its scroll: app.js places the map with scrollBy, and locking the
   page with overflow: hidden broke that on Android (the page stayed where
   it was, showing its foot). */
html[data-nn-map-on="true"] .fab-bar { touch-action: none; }
html[data-nn-map-on="true"] .day-nav-wrapper,
html[data-nn-map-on="true"] .day-nav-wrapper * { touch-action: pan-x; }
/* Isaac, 2026-10-05: a drag that started on Now | Busy, Downtown or Near
   me scrolled the page. Those controls sit over the map but outside
   Leaflet's container, so they kept the default touch-action; nothing on
   the map pans the page now (taps still work). */
.map-wrap, .map-wrap > * { touch-action: none; }
/* Isaac, 2026-10-06: dragging anywhere on the open Map must move the map
   only. The venue sheet scrolls itself but no longer hands the drag to the
   page at its end; any other sheet over the map (busy check-in, app
   prompt) takes no panning; and the page has no pull-to-refresh or rubber
   band. app.js adds a touchmove backstop for the browsers that still
   would. */
.nn-map-sheet { overscroll-behavior: contain; }
html[data-nn-map-on="true"] .nn-sheet:not(.nn-map-sheet) { touch-action: none; }
html[data-nn-map-on="true"], html[data-nn-map-on="true"] body { overscroll-behavior: none; }
.map-wrap { position: relative; margin: 0 -1rem; }
.map-wrap[hidden] { display: none; }
.nn-map { position: absolute; inset: 0; background: var(--map-ground); }
/* leaflet.min.css loads after this sheet (on first open), so these carry an
   extra class to beat .leaflet-container's grey ground and the attribution
   control's white chip and blue links. */
.nn-map.leaflet-container { background: var(--map-ground); }
/* Dims the OSM tiles for the dark theme; --map-tile-filter is none in
   light-theme. Only the tile pane is filtered: dots, labels, the sheet
   and the attribution control are separate panes/elements and stay put. */
.nn-map .leaflet-tile-pane { filter: var(--map-tile-filter); }
.map-home {
  position: absolute; top: 0.6rem; right: 0.6rem; z-index: 500;
  font: 700 0.75rem var(--font-body); padding: 0.4rem 0.7rem;
  background: var(--map-soon-bg); color: var(--map-soon-ink);
  border: 1px solid var(--border-strong); border-radius: var(--radius-pill); cursor: pointer;
}
.map-home[hidden] { display: none; }
/* Near me pill (2026-09-29, spec 2026-09-29-location-on-load-design.md):
   bottom centre, filled with the accent; an outline while the map is
   centred on the reader; dimmed while "Finding you". Hidden under a venue
   sheet (app.js). 1rem up, not 0.75rem: the attribution line (bottom
   right, about 13 px tall) reaches under the pill at 360 px wide. */
.map-locate {
  position: absolute; left: 50%; bottom: 1rem; transform: translateX(-50%); z-index: 500;
  display: inline-flex; align-items: center; gap: 0.5rem;
  min-height: 44px; padding: 0 1.1rem 0 0.9rem;
  border-radius: var(--radius-pill); border: 2px solid var(--accent-cyan);
  background: var(--accent-cyan); color: var(--ink-on-highlight);
  font: 700 0.9rem var(--font-body); cursor: pointer; white-space: nowrap;
  box-shadow: 0 4px 16px var(--map-me-shadow);
}
.map-locate svg { flex: none; }
.map-locate.centred { background: var(--map-soon-bg); color: var(--accent-cyan); }
.map-locate.busy { opacity: 0.75; cursor: progress; }
.map-locate[hidden] { display: none; }
.nn-toast .map-locate-settings { margin-left: 0.4rem; }
/* Live busyness's tonight total, bottom-left (the Downtown chip is top
   right, the attribution bottom right), raised above the Near me pill's
   row so the two never meet on a narrow phone. Read-only, never takes a
   tap. */
.map-tonight {
  position: absolute; left: 0.6rem; bottom: 3.9rem; z-index: 500; pointer-events: none;
  font: 600 0.72rem var(--font-body); padding: 0.3rem 0.6rem;
  background: var(--map-soon-bg); color: var(--map-soon-ink);
  border: 1px solid var(--border-strong); border-radius: var(--radius-pill);
}
.map-tonight[hidden] { display: none; }
.map-empty {
  position: absolute; left: 1rem; right: 1rem; top: 40%; z-index: 500; text-align: center;
  font-size: 0.9rem; color: var(--map-label); pointer-events: none;
  text-shadow: 0 0 4px var(--map-label-halo), 0 0 4px var(--map-label-halo);
}
.map-empty[hidden] { display: none; }
.nn-dot-icon { background: none; border: 0; }
.nn-dot { position: relative; width: 26px; height: 26px; cursor: pointer; }
.nn-dot-c {
  display: flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; box-sizing: border-box;
  border-radius: var(--radius-circle); border: 2px solid;
}
.nn-dot-c svg { width: 13px; height: 13px; }
/* The events ticket (2026-10-05) draws as large as it fits, 1px inside the
   ring: at 13px its notches and perforation closed up. 20px in the 26px map
   dot, 18px in the 24px sheet-row dot. */
.nn-dot-c svg[data-cat="events"] { width: 20px; height: 20px; }
.nn-map-row .nn-dot-c svg[data-cat="events"] { width: 18px; height: 18px; }
.nn-dot.live .nn-dot-c { background: var(--map-live); border-color: var(--map-live); }
.nn-dot.live .nn-dot-c svg { fill: var(--map-live-ink); }
.nn-dot.starting .nn-dot-c {
  background: var(--map-start-bg); border-color: var(--map-start-ring);
  animation: nn-dot-blink 1s ease-in-out infinite;
}
.nn-dot.starting .nn-dot-c svg { fill: var(--map-start-ink); }
@keyframes nn-dot-blink {
  0%, 100% { box-shadow: 0 0 0 0 var(--map-start-glow); border-color: var(--map-start-ring); }
  50% { box-shadow: 0 0 0 4px var(--map-start-glow); border-color: var(--map-start-ring-dim); }
}
/* No blink under reduced motion: a steady, thicker ring instead. */
@media (prefers-reduced-motion: reduce) {
  .nn-dot.starting .nn-dot-c { animation: none; border-width: 3px; }
}
.nn-dot.idle .nn-dot-c { background: var(--map-idle-bg); border-color: var(--map-idle-ring); }
.nn-dot.idle .nn-dot-c svg { fill: var(--map-idle-ink); }
.nn-dot.none .nn-dot-c {
  width: 12px; height: 12px; margin: 7px; border-width: 2px;
  background: var(--map-none-bg); border-color: var(--map-none-ring);
}
.nn-map-row .nn-dot.none .nn-dot-c { width: 12px; height: 12px; margin: 6px; }
.nn-dot-n {
  position: absolute; top: -6px; right: -9px; min-width: 14px; padding: 0 3px;
  font: 700 9px/14px var(--font-mono); text-align: center;
  background: var(--map-badge-bg); color: var(--map-badge-ink); border-radius: var(--radius-pill-full);
}
.nn-dot-l {
  position: absolute; top: 28px; left: 50%; transform: translateX(-50%);
  white-space: nowrap; font: 700 10.5px var(--font-body); color: var(--map-label);
  text-shadow: 0 0 3px var(--map-label-halo), 0 0 3px var(--map-label-halo), 0 0 3px var(--map-label-halo);
  pointer-events: none;
}
/* Busy mode (mockup 2, 2026-10-01): heat colour (OrRd) only for a live
   crowd report, with a white ring and its name always shown; BestTime's
   usual value is a neutral grey pie filled to its share of the bar's own
   usual peak (--busy-pct, set per dot); no data is a small faded dot. */
.nn-dot.busy .nn-dot-c { border-color: var(--map-busy-ring); }
.nn-dot.busy[data-src="forecast"] .nn-dot-c {
  background: conic-gradient(var(--map-gauge-fill) 0 var(--busy-pct), var(--map-gauge-empty) var(--busy-pct) 100%);
}
.nn-dot.busy[data-src="live"] .nn-dot-c { box-shadow: 0 0 0 2px var(--map-busy-pip); }
.nn-dot.busy[data-lv="0"] .nn-dot-c { background: var(--map-busy-0); }
.nn-dot.busy[data-lv="1"] .nn-dot-c { background: var(--map-busy-1); }
.nn-dot.busy[data-lv="2"] .nn-dot-c { background: var(--map-busy-2); }
.nn-dot.busy[data-lv="3"] .nn-dot-c { background: var(--map-busy-3); }
.nn-dot.busy[data-src="none"] .nn-dot-c {
  width: 12px; height: 12px; margin: 7px; background: var(--map-busy-none); opacity: 0.55;
}
/* Busy mode labels: a live report always, the rest from zoom 16. */
.nn-map:not(.nn-map-near) .nn-dot.busy:not([data-src="live"]) .nn-dot-l { display: none; }
.nn-map-busy-line { margin: 0 0 0.6rem; font: 500 0.88rem var(--font-body); color: var(--text); }
.nn-map-busy-line b { font-weight: 700; }
/* The Now | Busy chip, top left (right of the zoom buttons where they show),
   and the legend under it while Busy is on. */
.map-busy-toggle {
  position: absolute; top: 0.6rem; left: 0.6rem; z-index: 500; display: inline-flex;
  border-radius: var(--radius-pill-full); overflow: hidden;
  background: var(--map-soon-bg); box-shadow: 0 2px 8px var(--map-me-shadow);
}
.map-wrap:has(.leaflet-control-zoom) .map-busy-toggle { left: 3.4rem; }
.map-busy-toggle button {
  border: 0; background: none; color: var(--map-soon-ink); cursor: pointer;
  font: 700 0.78rem var(--font-body); padding: 0.4rem 0.8rem;
}
.map-busy-toggle button[aria-pressed="true"] { background: var(--accent-cyan); color: var(--ink-on-highlight); }
.map-busy-toggle button:disabled { opacity: 0.45; cursor: default; }
.map-busy-legend {
  position: absolute; top: 2.9rem; left: 0.6rem; z-index: 500; max-width: calc(100% - 1.2rem);
  display: flex; flex-wrap: wrap; gap: 0.3rem 0.6rem; align-items: center;
  padding: 0.35rem 0.6rem; border-radius: var(--radius-md);
  background: var(--map-soon-bg); color: var(--map-soon-ink);
  font: 600 0.72rem var(--font-body); box-shadow: 0 2px 8px var(--map-me-shadow); pointer-events: none;
}
.map-wrap:has(.leaflet-control-zoom) .map-busy-legend { left: 3.4rem; max-width: calc(100% - 4rem); }
.map-busy-legend[hidden] { display: none; }
.mbl-row { display: flex; flex-wrap: wrap; gap: 0.25rem 0.55rem; align-items: center; width: 100%; }
.mbl-t { font-weight: 700; }
.mbl-i { display: inline-flex; align-items: center; gap: 0.25rem; }
.mbl-d { position: relative; width: 11px; height: 11px; border-radius: var(--radius-circle); }
.mbl-d[data-lv="0"] { background: var(--map-busy-0); }
.mbl-d[data-lv="1"] { background: var(--map-busy-1); }
.mbl-d[data-lv="2"] { background: var(--map-busy-2); }
.mbl-d[data-lv="3"] { background: var(--map-busy-3); }
.mbl-pie { background: conic-gradient(var(--map-gauge-fill) 0 var(--busy-pct), var(--map-gauge-empty) var(--busy-pct) 100%); }
.mbl-note { width: 100%; font-weight: 500; opacity: 0.8; }
/* Grey dots' names only once zoomed in (app.js toggles .nn-map-near at 16). */
.nn-map:not(.nn-map-near) .nn-dot.idle .nn-dot-l,
.nn-map:not(.nn-map-near) .nn-dot.none .nn-dot-l { display: none; }
/* Sponsor dots (2026-10-06): app.js puts .nn-sp + data-tier on a dot led
   by a Premier or Featured venue (a sponsor always leads its stack and sits
   above every other dot). A ring in the tier color around the 26px box,
   drawn as its own layer so it never fights the starting blink or the
   live-report pip (both box-shadows), and the name shows at every zoom. */
.nn-dot.nn-sp::before {
  content: ""; position: absolute; inset: -4px; border-radius: var(--radius-circle);
  border: 3px solid var(--map-sponsor-premier); pointer-events: none;
}
.nn-dot.nn-sp[data-tier="featured"]::before { border-color: var(--map-sponsor-featured); }
.nn-map:not(.nn-map-near) .nn-dot.nn-sp .nn-dot-l,
.nn-map:not(.nn-map-near) .nn-dot.busy.nn-sp:not([data-src="live"]) .nn-dot-l { display: block; }
.nn-dot.nn-sp .nn-dot-l { top: 31px; }
/* The presenter on the Map (2026-10-06), top right across from the Now |
   Busy chip: logo, "Brought to you by", the name. A link, like the strip. */
.map-presenter {
  position: absolute; top: 0.6rem; right: 0.6rem; z-index: 500;
  display: flex; align-items: center; gap: 0.45rem; max-width: calc(100% - 10.5rem);
  padding: 0.3rem 0.65rem 0.3rem 0.4rem; border-radius: var(--radius-md);
  background: var(--map-presenter-bg); color: var(--map-presenter-ink);
  border: 1px solid var(--map-presenter-edge); box-shadow: 0 2px 8px var(--map-me-shadow);
  text-decoration: none;
}
.map-wrap:has(.leaflet-control-zoom) .map-presenter { max-width: calc(100% - 13.5rem); }
/* The chip takes the top right, so the Downtown chip (shown after a pan)
   moves to the bottom right, clear of the Near me pill and 1rem up off
   the attribution line, the same lift the pill uses. */
.map-wrap:has(.map-presenter) .map-home { top: auto; bottom: 1rem; }
.mp-logo { height: 26px; width: auto; flex: none; }
.mp-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.15; }
.mp-eyebrow {
  font: 600 0.5rem var(--font-mono); letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--map-presenter-eyebrow);
}
.mp-name {
  font: italic 400 0.86rem var(--font-serif); color: var(--map-presenter-ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* You are here: a steady blue dot with a white ring and a faint accuracy
   circle, in its own pane between the tiles and the venue dots. Leaflet
   gives both no pointer events (interactive: false). No pulse. */
.nn-me-icon { background: none; border: 0; }
.nn-me {
  display: block; width: 18px; height: 18px; box-sizing: border-box;
  border-radius: var(--radius-circle); background: var(--map-me);
  border: 3px solid var(--map-me-ring); box-shadow: 0 0 3px var(--map-me-shadow);
}
.nn-map .nn-me-acc { fill: var(--map-me-acc-fill); fill-opacity: 1; stroke: var(--map-me-acc-line); stroke-opacity: 1; stroke-width: 1; }
.nn-map.leaflet-container .leaflet-control-attribution { font-size: 9px; background: var(--map-attrib-bg); color: var(--map-attrib-ink); }
.nn-map.leaflet-container .leaflet-control-attribution a { color: var(--map-attrib-link); }
/* The sheet a dot opens: the venue's own list card, moved in by app.js (or,
   for a stacked dot, one row per venue there). The card's cascade-in
   animation restarts when it is moved, so it is switched off in here.
   The top 2.3rem is a header strip: the grab handle (the swipe-down
   target, full width left of the close button) and the close button, so
   neither sits on the card's own header. */
.nn-map-sheet { text-align: left; padding: 2.3rem 1rem 1rem; max-height: 70vh; overflow-y: auto; }
.nn-map-sheet .venue-card { margin: 0; animation: none; opacity: 1; transform: none; }
.nn-map-sheet .nn-sheet-close { top: 0.1rem; padding: 0.2rem 0.6rem; }
.nn-map-grab { position: absolute; top: 0; left: 3rem; right: 3rem; height: 2.3rem; }
.nn-map-grab::before {
  content: ""; position: absolute; top: 0.6rem; left: 50%; width: 36px; height: 4px; margin-left: -18px;
  background: var(--dimmer); border-radius: var(--radius-pill-full);
}
.nn-map-sheet .nn-sheet-title { text-align: left; margin-bottom: 0.4rem; }
.nn-map-row {
  display: grid; grid-template-columns: 28px 1fr auto; gap: 0.6rem; align-items: center;
  width: 100%; padding: 0.55rem 0; background: none; border: 0; border-top: 1px solid var(--border);
  color: var(--text); text-align: left; cursor: pointer; font: inherit;
}
.nn-map-row .nn-dot, .nn-map-row .nn-dot-c { width: 24px; height: 24px; }
.nn-map-row-t { display: flex; flex-direction: column; min-width: 0; }
.nn-map-row-n { font-weight: 700; font-size: 0.9rem; }
.nn-map-row-s { font-size: 0.75rem; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.nn-map-row-go { color: var(--muted); font-size: 1.1rem; }
/* The sheet of a venue with nothing listed that day. */
.nn-map-none-a { font-size: 0.85rem; color: var(--text-dim); }
.nn-map-none-t { margin: 0.6rem 0; font-size: 0.9rem; color: var(--muted); }
.nn-map-dir {
  display: inline-block; padding: 0.35rem 0.8rem; font-size: 0.85rem; font-weight: 700;
  color: var(--text); text-decoration: none;
  border: 1px solid var(--border-strong); border-radius: var(--radius-pill);
}

.saved-fab svg { width: 1.4rem; height: 1.4rem; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linejoin: round; }
.saved-fab.active { color: var(--gold); }
.saved-fab.active svg { fill: var(--gold); stroke: var(--gold); }
.saved-fab.active .fab-label { color: var(--gold); }
.saved-empty {
  display: none;
  margin: 0.75rem 0 1rem;
  padding: 0.9rem 1rem;
  border: 1px dashed var(--border);
  border-radius: var(--radius-lg);
  color: var(--muted);
  font-size: 0.9rem; line-height: 1.45;
}
.saved-empty.visible { display: block; }
/* Saved filter (2026-09-08): a card that is neither saved nor a sponsor
   nor a guide card gets this class from app.js. display:none !important,
   same convention as .hood-hidden above, so it composes as an AND with
   the category filter's inline display toggling: a card shows only when
   both filters allow it. */
.venue-card.saved-hidden { display: none !important; }

/* ===== Live busyness (features.live_busyness, 2026-09-28) =====
   Spec: docs/superpowers/specs/2026-09-28-live-busyness-design.md.
   app.js sets data-live, --lv and --live on the strip; the reading takes
   the label and tonight's bar. */
.busyness-strip[data-live] .bs-label { display: flex; flex-direction: column; gap: 2px; }
.bs-live-level {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--font-mono); font-size: 0.8rem; font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--lv);
}
.bs-live-dot {
  width: 6px; height: 6px; flex: none;
  border-radius: var(--radius-circle); background: var(--lv);
  animation: bs-live-pulse 1.8s ease-in-out infinite;
}
@keyframes bs-live-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }
.bs-live-meta {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 0.74rem; color: var(--muted); font-variant-numeric: tabular-nums;
}
.bs-live-count { display: inline-flex; align-items: center; gap: 3px; }
.bs-live-count svg { width: 1em; height: 1em; fill: currentColor; }
.busyness-strip[data-live] .bs-bar.bs-live {
  background: var(--lv); opacity: 1; box-shadow: none; outline: 0;
  height: max(calc(var(--live) * 22px), 3px);
}
.busyness-strip[data-live] .bs-bar.bs-live::after { background: var(--lv); box-shadow: none; }
.busyness-strip.bs-dim .bs-label,
.busyness-strip.bs-dim .bs-bar.bs-live { opacity: 0.62; }
.busyness-strip.bs-dim .bs-live-dot { animation: none; }
/* One report (2026-10-01): the live bar and level drawn lighter, the dot
   steady, so a single person's reading looks tentative next to one several
   people agree on. */
.busyness-strip.bs-single .bs-bar.bs-live { opacity: 0.55; }
.busyness-strip.bs-single .bs-live-level { opacity: 0.7; }
.busyness-strip.bs-single .bs-live-dot { animation: none; }
.busyness-strip.bs-live-only { grid-template-columns: 1fr; grid-template-areas: "label"; }

.busy-ask {
  display: flex; flex-direction: column; gap: 8px;
  padding: 0.65rem 0 0.2rem;
  border-top: 1px dashed var(--border-dashed-quiet);
}
.busy-ask-q { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; font-size: 0.86rem; color: var(--text); }
.busy-ask-here {
  font-family: var(--font-mono); font-size: 0.58rem; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--green);
  border: 1px solid currentColor; border-radius: var(--radius-sm);
  padding: 0.12rem 0.35rem;
}
.busy-ask-levels { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; }
.busy-ask-btn {
  appearance: none; cursor: pointer;
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  padding: 8px 0 7px;
  background: var(--surface-tint-quiet); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  font: 600 0.78rem var(--font-body);
}
.busy-ask-btn:hover { border-color: var(--lv); }
.busy-ask-btn:focus-visible { outline: 2px solid var(--lv); outline-offset: 2px; }
.busy-ask-btn[disabled] { opacity: 0.5; cursor: default; }
.busy-meter { display: inline-flex; gap: 1px; color: var(--lv); }
.busy-meter svg { width: 11px; height: 11px; fill: currentColor; }
.busy-meter svg.off { opacity: 0.22; fill: var(--text-dim); }
.busy-ask-done { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; font-size: 0.84rem; color: var(--text-dim); }
.busy-ask-done b { color: var(--lv); }
.busy-ask-done > span:not(.busy-meter) { flex: 1 1 0; min-width: 0; }
.busy-ask-change {
  appearance: none; background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--neon); font: inherit; text-decoration: underline; text-underline-offset: 2px;
}
.busy-ask-err { font-size: 0.8rem; color: var(--red); }
/* Tonight's total and the private monthly count (2026-09-29): quiet lines. */
.busy-ask-tonight { font-size: 0.74rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.busy-ask-tonight[hidden] { display: none; }
.busy-ask-mine { display: block; margin-top: 2px; font-size: 0.74rem; color: var(--muted); }

/* Arrival nudge (2026-09-29): once per venue per bar-night, bottom-centre
   above the bar like the toast, dismissible. */
/* Arrival nudge (2026-10-04): a .nn-sheet with the four levels in it. */
.nn-sheet.busy-nudge .busy-nudge-levels { flex: 1 0 100%; }
.nn-sheet.busy-nudge .busy-nudge-lv { padding: 14px 0 12px; font-size: 0.9rem; }
.nn-sheet.busy-nudge .busy-nudge-lv .busy-meter svg { width: 13px; height: 13px; }
.nn-sheet.busy-nudge .nn-sheet-copy b { color: var(--lv); }
.busy-nudge-later {
  flex: 1 0 100%; appearance: none; cursor: pointer;
  background: none; border: 0; padding: 0.6rem 0 0.1rem;
  color: var(--muted); font: 600 0.85rem var(--font-body);
}
.nn-sheet.busy-nudge.busy-nudge-done .busy-nudge-levels,
.nn-sheet.busy-nudge.busy-nudge-done .busy-nudge-later { display: none; }
@media (prefers-reduced-motion: reduce) { .bs-live-dot { animation: none; } }

/* Live busyness debug override bar (settings.html, 13 taps on Version):
   shown while nn-debug-at places this phone at a venue. */
.busy-debug {
  position: fixed; z-index: 1000;
  top: calc(env(safe-area-inset-top, 0px) + 8px); left: 50%; transform: translateX(-50%);
  max-width: calc(100% - 32px); box-sizing: border-box;
  display: flex; align-items: center; gap: 10px;
  padding: 6px 10px; white-space: nowrap;
  background: var(--card); color: var(--text);
  border: 1px solid var(--gold); border-radius: var(--radius-sm);
  box-shadow: 0 4px 14px var(--shadow-floating);
  font-family: var(--font-mono); font-size: 0.72rem;
}
.busy-debug-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.busy-debug-off {
  flex: none; appearance: none; background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--neon); font: inherit; text-decoration: underline; text-underline-offset: 2px;
}

/* ===================================================================
   Display audit (2026-10-06): shorter cards and less chrome, every theme.
   =================================================================== */
/* The tagline stays in the page as its h1 (search) but off screen. */
.brand .logo-seo {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
/* An ended row shrinks to one dimmed line, sponsor cards included: what
   is over is not what anyone came for. */
.event.past .event-title {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1;
  overflow: hidden;
}
/* Long specials stop at two lines on a standard card; a tap opens the row
   (app.js adds .ev-open). Premier and Featured cards are never clamped, so
   a sponsor card keeps every line while the rest get shorter. */
.venue-card:not(.premier):not(.featured) .event:not(.past) .event-title {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
}
.venue-card .event.ev-open .event-title { display: block; -webkit-line-clamp: unset; }
/* The week toggle reads "16 more this week" on its own line above the
   icon row, its panel opening right under it (.venue-week is display:
   contents, so both are the pill row's own flex items). A Near me
   distance on the map pill can no longer push it onto a line alone. */
.venue-pills .vw-toggle.vw-toggle {
  order: -2; flex-basis: 100%; justify-content: flex-start;
  margin: 0; padding: 0.2rem 0; min-height: 32px; border: 0; background: none;
}
.venue-pills .vw-panel { order: -1; }
.venue-pills .vw-count { font-size: 0.74rem; letter-spacing: 0.04em; }
/* The Tonight card on the Tonight tab only (app.js syncSpotlightDay). */
.spotlight-wrap.nn-later-day, .spotlight-card.nn-later-day { display: none; }
/* Grid: the list's two presenter strips would sit back to back once the
   cards are hidden, so the grid keeps the first. */
.day-section.grid-on .whiskey-sponsor ~ .whiskey-sponsor { display: none; }
/* Map: a name that overlaps a higher-ranked name (app.js declutter). */
.nn-dot-l.nn-lbl-hide { visibility: hidden; }

/* ---- modern_v2/tokens.css: every design decision. Edit looks here. ---- */
/* Nittany Nights design tokens.
 *
 * EVERY design decision (color, radius, type stack) for the modern_v2 theme
 * lives here. app.css must contain no raw hex, no rgb()/rgba(), no literal
 * border-radius (other than 0) and no literal font-family stack: it refers to
 * these tokens through var() only. Swap this one file to reskin the site.
 *
 * Load order: tokens.css must come before app.css.
 */

/* --- Core palette: surfaces, ink, brand hues, category colors --- */
:root {
  /* Base surfaces, ink, brand hues and category colors.
     WARNING: these names are the per-city override contract. Each city's
     theme_accent block (config.json -> CITY_ACCENT_CSS) is injected after
     this file and redefines --accent, --accent2, --neon, --bg, --bg2,
     --card, --card-hover, --text, --text-dim, --muted and --dimmer by name.
     Do not rename them. Add role aliases pointing AT them if you want
     friendlier names (e.g. --surface-card: var(--card)), never the reverse. */
  --bg: #040810;
  --bg2: #080f1e;
  --card: #0b1220;
  --card-hover: #111c30;
  --border: rgba(255,255,255,0.07);
  --border-strong: rgba(255,255,255,0.14);
  --accent: #1e407c;
  --accent2: #4a7bc4;
  --neon: #85aee0;
  --text: #eef3fb;
  --text-dim: #b7c6db;
  --muted: #8ba4be;
  --dimmer: #5a7a9e;
  --green: #10b981;
  --amber: #f59e0b;
  --red: #f43f5e;
  /* Live busyness levels (2026-09-28): Quiet, Light, Busy, Packed. Built
     from the state inks so light mode and city accents follow for free. */
  --busy-lv-0: var(--green);
  --busy-lv-1: var(--neon);
  --busy-lv-2: var(--gold);
  --busy-lv-3: var(--red);

  --music: #38bdf8;
  --drinks: #f2b04a;
  --food: #4ade80;
  --sports: #fb923c;
  --events: #b794f4;
  --info: #22d3ff;      /* guide-card Info rows: the guide accent */
  --gold: #d4af37;
  /* Typography stacks */
  --font-body: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
  --font-body-simple: 'DM Sans', sans-serif;
  --font-display: 'Bebas Neue', sans-serif;
  --font-mono: 'DM Mono', monospace;
  --font-serif: 'Fraunces', serif;
  --font-serif-alt: 'Fraunces', 'DM Sans', serif;

  /* Corner radii. `--radius-pill` / `--radius-pill-full` are the two pill
     shapes; `--radius-circle` is a true circle (dots, glow orbs). */
  --radius-xs: 3px;
  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 10px;
  --radius-xl: 12px;
  --radius-2xl: 14px;
  --radius-pill: 100px;
  --radius-pill-full: 999px;
  --radius-circle: 50%;
  --radius-bar-top: 1.5px 1.5px 0 0;
  --radius-sheet-top: 18px 18px 0 0;

  /* Brand cyan: the app accent used for the wordmark, FABs, find bar and
     guide cards. Distinct from --accent / --accent2 (the city hues). */
  --accent-cyan: #22d3ff;
  --accent-cyan-bright: #7fe3ff;
  --accent-cyan-strong: rgba(34,211,255,0.55);
  --accent-cyan-half: rgba(34,211,255,0.5);
  --accent-cyan-edge: rgba(34,211,255,0.45);
  --accent-cyan-line: rgba(34,211,255,0.4);
  --accent-cyan-soft: rgba(34,211,255,0.35);
  --accent-cyan-fill-hover: rgba(34,211,255,0.30);
  --accent-cyan-ring: rgba(34,211,255,0.25);
  --accent-cyan-fill: rgba(34,211,255,0.16);
  --accent-cyan-wash: rgba(34,211,255,0.12);
  --accent-cyan-veil: rgba(34,211,255,0.06);

  /* Wordmark glyph, per-city overridable. Each city's theme_accent block is
     injected after this file as :root{--wm-accent:...}, so these MUST stay
     var(--wm-*, <nittany default>) chains: the inline fallback is the State
     College default and an undefined --wm-* is what lets a city override it.
     Never flatten these to a bare literal. */
  --wordmark-accent: var(--wm-accent, #22d3ff);
  --wordmark-glow-strong: var(--wm-glow-strong, rgba(34,211,255,0.9));
  --wordmark-glow-mid: var(--wm-glow-mid, rgba(34,211,255,0.55));
  --wordmark-glow-soft: var(--wm-glow-soft, rgba(34,211,255,0.35));
  /* Inline tube glyph box: the display face's cap height (Bebas Neue caps
     are 0.70em) and the gap after the glyph. See app.css "Wordmark tube
     glyphs"; nightprint overrides for Alfa Slab One. */
  --wm-cap: 0.70em;
  --wm-glyph-gap: 0.04em;

  /* Ink */
  --ink-bright: #fff;
  --ink-on-highlight: #070a12;

  /* QR codes (footer store links, app sheet): scanners want dark modules on
     a light field, so the tile stays white in every theme and mode. */
  --qr-paper: #ffffff;

  /* Translucent scrims behind sticky/floating chrome */
  --scrim-header: rgba(7,10,18,0.85);
  --scrim-ts-bar: rgba(7,10,18,0.6);
  --scrim-announce: rgba(7,10,18,0.55);
  --scrim-day-nav: rgba(7,10,18,0.92);
  --scrim-sheet-backdrop: rgba(4,8,16,0.65);

  /* Ambient background glow orbs */
  --glow-orb-1: #1e407c;
  --glow-orb-2: #6d28d9;
  --glow-orb-3: #4a7bc4;

  /* Surface tints and hairline rules (white at low alpha) */
  --surface-tint-faint: rgba(255,255,255,0.015);
  --surface-tint-quiet: rgba(255,255,255,0.018);
  --surface-tint-strip: rgba(255,255,255,0.02);
  --surface-tint-weather: rgba(255,255,255,0.025);
  --border-hairline-faint: rgba(255,255,255,0.03);
  --border-strip: rgba(255,255,255,0.04);
  --border-hairline: rgba(255,255,255,0.05);
  --border-event-rule: rgba(255,255,255,0.06);
  --border-dashed-quiet: rgba(255,255,255,0.1);

  /* Drop shadows */
  --shadow-card: rgba(0,0,0,0.35);
  --shadow-floating: rgba(0,0,0,0.45);
  --shadow-floating-hover: rgba(0,0,0,0.55);
  --shadow-sheet: rgba(0,0,0,0.5);
  --shadow-premier: rgba(0,0,0,0.8);
  --shadow-premier-hover: rgba(0,0,0,0.9);
  --shadow-text-emboss: rgba(0,0,0,0.4);

  /* Live state (green) */
  --state-live-glow: rgba(16,185,129,0.6);
  --state-live-border: rgba(16,185,129,0.35);
  --state-live-ring: rgba(16,185,129,0.08);
  --state-live-halo: rgba(16,185,129,0.05);
  --state-live-border-hover: rgba(16,185,129,0.55);
  --state-live-ring-hover: rgba(16,185,129,0.15);
  --state-live-halo-hover: rgba(16,185,129,0.1);
  --state-live-badge-border: rgba(16,185,129,0.45);
  --state-live-badge-bg: rgba(16,185,129,0.12);
  --state-live-dot-glow: rgba(16,185,129,0.9);

  /* Upcoming state (pink) */
  --state-upcoming-ink: #f472b6;
  --state-upcoming-dot: #ec4899;
  --state-upcoming-border: rgba(236,72,153,0.5);
  --state-upcoming-bg: rgba(236,72,153,0.12);
  --state-upcoming-dot-glow: rgba(236,72,153,0.95);

  /* Busyness strip bars */
  --busy-bar-from: rgba(74,123,196,0.45);
  --busy-bar-to: rgba(133,174,224,0.75);
  --busy-zero-from: rgba(148,163,184,0.35);
  --busy-zero-to: rgba(148,163,184,0.5);
  --busy-peak-from: rgba(244,63,94,0.6);
  --busy-peak-to: rgba(251,113,133,0.95);
  --busy-now-from: rgba(34,197,94,0.9);
  --busy-now-to: rgba(134,239,172,1);
  --busy-now-glow: rgba(74,222,128,0.75);
  --busy-now-outline: rgba(220,252,231,0.35);
  --busy-now-marker: #dcfce7;
  --busy-now-marker-glow: rgba(134,239,172,0.95);
  --busy-now-peak-from: rgba(34,197,94,0.95);
  --busy-now-peak-to: rgba(251,113,133,1);
  --busy-now-peak-glow: rgba(244,63,94,0.85);

  /* Category filter dot glows (pair with --music/--drinks/--food/--sports/--events) */
  --music-glow: rgba(56,189,248,0.7);
  --drinks-glow: rgba(242,176,74,0.7);
  --food-glow: rgba(74,222,128,0.7);
  --sports-glow: rgba(251,146,60,0.7);
  --events-glow: rgba(183,148,244,0.7);

  /* Outbound link pills */
  --pill-lineleap-ink: #93c5fd;
  --pill-lineleap-ink-hover: #bfdbfe;
  --pill-lineleap-bg-hover: rgba(96,165,250,0.08);
  --pill-ig-ink: #f0abfc;
  --pill-ig-ink-hover: #f5d0fe;
  --pill-ig-bg-hover: rgba(232,121,249,0.06);

  /* Premier (gold) sponsor card */
  --premier-inset: rgba(212,175,55,0.10);
  --premier-ring: rgba(212,175,55,0.18);
  --premier-edge: rgba(212,175,55,0.22);
  --premier-inset-hover: rgba(212,175,55,0.4);
  --premier-ring-hover: rgba(212,175,55,0.35);
  --premier-glow-hover: rgba(212,175,55,0.45);
  --premier-badge-border: rgba(212,175,55,0.55);
  --premier-badge-ink: #e8c855;
  --premier-sheen-fade: rgba(212,175,55,0.0);
  --pricing-premier-border: rgba(212,175,55,0.3);
  --pricing-premier-slot-border: rgba(212,175,55,0.25);
  --pricing-premier-slot-bg: rgba(212,175,55,0.05);

  /* Whiskey Barber sponsor block */
  --whiskey-surface: #1a130a;
  --whiskey-ink: #f5e9c8;
  --whiskey-ink-hover: #fff7e0;
  --whiskey-gold-bright: rgba(229,200,120,0.85);
  --dp-name-ink: rgba(229,200,120,0.92);
  --whiskey-border-hover: rgba(229,200,120,0.55);
  --whiskey-inset: rgba(255,235,180,0.06);
  --whiskey-inset-hover: rgba(255,235,180,0.10);
  --whiskey-tag-corner-border: rgba(229,200,120,0.4);
  --whiskey-tag-corner-ink: rgba(229,200,120,0.78);
  --whiskey-sub-ink: rgba(229,200,120,0.65);
  --whiskey-cta-border: rgba(229,200,120,0.45);
  --whiskey-cta-bg-hover: rgba(229,200,120,0.12);

  /* Featured (blue) sponsor card and pricing CTAs */
  --featured-wash: rgba(74,123,196,0.05);
  --featured-border: rgba(74,123,196,0.35);
  --featured-badge-border: rgba(74,123,196,0.4);
  --pricing-featured-border: rgba(74,123,196,0.3);
  --pricing-featured-slot-border: rgba(74,123,196,0.25);
  --pricing-cta-border: rgba(74,123,196,0.5);

  /* Announcement bar */
  --announce-lead-ink: #fcd34d;
  --announce-lead-bg: #060b16;

  /* Staging banner */
  --staging-bg: rgba(239,68,68,0.15);
  --staging-border: rgba(239,68,68,0.5);
  --staging-ink: #fca5a5;

  /* In-page find (CSS Custom Highlight API) */
  --find-current-bg: #d4af37;

  /* Find results panel (2026-09-08) */
  --find-results-max-h: calc(100vh - 5.5rem - var(--fab-bar-h) - env(safe-area-inset-top) - env(safe-area-inset-bottom));
  --find-results-pad: 0.5rem 0.6rem 0.7rem;
  --find-results-gap: 0.35rem;
  --find-summary-ink: rgba(230,240,255,0.62);
  --find-summary-size: 0.72rem;
  --find-group-head-ink: #eaf6fb;
  --find-group-head-size: 0.88rem;
  --find-also-ink: rgba(230,240,255,0.55);
  --find-row-bg: rgba(255,255,255,0.03);
  --find-row-bg-hover: rgba(255,255,255,0.07);
  /* Left room on every row for the category dot (see --find-row-dot-*). */
  --find-row-pad: 0.45rem 0.55rem 0.45rem 1.35rem;
  --find-row-radius: 0.55rem;
  --find-row-when-ink: rgba(230,240,255,0.7);
  --find-row-when-size: 0.72rem;
  --find-row-what-size: 0.86rem;
  --find-row-rule-w: 3px;
  --find-row-rule-premier: var(--gold);
  --find-row-rule-featured: #4a7bc4;
  /* A venue hit is a grouped card (2026-09-12): the head is a filled bar
     that reads as a control, and the venue's nights hang off a spine under
     it. Day groups under "Also in events" stay plain. */
  --find-group-card-bg: rgba(255,255,255,0.015);
  --find-group-card-border: var(--border);
  --find-group-card-radius: 0.7rem;
  --find-group-head-bg: var(--accent-cyan-wash);
  --find-group-head-bg-hover: var(--accent-cyan-fill);
  --find-group-head-border: var(--accent-cyan-ring);
  --find-group-head-gap: 0.6rem;
  --find-group-head-pad: 0.6rem 0.65rem;
  --find-group-sub-size: 0.68rem;
  --find-group-sub-gap: 0.3rem;
  --find-group-cta-ink: var(--accent-cyan-bright);
  --find-group-cta-bg: var(--accent-cyan-fill);
  --find-group-cta-border: var(--accent-cyan-edge);
  --find-group-cta-size: 0.7rem;
  --find-group-cta-pad: 0.28rem 0.6rem;
  --find-spine-line: rgba(255,255,255,0.09);
  --find-spine-rule: rgba(255,255,255,0.05);
  --find-spine-x: 0.72rem;
  /* Category dot on a result row. Overridden per row by the .cat-* class,
     which points it at the same --music/--drinks/... hues the venue cards
     use for their event type stripes. */
  --find-row-dot: var(--dimmer);
  --find-row-dot-size: 0.44rem;
  --find-row-dot-x: 0.5rem;
  --find-row-dot-y: 0.75rem;
  --find-row-dot-ring: var(--card);
  --find-mark-bg: var(--accent-cyan-soft);
  --find-mark-radius: 2px;
  --find-empty-ink: rgba(230,240,255,0.7);
  /* Landing ring: the brief outline on the card or Quiet Tonight tile a
     tapped venue head jumps to. */
  --find-landed-ring-w: 2px;
  --find-landed-ring: var(--accent-cyan-strong);
  --find-landed-radius: 0.5rem;
  --find-landed-fade: 1.2s;  /* keep in step with FLASH_MS in app.js */

  /* Fixed-event guide cards */
  --guide-card-wash-fade: rgba(7,10,18,0);
  --guide-blurb-ink: rgba(230,240,255,0.78);
  --guide-teaser-ink: rgba(230,240,255,0.92);
  --guide-loc-ink: rgba(230,240,255,0.6);
  --guide-map-link-ink: #eaf6fb;

  /* Map view (2026-09-28): dots are fill-for-time, symbol-for-type.
     Colours match the list (2026-09-29): live is the LIVE NOW badge's
     green with the badge's --bg ink; starting is the Upcoming badge's
     colour. The Map bar icon's dot uses --map-live too. */
  --map-live: var(--green);
  --map-live-ink: var(--bg);
  /* Map Busy mode (2026-10-01): ColorBrewer OrRd, Quiet to Packed, chosen
     from the palette comparison; the light map uses the darker four steps
     (its palest would vanish on the white tiles). Ink flips to white on
     the two dark steps. No data is the muted grey, faded. */
  --map-busy-0: #fef0d9;
  --map-busy-1: #fdcc8a;
  --map-busy-2: #fc8d59;
  --map-busy-3: #d7301f;
  --map-busy-ink-lo: #4a1d05;
  --map-busy-ink-hi: #ffffff;
  --map-busy-none: var(--muted);
  /* The usual-busyness pie: a neutral grey on purpose, so heat colour only
     ever means a live report. */
  --map-gauge-fill: #c9d2de;
  --map-gauge-empty: #2a3a52;
  --map-busy-ring: var(--bg);
  --map-busy-pip: #ffffff;
  /* The Downtown chip. */
  --map-soon-bg: var(--bg);
  --map-soon-ink: var(--text);
  /* Starting within the hour (window.nnUpcomingMin, the list's Upcoming
     window): a ring in the Upcoming colour (blinking) on the page fill. */
  --map-start-bg: var(--bg);
  --map-start-ring: var(--state-upcoming-ink);
  --map-start-ring-dim: color-mix(in srgb, var(--map-start-ring) 35%, transparent);
  --map-start-glow: color-mix(in srgb, var(--map-start-ring) 45%, transparent);
  --map-start-ink: var(--state-upcoming-ink);
  /* Grey: listed that day but nothing on now, and nothing listed (smaller,
     no symbol). The light theme swaps in light greys below. */
  --map-idle-bg: var(--dimmer);
  --map-idle-ring: var(--muted);
  --map-idle-ink: var(--text);
  --map-none-bg: var(--dimmer);
  --map-none-ring: var(--bg);
  --map-label: var(--text);
  --map-label-halo: var(--bg);
  --map-badge-bg: var(--text);
  --map-badge-ink: var(--bg);
  /* Sponsor dots (2026-10-06): a ring in the tier's color around the
     dot, Premier gold and Featured blue, matching the list's card borders. */
  --map-sponsor-premier: #d4af37;
  --map-sponsor-featured: #4a7bc4;
  /* The presenter's "Brought to you by" chip on the Map, in the presenter
     strip's colors. */
  --map-presenter-bg: var(--whiskey-surface);
  --map-presenter-ink: var(--whiskey-ink);
  --map-presenter-eyebrow: var(--dp-name-ink);
  --map-presenter-edge: var(--premier-badge-border);
  --map-ground: var(--bg2);
  /* You are here (2026-09-29): the reader's own position, the usual map
     blue with a white ring, plus a faint accuracy circle. Its own blue:
     the theme blues are pale (--neon, --accent-cyan) or follow a city's
     theme_accent (--accent2), and this one has to read as "you" on both
     tile themes whatever the city's colours. */
  --map-me: #2f7df6;
  --map-me-ring: var(--ink-bright);
  --map-me-shadow: color-mix(in srgb, var(--ink-on-highlight) 45%, transparent);
  --map-me-acc-fill: color-mix(in srgb, var(--map-me) 14%, transparent);
  --map-me-acc-line: color-mix(in srgb, var(--map-me) 40%, transparent);
  /* The OSM credit, over Leaflet's own white chip and blue links. */
  --map-attrib-bg: color-mix(in srgb, var(--bg) 80%, transparent);
  --map-attrib-ink: var(--text-dim);
  --map-attrib-link: var(--text);
  /* Dims the OSM tile pane only (dots, labels, sheet and attribution stay
     unfiltered) so streets read as light lines on a navy-dark ground. */
  --map-tile-filter: invert(1) hue-rotate(180deg) brightness(0.9) contrast(0.85) saturate(0.6);

  /* Misc dots and sheens */
  --dot-all-bg: rgba(255,255,255,0.55);
  --dot-all-glow: rgba(255,255,255,0.25);
  --sheen-mid: rgba(255,255,255,0.38);
  --sheen-edge: rgba(255,255,255,0);

  /* Light theme: colors used by the :root.light-theme rules in app.css */
  --light-scrim-header: rgba(255,255,255,0.85);
  --light-scrim-ts-bar: rgba(255,255,255,0.6);
  --light-scrim-day-nav: rgba(244,245,247,0.92);
  --light-announce-bg: rgba(255,255,255,0.55);
  --light-announce-lead-bg: #e9ebef;
  --light-amber-ink: #92600a;
  --light-surface-quiet: rgba(15,23,42,0.035);
  --light-surface-quiet-hover: rgba(15,23,42,0.03);
  --light-border-rule: rgba(15,23,42,0.10);
  --light-border-rule-faint: rgba(15,23,42,0.07);
}

/* --- Light theme palette overrides (moved from app.css) --- */
:root.light-theme {
  --bg: #f4f5f7;
  --bg2: #e9ebef;
  --card: #ffffff;
  --card-hover: #f1f3f8;
  --border: rgba(15,23,42,0.10);
  --border-strong: rgba(15,23,42,0.20);
  --text: #171a21;
  --text-dim: #414958;
  --muted: #5d6675;
  --dimmer: #8b94a5;
  --green: #059669;
  --amber: #b45309;
  --red: #e11d48;
  --music: #0369a1;
  --drinks: #b45309;
  --food: #15803d;
  --sports: #c2410c;
  --events: #7c3aed;
  --info: #0e7490;
  --gold: #a16207;
  /* Live busyness levels (F3, 2026-09-28 review): dedicated colours, not
     var(--green)/var(--neon)/var(--gold)/var(--red), so they hold at least
     4.5:1 against every light background this theme (or a theme that
     inherits from it, e.g. nightprint, which has no light block of its
     own) ships: modern_v2's own #f4f5f7 and nittany's PSU light bg
     #E5EDF8 (config.json theme_accent_light.bg). The shared tokens read
     around 2.4-3.3:1 there; see final-fix-report.md F3 for the computed
     ratios. Same hue family as the dark tokens: green, blue, amber, red.
     This block wins over any plain :root --busy-lv-* (e.g. nightprint's
     --busy-lv-3: var(--pin)) because :root.light-theme is more specific. */
  --busy-lv-0: #046c4e;
  --busy-lv-1: #0369a1;
  --busy-lv-2: #8a5a00;
  --busy-lv-3: #be123c;
  --find-summary-ink: #5d6675;
  --find-group-head-ink: #171a21;
  --find-also-ink: #5d6675;
  --find-row-bg: rgba(15,23,42,0.035);
  --find-group-card-bg: rgba(15,23,42,0.02);
  /* The cyan head bar and its CTA need real ink on a white ground: the
     dark theme's --accent-cyan-bright washes out completely here. */
  --find-group-head-bg: rgba(3,105,161,0.08);
  --find-group-head-bg-hover: rgba(3,105,161,0.14);
  --find-group-head-border: rgba(3,105,161,0.22);
  --find-group-cta-ink: #0b607d;
  --find-group-cta-bg: rgba(3,105,161,0.10);
  --find-group-cta-border: rgba(3,105,161,0.35);
  --find-spine-line: rgba(15,23,42,0.14);
  --find-spine-rule: rgba(15,23,42,0.07);
  --find-row-bg-hover: rgba(15,23,42,0.07);
  --find-row-when-ink: #414958;
  --find-mark-bg: rgba(3,105,161,0.22);
  --find-empty-ink: #414958;
  --map-tile-filter: none;
  /* Map dots (2026-09-29): true light greys, mixed from the page fill and
     --muted, so idle and none sit clearly quieter than green live and the
     Upcoming ring; the symbol keeps --text-dim for legibility. The
     starting symbol darkens the Upcoming colour toward --text: the pale
     dark-theme tints wash out on a light fill. */
  --map-busy-0: #fdbb84;
  --map-busy-1: #fc8d59;
  --map-busy-2: #e34a33;
  --map-busy-3: #b30000;
  --map-busy-ring: #ffffff;
  --map-busy-pip: #ffffff;
  --map-gauge-fill: #5b6b80;
  --map-gauge-empty: #dde2ea;
  --map-idle-bg: color-mix(in srgb, var(--muted) 22%, var(--bg));
  --map-idle-ring: color-mix(in srgb, var(--muted) 50%, var(--bg));
  --map-idle-ink: var(--text-dim);
  --map-none-bg: color-mix(in srgb, var(--muted) 26%, var(--bg));
  --map-none-ring: color-mix(in srgb, var(--muted) 50%, var(--bg));
  --map-start-ring: color-mix(in srgb, var(--state-upcoming-ink) 55%, var(--text));
  --map-start-ink: color-mix(in srgb, var(--state-upcoming-ink) 45%, var(--text));
}
