/* Base layout for the renderer demo page. External stylesheet because the
   Day-1 CSP (default-src 'self') forbids inline styles. */

:root {
  /* Core Yuyu palette (R1). ui-primitives.jsx YUYU object is the source of
     truth; this file is the CSS translation. oklch() used where the
     prototype uses it — the WebGL2 baseline (iOS 15.4+, Chrome 111+)
     covers oklch support, so no @supports fallback is needed.

     See /tmp/yuyu-design/tokyotransit/project/ui-primitives.jsx (YUYU const)
     for the canonical token values, and frames-mobile.jsx / frames-desktop.jsx
     for usage patterns. */
  --bg: #F7F4EE;                          /* YUYU.bg (warm off-white page) */
  --card: #FFFDF8;                        /* YUYU.surface (card surface) */
  --text: #16181C;                        /* YUYU.ink (primary text) */
  --muted: #7A7A78;                       /* YUYU.muted (secondary copy) */
  --border: #E6DFD1;                      /* YUYU.hair (hairline border) */

  /* Primary action = ink. Matches the design system's Button `primary`
     variant (ui-primitives.jsx: background: YUYU.ink). Lighter hover via
     YUYU.ink2 for the standard hover-is-one-step-lighter pattern. */
  --primary: #16181C;                     /* YUYU.ink */
  --primary-hover: #3D4048;               /* YUYU.ink2 */

  /* Focal attention color: pin drop marker, pressed primary ("step up"),
     active badges, "COVERED ✓" tags, onboarding highlights. Distinct from
     --primary so the design language's semantic split stays visible in
     CSS rules. */
  --accent: oklch(0.58 0.14 35);          /* YUYU.accent (warm terracotta) */
  --accent-tint: oklch(0.92 0.04 35);     /* YUYU.accentIn (subtle focal-tint bg) */

  /* Attribution band height (≈ 11px font × 1.3 line + 2×2 padding + margin).
     Consumed by the mobile tracking-toast position so the magic number
     isn't chased across rules. */
  --attribution-band-px: 28px;
}

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  /* Was #555 — swap to palette bg so the boot screen is warm off-white
     instead of a cool grey flash before the canvas paints. */
  background: var(--bg);
  overflow: hidden;
  font: 14px/1.4 system-ui, sans-serif;
  color: var(--text);
}

canvas#c {
  display: block;
  width: 100%;
  height: 100%;
  /* Prevent iOS Safari double-tap zooming the canvas (Risk #14). */
  touch-action: none;
}

/* Visually hidden but exposed to assistive tech (aria-live still fires). */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.renderer-error {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  background: #222;
  color: #f66;
  padding: 2em;
  text-align: center;
}

/* Detail block for fatal overlays. <pre> keeps driver newlines intact
   (shader infoLogs are multi-line); overflow-wrap handles long single-word
   error strings so they don't blow past the viewport. */
.renderer-error-detail {
  white-space: pre-wrap;
  max-width: 90vw;
  overflow-wrap: anywhere;
}

/* Tile attribution. Bottom-right, ≥11 px per CARTO/OSM legibility guidance,
   white-on-translucent-black so it reads against both light Voyager tiles
   and dark city-night skins planned later. */
.attribution {
  position: fixed;
  right: 0;
  bottom: 0;
  padding: 2px 6px;
  background: #0009;
  color: #fff;
  font-size: 11px;
  line-height: 1.3;
  z-index: 10;
}
.attribution a {
  color: #fff;
  text-decoration: underline;
}
.attribution a:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
}

/* Yuyu brand wordmark (R1). Produced by src/ui/brand.ts::createWordmarkElement.
   CSS hooks defined here in R1; V2 (TopBar) adds the injection point in the
   DOM. Styles mirror the inline styles on ui-primitives.jsx Wordmark
   component (font-weight 700 + letter-spacing -0.01em + 16px title;
   10.5px/0.02em muted subtext). */
.brand-wordmark {
  display: flex;
  align-items: center;
  gap: 8px;
}
.brand-wordmark__text {
  line-height: 1;
}
.brand-wordmark__title {
  font-weight: 700;
  letter-spacing: -0.01em;
  font-size: 16px;
  color: var(--text);
}
.brand-wordmark__sep {
  color: var(--muted);
  font-weight: 500;
}
.brand-wordmark__sub {
  font-size: 10.5px;
  color: var(--muted);
  margin-top: 3px;
  letter-spacing: 0.02em;
}

/* Cards (info + tracking). Shared shell — content differs per card. */
.card {
  background: var(--card);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
  z-index: 1000;
}

/* Info card — shown on line/station selection. Lives inside
   .card-stack--left so positioning is owned by the wrapper, not the card.
   `position: relative` gives the .card__close absolute anchor (S5) a local
   positioning context — without it, the × would anchor to the wrapper. */
.card--info {
  position: relative;
  max-height: calc(100vh - 32px);
  overflow-y: auto;
  padding: 16px 18px 14px;
  min-height: 0;   /* allow flex-shrink inside .card-stack--left → internal scroll */
}
.card--info.hidden { display: none; }

.info-title {
  font-size: 16px;
  font-weight: 700;
  color: var(--text);
  margin: 0 0 4px;
}
.info-subtitle {
  font-size: 12px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin: 0 0 10px;
}
.popup-cta {
  margin: 8px 0 0;
  font-size: 13px;
}
/* Info-card hotels CTA. Promoted from text-link-with-dashed-underline to a
   styled block-level button (S7). Reuses the .pass-btn visual language —
   white fill, 1px border, 8px radius, blue hover lift — for a unified CTA
   grammar across info card and travel-passes panel. Full width of the
   parent `<p class="popup-cta">`; single-line content centered via flex.
   min-height / min-width come from the S7 unified rule further down. */
.popup-cta a {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 10px 14px;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 8px;
  text-decoration: none;
  color: var(--primary);
  font-weight: 500;
  transition: all 0.15s;
  box-sizing: border-box;
}
.popup-cta a:hover {
  border-color: var(--primary);
  /* R1: was #f8fbff + blue-tinted shadow — cool-tint jank on the warm palette.
     Now uses --accent-tint (YUYU.accentIn) for a subtle terracotta focal-attention
     signal on hover, + a warm-brown shadow matching frames-mobile.jsx. */
  background: var(--accent-tint);
  transform: translateY(-1px);
  box-shadow: 0 2px 8px rgba(20, 16, 8, 0.10);
}
/* Explicit keyboard focus — the dashed-underline implicit focus-cue is
   gone now, so match the .sidebar-toggle:focus-visible convention below. */
.popup-cta a:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

/* V1-ui station-card line list. Renders the full set of lines serving a
   selected station as a vertical list of pip+name rows. Source-of-truth
   is the design prototype's StationSheet in frames-mobile.jsx. Rows are
   built via src/ui/station_pips.ts::createLinePipRowElement (textContent
   + style.setProperty only; no innerHTML interpolation of user strings). */
.station-card__lines-list {
  display: flex;
  flex-direction: column;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  overflow: hidden;
  margin: 10px 0;
}
.station-card__line-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 14px;
  min-height: 44px;
  background: var(--card);
  border-bottom: 1px solid var(--border);
}
.station-card__line-row:last-child { border-bottom: none; }
.station-card__line-name {
  flex: 1;
  min-width: 0;
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--text);
}

/* Line pip — 22×22 rounded box, 2px colored border, white fill, 2-3 char
   code in monospace in the line's color. Matches the LinePip component
   in /tmp/yuyu-design/tokyotransit/project/ui-primitives.jsx. Color is
   set per-instance via the --pip-color CSS custom property (DOM factory
   uses style.setProperty); static fallback to YUYU.muted if unset. */
.line-pip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 6px;
  border: 2px solid var(--pip-color, var(--muted));
  background: #fff;
  color: var(--pip-color, var(--muted));
  font-size: 11px;
  font-weight: 700;
  letter-spacing: -0.01em;
  font-family: ui-monospace, 'SF Mono', Menlo, monospace;
  flex-shrink: 0;
}

/* Tracking card — top-right, shown while a vehicle is tracked. */
.card--tracking {
  position: absolute;
  top: 16px;
  right: 16px;
  width: 240px;
  padding: 14px 16px 12px;
}
.card--tracking.hidden { display: none; }
.tracking-title {
  margin: 0 0 4px;
  font-size: 14px;
  font-weight: 700;
  color: var(--text);
}
.tracking-status {
  margin: 0 0 4px;
  font-size: 12px;
  color: var(--muted);
}
.info-dwell {
  margin: 0;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  font-size: 12px;
  font-weight: 500;
}

/* Pin card — shown when the user drops a map pin. Lives inside
   .card-stack--left alongside .card--info (both describe "this location").
   Prior off-screen anchor (`right: 360px`) was a Fold-hunt artifact; see
   S2 note in CLAUDE.md for topology rationale. position: relative anchors
   the S5 .card__close button locally. */
.card--pin {
  position: relative;
  padding: 12px 14px 10px;
}
.card--pin.hidden { display: none; }
.pin-title {
  margin: 0 0 4px;
  font-size: 17px;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
/* V3: "DROPPED PIN" eyebrow. CSS ::before keeps index.html untouched —
   the static DOM is just .pin-title; the eyebrow renders above it via
   pseudo-element content. Non-selectable / non-translatable is a known
   pseudo trade-off; eyebrow copy is shipping-stable. */
.pin-title::before {
  content: 'Dropped pin';
  display: block;
  font-size: 10.5px;
  font-weight: 600;
  color: var(--muted);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  margin: 0 0 3px;
}
.pin-nearest {
  margin: 10px 0 0;
  font-size: 12px;
  color: var(--muted);
}
.pin-nearest:empty { margin: 0; }

/* V3: pin top-3 stations list — same card-shape as .station-card__lines-list
   but rows are 2-line (name + meta strip) instead of single. */
.pin-stations-list {
  display: flex;
  flex-direction: column;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  overflow: hidden;
}
.pin-station-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 14px;
  min-height: 52px;
  background: var(--card);
  border-bottom: 1px solid var(--border);
  color: var(--text);
}
.pin-station-row:last-child { border-bottom: none; }
.pin-station-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.pin-station-name {
  font-size: 14px;
  font-weight: 600;
  letter-spacing: -0.005em;
}
.pin-station-meta {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 11.5px;
  color: var(--muted);
}
.pin-station-dist {
  margin-left: 4px;
  font-variant-numeric: tabular-nums;
}
.pin-station-row > svg {
  flex-shrink: 0;
  color: var(--muted);
}

/* V3: compact pip variant for dense meta rows (pin list + future uses).
   Mirrors the .line-pip chrome at reduced size; same --pip-color hook. */
.line-pip--sm {
  width: 16px;
  height: 16px;
  border-radius: 4px;
  border-width: 1.5px;
  font-size: 9px;
}

/* V3: pin hotel nudge — inline affiliate anchor below the stations list.
   accent-tint bg signals "this is a related-product link, not a primary
   action." AFF tag + bed icon locate the ad in the "looks-like-ad" band
   so users can disambiguate from transit content. */
.pin-hotel-nudge {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 10px;
  padding: 10px 12px;
  border-radius: 10px;
  background: var(--accent-tint);
  color: var(--text);
  text-decoration: none;
  min-height: 44px;
}
.pin-hotel-nudge:hover { background: var(--accent-tint); filter: brightness(0.98); }
.pin-hotel-nudge__icon { color: var(--accent); flex-shrink: 0; }
.pin-hotel-nudge__copy {
  flex: 1;
  min-width: 0;
  font-size: 12.5px;
}

/* V3: early-boot fallback when PointStore is empty (pin drop raced data
   load). Rare on broadband; common on slow-network first visits. */
.pin-stations-empty {
  margin: 0;
  font-size: 12px;
  color: var(--muted);
  font-style: italic;
}

/* V3: pass-coverage nudge — sibling beneath the station card lines list,
   dashed-pill styling to differentiate from primary content. Single anchor;
   clicking routes to the Klook Tokyo Subway Pass affiliate link. */
.pass-nudge {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 14px;
  padding: 10px 12px;
  border-radius: 10px;
  background: transparent;
  border: 1px dashed var(--border);
  text-decoration: none;
  color: var(--text);
  min-height: 44px;
}
.pass-nudge:hover { background: var(--accent-tint); border-color: var(--accent-tint); }
.pass-nudge__icon {
  width: 28px;
  height: 28px;
  border-radius: 8px;
  background: var(--accent-tint);
  color: var(--accent);
  display: grid;
  place-items: center;
  flex-shrink: 0;
}
.pass-nudge__copy {
  flex: 1;
  min-width: 0;
  font-size: 12.5px;
  line-height: 1.4;
  color: var(--muted);
}
.pass-nudge__copy b { color: var(--text); font-weight: 600; }
.pass-nudge__chevron { color: var(--muted); flex-shrink: 0; }

/* V3: small-caps affiliate signal. Lives inside any anchor that points
   at a monetized partner link; visually distinguishes from transit UI. */
.aff-tag {
  font-size: 10.5px;
  color: var(--muted);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-weight: 600;
  flex-shrink: 0;
}

/* S5: Card × close button. 44×44 tap target (min-height/min-width from the
   S7 unified `button` rule). Absolute-positioned top-right of each card;
   cards carry position:relative/absolute/fixed so the × anchors locally.
   Card titles get `padding-right: 44px` so long titles don't flow under the
   × glyph (the × is absolute-positioned, so content flows under it by
   default — padding forces a visual gutter). */
.card__close {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 44px;
  height: 44px;
  padding: 0;
  background: transparent;
  border: none;
  border-radius: 50%;
  cursor: pointer;
  color: var(--muted);
  font-size: 24px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: color 0.12s, background 0.12s;
}
.card__close:hover { color: var(--text); background: var(--bg); }
.card__close:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: -2px;
}
.info-title, .tracking-title, .pin-title { padding-right: 44px; }

/* V4: First-run welcome card (replaces S3's pill banner). Bottom-centered
   dark card covering the grey-canvas boot moment. Inverse palette — ink bg
   with warm fg — deliberately distinct from other cards so a returning
   visitor's muscle memory doesn't confuse it with info/tracking/pin cards.
   Bottom-center placement + 440 px max-width works on desktop (plenty of
   margin to each side) and mobile (full-width minus gutters) in a single
   CSS path; no breakpoint fork needed per V4 design review.

   pointer-events: none on the outer card means drags pass through to the
   map — only children (CTA + × + canvas pointerdown listener) receive
   events via the `.welcome-card > *` override below. */
.welcome-card {
  position: fixed;
  bottom: calc(var(--attribution-band-px) + 28px);
  left: 50%;
  transform: translateX(-50%);
  max-width: 440px;
  width: calc(100% - 28px);
  background: var(--text);
  color: var(--bg);
  padding: 18px 20px 16px;
  border-radius: 18px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
  z-index: 1000;
  pointer-events: none;
  transition: opacity 0.4s;
}
.welcome-card.hidden { display: none; }
.welcome-card.fading { opacity: 0; }
/* Children (× button, CTA, listeners) receive events; outer card does not.
   Canvas pointerdown still dismisses because its listener is on #c, not
   blocked by this rule. */
.welcome-card > * { pointer-events: auto; }

/* Brand slot — populated at attach time by createWordmarkElement (R1).
   The wordmark's inherited colors invert against the ink card via the
   welcome-card's `color: var(--bg)`; brand.ts writes textContent that
   picks up currentColor + direct color: references as needed. */
.welcome-card__brand-slot {
  margin-bottom: 12px;
}
.welcome-card .brand-wordmark__title,
.welcome-card .brand-wordmark__sub {
  color: var(--bg);
}
.welcome-card .brand-wordmark__sub {
  opacity: 0.6;
}

.welcome-card__desc {
  margin: 0 0 14px;
  font-size: 14.5px;
  line-height: 1.5;
  opacity: 0.9;
}

.welcome-card__hints {
  margin: 0 0 14px;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.welcome-card__hints li {
  font-size: 12.5px;
  opacity: 0.85;
  line-height: 1.4;
  padding-left: 14px;
  position: relative;
}
.welcome-card__hints li::before {
  content: '·';
  position: absolute;
  left: 4px;
  color: var(--accent);
  font-weight: 600;
}

/* Primary CTA. Re-inverts the palette (warm bg, ink fg) so it visually
   pops on the dark card. Disabled state is opacity-only + native disabled
   (cursor not-allowed via UA default). */
.welcome-card__cta {
  display: block;
  width: 100%;
  min-height: 44px;
  padding: 0 16px;
  background: var(--bg);
  color: var(--text);
  border: none;
  border-radius: 10px;
  font-family: inherit;
  font-size: 14px;
  font-weight: 600;
  letter-spacing: -0.005em;
  cursor: pointer;
}
.welcome-card__cta:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.welcome-card__cta[disabled] {
  opacity: 0.55;
  cursor: not-allowed;
}

.welcome-card__footer {
  margin: 10px 0 0;
  font-size: 11px;
  text-align: center;
  opacity: 0.45;
}

/* × close button inherits S5's .card__close visual (44×44 circle + hover/
   focus). Override the outer pointer-events: none so it's clickable, and
   invert the color so × is visible against the ink-dark surface. */
.welcome-card .card__close {
  pointer-events: auto;
  color: var(--bg);
  opacity: 0.7;
}
.welcome-card .card__close:hover {
  color: var(--bg);
  opacity: 1;
  background: rgba(255, 255, 255, 0.08);
}

/* ---------------------------------------------------------------------------
   Left-rail card stack. Owns positioning + width for .card--info and
   .card--pin (which both describe "this location" — selected feature or
   dropped pin). Pre-S2, pin floated at `right: 360px` and info at
   `left: 16px`; the two cards are now task-grouped in one stack.

   Desktop: absolute top-left, 320 px wide, capped at viewport height.
   Mobile (<=600px): becomes a bottom-sheet via the @media block below.

   pointer-events: none on the wrapper = map pan/drag passes through the
   gap between cards (including the full wrapper strip when both are
   hidden). Cards opt back in via `.card-stack--left > .card`.
   --------------------------------------------------------------------------- */

.card-stack--left {
  position: absolute;
  top: 16px;
  left: 16px;
  width: 320px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-height: calc(100vh - 32px);
  z-index: 1000;
  pointer-events: none;
}
.card-stack--left > .card {
  pointer-events: auto;
}
.card-stack--left > .card--pin {
  flex-shrink: 0;   /* pin never shrinks; info absorbs space pressure via its own overflow */
}

/* ---------------------------------------------------------------------------
   Sidebar + search stack (right rail). Ported from legacy public/styles.css,
   simplified: no ad-slots, no SPA overlay, no legend-inline.

   The card-stack wraps the always-visible search card on top and the
   collapsible sidebar card below. Collapsed sidebar is a 48px gear bubble
   (right-aligned via align-self:flex-end); expanded it fills the stack
   width and becomes a full settings card. Toggle handled by
   ui/sidebar.ts::attachSidebarHandlers flipping .sidebar--collapsed +
   .collapsed on sidebar-body.
   --------------------------------------------------------------------------- */

.card-stack {
  position: absolute;
  top: 16px;
  right: 16px;
  width: 320px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-height: calc(100vh - 32px);
  z-index: 1000;
}

.card--search {
  flex-shrink: 0;
  padding: 10px 12px;
  position: relative;
  z-index: 2;
}
.card--search input[type="text"] {
  width: 100%;
  padding: 10px 14px;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 14px;
  font-family: inherit;
  background: #fff;
  outline: none;
  color: var(--text);
  box-sizing: border-box;
}
.card--search input[type="text"]:focus {
  border-color: var(--primary);
  /* V2: cool-tint residual caught by reviewer's check. The old rgba(37, 99, 235)
     was the pre-R1 blue #2563eb; R1's Nudge 1 swept 4 cool-tint hovers but
     missed this focus-ring. rgba(20, 16, 8, *) is the canonical warm shadow
     palette per R1 Nudge 1 (frames-mobile.jsx TopBar + StationSheet hue). */
  box-shadow: 0 0 0 3px rgba(20, 16, 8, 0.15);
}

.search { position: relative; }

/* V2: search leading icon. Hidden on desktop (preserves S2's existing
   card-padded input-pill layout pixel-identical); the mobile @media block
   below flips this to display:block and reshapes .search as a flex row. */
.search__icon {
  flex-shrink: 0;
  color: var(--muted);
  display: none;
}

.search-results {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 7px;
  max-height: 260px;
  overflow-y: auto;
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.12);
  z-index: 30;
  display: none;
}
.search-results.active { display: block; }

.search-result {
  padding: 9px 11px;
  cursor: pointer;
  border-bottom: 1px solid var(--border);
  line-height: 1.35;
}
.search-result:last-child { border-bottom: none; }
.search-result:hover,
.search-result.kbd-active { background: #eef3ff; }
.search-result .primary {
  font-size: 13px;
  font-weight: 500;
  color: var(--text);
}
.search-result .secondary {
  font-size: 11px;
  color: var(--muted);
  margin-top: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.search-empty {
  padding: 12px;
  font-size: 12px;
  color: var(--muted);
  text-align: center;
}

.status {
  font-size: 10px;
  color: var(--muted);
  text-align: right;
  line-height: 1.3;
  text-overflow: ellipsis;
  overflow: hidden;
  white-space: nowrap;
  margin-top: 4px;
}
.status:empty { display: none; }
.status.error { color: #b91c1c; }
.status.ok    { color: #047857; }

/* V5: sidebar position migrated from S2's top-right to bottom-right anchor
   near the settings-cluster. Reviewer's Flag-A β read: click-to-expand UX
   is noticeably better when the expanded card appears near where the user
   just clicked (bottom-right cluster → bottom-right sidebar). The prior α
   option (sidebar stays top-right) was initially sealed then reversed.
   transform-origin: bottom right lets any future expand animation grow
   upward from the click site. Width stays 320 to match V2 card-stack
   convention; height is bounded so the expanded card doesn't eclipse the
   attribution band. Desktop-only; mobile full-sheet rule below overrides. */
.card--sidebar {
  position: absolute;
  right: 16px;
  bottom: calc(var(--attribution-band-px) + 72px);
  top: auto;
  width: 320px;
  max-height: calc(100vh - var(--attribution-band-px) - 104px);
  transform-origin: bottom right;
  z-index: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
  transition: width 0.3s ease, border-radius 0.3s ease;
}
/* V5: collapsed state entirely hidden on desktop — the bubble cluster
   below is the new open-trigger; the in-sidebar toggle is only a close ×
   when expanded. Dimension reset vs S6's 48-px circle (which no longer
   renders since collapsed → display: none). */
.card--sidebar.sidebar--collapsed {
  display: none;
}

/* V5: #sidebar-toggle retained as the close × only (S6 pill machinery
   retired; .sidebar-toggle__gear and .sidebar-toggle__title spans dropped
   from index.html). Sits top-right of the expanded card. */
.sidebar-toggle {
  position: absolute;
  top: 8px;
  right: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  min-height: 44px;
  border: none;
  background: transparent;
  border-radius: 50%;
  cursor: pointer;
  font-family: inherit;
  color: var(--muted);
  transition: color 0.12s, background 0.12s;
}
.sidebar-toggle:hover { color: var(--text); background: var(--bg); }
.sidebar-toggle:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: -2px;
}
.sidebar-toggle__close {
  font-size: 24px;
  line-height: 1;
}

.card-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 0 18px 14px;
  max-height: 100vh;
  opacity: 1;
  transition: max-height 0.28s ease, padding 0.22s ease, opacity 0.22s ease;
}
.card-body.collapsed {
  max-height: 0;
  padding-top: 0;
  padding-bottom: 0;
  opacity: 0;
  pointer-events: none;
  overflow: hidden;
}

.panel { margin: 14px 0; }
.panel:first-of-type { margin-top: 4px; }
.panel h2 {
  margin: 0 0 6px;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  color: var(--muted);
  letter-spacing: 0.06em;
}

/* S7: Touch target minimums. Apple HIG = 44 CSS px; Material = 48.
   We target the 44 floor. Unified across interactive primitives:
   <button>, .toggle label-rows, .input-num, the promoted info-card CTA,
   and .pass-btn. `min-*` (not fixed dims) so controls already above the
   floor — .sidebar-toggle at 46-48, .pass-btn at ~55 — stay unchanged.

   Why min-height not padding: floor semantics compose cleanly with the
   existing flex `align-items: center` on .toggle (content vertical-centers
   inside the 44px-tall row without explicit padding math), and the rule
   stays correct if someone bumps the font-size later.

   Density cost on mobile: .toggle rows grow ~25 → 44 (+19 each); three
   toggles per panel × two panels + input-num ≈ +100 px in the sidebar
   bottom-sheet. .card-body has overflow-y: auto (S2); content scrolls
   gracefully. Zero desktop impact on controls already ≥44. */
button, .toggle, .input-num, .popup-cta a, .pass-btn {
  min-height: 44px;
  min-width: 44px;
}

.toggle {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 0;
  cursor: pointer;
  font-size: 13px;
  user-select: none;
}
.toggle input[type="checkbox"] {
  width: 15px;
  height: 15px;
  cursor: pointer;
  accent-color: var(--primary);
}
.toggle--inline { gap: 6px; }

.input-num {
  width: 48px;
  padding: 3px 6px;
  border: 1px solid var(--border);
  border-radius: 5px;
  font-size: 12px;
  font-family: inherit;
  text-align: center;
  outline: none;
}
.input-num:focus { border-color: var(--primary); }

.btn {
  display: block;
  width: 100%;
  padding: 10px 14px;
  border: none;
  border-radius: 8px;
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  font-family: inherit;
  transition: background 0.15s;
}
.btn--primary {
  background: var(--primary);
  color: var(--bg);   /* Was hardcoded white; palette-consistent warm off-white on ink */
}
.btn--primary:hover { background: var(--primary-hover); }
.btn--primary.active { background: var(--accent); }

/* Task #35 item 2 (path d): hide the sidebar Find panel on desktop,
   where the V5 bubble cluster's Drop-pin affordance is the sole
   pin-drop trigger. Mobile keeps the Find panel visible — the mobile
   cluster is display:none (see mobile @media), so the sidebar remains
   the only pin-drop path on mobile (open sidebar via TopBar settings
   → Find panel → #pin-btn). See V5 Q3 lean for the coexistence
   history; reviewer's option (d) reverse-media-query removes desktop
   redundancy without touching mobile affordances.

   Uses `:has(#pin-btn)` to hide the entire Find panel (h2 + button +
   help-text) rather than orphan-hiding just the button. Same :has()
   primitive V6 uses for the settings-cluster-on-sidebar-expand rule;
   browser baseline (Chrome 105+, Safari 15.4+, FF 121+) confirmed.

   Active-mirror feedback gap (cluster-pin on desktop doesn't visually
   reflect pinMode state today) is parked as a separate V-followup
   hygiene item; not absorbed into #35. See CLAUDE.md parking lot. */
@media (min-width: 601px) {
  .panel:has(#pin-btn) {
    display: none;
  }
}

.help-text {
  font-size: 11px;
  color: var(--muted);
  margin: 6px 0 0;
  line-height: 1.4;
}

.pass-btn {
  display: block;
  padding: 10px 12px;
  margin: 6px 0;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 8px;
  text-decoration: none;
  color: var(--text);
  transition: all 0.15s;
}
.pass-btn:hover {
  border-color: var(--primary);
  /* R1: matches .popup-cta a:hover treatment — warm focal tint + warm shadow. */
  background: var(--accent-tint);
  transform: translateY(-1px);
  box-shadow: 0 2px 8px rgba(20, 16, 8, 0.10);
}
.pass-btn__title { font-size: 13px; font-weight: 600; }
.pass-btn__sub { font-size: 11px; color: var(--muted); margin-top: 2px; }

/* H4: pre-H4 .affiliate-note rule (9px italic) retired. Replaced by the
   11px regular + clickable-link rule near end of file. The sidebar
   upgrade was a core H4 deliverable: prior 9px was FTC-illegible and
   un-clickable. */

/* ---------------------------------------------------------------------------
   Responsive layout (S2). Breakpoints:
     - <=600 px: phone / small tablet portrait. Right-stack becomes a
       top-rail full-bleed; left-stack (info + pin) becomes a bottom-sheet;
       tracking card becomes a toast above the attribution band.
     - <=340 px: Galaxy Z Fold cover. Expanded sidebar becomes a modal
       bottom-sheet so narrow-body content has room to breathe.

   z-index ladder (explicit, mobile):
     canvas(0) < .card default / mobile tracking toast (1000) <
     mobile left-stack bottom-sheet (1010) < Fold sidebar modal (1100).
   The +10 on the sheet is load-bearing: tracking-card sits later in the
   DOM than .card-stack--left, so at equal z-index tracking would paint
   on top. Bumping the sheet ensures "dismiss info/pin to reveal tracking"
   — the UX contract documented on the .card-stack--left rule below.
   --------------------------------------------------------------------------- */

@media (max-width: 600px) {
  /* V2: top-rail becomes a horizontal row of pills (search-card + gear).
     V6 will insert a city pill as the first child when multi-city lands;
     flex-gap already reserves space. Design source: frames-mobile.jsx
     TopBar (lines 34-75) — absolute top-10 / inset 10, 42-px pills, 6-px
     gap, warm 0 1px 2px shadow. */
  .card-stack {
    top: 10px;
    right: 10px;
    left: 10px;
    width: auto;
    max-height: none;
    flex-direction: row;
    align-items: center;
    gap: 6px;
  }

  /* Search pill: fill remaining row space, 42-px design height, warm
     shadow. Input itself goes naked (border/bg/padding stripped) so the
     card's pill-shape is the only visible chrome. Search leading icon
     becomes visible (global .search__icon is display:none). */
  .card--search {
    flex: 1;
    height: 42px;
    padding: 0 12px;
    box-shadow: 0 1px 2px rgba(20, 16, 8, 0.04);
  }
  .search {
    display: flex;
    align-items: center;
    gap: 10px;
  }
  .search__icon {
    display: block;
  }
  .card--search input[type="text"] {
    flex: 1;
    padding: 10px 0;
    border: none;
    background: transparent;
  }

  /* V5: collapsed sidebar is invisible on both desktop AND mobile — desktop
     uses the bubble cluster as the open-trigger; mobile uses the V2 TopBar
     settings button. The card--sidebar only becomes visible in its
     expanded state via the full-sheet rule below. */
  .card--sidebar.sidebar--collapsed { display: none; }

  /* V5: mobile full-sheet (supersedes the prior Fold-cover 340-px modal
     rule by covering the broader 600-px-and-below breakpoint). Expands the
     sidebar to the viewport edges below the TopBar (top: 120px preserves
     the V2 search + settings bar). z-index 1100 sits above the left-rail
     bottom-sheet (1010) so opening settings visually dominates. Drag
     handle ::before matches the V2 .card-stack--left pattern. */
  .card--sidebar:not(.sidebar--collapsed) {
    position: fixed;
    top: 120px;
    left: 0;
    right: 0;
    bottom: 0;
    width: auto;
    max-height: none;
    border-radius: 20px 20px 0 0;
    box-shadow: 0 -8px 30px rgba(20, 16, 8, 0.18);
    overflow-y: auto;
    padding-top: 26px;
    z-index: 1100;
  }
  .card--sidebar:not(.sidebar--collapsed)::before {
    content: '';
    position: absolute;
    top: 10px;
    left: 50%;
    transform: translateX(-50%);
    width: 36px;
    height: 4px;
    border-radius: 2px;
    background: var(--border);
  }

  /* Left-rail (info + pin) → bottom-sheet when either card is active.
     Sheet takes visual priority over .card--tracking (which sits at
     bottom: --attribution-band-px above the attribution band). User
     dismisses info/pin (map-tap-empty in today's build; × button + Esc
     land in S5) to reveal tracking when both coexist. In the common
     state (no selection, no pin) both children have .hidden → display:
     none → stack collapses to 0 height → no conflict. */
  .card-stack--left {
    position: fixed;
    top: auto;
    left: 0;
    right: 0;
    bottom: 0;
    width: auto;
    /* V2: 78vh matches design's StationSheet. V1-ui's full line list for
       busy stations (Shibuya ≈10 lines × 44 px = 440 px) needs the
       headroom; 50vh on a 600-tall viewport clipped the list. */
    max-height: 78vh;
    gap: 0;
    background: var(--card);
    /* V2: 20-px top corners + warm shadow to match design StationSheet.
       The ::before drag-handle sits inside the padding-top reservation. */
    border-radius: 20px 20px 0 0;
    box-shadow: 0 -4px 20px rgba(20, 16, 8, 0.08);
    overflow-y: auto;
    /* 26 px = 10 (handle top) + 4 (handle height) + 12 (breathing room).
       Cards inside lose their chrome via the > .card rule below, so this
       padding doesn't stack against any card-internal padding. */
    padding-top: 26px;
    /* Explicit z: wrapper background + box-shadow must render above canvas,
       AND the sheet must paint over .card--tracking (which sits later in
       the DOM at the default .card z:1000). +10 gives the sheet priority
       without exceeding the Fold sidebar modal (1100). */
    z-index: 1010;
    /* Opaque bottom-sheet needs clicks (dismiss target is the card, not the
       wrapper gap — there is no gap in mobile layout). */
    pointer-events: auto;
  }
  /* V2: drag-handle bar at the top of the sheet. Decorative only in V2 —
     no pointer-down / resize handler. Matches design's 36×4 rounded
     handle. Absolute-positioned against .card-stack--left (position:fixed
     creates a positioning context). */
  .card-stack--left::before {
    content: '';
    position: absolute;
    top: 10px;
    left: 50%;
    transform: translateX(-50%);
    width: 36px;
    height: 4px;
    border-radius: 2px;
    background: var(--border);
  }
  .card-stack--left > .card {
    border: none;
    border-radius: 0;
    box-shadow: none;
  }
  .card-stack--left > .card + .card {
    border-top: 1px solid var(--border);   /* visual divider between stacked info & pin */
  }
  .card--info {
    max-height: none;
    overflow-y: visible;                   /* sheet itself scrolls, not the card */
  }

  /* Tracking card → toast above the attribution band. Full-bleed horizontally
     with a small gutter; pinned to bottom so it doesn't fight the top-rail
     search / sidebar for attention. */
  .card--tracking {
    top: auto;
    right: 8px;
    left: 8px;
    bottom: var(--attribution-band-px);
    width: auto;
  }
}

/* V5: the Fold-cover (340-px) sidebar-expanded rule retired. The broader
   600-px full-sheet rule above now covers both Fold cover and standard
   mobile in a single code path. If Fold cover ever needs a distinct
   sidebar treatment (e.g., less padding), add a narrow 340-specific
   override — but the full-sheet shape works as-is at both widths. */

/* V5: settings bubble cluster (desktop-only). Three vertically-stacked
   buttons at bottom-right: Layers (labeled pill), Drop pin (labeled pill),
   Gear (compact unlabeled circle). Design source at frames-desktop.jsx:175-206.
   z-index 1000 matches the overlay tier (.card-stack) — below the mobile
   full-sheet's 1100 so opening settings on mobile covers the cluster. */
.settings-cluster {
  position: absolute;
  right: 20px;
  bottom: calc(var(--attribution-band-px) + 46px);
  z-index: 1000;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
  pointer-events: none;  /* children re-enable via `> *` below */
}
.settings-cluster > * { pointer-events: auto; }

/* V5: hide the cluster when the sidebar is expanded. Per reviewer's β-math
   pressure-test: the cluster at bottom: 46 + height ~130 = top at 176px; the
   expanded sidebar at bottom: 100 (attribution-band + 72) overlaps with the
   cluster's bottom. Visually confusing AND the sidebar's z-index 1 (inside
   .card-stack's 1000 stacking context) loses to the cluster's direct 1000,
   so the cluster would paint over the sidebar. Reviewer's (B) path: hide
   the cluster on expand — the user just triggered the expansion; the
   bubbles become redundant until close.

   Uses :has() (Chrome 105+, Safari 15.4+, FF 121+) which lands within the
   WebGL2 baseline. Graceful degradation: older browsers get the visual
   overlap (cosmetic only, no functional break), which is acceptable for
   the Firefox-under-121 edge case. */
body:has(.card--sidebar:not(.sidebar--collapsed)) .settings-cluster {
  display: none;
}

.cluster-btn {
  display: inline-flex;
  align-items: center;
  background: var(--card);
  border: 1px solid var(--border);
  color: var(--text);
  font-family: inherit;
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(20, 16, 8, 0.06);
  transition: background 0.12s, color 0.12s;
}
.cluster-btn:hover { background: var(--bg); }
.cluster-btn:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}
/* Pill variant — Layers + Drop pin labeled triggers. 38-px height matches
   design; padding gives a comfortable left-icon + label spacing. */
.cluster-btn--pill {
  height: 38px;
  padding: 0 14px 0 10px;
  border-radius: 999px;
  gap: 8px;
}
/* Circle variant — Gear unlabeled trigger. 38-px square with radius 999
   yields a circle. */
.cluster-btn--circle {
  width: 38px;
  height: 38px;
  padding: 0;
  border-radius: 999px;
  justify-content: center;
}
.cluster-btn__icon {
  flex-shrink: 0;
  color: var(--text);
}
/* Active state: Drop pin bubble highlights when pin-drop mode is on. The
   sidebar.ts handler flips a class on the existing #pin-btn via setPinMode
   (look for `.active`). The cluster version reflects the same state via a
   future .active mirror. V5 ships without the active-mirror; V5.1 hygiene
   slice can wire it — the visual grammar is defined here so wiring is a
   1-line DOM mutation. */
.cluster-btn.active {
  background: var(--accent-tint);
  color: var(--accent);
  border-color: var(--accent);
}

/* V5: global 48-px tap-target uplift on mobile (S7 shipped 44-px; TL's V5
   dispatch asked "48 px across the board" for mobile). Covers new V5
   affordances (cluster-btn, sidebar-toggle) and existing interactive
   surfaces. Desktop sizes stay 44-px per S7 — the WCAG 2.5.5 AAA bar is
   44-px, desktop mouse input doesn't need the larger target. */
@media (max-width: 600px) {
  button,
  .toggle,
  .input-num,
  .popup-cta a,
  .pass-btn,
  .welcome-card__cta,
  .station-card__line-row,
  .pin-hotel-nudge,
  .pass-nudge,
  .pin-station-row,
  .card__close,
  .sidebar-toggle,
  .cluster-btn {
    min-height: 48px;
  }
  .card__close,
  .sidebar-toggle,
  .cluster-btn--circle {
    min-width: 48px;
    width: 48px;
    height: 48px;
  }
  /* V5: settings cluster hidden on mobile — the V2 TopBar settings button
     already handles sidebar opening; cluster would be redundant chrome in
     a tighter viewport. */
  .settings-cluster { display: none; }

  /* Card title right-padding compensates for the 48-px × glyph area (up
     from 44-px on desktop). Prevents long titles from flowing under the
     absolute-positioned ×. */
  .info-title,
  .tracking-title,
  .pin-title {
    padding-right: 48px;
  }
}

/* ---------------------------------------------------------------------------
   V6 — city picker chip + mobile TopBar settings button + picker sheet.

   Scope: mobile-only. Desktop hides these via .city-pill, .mobile-settings,
   .city-picker-sheet each with `display: none` in the outer (desktop)
   cascade; mobile @media re-shows them.

   DOM: three top-level body siblings (NOT inside .card-stack) so the
   desktop flex-col structure stays unchanged. Mobile @media uses fixed
   positioning to lay out [city-pill, .card-stack's search, mobile-settings]
   as a horizontal TopBar row.

   This block fixes a V5 regression (commit db25096) where mobile lost
   its only settings-open trigger after the collapsed sidebar went to
   `display: none` globally. The mobile-settings button re-grants access.
   --------------------------------------------------------------------------- */

/* Desktop: hidden entirely. Mobile @media below re-shows. */
.city-pill,
.mobile-settings {
  display: none;
}
.city-picker-sheet {
  display: none;
}

@media (max-width: 600px) {
  /* City pill — top-left of the mobile TopBar row. 42-px height matches
     V2 TopBar grammar (search card is 42). 48-px min-height from V5
     global uplift. */
  .city-pill {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    position: fixed;
    top: 10px;
    left: 10px;
    height: 42px;
    min-height: 48px;
    padding: 0 10px 0 8px;
    border-radius: 12px;
    background: var(--card);
    border: 1px solid var(--border);
    color: var(--text);
    font-family: inherit;
    font-size: 12.5px;
    font-weight: 500;
    cursor: pointer;
    box-shadow: 0 1px 2px rgba(20, 16, 8, 0.04);
    z-index: 1010;  /* above .card-stack (1000), below picker sheet (1200) */
  }
  .city-pill:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
  }
  .city-pill__flag {
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: var(--bg);
    display: inline-grid;
    place-items: center;
    font-size: 13px;
  }
  .city-pill__name {
    letter-spacing: -0.005em;
  }
  .city-pill__chevron {
    color: var(--muted);
    flex-shrink: 0;
  }

  /* Mobile settings — top-right of the mobile TopBar row. Icon-only
     pill; matches the V2 TopBar grammar + V5 48-px touch target. */
  .mobile-settings {
    display: inline-grid;
    place-items: center;
    position: fixed;
    top: 10px;
    right: 10px;
    width: 42px;
    height: 42px;
    min-width: 48px;
    min-height: 48px;
    border-radius: 12px;
    background: var(--card);
    border: 1px solid var(--border);
    color: var(--text);
    cursor: pointer;
    box-shadow: 0 1px 2px rgba(20, 16, 8, 0.04);
    z-index: 1010;
  }
  .mobile-settings:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
  }
  .mobile-settings__icon { color: var(--text); }

  /* The existing .card-stack (search-card wrapper) sits between city-pill
     and mobile-settings on mobile. Shift its anchor so the 3 elements form
     a TopBar row. city-pill + mobile-settings are ~48-px square with 10-px
     viewport gutters → search gets (100% - 48*2 - 10*4) = (100% - 136)
     width between them. */
  .card-stack {
    left: calc(10px + 48px + 10px);
    right: calc(10px + 48px + 10px);
    width: auto;
  }

  /* Picker sheet — 30vh bottom-anchored non-opaque modal. Visually
     non-modal (user sees the map); ladder-modal in dismissal (inert
     siblings + role=dialog + aria-modal set dynamically on open). */
  .city-picker-sheet {
    display: flex;
    flex-direction: column;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    max-height: 40vh;
    padding: 26px 16px 20px;  /* 26-px top-padding reserves drag-handle zone */
    background: var(--card);
    border-radius: 20px 20px 0 0;
    box-shadow: 0 -8px 30px rgba(20, 16, 8, 0.18);
    z-index: 1200;  /* above sidebar full-sheet (1100) + cluster (1000) */
    overflow-y: auto;
    transition: opacity 0.25s ease;
  }
  .city-picker-sheet.hidden {
    display: none;
  }
  .city-picker-sheet::before {
    content: '';
    position: absolute;
    top: 10px;
    left: 50%;
    transform: translateX(-50%);
    width: 36px;
    height: 4px;
    border-radius: 2px;
    background: var(--border);
  }
  .city-picker-sheet .card__close {
    /* Override outer .card__close absolute top:8 right:8 — V5's rule
       already positions it correctly; override color for this dark-ish
       sheet surface. */
    color: var(--muted);
  }
  .city-picker-sheet__title {
    margin: 0 0 12px;
    padding-right: 48px;  /* room for absolute × at top-right */
    font-size: 15px;
    font-weight: 700;
    letter-spacing: -0.01em;
    color: var(--text);
  }
  .city-picker-sheet__list {
    display: flex;
    flex-direction: column;
    gap: 2px;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 10px;
    overflow: hidden;
  }
  .city-picker-sheet__stub {
    margin: 12px 0 0;
    font-size: 11.5px;
    color: var(--muted);
    text-align: center;
    font-style: italic;
  }

  /* Individual city row inside the picker. Active = accent-tint bg +
     checkmark icon (reuses V3 pass-coverage accent-tint grammar). */
  .city-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px 14px;
    min-height: 48px;
    background: var(--card);
    border: none;
    color: var(--text);
    font-family: inherit;
    font-size: 14px;
    font-weight: 500;
    cursor: pointer;
    text-align: left;
  }
  .city-row:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: -2px;
  }
  .city-row--active {
    background: var(--accent-tint);
    color: var(--text);
  }
  .city-row__flag {
    width: 24px;
    height: 24px;
    border-radius: 50%;
    display: inline-grid;
    place-items: center;
    font-size: 14px;
    background: #fff;
    flex-shrink: 0;
  }
  .city-row__name { flex: 1; }
  .city-row__check {
    color: var(--accent);
    flex-shrink: 0;
  }
}

/* ---------------------------------------------------------------------------
   H4 — affiliate disclosure overlay + 4 entry surfaces.

   FTC §16 CFR 255 "clear and conspicuous" compliance via 5 surfaces, per
   FTC v. DIRECTV / FTC v. Amazon case law (near-the-link disclosures
   carry more weight than out-of-context ones).

   Surfaces styled here (JS wiring in src/ui/disclosure.ts):
   1. Welcome card .welcome-card__disclosure line (styled below).
   2. #disclosure-page modal overlay (hybrid desktop-centered / mobile-sheet).
   3. (AFF-tag tooltips — no CSS, just runtime span attrs).
   4. Sidebar .panel__aff-chip + upgraded .affiliate-note.
   5. Attribution #attribution-affiliate-link.
   --------------------------------------------------------------------------- */

/* Welcome card disclosure line. Appended below .welcome-card__footer. */
.welcome-card__disclosure {
  margin: 6px 0 0;
  font-size: 11.5px;
  text-align: center;
  opacity: 0.72;
  line-height: 1.4;
}
.welcome-card__disclosure a {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}
.welcome-card__disclosure a:hover { opacity: 1; }
.welcome-card__disclosure a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 2px;
}

/* H4 Surface 4: sidebar Travel passes panel header chip. Design source at
   frames-states.jsx:58 — small muted "Affiliate" text right-aligned in the
   panel header. Visual-only signal; the click target is the upgraded
   .affiliate-note line below. */
.panel__aff-chip {
  float: right;
  margin-top: 2px;
  font-size: 10.5px;
  font-weight: 500;
  color: var(--muted);
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

/* H4 Surface 4 (continued): upgrade .affiliate-note from 9px italic to 11px
   regular + clickable link. Prior version was legally insufficient per R3
   H4 (illegible size + no clickthrough to the full disclosure). */
.affiliate-note {
  margin: 10px 0 0;
  font-size: 11px;
  font-style: normal;
  line-height: 1.45;
  color: var(--muted);
}
.affiliate-note a {
  color: var(--text);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
  font-weight: 500;
}
.affiliate-note a:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
  border-radius: 2px;
}

/* H4 Surface 5: attribution band "Affiliate" mini-link. Styled as a regular
   attribution-band link (matches OSM + CARTO treatment) — muted text with
   underline-on-hover. Always visible at bottom-left; serves users who
   miss the welcome card + skip sidebar + ignore AFF tags. */
#attribution-affiliate-link {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
}
#attribution-affiliate-link:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
  border-radius: 2px;
}

/* H4 Surface 2: dedicated disclosure overlay. Hybrid:
     - Desktop: centered dialog at 500-px max-width, backdrop is semi-opaque.
     - Mobile (<=600px): bottom-sheet starting at top:120, full-width.

   The .disclosure-backdrop sibling element is transparent on mobile (where
   the sheet sits above the TopBar) and semi-opaque on desktop (where the
   centered dialog benefits from a tint to signal focus). */
.disclosure-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(20, 16, 8, 0.35);
  z-index: 1250;  /* below overlay (1300) above other modals (1100-1200) */
  transition: opacity 0.25s ease;
}
.disclosure-backdrop.hidden { display: none; }

.disclosure-page {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: calc(100% - 40px);
  max-width: 500px;
  max-height: calc(100vh - 80px);
  padding: 24px 28px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 16px;
  box-shadow: 0 20px 60px rgba(20, 16, 8, 0.2);
  color: var(--text);
  z-index: 1300;
  overflow-y: auto;
}
.disclosure-page.hidden { display: none; }

.disclosure-page__title {
  margin: 0 0 14px;
  padding-right: 48px;  /* room for absolute × at top-right */
  font-size: 18px;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--text);
}
.disclosure-page__body {
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--text);
}
.disclosure-page__p {
  margin: 0 0 12px;
}
.disclosure-page__p:last-of-type { margin-bottom: 16px; }
.disclosure-page__contact {
  margin: 0 0 8px;
  font-size: 13px;
  color: var(--text);
}
.disclosure-page__updated {
  margin: 0;
  font-size: 11px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* Mobile full-sheet override. top:120 leaves the V6 TopBar visible; sheet
   fills rest of viewport. Matches V5 sidebar + V6 picker mobile-sheet
   shape. Drag-handle ::before for consistency. */
@media (max-width: 600px) {
  .disclosure-page {
    top: 120px;
    left: 0;
    right: 0;
    bottom: 0;
    width: auto;
    max-width: none;
    max-height: none;
    transform: none;
    padding: 26px 20px 28px;
    border-radius: 20px 20px 0 0;
    box-shadow: 0 -8px 30px rgba(20, 16, 8, 0.18);
  }
  .disclosure-page::before {
    content: '';
    position: absolute;
    top: 10px;
    left: 50%;
    transform: translateX(-50%);
    width: 36px;
    height: 4px;
    border-radius: 2px;
    background: var(--border);
  }
  /* On mobile, the backdrop is transparent — the sheet dominates the
     viewport below the TopBar; no need for a tint. */
  .disclosure-backdrop {
    background: transparent;
  }
}

/* H4 AFF-tag keyboard affordance. role=button + tabindex=0 in JS gives
   SR + Tab semantics; visual focus ring added here. */
.aff-tag:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 2px;
}
