/* ============================================================
   TINTED ARCADE — founder-ratified visual identity, 2026-09-21.
   Source of truth: variant 4 ("Tinted Arcade") in the arcade-glass
   scratchpad (.v-tinted tokens + the shared mock component rules)
   and card-4.png. Frosted near-white glass (~84% opaque) with a
   faint cool tint, full-strength black pixel-outline frames, hard
   offset shadows (no blur on the shadow itself, only on the glass),
   arcade-yellow CTA buttons that press into their own shadow.
   Three fonts: Press Start 2P (wordmark ONLY), Silkscreen
   (numerals — coins/level/charges/prices/selection count),
   Schibsted Grotesk (everything else).

   Three tiers (coding-style.md): primitives (raw) -> semantic
   (panel-bg/ink/outline/shadow/accent/money/danger) -> component
   (chip/pill/modal/palette/...). 8pt spacing scale.

   Adaptive tint (a future step, NOT built here): the ratified mock
   shows panels reading warm when they sit over red-painted map
   area and cool elsewhere. That needs live colour sampling under
   each panel — out of scope for this pass. Every panel here uses
   one static faint cool tint. See GAPS in the handoff.
   ============================================================ */
:root {
  /* ---- Tier 0: primitives (raw values, brand-agnostic) ---- */
  --pc-white: #ffffff;
  --pc-black: #0b0b0f;
  --pc-ink-900: #14121c;
  --pc-ink-600: #57536e;
  --pc-yellow: #ffd400;
  --pc-yellow-ink: #1a1400;
  --pc-red: #e50000;
  --pc-green: #02be01;
  --pc-lavender-100: #ece9f7;
  --pc-lavender-600: #4a4570;
  --pc-blue: #4f5dff;
  --pc-cool-tint-rgb: 240, 245, 255;
  --pc-map-bg: #f2f3f0;

  --pc-space-1: 4px;
  --pc-space-2: 8px;
  --pc-space-3: 16px;
  --pc-space-4: 24px;
  --pc-space-5: 32px;

  /* Blur/saturate ceiling is a measured perf constraint, not a taste
     choice: bigger blur halves frame rate while panning on a slow
     phone. Never raise --pc-blur past 12px or --pc-saturate past 160%. */
  --pc-blur: 8px;
  --pc-saturate: 140%;
  --pc-panel-alpha: 0.84;
  --pc-outline-w: 3px;
  --pc-shadow-w: 4px;

  --pc-font-word: 'Press Start 2P', 'Courier New', monospace;
  --pc-font-num:  'Silkscreen', ui-monospace, 'Courier New', monospace;
  --pc-font-body: 'Schibsted Grotesk', -apple-system, BlinkMacSystemFont,
                  "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;

  /* ---- Tier 1: semantic ---- */
  --bg: var(--pc-map-bg);
  --panel-bg: rgba(var(--pc-cool-tint-rgb), var(--pc-panel-alpha));
  --panel-backdrop: blur(var(--pc-blur)) saturate(var(--pc-saturate));
  --ink: var(--pc-ink-900);
  --ink-muted: var(--pc-ink-600);
  --outline: var(--pc-black);
  --outline-width: var(--pc-outline-w);
  --shadow-color: var(--pc-black);
  --shadow: var(--pc-shadow-w) var(--pc-shadow-w) 0 var(--shadow-color);
  --shadow-sm: 3px 3px 0 var(--shadow-color);
  --accent: var(--pc-yellow);
  --accent-ink: var(--pc-yellow-ink);
  --money: var(--pc-yellow);
  --money-ink: var(--pc-yellow-ink);
  --danger: var(--pc-red);
  --confirm: var(--pc-green);
  --locked-bg: var(--pc-lavender-100);
  --locked-ink: var(--pc-lavender-600);
  --focus-ring: var(--pc-blue);

  /* Pixel-overlay grid lattice (build order Fix 2), retuned dark-on-light
     for the positron basemap (Fix 1) -- the old rgba(255,255,255,..) line was
     tuned for a near-black background and would vanish against positron's
     near-white land/road fill. Still thin and low-contrast on purpose --
     readable enough to aim, never competing with painted pixels.
     --grid-max-opacity is the ceiling the zoom-based fade-in (viewport.js's
     gridOpacityFraction) multiplies against, not a fixed value. Unchanged by
     the Tinted Arcade restyle -- already tuned for this basemap. */
  --grid-line-color: rgba(0, 0, 0, 0.28);
  --grid-line-width: 1px;
  --grid-max-opacity: 0.55;
  /* Hover-cell highlight: a ring around the cell under the pointer -- ring
     only, no fill (Fix 2: this must read as "this is a cell", never "you are
     about to paint here"). Unchanged by the Tinted Arcade restyle. */
  --hover-ring-inner-color: #000000;
  --hover-ring-outer-color: #ffffff;
  --hover-ring-width: 2px;
  --hover-pulse-duration: 1.8s;
  --hover-pulse-glow-color: var(--hover-ring-outer-color);

  /* ---- Tier 2: component (pixel doctrine -- hard square edges
     everywhere; kept as tokens, not hardcoded, so a future variant
     can round them without a find-and-replace). ---- */
  --radius-chrome: 0px;
  --radius-btn: 0px;
  --radius-icon: 0px;

  font-family: var(--pc-font-body);
}

/* Apple's Reduce Transparency / iOS "Tinted" escape hatch: every panel
   references --panel-bg/--panel-backdrop, so flipping these two variables
   here removes the glass everywhere at once -- solid fill, no blur. */
@media (prefers-reduced-transparency: reduce) {
  :root {
    --panel-bg: rgb(var(--pc-cool-tint-rgb));
    --panel-backdrop: none;
  }
}

* { box-sizing: border-box; }

/* The HTML hidden attribute must win over any display rule below. Tailwind's
   reset used to supply this; with Tailwind removed, #selection-count's own
   display:flex made "0 / 0" show in view mode (measured 2026-09-21). */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  background: var(--bg);
  color: var(--ink);
}

/* Full-viewport basemap (build order Part 1/3): the map fills the entire
   screen, no letterboxing, no floating bounded box -- everything else is
   corner-pinned chrome floating on top of it. */
#map {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  z-index: 0;
}

/* Tile source: OpenFreeMap "bright" since 2026-09-22 (viewport.js; was "positron") --
   verified keyless, unlimited, and the lightest/cleanest of OpenFreeMap's
   three named styles by reading their style JSON directly (near-white
   rgb(242,243,240) background, fewest layers). Muted by construction, not a
   filter bolted on after the fact, so this stays at `none` -- the escape
   hatch if OpenFreeMap ever goes away. */
#map .maplibregl-canvas-container {
  filter: none;
}

/* The pixel overlay never intercepts pointer input itself -- the map
   underneath owns pan/zoom/pinch/click; app.js reads cell coordinates off
   the map's own click/mousemove events (evt.lngLat). */
#canvas-container {
  position: fixed;
  inset: 0;
  z-index: 5;
  pointer-events: none;
}

#canvas {
  position: absolute;
  top: 0;
  left: 0;
  image-rendering: pixelated;
}

#billboard-tooltip {
  position: absolute;
  background: var(--panel-bg);
  -webkit-backdrop-filter: var(--panel-backdrop);
  backdrop-filter: var(--panel-backdrop);
  border: var(--outline-width) solid var(--outline);
  border-radius: var(--radius-chrome);
  box-shadow: var(--shadow-sm);
  padding: 6px 10px;
  font-size: 12px;
  font-family: var(--pc-font-body);
  color: var(--ink);
  pointer-events: none;
  max-width: 220px;
  z-index: 20;
}

/* Cell-under-pointer highlight: an outline only, deliberately never filled
   (Fix 2 -- must read as "this is a cell", never "you are about to paint
   here"; the cursor-attached colour swatch this replaced is deleted
   entirely, not just hidden). Same ring in view and paint mode -- the actual
   pending-selection state (app.js's drawPendingCell, drawn on the map
   canvas) is what shows a colour, only after a real click. Double ring
   (black then white) stays readable regardless of what map feature is
   underneath it. Unchanged by the Tinted Arcade restyle. */
#hover-cell {
  position: absolute;
  pointer-events: none;
  z-index: 15;
  animation: hover-cell-pulse var(--hover-pulse-duration) ease-in-out infinite;
}

/* Breathing glow: the double ring (dark inner, light outer -- readable on
   the light basemap) stays constant, a soft glow layered outside it grows
   and fades. Nothing here moves the box itself, so it never fights the
   snap-to-cell positioning above. */
@keyframes hover-cell-pulse {
  0%, 100% {
    box-shadow:
      0 0 0 var(--hover-ring-width) var(--hover-ring-inner-color),
      0 0 0 calc(var(--hover-ring-width) * 2) var(--hover-ring-outer-color),
      0 0 0 calc(var(--hover-ring-width) * 2) transparent;
  }
  50% {
    box-shadow:
      0 0 0 var(--hover-ring-width) var(--hover-ring-inner-color),
      0 0 0 calc(var(--hover-ring-width) * 2) var(--hover-ring-outer-color),
      0 0 6px calc(var(--hover-ring-width) * 2) var(--hover-pulse-glow-color);
  }
}

/* Floating title + tab strip -- no full-width header bar, two small glass
   pills pinned to the top corners, so the map stays the dominant visual. */
#chrome-header,
#chrome-tabs {
  position: fixed;
  top: 12px;
  z-index: 30;
  background: var(--panel-bg);
  -webkit-backdrop-filter: var(--panel-backdrop);
  backdrop-filter: var(--panel-backdrop);
  border: var(--outline-width) solid var(--outline);
  border-radius: var(--radius-chrome);
  box-shadow: var(--shadow);
  padding: 8px 14px;
}
#chrome-header { left: 12px; max-width: 60vw; }
/* Phone width: the title pill and the tab strip overlapped (measured at
   400px). Drop the tagline and cap the title so both fit on one line. */
@media (max-width: 600px) {
  #chrome-header { max-width: calc(100vw - 230px); padding: 8px 10px; }
  #chrome-header .subtitle { display: none; }
  /* The pixel wordmark is too wide for one line here and showed as
     "PROBARA CANV..." (measured at 400px). Let it wrap to two lines. */
  #chrome-header h1 { white-space: normal; line-height: 1.6; }
  /* The two-line title box ends at ~66px; start the left tool column
     below it with room for its shadow. */
  .chrome-corner.chrome-top-left { top: 80px; }
  #chrome-tabs { padding: 6px 8px; }
}
#chrome-tabs { right: 12px; display: flex; gap: 6px; }

/* Wordmark: Press Start 2P is much wider per glyph than a normal typeface,
   so this stays small (10px) and tight -- it is the ONLY element allowed
   this font, per the Tinted Arcade spec. */
header h1 {
  margin: 0;
  font-family: var(--pc-font-word);
  font-size: 10px;
  line-height: 1.7;
  letter-spacing: 0.4px;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.subtitle { font-family: var(--pc-font-body); font-weight: normal; color: var(--ink-muted); font-size: 12px; margin-left: 6px; }
/* Video-HUD counter (2026-09-22 task): "X followers = Y pixels of New
   York" is now a primary on-screen story beat (FORMAT.md section 6 item 4),
   not a secondary caption -- bumped from 11px/muted to 13px/bold/full-ink
   so it reads clearly in a 9:16 phone screen recording at 400x800/360x740. */
#follower-badge { font-family: var(--pc-font-body); font-size: 13px; font-weight: 600; color: var(--ink); margin-top: 2px; }

/* Single glass container owns the outline/shadow; individual tabs stay
   transparent so two outlines never nest inside one another. */
.tab-btn {
  background: transparent;
  color: var(--ink-muted);
  font-family: var(--pc-font-body);
  border: none;
  padding: 5px 12px;
  border-radius: var(--radius-chrome);
  cursor: pointer;
  font-size: 12px;
}
.tab-btn.active {
  background: var(--accent);
  color: var(--accent-ink);
  border: 2px solid var(--outline);
  font-weight: 700;
}

/* Corner-pinned icon chrome, Wplace's pattern (T1 section 4). Top corners
   sit below the header/tab pills; bottom corners are free. */
.chrome-corner { position: fixed; z-index: 25; display: flex; gap: 8px; }
.chrome-top-left { top: 64px; left: 12px; flex-direction: column; }
/* Follower badge on (growing map): the title pill is taller, so the left
   tool column starts 8px under its measured bottom (app.js
   syncHeaderBottom). More specific than the phone-width top: 80px rule. */
body.has-follower-badge .chrome-corner.chrome-top-left { top: calc(var(--pc-header-bottom, 64px) + 8px); }
.chrome-top-right { top: 64px; right: 12px; flex-direction: column; }
.chrome-bottom-left { bottom: 16px; left: 12px; }
.chrome-bottom-right { bottom: 16px; right: 12px; }
.chrome-bottom-center { bottom: 16px; left: 50%; transform: translateX(-50%); }

.icon-btn {
  width: 42px;
  height: 42px;
  border-radius: var(--radius-icon);
  border: var(--outline-width) solid var(--outline);
  background: var(--panel-bg);
  -webkit-backdrop-filter: var(--panel-backdrop);
  backdrop-filter: var(--panel-backdrop);
  color: var(--ink);
  font-size: 17px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: var(--shadow-sm);
  transition: transform 80ms ease, box-shadow 80ms ease;
}
.icon-btn:active { transform: translate(3px, 3px); box-shadow: none; }
.icon-btn.active { border-color: var(--danger); color: var(--danger); }

/* Zoom buttons hide on small screens and rely on pinch, as Wplace does --
   MapLibre's touch handler already provides pinch-to-zoom natively. */
@media (max-width: 600px) {
  .zoom-btn { display: none; }
}

/* Display-options dropdown (opacity slider + layer toggle) -- CSS-only,
   opens on focus-within so no extra JS toggle wiring is needed. */
.chrome-dropdown { position: relative; }
.chrome-dropdown-panel {
  display: none;
  position: absolute;
  left: 50px;
  top: 0;
  min-width: 200px;
  background: var(--panel-bg);
  -webkit-backdrop-filter: var(--panel-backdrop);
  backdrop-filter: var(--panel-backdrop);
  border: var(--outline-width) solid var(--outline);
  border-radius: var(--radius-chrome);
  box-shadow: var(--shadow);
  padding: 12px;
  font-size: 12px;
  font-family: var(--pc-font-body);
  color: var(--ink);
}
.chrome-dropdown:focus-within .chrome-dropdown-panel,
.chrome-dropdown-panel:hover { display: block; }
.chrome-dropdown-row { display: flex; flex-direction: column; gap: 4px; margin-bottom: 8px; }
.chrome-dropdown-row:last-child { margin-bottom: 0; flex-direction: row; align-items: center; }

/* S6 (docs/plans/2026-09-21_player-economy-foundation.md): the free and
   locked tiers are now two independently-sized rows stacked in
   #palette-wrap rather than one flex container -- measured bug: with both
   groups crammed into one nowrap+640px-capped box, content ran 928px wide
   and the 8 locked swatches sat off-screen behind a horizontal scroll the
   floating pill never hinted at. Splitting them means each row's own
   max-width only has to fit ITS OWN swatches, so at 1200px neither row
   needs to scroll, and #palette-locked still wraps as a safety net at
   narrow (400px) widths. */
.palette-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}

/* Primary chrome, always visible (build brief item 5): what you're about to
   paint with. Single row, sane wrap rule -- scrolls horizontally on a
   screen too narrow for the full FREE palette instead of wrapping raggedly. */
#palette,
#palette-locked {
  display: flex;
  gap: 8px;
  padding: 8px 12px;
  background: var(--panel-bg);
  -webkit-backdrop-filter: var(--panel-backdrop);
  backdrop-filter: var(--panel-backdrop);
  border: var(--outline-width) solid var(--outline);
  border-radius: var(--radius-chrome);
  box-shadow: var(--shadow);
}
#palette {
  flex-wrap: nowrap;
  overflow-x: auto;
  /* Wide enough for all 24 colours (16 free + 8 unlockable) at 38px each, so
     unlocked colours never clip on a desktop screen. Narrower screens still scroll. */
  max-width: min(94vw, 960px);
}
#palette::-webkit-scrollbar { height: 4px; }

/* Phone width: 16 free swatches at 38px need ~630px, so a 400px screen
   used to show 9 of them behind a sideways scroll the pill never hinted at
   (measured 2026-09-21). Wrap to two rows of 8 instead -- every colour is
   visible without scrolling. */
@media (max-width: 700px) {
  #palette {
    flex-wrap: wrap;
    justify-content: center;
    overflow-x: visible;
    max-width: min(94vw, 340px);
  }
}

/* Locked tier's own row: wraps instead of scrolling, and is hidden
   entirely (no empty floating pill) once nothing is left to sell (buildPalette
   in app.js moves an owned colour into #palette and out of this row). */
#palette-locked {
  flex-wrap: wrap;
  justify-content: center;
  max-width: min(94vw, 420px);
}
#palette-locked:empty { display: none; }

#palette button,
#palette-locked button {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  border-radius: var(--radius-icon);
  border: 2px solid var(--outline);
  cursor: pointer;
  box-shadow: var(--shadow-sm);
}
#palette button.selected { border-color: var(--accent); border-width: 3px; transform: scale(1.1); }

/* Locked palette tier (Spec §5 step 4) -- a visually separate second row
   (#palette-locked), padlocked, opens the unlock modal. */
#palette button.locked-swatch,
#palette-locked button.locked-swatch {
  position: relative;
  opacity: 0.65;
  border-style: dashed;
}
#palette button.locked-swatch::after,
#palette-locked button.locked-swatch::after {
  content: "🔒";
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
}

/* Shared overlay shell for the unlock modal (Spec §5 step 4) and the rules
   modal (Spec §5 step 7). */
.pc-modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 40;
  background: rgba(11, 11, 15, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
}
.pc-modal-overlay[hidden] { display: none; }
.pc-modal-box {
  position: relative;
  background: var(--panel-bg);
  -webkit-backdrop-filter: var(--panel-backdrop);
  backdrop-filter: var(--panel-backdrop);
  border: var(--outline-width) solid var(--outline);
  border-radius: var(--radius-chrome);
  box-shadow: var(--shadow);
  padding: 20px;
  width: min(340px, 90vw);
  max-height: 80vh;
  overflow-y: auto;
  color: var(--ink);
  font-family: var(--pc-font-body);
}
/* Modal type scale, set here now that Tailwind's reset is gone (it used to
   flatten every heading to body size). */
.pc-modal-box h2 { font-size: 18px; margin: 0 0 12px; font-family: var(--pc-font-body); font-weight: 700; }
.pc-modal-box h3 { font-size: 14px; margin: 16px 0 8px; font-family: var(--pc-font-body); font-weight: 700; }
.pc-modal-box ul { margin: 0; padding-left: 20px; font-size: 14px; line-height: 1.45; }
.pc-modal-box p { margin: 0 0 8px; }
.pc-modal-close {
  position: absolute;
  top: 8px;
  right: 10px;
  background: transparent;
  border: none;
  color: var(--ink);
  font-size: 18px;
  cursor: pointer;
}
.pc-modal-swatch {
  width: 48px;
  height: 48px;
  border-radius: var(--radius-icon);
  border: var(--outline-width) solid var(--outline);
  margin-bottom: 10px;
}
.pc-modal-buy-msg {
  font-family: var(--pc-font-num);
  font-size: 12px;
  color: var(--danger);
  min-height: 14px;
  margin: 6px 0 0;
}

/* DEC-022 billboard tap card -- reuses .pc-modal-overlay/.pc-modal-box from
   the unlock modal above; only needs its own top anchor (never centered)
   so it can't land on top of the bottom paint dock on a phone, plus its
   own content rules. Readable at both 400px and 1200px. */
.pc-card-overlay { align-items: flex-start; padding-top: 72px; }
.pc-card-box { width: min(320px, 90vw); text-align: left; }
.pc-card-box:focus { outline: none; }
.pc-card-box:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
#pc-card-tagline { color: var(--ink-muted); font-size: 13px; margin: 0 0 10px; }
#pc-card-tagline[hidden] { display: none; }
.pc-card-tier { font-size: 12px; color: var(--ink-muted); margin: 0 0 4px; }
.pc-card-available { font-weight: 600; margin: 0; }
.pc-card-link { display: inline-block; margin-top: 4px; text-decoration: none; }
@media (max-width: 600px) {
  .pc-card-overlay { padding-top: 56px; }
  .pc-card-box { max-height: 60vh; }
}

/* DEC-021 capacity shop row -- reuses .pc-modal-box/.pc-modal-buy-msg from
   the unlock modal above; only the row-specific note needs its own rule. */
.pc-shop-row { text-align: left; }
.pc-shop-note {
  font-size: 12px;
  color: var(--ink-muted);
  margin: 6px 0 10px;
}

/* DEC-021 bot check modal -- reuses .pc-modal-overlay/.pc-modal-box/
   .pc-modal-buy-msg from the unlock modal above; only the widget mount
   point and the button spacing need their own rule. */
.pc-botcheck-box { text-align: left; }
.pc-botcheck-widget { margin: 12px 0; min-height: 65px; }
#pc-botcheck-cancel { margin-top: 4px; }

/* S6: coin + level chip (spec §5 step 2/3), populated from POST /api/me. */
.player-chip {
  background: var(--panel-bg);
  -webkit-backdrop-filter: var(--panel-backdrop);
  backdrop-filter: var(--panel-backdrop);
  border: var(--outline-width) solid var(--outline);
  border-radius: var(--radius-chrome);
  box-shadow: var(--shadow);
  padding: 6px 10px;
  font-size: 11px;
  text-align: center;
  min-width: 68px;
  color: var(--ink);
  /* DEC-021: the chip doubles as the shop's entry point (role="button"). */
  cursor: pointer;
}
.player-chip-row { font-family: var(--pc-font-num); line-height: 1.5; font-variant-numeric: tabular-nums; }
.player-level-bar {
  width: 100%;
  height: 6px;
  background: rgba(0, 0, 0, 0.12);
  border: 1px solid var(--outline);
  margin-top: 4px;
  overflow: hidden;
}
/* Segmented block level bar (CSS only -- player.js only ever sets `width`,
   never touched here): a repeating gradient reads as discrete blocks
   instead of one smooth fill, the arcade "health bar" look. */
.player-level-fill {
  height: 100%;
  width: 0%;
  background: repeating-linear-gradient(
    to right,
    var(--accent) 0px, var(--accent) 5px,
    var(--pc-black) 5px, var(--pc-black) 7px
  );
}

/* One bottom dock: zoom hint, paint controls, then the two palette rows,
   stacked in normal flow. They used to be separately fixed-positioned, and
   the palette covered the Paint button at every width (measured 1200px:
   button 687-725px under palette 682-730px). */
.pc-bottom-dock {
  flex-direction: column;
  align-items: center;
  gap: 8px;
  width: max-content;
  max-width: calc(100vw - 24px);
}
/* Phone: lift the dock clear of the map attribution strip, which must stay
   readable (licence condition). */
@media (max-width: 600px) {
  .pc-bottom-dock { bottom: 24px; }
}
.paint-controls { display: flex; gap: 8px; justify-content: center; }
.pc-charges { font-variant-numeric: tabular-nums; opacity: 0.9; margin-left: 2px; }
/* Task 3, one-tap-start hint: pulses the PAINT button once or twice on a
   genuine first visit (public/app.js's armOnboardingHint), removed on the
   first tap anywhere. Same accent-glow idiom as #hover-cell's own
   hover-cell-pulse keyframes above, scoped to a class so it never runs
   unless armed. */
.pc-onboard-pulse { animation: pc-onboard-pulse 1.4s ease-in-out 2; }
@keyframes pc-onboard-pulse {
  0%, 100% { box-shadow: var(--shadow-sm); transform: scale(1); }
  50% { box-shadow: 0 0 0 8px rgba(255, 212, 0, 0.55), var(--shadow-sm); transform: scale(1.05); }
}
.pc-zoom-hint {
  background: var(--panel-bg);
  -webkit-backdrop-filter: var(--panel-backdrop);
  backdrop-filter: var(--panel-backdrop);
  border: var(--outline-width) solid var(--outline);
  border-radius: var(--radius-chrome);
  box-shadow: var(--shadow-sm);
  padding: 6px 12px;
  font-size: 12px;
  font-family: var(--pc-font-body);
  color: var(--ink);
}
.pc-zoom-hint[hidden] { display: none; }

/* Arcade CTA button -- solid glass-outline pill by default (Confirm/Cancel);
   .primary (Paint/Buy) fills solid yellow. Button label includes the charge
   count ("Paint 30/30"), so the whole label uses the numeral font. */
.pill-btn {
  border: var(--outline-width) solid var(--outline);
  background: var(--panel-bg);
  -webkit-backdrop-filter: var(--panel-backdrop);
  backdrop-filter: var(--panel-backdrop);
  color: var(--ink);
  font-family: var(--pc-font-num);
  border-radius: var(--radius-btn);
  padding: 8px 16px;
  font-size: 13px;
  cursor: pointer;
  box-shadow: var(--shadow);
  transition: transform 80ms ease, box-shadow 80ms ease;
}
.pill-btn:active { transform: translate(4px, 4px); box-shadow: none; }
.pill-btn.primary { background: var(--accent); color: var(--accent-ink); }
/* Paint mode engaged (JS-toggled, persistent -- not the momentary :active
   press): reads as pressed-in, same mechanic as the real :active state. */
.pill-btn.primary.active { transform: translate(4px, 4px); box-shadow: none; }
.pill-btn.confirm { border-color: var(--confirm); color: var(--confirm); }
.pill-btn.cancel { border-color: var(--danger); color: var(--danger); }
/* No charges left: reads as locked/cooling down, not merely dimmed. */
.pill-btn.primary.pc-empty { background: var(--locked-bg); color: var(--locked-ink); box-shadow: none; }
.pill-btn:disabled { opacity: 0.55; cursor: not-allowed; box-shadow: none; }
/* "Need N more" must stay readable, not just faded (spec requirement) --
   the locked palette instead of a blanket opacity drop. */
.pill-btn.primary:disabled {
  background: var(--locked-bg);
  color: var(--locked-ink);
  border-color: var(--outline);
  opacity: 1;
}

#selection-count {
  background: var(--panel-bg);
  -webkit-backdrop-filter: var(--panel-backdrop);
  backdrop-filter: var(--panel-backdrop);
  border: var(--outline-width) solid var(--outline);
  border-radius: var(--radius-btn);
  box-shadow: var(--shadow-sm);
  padding: 8px 14px;
  font-family: var(--pc-font-num);
  font-size: 12px;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  display: flex;
  align-items: center;
}

#live-panel { display: none; } /* superseded by the map-anchored chrome above */

.side-panel {
  position: fixed;
  /* 340px clears the top-right icon stack + player-chip at every measured
     width -- re-measured 2026-09-21 (headless 400x800 and 1200x800): the
     chip's box is width-independent (top-right corner, fixed position) and
     its real bottom edge is 325px, not the "~275px" the old 288px value
     was tuned against -- that stale number left a real 37px vertical
     overlap at every screen size (caught live: a Replay-panel screenshot
     at 400x800 showed the panel's top border cutting through the coins
     chip). 24px clearance kept below, same as the old top/max-height
     delta. */
  top: 340px;
  right: 12px;
  z-index: 30;
  width: min(320px, 90vw);
  max-height: calc(100vh - 364px);
  overflow-y: auto;
  background: var(--panel-bg);
  -webkit-backdrop-filter: var(--panel-backdrop);
  backdrop-filter: var(--panel-backdrop);
  border: var(--outline-width) solid var(--outline);
  border-radius: var(--radius-chrome);
  box-shadow: var(--shadow);
  padding: 14px;
  color: var(--ink);
  font-family: var(--pc-font-body);
}
/* Phone: the panel's left edge (right:12px, width up to 320px) reaches
   into the legal-footer link stack pinned at left:12px/top:280px (measured
   400x800: an 8px horizontal sliver overlapped both boxes, y range
   288-352, visible in the same screenshot named above). Narrower here so
   the panel's own left edge clears the footer with real margin at every
   width this breakpoint covers. */
@media (max-width: 600px) {
  .side-panel { width: min(260px, 82vw); }
  /* WO-PHONE-LANDMARK-001: app.js's refreshLandmarksPanel renders exactly
     one row on phone widths (PC.landmarks.selectRows) -- the uppercase
     "Next landmark" heading would then be the only reason the panel needed
     a second line, so it is hidden rather than inlined: the single row
     text ("East: Grand Central · 412 px") already names the landmark, and
     inlining would force a truncation rule for long names that hiding
     avoids entirely. */
  .landmarks-panel-title { display: none; }
  /* WO-PHONE-LANDMARK-002: .side-panel's shared top:340px/right:12px corner
     still sat inside the viewport's centred 50%/50% band no matter how
     short the panel got (measured: even a 0-row panel intersected). Moves
     ONLY .landmarks-panel -- #replay-panel/#admin-panel/#live-panel keep
     .side-panel's own position untouched -- into the one genuinely free
     strip on this page: below the header/tab row (bottom edge measured
     50-63px across 360-1200px) and between the two corner icon columns
     (top-left column's right edge is a fixed 54px; top-right's left edge
     is 264-304px across 360-400px screens, right-anchored). top:64px
     clears both rows; the panel's own height (one line, no heading) never
     reaches the icon columns' y-range at all in practice, but left:58px
     still clears the left column on x alone as a second line of defence.
     width: calc(100vw - 158px) is DERIVED, not guessed: right-column left
     edge is (100vw - 12px - 83.75px) on every measured width, minus a 4px
     buffer, minus this rule's own left:58px --
     100vw - 95.75 - 4 - 58 = 100vw - 157.75, rounded up to 158 for margin.
     Capped at 260px (the old .side-panel width) so it does not grow
     unbounded as the breakpoint's own ceiling (600px) is approached. */
  .landmarks-panel {
    top: 64px;
    left: 58px;
    right: auto;
    width: min(calc(100vw - 158px), 260px);
    max-height: none;
  }
}

/* Video-HUD "next landmark per direction" panel (2026-09-22 task).
   Desktop: no new fixed position, reuses .side-panel's coordinates
   (Live-only, so it can never overlap #replay-panel/#admin-panel -- see
   index.html's own comment on this element). Small on purpose -- at most
   four short rows. Phone (WO-PHONE-LANDMARK-002): repositioned by the
   @media (max-width: 600px) block below -- see its own comment. */
.landmarks-panel-title { margin: 0 0 8px; font-family: var(--pc-font-body); font-size: 12px; font-weight: 700; color: var(--ink); text-transform: uppercase; letter-spacing: 0.4px; }
.landmarks-row { font-family: var(--pc-font-body); font-size: 12px; color: var(--ink); padding: 2px 0; }

.hint { font-size: 12px; color: var(--ink-muted); }

.replay-controls { display: flex; gap: 8px; margin: 10px 0; flex-wrap: wrap; }

fieldset { border: 2px solid var(--outline); margin-bottom: 12px; border-radius: var(--radius-chrome); }
fieldset input[type="number"], fieldset input[type="text"] { width: 60px; margin: 2px; }
#ban-token-hash { width: 100%; box-sizing: border-box; }

#admin-log { max-height: 200px; overflow-y: auto; font-size: 11px; }
#admin-log .log-row { display: flex; justify-content: space-between; gap: 6px; padding: 2px 0; border-bottom: 1px solid var(--pc-lavender-100); }
#admin-log button { font-size: 10px; }

/* Required tile-provider attribution (CARTO/OSM, viewport.js) -- restyled to
   match the light glass chrome, never hidden: rendering it visibly is a
   licence condition, not decoration. */
.maplibregl-ctrl-attrib {
  background: rgba(255, 255, 255, 0.84) !important;
  color: var(--ink) !important;
  border-top: 2px solid var(--outline) !important;
  border-left: 2px solid var(--outline) !important;
  border-radius: var(--radius-chrome) 0 0 0 !important;
}
.maplibregl-ctrl-attrib a { color: var(--ink) !important; }

body.chrome-hidden #chrome-header,
body.chrome-hidden #chrome-tabs,
body.chrome-hidden .side-panel:not(#replay-panel),
body.chrome-hidden #replay-panel .replay-controls,
body.chrome-hidden #replay-position,
body.chrome-hidden #replay-paging-hint,
body.chrome-hidden .chrome-corner,
body.chrome-hidden #hover-cell {
  display: none !important;
}

body.chrome-hidden #replay-panel { background: transparent; border: none; top: 16px; right: 16px; }

/* Visible keyboard focus everywhere -- one rule, covers every interactive
   element instead of a per-component override. */
button:focus-visible,
input:focus-visible,
select:focus-visible,
[role="button"]:focus-visible,
a:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
}

/* No press/transition motion for users who asked for it -- the instant
   :active transform still applies (a position jump, not an animation), only
   the eased transition is removed, same split the ratified spec uses. */
@media (prefers-reduced-motion: reduce) {
  .icon-btn, .pill-btn, .tab-btn, #player-chip { transition: none !important; }
}

/* Connection-health chrome (frontend build order: "a player must never be
   left looking at a silently broken page"). All three built from
   document.createElement in ui.js, never present in index.html. */

/* Live-connection pill -- top-center, below chrome-header, above the map.
   Reuses the same glass-panel look as .pill-btn/.side-panel rather than a
   third visual language. */
.pc-conn-pill {
  position: fixed;
  top: 12px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 50;
  background: var(--panel-bg);
  -webkit-backdrop-filter: var(--panel-backdrop);
  backdrop-filter: var(--panel-backdrop);
  border: var(--outline-width) solid var(--outline);
  border-radius: var(--radius-btn);
  box-shadow: var(--shadow-sm);
  padding: 8px 14px;
  font-family: var(--pc-font-body);
  font-size: 12px;
  color: var(--ink);
}
.pc-conn-pill--offline { border-color: var(--danger); color: var(--danger); }
.pc-conn-pill--online { border-color: var(--confirm); color: var(--confirm); }

/* First-load failure -- full-viewport, opaque (no backdrop-filter: there is
   nothing painted underneath yet to blur/saturate). Sits above every other
   layer including the map/canvas. */
.pc-boot-failure {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--pc-space-3);
  background: var(--bg);
  color: var(--ink);
  font-family: var(--pc-font-body);
  text-align: center;
  padding: var(--pc-space-4);
}
.pc-boot-failure-msg { font-size: 16px; max-width: 32ch; }
/* Deploy-outage guard (app.js): while the app is retrying by itself the
   Retry button is hidden and the message breathes, so the screen reads as
   "working on it" rather than "broken". */
.pc-boot-failure-busy .pc-boot-failure-msg { animation: pc-boot-breathe 1.6s ease-in-out infinite; }
@keyframes pc-boot-breathe { 50% { opacity: 0.5; } }
@media (prefers-reduced-motion: reduce) {
  .pc-boot-failure-busy .pc-boot-failure-msg { animation: none; }
}

/* Paint-failure notice -- bottom-center, same glass-pill treatment as the
   connection pill but its own element since the two can be visible at once
   (a paint attempt during a live reconnect). */
.pc-paint-notice {
  position: fixed;
  bottom: 24px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 50;
  max-width: min(360px, 90vw);
  background: var(--panel-bg);
  -webkit-backdrop-filter: var(--panel-backdrop);
  backdrop-filter: var(--panel-backdrop);
  border: var(--outline-width) solid var(--danger);
  border-radius: var(--radius-btn);
  box-shadow: var(--shadow-sm);
  padding: 8px 14px;
  font-family: var(--pc-font-body);
  font-size: 12px;
  color: var(--ink);
}

/* OD-17 roaming fly (canvas/strategy OPEN-DECISIONS.md, "did you spot
   it?"). #pc-fly is created by public/fly.js only after a placement
   succeeds (never in markup -- see fly.js's ensureFlyElement) and lives
   inside #canvas-container, which is already position:fixed; inset:0
   (style.css above), so its own top:0/left:0 + translate(x,y) tracks
   viewport pixels directly -- the same coordinate space fly.js's
   boundsFor() reads off containerEl.clientWidth/Height. #canvas-container
   itself is pointer-events:none (the map underneath owns all pan/zoom/
   click), so pointer-events is re-enabled here specifically so a tap can
   register as a catch attempt (fly.js's onCatchAttempt) -- OD-17: "no coin
   rewards ... at launch", so that attempt is a local visual flash only,
   nothing server-visible. */
.pc-fly {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 18;
  font-size: 22px;
  line-height: 1;
  pointer-events: auto;
  cursor: pointer;
  user-select: none;
  transition: transform 60ms linear;
}
.pc-fly-caught {
  transform: scale(1.6) !important;
  transition: transform 120ms ease-out;
}
/* WO-FLY-002 (founder decision): painting is the whole product -- while
   paint mode is active (public/app.js's enterPaintMode/exitPaintMode call
   fly.js's setPaintModeActive), the fly must never be able to consume a
   tap meant to paint a cell. pointer-events:none here means the browser's
   own hit-test skips this element entirely, so the click passes straight
   through to whatever is underneath (#canvas-container is already
   pointer-events:none itself, so the tap reaches the MapLibre canvas in
   #map) -- not merely "the JS handler ignores it", the DOM node is
   untouchable by the pointer for as long as this class is present. */
.pc-fly.pc-fly-passthrough {
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .pc-fly { transition: none; }
  .pc-fly-caught { transition: none; }
}
