/* ============================================================
 * GAME-PAGE CHROME — single source of truth for the in-game layout
 *
 * Loaded by every game.html (goi / bunpou / kanji / radicals / challenge).
 * Provides the shared layer: header chrome, controls, status, grid
 * scaffolding, .domino base + face transforms, glow progression,
 * long-press feedback, removed-tile fade, list mode, and
 * flipped-overlay neutralizers.
 *
 * Per-pillar CSS files (goi/css/style.css, kanji/css/style.css, etc.)
 * load AFTER this file and add only:
 *   - tile aspect-ratio + face padding (e.g. 2.2/1 for goi, kanji, radicals)
 *   - per-pillar data styling (.goi-pos pill, .bunpou-pattern, etc.)
 *   - per-pillar legacy / Mahjong-Hall blocks (still in place; cleanup
 *     of duplicates here vs. pillar files is tracked in ARCHITECTURE.md)
 *
 * Tokens come from css/design.css (--canvas, --bone, --vermilion, --brass,
 * --rule, --text, --text-soft, --text-faint, --tile-edge, --font-jp,
 * --font-en, --font-ui).
 *
 * Body-class state vocabulary used here:
 *   - body.game-page         applies the in-game layout
 *   - body.study-mode        re-tints palette to bamboo via design.css
 *   - body.flipped-active    the body-level overlay is open
 *   - body.reference-page    radicals/kana — keeps Shuffle visible in study mode
 * ============================================================ */

/* ---------- App shell — safe-area handling for iPhone notch/island ----------
 * iOS Safari + viewport-fit=cover means content extends UNDER the Dynamic
 * Island / notch unless we explicitly inset. Apply safe-area-inset to the
 * top-level #app so the back-link header isn't tappable-blocked. Same
 * formula used by the per-pillar CSS since the design system shipped — now
 * canonical here so every game pillar inherits it. */
body.game-page #app {
  min-height: 100vh;
  min-height: 100dvh;
  padding-top: calc(env(safe-area-inset-top, 0px) + 0.45rem);
  padding-bottom: max(1.2rem, env(safe-area-inset-bottom, 0px));
  /* iOS can report asymmetric L/R safe-area values; matching sides keeps
     the column visually centered. */
  --game-pad: max(1.5rem, env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px));
  padding-left: var(--game-pad);
  padding-right: var(--game-pad);
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  max-width: 100%;
  box-sizing: border-box;
  overflow-x: hidden;
}

/* ---------- .game-column + header/main width ----------
 * #app uses flex with align-items: center (above) — children take their
 * natural width unless stretched. .game-column needs width: 100% with a
 * max-width cap so the grid container fills its row. Header + main also
 * need explicit width: 100% so they share the column width with the
 * grid. Without these the grid collapses to ~245px on iPhone narrow
 * (the --cell-w formula goes circular). */
body.game-page .game-column {
  width: 100%;
  max-width: 420px;
  min-width: 0;
  flex-shrink: 0;
}
body.game-page .header,
body.game-page main {
  width: 100%;
  max-width: none;
  margin-left: 0;
  margin-right: 0;
  box-sizing: border-box;
  min-width: 0;
}

/* ---------- Header layout (back-link, title, subtitle, controls, status) ---------- */
body.game-page .header {
  text-align: center;
  margin-bottom: 1.2rem;
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--rule);
  position: relative;
}
body.game-page .header .back-link {
  display: inline-block;
  margin-bottom: 10px;
}
body.game-page .header h1 {
  font-family: var(--font-jp);
  font-weight: 700;
  font-size: 1.6rem;
  letter-spacing: 0.04em;
  color: var(--text);
  margin: 0 0 0.2rem;
}
body.game-page .subtitle {
  font-family: var(--font-en);
  font-style: italic;
  font-size: 0.85rem;
  color: var(--text-soft);
  font-weight: 400;
  margin: 0 0 1rem;
}
body.game-page .status {
  font-family: var(--font-ui);
  font-size: 0.78rem;
  color: var(--text-faint);
  letter-spacing: 0.04em;
  margin-top: 0.6rem;
}

/* ---------- Buttons (Reset / Shuffle / Finish + game-over Yes/No) ---------- */
/* Game-page .controls keep all buttons on a single row even at iPhone-SE
 * width (~375px). Tightened horizontal padding + gap inside .controls only —
 * game-over buttons and other .btn instances elsewhere keep their roomy
 * sizing. Specificity beats `body.game-page .btn` (0,0,2,1) via
 * `body.game-page .controls .btn` (0,0,3,1). */
body.game-page .controls {
  gap: 0.4rem;
  flex-wrap: nowrap;
  margin-bottom: 0.6rem;
}
body.game-page .controls .btn {
  padding: 0.5rem 0.7rem;
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  flex: 0 1 auto;
  min-width: 0;
  border-radius: 100px;
  font-family: var(--font-ui);
  font-weight: 500;
  text-transform: uppercase;
}
body.game-page .btn-secondary {
  background: rgba(232, 220, 196, 0.06);
  border: 1px solid var(--rule);
  color: var(--text);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  box-shadow: none;
}
body.game-page .btn-secondary:hover:not(:disabled) {
  background: rgba(232, 220, 196, 0.12);
  border-color: rgba(232, 220, 196, 0.25);
  transform: translateY(-1px);
}
body.game-page .btn-secondary:active:not(:disabled) {
  transform: translateY(0);
  background: rgba(232, 220, 196, 0.08);
}
body.game-page .btn-primary {
  background: var(--vermilion);
  color: var(--bone);
  border: 1px solid var(--vermilion);
}
body.game-page .btn-primary:hover:not(:disabled) {
  background: #e85544;
  border-color: #e85544;
}

/* ---------- Grid scaffolding ----------
 * --cell-w computes from the 3-col baseline; the container fits its
 * content (centered via `margin: 0 auto`) so 2×2 / 3×3 are simply
 * narrower, not differently-sized. Cell width still adapts to viewport
 * (smaller on iPhone narrow, capped at 420px / 3 on wider screens). */
body.game-page .grid-container {
  display: grid;
  row-gap: 0.35rem;
  column-gap: 0.25rem;
  width: 100%;
  margin: 0 auto 1rem;
  padding: 4px 0 8px;
  box-sizing: border-box;
  overflow: visible;
  /* No `contain: layout style` — WebKit treats it as a containing block
     for `position: fixed` descendants, which would re-anchor the flipped
     popup back to the grid. Paint optimization isn't worth that. */
  --grid-max: min(100%, 420px);
  --grid-gap: 0.25rem;
  --cell-w: calc((var(--grid-max) - 2 * var(--grid-gap)) / 3);
}

/* Baseline grids — fill container, 1fr distributes evenly. */
body.game-page .grid-container.grid-c3r8,
body.game-page .grid-container.grid-c3r5 {
  grid-template-columns: repeat(3, 1fr);
  max-width: 420px;
}

/* Shrunk variants — fixed cell width, container stays at 100% (so the
 * `100%` in --cell-w's calc resolves to a definite value), cells are
 * centered via `justify-content: center`. */
body.game-page .grid-container.grid-3 {
  grid-template-columns: repeat(3, var(--cell-w));
  justify-content: center;
}
body.game-page .grid-container.grid-2 {
  grid-template-columns: repeat(2, var(--cell-w));
  justify-content: center;
}
body.game-page .grid-container.grid-4 {
  grid-template-columns: repeat(4, 1fr);
  max-width: 480px;
}

/* List mode — full pool, all entries on one tall grid. Inherits the
   3-col template. design.css ships an earlier rule with `max-width:
   100%; width: 100%` that breaks the --cell-w calc (the `100%` inside
   `min(100%, 420px)` becomes circular when the container itself has
   max-width: 100%, collapsing tile widths to ~82px). Restate at the
   same specificity so this rule wins source-order. Keeps tiles at the
   same 106px width as the standard 3×8 grid — matching the kanji
   study-mode appearance. */
body.game-page .grid-container.grid-list {
  max-width: min(100%, 420px);
}

/* ---------- Domino base (compositor hints + transitions) ---------- */
.domino {
  position: relative;
  border-radius: 10px;
  cursor: pointer;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  will-change: transform;
  transform: translateZ(0);
  backface-visibility: hidden;
  transition: transform 0.4s ease, box-shadow 0.18s ease;
  isolation: isolate;
}
.domino .domino-inner {
  position: absolute;
  inset: 0;
  border-radius: 10px;
  transform-style: preserve-3d;
}
.domino .domino-face {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  border-radius: 10px;
  backface-visibility: hidden;
}

/* In-grid tile with `.flipped` class is just a state marker — the
   body-level overlay shows the rich back face. Hide the in-grid back
   face and neutralize transforms so the tile stays a closed tile. */
body.game-page .grid-container .domino.flipped .domino-face-back {
  display: none !important;
}
body.game-page .grid-container .domino.flipped {
  transform: none !important;
  z-index: auto !important;
}

/* ---------- Glow progression (peek-counter visual) ----------
   bone (default) → amber (1 peek) → orange (2 peeks) → vermilion
   (3 peeks → Challenge Box for goi/bunpou/kanji; just visual for
   reference sets). Generic .domino--glow-* applies to all pillars;
   per-pillar files can override if they want different color values
   (e.g. radicals/css/style.css retints glow=3 to bamboo-green when
   body.study-mode is active). */
.domino.domino--glow-edge .domino-face-front {
  background: linear-gradient(155deg, #f5dcae 0%, #ecc88e 50%, #b89968 100%);
  box-shadow:
    inset -1.5px -1.5px 3px rgba(74, 69, 51, 0.3),
    inset 1.5px 1.5px 3px rgba(255, 255, 255, 0.45);
}
.domino.domino--glow-edge {
  box-shadow:
    0 3px 0 var(--tile-edge),
    0 6px 16px rgba(0, 0, 0, 0.5),
    0 0 0 1.5px rgba(228, 175, 100, 0.5);
}
.domino.domino--glow-wide .domino-face-front {
  background: linear-gradient(155deg, #f2bf86 0%, #e6a06a 50%, #b87850 100%);
  box-shadow:
    inset -1.5px -1.5px 3px rgba(74, 69, 51, 0.35),
    inset 1.5px 1.5px 3px rgba(255, 255, 255, 0.4);
}
.domino.domino--glow-wide {
  box-shadow:
    0 3px 0 #6a4a30,
    0 6px 16px rgba(0, 0, 0, 0.5),
    0 0 0 2px rgba(228, 140, 80, 0.7),
    0 0 14px rgba(228, 140, 80, 0.35);
}
.domino.domino--glow-full .domino-face-front {
  background: linear-gradient(155deg, #ee8a78 0%, #d44a3a 50%, #a83025 100%);
  box-shadow:
    inset -1.5px -1.5px 3px rgba(60, 18, 14, 0.45),
    inset 1.5px 1.5px 3px rgba(255, 220, 210, 0.35);
}
.domino.domino--glow-full .domino-face-front .domino-kanji {
  color: var(--bone);
  text-shadow:
    0 1px 0 rgba(0, 0, 0, 0.4),
    0 -1px 0 rgba(255, 255, 255, 0.2),
    1px 1px 0 rgba(0, 0, 0, 0.3);
}
.domino.domino--glow-full {
  box-shadow:
    0 3px 0 #5a2820,
    0 6px 16px rgba(0, 0, 0, 0.5),
    0 0 0 2px rgba(212, 74, 58, 0.95),
    0 0 22px rgba(212, 74, 58, 0.55),
    0 0 10px rgba(212, 74, 58, 0.4);
}

/* ---------- Removed-tile fade ---------- */
.domino.removed {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease;
}

/* ---------- Long-press visual feedback ----------
 * Added by main.js on touchstart, removed on touchend / cancel / movement.
 * While the user holds (the "remove / I knew it" gesture), the tile fills with
 * a STRONG Hanada-blue treatment over the LONG_PRESS_MS hold: a bold blue ring,
 * a blue outer glow, and a deep-indigo inset that visibly darkens the face.
 * Blue (not vermilion) on purpose — it both reads clearly as feedback (iPad has
 * no haptics, so this IS the only cue there) and stays distinct from the
 * vermilion "peek heat" progression. Strengthened per Joshua 2026-06-17 (the
 * old subtle vermilion glow was too weak). */
.domino.long-press-active {
  transition:
    box-shadow 0.3s ease-out,
    filter 0.3s ease-out;
  /* !important so the per-pillar .domino-{type} resting box-shadow (same
     specificity, loaded later) can't beat it — same cascade gotcha as the
     flipped-overlay caps. */
  filter: brightness(0.78) saturate(1.1) !important;
  /* bold blue ring + strong blue outer glow + deep-indigo darkening fill */
  box-shadow:
    0 3px 0 var(--tile-edge),
    0 6px 16px rgba(0, 0, 0, 0.5),
    0 0 0 4px var(--hanada),
    0 0 28px 5px rgba(94, 133, 187, 0.85),
    inset 0 0 38px rgba(42, 52, 88, 0.78) !important;
}

/* ---------- Reduced-motion ---------- */
@media (prefers-reduced-motion: reduce) {
  .domino, .domino .domino-inner, .domino .domino-face {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
  }
}

/* ============================================================
 * iPad — Phase 2: three-region landscape layout
 *
 * Joshua's design (locked 2026-05-16):
 *   - Left 2/3:  game grid (auto-fills columns at iPhone cell width)
 *   - Right 1/3: persistent flipped-tile detail panel (Phase 3 will
 *                wire the back-face render here; Phase 2 shows a
 *                dashed placeholder so the layout shape is visible)
 *   - Bottom:    dock spanning the full width, containing all
 *                top-of-page info (back link, title, subtitle,
 *                controls, status counter) — horizontal arrangement
 *
 * Gated behind html.ipad (inline detect in every <head>) AND
 * @media (orientation: landscape) — Info.plist locks iPad to
 * landscape natively; the media query is defense for the PWA path.
 *
 * iPhone CSS above is the default and stays UNCHANGED.
 * ============================================================ */
@media (orientation: landscape) and (min-width: 700px) {

  /* #app fills the iPad canvas exactly (no overflow). The dock at the
     bottom of .game-column must stay anchored to the viewport bottom
     even when the grid area scrolls internally. That requires #app to
     have a DEFINITE height (not just min-height) so the grid's `1fr`
     row resolves to a fixed pixel value and the grid-container can
     overflow within it. */
  html.ipad body.game-page #app {
    align-items: stretch;
    height: 100dvh;
    min-height: 0;
    overflow: hidden;
  }

  /* .game-column becomes a CSS grid: <main> spans the left 2/3 (both
     grid + progress-panel inside it), panel on the right, dock spans
     full width at bottom. <main> stays a real grid item (NOT
     display:contents — that was breaking row-height resolution and
     causing the grid to compress instead of scroll). */
  html.ipad body.game-page .game-column {
    max-width: none;
    flex: 1 1 auto;
    display: grid;
    grid-template-areas:
      "main panel"
      "dock dock";
    grid-template-columns: 2fr 1fr;
    grid-template-rows: 1fr auto;
    column-gap: 1rem;
    row-gap: 0.6rem;
    height: 100%;
    min-height: 0;
    overflow: hidden;
  }

  /* <main> is a flex column inside the "main" grid area. progress-panel
     gets its natural height; .grid-container takes the remaining space
     with internal scroll for any overflow. */
  html.ipad body.game-page main {
    grid-area: main;
    display: flex;
    flex-direction: column;
    min-height: 0;
    gap: 0.5rem;
    overflow: hidden;
  }
  html.ipad body.game-page .progress-panel {
    flex: 0 0 auto;
    margin: 0;
  }

  /* Challenge-box empty state: center within the left "main" grid area on
     iPad. Without this the message appears top-left because <main> is a
     flex column whose default alignment packs children at the start. The
     `.challenge-empty` body class is added by challenge/js/main.js when the
     box has no entries, so the centering only applies in that state — when
     tiles ARE in play we still want the grid to anchor to the top. The
     right-side panel area stays as it is (empty when no tile is flipped). */
  html.ipad body.challenge-empty main {
    justify-content: center;
    align-items: center;
  }
  html.ipad body.challenge-empty main > #empty-state {
    max-width: 560px;
  }

  /* ---------- Bottom dock (was iPhone top header) ---------- */
  html.ipad body.game-page .header {
    grid-area: dock;
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0.8rem 1.2rem;
    margin: 0;
    padding: 0.55rem 1rem;
    text-align: left;
    border-top: 1px solid var(--rule);
    border-bottom: none;
  }
  html.ipad body.game-page .header .back-link { margin: 0; }
  html.ipad body.game-page .header h1 {
    font-size: 1.15rem;
    margin: 0;
    letter-spacing: 0.06em;
  }
  html.ipad body.game-page .header .subtitle { margin: 0; font-size: 0.72rem; }
  html.ipad body.game-page .header .controls { margin: 0; }
  html.ipad body.game-page .header .status { margin: 0; font-size: 0.72rem; }

  /* ---------- bccb MERGED into the dock — one unified bottom bar ----------
     js/bccb.js appends #bccb into .header (the dock) on iPad game pages and
     tags it .bccb--docked. Strip its fixed-bar chrome so it reads as an inline
     group of symbol buttons sitting in the dock row, pushed to the far right. */
  html.ipad body.game-page .header .bccb--docked {
    position: static;
    inset: auto;
    width: auto;
    flex: 0 1 auto;
    margin-left: auto;            /* push the nav/toggle cluster to the right end */
    background: transparent;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    border: none;
    box-shadow: none;
    padding: 0;
    gap: 2px;
    z-index: auto;
  }
  html.ipad body.game-page .header .bccb--docked .bccb-btn {
    height: 36px;
    min-width: 34px;
    max-width: 44px;
  }
  html.ipad body.game-page .header .bccb--docked .bccb-btn svg { width: 20px; height: 20px; }
  html.ipad body.game-page .header .bccb--docked .bccb-glyph { font-size: 17px; }

  /* ---------- Ambient side panel (Phase 3) ----------
     The #ipad-side-panel element is injected into .game-column by
     game-loop.js when html.ipad is detected. It carries BOTH the
     `ipad-side-panel` class (this layout block) AND the `flipped-overlay`
     class — the latter so every per-pillar back-face CSS rule
     (`.flipped-overlay .xxx` scoped) matches without duplication. The
     downside: design.css's `.flipped-overlay` rule sets
     `position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%);
      width: min(92vw, 420px); height: min(99vh, 660px)` for the modal.
     We override those here so the panel lives inside the CSS grid layout
     instead of floating fixed-position over the page. */
  html.ipad body.game-page #ipad-side-panel.flipped-overlay {
    position: relative !important;
    top: auto !important;
    left: auto !important;
    transform: none !important;
    width: auto !important;
    height: auto !important;
    z-index: auto !important;
    cursor: default !important;
  }
  html.ipad body.game-page #ipad-side-panel {
    grid-area: panel;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    overflow-y: auto;
    padding: 0.75rem;
    border-radius: 12px;
    background: rgba(232, 220, 196, 0.02);
    min-height: 0;
  }
  /* Empty-state hint via :empty — shown until the first tile is tapped,
     and briefly between tile swaps. Vanishes the moment any back-face
     content renders into the panel. */
  html.ipad body.game-page #ipad-side-panel:empty {
    align-items: center;
    justify-content: center;
    border: 1px dashed var(--rule);
    padding: 2rem;
  }
  html.ipad body.game-page #ipad-side-panel:empty::before {
    content: "Tap any tile in the grid\Ato see its detail here";
    white-space: pre-line;
    text-align: center;
    color: var(--text-faint);
    font-family: var(--font-en);
    font-style: italic;
    font-size: 0.95rem;
    line-height: 1.5;
  }
  /* The cloned back-face .domino wrapper inside the panel. Same
     !important override pattern as `.flipped-overlay > .domino` in
     design.css (lines 794-802): the wrapper must FILL the panel
     regardless of per-pillar legacy rules (e.g., bunpou + goi carry
     `.domino-{type}.flipped { height: min(82vh, 540px) }` and
     `.domino-kanji { aspect-ratio: 2.2/1 }` which would otherwise
     squeeze the back-face into a wide-short rectangle inside the panel,
     stripping internal proportions of chips / readings / Goi compounds.
     `aspect-ratio: auto !important` is what kills the squeeze.
     The neutralization on .domino-inner mirrors the flipped-overlay
     fix for the legacy `rotateY(180deg)` mirror. */
  html.ipad body.game-page #ipad-side-panel > .domino {
    width: 100% !important;
    height: 100% !important;
    position: relative !important;
    top: auto !important;
    left: auto !important;
    transform: none !important;
    aspect-ratio: auto !important;
    margin: 0 !important;
  }
  html.ipad body.game-page #ipad-side-panel > .domino .domino-inner {
    position: relative !important;
    inset: auto !important;
    transform: none !important;
    -webkit-transform: none !important;
    transform-style: flat !important;
    width: 100% !important;
    height: 100% !important;
  }
  html.ipad body.game-page #ipad-side-panel > .domino .domino-face-back {
    position: relative !important;
    inset: auto !important;
    display: block !important;
    transform: none !important;
    width: 100% !important;
    height: 100% !important;
  }

  /* Highlight the currently-active (selected) tile in the grid so the
     user knows which one's showing in the panel. The in-grid .flipped
     class is already neutralized for 3D transforms (see line ~254),
     so we just paint a vermilion outline. */
  html.ipad body.game-page .grid-container .domino.flipped {
    outline: 2px solid var(--vermilion);
    outline-offset: 2px;
  }

  /* ---------- CRITICAL iPad fix: neutralize the modal-card position
     on the in-grid .flipped tile ----------

     Each pillar's back-face CSS sets a fixed-position centered modal
     card via `.domino.domino-{type}.flipped { position: fixed; top: 50%;
     left: 50%; transform: translate(-50%,-50%); width: min(92vw, 420px);
     height: min(...) }` (see css/radical-back-face.css line 14,
     css/goi-back-face.css line 24, css/kana-back-face.css analog,
     bunpou/css/style.css analog). That CSS is INTENDED to style the
     back-face wrapper rendered into #flipped-overlay.

     But the .flipped CLASS is also added to the IN-GRID tile by the
     engine during peek (so .flipped + .domino-{type} CSS matches that
     in-grid tile too). On iPhone the modal #flipped-overlay covers the
     whole screen so the in-grid tile's wrong styling is invisible.
     On iPad ambient mode there's no modal — the in-grid flipped tile
     expands into a 420×~580px fixed-position rectangle visible in the
     middle of the canvas (Joshua's "blue half rectangle" 2026-05-17),
     and the original tile-cell position appears empty (Joshua's
     "tile disappears, replaced by another").

     Fix: neutralize position + dimensions on the IN-GRID flipped tile.
     The wrapper inside #ipad-side-panel is handled by the existing
     `#ipad-side-panel > .domino` rules (line ~504) which already force
     width:100%, position:relative. iPhone is unaffected: the rule's
     `html.ipad` gate doesn't fire there. */
  html.ipad body.game-page .grid-container .domino.flipped {
    position: relative !important;
    top: auto !important;
    left: auto !important;
    right: auto !important;
    bottom: auto !important;
    width: 100% !important;
    height: auto !important;
  }
  /* Each per-pillar back-face CSS hides the FRONT face when the tile is
     flipped (e.g. css/radical-back-face.css line 39, css/goi-back-face.css
     line 67, css/kana-back-face.css analog, kanji-back-face.css analog) —
     intended so the modal-card renders only the back face. On iPad ambient
     mode the in-grid tile keeps its grid position; we want it to keep
     showing its FRONT face (the kanji glyph) so the user can still see
     which tile is the active one in the grid. The back-face is in the
     side panel, not in the grid. */
  html.ipad body.game-page .grid-container .domino.flipped .domino-face-front {
    display: flex !important;
  }

  /* ---------- Challenge Box: tap-to-confirm-review gesture (iPad only)

     Joshua-decided 2026-05-17: in Challenge mode the only way to remove
     a tile is the SRS-review-complete gesture (single-tap on the flipped
     tile fires onDismiss → review recorded). On iPhone the modal covers
     the screen so dismiss-anywhere works as a natural close-the-modal.
     On iPad the panel is persistent — there's no natural "close" gesture,
     so we must explicitly force the user back to the tile to confirm.

     Treatment:
     1. Active flipped Challenge tile gets a thick brass outline + soft
        pulsing glow so the user can find it again at a glance.
     2. Every OTHER tile in the grid becomes non-interactive (pointer-
        events: none, dimmed) until the user taps the highlighted tile to
        confirm. The lockout uses :has() to detect "any tile is flipped"
        and gates accordingly.
     3. Word-link drilling inside the panel back-face is unaffected — the
        lockout only targets .grid-container children, panel popups are
        separate.

     iPhone unchanged: gated by html.ipad. Other game pillars unchanged:
     scoped to body.challenge-page. ---------- */
  html.ipad body.challenge-page .grid-container .domino.flipped {
    outline: 3px solid var(--brass) !important;
    outline-offset: 3px !important;
    animation: challenge-active-pulse 1.4s ease-in-out infinite;
    z-index: 5;
  }
  html.ipad body.challenge-page .grid-container:has(.domino.flipped) .domino:not(.flipped) {
    pointer-events: none;
    opacity: 0.45;
    filter: saturate(0.6);
    transition: opacity 0.25s ease, filter 0.25s ease;
  }
  @keyframes challenge-active-pulse {
    0%, 100% {
      box-shadow:
        0 0 0 0 rgba(201, 164, 100, 0.55),
        0 0 22px rgba(201, 164, 100, 0.30);
    }
    50% {
      box-shadow:
        0 0 0 8px rgba(201, 164, 100, 0),
        0 0 28px rgba(201, 164, 100, 0.55);
    }
  }
  @media (prefers-reduced-motion: reduce) {
    html.ipad body.challenge-page .grid-container .domino.flipped {
      animation: none;
    }
  }

  /* ---------- Secondary popups inside the panel ----------
     The kanji-examples, radical card, goi-jump, and goi-from-kanji
     overlays are body-level position:fixed inset:0 modals by default
     (they cover the whole iPhone viewport with a backdrop + centered
     card). On iPad, game-loop.js moves them into #ipad-side-panel, and
     these rules reposition them to absolute:inset:0 INSIDE the panel —
     so they overlay just the panel area, leaving the grid + dock fully
     interactive underneath. The inner card max-width / max-height
     constraints are relaxed so the card fills the panel rather than
     trying to be a small centered floater in a small container. */
  html.ipad body.game-page #ipad-side-panel #kanji-examples-overlay,
  html.ipad body.game-page #ipad-side-panel #radical-overlay,
  html.ipad body.game-page #ipad-side-panel #goi-jump-overlay,
  html.ipad body.game-page #ipad-side-panel #goi-from-kanji-overlay {
    position: absolute !important;
    top: 0 !important;
    left: 0 !important;
    right: 0 !important;
    bottom: 0 !important;
    width: auto !important;
    height: auto !important;
    max-width: none !important;
    max-height: none !important;
    transform: none !important;
    margin: 0 !important;
  }
  /* Inner cards fill the panel — relax modal-sized max-width/height
     so the content uses the full panel area. The `.domino.domino-goi.
     flipped` rule in css/goi-back-face.css sets `position: fixed; top:
     50%; left: 50%; transform: translate(-50%,-50%); width: min(92vw,
     420px); height: min(82vh, 540px)` for the centered iPhone modal —
     when that card is rendered inside our absolute-positioned popup
     container, the position:fixed makes width:100% resolve to 100vw
     (viewport) instead of the panel. Override position + transform too
     so the inner card stays inside the popup's box. Same for the kanji
     examples-card. This way, every "basic flipped domino window" (the
     primary back-face + the Goi cards opened via goi-jump and
     goi-from-kanji chains) is the SAME panel-sized rectangle — no
     visual oscillation between flips/chains. (The kanji-examples-
     overlay and radical-overlay remain "special" surfaces with their
     own internal layouts; only their outer container is panel-sized.) */
  html.ipad body.game-page #ipad-side-panel #kanji-examples-overlay .examples-card,
  html.ipad body.game-page #ipad-side-panel #radical-overlay > .domino,
  html.ipad body.game-page #ipad-side-panel #goi-jump-overlay > .domino,
  html.ipad body.game-page #ipad-side-panel #goi-from-kanji-overlay > .domino {
    position: absolute !important;
    top: 0 !important;
    left: 0 !important;
    right: 0 !important;
    bottom: 0 !important;
    transform: none !important;
    width: auto !important;
    max-width: none !important;
    height: auto !important;
    max-height: none !important;
    margin: 0 !important;
    /* `.domino { justify-self: center }` in kanji/css/style.css:762 (and
       analogous per-pillar rules) is designed to center tiles in grid
       cells. Without this stretch override, CSS Box Alignment shrink-to-
       fits the absolutely-positioned card to its content width — which
       on the goi-from-kanji popup collapses width to 0 because the card
       has no intrinsic inline content (everything is in block children).
       Result: only a thin vertical sliver of the card was visible at the
       right edge of the panel. Forcing stretch restores left:0+right:0
       behavior (fill the inset rectangle). */
    justify-self: stretch !important;
  }

  /* ---------- Bunpou example: shrink JP text + EN on iPad ----------
     Same treatment as the Goi noun shrink above — bunpou example
     sentences are also dense (multiple particles + verbs + ruby) and
     overflow the panel at the default 2.25rem. Apply unconditionally
     for bunpou (every back-face has this — no per-pos distinction
     needed here). Furigana <rt> stays em-relative so it scales. */
  html.ipad body.game-page #ipad-side-panel .domino.domino-bunpou.flipped .bunpou-example-jp {
    font-size: 1.55rem;
    line-height: 2.2;
  }
  html.ipad body.game-page #ipad-side-panel .domino.domino-bunpou.flipped .bunpou-example-en {
    font-size: 0.85rem;
  }

  /* ---------- Goi NOUN example: shrink JP text + furigana ----------
     Noun example sentences are typically the most complex (subject +
     verb + multiple clauses with kanji + particle chains), so they
     overflow the iPad side panel at the default 2.1rem font size.
     Reduce font-size on the JP block; furigana <rt> is em-relative so
     it scales automatically. Scoped to `.goi-pos-noun` — added to
     `.goi-back-content` by js/goi-back-face.js based on entry.pos_label
     — so only nouns are affected. Other Goi categories (adverbs,
     adjectives, verbs, etc.) keep the default 2.1rem. */
  html.ipad body.game-page #ipad-side-panel .goi-back-content.goi-pos-noun .goi-example-jp {
    font-size: 1.45rem;
    line-height: 2.2;
  }
  html.ipad body.game-page #ipad-side-panel .goi-back-content.goi-pos-noun .goi-example-en {
    font-size: 0.8rem;
  }

  /* ---------- Grid auto-fill within the left 2/3 area ----------
     .grid-container is a flex child of <main> with flex:1 — takes
     remaining height after progress-panel. min-height:0 lets it
     shrink; overflow-y:auto scrolls when tile rows exceed available
     height. grid-auto-rows:min-content forces rows to honor the tile's
     natural aspect-ratio height (otherwise iOS WebKit compressed them
     to fit, ignoring aspect-ratio). */
  html.ipad body.game-page .grid-container {
    --grid-max: 100%;
    --cell-w: 137px;
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    align-content: start;
    grid-auto-rows: min-content;
  }
  html.ipad body.game-page .grid-container.grid-c3r8,
  html.ipad body.game-page .grid-container.grid-c3r5,
  html.ipad body.game-page .grid-container.grid-list {
    grid-template-columns: repeat(auto-fill, var(--cell-w));
    max-width: none;
    justify-content: center;
  }


  /* ---------- Panel placeholder area also scrolls internally ----------
     (Phase 3 will populate this with the back-face render which can be
     tall — same constraint as the grid.) */
  html.ipad body.game-page .game-column::after {
    min-height: 0;
    overflow-y: auto;
  }
}
