/* Self-hosted, not a CDN: a cross-origin font request is returned early by the
   service worker (sw.js leaves cross-origin to the browser), so an offline
   launch would silently fall back to system fonts. */
@font-face {
  font-family: 'DM Sans'; font-style: normal; font-weight: 400; font-display: swap;
  src: url('./fonts/dm-sans-400.woff2') format('woff2');
}
@font-face {
  font-family: 'DM Sans'; font-style: normal; font-weight: 700; font-display: swap;
  src: url('./fonts/dm-sans-700.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 400; font-display: swap;
  src: url('./fonts/plex-mono-400.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 600; font-display: swap;
  src: url('./fonts/plex-mono-600.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 700; font-display: swap;
  src: url('./fonts/plex-mono-700.woff2') format('woff2');
}

*, *::before, *::after { box-sizing: border-box; }

/* viewport-fit=cover + black-translucent means content sits under the notch and
   home indicator. Without these insets the top of the UI is unreachable.

   height (not min-height) + overflow:hidden because this app never scrolls: the
   playfield is sized to whatever #stage measures, and a scrollable body would
   let a mis-registered drag pan the page out from under the joystick. The dvh
   line wins where supported and is ignored where not; it only matters in a
   browser tab, since an installed standalone PWA has no URL bar to collapse. */
html { height: 100%; overscroll-behavior: none; }
body {
  margin: 0;
  padding-top: var(--safe-top);
  padding-bottom: var(--safe-bottom);
  padding-left: var(--safe-left);
  padding-right: var(--safe-right);
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  overscroll-behavior: none;
  display: flex;
  flex-direction: column;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-prose);
  line-height: 1.55;
  -webkit-text-size-adjust: 100%;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}

/* ── app bar ─────────────────────────────────────────────────────────── */
.app-bar {
  position: sticky; top: 0; z-index: 10;
  display: flex; align-items: center; gap: var(--gap);
  padding: var(--gap) 1rem;
  padding-top: calc(var(--gap) + var(--safe-top));
  margin-top: calc(-1 * var(--safe-top));
  background: color-mix(in srgb, var(--color-bg) 88%, transparent);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--color-border);
}
.app-bar { flex: none; }
.app-title {
  margin: 0;
  font-family: var(--font-data); font-size: 1rem; font-weight: 700;
  letter-spacing: 0.02em; color: var(--color-accent-green);
}

/* ── controls ────────────────────────────────────────────────────────── */
.btn {
  min-height: var(--tap); min-width: var(--tap);
  padding: 0 1rem;
  border: 1px solid transparent; border-radius: var(--radius);
  background: var(--color-accent-blue); color: #06101f;
  font-family: var(--font-prose); font-size: 0.95rem; font-weight: 700;
  cursor: pointer;
}
.btn:hover { filter: brightness(1.08); }
.btn:disabled { opacity: 0.5; cursor: default; }
.btn-ghost {
  background: transparent; color: var(--color-text);
  border-color: var(--color-border); font-weight: 400;
}
.btn-ghost:hover { background: var(--color-surface); }
:where(button, input, a):focus-visible {
  outline: 2px solid var(--color-accent-green); outline-offset: 2px;
}

.row { display: flex; gap: var(--gap); align-items: center; margin: var(--gap) 0; }
.row-end { justify-content: flex-end; }
.row-center { justify-content: center; }
.muted { color: var(--color-text-dim); font-size: 0.9rem; }

/* ── banner + toast ──────────────────────────────────────────────────── */
/* Anchored to the TOP, unlike the template's bottom default. The joystick lives
   in the bottom-left and the rest of the screen is the pump-tap target, so a
   bottom-pinned banner would sit on the stick — and the iOS install hint shows
   on every pre-install visit, i.e. for a new player's entire first session. */
.banner, .toast {
  position: fixed; left: 1rem; right: 1rem; z-index: 20;
  top: calc(1rem + var(--safe-top));
  display: flex; align-items: center; gap: var(--gap);
  padding: 0.75rem 1rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border); border-radius: var(--radius);
  box-shadow: 0 8px 28px rgb(0 0 0 / 45%);
}
.banner[hidden], .toast[hidden] { display: none; }
.banner-text, .toast-text { flex: 1; margin: 0; font-size: 0.92rem; }
.toast { border-color: var(--color-accent-green); }

@media (min-width: 34rem) {
  .banner, .toast { left: auto; right: 1rem; max-width: 26rem; }
}

/* ── game ────────────────────────────────────────────────────────────── */
/* touch-action:none is not optional and not cosmetic. Without it the browser
   spends the first ~100ms of a drag deciding whether the gesture is a pan, and
   if it decides yes it claims the pointer and STOPS DELIVERING pointermove for
   the rest of that touch. The symptom is a joystick that works for a fraction
   of a second and then freezes with the player still walking. It is also what
   actually kills pinch and double-tap zoom on iOS >= 10, which ignores
   user-scalable=no. Scoped to the playfield so the overlay cards stay zoomable. */
#stage {
  position: relative;
  flex: 1;
  min-height: 0;
  touch-action: none;
}
#game {
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;
  background: var(--color-bg);
}

.hud {
  display: flex; flex: 1; gap: 0.9rem; justify-content: flex-end;
  font-family: var(--font-data); font-size: 0.8rem; line-height: 1.2;
  font-variant-numeric: tabular-nums;
}
.hud-cell { display: flex; flex-direction: column; align-items: flex-end; }
.hud-key { font-size: 0.6rem; font-weight: 400; letter-spacing: 0.08em; color: var(--color-text-dim); }

.overlay {
  position: absolute; inset: 0; z-index: 15;
  display: flex; align-items: center; justify-content: center;
  padding: 1rem;
  background: color-mix(in srgb, var(--color-bg) 82%, transparent);
  backdrop-filter: blur(3px);
}
.overlay[hidden] { display: none; }
.overlay-card {
  width: 100%; max-width: 20rem; padding: 1.25rem;
  text-align: center;
  background: var(--color-surface);
  border: 1px solid var(--color-border); border-radius: var(--radius);
  box-shadow: 0 12px 40px rgb(0 0 0 / 55%);
}
.overlay-title {
  margin: 0 0 0.4rem;
  font-family: var(--font-data); font-size: 1.5rem; letter-spacing: 0.04em;
  color: var(--color-accent-blue);
}
.overlay-body { margin: 0 0 var(--gap); }

/* ── the surface menu ─────────────────────────────────────────────────────
   Everything you do between runs. Sits UNDER a window onto the camp, which
   keeps drawing on the canvas behind as scenery rather than as the interface.

   The top strip and the Descend button are pinned and the middle scrolls, so
   the two things you always want — how deep you have been, and the way out —
   never scroll away. `min-height: 0` on the scroller is what actually lets it
   scroll inside a flex column; without it the panel grows and pushes the
   button off the bottom of the stage, where nothing can reach it.

   The menu itself is now TRANSPARENT and carries no backdrop-filter. It used to
   be `inset: 0` under one 72–96% gradient with a 2px blur, which meant the camp
   behind it was, in practice, never seen — and a camp you cannot see cannot be
   the reward for buying anything. The backdrop moved to .menu-body so it starts
   below the band, and the blur went with it: blurring the whole overlay blurs
   the diorama, which is the one thing on this screen that is meant to be
   looked at. */
.menu {
  position: absolute; inset: 0; z-index: 15;
  display: flex; flex-direction: column;
  background: none;
}
.menu[hidden] { display: none; }

/* The window onto the camp. Empty by design — the picture is the canvas behind
   it. Its height is set inline from base.campHeight(), the same call basecamp.js
   sizes its drawing to. It still swallows taps: it is scenery, not a control. */
.menu-camp {
  flex: none;
  height: var(--camp-h, 190px);
}

/* Everything below the window. The gradient lives here rather than on .menu so
   nothing has to express "start below the camp" in CSS length arithmetic. */
.menu-body {
  flex: 1; min-height: 0;
  display: flex; flex-direction: column;
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--color-bg) 58%, transparent) 0%,
    color-mix(in srgb, var(--color-bg) 90%, transparent) 10%,
    color-mix(in srgb, var(--color-bg) 96%, transparent) 100%);
  backdrop-filter: blur(3px);
}

.menu-head { flex: none; padding: 0.75rem 0.75rem 0.5rem; }
.stat-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.4rem; }
.stat {
  display: flex; flex-direction: column; gap: 0.1rem;
  padding: 0.45rem 0.5rem;
  background: color-mix(in srgb, var(--color-surface) 80%, transparent);
  border: 1px solid var(--color-border); border-radius: 8px;
}
.stat-key {
  font-family: var(--font-data); font-size: 0.55rem; font-weight: 400;
  letter-spacing: 0.1em; color: var(--color-text-dim);
}
.stat-val {
  font-family: var(--font-data); font-size: 1rem; font-weight: 700;
  line-height: 1.1; font-variant-numeric: tabular-nums;
  color: var(--color-text);
}
/* Deepest is the headline number, so it gets the accent and nothing else does. */
.stat:first-child .stat-val { color: var(--color-accent-blue); }

/* Five tabs on a 375pt screen. `min-width: 0` is what lets them share the row
   evenly — without it a flex item refuses to shrink below its text, and the
   fifth tab pushes the strip into horizontal overflow. The type shrinks with
   the viewport for the same reason, and the height never does: 44px is the tap
   target and it is not negotiable. */
.menu-tabs {
  flex: none; display: flex; gap: 0.3rem;
  padding: 0 0.75rem 0.5rem;
}
.tab {
  flex: 1; min-width: 0; min-height: var(--tap);
  padding: 0 0.1rem;
  border: 1px solid var(--color-border); border-radius: 8px;
  background: transparent; color: var(--color-text-dim);
  font-family: var(--font-data);
  font-size: clamp(0.6rem, 2.9vw, 0.75rem); font-weight: 600;
  letter-spacing: 0.04em; cursor: pointer;
  white-space: nowrap; overflow: hidden;
}
.tab-on {
  background: var(--color-surface); color: var(--color-text);
  border-color: color-mix(in srgb, var(--color-accent-blue) 55%, var(--color-border));
}

.menu-panel {
  flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: 0 0.75rem 0.75rem;
}
.menu-panel .group {
  margin: 0.75rem 0 0.35rem;
  font-family: var(--font-data); font-size: 0.6rem; font-weight: 600;
  letter-spacing: 0.14em; color: var(--color-text-dim);
}
.menu-panel .note { margin: 0.5rem 0 0.75rem; font-size: 0.82rem; color: var(--color-text-dim); }

/* ── kit rows ── */
.kit {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.5rem 0.6rem; margin-bottom: 0.3rem;
  background: color-mix(in srgb, var(--color-surface) 70%, transparent);
  border: 1px solid var(--color-border); border-radius: 8px;
}
.kit-main { flex: 1; min-width: 0; }
.kit-name { font-size: 0.9rem; font-weight: 700; line-height: 1.2; }
.kit-blurb { font-size: 0.72rem; color: var(--color-text-dim); line-height: 1.3; }
.kit-maxed { opacity: 0.55; }

.pips { display: flex; gap: 3px; margin-top: 0.3rem; }
.pip {
  width: 14px; height: 4px; border-radius: 2px;
  background: var(--color-border);
}
.pip-on { background: var(--color-accent-blue); }

/* The price IS the button. A separate label and button would be two things
   saying the same number, and the row is already narrow on a phone. */
.buy {
  flex: none; min-width: 3.4rem; min-height: var(--tap);
  padding: 0 0.6rem;
  border: 1px solid var(--color-border); border-radius: 8px;
  background: transparent; color: var(--color-text-dim);
  font-family: var(--font-data); font-size: 0.85rem; font-weight: 700;
  font-variant-numeric: tabular-nums; cursor: pointer;
}
.buy-ok {
  background: var(--color-accent-blue); border-color: var(--color-accent-blue);
  color: #1a0f04; cursor: pointer;
}
.buy:disabled { cursor: default; opacity: 0.75; }
.buy-ok:hover { filter: brightness(1.08); }

/* ── cosmetics: the Camp tab ──
   Same row skeleton as .kit and .beast, deliberately: art, a stack of text, one
   44px control on the right. A shop that invented its own layout would make the
   two currencies feel like two different apps, when the only real difference is
   which number comes out.

   A locked row is DIMMED, never hidden. cosmeticLock() returns a printable
   reason precisely so the player can see what to aim at — an item that vanishes
   until you qualify cannot be a goal. */
.cos {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.4rem 0.6rem; margin-bottom: 0.3rem;
  background: color-mix(in srgb, var(--color-surface) 70%, transparent);
  border: 1px solid var(--color-border); border-radius: 8px;
}
.cos-worn {
  border-color: color-mix(in srgb, var(--color-accent-blue) 55%, var(--color-border));
  background: color-mix(in srgb, var(--color-accent-blue) 10%, transparent);
}
.cos-locked { opacity: 0.62; }

/* Fixed before the sheet lands, so a sprite arriving 200ms in cannot reflow the
   list under the player's thumb — the same rule .beast-art follows. */
.cos-art {
  flex: none;
  display: flex; align-items: center; justify-content: center;
  width: 48px; min-width: 48px; height: 48px;
  border-radius: 6px;
  background: color-mix(in srgb, var(--color-bg) 55%, transparent);
  overflow: hidden;
}
.cos-art canvas { display: block; image-rendering: pixelated; }

.cos-main { flex: 1; min-width: 0; }
.cos-name { font-size: 0.9rem; font-weight: 700; line-height: 1.2; }
.cos-blurb { font-size: 0.72rem; color: var(--color-text-dim); line-height: 1.3; }
.cos-lock {
  margin-top: 0.15rem;
  font-family: var(--font-data); font-size: 0.62rem; letter-spacing: 0.04em;
  color: var(--color-accent-green);
}
.cos-on {
  border-color: color-mix(in srgb, var(--color-accent-blue) 55%, var(--color-border));
  color: var(--color-accent-blue); opacity: 1;
}

/* ── biomes ── */
.biome {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.5rem 0.6rem; margin-bottom: 0.3rem;
  background: color-mix(in srgb, var(--color-surface) 70%, transparent);
  border: 1px solid var(--color-border); border-radius: 8px;
}
.biome-swatch { flex: none; display: flex; width: 44px; height: 30px; border-radius: 4px; overflow: hidden; }
.biome-swatch i { flex: 1; display: block; }
.biome-main { flex: 1; min-width: 0; }
.biome-name { font-size: 0.9rem; font-weight: 700; line-height: 1.2; }
.biome-sub { font-family: var(--font-data); font-size: 0.68rem; color: var(--color-text-dim); }
.biome-mark { flex: none; font-size: 1.1rem; color: var(--color-border); }
.biome-got .biome-mark { color: var(--color-accent-blue); }

/* ── beasts ──
   Seven kinds, each with a sprite, prose and flag chips, on a 375pt screen and
   above a Descend button that must stay reachable. So a row COLLAPSES: what you
   always want is the silhouette, the name and the pump count, and the paragraph
   explaining why is one tap away. Seven expanded cards would be a screen and a
   half of scrolling to get back to the button.

   The whole head is the button — a 44px chevron on the right of a 300px row is a
   target the thumb has to aim for, where the row itself cannot be missed. */
.beast {
  margin-bottom: 0.3rem;
  background: color-mix(in srgb, var(--color-surface) 70%, transparent);
  border: 1px solid var(--color-border); border-radius: 8px;
  overflow: hidden;
}
.beast-open { border-color: color-mix(in srgb, var(--color-accent-blue) 45%, var(--color-border)); }

.beast-head {
  display: flex; align-items: center; gap: 0.6rem;
  width: 100%; min-height: var(--tap);
  padding: 0.4rem 0.5rem;
  border: 0; background: transparent; color: inherit;
  text-align: left; cursor: pointer;
  font: inherit;
}

/* The art slot is fixed at its own size before the sheet lands, so the sprite
   arriving cannot reflow seven rows under the player's thumb. */
.beast-art {
  flex: none;
  display: flex; align-items: center; justify-content: center;
  width: 48px; height: 48px;
  border-radius: 6px;
  background: color-mix(in srgb, var(--color-bg) 60%, transparent);
  overflow: hidden;
}
.beast-sprite { display: block; image-rendering: pixelated; }
/* Fallback only: no sheet, so the row still says which kind it is. */
.beast-mark {
  font-family: var(--font-data); font-size: 1.1rem; font-weight: 700;
  font-style: normal; color: var(--beast-tint);
}

.beast-main { flex: 1; min-width: 0; }
.beast-top { display: flex; align-items: baseline; gap: 0.4rem; }
.beast-name { flex: none; font-size: 0.9rem; font-weight: 700; line-height: 1.3; }
.beast-kill {
  flex: 1; min-width: 0;
  font-family: var(--font-data); font-size: 0.66rem;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-dim);
  text-align: right;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.beast-tags { display: flex; flex-wrap: wrap; gap: 0.2rem; margin-top: 0.2rem; }
.beast-tag {
  padding: 0.05rem 0.28rem;
  border: 1px solid var(--color-border); border-radius: 3px;
  font-family: var(--font-data); font-size: 0.5rem; font-style: normal;
  font-weight: 600; letter-spacing: 0.06em; line-height: 1.5;
  color: var(--color-text-dim); white-space: nowrap;
}
/* The chips that change how you fight it, rather than how it walks. */
.beast-tag-warn {
  color: var(--color-danger);
  border-color: color-mix(in srgb, var(--color-danger) 45%, var(--color-border));
}
.beast-chev {
  flex: none; width: 1rem; text-align: center;
  font-size: 1.1rem; line-height: 1; color: var(--color-text-dim);
  transform: rotate(90deg); transform-origin: 50% 50%;
  transition: transform 120ms ease;
}
.beast-open .beast-chev { transform: rotate(270deg); }

.beast-body { padding: 0 0.6rem 0.55rem; }
.beast-body[hidden] { display: none; }
.beast-para { margin-top: 0.5rem; }
.beast-para-key {
  margin: 0 0 0.1rem;
  font-family: var(--font-data); font-size: 0.55rem; font-weight: 600;
  letter-spacing: 0.12em; color: var(--color-accent-blue);
}
.beast-para-body { margin: 0; font-size: 0.8rem; line-height: 1.45; color: var(--color-text); }

.beast-facts {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.35rem;
  margin-top: 0.6rem;
}
.beast-fact {
  display: flex; flex-direction: column; gap: 0.05rem;
  padding: 0.3rem 0.4rem;
  background: color-mix(in srgb, var(--color-bg) 45%, transparent);
  border: 1px solid var(--color-border); border-radius: 6px;
}
.beast-fact-key {
  font-family: var(--font-data); font-size: 0.5rem; font-weight: 400;
  letter-spacing: 0.1em; color: var(--color-text-dim);
}
.beast-fact-val {
  font-family: var(--font-data); font-size: 0.85rem; font-weight: 700;
  line-height: 1.1; font-variant-numeric: tabular-nums;
}

.beast-where { margin-top: 0.5rem; }
.beast-where-val {
  display: block;
  font-family: var(--font-data); font-size: 0.7rem; line-height: 1.35;
  color: var(--color-text);
}
.beast-where-note {
  display: block; margin-top: 0.1rem;
  font-size: 0.66rem; color: var(--color-text-dim);
}

/* ── run log ── */
.logrow {
  display: flex; align-items: baseline; gap: 0.5rem;
  padding: 0.4rem 0.6rem; margin-bottom: 0.2rem;
  border-bottom: 1px solid color-mix(in srgb, var(--color-border) 60%, transparent);
  font-family: var(--font-data); font-size: 0.76rem;
  font-variant-numeric: tabular-nums;
}
.log-depth { flex: none; min-width: 3.6rem; font-weight: 700; color: var(--color-accent-blue); }
.log-mid { flex: 1; min-width: 0; color: var(--color-text-dim); }
.log-when { flex: none; color: var(--color-text-dim); opacity: 0.7; }

.menu-foot {
  flex: none;
  padding: 0.6rem 0.75rem calc(0.6rem + var(--safe-bottom));
  border-top: 1px solid var(--color-border);
  background: color-mix(in srgb, var(--color-bg) 90%, transparent);
}
.btn-descend {
  width: 100%; min-height: 3rem;
  font-family: var(--font-data); font-size: 0.95rem; font-weight: 700;
  letter-spacing: 0.08em;
}

/* On anything wider than a phone the menu stops stretching to fill the window
   and sits as a column, because 15 upgrade rows at 1200px wide is unreadable. */
@media (min-width: 34rem) {
  .menu-head, .menu-tabs, .menu-panel, .menu-foot {
    width: 100%; max-width: 30rem; margin-inline: auto;
  }
  /* basecamp.js caps the composed camp at 620px and centres it for the same
     reason: five buildings spread across a 1280px window is not a camp. */
  .tab { font-size: 0.75rem; }
}
