/* ---- Game UI design tokens + components (main/pages/static/css/game-ui.css) ----
 *
 * The design-token layer for the 3D-space / crypt-engine UI (mausoleum, hall,
 * library, arcanum + everything the game JS builds inside them). Plain static
 * CSS, served as-is — NO Tailwind build step, so nothing in here may rely on
 * the compiled safelist. Linked from the four space templates immediately
 * above their <style> blocks so `var(--g-*)` is available to
 * tales/_space_styles.html and each page's own rules.
 *
 * HOUSE RULE (D1, game-UI overhaul): game markup uses .g-* classes and
 * var(--g-*) for anything with COLOUR; Tailwind stays for pure layout
 * (flex/grid/gap/spacing). This freezes safelist growth — new game chrome
 * should not mint new Tailwind colour classes.
 *
 * Every token value matches the Tailwind palette value already in use
 * (stone/amber scheme) EXACTLY — introducing this file changes zero pixels.
 * The .g-* component classes are drop-in equivalents of the per-module
 * Tailwind constant strings (BTN / BTN_GOLD / TILE / SLOT in levelUpUI.js,
 * inventoryUI.js, battle.js, and the space_hud.html panels); nothing consumes
 * them yet — migration is later phases swapping class strings.
 */

#crypt-root {
  /* Surfaces (stone-950 → stone-700). */
  --g-surface-0: #0c0a09;
  --g-surface-1: #1c1917;
  --g-surface-2: #292524;
  --g-surface-3: #44403c;

  /* Edges. --g-edge is the neutral hairline (stone-500/50); --g-edge-strong is
     the amber panel border (amber-800/50); --g-edge-soft is the quieter HUD
     chip border (stone-600/50 — what the round canvas buttons always wore). */
  --g-edge: rgba(120, 113, 108, 0.5);
  --g-edge-strong: rgba(146, 64, 14, 0.5);
  --g-edge-soft: rgba(87, 83, 78, 0.5);

  /* Ink (stone-200 / stone-300 / stone-400). */
  --g-ink-hi: #e7e5e4;
  --g-ink-mid: #d6d3d1;
  --g-ink-low: #a8a29e;

  /* Accent golds (amber-500 / 400 / 600 / 300 / 900). */
  --g-gold: #f59e0b;
  --g-gold-bright: #fbbf24;
  --g-gold-deep: #d97706;
  --g-gold-ink: #fcd34d;
  --g-gold-dim: #78350f;

  /* Semantic (red-400, emerald-400, sky-300, violet-300; xp shares gold). */
  --g-danger: #f87171;
  --g-heal: #34d399;
  --g-resource: #7dd3fc;
  --g-charge: #c4b5fd;
  --g-xp: #f59e0b;

  /* Radii (Tailwind rounded-lg / rounded-xl). */
  --g-radius: 0.5rem;
  --g-radius-lg: 0.75rem;

  /* Safe-area composites: the standard 1rem HUD inset plus the device notch /
     home-indicator inset (0 everywhere except notched phones — and only once
     base.html's viewport meta carries viewport-fit=cover). */
  --g-safe-b: calc(1rem + env(safe-area-inset-bottom, 0px));
  --g-safe-l: calc(1rem + env(safe-area-inset-left, 0px));
  --g-safe-r: calc(1rem + env(safe-area-inset-right, 0px));

  /* THE STACKING CONTRACT for everything inside #crypt-root. All game z-indexes
     come from these tokens — never a bare number, never a Tailwind z-[N]
     arbitrary (those aren't in the compiled CSS). The site header sits at z-30
     in page flow, which is why the overlay (25) stays below it while the
     fullscreen-only layers (60+) may exceed it — in fullscreen the header
     isn't rendered. Order, bottom to top:
       vignette (4)  < HUD buttons/D-pad (10) < nameplate tags (12)
       < settings panel (20) < popup overlay shell (25) < caption bar (30)
       < transition veil (60) < battle layer (62) < dice tray (65)
       < death screen (70). */
  --g-z-vignette: 4;
  --g-z-hud: 10;
  --g-z-tag: 12;
  --g-z-settings: 20;
  --g-z-overlay: 25;
  --g-z-caption: 30;
  --g-z-veil: 60;
  --g-z-battle: 62;
  --g-z-dice: 65;
  --g-z-death: 70;
}

/* ---- Components ------------------------------------------------------------
 * Drop-in equivalents of the Tailwind constant strings hardcoded per module.
 * One deliberate normalization: hover states are gated on :not(:disabled) —
 * the Tailwind originals reverted hover on disabled where it mattered
 * (disabled:hover:bg-amber-600 on BTN_GOLD); here every class does.
 * "transition-colors" expands to the standard Tailwind property list/timing.
 */

/* .g-btn ≡ BTN (levelUpUI.js): px-3 py-1.5 text-sm font-semibold rounded-lg
   bg-stone-700 hover:bg-stone-600 text-amber-200 transition-colors
   disabled:opacity-40 */
#crypt-root .g-btn {
  padding: 0.375rem 0.75rem;
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 600;
  border-radius: var(--g-radius);
  background-color: var(--g-surface-3);
  color: #fde68a; /* amber-200 */
  transition-property:
    color, background-color, border-color, text-decoration-color, fill, stroke;
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  transition-duration: 150ms;
}
#crypt-root .g-btn:hover:not(:disabled):not([aria-disabled="true"]) {
  background-color: #57534e; /* stone-600 */
}
#crypt-root .g-btn:disabled,
#crypt-root .g-btn[aria-disabled="true"] {
  opacity: 0.4;
}

/* .g-btn--sm ≡ the small variant (inventoryUI.js BTN): px-2 py-0.5 text-xs
   rounded (the small buttons use the tighter 0.25rem radius). */
#crypt-root .g-btn--sm {
  padding: 0.125rem 0.5rem;
  font-size: 0.75rem;
  line-height: 1rem;
  border-radius: 0.25rem;
}

/* .g-btn--tall — the settings panel's full-width rows (space_hud.html): the
   .g-btn face with the py-2 (0.5rem) vertical padding those buttons always
   had; the compact py-1.5 default reads thin on a full-width touch row. */
#crypt-root .g-btn--tall {
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
}

/* .g-btn--gold ≡ BTN_GOLD (levelUpUI.js / battle.js / inventoryUI.js):
   font-bold bg-amber-600 hover:bg-amber-500 text-stone-900
   disabled:hover:bg-amber-600 */
#crypt-root .g-btn--gold {
  font-weight: 700;
  background-color: var(--g-gold-deep);
  color: var(--g-surface-1);
}
#crypt-root .g-btn--gold:hover:not(:disabled):not([aria-disabled="true"]) {
  background-color: var(--g-gold);
}
#crypt-root .g-btn--gold:disabled,
#crypt-root .g-btn--gold[aria-disabled="true"] {
  background-color: var(--g-gold-deep);
}

/* .g-tile ≡ TILE (battle.js hotbar): relative w-14 h-14 rounded-lg border
   bg-stone-900/70 flex items-center justify-center transition-colors —
   with the neutral hairline as the default border colour. */
#crypt-root .g-tile {
  position: relative;
  width: 3.5rem;
  height: 3.5rem;
  border-radius: var(--g-radius);
  border: 1px solid var(--g-edge);
  background-color: rgba(28, 25, 23, 0.7); /* stone-900/70 */
  display: flex;
  align-items: center;
  justify-content: center;
  transition-property:
    color, background-color, border-color, text-decoration-color, fill, stroke;
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  transition-duration: 150ms;
}

/* .g-slot ≡ SLOT (inventoryUI.js paper-doll / backpack): same tile, but sized
   by its grid cell (aspect-square) instead of a fixed 3.5rem square. */
#crypt-root .g-slot {
  position: relative;
  aspect-ratio: 1 / 1;
  border-radius: var(--g-radius);
  border: 1px solid var(--g-edge);
  background-color: rgba(28, 25, 23, 0.7); /* stone-900/70 */
  display: flex;
  align-items: center;
  justify-content: center;
  transition-property:
    color, background-color, border-color, text-decoration-color, fill, stroke;
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  transition-duration: 150ms;
}

/* .g-panel ≡ the modal card (space_hud.html overlay_shell / settings panel /
   about): rounded-xl border border-amber-800/50 bg-gradient-to-b
   from-stone-900 to-stone-950 shadow-2xl shadow-black/60 */
#crypt-root .g-panel {
  border-radius: var(--g-radius-lg);
  border: 1px solid var(--g-edge-strong);
  background-image: linear-gradient(
    to bottom,
    var(--g-surface-1),
    var(--g-surface-0)
  );
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.6); /* shadow-2xl @ black/60 */
}

/* .g-stat ≡ the stat box (inventoryUI.js statBox): rounded-lg border
   border-stone-700 bg-stone-800/60 px-2 py-1.5 — the padding is part of the
   pattern, so uiKit.statBlock callers never have to add it back. */
#crypt-root .g-stat {
  border-radius: var(--g-radius);
  border: 1px solid var(--g-surface-3);
  background-color: rgba(41, 37, 36, 0.6); /* stone-800/60 */
  padding: 0.375rem 0.5rem;
}

/* .g-bar ≡ the meter track (battle.js hpBar): h-1.5 rounded bg-stone-700
   overflow-hidden. The fill defaults to gold — today's HP and XP bars are
   BOTH amber-500, so the --hp / --xp modifiers are semantic hooks that
   currently resolve to the same gold (retune later without touching JS). */
#crypt-root .g-bar {
  height: 0.375rem;
  border-radius: 0.25rem;
  background-color: var(--g-surface-3);
  overflow: hidden;
}
#crypt-root .g-bar__fill {
  height: 100%;
  background-color: var(--g-gold);
}
#crypt-root .g-bar--hp > .g-bar__fill {
  background-color: var(--g-gold);
}
#crypt-root .g-bar--xp > .g-bar__fill {
  background-color: var(--g-xp);
}

/* .g-tab — pill tab (new; no Tailwind-string ancestor). Base geometry matches
   .g-btn, but transparent with low ink; the active pill lights gold with an
   inset underline (box-shadow, so activation never shifts layout). */
#crypt-root .g-tab {
  padding: 0.375rem 0.75rem;
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 600;
  border-radius: var(--g-radius);
  background-color: transparent;
  color: var(--g-ink-low);
  transition-property:
    color, background-color, border-color, text-decoration-color, fill, stroke,
    box-shadow;
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  transition-duration: 150ms;
}
#crypt-root .g-tab:hover:not(:disabled):not([aria-disabled="true"]) {
  color: var(--g-gold-bright);
}
#crypt-root .g-tab:disabled,
#crypt-root .g-tab[aria-disabled="true"] {
  opacity: 0.4;
}
#crypt-root .g-tab--active {
  color: var(--g-gold-ink);
  box-shadow: inset 0 -2px 0 0 var(--g-gold);
}

/* .g-settabs / .g-setpane — the settings panel's sub-menus (Movement, Graphics,
   Audio, Dice). The strip is the same .g-tab vocabulary as the character sheet,
   laid out under the panel's title; the panes are plain blocks, shown one at a
   time by crypt/settings.js assigning the whole className ("g-setpane" /
   "g-setpane hidden"). The phone treatment is at the bottom of this file, beside
   the sheet handle it sticks under — it is NOT the sheet's bottom-tab layout: the
   settings card is a plain scroller, and its tabs belong at the top with the
   heading. */
#crypt-root .g-settabs {
  display: flex;
  gap: 0.25rem;
  border-bottom: 1px solid var(--g-edge-soft);
  overflow-x: auto;
}
/* Four tabs have to share a max-w-xs card, so they split it evenly and give up
   the pill's side padding rather than scrolling the strip sideways. */
#crypt-root .g-settabs .g-tab {
  flex: 1 1 0;
  padding-left: 0.25rem;
  padding-right: 0.25rem;
  font-size: 0.8125rem;
  text-align: center;
}
#crypt-root .g-setpane {
  display: block;
}
/* Tailwind's `.hidden` is one specificity step below the rule above, so the
   hidden panes need saying explicitly — without this they would all show. */
#crypt-root .g-setpane.hidden {
  display: none;
}

/* .g-controls — the read-only key reference in the Controls pane: a two-column
   key → action list. .g-kbd is the little key cap (replaces the old inline-Tailwind
   crypt-kbd-hint chips that used to sit under the viewport). */
#crypt-root .g-controls {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.4rem 0.9rem;
  align-items: center;
  margin: 0;
}
#crypt-root .g-controls dt {
  display: flex;
  gap: 0.2rem;
  align-items: center;
  justify-content: flex-start;
}
#crypt-root .g-controls dd {
  margin: 0;
  color: rgb(214 211 209); /* stone-300 */
  font-size: 0.875rem;
}
/* The rule between the Keyboard and On-a-touchscreen halves of the Controls pane. */
#crypt-root .g-controls-divider {
  height: 1px;
  margin: 0.9rem 0;
  background: rgba(120, 113, 108, 0.35); /* stone-500/35 */
}
#crypt-root .g-kbd {
  display: inline-block;
  min-width: 1.4rem;
  padding: 0.05rem 0.4rem;
  border-radius: 0.3rem;
  background: rgba(28, 25, 23, 0.9); /* stone-900 */
  border: 1px solid rgba(120, 113, 108, 0.5); /* stone-500/50 */
  color: rgb(231 229 228); /* stone-200 */
  font-size: 0.8rem;
  text-align: center;
  line-height: 1.3;
}

/* .g-swatches / .g-swatch* / .g-dice-preview / .g-warn — the settings panel's
   Dice pane. A row is a wrapping strip of preset chips with the native colour
   picker at its end, so "one of these" and "anything at all" read as one
   control: same size, same rim, same hover. crypt/settings.js builds the chips
   (from game/diceColors.js's preset lists) and paints each one's background
   inline — the colour a chip SETS is the only thing it has to say. */
#crypt-root .g-swatches {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.375rem;
}
/* The chip strip inside that row: its own wrapping flex box so the chips can fall
   onto a second line inside a max-w-xs card while the picker stays put. (Both
   palettes are seven, which fits one line beside the picker — see BODY_PRESETS.) */
#crypt-root .g-swatches__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
  flex: 1 1 auto;
  min-width: 0;
}
#crypt-root .g-swatch,
#crypt-root .g-swatch-wheel {
  flex: 0 0 auto;
  width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  border-radius: var(--g-radius);
  border: 1px solid var(--g-edge-soft);
  cursor: pointer;
  transition-property: border-color, box-shadow;
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  transition-duration: 150ms;
}
#crypt-root .g-swatch:hover,
#crypt-root .g-swatch-wheel:hover {
  border-color: var(--g-gold);
}
/* Selected: the same gold ring the level-up cards use — a box-shadow, so
   nothing in the strip shifts as the choice moves. */
#crypt-root .g-swatch--sel {
  border-color: var(--g-gold);
  box-shadow: 0 0 0 2px var(--g-gold-bright);
}
/* Strip the platform chrome off <input type="color"> so its well fills the chip
   and the picker reads as one more swatch rather than as a form field. */
#crypt-root .g-swatch-wheel {
  -webkit-appearance: none;
  appearance: none;
  background-color: transparent;
}
#crypt-root .g-swatch-wheel::-webkit-color-swatch-wrapper {
  padding: 0;
}
#crypt-root .g-swatch-wheel::-webkit-color-swatch {
  border: none;
  border-radius: calc(var(--g-radius) - 1px);
}
#crypt-root .g-swatch-wheel::-moz-color-swatch {
  border: none;
  border-radius: calc(var(--g-radius) - 1px);
}
/* ...which left it reading as one more preset chip: same size, same rim, same
   well, and nothing at all to say that THIS one opens a picker. So the wrapper
   stamps a dropper badge over it. A DARK DISC with a white glyph, rather than a
   bare glyph: the well underneath is any colour the player likes, including
   white and near-black, and only an opaque backing survives both. It is the
   wrapper's ::after with pointer-events:none, so the whole 28px well stays one
   click target — the badge is decoration, never a second button. */
#crypt-root .g-swatch-wheel-wrap {
  position: relative;
  display: block;
  flex: 0 0 auto;
  width: 1.75rem;
  height: 1.75rem;
  line-height: 0;
}
#crypt-root .g-swatch-wheel-wrap .g-swatch-wheel {
  display: block;
  width: 100%;
  height: 100%;
}
#crypt-root .g-swatch-wheel-wrap::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 1rem;
  height: 1rem;
  margin: -0.5rem 0 0 -0.5rem;
  pointer-events: none;
  background-repeat: no-repeat;
  background-size: contain;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='7' fill='%230c0a09' fill-opacity='.82'/%3E%3Cg fill='%23ffffff' transform='rotate(-45 8 8)'%3E%3Crect x='6.9' y='2.9' width='2.2' height='2.7' rx='1.1'/%3E%3Crect x='7.3' y='5.2' width='1.4' height='5'/%3E%3Cpath d='M7.3 10.2h1.4L8 12.7z'/%3E%3C/g%3E%3C/svg%3E");
}

/* .g-finishes / .g-finish* — the finish strip under each colour row (matte,
   gloss, metallic, the two glitters, pearlescent). TEXT chips rather than
   swatches: a finish has no colour of its own to show, and six unlabelled tiles
   would be a guessing game. crypt/settings.js builds them from
   game/diceColors.js's FINISHES and assigns the whole className to move the
   selection, so the selected rule may add nothing that changes the chip's size. */
#crypt-root .g-finishes {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
}
#crypt-root .g-finish {
  flex: 0 0 auto;
  padding: 0.25rem 0.5rem;
  border-radius: var(--g-radius);
  border: 1px solid var(--g-edge-soft);
  background-color: var(--g-surface-2);
  color: var(--g-ink-low);
  font-size: 0.6875rem;
  line-height: 1rem;
  cursor: pointer;
  transition-property: color, border-color, box-shadow;
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  transition-duration: 150ms;
}
#crypt-root .g-finish:hover {
  border-color: var(--g-gold);
  color: var(--g-ink-mid);
}
/* Selected: the same gold ring the swatches (and the level-up cards) wear, so
   "this colour" and "this finish" read as one choice made twice. */
#crypt-root .g-finish--sel {
  border-color: var(--g-gold);
  box-shadow: 0 0 0 2px var(--g-gold-bright);
  color: var(--g-ink-hi);
}

/* The d20 preview (#set-dice-preview): a flat SVG of a die face in the chosen
   pair and finishes, centred over the two rows. Deliberately not a THREE rig —
   see the comment on initDicePane in crypt/settings.js. */
#crypt-root .g-dice-preview {
  display: flex;
  justify-content: center;
}
#crypt-root .g-dice-preview svg {
  width: 4rem;
  height: 4rem;
}

/* .g-warn — an inline caution under a control (the dice contrast notice). It
   warns and nothing else, so it takes the danger ink without the toast's frame.
   The MARGIN belongs here rather than as a Tailwind utility in the markup:
   settings.js shows and hides it by assigning the whole className, so any
   utility on the element would be dropped the first time it toggled — and the
   gap has to disappear with the line anyway. `.hidden` needs saying explicitly:
   Tailwind's rule is a specificity step below the #crypt-root display above
   (same reason as .g-setpane). */
#crypt-root .g-warn {
  display: block;
  margin-bottom: 0.75rem;
  font-size: 0.6875rem;
  line-height: 1rem;
  color: var(--g-danger);
}
#crypt-root .g-warn.hidden {
  display: none;
}

/* .g-toast — notice pill (successor to game/ui.js notice(); geometry matches
   its cssText block). Panel-dark, small text, a left-border accent carrying
   the tone: gold for information, danger for warnings. */
#crypt-root .g-toast {
  padding: 0.5rem 0.875rem;
  border-radius: var(--g-radius);
  border: 1px solid var(--g-edge);
  border-left: 3px solid var(--g-gold);
  background-color: rgba(12, 10, 9, 0.92); /* stone-950/92 */
  color: var(--g-ink-hi);
  font-size: 0.875rem;
  line-height: 1.25rem;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.5);
  max-width: min(90vw, 420px);
}
#crypt-root .g-toast--warn {
  border-left-color: var(--g-danger);
  color: var(--g-danger);
}

/* .g-hud-btn — the round HUD chips floating over the canvas (fullscreen / map /
   settings / torch, plus the overlay-shell close). Shape + colour only:
   display and position stay Tailwind layout utilities in the markup — this
   class must never set `display`, or its #crypt-root specificity would beat
   the `hidden` toggles the space boots rely on. 40px round on desktop; on
   coarse pointers it grows to a 44px thumb target, and the --primary variant
   (the fullscreen button — the main path into a proper game view on a phone)
   grows larger still. */
#crypt-root .g-hud-btn {
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 9999px;
  background-color: rgba(12, 10, 9, 0.7); /* stone-950/70 */
  border: 1px solid var(--g-edge-soft);
  color: var(--g-ink-mid);
  transition-property:
    color, background-color, border-color, transform, box-shadow;
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  transition-duration: 150ms;
  -webkit-tap-highlight-color: transparent;
  touch-action: none; /* a drag on game chrome (torch/map/fullscreen) must not pan the page (item 10) */
}
/* Hover tint only on hover-CAPABLE devices. On touch, `:hover` STICKS after a tap, and this gold is
   the same gold as the torch "on" tint (.text-amber-400) AND outranks the torch off-tint
   (.text-stone-500) — its two :not()s make it specificity (1,4,0) vs the tint's (1,2,0) — so a
   toggled-off torch kept looking on. Gating it behind @media(hover:hover) lets the torch tints be the
   last word on touch (the off-state greys correctly). */
@media (hover: hover) {
  #crypt-root .g-hud-btn:hover:not(:disabled):not([aria-disabled="true"]) {
    color: var(--g-gold-bright);
  }
}
/* Pressed: sink slightly and firm the rim — kin to the D-pad's press glow but
   much quieter (these are chrome, not game controls). The scale COMPOSES with
   Tailwind's translate vars: the fullscreen button is centred with
   -translate-x-1/2 (which is itself a `transform`), and a bare scale() here
   would replace it and jump the button sideways while pressed. */
#crypt-root .g-hud-btn:active {
  transform: translate(var(--tw-translate-x, 0), var(--tw-translate-y, 0))
    scale(0.92);
  background-color: rgba(41, 37, 36, 0.85); /* stone-800/85 */
  border-color: var(--g-edge);
}
/* Torch state tints — arcanum/index.js toggles these two Tailwind classes on
   #btn-torch to reflect on/off. Re-declared at higher specificity so the
   chip's base ink above can't swallow them (declared last so the state also
   outranks :hover/:active at equal specificity). */
#crypt-root .g-hud-btn.text-amber-400 {
  color: var(--g-gold-bright);
}
#crypt-root .g-hud-btn.text-stone-500 {
  color: #78716c; /* stone-500 */
}
@media (pointer: coarse) {
  #crypt-root .g-hud-btn {
    width: 2.75rem;
    height: 2.75rem;
  }
}

/* Fullscreen button: right-justified just LEFT of the settings gear (#btn-settings, right:.75rem),
   not top-centre — the wider hero HUD card (12rem) overlapped a centred button on narrow portrait
   phones. `right` = gear inset + gear width + gap; the gear is 2.5rem (fine) / 2.75rem (coarse). Its
   `top-3` stays from the markup. */
#crypt-root #btn-fullscreen {
  right: calc(0.75rem + 2.5rem + 0.5rem);
}
@media (pointer: coarse) {
  #crypt-root #btn-fullscreen {
    right: calc(0.75rem + 2.75rem + 0.5rem);
  }
}

/* .g-hud-pill — the FPS counter's chip (#crypt-fps): the same surface/edge as
   .g-hud-btn in a small text pill; geometry stays Tailwind layout in the
   markup. Ink is the gold the counter always used (amber-300). */
#crypt-root .g-hud-pill {
  background-color: rgba(12, 10, 9, 0.7); /* stone-950/70 */
  border: 1px solid var(--g-edge-soft);
  color: var(--g-gold-ink);
}
/* Position: on the top HUD row, just LEFT of the fullscreen button — one more
   step along the gear→fullscreen chain above, so `right` = the fullscreen
   button's own inset + a button width + the same 0.5rem gap. Vertically
   centred against the 2.5rem (fine) / 2.75rem (coarse) buttons: the pill is
   shorter than the row, so it anchors at the row's midline (top-3 + half a
   button) and translates back by half its own height. */
#crypt-root #crypt-fps {
  right: calc(0.75rem + 2.5rem + 0.5rem + 2.5rem + 0.5rem);
  top: calc(0.75rem + 1.25rem);
  transform: translateY(-50%);
}
@media (pointer: coarse) {
  #crypt-root #crypt-fps {
    right: calc(0.75rem + 2.75rem + 0.5rem + 2.75rem + 0.5rem);
    top: calc(0.75rem + 1.375rem);
  }
}

/* .g-section-label — the small uppercase group heading inside panels
   ("Dice appearance" in the settings sheet's Dice pane). */
#crypt-root .g-section-label {
  font-size: 0.6875rem; /* text-[11px] */
  line-height: 1rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em; /* tracking-wide */
  color: var(--g-ink-low);
}

/* .g-divider — a plain rule between two regions of one panel. In the settings
   card it separates the pane region from the footer (Reset / About / Save /
   Cancel), which shows on every tab: without it those buttons read as the bottom
   of whichever pane is up. Bottom margin is left to the first footer button's
   own utility. */
#crypt-root .g-divider {
  height: 1px;
  margin-top: 0.875rem;
  background-color: var(--g-edge-soft);
}

/* .g-keys — the Movement pane's read-only keyboard reference (#set-controls-list).
   A two-column definition list: the action on the left, its keys on the right, in
   the same quiet ink as a row label. Nothing here is interactive; the <kbd> caps
   are the same shape the crypt's own hint line under the viewport uses, so a key
   looks like a key wherever it is named. */
#crypt-root .g-keys {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.3rem 0.625rem;
  align-items: baseline;
  font-size: 0.6875rem;
  line-height: 1rem;
}
#crypt-root .g-keys dt {
  color: var(--g-ink-low);
}
#crypt-root .g-keys dd {
  margin: 0;
  color: var(--g-ink-mid);
}
#crypt-root .g-keys kbd {
  display: inline-block;
  min-width: 1.1rem;
  margin-right: 0.1875rem;
  padding: 0 0.25rem;
  border-radius: 0.25rem;
  border: 1px solid var(--g-edge);
  background-color: var(--g-surface-2);
  color: var(--g-ink-hi);
  font-family: inherit;
  font-size: 0.625rem;
  line-height: 1rem;
  text-align: center;
}
#crypt-root .g-keys__or {
  color: var(--g-ink-low);
}

/* .g-select — the settings panel's dropdown rows (#set-controls). The .g-btn
   surface on a native <select>; width/margins stay Tailwind layout in the
   markup. */
#crypt-root .g-select {
  padding: 0.375rem 0.5rem;
  font-size: 0.875rem;
  line-height: 1.25rem;
  border-radius: var(--g-radius);
  background-color: var(--g-surface-1);
  border: 1px solid var(--g-edge);
  color: var(--g-ink-hi);
}

/* .g-stick — the floating touch joystick (spaces/touchControls.js). While the
   "Joystick" control scheme is active the ring PARKS visible at low opacity
   bottom-left (--parked — the idle affordance: players must be able to SEE the
   stick before they ever touch it); a left-half canvas touch re-anchors the
   same node under the thumb at full opacity (--live). With neither class
   (Buttons scheme, or mid-teardown) it is invisible. Feedback only, NEVER a
   target (pointer-events: none — the gesture itself is tracked on the
   viewport). Dark and translucent like .crypt-btn; the negative margins centre
   the ring on the anchor point, so the parked left/bottom offsets below place
   the ring's CENTRE — half the ring above/right of the safe-area insets puts
   its edges exactly ON them, where the D-pad's corner sat. The fade is the
   whole park/lift transition (left/top return to auto, which cannot animate).
   The 3.25rem (52px) ring radius comfortably contains the 40px nub travel
   clamp (STICK_TRAVEL_PX). */
#crypt-root .g-stick {
  position: absolute;
  width: 6.5rem;
  height: 6.5rem;
  margin: -3.25rem;
  border-radius: 9999px;
  background-color: rgba(
    28,
    25,
    23,
    0.45
  ); /* stone-900 @ .45 — quieter than .crypt-btn */
  border: 1px solid var(--g-edge);
  z-index: var(--g-z-hud);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.2s;
}
#crypt-root .g-stick--parked {
  left: calc(var(--g-safe-l) + 3.25rem);
  bottom: calc(var(--g-safe-b) + 3.25rem);
  opacity: 0.4;
}
#crypt-root .g-stick--live {
  opacity: 1;
}
#crypt-root .g-stick__nub {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 2.5rem;
  height: 2.5rem;
  margin: -1.25rem;
  border-radius: 9999px;
  background-color: rgba(
    41,
    37,
    36,
    0.92
  ); /* stone-800 @ .92 — the .crypt-btn hover surface */
  border: 1px solid var(--g-edge-soft);
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.45);
}

/* .g-look-hint — the parked "drag to look" glyph (spaces/freelook.js, touch
   only): an eye flanked by drag chevrons on the right edge, above the
   Use/Torch action cluster (whose --btn-scaled offsets it rides; the fallback
   only matters if a page ever forgot to define --btn). Pure affordance,
   permanently visible at low opacity — the right-half look-drag leaves no
   other trace of its existence — brightening (--live) while a drag is in
   flight. Never a target (pointer-events: none). Desktop never creates the
   node, so these rules cost it nothing. */
#crypt-root .g-look-hint {
  position: absolute;
  right: var(--g-safe-r);
  bottom: calc(var(--g-safe-b) + var(--btn, 3.2rem) * 1.05 + 3.4rem);
  width: 2.25rem;
  height: 2.25rem;
  color: var(--g-ink-mid);
  z-index: var(--g-z-hud);
  pointer-events: none;
  opacity: 0.4;
  transition: opacity 0.2s;
}
#crypt-root .g-look-hint--live {
  opacity: 0.9;
}
#crypt-root .g-look-hint svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* Parked-affordance fades (stick + look glyph) go instant under reduced motion. */
@media (prefers-reduced-motion: reduce) {
  #crypt-root .g-stick,
  #crypt-root .g-look-hint {
    transition: none;
  }
}

/* ---- The persistent exploration HUD (game/hudBar.js, P3) -------------------
 * Game mode only. .g-hud is a full-bleed sheet at HUD depth that catches NO
 * pointer input — only the hero chip, the quick-use tiles and the quest chip
 * opt back in, so world drags/taps pass straight through everywhere else.
 * Everything wears the .g-hud-btn family's quiet surface (stone-950/70 +
 * the soft edge): chrome that whispers over the canvas, not a widget pile. */
#crypt-root .g-hud {
  position: absolute;
  inset: 0;
  z-index: var(--g-z-hud);
  pointer-events: none;
  /* The left column's shared width: the hero chip's width, the minimap's
     square side, and the FPS pill's horizontal offset all resolve from this,
     so the card and the map beneath it line up (defined once). 12rem fits the
     chip's fixed content (2.5rem portrait + 7.5rem info + padding/border) with
     no squish, so the chip's fixed width EQUALS the square map's side exactly. */
  --g-hud-col: 12rem;
}

/* A sign/dialogue panel is up (hudBar `dimmed`): the HUD sits BELOW the overlay (z-hud < z-overlay)
   so the panel's backdrop already darkens it — this just deepens the grey a touch and makes the
   normally-interactive chips (hero card, quick-use tiles, quest chip) inert while the panel owns
   input, so the HP card and minimap read as "still here, inactive" rather than vanishing. */
#crypt-root .g-hud--dimmed {
  opacity: 0.5;
}
#crypt-root .g-hud--dimmed .g-hud-hero,
#crypt-root .g-hud--dimmed .g-hud-quick,
#crypt-root .g-hud--dimmed .g-hud-quick__tile,
#crypt-root .g-hud--dimmed .g-hud-quest {
  pointer-events: none;
}

/* The hero chip, top-left AT the screen edge — it owns the corner; the
   minimap (or the fallback #btn-map, whichever the setting shows) sits
   BELOW it (see .g-hud-map and the scoped #btn-map override). */
#crypt-root .g-hud-hero {
  position: absolute;
  left: 0.75rem;
  top: 0.75rem;
  /* A FIXED width (not min-width) so the card's outer box equals the square minimap beneath it
     exactly, and the FPS pill can sit to its RIGHT at a fixed offset (below). The name truncates
     (see .g-hud-hero__name) so a long class name can't force the card past this width. */
  width: var(--g-hud-col);
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.3125rem 0.625rem 0.3125rem 0.3125rem;
  border-radius: var(--g-radius-lg);
  border: 1px solid var(--g-edge-soft);
  background-color: rgba(
    12,
    10,
    9,
    0.7
  ); /* stone-950/70 — the .g-hud-btn surface */
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition-property: background-color, border-color;
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  transition-duration: 150ms;
}
#crypt-root .g-hud-hero:hover {
  border-color: var(--g-edge);
  background-color: rgba(12, 10, 9, 0.82);
}
#crypt-root .g-hud-hero:active {
  transform: scale(0.97);
}
/* The class portrait (an <img>, or icons.js's letter-chip <span> fallback —
   the font sizing only matters to the letter). */
#crypt-root .g-hud-hero__face {
  width: 2.5rem;
  height: 2.5rem;
  flex-shrink: 0;
  border-radius: var(--g-radius);
  border: 1px solid var(--g-edge-soft);
  background-color: var(--g-surface-2);
  object-fit: cover;
  object-position: center top; /* class art is full-figure — crop to the face */
  font-size: 1.125rem;
}
#crypt-root .g-hud-hero__info {
  width: 7.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
#crypt-root .g-hud-hero__name-row {
  display: flex;
  align-items: baseline;
  gap: 0.375rem;
}
#crypt-root .g-hud-hero__name {
  color: var(--g-ink-hi);
  font-size: 0.8125rem;
  line-height: 1rem;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
#crypt-root .g-hud-hero__level {
  margin-left: auto;
  flex-shrink: 0;
  color: var(--g-ink-low);
  font-size: 0.6875rem;
  line-height: 1rem;
}
/* HP numbers + pips share one line and the card is a FIXED height, so the row
   gives nothing away: min-width:0 lets the pips shrink inside it rather than
   pushing, and the HP readout never wraps or shrinks — "HP 57/57" stays whole
   and on one line whatever the resource beside it is doing. */
#crypt-root .g-hud-hero__vitals {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  min-width: 0;
}
#crypt-root .g-hud-hero__hp-text {
  color: var(--g-ink-low);
  font-size: 0.625rem;
  line-height: 0.875rem;
  white-space: nowrap;
  flex-shrink: 0;
}
/* Class-resource pips: the battle hotbar's ◆/◇ in its resource ink. The one
   thing on the row allowed to give — a long run clips at the card's edge
   instead of wrapping the line under it. */
#crypt-root .g-hud-hero__pips {
  margin-left: auto;
  color: var(--g-resource);
  font-size: 0.625rem;
  line-height: 0.875rem;
  letter-spacing: 0.08em;
  min-width: 0;
  overflow: hidden;
}
/* Past six slots hudBar draws "◆ 7/14" instead of the run: numbers want no
   pip tracking, and they hold their one line. */
#crypt-root .g-hud-hero__pips--num {
  letter-spacing: normal;
  white-space: nowrap;
}
/* Low HP (<25%): the fill goes danger-red. hudBar rebuilds the track's whole
   className (`g-bar g-bar--hp is-low`) rather than toggling classList. */
#crypt-root .g-bar--hp.is-low > .g-bar__fill {
  background-color: var(--g-danger);
}
/* The XP sliver: the semantic xp bar squeezed to a 2px whisper. */
#crypt-root .g-hud-hero .g-bar--xp {
  height: 2px;
  border-radius: 1px;
}
/* The FPS pill's template slot is top-14 left-3: while the HUD exists, move it to the RIGHT
   of the character-stats chip, top-justified with it (the chip's min-width fixes the offset), so
   the two read as one top-left cluster and the space below the chip is free for the minimap. */
#crypt-root:has(.g-hud) #crypt-fps {
  top: 0.75rem;
  left: calc(0.75rem + var(--g-hud-col) + 0.5rem);
}
/* The fallback map button (settings.js swaps it back in when the minimap is
   off) drops BELOW the hero chip while the HUD exists — the chip owns the
   corner. Scoped on .g-hud so the hall/mausoleum/arcanum pages (map button,
   no HUD) keep their template top-3. Matches .g-hud-map's top exactly. */
#crypt-root:has(.g-hud) #btn-map {
  top: calc(0.75rem + 3.4rem + 0.5rem);
}
/* Class-less crypt onramp: the hero chip is hidden (drawChip), so there is no chip lane to clear —
   hudBar.js sets `g-hud--no-hero` on the layer. Reflow the minimap and the fallback map button UP
   into the vacated top-left corner (matching the chip's own top) instead of leaving an empty gap,
   and fixing the "map snaps down after load" glitch (it now settles top-left). Both selectors: the
   minimap lives inside .g-hud (descendant); #btn-map is a sibling of it (:has on the root). */
#crypt-root .g-hud--no-hero .g-hud-map,
#crypt-root:has(.g-hud--no-hero) #btn-map {
  top: 0.75rem;
}

/* Quick-use slots: a short row of small tiles LEFT of the Use button (whose
   width is --btn * 1.2), bottom-aligned with it. pointer-events opt back in
   per tile; 2.75rem = a 44px thumb target on every layout. */
#crypt-root .g-hud-quick {
  position: absolute;
  right: calc(var(--g-safe-r) + var(--btn, 3.2rem) * 1.2 + 0.75rem);
  bottom: var(--g-safe-b);
  display: flex;
  align-items: flex-end;
  gap: 0.375rem;
  pointer-events: none;
}
#crypt-root .g-hud-quick__tile {
  width: 2.75rem;
  height: 2.75rem;
  pointer-events: auto;
  cursor: pointer;
  background-color: rgba(
    12,
    10,
    9,
    0.7
  ); /* quieter than .g-tile's stone-900/70 */
  border-color: var(--g-edge-soft);
  -webkit-tap-highlight-color: transparent;
}
#crypt-root .g-hud-quick__tile:hover:not(:disabled) {
  border-color: var(--g-edge);
}
#crypt-root .g-hud-quick__tile:active {
  transform: scale(0.92);
}
/* The EMPTY belt slot: a hollow, dashed outline holding a faint satchel glyph — reads as "put
   something here", and tapping it opens the bag (hudBar drawQuick -> openSheet). Brightens on hover. */
#crypt-root .g-hud-quick__tile--empty {
  background-color: transparent;
  border-style: dashed;
  border-color: var(--g-edge-soft);
  color: var(--g-ink-low);
  opacity: 0.75;
}
#crypt-root .g-hud-quick__tile--empty:hover:not(:disabled) {
  border-color: var(--g-edge);
  color: var(--g-ink-mid);
  opacity: 1;
}
#crypt-root .g-hud-quick__qty {
  position: absolute;
  bottom: 0.125rem;
  right: 0.25rem;
  font-size: 0.6875rem;
  line-height: 1;
  font-weight: 700;
  color: #fef3c7; /* amber-100 — the inventory's ×N badge ink */
  text-shadow: 0 1px 2px #000;
  pointer-events: none;
}

/* The quest tracker chip, top-right under the settings gear. Collapsed it is
   a lone .g-hud-btn (markup supplies the layout classes); expanded it is this
   compact panel. Width-capped so a long objective wraps instead of crossing
   the toast stack's centre column. */
#crypt-root .g-hud-quest {
  position: absolute;
  right: 0.75rem;
  top: calc(0.75rem + 2.5rem + 0.5rem);
  pointer-events: auto;
  display: flex;
  justify-content: flex-end;
  max-width: min(60%, 17rem);
}
#crypt-root .g-hud-quest__panel {
  border-radius: var(--g-radius);
  border: 1px solid var(--g-edge-soft);
  background-color: rgba(12, 10, 9, 0.72);
  padding: 0.4375rem 0.625rem;
  cursor: pointer;
}
#crypt-root .g-hud-quest__name {
  color: var(--g-gold-ink);
  font-size: 0.6875rem;
  line-height: 1rem;
  letter-spacing: 0.03em;
}
#crypt-root .g-hud-quest__line {
  color: var(--g-ink-mid);
  font-size: 0.75rem;
  line-height: 1.1rem;
  margin-top: 0.125rem;
}

/* Coarse pointers: the top chrome buttons grow to 2.75rem (see .g-hud-btn
   above), so the quest-tracker slot — under the settings gear — drops with
   them. The hero chip is unaffected: nothing sits above it any more. */
@media (pointer: coarse) {
  #crypt-root .g-hud-quest {
    top: calc(0.75rem + 2.75rem + 0.5rem);
  }
  /* The EXPANDED panel drops one lane below the chip row on touch: at phone
     widths the hero chip and a 60%-wide panel share the same top line and
     collide (the first mobile gallery run caught exactly that). Collapsed,
     the lone icon stays up beside the chip. */
  #crypt-root .g-hud-quest--open {
    top: calc(0.75rem + 2.75rem + 0.5rem + 3.125rem + 0.5rem);
  }
  /* One lane below the hero chip (3.125rem tall) rather than beside it: at
     phone widths the beside-the-chip slot runs into the top-centre
     fullscreen button. */
  #crypt-root:has(.g-hud) #crypt-fps {
    top: calc(0.75rem + 3.125rem + 0.5rem);
  }
}
/* Phone layouts nudge the Use button in by 0.25rem (_space_styles.html) —
   the quick row rides the same nudge so the gap to Use stays constant. */
@media (max-width: 767px), (pointer: coarse) and (max-height: 500px) {
  #crypt-root .g-hud-quick {
    right: calc(var(--g-safe-r) + 0.25rem + var(--btn, 3.2rem) * 1.2 + 0.75rem);
    bottom: calc(var(--g-safe-b) + 0.25rem);
  }
}
@media (prefers-reduced-motion: reduce) {
  #crypt-root .g-hud-hero,
  #crypt-root .g-hud-quick__tile {
    transition: none;
    transform: none;
  }
}

/* ---- Ink + badge utilities -------------------------------------------------
 * The D1 house rule says colour comes from `.g-*`, and the panels are full of
 * one-word colour decisions ("this line is quiet", "this number is the gold
 * one"). These are those decisions, named once, so a palette retune is a
 * handful of edits here rather than a grep for text-stone-400 across six
 * modules. Layout (size, weight, spacing) stays Tailwind at the call site. */
#crypt-root .g-ink-hi {
  color: var(--g-ink-hi);
}
#crypt-root .g-ink-mid {
  color: var(--g-ink-mid);
}
#crypt-root .g-ink-low {
  color: var(--g-ink-low);
}
#crypt-root .g-ink-faint {
  color: #78716c; /* stone-500 — the "nothing here" voice */
}
#crypt-root .g-ink-gold {
  color: #fde68a; /* amber-200 — an item / ability name */
}
#crypt-root .g-ink-charge {
  color: var(--g-charge);
}
#crypt-root .g-ink-danger {
  color: var(--g-danger);
}

/* A small uppercase badge riding beside a title (an ability's cost, its
   per-rest crescent). The .g-section-label voice, tighter. */
#crypt-root .g-badge {
  font-size: 0.625rem;
  line-height: 1rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--g-ink-low);
}
#crypt-root .g-badge--charge {
  color: var(--g-charge);
}
#crypt-root .g-badge--gold {
  color: var(--g-gold-ink);
}

/* .g-card — the generic bordered content card the panels list things in
   (ability rows, quest-pouch chips, shop wares). Quieter than .g-panel: this
   sits INSIDE a panel, it is not the panel. */
#crypt-root .g-card {
  border-radius: var(--g-radius);
  border: 1px solid var(--g-surface-3);
  background-color: rgba(41, 37, 36, 0.5); /* stone-800/50 */
  padding: 0.5rem 0.625rem;
}

/* ---- Slot states (inventoryUI paper doll + backpack grid) ------------------
 * `.g-slot` owns the border COLOUR, which means a Tailwind `border-amber-500`
 * at the call site would lose on specificity — so every slot state is a
 * modifier here instead. `--live` is hoverable, `--sel` is the selected tile,
 * `--target` a legal equip destination for the current bag selection, `--empty`
 * a gap in the fixed grid, `--trash` the discard brazier, `--locked` a slot a
 * two-handed weapon has closed. NB the drag highlight writes an INLINE
 * box-shadow (inventoryUI highlight()) — inline always wins, and endDrag clears
 * it back to whatever the class says. */
#crypt-root .g-slot--live {
  cursor: pointer;
}
#crypt-root .g-slot--live:hover {
  border-color: var(--g-gold);
}
#crypt-root .g-slot--sel {
  border-color: var(--g-gold);
  box-shadow: 0 0 0 2px var(--g-gold-bright);
}
#crypt-root .g-slot--target {
  border-color: rgba(245, 158, 11, 0.7);
  box-shadow: 0 0 0 1px rgba(245, 158, 11, 0.4);
}
#crypt-root .g-slot--empty {
  border-color: rgba(41, 37, 36, 0.9); /* stone-800 — a gap, not a tile */
  background-color: rgba(12, 10, 9, 0.45);
}
#crypt-root .g-slot--trash {
  flex-direction: column;
  border-style: dashed;
  border-color: rgba(159, 18, 57, 0.7); /* rose-800/70 */
  color: rgba(251, 113, 133, 0.9); /* rose-400/90 */
}
#crypt-root .g-slot--trash:hover {
  border-color: var(--g-danger);
}
#crypt-root .g-slot--locked {
  opacity: 0.5;
}
/* The word inside an empty slot ("shield", "Discard") and the caption beside a
   doll slot. */
#crypt-root .g-slot__hint {
  font-size: 0.625rem;
  line-height: 1rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: #57534e; /* stone-600 */
}
#crypt-root .g-slot--trash .g-slot__hint {
  color: inherit;
}
#crypt-root .g-slot__caption {
  font-size: 0.6875rem;
  line-height: 1rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--g-ink-low);
}

/* ---- The character sheet (game/sheetUI.js, P4) -----------------------------
 * A D&D-Beyond frame inside the shared overlay shell: a header that never
 * scrolls, a tab strip, and ONE pane scrolling between them.
 *
 * The shell's scroll container normally pads its content and scrolls the lot;
 * for the sheet it becomes a flex column with no padding of its own, so the
 * header can be full-bleed and the pane can own the scrolling. `:has` is the
 * whole mechanism — no JS branches, no second DOM. */
#crypt-root #crypt-scroll-content:has(.g-sheet) {
  display: flex;
  flex-direction: column;
  padding: 0;
  /* FILL the shell rather than sizing to the sheet. Without this the scroll
     container is content-sized, the body never has a height to scroll against,
     and the bottom-pinned mobile tab strip gets pushed off the screen by a
     long backpack (the first mobile gallery run caught exactly that). */
  flex: 1 1 auto;
  min-height: 0;
}
#crypt-root .g-sheet {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

/* The persistent header. The right padding clears the shell's absolute close
   chip (top-3 right-3), which floats above everything in here. */
#crypt-root .g-sheet__head {
  flex-shrink: 0;
  order: 1;
  padding: 0.875rem 3rem 0.75rem 1rem;
  border-bottom: 1px solid var(--g-edge-soft);
  background-color: rgba(12, 10, 9, 0.45);
}
#crypt-root .g-sheet__face {
  width: 3.25rem;
  height: 3.25rem;
  flex-shrink: 0;
  border-radius: var(--g-radius);
  border: 1px solid var(--g-edge-soft);
  background-color: var(--g-surface-2);
  object-fit: cover;
  object-position: center top; /* class art is full-figure — crop to the face */
  font-size: 1.5rem;
}
#crypt-root .g-sheet__name {
  color: var(--g-gold-bright);
  font-size: 1.25rem;
  line-height: 1.75rem;
}
#crypt-root .g-sheet__sub {
  color: var(--g-ink-low);
  font-size: 0.8125rem;
  line-height: 1.125rem;
}
/* The pending level-up is the one thing on this screen that should catch your
   eye without being read — a slow ring, opacity/box-shadow only. */
#crypt-root .g-sheet__levelup {
  animation: g-levelup-pulse 2.4s ease-in-out infinite;
}
@keyframes g-levelup-pulse {
  50% {
    box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.35);
  }
}
@media (prefers-reduced-motion: reduce) {
  #crypt-root .g-sheet__levelup {
    animation: none;
  }
}

/* The vitals row: HP / AC / Attack / Gold / the class resource, always on
   screen. Auto-fit so four pills spread on a phone and five sit in one line on
   a desktop card. */
#crypt-root .g-sheet__pills {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(5.25rem, 1fr));
  gap: 0.375rem;
  margin-top: 0.625rem;
}
#crypt-root .g-pill {
  border-radius: var(--g-radius);
  border: 1px solid var(--g-surface-3);
  background-color: rgba(41, 37, 36, 0.6); /* the .g-stat surface */
  padding: 0.25rem 0.5rem 0.3125rem;
  min-width: 0;
}
#crypt-root .g-pill__label {
  display: block;
  font-size: 0.5625rem;
  line-height: 0.875rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--g-ink-low);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
#crypt-root .g-pill__value {
  display: block;
  font-size: 0.9375rem;
  line-height: 1.25rem;
  font-weight: 600;
  color: #fde68a; /* amber-200 */
}
#crypt-root .g-pill .g-bar {
  height: 3px;
  border-radius: 2px;
  margin-top: 0.1875rem;
}

/* The tab strip: under the header on a desktop card; pinned to the BOTTOM of
   the sheet on a phone, where the thumb is (see the media block below). */
#crypt-root .g-sheet__tabs {
  flex-shrink: 0;
  order: 2;
  display: flex;
  gap: 0.25rem;
  padding: 0.375rem 0.75rem;
  border-bottom: 1px solid var(--g-edge-soft);
  overflow-x: auto;
}

/* The one mounted pane. This is the ONLY scroller inside the sheet. */
#crypt-root .g-sheet__body {
  order: 3;
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 1rem;
}
@media (min-width: 640px) {
  #crypt-root .g-sheet__body {
    padding: 1.25rem 1.5rem;
  }
}
#crypt-root .g-sheet__empty {
  color: #78716c;
  font-size: 0.875rem;
  padding: 1.5rem 0;
  text-align: center;
}
#crypt-root .g-sheet__section {
  font-family: Cinzel, serif;
  font-size: 1rem;
  line-height: 1.5rem;
  color: var(--g-gold-ink);
  margin-top: 1rem;
  margin-bottom: 0.375rem;
}
#crypt-root .g-sheet__blurb {
  color: var(--g-ink-mid);
  font-size: 0.875rem;
  line-height: 1.35rem;
}

/* Desktop: the sheet earns the class picker's elbow room AND a stable height,
   so switching tabs never resizes the card under the cursor. Media-gated to
   real desktop geometry so it can never outrank P1's full-height mobile sheet
   (whose `max-width: none` rule is one specificity step lower). */
@media (min-width: 768px) and (min-height: 501px) {
  #crypt-root #crypt-scroll-shell.g-shell--wide {
    max-width: 52rem;
    height: 100%;
    max-height: 44rem;
  }
}

/* Phones (and landscape phones): the strip moves to the bottom edge, tabs
   share the width evenly, and the safe-area inset keeps them clear of the home
   indicator. Same DOM — only `order` and which side wears the border change. */
@media (max-width: 767px), (pointer: coarse) and (max-height: 500px) {
  #crypt-root .g-sheet__tabs {
    order: 4;
    border-bottom: 0;
    border-top: 1px solid var(--g-edge-soft);
    background-color: rgba(12, 10, 9, 0.55);
    padding-bottom: calc(0.375rem + env(safe-area-inset-bottom, 0px));
  }
  #crypt-root .g-sheet__tabs .g-tab {
    flex: 1 1 0;
    padding-left: 0.25rem;
    padding-right: 0.25rem;
    text-align: center;
  }
  #crypt-root .g-sheet__head {
    padding-top: 0.75rem;
  }
}

/* LANDSCAPE PHONES ONLY — the HEIGHT budget, which is a different problem from
   the phone LAYOUT one above (see docs/crypt_engine/game-ui.md §5).
 *
 * At 812×375 the sheet gets about 243px of shell. The header spent ~138 of it, the
 * drag handle 40 and the tab strip ~45 — leaving the pane, the only part with
 * anything to read in it, a ~20px scrollport. Every rule here buys height back from
 * the chrome and gives it to the body; nothing is hidden, because a character sheet
 * that omits a number is worse than one you have to scroll.
 *
 * Placed AFTER the `min-width: 640px` body-padding rule on purpose: same
 * specificity, so source order is what decides, and a landscape phone matches BOTH.
 * Portrait phones are untouched — they have the height and want the bigger targets. */
@media (pointer: coarse) and (max-height: 500px) {
  #crypt-root .g-sheet__head {
    /* right padding still clears the shell's absolute close chip */
    padding: 0.5rem 2.5rem 0.375rem 0.75rem;
  }
  #crypt-root .g-sheet__face {
    width: 2rem;
    height: 2rem;
    font-size: 1rem; /* the letter-chip fallback, sized to the smaller box */
  }
  #crypt-root .g-sheet__name {
    font-size: 1rem;
    line-height: 1.25rem;
  }
  #crypt-root .g-sheet__sub {
    font-size: 0.6875rem;
    line-height: 1rem;
  }
  /* The vitals: ONE row, never two. Auto-fit would wrap the fifth pill onto a
     second line and spend 40px doing it — and the row is wide here, not tall. */
  #crypt-root .g-sheet__pills {
    display: flex;
    flex-wrap: nowrap;
    gap: 0.25rem;
    margin-top: 0.375rem;
  }
  #crypt-root .g-sheet__pills .g-pill {
    flex: 1 1 0;
    padding: 0.125rem 0.375rem 0.1875rem;
  }
  #crypt-root .g-pill__label {
    font-size: 0.5rem;
    line-height: 0.75rem;
  }
  #crypt-root .g-pill__value {
    font-size: 0.8125rem;
    line-height: 1.125rem;
  }
  /* The bottom tab strip keeps its safe-area inset — a landscape phone still has a
     home indicator down there — but gives up everything else. */
  #crypt-root .g-sheet__tabs {
    padding-top: 0.1875rem;
    padding-bottom: calc(0.1875rem + env(safe-area-inset-bottom, 0px));
  }
  #crypt-root .g-sheet__tabs .g-tab {
    padding: 0.25rem;
    font-size: 0.6875rem;
    line-height: 1rem;
  }
  #crypt-root .g-sheet__body {
    padding: 0.5rem 0.75rem;
  }
  /* An empty pane's message does not need an inch of air around it. */
  #crypt-root .g-sheet__empty {
    padding: 0.625rem 0;
  }
  #crypt-root .g-sheet__section {
    font-size: 0.875rem;
    line-height: 1.25rem;
    margin-top: 0.625rem;
  }
}

/* ---- Stats pane -----------------------------------------------------------
 * Ability scores as D&D-Beyond-ish blocks: the score large, the modifier on its
 * own line under it (which is the number you actually roll with). */
#crypt-root .g-score {
  border-radius: var(--g-radius);
  border: 1px solid var(--g-surface-3);
  background-color: rgba(41, 37, 36, 0.6);
  padding: 0.375rem 0.25rem 0.4375rem;
}
#crypt-root .g-score__label {
  font-size: 0.625rem;
  line-height: 0.875rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--g-ink-low);
}
#crypt-root .g-score__value {
  font-size: 1.25rem;
  line-height: 1.625rem;
  font-weight: 600;
  color: #fde68a;
}
#crypt-root .g-score__mod {
  font-size: 0.75rem;
  line-height: 1rem;
  color: var(--g-ink-low);
}

/* ---- Inventory pane: the detail bar under the grid (inventoryUI detailBar) -- */
#crypt-root .g-detail {
  border-radius: var(--g-radius);
  border: 1px solid var(--g-surface-3);
  background-color: rgba(41, 37, 36, 0.5);
  padding: 0.5rem 0.75rem;
}

/* The paper doll on a LANDSCAPE PHONE. Its layout is Tailwind's (`sm:` — a viewport
   640px or wider gets the tall one-column doll with a caption beside each slot), and
   a landscape phone is 812px wide, so it trips that gate while having none of the
   height it assumes: three tall rows of slot-plus-caption push the backpack off the
   screen entirely.
 *
 * This is the portrait layout put back — three across, caption under the slot — under
 * the height gate alone. `#crypt-root .g-inv-doll` scores (1,1,0) against a Tailwind
 * utility's (0,1,0), so it wins regardless of source order; the hook classes are added
 * in game/inventoryUI.js and carry nothing else. */
@media (pointer: coarse) and (max-height: 500px) {
  #crypt-root .g-inv-doll {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 0.375rem;
  }
  #crypt-root .g-inv-doll__row {
    flex-direction: column;
    gap: 0.25rem;
    text-align: center;
  }
  #crypt-root .g-inv-doll__cap {
    width: auto;
    text-align: center;
  }
  /* …and the two halves stack no longer: the doll sits beside the backpack, which is
     what the width is actually for. */
  #crypt-root .g-inv-top {
    gap: 0.75rem;
    margin-bottom: 0.5rem;
  }
}

/* ---- Features pane ---------------------------------------------------------
 * Pros/cons chips are the same verdicts the creation screen showed on the card
 * this character was picked from — the sheet must never contradict it. */
#crypt-root .g-chip {
  border-radius: 9999px;
  border: 1px solid var(--g-edge-soft);
  padding: 0.0625rem 0.5rem;
  font-size: 0.6875rem;
  line-height: 1.125rem;
  color: var(--g-ink-mid);
}
#crypt-root .g-chip--pro {
  border-color: rgba(52, 211, 153, 0.4);
  color: var(--g-heal);
}
#crypt-root .g-chip--con {
  border-color: rgba(248, 113, 113, 0.4);
  color: var(--g-danger);
}
/* A granted ability inside a level row; dimmed when that level is not reached. */
#crypt-root .g-feat-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  max-width: 100%;
  border-radius: var(--g-radius);
  border: 1px solid var(--g-surface-3);
  background-color: rgba(28, 25, 23, 0.7);
  padding: 0.125rem 0.5rem 0.125rem 0.25rem;
  font-size: 0.75rem;
  line-height: 1.25rem;
  color: #fde68a;
}
#crypt-root .g-feat-chip.is-locked {
  opacity: 0.45;
}
/* One row of the class ladder (or one taken feat). */
#crypt-root .g-feature {
  border-radius: var(--g-radius);
  border: 1px solid var(--g-surface-3);
  background-color: rgba(41, 37, 36, 0.4);
  padding: 0.4375rem 0.625rem;
}
#crypt-root .g-feature.is-locked {
  opacity: 0.55;
}
/* A feat row carries its icon — the same one the level-up card offered it under, so the
   picture you chose by is the picture on your sheet. The class-progression rows keep the
   plain block layout, which is why this is a modifier rather than a change to .g-feature. */
#crypt-root .g-feature--icon {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
}
#crypt-root .g-feature__name {
  display: block;
  color: #fde68a;
  font-size: 0.875rem;
  font-weight: 600;
}
#crypt-root .g-feature__level {
  font-family: Cinzel, serif;
  font-size: 0.8125rem;
  color: var(--g-gold-ink);
}
#crypt-root .g-feature__gain {
  font-size: 0.6875rem;
  color: var(--g-heal);
}
#crypt-root .g-feature__lock {
  margin-left: auto;
  font-size: 0.625rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--g-ink-low);
}
#crypt-root .g-feature__text {
  display: block;
  font-size: 0.6875rem;
  line-height: 1rem;
  color: var(--g-ink-low);
  margin-top: 0.125rem;
}
/* What a feat GAVE you — "CON +1 · Second Wind". In the resource ink rather than
   the flavour text's low ink, because it is the mechanical line: the row above
   says why the feat exists, this says what it did to your character. */
#crypt-root .g-feature__effect {
  display: block;
  font-size: 0.6875rem;
  line-height: 1rem;
  color: var(--g-resource);
  margin-top: 0.125rem;
}

/* ---- The vendor panel (inventoryUI drawShop, P4) ---------------------------
 * A merchant, not a debug menu: a name plate carrying the two numbers a trade
 * turns on (your purse, their rate), then two stalls of item cards. No sheet
 * header and no tabs — this is a conversation with a shopkeeper, and BOTH
 * stalls stay on screen so a stockless fence still obviously buys. */
#crypt-root .g-vendor__plate {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  border-radius: var(--g-radius-lg);
  border: 1px solid var(--g-edge-strong);
  background-color: rgba(41, 37, 36, 0.45);
  padding: 0.625rem 0.875rem;
}
/* The shell's close chip floats over the panel's top-right corner (top-3
   right-3), and on a phone the shop panel starts right under it — so the purse
   gets out of its way. Desktop's roomier padding already clears it. */
@media (max-width: 767px), (pointer: coarse) and (max-height: 500px) {
  #crypt-root .g-vendor__plate {
    padding-right: 2.75rem;
  }
}
#crypt-root .g-vendor__name {
  color: var(--g-gold-bright);
  font-size: 1.25rem;
  line-height: 1.75rem;
}
#crypt-root .g-vendor__rate {
  color: var(--g-ink-low);
  font-size: 0.75rem;
  line-height: 1.125rem;
}
#crypt-root .g-vendor__purse {
  flex-shrink: 0;
  text-align: right;
  border-left: 1px solid var(--g-edge-soft);
  padding-left: 0.75rem;
}
#crypt-root .g-vendor__gold {
  display: block;
  color: var(--g-gold-bright);
  font-size: 1.375rem;
  line-height: 1.75rem;
  font-weight: 700;
}
/* One ware. The tile keeps the icon square whatever the card's height. */
#crypt-root .g-ware__tile {
  background-color: rgba(12, 10, 9, 0.6);
}
#crypt-root .g-price {
  font-size: 0.6875rem;
  line-height: 1rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  color: var(--g-gold-ink);
}
/* The buy row's class advisory ("your class can't use shields"). Amber, not danger red: this is
   caution, not a refusal — the Buy button beside it still works. Wraps rather than truncating
   like the lines above it, because half a sentence about your class is worse than none. */
#crypt-root .g-shop__warn {
  display: block;
  margin-top: 0.125rem;
  font-size: 0.6875rem;
  line-height: 1rem;
  color: var(--g-gold);
}

/* ---- The level-up ceremony (game/levelUpUI.js, P6) -------------------------
 * Levelling is the one moment the game hands something BACK, and it used to be
 * a form: a title, a list, two buttons. It is now a three-band screen in the
 * character sheet's shape — a header that states the occasion and never
 * scrolls, the choices scrolling between, and Confirm pinned where the thumb
 * already is. `:has` does the whole thing (exactly as `:has(.g-sheet)`): no JS
 * branch, no second DOM, and the mobile full-height sheet is inherited for free.
 *
 * Every animation here is opacity/transform only, one-shot, and off entirely
 * under prefers-reduced-motion — a celebration should never be a GPU event. */
#crypt-root #crypt-scroll-content:has(.g-levelup) {
  display: flex;
  flex-direction: column;
  padding: 0;
  flex: 1 1 auto;
  min-height: 0;
}
#crypt-root .g-levelup {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

/* The header. Right padding clears the shell's absolute close chip; the warm
   bloom behind it is the only place in the overlays that glows gold. */
#crypt-root .g-levelup__head {
  flex-shrink: 0;
  padding: 1.125rem 3rem 0.875rem;
  text-align: center;
  border-bottom: 1px solid var(--g-edge-soft);
  background-image: radial-gradient(
    ellipse at 50% 0%,
    rgba(245, 158, 11, 0.16) 0%,
    rgba(245, 158, 11, 0) 72%
  );
}
/* The hero's face crowning the ceremony (icons.js classPortrait — an <img>, or its
   letter-chip <span> fallback). Centred: the <img> is inline so the head's
   text-align does it; the chip is a block-level flex box so margin-auto does. Mirrors
   the character sheet's face box. */
#crypt-root .g-levelup__face {
  width: 3.25rem;
  height: 3.25rem;
  margin: 0 auto 0.5rem;
  border-radius: var(--g-radius);
  border: 1px solid var(--g-edge-soft);
  background-color: var(--g-surface-2);
  object-fit: cover;
  object-position: center top; /* class art is full-figure — crop to the face */
  font-size: 1.5rem;
}
/* The flourish: a hairline rule that thickens to gold at its centre, with a
   struck diamond on top. Drawn entirely in CSS — no asset, no SVG. */
#crypt-root .g-levelup__flourish {
  position: relative;
  height: 0.75rem;
  max-width: 13rem;
  margin: 0 auto 0.375rem;
  background-image: linear-gradient(
    to right,
    rgba(120, 53, 15, 0) 0%,
    var(--g-gold-dim) 22%,
    var(--g-gold) 50%,
    var(--g-gold-dim) 78%,
    rgba(120, 53, 15, 0) 100%
  );
  background-size: 100% 1px;
  background-position: 0 50%;
  background-repeat: no-repeat;
  animation: g-flourish-in 700ms cubic-bezier(0.2, 0.8, 0.3, 1) both;
}
#crypt-root .g-levelup__flourish::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 0.5rem;
  height: 0.5rem;
  margin: -0.25rem;
  transform: rotate(45deg);
  background-color: var(--g-gold-bright);
  box-shadow: 0 0 10px rgba(245, 158, 11, 0.7);
}
#crypt-root .g-levelup__title {
  font-size: 1.375rem;
  line-height: 1.9rem;
  letter-spacing: 0.03em;
  color: var(--g-gold-bright);
  text-shadow: 0 0 20px rgba(245, 158, 11, 0.35);
  animation: g-levelup-in 520ms cubic-bezier(0.2, 0.8, 0.3, 1) both;
}
@media (min-width: 640px) {
  #crypt-root .g-levelup__title {
    font-size: 1.625rem;
    line-height: 2.25rem;
  }
}
#crypt-root .g-levelup__sub {
  font-size: 0.6875rem;
  line-height: 1rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--g-ink-low);
}
#crypt-root .g-levelup__gains {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.375rem 0.625rem;
  margin-top: 0.625rem;
}
/* What the level already PAID OUT (the HP is applied the moment the XP lands —
   this is the receipt, not an offer). Green, because it is a gain. */
#crypt-root .g-delta {
  border-radius: 9999px;
  border: 1px solid var(--g-edge-soft);
  padding: 0.0625rem 0.625rem;
  font-size: 0.75rem;
  line-height: 1.25rem;
  font-weight: 700;
  color: var(--g-ink-mid);
}
#crypt-root .g-delta--hp {
  border-color: rgba(52, 211, 153, 0.45);
  color: var(--g-heal);
}
/* The class pool (Stamina / Spell Slots / Rage…) — the resource blue the HUD and the
   hotbar already spend in. */
#crypt-root .g-delta--slots {
  border-color: rgba(125, 211, 252, 0.45);
  color: var(--g-resource);
}
/* The derived gains the game used to keep to itself: the proficiency curve, and
   Extra Attack — the one that changes how a turn is PLAYED, so it takes the gold. */
#crypt-root .g-delta--prof {
  border-color: rgba(196, 181, 253, 0.45);
  color: var(--g-charge);
}
#crypt-root .g-delta--attack {
  border-color: var(--g-gold);
  color: var(--g-gold-ink);
  box-shadow: 0 0 0 1px rgba(245, 158, 11, 0.25);
}
#crypt-root .g-levelup__step {
  font-size: 0.6875rem;
  line-height: 1.25rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--g-ink-low);
}
@keyframes g-flourish-in {
  from {
    opacity: 0;
    transform: scaleX(0.35);
  }
}
@keyframes g-levelup-in {
  from {
    opacity: 0;
    transform: translateY(-0.375rem);
  }
}
@media (prefers-reduced-motion: reduce) {
  #crypt-root .g-levelup__flourish,
  #crypt-root .g-levelup__title {
    animation: none;
  }
}

/* The scrolling middle: the ONLY scroller in the ceremony. */
#crypt-root .g-levelup__body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 1rem;
}
@media (min-width: 640px) {
  #crypt-root .g-levelup__body {
    padding: 1.25rem 1.5rem;
  }
}
/* On a phone the sheet is full-height, so two cards would float at the top of a
   screen of nothing. Centre them — `safe` so a list TALLER than the scroller
   still starts at the top and stays reachable (and browsers that don't know the
   keyword drop the declaration and land on exactly that behaviour anyway). */
@media (max-width: 767px), (pointer: coarse) and (max-height: 500px) {
  #crypt-root .g-levelup__body {
    display: flex;
    flex-direction: column;
    justify-content: safe center;
  }
}
#crypt-root .g-levelup__prompt {
  text-align: center;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--g-ink-mid);
  margin-bottom: 0.75rem;
}
#crypt-root .g-levelup__remaining {
  text-align: center;
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 600;
  color: var(--g-gold-ink);
  margin-bottom: 0.75rem;
}

/* One offered card — an ability or a feat, same anatomy either way. Selection
   is a gold RING (box-shadow, so nothing shifts) plus a gold rim. */
#crypt-root .g-lvl-card {
  display: flex;
  align-items: flex-start;
  gap: 0.625rem;
  cursor: pointer;
  transition-property: border-color, box-shadow, background-color;
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  transition-duration: 150ms;
}
#crypt-root .g-lvl-card:hover {
  border-color: var(--g-gold);
}
#crypt-root .g-lvl-card.is-sel {
  border-color: var(--g-gold);
  box-shadow: 0 0 0 2px var(--g-gold-bright);
  background-color: rgba(120, 53, 15, 0.28);
}
/* A card the level HANDED you (the gains page): gold like a made choice, but with
   nothing to click — the decision was the class's. */
#crypt-root .g-lvl-card.is-granted {
  border-color: var(--g-gold);
  cursor: default;
}
#crypt-root .g-lvl-card.is-granted:hover {
  border-color: var(--g-gold);
}
#crypt-root .g-lvl-card.is-taken {
  opacity: 0.45;
  cursor: default;
}
#crypt-root .g-lvl-card.is-taken:hover {
  border-color: var(--g-surface-3);
}
#crypt-root .g-lvl-card__name {
  display: block;
  color: #fde68a; /* amber-200 */
  font-weight: 600;
}
#crypt-root .g-lvl-card__text {
  display: block;
  font-size: 0.6875rem;
  line-height: 1rem;
  color: var(--g-ink-low);
}
/* What it actually GIVES — the line a feat cannot be chosen without. */
#crypt-root .g-lvl-card__effect {
  display: block;
  margin-top: 0.125rem;
  font-size: 0.6875rem;
  line-height: 1rem;
  color: var(--g-resource);
}
#crypt-root .g-lvl-card__taken {
  display: block;
  margin-top: 0.125rem;
  font-size: 0.6875rem;
  line-height: 1rem;
  color: #78716c; /* stone-500 */
}
/* Why a card cannot be taken — a feat whose bump would pass the cap. Amber, the same
   caution ink the shop's class advisory uses (.g-shop__warn): "Already taken." above is
   a fact about your own history, this is the rule standing between you and a choice. */
#crypt-root .g-lvl-card__blocked {
  display: block;
  margin-top: 0.125rem;
  font-size: 0.6875rem;
  line-height: 1rem;
  color: var(--g-gold);
}

/* ASI rows: label, the score with its modifier under it, and two steppers big
   enough to hit. */
#crypt-root .g-asi {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  border-radius: var(--g-radius);
  border: 1px solid var(--g-surface-3);
  background-color: rgba(41, 37, 36, 0.6);
  padding: 0.375rem 0.5rem;
}
#crypt-root .g-asi.is-raised {
  border-color: var(--g-gold-dim);
}
#crypt-root .g-asi__label {
  width: 2.5rem;
  flex-shrink: 0;
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--g-ink-low);
}
#crypt-root .g-asi__value {
  display: block;
  font-size: 0.9375rem;
  line-height: 1.25rem;
  font-weight: 600;
  color: #fde68a;
}
#crypt-root .g-asi__mod {
  display: block;
  font-size: 0.625rem;
  line-height: 0.875rem;
  color: var(--g-ink-low);
}
#crypt-root .g-asi__mod.is-up {
  color: var(--g-heal);
}
/* The steppers. `.g-btn--sm` gives them the kit's small face; this squares them
   up to a real target (44px on a coarse pointer) and centres the glyph. It must
   follow .g-btn--sm in the file — same specificity, later wins. */
#crypt-root .g-step {
  flex-shrink: 0;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.125rem;
  line-height: 1;
  border-radius: var(--g-radius);
}
@media (pointer: coarse) {
  #crypt-root .g-step {
    width: 2.75rem;
    height: 2.75rem;
  }
}

/* The footer: Confirm where the thumb is, "Decide later" quiet beside it, and
   the deferral promise underneath. Never scrolls. */
#crypt-root .g-levelup__foot {
  flex-shrink: 0;
  padding: 0.75rem 1rem calc(0.75rem + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--g-edge-soft);
  background-color: rgba(12, 10, 9, 0.55);
  text-align: center;
}
#crypt-root .g-levelup__confirm {
  min-width: 9rem;
}
#crypt-root .g-levelup__hint {
  margin-top: 0.5rem;
  font-size: 0.6875rem;
  line-height: 1rem;
  color: #78716c;
}

/* The ceremony on a LANDSCAPE PHONE — the character sheet's height problem, and the
   same answer. The header is the whole occasion here (portrait, flourish, title, the
   gains chips), so it is trimmed rather than cut: the ceremony must still READ as one,
   or the level-up stops feeling like a reward.
 *
 * After the `min-width: 640px` title and body rules on purpose — a landscape phone
 * matches both, and at equal specificity the later block is the one that lands. */
@media (pointer: coarse) and (max-height: 500px) {
  #crypt-root .g-levelup__head {
    padding: 0.5rem 2.5rem 0.5rem;
  }
  #crypt-root .g-levelup__face {
    width: 2rem;
    height: 2rem;
    margin-bottom: 0.25rem;
    font-size: 1rem;
  }
  #crypt-root .g-levelup__flourish {
    height: 0.5rem;
    margin-bottom: 0.25rem;
  }
  #crypt-root .g-levelup__title {
    font-size: 1.125rem;
    line-height: 1.5rem;
  }
  #crypt-root .g-levelup__sub {
    font-size: 0.625rem;
    line-height: 0.875rem;
  }
  /* The gains chips are the receipt — one wrapped row of them, tighter. */
  #crypt-root .g-levelup__gains {
    gap: 0.25rem 0.375rem;
    margin-top: 0.375rem;
  }
  #crypt-root .g-delta {
    padding: 0 0.5rem;
    font-size: 0.6875rem;
    line-height: 1.125rem;
  }
  #crypt-root .g-levelup__step {
    font-size: 0.625rem;
    line-height: 1rem;
  }
  #crypt-root .g-levelup__body {
    padding: 0.5rem 0.75rem;
  }
  #crypt-root .g-levelup__prompt,
  #crypt-root .g-levelup__remaining {
    font-size: 0.8125rem;
    line-height: 1.125rem;
    margin-bottom: 0.5rem;
  }
  #crypt-root .g-levelup__foot {
    padding: 0.375rem 0.75rem calc(0.375rem + env(safe-area-inset-bottom, 0px));
  }
  #crypt-root .g-levelup__hint {
    margin-top: 0.25rem;
    font-size: 0.625rem;
    line-height: 0.875rem;
  }
}

/* ---- The creation screen (game/classPickerUI.js, P6) -----------------------
 * Two panes on a desktop; PAGED on a phone. The paging is `data-create-page` on
 * the root plus the rules at the bottom of this section — the JS sets the
 * attribute unconditionally and the desktop simply never reads it. Both pages
 * keep every seam mounted (the roster cards, the name field and Begin), so the
 * same click path drives the screen at either size. */
#crypt-root #crypt-scroll-content:has(.g-create) {
  display: flex;
  flex-direction: column;
  padding: 0;
  flex: 1 1 auto;
  min-height: 0;
}
#crypt-root .g-create {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}
#crypt-root .g-create__head {
  flex-shrink: 0;
  padding: 1rem 3rem 0.75rem;
  text-align: center;
  border-bottom: 1px solid var(--g-edge-soft);
}
#crypt-root .g-create__title {
  font-size: 1.375rem;
  line-height: 1.9rem;
  color: var(--g-gold-bright);
}
#crypt-root .g-create__sub {
  font-size: 0.8125rem;
  line-height: 1.125rem;
  color: var(--g-ink-low);
}

/* The panes. Each side scrolls on its own: a class with nine level grants makes
   the detail taller than the modal by itself. */
#crypt-root .g-create__panes {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  grid-template-columns: minmax(0, 20rem) minmax(0, 1fr);
}
#crypt-root .g-create__roster {
  min-height: 0;
  overflow-y: auto;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-content: start;
  gap: 0.5rem;
  padding: 0.875rem;
  border-right: 1px solid var(--g-edge-soft);
}
#crypt-root .g-create__roster--wide {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
#crypt-root .g-create__detail {
  min-height: 0;
  overflow-y: auto;
  padding: 1rem 1.125rem 1.25rem;
}
#crypt-root .g-create__empty {
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: #78716c;
}

/* One roster card. `.game-class-card` (the long-standing test seam) stays on the
   element; the look is all here. */
#crypt-root .g-class-card {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  width: 100%;
  cursor: pointer;
  transition-property: border-color, box-shadow, background-color;
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  transition-duration: 150ms;
  -webkit-tap-highlight-color: transparent;
}
#crypt-root .g-class-card:hover {
  border-color: var(--g-gold);
}
#crypt-root .g-class-card.is-sel {
  border-color: var(--g-gold);
  box-shadow: 0 0 0 2px var(--g-gold-bright);
  background-color: rgba(120, 53, 15, 0.28);
}
#crypt-root .g-class-card__face {
  width: 2.75rem;
  height: 2.75rem;
  flex-shrink: 0;
  border-radius: var(--g-radius);
  border: 1px solid var(--g-edge-soft);
  background-color: var(--g-surface-2);
  object-fit: cover;
  object-position: center top; /* class art is full-figure — crop to the face */
  font-size: 1.125rem;
}
#crypt-root .g-class-card__name {
  display: block;
  color: var(--g-gold-ink);
  font-size: 0.9375rem;
  line-height: 1.375rem;
}
#crypt-root .g-class-card__stats {
  display: block;
  font-size: 0.6875rem;
  line-height: 1rem;
  color: var(--g-ink-low);
}
/* "…and there is a page behind this card". Phones only — on a desktop the
   detail is already open beside the roster. */
#crypt-root .g-class-card__more {
  display: none;
  flex-shrink: 0;
  color: var(--g-ink-low);
  font-size: 1.125rem;
  line-height: 1;
}

/* The detail pane's own furniture. */
#crypt-root .g-create__portrait {
  width: 6rem;
  height: 6rem;
  flex-shrink: 0;
  border-radius: var(--g-radius);
  border: 1px solid var(--g-edge-soft);
  background-color: var(--g-surface-2);
  object-fit: cover;
  object-position: center top;
  font-size: 2rem;
}
/* The portrait is click-to-upload: a positioning context for the red-X, a pointer to
   say so, and a soft gold ring on hover to advertise it. */
#crypt-root .g-create__portrait-wrap {
  position: relative;
  flex-shrink: 0;
  align-self: flex-start;
  cursor: pointer;
  line-height: 0;
}
#crypt-root .g-create__portrait-wrap:hover .g-create__portrait {
  border-color: var(--g-gold);
}
/* The click-to-upload discovery cue, stacked under the portrait while it still wears
   the class default. The wrap sets line-height:0 to kill the img's whitespace, so the
   caption must restore its own line-height or it collapses to nothing. Small + muted,
   matching the creation captions; hidden on touch in the mobile block below. */
#crypt-root .g-create__portrait-hint {
  display: block;
  margin-top: 0.375rem;
  text-align: center;
  font-size: 0.6875rem;
  line-height: 0.9rem;
  color: var(--g-ink-low);
}
/* Clear a player-uploaded face, reverting to the class default. */
#crypt-root .g-create__portrait-x {
  position: absolute;
  top: -0.4rem;
  right: -0.4rem;
  width: 1.5rem;
  height: 1.5rem;
  min-width: 0;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 9999px;
  border: 1px solid rgba(0, 0, 0, 0.35);
  background-color: #b91c1c; /* red-700 */
  color: #fff;
  font-size: 1rem;
  line-height: 1;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
#crypt-root
  .g-create__portrait-x:hover:not(:disabled):not([aria-disabled="true"]) {
  background-color: #dc2626; /* red-600 */
}
#crypt-root .g-create__class-name {
  font-size: 1.25rem;
  line-height: 1.75rem;
  color: var(--g-gold-bright);
}
#crypt-root .g-create__blurb {
  margin-top: 0.25rem;
  font-size: 0.875rem;
  line-height: 1.3rem;
  color: var(--g-ink-mid);
}
#crypt-root .g-create__pills {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(6rem, 1fr));
  gap: 0.375rem;
  margin-top: 0.875rem;
}
#crypt-root .g-create__scores {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.375rem;
  margin-top: 0.5rem;
}
@media (min-width: 640px) {
  #crypt-root .g-create__scores {
    grid-template-columns: repeat(6, minmax(0, 1fr));
  }
}

/* Class progression is COLLAPSIBLE, collapsed by default — the tall level ladder is what
   forced the creator to scroll. The toggle wears the section-heading voice; the caret
   rotates open; the body only takes space (and only lets the pane scroll to it) once
   `.is-open` is set. Rows stay in the DOM when closed, hidden here, so nothing that reads
   the ladder loses it. */
#crypt-root .g-create__progression {
  margin-top: 1rem;
}
#crypt-root .g-create__progression-toggle {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  width: 100%;
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
#crypt-root .g-create__progression-title {
  font-family: Cinzel, serif;
  font-size: 1rem;
  line-height: 1.5rem;
  color: var(--g-gold-ink);
}
#crypt-root .g-create__progression-toggle:hover .g-create__progression-title {
  color: var(--g-gold-bright);
}
#crypt-root .g-create__progression-caret {
  color: var(--g-ink-low);
  font-size: 0.75rem;
  line-height: 1;
  transition: transform 150ms ease;
}
#crypt-root .g-create__progression.is-open .g-create__progression-caret {
  transform: rotate(90deg);
}
/* Closed: the ladder takes no space (id + 2 classes outweighs Tailwind's .flex). */
#crypt-root .g-create__progression:not(.is-open) .g-create__progression-body {
  display: none;
}
#crypt-root .g-create__progression.is-open .g-create__progression-body {
  margin-top: 0.5rem;
}

/* The commit bar: name + Begin, with Back joining it on a phone's detail page. */
#crypt-root .g-create__foot {
  flex-shrink: 0;
  display: flex;
  align-items: flex-end;
  gap: 0.625rem;
  padding: 0.75rem 1rem calc(0.75rem + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--g-edge-soft);
  background-color: rgba(12, 10, 9, 0.55);
}
/* The name group no longer holds the dice — it is a bounded field so the row has
   space for the Randomise button and Begin beside it, shrinking rather than eating
   the bar. */
#crypt-root .g-create__name {
  flex: 0 1 20rem;
  min-width: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 0.375rem;
}
#crypt-root .g-create__name .g-input {
  flex: 1 1 auto;
  min-width: 0;
}
/* Randomise is a footer sibling now (between the name group and Begin), so it must
   size its own height to sit level with the input — the bar aligns to flex-end. */
#crypt-root .g-create__randomise {
  flex: 0 0 auto;
  align-self: flex-end;
  padding: 0.5rem 0.7rem;
  font-size: 1.05rem;
  line-height: 1.25rem;
}
#crypt-root .g-create__name-label {
  flex-basis: 100%;
  display: block;
  font-size: 0.6875rem;
  line-height: 1rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--g-ink-low);
  margin-bottom: 0.1875rem;
}
#crypt-root .g-input {
  width: 100%;
  padding: 0.5rem 0.75rem;
  border-radius: var(--g-radius);
  background-color: var(--g-surface-2);
  border: 1px solid var(--g-edge);
  color: var(--g-ink-hi);
  font-size: 0.875rem;
  line-height: 1.25rem;
}
#crypt-root .g-input:focus {
  outline: none;
  border-color: var(--g-gold);
}
#crypt-root .g-create__begin {
  flex-shrink: 0;
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
}
/* Back exists only on the phone's detail page (see below). */
#crypt-root .g-create__back {
  display: none;
  flex-shrink: 0;
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
}

/* Phones (and landscape phones): ONE PAGE AT A TIME. Page 1 is the roster, full
   width, tap-to-open; page 2 is that class's detail with Back in the commit
   bar. Nothing is removed from the DOM — the hidden page is display:none, so
   every id and data seam stays queryable and the same script drives both
   sizes. */
@media (max-width: 767px), (pointer: coarse) and (max-height: 500px) {
  #crypt-root .g-create__panes {
    display: flex;
    flex-direction: column;
  }
  #crypt-root .g-create__roster,
  #crypt-root .g-create__detail {
    flex: 1 1 auto;
    min-height: 0;
  }
  #crypt-root .g-create__roster {
    grid-template-columns: minmax(0, 1fr); /* one full-width card per row */
    border-right: 0;
  }
  #crypt-root .g-class-card__more {
    display: block;
  }
  #crypt-root .g-class-card__face {
    width: 3.25rem;
    height: 3.25rem;
  }
  /* No hover on touch, and the sheet is tight — retire the click-to-upload cue. */
  #crypt-root .g-create__portrait-hint {
    display: none;
  }
  #crypt-root .g-create[data-create-page="roster"] .g-create__detail {
    display: none;
  }
  #crypt-root .g-create[data-create-page="detail"] .g-create__roster,
  #crypt-root .g-create[data-create-page="detail"] .g-create__head {
    display: none;
  }
  #crypt-root .g-create[data-create-page="detail"] .g-create__back {
    display: block;
  }
  /* Page two starts under the shell's absolute close chip (top-3 right-3), with
     no header of its own to keep out of its way — so the identity row does it. */
  #crypt-root .g-create[data-create-page="detail"] .g-create__id {
    padding-right: 2.5rem;
  }
  /* The bar wraps rather than squeezing the name field to nothing: the field
     takes a full row of its own, Back and Begin share the row under it. The bar
     rides BOTH pages — naming your hero is a fine thing to do while browsing the
     roster, and it keeps every seam on screen at both steps. */
  #crypt-root .g-create__foot {
    flex-wrap: wrap;
  }
  #crypt-root .g-create__name {
    flex: 1 0 100%;
    order: -1;
  }
  #crypt-root .g-create__begin {
    flex: 1 1 auto;
  }
}

/* ---- The battle overlay (game/battle.js + game/battleFx.js, P5) ------------
 * Two contracts drive this section, and both are about a column that must
 * never break its budget:
 *
 *   1. THE ART FITS ITS LANE. The monster portrait used to be sized in
 *      VIEWPORT units (max-h-[46vh] x spriteScale) inside a layer that is only
 *      78vh tall on desktop — so a big sprite reached under the turn banner and
 *      shoved the log off the bottom. Now the layer is a flex column
 *      [banner (absolute, its height RESERVED as the lane's top padding)] /
 *      [lane (flex:1, min-height:0)] / [log (fixed)] / [hero strip (shrink-0)],
 *      and the art is capped at 100% of whatever the lane has left. spriteScale
 *      scales WITHIN that box (a percentage, clamped at the lane), so a rat
 *      stays a rat and a giant simply fills the lane instead of escaping it.
 *
 *   2. THE HOTBAR NEVER GROWS. A hero accumulates abilities for the whole game;
 *      the bar's FOOTPRINT is fixed from the first fight to the last. On a
 *      coarse pointer it is one thumb-sized row that scrolls sideways with
 *      snap; on desktop the grouped grid keeps its cap and scrolls inside
 *      itself. Both edges fade — and the fade eats the scroller's own PADDING
 *      at either extreme, so at rest the first/last tile is crisp and the fade
 *      only ever covers content there is more of. No JS scroll-spy.
 *
 * The art is sized with max-height/max-width and object-contain and NOTHING
 * else on purpose: a replaced element constrained that way has a border box
 * identical to the visible art, which is what makes game/introShot.js's
 * measured rect the true on-screen rect. Give the <img> an explicit width or
 * height and the box becomes a letterbox around the art, and the walk-in
 * animation lands off-target.
 */
#crypt-root {
  /* The hotbar's fixed geometry. One tile size, and a region height derived
     from it (group label + tile + name label + the gaps between). */
  --g-hotbar-tile: 3.5rem;
  /* +0.5rem over the tile+labels so the bottom padding that keeps the horizontal
     scrollbar off the tile NAMES (see .g-hotbar padding) doesn't squeeze the row. */
  --g-hotbar-h: calc(var(--g-hotbar-tile) + 3rem);
  /* Vertical space the turn banner occupies, reserved as the lane's top
     padding. The banner is absolutely positioned (it is raised lazily, AFTER
     the intro has already measured the portrait — putting it in flow would
     resize the art out from under the walk-in), so the lane pays for it here. */
  --g-battle-banner-h: 3.5rem;
  /* How far a scroll fade reaches, and therefore how much padding the scroller
     carries at that edge. */
  --g-hotbar-fade: 1.25rem;
}
@media (pointer: coarse) {
  #crypt-root {
    --g-hotbar-tile: 3.75rem; /* thumb-sized */
  }
}

/* The layer itself: full-bleed inside #crypt-viewport, a column, and a vignette
   that darkens toward the edges so the fight reads over the frozen 3D scene. */
#crypt-root .g-battle {
  position: absolute;
  inset: 0;
  z-index: var(--g-z-battle);
  display: flex;
  flex-direction: column;
  align-items: center;
  background: radial-gradient(
    ellipse at center,
    rgba(28, 25, 23, 0.55) 0%,
    rgba(12, 10, 9, 0.9) 100%
  );
}

/* ---- The turn banner — the fight's narrator -------------------------------
 * A status line, not an announcement: raised once and then EDITED as the turn
 * changes. It earns its presence with a warm bloom behind the strip and the
 * gold panel rim, and it never leaves — so "whose go is it?" is always
 * answerable at a glance. */
#crypt-root .g-battle__banner {
  position: absolute;
  left: 50%;
  top: 0;
  transform: translateX(-50%);
  z-index: 6;
  min-width: 16rem;
  max-width: min(92%, 34rem);
  padding: 0.25rem 2rem 0.375rem;
  text-align: center;
  pointer-events: none;
  border-radius: 0 0 var(--g-radius-lg) var(--g-radius-lg);
  border: 1px solid var(--g-edge-strong);
  border-top: 0;
  background-image: linear-gradient(
    to bottom,
    rgba(28, 25, 23, 0.95),
    rgba(12, 10, 9, 0.92)
  );
  box-shadow:
    0 8px 22px -10px rgba(0, 0, 0, 0.9),
    0 0 18px -6px rgba(180, 83, 9, 0.45);
}
#crypt-root .g-battle__banner-title {
  font-family: Cinzel, serif;
  font-size: 1.125rem;
  line-height: 1.6rem;
  letter-spacing: 0.04em;
  color: var(--g-gold-ink);
}
/* The subtitle keeps its line box when empty (battle.js writes a space), so
   swapping "Thinking…" for the enemy's move never makes the strip jump. */
#crypt-root .g-battle__banner-sub {
  font-size: 0.75rem;
  line-height: 1.1rem;
  color: var(--g-ink-low);
}

/* ---- The art lane ---------------------------------------------------------
 * `lane` is the ONE region that flexes; everything below it is shrink-0. Its
 * top padding is the banner's reserved height. */
#crypt-root .g-battle__lane {
  position: relative;
  flex: 1 1 0;
  min-height: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  padding-top: calc(var(--g-battle-banner-h) + 0.5rem);
}
/* The stage takes whatever the lane has left after the name plate, and the art
   is BOTTOM-aligned in it — creatures stand on a ground line, and it means the
   grease slick pinned to the stage's bottom edge is at the feet of a sprite of
   any height or spriteScale, with nothing to measure. */
#crypt-root .g-battle__stage {
  position: relative;
  flex: 1 1 0;
  min-height: 0;
  width: 100%;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}
#crypt-root .g-battle__art {
  max-height: 100%;
  max-width: 76%;
  object-fit: contain;
  filter: drop-shadow(0 10px 24px rgba(0, 0, 0, 0.8));
}
/* No artwork: a name placard standing in for the portrait. */
#crypt-root .g-battle__art--placard {
  padding: 3.5rem 2.5rem;
  border-radius: var(--g-radius-lg);
  border: 1px solid var(--g-edge);
  background-color: rgba(28, 25, 23, 0.8);
  font-family: Cinzel, serif;
  font-size: 1.875rem;
  line-height: 2.25rem;
  color: var(--g-gold-ink);
}

/* The enemy's name plate, directly under the art.
 *
 * A ROW — status chips on the left, the name/HP/bar column on the right — and
 * that direction is load-bearing. The plate is flex-shrink:0 inside a column
 * whose other child is the art's stage, and .g-battle__art is capped at 100% OF
 * THAT STAGE, so anything that made the plate TALLER made the sprite smaller.
 * A debuff landing used to resize the enemy mid-fight. Growing sideways costs
 * the sprite nothing: the lane centres the plate, so it opens left and right
 * into space that was empty anyway. */
#crypt-root .g-battle__plate {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.375rem;
  padding: 0.25rem 1rem 0.375rem;
  min-width: 12.5rem;
  /* Landscape phones squeeze the lane between the banner chip and the log
     column, so a six-chip rail has to wrap inside the plate rather than push
     the plate through them. */
  max-width: 100%;
  text-align: center;
  border-radius: var(--g-radius);
  border: 1px solid var(--g-edge-soft);
  background-color: rgba(28, 25, 23, 0.85);
}
/* Name, numbers and bar, stacked — the column the rail sits beside. It is the
   taller of the two, which is why chips no longer change the plate's height. */
#crypt-root .g-battle__readout {
  flex: 1 1 auto;
  min-width: 0;
}
#crypt-root .g-battle__monname {
  font-family: Cinzel, serif;
  font-size: 1.0625rem;
  line-height: 1.5rem;
  color: var(--g-gold-ink);
  /* The one thing allowed to give when a long name and a full status rail meet
     on a narrow screen — the plate holding its height matters more than the
     last few letters of "Skeleton Warrior". */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
#crypt-root .g-battle__monhp {
  font-size: 0.75rem;
  line-height: 1.1rem;
  color: var(--g-ink-mid);
}
/* The battle meters sit tight under their label. */
#crypt-root .g-bar--battle {
  margin-top: 0.25rem;
}
/* A pool under a quarter goes red — the same signal the exploration HUD's hero
   chip uses, so "you are in trouble" looks the same everywhere. */
#crypt-root .g-bar--low > .g-bar__fill {
  background-color: var(--g-danger);
}

/* ---- Enemy decals ---------------------------------------------------------
 * Scene dressing for the two statuses that want to be ON the creature rather
 * than in a chip. Both ride the STAGE, whose bottom edge is the ground line. */
#crypt-root .g-battle__grease {
  position: absolute;
  left: 50%;
  bottom: 0;
  transform: translate(-50%, 42%);
  height: 4.5rem;
  border-radius: 9999px;
  background: radial-gradient(
    ellipse at center,
    rgba(163, 190, 74, 0.55) 0%,
    rgba(101, 133, 45, 0.32) 52%,
    rgba(60, 84, 28, 0) 76%
  );
  filter: blur(2px);
  z-index: 3;
  pointer-events: none;
}
/* Caltrops carry their own SVG of scattered iron spikes, so the green oil-slick gradient
   and its blur come off — the spikes must stay crisp, hence filter:none. What replaces the
   slick is a faint patch of disturbed grey-brown floor: without it the spikes hovered over
   nothing, and the ground effect read as decals pasted on the sprite rather than as something
   strewn where the creature is standing. Deliberately weak — the SVG is the decal, this is
   only the dust it was thrown into. */
#crypt-root .g-battle__grease[data-decal="caltrops"] {
  background: radial-gradient(
    ellipse at center,
    rgba(120, 113, 108, 0.34) 0%,
    rgba(87, 83, 78, 0.18) 55%,
    rgba(68, 64, 60, 0) 78%
  );
  filter: none;
}
/* "Your class can't wear X" note where the Equip button would be, when the selected armour/shield
   is right for the slot but wrong for the class (the equip gate refuses it). */
#crypt-root .g-inv-cant {
  font-size: 0.8rem;
  color: #d9a066;
  opacity: 0.9;
  align-self: center;
}
/* The mark's brand hovers over the creature's upper body. Its height is set
   inline from spriteScale (battle.js) — a percentage of the stage, because the
   art's height IS spriteScale x the stage for every portrait-shaped sprite,
   and none of this is worth a ResizeObserver. */
#crypt-root .g-battle__mark {
  position: absolute;
  left: 50%;
  transform: translate(-50%, 50%);
  width: 5rem;
  height: 5rem;
  z-index: 5;
  opacity: 0.85;
  filter: drop-shadow(0 0 7px rgba(244, 63, 94, 0.8));
  pointer-events: none;
}
/* The poison seep (battle.js VENOM_DECAL) shares the brand's placement machinery and nothing
   else: a smaller glyph, lower on the body, and an acid-green bloom instead of the mark's rose —
   the two can be up at once, and a green drop-shadow is what stops them reading as one effect. */
#crypt-root .g-battle__mark--venom {
  width: 4rem;
  height: 4rem;
  filter: drop-shadow(0 0 7px rgba(132, 204, 22, 0.8));
}

/* ---- Status rails ---------------------------------------------------------
 * The readable inventory of everything in effect: the enemy's beside the name
 * on its plate, the hero's inline beside their name. */
#crypt-root .g-battle__rail--mon {
  display: flex;
  /* ONE row, never wrapped. A second row of chips would be taller than the
     readout beside it and the plate would grow again — the exact thing this
     layout exists to prevent. Six chips (every status a monster can carry at
     once) come to ~11.75rem, which fits the lane on a portrait phone and
     inside the landscape lane's padding; if it ever did not, the name beside
     it ellipses rather than the plate growing. */
  flex-wrap: nowrap;
  flex-shrink: 0;
  justify-content: center;
  align-items: center;
  gap: 0.25rem;
  pointer-events: none;
}
/* An empty rail takes no room at all, so an unafflicted enemy's plate is
   exactly the plate it always was. */
#crypt-root .g-battle__rail--mon:empty {
  display: none;
}
/* The enemy's chips run bigger than the hero's: they are the ones a player
   reads mid-fight, and the plate is the one place with room to say so. */
#crypt-root .g-battle__rail--mon > .g-battle__status {
  width: 1.75rem;
  height: 1.75rem;
}
#crypt-root .g-battle__rail--hero {
  display: flex;
  align-items: center;
  gap: 0.375rem;
}
/* One affliction. The ring/glow colour is the status registry's own tone, so it
   is the one thing that stays inline (statusIcons.js toneOf). */
#crypt-root .g-battle__status {
  position: relative;
  flex-shrink: 0;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 9999px;
  border: 1.5px solid var(--g-edge);
  background-color: rgba(12, 10, 9, 0.72);
  display: flex;
  align-items: center;
  justify-content: center;
}
#crypt-root .g-battle__status-icon {
  width: 68%;
  height: 68%;
  object-fit: contain;
  pointer-events: none;
}
#crypt-root .g-battle__status-n {
  position: absolute;
  right: -0.25rem;
  bottom: -0.25rem;
  padding: 0 0.25rem;
  border-radius: 9999px;
  background-color: var(--g-surface-0);
  font-size: 0.5625rem;
  font-weight: 700;
  line-height: 1;
}

/* ---- The combat log — a ticker --------------------------------------------
 * Fixed height (5 lines, 3 on a phone) and filled from the bottom, so it never
 * grows and never nudges the art. The newest line is the bright one and the
 * older ones recede: OPACITY, not colour, because individual lines carry their
 * own emphasis class (a crit, a fumble, bonus damage) that must still win. */
#crypt-root .g-battle__log {
  width: 100%;
  max-width: 36rem;
  flex-shrink: 0;
  padding: 0 1rem;
  height: 6.25rem;
  font-size: 0.75rem;
  line-height: 1.25rem;
  text-align: center;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85);
}
#crypt-root .g-battle__log--short {
  height: 3.875rem;
}
/* "Battle log" off (crypt/settings.js battleLog): the band collapses entirely and the
   lane reclaims it, and the dice overlay puts its label + sum back (game/dice.js `quiet`).
   The element stays in the column — battle.js keeps filling it, so switching the setting
   back on shows the history it collected while hidden. Placed after the base rule: equal
   specificity, source order decides. */
#crypt-root .g-battle__log--off {
  display: none;
}
#crypt-root .g-battle__log > div {
  opacity: 0.45;
}
#crypt-root .g-battle__log > div:nth-last-child(3) {
  opacity: 0.62;
}
#crypt-root .g-battle__log > div:nth-last-child(2) {
  opacity: 0.8;
}
#crypt-root .g-battle__log > div:last-child {
  opacity: 1;
}
/* Standout lines — a crit, a fumble, the Dodge action, a status paying out its
   bonus damage. Bold and a notch larger so they read as events rather than
   bookkeeping; the recession above is opacity, so these colours still win. */
#crypt-root .g-log--crit,
#crypt-root .g-log--fumble,
#crypt-root .g-log--dodge {
  font-weight: 700;
  font-size: 1rem;
  line-height: 1.5rem;
}
#crypt-root .g-log--crit {
  color: #6ee7b7; /* emerald-300 */
}
#crypt-root .g-log--fumble {
  color: #fda4af; /* rose-300 */
}
#crypt-root .g-log--dodge {
  color: #fde68a; /* amber-200 */
}
#crypt-root .g-log--bonus {
  font-weight: 700;
  color: #f0abfc; /* fuchsia-300 — the buff, not the blow */
}
/* Category tints. Deliberately NOT bold and NOT enlarged — the four above are
   EVENTS (something unusual just happened); these three are a running fight's
   ordinary business, sorted by kind so a scrollback can be skimmed. There is no
   --attack: plain weapon swings are the bulk of the log and stay ink, so that
   everything with a colour on it means something. */
#crypt-root .g-log--spell {
  color: #c4b5fd; /* violet-300 */
}
#crypt-root .g-log--heal {
  color: #86efac; /* green-300 */
}
#crypt-root .g-log--debuff {
  color: #bef264; /* lime-300 */
}
/* The name of the thing that was cast, at the head of its own line. A SPAN
   inside a line, not a line class — battle.js's log() takes a parts array for
   exactly this. */
#crypt-root .g-log__name {
  color: #fcd34d; /* amber-300 */
  font-weight: 600;
}

/* ---- The hero strip -------------------------------------------------------
 * Name + statuses + HP, the hero's meter, and the hotbar. shrink-0: this and
 * the log are the fixed ends of the column the lane flexes between. */
#crypt-root .g-battle__strip {
  width: 100%;
  max-width: 36rem;
  flex-shrink: 0;
  margin: 0.5rem 0.75rem 0.75rem;
  padding: 0.375rem 1rem 0.5rem;
  border-radius: var(--g-radius-lg);
  border: 1px solid var(--g-edge-soft);
  background-color: rgba(28, 25, 23, 0.9);
}
@media (min-width: 640px) {
  #crypt-root .g-battle__strip {
    max-width: 48rem;
  }
}
/* Fixed thirds, not space-between: with several children "space between every
   pair" drifts each one with the length of its neighbours. The middle third is
   deliberately EMPTY — hit/miss text lands over that part of the screen. The
   min-height is text-base's line box, which is also a status chip's height, so
   a status appearing never grows the row. */
#crypt-root .g-battle__heroline {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  align-items: center;
  gap: 0.75rem;
  min-height: 1.5rem;
  font-size: 1rem;
  line-height: 1.5rem;
  color: var(--g-ink-mid);
}
#crypt-root .g-battle__heroname {
  font-family: Cinzel, serif;
  color: var(--g-gold-ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* AC, beside the HP on the heroline. Same size as the HP text it sits next to —
   they are one reading — but a step back in the ink so the pool the player is
   watching drop stays the louder of the two. */
#crypt-root .g-battle__heroac {
  color: var(--g-ink-low);
}

/* The action region. Its height is the hotbar's — constant whatever the hero
   has learned — plus the one meta line. */
#crypt-root .g-battle__actions {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.25rem;
  margin-top: 0.5rem;
  min-height: 2.625rem;
}

/* The victory / fled end state: the Continue button with its keyboard chip
   underneath. Deliberately by eye the same as the death screen's own hint pair
   (.g-death__cell / __hint / __key) — the two are the game's only "press this to
   leave a panel" chips and must read identically — but named for the battle,
   because game/battle.js may not reach into deathScreen's classes any more than
   deathScreen may reach into crypt/interaction.js's. Suppressed on a coarse
   pointer, where the cell is never built at all. */
#crypt-root .g-battle__cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
}
#crypt-root .g-battle__hint {
  font-family: Cinzel, serif;
  font-size: 0.75rem;
  line-height: 1rem;
  color: #78716c; /* stone-500 */
}
#crypt-root .g-battle__key {
  padding: 0.125rem 0.375rem;
  border-radius: 0.25rem;
  background-color: rgba(41, 37, 36, 0.9); /* stone-800/90 */
  border: 1px solid #57534e; /* stone-600 */
  color: #fde68a; /* amber-200 */
}

/* ---- The hotbar -----------------------------------------------------------
 * Groups (Actions / the class resource / Imbued / Items) sit left to right,
 * each a labelled cluster. */
#crypt-root .g-hotbar {
  width: 100%;
  display: flex;
  flex-direction: row;
  /* ONE row that scrolls SIDEWAYS — the same treatment mobile gets (below), instead of wrapping
     into a tall block that grew with every ability the hero learned. `safe center` keeps it
     centred when it fits but falls back to start-aligned (and therefore fully scrollable) when it
     overflows, sidestepping the flexbox centre-overflow-unreachable trap. */
  flex-wrap: nowrap;
  align-items: flex-start;
  justify-content: safe center;
  column-gap: 1rem;
  height: var(--g-hotbar-h);
  overflow-x: auto;
  overflow-y: hidden;
  /* Bottom padding gives the horizontal scrollbar a lane of its own so it stops drawing over the
     tile NAME labels; --g-hotbar-h is bumped to match so the tiles aren't squeezed. */
  padding: 0.125rem var(--g-hotbar-fade) 0.5rem;
  -webkit-mask-image: linear-gradient(
    to right,
    transparent 0,
    #000 var(--g-hotbar-fade),
    #000 calc(100% - var(--g-hotbar-fade)),
    transparent 100%
  );
  mask-image: linear-gradient(
    to right,
    transparent 0,
    #000 var(--g-hotbar-fade),
    #000 calc(100% - var(--g-hotbar-fade)),
    transparent 100%
  );
  /* A thin, theme-matched scrollbar; auto-hides on touch. */
  scrollbar-width: thin;
  scrollbar-color: rgba(180, 140, 60, 0.5) transparent;
}
#crypt-root .g-hotbar::-webkit-scrollbar {
  width: 6px;
  height: 6px;
}
#crypt-root .g-hotbar::-webkit-scrollbar-thumb {
  background: rgba(180, 140, 60, 0.5);
  border-radius: 9999px;
}
#crypt-root .g-hotbar::-webkit-scrollbar-track {
  background: transparent;
}
#crypt-root .g-hotbar__group {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-width: 0;
  flex-shrink: 0; /* a group keeps its width and the bar scrolls past it, never squashing tiles */
}
#crypt-root .g-hotbar__label {
  font-size: 0.625rem;
  line-height: 1rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--g-ink-low);
}
#crypt-root .g-hotbar__row {
  display: flex;
  flex-wrap: nowrap;
  align-items: flex-start;
  justify-content: flex-start;
  gap: 0.5rem;
  margin-top: 0.125rem;
  /* The OUTER .g-hotbar is the only scroller now (single row, sideways) — a nested scroll here
     would trap tiles in a sub-scroller the outer swipe can't reach. */
  max-width: none;
  padding: 0.125rem 0;
  overflow-x: visible;
  scrollbar-width: none;
}
#crypt-root .g-hotbar__row::-webkit-scrollbar {
  display: none;
}
#crypt-root .g-hotbar__cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.125rem;
  flex-shrink: 0;
  width: var(--g-hotbar-tile);
}
#crypt-root .g-hotbar__name {
  font-size: 0.5625rem;
  line-height: 0.75rem;
  text-align: center;
  color: var(--g-ink-low);
  width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
#crypt-root .g-hotbar .g-tile {
  width: var(--g-hotbar-tile);
  height: var(--g-hotbar-tile);
}

/* Coarse pointers: ONE row, scrolling sideways with snap, which can never grow
   taller no matter how many abilities the hero has learned. The group labels
   stay above their own cluster (they cost width, which scrolls, not height),
   separated by a hairline so the grouping still reads. */
@media (pointer: coarse), (max-width: 767px) {
  #crypt-root .g-hotbar {
    flex-wrap: nowrap;
    justify-content: flex-start;
    height: var(--g-hotbar-h);
    max-height: none;
    overflow-x: auto;
    overflow-y: hidden;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
    /* Snapped to the tile's START, never its centre: `center` makes the browser
       pick a snap position on FIRST layout, which silently scrolled the bar off
       zero and hid Attack behind the left fade before the player touched it. */
    scroll-padding-left: var(--g-hotbar-fade);
    -webkit-overflow-scrolling: touch;
    column-gap: 0.75rem;
    padding-bottom: 0.5rem; /* same scrollbar lane as desktop, so the touch scrollbar clears the labels */
    padding-left: var(--g-hotbar-fade);
    padding-right: var(--g-hotbar-fade);
    -webkit-mask-image: linear-gradient(
      to right,
      transparent 0,
      #000 var(--g-hotbar-fade),
      #000 calc(100% - var(--g-hotbar-fade)),
      transparent 100%
    );
    mask-image: linear-gradient(
      to right,
      transparent 0,
      #000 var(--g-hotbar-fade),
      #000 calc(100% - var(--g-hotbar-fade)),
      transparent 100%
    );
  }
  /* A group here is WIDER than the screen, so its label is left-aligned with
     the cluster it names — centred, it drifts off past the right edge and the
     player scrolls to tiles under a heading they can no longer see. */
  #crypt-root .g-hotbar__group {
    flex-shrink: 0;
    align-items: flex-start;
  }
  #crypt-root .g-hotbar__group + .g-hotbar__group {
    border-left: 1px solid var(--g-edge-soft);
    padding-left: 0.75rem;
  }
  /* The outer bar is the only scroller — a nested one would swallow the swipe. */
  #crypt-root .g-hotbar__row {
    overflow-x: visible;
    max-width: none;
  }
  #crypt-root .g-hotbar__cell {
    scroll-snap-align: start;
  }
}

/* ---- Mobile LANDSCAPE battle reflow ---------------------------------------
 * A short landscape viewport crushes the portrait column (banner reserve + log band + the tall hotbar
 * strip leave nothing for the art). Lay it out in three zones instead: the turn indicator as a compact
 * chip pinned TOP-LEFT, the combat log as a TOP-RIGHT column (pulled OUT of the column flow so the lane
 * + strip get the height back), a SHRUNK action bar at the bottom, and the monster art holding the
 * padded center between the two side columns. CSS-only — every hook already exists. On-device tuning
 * of the widths/paddings is expected. */
@media (pointer: coarse) and (max-height: 500px) {
  #crypt-root {
    --g-hotbar-tile: 2.75rem; /* down from the 3.75rem coarse default — the bar no longer dominates */
    /* +2.75rem, not +2rem: the region must clear the tile PLUS the .g-hotbar__name label beneath it
       (0.75rem line) + the scrollbar/label lane, or the bottom of each action name gets clipped. */
    --g-hotbar-h: calc(var(--g-hotbar-tile) + 2.75rem);
  }
  /* Turn indicator → compact top-LEFT chip (was a centered top strip). */
  #crypt-root .g-battle__banner {
    left: 0.5rem;
    top: 0.5rem;
    transform: none;
    min-width: 0;
    max-width: 11rem;
    padding: 0.25rem 0.75rem 0.3125rem;
    border-top: 1px solid var(--g-edge-strong);
    border-radius: var(--g-radius-lg);
    text-align: left;
  }
  #crypt-root .g-battle__banner-title {
    font-size: 0.9375rem;
    line-height: 1.3rem;
  }
  /* Combat log → a top-RIGHT column ticker, absolutely placed so the column flow (lane + strip)
     reclaims its band. Both the base and --short heights are overridden to auto. */
  #crypt-root .g-battle__log,
  #crypt-root .g-battle__log--short {
    position: absolute;
    right: 0.5rem;
    top: 0.5rem;
    bottom: auto;
    width: 12rem;
    max-width: 40vw;
    height: auto;
    max-height: 46%;
    padding: 0;
    text-align: right;
  }
  /* The art lane keeps the CENTER: no banner reserve (the banner is a left chip now), with side
     padding that clears the two columns so a tall sprite never underlaps them. */
  #crypt-root .g-battle__lane {
    padding: 0.5rem 12.5rem 0 11.5rem; /* top / right(log+gap) / bottom / left(banner+gap) */
  }
  #crypt-root .g-battle__art {
    max-width: 100%; /* the lane padding already frames it between the columns */
  }
  /* Shrink the action bar so it stops dominating. */
  #crypt-root .g-battle__strip {
    max-width: 32rem;
    margin: 0.25rem auto 0.375rem;
    padding: 0.25rem 0.75rem 0.375rem;
  }
  #crypt-root .g-battle__heroline {
    min-height: 0;
    font-size: 0.875rem;
  }
  #crypt-root .g-battle__actions {
    margin-top: 0.25rem;
    min-height: 0;
    gap: 0.15rem;
  }
}

/* The meta line: the class-resource pips and the turn's two halves, on ONE row
   (they were two, and on a phone that spent a whole tile's worth of height on
   four glyphs). */
#crypt-root .g-hotbar__meta {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.25rem 1rem;
  width: 100%;
}
#crypt-root .g-hotbar__pips {
  display: flex;
  align-items: center;
  gap: 0.125rem;
  color: var(--g-resource);
  font-size: 0.875rem;
  line-height: 1;
}
/* Collapsed to "Spell Slots ◆ 10/10" — on a phone, and past PIP_MAX anywhere.
   Numbers want no pip tracking and must hold their one line, or the meta row
   wraps and the hotbar loses height it has none of. Matches the HUD hero
   chip's own collapsed readout (.g-hud-hero__pips--num). */
#crypt-root .g-hotbar__pips--num {
  gap: 0;
  letter-spacing: normal;
  white-space: nowrap;
}
#crypt-root .g-hotbar__pip-label,
#crypt-root .g-hotbar__econ {
  font-size: 0.625rem;
  line-height: 1rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
#crypt-root .g-hotbar__pip-label {
  color: var(--g-ink-low);
  margin-right: 0.25rem;
}
#crypt-root .g-hotbar__econ {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}
#crypt-root .g-hotbar__econ-chip {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  color: var(--g-gold-ink);
}
#crypt-root .g-hotbar__econ-chip--spent {
  color: #57534e; /* stone-600 — spent, but still legible as a label */
}
/* The idle nudge (battle.js armIdleHint): after five quiet seconds the half of the
   turn you have NOT spent breathes. Sibling of .g-sheet__levelup's g-levelup-pulse
   and deliberately the same grammar — box-shadow/colour only, 2.4s, infinite, and
   off entirely under reduced motion (the JS gate already refuses to arm the timer
   there; this is the belt to its braces).
   The kept-together keyframes: this file has exactly two "look at me" pulses, and
   they should read as one idea. */
#crypt-root .g-hotbar__econ-chip--hint {
  color: var(--g-gold-bright);
  border-radius: 9999px;
  padding: 0 0.375rem;
  margin: 0 -0.375rem; /* the ring is decoration — it must not move the row */
  animation: g-econ-pulse 2.4s ease-in-out infinite;
}
/* A tile in the hinted half. Much quieter than the chip: a whole row of pulsing
   tiles would be a strobe, but the chips only exist for a hero with a bonus kit —
   so for a Fighter holding a banked Action Surge this IS the signal. */
#crypt-root .g-tile.is-hint {
  animation: g-econ-pulse-tile 2.4s ease-in-out infinite;
}
@keyframes g-econ-pulse {
  50% {
    box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.45);
    text-shadow: 0 0 8px rgba(245, 158, 11, 0.55);
  }
}
@keyframes g-econ-pulse-tile {
  50% {
    border-color: var(--g-gold);
    box-shadow: 0 0 0 1px rgba(245, 158, 11, 0.3);
  }
}
@media (prefers-reduced-motion: reduce) {
  #crypt-root .g-hotbar__econ-chip--hint,
  #crypt-root .g-tile.is-hint {
    animation: none;
  }
}

/* ---- Tiles ----------------------------------------------------------------
 * `.g-tile` owns the border colour, so these states are modifiers here rather
 * than Tailwind at the call site (which would lose on specificity). A disabled
 * tile GREYS and stays in place — the bar must never change shape mid-turn. */
#crypt-root .g-tile--live {
  cursor: pointer;
}
#crypt-root .g-tile--live:hover {
  border-color: var(--g-gold);
}
#crypt-root .g-tile--off {
  border-color: var(--g-surface-3);
  opacity: 0.4;
}
#crypt-root .g-tile__icon {
  width: 64%;
  height: 64%;
  object-fit: contain;
  pointer-events: none;
  filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.6));
}
/* Corner badges: the slot cost top-right, everything countable bottom-right.
   Each pool wears its own ink AND its own glyph (◆ slots, ⚡ item charges,
   ☾ per-rest uses) — a player about to spend their last one needs to know
   which pool it comes out of. */
#crypt-root .g-tile__badge {
  position: absolute;
  right: 0.25rem;
  font-size: 0.625rem;
  font-weight: 700;
  line-height: 1;
  pointer-events: none;
  text-shadow: 0 1px 2px #000;
}
#crypt-root .g-tile__badge--cost {
  top: 0.125rem;
  color: var(--g-resource);
}
#crypt-root .g-tile__badge--qty {
  bottom: 0.125rem;
  color: #fef3c7; /* amber-100 */
}
#crypt-root .g-tile__badge--charge {
  bottom: 0.125rem;
  font-size: 0.5625rem;
  color: var(--g-charge);
}
#crypt-root .g-tile__badge--uses {
  bottom: 0.125rem;
  font-size: 0.5625rem;
  color: var(--g-gold-ink);
}

/* ---- The announcement panel (victory / level up) --------------------------
 * Drawn OVER the art, which is why it is near-opaque and why the enemy's
 * dissolve is allowed to finish first (battle.js VICTORY_PANEL_DELAY_MS). */
#crypt-root .g-battle__panel {
  position: absolute;
  left: 50%;
  top: 30%;
  transform: translateX(-50%);
  z-index: 4;
  max-width: 28rem;
  padding: 1.25rem 2rem;
  text-align: center;
  border-radius: var(--g-radius-lg);
  border: 1px solid var(--g-edge-strong);
  background-image: linear-gradient(
    to bottom,
    rgba(28, 25, 23, 0.97),
    rgba(12, 10, 9, 0.97)
  );
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.7);
}
#crypt-root .g-battle__panel-title {
  font-family: Cinzel, serif;
  font-size: 1.5rem;
  line-height: 2rem;
  color: var(--g-gold-bright);
  margin-bottom: 0.25rem;
}
#crypt-root .g-battle__panel-body {
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--g-ink-mid);
}
/* Loot that is PROGRESS, not plunder — set apart from the gold-and-daggers
   line, so a dropped key is never read past. */
#crypt-root .g-battle__quest {
  margin-top: 0.75rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--g-edge-strong);
}
#crypt-root .g-battle__quest-label {
  font-size: 0.6875rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--g-gold);
}
#crypt-root .g-battle__quest-item {
  font-family: Cinzel, serif;
  font-size: 1.125rem;
  line-height: 1.75rem;
  color: #fde68a; /* amber-200 */
}

/* ---- Damage / miss chips --------------------------------------------------
 * The v1 damage-dice presentation: a big number over the struck target. Its
 * SIZE lives here rather than in a Tailwind text-* class because these are
 * built at runtime and were reading tiny whenever the CDN JIT had not seen the
 * class; only the (randomised) position stays inline. */
#crypt-root .g-battle__chip {
  position: absolute;
  z-index: 7;
  font-family: Cinzel, serif;
  font-weight: 700;
  pointer-events: none;
  text-shadow: 0 3px 10px #000;
  line-height: 1;
}
#crypt-root .g-battle__chip--damage {
  font-size: 5rem;
  color: var(--g-danger);
}
#crypt-root .g-battle__chip--heal {
  font-size: 5rem;
  color: #4ade80; /* green-400 — brighter than --g-heal; it is a flash */
}
#crypt-root .g-battle__chip--miss {
  font-size: 2.25rem;
  color: #cbd5e1; /* slate-300 — a shrug, not a stat */
}

/* ---- Battle keyframes -----------------------------------------------------
 * Every one of these is STATIC, which is why they live in the stylesheet
 * instead of the <style> tags battle.js and battleFx.js used to inject on
 * first use. The few effects that vary per cast pass their variance as custom
 * properties (--rot / --from / --dx / --dy) set inline on the element, so the
 * animation itself never has to be rebuilt.
 *
 * Everything here animates opacity and transform ONLY — no backdrop-filter, no
 * blur transitions (a Firefox GPU lesson this repo has already paid for; the
 * one filter, the death dissolve, is a 700ms one-shot on a single sprite). */

/* battle.js: a hit number POPS in then drifts up; a Miss shakes; a landed blow
   recoils the struck portrait, or jitters the hero's meter when it is the hero
   who was struck (shaking the whole layer used to show white at the edges). */
@keyframes bt-pop {
  0% {
    transform: translateX(-50%) scale(0.4);
  }
  55% {
    transform: translateX(-50%) scale(1.35);
  }
  100% {
    transform: translateX(-50%) scale(1);
  }
}
@keyframes bt-miss {
  0%,
  100% {
    transform: translateX(-50%);
  }
  20% {
    transform: translateX(-64%);
  }
  40% {
    transform: translateX(-38%);
  }
  60% {
    transform: translateX(-58%);
  }
  80% {
    transform: translateX(-46%);
  }
}
@keyframes bt-recoil {
  0%,
  100% {
    transform: translate(0, 0);
  }
  25% {
    transform: translate(-9px, 4px) rotate(-1.5deg);
  }
  55% {
    transform: translate(7px, -2px) rotate(1deg);
  }
}
@keyframes bt-hpshake {
  0%,
  100% {
    transform: translateX(0);
  }
  20% {
    transform: translateX(-4px);
  }
  40% {
    transform: translateX(3px);
  }
  60% {
    transform: translateX(-3px);
  }
  80% {
    transform: translateX(2px);
  }
}
@keyframes bt-dissolve {
  0% {
    opacity: 1;
    filter: none;
    transform: scale(1);
  }
  100% {
    opacity: 0;
    filter: grayscale(1) blur(5px);
    transform: scale(0.82);
  }
}
/* The OTHER battle entrance (battle.js armEntrance). A creature the host has no
   billboard for — every crypt fight, and every `startBattle`/`startBossBattle`
   verb row — has nothing to grow out of, so it ARRIVES instead of cutting in.
   Rank and file POUNCE from --enter-x: one fast lunge that is home by 70%, then
   a squash landing and a small settle. Bosses SLAM down from off the top of the
   stage, deeper and heavier, and jolt the ground they land on.

   Both are transform ONLY and anchored at the FEET (the world sprites' own
   0.5/0 centre) so a landing flattens the creature onto the floor rather than
   scaling it about its middle. The art element itself keeps no width/height —
   battle.js measures it, and .g-battle__art's caps ARE its box
   (tests/js/battle-layout.test.mjs pins this).

   All three are armed PAUSED, so the overlay's first painted frame already has
   the creature off-stage; battle.js adds .is-entering on the frame after the
   reveal, which is also what starts the shake's delay counting. It writes
   --enter-ms because it has to hold the initiative die for exactly that long,
   and --shake-delay so the jolt can never drift from the fall it belongs to;
   the values below are only fallbacks.

   A slower squish-walk on was tried first and playtested badly — over a second
   of stroll before every fight reads as waiting, not as an entrance. */
#crypt-root .g-battle__art--pounce {
  transform-origin: 50% 100%;
  animation: bt-pounce var(--enter-ms, 400ms) ease-out both;
  animation-play-state: paused;
}
#crypt-root .g-battle__art--slam {
  transform-origin: 50% 100%;
  animation: bt-slam var(--enter-ms, 600ms) linear both;
  animation-play-state: paused;
}
#crypt-root .g-battle__stage--slammed {
  animation: bt-stage-shake 180ms ease-out both;
  animation-delay: var(--shake-delay, 360ms); /* longhand: the shorthand above zeroes it */
  animation-play-state: paused;
}
#crypt-root .g-battle__art--pounce.is-entering,
#crypt-root .g-battle__art--slam.is-entering,
#crypt-root .g-battle__stage--slammed.is-entering {
  animation-play-state: running;
}
@keyframes bt-pounce {
  0% {
    transform: translateX(var(--enter-x, 58%));
  }
  70% {
    transform: translateX(0);
  }
  80% {
    transform: scale(1.08, 0.9); /* the landing, squashed onto its feet */
  }
  90% {
    transform: translateY(-2%) scale(0.98, 1.02);
  }
  100% {
    transform: none;
  }
}
@keyframes bt-slam {
  0% {
    /* off the top of the stage, and nearer the "camera" for the drop */
    transform: translateY(-120%) scale(1.06);
    animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); /* it FALLS */
  }
  60% {
    transform: translateY(0) scale(1.02);
  }
  70% {
    transform: scale(1.15, 0.82); /* impact — flattened onto the floor */
  }
  85% {
    transform: translateY(-3%) scale(0.96, 1.05);
  }
  100% {
    transform: none;
  }
}
/* The ground's answer to a boss landing on it: a few pixels, gone in a blink. */
@keyframes bt-stage-shake {
  0%,
  100% {
    transform: translate(0, 0);
  }
  20% {
    transform: translate(-3px, 2px);
  }
  45% {
    transform: translate(3px, -1px);
  }
  70% {
    transform: translate(-2px, 1px);
  }
}
/* Belt and braces with battle.js's own `reduced` gate: nothing lunges, nothing
   drops, nothing shakes — the fight simply opens. */
@media (prefers-reduced-motion: reduce) {
  #crypt-root .g-battle__art--pounce,
  #crypt-root .g-battle__art--slam,
  #crypt-root .g-battle__stage--slammed,
  #crypt-root .g-battle__art--pounce.is-entering,
  #crypt-root .g-battle__art--slam.is-entering,
  #crypt-root .g-battle__stage--slammed.is-entering {
    animation: none;
    transform: none;
  }
}
/* The turn banner slides down ONCE, on the fight's first turn, and then stays. */
@keyframes bt-turn-in {
  from {
    transform: translate(-50%, -130%);
    opacity: 0;
  }
  to {
    transform: translate(-50%, 0);
    opacity: 1;
  }
}
/* The mark's brand: the ringed outer element turns forever, the inner glyph
   pulses. Both live inside markRune's innerHTML (battle.js MARK_DECALS). */
@keyframes bt-mark-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
@keyframes bt-mark-pulse {
  0%,
  100% {
    transform: scale(0.94);
    opacity: 0.7;
  }
  50% {
    transform: scale(1.08);
    opacity: 1;
  }
}
.bt-mark-ring {
  transform-origin: 50% 50%;
  animation: bt-mark-spin 12s linear infinite;
}
.bt-mark-glyph {
  transform-origin: 50% 50%;
  animation: bt-mark-pulse 2s ease-in-out infinite;
}

/* battleFx.js: the attack effects. COLOUR is a parameter (inline, decided by
   attackFx.js), which is what lets seven shapes cover thirteen damage types. */
@keyframes bfx-slash {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) rotate(-32deg) scale(0.4);
  }
  18% {
    opacity: 1;
  }
  55% {
    opacity: 1;
    transform: translate(-50%, -50%) rotate(-32deg) scale(1.12);
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -50%) rotate(-32deg) scale(1.3);
  }
}
@keyframes bfx-burst {
  0% {
    opacity: 0.85;
    transform: translate(-50%, -50%) scale(0.15);
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(1.7);
  }
}
@keyframes bfx-arrow {
  0% {
    opacity: 0;
    transform: translate(-190%, -50%) rotate(7deg);
  }
  18% {
    opacity: 1;
  }
  100% {
    opacity: 0.95;
    transform: translate(-50%, -50%) rotate(7deg);
  }
}
@keyframes bfx-fire {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.3);
  }
  22% {
    opacity: 1;
  }
  65% {
    opacity: 1;
    transform: translate(-50%, -56%) scale(1.25);
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -62%) scale(1.55);
  }
}
@keyframes bfx-ember {
  0% {
    opacity: 1;
    transform: translate(0, 0) scale(1);
  }
  100% {
    opacity: 0;
    transform: translate(var(--dx), var(--dy)) scale(0.25);
  }
}
@keyframes bfx-heal {
  0% {
    opacity: 0;
    transform: translate(-50%, 14px) scale(0.6);
  }
  30% {
    opacity: 1;
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -64px) scale(1.1);
  }
}
@keyframes bfx-splat {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.25);
  }
  30% {
    opacity: 1;
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(1.4);
  }
}
@keyframes bfx-rune {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(1.5) rotate(-24deg);
  }
  28% {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1) rotate(0deg);
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.85) rotate(0deg);
  }
}
@keyframes bfx-shard {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) rotate(var(--rot)) translateY(var(--from))
      scale(0.5);
  }
  25% {
    opacity: 1;
  }
  70% {
    opacity: 1;
    transform: translate(-50%, -50%) rotate(var(--rot)) translateY(0) scale(1);
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -50%) rotate(var(--rot)) translateY(6px)
      scale(1.05);
  }
}
@keyframes bfx-arc {
  0% {
    opacity: 0;
  }
  10% {
    opacity: 1;
  }
  28% {
    opacity: 0.25;
  }
  42% {
    opacity: 1;
  }
  64% {
    opacity: 0.4;
  }
  78% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}
@keyframes bfx-cloud {
  0% {
    opacity: 0;
    transform: translate(-50%, -40%) scale(0.35);
  }
  30% {
    opacity: 0.95;
  }
  70% {
    opacity: 0.8;
    transform: translate(-50%, -52%) scale(1.15);
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -58%) scale(1.35);
  }
}
@keyframes bfx-wave {
  0% {
    opacity: 0.9;
    transform: translate(-50%, -50%) scale(0.2);
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(2.1);
  }
}
@keyframes bfx-aura {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.25);
  }
  22% {
    opacity: 0.95;
  }
  62% {
    opacity: 0.72;
    transform: translate(-50%, -50%) scale(1.06);
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(1.28);
  }
}
@keyframes bfx-sigil {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.5) rotate(-34deg);
  }
  30% {
    opacity: 0.95;
    transform: translate(-50%, -50%) scale(1) rotate(0deg);
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(1.06) rotate(20deg);
  }
}
@keyframes bfx-rise {
  0% {
    opacity: 0;
    transform: translate(-50%, 22px) scale(0.65);
  }
  35% {
    opacity: 1;
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -48px) scale(1.06);
  }
}
@keyframes bfx-sweep {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) scaleX(0.08);
  }
  24% {
    opacity: 1;
  }
  68% {
    opacity: 0.9;
    transform: translate(-50%, -50%) scaleX(1.15);
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -50%) scaleX(1.34);
  }
}
@keyframes bfx-lift {
  0% {
    opacity: 0;
    transform: translateY(12px) scale(0.55);
  }
  30% {
    opacity: 1;
  }
  100% {
    opacity: 0;
    transform: translateY(-54px) scale(1);
  }
}
/* The capstone's full-layer bloom (battleFx.js vignette): opacity only — the colour is the
 * spell's own, injected inline. */
@keyframes bfx-flash {
  0% {
    opacity: 0;
  }
  22% {
    opacity: 0.62;
  }
  100% {
    opacity: 0;
  }
}
/* ---- The signature shapes' keyframes (battleFx.js, Stage 2) -----------------
 * Same contract as everything above: opacity/transform only, colourless, custom
 * properties (--dx/--dy/--rot) carry the per-element variation. */
/* A projectile's travel: in from the left, fast-out, holding its lane (--dy). */
@keyframes bfx-dart {
  0% {
    opacity: 0;
    transform: translate(calc(-50% - 170px), calc(-50% + var(--dy, 0px)));
  }
  14% {
    opacity: 1;
  }
  100% {
    opacity: 1;
    transform: translate(-50%, calc(-50% + var(--dy, 0px)));
  }
}
/* A vertical strike/column growing DOWN from above (transform-origin 50% 0% inline).
 * The uneven opacity stops are the lightning's strobe — and a flame column's flicker. */
@keyframes bfx-column {
  0% {
    opacity: 0;
    transform: translate(-50%, -62%) scaleY(0.12);
  }
  10% {
    opacity: 1;
    transform: translate(-50%, -62%) scaleY(1);
  }
  22% {
    opacity: 0.35;
  }
  32% {
    opacity: 1;
  }
  48% {
    opacity: 0.55;
  }
  60% {
    opacity: 0.95;
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -62%) scaleY(1);
  }
}
/* A ring CLOSING — ease-in inline, so it accelerates into the shut. */
@keyframes bfx-implode {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(1.6);
  }
  18% {
    opacity: 1;
  }
  100% {
    opacity: 0.9;
    transform: translate(-50%, -50%) scale(0.12);
  }
}
/* A wisp dragged from the rim (--dx/--dy) into the centre. */
@keyframes bfx-wisp {
  0% {
    opacity: 0;
    transform: translate(var(--dx), var(--dy)) scale(1);
  }
  25% {
    opacity: 1;
  }
  100% {
    opacity: 0;
    transform: translate(0, 0) scale(0.35);
  }
}
/* A sky beam: drops fast, holds, pinches out sideways as it fades. */
@keyframes bfx-beam {
  0% {
    opacity: 0;
    transform: translate(-50%, -62%) scaleY(0.05) scaleX(1.5);
  }
  14% {
    opacity: 1;
    transform: translate(-50%, -62%) scaleY(1) scaleX(1);
  }
  62% {
    opacity: 0.95;
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -62%) scaleY(1) scaleX(0.5);
  }
}
/* A heartbeat ring: arrives fast, HOLDS, then releases outward. */
@keyframes bfx-pulsering {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.18);
  }
  14% {
    opacity: 1;
    transform: translate(-50%, -50%) scale(0.9);
  }
  46% {
    opacity: 0.85;
    transform: translate(-50%, -50%) scale(1);
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(1.55);
  }
}
/* A displacement ghost: the shape shows OFF to one side (--dx), snaps back, shows again —
 * steps(2, end) inline keeps the moves hard. */
@keyframes bfx-flicker {
  0% {
    opacity: 0;
    transform: translate(calc(-50% + var(--dx, 20px)), -50%);
  }
  30% {
    opacity: 0.6;
    transform: translate(calc(-50% + var(--dx, 20px)), -50%);
  }
  55% {
    opacity: 0.15;
    transform: translate(-50%, -50%);
  }
  80% {
    opacity: 0.5;
    transform: translate(calc(-50% - var(--dx, 20px)), -50%);
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -50%);
  }
}
/* An orbiting crescent: sweeps onward through its arc (--rot) as it grows and goes. */
@keyframes bfx-whirl {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) rotate(var(--rot, 0deg)) scale(0.5);
  }
  20% {
    opacity: 1;
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -50%) rotate(calc(var(--rot, 0deg) + 80deg)) scale(1.15);
  }
}
/* A note-glyph spiralling out and up (--dx/--dy) with its own tilt (--rot). */
@keyframes bfx-spiral {
  0% {
    opacity: 0;
    transform: translate(0, 0) rotate(0deg) scale(0.6);
  }
  25% {
    opacity: 1;
  }
  100% {
    opacity: 0;
    transform: translate(var(--dx, 40px), var(--dy, -60px)) rotate(var(--rot, 30deg)) scale(1.05);
  }
}
/* The oversized crossing crescent (--rot fixed per copy): a bigger, harder bfx-slash. */
@keyframes bfx-rend {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) rotate(var(--rot, 0deg)) scale(0.55);
  }
  22% {
    opacity: 1;
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -50%) rotate(var(--rot, 0deg)) scale(1.35);
  }
}

/* Black Tentacles: a tentacle erupting and writhing — scales up from its rooted base (paired
   with transform-origin:50% 100% inline) and sways left/right (--rot) before sinking. */
@keyframes bfx-tentacle {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) scaleY(0.08) rotate(var(--rot, 0deg));
  }
  22% {
    opacity: 1;
  }
  50% {
    transform: translate(-50%, -50%) scaleY(1.06) rotate(calc(var(--rot, 0deg) + 9deg));
  }
  72% {
    transform: translate(-50%, -50%) scaleY(0.98) rotate(calc(var(--rot, 0deg) - 7deg));
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -50%) scaleY(1.02) rotate(var(--rot, 0deg));
  }
}

/* A flame tongue licking upward: rises, wavers side to side (scaleX), and dissipates. What
   makes a fire column read as fire rather than a beam — several of these, staggered, roil. */
@keyframes bfx-flamelick {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) scaleY(0.5) scaleX(1);
  }
  25% {
    opacity: 1;
    transform: translate(-50%, -58%) scaleY(1.1) scaleX(0.82);
  }
  55% {
    transform: translate(-50%, -66%) scaleY(0.9) scaleX(1.14);
  }
  80% {
    opacity: 0.8;
    transform: translate(-50%, -74%) scaleY(1.06) scaleX(0.9);
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -82%) scaleY(1.2) scaleX(0.8);
  }
}

/* Quivering Palm's fist: a punch that overshoots in, settles, and fades — --dx is the jab's
   left/right offset so the two quick jabs land off-centre and the killing blow square on. */
@keyframes bfx-fist {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) translateX(var(--dx, 0px)) scale(0.4);
  }
  30% {
    opacity: 1;
    transform: translate(-50%, -50%) translateX(var(--dx, 0px)) scale(1.12);
  }
  60% {
    transform: translate(-50%, -50%) translateX(var(--dx, 0px)) scale(0.92);
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -50%) translateX(var(--dx, 0px)) scale(1.04);
  }
}

/* The nature motif: a leaf that flutters down, swaying and spinning as it falls. The one
   particle motion the shapes never had (embers fly straight out, motes rise straight up) —
   a leaf drifts, so it needs its own sway. --dx is the drift, --sp the spin. */
@keyframes bfx-leaf {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) translateY(-8px) rotate(0deg);
  }
  18% {
    opacity: 1;
  }
  50% {
    transform: translate(-50%, -50%) translate(calc(var(--dx, 0px) * 0.5), 44px) rotate(calc(var(--sp, 180deg) * 0.5));
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -50%) translate(var(--dx, 0px), 94px) rotate(var(--sp, 340deg));
  }
}

/* ---- The conversation panel (spaces/dialogue.js, P7) -----------------------
 * Signs, linear NPC dialogue and branching conversations all draw ONE shape
 * inside the shared overlay shell: a speaker plate (face + name), the spoken
 * line, and the controls under it. The point of the exercise is that when Joe
 * finally authors a conversation it lands in a room that already looks like an
 * RPG talking to you, rather than in a modal form.
 *
 * The SHELL itself changes shape for these panels — see .g-shell--dialogue at
 * the end of this section: a phone gets a bottom-anchored partial sheet with a
 * see-through backdrop, so the NPC stays on screen above their own words. */
#crypt-root .g-dlg {
  display: flex;
  flex-direction: column;
  gap: 0.875rem;
}

/* The plate. Right padding clears the shell's absolute close chip (top-3
   right-3), which floats over everything in the panel. */
#crypt-root .g-dlg__plate {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0 2.75rem 0.625rem 0;
  border-bottom: 1px solid var(--g-edge-soft);
}
#crypt-root .g-dlg__name {
  font-family: Cinzel, serif;
  font-size: 1.25rem;
  line-height: 1.75rem;
  color: var(--g-gold-bright);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The face: a lettered plate, with an authored portrait laid OVER it when the
   data carries one (the <img> removes itself if the file 404s, revealing the
   letter again — the same fallback game/icons.js draws for classes). */
#crypt-root .g-dlg__face {
  position: relative;
  flex-shrink: 0;
  width: 3.25rem;
  height: 3.25rem;
  border-radius: var(--g-radius);
  border: 1px solid var(--g-edge-strong);
  background-color: var(--g-surface-2);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
#crypt-root .g-dlg__letter {
  font-family: Cinzel, serif;
  font-size: 1.5rem;
  line-height: 1;
  color: var(--g-gold-ink);
}
#crypt-root .g-dlg__portrait {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center top; /* portraits are full-figure — crop to the face */
}
/* The PLAYER's own face (game/icons.js classPortrait), appended into the CHOICES footer
   beside the replies — in GAME MODE only. It sat top-right of the name plate first, where
   it read as a second speaker; beside the things you might say it labels the column as
   yours. It reuses the .g-dlg__face box, but the node IS the portrait (an <img>, or its
   letter-chip <span>), so it carries the crop itself rather than laying an overlay.
   Taken out of the footer's flow and pinned to the left of the choice lane, vertically
   centred against however many replies there are — see .g-dlg__foot--choices, which
   opens the lane only when a face is actually in there. Hidden on the phone's PORTRAIT
   sheet, which has no width to spare; landscape gets it back at 2rem. */
#crypt-root .g-dlg__face--player {
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  margin-left: 0;
  object-fit: cover;
  object-position: center top;
  font-size: 1.5rem; /* the letter-chip fallback, matching .g-dlg__letter */
}

/* The spoken line. `pre-line` keeps an author's own breaks; the min-height stops
   the controls jumping between a one-line and a two-line page. */
#crypt-root .g-dlg__body {
  position: relative;
  padding-right: 1.25rem; /* the continue caret's lane */
}
#crypt-root .g-dlg__text {
  color: var(--g-ink-hi);
  font-size: 1.0625rem;
  line-height: 1.6;
  white-space: pre-line;
  min-height: 3.2rem;
}
/* A sign is read, not spoken: quieter ink, no minimum, and a plate with no face. */
#crypt-root .g-dlg--sign .g-dlg__text {
  color: var(--g-ink-mid);
  font-size: 1rem;
  min-height: 0;
}

/* The continue affordance: a caret that breathes while there is another page to
   turn. Opacity/transform only (the Firefox rule), and stilled under reduced
   motion — where it stays visible, because it is information, not decoration. */
#crypt-root .g-dlg__more {
  position: absolute;
  right: 0;
  bottom: 0.125rem;
  color: var(--g-gold);
  font-size: 1rem;
  line-height: 1;
  animation: g-dlg-more 1.6s ease-in-out infinite;
}
@keyframes g-dlg-more {
  0%,
  100% {
    opacity: 0.3;
    transform: translateX(0);
  }
  50% {
    opacity: 0.95;
    transform: translateX(3px);
  }
}
@media (prefers-reduced-motion: reduce) {
  #crypt-root .g-dlg__more {
    animation: none;
    opacity: 0.8;
  }
}

/* The pager row: Back and the page count on the left, Trade/Next on the right. */
#crypt-root .g-dlg__foot--pager {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
#crypt-root .g-dlg__count {
  margin-right: auto;
  color: var(--g-ink-low);
  font-size: 0.8125rem;
  line-height: 1.25rem;
}
#crypt-root .g-dlg__nav {
  padding: 0.5rem 1.125rem;
  min-height: 2.5rem;
}
/* Trade is the ENGINE's choice, not one the author wrote: gold-outlined in both
   layouts so it never passes for a line of the conversation. */
#crypt-root .g-dlg__trade {
  background-color: transparent;
  border: 1px solid var(--g-gold);
  color: #fde68a; /* amber-200 */
}
#crypt-root .g-dlg__trade:hover:not(:disabled):not([aria-disabled="true"]) {
  background-color: rgba(41, 37, 36, 0.7);
}

/* The choice: full-width, tap-sized, left-aligned buttons — a list of things you
   might SAY, not a button bar. `position: relative` is the anchor for the player's
   face, which is pinned into the left margin of this block (see --player above). */
#crypt-root .g-dlg__foot--choices {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
#crypt-root .g-dlg__back {
  align-self: flex-start; /* re-reading is an aside, not an answer */
}
#crypt-root .g-choice {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  width: 100%;
  text-align: left;
  padding: 0.625rem 0.875rem;
  min-height: 2.75rem;
  border-radius: var(--g-radius);
  border: 1px solid var(--g-edge);
  background-color: rgba(28, 25, 23, 0.6); /* stone-900/60 */
  color: var(--g-ink-hi);
  font-size: 0.9375rem;
  line-height: 1.35rem;
  transition-property: color, background-color, border-color;
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  transition-duration: 150ms;
}
#crypt-root .g-choice::before {
  content: "◆";
  flex-shrink: 0;
  font-size: 0.5rem;
  line-height: 1;
  color: var(--g-gold-dim);
}
#crypt-root .g-choice:hover:not(:disabled) {
  border-color: var(--g-gold);
  background-color: rgba(41, 37, 36, 0.8);
  color: var(--g-gold-ink);
}
#crypt-root .g-choice:hover:not(:disabled)::before {
  color: var(--g-gold);
}
#crypt-root .g-choice:disabled {
  opacity: 0.5;
}
#crypt-root .g-choice--trade {
  border-color: var(--g-gold);
  color: #fde68a; /* amber-200 */
  font-weight: 600;
}
#crypt-root .g-choice--trade::before {
  color: var(--g-gold);
}

/* ---- .g-shell--dialogue: the panel's shape ---------------------------------
 * Set by every renderer in spaces/dialogue.js as it draws, cleared by
 * crypt/interaction.js (openPanel + close) — the same "a panel asks the shell
 * for its shape" mechanism as .g-shell--wide, with the lifecycle owning the
 * cleanup so an Escape can never leave the next panel wearing this shape.
 *
 * Desktop: a card sized to two lines of speech instead of the shared max-w-2xl
 * barn. Media-gated exactly like .g-shell--wide so it cannot fight the phone
 * rules below. */
@media (min-width: 768px) and (min-height: 501px) {
  /* Anchor the card NEAR the bottom (not centred over the scene) and ease the backdrop, so the NPC
     you are speaking to stays visible from the head down — the same reason the phone sheet sits at
     the bottom. A margin keeps it clear of the very edge rather than stuck to it. */
  #crypt-root #crypt-overlay:has(#crypt-scroll-shell.g-shell--dialogue) {
    align-items: flex-end;
    padding-bottom: 3rem;
    background-color: rgba(12, 10, 9, 0.32);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  #crypt-root #crypt-scroll-shell.g-shell--dialogue {
    max-width: 34rem;
  }
  #crypt-root #crypt-scroll-shell.g-shell--dialogue #crypt-scroll-content {
    padding: 1.25rem 1.5rem 1.375rem;
  }
  /* The lane the player's face sits in — opened ONLY when a face is actually there, so an
     explore visit, a builder Play preview and a class-less intro keep the replies flush
     left instead of indenting past an empty gutter. The min-height gives a lone one-line
     reply something to centre a 3.25rem face against. */
  #crypt-root .g-dlg__foot--choices:has(> .g-dlg__face--player) {
    padding-left: 4rem;
    min-height: 3.25rem;
  }
}

/* Phones (and landscape phones): a BOTTOM-anchored partial sheet. The whole
   point is that the person you are talking to stays on screen — so the backdrop
   drops its dimming and its blur for this variant only, and the sheet takes a
   little over half the height, sized to its content under that cap. P1's
   slide-up animation and full-bleed sheet rules still apply; all that changes
   is where it stops. */
@media (max-width: 767px), (pointer: coarse) and (max-height: 500px) {
  #crypt-root #crypt-overlay:has(#crypt-scroll-shell.g-shell--dialogue) {
    align-items: flex-end;
    background-color: rgba(12, 10, 9, 0.32);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  #crypt-root #crypt-scroll-shell.g-shell--dialogue {
    max-height: 58%;
    box-shadow: 0 -14px 36px rgba(0, 0, 0, 0.55);
  }
  #crypt-root #crypt-scroll-shell.g-shell--dialogue #crypt-scroll-content {
    padding: 1rem 1rem calc(1rem + env(safe-area-inset-bottom, 0px));
  }
  /* Thumb-sized controls down here, and a face that doesn't eat the sheet. */
  #crypt-root .g-dlg__face {
    width: 2.75rem;
    height: 2.75rem;
  }
  /* PORTRAIT phone: the sheet is only as wide as the phone, and a left lane would eat the
     reply labels — so the player's face is dropped and only the NPC has one. Landscape
     takes it back below (wider sheet, smaller face). */
  #crypt-root .g-dlg__face--player {
    display: none;
  }
  #crypt-root .g-dlg__letter {
    font-size: 1.25rem;
  }
  #crypt-root .g-dlg__name {
    font-size: 1.125rem;
  }
  #crypt-root .g-dlg__text {
    font-size: 1rem;
  }
  #crypt-root .g-dlg__nav {
    min-height: 2.75rem;
  }
  /* The continuation stretches into the space Back and the counter leave: the
     biggest thumb target in the sheet, and no longer a small button parked in
     the exact corner the site's floating bug-report chip covers. */
  #crypt-root .g-dlg__nav--go {
    flex: 1 1 auto;
  }
}

/* Landscape phone: the dialogue sheet is full-bleed WIDE (no max-width cap applies — the desktop cap
   is gated to min-height:501), so the portrait rule above stretches the single "Close"/"Next" button
   across the whole row. Cap it back to a sane size in landscape only; portrait keeps the thumb-wide
   button (it matches max-width:767 but not this max-height:500 query). */
@media (pointer: coarse) and (max-height: 500px) {
  #crypt-root .g-dlg__nav--go {
    flex: 0 1 auto;
    max-width: 14rem;
  }
  /* Landscape has little vertical room, so the shared mobile dialogue (2.75rem portrait header +
     3.2rem text floor + a COLUMN of full-width choice buttons, capped at 58% height) overflows into a
     scrollbar. Give it more room and pack it tighter for landscape only (portrait keeps the shared
     block above): a taller sheet, a smaller header, no text floor, and choices side-by-side. */
  #crypt-root #crypt-scroll-shell.g-shell--dialogue {
    max-height: 80%;
  }
  #crypt-root #crypt-scroll-shell.g-shell--dialogue #crypt-scroll-content {
    padding: 0.625rem 0.875rem calc(0.625rem + env(safe-area-inset-bottom, 0px));
  }
  #crypt-root .g-dlg {
    gap: 0.5rem;
  }
  #crypt-root .g-dlg__face {
    width: 2rem;
    height: 2rem;
  }
  #crypt-root .g-dlg__letter {
    font-size: 1rem;
  }
  #crypt-root .g-dlg__plate {
    padding-bottom: 0.375rem;
  }
  #crypt-root .g-dlg__name {
    font-size: 1rem;
  }
  #crypt-root .g-dlg__text {
    min-height: 0; /* a one-line reply stops reserving 3.2rem */
  }
  /* Choices beside each other instead of a full-width column: two per row, wrapping when a label is
     long or the count is odd. */
  #crypt-root .g-dlg__foot--choices {
    flex-flow: row wrap;
  }
  #crypt-root .g-dlg__foot--choices .g-choice {
    width: auto;
    flex: 1 1 45%;
    min-width: 12rem;
  }
  /* The Back re-read button stays on its own full row above the choices. */
  #crypt-root .g-dlg__foot--choices .g-dlg__back {
    flex: 1 0 100%;
  }
  /* Landscape is wide enough for the player's face after all, at the 2rem the block above
     sizes every face to. The extra class beats the portrait rule's `display:none` (which
     this query also matches — it is `max-width:767 OR coarse+short`), so the child
     selector here is load-bearing, not decoration; `flex` is what .g-dlg__face itself
     sets, so the letter-chip fallback keeps centring its initial. */
  #crypt-root .g-dlg__foot--choices > .g-dlg__face--player {
    display: flex;
  }
  #crypt-root .g-dlg__foot--choices:has(> .g-dlg__face--player) {
    padding-left: 2.5rem;
    min-height: 2rem;
  }
}

/* ---- The quest journal (game/journalUI.js, P7) -----------------------------
 * Two lists of cards. An active quest shows where it stands on its stage ladder
 * (the stages are already in the game doc, and "which of these am I on" is the
 * question a journal exists to answer); a finished one dims behind its seal. */
#crypt-root .g-journal {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
#crypt-root .g-journal__title {
  font-family: Cinzel, serif;
  font-size: 1.5rem;
  line-height: 2rem;
  color: var(--g-gold-bright);
  text-align: center;
  margin-bottom: 0.25rem;
}
#crypt-root .g-journal__section {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  margin-top: 0.5rem;
  padding-bottom: 0.25rem;
  border-bottom: 1px solid var(--g-edge-soft);
}
#crypt-root .g-journal__section-name {
  font-family: Cinzel, serif;
  font-size: 1rem;
  line-height: 1.5rem;
  color: var(--g-gold-ink);
}
#crypt-root .g-journal__count {
  margin-left: auto;
  font-size: 0.75rem;
  color: var(--g-ink-low);
}
#crypt-root .g-journal__list {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

/* One quest. The gold spine is the "this is live" tell you can read at a glance
   down the column. */
#crypt-root .g-quest {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  border-left: 2px solid var(--g-gold-dim);
}
#crypt-root .g-quest__head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
#crypt-root .g-quest__name {
  font-family: Cinzel, serif;
  font-size: 1rem;
  line-height: 1.5rem;
  color: var(--g-gold-ink);
}
#crypt-root .g-quest__line {
  color: var(--g-ink-mid);
  font-size: 0.875rem;
  line-height: 1.35rem;
}
#crypt-root .g-quest__rail {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.125rem;
}
#crypt-root .g-quest__pips {
  display: flex;
  gap: 0.25rem;
}
#crypt-root .g-quest__pip {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 9999px;
  border: 1px solid var(--g-edge-soft);
  background-color: var(--g-surface-2);
}
#crypt-root .g-quest__pip.is-on {
  background-color: var(--g-gold);
  border-color: var(--g-gold);
}
#crypt-root .g-quest__step {
  font-size: 0.6875rem;
  line-height: 1rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--g-ink-low);
}
#crypt-root .g-quest.is-done {
  opacity: 0.6;
  border-left-color: var(--g-surface-3);
}
#crypt-root .g-quest__seal {
  margin-left: auto;
  flex-shrink: 0;
  color: var(--g-heal);
  font-size: 0.8125rem;
  line-height: 1;
}
#crypt-root .g-journal__empty {
  text-align: center;
  padding: 2rem 1rem;
}
#crypt-root .g-journal__empty-line {
  font-family: Cinzel, serif;
  font-size: 1rem;
  line-height: 1.5rem;
  color: var(--g-ink-low);
}
#crypt-root .g-journal__empty-hint {
  margin-top: 0.375rem;
  font-size: 0.8125rem;
  color: #78716c; /* stone-500 */
}

/* ---- The death screen (game/deathScreen.js, P7) ----------------------------
 * The fade, the smoke and the flavour line were already the most atmospheric
 * thing in the game; this is only the type and the two buttons. Everything the
 * module still writes inline is the stuff it ANIMATES (the opacity ramps) —
 * what lives here is what never changes frame to frame. */
#crypt-root .g-death__content {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(1rem, 4vh, 1.5rem);
  max-width: 36rem;
  box-sizing: border-box;
  padding: 0 1.25rem;
  /* The two buttons must never sit under a home indicator. */
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
#crypt-root .g-death__title {
  font-family: Cinzel, serif;
  /* A phone is 412px wide and this is the loudest text in the game — let it
     shrink rather than shout. */
  font-size: clamp(1.5rem, 6vw, 1.875rem);
  line-height: 1.2;
  letter-spacing: 0.025em;
  color: var(--g-ink-mid);
  text-align: center;
  text-shadow: 0 2px 14px #000;
}
#crypt-root .g-death__flavour {
  font-family: Cinzel, serif;
  font-style: italic;
  font-size: clamp(0.9rem, 3.6vw, 1.05rem);
  line-height: 1.5;
  color: var(--g-ink-low);
  text-align: center;
  max-width: 34rem;
  text-shadow: 0 2px 10px #000;
}
#crypt-root .g-death__row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 1rem;
}
#crypt-root .g-death__cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.45rem;
}
/* Bigger than a panel button: these two are the only things on the screen. */
#crypt-root .g-death__act {
  padding: 0.625rem 1.5rem;
  font-size: 1rem;
  line-height: 1.5rem;
  min-height: 2.75rem;
}
#crypt-root .g-death__hint {
  font-family: Cinzel, serif;
  font-size: 0.75rem;
  line-height: 1rem;
  color: #78716c; /* stone-500 */
}
#crypt-root .g-death__key {
  padding: 0.125rem 0.375rem;
  border-radius: 0.25rem;
  background-color: rgba(41, 37, 36, 0.9); /* stone-800/90 */
  border: 1px solid #57534e; /* stone-600 */
  color: #fde68a; /* amber-200 */
}

/* ---- The floor-map popup's chrome (crypt/minimap.js + crypt/plinthUI.js, P7)
 * CHROME ONLY. Nothing in here touches the parchment canvas, the pan/zoom
 * transforms or the touch-action the gestures depend on — the viewer keeps
 * those inline, where the pointer handlers can see them. This is the frame
 * around the chart, the fast-travel rail beside it, and the hint pill.
 * (Replaces the .crypt-map-wrap / .crypt-year-* blocks that were copy-pasted
 * into hall.html and mausoleum.html.) */
#crypt-root .g-map-viewer {
  position: relative;
  overflow: hidden;
  height: min(62vh, 640px);
  border-radius: var(--g-radius);
  border: 1px solid var(--g-edge-strong);
  background-color: rgba(12, 10, 8, 0.35);
  box-shadow: inset 0 0 22px rgba(0, 0, 0, 0.55); /* the chart sits IN a frame */
}
#crypt-root .g-map-hint {
  position: absolute;
  left: 50%;
  bottom: 10px;
  transform: translateX(-50%);
  z-index: 2;
  padding: 0.25rem 0.75rem;
  border-radius: 9999px;
  background-color: rgba(12, 10, 9, 0.78);
  border: 1px solid var(--g-edge-soft);
  color: var(--g-ink-low);
  font-size: 0.75rem;
  letter-spacing: 0.05em;
  white-space: nowrap;
  pointer-events: none;
  transition: opacity 600ms;
}
@media (prefers-reduced-motion: reduce) {
  #crypt-root .g-map-hint {
    transition: none;
  }
}
#crypt-root .g-map-wrap {
  display: flex;
  gap: 1rem;
  align-items: stretch;
  min-height: 0;
}
/* Mobile layout: viewer on top, fast-travel dropdown underneath. The viewer
   gives up some height so the dropdown stays above the fold. */
#crypt-root .g-map-wrap--column {
  flex-direction: column;
  align-items: stretch;
}
#crypt-root .g-map-wrap--column .g-map-viewer {
  height: min(46vh, 460px);
}
/* max-height matches the viewer's, so the year list scrolls INSIDE the rail
   instead of stretching the whole popup. */
#crypt-root .g-map-rail {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  flex-shrink: 0;
  overflow-y: auto;
  padding-right: 0.35rem;
  max-height: min(62vh, 640px);
}
#crypt-root .g-map-rail__head {
  font-family: Cinzel, serif;
  font-size: 0.8rem;
  letter-spacing: 0.08em;
  color: var(--g-gold-bright);
  margin-bottom: 0.15rem;
  padding-left: calc(
    0.9rem + 1px
  ); /* shares a left edge with the buttons' text */
}
#crypt-root .g-map-rail__hint {
  font-size: 0.65rem;
  line-height: 1.35;
  color: var(--g-ink-low);
  max-width: 7rem;
  padding-left: calc(0.9rem + 1px);
  margin-bottom: 0.3rem;
}
#crypt-root .g-map-rail__hint.is-wide {
  max-width: none;
  padding-left: 0.1rem;
  margin: 0.15rem 0 0;
}
/* A year button: the .g-btn face, gone quiet and left-aligned. Declared after
   .g-btn (and matching its :not() specificity) so these win. */
#crypt-root .g-map-year {
  font-family: Cinzel, serif;
  font-size: 0.9rem;
  text-align: left;
  padding: 0.35rem 0.9rem;
  background-color: transparent;
  border: 1px solid var(--g-edge);
  color: var(--g-ink-mid);
  transition-property: color, background-color, border-color;
}
#crypt-root .g-map-year:hover:not(:disabled):not([aria-disabled="true"]) {
  background-color: transparent;
  color: var(--g-gold-bright);
  border-color: rgba(180, 83, 9, 0.6);
}
#crypt-root .g-map-year.is-current {
  color: var(--g-gold-bright);
  border-color: rgba(180, 83, 9, 0.8);
  background-color: rgba(120, 53, 15, 0.25);
}
#crypt-root .g-map-select {
  width: 100%;
  padding: 0.55rem 0.9rem;
  font-family: Cinzel, serif;
  font-size: 0.95rem;
  color: var(--g-ink-mid);
  cursor: pointer;
}
/* The district waystone's fast-travel list: a plain column of year-styled
   buttons, centred in the standard-width scroll. */
#crypt-root .g-map-districts {
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
  max-width: 26rem;
  margin: 0 auto;
}

/* ---- .g-sheet-handle: the phone sheet's drag bar (crypt/sheetDrag.js, P8) ---
 * P1 turned the overlay shell and the settings card into full-height sheets on
 * phones, but left them with one way out: a chip in the far top-right corner.
 * This is the affordance that says "push me back down" — and the only surface
 * the drag listens to, so the content underneath keeps scrolling normally.
 *
 * DESKTOP RENDERS NOTHING: display:none outside the sheet media query, which is
 * why the desktop gallery is byte-identical and the dialogue CARD (a centred
 * panel above 767px) grows no handle it could not usefully drag. */
#crypt-root .g-sheet-handle {
  display: none;
}
@media (max-width: 767px), (pointer: coarse) and (max-height: 500px) {
  #crypt-root .g-sheet-handle {
    display: flex;
    flex-shrink: 0;
    align-items: center;
    justify-content: center;
    height: 2.5rem; /* a 40px thumb target, not just the 4px pill it draws */
    touch-action: none; /* the browser must not steal the drag as a page pan */
    cursor: grab;
    -webkit-tap-highlight-color: transparent;
  }
  #crypt-root .g-sheet-handle::after {
    content: "";
    width: 2.5rem;
    height: 0.25rem;
    border-radius: 9999px;
    background-color: var(--g-edge-strong);
  }
  #crypt-root .g-sheet-handle:active {
    cursor: grabbing;
  }
  /* The settings card is itself the scroll container (overflow-y-auto p-5), so
     its handle STICKS to the top of the scrollport and spans the padding —
     otherwise the one grabbable thing scrolls away with the content. The card
     gives up its top padding to it (the handle's own 2.5rem is the spacing);
     pulling the handle up with a negative margin instead let the heading ride
     under it, which the first mobile gallery run caught. */
  #crypt-root #crypt-settings-body {
    padding-top: 0; /* the about card keeps its padding — it has no handle */
  }
  #crypt-root .g-sheet-handle--sticky {
    position: sticky;
    top: 0;
    z-index: 3;
    margin: 0 -1.25rem;
    background-color: var(--g-surface-1);
  }
  /* The settings sub-menu tabs stick DIRECTLY UNDER that handle (its 2.5rem is the
     offset), so you can change pane without scrolling back up a long card. Same
     negative side margins as the handle, so the strip spans the card's padding and
     its border reads as a full-width rule. Deliberately NOT the character sheet's
     bottom-tabs treatment (.g-sheet__tabs order: 4) — that sheet is a flex column
     with its own pane scroller, while this card is one plain scroller. */
  #crypt-root .g-settabs {
    position: sticky;
    top: 2.5rem;
    z-index: 2;
    margin-left: -1.25rem;
    margin-right: -1.25rem;
    padding: 0.25rem 1.25rem;
    background-color: var(--g-surface-1);
    border-bottom: 1px solid var(--g-edge-soft);
    overflow-x: auto;
  }
}
/* Landscape: the handle costs 40px of a 243px sheet for a 4px pill. It stays
   grabbable — the pill is unchanged and the bar still spans the full width, which is
   what the thumb actually aims at — but at half the height. The sticky settings
   variant's `top` offsets follow it down. */
@media (pointer: coarse) and (max-height: 500px) {
  #crypt-root .g-sheet-handle {
    height: 1.25rem;
  }
  #crypt-root .g-settabs {
    top: 1.25rem;
  }
}

/* ---- .g-hud-map: the minimap widget (game/minimapWidget.js, P8) ------------
 * A small square plan top-right, one lane below the quest chip, DESKTOP ONLY:
 * a phone viewport has no room for it (the map button is the phone's answer),
 * so the setting defaults off and its row is hidden on coarse pointers — this
 * rule is the last word on it, whatever a settings blob synced from a desktop
 * says. Everything inside is one cached parchment canvas, CSS-scaled and
 * translated to keep the player centred; only the arrow is drawn by the DOM. */
#crypt-root .g-hud-map {
  position: absolute;
  /* Under the character-stats cluster on the LEFT (confirmed layout), not top-right: the stats
     chip anchors the top-left, and the minimap reads as part of the same cluster below it. */
  left: 0.75rem;
  top: calc(
    0.75rem + 3.4rem + 0.5rem
  ); /* hero top + hero-chip lane + gap — clears the chip */
  width: var(--g-hud-col);
  height: var(--g-hud-col);
  pointer-events: auto;
  overflow: hidden;
  border-radius: var(--g-radius);
  border: 1px solid var(--g-edge-soft);
  background-color: rgba(12, 10, 9, 0.7);
  box-shadow: inset 0 0 22px rgba(0, 0, 0, 0.55);
  cursor: pointer;
  padding: 0;
}
#crypt-root .g-hud-map:hover {
  border-color: var(--g-edge);
}
/* The plan itself: positioned by transform only (translate + scale), never by
   layout, so a step costs one composited write and no reflow. */
#crypt-root .g-hud-map__plan {
  position: absolute;
  left: 0;
  top: 0;
  transform-origin: 0 0;
  max-width: none; /* Tailwind preflight's canvas max-width:100% would fight the scale */
  will-change: transform;
  pointer-events: none;
}
/* The player: a blood-ink arrow over the plan, rotated to the heading — the
   same colour as the full map's marker. The widget places it (translate) rather
   than CSS pinning it to the centre, because near a map edge the pan is clamped
   to keep the frame covered and the player is legitimately off-centre. The
   margins put the triangle's middle on the translated point. */
#crypt-root .g-hud-map__arrow {
  position: absolute;
  left: 0;
  top: 0;
  width: 0;
  height: 0;
  margin: -0.5rem 0 0 -0.4375rem;
  border-left: 0.4375rem solid transparent;
  border-right: 0.4375rem solid transparent;
  border-bottom: 0.875rem solid rgba(122, 47, 35, 0.92);
  transform-origin: 50% 66%;
  pointer-events: none;
}
/* The monsters: small red pips over the plan, one per living monster on this storey (the
   map's `minimapEnemies` setting decides which). Placed by transform exactly as the arrow
   is — same plan-pixel space, same lerp — so a patroller's dot glides with the plan under
   it and costs no layout. The dark ring keeps a pip legible over the parchment's ink. */
#crypt-root .g-hud-map__dot {
  position: absolute;
  left: 0;
  top: 0;
  width: 5px;
  height: 5px;
  margin: -2.5px 0 0 -2.5px; /* the translated point is the pip's CENTRE, not its corner */
  border-radius: 50%;
  background: var(--g-danger);
  box-shadow: 0 0 0 1px rgba(12, 10, 9, 0.7);
  pointer-events: none;
}
/* No minimap on a phone, whatever the stored setting says. */
@media (pointer: coarse) {
  #crypt-root .g-hud-map {
    display: none !important;
  }
}
