/* layout.css — page grid: status line, main pane, message log */

.page {
  max-width: 96ch;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* body has 8px top padding only; reserve full remaining height so
     the flex layout (with `margin-top: auto` on the log) docks the
     log flush against the viewport bottom. */
  min-height: calc(100vh - 8px);
}
/* Status horizontally centered within the page. The renderer paints
   `.status-line` (and any header banner) inside `#status`. */
.page > #status,
.page > .status-line {
  align-self: center;
}
/* Push the message log to the bottom of the page so it always sits
   pinned to the viewport floor, with the dungeon grid filling the
   space above it. */
.page > .message-log {
  margin-top: auto;
}

/* Sticky status line at the top */
.page > #status {
  position: sticky;
  top: 0;
  background: var(--bg);
  z-index: 2;
}

/* Toolbar row (legacy — kept for the proprietor page chrome). */
.page > .toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  align-items: center;
  padding: 1px 0;
}
.page > .toolbar .spacer { flex: 1; }
.page > .toolbar .nav { color: var(--yellow); font-weight: bold; cursor: pointer; }
.page > .toolbar.toolbar-center {
  justify-content: center;
  width: 78ch;
}

/* Compact action bar — high-frequency commands as chip buttons.
   Sits between the status block and the dungeon viewport. Each chip
   is a tap-target on touch + a keyboard reminder on desktop. */
.action-bar {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 78ch;
  max-width: 100%;
  padding: 4px 0 6px;
  flex-wrap: wrap;
}
.action-spacer { flex: 1; }
.action-chip {
  background: rgba(20, 20, 20, 0.55);
  border: 1px solid var(--dim);
  color: var(--yellow);
  padding: 3px 8px;
  font: inherit;
  font-size: 0.92em;
  cursor: pointer;
  letter-spacing: 0.5px;
}
.action-chip:hover { border-color: var(--yellow); background: var(--selected-bg); }
.action-chip:active { background: var(--selected-bg); }
.action-chip.action-help { color: var(--cyan); border-color: rgba(64, 192, 224, 0.5); }
.action-chip.action-help:hover { border-color: var(--cyan); }

/* Main pane is just stacked panels */
.main { padding: 4px 0; }

/* Two-column layout for adventurer view (catalogue + cart side-by-side on wide screens) */
@media (min-width: 1200px) {
  .main.two-col {
    display: grid;
    grid-template-columns: 1.4fr 1fr;
    gap: 24px;
    align-items: start;
  }
}

/* Inventory table (Proprietor view) */
.inv-table {
  white-space: pre;
}

/* Add-snack form (Proprietor) — terminal-style labels */
.add-form {
  white-space: pre;
}
.add-form .field {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  margin: 0 12px 4px 0;
}
.add-form input,
.add-form select { width: auto; }
.add-form input.name { width: 22ch; }
.add-form input.short { width: 6ch; }
.add-form input.medium { width: 12ch; }

/* Modal-as-panel (pickup picker, confirm prompts) */
.modal-veil {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.75);
  display: flex;
  justify-content: center;
  align-items: flex-start;
  padding-top: 80px;
  z-index: 10;
}
.modal-veil.hidden { display: none; }
.modal-panel {
  background: var(--bg);
  max-width: 84ch;
  width: 100%;
}

/* `.dialog` wraps form-style modal content (edit-snack, batch-randomize).
   Picker/receipt modals do NOT use this — they render their own box-drawing
   frame via `.panel.active` and would end up with a double border otherwise. */
.dialog {
  background: #0a0a0a;
  border: 1px solid var(--dim);
  padding: 14px 20px 18px;
  box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
}
.dialog-title {
  color: var(--yellow);
  font-weight: bold;
  margin-bottom: 4px;
  letter-spacing: 0.5px;
  font-size: 1.05em;
}
.dialog-rule {
  color: var(--dim);
  white-space: pre;
  margin-bottom: 12px;
}
/* Row hover/affordance inside form dialogs — a subtle background +
   yellow left-border lift, so clickable rows read as interactive
   without needing a real button look. Hand cursor already set inline. */
.dialog div.row { transition: background 80ms ease-out, color 80ms ease-out; border-left: 2px solid transparent; padding-left: 6px; }
.dialog div.row:hover {
  background: rgba(255, 208, 64, 0.06);
  border-left-color: var(--yellow);
}
.dialog .add-form > div { margin-bottom: 6px; }
.dialog .add-form > div:last-child { margin-bottom: 0; }
/* Inside form-style dialogs, list-style rows (rendered as <div class="row">)
   each get their own line. Span-based `.row` elements (catalogue/cart inside
   panels) stay inline to preserve box-drawing alignment. */
.dialog div.row {
  display: block;
  padding: 3px 4px;
  margin: 1px 0;
}

/* Pickup point sub-line */
.pp-blurb { color: var(--dim); display: block; padding-left: 6ch; }

/* -----------------------------------------------------------------
   Fullscreen 3D mode.
   When `view3d.show()` succeeds it adds `.view-3d` to <body>. In that
   state the WebGPU canvas fills the entire viewport and the rest of
   the UI (status line, toolbar, message log) floats over it as
   translucent panels, horizontally centered. The 2D ASCII flow is
   untouched.

   `!important` is used on a handful of rules to defeat the higher-
   specificity `.page > .toolbar.toolbar-center { width: 78ch }`
   (and similar) page-flow rules above.
   ----------------------------------------------------------------- */
body.view-3d {
  padding: 0 !important;
  overflow: hidden;
}
body.view-3d .page {
  max-width: none !important;
  margin: 0 !important;
  min-height: 100vh;
  display: block;
}
body.view-3d #view3d-canvas {
  position: fixed;
  inset: 0;
  width: 100vw !important;
  height: 100vh !important;
  z-index: 0;
  border: 0;
  background: #000;
  margin: 0;
}
/* Status line wrapper pins to the top with a translucent backdrop so
   the player can still read it against the rendered dungeon below.
   `#status` holds the new two-row HUD; the row layout already
   handles its own internal alignment. */
body.view-3d #status {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 3;
  background: rgba(0, 0, 0, 0.62);
  padding: 4px 10px 2px;
  text-align: center;
}
body.view-3d #status .status-line {
  position: static !important;
  background: transparent !important;
  margin-left: auto;
  margin-right: auto;
  border-bottom: 0;
  padding-bottom: 0;
}
body.view-3d #status .status-row { justify-content: center; }
/* Hotkey toolbar sits just under the status line. `!important` on
   width to defeat the `.page > .toolbar.toolbar-center { width: 78ch }`
   rule above, so the toolbar sizes to its content and the centering
   `left: 50%; translateX(-50%)` actually centers it. */
body.view-3d .toolbar-center {
  position: fixed !important;
  top: 2.7em;
  left: 50% !important;
  transform: translateX(-50%);
  z-index: 3;
  background: rgba(0, 0, 0, 0.62);
  padding: 3px 12px;
  width: auto !important;
  max-width: calc(100vw - 24px);
  justify-content: center;
}
/* Action bar floats under the status block in 3D mode. */
body.view-3d .action-bar {
  position: fixed;
  top: 3.5em;
  left: 50%;
  transform: translateX(-50%);
  z-index: 3;
  background: rgba(0, 0, 0, 0.55);
  padding: 4px 10px;
  width: auto;
  max-width: calc(100vw - 24px);
  border: 1px solid rgba(112, 112, 112, 0.35);
}
/* Message log centered + pinned to the bottom, translucent. The
   box-drawing header/footer borders still render — they just sit on
   top of the dark backdrop now. */
body.view-3d .message-log {
  position: fixed;
  left: 50%;
  bottom: 0;
  transform: translateX(-50%);
  z-index: 3;
  background: rgba(0, 0, 0, 0.62);
  padding: 0;
  margin: 0 !important;
  max-width: calc(100vw - 16px);
}
body.view-3d .log-mid--expanded {
  /* Shrink the log a touch in fullscreen since it floats over gameplay. */
  max-height: min(11em, max(4em, calc(100vh - 220px)));
}
body.view-3d .log-mid--collapsed {
  /* Tight 2-line ticker over the 3D viewport — keeps gameplay
     real estate free unless the player explicitly expands. */
  max-height: 2.6em;
}
/* The unused 2D grid is set display:none by view3d.show, but defensively
   hide it via CSS too in case the JS hasn't run yet. */
body.view-3d #dungeon-grid { display: none !important; }

/* FPS overlay — toggleable via Shift+F (DSS.fps.toggle). Pinned to the
   top-right of the viewport, above the status line, with a translucent
   backdrop so it stays readable over both 2D ASCII and 3D canvas. */
.fps-meter {
  position: fixed;
  top: 4px;
  right: 8px;
  z-index: 40;
  font: bold 12px/1 'IBM Plex Mono', 'Cascadia Mono', monospace;
  color: var(--green);
  background: rgba(0, 0, 0, 0.62);
  padding: 2px 6px;
  pointer-events: none;
  letter-spacing: 0;
  border: 1px solid var(--dim);
}

/* Target picker panel — opens when `DSS.targeting.open()` fires (ranged
   fire, single-target spells). Lists every visible candidate as a tap
   chip plus [FIRE] / [CANCEL] action buttons. Position-fixed above the
   touch controls so the player can still see the dungeon underneath
   and the d-pad below. */
.target-panel {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: 200px;
  z-index: 25;
  background: rgba(20, 20, 20, 0.92);
  border: 1px solid var(--yellow);
  padding: 6px 8px;
  min-width: 220px;
  max-width: calc(100vw - 16px);
  font: 13px/1.3 'IBM Plex Mono', 'Cascadia Mono', monospace;
}
.target-panel-header {
  margin-bottom: 4px;
  text-align: center;
}
.target-panel-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  max-height: 96px;
  overflow-y: auto;
}
.target-chip {
  background: rgba(0, 0, 0, 0.7);
  color: var(--fg);
  border: 1px solid var(--dim);
  padding: 4px 8px;
  cursor: pointer;
  font: inherit;
  -webkit-tap-highlight-color: transparent;
}
.target-chip.selected {
  border-color: var(--yellow);
  background: var(--selected-bg);
}
.target-chip:active { background: var(--selected-bg); }
.target-panel-actions {
  margin-top: 6px;
  display: flex;
  gap: 6px;
  justify-content: center;
}
.target-panel-fire,
.target-panel-cancel {
  padding: 6px 14px;
  font: bold 12px/1 'IBM Plex Mono', 'Cascadia Mono', monospace;
  letter-spacing: 1px;
  cursor: pointer;
  border: 1px solid var(--dim);
  background: rgba(0, 0, 0, 0.7);
  -webkit-tap-highlight-color: transparent;
}
.target-panel-fire { color: var(--red); border-color: var(--red); }
.target-panel-cancel { color: var(--dim); }
.target-panel-fire:active,
.target-panel-cancel:active { background: var(--selected-bg); }
@media (max-width: 720px) {
  .target-panel { bottom: 200px; min-width: 0; }
  .target-chip { padding: 6px 10px; font-size: 13px; }
}

/* Touch controls — hidden on desktop, visible on coarse-pointer viewports.
   Pinned to the bottom of the viewport so they don't overlap the dungeon
   grid or the message log on mobile. D-pad lives on the left, action
   buttons on the right; both are fixed so scrolling the log doesn't lose
   them. */
.touch-controls {
  display: none;
  position: fixed;
  left: 0; right: 0; bottom: 0;
  /* Above modal-veil (z-index 10) and the stacked sell sub-veil
     (z-index 20) so the Esc button on the touch grid can dismiss any
     dialog. Below `.hit-flash` (z-index 50) so the damage tint paints
     over the buttons during a hit. */
  z-index: 30;
  padding: 8px;
  pointer-events: none; /* let children claim pointer-events */
}
.touch-controls > * { pointer-events: auto; }
.touch-dpad {
  position: absolute;
  left: 12px;
  /* Lift the d-pad above the iOS home indicator on notched iPhones —
     `env(safe-area-inset-bottom)` is 0 on devices without one, so the
     desktop / Android layout is unchanged. */
  bottom: calc(12px + env(safe-area-inset-bottom, 0px));
  display: grid;
  grid-template-columns: repeat(3, 62px);
  grid-template-rows: repeat(3, 62px);
  gap: 4px;
}
.touch-dpad .touch-btn[data-key="ArrowUp"]    { grid-column: 2; grid-row: 1; }
.touch-dpad .touch-btn[data-key="ArrowLeft"]  { grid-column: 1; grid-row: 2; }
.touch-dpad .touch-btn.touch-center           { grid-column: 2; grid-row: 2; }
.touch-dpad .touch-btn[data-key="ArrowRight"] { grid-column: 3; grid-row: 2; }
.touch-dpad .touch-btn[data-key="ArrowDown"]  { grid-column: 2; grid-row: 3; }
.touch-actions {
  position: absolute;
  right: 12px;
  bottom: calc(12px + env(safe-area-inset-bottom, 0px));
  display: grid;
  /* Right cluster: 2 columns × 2 rows of essentials. Wider columns
     (78px) so 4-5 char labels (BAG / AGAIN / BACK / MORE) fit
     without truncation. */
  grid-template-columns: repeat(2, 78px);
  grid-auto-rows: 62px;
  gap: 4px;
}
.touch-btn {
  /* Default size — used by arrow buttons. Labeled buttons override
     width via the `.touch-labeled` rule so multi-char labels fit. */
  width: 62px; height: 62px;
  background: rgba(20, 20, 20, 0.85);
  color: var(--yellow);
  border: 1px solid var(--dim);
  font: bold 18px/1 'IBM Plex Mono', 'Cascadia Mono', monospace;
  cursor: pointer;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  /* Suppress the 300ms tap delay and the browser's double-tap-to-zoom
     gesture on the touch d-pad — both make rapid movement feel laggy. */
  touch-action: manipulation;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 0 2px;
  transition: background 80ms ease-out, border-color 80ms ease-out;
}
.touch-btn:active {
  background: var(--selected-bg);
  border-color: var(--yellow);
  transform: scale(0.96);
}

/* Labeled buttons stack a wide label (top, big-readable) over a small
   keyboard hint (bottom, dim) so the player both learns the action AND
   the keyboard shortcut next to it. */
.touch-labeled {
  width: 78px;
  font-size: 11px;
  font-weight: bold;
  letter-spacing: 0.5px;
  gap: 2px;
}
.touch-label {
  display: block;
  color: var(--yellow);
  font-size: 11px;
  line-height: 1;
  text-transform: uppercase;
}
.touch-hint {
  display: block;
  color: var(--dim);
  font-size: 9px;
  line-height: 1;
  letter-spacing: 0;
}
.touch-arrow { font-size: 22px; }
.touch-more-btn .touch-label { color: var(--green); }

/* The d-pad center button hosts the context-action label. The longest
   labels are "DESCEND" / "PICK UP" (7 chars), which fit comfortably in
   the 56px cell at 10px text. Idle state is fg-grey; when the cell
   under the player offers a real action the label flips green so the
   player can see at a glance that there's something to do. */
.touch-dpad .touch-center .touch-label { color: var(--fg); font-size: 10px; letter-spacing: 0; }
.touch-dpad .touch-center.touch-ctx-ready .touch-label { color: var(--green); }
.touch-dpad .touch-center .touch-hint { color: var(--dim); font-size: 9px; }

/* More tray — hidden by default, appears above the right-side essentials
   when [MORE] is toggled. Anchored to the right edge to align with the
   .touch-actions cluster underneath. No transitions (terminal aesthetic).
   Wider tile size to fit 4-5 char labels (CAST / FIRE / QUEST / STATS). */
.touch-more-tray {
  display: none;
  position: absolute;
  right: 12px;
  /* Sit just above the right-side cluster (2 rows × 62px ≈ 132px + gap),
     and lift past the iOS home-indicator safe area when present. */
  bottom: calc(142px + env(safe-area-inset-bottom, 0px));
  background: rgba(10, 10, 10, 0.95);
  border: 1px solid var(--yellow);
  padding: 8px;
  grid-template-columns: repeat(3, 78px);
  grid-auto-rows: 62px;
  gap: 4px;
  z-index: 31;
  box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.6);
}
.touch-controls.more-open .touch-more-tray { display: grid; }

/* Show touch controls only on coarse-pointer (touchscreen) viewports.
   Reserve bottom padding for the d-pad / action buttons so they don't
   overlap the message log or dungeon. Individual properties so the
   narrow-viewport block below can override left/right/top without
   resetting padding-bottom (CSS shorthand is the trap here). */
@media (pointer: coarse) {
  .touch-controls { display: block; }
  body {
    padding-top: 8px;
    padding-left: 8px;
    padding-right: 8px;
    /* 240px reserves the d-pad area; `env(safe-area-inset-bottom)` adds
       the iOS home-bar gap (~34px on notched iPhones, 0 elsewhere). */
    padding-bottom: calc(240px + env(safe-area-inset-bottom, 0px));
  }
  .page { max-width: 100%; }
  .dungeon-grid { width: auto; }
  .page > .toolbar.toolbar-center { width: 100%; }

  /* Message log on ANY touch device (portrait or landscape, phone or
     tablet). Pinned to the viewport bottom with the d-pad / action
     cluster height (~170px) reserved above so the log never sits
     under the touch controls. Width is the full viewport minus a
     small inset so the log reads at terminal scale instead of being
     squeezed to a narrow column. The previous rules lived in
     `max-width: 720px` only, which excluded landscape phones whose
     viewport width can exceed 720. */
  .message-log {
    position: fixed !important;
    /* 62×62 d-pad with `bottom: 12px` + 4px gaps spans roughly the
       bottom 206px of the viewport. Lift the log to 210 so the
       collapsed 2-line ticker (and the expand chip's hit area) clear
       the touch controls. Add `env(safe-area-inset-bottom)` on top so
       the log clears the iOS home indicator. Narrow-phone +
       landscape-short overrides below tighten this where the controls
       themselves shrink. `!important` on every positioning prop so the
       `body.view-3d .message-log` rule (which has higher specificity)
       doesn't reclaim positioning when 3D mode is active on a touch
       device. */
    bottom: calc(210px + env(safe-area-inset-bottom, 0px)) !important;
    left: 4px !important;
    right: 4px !important;
    top: auto !important;
    transform: none !important;
    width: auto !important;
    max-width: calc(100vw - 8px) !important;
    margin: 0 !important;
    z-index: 4;
    background: rgba(0, 0, 0, 0.78);
  }
  .log-head, .log-foot {
    overflow: hidden;
    white-space: pre;
    font-size: 12px;
  }
  .log-mid {
    overflow-x: auto;
    overflow-y: auto;
    font-size: 12px;
    padding: 3px 6px;
  }
  /* Touch collapsed: ~2.5 lines so the most recent message + the one
     before it are both visible without expanding. One line was too
     cramped — players couldn't read what just happened mid-combat. */
  .log-mid--collapsed { max-height: 2.8em !important; }
  /* Touch expanded: roughly five lines (was three). Players who pin
     the log open want enough history to retrace a fight. */
  .log-mid--expanded  { max-height: 6.5em !important; }
  body.view-3d .log-mid--collapsed { max-height: 2.8em !important; }
  body.view-3d .log-mid--expanded  { max-height: 6.5em !important; }
}
/* Landscape phones — short viewports where the default 62px touch
   buttons + 210px log lift would eat half the screen. Shrink the
   touch controls to 48px (mirroring the narrow-phone block) and
   lift the log only as far as the smaller controls demand.
   `(pointer: coarse) and (max-height: 500px)` matches any phone in
   landscape regardless of viewport width. */
@media (pointer: coarse) and (max-height: 500px) {
  .touch-dpad {
    grid-template-columns: repeat(3, 48px);
    grid-template-rows: repeat(3, 48px);
    gap: 3px;
    left: 6px;
    bottom: calc(6px + env(safe-area-inset-bottom, 0px));
  }
  .touch-actions {
    grid-template-columns: repeat(2, 64px);
    grid-auto-rows: 48px;
    gap: 3px;
    right: 6px;
    bottom: calc(6px + env(safe-area-inset-bottom, 0px));
  }
  .touch-btn { width: 48px; height: 48px; font-size: 14px; }
  .touch-labeled { width: 64px; font-size: 10px; }
  .touch-label { font-size: 10px; }
  .touch-hint { font-size: 8px; }
  .touch-dpad .touch-center .touch-label { font-size: 9px; }
  .touch-more-tray {
    grid-template-columns: repeat(3, 64px);
    grid-auto-rows: 48px;
    bottom: calc(168px + env(safe-area-inset-bottom, 0px));
  }
  /* Smaller touch controls = log can sit closer. 3*48 + 2*3 + 6 = 156. */
  .message-log { bottom: calc(162px + env(safe-area-inset-bottom, 0px)) !important; }
  /* Trim the modal bottom-pad too so dialogs aren't pushed off-screen
     on a short landscape viewport. */
  body { padding-bottom: calc(170px + env(safe-area-inset-bottom, 0px)); }
  .modal-veil { padding-bottom: calc(170px + env(safe-area-inset-bottom, 0px)); }
  .modal-panel { max-height: calc(100vh - 174px - env(safe-area-inset-bottom, 0px)) !important; }
}

/* -----------------------------------------------------------------
   Narrow viewports — phone-sized regardless of input type. The native
   78-character box-drawing chrome doesn't fit, so:
     - The hotkey toolbar at the top is hidden (touch buttons + the
       help dialog at [?] cover everything it lists).
     - The status line wraps onto multiple short lines.
     - The status rule + dialog rules switch from 74 `─` chars to a CSS
       `border-bottom` so they fill the actual width without overflow.
     - The message log shrinks to a few lines and allows horizontal
       scroll for the box-drawing frame.
     - Modal dialogs fill the viewport, scroll internally, and any
       `.panel` content inside them gets `overflow-x: auto` so wide
       formatted rows (catalogue, pickup picker, receipt) can be
       horizontally panned instead of pushed off the side of the page.
     - The 2D dungeon grid is wrapped in a horizontal-scroll container
       inline so the player can pan large floors without losing the
       status line or log. The 3D viewport remains the recommended
       mode on mobile and is the default on first launch.
   ----------------------------------------------------------------- */
@media (max-width: 720px) {
  /* Individual padding props so we don't trample the bottom padding
     that `(pointer: coarse)` sets for the touch controls above. */
  body {
    padding-top: 4px;
    padding-left: 4px;
    padding-right: 4px;
  }
  .page { max-width: 100%; min-height: calc(100vh - 4px); }

  /* Hide the desktop action bar — touch buttons cover the same set. */
  .page > .toolbar.toolbar-center { display: none; }
  .page > .action-bar { display: none; }

  /* Status: stretch to the viewport width and pack the two rows
     vertically with tighter font sizing for phone screens. */
  .page > #status { align-self: stretch; padding: 0; width: 100%; }
  .status-line {
    font-size: 12.5px;
    line-height: 1.35;
    padding: 2px 4px;
    width: 100%;
  }
  .status-row { gap: 8px; padding: 1px 2px; }
  .status-vitals { gap: 6px; flex: 1 1 100%; }
  .vital-bar { width: 11ch; }
  .status-id .status-role { display: none; } /* role is implied by class kit / spells; saves space */

  /* Dungeon grid: pan horizontally if the floor is wider than the
     viewport. `inline-block` so the pre sizes to its actual content. */
  .dungeon-grid {
    width: 100%;
    max-width: 100vw;
    overflow-x: auto;
    overflow-y: hidden;
    font-size: 12px;
    line-height: 1;
    margin: 0 0 8px;
  }

  /* Modal: fill the viewport, scroll internally. */
  .modal-veil {
    padding: 4px;
    align-items: stretch;
  }
  .modal-panel {
    max-width: 100% !important;
    max-height: calc(100vh - 8px);
    overflow-y: auto;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }

  /* Dialog (form-style modal body): compact padding + size. */
  .dialog {
    padding: 8px 10px 12px;
    font-size: 13px;
  }
  .dialog-title { font-size: 14px; }
  /* The 74-`─` rule overflows. Hide the chars, use a CSS border that
     auto-fits the viewport. */
  .dialog-rule {
    color: transparent !important;
    height: 0;
    line-height: 0;
    margin-bottom: 8px;
    border-bottom: 1px solid var(--dim);
    overflow: hidden;
  }

  /* Pre-formatted panels (pickup picker, receipt, shop catalogue / cart)
     are 78ch wide box-drawing — let them scroll horizontally inside the
     modal instead of pushing the page wider. */
  .panel, .receipt {
    overflow-x: auto;
    overflow-y: hidden;
    font-size: 12px;
    -webkit-overflow-scrolling: touch;
    /* iOS / Android won't trigger native momentum scroll on a single-axis
       pan without an explicit touch-action hint. `pan-x` lets the player
       fling-scroll the 78ch catalogue rows without the browser
       intercepting the swipe for page navigation. */
    touch-action: pan-x;
    overscroll-behavior-x: contain;
  }

  /* In-3D-mode chrome shrinks too. */
  body.view-3d #status { padding: 2px 4px 1px; }
}

/* Reserve room at the bottom of every modal for the touch controls
   so dialogs don't render under the d-pad. Fires on any touch device
   (portrait phone, landscape phone, tablet — anywhere `.touch-controls`
   is rendered). The landscape-short override above tightens this
   further for short viewports where 240 wastes too much. */
@media (pointer: coarse) {
  .modal-veil {
    padding-bottom: calc(240px + env(safe-area-inset-bottom, 0px));
    align-items: flex-start;
  }
  .modal-panel {
    max-height: calc(100vh - 244px - env(safe-area-inset-bottom, 0px)) !important;
  }

  /* When any modal is up, hide the movement d-pad + more-tray and
     reclaim that ~190px of bottom space for the modal. Movement keys
     are useless inside dialogs (inventory / shop / perks / help all
     navigate by letter or directly); the right-side action cluster
     stays visible so [BACK] / [BAG] still dismiss/swap.
     `:has()` is supported on Chrome 105+ / Safari 15.4+ / Firefox 121+
     — browsers without it just keep the legacy 240px reserve. */
  body:has(.modal-veil:not(.hidden)) .touch-dpad,
  body:has(.modal-veil:not(.hidden)) .touch-more-tray {
    display: none !important;
  }
  body:has(.modal-veil:not(.hidden)) .modal-veil {
    padding-bottom: calc(144px + env(safe-area-inset-bottom, 0px));
  }
  body:has(.modal-veil:not(.hidden)) .modal-panel {
    max-height: calc(100vh - 148px - env(safe-area-inset-bottom, 0px)) !important;
  }
}

/* Very narrow phones (under 400px) — squeeze the touch d-pad and
   actions in tighter so they don't overlap each other in portrait.
   Labels stay legible by shrinking type rather than dropping them. */
@media (max-width: 400px) {
  .touch-dpad {
    grid-template-columns: repeat(3, 48px);
    grid-template-rows: repeat(3, 48px);
    gap: 3px;
    left: 6px;
    bottom: calc(6px + env(safe-area-inset-bottom, 0px));
  }
  .touch-actions {
    grid-template-columns: repeat(2, 64px);
    grid-auto-rows: 48px;
    gap: 3px;
    right: 6px;
    bottom: calc(6px + env(safe-area-inset-bottom, 0px));
  }
  .touch-btn { width: 48px; height: 48px; font-size: 14px; }
  .touch-labeled { width: 64px; font-size: 10px; }
  .touch-label { font-size: 10px; }
  .touch-hint { font-size: 8px; }
  .touch-dpad .touch-center .touch-label { font-size: 9px; }
  .touch-more-tray {
    grid-template-columns: repeat(3, 64px);
    grid-auto-rows: 48px;
    bottom: calc(170px + env(safe-area-inset-bottom, 0px));
  }
  /* Narrow phone controls shrank to 48px — log lift can come down too. */
  .message-log { bottom: calc(162px + env(safe-area-inset-bottom, 0px)) !important; }
  body { padding-bottom: calc(170px + env(safe-area-inset-bottom, 0px)); }
  .modal-veil { padding-bottom: calc(170px + env(safe-area-inset-bottom, 0px)); }
  .modal-panel { max-height: calc(100vh - 174px - env(safe-area-inset-bottom, 0px)) !important; }
}

/* -----------------------------------------------------------------
   Portrait-phone right-column control layout.

   The default touch layout puts the d-pad and action cluster across
   the bottom of the screen, with the message log lifted above them.
   On portrait phones that bottom band eats ~210-240px of vertical
   space — roughly a third of the viewport on a 720-tall phone — and
   the log slab sits awkwardly between the dungeon and the controls.

   This block restacks the controls into a single column on the RIGHT
   edge (d-pad anchored bottom-right for thumb-natural movement, action
   cluster directly above), and moves the log to a thin strip pinned
   to the very bottom of the viewport. Net result: the dungeon area
   gets the full viewport height minus a slim log bar, with the right
   ~140px reserved for controls. More vertical visibility for the
   dungeon at the cost of some horizontal width — the dungeon scrolls
   horizontally on overflow, so panning still works.

   Restricted to portrait + max-width:720 so iPads in portrait keep
   the default bottom-bar layout (they have horizontal room to spare)
   and landscape phones keep theirs (the short-viewport block above
   handles them).
   ----------------------------------------------------------------- */
@media (pointer: coarse) and (orientation: portrait) and (max-width: 720px) {
  /* D-pad: pinned to the right edge above the log strip. Two modes:
     COLLAPSED (default) shows just the center context button (44×44);
     EXPANDED (.dpad-open) fans out the four arrow buttons around the
     center in a 3×3 grid (138×138). Tap-to-move on the dungeon viewport
     covers most movement, so the arrows hide by default to keep the
     control column compact. */
  .touch-dpad {
    left: auto;
    right: 6px;
    bottom: calc(98px + env(safe-area-inset-bottom, 0px));
    /* Collapsed: 1×1 grid with just the center button. */
    grid-template-columns: 44px;
    grid-template-rows: 44px;
    gap: 3px;
  }
  .touch-controls.dpad-open .touch-dpad {
    /* Expanded: 3×3 grid with arrows fanned around the center. */
    grid-template-columns: repeat(3, 44px);
    grid-template-rows: repeat(3, 44px);
  }
  /* Arrows hidden in collapsed mode; revealed when .dpad-open is set. */
  .touch-dpad .touch-arrow { display: none; }
  .touch-controls.dpad-open .touch-dpad .touch-arrow { display: flex; }
  /* Center button position depends on grid layout. Collapsed: occupies
     the single 1×1 cell. Expanded: snaps back to the 3×3 grid center. */
  .touch-dpad .touch-btn.touch-center { grid-column: 1; grid-row: 1; }
  .touch-controls.dpad-open .touch-dpad .touch-btn.touch-center {
    grid-column: 2; grid-row: 2;
  }
  /* Action cluster: single column (1×5) of 64×44 labeled buttons stacked
     above the d-pad — BAG / AGAIN / BACK / MORE / DPAD. Position depends
     on d-pad state: closer to the bottom when the d-pad is collapsed,
     lifted further when expanded. */
  .touch-actions {
    right: 6px;
    /* Collapsed d-pad is 44 tall: 98 bottom + 44 + 6 gap = 148. */
    bottom: calc(148px + env(safe-area-inset-bottom, 0px));
    grid-template-columns: 64px;
    grid-auto-rows: 44px;
    gap: 3px;
  }
  .touch-controls.dpad-open .touch-actions {
    /* Expanded d-pad is 138 tall: 98 + 138 + 6 = 242. */
    bottom: calc(242px + env(safe-area-inset-bottom, 0px));
  }
  /* DPAD toggle: green label when the d-pad is open so the player can
     see at a glance that it's the active toggle (mirrors how MORE flags
     its open state). */
  .touch-controls.dpad-open .touch-dpad-btn .touch-label { color: var(--green); }
  /* Button sizing for the smaller right-column form factor. */
  .touch-btn { width: 44px; height: 44px; font-size: 13px; }
  .touch-labeled { width: 64px; font-size: 10px; }
  .touch-label { font-size: 10px; }
  .touch-hint { font-size: 8px; }
  .touch-dpad .touch-center .touch-label { font-size: 9px; }
  /* More-tray opens to the LEFT of the action cluster (was: above).
     The right edge of the tray sits past the actions column so it
     doesn't visually merge with the always-visible buttons.
     Single-column actions are 64 wide → tray right inset = 64 + 12 = 76.
     Bottom matches the actions cluster (depends on d-pad state). */
  .touch-more-tray {
    right: calc(64px + 12px);
    bottom: calc(148px + env(safe-area-inset-bottom, 0px));
    grid-template-columns: repeat(3, 60px);
    grid-auto-rows: 44px;
    gap: 3px;
  }
  .touch-controls.dpad-open .touch-more-tray {
    bottom: calc(242px + env(safe-area-inset-bottom, 0px));
  }
  /* Body reserves: right column for the control cluster + log strip at
     the bottom. The right reserve narrows when the d-pad is collapsed
     (control column is 64px-wide actions only) and widens when the
     d-pad fans out (138px). The 240px bottom reserve from the generic
     (pointer: coarse) block above is wrong for this layout — we override
     it here. */
  body {
    padding-right: 70px;
    padding-bottom: calc(92px + env(safe-area-inset-bottom, 0px)) !important;
  }
  body:has(.touch-controls.dpad-open) {
    padding-right: 144px;
  }
  /* Message log: pin to the very bottom of the viewport at full width.
     The d-pad sits above it (bottom: 62px) so no horizontal inset is
     needed to clear the controls — the log gets the entire bottom strip.
     !important wins over the coarse-pointer override AND the view-3d
     positioning rule above. */
  .message-log {
    bottom: env(safe-area-inset-bottom, 0px) !important;
    left: 4px !important;
    right: 4px !important;
    top: auto !important;
    transform: none !important;
    width: auto !important;
    max-width: calc(100vw - 8px) !important;
    margin: 0 !important;
  }
  /* Modal: match the body reserves so dialogs don't slide under the
     control column or the bottom log strip. Also widen the panel to
     fill the available horizontal area (the 100% rule from max-width:720
     above clamps to body content width = viewport - 138px right reserve). */
  .modal-veil {
    padding-right: 138px;
    padding-bottom: calc(96px + env(safe-area-inset-bottom, 0px));
  }
  .modal-panel {
    max-width: calc(100vw - 146px) !important;
    max-height: calc(100vh - 100px - env(safe-area-inset-bottom, 0px)) !important;
  }
  /* When a modal is up the generic coarse-pointer rule hides the d-pad
     to reclaim space; with right-column controls that reclaim is on the
     RIGHT instead. Drop the right reserve so the modal fills horizontally,
     and reduce bottom reserve to just the log strip. */
  body:has(.modal-veil:not(.hidden)) {
    padding-right: 8px !important;
  }
  body:has(.modal-veil:not(.hidden)) .modal-veil {
    padding-right: 8px;
    padding-bottom: calc(96px + env(safe-area-inset-bottom, 0px));
  }
  body:has(.modal-veil:not(.hidden)) .modal-panel {
    max-width: calc(100vw - 16px) !important;
  }
  /* Compact single-row status — the default two-row HUD eats ~50px at
     the top, which is precious on a phone. Flatten the two `.status-row`
     wrappers via `display: contents` so HP/MP bars, floor, gold, and any
     active status tag all flow into one wrapping flex row. Hide the
     less-critical info (player name, level, coords, turn, atk/def/xp);
     the player can see it on the stats screen [S]. */
  .status-line {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px 10px;
    font-size: 12px;
    line-height: 1.3;
    padding: 2px 6px;
  }
  .status-row { display: contents; }
  .status-id .status-name,
  .status-id .status-lvl,
  .status-pos,
  .status-turn,
  .status-combat {
    display: none;
  }
  .status-id .player { font-size: 1em; }
  .status-vitals { flex: 1 1 auto; min-width: 0; gap: 8px; }
  .vital-bar { width: 10ch; }
  .status-line { border-bottom: 1px solid var(--dim); padding-bottom: 3px; margin-bottom: 4px; }
}
