:root {
  --bg: #ece1c6;
  --surface: #f4ecd6;
  --surface-2: #ece1c6;
  --border: #c3b78e;
  --border-strong: #a8916a;
  --border-heavy: #4a3826;
  --text: #3d2b1a;
  --text-dim: #6d5d3e;
  --text-faint: #6d5d3e;
  --outline: #2c2013;
  --divider-line: #8a7a52;
  --scrim: rgba(40,28,14,0.55);
  --scrim-light: rgba(40,28,14,0.4);
  /* The Options sheet's own lift-off-the-page shadow -- dark enough against
     this theme's light backdrop to read as a soft shadow rather than a hard
     edge (see the dark-mode override below, which needs a different value
     for the same reason). */
  --sheet-shadow: rgba(20,14,6,0.3);
  /* A dedicated, contrast-checked token for the My Escapes histogram bars
     -- border-heavy already clears 3:1 against the track in light mode, but
     the same pairing drops to ~2.7:1 in dark mode, so dark needs its own
     brighter value rather than sharing border-heavy's. */
  --bar: #4a3826;

  --font-display: 'Cinzel', Georgia, serif;
  --font-body: 'IM Fell English', Georgia, serif;
  --font-label: 'IM Fell English SC', Georgia, serif;

  --app-max: 460px;
  --radius: 10px;
  --radius-sm: 4px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #1e1912;
    --surface: #2b2419;
    --surface-2: #1e1912;
    --border: #3d3527;
    --border-strong: #6e5f42;
    --border-heavy: #8a7550;
    --text: #e8dcc4;
    --text-dim: #a99872;
    --text-faint: #a99872;
    --outline: #17130d;
    --divider-line: #a99872;
    --scrim: rgba(8,6,3,0.62);
    --scrim-light: rgba(8,6,3,0.45);
    /* A dark-on-dark shadow this faint is indistinguishable from the
       backdrop's own color here, which left the Options sheet's edge
       looking like a flat, hard-cut border instead of a lifted card --
       needs real contrast against the near-black backdrop instead of just
       reusing the light theme's value. */
    --sheet-shadow: rgba(0,0,0,0.55);
    --bar: #a99872;
  }
}

/* Explicit Appearance override (Options menu) — beats the media-query block
   above on specificity regardless of source order, so it wins in either
   direction: forcing light while the OS is dark, or dark while it's light. */
:root[data-theme="dark"] {
  --bg: #1e1912;
  --surface: #2b2419;
  --surface-2: #1e1912;
  --border: #3d3527;
  --border-strong: #6e5f42;
  --border-heavy: #8a7550;
  --text: #e8dcc4;
  --text-dim: #a99872;
  --text-faint: #a99872;
  --outline: #17130d;
  --divider-line: #a99872;
  --scrim: rgba(8,6,3,0.62);
  --scrim-light: rgba(8,6,3,0.45);
  --sheet-shadow: rgba(0,0,0,0.55);
  --bar: #a99872;
}
:root[data-theme="light"] {
  --bg: #ece1c6;
  --surface: #f4ecd6;
  --surface-2: #ece1c6;
  --border: #c3b78e;
  --border-strong: #a8916a;
  --border-heavy: #4a3826;
  --text: #3d2b1a;
  --text-dim: #6d5d3e;
  --text-faint: #6d5d3e;
  --outline: #2c2013;
  --divider-line: #8a7a52;
  --scrim: rgba(40,28,14,0.55);
  --scrim-light: rgba(40,28,14,0.4);
  --sheet-shadow: rgba(20,14,6,0.3);
  --bar: #4a3826;
}

/* Text Size (Options -> Accessibility) -- scoped to #root, not :root, so it
   scales the game's own text without touching the marketing page's hero/
   feature copy around it (same scoping the landscape-guard comment above
   already uses for the same reason). See the effect in App that keeps this
   in sync with settings.textSize. */
#root { --text-mult: 1; }

* { box-sizing: border-box; }

html, body { margin: 0; padding: 0; }

body {
  background:
    radial-gradient(ellipse 260px 200px at 8% 6%, rgba(150,110,55,0.10), transparent 60%),
    radial-gradient(ellipse 220px 260px at 96% 92%, rgba(120,85,40,0.09), transparent 60%),
    var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  -webkit-tap-highlight-color: transparent;
}

button { font: inherit; color: inherit; }

:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }

.app {
  max-width: var(--app-max);
  margin: 0 auto;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  position: relative;
  overflow-x: hidden;
}

/* ── Topbar ─────────────────────────────────────────── */

.topbar {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  /* Missing safe-area-inset-top entirely -- on a notched phone visiting
     the site directly, the game's own title bar overlapped the browser's
     own UI the same way it did in the wrapped app. */
  padding: calc(10px + env(safe-area-inset-top, 0px)) 18px 2px;
}

.brand h1 {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: calc(24px * var(--text-mult, 1));
  letter-spacing: 0.06em;
  text-transform: uppercase;
  margin: 0;
  line-height: 1;
  color: var(--text);
}

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

/* ── Key display (upper right, 3 cols x 2 rows) ──────── */

.key-grid {
  display: grid;
  grid-template-columns: repeat(3, 39px);
  grid-template-rows: repeat(2, 39px);
  gap: 5px;
  flex: none;
}
.key-chip { display: flex; align-items: center; justify-content: center; }
.key-ring {
  width: 39px;
  height: 39px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* Border/fill are now drawn inside the KeySymbol SVG itself (the split-color
   ring is the config identifier); this class only keeps the held-state pop. */
.key-ring.held { transform: scale(1.05); }
.key-ring svg { width: 39px; height: 39px; }

/* ── Stats ──────────────────────────────────────────── */

.statsrow {
  display: flex;
  gap: 16px;
  margin: 6px 0 0;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--border);
  font-family: var(--font-label);
  font-size: calc(14px * var(--text-mult, 1));
  color: var(--text-dim);
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
}
.statsrow b { color: var(--text); font-weight: 600; }

/* ── Configuration seals ─────────────────────────────── */

.config-tabs {
  display: flex;
  justify-content: center;
  gap: 16px;
  padding: 14px 16px;
}
.config-tab {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 2.6px solid var(--outline);
  padding: 0;
  cursor: pointer;
  background: var(--tab-c);
  touch-action: none;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.2s ease;
}
.config-tab.active { box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--tab-c); }
.config-tab.previewing { transform: scale(1.2); box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--tab-c), 0 4px 16px -3px var(--tab-c); }

/* Shape Markers on (Options -> Shape Markers): the seal's own SVG already
   draws its shape, its outline, and (for active/previewing) a same-shape
   ring -- so every bit of the round-specific chrome above is switched off
   here rather than fighting a circular box-shadow against a triangle. */
.config-tab.shaped { width: auto; height: auto; border-radius: 0; border: none; background: none; }
.config-tab.shaped.active, .config-tab.shaped.previewing { box-shadow: none; }
.config-tab.shaped svg { display: block; }
.config-tab.shaped.previewing svg { filter: drop-shadow(0 4px 10px var(--tab-c)); }

/* ── Contextual action bar ──────────────────────────── */

.context-bar {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 10px;
  padding: 12px 16px 0;
  min-height: 66px;
}
.ctx-btn {
  width: 54px;
  height: 54px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface);
  border: 1.4px solid var(--border-heavy);
  border-radius: var(--radius-sm);
  cursor: pointer;
  padding: 0;
  transition: transform 0.15s ease, border-color 0.2s ease;
}
.ctx-btn:not(:disabled):hover { border-color: var(--text); }
.ctx-btn:not(:disabled):active { transform: scale(0.93); }
.ctx-btn:disabled { border-style: dashed; border-color: var(--border-strong); cursor: not-allowed; }
/* Switches carry two INDEPENDENT disabled reasons, not one: wrong-config
   (dashed border, full-strength glyph — the base rule above, shared with
   chests and the portcullis, whose sole disabled reason is a context
   mismatch too) and no-key (solid border, dimmed glyph) can each be true or
   false regardless of the other, and both borders and both alpha states
   must be able to combine on one badge at once. */
.ctx-btn.no-key:disabled { border-style: solid; border-color: var(--border-heavy); }
.ctx-btn.no-key.wrong-config:disabled { border-style: dashed; border-color: var(--border-strong); }
@media (prefers-reduced-motion: no-preference) {
  :root:not([data-reduce-motion="true"]) .ctx-btn { animation: ctxIn 0.22s ease both; }
}
:root[data-reduce-motion="true"] .ctx-btn { animation: none; }
@keyframes ctxIn { from { opacity: 0; transform: translateY(-5px) scale(0.9); } to { opacity: 1; transform: none; } }

/* ── Map ────────────────────────────────────────────── */

.stage {
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: 14px 0 0;
  min-height: 0;
}
/* .map-frame above this keeps its own intrinsic height (aspect-ratio on
   .map-card); this absorbs whatever .stage has left over on a tall phone
   and centers ConfigTabs inside it, rather than leaving that leftover
   space collect below it as dead space. */
.stage-lower {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: 0;
}

/* The border used to live directly on .map-card, which also carries
   aspect-ratio: 16/17 to match the map SVG's own MAP_W×MAP_H viewBox 1:1.
   A content-box override made that combination render correctly in Chrome,
   but iOS Safari resolves aspect-ratio + a border + a box-sizing override
   on the same flex-column item differently -- it was putting the border's
   extra space at the TOP of the box instead of the bottom, so the grid's
   last row stayed clipped there while a sliver of empty space appeared
   above the first row. Rather than chase that engine disagreement further,
   the border now lives on this separate, unstyled-otherwise wrapper, so
   the aspect-ratio'd box below has no border of its own at all -- content-
   box vs. border-box becomes moot, and every engine has one fewer axis to
   disagree on. */
.map-frame {
  border-top: 3px solid var(--border-heavy);
  border-bottom: 3px solid var(--border-heavy);
}
.map-card {
  background: var(--surface);
  border: none;
  border-radius: 0;
  aspect-ratio: 16 / 17;
  width: 100%;
  position: relative;
  overflow: hidden;
  box-shadow: inset 0 0 22px rgba(120,95,50,0.14);
}
.map-card > svg { width: 100%; height: 100%; display: block; }

.map-loading {
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: 10px;
  font-family: var(--font-label);
  font-size: calc(12px * var(--text-mult, 1));
  color: var(--text-dim);
  letter-spacing: 0.03em;
}
.map-loading .ring {
  width: 26px; height: 26px;
  border-radius: 50%;
  border: 2px solid var(--border);
  border-top-color: var(--text);
}
@media (prefers-reduced-motion: no-preference) {
  :root:not([data-reduce-motion="true"]) .map-loading .ring { animation: spin 0.9s linear infinite; }
}
:root[data-reduce-motion="true"] .map-loading .ring { animation: none; }
.daily-fetch-error-sub { font-size: calc(12px * var(--text-mult, 1)); color: var(--text-dim); }
.daily-fetch-error-actions { display: flex; gap: 10px; margin-top: 8px; }

.node-hit { cursor: pointer; }

.mapline { transition: stroke 0.35s ease, stroke-opacity 0.35s ease; }
.mapnode-fill { transition: fill 0.3s ease, stroke 0.3s ease; }

.mote-core { transition: none; }

.hint-btn {
  position: absolute;
  right: 10px;
  bottom: 10px;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--surface);
  border: 1.6px solid var(--border-heavy);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 2px 6px rgba(40,28,14,0.25);
  padding: 0;
  cursor: pointer;
}
.hint-btn:active { transform: scale(0.94); }
.hint-btn:disabled { opacity: 0.5; cursor: not-allowed; }

/* Persistent floating mute button (web build only -- see MuteButton in
   app.js). Fixed to the viewport's bottom-left corner: every other
   floating/absolute control claims a different spot (hint-btn bottom-right
   of the map, key-grid top-right of the topbar, config-tabs/context-bar
   centered), and the title screen's own full-width buttons sit inside 32px
   of side padding -- that corner is clear on every screen this mounts
   into. z-index sits below options-scrim's 20 so any modal/sheet still
   covers it, same as everything else on the page. */
.mute-btn {
  position: fixed;
  left: 14px;
  bottom: calc(14px + env(safe-area-inset-bottom, 0px));
  z-index: 15;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--surface);
  border: 1.6px solid var(--border-heavy);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 2px 6px rgba(40,28,14,0.25);
  padding: 0;
  cursor: pointer;
}
.mute-btn:active { transform: scale(0.94); }
/* Today's Dungeon: LOOKS disabled but stays a real, clickable button --
   like the tutorial's ctxFail buttons, a native disabled attribute would
   swallow the tap this needs to register the "hints are off" cue. */
.hint-btn.looks-disabled { opacity: 0.5; }

@keyframes hintPulse {
  0%, 100% { opacity: 0.35; transform: scale(0.9); }
  50% { opacity: 1; transform: scale(1.12); }
}
.hint-pulse {
  transform-box: fill-box;
  transform-origin: center;
  pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
  :root:not([data-reduce-motion="true"]) .hint-pulse { animation: hintPulse 1.5s ease-in-out infinite; }
}
:root[data-reduce-motion="true"] .hint-pulse { animation: none; }

/* Corridor-segment highlight (tutorial only) -- a scale pulse would distort
   a line drawn along its own bounding box, so this pulses stroke-opacity
   only. */
@keyframes hintPulseStroke {
  0%, 100% { stroke-opacity: 0.32; }
  50% { stroke-opacity: 0.9; }
}
.hint-pulse-path { pointer-events: none; }
@media (prefers-reduced-motion: no-preference) {
  :root:not([data-reduce-motion="true"]) .hint-pulse-path { animation: hintPulseStroke 1.5s ease-in-out infinite; }
}
:root[data-reduce-motion="true"] .hint-pulse-path { stroke-opacity: 0.7; }

/* ── Options bar & sheet ──────────────────────────────── */

.options-bar {
  flex: none;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 12px 18px calc(12px + env(safe-area-inset-bottom, 0px));
  /* This is a real <button>, so without an explicit appearance reset and a
     border on every side, the browser's own default button chrome (a
     faint 3D bezel drawn in system colors, independent of this app's own
     --surface/--border theming) still shows through on the left/right/
     bottom edges -- every OTHER button in this file avoids that by
     setting `border: none` or a full border, which fully overrides the
     native chrome. That native bezel reads as a stray, mismatched shadow
     wherever the OS/browser's own dark control theme is much darker than
     its light one (which is most of them), even though nothing in this
     app's own CSS put it there. */
  appearance: none;
  -webkit-appearance: none;
  border: 1px solid transparent;
  border-top: 1px solid var(--border);
  /* Same top-corner radius as .options-sheet's own open state (see the
     matching border-radius there) -- no box-shadow here on purpose,
     since that's what actually signals "elevated/floating" (the sheet's
     shadow is cast upward, `0 -10px`, which only makes sense for
     something lifted above the surface behind it); this bar is flush at
     rest, not elevated, so it keeps that distinction while still
     matching the sheet's silhouette. */
  border-radius: 14px 14px 0 0;
  background: var(--surface);
  font-family: var(--font-label);
  font-size: calc(13px * var(--text-mult, 1));
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-dim);
  cursor: pointer;
}
.options-bar svg { width: 13px; height: 13px; transition: transform 0.2s ease; }
.options-bar.expanded svg { transform: rotate(180deg); }

.options-scrim {
  position: fixed;
  inset: 0;
  background: var(--scrim-light);
  z-index: 20;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease;
}
.options-scrim.show { opacity: 1; pointer-events: auto; }

.options-sheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 21;
  max-width: var(--app-max);
  margin: 0 auto;
  background: var(--surface);
  border-top: 2px solid var(--border-heavy);
  border-radius: 14px 14px 0 0;
  box-shadow: 0 -10px 30px var(--sheet-shadow);
  padding: 10px 18px calc(20px + env(safe-area-inset-bottom, 0px));
  max-height: 78vh;
  overflow-y: auto;
  transform: translateY(100%);
  transition: transform 0.3s cubic-bezier(0.2, 0.7, 0.3, 1);
}
.options-sheet.show { transform: translateY(0); }

.options-handle {
  width: 36px;
  height: 4px;
  border-radius: 2px;
  background: var(--border-strong);
  margin: 0 auto 14px;
}

.options-eyebrow {
  display: block;
  font-family: var(--font-label);
  font-size: calc(12px * var(--text-mult, 1));
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin-bottom: 6px;
}

/* Preferences and Accessibility live on their own sheets (SettingsSubSheet),
   stacked above the main Options sheet rather than inline within it. */
.options-scrim.sub-scrim { z-index: 24; }
.options-sheet.sub-sheet { z-index: 25; }
.options-sub-head { display: flex; align-items: center; gap: 14px; margin-bottom: 14px; }
.options-sub-head .options-eyebrow { margin-bottom: 0; }

.toggle-group { margin-bottom: 16px; }
.toggle-row {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 0;
  border-bottom: 1px solid var(--border);
  background: none;
  border-left: none; border-right: none; border-top: none;
  font: inherit;
  font-size: calc(15px * var(--text-mult, 1));
  color: var(--text);
  cursor: pointer;
  text-align: left;
}
.toggle-group .toggle-row:last-child { border-bottom: none; }
.toggle-row:disabled { opacity: 0.5; cursor: not-allowed; }

.toggle-box {
  width: 22px;
  height: 22px;
  border-radius: 5px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
}
/* --outline is deliberately a near-black "ink" tone in BOTH themes (see
   its own :root comment) -- correct for a map node's ink stroke against a
   colored fill, but this box's own background is --surface, which DOES
   flip dark in dark mode, so an --outline border/checkmark went invisible
   there. --text always has real contrast against --surface in either
   theme, which is what a checked toggle actually needs. */
.toggle-box.on { border: 1.6px solid var(--text); color: var(--text); }
.toggle-box.off { border: 1.6px dashed var(--border-strong); }
.toggle-box svg { width: 14px; height: 14px; }

.slider-row { width: 100%; padding: 10px 0; border-bottom: 1px solid var(--border); }
.toggle-group .slider-row:last-child { border-bottom: none; }
.slider-row.disabled { opacity: 0.5; }
.slider-row-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.slider-value { font-variant-numeric: tabular-nums; color: var(--text-dim); }
.slider-input {
  width: 100%;
  margin: 0;
  /* Lets the browser paint the native thumb/filled-track in the same ink
     the rest of the UI uses, rather than hand-rolling ::-webkit-slider-*
     pseudo-elements just to match a color that's already themed here. */
  accent-color: var(--text);
}
.slider-row.disabled .slider-input { pointer-events: none; }

.appearance-row { margin-bottom: 16px; }
.appearance-label {
  display: block;
  font-size: calc(15px * var(--text-mult, 1));
  color: var(--text);
  margin-bottom: 8px;
}
.segmented {
  display: flex;
  border: 1.3px solid var(--border-strong);
  border-radius: 6px;
  overflow: hidden;
}
.seg-btn {
  flex: 1;
  padding: 8px 0;
  font-family: var(--font-label);
  font-size: calc(12px * var(--text-mult, 1));
  letter-spacing: 0.06em;
  text-transform: uppercase;
  background: var(--surface);
  color: var(--text);
  border: none;
  border-left: 1px solid var(--border-strong);
  cursor: pointer;
}
.seg-btn:first-child { border-left: none; }
.seg-btn.active { background: var(--text); color: var(--bg); }

.link-group { margin-bottom: 16px; }
.option-link {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 0;
  border-bottom: 1px solid var(--border);
  background: none;
  border-left: none; border-right: none; border-top: none;
  font: inherit;
  font-size: calc(15px * var(--text-mult, 1));
  color: var(--text);
  cursor: pointer;
  text-align: left;
}
.link-group .option-link:last-child { border-bottom: none; }
.option-link svg { width: 14px; height: 14px; color: var(--text-dim); }

.options-new-dungeon { width: 100%; }

.confirm-row {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 14px;
  font-size: calc(15px * var(--text-mult, 1));
  color: var(--text);
}
.confirm-actions { display: flex; gap: 8px; justify-content: center; }
.confirm-actions button { padding: 9px 14px; min-height: 0; }

/* ── Win modal ──────────────────────────────────────── */

.win-scrim {
  position: fixed;
  inset: 0;
  background: var(--scrim);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  z-index: 30;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease;
}
.win-scrim.show { opacity: 1; pointer-events: auto; }

/* Tutorial scene fade — the Title → Tutorial launch transition. Always
   the app's own opaque ink tone (outlineC(), set inline) in both themes,
   matching the existing --scrim convention rather than swapping to white
   in light mode. Sits above everything else in the app. */
.scene-fade {
  position: fixed; inset: 0; z-index: 100;
  transition: opacity 0.4s ease;
}

/* Ending narration fade — the game view behind the closing recap cards
   gradually darkens (see endingFade in App). Sits behind TutorialCentered's
   own scrim (z-index 40) but above everything else, including win-scrim. */
.ending-fade {
  position: fixed; inset: 0; z-index: 32; pointer-events: none;
}

/* Typewriter "ready to advance" cursor — a down-pointing chevron (the
   options sheet's own right-pointing ChevronIcon, rotated) that bounces
   gently on its own local x-axis; composed after the 90deg rotation below,
   that reads as a vertical bob in screen space, the natural motion for a
   cursor now pointing down. Positioned out of normal flow — not a layout
   sibling of the text — so it never changes the box's own rendered height;
   that height feeds directly into where a hardcoded tailAt% lands along a
   left/right-sided tail's edge. */
/* Persistent Skip Tutorial control -- visible for the tutorial's whole
   duration, map and closing cards alike, so above everything else that
   can be on screen at once (including the scene-fade transition itself,
   z-index 100, briefly -- an acceptable trade for never disappearing at
   the wrong moment). */
.tut-skip-btn {
  position: fixed;
  /* The tutorial's fixed dungeon always has exactly 3 keys (one row of the
     key-grid, 39px tall) -- clearing that row, rather than sharing its top
     edge, is what keeps this from painting over "Keys collected" the whole
     time the tutorial runs. */
  top: calc(max(10px, env(safe-area-inset-top, 0px)) + 50px);
  right: 14px;
  z-index: 101;
  font-family: var(--font-label);
  font-size: calc(12px * var(--text-mult, 1));
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-dim);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  padding: 7px 14px;
  cursor: pointer;
}
.tut-skip-btn:active { transform: scale(0.96); }

.tut-cursor { position: absolute; right: 10px; bottom: 6px; }
.tut-cursor-chevron {
  width: 13px; height: 13px; color: var(--text-dim);
  transform: rotate(90deg);
  animation: tutCursorBounce 1.1s ease-in-out infinite;
}
.tut-cursor-chevron.still { animation: none; }
@keyframes tutCursorBounce {
  0%, 100% { transform: rotate(90deg) translateX(0); }
  50% { transform: rotate(90deg) translateX(4px); }
}

.win-card {
  background: var(--surface);
  border: 2px solid var(--border-heavy);
  border-radius: 3px;
  padding: 30px 24px;
  max-width: 320px;
  width: 100%;
  text-align: center;
  transform: translateY(10px) scale(0.97);
  opacity: 0;
  transition: transform 0.3s cubic-bezier(0.2, 0.7, 0.3, 1), opacity 0.3s ease;
}
.win-scrim.show .win-card { transform: translateY(0) scale(1); opacity: 1; }

.win-card .eyebrow {
  display: block;
  font-family: var(--font-label);
  font-size: calc(12px * var(--text-mult, 1));
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin-bottom: 8px;
}
.win-card .lead {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: calc(22px * var(--text-mult, 1));
  letter-spacing: 0.02em;
  margin: 0 0 6px;
  line-height: 1.3;
  color: var(--text);
}

.win-efficiency {
  font-family: var(--font-label);
  font-size: calc(13px * var(--text-mult, 1));
  letter-spacing: 0.04em;
  color: var(--text-dim);
  margin: 0 0 22px;
}

.win-daily-share { margin-bottom: 14px; }
.win-daily-share .btn-secondary { width: 100%; }
.win-share-preview {
  display: block;
  width: 100%;
  border-radius: var(--radius-sm);
  margin-top: 10px;
  border: 1px solid var(--border);
}
.win-share-note {
  font-family: var(--font-label);
  font-size: calc(12px * var(--text-mult, 1));
  font-style: italic;
  color: var(--text-dim);
  margin: 8px 0 0;
}

.win-actions {
  display: flex;
  gap: 8px;
  margin-bottom: 10px;
}
.win-actions .btn-secondary { flex: 1; }

.win-quit {
  display: block;
  margin: 14px auto 0;
  font-family: var(--font-label);
  font-size: calc(12px * var(--text-mult, 1));
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-dim);
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
}

.btn-secondary {
  font-family: var(--font-body);
  font-size: calc(14px * var(--text-mult, 1));
  background: var(--surface-2);
  color: var(--text);
  border: 1.3px solid var(--border-strong);
  border-radius: 2px;
  padding: 11px 8px;
  cursor: pointer;
  min-height: 44px;
}
.btn-secondary:active { transform: scale(0.98); }

.btn-primary {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: calc(15px * var(--text-mult, 1));
  letter-spacing: 0.04em;
  text-transform: uppercase;
  background: var(--text);
  color: var(--bg);
  border: none;
  border-radius: 2px;
  padding: 13px 20px;
  cursor: pointer;
  min-height: 44px;
}
.btn-primary:active { transform: scale(0.98); }

/* ── Title screen ───────────────────────────────────────── */

.title-ring { width: 100%; aspect-ratio: 390 / 320; overflow: hidden; flex: none; }
.title-ring svg { display: block; }

.title-body { padding: 8px 34px 0; }
.title-wordmark {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: calc(34px * var(--text-mult, 1));
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text);
  margin: 0;
}
.title-tagline-row { display: flex; align-items: center; gap: 10px; margin-top: 12px; }
.title-rule { width: 26px; height: 1px; background: var(--divider-line); flex: none; }
.title-tagline { font-family: var(--font-body); font-style: italic; font-size: calc(14px * var(--text-mult, 1)); color: var(--text-dim); margin: 0; }

.title-spacer { flex: 1; }

.title-actions { padding: 0 32px 20px; }
/* Whichever title-screen button is currently .btn-primary (uppercase by
   default -- see that rule) vs. .btn-secondary (never uppercase) depends
   on resume state, so without this override the same button's case would
   flip depending on whether there's a run to resume. Normalized here so
   all title-screen buttons read consistently regardless. .btn-primary
   also sets the Cinzel display font, which has no real lowercase glyphs
   -- it substitutes small-cap forms for lowercase input, so
   text-transform:none alone still reads as small caps; switching to the
   body font (already used by .btn-secondary, with real lowercase) is
   what actually matches the other buttons' capitalization. */
.title-cta { width: 100%; text-transform: none; letter-spacing: normal; font-family: var(--font-body); }
.title-cta-secondary { margin-top: 10px; }
.title-daily-caption {
  font-family: var(--font-label);
  font-size: calc(11.5px * var(--text-mult, 1));
  letter-spacing: 0.03em;
  color: var(--text-dim);
  text-align: center;
  margin: 8px 0 0;
  font-variant-numeric: tabular-nums;
}
/* Today's Dungeon, once already played -- stays labeled the same (no
   checkmark/badge clutter) but reads inert, matching the disabled look
   ctx-btn already uses elsewhere rather than inventing a new treatment. */
.btn-primary.dimmed {
  background: var(--surface-2);
  color: var(--text-dim);
  border: 1.3px solid var(--border-strong);
}
.title-links { display: flex; justify-content: center; gap: 28px; margin-top: 20px; }
.title-link {
  font-family: var(--font-label);
  font-size: calc(12px * var(--text-mult, 1));
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-dim);
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
}
/* ── Credits screen ─────────────────────────────────────── */

.credits-screen { overflow: hidden; }
.credits-ring-bg { position: absolute; inset: 0; pointer-events: none; z-index: 0; }
.credits-ring-bg svg { width: 100%; height: 100%; display: block; }

/* Shared by any screen reached from Options that navigates away and back
   via a chevron topbar (Credits, My Escapes, …). */
.sub-topbar { position: relative; z-index: 1; display: flex; align-items: center; gap: 14px; padding: 18px 18px 14px; border-bottom: 1px solid var(--border); }
.sub-back {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 1.4px solid var(--border-heavy);
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  background: none;
  cursor: pointer;
  padding: 0;
}
.sub-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: calc(16px * var(--text-mult, 1));
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text);
}
.credits-body {
  position: relative;
  z-index: 1;
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 0 32px;
  text-align: center;
}

/* ── Privacy / Terms screens (shared "single column of prose" layout) ── */

.doc-body {
  flex: 1;
  overflow-y: auto;
  padding: 22px 22px 32px;
  font-size: calc(15px * var(--text-mult, 1));
  line-height: 1.6;
  color: var(--text-dim);
}
.doc-body p { margin: 0 0 16px; }
.doc-lead {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: calc(19px * var(--text-mult, 1));
  color: var(--text);
  line-height: 1.35;
}
.doc-foot {
  font-family: var(--font-label);
  font-size: calc(11px * var(--text-mult, 1));
  letter-spacing: 0.06em;
  color: var(--text-faint);
  margin-top: 26px !important;
}

/* ── Symbol Reference screen ─────────────────────────────── */

.sym-body { flex: 1; overflow-y: auto; padding: 18px 0 8px; }
.sym-group { padding: 18px 22px 4px; }
.sym-group-title {
  font-family: var(--font-label);
  font-size: calc(11.5px * var(--text-mult, 1));
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin: 0 0 14px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border);
}
.sym-row { display: flex; align-items: center; gap: 14px; padding: 10px 0; }
.sym-badge { flex: none; cursor: default; }
/* Chamber/Key/Hint examples aren't real buttons -- give them the same
   54px column width a badge occupies so every group's icons line up on
   one left edge, without borrowing .ctx-btn's own button chrome. */
.sym-node { flex: none; width: 54px; height: 54px; display: flex; align-items: center; justify-content: center; }
.sym-text { flex: 1; }
.sym-name {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: calc(14.5px * var(--text-mult, 1));
  letter-spacing: 0.02em;
  color: var(--text);
  margin: 0 0 3px;
}
.sym-desc { font-size: calc(13.5px * var(--text-mult, 1)); line-height: 1.5; color: var(--text-dim); margin: 0; }
/* Four equal-area shapes (a triangle in particular) are too wide to sit
   beside their description on a phone-width column like every other
   badge row does -- stack icon-strip-then-text instead of side by side. */
.sym-row.sym-row-seals { flex-direction: column; align-items: stretch; gap: 8px; }
.sym-seals .config-tabs { justify-content: flex-start; padding: 4px 0; }
/* Reuses the real .hint-btn absolutely-positioned corner placement (right:
   10px; bottom: 10px), so it needs a small positioned box of its own to
   sit inside instead of the whole screen. */
.sym-hint-wrap { position: relative; width: 64px; height: 64px; }
.sym-foot { padding: 0 22px; }

/* ── My Escapes screen ──────────────────────────────────── */

.escapes-tabs { padding: 2px 18px 0; }
.escapes-body {
  position: relative;
  z-index: 1;
  flex: 1;
  overflow-y: auto;
  padding: 14px 18px 28px;
}
.escapes-empty {
  padding: 44px 20px;
  text-align: center;
  font-style: italic;
  font-size: calc(15px * var(--text-mult, 1));
  color: var(--text-dim);
  line-height: 1.6;
}
.escape-stat-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
  padding: 14px 0;
  border-bottom: 1px solid var(--border);
}
.escape-stat-row:last-child { border-bottom: none; }
.escape-stat-label {
  font-family: var(--font-label);
  font-size: calc(13px * var(--text-mult, 1));
  letter-spacing: 0.04em;
  color: var(--text-dim);
}
.escape-stat-value {
  font-family: var(--font-body);
  font-size: calc(18px * var(--text-mult, 1));
  font-weight: 600;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  text-align: right;
  flex: none;
}
.escape-stat-sub {
  display: block;
  font-family: var(--font-label);
  font-size: calc(11px * var(--text-mult, 1));
  font-weight: 400;
  letter-spacing: 0.02em;
  color: var(--text-dim);
  margin-top: 3px;
}

.histogram-section { margin-top: 28px; }
.histogram-section-eyebrow {
  font-family: var(--font-label);
  font-size: calc(12px * var(--text-mult, 1));
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin: 0 0 2px;
}
.histogram-section-note {
  font-family: var(--font-body);
  font-style: italic;
  font-size: calc(12px * var(--text-mult, 1));
  color: var(--text-dim);
  margin: 0 0 16px;
}
.histogram-tabs { margin-bottom: 18px; }
.histogram { margin-bottom: 0; }
.histogram-title {
  font-family: var(--font-label);
  font-size: calc(12px * var(--text-mult, 1));
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text);
  margin-bottom: 14px;
}
/* Rows instead of columns -- each bucket's count numeral sits in the same
   fixed column at the end of its own row, however short that row's bar
   is, rather than perched above a variable-height column. */
.histogram-rows { display: flex; flex-direction: column; gap: 9px; }
.histogram-row { display: flex; align-items: center; gap: 8px; }
.histogram-bucket-label {
  flex: 0 0 54px;
  font-family: var(--font-label);
  font-size: calc(12px * var(--text-mult, 1));
  letter-spacing: 0.01em;
  color: var(--text-dim);
  text-align: right;
  line-height: 1.2;
}
.histogram-track {
  flex: 1;
  min-width: 0;
  height: 13px;
  background: var(--border);
  border-radius: 0 3px 3px 0;
}
.histogram-bar {
  height: 100%;
  min-width: 2px;
  background: var(--bar);
  border-radius: 0 3px 3px 0;
}
.histogram-count {
  flex: 0 0 24px;
  font-family: var(--font-body);
  font-size: calc(12.5px * var(--text-mult, 1));
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

.escapes-clear-section { margin-top: 32px; padding-top: 18px; border-top: 1px solid var(--border); }
.escapes-clear-btn {
  display: block;
  margin: 0 auto;
  font-family: var(--font-label);
  font-size: calc(12px * var(--text-mult, 1));
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-dim);
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
}
.escapes-confirm-text {
  font-size: calc(14px * var(--text-mult, 1));
  color: var(--text);
  text-align: center;
  line-height: 1.45;
  margin: 0 0 12px;
}
.credits-wordmark {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: calc(30px * var(--text-mult, 1));
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text);
  margin: 0;
  line-height: 1;
}
.credits-tagline { font-family: var(--font-body); font-style: italic; font-size: calc(14px * var(--text-mult, 1)); color: var(--text-dim); margin: 12px 0 0; }
.credits-divider { width: 40px; height: 1px; background: var(--divider-line); margin: 30px 0; }
.credits-byline {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: calc(19px * var(--text-mult, 1));
  letter-spacing: 0.02em;
  color: var(--text);
  text-align: center;
  margin: 0;
  line-height: 1.5;
}

/* ── Pondhawk Games publisher mark ───────────────────── */

.pondhawk-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font-family: 'Jost', sans-serif;
  font-weight: 300;
  letter-spacing: 0.16em;
  text-transform: lowercase;
}
.title-pondhawk { margin-top: 16px; font-size: calc(11px * var(--text-mult, 1)); color: var(--text-dim); }
.credits-published-by {
  font-family: var(--font-body);
  font-style: italic;
  font-size: calc(11.5px * var(--text-mult, 1));
  color: var(--text-dim);
  text-align: center;
  margin: 0 0 8px;
}
.credits-pondhawk { font-size: calc(12px * var(--text-mult, 1)); color: var(--text-dim); }
.credits-copyright {
  font-family: var(--font-label);
  font-size: calc(11px * var(--text-mult, 1));
  letter-spacing: 0.04em;
  color: var(--text-faint);
  margin: 16px 0 0;
}
.credits-version {
  font-family: var(--font-label);
  font-size: calc(11px * var(--text-mult, 1));
  letter-spacing: 0.06em;
  color: var(--text-faint);
  margin: 6px 0 0;
}

/* Nodality is portrait-only -- see the orientation-lock attempt near
   App's mount effect. That API is best-effort and unsupported in plenty
   of contexts (a plain browser tab, an iframe, most of iOS Safari), so
   this pure-CSS guard is the real fallback: a short phone-landscape
   viewport (a phone rotated, not a tablet or a wide window) is caught
   entirely outside React, with no JS dependency, so it still shows even
   if the app bundle fails to boot. Static markup, not a React-rendered
   piece -- toggled purely by the media query below.
   Positioned against .mkt-play-frame (which it lives inside, in the
   markup), not the viewport -- on the marketing site only the game
   itself needs portrait, so this covers just that frame and leaves the
   hero copy, feature cards, and footer readable and scrollable while a
   phone is sideways. .mkt-play-frame's own min-height is what keeps this
   working before the game has mounted, since #root has no height of its
   own to size the frame against yet at that point. */
.landscape-guard {
  display: none;
  position: absolute; inset: 0; z-index: 500;
  background: var(--bg);
  color: var(--text);
  align-items: center; justify-content: center; text-align: center;
  padding: 32px;
  font-family: var(--font-body);
  font-size: 17px;
  line-height: 1.5;
}
@media (orientation: landscape) and (max-height: 500px) {
  .landscape-guard { display: flex; }
}

/* ═══════════════════════════════════════════════════════════════
   MARKETING PAGE (nodality.com) — everything below is specific to
   site/index.html's hero/feature/play-area shell built around the
   mounted game above. The game's own CSS (everything above this
   point) is untouched and still governs the mounted .app itself.
   ═══════════════════════════════════════════════════════════════ */

.mkt-body {
  min-height: 100dvh;
}

.mkt-wrap {
  max-width: 760px;
  margin: 0 auto;
  padding: 0 24px;
}

/* ── Hero ───────────────────────────────────────────── */

.mkt-hero {
  padding: 64px 0 44px;
  text-align: center;
}
.mkt-wordmark {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 52px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text);
  margin: 0;
  line-height: 1;
}
.mkt-tagline-row { display: flex; align-items: center; justify-content: center; gap: 12px; margin: 16px 0 0; }
.mkt-tagline {
  font-family: var(--font-body);
  font-style: italic;
  font-size: 17px;
  color: var(--text-dim);
  margin: 0;
}
.mkt-pitch {
  font-family: var(--font-body);
  font-size: 17px;
  line-height: 1.6;
  color: var(--text);
  max-width: 46em;
  margin: 26px auto 0;
}
.mkt-hero-cta {
  margin-top: 30px;
  display: inline-block;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 15px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  background: var(--text);
  color: var(--bg);
  border: none;
  border-radius: 2px;
  padding: 14px 30px;
  cursor: pointer;
  min-height: 44px;
  text-decoration: none;
}
.mkt-hero-cta:active { transform: scale(0.98); }

/* ── Feature call-outs ─────────────────────────────── */

/* .mkt-columns wraps .mkt-features + .mkt-play as plain stacked flow below
   ~900px (mobile and tablet keep today's proven layout untouched) -- the
   grid reflow into a sidebar-beside-the-game shape only kicks in at the
   desktop breakpoint below. */
.mkt-features { padding: 8px 0 52px; }
.mkt-features .mkt-wrap {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 18px;
}
@media (max-width: 640px) {
  .mkt-features .mkt-wrap { grid-template-columns: 1fr; }
}
.mkt-feature {
  background: var(--surface);
  border: 1px solid var(--border);
  border-top: 3px solid var(--border-heavy);
  border-radius: var(--radius);
  padding: 22px 20px 24px;
}
.mkt-feature-title {
  font-family: var(--font-label);
  font-size: 13px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text);
  margin: 0 0 10px;
}
.mkt-feature-body {
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.55;
  color: var(--text-dim);
  margin: 0;
}

/* The one card that isn't explaining a mechanic -- inverted fill (the same
   treatment .mkt-hero-cta already uses for its own single point of
   emphasis) sets it apart as an announcement rather than another fact in
   the list. Spans the full row in the 3-column tablet grid (641-899px) so
   it doesn't end up as a single narrow card orphaned on its own row --
   a no-op at both the single-column mobile width and the desktop sidebar,
   where cards already stack in one column either way. */
.mkt-feature-promo {
  grid-column: 1 / -1;
  background: var(--text);
  border: none;
}
.mkt-feature-eyebrow {
  display: block;
  font-family: var(--font-label);
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--bg);
  opacity: 0.7;
  margin: 0 0 8px;
}
.mkt-feature-promo .mkt-feature-title { color: var(--bg); }
.mkt-feature-promo .mkt-feature-body { color: var(--bg); opacity: 0.88; }

/* ── Play area ──────────────────────────────────────── */

.mkt-play {
  padding: 8px 0 60px;
}
.mkt-play-frame {
  max-width: var(--app-max);
  /* Reserves real box height before the game has mounted (#root starts
     empty) -- .landscape-guard below is positioned against this frame, not
     the viewport, specifically so it still has somewhere to show even if
     app.js never runs at all. Comfortably under a typical mounted game's
     height, so it never adds visible whitespace once .app is in. */
  min-height: 480px;
  margin: 0 auto;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: 0 12px 34px -12px rgba(20,14,6,0.35);
  position: relative;
}
/* The mounted game renders its own .app div (margin:0 auto, its own
   min-height:100dvh) inside #root -- collapse that min-height here so
   it sizes to its actual content instead of filling the browser
   viewport a second time inside the frame. */
.mkt-play-frame .app { min-height: 0; }

/* ── Desktop: features become a sidebar beside the game ──────────────
   Below this breakpoint (phones and most tablets, where the game already
   fills the useful width on its own) the two sections above just stack in
   document order, exactly as they always have. At this width there's
   enough room to seat the game's own fixed --app-max column next to a
   sidebar without either feeling cramped -- putting them side by side
   instead of the game sitting below a full-width row of cards cuts most
   of the scroll a desktop visitor used to hit before reaching gameplay. */
@media (min-width: 900px) {
  .mkt-wrap { max-width: 980px; }
  .mkt-hero { padding: 40px 0 32px; }
  .mkt-wordmark { font-size: 42px; }
  .mkt-pitch { margin-top: 20px; }
  /* The game is already right there in the second column at this width --
     a "jump down to it" CTA has nothing left to do. */
  .mkt-hero-cta { display: none; }

  .mkt-columns {
    max-width: 980px;
    margin: 0 auto 48px;
    padding: 0 24px;
    display: grid;
    grid-template-columns: var(--app-max) 1fr;
    gap: 40px;
    align-items: start;
  }
  /* .mkt-features comes first in the markup (so it's the one that appears
     on top when this collapses back to a single stacked column below the
     breakpoint) -- these two `order` values are what actually put the
     game on the left and the cards on the right at this width, without
     touching that markup order or anything about the mobile layout. */
  .mkt-columns .mkt-play { order: 1; }
  .mkt-columns .mkt-features { order: 2; }
  .mkt-columns .mkt-features,
  .mkt-columns .mkt-play {
    padding: 0;
  }
  .mkt-columns .mkt-features .mkt-wrap {
    display: flex;
    flex-direction: column;
    gap: 18px;
    max-width: none;
    padding: 0;
    /* Follows the game down the page as far as its own (taller) column
       runs, rather than stranding the last card once its own shorter
       column ends. */
    position: sticky;
    top: 32px;
  }
}

/* ── Legal / credits modals (Privacy, Terms, Credits) ────────────────
   These used to be full in-app screens reached only through Options --
   now small page-level modals (plain markup/CSS/JS in index.html, no
   React involved), opened from Options or the footer, dismissed without
   ever leaving the page they're already on. */
.mkt-modal-scrim {
  position: fixed;
  inset: 0;
  background: var(--scrim);
  z-index: 300;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.mkt-modal-scrim.show { display: flex; }
.mkt-modal-card {
  position: relative;
  width: 100%;
  max-width: 480px;
  max-height: 82vh;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-top: 3px solid var(--border-heavy);
  border-radius: var(--radius);
  padding: 30px 26px 26px;
  box-shadow: 0 20px 50px -12px rgba(20,14,6,0.5);
}
.mkt-modal-close {
  position: absolute;
  top: 14px;
  right: 14px;
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  border-radius: 50%;
  color: var(--text-dim);
  cursor: pointer;
}
.mkt-modal-close:hover { background: var(--surface-2); color: var(--text); }
/* Symbol Reference has a lot more content than the Privacy/Terms/Credits
   modals -- a bit wider, and its rows' own horizontal padding (sized for
   the old full-bleed mobile screen, no card around it) would otherwise
   stack with this card's own padding. */
.symbols-modal-card { max-width: 540px; }
.symbols-modal-card .sym-group,
.symbols-modal-card .sym-foot { padding-left: 0; padding-right: 0; }
.symbols-modal-card .sym-body { padding-top: 4px; }
.mkt-modal-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 22px;
  letter-spacing: 0.03em;
  color: var(--text);
  margin: 0 0 16px;
  padding-right: 24px;
}
.mkt-modal-lead {
  font-family: var(--font-body);
  font-style: italic;
  font-size: 17px;
  color: var(--text);
  margin: 0 0 16px;
}
.mkt-modal-card p {
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.6;
  color: var(--text-dim);
  margin: 0 0 14px;
}
.mkt-modal-foot {
  font-family: var(--font-label);
  font-size: 12px !important;
  letter-spacing: 0.03em;
  color: var(--text-faint) !important;
  margin: 22px 0 0 !important;
}
.mkt-credits-byline {
  font-family: var(--font-body);
  font-style: italic;
  font-size: 16px;
  color: var(--text);
  margin: 0 0 14px;
}
.mkt-credits-divider {
  width: 40px;
  height: 1px;
  background: var(--border-strong);
  margin: 18px 0;
}
.mkt-credits-published-by {
  font-family: var(--font-label);
  font-size: 12px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin: 0 0 10px;
}
.mkt-pondhawk-mark {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--text-dim);
  font-family: var(--font-label);
  font-size: 13px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  margin-bottom: 4px;
}

/* ── Footer ─────────────────────────────────────────── */

.mkt-footer {
  padding: 0 0 48px;
  text-align: center;
  font-family: var(--font-label);
  font-size: 12px;
  letter-spacing: 0.03em;
  color: var(--text-faint);
}
.mkt-footer p { margin: 4px 0; }

/* ── Desktop: Options/Preferences/Accessibility/Symbol Reference become a
   right-side panel instead of a mobile bottom sheet or full-screen swap ──
   Pure repositioning of the exact same elements/content (no JS changes) --
   below this breakpoint every one of these keeps today's mobile behavior
   untouched. Tutorial no longer has a row in Options at all (see app.js --
   it's promoted to its own title-screen button); Privacy/Terms/Credits and
   Symbol Reference all left the in-app screen system for modals (see
   .mkt-modal-* above), the same at every width, so this only ever has
   Preferences/Accessibility left to reposition. */
@media (min-width: 900px) {
  /* z-index values deliberately untouched here -- the existing 20/21/24/25
     stack already sits correctly under the tutorial overlay's own 30-101
     range (see .options-scrim/.options-sheet/.sub-sheet above and the
     tutorial/skip-dialog rules elsewhere in this file); bumping them would
     wrongly bury the tutorial's "Skip?" confirm behind this panel the one
     time both could plausibly be relevant (leaving the tutorial to open
     Options). Only position/size/shape change here. */
  .options-sheet {
    left: auto;
    right: 0;
    top: 0;
    bottom: 0;
    max-width: 400px;
    width: 400px;
    margin: 0;
    max-height: none;
    height: 100dvh;
    border-radius: 0;
    border-top: none;
    border-left: 2px solid var(--border-heavy);
    box-shadow: -14px 0 34px -10px var(--sheet-shadow);
    padding: 22px 26px 26px;
    transform: translateX(100%);
  }
  .options-sheet.show { transform: translateX(0); }
  /* The drag handle is a mobile bottom-sheet affordance; a side panel has
     no equivalent gesture, so it's just visual clutter here. */
  .options-handle { display: none; }
}
