/* style.css — Trample design system + game shell layout
 *
 * The palette, type scale and "chunky stone" button treatment mirror the
 * Trample landing page and launch checklist, so the game, the site and the
 * marketing material all read as one product.
 *
 * Colours are declared twice: a hex fallback first, then the oklch value the
 * brand is authored in. Browsers that can't parse oklch drop that declaration
 * and keep the hex.
 */

:root {
  --meadow:        #57B85A;
  --meadow:        oklch(72% 0.18 145);
  --meadow-light:  #7FD07A;
  --meadow-light:  oklch(80% 0.16 145);
  --meadow-dark:   #2F7A38;
  --meadow-dark:   oklch(50% 0.13 148);
  --forest:        #244B28;
  --forest:        oklch(32% 0.08 150);
  --forest-deep:   #16301A;
  --forest-deep:   oklch(22% 0.05 152);

  --parchment:     #F2EDE0;
  --parchment:     oklch(94% 0.025 88);
  --parchment-dim: #E6DECD;
  --parchment-dim: oklch(89% 0.03 86);
  --edge:          #D2C6AC;
  --edge:          oklch(82% 0.04 78);

  --mud:           #7A5731;
  --mud:           oklch(42% 0.07 55);
  --mud-deep:      #55371C;
  --mud-deep:      oklch(30% 0.05 50);

  --bounty:        #E8B84A;
  --bounty:        oklch(80% 0.16 85);
  --bounty-deep:   #B07B22;
  --bounty-deep:   oklch(60% 0.16 65);

  --danger:        #C4462F;
  --danger:        oklch(55% 0.17 30);

  --ink:           #1E2A20;
  --ink:           oklch(22% 0.02 150);
  --ink-soft:      #4A554C;
  --ink-soft:      oklch(38% 0.02 150);

  --display: 'Bricolage Grotesque', system-ui, sans-serif;
  --body:    'Fredoka', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --mono:    'JetBrains Mono', ui-monospace, monospace;

  /* The screen's own insets: the status bar a home-screen iOS app draws
     under (black-translucent, index.html) and the home indicator below. Both
     are 0 in a browser tab, on Android, on desktop and in an itch.io frame,
     so every rule that reads them is a no-op there and one layout serves the
     lot — nothing branches on device or context. Read through these rather
     than env() directly, so the fallback is written once. */
  --safe-top:    env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  /* A HUD pixel (see #ui-panel). 1px anywhere outside the panel. */
  --hud-px: 1px;
  /* The one column everything stands in on a wide window: the game shell,
     the world map's viewport and bar, and the map scene itself, which is
     authored 448 wide — a phone's width, so a desktop or a tablet plays the
     phone's view, letterboxed on the body's ground. It is the board's cap
     too: inside it no board's cell can outgrow a phone's, and the shell's own
     margins centre it once it is hit. */
  --column: 448px;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html, body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: var(--forest-deep);
  font-family: var(--body);
  color: var(--ink);
  user-select: none;
  -webkit-user-select: none;
  -webkit-font-smoothing: antialiased;
}
/* The status bar. Only a home-screen iOS app has one to clear (--safe-top is
   0 everywhere else), and it is cleared here, on the body, so the game shell
   below (height: 100%) fills what is left: the board starts under the clock
   rather than beneath it, and main.js's canvas maths, which reads the shell's
   clientHeight, needs no inset of its own. The fixed overlays ignore body
   padding and go on painting under the bar — the look black-translucent is
   for — and keep their cards clear of it themselves (.t-overlay). */
body { padding-top: var(--safe-top); }

/* ═══════════════════════════════════ GAME SHELL */

#game-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  height: 100%;
  max-width: var(--column);
  margin: 0 auto;
  background: var(--forest);
  /* The containing block for the floating Undo / Confirm. `margin: 0 auto`
     centres the shell in anything wider than the column, and main.js measures the
     head tile against THIS box — so with nothing positioned here the absolute
     box resolved against the viewport instead and landed a whole left margin
     out over the desktop letterboxing. On a phone the margin is 0, which is
     why it only ever showed on a wide window.
     Relative with no z-index starts no stacking context, so #drag-confirm's
     60 still competes at the root against the overlays' 100+. */
  position: relative;
}

#game-canvas {
  display: block;
  width: 100%;
  flex: 1;
  min-height: 0;
  touch-action: none;
}

/* UI Panel below the canvas. Hidden until there is a game to play — the player
   rows and the control strip are meaningless in the menus, and a panel that
   starts visible flashes the game shell before the main menu is built
   (issue #84). ui.showGameUI() sets the inline display either way. */
#ui-panel {
  width: 100%;
  background:
    radial-gradient(ellipse 80% 120% at 50% 100%, rgba(0,0,0,.28) 0%, transparent 70%),
    var(--forest);
  border-top: 2px solid var(--meadow-dark);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.08), 0 -8px 22px -14px rgba(0,0,0,.9);
  /* ONE dial sizes the whole panel. --hud-px is a pixel at the panel's floor
     and grows with the viewport, and everything in here that has to stay
     legible — the type, the icons, the rows, the strip, the chip — is written
     as a count of them. So the panel is exactly the design below on the phone
     it was tuned on (a 430x739 Safari viewport, where 13px is 1.76% of the
     height), up to 15% bigger where there is room, and never smaller: the
     board absorbs the variance and the HUD holds steady, because a board 8%
     smaller is barely noticed and an unreadable score is the whole game.
     Width is in the min() so the strip cannot outgrow a narrow phone, and
     svh rather than vh because vh is the viewport with the browser chrome
     collapsed — on iOS a hundred pixels more than a player ever sees, which
     would have grown the panel on the very phone it was tuned for. The
     @supports below is where the growth is switched on: a custom property
     cannot carry its own fallback (an unsupported unit only fails where it is
     used), so a browser without svh keeps the floor. */
  --hud-px: 1px;
  --hud-pad-y: 7px;
  --hud-pad-x: 9px;
  /* The home indicator's inset beyond the panel's own padding, ADDED to the
     height rather than taken from it. It used to come out of the fixed
     height, which on a home-screen iOS app (34px of inset) squeezed the
     2-player rows from 28px to 14px and left the strip standing a band above
     the bottom — the squashed scores every screenshot from the home screen
     showed. Zero wherever there is no inset. */
  --hud-safe: max(0px, var(--safe-bottom) - var(--hud-pad-y));
  padding: var(--hud-pad-y) var(--hud-pad-x) max(var(--hud-pad-y), var(--safe-bottom));
  display: none;         /* showGameUI(true) sets display:flex inline */
  flex-direction: column;
  gap: 4px;
  flex-shrink: 0;
  /* Pinned per player count. Player count is fixed for a game, so the board
     never resizes under anyone — the unit moves only with the viewport, which
     re-lays the board out anyway.

     The heights buy every count the SAME ~27px row, which is what lets all
     four share one type scale — four players used to be squeezed to a 10px
     font in an 18.75px row and read as a different, smaller panel.

     They cost almost nothing. The board takes min(width/cols, height/rows)
     and is width-bound on nearly every phone this ships to, so the height
     comes out of slack rather than out of cells. Measured at 165 against the
     old 131, per board:
       Large 12x16   free everywhere (holds its cell to a 176px panel even on
                     a 360x640, and past 300 on the bigger phones)
       Medium 10x14  free except 1px of cell on a 375x667 SE and a 360x640
       Small 8x12    the same two, 1px each — Small has the tallest aspect
                     ratio, so it is the one that binds first
     A 1px cell on the two smallest phones is the whole price. Anything past
     165 starts costing the mid-size phones too. */
  --hud-h: 165;
  height: calc(var(--hud-h) * var(--hud-px) + var(--hud-safe));
}
#ui-panel.players-2 { --hud-h: 110; }   /* 2 rows at 28.5px */
#ui-panel.players-3 { --hud-h: 138; }   /* 3 rows at 27.25px */
                                        /* 4 rows at 27.25px is the 165 above */
@supports (height: 1svh) {
  #ui-panel { --hud-px: calc(clamp(13px, min(1.76svh, 3.5vw), 15px) / 13); }
}

/* ── PLAYER ROWS
   The score chip and the turn log are one element: who, what they last
   scored, and what that leaves them on. Side-by-side chips could not hold
   four players on a phone without wrapping to a 2x2 grid, and that grid cost
   two rows of history; stacked rows hold four and have width to spare.

   The rows are sunk into the panel's own ground and the bounty chip below
   them takes the raised, pressable treatment, so weight sits on the elements
   that are actually controls. Gold means "bounty" here, not "your turn" —
   whose turn it is is a lit green row. */
#player-rows {
  display: flex;
  flex-direction: column;
  gap: 3px;
  flex: 1;
  min-height: 0;
}
#player-rows.players-4 { gap: 2px; }

.player-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 8px;
  flex: 1;
  min-height: 0;
  border-radius: 9px;
  background: rgba(0,0,0,.26);
  color: var(--parchment);
  font-size: calc(13 * var(--hud-px));
  font-weight: 600;
  white-space: nowrap;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.09);
}
/* Active: lit, not recoloured. */
.player-row.active {
  background: var(--meadow-dark);
  box-shadow: inset 0 0 0 1px var(--meadow), 0 0 0 1px rgba(0,0,0,.25);
}
.player-row .player-dot { width: calc(11 * var(--hud-px)); height: calc(11 * var(--hud-px)); flex-shrink: 0; }

.player-row__name {
  font-family: var(--mono);
  font-size: calc(12 * var(--hud-px));
  letter-spacing: .5px;
  color: var(--parchment-dim);
  opacity: .85;
  flex-shrink: 0;
}
.player-row.active .player-row__name { color: var(--parchment); opacity: 1; }

/* The turn's deltas take the row's slack, which is what keeps every row's
   deltas starting at the same x and pushes the boss mark, the bridges and the
   total into a right-hand group that lines up down the panel. */
.player-row__deltas {
  position: relative;
  align-self: stretch;      /* a definite box for the lines to scroll inside */
  flex: 1;
  min-width: 0;
  overflow: hidden;
  font-family: var(--mono);
  font-size: calc(12 * var(--hud-px));
}
/* One turn's entry. Absolutely placed so the outgoing and incoming lines pass
   each other inside the clipped box rather than sitting side by side — two
   turns that earned the same thing are identical to read, and a straight text
   swap between them looks like nothing happened. */
.t-delta-line {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  gap: 6px;
  /* The arriving line waits for the leaving one to get clear. Without the
     delay both sit within a few px of centre at the midpoint and the frame
     reads as one smudged line rather than two passing. */
  animation: deltaLineIn 240ms cubic-bezier(.2,.85,.3,1) 90ms both;
}
.t-delta-line.is-leaving {
  animation: deltaLineOut 200ms cubic-bezier(.45,0,.75,.3) both;
}
/* The bridge mark is fixed-colour artwork drawn for parchment, so it cannot
   follow the row's text colour and goes muddy on a dark green row. Lifted
   rather than redrawn, so it stays the same bridge everywhere it appears. */
/* Sized with the row rather than under it. At 10px against a 14px total it
   read as a footnote, when it is one of the three numbers a player is here to
   check. The mark grows with it, being sized in em. */
.player-row__bridges {
  flex-shrink: 0;
  font-size: calc(13 * var(--hud-px));
  color: var(--parchment-dim);
  opacity: .85;
}
.player-row__bridges .t-bridge-icon { filter: brightness(1.45) saturate(.85); }
.player-row.active .player-row__bridges { opacity: 1; }

/* The opening total ticks up one component at a time (issue #90) and each
   arrival pops. The class rode through the HUD rewrite on the element; the
   rule that gave it an animation did not, because it was keyed on the old
   .score-value — so the pop was silently dead until this was put back. */
.player-row__total.is-bumped { animation: scorePop 360ms ease-out; }
.player-row__total {
  flex-shrink: 0;
  min-width: calc(26 * var(--hud-px));
  text-align: right;
  font-weight: 700;
  font-size: calc(15 * var(--hud-px));
  font-variant-numeric: tabular-nums;
}

/* ONE type scale for every count. The panel used to hold 131px whatever the
   player count and step the type down to fit — four players landed on a 10px
   font in an 18.75px row, which read as a different, smaller panel rather than
   as the same one with more in it. Since the height is free (see #ui-panel),
   the rows get the height instead and the type stays put.
   Three and four still lose a little horizontal padding, because a shorter row
   wants less of it, and four tightens its gap to buy the 25px. */
#player-rows.players-3 .player-row { padding: 0 7px; }
#player-rows.players-4 .player-row { padding: 0 7px; border-radius: 8px; }

/* A turn's gain or loss, in the same icon vocabulary as the score floats. */
.t-delta { display: inline-flex; align-items: center; gap: 2px; }
.t-delta--gain { color: var(--meadow-light); }
.t-delta--loss { color: #FF8A73; }
.player-row.active .t-delta--gain { color: #C8F0BE; }
.t-delta .t-svg-icon { width: 1.05em; height: 1.05em; }
/* A turn's entries are laid out at once but shown one by one, each as its
   score float lands on it (issue #116). Hidden rather than absent so the slots
   hold their places and the floats have somewhere to aim. */
.t-delta.is-pending { visibility: hidden; }
.t-delta.is-landed { animation: deltaLand 320ms cubic-bezier(.2,.85,.3,1); }
/* Sized wherever it lands, not just inside a row's delta. The SVG carries a
   viewBox but no width or height, so unsized it has no box to lay out with —
   as an inline element it fell back to a default one, but as a flex item in a
   score float it collapsed to 0x0 and the sack simply vanished. The bounty
   chip sizes its own (.t-rung svg, more specific), so that one is unaffected. */
.t-sack-mark { width: 1.15em; height: 1.15em; display: block; }

/* A boss's abilities, in the right-hand group just before the bridge count.
   Beside the name it pushed that one row's deltas out of line with everyone
   else's, and the column of what each player just scored is the thing being
   read down the panel. Rows without an ability hold the empty slot, but only
   on levels that have a boss at all.
   One mark per ability (issue #124), 13px each in a 17px chip, so a boss
   with two wears a 30px one — and every slot on the panel is sized by the
   widest chip (--boss-marks, set on #player-rows by initPlayerRows), which
   is what keeps the bridges and totals in one column. The boss's own chip is
   a button that opens the ability card; the empty slots are spans. The
   button reset is here rather than on button.t-boss-chip so the two are the
   same box. */
.t-boss-chip {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 1px;
  width: calc((var(--boss-marks, 1) * 13 + 4) * var(--hud-px));
  height: calc(17 * var(--hud-px));
  padding: 0;
  border: none;
  border-radius: 5px;
  flex-shrink: 0;
  font: inherit;
  color: #FF8A73;
  background: rgba(196,70,47,.26);
  box-shadow: inset 0 0 0 1px rgba(255,138,115,.45);
}
.t-boss-chip.is-empty { background: none; box-shadow: none; }
.t-boss-chip .t-svg-icon { width: calc(12 * var(--hud-px)); height: calc(12 * var(--hud-px)); flex-shrink: 0; }
button.t-boss-chip { cursor: pointer; }
/* The chip is 17px in a 27px row: the hit area takes the row's height
   without the mark growing to fill it. */
button.t-boss-chip::before { content: ''; position: absolute; inset: -5px -4px; }
button.t-boss-chip:active { background: rgba(196,70,47,.48); }

/* The bridge mark (ICONS.bridge) — the old How to Play illustration, cropped.
   Sized in em so the one drawing serves the score row and the 17px score
   floats; the ratio matches its cropped viewBox, so it never squashes. */
.t-bridge-icon { width: 1.687em; height: 1em; vertical-align: -.16em; }
.player-row__bridges .t-bridge-icon { margin-right: .05em; }
/* The row's gap spaces it now; this only tops it up to the bridge's old feel. */
.score-float .t-bridge-icon { margin-left: .08em; }

/* ── CONTROL STRIP
   Back, bounty, Skip. The two labelled controls sit at either end with the
   chip between them: they were a stacked pair of identical cream buttons in
   an earlier draft, and one forfeits a turn while the other leaves the level. */
#hud-controls {
  display: flex;
  align-items: stretch;
  gap: 7px;
  flex-shrink: 0;
  height: calc(32 * var(--hud-px));
}
.t-hud-mid {
  flex: 1;
  display: flex;
  align-items: stretch;
  justify-content: center;
  min-width: 0;
}

.t-hud-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  height: 100%;
  padding: 0 12px;
  border: none;
  border-radius: 9px;
  cursor: pointer;
  font-family: var(--display);
  font-weight: 700;
  font-size: calc(13 * var(--hud-px));
  letter-spacing: .2px;
  color: var(--mud-deep);
  background: var(--parchment-dim);
  box-shadow: 0 3px 0 var(--edge);
  transition: opacity .15s;
}
.t-hud-btn .t-svg-icon { width: calc(13 * var(--hud-px)); height: calc(13 * var(--hud-px)); }
.t-hud-btn:active:not(:disabled) { transform: translateY(2px); box-shadow: 0 1px 0 var(--edge); }
/* Skip has two states and no third: active, or dimmed where it cannot act —
   mid-drag there is a path to finish, and on the rival's turn there is no
   turn of yours to skip. It stays in place and dims rather than leaving, so
   the strip is composed identically whichever it is and nothing flickers in
   and out beside a thumb that is already moving. */
.t-hud-btn:disabled { opacity: .34; cursor: default; }
/* The way back to the score card once it has been put away. It stands in the
   bounty chip's slot, which is empty by then — the game is over, so there is
   no next drop and no endgame to count. Gold, because it is the only thing on
   the strip anyone still wants.
   Its emboss has to be named, not inherited: .t-hud-btn's is --edge, the
   cream buttons' own shade, which under gold reads as a lighter yellow and
   makes this the one gold button in the game without a brown lip. */
.t-hud-btn--restore {
  background: var(--bounty, #f5b027);
  color: var(--mud-deep);
  box-shadow: 0 3px 0 var(--bounty-deep);
}
.t-hud-btn--restore:active:not(:disabled) { box-shadow: 0 1px 0 var(--bounty-deep); }

/* ── BOUNTY CHIP — the ladder, not a count
   A playtester played the best part of an hour without noticing the three
   sacks are worth different amounts, so the chip carries the whole ladder in
   the order it drops. Each sack shows the board's own coin pile for its value
   (js/sack-art.js), which makes the chip a legend for the board art — and the
   only place the value survives once a sack is taken.
   It is a real button: pressing it re-opens the round-start popup. */
.t-bounty {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  height: 100%;
  padding: 0 6px;
  min-width: calc(158 * var(--hud-px));
  border: none;
  border-radius: 9px;
  cursor: pointer;
  background: var(--parchment);
  box-shadow: 0 3px 0 var(--edge);
}
.t-bounty:active { transform: translateY(2px); box-shadow: 0 1px 0 var(--edge); }
/* A class rule outranks the browser's own [hidden], so the display above would
   otherwise keep the chip's 158px in the strip at game over and shunt the
   results ▲ over Menu and Skip. */
.t-bounty[hidden] { display: none; }

.t-rung { display: flex; align-items: center; gap: 0; flex-shrink: 0; }
.t-rung svg { width: calc(24 * var(--hud-px)); height: calc(24 * var(--hud-px)); display: block; }
.t-rung span {
  font-family: var(--mono);
  font-weight: 600;
  font-size: calc(12 * var(--hud-px));
  color: var(--mud-deep);
  font-variant-numeric: tabular-nums;
}
/* Landed: dimmed, but it keeps its coins, so the ladder goes on teaching
   which value is which after the drop. While its sack is in the air on the
   way to the board the rung is blank (issue #116) — the sack is in one place
   at a time — and it fades up to its spent shade once the sack has landed. */
.t-rung.is-spent { opacity: .42; transition: opacity 360ms ease-out; }
.t-rung.is-lifting { opacity: 0; transition: none; }
/* The sack in flight: a copy of the rung's own drawing, sized to the rung at
   take-off and transformed to the tile's size and place by the flight. */
.t-sack-flight {
  position: fixed;
  z-index: 310;   /* over the phase banner (300): it crosses the words mid-flight, and a
                     moving thing that vanishes behind text reads as lost */
  pointer-events: none;
  transform-origin: center;
  filter: drop-shadow(0 3px 3px rgba(0,0,0,.35));
}
.t-sack-flight svg { width: 100%; height: 100%; display: block; }
.t-rung.is-spent span { color: var(--ink-soft); }

/* The countdown sits immediately before the next sack to fall, so it always
   names a value. Once every sack has fallen there is nothing left for it to
   sit in front of, so it comes to rest past the end of the ladder — and that
   position is the endgame: no second chip, no swapped frame, the same row of
   sacks with the clock on the other side of them.
   The count rides the dial's shoulder: level with the values it sits between,
   it read as a fourth number in the ladder. */
.t-clock {
  display: inline-flex;
  align-items: center;
  gap: 0;
  flex-shrink: 0;
  color: var(--mud-deep);
  margin: 0 3px 0 2px;
}
.t-clock .t-svg-icon { width: calc(16 * var(--hud-px)); height: calc(16 * var(--hud-px)); }
.t-clock span {
  align-self: flex-start;
  margin: 0 0 0 1px;
  font-family: var(--mono);
  font-weight: 700;
  font-size: calc(10.5 * var(--hud-px));
  line-height: 1;
}
/* What the countdown is counting towards once the sacks are gone. It stands
   in the slot a sack would occupy, so the chip still reads left to right as
   "in N turns, this" — position alone was too quiet a signal that the clock
   had changed what it was counting. */
.t-endmark { display: inline-flex; align-items: center; color: var(--mud-deep); flex-shrink: 0; }
.t-endmark .t-svg-icon { width: calc(17 * var(--hud-px)); height: calc(17 * var(--hud-px)); }

/* ── FLOATING UNDO / CONFIRM
   Anchored to a corner of the last tile dragged and opening away from it, so
   it never covers the head — pressing the head is how a short path is
   continued. It carries its own dark ground rather than sitting bare on the
   grass: the board runs six shades of green and a cream button on the light
   ones has almost no edge. */
.t-dragbox {
  --dragbox-rim: rgba(16,29,19,.62);
  position: absolute;
  z-index: 60;
  display: flex;
  border-radius: 13px;
  box-shadow:
    0 0 0 2px var(--dragbox-rim),
    0 4px 0 rgba(16,29,19,.42),
    0 9px 18px -7px rgba(0,0,0,.7);
}
/* A class rule outranks the browser's own [hidden], so the display above would
   otherwise park the box over the main menu. */
.t-dragbox[hidden] { display: none; }

/* The tail names the tile the box is asking about — one capsule can be
   anchored to any of four corners, and without it the box is just a control
   that appeared somewhere. It grows from whichever edge faces the head, in
   the colour of the half it grows out of.
   Only its two OUTER edges carry the rim: a square rotated 45deg points its
   bottom-right corner straight down, so border-right and border-bottom are
   exactly the two that show. The other half of the diamond is inside the
   capsule, over a button of its own colour, where it cannot be seen.
   Its length is DRAG_GAP (js/ui.js) — it bridges the gap to the tile exactly,
   touching it without covering it, which is what the head must never be. */
.t-dragbox[data-arm]::after {
  content: '';
  position: absolute;
  width: 10px;
  height: 10px;
  transform: rotate(45deg);
}
.t-dragbox[data-arm^="up"]::after {
  bottom: -5px;
  border-right: 2px solid var(--dragbox-rim);
  border-bottom: 2px solid var(--dragbox-rim);
}
.t-dragbox[data-arm^="down"]::after {
  top: -5px;
  border-left: 2px solid var(--dragbox-rim);
  border-top: 2px solid var(--dragbox-rim);
}
/* Opened to the right of the head, so the head is off the box's left end —
   under Undo, which is the cream half. */
.t-dragbox[data-arm$="right"]::after { left: 14px; background: var(--parchment-dim); }
.t-dragbox[data-arm$="left"]::after { right: 14px; background: var(--bounty); }
/* The width is still a constraint, just a looser one: an edge-aligned box runs
   from one edge of the head tile to the board's, which from the CENTRE column
   caps it at (board / 2) + (cell / 2) — 200px on a 360px board at a 40px cell.
   Over that and both horizontal arms miss at the one position a player is most
   likely to be drawing in, and the clamp takes over. The capsule lands at
   142px, so there is room now for an icon (~19px) that the old corner-offset
   placement, capped at ~151px, had none for. */
.t-dragbtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  height: 40px;
  padding: 0 12px;
  border: none;
  cursor: pointer;
  font-family: var(--display);
  font-weight: 700;
  font-size: 12.5px;
  letter-spacing: .2px;
  color: var(--mud-deep);
}
/* The ends are rounded on the buttons themselves rather than clipped by the
   capsule: overflow:hidden would take the tail off with the corners. */
.t-dragbtn--undo {
  background: var(--parchment-dim);
  border-radius: 13px 0 0 13px;
  box-shadow: inset -1px 0 0 rgba(16,29,19,.28);
}
.t-dragbtn--confirm {
  background: var(--bounty);
  border-radius: 0 13px 13px 0;
}
/* Bigger than the 1em default: at the label's 12.5px a tick loses its corner. */
.t-dragbtn .t-svg-icon { width: 15px; height: 15px; }
/* One capsule, so a pressed half darkens where a separate button would sink —
   dropping one end of a joined pair pulls the shape apart. */
.t-dragbtn--undo:active { background: var(--edge); }
.t-dragbtn--confirm:active { background: var(--bounty-deep); }

/* Small raised button. Down to one user — the world map's back control — now
   that the campaign exit and the results restore have moved into the HUD's
   own control strip and wear .t-hud-btn. */
.t-stone-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 40px;
  padding: 9px 11px;
  border: none;
  border-radius: 12px;
  background: var(--parchment);
  color: var(--mud-deep);
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 4px 0 var(--edge), 0 8px 16px -8px rgba(0,0,0,.6);
  transition: transform .1s, box-shadow .1s;
}
.t-stone-btn:active { transform: translateY(3px); box-shadow: 0 1px 0 var(--edge); }
.t-stone-btn .t-svg-icon { width: 18px; height: 18px; }

/* A stone button that names where it goes. The world map's back arrow used to
   be bare, which left the one control on that screen saying nothing — and an
   arrow alone is the same mark the in-game Undo wears. It matches the HUD's
   own back button, which is labelled for exactly that reason. */
.t-stone-btn--labelled {
  gap: 6px;
  padding: 9px 13px 9px 11px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 13px;
  letter-spacing: .2px;
}


/* ═══════════════════════════════════ OVERLAYS & CARDS */

.t-overlay {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  /* The insets: a card is centred in the safe area rather than the screen,
     and a scrolling overlay's content starts under the status bar's edge
     rather than beneath the clock. Both are 20px wherever there is none. */
  padding: max(20px, var(--safe-top)) 20px max(20px, var(--safe-bottom));
  font-family: var(--body);
}

/* The menu backdrop: a meadow at dusk, built out of the brand palette alone
   (issue #95). It used to be a 2.2 MB painted map, which the boot screen — and
   so the very first frame of the game — had to wait on. Gradients paint
   instantly and weigh nothing, which is the whole point on a phone opening a
   link.

   Read it bottom-up: the meadow rises out of the forest, two hills sit on the
   horizon, warm light comes over the top, and a barely-there weave keeps the
   whole thing from banding. */
.t-overlay--map {
  background:
    repeating-linear-gradient(64deg, rgba(255,255,255,.028) 0 2px, transparent 2px 9px),
    radial-gradient(ellipse 78% 36% at 50% -8%, rgba(232,184,74,.14) 0%, transparent 72%),
    radial-gradient(132% 21% at 24% 103%, #3E9445 0 60%, transparent 62%),
    radial-gradient(154% 23% at 80% 107%, #35803B 0 58%, transparent 60%),
    radial-gradient(184% 16% at 52% 100%, #2A6330 0 62%, transparent 64%),
    linear-gradient(180deg, var(--forest-deep) 0%, var(--forest) 40%, #23532A 70%, #2C6832 100%);
  background-color: var(--forest-deep);
}
/* Warm scrim for card readability. Lighter than it was under the painting —
   there is no busy artwork left to quieten, only contrast to hold. */
.t-overlay--map::before {
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 92% 62% at 50% 42%, rgba(10,26,12,.16) 0%, rgba(10,26,12,.50) 100%);
}

/* The first frame, before js/main.js has run: the menu's backdrop with nothing
   on it yet. It covers the game shell, and sits below every overlay (lowest
   100) so that if it were ever left behind it would hide rather than block.
   pointer-events:none for the same reason — it can never swallow a tap. */
#boot-screen {
  z-index: 50;
  pointer-events: none;
}

.t-overlay--dim   { background: rgba(12,26,14,.72); }
.t-overlay--dimmer{ background: rgba(12,26,14,.85); }

/* Scrollable overlay that still centres its card. Auto margins beat
   align-items in the cross axis: centred when there's room, scrolling from
   the top edge when there isn't (align-items:center would clip the top). */
.t-overlay--scroll {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  align-items: flex-start;
}
.t-overlay--scroll > * {
  margin-top: auto;
  margin-bottom: auto;
}

/* Parchment card — the default surface for every menu and modal */
.t-card {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 380px;
  max-height: 88vh;
  max-height: 88svh;   /* the viewport a player sees, chrome and all — see #ui-panel */
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  background: var(--parchment);
  border-radius: 22px;
  padding: 24px 22px;
  text-align: center;
  color: var(--ink);
  box-shadow:
    0 4px 0 rgba(0,0,0,.10),
    0 30px 60px -20px rgba(0,0,0,.55),
    inset 0 0 0 1px var(--edge);
}

.t-card--wide { max-width: 420px; }

/* Two gold studs at the top corners — the landing page signup-card motif.
   They sit inside the padding box so a scrolling card never clips them. */
.t-card--studs { padding-top: 30px; }
.t-card--studs::before,
.t-card--studs::after {
  content: '';
  position: absolute;
  width: 14px; height: 14px;
  background: var(--bounty);
  border-radius: 50%;
  top: 13px;
  box-shadow: inset 0 -3px 0 var(--bounty-deep);
}
.t-card--studs::before { left: 16px; }
.t-card--studs::after  { right: 16px; }

/* The boss briefing has no card variant of its own: it is an ordinary
   parchment card, and the red warning sign in its heading carries the whole
   signal. It used to be a scorched dark card ringed in orange, which read as
   aggression rather than as a heads-up. */

/* Campaign complete — gilded variant */
.t-card--gold {
  background: linear-gradient(160deg, #2a5a2a 0%, #14330f 100%);
  color: var(--parchment);
  box-shadow:
    0 4px 0 rgba(0,0,0,.3),
    0 0 40px rgba(232,184,74,.28),
    0 30px 60px -20px rgba(0,0,0,.6),
    inset 0 0 0 2px var(--bounty);
}
.t-card--gold .t-title { color: var(--bounty); }
.t-card--gold .t-body,
.t-card--gold .t-note { color: var(--parchment-dim); }

/* Main menu — hero lockup on the map, no card (mirrors the landing hero) */
.t-menu-hero {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 340px;
  text-align: center;
}
.t-menu-actions { margin-top: 30px; }

/* ═══════════════════════════════════ TYPE */

.t-title {
  font-family: var(--display);
  font-weight: 800;
  font-size: 26px;
  line-height: 1.05;
  letter-spacing: -.02em;
  color: var(--forest);
  margin: 0 0 10px;
}

/* Heading with the warning sign in front of it — the boss briefing. The sign
   is the one red thing on the card; the words stay the ordinary title colour.
   Flex rather than an inline glyph because .t-svg-icon is a block. */
.t-title--warn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}
.t-warn-sign { display: inline-flex; color: var(--danger); }
.t-warn-sign .t-svg-icon { width: .92em; height: .92em; }

.t-title--hero {
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(48px, 15vw, 78px);
  line-height: .9;
  letter-spacing: -.04em;
  color: #fff;
  margin: 0;
  text-shadow: 0 3px 0 var(--meadow-dark), 0 12px 26px rgba(0,0,0,.45);
}

.t-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: rgba(255,255,255,.94);
  background: rgba(0,0,0,.3);
  border: 1px solid rgba(255,255,255,.22);
  padding: 7px 14px;
  border-radius: 999px;
  margin: 14px 0 0;
}
.t-eyebrow .pip {
  width: 7px; height: 7px;
  background: var(--bounty);
  border-radius: 50%;
  box-shadow: 0 0 0 4px rgba(232,184,74,.25);
}

.t-pitch {
  font-family: var(--display);
  font-weight: 600;
  font-size: 17px;
  color: #fff;
  margin: 14px 0 0;
  text-shadow: 0 1px 6px rgba(0,0,0,.5);
}
.t-pitch .accent { color: var(--bounty); }

.t-label {
  display: block;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-bottom: 7px;
}

.t-body {
  font-size: 14.5px;
  line-height: 1.5;
  color: var(--ink-soft);
  margin: 0 0 14px;
}
.t-body strong { color: var(--ink); font-weight: 600; }
/* A second, quieter line under a card's opening paragraph. */
.t-body--aside { font-size: 13px; }

.t-note {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: .4px;
  line-height: 1.5;
  color: var(--ink-soft);
  margin: 0;
}

/* ═══════════════════════════════════ BUTTONS */

.t-btn {
  display: block;
  width: 100%;
  margin: 9px 0 0;
  padding: 14px 22px;
  border: none;
  border-radius: 14px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 17px;
  letter-spacing: .2px;
  color: var(--mud-deep);
  background: var(--parchment-dim);
  cursor: pointer;
  box-shadow: 0 4px 0 var(--edge), 0 10px 20px -12px rgba(0,0,0,.5);
  transition: transform .1s, box-shadow .1s;
}
.t-btn:active {
  transform: translateY(3px);
  box-shadow: 0 1px 0 var(--edge);
}

/* A link dressed as a button (e.g. the sign-up on the castle menu) */
a.t-btn {
  box-sizing: border-box;
  text-decoration: none;
  text-align: center;
}

/* A link dressed as a button with a mark beside its label — the sign-up and
   the feedback link (issue #155), which sit one above the other at the foot
   of the cards that end a game. The shape is shared so the pair reads as one
   block under the buttons you came to press, and the mark beside each label
   is what tells them apart. */
.t-btn--iconlink {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font-size: 15px;
  padding: 11px 18px;
}
.t-btn--iconlink:active { transform: translateY(2px); }
.t-btn--iconlink .t-svg-icon { width: 16px; height: 16px; }

/* Sign-up CTA: an offer, so it shouldn't compete with the button you came to
   press — but it still has to be readable. The bounty-coloured ring gives it
   presence on both the parchment and the dark cards, while the text colour
   comes from whichever ghost variant the caller picked for that background.
   It reaches the button only where the caller picked a class that doesn't
   set one of its own — the castle card's gold — which is the one card where
   the sign-up is the thing you came to press. */
.t-btn--signup {
  box-shadow: inset 0 0 0 2px var(--bounty-deep);
}
.t-btn--signup:active {
  box-shadow: inset 0 0 0 2px var(--bounty-deep);
}

/* The in-game ✉ is a link, not a button, so it needs the same reset a.t-btn
   gets. */
a.t-stone-btn { text-decoration: none; }

/* ═══════════════════════════════════ BUILD STAMP */

/* Top-left on every screen. Above the overlays (max z-index 400) but
   pointer-events:none, so it can never intercept a tap. The shadow keeps it
   legible on the parchment cards as well as the dark backdrops. */
.t-version {
  position: fixed;
  top: max(5px, var(--safe-top));
  left: max(7px, env(safe-area-inset-left));
  z-index: 500;
  pointer-events: none;
  user-select: none;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .5px;
  color: rgba(255, 255, 255, .8);
  text-shadow: 0 1px 2px rgba(0, 0, 0, .6);
}

/* On the test site the stamp is a <button> that crosses to the other build.
   It stays the same quiet label until the main menu adds .is-live: only then
   does it accept a tap and admit what it does. Both builds ship the same
   stamp, so the shape here must read as version text first, control second. */
button.t-version {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 0;
  border: 0;
  background: none;
}

.t-version__to {
  display: none;
  align-items: center;
  gap: 2px;
  opacity: .75;
}

.t-version.is-live {
  pointer-events: auto;
  cursor: pointer;
}

.t-version.is-live .t-version__to { display: inline-flex; }

.t-version .t-svg-icon { width: 9px; height: 9px; stroke-width: 3.5; }

/* The mute toggle (issue #133) — the version stamp's opposite corner, and the
   same idea: pinned, outliving every screen. It is live everywhere the stamp
   is not, so it carries a real hit area (the icon is 18px; the padding takes
   the target past 40px) while staying quiet enough to ignore. Dimmed when
   muted, so the state reads at a glance from the weight rather than from
   reading the mark. It sits above the scenes it lives on (the menus and the
   map are 200) and under every card that can open over one (300 and up), so
   a boss warning or the scoreboard covers it as it covers the rest of the
   scene — unlike the stamp, which can float over them because it takes no
   taps. The one card it stands over is a confirm: see .is-raised below. */
.t-audio {
  position: fixed;
  top: max(5px, var(--safe-top));
  right: max(7px, env(safe-area-inset-right));
  z-index: 250;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 11px;
  border: 0;
  background: none;
  cursor: pointer;
  color: rgba(255, 255, 255, .8);
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .6));
  -webkit-tap-highlight-color: transparent;
}

/* It is hidden for the whole of a game (see showAudioToggle), and a class rule
   outranks the browser's own [hidden] { display: none } — without this the
   button would sit over the top-right cell of every board. */
.t-audio[hidden] { display: none; }

/* Over a confirm card (see showAudioToggle) it is raised above the card's
   scrim (400) so it takes taps: the quit-game confirm is the one way to reach
   it during a game, and it stays in its usual corner so it is found where it
   always is. */
.t-audio.is-raised { z-index: 450; }

.t-audio .t-svg-icon { width: 18px; height: 18px; stroke-width: 2; }

.t-audio.is-muted { color: rgba(255, 255, 255, .45); }
.t-audio.is-partial { color: rgba(255, 255, 255, .5); }

.t-audio:active { transform: scale(.92); }

/* The sound menu under the speaker: a switch each for the music and the
   effects. Placed from the same corner as the button, in the same band, and
   raised with it over a confirm. */
.t-audio-menu {
  position: fixed;
  top: calc(max(5px, var(--safe-top)) + 42px);
  right: max(7px, env(safe-area-inset-right));
  z-index: 250;
  min-width: 172px;
  padding: 6px;
  border-radius: 14px;
  background: var(--parchment);
  color: var(--ink);
  box-shadow:
    0 4px 0 rgba(0,0,0,.10),
    0 18px 40px -14px rgba(0,0,0,.55),
    inset 0 0 0 1px var(--edge);
}
.t-audio-menu[hidden] { display: none; }
.t-audio-menu.is-raised { z-index: 450; }

.t-audio-menu__item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 9px 10px;
  border: 0;
  border-radius: 9px;
  background: none;
  color: inherit;
  text-align: left;
  font-family: var(--display);
  font-weight: 700;
  font-size: 14px;
  line-height: 1.2;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.t-audio-menu__item:active { background: rgba(0, 0, 0, .06); }

.t-audio-menu__box {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 20px;
  height: 20px;
  border-radius: 6px;
  background: #fff;
  color: #fff;
  box-shadow: inset 0 0 0 1px var(--edge);
}
.t-audio-menu__box .t-svg-icon { width: 14px; height: 14px; stroke-width: 3; opacity: 0; }
.t-audio-menu__item.is-on .t-audio-menu__box {
  background: var(--mud);
  box-shadow: inset 0 0 0 1px var(--mud-deep);
}
.t-audio-menu__item.is-on .t-audio-menu__box .t-svg-icon { opacity: 1; }

/* ?audio=debug only — never in an ordinary session. */
.t-audio-debug {
  position: fixed;
  left: max(6px, env(safe-area-inset-left));
  bottom: max(6px, env(safe-area-inset-bottom));
  z-index: 600;
  pointer-events: none;
  font-family: var(--mono);
  font-size: 9px;
  line-height: 1.4;
  color: #fff;
  background: rgba(0, 0, 0, .55);
  padding: 3px 6px;
  border-radius: 4px;
}

.t-btn--gold {
  background: var(--bounty);
  color: var(--mud-deep);
  box-shadow: 0 4px 0 var(--bounty-deep), 0 10px 20px -12px rgba(0,0,0,.5);
}
.t-btn--gold:active { box-shadow: 0 1px 0 var(--bounty-deep); }

.t-btn--green {
  background: var(--meadow);
  color: #fff;
  text-shadow: 0 1px 0 rgba(0,0,0,.18);
  box-shadow: 0 4px 0 var(--meadow-dark), 0 10px 20px -12px rgba(0,0,0,.5);
}
.t-btn--green:active { box-shadow: 0 1px 0 var(--meadow-dark); }

.t-btn--mud {
  background: var(--mud);
  color: #fff;
  box-shadow: 0 4px 0 var(--mud-deep), 0 10px 20px -12px rgba(0,0,0,.5);
}
.t-btn--mud:active { box-shadow: 0 1px 0 var(--mud-deep); }

.t-btn--ember {
  background: #FF6B35;
  color: #2a1200;
  box-shadow: 0 4px 0 #A83E14, 0 10px 20px -12px rgba(0,0,0,.6);
}
.t-btn--ember:active { box-shadow: 0 1px 0 #A83E14; }

/* Quiet, secondary action on a parchment card */
.t-btn--ghost {
  background: transparent;
  color: var(--ink-soft);
  box-shadow: inset 0 0 0 2px var(--edge);
  font-size: 15px;
  padding: 11px 20px;
}
.t-btn--ghost:active { transform: translateY(2px); box-shadow: inset 0 0 0 2px var(--edge); }

/* Quiet, secondary action on a dark card */
.t-btn--ghost-dark {
  background: transparent;
  color: var(--parchment-dim);
  box-shadow: inset 0 0 0 2px rgba(255,255,255,.28);
  font-size: 15px;
  padding: 11px 20px;
}
.t-btn--ghost-dark:active { transform: translateY(2px); box-shadow: inset 0 0 0 2px rgba(255,255,255,.28); }

.t-btn--danger {
  background: transparent;
  color: var(--danger);
  box-shadow: inset 0 0 0 2px var(--danger);
  font-size: 15px;
  padding: 11px 20px;
}
.t-btn--danger:active { transform: translateY(2px); box-shadow: inset 0 0 0 2px var(--danger); }

/* Inline SVG icons — glyphs like ↩ get emoji presentation on iOS, so every
   piece of UI chrome that isn't deliberately an emoji uses a drawn icon. */
.t-svg-icon {
  width: 1em;
  height: 1em;
  display: block;
}
.t-minimise-btn .t-svg-icon { width: 16px; height: 16px; }
.t-minimise-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* ═══════════════════════════════════ SEGMENTED OPTIONS ("paving stones") */

.t-field { margin-bottom: 16px; }

.t-seg {
  display: flex;
  gap: 7px;
  justify-content: center;
}

.t-opt {
  flex: 1;
  min-width: 0;
  padding: 12px 8px;
  border: none;
  border-radius: 12px;
  background: #fff;
  color: var(--ink);
  font-family: var(--display);
  font-weight: 700;
  font-size: 13px;
  line-height: 1.2;
  cursor: pointer;
  box-shadow: 0 3px 0 var(--edge), inset 0 0 0 1px var(--edge);
  transition: transform .1s, box-shadow .1s, background .12s, color .12s;
}
.t-opt:active { transform: translateY(2px); box-shadow: 0 1px 0 var(--edge), inset 0 0 0 1px var(--edge); }

.t-opt.is-selected {
  background: var(--mud);
  color: #fff;
  box-shadow: 0 3px 0 var(--mud-deep), inset 0 0 0 1px var(--mud-deep);
}

.t-opt__sub {
  display: block;
  font-family: var(--mono);
  font-weight: 400;
  font-size: 9px;
  letter-spacing: .8px;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-top: 3px;
}
.t-opt.is-selected .t-opt__sub { color: var(--parchment-dim); }

.t-select {
  font-family: var(--body);
  font-size: 14px;
  font-weight: 500;
  padding: 10px 14px;
  min-width: 170px;
  text-align: center;
  color: var(--ink);
  background: #fff;
  border: none;
  border-radius: 12px;
  box-shadow: 0 3px 0 var(--edge), inset 0 0 0 1px var(--edge);
  outline: none;
}

/* ═══════════════════════════════════ SLIDERS */

.t-label--row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}

.t-slider__val {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .6px;
  text-transform: none;
  color: var(--mud);
  font-weight: 600;
}

.t-slider {
  -webkit-appearance: none;
  appearance: none;
  display: block;
  width: 100%;
  height: 12px;
  margin: 0;
  padding: 0;
  background: #fff;
  border: none;
  border-radius: 999px;
  box-shadow: inset 0 0 0 1px var(--edge), inset 0 2px 0 rgba(0,0,0,.07);
  outline: none;
  cursor: pointer;
}

/* Thumb repeated per engine — a combined selector list is dropped whole by
   browsers that don't recognise every prefix in it. */
.t-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 26px;
  height: 26px;
  border: none;
  border-radius: 50%;
  background: var(--mud);
  box-shadow: 0 3px 0 var(--mud-deep), inset 0 0 0 1px var(--mud-deep);
  cursor: pointer;
}
.t-slider::-moz-range-thumb {
  width: 26px;
  height: 26px;
  border: none;
  border-radius: 50%;
  background: var(--mud);
  box-shadow: 0 3px 0 var(--mud-deep), inset 0 0 0 1px var(--mud-deep);
  cursor: pointer;
}
.t-slider::-moz-range-track { background: transparent; }

.t-slider:focus-visible { box-shadow: inset 0 0 0 2px var(--bounty); }

/* Per-player path style row */
.t-player-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin-bottom: 7px;
}
.t-player-row .player-dot { box-shadow: inset 0 -2px 0 rgba(0,0,0,.22), 0 0 0 1.5px rgba(0,0,0,.12); }
.t-player-row .t-player-tag {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 1px;
  color: var(--ink-soft);
  min-width: 22px;
}
.t-player-row .t-select { min-width: 0; flex: 0 1 150px; font-size: 13px; padding: 8px 10px; }

/* ═══════════════════════════════════ RESULTS */

.t-stars {
  display: flex;
  justify-content: center;
  gap: 6px;
  font-size: 30px;
  line-height: 1;
  margin: 0 0 14px;
}
.t-star--on  { color: var(--bounty); text-shadow: 0 2px 0 var(--bounty-deep); }
.t-star--off { color: var(--edge); }
.t-card--gold .t-star--off { color: rgba(255,255,255,.22); }
/* The star key under the stars themselves (issue #120): each star names what
   earns it, so a hollow one says what was missed. Columns, so the three
   captions sit under their stars whatever their width, and the row costs
   the card one line of small type rather than a key of its own. */
.t-stars--keyed { gap: 6px; align-items: flex-start; }
/* The plus between the stars: the ladder is cumulative, and the row reads
   as one sum — win + most bounty + bridges kept. The mud the card's buttons
   are lettered in, held back to half: at full strength it outweighed the
   hollow stars either side of it, and in the hollow star's own colour it
   vanished. A sign between the marks, quieter than any of them. */
.t-star-plus {
  font-family: var(--display);
  font-weight: 800;
  font-size: 22px;
  line-height: 30px;
  margin: 0 2px;
  color: var(--mud);
  opacity: .42;
}
.t-card--gold .t-star-plus { color: #fff; opacity: .38; }
.t-star-col {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  min-width: 72px;
}
.t-star-cap {
  font-family: var(--mono);
  font-size: 9.5px;
  letter-spacing: .8px;
  text-transform: uppercase;
  color: var(--mud);
  white-space: nowrap;
}
.t-star-col.is-off .t-star-cap { color: var(--ink-soft); opacity: .55; }

.t-result {
  margin-bottom: 8px;
  border-radius: 12px;
  background: #fff;
  box-shadow: inset 0 0 0 1px var(--edge);
  overflow: hidden;
}
.t-result--winner {
  background: var(--bounty);
  box-shadow: inset 0 0 0 1px var(--bounty-deep);
}
.t-result__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 9px 12px;
  font-weight: 600;
  font-size: 15px;
}
.t-result__who { display: flex; align-items: center; gap: 8px; }
.t-result__score { font-weight: 700; font-size: 18px; font-variant-numeric: tabular-nums; }
.t-result__detail {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .4px;
  color: var(--ink-soft);
  padding: 0 12px 8px;
  text-align: right;
}
.t-result--winner .t-result__detail { color: var(--mud-deep); }

.t-best {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--mud);
  opacity: .75;
  margin: -6px 0 10px;
}
.t-best__stars { font-size: 13px; letter-spacing: 2px; color: var(--bounty); text-shadow: 0 1px 0 var(--bounty-deep); }
.t-card--gold .t-best { color: rgba(255,255,255,.7); }

.t-flag {
  display: inline-block;
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--bounty-deep);
  border: 1px solid var(--bounty-deep);
  border-radius: 5px;
  padding: 3px 8px;
  margin-bottom: 10px;
}

/* Boss briefing */
.t-boss-crest {
  font-size: 34px;
  line-height: 1;
  margin-bottom: 8px;
}
/* Same white-on-parchment panel the results rows use, so an ability reads as
   one more piece of briefing rather than as an alarm. */
.t-ability {
  text-align: center;
  margin-bottom: 14px;
  padding: 12px;
  border-radius: 12px;
  background: #fff;
  box-shadow: inset 0 0 0 1px var(--edge);
}
/* Stroked line art, not an emoji — so it takes a colour, and takes the
   heading's. See BOSS_ABILITIES in js/boss-abilities.js for why. */
/* Flex, not text-align: .t-svg-icon is a block, so the card centring the text
   around it leaves the mark itself sitting against the left edge. Same reason
   .t-title--warn is a flex row. */
.t-ability__icon {
  display: flex;
  justify-content: center;
  font-size: 30px;
  line-height: 1;
  color: var(--forest);
  margin-bottom: 3px;
}
.t-ability__name {
  font-family: var(--display);
  font-weight: 700;
  font-size: 16px;
  color: var(--forest);
  margin: 5px 0 3px;
}
.t-ability__text { font-size: 13.5px; line-height: 1.45; color: var(--ink-soft); }

.t-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13.5px;
}
.t-table th {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 1px;
  text-transform: uppercase;
  font-weight: 400;
  color: var(--ink-soft);
  padding: 6px 8px;
  border-bottom: 2px solid var(--edge);
}
.t-table td { padding: 9px 8px; border-bottom: 1px solid var(--edge); }
.t-table tfoot td {
  border-top: 2px solid var(--edge);
  border-bottom: none;
  font-weight: 700;
  padding-top: 11px;
}
/* A TALL CARD ON A SHORT PHONE (issue #155). The campaign scoreboard is the
   tallest card in the game — five ranks, a total, a note and three buttons —
   and on a 360x640 it wanted 595px inside a card that caps at 88svh = 563.
   The overflow does not run off the screen, which is why it is easy to miss:
   `.t-card` scrolls, so the last call to action sat below the card's OWN
   fold, with nothing on screen to say it was there. Measure a card with
   `scrollHeight > clientHeight`, never against the viewport.

   What gives is the rhythm, never the type: a table row loses padding and
   the card loses some of its own, so nothing gets smaller to read. 45px,
   which clears the 640 with room and takes the 375x667 SE with it. */
@media (max-height: 700px) {
  .t-card { padding: 18px 22px; }
  .t-table td { padding: 6px 8px; }
  .t-table tfoot td { padding-top: 8px; }
  /* Three full-width buttons stacked: 9px between them is generous on a
     screen this size, and the gaps are the last thing a player reads. */
  .t-btn { margin-top: 6px; }
  .t-body { margin-bottom: 10px; }
  .t-boss-crest { font-size: 28px; margin-bottom: 6px; }
}

.t-table .col-rank  { text-align: left; font-weight: 600; }
.t-table .col-stars { text-align: center; color: var(--bounty); text-shadow: 0 1px 0 var(--bounty-deep); font-weight: 600; }
.t-table .col-score { text-align: right; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
.t-table tr.is-locked { opacity: .4; }
.t-rank-dot {
  display: inline-block;
  width: 9px; height: 9px;
  border-radius: 50%;
  margin-right: 7px;
  vertical-align: middle;
  box-shadow: 0 0 0 1px rgba(0,0,0,.25);
}
/* Dark-card variants of the table */
.t-card--gold .t-table th,
.t-card--gold .t-table td { border-color: rgba(255,255,255,.14); }
.t-card--gold .t-table th,
.t-card--gold .t-table .col-score { color: var(--parchment-dim); }
.t-card--gold .t-table .col-stars { color: var(--bounty); }

/* Minimise (▼) control pinned to the top-right of a results card */
.t-minimise-bar {
  position: sticky;
  top: 0;
  height: 0;
  display: flex;
  justify-content: flex-end;
  z-index: 2;
}
.t-minimise-btn {
  width: 32px; height: 32px;
  border: none;
  border-radius: 10px;
  background: var(--parchment-dim);
  color: var(--ink-soft);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
  padding: 0;
  box-shadow: inset 0 0 0 1px var(--edge);
}
.t-card--gold .t-minimise-btn {
  background: rgba(255,255,255,.14);
  color: #fff;
  box-shadow: none;
}

/* ═══════════════════════════════════ WORLD MAP */

#campaign-map-overlay {
  position: fixed;
  inset: 0;
  /* The letterbox, the body's own ground, either side of the column below. */
  background: var(--forest-deep);
  overflow: hidden;
}
/* The column the map stands in: the one the game shell is capped at
   (--column), so the map's bar ends where the HUD's does (issue #153). It
   used to be the overlay itself, edge to edge: on a desktop window the scene
   stood 448px wide in the middle of it with Menu and the stars pill a screen
   apart at the far edges, while the game beside it kept to a column. The
   scene is authored at the column's width, so the bar is exactly as wide as
   the map. */
.t-map-shell {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
  max-width: var(--column);
  margin: 0 auto;
  background: var(--forest);
}

.t-map-viewport {
  flex: 1;
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  position: relative;
  /* The status bar: the scene scrolls under it, as a map should, and this is
     what lets its top — the castle's sky — still be brought fully into view.
     showWorldMap's scroll maths reads the scene's offset below it. */
  padding-top: var(--safe-top);
}
.t-map-container { position: relative; max-width: var(--column); margin: 0 auto; }
.t-map-scene-wrap { position: relative; }
/* The scene carries its 448x2319 size as attributes, so it has an intrinsic
   aspect ratio to scale by — the level nodes are positioned in percentages of
   exactly that box. */
.t-map-scene { width: 100%; height: auto; display: block; }
.t-map-layer { position: absolute; inset: 0; }
/* The trail is trodden only as far as the player has come (issue #100). The
   scene draws the whole road, a cell at a time, and the map puts every cell
   past the highest unlocked level back to grass — the ground is painted under
   the road, so hiding a cell is all it takes. The stretch just earned stomps
   in instead, one cell after another on its own delay: each comes down a
   little over size and settles, the way a foot does. */
.t-map-scene .is-grass { display: none; }
.t-map-scene .is-stomp {
  transform-box: fill-box;
  transform-origin: center;
  animation: mapStomp .28s cubic-bezier(.3, 1.4, .5, 1) both;
}

.t-rank-label {
  position: fixed;
  top: calc(14px + var(--safe-top)); left: 50%;
  transform: translateX(-50%);
  z-index: 210;
  pointer-events: none;
  padding: 8px 18px;
  border-radius: 999px;
  background: rgba(12,26,14,.8);
  border: 2px solid rgba(255,255,255,.18);
  color: #fff;
  font-family: var(--display);
  font-weight: 700;
  font-size: 15px;
  letter-spacing: .5px;
  white-space: nowrap;
  transition: opacity .3s, border-color .3s;
}

.t-level-node {
  position: absolute;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  padding: 0;
  font-family: var(--display);
  font-weight: 700;
  color: #fff;
  text-shadow: 0 1px 3px rgba(0,0,0,.9);
  cursor: pointer;
  transition: transform .1s;
}
.t-level-node.is-locked { cursor: default; }
/* A boss mark on a level node rides in the label's text flow: the base
   .t-svg-icon is a block, which would stack the icon above the number instead
   of beside it. Sized in em so it follows the node's own font-size, and given
   its shadow as a filter — text-shadow does not reach an SVG's strokes, and
   without one the mark washes out over the map's pale cells. */
.t-level-node .t-svg-icon {
  display: inline-block;
  width: 1em;
  height: 1em;
  vertical-align: -.14em;
  stroke-width: 3;
  filter: drop-shadow(0 1px 2px rgba(0,0,0,.9));
}
.t-level-node__stars {
  font-size: 10px;
  line-height: 1;
  letter-spacing: 1px;
  color: var(--bounty);
  text-shadow: 0 1px 2px rgba(0,0,0,.8);
}

.t-map-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 12px 14px;
  padding-bottom: max(12px, var(--safe-bottom));
  flex-shrink: 0;
  background: var(--forest-deep);
  border-top: 2px solid var(--meadow-dark);
  box-shadow: 0 -8px 20px rgba(0,0,0,.6);
  min-height: 54px;
}
.t-map-footer__title {
  font-family: var(--display);
  font-weight: 800;
  font-size: 17px;
  letter-spacing: -.01em;
  color: #fff;
}
.t-stars-pill {
  border: none;
  border-radius: 999px;
  background: var(--bounty);
  color: var(--mud-deep);
  font-family: var(--display);
  font-weight: 700;
  font-size: 14px;
  padding: 8px 15px;
  cursor: pointer;
  box-shadow: 0 3px 0 var(--bounty-deep);
  transition: transform .1s, box-shadow .1s;
}
.t-stars-pill:active { transform: translateY(2px); box-shadow: 0 1px 0 var(--bounty-deep); }

/* ═══════════════════════════════════ TRANSIENT LAYERS */

/* One card, whether it times out or waits to be dismissed — the two say the
   same sentence and must look the same saying it. The chevron is absolutely
   positioned, so it takes no width off a short line, and the min-width stops
   one shrink-wrapping to a sliver beside it. */
#turn-popup {
  position: fixed;
  top: 20%; left: 50%;
  transform: translateX(-50%);
  background: var(--parchment);
  color: var(--ink);
  padding: 22px 26px 15px;
  /* `left: 50%` leaves only half the viewport as available width, and a
     shrink-to-fit box takes that as its ceiling — which wrapped "The 6 point
     bounty" mid-line however much room was really there. max-content sizes to
     the longest line the copy asks for and max-width brings it back in. */
  width: max-content;
  min-width: 210px;
  max-width: min(78vw, 340px);
  border-radius: 16px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 17px;
  line-height: 1.35;
  text-align: center;
  /* The board underneath stays live, so the card must not intercept it: a
     stroke that begins under the card has to reach the canvas, and beginning
     it is what puts the card away. Only the chevron takes taps. */
  pointer-events: none;
  box-shadow: 0 4px 0 var(--edge), 0 20px 40px -18px rgba(0,0,0,.7);
  opacity: 1;
  transition: opacity .3s ease;
}
/* The one part of the card that takes a tap — the card itself is
   pointer-events: none so the live board underneath still gets its strokes. */
.t-popup-x {
  pointer-events: auto;
  position: absolute;
  top: 5px;
  right: 8px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 22px;
  padding: 0;
  border: none;
  border-radius: 6px;
  background: none;
  color: var(--ink-soft);
  cursor: pointer;
}
.t-popup-x .t-svg-icon { width: 16px; height: 16px; }

#phase-banner {
  position: fixed;
  top: 42%; left: 50%;
  transform: translate(-50%, -50%);
  color: #fff;
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(40px, 12vw, 84px);
  letter-spacing: -.03em;
  text-align: center;
  white-space: nowrap;
  pointer-events: none;
  text-shadow: 0 3px 0 var(--meadow-dark), 0 8px 30px rgba(0,0,0,.85);
}
/* The level's name over the kick-off headline (issue #120): "LEVEL 1 · FIRST
   STEPS", mono caps, the way the map's rank label and the chip's numbers
   speak — a label, so the headline stays the event. */
#phase-banner .t-banner-eyebrow {
  display: block;
  margin-bottom: 10px;
  font-family: var(--mono);
  font-weight: 600;
  font-size: clamp(11px, 3.3vw, 13px);
  letter-spacing: 2.2px;
  text-transform: uppercase;
  white-space: nowrap;
  color: rgba(255, 255, 255, .92);
  text-shadow: 0 2px 0 var(--meadow-dark), 0 4px 14px rgba(0, 0, 0, .8);
}
/* The banner's one line of news, under the headline: what this moment did and
   what is coming, in the chip's own words (ui.bountyPopupText). It took over
   from the round-start card, which said the same thing in a second object on
   the same beat. Body type, so the headline stays the event and this stays
   the footnote to it. */
#phase-banner .t-banner-sub {
  display: block;
  margin-top: 8px;
  font-family: var(--body);
  font-weight: 600;
  font-size: clamp(15px, 4.4vw, 19px);
  letter-spacing: 0;
  line-height: 1.25;
  white-space: normal;
  max-width: 88vw;
  margin-left: auto;
  margin-right: auto;
  text-shadow: 0 2px 0 var(--meadow-dark), 0 4px 16px rgba(0,0,0,.8);
}

/* ═══════════════════════════════════ THE COACH (issue #120)
   Level 1's lesson cards (js/coach.js). A card of the round-start card's
   kind — the same parchment, the same face — with a tail to the thing it is
   about, and that thing lit by a spotlight: a hole in a dim scrim that
   slides from subject to subject as a tour steps on. The board under it all
   stays live: the scrim and the card take no taps, only the card's buttons
   do, and the first stroke puts the card away (main.js). */
.t-coach-scrim {
  position: fixed;
  inset: 0;
  z-index: 140;
  pointer-events: none;
  overflow: hidden;
  transition: opacity .3s;
}
.t-coach-scrim.is-off { opacity: 0; }
/* The hole IS the element: its shadow is the scrim, thrown past every edge,
   so moving the element moves the hole and the transition slides it from
   one subject to the next. */
.t-coach-spot {
  position: absolute;
  border-radius: 14px;
  box-shadow: 0 0 0 200vmax rgba(10, 22, 12, .46), inset 0 0 0 2px rgba(255, 255, 255, .5);
  transition: left .38s cubic-bezier(.2,.85,.3,1), top .38s cubic-bezier(.2,.85,.3,1),
              width .38s cubic-bezier(.2,.85,.3,1), height .38s cubic-bezier(.2,.85,.3,1);
}
/* A breathing gold rim, so the lit thing reads as chosen rather than merely
   left undimmed. */
.t-coach-spot::after {
  content: '';
  position: absolute;
  inset: -3px;
  border-radius: 17px;
  box-shadow: 0 0 0 2px rgba(232, 184, 74, .8), 0 0 22px 5px rgba(232, 184, 74, .38);
  animation: coachSpotBreathe 1.8s ease-in-out infinite;
}

.t-coach {
  position: fixed;
  z-index: 150;
  width: max-content;
  min-width: 220px;
  max-width: min(84vw, 320px);
  padding: 13px 18px 13px;
  border-radius: 16px;
  background: var(--parchment);
  color: var(--ink);
  text-align: left;
  pointer-events: none;
  box-shadow: 0 4px 0 var(--edge), 0 20px 40px -18px rgba(0,0,0,.7);
  transition: left .32s cubic-bezier(.2,.85,.3,1), top .32s cubic-bezier(.2,.85,.3,1);
}
.t-coach.is-in { animation: coachIn 260ms cubic-bezier(.2,.85,.3,1) both; }
.t-coach button { pointer-events: auto; }
/* The tail: a diamond on whichever edge faces the subject, at --tail-x —
   the subject's centre, not the card's, so a card clamped to the viewport
   edge still points at the right thing. Rotated 45deg, a shadow offset down
   its own axes lands under the tip, which is where the card's emboss is. */
.t-coach::after {
  content: '';
  position: absolute;
  left: var(--tail-x, 50%);
  width: 14px;
  height: 14px;
  margin-left: -7px;
  background: var(--parchment);
  transform: rotate(45deg);
}
.t-coach[data-arm="up"]::after { bottom: -6px; box-shadow: 3px 3px 0 -1px var(--edge); }
.t-coach[data-arm="down"]::after { top: -6px; }
.t-coach:not([data-arm])::after { display: none; }

.t-coach__title {
  font-family: var(--display);
  font-weight: 800;
  font-size: 18px;
  line-height: 1.1;
  letter-spacing: -.01em;
  color: var(--forest);
  margin: 0 0 5px;
}
.t-coach__body {
  font-size: 14px;
  line-height: 1.4;
  color: var(--ink-soft);
  margin: 0;
}
.t-coach__body strong { color: var(--ink); font-weight: 600; }
.t-coach__actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 11px;
}
.t-coach__btn {
  border: none;
  border-radius: 10px;
  padding: 8px 16px;
  background: var(--bounty);
  color: var(--mud-deep);
  font-family: var(--display);
  font-weight: 700;
  font-size: 14px;
  cursor: pointer;
  box-shadow: 0 3px 0 var(--bounty-deep);
}
.t-coach__btn:active { transform: translateY(2px); box-shadow: 0 1px 0 var(--bounty-deep); }
/* Skip tutorial: a quiet mono link at the other end of the row from Got it,
   on the first card only. */
.t-coach__skip {
  margin-right: auto;
  padding: 6px 0;
  border: none;
  background: none;
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--ink-soft);
  cursor: pointer;
}

/* The ghost finger: a fingertip's soft disc, sized to the tile, pressed
   (small, dense) while it draws and lifted (larger, fainter) between
   strokes. The wrapper carries the position and the disc the press, so
   the two transitions never fight over one transform. */
.t-ghost {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 145;
  pointer-events: none;
  transition: transform .3s cubic-bezier(.4, 0, .5, 1);
}
.t-ghost::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: radial-gradient(circle at 40% 38%, rgba(255,255,255,.96), rgba(255,255,255,.62) 52%, rgba(255,255,255,0) 72%);
  box-shadow: 0 0 0 2px rgba(255,255,255,.7), 0 6px 14px rgba(0,0,0,.4);
  transform: scale(.78);
  opacity: .96;
  transition: transform .3s ease, opacity .3s ease;
}
.t-ghost.is-up::before { transform: scale(1.3); opacity: .3; }
/* The pulse on the path's dot while the finger is lifted: "press here". */
.t-ghost-ring {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 144;
  pointer-events: none;
  border-radius: 50%;
  box-shadow: 0 0 0 3px rgba(255,255,255,.9), 0 0 0 7px rgba(232,184,74,.55);
  animation: ghostRing 1s ease-out infinite;
}

/* A flex row, so the mark sits BESIDE the number whatever it is. The base
   .t-svg-icon is display:block — fine inside .t-delta, which is a flex row of
   its own, but in a plain block it dropped the grass blades onto a line under
   the +3. The bridge and the sack happened not to hit it (neither carries
   that class here), which is exactly why the row is the fix and not another
   per-icon display override. */
.score-float {
  position: fixed;
  display: flex;
  align-items: center;
  gap: .12em;
  font-family: var(--display);
  font-weight: 800;
  font-size: 17px;
  white-space: nowrap;
  text-shadow: 0 1px 3px rgba(0,0,0,.85), 0 0 8px rgba(0,0,0,.45);
  pointer-events: none;
  transform: translate(-50%, 0);
  animation: scoreFloatRise 1200ms ease-out both;
}
.score-float--gain { color: var(--meadow-light); }
.score-float--loss { color: #FF6B5C; }
/* A float with a row to land in is driven by the Web Animations API instead
   (ui._launchFloat): the rise is the same, but its end is a measured point in
   the panel rather than 52px up. */
.score-float--fly { animation: none; }

/* ═══════════════════════════════════ KEYFRAMES */

/* The log advancing: the finished turn drops out of the bottom while the new
   one comes down from above, both clipped by the row. Downwards because that
   is the way the score floats arrive (issue #116) — the panel sits under the
   board, so a float dives down into its row and the last turn has to give way
   in the same direction, or the two pass each other head-on. Past 105% so
   neither shows a sliver at the edge on a fractional row height. */
@keyframes deltaLineIn {
  from { transform: translateY(-105%); opacity: 0; }
  to   { transform: none;              opacity: 1; }
}
@keyframes deltaLineOut {
  from { transform: none;             opacity: 1; }
  to   { transform: translateY(105%); opacity: 0; }
}

@keyframes scoreFloatRise {
  0%   { opacity: 0; transform: translate(-50%, 6px) scale(0.7); }
  12%  { opacity: 1; transform: translate(-50%, -6px) scale(1.12); }
  24%  { transform: translate(-50%, -12px) scale(1); }
  70%  { opacity: 1; }
  100% { opacity: 0; transform: translate(-50%, -52px) scale(1); }
}

/* An entry arriving under its float: a little too big, then settling. */
@keyframes deltaLand {
  0%   { transform: scale(1.4); }
  100% { transform: none; }
}

@keyframes scorePop {
  0%   { transform: scale(1); }
  35%  { transform: scale(1.5); }
  100% { transform: scale(1); }
}

@keyframes phaseBannerFlash {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(0.6); }
  14%  { opacity: 1; transform: translate(-50%, -50%) scale(1.08); }
  24%  { transform: translate(-50%, -50%) scale(1); }
  78%  { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(1.12); }
}

@keyframes bossGlow {
  0%, 100% { box-shadow: 0 0 8px 3px rgba(220, 40, 40, 0.6); }
  50%      { box-shadow: 0 0 16px 6px rgba(220, 40, 40, 0.9); }
}

@keyframes currentLevel {
  0%, 100% { box-shadow: 0 0 10px 4px rgba(232, 184, 74, 0.55); }
  50%      { box-shadow: 0 0 20px 8px rgba(232, 184, 74, 0.9); }
}

.t-level-node--boss    { animation: bossGlow 2s ease-in-out infinite; }
.t-level-node--current { animation: currentLevel 1.5s ease-in-out infinite; }

@keyframes coachIn {
  from { opacity: 0; transform: translateY(6px) scale(.94); }
  to   { opacity: 1; transform: none; }
}
@keyframes coachSpotBreathe {
  0%, 100% { opacity: .55; }
  50%      { opacity: 1; }
}
@keyframes ghostRing {
  0%   { transform: scale(.55); opacity: 1; }
  100% { transform: scale(1.5); opacity: 0; }
}

@keyframes mapStomp {
  0%   { opacity: 0; transform: scale(.3); }
  55%  { opacity: 1; transform: scale(1.14); }
  100% { opacity: 1; transform: scale(1); }
}

/* ═══════════════════════════════════ RESPONSIVE */

@media (max-width: 400px) {
  /* The padding is set through its variables so the safe-area inset and the
     height's sum (see #ui-panel) follow it — a shorthand here once reset the
     inset padding to 6px on every phone under 400px wide. */
  #ui-panel { --hud-pad-y: 6px; --hud-pad-x: 7px; }
  .t-card { padding: 20px 16px; }
  .t-btn { font-size: 16px; padding: 13px 18px; }
}

/* Narrow phones. The bounty chip is what gives first: it is the only element
   in the strip with slack, and the two labelled controls either side of it
   have to stay pressable. Plain px: at 360px wide the HUD unit is at its 1px
   floor (its width term, 3.5vw, is under the 13px the clamp starts from), so
   these are exact. */
@media (max-width: 360px) {
  #hud-controls { gap: 5px; }
  .t-hud-btn { padding: 0 9px; font-size: 12px; }
  .t-bounty { min-width: 0; gap: 2px; padding: 0 4px; }
  .t-rung svg { width: 21px; height: 21px; }
  .t-rung span { font-size: 11px; }
}

@media (max-height: 700px) {
  .t-title--hero { font-size: clamp(40px, 12vw, 60px); }
  .t-btn { margin-top: 7px; padding: 12px 20px; }
}

/* Landscape warning */
@media (orientation: landscape) and (max-height: 500px) {
  #game-container::before {
    content: 'Please rotate to portrait mode';
    position: fixed;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--forest-deep);
    color: var(--parchment-dim);
    font-family: var(--display);
    font-weight: 700;
    font-size: 18px;
    z-index: 1000;
  }
}
