/* Press Start 2P (the Muse build's pixel font) is bundled locally so the
   local package renders identically offline — no CDN dependency. The font
   is SIL Open Font License 1.1 (Copyright 2012 The Press Start 2P Project
   Authors); this latin subset file is served unmodified. */
@font-face {
  font-family: "Press Start 2P";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/press-start-2p-latin.woff2') format('woff2');
}
/* Leg Power Factory — port client styling.
   LOOK PARITY: this sheet carries the Muse build's locked palette and
   component styling (theme.css of the Muse build): the bright pixel world
   against deep teal ship panels.
     bg #071e26 · surface #0a2a33 · surface-alt #0d3541 · border #2b6f5e
     text #eafff8 · dim #8fd8c7 · accent #2fe6a7 · mint #4fe3c1
     primary button #17b978 with #4fe3c1 border · gold #ffd83d
     danger #c0392b with #ff9d7a border · warning #ff9d7a · error #ff5f45
     bar track #12333d with #5b7a80 border · disabled #39555a/#5b7a80/#b9cdd0
   Headings/labels use the pixel font ("Press Start 2P", as in the Muse
   build); prose stays in the plain sans, same split as the Muse build. */
:root {
  --bg: #071e26;
  --surface: #0a2a33;
  --surface-alt: #0d3541;
  --text: #eafff8;
  --dim: #8fd8c7;
  --border: #2b6f5e;
  --accent: #2fe6a7;
  --mint: #4fe3c1;
  --gold: #ffd83d;
  --btn: #17b978;
  --danger: #c0392b;
  --danger-border: #ff9d7a;
  --warn: #ff9d7a;
  --error: #ff5f45;
  --track: #12333d;
  --track-border: #5b7a80;
  --disabled-bg: #39555a;
  --disabled-border: #5b7a80;
  --disabled-text: #b9cdd0;
  --deep: #12262c;
  --grass: #39c12d;
  --font-pixel: "Press Start 2P", ui-monospace, monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
/* S5 fix (F3): the document itself must NEVER scroll or pan sideways —
   on the owner's phone the page could be dragged left/right (a zoomed
   or over-wide layout in the WebView), which carried fixed panels'
   close buttons off the visible edge. overflow-x is locked on html as
   well as body (body alone does not always propagate to the viewport
   in Android WebView), and overscroll is off so no drag can shift the
   page at all. */
html { overflow-x: hidden; }
body {
  font-family: Verdana, Geneva, sans-serif; background: var(--bg); color: var(--text);
  overflow: hidden; -webkit-user-select: none; user-select: none;
  touch-action: manipulation; overscroll-behavior: none;
}
.hidden { display: none !important; }
.dim { color: var(--dim); font-size: 0.8em; }
button {
  font-family: var(--font-pixel); font-size: 10px; line-height: 1.45;
  background: var(--surface-alt); color: var(--text);
  border: 2px solid var(--border); border-radius: 4px; padding: 8px 12px; cursor: pointer;
}
button:active { background: var(--btn); }
button:disabled { background: var(--disabled-bg); border-color: var(--disabled-border); color: var(--disabled-text); cursor: default; }
input, select {
  font-family: inherit; font-size: 1em; padding: 7px; border-radius: 4px;
  border: 2px solid var(--border); background: var(--surface-alt); color: var(--text);
}
input[type="range"], input[type="checkbox"] { accent-color: var(--btn); }

/* Build V (Part 4): the title screen — a full-screen fixed layer
   over everything (auth and game alike), shown from page load and
   dismissed only by the player's tap. The art fills the screen. */
/* Build W (Part 2): the title art fits WHOLE — object-fit: contain,
   nothing cropped on any side (Build V's cover clipped the top of
   the STRIDE FACTORY lettering on the owner's phone). Leftover
   space takes the art's own starfield colour, #06091d sampled from
   the art's top corners, so the screen still reads as one picture. */
#titleScreen { position: fixed; inset: 0; z-index: 60; background: #06091d; cursor: pointer; }
#titleScreen img { width: 100%; height: 100%; object-fit: contain; display: block; }

/* ---------- auth ---------- */
#authView { display: flex; align-items: center; justify-content: center; height: 100%; background: var(--bg); }
.authCard { background: var(--surface); border: 2px solid var(--border); border-radius: 6px; padding: 22px; width: min(92vw, 380px); box-shadow: 0 8px 30px rgba(0,0,0,.4); }
.authCard h1 { font-family: var(--font-pixel); font-size: 15px; line-height: 1.5; text-align: center; color: #fff; text-shadow: 2px 2px 0 #04160a; }
.tagline { text-align: center; color: var(--dim); margin: 8px 0 14px; font-size: .9em; }
.authTabs { display: flex; gap: 6px; margin-bottom: 12px; }
.authTabs .tab { flex: 1; background: var(--surface-alt); color: var(--dim); }
.authTabs .tab.active { background: var(--btn); border-color: var(--mint); color: #fff; }
.authCard label { display: block; margin: 10px 0; font-size: .9em; }
.authCard input { width: 100%; margin-top: 4px; }
.bigBtn { width: 100%; padding: 12px; font-size: 11px; background: var(--btn); border-color: var(--mint); color: #fff; margin-top: 8px; }
.bigBtn.alt { background: var(--surface-alt); border-color: var(--border); color: var(--mint); }
.msg { min-height: 1.2em; color: var(--warn); text-align: center; margin-top: 8px; font-size: .9em; }
.fineprint { color: var(--dim); font-size: .75em; margin-top: 12px; text-align: center; }

/* ---------- game frame ---------- */
/* S5 fix (F4): height tracks the DYNAMIC viewport where supported, so
   the layout fills exactly the visible screen in the app (no dead band
   under the dock from a taller layout viewport) and the map takes the
   reclaimed height through #worldWrap's flex. */
#gameView { display: flex; flex-direction: column; height: 100%; height: 100dvh;
  /* Build C fix 2: the frame is PINNED to all four viewport edges. The
     old in-flow 100dvh box could end short of the visual viewport's
     bottom in the app WebView, leaving a blank band UNDER the dock
     chips; inset:0 makes the flex column end exactly at the bottom
     edge, so the dock (its last child) always reaches the bottom of
     the game screen and the map takes the space above. ABSOLUTE, not
     fixed: a fixed element always forms a stacking context, which
     trapped the harvest strip's z-index inside this frame and let the
     sibling panel backdrop paint over it (STOP unreachable). */
  position: absolute; inset: 0; height: auto; }
#topbar {
  display: flex; align-items: center; gap: 6px; padding: 6px 10px;
  background: var(--surface); border-bottom: 2px solid var(--border); font-size: .92em; z-index: 5;
}
/* Build F (Item 2): the section label NEVER wraps — "A-1" split as
   "A-" over "1" on the owner's phone because the flex row shrank this
   box below one line's width and the hyphen is a break opportunity.
   The box now keeps its full one-line width (flex: none) and the row
   absorbs the squeeze elsewhere. Holds for every name A-1 … L-12. */
#sectionBox { background: var(--surface-alt); color: #fff; font-family: var(--font-pixel); font-size: 11px; padding: 5px 9px; border-radius: 4px; border: 2px solid var(--border); white-space: nowrap; flex: none; }
/* Build F (Item 2, companion): the squeeze the section label no
   longer absorbs lands on the username instead of pushing the
   topbar's right-hand buttons off the screen at narrow widths — the
   name is the one element in the row that can shorten without
   losing information the player needs. */
/* Build P (slip-in Q6): the player name shows IN FULL — it used to
   be the bar's only shrinkable item (min-width: 0 + ellipsis), so
   any overflow in the row came out of the name ("te…" for testy).
   The name is now flex-none; the row's breathing room comes from
   the tighter gap below. */
#userName { flex: none; white-space: nowrap; }
#bankBox { font-family: var(--font-pixel); font-size: 11px; color: var(--mint); }
/* Build AT (AT2): the credits readout NEVER wraps and never
   shrinks — at max UI scale "19,731 CR" split mid-number over two
   lines because this box had no wrap/flex protection. Like the
   section box and the player name, it keeps its full one-line
   width; the bar's breathing room absorbs the squeeze. */
#creditsBox { font-family: var(--font-pixel); font-size: 11px; color: #fff; white-space: nowrap; flex: none; }
.spacer { flex: 1; }
#topbar button { padding: 4px 9px; background: transparent; border-color: transparent; color: var(--mint); font-size: 15px; }
#worldWrap { position: relative; flex: 1; overflow: hidden; background: var(--grass); }
#world { position: absolute; inset: 0; width: 100%; height: 100%; display: block; touch-action: none; }
/* Build E (Part 11) + Build I (Part 21): the hh challenge bar —
   pinned to the TOP of the map area, full width edge to edge, in the
   owner's approved mockup proportions (hh-challenge-bar-gold-half-
   v2): a dark teal bar in a mint frame, gold trophy, the mint
   time-to-pass line, a mint STOP button, and a THICK drain bar along
   the bottom spanning the full width (mint fill on the dark track).
   It is THICKER than the HARVESTING strip it replaces. It lives
   inside #worldWrap (the map screen), under the panels' z-index, so
   opening any panel — the node window included — covers it, and
   renderHud hides it outright whenever a panel is open. Only the
   STOP button takes a tap. */
#hhBar {
  position: absolute; top: 0; left: 0; right: 0; z-index: 6;
  background: rgba(10, 42, 51, .97); border: 3px solid var(--mint);
  color: var(--mint); font-family: var(--font-pixel);
  display: flex; align-items: center; gap: 12px;
  padding: 13px 14px 19px; /* bottom room = the drain bar's band */
  pointer-events: none;
}
#hhBar .hhTrophy { font-size: 22px; line-height: 1; flex: none; }
/* Build J (Part 6): the time-to-pass line lives in its OWN box to
   the LEFT of STOP — it wraps inside that box (min-width: 0 lets the
   flex item actually shrink) and can never run under the button at
   any text length. */
#hhBar .hhEta { flex: 1 1 auto; min-width: 0; font-size: 11px; line-height: 1.45; text-align: left; white-space: normal; overflow-wrap: break-word; }
#hhBar #hhStop {
  pointer-events: auto; flex: none;
  background: var(--accent); border: 2px solid var(--accent); border-radius: 6px;
  color: #07301f; font-family: var(--font-pixel); font-size: 12px; padding: 12px 18px;
}
#hhBar .hhProg {
  position: absolute; left: 0; right: 0; bottom: 0; height: 13px;
  background: #11404d; border-top: 2px solid var(--mint); display: block;
}
#hhBar .hhProgFill { display: block; height: 100%; background: var(--accent); width: 0; }
/* Build H (Part 2): the tutorial card — the approved mockup: a dark
   panel in the game's style with a 3px bright mint border, a pixel
   "TUTORIAL n/8" header, a pixel title and a plain body. It sits at
   the top of the map screen (below the hh bar if that is ever up at
   the same time), spans the map width with a margin, and never
   reaches the bottom controls. Only step 8 carries a button. */
#tutCard {
  position: absolute; top: 10px; left: 10px; right: 10px; z-index: 7;
  background: var(--surface); border: 3px solid var(--mint); border-radius: 6px;
  padding: 14px 16px 16px; box-shadow: 0 8px 30px rgba(0,0,0,.45);
  /* Build J (Part 3): the card is a flex column — the header row
     (with the NEXT button at the TOP RIGHT) never scrolls; only the
     BODY scrolls, inside the half-length cap layoutTutCard sets. */
  display: flex; flex-direction: column; overflow: hidden;
}
.tutTop { display: flex; align-items: flex-start; gap: 10px; flex: none; }
.tutHeadWrap { flex: 1; min-width: 0; }
#tutHead { font-family: var(--font-pixel); font-size: 10px; color: var(--mint); margin-bottom: 8px; }
#tutTitle { font-family: var(--font-pixel); font-size: 14px; line-height: 1.5; color: #fff; margin-bottom: 10px; }
#tutTag { font-family: var(--font-pixel); font-size: 10px; line-height: 1.6; color: var(--gold); margin-bottom: 8px; }
/* Build M (Part 1): below the header, the card's main row — the
   text takes the whole left side and the chevron rail rides the
   right edge (see #tutRail). */
.tutMain { display: flex; flex: 1; min-height: 0; gap: 10px; }
#tutBodyWrap { position: relative; flex: 1; min-width: 0; min-height: 0; display: flex; }
#tutBody { overflow-y: auto; min-height: 0; flex: 1; }
#tutBody p { line-height: 1.55; margin-bottom: 10px; }
#tutBody p:last-child { margin-bottom: 0; }
/* Build K (Part 6): the scroll cue is BUTTONS — finger-sized
   chevron tap targets (44 × 36 px). Build M (Part 1, the owner's
   approved mockup): the buttons stack in a RAIL under the NEXT
   button (up over down), in their own column at the card's right
   edge — the Build L bottom strip is gone, the text takes the
   whole left side, and a chevron can still never cover a word.
   The rail renders only while the body can scroll; the buttons
   keep the cue rules (down while more below, up once scrolled),
   and one tap scrolls ONE LINE (app.js tutLineStep). */
#tutRail {
  /* Build S (Part 2): the gap between the UP and DOWN chevrons is
     slightly bigger than Build R's 6px (owner's order). */
  flex: none; display: flex; flex-direction: column; gap: 10px;
  width: 44px; align-items: stretch;
}
.tutChev {
  width: 44px; height: 36px; min-width: 44px; min-height: 36px;
  color: var(--gold); font-size: 16px; line-height: 1;
  background: rgba(10, 42, 51, .82); border: 2px solid var(--gold); border-radius: 6px;
  text-shadow: 0 1px 3px #000; padding: 0;
}
/* Build AZ (AZ14): the chevron at its end (up at the top, down at
   the bottom) stays VISIBLE but dimmed — it cannot scroll that way. */
.tutChev.dimmed { opacity: 0.35; }
/* Build I (Part 15): the NEXT button — GREY and dead until the
   part's action is complete, GOLD when it is (the .gold class,
   driven by the server's tut_next fact). Part 8's reads START
   PLAYING (label set by renderTut). Build J (Part 3): it sits at
   the TOP RIGHT of the card. */
#tutNext {
  background: var(--disabled-bg); border: 2px solid var(--disabled-border); color: var(--disabled-text);
  padding: 11px 18px; font-size: 11px; flex: none;
}
#tutNext.gold { background: var(--gold); border-color: var(--gold); color: #4a3200; }
/* Build J (Part 3): the scanner-arrow layer — above the tutorial
   card (z 7), below the body-level panels. Pointer-transparent. */
#arrowLayer { position: absolute; inset: 0; z-index: 8; pointer-events: none; width: 100%; height: 100%; }
/* Build J (Part 11): the boost mini-game. The bar rides the TOP of
   the map screen; the zone is a gold stretch of it, the line a white
   needle bouncing along it. Build R (Part 6.2): the BIG RED BUTTON
   moves DOWN permanently — it sits just above the bottom dock
   chips, horizontally DEAD CENTRE (it used to float below the
   character at screen centre). */
#boostBar {
  position: absolute; top: 8px; left: 10px; right: 10px; height: 24px; z-index: 8;
  background: rgba(10,42,51,.92); border: 2px solid var(--border); border-radius: 6px;
  overflow: hidden; pointer-events: none;
}
#boostZone { position: absolute; top: 0; bottom: 0; background: rgba(255,216,61,.55); border-left: 2px solid var(--gold); border-right: 2px solid var(--gold); }
/* Build P (Part 6): the ECO BONUS "100m" flash — positioned by
   fireEcoFlash under the target zone, then floated up to the MOVE
   readout by its WAAPI animation. */
#ecoFlash {
  position: absolute; z-index: 9; pointer-events: none;
  font-family: var(--font-pixel); font-size: 15px; color: var(--gold);
  text-shadow: 0 2px 4px #000; transform: translate(-50%, 0); white-space: nowrap;
}
#boostLine { position: absolute; top: 0; bottom: 0; width: 4px; margin-left: -2px; background: #fff; box-shadow: 0 0 6px #fff; }
/* Build K (Part 1): the live chain multiplier, plain text riding the
   bar's right end. */
#boostMult {
  position: absolute; right: 8px; top: 0; bottom: 0; display: flex; align-items: center;
  color: #fff; font-family: var(--font-pixel); font-size: 10px; text-shadow: 0 1px 3px #000;
}
#boostBtn {
  position: absolute; left: 50%; bottom: 8px; transform: translateX(-50%); z-index: 8;
  width: 98px; height: 98px; border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #ff7a6b, #d42b2b 62%, #8f1616);
  border: 4px solid #5f0d0d; color: #fff; font-family: var(--font-pixel); font-size: 12px;
  box-shadow: 0 8px 22px rgba(0,0,0,.5); touch-action: manipulation;
}
#boostBtn:active { transform: translateX(-50%) scale(.94); }
/* Build J (Part 11): the booster icon stacked over the scanner box. */
#scanStack { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; flex: none; }
/* Build BF (BF2): the admin test buttons moved into the left
   column (top of #scanStack, above the CRAWLING label and the
   rocket). They keep the exact look they had in the right stack —
   the old #worldBtns button metrics and .on state, restated here
   for their new home. */
#scanStack .testBtn { padding: 7px 10px; font-size: 9px; opacity: .95; }
#scanStack .testBtn.on { background: var(--btn); border-color: var(--mint); color: #fff; }
/* Build BC (BC2): the CRAWLING label above the rocket button — the
   game's pixel lettering, white with the house dark outline (same
   shadow as the auth heading). While shown it PULSES: a slow fade
   out and back in over a 3.6 s cycle, never a hard on/off snap. */
#crawlLabel {
  font-family: var(--font-pixel); font-size: 10px; line-height: 1.4;
  color: #fff; text-shadow: 2px 2px 0 #04160a;
  pointer-events: none; white-space: nowrap;
  animation: crawlPulse 3.6s ease-in-out infinite;
}
@keyframes crawlPulse {
  0%, 100% { opacity: 1; }
  50% { opacity: .08; }
}
#btnBoost {
  background: rgba(10,42,51,.95); border: 2px solid var(--border); border-radius: 6px;
  padding: 3px 6px; line-height: 0; cursor: pointer;
}
#btnBoost img { width: 34px; height: 34px; object-fit: contain; image-rendering: pixelated; display: block; }
/* Build K (Part 2): the booster icon is VISIBLY DISABLED whenever
   the player is not walking a scanner-arrow path (standing still
   or on a plain ground-tap walk). */
#btnBoost.off { opacity: .35; filter: grayscale(1); }
#btnBoost.on { border-color: var(--gold); background: rgba(120,80,10,.9); }
#firstDeployContinue { background: var(--btn); border-color: var(--mint); color: #fff; padding: 11px 26px; font-size: 11px; }
/* Build H (entry animation): the input lock cover — transparent,
   above the map, dock and panels inside the game view, below the
   body-level popups. Shown only for the animation's ~3 seconds. */
#entryLock { position: absolute; inset: 0; z-index: 30; background: transparent; touch-action: none; }
#harvestStrip {
  /* The strip + STOP ride ABOVE every panel (canon): fixed to the viewport
     with a z-index over the panels and the slider sheet, so STOP is always
     reachable while harvesting, whatever screen is open. */
  position: fixed; top: 52px; left: 50%; transform: translateX(-50%);
  background: var(--surface-alt); border: 2px solid var(--border); color: var(--mint);
  font-family: var(--font-pixel); font-size: 9px; line-height: 1.5;
  padding: 6px 10px; border-radius: 6px; display: flex; gap: 10px; align-items: center; z-index: 30;
  white-space: nowrap;
  /* Build AT (AT2): the strip can never be wider than the screen —
     a long harvest line ("HARVESTING LITHIUM ORE — 13.0/MIN") at
     max UI scale pushed its first letter off the left edge. The
     cap makes the overflow measurable, and renderHud steps the
     strip's text size down until the whole line fits. */
  max-width: calc(100vw - 16px); box-sizing: border-box;
}
#harvestStrip button { background: var(--btn); border-color: var(--mint); color: #fff; padding: 5px 10px; font-size: 9px; }
/* Build BE (BE3): the vertical SESSION BAR — replaces the wide
   horizontal session strip. It stands at the top of the right-hand
   button stack (#worldBtns: bar, Fuel button, GPS button), shows
   ONE number (renderSessionBar in app.js), fills from the bottom
   with the session distance inside the current kilometre, and its
   outline morphs stage to stage as the fill climbs: teal→gold
   (km 1), pulsing glow→marching dashes (km 2), double outline→
   travelling spark (km 3, wobbling since Build BG), rainbow→final
   form (km 4, unevenly pulsing since Build BG), animated flames
   (km 5 to the session's end, Build BG — the old constant
   brilliant-white stage is gone). */
/* Build BF (BF3): the bar is a slim ≈40 px column now (the whole
   right stack narrows to its width — see #worldBtns) and its
   height is set by fitSessionBar in app.js: ≈3× the Build BE
   118 px, capped so its top never reaches the harvest bar or the
   boost mini-game bar. The 118 px here is only the pre-script
   fallback. */
#sessionBar {
  position: relative; width: 100%; height: 118px; box-sizing: border-box;
  /* Build BG (BG11): the UNFILLED part of the bar is TRANSPARENT —
     the map shows through above the fill. The fill, the outline
     morph, the number and the falling icons are unchanged; only
     the old dark backing (rgba(10,42,51,.92)) is gone. */
  background: transparent; border: 2px solid #35c4b5; border-radius: 8px;
}
#sessionBarFill {
  position: absolute; left: 0; right: 0; bottom: 0; height: 0%;
  /* Build BJ (BJ3): the fade is STRONGER — .80 at the bottom to
     .35 at the top (was .55 → .15, which read too transparent).
     overflow hidden clips the BJ3 sweep band to the filled part.
     Build BL (BL1): the MOVE fill colour is GOLDEN ORANGE —
     the owner's pick from the check-321 mock rounds (was the old
     session green); the fade is unchanged.
     Build BM (BM1): the fade goes FULL SOLID at the bottom —
     1.00 at the bottom to .50 at the top (owner's order after
     check 325); the colour is unchanged. */
  background: linear-gradient(to top, rgba(218,138,24,1), rgba(218,138,24,.50));
  border-radius: 0 0 6px 6px; pointer-events: none; overflow: hidden;
}
/* Build BF (BF6): on a FUEL session the rising fill is PURPLE; a
   MOVE session keeps the gradient above (golden orange since BL1). */
#sessionBar.sbFuel #sessionBarFill {
  /* Build BM (BM1): the FUEL fill takes the same new fade as the
     MOVE fill — 1.00 at the bottom to .50 at the top. */
  background: linear-gradient(to top, rgba(155,89,255,1), rgba(155,89,255,.50));
}
/* Build BJ (BJ3): the MOVING SWEEP — a lighter band that rises
   through the FILLED part only (it lives inside #sessionBarFill,
   so it is clipped to the fill and sits behind the outline SVG,
   the number and the falling cans), reaches the current fill
   level, then starts over from the bottom, looping while the
   session runs. It makes the fill read as rising faster than it
   really is; the fill height itself is untouched. The band is
   55% of the fill tall; animating `top` from 100% to -55% (of
   the fill's own height) carries it fully across the filled part. */
#sessionBarSweep {
  position: absolute; left: 0; right: 0; height: 55%; top: 100%;
  background: linear-gradient(to top, rgba(255,255,255,0), rgba(255,255,255,.38), rgba(255,255,255,0));
  animation: sbSweepRise 1.6s linear infinite; pointer-events: none;
}
@keyframes sbSweepRise {
  from { top: 100%; }
  to { top: -55%; }
}
#sessionBarSvg { position: absolute; inset: -2px; width: calc(100% + 4px); height: calc(100% + 4px); pointer-events: none; overflow: visible; }
#sbDashRect { fill: none; stroke: #ffc61a; stroke-width: 2; stroke-dasharray: 7 5; vector-effect: non-scaling-stroke; opacity: 0; animation: sbMarch 1s linear infinite; }
#sbSparkRect { fill: none; stroke: #ffffff; stroke-width: 3; stroke-dasharray: 16 404; vector-effect: non-scaling-stroke; opacity: 0; animation: sbTravel 2.4s linear infinite; filter: drop-shadow(0 0 3px #ffffff); }
#sessionBarGlow { position: absolute; inset: -2px; border: 2px solid transparent; border-radius: 8px; pointer-events: none; opacity: 0; }
#sessionBarGlow.sbPulsing { animation: sbPulseGlow 1.6s ease-in-out infinite; }
#sessionBar.sbRainbowOn { animation: sbRainbow 3s linear infinite, sbRainbowBarPulse 2.6s ease-in-out infinite; }
/* Build BG (BG12): km 3's outline WOBBLES — while the stage morphs
   from the double outline toward the travelling spark, the line
   wavers: the outline offset breathes unevenly and the corners
   shift shape, on top of the existing morph. Only cycle 3 carries
   the class (renderSessionBar toggles it), so no other cycle's
   geometry changes. */
#sessionBar.sbWobble { animation: sbWobbleK 1.5s ease-in-out infinite; }
@keyframes sbWobbleK {
  0%, 100% { outline-offset: 3px; border-radius: 8px 12px 9px 13px / 12px 8px 13px 9px; }
  25% { outline-offset: 5px; border-radius: 12px 8px 13px 9px / 9px 13px 8px 12px; }
  50% { outline-offset: 2px; border-radius: 9px 13px 8px 12px / 13px 9px 12px 8px; }
  75% { outline-offset: 4px; border-radius: 13px 9px 12px 8px / 8px 12px 9px 13px; }
}
/* Build BG (BG13): km 4's rainbow outline PULSES, unevenly — the
   glow ring swells outward and back, each side by a different
   amount (keyframed per-side insets + shifting corner shapes), so
   parts of the outline bulge more than others instead of scaling
   uniformly. The bar itself joins in with a slight uneven corner
   swell (sbRainbowBarPulse, in the .sbRainbowOn list above). The
   rainbow colour cycle and the morph toward the final form are
   unchanged; only cycle 4 carries these classes. */
#sessionBarGlow.sbRainbowPulse { animation: sbRainbowPulse 2.6s ease-in-out infinite; }
@keyframes sbRainbowPulse {
  0%, 100% { top: -2px; right: -2px; bottom: -2px; left: -2px; border-radius: 8px 11px 9px 12px / 11px 8px 12px 9px; }
  30% { top: -6px; right: -3px; bottom: -2px; left: -4px; border-radius: 13px 9px 14px 10px / 9px 14px 10px 13px; }
  55% { top: -3px; right: -6px; bottom: -5px; left: -2px; border-radius: 9px 14px 10px 13px / 14px 10px 13px 9px; }
  80% { top: -2px; right: -3px; bottom: -6px; left: -5px; border-radius: 12px 10px 13px 9px / 10px 13px 9px 14px; }
}
@keyframes sbRainbowBarPulse {
  0%, 100% { border-radius: 8px 10px 9px 11px / 10px 8px 11px 9px; }
  35% { border-radius: 11px 8px 12px 9px / 9px 12px 8px 11px; }
  65% { border-radius: 9px 12px 8px 10px / 12px 9px 11px 8px; }
}
/* Build BG (BG14): km 5 and every later km — ANIMATED FLAMES on
   the outline, the final form for good (replaces the old constant
   brilliant-white stage). Two irregular dashed strokes are the
   flame tongues: their dash lengths vary on purpose, so flame
   size differs around the perimeter; the tongues TRAVEL around
   the ring (dashoffset) at two speeds while an irregular FLICKER
   works their opacity, and the whole flame outline PULSES —
   sbFlamePulse swells the SVG ring unevenly per side, like
   BG13's rainbow pulse, while the orange halo flickers with it.
   Every animation is gated under .sbFlames (renderSessionBar
   toggles it for the final stage only), so the rects' inline
   opacity still shows/hides them in every other cycle. */
#sbFlameRect {
  fill: none; stroke: url(#sbFlameGrad); stroke-width: 5; vector-effect: non-scaling-stroke;
  stroke-linecap: round; opacity: 0; stroke-dasharray: 10 6 4 8 13 5 7 9 3 7 11 6 5 8;
  filter: drop-shadow(0 0 3px rgba(255,106,0,.9));
}
#sbFlameRect2 {
  fill: none; stroke: #ffd23e; stroke-width: 3; vector-effect: non-scaling-stroke;
  stroke-linecap: round; opacity: 0; stroke-dasharray: 5 9 12 4 6 10 3 8 7 5;
  filter: drop-shadow(0 0 2px rgba(255,210,62,.9));
}
#sessionBar.sbFlames #sbFlameRect { animation: sbFlameTravelA 1.8s linear infinite, sbFlameFlicker 1.1s linear infinite; }
#sessionBar.sbFlames #sbFlameRect2 { animation: sbFlameTravelB 2.6s linear infinite, sbFlameFlicker 0.9s linear infinite reverse; }
#sessionBar.sbFlames #sessionBarSvg { animation: sbFlamePulse 2.6s ease-in-out infinite; }
#sessionBar.sbFlames #sessionBarGlow { animation: sbFlameGlowFlicker 1.1s linear infinite; }
@keyframes sbFlameTravelA { to { stroke-dashoffset: -102; } }
@keyframes sbFlameTravelB { to { stroke-dashoffset: -69; } }
@keyframes sbFlameFlicker {
  0%, 100% { opacity: 1; }
  18% { opacity: .72; }
  37% { opacity: .95; }
  52% { opacity: .8; }
  71% { opacity: 1; }
  86% { opacity: .78; }
}
@keyframes sbFlameGlowFlicker {
  0%, 100% { opacity: 1; }
  22% { opacity: .8; }
  44% { opacity: .95; }
  63% { opacity: .75; }
  82% { opacity: .92; }
}
@keyframes sbFlamePulse {
  0%, 100% { top: -2px; left: -2px; width: calc(100% + 4px); height: calc(100% + 4px); }
  30% { top: -6px; left: -4px; width: calc(100% + 7px); height: calc(100% + 8px); }
  55% { top: -3px; left: -2px; width: calc(100% + 8px); height: calc(100% + 7px); }
  80% { top: -2px; left: -5px; width: calc(100% + 8px); height: calc(100% + 8px); }
}
/* Build BF (BF5): the falling-fuel stream. The layer spans the
   whole right stack (bar top to the Fuel button) and never takes
   a tap. A falling icon rides the track by its distance to
   landing; its top is set by renderFuelFall and eased between
   polls. A landing flash is the same icon dropped exactly over
   the Fuel button's icon, outlined in the bar's own colours at
   that moment (border colour + glow copied inline by the script),
   flaring once and gone. */
/* Build BG (BG3): the layer paints ABOVE the Fuel and GPS buttons
   (it had no z-index and sat before them in the stack, so the
   buttons painted over it and a landing icon disappeared behind
   the Fuel button instead of landing on its icon). z-index 6 puts
   the icons and flashes over both buttons; the session bar number
   sits higher still (z-index 7, BG1). pointer-events stays none —
   the buttons still take every tap through it. */
#fuelFallLayer { position: absolute; inset: 0; pointer-events: none; overflow: visible; z-index: 6; }
.fuelFallIcon {
  position: absolute; width: 18px; height: 22px; margin: 0;
  transform: translate(-50%, -50%); transition: top .45s linear;
}
.fuelFallIcon img { width: 100%; height: 100%; object-fit: contain; display: block; }
.fuelFallIcon.enter { animation: fuelFallIn .5s ease-out; }
@keyframes fuelFallIn { from { opacity: 0; } to { opacity: 1; } }
.fuelFallFlash {
  position: absolute; width: 26px; height: 30px; box-sizing: border-box;
  transform: translate(-50%, -50%); border: 2px solid #35c4b5; border-radius: 6px;
  display: flex; align-items: center; justify-content: center;
  animation: fuelFallFlashAnim .65s ease-out forwards; /* Build BG (BG4): ~700 ms of life (the script removes it at 700 ms) */
}
.fuelFallFlash img { width: 20px; height: 24px; object-fit: contain; display: block; }
@keyframes fuelFallFlashAnim {
  /* Build BG (BG4): full strength for the first ~55% (≈360 ms —
     a clear pulse a player can actually see), then the fade. */
  0% { opacity: 1; filter: brightness(2.2); }
  55% { opacity: 1; filter: brightness(1.9); }
  100% { opacity: 0; filter: brightness(1); }
}
/* Build BF (BF3): the number is rotated to read TOP-TO-BOTTOM
   (the owner's flipped mock) and sits at the BOTTOM of the bar:
   a horizontal span rotated −90° about its centre — its start
   rests lowest, exactly as mock-sessionbar-narrow-3.png shows. */
#sessionBarNum {
  position: absolute; left: 50%; bottom: 20px; width: max-content;
  transform: translateX(-50%) rotate(-90deg); transform-origin: center;
  font-family: var(--font-pixel); font-size: 10px; color: #ffffff;
  text-shadow: 0 1px 2px #000000, 0 0 5px rgba(0,0,0,.85); pointer-events: none; white-space: nowrap;
  /* Build BG (BG1): the number renders ABOVE the falling icons —
     it had no z-index, so the lowest falling can (z-index 6 layer)
     painted over the digits as it passed. 7 puts the number over
     the icon layer; the text shadow is unchanged. */
  z-index: 7;
}
@keyframes sbMarch { to { stroke-dashoffset: -24; } }
@keyframes sbTravel { to { stroke-dashoffset: -420; } }
@keyframes sbPulseGlow { 0%, 100% { opacity: var(--sbGlowBase, .9); } 50% { opacity: calc(var(--sbGlowBase, .9) * .3); } }
@keyframes sbRainbow {
  0% { border-color: #ff4040; } 20% { border-color: #ff9f1a; } 40% { border-color: #f5e663; }
  60% { border-color: #3ddc84; } 80% { border-color: #4dabff; } 100% { border-color: #b56bff; }
}
/* Build BE (BE3): the Fuel button carries the Fuel item icon,
   centred, at the GPS button's own size (the #worldBtns button rule). */
#btnFuel { line-height: 0; }
#btnFuel img { width: 22px; height: 22px; object-fit: contain; image-rendering: pixelated; display: block; margin: 0 auto; }
/* Toasts — the game's ONE transient-message layer (owner's rule: "no
   messages hidden behind boxes or ui"). The container lives at body
   level, fixed to the viewport, z-index above every panel (9), sheet
   (15), popup (20) and the harvest strip (30), so a message stays fully
   visible for its whole display time no matter what is opened or closed
   meanwhile. The only things allowed above toasts are the full-screen
   takeovers (Daily Bonus + the outcome queue) — those win by QUEUEING
   in app.js (toasts wait until the takeover clears), not by z-index.
   top: 92px keeps the layer just under the fixed harvest strip's band.
   The container never blocks input; each toast itself is hit-testable
   so the layer's topmost claim is mechanically verifiable. */
#toasts { position: fixed; top: 92px; left: 50%; transform: translateX(-50%); z-index: 40; display: flex; flex-direction: column; gap: 6px; align-items: center; pointer-events: none; width: max-content; max-width: 94vw; }
/* Build BJ (BJ2): the TOP UP hold bar — across the very top of the
   screen, above every panel/card (z-index 70: title screen is 60,
   toasts 40), present only while TOP UP is held. The fill is the
   held machine's tank level, left to right. No text. */
#topupBar { position: fixed; top: 0; left: 0; right: 0; height: 14px; z-index: 70; background: rgba(8,30,38,.88); border-bottom: 2px solid #0b3542; pointer-events: none; }
#topupBarFill { height: 100%; width: 0%; background: linear-gradient(to bottom, #ffc46b, #f59a23); transition: width .12s linear; }
.toast { background: rgba(10,42,51,.95); border: 2px solid var(--border); color: var(--text); padding: 7px 12px; border-radius: 6px; font-size: .88em; text-align: center; pointer-events: auto; }
/* Build D fix F1: the bottom padding is a FLAT 7px. The old
   calc(7px + env(safe-area-inset-bottom)) double-counted the
   navigation-bar inset on the owner's S26: the Android shell already
   pads its root layout by the system-bar insets (MainActivity), and
   the WebView still reports a non-zero safe-area inset — so the dock
   grew a dark empty band of dock-coloured padding UNDER the second
   chip row. Headless Chromium reports a 0 inset, which is why the
   390x844 geometry check passed while the device showed the band.
   Flat padding: the chip rows end flush with the bottom of the game
   area on the device too. */
#bottombar { display: flex; flex-wrap: wrap; gap: 6px; padding: 7px; background: var(--surface); border-top: 2px solid var(--border); z-index: 5; }
/* S5 fix (F4, the approved mockup): the dock buttons are REAL buttons —
   the standard bordered surface-alt look — not bare text. */
/* Fix pack A, item A5: the dock is TWO rows of four chips at every
   width (row 1 FACTORY/STORES/MARKET/WORKSHOP, row 2 CHALLENGES/
   RANKS/CHAT/MACHINES), and the dock's height is exactly its two
   rows, so the map (#worldWrap flex) takes everything the old
   full-width LegLog bar row used to occupy. Build K (Part 4): the
   25% basis below is only the pre-fit fallback — at runtime
   fitDockLabels (app.js) measures each chip's natural content
   width and sets explicit per-chip widths, each row of four sized
   in proportion to content (CHAT narrower, CHALLENGES wider). */
#bottombar button { flex: 1 1 calc(25% - 6px); padding: 11px 2px; font-size: 9px; background: var(--surface-alt); border: 2px solid var(--border); color: #fff; white-space: nowrap; min-width: 0;
  /* Build G (Item 4): the browser must render the dock labels at
     exactly the size the game's own fit sets — no free-form device
     text scaling (font boosting) on top of it. This is the app's own
     fixed-size game UI; players scale it with the in-game UI-size
     setting, which the fit measures and honours. */
  text-size-adjust: 100%; -webkit-text-size-adjust: 100%; }
/* Build E (Part 13): dock chips carry the Muse build's tab icons
   beside the name (the name stays visible — pictures never replace
   it). The img is pointer-transparent so presses land on the chip.
   On phone widths the chip row is tight (CHALLENGES is the long
   word): icon, gap and label all step down together so icon + name
   still fit inside the chip — measured against the real layout, not
   guessed. */
#bottombar button { display: flex; align-items: center; justify-content: center; gap: 3px; }
#bottombar .dockIcon { width: 20px; height: 20px; object-fit: contain; image-rendering: pixelated; pointer-events: none; flex: none; }
/* The narrow-width step-downs for these rules live at the END of
   this file (they must override the older phone media rules). */
#bottombar button.on, #bottombar button.active { background: rgba(23,185,120,.35); color: var(--gold); border-color: var(--mint); }
/* Build I (Part 3): the CHAT chip BRIGHTER while an unread message
   from someone else waits (renderHud toggles .chatHot; opening chat
   clears it on screen at once). Brighter = the panel's own green lit
   up with the mint border — clearly above the plain chip and the
   merely-active state. */
#btnChat.chatHot { background: rgba(47,230,167,.55); border-color: var(--mint); color: #fff; }
/* Build R (Part 8): the unread-count BADGE on the CHAT chip — the
   number Build N removed, back as an opt-in (the CHAT
   NOTIFICATIONS setting, default OFF). renderHud fills/hides it. */
#btnChat { position: relative; }
.chatBadge {
  position: absolute; top: 2px; right: 4px; min-width: 16px; height: 16px;
  padding: 0 4px; border-radius: 8px; background: #d42b2b; color: #fff;
  font-size: 10px; line-height: 16px; text-align: center; font-weight: bold;
}
/* Build I (Part 17): the dock label span may never be compressed by
   the flex row — its measured width must be its true content width,
   which is what the canvas-based fit (fitDockLabels) relies on. */
#bottombar .dockLabel { flex: none; white-space: nowrap; }
.leglogLine { display: flex; gap: 10px; align-items: baseline; padding: 7px 2px; border-bottom: 1px solid var(--border); }
.leglogWhen { color: var(--dim, #9db8c0); font-size: .85em; min-width: 118px; }
.leglogKind { flex: 1; }
.leglogDist { color: var(--gold); }
/* Phone widths: same two rows of four as everywhere (item A5); just
   the smaller type and padding. The basis subtracts the row gap: at
   a bare 25% the three 6 px gaps would push the fourth chip onto its
   own row. */
@media (max-width: 560px) {
  #bottombar button { font-size: 8px; padding: 9px 2px; }
}

/* ---------- panels ---------- */
#panelBackdrop { position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 8; }
.panel {
  position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: min(94vw, 560px); max-height: 86vh; overflow-y: auto; z-index: 9;
  /* S5 fix (F3): a panel can never be dragged sideways inside itself —
     horizontal overflow is clipped, so every close button stays exactly
     where the panel puts it: reachable, on screen. */
  overflow-x: hidden;
  background: var(--surface); border: 2px solid var(--border); border-radius: 6px; padding: 0 14px 14px;
  box-shadow: 0 10px 40px rgba(0,0,0,.5);
}
/* Panel headers. Build P (slip-in Q3): the spaceship banner is
   REMOVED from every panel — all headers are PLAIN now, the Build
   I (Part 18) node-panel pattern applied across the board: the
   panel's own flat surface, the title readable against it, sized
   by its content, title and credits pills without dark backing.
   (The spaceship.png art file is KEPT — it is reserved for a
   future title screen; nothing references it from here any more.)
   Builds I–O carried the banner (design §29, the Muse build's
   PanelHeader) on every panel except the node panel. */
.panel header {
  display: flex; justify-content: space-between; align-items: flex-start;
  margin: 0 -14px 12px; padding: 10px 12px; height: auto;
  background: var(--surface);
  border-bottom: 2px solid var(--border); border-radius: 4px 4px 0 0;
}
.panel header h2 { background: transparent; padding: 6px 10px; border-radius: 4px; }
#nodePanel header { background: var(--surface); height: auto; }
#nodePanel header h2, #nodePanel .hdrCredits { background: transparent; }
/* Credits chip on every panel header (design §29 shell: title +
   Credits). Build P (Q3): with the banner gone, the chip's dark
   backing goes too — plain like the node panel's. */
.hdrCredits {
  font-family: var(--font-pixel); font-size: 9px; color: var(--gold);
  background: transparent; padding: 6px 10px; border-radius: 4px; white-space: nowrap;
}
.panel h2 { font-family: var(--font-pixel); font-size: 13px; line-height: 1.5; color: #fff; text-shadow: 2px 2px 0 #04160a; }
.panel h3 { font-family: var(--font-pixel); margin: 10px 0 6px; font-size: 11px; line-height: 1.5; color: #fff; }
/* Build AX (AX6): the RANK LADDER dropdown head — a head that taps.
   Build AZ (AZ1): the head is now the owner's approved TEAL BAR
   (mock: look-pass/mock-rankladder-1.png) — a full-width teal
   gradient bar with bigger pixel text (wrapping to a second line
   at phone size, as the mock shows) and the dropdown arrow in a
   gold-bordered dark chip at the right end. Geometry and colours
   are the mock's own CSS, captured when the mock was approved. */
.ladderHead { display: flex; align-items: center; justify-content: space-between; width: 100%; box-sizing: border-box; margin: 12px 0 8px; padding: 13px 14px; background: linear-gradient(180deg, #116d6e 0%, #0a4f57 100%); border: 2px solid #1f8a8c; border-radius: 8px; cursor: pointer; font-family: var(--font-pixel); color: #fff; text-align: left; }
.ladderHead .lhName { font-size: 15px; line-height: 1.5; letter-spacing: 0.5px; }
.ladderHead .lhChip { flex: none; margin-left: 10px; width: 44px; height: 38px; display: flex; align-items: center; justify-content: center; background: #083c44; border: 2px solid #ffc61a; border-radius: 6px; color: #ffc61a; font-size: 20px; line-height: 1; }
/* Build AZ (AZ25): ladder rows never overlap — the shared .whRow
   fixes .nm at 108px and .amt at 128px, so at big UI text sizes a
   long rank name (CONTRACTOR, INDUSTRIALIST) spilled out of its box
   and the EXCHANGE text printed over its end (owner's photo
   2026-11-04). Scoped to the ladder only: the name flexes (and may
   wrap at its space), the points/exchange block keeps its natural
   width, right-aligned, wrapping internally like the approved
   mock's rows. Text content is unchanged. */
#rankLadder .whRow { align-items: flex-start; }
#rankLadder .whRow .nm { flex: 1 1 auto; width: auto; min-width: 0; }
#rankLadder .whRow .amt { flex: 0 1 auto; width: auto; max-width: 58%; }
.panel h4 { font-family: var(--font-pixel); margin: 12px 0 6px; font-size: 10px; line-height: 1.5; color: var(--dim); }
.closeBtn { background: rgba(7,30,38,.82); border-color: transparent; color: var(--mint); padding: 6px 10px; font-size: 11px; }
/* Panel X fix (owner's ruling): the harvest strip (fixed, top:52px,
   ~43px tall → bottom edge ≈ 95px, z-index 30) rides above every
   panel (z-index 9), and a centred panel's header — the close X
   included — reaches into that band, so while harvesting the X sat
   UNDER the strip and could not be tapped. The panels do NOT move
   and do NOT rise above the strip: while the strip is visible, ONLY
   the X drops 56px inside the 104px banner. Worst case (panel at its
   86vh max-height) the X top lands at 0.07·vh + 68px — 96px at a
   400px-tall viewport, lower on anything taller — fully below the
   strip's bottom edge, and the X still fits inside the banner
   (10 + 56 + 32 = 98 ≤ 104). body.harvesting is toggled in
   renderHud(), in the same place the strip itself is toggled. */
body.harvesting .panel .closeBtn { margin-top: 56px; }
.row { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
/* Build BM slip-in (BN1): the Settings ACCOUNT buttons touched —
   CUSTOMIZE, Record and LOG OUT stacked flush, no air between
   them (buttons carry no margins and .row adds none). The two
   lower rows gain an even 8px of air above the row — inside the
   panel's own spacing language (h4 runs 12px/6px, .row's gap is
   6px) — so Record stands clear of CUSTOMIZE and LOG OUT stands
   clear of Record by the same amount. */
#settingsPanel .acctGap { margin-top: 8px; }
/* Build BM slip-in (BN2): the FAQ card is a panel that covers the
   WHOLE screen — the game screen PLUS the top bar above it: fixed
   at inset 0 with the centring transform and the panel's size caps
   removed, square corners, and z-index 80, above every other piece
   of game UI (panels 9, popups 20, toasts 40, the title gate 60,
   the topup strip 70), so nothing paints over it while it is open.
   It opens and closes through the plain panel system (showPanel /
   the header ✕ → closePanels()). */
#faqPanel {
  left: 0; top: 0; right: 0; bottom: 0; transform: none;
  width: auto; max-height: none; border: none; border-radius: 0;
  box-shadow: none; z-index: 80;
}
.card { border: 2px solid var(--border); border-radius: 6px; padding: 10px; margin-bottom: 10px; background: var(--surface-alt); }
.sellBox { border: 2px solid var(--border); border-radius: 6px; padding: 10px; margin-bottom: 12px; background: var(--surface-alt); }
.sellBox .row > * { flex: 1; min-width: 70px; }
.whRow { display: flex; align-items: center; gap: 8px; padding: 6px 4px; border-bottom: 1px solid var(--border); font-size: .9em; }
/* drag-reorder (canon house rule): long-press picks a row up — gold border —
   and the order persists per player. The wrapper carries a warehouse row
   plus its upgrade-cost line so the two never separate. */
.whEntry { border-bottom: 1px solid var(--border); }
.whEntry .whRow { border-bottom: none; }
.whEntry .costLine { padding: 0 4px 6px; }
/* Build D fix F4: draggable elements opt OUT of touch scrolling
   entirely (touch-action: none). Before this, a touch hold on a row
   or chip started a scroll gesture: the browser took the pointer over
   for scrolling (pointercancel), the hold timer's drag never engaged,
   and dragging a Market row just scrolled the list. With touch-action
   none on the draggable itself, the pointer stays with the page from
   the first move, the hold registers, and once it does the drag wins
   over scrolling — the panel still scrolls from its non-draggable
   areas (headers, padding, gaps). */
[data-drag-key] { cursor: grab; touch-action: none; }
#bottombar button { touch-action: none; }
/* Build E (Part 3): panel ROWS hand the vertical axis back to the
   list — a swipe on a row scrolls the panel again (Build D's blanket
   touch-action:none made every row swallow the swipe, so the lists
   stopped scrolling). The hold still wins: a finger that stays put
   for the 350 ms hold never becomes a scroll gesture, and once the
   drag activates, attachDrag locks the panel's scrolling until the
   drop. Only the three panel lists are listed here — dock chips and
   panel chip rows keep touch-action:none exactly as Build D. */
#warehouseList [data-drag-key], #factoryList [data-drag-key], #marketList [data-drag-key] { touch-action: pan-y; }
[data-drag-key].dragging { outline: 3px solid var(--gold); outline-offset: -3px; opacity: .92; cursor: grabbing; position: relative; z-index: 3; }
/* Settings Record (Build D): plain text lines, newest first — no
   cards, no sections, no graphs. */
.recordLine { padding: 6px 2px; border-bottom: 1px solid var(--border); font-size: .9em; line-height: 1.5; }
.whRow .nm { width: 108px; font-weight: bold; }
.whRow .barWrap { flex: 1; background: var(--track); border: 1px solid var(--track-border); height: 14px; overflow: hidden; }
.whRow .bar { height: 100%; background: var(--accent); }
/* Build P (Part 4): the Stores fill bar on its own full-width line
   under the row — a real track presence at phone width / max UI,
   where the old in-row barWrap was squeezed to a sliver. */
.whEntry .whBar { height: 12px; background: var(--track); border: 1px solid var(--track-border); margin: 2px 4px 8px; overflow: hidden; }
.whEntry .whBar span { display: block; height: 100%; background: var(--accent); }
.whRow .amt { width: 128px; text-align: right; }
.whRow button { padding: 5px 8px; font-size: 9px; }
.fitRow, .stockRow { display: flex; align-items: center; gap: 8px; padding: 5px 4px; border-bottom: 1px solid var(--border); font-size: .9em; flex-wrap: wrap; }
/* Build Y (Part 2): the power-up craft picker — the chosen target
   and any stock power-up picked as a craft part read as selected. */
.puCraftTargets button.active, .stockRow button.picked { background: var(--btn); border-color: var(--mint); color: #fff; }
.puCraftStatus { font-weight: bold; margin: 8px 0; }
.chargeWrap { flex: 1; min-width: 70px; background: var(--track); border: 1px solid var(--track-border); height: 12px; overflow: hidden; }
.chargeBar { height: 100%; background: var(--accent); }
#scanTargets { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; }
#scanTargets button { padding: 11px 6px; }
/* Build E (Part 9): a locked scan target is tappable (it offers the
   unlock) but visibly locked — dimmed with a gold edge. */
#scanTargets button.locked { opacity: .75; border-color: var(--gold); color: var(--gold); }
.setRow { display: flex; justify-content: space-between; align-items: center; padding: 9px 2px; border-bottom: 1px solid var(--border); }
.setRow input[type="range"] { width: 150px; }
/* Build O (Part 4): the CUSTOMIZE colour fields are swatch BUTTONS
   (a colour bar showing the current pick) opening the in-game
   picker below — no <input type="color">, so nothing can render
   squashed at max UI size. */
.custSwatch { width: 84px; height: 30px; padding: 0; border: 2px solid var(--border); border-radius: 4px; cursor: pointer; }
#custPicker { border: 1px solid var(--border); border-radius: 6px; padding: 10px; margin: 10px 0; }
#custSwatches { display: grid; grid-template-columns: repeat(8, 1fr); gap: 7px; margin: 8px 0 10px; }
#custSwatches button { height: 30px; padding: 0; border: 2px solid var(--border); border-radius: 4px; cursor: pointer; }
#custEditor .cpLab { min-width: 74px; }
#custEditor input[type="range"] { flex: 1; margin-left: 10px; }
.cpHexWrap { display: flex; align-items: center; gap: 10px; }
#cpHex { font-family: var(--font-pixel); font-size: 12px; text-transform: uppercase; }
#cpPreview { display: inline-block; width: 44px; height: 26px; border: 2px solid var(--border); border-radius: 4px; }
/* Build M (Part 3): the CUSTOMIZE button in Settings — greyed with
   REACH LEGEND below Legend (dead to taps; the server refuses the
   save anyway), normal above it. */
#btnCustomize.greyed {
  background: var(--disabled-bg); border-color: var(--disabled-border);
  color: var(--disabled-text); cursor: default;
}
/* Build M (Part 3): the CUSTOMIZE screen — the live preview box
   (grass backdrop, the figure drawn by the game's own sprite
   pipeline in app.js), the shoe style toggle and the colour bars. */
#custPreviewWrap {
  position: relative; border: 2px solid var(--border); border-radius: 6px;
  background: #5e7c34; margin-bottom: 10px; overflow: hidden;
}
#custPreview { display: block; width: 100%; height: auto; }
.custPreviewTag {
  position: absolute; left: 0; right: 0; bottom: 6px; text-align: center;
  font-family: var(--font-pixel); font-size: 9px; color: #fff;
  text-shadow: 0 1px 3px #000; pointer-events: none;
}
.custStyleRow { gap: 8px; }
.custStyleBtn { flex: 1; }
.custStyleBtn.sel { background: var(--gold); border-color: var(--gold); color: #4a3200; }
#customizePanel input[type="color"] {
  width: 150px; height: 40px; padding: 2px; cursor: pointer;
  background: var(--surface-alt); border: 2px solid var(--border); border-radius: 6px;
}
#nodeBody p { margin: 5px 0; font-size: .95em; }
#nodePanel .row { margin-top: 12px; }
#nodePanel .row button { flex: 1; padding: 11px 4px; }

/* ---------- popups ---------- */
.popup { position: fixed; inset: 0; background: rgba(0,0,0,.6); z-index: 20; display: flex; align-items: center; justify-content: center; }
.popupCard { background: var(--surface); border: 2px solid var(--border); border-radius: 6px; padding: 20px; width: min(92vw, 400px); text-align: center; }
.popupCard h3 { font-family: var(--font-pixel); font-size: 12px; line-height: 1.6; color: #fff; margin-bottom: 10px; }
.popupCard p { margin-bottom: 16px; }
.popupCard .row { justify-content: center; }
/* Build BS (BS2/BS3): the BIG machine card — the confirm popup's
   machine variant (shelf BUY and ASSEMBLE only). Wider than the
   standard card, a flex column capped by the viewport: title
   fixed, the picture + stat rows scroll in the middle, and the
   YES / NO row sits in a footer pinned at the card's bottom. */
.popupCard.machineBig { display: flex; flex-direction: column; width: min(94vw, 460px); max-height: 92vh; max-height: 92dvh; }
.popupCard.machineBig > h3 { flex: none; }
.popupCard.machineBig > #confirmText { flex: 1 1 auto; min-height: 0; overflow-y: auto; margin-bottom: 12px; }
.popupCard.machineBig > .row { flex: none; margin-top: auto; }
.machBigPic { display: block; width: min(56%, 200px); height: auto; margin: 2px auto 6px; }
.machBigName { font-family: var(--font-pixel); font-size: 13px; line-height: 1.5; color: #fff; margin-bottom: 12px; }
.machStat { display: flex; justify-content: space-between; align-items: baseline; gap: 14px; padding: 7px 2px; border-top: 1px solid var(--border); text-align: left; }
.machStatLabel { font-family: var(--font-pixel); font-size: 10px; color: var(--dim); }
.machStatVal { font-weight: bold; color: #fff; white-space: nowrap; }
.machLeaves { text-align: right; color: var(--dim); font-size: .9em; padding: 4px 2px 8px; }
.machCost { text-align: left; padding: 3px 2px; }
#confirmYes { background: var(--btn); border-color: var(--mint); color: #fff; padding: 11px 26px; font-size: 11px; }
#confirmNo { background: var(--danger); border-color: var(--danger-border); color: #fff; padding: 11px 26px; font-size: 11px; }
/* Build AO (AO1): the factory UPGRADE confirm's visual card —
   centred header and SPEED lines, then one row per cost item.
   Rows are single-line at every UI scale: the icon is fixed, the
   NEED · HAVE block never wraps, and the name flexes (ellipsised
   rather than wrapped in the extreme). */
.upgHead { text-align: center; font-size: 1.05em; margin-bottom: 2px; }
.upgSpeed { text-align: center; margin-bottom: 8px; }
/* Build AT (AT5): cost rows WRAP instead of clipping (owner's
   pick over shrink-to-fit). At max UI scale with 3-digit figures
   the old single-line row collapsed the name to "Ge…" / nothing
   and pushed "SHORT 156" off the card edge. Now the row wraps:
   when name + figures cannot fit on one line, the amount drops
   to its own line inside the row, right-aligned; the name keeps
   a sane minimum share and its full text (no ellipsis). Rows
   that fit stay on one line, unchanged. Covers every .upgRow
   user — the upgrade/build/unlock confirms, the cancel-warning
   card, the enrol cards and the AT4 factory-card stock rows. */
.upgRow { display: flex; align-items: center; gap: 8px; margin: 10px 0; text-align: left; flex-wrap: wrap; }
.upgIcon { width: 34px; height: 34px; object-fit: contain; image-rendering: pixelated; flex: none; }
.upgName { flex: 1 1 40%; min-width: 0; }
.upgAmt { flex: none; white-space: nowrap; font-size: .92em; margin-left: auto; }
/* Build AU (AU3): factory-card stock rows (.facStockRow) never
   wrap — the amount always stays on the resource's own line, and
   the NAME truncates (ellipsis) before the amount can leave the
   line. Confirm-card rows do not carry the modifier and keep the
   AT5 wrap above. */
.upgRow.facStockRow { flex-wrap: nowrap; }
.upgRow.facStockRow .upgName { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.upgFoot { text-align: center; opacity: .75; font-size: .85em; margin-top: 8px; }
/* Build AP (AP1–AP4): the AO visual-card pattern extended — the
   BUILD FACTORY, UNLOCK WAREHOUSE and HAND-HARVEST CHALLENGE
   confirms reuse the .upg* rows (label/value rows share them too);
   the extras below are the challenge card's clock cell, the amber
   RETURNS tag, and the cancel warning's warehouse fill bar. Same
   single-line rule as AO: fixed cells, nowrap figures, the label
   flexes and ellipsises rather than wrapping. */
.vClock { width: 34px; flex: none; text-align: center; font-size: 20px; }
.vAmber { color: #ffb02e; font-weight: bold; }
.vwBlock { margin: 10px 0; text-align: left; }
.vwHead { display: flex; align-items: baseline; gap: 8px; white-space: nowrap; }
.vwName { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.vwBar { height: 12px; background: var(--track); border: 1px solid var(--track-border); margin-top: 4px; overflow: hidden; }
.vwBar span { display: block; height: 100%; background: #ff5252; }
.vwLost { color: #ff5252; font-size: .85em; margin-top: 3px; }
/* Leave-the-game popup (S5 fix, F6): the game's green YES / red NO rule. */
#leaveYes { background: var(--btn); border-color: var(--mint); color: #fff; padding: 11px 26px; font-size: 11px; }
#leaveNo { background: var(--danger); border-color: var(--danger-border); color: #fff; padding: 11px 26px; font-size: 11px; }
/* Daily Bonus — the approved "Classic Boots" look: cream panel, teal frame. */
.popupCard.bonus { background: #fee8b6; border: 4px solid #12b5a5; border-radius: 6px; color: #0a3d3a; }
.popupCard.bonus h3 { color: #0a3d3a; text-shadow: none; font-size: 13px; }
.bonusRule { border-top: 4px dotted #12b5a5; width: 80%; margin: 0 auto 10px; }
.bonusBoots { width: 240px; max-width: 80%; height: auto; image-rendering: pixelated; }
.popupCard.bonus p { color: #0e8a7d; font-family: var(--font-pixel); font-size: 11px; line-height: 1.6; margin: 10px 0 14px; }
#bonusContinue { background: #12b5a5; border-color: #0a3d3a; color: #fff; padding: 11px 26px; font-size: 11px; }

/* ---------- Port Stage 2 ---------- */
/* The world's bottom cluster (post-S4 fix, Item 7d): ONE flex row
   anchored to the bottom of the world — scanner box, session strip,
   world buttons side by side. Overlap is structurally impossible;
   pointer events pass through the gaps to the map. */
#worldBottom {
  position: absolute; left: 8px; right: 8px; bottom: 8px; z-index: 4;
  display: flex; align-items: flex-end; gap: 8px; pointer-events: none;
}
#worldBottom > * { pointer-events: auto; }
/* Scanner box restored to the Muse build exactly (Item 5): TWO
   separate icon-only buttons — the satellite DISH (tap = scan for the
   current target) and the current target resource's ICON (tap = open
   the picker). No resource name text in the box. */
#targetBox {
  flex: none; display: flex; align-items: center; gap: 6px; padding: 4px 6px;
  background: rgba(10,42,51,.95); border: 2px solid var(--border); border-radius: 6px;
}
#targetBox button { background: transparent; border: 2px solid transparent; border-radius: 4px; padding: 2px; line-height: 0; cursor: pointer; }
#btnScanDish img { width: 40px; height: 40px; object-fit: contain; image-rendering: pixelated; display: block; }
#btnTargetPick { border-color: var(--border); background: var(--surface-alt); }
#btnTargetPick img { width: 36px; height: 36px; object-fit: contain; image-rendering: pixelated; display: block; }
/* Build BF (BF3): the right stack narrows to the session bar's
   column width (≈40 px) — the Fuel and GPS buttons take the same
   width as the bar. position: relative anchors the falling-fuel
   layer (BF5) to this stack. */
#worldBtns { display: flex; flex-direction: column; gap: 6px; flex: none; margin-left: auto; width: 40px; min-width: 40px; position: relative; }
/* Build BG (BG10): TOP UP buttons are press-and-hold — a hold must
   not scroll the panel or select text. */
[data-topup], [data-mtopup] { touch-action: none; -webkit-user-select: none; user-select: none; }
/* Phone widths: the bottom cluster wraps rather than squeezing. */
@media (max-width: 560px) {
  #worldBottom { flex-wrap: wrap; }
}
#worldBtns button { padding: 7px 10px; font-size: 9px; opacity: .95; }
/* BF3: inside the 40 px column the Fuel/GPS buttons keep their
   glyphs — the side padding is trimmed so "GPS" and the Fuel icon
   fit the narrower box (the label size itself is unchanged).
   Build BG (BG7): that trim never actually applied — "#worldBtns
   button" (an ID + an element) outranks "#btnGps" (an ID alone),
   so the buttons kept 10 px side padding: the "GPS" text had a
   16 px content box to sit in and was shoved against the right
   edge (the owner's locked, no-session screenshot). The rule
   below is stated with the stack's ID as well, so it wins in
   EVERY state (locked or unlocked, session or none); the width
   and centring are unconditional too, not left to whichever
   elements happen to be rendered around the buttons. */
#worldBtns #btnFuel, #worldBtns #btnGps {
  padding-left: 2px; padding-right: 2px;
  width: 40px; min-width: 40px; max-width: 40px; box-sizing: border-box;
  text-align: center;
}
#worldBtns button.on { background: var(--btn); border-color: var(--mint); color: #fff; }
#gameView.deploying #topbar, #gameView.deploying #bottombar { opacity: 0; transition: opacity .3s; pointer-events: none; }
/* Build L (Part 8): while the drop pod flies, EVERY card and panel
   hides — the tutorial card, the dock panels, the popups, the
   slider sheet, the panel backdrop (Build K only faded the bars,
   and the pod dropped BEHIND the tutorial card on the phone).
   visibility, not display: nothing closes, re-renders or loses
   state, so whatever was open returns exactly as it was when the
   flight ends. Toasts keep their existing hold rules. */
body.podFlying .panel,
body.podFlying .popup,
body.podFlying #sheet,
body.podFlying #panelBackdrop,
body.podFlying #tutCard { visibility: hidden; }
#topbar, #bottombar { transition: opacity .3s; }
.itemIcon { width: 26px; height: 26px; object-fit: contain; vertical-align: middle; image-rendering: pixelated; }
.itemIcon.lg { width: 40px; height: 40px; }
.costOk { color: var(--mint); }
.costShort { color: var(--warn); font-weight: bold; }
.progressWrap { background: var(--track); border: 1px solid var(--track-border); height: 12px; overflow: hidden; flex: 1; }
.progressBar { height: 100%; background: var(--gold); }
.fuelWrap { background: var(--track); border: 1px solid var(--track-border); height: 12px; overflow: hidden; width: 110px; display: inline-block; vertical-align: middle; }
.fuelBar { height: 100%; background: var(--accent); }
.fuelBar.low { background: var(--error); }
/* Build E (Part 10, bars parity with the Muse build): the amber fuel
   tier (25% or under; red stays 5% or under), the purple RODS bar the
   Muse build pairs with Mk3 machines, and the node DRAIN BAR (the
   node's remaining contents, emptying as it is used up). */
.fuelBar.warn { background: #ffb03a; }
.rodLine { display: flex; align-items: center; gap: 8px; margin-top: 3px; }
.rodWrap { background: var(--track); border: 1px solid var(--track-border); height: 10px; overflow: hidden; width: 110px; display: inline-block; vertical-align: middle; }
.rodBar { height: 100%; background: #c792ff; }
.nodeDrain { background: var(--track); border: 1px solid var(--track-border); height: 14px; overflow: hidden; margin: 4px 0 8px; }
.nodeDrain > span { height: 100%; background: var(--accent); }

/* factory cards */
.facCard { border: 2px solid var(--border); border-radius: 6px; padding: 10px; margin-bottom: 9px; background: var(--surface-alt); }
.facCard.silhouette { opacity: .45; background: var(--surface); }
.facCard.next { border-color: var(--gold); }
.facHead { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
.facHead .facName { font-weight: bold; flex: 1; }
.facStatus { font-family: var(--font-pixel); font-size: 8px; background: var(--surface); padding: 3px 7px; border-radius: 4px; border: 1px solid var(--border); color: var(--dim); }
/* Build BE (BE2): the status column stacks the WAREHOUSE FULL pill
   directly under the BUILT pill, right-aligned with it. */
.facStatusCol { display: flex; flex-direction: column; align-items: flex-end; gap: 3px; }
.facWhFull { font-family: var(--font-pixel); font-size: 8px; background: #6e1d1d; padding: 3px 7px; border-radius: 4px; border: 1px solid #ff7e1a; color: #ffd9b0; white-space: nowrap; }
.facCard .row { margin-top: 7px; }
.facCard button { padding: 7px 12px; }
.costLine { font-size: .86em; margin: 2px 0; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }

/* market */
.chips { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; }
.chips button { padding: 6px 12px; font-size: 9px; background: var(--surface-alt); border-color: var(--border); color: var(--dim); }
.chips button.active { background: var(--btn); border-color: var(--mint); color: #fff; }
.mktRow { display: flex; align-items: center; gap: 8px; padding: 6px 4px; border-bottom: 1px solid var(--border); font-size: .9em; flex-wrap: wrap; }
.mktRow .nm { font-weight: bold; min-width: 110px; display: flex; align-items: center; gap: 6px; }
.mktRow .own { color: var(--dim); min-width: 74px; text-align: right; }
.mktRow .pr { min-width: 118px; text-align: right; }
.driftUp { color: var(--accent); }
.driftDown { color: var(--warn); }
/* Build P (Part 11): the inline sell controls — a second line in
   the market row: slider (grows), the amount readout, and the
   credit-labelled sell button. */
/* Build W (Part 3): the sell slider sits ALONE on its own
   full-width line; the quantity box and the SELL button sit on
   the line below it (the owner's fix at check 177, superseding
   the shared-line layout of Builds P–V). */
.mktRow .sellSliderLine { flex: 1 1 100%; display: flex; align-items: center; }
.mktRow .sellBtnLine { flex: 1 1 100%; display: flex; align-items: center; gap: 8px; }
/* Build V (Part 1): the slider takes ALL the width its line offers
   (flex basis 0, min-width 0 — nothing squeezes it); the quantity
   box and the commit button keep their places at the end of the
   line. Build V (Part 2): the quantity is a tappable input box
   (the base input rule gives it the box border/background).
   Build W (Part 3): the box and the button no longer share the
   slider's line — see the SliderLine/BtnLine rules. */
.mktRow .sellSlider { flex: 1 1 0; min-width: 0; }
.mktRow .sellQty { width: 64px; min-width: 64px; flex: 0 0 auto; text-align: center; font-weight: bold; padding: 4px; }
.mktRow .sellGo { min-width: 96px; }
/* Build R (Part 5): the inline BUY controls — the row's first
   control line. The standing price number and drift % that used
   to sit in .pr are gone from the rows. Build U (Part 1): the
   buy line is an EXACT COPY of the sell line above (long slider,
   the plain quantity number, the commit button) — the live trade
   readout that sat between the slider and the button from
   Build R through Build T is REMOVED entirely (owner's confirmed
   mockup at re-check 173), so the .buy* rules below mirror the
   .sell* rules value for value. */
/* Build W (Part 3): same two-line split as the sell side — the
   buy slider alone on its own full-width line, the quantity box
   and the BUY button on the line below. */
.mktRow .buySliderLine { flex: 1 1 100%; display: flex; align-items: center; }
.mktRow .buyBtnLine { flex: 1 1 100%; display: flex; align-items: center; gap: 8px; }
.mktRow .buySlider { flex: 1 1 0; min-width: 0; }
.mktRow .buyQty { width: 64px; min-width: 64px; flex: 0 0 auto; text-align: center; font-weight: bold; padding: 4px; }
.mktRow .buyGo { min-width: 96px; }
/* Build BI (BI1): the per-unit price after the BUY / SELL button and
   its drift arrow (green up, red down, none at baseline). */
.mktRow .mktPrice { font-weight: bold; white-space: nowrap; }
.mktRow .driftUp { color: var(--accent); }
.mktRow .driftDown { color: var(--error); }
/* Build V (Part 1): the MARKET panel itself runs the full width of
   the phone (the generic panel caps at 94vw with 14px side
   padding) — edge-to-edge rows, header bleeding to match, so the
   control lines above use every pixel the phone offers. The 560px
   cap stays for wide screens. */
#marketPanel { width: min(100vw, 560px); padding: 0 6px 14px; }
#marketPanel header { margin: 0 -6px 12px; }
.gearCard { border: 2px solid var(--border); border-radius: 6px; padding: 10px; margin-bottom: 9px; background: var(--surface-alt); }
.gearCard .row { margin-top: 7px; }

/* workshop + machines */
.machCard { border: 2px solid var(--border); border-radius: 6px; padding: 10px; margin-bottom: 9px; background: var(--surface-alt); display: flex; gap: 10px; align-items: flex-start; flex-wrap: wrap; }
.machCard img { width: 54px; height: 54px; object-fit: contain; image-rendering: pixelated; }
.machInfo { flex: 1; min-width: 180px; font-size: .9em; }
.machInfo .row { margin-top: 6px; }
/* Build J (Part 9): the MACHINES list's drop-down toggle below the
   first three machines. */
.machMoreRow { text-align: center; margin: 2px 0 10px; }
.machMoreRow button { padding: 8px 26px; font-size: 12px; }
.segBtns { display: flex; gap: 6px; flex-wrap: wrap; margin: 6px 0; }
.segBtns button { background: var(--surface-alt); border-color: var(--border); color: var(--dim); font-size: 9px; }
.segBtns button.active { background: var(--btn); border-color: var(--mint); color: #fff; }

/* slider sheet */
.sheetWrap { position: fixed; inset: 0; background: rgba(0,0,0,.6); z-index: 15; display: flex; align-items: flex-end; justify-content: center; }
.sheetCard { background: var(--surface); border: 2px solid var(--border); border-bottom: none; border-radius: 6px 6px 0 0; padding: 16px; width: min(96vw, 560px); max-height: 82vh; overflow-y: auto; }
.sheetCard h3 { font-family: var(--font-pixel); font-size: 12px; line-height: 1.6; color: #fff; margin-bottom: 8px; }
.sheetCtl { margin: 12px 0 4px; }
.sheetLiveQty { font-family: var(--font-pixel); font-size: 17px; color: var(--gold); text-align: center; margin: 8px 0 2px; }
.goldVal { color: var(--gold); font-family: var(--font-pixel); font-size: 10px; }
.sheetCtl input[type="range"] { flex: 1; min-width: 140px; }
.sheetCtl input[type="number"] { width: 92px; }
.sheetBtns { margin-top: 12px; }
#sheetConfirm { background: var(--btn); border-color: var(--mint); color: #fff; padding: 10px 18px; font-size: 11px; }
#sheetCancel { background: transparent; border-color: var(--border); color: var(--mint); padding: 10px 18px; }
#sheetMax { padding: 10px 18px; }
.recipeBoxes { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.rbox { border: 2px solid var(--border); border-radius: 6px; padding: 6px 8px; background: var(--surface); font-size: .88em; text-align: center; min-width: 78px; }
.rbox.out { background: rgba(255,216,61,.16); border-color: var(--gold); }
.rbox .stk { display: block; font-size: .82em; color: var(--dim); margin-top: 2px; }
.rArrow { font-size: 1.2em; font-weight: bold; color: var(--mint); }

/* ============ Port Stage 3 ============ */
.hdrRank { font-family: var(--pix); font-size: 9px; color: var(--mint); letter-spacing: 1px; }
.badge { background: var(--bad); color: #fff; border-radius: 8px; padding: 1px 6px; font-size: 8px; vertical-align: top; }
.jpStrip { background: var(--panel2); border: 2px solid var(--gold); color: var(--gold);
  font-family: var(--pix); font-size: 8px; line-height: 1.7; padding: 8px; margin-bottom: 10px; }
.attCard { background: var(--panel2); border: 2px solid var(--line); padding: 8px; margin-bottom: 8px; }
.attCard .row { justify-content: space-between; align-items: center; }
.bar { height: 10px; background: #0a1a10; border: 1px solid var(--line); margin: 6px 0; }
.bar i { display: block; height: 100%; background: var(--mint); }
.rankCard { background: var(--panel2); border: 2px solid var(--gold); padding: 10px; margin-bottom: 10px; }
.rankName { font-family: var(--pix); font-size: 15px; color: var(--gold); }
.whRow.mine { border-color: var(--gold); background: #1c2b16; }
.cutLine { font-family: var(--pix); font-size: 8px; color: var(--bad); text-align: center; padding: 4px 0; }
.dailyOpts button { flex: 1; }
/* Build R (Part 21): the selected Daily tier button reads active. */
.dailyOpts button.active { background: var(--btn); border-color: var(--mint); color: #fff; }
#dailyStakeRange { flex: 1; accent-color: var(--mint); }
#chatMsgs { max-height: 46vh; overflow-y: auto; background: var(--panel2);
  border: 2px solid var(--line); padding: 8px; margin-bottom: 8px; }
.chatMsg { margin-bottom: 8px; font-size: 14px; word-break: break-word; }
.chatMsg.mine { color: var(--cream); }
.chatHead { font-family: var(--pix); font-size: 8px; color: var(--mint); }
.chatRow input { flex: 1; }
.popupCard.takeover { border-color: var(--gold); text-align: center; }
.popupCard.takeover.fail { border-color: var(--bad); }
#takeoverBig { font-family: var(--pix); font-size: 17px; color: var(--gold); margin: 10px 0; line-height: 1.5; }
.popupCard.takeover.fail #takeoverBig { color: var(--bad); }
/* Build L1 (hotfix, device-verified 2026-10-28): the takeover card
   caps its height and its TEXT scrolls. A long announcement
   (1,806 chars) overflowed the card — the text could not be
   scrolled and CONTINUE was not properly reachable. The card is a
   flex column: the title, the big line and CONTINUE stay FIXED;
   only #takeoverText scrolls. Line breaks in the text are kept
   (pre-wrap — an operator message's blank lines survive) and long
   unbroken words wrap inside the card instead of spilling out. */
/* Build L2 (hotfix, device-verified 2026-10-28): at the game's
   MAXIMUM UI size the L1 card still outgrew the phone screen —
   its 86vh cap is measured against the LARGE viewport, so on the
   device the card centred on area that was not visible and
   CONTINUE fell off the bottom; the modal could not be dismissed
   (the player escaped with the phone's back button). The owner's
   design: the card is HALF THE SCREEN — max-height 50dvh (the
   dynamic viewport tracks the VISIBLE area; 50vh is the fallback
   line for old WebViews) — and pumpPopups ALSO sets the cap from
   the client's own measured innerHeight, which is the measure
   that holds on the real device. The text area gains a chevron
   RAIL on its right side (#takeoverChevRail, inside
   #takeoverBodyRow): the down chevron steps the text down, the up
   chevron appears once scrolled — the tutorial card's cue rules —
   in its own rail, never over the text. Touch-drag scrolling
   keeps working alongside. Title + CONTINUE stay fixed, so the
   trap cannot recur at any UI size. */
.popupCard.takeover { display: flex; flex-direction: column; max-height: 92vh; max-height: 92dvh; } /* Build AY (AY1): grown to fit all its text; the JS fit (fitTakeoverCard) sets the same cap from the game view */
.popupCard.takeover > h3, .popupCard.takeover > #takeoverBig,
.popupCard.takeover > #takeoverContinue { flex: none; }
/* Build AQ: the popup picture at the TOP of the takeover card —
   full card width, rounded corners, fixed in place while the text
   scrolls under the half-screen cap (the picture counts toward
   the cap: #takeoverBodyRow is the flex item that gives way). */
/* Build AT (AT3): the away card's win picture (#awayPic) is
   presented exactly like the takeover card's picture. */
#takeoverPic, #takeoverRankPic, #awayPic { display: block; width: 100%; height: auto; border-radius: 8px; margin-bottom: 10px; }
.popupCard.takeover > #takeoverPic, .popupCard.takeover > #takeoverRankPic { flex: none; }
#takeoverBodyRow { flex: 1 1 auto; min-height: 0; display: flex; gap: 8px; margin-bottom: 16px; }
#takeoverText { flex: 1 1 auto; min-width: 0; min-height: 0; overflow-y: auto; white-space: pre-wrap; overflow-wrap: anywhere; margin-bottom: 0; }
#takeoverChevRail {
  flex: none; display: flex; flex-direction: column;
  justify-content: center; gap: 12px; align-items: center;
}
.takeoverChev {
  width: 44px; height: 36px; min-width: 44px; min-height: 36px;
  color: var(--gold); font-size: 16px; line-height: 1;
  background: rgba(10, 42, 51, .82); border: 2px solid var(--gold); border-radius: 6px;
  text-shadow: 0 1px 3px #000; padding: 0;
}

/* Build E (Part 13): narrow-width dock step-downs — placed last so
   they override the older phone media rules. Icon + name must fit
   inside the chip at phone widths (CHALLENGES is the binding case). */
@media (max-width: 480px) {
  #bottombar { padding: 7px 4px; gap: 4px; }
  #bottombar button { font-size: 7px; gap: 2px; padding: 10px 0; }
  #bottombar .dockIcon { width: 15px; height: 15px; }
}
@media (max-width: 380px) {
  #bottombar button { font-size: 6.5px; }
  #bottombar .dockIcon { width: 13px; height: 13px; }
}
/* Build F (Item 1): one more step down for narrow phones. The pixel
   font's advance is exactly 1em per glyph, so CHALLENGES (10 glyphs)
   + icon + gap must fit a quarter of the bar: at 320 px wide the chip
   interior is ~71 px, which the 6.5px tier above (65 + 13 + 2 = 80)
   overflows. This tier (55 + 12 + 2 = 69) fits it in CSS alone; the
   measured auto-fit in app.js (fitDockLabels) covers any larger
   text rendering a device applies on top. */
@media (max-width: 350px) {
  #bottombar button { font-size: 5.5px; }
  #bottombar .dockIcon { width: 12px; height: 12px; }
}
