/* SMWD Water Festival 2026 — global stylesheet.
 * Tokens, type, layout primitives. See ADR-0004.
 *
 * Self-hosted fonts only (no Google Fonts, no CDN). Open Sans is bundled as
 * a single ~43 KB latin variable woff2 covering both regular (400) and bold
 * (700) — see ADR-0004 Changelog 2026-05-04.
 */

@font-face {
  font-family: "Fjalla One";
  src: url("assets/fonts/FjallaOne-Regular.ttf") format("truetype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* Playlist Script retired 2026-05-07 (user-facing pass — K–2 cursive
   legibility). See ADR-0004 Changelog 2026-05-07. */

@font-face {
  font-family: "Open Sans";
  src: url("assets/fonts/OpenSans-VariableFont.woff2") format("woff2-variations"),
       url("assets/fonts/OpenSans-VariableFont.woff2") format("woff2");
  font-weight: 300 800;
  font-style: normal;
  font-display: swap;
}

:root {
  /* Brand color tokens (ADR-0004) */
  --color-primary: #005E85;
  --color-primary-dark: #003F5C;
  --color-accent: #42A7C6;
  --color-accent-light: #79C8DE;
  --color-cta: #FF9E18;
  --color-cta-hover: #E6890A;
  --color-success: #80BC00;
  --color-success-deep: #6CA000;
  --color-text: #58595B;
  --color-text-soft: #7A7B7D;
  --color-error: #DF1833;
  --color-bg: #F8FAFC;
  --color-bg-card: #FFFFFF;
  --color-bg-hero: linear-gradient(180deg, #42A7C6 0%, #79C8DE 100%);
  --color-bg-celebrate: linear-gradient(180deg, #005E85 0%, #007AAB 100%);

  /* Layout tokens — ADR-0009 responsive-viewport-strategy.
     --col-max raised 420 → 430 so iPhone 14/15/16 Plus + Pro Max (~25% of
     iPhone install base) get edge-to-edge content instead of a 5 px sliver
     each side. Below the cap, .shell width = 100vw (the proportional scale
     band 360–430 applies). */
  --col-max: 430px;
  --radius-card: 18px;
  --radius-pill: 999px;
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-8: 32px;
  --space-10: 40px;
  --shadow-card: 0 6px 18px rgba(0, 30, 50, 0.12);
  --shadow-strong: 0 10px 28px rgba(0, 30, 50, 0.20);

  /* Typography */
  --font-display: "Fjalla One", "Impact", "Helvetica Neue", Arial, sans-serif;
  /* --font-script retired 2026-05-07 — Playlist Script removed (ADR-0004 Changelog). */
  --font-body: "Open Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, "SF Mono", Menlo, Consolas, "Roboto Mono", monospace;

  /* Typography wrap policy — ADR-0008. Whole words always; never break mid-syllable. */
  --wrap-default: normal;
  --hyphens-default: manual;

  /* Motion easing — ADR-0007 extension (unified entrance). */
  --ease-soft-stop: cubic-bezier(.2, .8, .2, 1);

  /* Speech bubble anchoring — ADR-0010. Mouth Y is uniform at 21% across all
     10 poses (variance ≤ 1pp = ≤ 4 px at the largest rendered pose). Per-pose
     `--mouth-x` lives on each <img>'s class block. */
  --mouth-y: 21%;
}

* { box-sizing: border-box; }

html {
  hyphens: var(--hyphens-default);
  -webkit-hyphens: var(--hyphens-default);
  word-break: var(--wrap-default);
  overflow-wrap: var(--wrap-default);
}

html, body {
  margin: 0;
  padding: 0;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: 18px;
  line-height: 1.4;
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
}

body {
  min-height: 100svh;
}

#splash {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  min-height: 60vh;
  color: var(--color-primary);
  font-family: var(--font-display);
}

.splash__spinner {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  border: 2px solid currentColor;
  border-top-color: transparent;
  animation: splash-spin 0.8s linear infinite;
  flex: none;
}

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

@media (prefers-reduced-motion: reduce) {
  .splash__spinner { animation-duration: 2.4s; }
}

button {
  font-family: inherit;
  cursor: pointer;
}

input {
  font-family: inherit;
  font-size: inherit;
}

/* ============================================================== */
/* App shell                                                        */
/* ============================================================== */

.app {
  min-height: 100svh;
  position: relative;
  overflow-x: hidden;
}

.shell {
  max-width: var(--col-max);
  margin: 0 auto;
  position: relative;
  background: var(--color-bg);
  min-height: 100svh;
}

@media (min-width: 720px) {
  .app {
    background:
      radial-gradient(circle at 12% 20%, rgba(66,167,198,.15), transparent 40%),
      radial-gradient(circle at 88% 75%, rgba(255,158,24,.10), transparent 45%),
      radial-gradient(circle at 65% 30%, rgba(128,188,0,.08), transparent 35%),
      var(--color-bg);
  }
  .shell {
    box-shadow: var(--shadow-strong);
  }
}

/* ============================================================== */
/* Buttons                                                          */
/* ============================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  border: none;
  border-radius: var(--radius-pill);
  /* ADR-0009 — proportional sizing across the 360–430 width band.
     Design values: padding 18 28, font 22, min-height 64 (all @ 393).
     Each becomes clamp(value*0.916, value/393*100vw, value*1.094). */
  padding: clamp(16.49px, 4.58vw, 19.69px) clamp(25.65px, 7.12vw, 30.63px);
  font-family: var(--font-display);
  font-size: clamp(20.15px, 5.60vw, 24.07px);
  letter-spacing: 0.02em;
  min-height: clamp(58.62px, 16.28vw, 70.02px);
  width: 100%;
  color: white;
  background: var(--color-cta);
  box-shadow: 0 4px 0 rgba(0,0,0,0.10), var(--shadow-card);
  transition: transform .08s ease, background .15s ease;
}

.btn:hover { background: var(--color-cta-hover); }
.btn:active { transform: translateY(2px); }
.btn:focus-visible {
  outline: 4px solid white;
  outline-offset: 2px;
  box-shadow: 0 0 0 6px var(--color-cta), 0 4px 0 rgba(0,0,0,0.10);
}
.btn:disabled {
  background: #b8b9bb;
  color: #fff;
  cursor: not-allowed;
  box-shadow: none;
}

.btn--primary  { background: var(--color-primary); }
.btn--primary:hover  { background: var(--color-primary-dark); }
.btn--primary:focus-visible { box-shadow: 0 0 0 6px var(--color-primary), 0 4px 0 rgba(0,0,0,0.10); }

.btn--ghost {
  background: transparent;
  color: var(--color-primary);
  box-shadow: none;
  border: 2px solid var(--color-primary);
}
.btn--ghost:hover { background: rgba(0, 94, 133, 0.08); }

.btn--cta { background: var(--color-cta); }
.btn--cta:hover { background: var(--color-cta-hover); }

.btn--big {
  /* ADR-0009 — design 26 / 72 / 20-28 @ 393. */
  font-size: clamp(23.82px, 6.62vw, 28.44px);
  min-height: clamp(65.95px, 18.32vw, 78.77px);
  padding: clamp(18.32px, 5.09vw, 21.88px) clamp(25.65px, 7.12vw, 30.63px);
}

.icon-btn {
  background: transparent;
  border: none;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--color-primary);
  cursor: pointer;
}

.icon-btn:hover { background: rgba(0, 94, 133, 0.08); }
.icon-btn:focus-visible {
  outline: 3px solid var(--color-primary);
  outline-offset: 2px;
}

/* ============================================================== */
/* Welcome screen                                                   */
/* ============================================================== */

.screen-welcome {
  min-height: 100svh;
  background: var(--color-bg-hero);
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  /* ADR-0004 Changelog 2026-05-12 PM-16 — top padding 24 → 8 to lift the
     character + CTA up so the button fits inside a regular phone viewport
     (393×640 dynamic Safari floor). Side padding unchanged. */
  padding: max(var(--space-2), env(safe-area-inset-top)) var(--space-5) var(--space-5);
  color: white;
  position: relative;
  overflow: hidden;
  /* PM-23 ADR-0010 Changelog — horizontal offset for Jake ALONE on the
     Welcome screen so the IMG silhouette clears the bubble nub region.
     Was applied to the shared .welcome__jake-stack wrapper in PM-22 which
     dragged the bubble right along with Jake — operator-corrected PM-23:
     the bubble re-centers in the column while only Jake carries the shift.
     Target ~16 px @ 393 (4vw); floor 8 px @ 360; ceiling 20 px @ 430.
     Clamp value unchanged from PM-22; only the rule consuming it moved. */
  --welcome-jake-offset-x: clamp(8px, 4vw, 20px);
}

/* PM-23 ADR-0010 Changelog — wrapper around the Welcome bubble + Jake.
   Retains its flex-column / align-items: center layout role so the bubble
   stays baseline-centered in the column. The PM-22 transform was relocated
   to .welcome__jake (below) so Jake alone shifts right; the bubble
   re-anchors to column center. The gap_tail_to_jake = 6 px invariant is
   unchanged (the relocated transform is horizontal-only; vertical layout
   between bubble and Jake is unaffected). */
.welcome__jake-stack {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  position: relative;
  z-index: 1;
}

.welcome__sun {
  position: absolute;
  top: -40px;
  right: -40px;
  width: 200px;
  height: 200px;
  border-radius: 50%;
  background: radial-gradient(circle, #FFE08A 0%, #FFC851 60%, transparent 75%);
  opacity: 0.55;
  pointer-events: none;
}

/* ADR-0004 Changelog 2026-05-11 PM-12 — slim horizontal logo header band.
   Replaces the prior vertically-stacked logo block (WF ~306 px square +
   AJ ~236×185 = ~491 px combined). Both logos sit side-by-side, vertically
   centered to each other (WF is square, AJ is 1.273 wide-rect), so the row
   height is the taller of the two ≈ 100 px. Saves ~400 px of vertical
   real estate so Jake (380 px) + speech bubble (86 px) + CTA all fit
   above the 852 px fold at 393 viewport. */
.welcome__logos-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-5);
  width: 100%;
  /* PM-16: 16 → 4. Tightens the gap between the safe-area inset and the
     logo strip so the rest of the stack rises ~12 px. */
  margin-top: var(--space-1);
  position: relative;
  z-index: 1;
}

.welcome__logo {
  /* PM-22 ADR-0004 Changelog — width clamp raised to match the rendered
     height of .welcome__game-logo (≈99 px @ 393) so the two logos read
     as visually equal mass. Prior band clamp(64, 18vw, 84) rendered the
     WF logo at ~71 px tall, visibly smaller than the AJ game logo.
     New band: target 102 px @ 393 (26vw); floor 90 px @ 360; ceiling
     110 px @ 430. height:auto preserves natural 1:1. */
  width: clamp(90px, 26vw, 110px);
  aspect-ratio: 1 / 1;
  height: auto;
  filter: drop-shadow(0 6px 14px rgba(0,0,0,0.20));
}

/* ADR-0004 Changelog 2026-05-11 PM — Ask Jake game logo on Welcome.
   Static brand asset. NOT a Jake utterance — no tail, no anchor, no
   filter (the PNG ships with its own background treatment per Mary's
   delivery; preserve untouched). Width per ADR-0009 single-rule
   (PM-12 rescale): target 130 px @ 393 (≈ 33.1vw); floor 118 px @ 360
   (32.78vw); ceiling 154 px @ 430 (35.81vw). Single-rule
   clamp(118, 32vw, 154) covers the 360–430 band. Rendered height
   follows natural 1.273 aspect ratio → ~92.7 px @ 360, ~102.1 px @ 393,
   ~121 px @ 430. min-height removed in PM-12 — the .is-ready gate awaits
   `img.decode()` (ADR-0011) so the rendered height is reserved by HTML
   width/height attrs + CSS aspect-ratio cascade before opacity-fade. */
.welcome__game-logo {
  width: clamp(118px, 32vw, 154px);
  height: auto;
}

.welcome__bubble {
  background: white;
  color: var(--color-text);
  padding: var(--space-4) var(--space-5);
  border-radius: 22px;
  /* PM-16: 24 → 12. Pulls the bubble closer to the logo strip. */
  margin-top: var(--space-3);
  box-shadow: var(--shadow-card);
  font-family: var(--font-display);
  font-size: 22px;
  line-height: 1.25;
  position: relative;
  max-width: 360px;
  /* ADR-0008 Rule 2 envelope — PM-16: text grew from 46 chars to ~75 chars
     ("Hi I'm Jake! Ready for some trivia? Click \"Let's Go\" to get started!")
     which wraps to 3 lines @ 22 px Fjalla One in a 360 px column. New
     reserved height ≈ 3 × line-height(1.25 × 22) + vertical padding =
     3 × 27.5 + 32 ≈ 115. */
  min-height: 115px;
  z-index: 1;
}

.welcome__bubble::after {
  /* ADR-0010 Welcome variant (no-overlap, 2026-05-05). Originally a 80 px
     tail traversed Jake's hat to land on his mouth (Pattern A "top-down").
     Per user feedback "the chat bubble inside of Jake's face. These need to
     be separated, no overlapping elements," the Welcome bubble now keeps a
     short 14 px comic-strip nub that sits in the gap ABOVE Jake's head — the
     downward-pointing tail still unambiguously points at Jake, but no white
     pixels intrude into his bounding box. Tail base 24 px (12+12) preserved.
     Tail is geometrically constant; ADR-0008 zero-CLS preserved. */
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  width: 0;
  height: 0;
  border-left: 12px solid transparent;
  border-right: 12px solid transparent;
  border-top: 14px solid white;
  z-index: 0;
}

.welcome__jake {
  /* ADR-0008 Rule 2 — pose envelope. aspect-ratio: 5/8 (0.625) is wider than
     the widest pose's natural ratio (0.534) so all 10 poses fit with horizontal
     whitespace inside the box; box-h * 5/8 = box-w.
     PM-16 (2026-05-12): height clamped down min(48vh, 380px) → min(40vh, 300px)
     to free ~80 px so the CTA fits inside a small-phone viewport (393×640
     dynamic Safari floor). The pose still reads as character-dominant —
     300 px tall at 393 viewport = 0.76 × screen-width. */
  height: min(40vh, 300px);
  width: auto;
  aspect-ratio: 5 / 8;
  object-fit: contain;
  /* ADR-0010 Welcome variant (no-overlap, 2026-05-05). PM-16: gap above
     Jake collapsed --space-5 → --space-1 (20 → 4) for further vertical
     compression. The 14 px tail nub still sits clear of Jake's bounding
     box because the bubble itself moved up by ~12 px (its margin-top was
     also reduced); the 4 px gap leaves the tail visually separated. */
  margin-top: var(--space-1);
  /* PM-23 ADR-0010 Changelog — right-shift relocated here from
     .welcome__jake-stack so Jake alone moves right while the bubble
     re-centers in the column (operator correction to PM-22's coupled
     shift). Same clamp(8px, 4vw, 20px) value; transform is horizontal-only
     so gap_tail_to_jake = 6 px invariant and the Pattern A2 separated-nub
     geometry are unchanged. */
  transform: translateX(var(--welcome-jake-offset-x));
  filter: drop-shadow(0 10px 16px rgba(0, 30, 50, 0.25));
  z-index: 1;
  /* ADR-0010 — per-pose mouth x (jake-waving = 51%). Informational; tail is
     centered, but kept here for any future Welcome pose change. */
  --mouth-x: 51%;
}

.welcome__cta-wrap {
  width: 100%;
  max-width: 360px;
  /* PM-22 ADR-0005 Changelog — was margin-top: auto + padding-bottom: --space-3.
     Lift the CTA toward Jake's feet by removing the auto-margin and
     substituting a small clamp gap above the CTA. The spec probe asserts
     `cta_top − jake_bottom ∈ [20, 60] CSS px` — empirically 16 px landed
     just under the 20 lower bound at 393×852, so the clamp is bumped to
     clamp(20px, 2.8vh, 28px) (target ~24 px @ 852, floor 20 px). CTA→footer
     internal gap stays preserved via padding-bottom + footer's default
     top-margin: 0. */
  margin-top: clamp(20px, 2.8vh, 28px);
  padding-bottom: var(--space-3);   /* PRESERVED — CTA→footer spacing */
  z-index: 1;
}

.welcome__resume-note {
  margin-top: var(--space-3);
  margin-bottom: var(--space-3);
  font-size: 16px;
  color: white;
  text-shadow: 0 1px 4px rgba(0, 30, 50, 0.30);
}

.welcome__footer {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  font-size: 13px;
  color: rgba(255,255,255,0.85);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-family: var(--font-display);
  padding-bottom: max(var(--space-3), env(safe-area-inset-bottom));
  z-index: 1;
}

.welcome__bell {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  color: rgba(255,255,255,0.95);
}

/* PM-18 Changelog 2026-05-12 — tiny dot separator between district credit and
   the Privacy link in the Welcome footer. */
.welcome__footer-dot {
  opacity: 0.6;
  font-size: 16px;
  line-height: 1;
}

/* ============================================================== */
/* PM-18 — Privacy disclosure (inline link + modal overlay)         */
/* ============================================================== */

/* The Privacy link is a button (so it's keyboard-focusable + accessible)
   styled to look like a plain text link. Two variants — `--light` for the
   teal Welcome footer, `--dark` for the off-white Form CTA tail. */
.privacy-link {
  background: none;
  border: 0;
  padding: 4px 4px;
  cursor: pointer;
  font: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
  min-height: var(--tap-min, 44px);
  display: inline-flex;
  align-items: center;
}
.privacy-link:focus-visible {
  outline: 2px solid #FFB400;
  outline-offset: 2px;
  border-radius: 2px;
}
.privacy-link--light {
  color: rgba(255,255,255,0.92);
  font-family: var(--font-display);
  font-size: 13px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.privacy-link--dark {
  color: var(--smwd-teal);
  font-family: var(--font-body, var(--font-display));
  font-size: 14px;
  letter-spacing: 0;
  text-transform: none;
  padding: 0 2px;
  min-height: 0;
}

/* Modal overlay covers the whole shell. Backdrop is a translucent navy
   so the underlying screen stays subtly visible (KISS — no animation
   beyond the fade-in inherited from the rest of the app). */
.privacy-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 30, 50, 0.55);
  z-index: 9000;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 24px 12px;
  overflow-y: auto;
  /* PM-18 — the modal can be rendered inside .welcome__footer, which
     applies `text-transform: uppercase`, `letter-spacing`, and
     `text-align: center` to all descendants. Reset every inheritable
     text property here so the policy text reads as authored. */
  text-transform: none;
  letter-spacing: 0;
  text-align: left;
  color: #1A1A1A;
}

.privacy-card {
  position: relative;
  background: #FFFFFF;
  color: var(--fg, #1A1A1A);
  border-radius: 14px;
  box-shadow: 0 6px 24px rgba(0, 30, 50, 0.4);
  width: 100%;
  max-width: 480px;
  padding: 28px 22px 22px;
  font-family: "Open Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  font-size: 15px;
  line-height: 1.5;
  text-align: left;
  text-transform: none;
  letter-spacing: 0;
}

.privacy-card__close {
  position: absolute;
  top: 6px;
  right: 8px;
  background: none;
  border: 0;
  font-size: 28px;
  line-height: 1;
  padding: 6px 12px;
  cursor: pointer;
  color: rgba(0, 30, 50, 0.6);
  min-height: 44px;
  min-width: 44px;
}
.privacy-card__close:focus-visible {
  outline: 2px solid #FFB400;
  outline-offset: 2px;
  border-radius: 4px;
}
.privacy-card__close:hover { color: rgba(0, 30, 50, 0.9); }

.privacy-card__title {
  margin: 0 0 4px;
  font-family: var(--font-display);
  font-size: 22px;
  color: var(--smwd-teal, #005E85);
  line-height: 1.15;
}
.privacy-card__sub {
  margin: 0 0 14px;
  font-size: 12px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: rgba(0, 30, 50, 0.65);
}

.privacy-card__body h3 {
  margin: 14px 0 4px;
  font-family: var(--font-display);
  font-size: 15px;
  color: var(--smwd-teal, #005E85);
  text-transform: none;
  letter-spacing: 0;
}
.privacy-card__body p {
  margin: 0 0 8px;
}
.privacy-card__body a {
  color: var(--smwd-teal, #005E85);
}
.privacy-card__updated {
  margin-top: 14px;
  font-size: 12px;
  color: rgba(0, 30, 50, 0.5);
  font-style: italic;
}

/* Form screen — small italic line below the CTA pointing at the
   Privacy link. Sized to NOT compete visually with the button. */
.form-privacy-note {
  margin: 10px 4px 0;
  font-size: 12px;
  line-height: 1.4;
  color: rgba(0, 30, 50, 0.6);
  text-align: center;
}
.form-privacy-note em {
  font-style: normal;
  font-weight: 600;
}

/* ============================================================== */
/* Map screen                                                       */
/* ============================================================== */

/* ADR-0006 Changelog 2026-05-11 PM-4 — pannable canvas + Jake-fixed overlay.
   .screen-map is now a full-viewport stage (relative + overflow-hidden) with
   the .map-header strip fixed at top, the .map-canvas pannable viewport
   below it, and .map-jake + .map-claim-cta as viewport-fixed overlays
   outside .map-canvas (so they do NOT pan with the map). */
.screen-map {
  position: relative;
  min-height: 100svh;
  height: 100svh;
  overflow: hidden;
  /* PM-5c follow-up (2026-05-11) — solid shell-white (was a pale-blue
     gradient #E8F6FB → #F8FAFC). The PM-5b vertical inset on .map-canvas
     exposed the gradient as a blue strip above/below the map; match the
     shell edge color so the inset reads as continuous framing. */
  background: var(--color-bg);
}

.map-header {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: max(var(--space-3), env(safe-area-inset-top)) var(--space-4) var(--space-3);
  background: white;
  box-shadow: 0 1px 0 rgba(0, 94, 133, 0.08);
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 5;
}

.map-header__logo {
  height: 40px;
  width: 40px;
  aspect-ratio: 1 / 1;
  flex: 0 0 auto;
}

.map-header__progress {
  flex: 1;
  text-align: center;
  font-family: var(--font-display);
  font-size: 18px;
  color: var(--color-primary);
  background: rgba(66,167,198,0.12);
  border-radius: var(--radius-pill);
  padding: 6px 12px;
  letter-spacing: 0.04em;
  min-width: 8em;
}

/* ADR-0006 Changelog 2026-05-11 PM-4 — pannable viewport. Anchors below the
   56-px header strip, fills the rest of the viewport. overflow-hidden +
   touch-action:none so pointer gestures don't trigger browser scroll. */
.map-canvas {
  /* PM-5b/d framing — symmetric vertical inset above + below the map; PM-5d
     also reserves space below for the .map-stash-strip (4 stash booths
     relocated from the cropped right-side index region to a horizontal row
     at the bottom). */
  --map-canvas-inset: clamp(10px, 2.5vh, 22px);
  --map-stash-strip-h: 92px;
  /* ADR-0006 Changelog 2026-05-12 PM-16 — default map zoom. The festival
     site map's square booth icons (~25 px in the 792×612 viewBox) were
     visibly smaller than the 56-design circle booth chips overlaid on top,
     producing a size-mismatch. Scaling the pannable to 1.6× canvas height
     grows the SVG (and therefore the square icons) without resizing the
     chips — narrowing the visual delta. The chips are positioned at
     `calc(--booth-x * 100%)` of the pannable, so uniform scaling preserves
     chip-on-square alignment automatically (no booth-coord retuning
     required). Trade-off: users pan further to reach right/bottom booths;
     operator-accepted. */
  --map-zoom: 1.6;
  position: absolute;
  top: calc(56px + var(--map-canvas-inset));
  left: 0;
  right: 0;
  bottom: calc(var(--map-canvas-inset) + var(--map-stash-strip-h));
  overflow: hidden;
  cursor: grab;
  touch-action: none;
  background: linear-gradient(180deg, #DDF1FA 0%, #F2FBFE 100%);
}
.map-canvas.is-dragging { cursor: grabbing; }

/* ADR-0006 Changelog 2026-05-11 PM-5 rule 4 — selection hardening. PM-4 only
   set these on .map-canvas__svg and .booth; far-edge drags still selected
   the canvas chrome and the pannable wrapper, surfacing as the intake-flagged
   blue text-selection highlight. The four-property block neutralizes it. */
.map-canvas,
.map-canvas__pannable {
  user-select: none;
  -webkit-user-select: none;
  -webkit-user-drag: none;
  -webkit-tap-highlight-color: transparent;
}

/* ADR-0006 Changelog PM-5 / PM-9 — height-driven sizing + full-width pannable.
   PM-5: pannable width derives from <img>'s 792:612 (≈ height × 1.2941),
   horizontal-pan-dominant, vertical pan range = 0.
   PM-9 (2026-05-11): revert the PM-5d 575:612 right-side crop. After two
   failed attempts to also hide the REFRESHMENTS legend (PM-6 ::after mask)
   the operator opted to leave the right-side index legend in place rather
   than continue fighting it. Full 792:612 SVG is now visible; users can pan
   horizontally to reach the right-side content. Booth normalized x coords
   in content/booths.toml are rescaled back from the PM-5d 575-base to the
   full 792-base (divide by 1.3774). Pannable holds 5 anchored booths only;
   the 4 stash booths render in .map-stash-strip outside the canvas. */
.map-canvas__pannable {
  position: absolute;
  top: 0;
  left: 0;
  /* PM-16: height-driven sizing scaled by --map-zoom (default 1.6).
     Width follows aspect-ratio. compute_bounds() in src/screens/map.rs
     reads the rendered rect at runtime, so pan bounds pick up the new
     larger pannable automatically — no Rust-side coord retuning.
     2026-05-13: WFMapFINAL.png (1688×1313) → NoNumbers.png (3375×2625,
     9/7). Aspect 1.2856 → 1.2857 — 0.01% delta, no layout impact. */
  height: calc(100% * var(--map-zoom, 1));
  aspect-ratio: 9 / 7;
  width: auto;
  overflow: hidden;
  will-change: transform;
  transform: translate(0, 0);
}

.map-canvas__svg {
  display: block;
  width: auto;
  height: 100%;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
}

/* ADR-0006 Changelog 2026-05-11 PM-5 rule 2 — shell-anchored chevrons +
   filled-circle restyle. Previously position: fixed (viewport-anchored),
   which drifted past .shell's 430-px max-width on every wide viewport.
   Now position: absolute, anchored inside .screen-map (which lives inside
   .shell), so chevrons stay inside the shell at every viewport. Restyled
   from bare glyph to filled circle with backdrop-blur + 2-px white border
   for distinctness over Mary's colorful festival map. Animation properties
   are still transform + opacity only (ADR-0008 Rule 4 preserved). */
.map-chevron {
  position: absolute;
  z-index: 20;
  width: clamp(28.8px, 8vw, 34.4px);
  height: clamp(28.8px, 8vw, 34.4px);
  border-radius: 50%;
  background: rgba(0, 30, 50, 0.55);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  border: 2px solid white;
  box-shadow: 0 2px 6px rgba(0, 30, 50, 0.5);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-family: var(--font-display);
  font-weight: bold;
  line-height: 1;
  font-size: clamp(18px, 5vw, 22px);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease;
  user-select: none;
}
.map-chevron.visible { opacity: 0.85; }

/* PM-17: all four chevrons render the same `›` glyph (Fjalla One U+203A) inside
   .map-chevron__glyph; rotation lives on the inner span so the container's
   transform property is free for the pulse keyframes + translate-centering.
   Result: identical glyph shape / weight / x-height in all four orientations. */
.map-chevron__glyph {
  display: inline-block;
  line-height: 1;
  /* Optical centering — Fjalla One's `›` glyph has slight visual weight bias
     toward its right edge; nudge the rotation pivot back by ~5% so the rotated
     copies sit visually centered in the circle. */
  transform-origin: 50% 50%;
}
.map-chevron--right  .map-chevron__glyph { /* 0deg — reference orientation */ }
.map-chevron--bottom .map-chevron__glyph { transform: rotate(90deg); }
.map-chevron--left   .map-chevron__glyph { transform: rotate(180deg); }
.map-chevron--top    .map-chevron__glyph { transform: rotate(-90deg); }

.map-chevron--right  { right: 8px; top: 50%; transform: translateY(-50%); }
.map-chevron--left   { left:  8px; top: 50%; transform: translateY(-50%); }
.map-chevron--top    { top: 64px; left: 50%; transform: translateX(-50%); }
/* PM-17: bottom chevron clears the 92-px .map-stash-strip (Jake's corner-perch
   + .map-strip-bubble) so the down-arrow sits at the bottom edge of
   .map-canvas (its referenced surface) instead of overlapping the bubble. */
.map-chevron--bottom { bottom: calc(var(--map-stash-strip-h, 92px) + 6px); left: 50%; transform: translateX(-50%); }

@keyframes chevronPulseRight {
  0%, 100% { transform: translate(0, -50%);   opacity: 0.85; }
  50%      { transform: translate(4px, -50%); opacity: 1.0; }
}
@keyframes chevronPulseLeft {
  0%, 100% { transform: translate(0, -50%);    opacity: 0.85; }
  50%      { transform: translate(-4px, -50%); opacity: 1.0; }
}
@keyframes chevronPulseUp {
  0%, 100% { transform: translate(-50%, 0);    opacity: 0.85; }
  50%      { transform: translate(-50%, -4px); opacity: 1.0; }
}
@keyframes chevronPulseDown {
  0%, 100% { transform: translate(-50%, 0);   opacity: 0.85; }
  50%      { transform: translate(-50%, 4px); opacity: 1.0; }
}

.map-chevron--right.visible  { animation: chevronPulseRight 1.5s ease-in-out infinite; transform: translate(0, -50%); }
.map-chevron--left.visible   { animation: chevronPulseLeft  1.5s ease-in-out infinite; transform: translate(0, -50%); }
.map-chevron--top.visible    { animation: chevronPulseUp    1.5s ease-in-out infinite; transform: translate(-50%, 0); }
.map-chevron--bottom.visible { animation: chevronPulseDown  1.5s ease-in-out infinite; transform: translate(-50%, 0); }

@media (prefers-reduced-motion: reduce) {
  .map-chevron.visible {
    animation: none !important;
    opacity: 0.85;
  }
}

/* ADR-0006 Changelog 2026-05-11 PM-4 — booth icons.
   Tile shrinks from 84-design to 56-design (ADR-0009 Minor Changelog
   2026-05-11 PM-4) to match the festival map's small booth circles.
   Label moves from inside-tile to below-tile caption (Fjalla One ~10 px
   clamp-scaled, single-line preferred, whole-word wrap). The wrapper is
   positioned absolutely against the .map-canvas__pannable bounding box via
   normalized --booth-x / --booth-y CSS vars, translated to center the icon
   on the festival booth's circle. */
.booth {
  position: absolute;
  left: calc(var(--booth-x) * 100%);
  top: calc(var(--booth-y) * 100%);
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  /* 56-design width — clamp(51.30px, 14.25vw, 61.27px) per spec § Module 5.1. */
  width: clamp(51.30px, 14.25vw, 61.27px);
  border: none;
  background: transparent;
  padding: 0;
  cursor: pointer;
  text-align: center;
  z-index: 2;
  transition: transform .15s ease;
  -webkit-tap-highlight-color: transparent;
}

.booth__num {
  /* 56-design circle holding the displayed booth number. PM-17: on anchored
     (in-canvas) chips this is `festival_booth_number` (Mary's printed map
     numerals 51/49/8/15/30); on stash chips it's the internal `id`.
     Equal w/h ⇒ border-radius 50%. */
  width: clamp(51.30px, 14.25vw, 61.27px);
  height: clamp(51.30px, 14.25vw, 61.27px);
  border-radius: 50%;
  background: var(--color-cta);
  color: white;
  font-family: var(--font-display);
  font-size: clamp(20.16px, 5.60vw, 24.08px);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  box-shadow: 0 3px 6px rgba(0, 30, 50, 0.30), inset 0 -2px 0 rgba(0,0,0,0.10);
  border: 2px solid white;
}

.booth__label {
  /* ADR-0006 Changelog 2026-05-11 PM-4 + ADR-0009 — 10-design font, 80-design
     max-width. Whole-word wrap allowed (ADR-0008 Rule 3 — no hyphens auto).
     2-line wrap acceptable per spec § 5.2. Text-shadow for legibility on the
     colorful festival-map background.

     PM-7 (2026-05-11): label is HIDDEN on anchored booths inside .screen-map
     to declutter the festival map and to collapse the booth element to chip-
     only so chip-center = element-center (viewport-invariant alignment).
     Stash booths in .map-stash-strip keep their labels (rule below).
     Style preserved for use in .map-stash-strip and any future on-map screen
     that wants below-chip captions. */
  margin-top: 3px;
  font-family: var(--font-display);
  font-size: clamp(9.16px, 2.54vw, 10.94px);
  font-weight: 400;
  color: white;
  line-height: 1.1;
  max-width: clamp(73.28px, 20.36vw, 87.59px);
  text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.6),
               0 0 4px rgba(0, 0, 0, 0.4);
  word-break: var(--wrap-default);
  overflow-wrap: var(--wrap-default);
  white-space: normal;
  letter-spacing: 0.02em;
}

/* ADR-0006 PM-7 Minor Changelog — rule 1.
   Hide booth captions on anchored booths inside .screen-map to declutter the
   festival map and collapse the booth element to chip-only. With the label
   hidden, transform: translate(-50%, -50%) centers the chip at the anchor
   point, making alignment viewport-invariant. Stash booths (.booth--stash)
   keep their labels — they sit on the dedicated bottom strip, not on map. */
.screen-map .booth:not(.booth--stash) .booth__label {
  display: none;
}

.booth:focus-visible {
  outline: 3px solid var(--color-cta);
  outline-offset: 3px;
  border-radius: 50%;
}

/* Booth completion-state classes (ADR-0006 unchanged). */
.booth--incomplete .booth__num {
  /* Halo ring pulse for taps-me appeal. */
  position: relative;
}
.booth--incomplete .booth__num::before {
  content: "";
  position: absolute;
  inset: -8px;
  border-radius: inherit;
  background: radial-gradient(circle, rgba(255, 158, 24, 0) 48%, rgba(255, 158, 24, 0.85) 100%);
  pointer-events: none;
  will-change: transform, opacity;
  z-index: -1;
  /* Always-visible base halo so the tap target is apparent between pulses. */
  opacity: 0.45;
  transform: scale(1.04);
}
.screen-map.is-ready .booth--incomplete .booth__num::before {
  animation: boothPulse 1.6s ease-in-out infinite;
}

@keyframes boothPulse {
  0%, 100% { transform: scale(1.04); opacity: 0.40; }
  50%      { transform: scale(1.28); opacity: 0.90; }
}

.booth--complete .booth__num {
  background: var(--color-success);
  filter: none;
}
.booth--complete .booth__num::after {
  content: "";
  position: absolute;
  top: -2px;
  right: -2px;
  width: clamp(18px, 5vw, 22px);
  height: clamp(18px, 5vw, 22px);
  border-radius: 50%;
  background: var(--color-success-deep) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'><polyline points='4 12 10 18 20 6'/></svg>") center/70% no-repeat;
  box-shadow: 0 2px 6px rgba(0,0,0,0.30);
}
.booth--complete .booth__label {
  opacity: 0.75;
}

/* PM-5d — bottom stash strip (replaces the bottom-right in-canvas stash
   column retired by the right-index crop). Renders the 4 unanchored booths
   (app 6/7/8/9) as a horizontal flex row sitting BELOW .map-canvas, in the
   bottom 92 px reserved by --map-stash-strip-h. Background matches the
   shell-white screen-map so the strip reads as continuous framing. */
.map-stash-strip {
  position: absolute;
  left: 12px;
  right: 116px;   /* leaves room for Jake's corner-perch (PM-5d) */
  bottom: 0;
  height: var(--map-stash-strip-h, 92px);
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: clamp(6px, 1.8vw, 10px);
  padding: 0 4px;
  z-index: 4;
}
.map-stash-strip .booth--stash {
  /* Override the anchored-booth's absolute positioning — stash booths
     flow horizontally inside the strip. */
  position: static;
  left: auto;
  top: auto;
  transform: none;
  flex: 0 0 auto;
}

/* ADR-0006 Changelog PM-4 + PM-5d — Jake.
   PM-4: viewport-fixed center-bottom-third overlay; Jake didn't pan.
   PM-5d: relocated to bottom-right corner-perch (Candidate B from the
   Jake-on-map UX research — Clippy / Pokémon Willow archetype). He's now
   `position: absolute` inside .screen-map (the shell), peeking in from the
   right edge half-cropped. A white halo plate behind his torso + drop
   shadow on the img read as chrome ABOVE the map, not scenery IN it. The
   speech bubble floats up-left toward the map content.
   PM-6 (ADR-0010 Changelog 2026-05-11 PM-6): bubble reverts to Pattern B
   (side-pointing) — sits to the LEFT of Jake with a right-pointing tail
   landing at his left cheek (mouth-y). Frees the vertical column above
   Jake's corner-perch so the central-bottom map content is visible. */
.map-jake {
  position: absolute;
  right: -52px;   /* peek: ~30% of Jake's body sits beyond shell right edge */
  bottom: 0;
  /* PM-6 — expose Jake's image height as a custom prop so the Pattern B
     bubble can math its vertical position against mouth-y (21% from img top
     ⇒ 79% from img bottom).  Mirrored on .map-jake__img height rule. */
  --map-jake-h: clamp(196px, 54vw, 232px);
  --map-jake-bubble-h: 50px;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  pointer-events: none;
  z-index: 30;
}

/* Halo plate behind Jake — soft white circle providing visual separation
   from Mary's busy festival map. Rendered as ::before so it doesn't affect
   layout of img + bubble.
   PM-17: `right: 8px` → `right: -56px` so the halo's left arc clears the
   Pattern E `.map-strip-bubble` at every viewport in the 360–430 design band.
   Math: halo.left = Jake.right - right_offset - halo.width must be > bubble.right
   = viewport.right - 100. With Jake.right = viewport.right + 52 (the
   .map-jake `right: -52px` corner-perch peek), the constraint reduces to
   right_offset < 152 - halo.width. At the tightest viewport (430, halo.width=198)
   that needs right_offset < -46; using -56 for an additional 10-px safety
   margin. The halo's right portion already extends off-screen by ~44 px in the
   prior `right: 8` rule (Jake's body itself peeks 30 % off-shell), so shifting
   further right just continues that off-screen-crop pattern; visible halo
   behind Jake's body retains its separation-from-busy-map purpose. */
.map-jake::before {
  content: "";
  position: absolute;
  /* Shifted right to clear the bottom-strip bubble — see comment above. */
  right: -56px;
  bottom: 12px;
  width: clamp(168px, 46vw, 198px);
  height: clamp(168px, 46vw, 198px);
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255,255,255,0.92) 50%, rgba(255,255,255,0.45) 80%, rgba(255,255,255,0) 100%);
  z-index: -1;
  pointer-events: none;
}

.map-jake__img {
  /* PM-5d corner-perch size — 60% of PM-4 standalone; intrinsic 279×526
     aspect 5/8 preserved. PM-6 binds the height to --map-jake-h on the
     parent so the bubble can math against mouth-y in a single source. */
  height: var(--map-jake-h);
  width: auto;
  aspect-ratio: 5 / 8;
  object-fit: contain;
  filter: drop-shadow(0 6px 10px rgba(0, 30, 50, 0.35));
  --mouth-x: 48%;
  margin-top: var(--space-5);
}

/* ADR-0010 Changelog 2026-05-12 PM-16 — Pattern E (bottom-strip).
   The map-screen bubble was Pattern B (PM-6) — bubble to the LEFT of Jake's
   corner-perch with a right-pointing tail at his cheek. That layout meant
   the bubble floated over the map content (booth chips, festival map
   labels). Per operator feedback, the bubble now occupies the empty bottom
   strip beside Jake (the .map-stash-strip horizontal band, which is empty
   under the 5-booth lineup) — wide horizontal pill anchored to the bottom
   of .screen-map, spanning from the left edge up to Jake's left edge with
   a small upward-pointing tail near its right side that visually points
   into Jake's body. The bubble lives as a sibling of .map-jake in the DOM
   (PM-16 Rust refactor) so it can size independently of Jake's flex box. */
.map-strip-bubble {
  position: absolute;
  left: 12px;
  /* Right edge stops 8 px clear of Jake's body. Jake corner-perch:
     .map-jake right:-52, width clamp(122-145). At 393 viewport, Jake
     visible-left ≈ 393 + 52 - 145 = 300. Right offset 100 leaves the
     bubble ending at 393 - 100 = 293 → 7 px gap to Jake. */
  right: 100px;
  /* Sits inside the 92-px bottom strip reserved by --map-stash-strip-h.
     14 px clear below + bubble height ≈ 56 → top at 70 px from screen
     bottom, 22 px clear below the canvas-bottom edge. */
  bottom: 14px;
  min-height: 56px;
  background: white;
  color: var(--color-text);
  padding: clamp(8px, 2.2vw, 12px) clamp(14px, 3.8vw, 18px);
  border-radius: clamp(14px, 3.8vw, 18px);
  font-family: var(--font-display);
  font-size: clamp(14px, 3.8vw, 17px);
  box-shadow: var(--shadow-card);
  line-height: 1.2;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  /* Sits BEHIND Jake (z-index 28 vs Jake 30) so Jake's body covers any
     residual overlap on the right edge. */
  z-index: 28;
  pointer-events: none;
}
.map-strip-bubble::after {
  /* Upward-pointing tail near the right edge of the bubble, pointing into
     Jake's body. CSS border trick: transparent left/right + solid white
     border-bottom = white triangle with tip at TOP, base at bottom. */
  content: "";
  position: absolute;
  right: 24px;
  top: -10px;
  width: 0;
  height: 0;
  border-left: 10px solid transparent;
  border-right: 10px solid transparent;
  border-bottom: 11px solid white;
  filter: drop-shadow(0 -1px 1px rgba(0, 30, 50, 0.10));
}

/* ADR-0006 Changelog 2026-05-11 PM-4 — Claim CTA repositioned to viewport-
   fixed (was inside .map-canvas; would have panned off-screen). Z-index 31
   sits one above Jake's overlay; Jake hides at 9/9 anyway. */
.map-claim-cta {
  position: fixed;
  left: 50%;
  bottom: 6vh;
  transform: translateX(-50%);
  width: calc(100% - 32px);
  max-width: 360px;
  z-index: 31;
  will-change: transform;
}
.screen-map.is-ready .map-claim-cta {
  animation: claimPulse 1.4s ease-in-out infinite;
}

@keyframes claimPulse {
  0%, 100% { transform: translateX(-50%) scale(1);    opacity: 1;    }
  50%      { transform: translateX(-50%) scale(1.04); opacity: 0.94; }
}

/* ============================================================== */
/* Settings sheet                                                   */
/* ============================================================== */

.sheet-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 30, 50, 0.55);
  z-index: 50;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}

.sheet {
  background: white;
  border-radius: 22px 22px 0 0;
  width: 100%;
  max-width: var(--col-max);
  padding: var(--space-5) var(--space-5) max(var(--space-6), env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.sheet__handle {
  width: 48px;
  height: 5px;
  border-radius: 3px;
  background: #C9CDD0;
  margin: 0 auto var(--space-2);
}

.sheet__title {
  font-family: var(--font-display);
  font-size: 22px;
  color: var(--color-primary);
  margin: 0;
  text-align: center;
}

.sheet-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: var(--space-3) var(--space-2);
  border-bottom: 1px solid #ECEFF1;
}

.sheet-row:last-of-type { border-bottom: none; }

.toggle {
  position: relative;
  width: 52px;
  height: 30px;
  background: #C9CDD0;
  border-radius: 999px;
  border: none;
  cursor: pointer;
  transition: background .15s ease;
}
.toggle::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: white;
  box-shadow: 0 1px 3px rgba(0,0,0,0.20);
  transition: transform .15s ease;
}
.toggle--on { background: var(--color-success); }
.toggle--on::after { transform: translateX(22px); }

.confirm-modal {
  background: white;
  border-radius: var(--radius-card);
  padding: var(--space-5);
  margin: var(--space-5);
  max-width: 360px;
  width: 100%;
  text-align: center;
  box-shadow: var(--shadow-strong);
}
.confirm-modal h3 {
  font-family: var(--font-display);
  color: var(--color-primary);
  margin: 0 0 var(--space-3);
}
.confirm-modal p {
  margin: 0 0 var(--space-4);
}
.confirm-modal__row {
  display: flex;
  gap: var(--space-3);
}
.confirm-modal__row .btn { flex: 1; }

.version {
  font-size: 13px;
  color: var(--color-text-soft);
  text-align: center;
  margin: 0;
}

/* ============================================================== */
/* Question modal                                                   */
/* ============================================================== */

.question-overlay {
  position: fixed;
  inset: 0;
  z-index: 30;
  background: rgba(0, 30, 50, 0.55);
  display: flex;
  align-items: stretch;
  justify-content: center;
  /* ADR-0011 Rule 6 — overlay scrim is opacity 1 from t=0 (no transition).
     The card rides as a child and gets its own .is-ready unveil. This
     inverts the prior "card-first, scrim-after" ordering measured at
     t=4.3 ms card / t=20.1 ms overlay (baseline-frontend-load-sync.md F4):
     backdrop now establishes immediately, card unveils into the dimmed
     scene. Replaces the prior 220 ms fadeIn animation. */
}

@keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }

.question-card {
  background: var(--color-bg);
  width: 100%;
  max-width: var(--col-max);
  display: flex;
  flex-direction: column;
  padding: max(var(--space-3), env(safe-area-inset-top)) var(--space-4) max(var(--space-4), env(safe-area-inset-bottom));
  overflow-y: auto;
}

.question-card__top {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}

.question-chip {
  flex: 1;
  background: var(--color-accent);
  color: white;
  padding: 8px 14px;
  border-radius: var(--radius-pill);
  font-family: var(--font-display);
  font-size: 14px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

.question-body {
  display: grid;
  /* ADR-0006 Changelog 2026-05-05 + ADR-0009 — design 104px @ 393 → 26.46vw
     clamped 95.26–113.78 px. Jake-column-as-percent-of-viewport stable at
     26.46% across the band (was 28.9 / 26.5 / 24.2 % at 360 / 393 / 430). */
  grid-template-columns: clamp(95.26px, 26.46vw, 113.78px) 1fr;
  gap: var(--space-3);
  align-items: start;
  margin-bottom: var(--space-4);
}

.question-jake {
  /* ADR-0008 Rule 2 — Jake-pose envelope. aspect-ratio: 5/8 (0.625) is
     wider than the widest pose (0.534) so all 10 fit with horizontal
     whitespace inside; pose changes never affect outer box height. */
  align-self: end;
  width: 100%;
  height: auto;
  aspect-ratio: 5 / 8;
  max-height: 200px;
  object-fit: contain;
  filter: drop-shadow(0 6px 10px rgba(0,30,50,0.18));
  /* ADR-0010 — per-pose mouth x. The question modal cycles 10 poses (one per
     booth × 3 feedback states); horizontal anchors range 43–53%. The bubble
     tail is column-centered (Pattern C), so per-pose horizontal drift is
     absorbed by the natural variance of where Jake stands within his envelope.
     Default to 48% (median of the 10 mouth-x values). */
  --mouth-x: 48%;
}

.question-jake__bubble {
  /* ADR-0010 Pattern A2 (separated-nub) since 2026-05-07 user-facing pass —
     was Pattern A1 in-column-down with a 44 px traversing tail; user reported
     the white tail "phasing into the face of Jake" on every booth. Now matches
     Welcome and Celebration: 14 px tail nub above a 20 px wrap-gap, so
     gap_tail_to_jake = 6 px constant across the responsive band. The bubble's
     own margin-bottom is dropped — the gap lives entirely on .question-jake-wrap. */
  background: white;
  border-radius: 14px;
  padding: 8px 10px;
  font-family: var(--font-display);
  font-size: 13px;
  color: var(--color-text);
  box-shadow: var(--shadow-card);
  line-height: 1.2;
  /* ADR-0008 Rule 2 envelope — "Not quite — try again!" wraps to 2 lines
     in the 104 px column (worst case among Idle/Flash/WrongLinger states). */
  min-height: 47px;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  position: relative;
}
.question-jake__bubble::after {
  /* ADR-0010 Pattern A2 separated-nub since 2026-05-07 — 14 px tail height,
     24 px base (12+12) — same constants as .welcome__bubble::after and
     .celebrate__bubble::after. Tail does NOT reach Jake's mouth; the
     downward nub still unambiguously reads as Jake speaking because nothing
     else sits directly below. Tail is geometrically constant; ADR-0008
     zero-CLS preserved. */
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  width: 0;
  height: 0;
  border-left: 12px solid transparent;
  border-right: 12px solid transparent;
  border-top: 14px solid white;
  z-index: 0;
}

.question-jake-wrap {
  display: flex;
  flex-direction: column;
  align-items: stretch;  /* was center — center caused horizontal jitter when bubble width changed */
  /* ADR-0010 A2 (since 2026-05-07): 20 px gap places jake.top 20 px below
     bubble.bottom; with the 14 px tail nub this leaves 6 px of clear air
     between tail tip and Jake's bbox — matches .welcome__jake margin-top.
     Was 4 px (Pattern A1 traversing tail era). */
  gap: var(--space-5);
}

.question-prompt {
  font-family: var(--font-display);
  /* ADR-0008 Rule 3 — clamp gives long words a step-down before any break
     is attempted; 18 px floor still readable at the 4–9 audience. */
  font-size: clamp(18px, 5.4vw, 22px);
  color: var(--color-primary);
  line-height: 1.15;
  margin: 0;
  align-self: start;
  /* ADR-0008 Rule 3 — whole-word wrap; no mid-syllable hyphenation. */
  hyphens: var(--hyphens-default);
  -webkit-hyphens: var(--hyphens-default);
  word-break: var(--wrap-default);
  overflow-wrap: var(--wrap-default);
}

.choices {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.choice {
  width: 100%;
  /* ADR-0009 — design 64 / 18 / 16-20 @ 393. Choice-button rhythm scales with
     viewport so wrap behavior is consistent across the band (whole-word wrap
     per ADR-0008 still holds; hyphens: manual). */
  min-height: clamp(58.62px, 16.28vw, 70.02px);
  padding: clamp(14.66px, 4.07vw, 17.50px) clamp(18.32px, 5.09vw, 21.88px);
  background: white;
  border: 2px solid var(--color-accent);
  border-radius: 16px;
  font-size: clamp(16.49px, 4.58vw, 19.69px);
  font-weight: 700;
  color: var(--color-primary);
  text-align: left;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  cursor: pointer;
  transition: transform .08s ease, background .15s ease, border-color .15s ease;
}
.choice:hover { background: rgba(66,167,198,0.08); }
.choice:focus-visible {
  outline: 3px solid var(--color-primary);
  outline-offset: 3px;
}
.choice:active { transform: translateY(2px); }

.choice__letter {
  /* ADR-0009 — design 32×32, font 17 @ 393. */
  width: clamp(29.31px, 8.14vw, 35.00px);
  height: clamp(29.31px, 8.14vw, 35.00px);
  flex: 0 0 auto;
  border-radius: 50%;
  background: var(--color-accent);
  color: white;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: clamp(15.57px, 4.33vw, 18.60px);
}

.choice--correct {
  background: var(--color-success);
  border-color: var(--color-success-deep);
  color: white;
  animation: flashGreen .9s ease;
}
.choice--correct .choice__letter { background: white; color: var(--color-success-deep); }

.choice--wrong {
  background: var(--color-cta);
  border-color: var(--color-cta-hover);
  color: white;
  animation: flashOrange .5s ease;
}
.choice--wrong .choice__letter { background: white; color: var(--color-cta-hover); }

.choice--locked-correct { background: var(--color-success); border-color: var(--color-success-deep); color: white; }
.choice--locked-correct .choice__letter { background: white; color: var(--color-success-deep); }

@keyframes flashGreen {
  0%   { transform: scale(1); }
  20%  { transform: scale(1.03); box-shadow: 0 0 0 6px rgba(128,188,0,0.30); }
  100% { transform: scale(1); }
}
@keyframes flashOrange {
  0%, 100% { transform: translateX(0); }
  25%      { transform: translateX(-8px); }
  75%      { transform: translateX(8px); }
}

/* ADR-0007 Changelog 2026-05-07 — celebration pose-swap entrance.
   400 ms compositor-only spring-in fires when the Question modal swaps
   .question-jake to jake-thumbs-up on a correct answer. Triggered via
   :has(.choice--correct) so no Rust-side class plumbing is needed.
   Concurrent with the existing 3.5 s reading-time window — the bounce
   is over by 400 ms, leaving ~3,100 ms for the kid to read "Yes! That's
   right!" Reduced-motion is handled by the universal animation-duration
   override at the end of the stylesheet. */
@keyframes celebrationBounce {
  0%   { transform: scale(0.92) rotate(-2deg); }
  50%  { transform: scale(1.12) rotate(1deg); }
  100% { transform: scale(1)    rotate(0deg); }
}
.question-card:has(.choice--correct) .question-jake {
  animation: celebrationBounce 400ms cubic-bezier(0.34, 1.56, 0.64, 1) 1;
  transform-origin: 50% 90%;  /* anchor near feet so the bounce lifts upward */
}

.confetti {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 60;
}

/* ============================================================== */
/* Celebration screen                                               */
/* ============================================================== */

.screen-celebrate {
  min-height: 100svh;
  background: var(--color-bg-celebrate);
  color: white;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  /* D2 fix 2026-05-11 PM-3: tighten top padding so Claim CTA fits within
     852 px viewport at 360 width. Was max(var(--space-6), ...). */
  padding: max(var(--space-4), env(safe-area-inset-top)) var(--space-5) max(var(--space-5), env(safe-area-inset-bottom));
  position: relative;
  overflow: hidden;
}

.celebrate__headline {
  font-family: var(--font-display);
  font-size: 44px;
  letter-spacing: 0.02em;
  /* D2 fix 2026-05-11 PM-3: trim top margin (was var(--space-4)) to claw
     back vertical space for the below-fold CTA at 360 viewport. */
  margin: var(--space-2) 0 var(--space-2);
  text-shadow: 0 3px 0 rgba(0, 30, 50, 0.30);
}

/* ADR-0010 Pattern A2 (separated-nub) — closes F4 from the bubble-anchor pass.
   Replaces the prior `.celebrate__subline` page banner with a Jake-attributed
   speech bubble that sits ABOVE Jake with a 6 px gap from tail-tip to jake.top
   (matches `.welcome__bubble` geometry). Script font + CTA-orange text preserve
   the celebration aesthetic on a white bubble. ADR-0008 Rule 2 envelope:
   `min-height: 80px` reserves layout for the single-line phrase before script
   font swaps in (zero CLS). The headline "🎉 You Did It!" stays as a page-level
   exclamation (not a Jake utterance) per user direction 2026-05-07. */
.celebrate__bubble {
  background: white;
  color: var(--color-cta);
  padding: var(--space-3) var(--space-5);
  border-radius: 22px;
  margin: 0 auto;
  box-shadow: var(--shadow-card);
  /* ADR-0004 Changelog 2026-05-07 — was var(--font-script) (Playlist Script
     36 px); cursive Latin script is unreliable for K–2 readers. Display font
     at 28 px reads larger than the prior 36 px Playlist Script and stays
     unambiguously celebratory in screenshot review. */
  font-family: var(--font-display);
  font-size: 28px;
  line-height: 1.1;
  letter-spacing: 0.01em;
  text-align: center;
  position: relative;
  max-width: 360px;
  /* ADR-0008 Rule 2 envelope (recalculated 2026-05-07 for Fjalla One 28 px).
     Worst case is 2-line wrap at the 360 floor during the Impact-fallback →
     Fjalla One swap window. 2 × (28 px × 1.1) + 24 px padding ≈ 86 px;
     reserve 88 px for a small headroom. At 393+ the phrase fits on one line
     (~265 px wide ≪ ~280 px content area) and the extra vertical absorbs
     into flex centering. Was 104 px under script font. */
  min-height: 88px;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1;
}

.celebrate__bubble::after {
  /* A2 separated-nub tail. Constants match `.welcome__bubble::after`:
     14 px tail height + 24 px base; Jake gets a 20 px margin-top so
     gap_tail_to_jake = 6 px (constant across the responsive band). */
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  width: 0;
  height: 0;
  border-left: 12px solid transparent;
  border-right: 12px solid transparent;
  border-top: 14px solid white;
  z-index: 0;
}

.celebrate__jake {
  /* D2 fix 2026-05-11 PM-3: was min(50vh, 420px). Shrunk to min(34vh, 290px)
     to bring the Claim CTA above the 852 px fold at 360 width. Jake renders
     at ~114 × 290 at the 360 floor (vs prior ~165 × 420) — still a clearly
     dominant figure but no longer pushes the CTA below the fold. Combined
     with the trimmed top padding + headline + raffle-info margins above,
     total stack height fits within ~852 px at 360. Natural ratio preserved
     via width: auto (ADR-0010 Rule 4 — same width:auto rule as
     `.form-jake-row__img` and `.welcome__jake`). */
  height: min(34vh, 290px);
  width: auto;
  filter: drop-shadow(0 10px 18px rgba(0, 0, 0, 0.30));
  /* A2 separation gap — matches `.welcome__jake` margin-top pattern. */
  margin-top: var(--space-5);
}

/* ADR-0005 Changelog 2026-05-12 PM-19 — prize-drawing-framing info paragraph
   between Jake and the Claim CTA. Plain readable text (not a Jake speech
   bubble). White on the celebration gradient background, Fjalla One via
   var(--font-display), body-paragraph size ~16-17 px. ADR-0008 Rule 2 envelope:
   min-height reserves ~3 lines of wrap at the 360 px floor so the Fjalla One
   late-swap from the system fallback cannot push the CTA down after first
   paint (CLS protection). Sized via clamp() per ADR-0009. Class renamed
   `.celebrate__raffle-info` → `.celebrate__drawing-info` at PM-19. */
.celebrate__drawing-info {
  font-family: var(--font-display);
  font-size: clamp(15.10px, 4.20vw, 18.05px);
  line-height: 1.35;
  letter-spacing: 0.01em;
  color: white;
  text-align: center;
  max-width: 320px;
  /* D2 fix 2026-05-11 PM-3: top margin was var(--space-4) → var(--space-3)
     to shave 4 px of stack height at 360 viewport. */
  margin: var(--space-3) auto 0;
  padding: 0 var(--space-2);
  /* 3-line wrap reserve at the 360 floor:
     line-height 17 * 1.35 = 22.95 px; 3 lines = ~69 px; +6 px headroom = 75 px. */
  min-height: 75px;
  text-shadow: 0 2px 0 rgba(0, 30, 50, 0.25);
  position: relative;
  z-index: 1;
}

/* ADR-0004 Changelog 2026-05-11 PM — Ask Jake game logo on Celebration.
   Slotted between .celebrate__drawing-info and .celebrate__cta-wrap as the
   visual lead-in to the Claim CTA. Static brand asset — no tail, no anchor,
   no filter. Width per ADR-0009 single-rule: target 196 px @ 393 (≈ 50vw);
   floor 179.54 px @ 360; ceiling 214.42 px @ 430. height:auto preserves
   natural 1.273 aspect ratio. min-height 141 px reserves the rendered
   height at the 360 floor per ADR-0008 Rule 2. z-index keeps the logo
   above the floating .bubble-bg decorations. */
.celebrate__game-logo {
  width: clamp(179.54px, 49.87vw, 214.42px);
  height: auto;
  min-height: 141px;
  margin-top: var(--space-3);
  margin-left: auto;
  margin-right: auto;
  display: block;
  position: relative;
  z-index: 1;
}

.celebrate__cta-wrap {
  width: 100%;
  max-width: 360px;
  margin-top: auto;
}

.bubble-bg {
  position: absolute;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255,255,255,0.30) 0%, transparent 70%);
  pointer-events: none;
  /* ADR-0011 Rule 5 — float starts only once Celebration flips to .is-ready. */
}
.screen-celebrate.is-ready .bubble-bg {
  animation: bubbleFloat 6s ease-in-out infinite;
}
@keyframes bubbleFloat {
  0%, 100% { transform: translateY(0) }
  50%      { transform: translateY(-20px) }
}

/* ============================================================== */
/* PrizeDrawing confirmation screen — ADR-0005 PM-19 (renamed from  */
/* RaffleConfirmation; class names `.raffle__*` → `.drawing__*`,    */
/* `.screen-raffle` → `.screen-drawing`). 2026-05-12.               */
/* ============================================================== */
/* Replaces the retired PrizeConfirmation. Visual treatment matches
   Celebration (deep teal gradient, white Fjalla One). PM-19 retired
   the decorative Jake to make room for the public schedule panel +
   "Connect with us" social row in a single 393×852 viewport without
   scrolling. The screen joins the ADR-0011 .is-ready opacity gate
   alongside the other five screens. */

.screen-drawing {
  min-height: 100svh;
  background: var(--color-bg-celebrate);
  color: white;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: max(var(--space-5), env(safe-area-inset-top)) var(--space-5) max(var(--space-6), env(safe-area-inset-bottom));
  position: relative;
  /* PM-20 — adding the Festival Schedule (11 rows) pushes content past
     viewport on 393×852. Vertical scroll allowed; horizontal clipped so the
     .bubble-bg decorations don't bleed and produce a 100% × N% page. */
  overflow-x: hidden;
  overflow-y: auto;
}

.drawing__headline {
  font-family: var(--font-display);
  font-size: clamp(34px, 8.7vw, 44px);
  letter-spacing: 0.02em;
  margin: var(--space-3) 0 var(--space-2);
  text-shadow: 0 3px 0 rgba(0, 30, 50, 0.30);
}

/* ADR-0008 Rule 2 envelope — at 360 px viewport the rephrased confirmation
   paragraph wraps to ~5 lines in Fjalla One; line-height ~17 px × 1.4 × 5
   ≈ 119 px. Reserve 130 px to absorb the Impact-fallback → Fjalla One swap
   window without CLS. */
.drawing__body {
  font-family: var(--font-display);
  font-size: clamp(15.10px, 4.20vw, 17.5px);
  line-height: 1.4;
  letter-spacing: 0.01em;
  color: white;
  text-align: center;
  max-width: 340px;
  margin: 0 auto var(--space-3);
  padding: 0 var(--space-2);
  min-height: 130px;
  text-shadow: 0 2px 0 rgba(0, 30, 50, 0.25);
  position: relative;
  z-index: 1;
}

/* ---- Prize Drawing Schedule panel ---- */
.drawing__schedule {
  width: 100%;
  max-width: 340px;
  margin: 0 auto var(--space-3);
  padding: var(--space-3) var(--space-3);
  background: rgba(255, 255, 255, 0.10);
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: 14px;
  position: relative;
  z-index: 1;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
}

.drawing__schedule-title {
  font-family: var(--font-display);
  font-size: clamp(17px, 4.6vw, 20px);
  letter-spacing: 0.04em;
  margin: 0 0 var(--space-2);
  text-transform: uppercase;
  color: white;
}

.drawing__schedule-fallback {
  font-family: var(--font-display);
  font-size: 15px;
  color: rgba(255, 255, 255, 0.85);
  margin: 0;
  /* Reserve ~6 rows of height so the panel doesn't jump on first paint. */
  min-height: 168px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.drawing__schedule-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  /* 6 rows × ~28 px = 168 px — keeps reserve identical to the fallback
     state so loading→loaded never reflows. */
  min-height: 168px;
}

.drawing__schedule-row {
  display: grid;
  grid-template-columns: 22px 60px 1fr;
  align-items: center;
  gap: var(--space-2);
  padding: 4px 8px;
  border-radius: 8px;
  font-family: var(--font-display);
  font-size: 15px;
  letter-spacing: 0.02em;
  color: white;
  min-height: 28px;
}

.drawing__schedule-glyph {
  font-size: 16px;
  text-align: center;
  line-height: 1;
}

.drawing__schedule-time {
  text-align: left;
}

.drawing__schedule-status {
  text-align: right;
  font-size: 13px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  opacity: 0.85;
}

.drawing__schedule-row--done {
  background: rgba(255, 255, 255, 0.08);
  opacity: 0.72;
}
.drawing__schedule-row--done .drawing__schedule-glyph { color: #8ee8c4; }

.drawing__schedule-row--current {
  background: rgba(255, 255, 255, 0.22);
  border: 1px solid rgba(255, 255, 255, 0.45);
  font-weight: 600;
}
.drawing__schedule-row--current .drawing__schedule-glyph {
  color: #ffd154;
  animation: drawing-pulse 1.6s ease-in-out infinite;
}

.drawing__schedule-row--upcoming {
  opacity: 0.85;
}
.drawing__schedule-row--upcoming .drawing__schedule-glyph { color: rgba(255, 255, 255, 0.65); }

@keyframes drawing-pulse {
  0%, 100% { transform: scale(1); opacity: 1; }
  50%      { transform: scale(1.15); opacity: 0.85; }
}

/* ---- Festival Schedule (PM-20, static) ----                       */
/* Distinct from the Prize Drawing Schedule above: this is the list  */
/* of festival activities (Main Stage / Garden / Bus Tours). Static  */
/* compile-time data from src/screens/raffle_confirmation.rs ::      */
/* FESTIVAL_EVENTS. Same panel chrome as .drawing__schedule for      */
/* visual parity, but rendered with a 3-column grid (time / name /   */
/* location) and no live-poll status glyphs.                         */
.drawing__events {
  width: 100%;
  max-width: 340px;
  margin: 0 auto var(--space-3);
  padding: var(--space-3) var(--space-3);
  background: rgba(255, 255, 255, 0.10);
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: 14px;
  position: relative;
  z-index: 1;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
}

.drawing__events-title {
  font-family: var(--font-display);
  font-size: clamp(17px, 4.6vw, 20px);
  letter-spacing: 0.04em;
  margin: 0 0 var(--space-2);
  text-transform: uppercase;
  color: white;
}

.drawing__events-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.drawing__events-row {
  display: grid;
  /* time | name | location — location wraps to a second line on narrow */
  /* viewports when name is long (e.g. "Bus Tour: San Juan…").          */
  grid-template-columns: 50px 1fr auto;
  align-items: baseline;
  column-gap: var(--space-2);
  row-gap: 0;
  padding: 6px 8px;
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.06);
  font-family: var(--font-display);
  font-size: 14.5px;
  letter-spacing: 0.01em;
  color: white;
  text-align: left;
  min-height: 28px;
}

.drawing__events-time {
  font-size: 14px;
  letter-spacing: 0.04em;
  color: #ffd154;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.drawing__events-name {
  color: white;
  word-break: break-word;
  hyphens: none;
  -webkit-hyphens: none;
  line-height: 1.25;
}

.drawing__events-loc {
  font-size: 12px;
  color: rgba(255, 255, 255, 0.72);
  letter-spacing: 0.02em;
  white-space: nowrap;
  text-transform: uppercase;
  /* When the name wraps, the loc chip drops to its own grid row. */
}

.drawing__events-loc--empty {
  /* Bus tours have no parenthetical location — keep the cell present */
  /* so the grid template stays aligned, but render nothing.          */
  display: none;
}

/* ---- Connect with us social block ---- */
.drawing__socials {
  width: 100%;
  max-width: 340px;
  margin: 0 auto var(--space-3);
  padding: 0 var(--space-2);
  position: relative;
  z-index: 1;
}

.drawing__socials-title {
  font-family: var(--font-display);
  font-size: clamp(17px, 4.6vw, 20px);
  letter-spacing: 0.04em;
  margin: 0 0 4px;
  text-transform: uppercase;
  color: white;
  text-align: center;
}

.drawing__socials-sub {
  font-family: var(--font-display);
  font-size: 13.5px;
  line-height: 1.35;
  margin: 0 auto var(--space-2);
  max-width: 300px;
  color: rgba(255, 255, 255, 0.92);
  text-align: center;
}

.drawing__socials-row {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  justify-content: center;
  gap: 14px;
  flex-wrap: wrap;
}

.drawing__social-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  background: white;
  color: var(--color-primary, #005E85);
  text-decoration: none;
  box-shadow: 0 3px 8px rgba(0, 0, 0, 0.22);
  transition: transform 120ms ease-out, box-shadow 120ms ease-out;
  min-width: var(--tap-min, 44px);
  min-height: var(--tap-min, 44px);
}

.drawing__social-btn:active {
  transform: translateY(1px);
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.22);
}

.drawing__social-icon {
  display: inline-flex;
  width: 24px;
  height: 24px;
}

.screen-drawing.is-ready .bubble-bg {
  animation: bubbleFloat 6s ease-in-out infinite;
}

/* ============================================================== */
/* Form screen                                                      */
/* ============================================================== */

.screen-form {
  min-height: 100svh;
  background: var(--color-bg);
  display: flex;
  flex-direction: column;
  padding: max(var(--space-4), env(safe-area-inset-top)) var(--space-5) max(var(--space-5), env(safe-area-inset-bottom));
}

.form-header {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}

.form-header__title {
  font-family: var(--font-display);
  color: var(--color-primary);
  margin: 0;
  font-size: 22px;
  letter-spacing: 0.02em;
}

.form-jake-row {
  display: flex;
  gap: var(--space-3);
  align-items: flex-start;
  margin-bottom: var(--space-4);
}

.form-jake-row__img {
  /* ADR-0008 Rule 1 (extended 2026-05-05) — explicit HTML width="290"
     would otherwise force the rendered <img> to 290 px wide regardless of
     CSS height. `width: auto` restores natural aspect ratio so jake-explaining
     renders at 96 × 180 (290/543 ratio) instead of the regression's 290 × 140
     stretch. */
  height: 180px;
  width: auto;
  flex: 0 0 auto;
  filter: drop-shadow(0 6px 10px rgba(0, 30, 50, 0.18));
  /* ADR-0010 — jake-explaining mouth at (48%, 20%). The form bubble's tail
     tip lands at (jake.right + small gap, jake.top + 0.20 × jake.h). */
  --mouth-x: 48%;
}

.form-jake__bubble {
  /* ADR-0010 (Pattern B — side-pointing) — was `.privacy-card` (info-card,
     no Jake anchor). Now a proper speech bubble with a left-pointing tail
     whose tip lands on Jake's mouth. Visual reads as Jake speaking the
     privacy reassurance. */
  flex: 1;
  background: white;
  border-radius: 14px;
  padding: var(--space-3) var(--space-4);
  font-family: var(--font-body);
  font-size: 14px;
  color: var(--color-text);
  line-height: 1.35;
  box-shadow: var(--shadow-card);
  position: relative;
  /* Reset browser default <p> margin — without this, the bubble is offset
     14 px below Jake's top, dropping the tail tip below the mouth. */
  margin: 0;
  /* Vertical: bubble.top aligned to jake.top via `align-items: flex-start`
     on the row. Tail's vertical center should land on mouth y = 0.20 ×
     180 = 36 px from jake.top. With tail box height 16 px (8 + 8), set
     `top: 28px` so vertical-center y = 36 px from bubble top. */
}
.form-jake__bubble::before {
  content: "";
  position: absolute;
  left: -10px;
  top: 28px;
  width: 0;
  height: 0;
  border-top: 8px solid transparent;
  border-bottom: 8px solid transparent;
  border-right: 12px solid white;
}

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  /* PM-22 ADR-0005 Changelog — was var(--space-4) (16 px). Tighten to
     var(--space-3) (12 px) so the 3-field stack compacts ~12 CSS px and
     the CTA block can rise commensurately without colliding with the
     email field's error placeholder. Compact-but-not-cramped — labels
     still breathe at 16 px Fjalla One uppercase. */
  margin-bottom: var(--space-3);
}

.field__label {
  font-family: var(--font-display);
  font-size: 16px;
  color: var(--color-primary);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.field__input {
  border: 2px solid #CDD5DA;
  border-radius: 14px;
  /* ADR-0009 — design 20 / 16-18 @ 393. */
  padding: clamp(14.66px, 4.07vw, 17.50px) clamp(16.49px, 4.58vw, 19.69px);
  font-size: clamp(18.32px, 5.09vw, 21.88px);
  background: white;
  color: var(--color-text);
  transition: border-color .15s ease;
}

.field__input:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 4px rgba(0,94,133,0.18);
}

.field__input--error {
  border-color: var(--color-error);
}

.field__error {
  font-size: 14px;
  color: var(--color-error);
  margin: 0;
  font-weight: 600;
}

.form-cta {
  /* PM-22 ADR-0005 Changelog — was margin-top: auto which spaced the
     CTA to the screen bottom and forced scroll at 393×852. Replace with
     a clamp gap above the CTA so the button + privacy footer sit
     ~16 CSS px below the email field at the design viewport. The
     CTA→privacy-note internal gap is preserved via .form-privacy-note
     margin: 10px 4px 0 (unchanged). */
  margin-top: clamp(12px, 3vh, 20px);
}

/* ============================================================== */
/* Misc / utility                                                   */
/* ============================================================== */

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0,0,0,0);
  white-space: nowrap;
  border: 0;
}

.fade-in { animation: fadeIn .25s ease; }

/* ============================================================== */
/* ADR-0011 Rule 4 — per-screen ready-gate unveil                   */
/* ============================================================== */
/* Each screen mounts opacity-0 and fades in over 120 ms once its
   ready-gate signal flips (images decoded + fonts loaded + 1 rAF or
   the 350 ms cap, whichever comes first). The whole screen unveils
   as one transition — no element-by-element stagger.

   .question-card inherits the same gate; it's the modal foreground
   that mounts on top of the already-ready Map. The .question-overlay
   scrim is opacity-1 from t=0 (no gate) per Rule 6 — backdrop first,
   card unveils inside it. */
.screen-welcome,
.screen-map,
.screen-celebrate,
.screen-form,
.screen-drawing,
.question-card {
  opacity: 0;
  transition: opacity 120ms ease-out;
}

.screen-welcome.is-ready,
.screen-map.is-ready,
.screen-celebrate.is-ready,
.screen-form.is-ready,
.screen-drawing.is-ready,
.question-card.is-ready {
  opacity: 1;
}

/* ============================================================== */
/* Reduced-motion respect — ADR-0007 Changelog 2026-05-04 (visual-  */
/* smoothness extension). Reading-time + auto-advance windows from  */
/* the original ADR-0007 are unchanged; reduced-motion only governs */
/* HOW the visual delivers the timing, not WHEN the timing fires.   */
/* ============================================================== */

@media (prefers-reduced-motion: reduce) {
  /* Pause all ambient infinite loops. */
  .booth--incomplete,
  .booth--incomplete .booth__num::before,
  .map-claim-cta,
  .bubble {
    animation: none !important;
  }

  /* Tap-driven feedback reduces to opacity/border swap; total visible
     duration unchanged so ADR-0007 timing windows still hold. */
  .choice--correct {
    animation: none;
    /* held green still applies via .choice--correct base rules above */
  }
  .choice--wrong {
    animation: none;
    /* held orange still applies via .choice--wrong base rules above */
  }

  /* ADR-0011 Rule 6 — overlay has no entrance animation now (scrim is
     opacity-1 from t=0). Nothing to override here, but the universal
     transition-duration override below collapses the .is-ready unveil
     to ~instant for reduced-motion users so screens still appear at
     once but without the 120 ms fade. */
  .screen-welcome,
  .screen-map,
  .screen-celebrate,
  .screen-form,
  .screen-drawing,
  .screen-howto,
  .question-card {
    transition: none !important;
    opacity: 1 !important;
  }

  /* Welcome bubble float */
  .bubble { animation: none !important; }

  /* Generic .fade-in utility */
  .fade-in { animation: none; }

  /* Respect transition properties on hover/active too. */
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ==============================================================
 * PM-21 — HowToPlay onboarding screen + booth-31 modal + .booth--info chip.
 * Spec § 5.5 (DOM), § 8.2 (chip), § 9 (animation), § 10 (bubble), § 11 (layout),
 * § 12 (modal). KISS — appended at the file tail, no refactor of prior rules.
 * ============================================================== */

/* --- Screen shell -------------------------------------------------------- */
.screen-howto {
  position: relative;
  width: 100%;
  min-height: 100vh;
  padding: clamp(12px, 3vw, 18px) clamp(14px, 4vw, 22px) clamp(20px, 5vw, 28px);
  display: flex;
  flex-direction: column;
  align-items: center;
  background: var(--color-bg);
  /* Allow scroll if content exceeds the design viewport (393 × 852) — same
     doctrine as PM-20 `.screen-drawing`; operator preferred scrolling over
     content cuts on small phones. */
  overflow-x: hidden;
  overflow-y: auto;
}

/* Body container — vertical stack with proportional spacing. */
.howto {
  width: 100%;
  max-width: 380px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(10px, 2.6vw, 14px);
  /* PM-25 — horizontal offset for Jake ALONE inside `.howto__jake`. Mirrors
     the Welcome PM-23 pattern: bubble re-centers in the column, only the
     image carries the shift so the A2 separated-nub tail no longer visually
     projects into Jake's head silhouette. Lives on `.howto` (not
     `.screen-howto`) so both the full-screen tutorial route AND the in-Map
     `<HowToPlayModal/>` overlay inherit it. Target ~16 px @ 393 (4vw);
     floor 8 px @ 360; ceiling 20 px @ 430 — same clamp band as
     `--welcome-jake-offset-x` for cross-surface consistency. */
  --howto-jake-offset-x: clamp(8px, 4vw, 20px);
}

/* Reference sign figure. aspect-ratio: 4/3 reserves the envelope so the
   image swap from placeholder → real photo is zero-CLS (ADR-0008 Rule 1). */
.howto__sign {
  width: 100%;
  max-width: clamp(260px, 70vw, 320px);
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
}
.howto__sign-img {
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  border-radius: 12px;
  box-shadow: var(--shadow-card);
  object-fit: cover;
}
.howto__sign-caption {
  font-family: var(--font-body);
  font-size: 13px;
  line-height: 1.3;
  font-style: italic;
  color: var(--color-primary-dark);
  text-align: center;
  margin: 0;
}

/* Jake column — bubble above, pose below (Pattern A2 top-down). */
.howto__jake {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  /* PM-22 ADR-0010 Changelog — push the bubble+Jake pair down inside
     the HowToPlay column so the bubble text clears Jake's hairline.
     The 6 px gap_tail_to_jake invariant is unaffected because both
     bubble and jake-img live inside this flex column; only the
     column's top margin grows. Was var(--space-1) (4 px). */
  margin-top: clamp(10px, 1.7vh, 16px);
}
.howto__bubble {
  background: white;
  color: var(--color-text);
  padding: var(--space-3) var(--space-4);
  border-radius: 18px;
  box-shadow: var(--shadow-card);
  font-family: var(--font-display);
  font-size: clamp(16px, 4.2vw, 19px);
  line-height: 1.25;
  text-align: center;
  position: relative;
  max-width: 320px;
  /* ADR-0008 Rule 2 envelope — "Hi! I'm Jake. Let's go over how to play."
     is 8 words / ~40 chars, fits on 2 lines at the 360 floor. Reserve
     ~64 px for 2 × 22 line-height + 24 padding. */
  min-height: clamp(48px, 12vw, 64px);
  display: flex;
  align-items: center;
  justify-content: center;
}
.howto__bubble::after {
  /* A2 separated-nub tail — same constants as `.welcome__bubble::after`
     and `.celebrate__bubble::after`: 14 px height + 24 px base. The pose
     below gets a 6 px top margin so gap_tail_to_jake = 6 px (ADR-0010 PM-7
     uniform rule across A2 consumers). */
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  width: 0;
  height: 0;
  border-left: 12px solid transparent;
  border-right: 12px solid transparent;
  border-top: 14px solid white;
  z-index: 0;
}
.howto__jake-img {
  /* `jake-explaining.png` natural 600×900 (2/3). Height-clamp keeps the
     character dominant without overrunning the small-phone viewport. */
  height: clamp(150px, 28vh, 220px);
  width: auto;
  aspect-ratio: 2 / 3;
  object-fit: contain;
  margin-top: 6px;  /* gap_tail_to_jake = 6 px (ADR-0010 PM-7) */
  /* PM-25 — right-shift Jake alone (mirrors Welcome PM-23). The
     `.howto__jake` flex column still centers the bubble; only the
     image carries the horizontal translate, so the bubble + A2 nub
     no longer visually overlap Jake's head silhouette. Transform is
     horizontal-only — gap_tail_to_jake = 6 px invariant preserved. */
  transform: translateX(var(--howto-jake-offset-x));
  filter: drop-shadow(0 8px 14px rgba(0, 30, 50, 0.18));
}
.howto__jake-img.jake-pose--jake-explaining {
  --mouth-x: 49%;
  --mouth-y: 21%;
}

/* Body paragraphs — ordered list of 3 instructional paragraphs. */
.howto__body {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  width: 100%;
}
.howto__para {
  font-family: var(--font-body);
  font-size: clamp(14px, 3.9vw, 16px);
  line-height: 1.4;
  color: var(--color-text);
  text-align: center;
  margin: 0;
  padding: 0 var(--space-2);
  word-break: var(--wrap-default);
  overflow-wrap: var(--wrap-default);
  hyphens: var(--hyphens-default);
}

/* Sample-chip animated demo strip. Spec § 9. */
.howto__demo {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  margin-top: var(--space-2);
}
.howto__demo-caption {
  font-family: var(--font-display);
  font-size: clamp(13px, 3.5vw, 15px);
  color: var(--color-primary-dark);
  letter-spacing: 0.01em;
}
.howto__demo-stage {
  position: relative;
  width: clamp(120px, 32vw, 140px);
  height: 80px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 8px;
}
.howto__demo-chip {
  position: relative;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--color-cta);
  border: 2px solid white;
  box-shadow: 0 2px 6px rgba(0, 30, 50, 0.30), inset 0 -2px 0 rgba(0,0,0,0.10);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  z-index: 2;
  will-change: transform;
}
.screen-howto.is-ready .howto__demo-chip,
.help-modal[data-visible="true"] .howto__demo-chip {
  animation: boothPulse 2.0s ease-in-out infinite;
}
.howto__demo-num {
  font-family: var(--font-display);
  font-size: 22px;
  color: white;
  line-height: 1;
}
.howto__demo-ripple {
  position: absolute;
  left: 8px;
  top: 50%;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 3px solid var(--color-cta);
  transform: translate(0, -50%) scale(0.6);
  opacity: 0;
  pointer-events: none;
  z-index: 1;
  will-change: transform, opacity;
}
.screen-howto.is-ready .howto__demo-ripple,
.help-modal[data-visible="true"] .howto__demo-ripple {
  animation: demoRipple 2.0s ease-out infinite;
}
.howto__demo-finger {
  position: absolute;
  left: 38px;
  top: 8px;
  font-size: 22px;
  pointer-events: none;
  z-index: 3;
  will-change: transform;
  /* The `\u{1F446}` emoji renders as the OS's "backhand index pointing up"
     glyph — recognizable across mobile platforms without bundling an asset. */
}
.screen-howto.is-ready .howto__demo-finger,
.help-modal[data-visible="true"] .howto__demo-finger {
  animation: demoFingerBob 2.0s ease-in-out infinite;
}
.howto__demo-qpop {
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%) scale(0);
  opacity: 0;
  font-family: var(--font-display);
  font-size: 22px;
  color: var(--color-primary);
  background: white;
  border: 2px solid var(--color-primary);
  border-radius: 12px;
  padding: 2px 8px;
  pointer-events: none;
  z-index: 2;
  will-change: transform, opacity;
}
.screen-howto.is-ready .howto__demo-qpop,
.help-modal[data-visible="true"] .howto__demo-qpop {
  animation: demoQPop 2.0s ease-out infinite;
}

@keyframes demoRipple {
  0%   { transform: translate(0, -50%) scale(0.6); opacity: 0.6; }
  100% { transform: translate(0, -50%) scale(1.6); opacity: 0;   }
}
@keyframes demoFingerBob {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(6px); }
}
@keyframes demoQPop {
  0%, 20%  { transform: translateY(-50%) scale(0);   opacity: 0; }
  40%      { transform: translateY(-50%) scale(1.1); opacity: 1; }
  80%      { transform: translateY(-50%) scale(1.0); opacity: 1; }
  100%     { transform: translateY(-50%) scale(0);   opacity: 0; }
}

/* CTA — reuses the existing .btn--cta styling; ambient `boothPulse` 2.2 s
   loop is inherited from the global rule because `.howto__cta` carries
   no own animation. The CTA is a plain `.btn--cta` here — no extra pulse
   to keep ADR-0007 ambient-pulse band clean. */
.howto__cta {
  width: min(280px, 60vw);
  margin-top: var(--space-3);
}

/* Footer with privacy link (screen variant only). */
.howto__footer {
  margin-top: var(--space-4);
  display: flex;
  justify-content: center;
  font-family: var(--font-body);
  font-size: 12px;
  color: var(--color-text-soft);
}

/* --- Booth-31 chip on the Map (`.booth--info`) ------------------------- */
/* Square variant — distinct from the 5 round quiz chips. Inherits
   `.booth` so PM-10's `target_is_booth_chip()` gates pointer-capture
   identically.
   PM-22 ADR-0009 Changelog — clamp band shrunk from PM-4 56-design to
   40-design so the booth-31 chip clears the booth-5 (festival #30)
   chip below it without coord drift (operator's "shrink + nudge" choice
   for booth-30/booth-31 overlap resolution). Square corners preserved
   (border-radius: 8 px). The numeral font-size clamp is left at the
   inherited 56-design band so "31" stays comfortably readable at the
   smaller chip width — proportionally larger numeral on a smaller chip
   reads as a deliberate "info" affordance. */
.booth--info .booth__num {
  background: var(--color-primary);
  color: white;
  border: 2px solid white;
  /* Override the 50% circle → 8 px square corners. */
  border-radius: 8px;
  box-shadow: 0 3px 6px rgba(0, 30, 50, 0.30), inset 0 -2px 0 rgba(0,0,0,0.10);
  /* PM-22: explicit 40-design clamp band overrides the inherited 56-design. */
  width: clamp(36.64px, 10.18vw, 43.77px);
  height: clamp(36.64px, 10.18vw, 43.77px);
}
/* PM-22 — Also constrain the parent .booth--info button width so the
   element's transform-translate(-50%, -50%) centers correctly on the new
   smaller chip footprint. Without this, the parent inherits the 56-design
   width clamp from .booth and the chip sits offset inside its bounding box. */
.booth--info {
  width: clamp(36.64px, 10.18vw, 43.77px);
}
.booth--info:focus-visible {
  outline: 3px solid var(--color-primary);
  outline-offset: 3px;
  border-radius: 10px;
}
/* The booth-31 chip never completes, never enters `boothCompletions` — so
   the ambient halo pulse (`.booth--incomplete .booth__num::before`) does
   NOT apply because the chip is rendered without `.booth--incomplete`.
   No keyframe attached; chip is static on the map. */

/* --- HowToPlayModal overlay (in-Map booth-31 tap) --------------------- */
:root {
  /* Stacks above all Map content (Jake z=30, chevrons z=20) but below the
     Question modal (--z-modal: 100) so order is deterministic in the rare
     case both ever co-exist. */
  --z-help-modal: 90;
}
.help-modal {
  position: fixed;
  inset: 0;
  z-index: var(--z-help-modal);
  pointer-events: none;  /* the overlay + panel re-enable on themselves */
}
.help-modal[data-visible="true"] {
  pointer-events: auto;
}
.help-modal__overlay {
  position: absolute;
  inset: 0;
  background: rgba(15, 30, 60, 0.55);
  opacity: 0;
  transition: opacity 120ms ease-out;
}
.help-modal[data-visible="true"] .help-modal__overlay {
  opacity: 1;
}
.help-modal__panel {
  position: absolute;
  left: clamp(8px, 2vw, 16px);
  right: clamp(8px, 2vw, 16px);
  top: clamp(40px, 12vh, 80px);
  max-width: 380px;
  margin: 0 auto;
  background: var(--color-bg);
  border-radius: 16px;
  padding: clamp(14px, 4vw, 22px) clamp(14px, 4vw, 22px) clamp(18px, 5vw, 26px);
  overflow-y: auto;
  max-height: calc(100vh - clamp(80px, 24vh, 160px));
  box-shadow: var(--shadow-strong);
}
.help-modal__close {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 36px;
  height: 36px;
  border: none;
  background: transparent;
  font-family: var(--font-display);
  font-size: 28px;
  line-height: 1;
  color: var(--color-text);
  cursor: pointer;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.help-modal__close:hover {
  background: rgba(0, 0, 0, 0.06);
}
.help-modal__close:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}
/* The modal renders the shared `<HowToPlayBody/>` — inside the modal panel
   the body should not also paint its own padding above the panel padding,
   so reduce the body's outer margin a touch. */
.help-modal__panel .howto {
  gap: clamp(8px, 2vw, 12px);
}
.help-modal__panel .howto__sign {
  max-width: clamp(220px, 60vw, 280px);
}
.help-modal__panel .howto__jake-img {
  height: clamp(130px, 24vh, 180px);
}

/* --- Reduced-motion fallback for the demo strip + modal --------------- */
@media (prefers-reduced-motion: reduce) {
  .howto__demo-chip,
  .howto__demo-ripple,
  .howto__demo-finger,
  .howto__demo-qpop {
    animation: none !important;
  }
  .howto__demo-qpop {
    /* Static end-state — show the "Q?" so the affordance still communicates. */
    transform: translateY(-50%) scale(1);
    opacity: 1;
  }
  .howto__demo-ripple {
    opacity: 0;
  }
  .help-modal__overlay {
    transition: none !important;
  }
}

/* --- ADR-0011 .is-ready unveil for `.screen-howto` -------------------- */
.screen-howto {
  opacity: 0;
  transition: opacity 120ms ease-out;
}
.screen-howto.is-ready {
  opacity: 1;
}
