/* ---- 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); }

.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;
}
/* App promo cycle in the updated bar (see app.js). .ts-fade is an inner
   wrapper injected only for eligible Android/iOS visitors; fading it keeps
   the bar's border/background solid during the content swap. The .ts-promo
   rule out-specifies the generic .ts-bar a underline rule above. */
.ts-fade { transition: opacity 0.3s ease; }
.ts-bar a.ts-promo, .ts-bar a.ts-promo:link, .ts-bar a.ts-promo:visited {
  color: var(--text-dim); text-decoration: none;
}
.promo-play {
  width: 10px; height: 10px; margin-right: 6px;
  vertical-align: middle; position: relative; top: -1px;
}
.promo-appstore {
  width: 11px; height: 11px; margin-right: 6px;
  vertical-align: middle; position: relative; top: -1px;
  color: var(--wordmark-accent);
}
.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; }
}
.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); }
.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: center;
  gap: 10px; flex-wrap: wrap;
}
.play-badge-row a { display: block; line-height: 0; }
.play-badge-row img { height: 44px; width: 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 {
  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 {
  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)
   re-enables pointer events so the store link can be tapped. */
.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 { pointer-events: auto; }
.nn-toast a { color: var(--accent-cyan); font-weight: 600; }

/* Dwell app sheet: dimmed backdrop + slide-up bottom card (built by the
   app.js dwell-sheet block; mobile browsers only). 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); }
@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 .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;
}

/* ===== 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 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; }
/* 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 .g-now, .grid-fab.fidget .g-blk { animation: none; }
}
.day-section.grid-on > .venue-card, .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 > .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; }

.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; }

/* ---- nightprint/app.css ---- */
/* Nightprint structural overrides.
 *
 * Concatenated AFTER modern_v2/app.css and after both tokens files, so this
 * only has to state what STRUCTURE differs. Colour, type and radius are
 * already handled by tokens.css.
 *
 * Three jobs:
 *   1. delete the decoration layer (orbs, noise, glass, entrance animation)
 *   2. turn the card stack into a ruled listing with a left time gutter
 *   3. give the page a masthead and a stamped LIVE mark
 */

/* ===================================================================
   1. DECORATION LAYER: DELETED
   The drifting blurred gradient orbs are the single most recognisable
   AI-landing-page artifact there is, and one of them was Tailwind
   violet-700. None of this carries information.
   =================================================================== */
.glow, .glow-1, .glow-2, .glow-3 { display: none !important; }
body::before { display: none !important; }          /* feTurbulence noise */

header, .day-nav-wrapper, .staging-banner,
.fab-bar, .near-hint, .find-bar {
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
}

/* Content that animates in for no reason other than that it can. On a page
   you want to SCAN, the cascade costs you. */
/* Kill the staggered entrance only. NOT !important: that also suppressed
   the LIVE ring animation added later in this file, since !important beats
   a more specific selector. Plain declarations are enough, because this
   rule already comes after the base one at equal specificity. */
.venue-card { animation: none; opacity: 1; transform: none; }
.venue-card:hover { transform: none; box-shadow: none; }

body { background: var(--bg); color: var(--text); }

/* ===================================================================
   2. THE LISTING
   =================================================================== */

/* A printed listing separates with a rule and whitespace, not with a
   rounded card and a drop shadow repeated forty times. */
.venue-card {
  background: none;
  border: 0;
  border-top: 1px solid var(--border-strong);
  border-radius: 0;
  padding: 1.05rem 0 1.15rem;    /* no side padding: a rule, not a box */
  margin: 0;
}
.venue-card:hover { background: var(--surface-tint-faint); }

/* A BORDERED card is a different problem from a ruled row. Type set hard
   against a drawn border reads as a mistake, and the tighter the border the
   more air the text needs inside it. Borders can sit close to each other;
   text cannot sit close to a border. So every boxed block gets real interior
   padding back, and the box itself gets separated from its neighbours. */
.venue-card.premier,
.venue-card.featured,
.venue-card.guide {
  background: var(--card);
  padding: 0.95rem 1.15rem 1.05rem;
  margin: 1.1rem 0;
  border-top-width: 1px;
}
.venue-card.premier { border-left-width: 4px; }

/* Downtown tonight card: same boxed treatment as the premier/featured/guide
   blocks above, no rounded corners, no drop shadow. */
.spotlight-card { background: var(--card); border: 1px solid var(--border-strong); border-radius: 0; padding: 0.95rem 1.15rem 1.05rem; }
.spotlight-kicker { color: var(--neon); }

/* The premier badge straddles the top border, so it needs its own inset or
   it collides with the venue name beneath it. */
.venue-card.premier .premier-badge,
.venue-card.premier .sponsor-badge { margin-bottom: 0.5rem; }

/* Same rule for every other boxed block on the page. Note the converse of
   the principle: this padding is for blocks whose TEXT meets the border.
   Quiet Tonight is excluded because its contents are themselves bordered
   cards, and a border may sit close to another border. Adding the text
   inset there stacked with .qt-grid's own padding and pushed the inner
   cards 30px off the dashed frame. */
.guide-card, .no-results,
.tonight-forecast, .whiskey-sponsor, .pricing-card {
  padding-left: 1.1rem;
  padding-right: 1.1rem;
}

/* Quiet Tonight: card-inside-a-frame, so keep the frame tight. The summary
   row is text against the border and keeps its own inset. */
.quiet-tonight { padding-left: 0; padding-right: 0; }
.qt-grid { padding: 0 0.4rem 0.45rem; gap: 0.28rem; }
.quiet-tonight > summary { padding: 0.7rem 0.85rem; }

/* Venue name at poster scale, with a dotted leader running to the address:
   the menu / index-page device, and unmistakably typeset rather than
   rendered. */
.venue-head {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.5rem 0.6rem;
  margin-bottom: 0.6rem;
}
.venue-name {
  font-family: var(--font-body);
  font-variation-settings: 'wdth' 78, 'wght' 800;
  font-size: 1.5rem; line-height: 1.02;
  letter-spacing: -0.015em; text-transform: uppercase;
  color: var(--text);
}
.venue-name:hover { color: var(--neon); }
.venue-meta {
  font-family: var(--font-body);
  font-variation-settings: 'wdth' 88;
  font-size: 0.86rem; letter-spacing: 0; text-transform: none;
  color: var(--text-dim);
}

/* Event rows: one line, three parts. Category label, then the item, then
   the time hard right.

   The scheme, so the parts read as a single row rather than three stranded
   fragments:
     - the label column is sized to the WIDEST label ("Drinks"/"Events") and
       no wider. Earlier it was 4.1rem, which left ~22px of dead space after
       a short label like "Music" and made the title look detached.
     - the row never wraps. The title wraps INSIDE its own box (min-width:0),
       so a long title pushes nothing: the time keeps its place instead of
       dropping to a line of its own.
     - the time gets its own min-width and right-aligns, so times stack into
       a clean right edge down the card without a rigid grid.
     - everything sits on a shared baseline, so a two-line title still reads
       as one row with the label and time on its first line. */
.event {
  display: flex;
  align-items: baseline;
  flex-wrap: nowrap;
  gap: 0 0.6rem;
  padding: 0.34rem 0;
  border-top: 0;
}
.event:first-child { padding-top: 0.34rem; }

.event-type {
  flex: 0 0 auto;
  width: auto;
  min-width: 3rem;                  /* fits "Drinks"/"Events" exactly */
  font-family: var(--font-mono);
  font-size: 0.6rem; font-weight: 700;
  letter-spacing: 0.06em;
  border-left: 3px solid;
  padding: 0 0 0 0.4rem;
  margin-top: 0;
}

.event-body { flex: 1 1 auto; min-width: 0; }
.event-title {
  font-family: var(--font-serif);   /* a serif in the rows: no generated dark
                                       listing page has ever used one */
  font-size: 1.02rem; font-weight: 400; line-height: 1.32;
}

.event-time {
  flex: 0 0 auto;
  min-width: 3.4rem;
  text-align: right;
  align-items: flex-end;
  font-family: var(--font-mono);
  font-size: 0.74rem; color: var(--text-dim);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  padding-top: 0;
}
.event-time:empty { min-width: 0; }
.event.live .event-type { box-shadow: none; }
.event.live .event-time .time-text { color: var(--green); font-weight: 700; }

/* ===================================================================
   3. MASTHEAD, NAV, STAMPS
   =================================================================== */

/* A publication, not an app: wood-type wordmark, hard rule beneath. */
header {
  background: var(--bg);
  /* Masthead rule: the same stroke the day tabs and category pills are drawn
     with (1.5px var(--border)), so every horizontal line in the top chrome is
     one line. It used to be 1.5px of var(--text), a bright cream band that
     was the loudest rule on the page and matched nothing else. */
  border-bottom: 1.5px solid var(--border);
}
.wordmark {
  font-family: var(--font-display);
  font-size: 2.1rem; letter-spacing: 0; line-height: 0.95;
  text-transform: uppercase; color: var(--text);
}
.wordmark .accent { color: var(--wordmark-accent); text-shadow: none; }
.tagline, .updated-bar {
  font-family: var(--font-mono);
  text-transform: uppercase; letter-spacing: 0.08em;
}

.day-label {
  font-family: var(--font-display);
  font-size: 2rem; letter-spacing: 0; text-transform: uppercase;
  color: var(--text);
}

/* Day tabs and filters: knockout blocks, not lozenges. */
.day-nav-wrapper { background: var(--bg); border-bottom: 1px solid var(--border-strong); }
.day-tab, .filter-pill {
  border-radius: 0;
  border: 1.5px solid var(--border);
  background: none;
  font-family: var(--font-body);
  font-variation-settings: 'wdth' 82, 'wght' 700;
  text-transform: uppercase; letter-spacing: 0.03em;
}
.day-tab.active, .filter-pill.active {
  background: var(--text); color: var(--bg); border-color: var(--text);
}
.day-tab.today { color: var(--neon); box-shadow: inset 0 -3px 0 var(--neon); }
.day-tab.active.today { color: var(--bg); }

/* LIVE NOW as a rubber stamp. One rotated element is the entire asymmetry
   budget on the page, and it buys a lot: it reads as something stamped ONTO
   the page rather than rendered by it. */
.live-badge {
  display: inline-block;
  font-family: var(--font-display);
  font-size: 0.66rem; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--bg); background: var(--green);
  border: 0; border-radius: 0;
  padding: 0.16rem 0.44rem;
  transform: rotate(-3.5deg);
}
.live-badge::before { content: none; }          /* drop the pulsing dot */
.live-dot { box-shadow: none; }
/* UPCOMING: the same stamp as LIVE NOW, in its own colour.
   Same wood-type face, same rotation, same breathing field, so the two
   states read as one family with the colour carrying the difference.
   The dot is dropped: it duplicated a signal the stamp already gives. */
.upcoming-badge {
  display: inline-block;
  font-family: var(--font-display);
  font-size: 0.66rem; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--bg);
  background: var(--state-upcoming-ink);
  border: 0; border-radius: 0;
  padding: 0.16rem 0.44rem;
  max-width: 100%;
  transform: rotate(-3.5deg);
  transform-origin: left center;
  animation: nn-upcoming-breathe 2.6s ease-in-out infinite;
}
.upcoming-badge::before { content: none; }

/* Floored at a mid lilac for the same reason the LIVE stamp is floored at a
   mid green: the lettering is the ground colour and would vanish at zero. */
@keyframes nn-upcoming-breathe {
  0%, 100% {
    background-color: var(--stamp-upcoming-floor);
    box-shadow: 0 0 0 0 var(--stamp-upcoming-halo-off);
  }
  50% {
    background-color: var(--state-upcoming-ink);
    box-shadow: 0 0 13px 1px var(--stamp-upcoming-halo);
  }
}

/* A blinking badge is the opposite of confident. */
@keyframes up-blink { 0%, 100% { opacity: 1; } }

/* Badges and pills: outlined ink, square.
   Boxing a previously borderless button changes its spacing needs. The base
   row sits at gap 0.35rem because bare icons can crowd safely; once each one
   carries a drawn border, the borders read as touching. Widen the gap and
   pad the interiors, or the row looks like a broken table. */
.age-badge, .downtown-badge, .hood-badge, .pill {
  border-radius: 0;
  font-family: var(--font-mono);
  letter-spacing: 0.04em;
}
.age-badge, .downtown-badge, .hood-badge { padding: 0.16rem 0.5rem; }

.venue-pills {
  gap: 0.55rem;
  row-gap: 0.5rem;
  margin-top: 0.7rem;
  padding-top: 0.7rem;
}
.pill {
  border: 1px solid var(--border);
  padding: 0.5rem 0.85rem;
}
.pill:hover { border-color: var(--border-strong); }
/* The share/count cluster on the right is a separate group: give it a real
   gap from the link pills rather than letting it butt against them. */
.venue-pills .vw-toggle { margin-left: 0.35rem; padding: 0.5rem 0.7rem; border: 1px solid var(--border); }
/* Phones (2026-09-07): six pills on a busy card (LineLeap, Instagram, map,
   share, save, the week toggle) need the whole row at a large Android font
   scale; a slightly tighter gap keeps them on one line at 412px. */
@media (max-width: 420px) { .venue-pills { gap: 0.4rem; } }
/* Icon pills keep pixel padding so only the glyph scales with the font
   (2026-09-07). With rem padding, the largest Android font scales grew six
   pills past the row and the week toggle wrapped even though the first
   line had room to give. The map pin keeps its own smaller rem padding. */
.pill.pill.ig, .pill.pill.lineleap, .pill.pill.share-btn, .pill.pill.save-btn { padding: 8px 11px; }
.venue-pills .vw-toggle { padding: 8px 10px; }
.pill.pill.ig svg, .pill.pill.lineleap svg, .pill.pill.share-btn svg, .pill.pill.save-btn svg { width: 20px; height: 20px; }
/* Narrow phones (360 to 400px): two fewer pixels of side padding per pill
   keeps the same six pills on one line at the default font scale. */
@media (max-width: 400px) {
  .pill.pill.ig, .pill.pill.lineleap, .pill.pill.share-btn, .pill.pill.save-btn { padding: 8px 9px; }
  .venue-pills .vw-toggle { padding: 8px 8px; }
}

/* Weather emoji pull an unrelated palette into a two-ink page. */
.th-emoji, .day-weather-icon, .weather-pill .wx-emoji { filter: grayscale(1); }

/* Busyness strip: ink bars on paper. */
.busyness-strip { background: none; border: 0; border-top: 1px solid var(--border-hairline); }
.bs-bar { border-radius: 0; }
.bs-label { font-size: 0.9rem; color: var(--text-dim); }

/* ===================================================================
   4. THE PRICING BLOCK
   Was an eyebrow → centred headline → 480px subhead → two equal cards →
   scarcity badge → pill CTA, i.e. the Stripe pricing module verbatim, on a
   page about $2 tacos. Restyled as an alt-weekly back-page ad, which is
   honest about what it is.
   =================================================================== */
.pricing-section {
  border-top: 3px solid var(--text);
  border-bottom: 3px solid var(--text);
  background: var(--bg2);
}
.pricing-eyebrow { text-align: left; font-family: var(--font-mono); }
.pricing-header h2 {
  font-family: var(--font-display);
  font-size: 2.2rem; text-align: left; text-transform: uppercase;
  line-height: 0.95; color: var(--text);
}
.pricing-header p { text-align: left; margin-left: 0; max-width: none; }
.pricing-grid { grid-template-columns: 1fr; max-width: none; }
.pricing-card {
  border: 2px solid var(--text); border-radius: 0;
  text-align: left; background: none;
}
.pricing-features li::before { content: '\2022  ';   /* bullet, not an em dash (operating-rules Rule 8) */ }
.pricing-slots { border-radius: 0; font-family: var(--font-mono); }
.pricing-cta-wrap { text-align: left; }
.pricing-cta {
  border-radius: 0; background: var(--text); color: var(--bg);
  font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.1em;
}
.pricing-cta:hover { transform: none; background: var(--neon); color: var(--bg); }
.pricing-note { text-align: left; }

/* The bottom bar (since 2026-09-07) is a ruled strip, matching the day-nav
   bar above it, not a floating object: buttons in it are no longer circles. */
.fab-bar { border-top: 1px solid var(--border-strong); }

/* The base bars carry opacity:0.55 because they were tuned to glow on black.
   On paper that reads as a faint dashed line, so ink them in properly. */
.bs-bars { height: 26px; }
.bs-bar { width: 8px; height: max(calc(var(--v, 0) * 26px), 2px); opacity: 1; }
.bs-bar.zero { opacity: 1; }
.bs-bar.peak { opacity: 1; }
.bs-bar.now { outline: 1px solid var(--text); }
.bs-bar.now::after { animation: none; box-shadow: none; }

/* ===================================================================
   5. REFERENCE VIEWPORT: GALAXY S23+ (412 x 915 CSS px, DPR 2.625)
   This is the design target. The hard requirement: the category filter
   row (ALL / DRINKS / MUSIC / FOOD / EVENTS) must fit on ONE line.
   Budget at 412px: 412 - 2*1.25rem page padding = 372px for 5 pills
   plus 4 gaps, so roughly 67px per pill. The base sizing needs ~490px
   and wraps to two lines, which wastes a whole row of vertical space
   on the most-used control on the page.
   =================================================================== */
.filters {
  flex-wrap: nowrap;
  gap: 0.35rem;
  padding-left: 1rem; padding-right: 1rem;
}
.filter-pill {
  flex: 0 1 auto;
  min-width: 0;
  gap: 0.3rem;
  padding: 0.45rem 0.5rem;
  font-size: 0.72rem;
  letter-spacing: 0.02em;
  white-space: nowrap;
}
.filter-pill .dot { width: 6px; height: 6px; flex-shrink: 0; }

/* The date rail is the ONE sanctioned horizontal scroller on the page: it is
   a self-contained control, and swiping it is how you reach later days. What
   must never scroll horizontally is the page CONTENT. Wrapping these instead
   was tried and rejected: 14 tabs wrap to 3 rows at 412px and push the sticky
   header to 268px, eating a third of the screen. Bar hidden in every engine
   (see section 6) and overscroll contained so the rail cannot hand its scroll
   to the page. */
.day-nav { flex-wrap: nowrap; overflow-x: auto; }
.day-tab { flex: 0 0 auto; }

/* Long venue names must not push the badge row off the card at 412px. */
.venue-name { overflow-wrap: anywhere; }

/* ===================================================================
   6. NO HORIZONTAL SCROLLBAR, EVER
   Hard rule: the page must never show a horizontal scrollbar, and no
   in-page scroll region may show one either. A stray horizontal bar is
   the most obvious "broken layout" signal there is, and on a sticky
   element it appears mid-scroll, which looks like a glitch.

   Belt and braces, because engines differ on overlay scrollbars:
   `overflow-x: clip` on the root clips without creating a scroll
   container (unlike `hidden`, which does and can trap sticky elements),
   and the day rail hides its bar in every engine while staying swipeable.
   =================================================================== */
html, body { max-width: 100%; overflow-x: clip; }

.day-nav,
.day-nav-wrapper,
.filters {
  scrollbar-width: none;          /* Firefox */
  -ms-overflow-style: none;       /* legacy Edge */
}
.day-nav::-webkit-scrollbar,
.day-nav-wrapper::-webkit-scrollbar,
.filters::-webkit-scrollbar {     /* Blink / WebKit */
  width: 0; height: 0; display: none;
}
/* Swipeable, never scrollbar'd. overscroll-behavior-x stops the rail from
   handing its scroll to the page and triggering back-navigation gestures. */
.day-nav { overscroll-behavior-x: contain; -webkit-overflow-scrolling: touch; }

/* Nothing may exceed the viewport. */
img, svg, video, iframe, table, pre { max-width: 100%; }
.venue-name, .event-title, .qt-venue, .guide-teaser { overflow-wrap: anywhere; }

/* The LIVE stamp is rotated, so its painted box is wider than its layout
   box and can poke past the right edge on a narrow screen. */
.live-badge { transform-origin: left center; max-width: 100%; }


/* ===================================================================
   7. LIVE NOW
   Green stamp, and the whole card is ringed in the same green so a venue
   that is open right now is findable by colour alone from across a dim
   room. Ringing the card makes it a bordered box, so per the spacing rule
   it also needs interior padding it does not have as a plain ruled row.
   =================================================================== */
.venue-card.live-now {
  border: 1px solid var(--green);
  border-left-width: 4px;
  padding: 0.95rem 1.15rem 1.05rem;
  margin: 1.1rem 0;
  background: var(--card);
}
.venue-card.live-now:hover { border-color: var(--green); background: var(--card-hover); }
/* Premier keeps its gold identity; the green rides on the left edge. */
.venue-card.premier.live-now,
.venue-card.premier.live-now:hover { border-color: var(--gold); border-left-color: var(--green); }
.live-badge, .venue-card.live-now .live-badge { background: var(--green); color: var(--bg); }
.live-dot { background: var(--green); }
.event.live .event-time .time-text { color: var(--green); }

/* ===================================================================
   8. PRESENTER BYLINE
   "Brought to you by <sponsor>" was gridded into the top-right corner
   beside the day label, which on a phone squeezes it into a 3-line stack
   jammed against the edge. Give it its own full-width line under the day
   label, left-aligned with everything else.
   =================================================================== */
/* Date and byline centre with the masthead above them. */
.day-header {
  justify-content: center;
  text-align: center;
}
.day-label { text-align: center; width: 100%; }

@media (max-width: 560px) {
  .day-header.has-presenter {
    grid-template-columns: 1fr;
    row-gap: 0.35rem;
    justify-items: center;
  }
  .day-presenter {
    align-items: center;
    justify-content: center;
    text-align: center;
    flex-direction: row;
    flex-wrap: wrap;
    gap: 0.35rem;
    width: 100%;
  }
  .dp-eyebrow { margin-bottom: 0; font-size: 0.5rem; letter-spacing: 0.16em; align-self: center; }
  .dp-name { font-size: 1.05rem; }
}


/* ===================================================================
   9. WORDMARK: the algorithmic N, lit
   The capital N's are the parametric neon-tube glyph from
   logo/render_logo.py, not a font letter. logo_paths() gives the tube
   centreline as one closed path in 512-space, stroked at tube_w=5.25 with
   round caps, ink flush to the viewBox so the box IS the glyph.

   Alignment: the box is sized to the DISPLAY FACE'S CAP HEIGHT (0.801em,
   measured from Alfa Slab One) and left on the baseline, so the N's cap
   line and baseline match the letters beside it. Sizing by em cannot do
   that, because 1em is not the cap height.

   Glow: a drop-shadow stack alone read as almost nothing here, because it
   spreads thin alpha over a wide radius and blue-on-navy gives it little
   contrast to work with. render_logo builds its halo from gaussian copies
   of the tube, so this does the same: two blurred copies of the glyph sit
   behind the crisp tube (::before wide and soft, ::after tight and bright).
   That is a real bloom rather than a shadow.
   =================================================================== */
.wordmark .accent {
  position: relative;
  display: inline-block;
  height: 0.801em;
  width: 0.6515em;
  vertical-align: baseline;
  margin-right: 0.055em;
  color: transparent;                 /* hide the font's N */
  text-shadow: none;
  background: none;
  /* The GLOW lives here, on an UNMASKED element. CSS applies filters BEFORE
     masking, so a mask and a drop-shadow on the same element cancel out: the
     mask clips the halo the filter just drew. Splitting them is the only way
     to light this glyph. Passes compound, tight bright core out to a bloom. */
  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));
}
/* The TUBE lives here. Since 2026-09-02 the renderer emits the tube's
   centerline as an inline SVG inside .accent (nightprint/__init__.py
   N_TUBE_SVG) and it is stroked at tube_w with round joins: the same
   silhouette the --wordmark-glyph mask painted, verified pixel-identical
   against the masked version at phone width. A stroke can be lit along
   its length, which the mask could not. The parent's filter lights it.
   The masked ::before stays as the fallback for an .accent with no svg
   (another city's letter). */
.wordmark .accent::before {
  content: '';
  position: absolute;
  inset: 0;
  background-color: var(--wordmark-accent);
  -webkit-mask: var(--wordmark-glyph) no-repeat center / contain;
          mask: var(--wordmark-glyph) no-repeat center / contain;
}
/* With the inline tube present the mask is redundant; the tube's box,
   stroke, glow and strike are the base theme's (modern_v2/app.css
   "Wordmark tube glyphs"), sized from --wm-cap (0.801em here). */
.wordmark .accent:has(.neon-n)::before { content: none; }

/* ===================================================================
   10. HEADER: centred masthead, one row of widgets
   The base header is a two-column flex row (brand left, meta right), which
   at phone widths crowds the wordmark against the weather pill. This stacks
   it into three centred bands instead:

       NITTANY NIGHTS          <- wordmark, centred
       Downtown State College  <- tagline, centred under it
       [83/57] [mail] [IG] [Play]   <- every widget, one row, centred

   .header-meta becomes a ROW (it is a column in the base theme) so the
   weather pill, the two icons and the store badge share a single line,
   centred horizontally and aligned on their vertical centres.
   =================================================================== */
.header-inner {
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: 0.5rem;
}

.brand {
  flex: 0 0 auto;
  width: 100%;
  min-width: 0;
  align-items: center;
  text-align: center;
}
.brand-row {
  justify-content: center;
  align-items: baseline;
  flex-wrap: wrap;
  width: 100%;
}
.wordmark {
  text-align: center;
  overflow-wrap: anywhere;
}
.logo-seo,
.tagline {
  text-align: center;
  width: 100%;
}

/* One row for every widget: pill, mail, Instagram, store badge. */
.header-meta {
  flex: 0 0 auto;
  width: 100%;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.55rem 0.65rem;
}
.header-meta-row {
  justify-content: center;
  align-items: center;
  gap: 0.65rem;
}
.header-store-badge {
  display: flex;
  align-items: center;
}
.header-store-badge a { line-height: 0; }

/* ===================================================================
   11. WORDMARK LINE BREAKS
   .accent is an inline-block (it paints the N glyph), and an inline-block
   is a break opportunity, so at narrow widths the glyph fell off the front
   of its own word: "N / ITTANY". _shared.py wraps each word in .wm-word;
   this keeps each word intact and lets the break happen at the space.
   =================================================================== */
.wm-word { white-space: nowrap; }

/* Real device check: Galaxy S24 is 360x780 CSS px, narrower than the S23+.
   Give the masthead a little less to carry there. */
@media (max-width: 400px) {
  .wordmark { font-size: 1.8rem; }
}


/* ===================================================================
   12. MAP PIN
   The map link is a drawn teardrop marker instead of the word "Map".
   Sized to the pill's other marks and inheriting its colour, so it dims
   and lights with the rest of the row.
   =================================================================== */
/* The map pill is icon-only like .ig and .lineleap, so it must take their
   geometry at EVERY width. The base steps those down at 640px and 400px
   using a doubled-class selector (.pill.pill.ig) for specificity; .map was
   not in those groups, so it kept the desktop padding on a phone and came
   out 47px wide against their 37px. Mirror the group, doubled class and
   all, at each breakpoint. */
.pill.pill.map { padding: 0.55rem 0.75rem; }
@media (max-width: 640px) { .pill.pill.map { padding: 0.4rem 0.55rem; } }
@media (max-width: 400px) { .pill.pill.map { padding: 0.35rem 0.45rem; } }
.pill .pin {
  width: 1.3rem;
  height: 1.3rem;
  display: block;
}
.qt-map .pin {
  width: 15px;
  height: 15px;
  display: block;
}
/* Amber pin: pops against the navy without leaving the palette (same ink as
   the LIVE stamp and the sponsor gold). The pill's own text stays muted. */
.pill.map .pin,
.qt-map .pin { fill: var(--pin); }
.pill.map:hover .pin,
.qt-map:hover .pin { fill: var(--pin-hover); }

/* Label kept for screen readers now that the word is gone. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ===================================================================
   13. HEADER ICON BOXES
   The mail and Instagram links were bare glyphs sitting beside a boxed
   weather pill, so the row read as two different kinds of thing. Box them
   the same way and match their height to the pill so the row lines up on
   one baseline: every widget in the masthead is now the same object.
   =================================================================== */
.header-meta-row .header-icon {
  border: 1px solid var(--border);
  border-radius: 0;
  width: auto;
  min-width: 2.15rem;
  height: 2.15rem;
  padding: 0 0.45rem;
  color: var(--muted);
}
.header-meta-row .header-icon:hover {
  color: var(--text);
  border-color: var(--border-strong);
  background: none;
}
/* The weather pill sets the row's height; hold the icons to it exactly. */
.header-meta-row .weather-pill {
  height: 2.15rem;
  padding: 0 0.7rem;
}
/* The store badge is artwork with its own edge, so it keeps its own box
   but matches the height rather than sitting proud of the row. */
.header-store-badge img { height: 2.15rem; width: auto; }

/* ===================================================================
   14. LIVE NOW: the badge breathes
   The green field AROUND THE WORDS pulses; the card keeps a steady green
   border. Slow (2.6s) and eased, because a hard blink reads as an error.

   The fill floors at a mid green rather than fading all the way to the
   card ground: the words are set in the ground colour, so at a true zero
   they would vanish into it. Floored, the lettering stays legible through
   the whole cycle while the field still visibly swells. A halo fades in
   with it, which is what carries the attention across a dark room.

   Colour and shadow only, so nothing reflows.
   =================================================================== */
@keyframes nn-live-breathe {
  0%, 100% {
    background-color: var(--stamp-live-floor);
    box-shadow: 0 0 0 0 var(--stamp-live-halo-off);
  }
  50% {
    background-color: var(--green);
    box-shadow: 0 0 13px 1px var(--stamp-live-halo);
  }
}
.live-badge {
  animation: nn-live-breathe 2.6s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .live-badge { animation: none; background-color: var(--green); }
  .upcoming-badge { animation: none; background-color: var(--state-upcoming-ink); }
}

/* ---- 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;

  --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;

  /* 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;

  /* 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;
  --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;
}

/* ---- nightprint/tokens.css: every design decision. Edit looks here. ---- */
/* Nightprint design tokens.
 *
 * Overrides only. Concatenated AFTER modern_v2/tokens.css, so anything not
 * redefined here keeps the base value.
 *
 * Brief: a night edition of a printed listings page. Dark, because it is
 * read standing in a dim bar, but printed rather than neon: ink rules,
 * wood-type masthead, tabular times, square corners, stamped LIVE.
 *
 * Palette is Penn State's official one. Nittany Navy is the ground, Beaver
 * Blue the structure, Pugh Blue the link/highlight. The warm amber is the
 * accent already proven on og-card.png. No cyan, no violet, no Tailwind
 * ramp stops.
 *
 *   Nittany Navy #001E44   Beaver Blue #1E407C   Pugh Blue #96BEE6
 *   Limestone   #A2AAAD   Slate       #314D64   Amber     #E8A838
 *
 * WARNING: --accent/--accent2/--neon/--bg/--bg2/--card/--card-hover/--text/
 * --text-dim/--muted/--dimmer are the per-city override contract. Keep names.
 */

:root {
  /* --- Ground and ink -------------------------------------------------- */
  /* Navy-black, not blue-black and not pure black: dark enough for a dim
     room, warm-white ink so it reads as printed rather than backlit. */
  --bg: #04101F;
  --bg2: #0A1B31;
  --card: #0A1B31;
  --card-hover: #102A47;
  --text: #F2EFE6;            /* warm paper white */
  --text-dim: #C4CFDD;
  --muted: #93A4BA;
  --dimmer: #6E8098;
  --ink-bright: #FFFFFF;
  --ink-on-highlight: #04101F;

  /* Rules read as printed lines: Pugh-tinted, not white haze. */
  --border: rgba(150,190,230,0.24);
  --border-strong: rgba(150,190,230,0.52);
  --border-hairline: rgba(150,190,230,0.16);
  --border-hairline-faint: rgba(150,190,230,0.11);
  --border-event-rule: rgba(150,190,230,0.13);
  --border-strip: rgba(150,190,230,0.14);
  --border-dashed-quiet: rgba(150,190,230,0.3);

  /* --- Brand: Penn State ------------------------------------------------ */
  --accent: #1E407C;          /* Beaver Blue */
  --accent2: #2F5DA8;
  --neon: #96BEE6;            /* Pugh Blue: links, hovers, highlights */
  --gold: #E8A838;            /* amber accent */
  /* Map-pin red. Sits in the same light/chroma family as the category inks
     (#FF8FA8 rose, #E8A838 amber, #7FD1A0 mint) so it reads as part of the
     palette rather than a browser-default red dropped onto navy. */
  --pin: #FF6B5A;
  --pin-hover: #FF8E80;

  /* --- Category inks, tuned for legibility on navy in a dark room ------- */
  --music:  #FF8FA8;
  --drinks: #E8A838;
  --food:   #7FD1A0;
  --sports: #96BEE6;
  --events: #C9A2D9;
  --info:   #96BEE6;   /* guide-card Info rows: the guide accent */

  /* No glows: this is ink on dark stock, not neon tube. */
  --music-glow: transparent;
  --drinks-glow: transparent;
  --food-glow: transparent;
  --sports-glow: transparent;
  --events-glow: transparent;
  --dot-all-bg: rgba(242,239,230,0.6);
  --dot-all-glow: transparent;

  /* --- Type ------------------------------------------------------------- */
  --font-display: 'Alfa Slab One', Georgia, serif;
  --font-body: 'Archivo', -apple-system, BlinkMacSystemFont, sans-serif;
  --font-body-simple: 'Archivo', sans-serif;
  --font-mono: 'Space Mono', ui-monospace, monospace;
  --font-serif: 'Newsreader', Georgia, serif;
  --font-serif-alt: 'Newsreader', Georgia, serif;

  /* --- Corners: print has edges ----------------------------------------- */
  --radius-xs: 0; --radius-sm: 0; --radius-md: 0; --radius-lg: 0;
  --radius-xl: 0; --radius-2xl: 0; --radius-pill: 0; --radius-pill-full: 0;
  --radius-bar-top: 0; --radius-sheet-top: 0;
  --radius-circle: 50%;

  /* --- Wordmark: keep the var(--wm-*, default) chain intact ------------- */
  /* The parametric N, emitted from logo/render_logo.py logo_paths().
     Shared by the crisp tube and the two bloom layers. */
  --wordmark-glyph: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2088.65%20109.00%27%3E%3Cpath%20d%3D%27M2.62%2C6.89%20L2.66%2C6.37%20L2.75%2C5.87%20L2.90%2C5.37%20L3.11%2C4.91%20L3.38%2C4.47%20L3.70%2C4.06%20L4.06%2C3.70%20L4.47%2C3.38%20L4.91%2C3.11%20L5.37%2C2.90%20L5.87%2C2.75%20L6.37%2C2.66%20L6.89%2C2.62%20L9.10%2C2.62%20L9.65%2C2.66%20L10.19%2C2.76%20L10.71%2C2.92%20L11.21%2C3.15%20L11.68%2C3.43%20L12.11%2C3.77%20L12.49%2C4.16%20L16.44%2C8.69%20L20.40%2C13.21%20L24.35%2C17.73%20L28.30%2C22.26%20L32.26%2C26.78%20L36.21%2C31.30%20L40.16%2C35.82%20L44.12%2C40.35%20L48.07%2C44.87%20L52.02%2C49.39%20L55.97%2C53.92%20L59.93%2C58.44%20L63.88%2C62.96%20L67.83%2C67.48%20L71.79%2C72.01%20L72.45%2C72.76%20L72.85%2C73.09%20L73.34%2C73.25%20L73.85%2C73.25%20L74.34%2C73.07%20L74.73%2C72.73%20L74.99%2C72.28%20L75.07%2C71.77%20L75.07%2C65.76%20L75.07%2C59.76%20L75.07%2C53.75%20L75.07%2C47.74%20L75.07%2C41.73%20L75.07%2C35.72%20L75.07%2C29.71%20L75.07%2C23.70%20L75.07%2C17.69%20L75.07%2C11.68%20L75.07%2C7.17%20L75.10%2C6.67%20L75.19%2C6.17%20L75.33%2C5.69%20L75.52%2C5.22%20L75.76%2C4.78%20L76.06%2C4.37%20L76.39%2C3.99%20L76.77%2C3.66%20L77.18%2C3.36%20L77.62%2C3.12%20L78.09%2C2.93%20L78.57%2C2.79%20L79.07%2C2.70%20L79.57%2C2.67%20L81.53%2C2.67%20L82.03%2C2.70%20L82.53%2C2.79%20L83.01%2C2.93%20L83.48%2C3.12%20L83.92%2C3.36%20L84.33%2C3.66%20L84.71%2C3.99%20L85.04%2C4.37%20L85.34%2C4.78%20L85.58%2C5.22%20L85.77%2C5.69%20L85.91%2C6.17%20L86.00%2C6.67%20L86.03%2C7.17%20L86.03%2C13.19%20L86.03%2C19.21%20L86.03%2C25.22%20L86.03%2C31.24%20L86.03%2C37.25%20L86.03%2C43.27%20L86.03%2C49.29%20L86.03%2C55.30%20L86.03%2C61.32%20L86.03%2C67.33%20L86.03%2C73.35%20L86.03%2C79.36%20L86.03%2C85.38%20L86.03%2C91.40%20L86.03%2C97.41%20L86.03%2C101.92%20L85.99%2C102.46%20L85.90%2C102.99%20L85.74%2C103.50%20L85.52%2C103.99%20L85.24%2C104.45%20L84.91%2C104.88%20L84.53%2C105.26%20L84.10%2C105.59%20L83.64%2C105.87%20L83.15%2C106.09%20L82.64%2C106.25%20L82.11%2C106.34%20L81.57%2C106.37%20L79.16%2C106.37%20L78.62%2C106.34%20L78.08%2C106.24%20L77.56%2C106.08%20L77.06%2C105.85%20L76.59%2C105.57%20L76.16%2C105.23%20L75.78%2C104.84%20L71.81%2C100.30%20L67.84%2C95.76%20L63.88%2C91.22%20L59.91%2C86.68%20L55.94%2C82.14%20L51.98%2C77.61%20L48.01%2C73.07%20L44.04%2C68.53%20L40.08%2C63.99%20L36.11%2C59.45%20L32.14%2C54.91%20L28.18%2C50.37%20L24.21%2C45.84%20L20.24%2C41.30%20L16.28%2C36.76%20L14.95%2C35.25%20L14.55%2C34.92%20L14.06%2C34.75%20L13.55%2C34.76%20L13.06%2C34.94%20L12.67%2C35.28%20L12.41%2C35.72%20L12.33%2C36.23%20L12.33%2C42.24%20L12.33%2C48.24%20L12.33%2C54.24%20L12.33%2C60.25%20L12.33%2C66.25%20L12.33%2C72.26%20L12.33%2C78.26%20L12.33%2C84.26%20L12.33%2C90.27%20L12.33%2C96.27%20L12.33%2C101.78%20L12.30%2C102.28%20L12.21%2C102.78%20L12.07%2C103.26%20L11.88%2C103.73%20L11.64%2C104.17%20L11.34%2C104.58%20L11.01%2C104.96%20L10.63%2C105.29%20L10.22%2C105.59%20L9.78%2C105.83%20L9.31%2C106.02%20L8.83%2C106.16%20L8.33%2C106.25%20L7.83%2C106.28%20L7.12%2C106.28%20L6.62%2C106.25%20L6.12%2C106.16%20L5.64%2C106.02%20L5.17%2C105.83%20L4.73%2C105.59%20L4.32%2C105.29%20L3.94%2C104.96%20L3.61%2C104.58%20L3.31%2C104.17%20L3.07%2C103.73%20L2.88%2C103.26%20L2.74%2C102.78%20L2.65%2C102.28%20L2.62%2C101.78%20L2.62%2C95.75%20L2.62%2C89.73%20L2.62%2C83.70%20L2.62%2C77.68%20L2.62%2C71.65%20L2.62%2C65.63%20L2.62%2C59.60%20L2.62%2C53.58%20L2.62%2C47.55%20L2.62%2C41.53%20L2.62%2C35.50%20L2.62%2C29.48%20L2.62%2C23.45%20L2.62%2C17.43%20L2.62%2C11.40%20L2.62%2C6.89%20Z%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%275.25%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%2F%3E%3C%2Fsvg%3E");
  --wordmark-accent: var(--wm-accent, #66E5FF);
  /* Alfa Slab One cap height, measured; the masked glyph used the same. */
  --wm-cap: 0.801em;
  --wm-glyph-gap: 0.055em;
  /* The N is a neon tube, so it keeps its glow even though the rest of the
     theme is printed ink. This is the one lit object on the page. */
  --wordmark-glow-strong: var(--wm-glow-strong, rgba(180,244,255,0.95));
  --wordmark-glow-mid: var(--wm-glow-mid, rgba(102,229,255,0.7));
  --wordmark-glow-soft: var(--wm-glow-soft, rgba(40,175,230,0.45));

  /* Opaque chrome, no backdrop blur. */
  --scrim-header: #04101F;
  --scrim-ts-bar: #0A1B31;
  --scrim-announce: #0A1B31;
  --scrim-day-nav: #04101F;
  --scrim-sheet-backdrop: rgba(2,8,16,0.72);
  /* Light-mode chrome stays opaque like the dark scrims above (2026-09-09):
     the header, the sticky day-tab strip, and the bottom bar paint the
     theme's light background (a city's light --bg override included) instead
     of the base theme's translucent scrims, so the Grid never shows through. */
  --light-scrim-header: var(--bg);
  --light-scrim-day-nav: var(--bg);

  --glow-orb-1: transparent;
  --glow-orb-2: transparent;
  --glow-orb-3: transparent;

  --surface-tint-faint: rgba(150,190,230,0.03);
  --surface-tint-quiet: rgba(150,190,230,0.04);
  --surface-tint-strip: rgba(150,190,230,0.04);
  --surface-tint-weather: rgba(150,190,230,0.05);

  /* Shadows exist (it is dark) but stay tight and neutral. */
  --shadow-card: rgba(0,0,0,0.5);
  --shadow-floating: rgba(0,0,0,0.6);
  --shadow-floating-hover: rgba(0,0,0,0.7);
  --shadow-sheet: rgba(0,0,0,0.65);
  --shadow-premier: rgba(0,0,0,0.55);
  --shadow-premier-hover: rgba(0,0,0,0.65);
  --shadow-text-emboss: transparent;

  /* --- States ----------------------------------------------------------- */
  /* LIVE is a stamped amber mark: the highest-contrast thing on the page,
     which is what you want to spot across a dark room. */
  --green: #7FD1A0;
  --amber: #E8A838;
  --red: #FF8FA8;
  --state-live-glow: transparent;
  --state-live-border: rgba(232,168,56,0.5);
  --state-live-ring: rgba(232,168,56,0.07);
  --state-live-halo: transparent;
  --state-live-border-hover: rgba(232,168,56,0.8);
  --state-live-ring-hover: rgba(232,168,56,0.14);
  --state-live-halo-hover: transparent;
  --state-live-badge-border: #E8A838;
  --state-live-badge-bg: #E8A838;
  --state-live-dot-glow: transparent;

  /* Breathing status stamps (see app.css section 14). The floor is a mid
     tone rather than the ground colour: the lettering is set in the ground,
     so a true zero would erase the words mid-cycle. */
  --stamp-live-floor: #3F8A63;
  --stamp-live-halo: rgba(127,209,160,0.55);
  --stamp-live-halo-off: rgba(127,209,160,0);
  --stamp-upcoming-floor: #7E5E92;
  --stamp-upcoming-halo: rgba(201,162,217,0.55);
  --stamp-upcoming-halo-off: rgba(201,162,217,0);

  --state-upcoming-ink: #C9A2D9;
  --state-upcoming-dot: #C9A2D9;
  --state-upcoming-border: rgba(201,162,217,0.6);
  --state-upcoming-bg: rgba(201,162,217,0.12);
  --state-upcoming-dot-glow: transparent;

  /* --- Busyness bars ---------------------------------------------------- */
  --busy-bar-from: #2F5DA8;
  --busy-bar-to: #96BEE6;
  --busy-zero-from: rgba(150,190,230,0.16);
  --busy-zero-to: rgba(150,190,230,0.16);
  --busy-peak-from: #E8A838;
  --busy-peak-to: #F5C46B;
  --busy-now-from: #4FB07A;
  --busy-now-to: #7FD1A0;
  --busy-now-glow: transparent;
  --busy-now-outline: rgba(242,239,230,0.45);
  --busy-now-marker: #F2EFE6;
  --busy-now-marker-glow: transparent;
  --busy-now-peak-from: #4FB07A;
  --busy-now-peak-to: #E8A838;
  --busy-now-peak-glow: transparent;

  /* --- Link pills ------------------------------------------------------- */
  --pill-lineleap-ink: #96BEE6;
  --pill-lineleap-ink-hover: #F2EFE6;
  --pill-lineleap-bg-hover: rgba(150,190,230,0.14);
  --pill-ig-ink: #C9A2D9;
  --pill-ig-ink-hover: #F2EFE6;
  --pill-ig-bg-hover: rgba(201,162,217,0.14);

  /* --- Sponsor blocks --------------------------------------------------- */
  --premier-inset: rgba(232,168,56,0.12);
  --premier-ring: rgba(232,168,56,0.3);
  --premier-edge: #E8A838;
  --premier-inset-hover: rgba(232,168,56,0.2);
  --premier-ring-hover: rgba(232,168,56,0.45);
  --premier-glow-hover: transparent;
  --premier-badge-border: #E8A838;
  --premier-badge-ink: #E8A838;
  --premier-sheen-fade: transparent;
  --sheen-mid: transparent;
  --sheen-edge: transparent;
  --pricing-premier-border: #E8A838;
  --pricing-premier-slot-border: rgba(232,168,56,0.45);
  --pricing-premier-slot-bg: rgba(232,168,56,0.08);

  --whiskey-surface: #171207;
  --whiskey-ink: #F0DFB4;
  --whiskey-ink-hover: #FFF7E0;
  --whiskey-gold-bright: #E8A838;
  --dp-name-ink: #EBD9A8;
  --whiskey-border-hover: rgba(232,168,56,0.6);
  --whiskey-inset: rgba(232,168,56,0.08);
  --whiskey-inset-hover: rgba(232,168,56,0.14);
  --whiskey-tag-corner-border: rgba(232,168,56,0.45);
  --whiskey-tag-corner-ink: #E8A838;
  --whiskey-sub-ink: #C4AE7A;
  --whiskey-cta-border: rgba(232,168,56,0.55);
  --whiskey-cta-bg-hover: rgba(232,168,56,0.14);

  --featured-wash: rgba(47,93,168,0.10);
  --featured-border: rgba(150,190,230,0.5);
  --featured-badge-border: #96BEE6;
  --pricing-featured-border: #96BEE6;
  --pricing-featured-slot-border: rgba(150,190,230,0.4);
  --pricing-cta-border: #96BEE6;

  /* --- Misc chrome ------------------------------------------------------ */
  --announce-lead-ink: #E8A838;
  --announce-lead-bg: #0A1B31;
  --staging-bg: rgba(255,143,168,0.14);
  --staging-border: #FF8FA8;
  --staging-ink: #FF8FA8;
  --find-current-bg: #E8A838;

  --guide-card-wash-fade: rgba(4,16,31,0);
  --guide-blurb-ink: #C4CFDD;
  --guide-teaser-ink: #F2EFE6;
  --guide-loc-ink: #93A4BA;
  --guide-map-link-ink: #96BEE6;

  /* The base theme's "cyan" family, repointed at Pugh Blue. */
  --accent-cyan: #96BEE6;
  --accent-cyan-bright: #C4DDF5;
  --accent-cyan-strong: rgba(150,190,230,0.55);
  --accent-cyan-half: rgba(150,190,230,0.5);
  --accent-cyan-edge: rgba(150,190,230,0.45);
  --accent-cyan-line: rgba(150,190,230,0.4);
  --accent-cyan-soft: rgba(150,190,230,0.35);
  --accent-cyan-fill-hover: rgba(150,190,230,0.28);
  --accent-cyan-ring: rgba(150,190,230,0.25);
  --accent-cyan-fill: rgba(150,190,230,0.16);
  --accent-cyan-wash: rgba(150,190,230,0.12);
  --accent-cyan-veil: rgba(150,190,230,0.06);
}
