/* mobile.css — mobile-phone layout for the Cairnbourne prototype.
 *
 * Everything is gated behind @media (max-width: 900px), so:
 *   - a narrow window / phone / DevTools device toolbar  -> mobile shell
 *   - a wide desktop window                              -> untouched desktop game
 * (resize the browser, or Ctrl+Shift+M in Chrome, to flip between them.)
 *
 * Strategy: keep the exact desktop DOM; re-flow it. The single <canvas> fills the
 * screen (mobile_shell.js drives fit-height + horizontal pan + pinch). The 8 sidebar
 * tab buttons become a fixed bottom nav; their content panels + the build menu open as
 * full-screen sheets that slide up over the map.
 */

@media (max-width: 900px) {

  :root {
    --mnav-h: 76px;              /* fixed bottom nav (two tab rows) */
    --mseason-h: 30px;           /* season/speed strip above the nav */
    --msheet-head-h: 46px;       /* injected sheet close-bar */
  }

  html, body {
    margin: 0 !important;
    height: 100% !important;
    overflow: hidden !important;
    overscroll-behavior: none !important;
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
    -webkit-tap-highlight-color: transparent;
  }

  /* ── Shell: single column (top bars, canvas fills, fixed bottom nav) ── */
  #game-shell {
    display: flex !important;
    flex-direction: column !important;
    width: 100% !important;
    height: 100vh !important;
    height: 100dvh !important;
    /* reserve space for the fixed bottom nav + season strip so the canvas sits above them */
    padding-bottom: calc(var(--mnav-h) + var(--mseason-h) + env(safe-area-inset-bottom, 0px)) !important;
    box-sizing: border-box !important;
  }

  /* ── Top HUD: resources + info/controls, compact + horizontally scrollable ── */
  #top-bar, #info-bar {
    grid-column: auto !important;
    grid-row: auto !important;
    flex: 0 0 auto !important;
    height: auto !important;
    min-height: 38px !important;
    overflow-x: auto !important;
    overflow-y: hidden !important;
    flex-wrap: nowrap !important;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  #top-bar::-webkit-scrollbar, #info-bar::-webkit-scrollbar { display: none; }
  #top-bar { padding-top: calc(env(safe-area-inset-top, 0px) + 4px) !important; }
  #top-bar > *, #info-bar > * { flex: 0 0 auto !important; }

  /* Trim the noisiest desktop-only controls on a phone */
  #info-bar .speed-btn[data-speed="10"],
  #info-bar .speed-btn[data-speed="20"],
  #info-bar .speed-btn[data-speed="50"],
  #info-bar .speed-btn[data-speed="100"] { display: none !important; }

  /* ── Canvas fills the middle. mobile_shell.js sets transform/pan/zoom. ── */
  #canvas-container {
    grid-column: auto !important;
    grid-row: auto !important;
    flex: 1 1 auto !important;
    min-height: 0 !important;
    align-items: flex-start !important;
    justify-content: flex-start !important;
    overflow: hidden !important;
    touch-action: none !important;   /* we own all gestures */
  }
  #gameCanvas { transform-origin: top left !important; }

  /* ── Right sidebar becomes a passthrough; its children are placed independently ── */
  #right-sidebar { display: contents !important; }
  /* The log BOX stays hidden — a 320px scrolling panel has nowhere to live on a phone — but it is
     still the thing logMessage writes to, and mobile_ui.js's watchEventLog() observes it and floats
     each new line over the map instead. It was hidden here with "fold into a sheet later" and never
     folded, so for the whole life of this build every rewind line, starvation, offering and line of
     the Cairn's own voice was written into a 0x0 element and discarded. */
  #event-log { display: none !important; }
  /* #bottom-bar HOSTS the combat bars (placement/combat/squad/structures), so it can't be
     display:none — pass it through and hide only its city-view noise. */
  #bottom-bar { display: contents !important; }
  #bottom-bar-text, #testing-cheat-panel, #build-menu, #resume-continue-hint { display: none !important; }

  /* The 8 tab buttons -> fixed bottom nav */
  #sidebar-tabs {
    position: fixed !important;
    left: 0; right: 0; bottom: 0;
    z-index: 4000;
    display: none;                /* revealed only once the shell is ready — keeps the nav off the loading screen */
    background: var(--deep, #12101a);
    border-top: 1px solid var(--border, #2a2740);
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
  body.mproto-ready #sidebar-tabs { display: block; }
  .sidebar-tab-row { display: flex !important; width: 100%; }
  .sidebar-tab {
    flex: 1 1 0 !important;
    min-width: 0 !important;
    padding: 7px 2px !important;
    font-size: 14.5px !important;
    min-height: 36px !important;
    border-bottom: 2px solid transparent;
  }

  /* Tab CONTENT -> full-screen sheet that slides up over the map */
  #sidebar-tab-area {
    position: fixed !important;
    left: 0; right: 0;
    top: 0;
    bottom: calc(var(--mnav-h) + env(safe-area-inset-bottom, 0px));
    z-index: 3900;
    background: var(--deep, #12101a);
    overflow-y: auto !important;
    -webkit-overflow-scrolling: touch;
    padding: calc(var(--msheet-head-h) + env(safe-area-inset-top, 0px)) 10px 16px !important;
    transform: translateY(112%);
    transition: transform .22s ease;
    visibility: hidden;
  }
  body.mproto-sheet-open #sidebar-tab-area {
    transform: translateY(0);
    visibility: visible;
  }

  /* Build menu -> full-screen sheet */
  #left-sidebar {
    position: fixed !important;
    left: 0; right: 0;
    top: 0;
    bottom: calc(var(--mnav-h) + env(safe-area-inset-bottom, 0px));
    width: auto !important;
    grid-column: auto !important;
    grid-row: auto !important;
    z-index: 3900;
    overflow-y: auto !important;
    -webkit-overflow-scrolling: touch;
    padding-top: calc(var(--msheet-head-h) + env(safe-area-inset-top, 0px)) !important;
    padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 28px) !important;
    transform: translateY(112%);
    transition: transform .22s ease;
    visibility: hidden;
  }
  body.mproto-build-open #left-sidebar {
    transform: translateY(0);
    visibility: visible;
  }
  /* Build sheet: stack all four categories and let the SHEET itself scroll.
     Desktop lays the columns out as a 2x2 grid where each column list scrolls
     on its own; on a phone that inner clipping hid the lower categories, so the
     roads at the bottom of "Surviving" were unreachable (the stone road could
     never be tapped). Neutralise the desktop flex/overflow on the sheet's inner
     nodes so everything flows to natural height into #left-sidebar's own scroll
     (which inherits the standard themed scrollbar from the global `*` rule). */
  #left-sidebar #building-columns {
    display: block !important;
    flex: 0 0 auto !important;
    height: auto !important;
    overflow: visible !important;
  }
  #left-sidebar .build-col {
    display: block !important;
    height: auto !important;
    overflow: visible !important;
    border-right: none !important;
    border-bottom: 1px solid var(--border-dim) !important;
  }
  #left-sidebar .build-col:last-child { border-bottom: none !important; }
  #left-sidebar .build-col-list {
    flex: none !important;
    height: auto !important;
    overflow: visible !important;
  }
  /* Sticky category headers so you always know which list you're scrolling. */
  #left-sidebar .build-col-header {
    position: sticky; top: 0; z-index: 1;
  }

  /* Pinned balance bars: keep the Soul Reserve (SE ◆) and the Cairn shard
     balance (TS ◈) visible at the top of their menus while the cards scroll
     under them. Mirrors the .build-col-header sticky pattern; an opaque
     background hides the cards scrolling underneath.

     `top` MUST be 0. The scroll container here is the tab PANEL itself
     (#tab-souls / #tab-cairn carry overflow-y:auto), and the panel already sits
     below the fixed close-bar because its parent #sidebar-tab-area carries
     padding-top: var(--msheet-head-h). A sticky inset is measured from ITS OWN
     scrollport, so repeating the close-bar height here counted it twice: the bar
     was shoved 46px down its own panel, which opened a band of empty space above
     it AND made it paint over the category rail that follows it in flow (the
     All / Economy / Town / Enemies tabs sat underneath it, invisible).

     …and `top: 0` is only half of flush. Measured: the sheet head ends at 46 and the bar sat at 56, with
     a 10px band of scrolling cards showing through, at rest AND while scrolled. The scroller is the
     PANEL (#tab-souls / #tab-cairn carry overflow-y:auto and are what actually scroll), and a sticky
     inset of 0 pins the element to the scrollport's CONTENT edge, not its border edge — so the
     panel's own `.tab-panel { padding: 10px }` is a floor the bar can never rise above. The fix is
     to hand that top padding to the bar itself: the panel gives up its padding-top, so the bar's
     content box starts exactly where the sheet head ends and there is nothing left to show through.
     Do NOT do it with a negative `top` instead — that is the 10px written down a second time, and
     it would drift the moment .tab-panel's padding moves.

     #tab-jobs is in this list for the SAME reason and not for a bar of its own: the bulk chip is
     sticky at the top of that panel too (placeBulkChip drops it in as the first child), so it was
     sitting 10px down its own scrollport with job rows sliding through the band above it. Measured
     before: gap 10.00 in Jobs against 0.00 in the Cairn, which is exactly the two panels that had
     given up their padding and the one that had not. Whatever pins to the top of one of these
     panels, the panel owes it that padding. */
  #tab-jobs, #tab-souls, #tab-cairn { padding-top: 0 !important; }
  #tab-souls .sf-sb-head,
  #tab-cairn #cairn-balance {
    position: sticky;
    top: 0;
    z-index: 5;
    background: var(--deep, #12101a);
  }
  /* Bleed the sticky bar across the sheet's side padding so scrolled cards
     don't peek past its edges, and give it a divider + a little breathing room. */
  #tab-souls .sf-sb-head {
    margin: 0 -10px;
    padding-left: 12px; padding-right: 12px;
  }
  #tab-cairn #cairn-balance {
    margin: 0 -10px 10px;
    padding: 8px 12px;
    border-bottom: 2px solid var(--gold-dim, #4a3d1a);
  }

  /* ── Injected chrome (mobile_shell.js): sheet close-bar + Build button ── */
  #mproto-sheet-head {
    position: fixed;
    top: 0; left: 0; right: 0;
    height: calc(var(--msheet-head-h) + env(safe-area-inset-top, 0px));
    padding: env(safe-area-inset-top, 0px) 8px 0 14px;
    box-sizing: border-box;
    z-index: 4100;
    display: none;
    align-items: center;
    justify-content: space-between;
    background: var(--panel, #1a1726);
    border-bottom: 1px solid var(--border, #2a2740);
    font-family: inherit;
    color: var(--bone, #e8dcc0);
    font-size: 18px;
    letter-spacing: 1px;
  }
  body.mproto-sheet-open #mproto-sheet-head,
  body.mproto-build-open #mproto-sheet-head { display: flex; }
  #mproto-sheet-title { font-weight: 700; text-transform: uppercase; opacity: .92; }
  #mproto-sheet-close {
    background: none; border: 1px solid var(--border, #3a3550);
    color: var(--bone, #e8dcc0);
    width: 34px; height: 34px; line-height: 1;
    border-radius: 6px; font-size: 24px; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
  }
  #mproto-sheet-close:active { background: rgba(255,255,255,.08); }

  /* Floating "Build" button, above the nav, left side */
  #mproto-build-fab {
    position: fixed;
    left: 12px;
    bottom: calc(var(--mnav-h) + var(--mseason-h) + env(safe-area-inset-bottom, 0px) + 10px);
    z-index: 3800;
    display: flex; align-items: center; gap: 6px;
    padding: 10px 15px;
    border-radius: 24px;
    background: var(--panel, #211c30);
    border: 1px solid var(--gold, #6a5a2e);
    color: var(--bone, #e8dcc0);
    font-family: inherit; font-size: 17px; font-weight: 700;
    letter-spacing: 1px;
    box-shadow: 0 3px 12px rgba(0,0,0,.5);
    cursor: pointer;
  }
  #mproto-build-fab:active { filter: brightness(1.2); }
  body.mproto-sheet-open #mproto-build-fab,
  body.mproto-build-open #mproto-build-fab { display: none; }

  /* Overlays that render mid-screen (job overlay, defeat, guides) get room to breathe.
     They mostly use their own fixed positioning already; just make sure they sit on top. */
  #job-overlay, #defeat-overlay, #victory-overlay, .guide-overlay { z-index: 5000 !important; }

  /* ── The rest of the full-screen pages, lifted over the chrome ────────────
     The desktop overlays live in a z band of 30 to 42, which was fine when the only things above
     the map were the map's own panels. The touch chrome then took the whole 3600 to 4400 band —
     event toasts, the season strip, the nav, the sheet close-bar, the placement banner — so every
     one of these pages opens UNDERNEATH it. Four of them were lifted one at a time as they were
     noticed (the rule above); these are the remainder, done as a band rather than one by one so the
     next page added does not have to be noticed the same way.

     Their RELATIVE order is preserved exactly — prestige 30 < options 35 = soul forge 35 <
     feedback 36 < offering 40 < save string 4000 — so any pairing that could open together stacks
     on a phone the way it does on a desktop. The band sits below the guides (4820) and below the
     job / defeat / victory overlays (5000), both of which are meant to win over everything. */
  #prestige-overlay:not(.hidden)      { z-index: 4700 !important; }
  #options-overlay:not(.hidden)       { z-index: 4705 !important; }
  #soul-forge-overlay:not(.hidden)    { z-index: 4705 !important; }
  #feedback-overlay:not(.hidden)      { z-index: 4706 !important; }
  #offering-overlay:not(.hidden)      { z-index: 4710 !important; }
  #savestring-overlay:not(.hidden)    { z-index: 4730 !important; }
  /* ── The save modal's three buttons must fit ONE line, down to 320 ────────
     Measured before: the import row (Load Save / From File / Close) needed 356.9px against 254px of
     inner panel at 320, and 356.9 against 319 even at 390, so Close wrapped onto a row of its own on
     every phone width there is. The type size is not the lever and was left alone. Three things were
     spending the budget instead: 16px of padding a side on each button (96px across three, more than
     a third of a 320 panel), a 10px gap, and the two longest strings in the modal.
     The buttons are flex: 0 0 auto rather than allowed to shrink, because a squeezed VT323 button
     CLIPS its label instead of narrowing it, and a clipped label is what this rule exists to prevent.
     The panel gives up some of its own side padding too, so the fit is not down to the last pixel. */
  #savestring-panel { padding-left: 14px !important; padding-right: 14px !important; }
  #savestring-row { gap: 4px !important; }
  #savestring-row .btn { padding: 6px 6px !important; flex: 0 0 auto; }
}

/* Very short landscape phones: shrink the nav so the map keeps most of the height. */
@media (max-width: 900px) and (max-height: 480px) {
  :root { --mnav-h: 64px; }
  .sidebar-tab { min-height: 30px !important; padding: 6px 2px !important; font-size: 11px !important; }
}

/* ══════════════════════════════════════════════════════════════════════════
   v2 workability pass — season strip, compact HUD + Settings, placement
   fertility banner, combat thumb-buttons. All chrome injected by mobile_ui.js
   is gated on body.mproto-ready so it never shows on the wide desktop fallback.
   ══════════════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {

  /* ── Compact top HUD ──────────────────────────────────────────────────── */
  /* Resources wrap to as many short rows as needed and stay fully visible
     (no horizontal scroll). Save/Import/New Game/Options move into Settings. */
  #top-bar {
    flex-wrap: wrap !important;
    height: auto !important;
    row-gap: 1px !important;
    padding: calc(env(safe-area-inset-top, 0px) + 3px) 4px 3px !important;
    overflow: visible !important;
    align-content: flex-start !important;
  }
  #top-bar .res-chip { font-size: 13px !important; padding: 0 3px !important; gap: 2px !important; }
  #top-bar .res-lbl { display: none !important; }        /* icon + number only */
  /* Each chip's width is RESERVED at runtime by reserveResourceWidths() in mobile_ui.js, as an inline
     min-width in `ch` derived from that resource's cap. Without it the bar re-wraps every time a value
     gains or loses a digit and the whole screen below it hops a row. The desktop `min-width:64px` is
     dropped here (it is far too wide for a phone) but NOT with !important, because an !important
     declaration would beat the inline reservation and put the jitter straight back.
     Tabular figures and no letter spacing so the reservation in `ch` is EXACT: `ch` is the advance
     of a single "0" and knows nothing about the gap the desktop rule puts after every character, so
     with it on, an N character string is wider than N ch and the reservation never quite binds. */
  #top-bar .res-val { min-width: 0; font-variant-numeric: tabular-nums; text-align: center;
                      letter-spacing: 0; }
  #top-bar .hud-divider { display: none !important; }
  #top-bar .hud-living { font-size: 13px !important; margin-left: 2px !important; }
  #hud-right-group { display: none !important; }         /* relocated into Settings */

  #info-bar {
    height: auto !important; min-height: 0 !important;
    flex-wrap: wrap !important; overflow: visible !important;
    padding: 2px 5px !important; gap: 5px !important;
  }
  #info-bar .phase-label { font-size: 12px !important; }
  /* ── The row must hold on ONE line, and it is a budget, not a wish ───────────────────────────
     Measured at the stated worst case (phase 9999, year 99999): "Phase 9999 · Year 99999" is 151px,
     the AR/BCP/BSP group 140, the gear 32, the divider 1 — 324px of content plus the gaps, against
     378px of content width on a 390 phone and 308 on a 320 one. The gear wrapped to a second line at
     every width, and at 320 it wrapped on a FRESH game too.
     The seconds countdown goes first because it is the one thing here that is already told twice:
     the season strip above the bottom nav carries a live progress bar for the same year, so the
     number is a duplicate readout, and it is the only item on the row that changes every second.
     (Hidden, not removed — ui.js still writes to it every tick and must keep finding it.) */
  #info-bar #hud-timer { display: none !important; }
  /* The top bar drops its dividers for the same reason; this is the last one left. */
  #info-bar .hud-divider { display: none !important; }
  /* season + the whole right group (checkboxes + speeds) are relocated by JS;
     the selectors below only bite while they still sit in #info-bar. */
  #info-bar #hud-season, #info-bar #info-right-group { display: none !important; }

  /* A small gear injected into #info-bar opens the Settings sheet. */
  /* AR / BCP / BSP auto-toggle chips (in the info bar, left of the gear) */
  #mproto-auto-toggles { display: none; }
  body.mproto-ready #mproto-auto-toggles { display: inline-flex; gap: 4px; margin-left: auto; align-items: center; }
  #mproto-auto-toggles .mproto-auto-tgl {
    font-family: inherit; font-size: 13px; font-weight: 700; letter-spacing: .5px;
    padding: 4px 7px; min-height: 26px; border-radius: 5px; cursor: pointer;
    background: #201c2c; border: 1px solid var(--border, #3a3550); color: #6f6a86;
  }
  #mproto-auto-toggles .mproto-auto-tgl.on { background: #2f3a22; border-color: #6a8a3a; color: #a8d060; }
  #mproto-auto-info { color: #9aa0b8 !important; font-style: italic; font-weight: 700; }
  #mproto-auto-info-pop {
    position: fixed; z-index: 4600; display: none; max-width: 262px;
    background: rgba(18,16,26,0.98); border: 1px solid var(--gold, #6a5a2e);
    border-radius: 8px; padding: 9px 11px; color: var(--bone, #e8dcc0);
    font-size: 14.5px; line-height: 1.5; box-shadow: 0 4px 16px rgba(0,0,0,.6);
  }
  #mproto-auto-info-pop.show { display: block; }
  #mproto-auto-info-pop b { color: #a8d060; }

  #mproto-settings-btn {
    margin-left: 6px !important;
    background: none; border: 1px solid var(--border, #3a3550);
    color: var(--bone, #e8dcc0); border-radius: 6px;
    width: 32px; height: 26px; font-size: 18px; line-height: 1; cursor: pointer;
    display: none;
  }
  body.mproto-ready #mproto-settings-btn { display: inline-block; }

  /* Below 390 the row is 30px short of its own content even with the countdown gone (measured: 339px
     of items and gaps against 310px of content width at 320), so the type and the padding tighten
     just enough to seat all three groups — the same treatment, at the same breakpoint, as the
     placement bar further down. The chips keep their 26px height, so the fingertip target only
     narrows, it does not shrink. */
  @media (max-width: 389px) {
    #info-bar { gap: 3px !important; }
    #info-bar .phase-label { font-size: 11px !important; }
    body.mproto-ready #mproto-auto-toggles { gap: 3px; }
    #mproto-auto-toggles .mproto-auto-tgl { padding: 4px 6px; letter-spacing: 0; }
    /* The VT323 compensation pass took these from 11px to 13px, which is right everywhere else and
       is 1.3px too wide HERE: measured at 320, the row's three groups came to 303.4px of content
       plus 8px of gaps against 310px of usable width, so the gear wrapped to a second line. Pulled
       back to 12px on the narrowest phones only, which is still above where they started. The row
       is a hard WIDTH BUDGET and it is the one place in the interface where type size is not a free
       choice — see the note above this media query. */
    #mproto-auto-toggles .mproto-auto-tgl,
    #mproto-auto-info { font-size: 12px !important; }
    #mproto-settings-btn { margin-left: 4px !important; width: 30px; }
  }

  /* ── Season / speed strip (above the bottom nav) ──────────────────────── */
  #mproto-season-bar {
    position: fixed; left: 0; right: 0;
    bottom: calc(var(--mnav-h) + env(safe-area-inset-bottom, 0px));
    height: var(--mseason-h);
    z-index: 3700; display: none;
    align-items: center; gap: 6px; padding: 0 8px;
    background: var(--deep, #12101a);
    border-top: 1px solid var(--border-dim, #23202f);
    overflow: hidden;
  }
  body.mproto-ready #mproto-season-bar { display: flex; }
  #mproto-season-bar #hud-season { display: inline !important; font-size: 13px !important; letter-spacing: 1px; }
  #mproto-season-bar .mproto-spacer { flex: 1; }
  /* The desktop speed row never appears on touch: #mproto-pause-btn + #mproto-speed-btn replace the
     whole thing with two controls, so the strip's width no longer depends on how many speeds are
     unlocked. Kept in the DOM because they are what togglePause/setSpeed report into. */
  #mproto-season-bar .speed-btn, #info-right-group .speed-btn { display: none !important; }
  /* An explicit HEIGHT, not a padded text box. At font 18 / line-height 1.1 plus 4px of padding
     each side and a border these came to ~30px inside a 30px strip, so pause and speed touched
     both edges and read as slabs wedged into the bar — the AR/BCP/BSP chips in the info bar sit
     26px inside a taller row and that gap is the whole difference. Sizing from the text box means
     any type change moves the chip; sizing the chip means it stays where it was put. */
  #mproto-season-bar .mproto-speed-ctl {
    flex: 0 0 auto; margin: 0; min-width: 40px; padding: 0 10px;
    height: 24px; display: inline-flex; align-items: center; justify-content: center;
    font-family: 'VT323','Courier New',monospace; font-size: 18px; line-height: 1;
    background: #1b1826; color: var(--bone, #e8dcc0);
    border: 1px solid var(--border-dim, #35314a); border-radius: 6px; cursor: pointer;
    box-sizing: border-box;
  }
  /* The pause bars are a canvas the JS draws and hands over as a background image (see _glyphUrl
     in mobile_ui.js); the play state is the plain ▶ text glyph, kept by request. While the bars
     show, the button text is a no-break space — it no longer has to be, now that the chip above
     carries an explicit height rather than deriving one from its text box, but an empty string
     would still collapse the flex line so it stays. background-size matches the 12px logical grid
     the bars are drawn on, so at any device pixel ratio one drawn pixel covers exactly one CSS
     pixel and the mark stays hard. */
  #mproto-season-bar #mproto-pause-btn {
    min-width: 34px;
    background-repeat: no-repeat; background-position: center;
    background-size: 12px 12px; image-rendering: pixelated;
  }
  #mproto-season-bar .mproto-speed-ctl.on { color: var(--gold, #e0c060); border-color: var(--gold-dim, #6a5a2e); }
  #mproto-season-bar .mproto-speed-ctl.dim { opacity: 0.55; }

  /* ── Settings sheet ───────────────────────────────────────────────────── */
  #mproto-settings-sheet {
    position: fixed; inset: 0; z-index: 4300; display: none;
    flex-direction: column; background: var(--deep, #12101a);
  }
  body.mproto-settings-open #mproto-settings-sheet { display: flex; }
  #mproto-settings-head {
    display: flex; align-items: center; justify-content: space-between;
    padding: calc(env(safe-area-inset-top, 0px) + 10px) 12px 10px;
    border-bottom: 1px solid var(--border-dim, #23202f);
    font-weight: 700; letter-spacing: 1px; color: var(--bone, #e8dcc0);
  }
  #mproto-settings-body {
    overflow-y: auto; -webkit-overflow-scrolling: touch;
    padding: 14px 14px calc(env(safe-area-inset-bottom, 0px) + 20px);
    display: flex; flex-direction: column; gap: 16px;
  }
  .mproto-set-group { display: flex; flex-direction: column; gap: 10px; }
  .mproto-set-title { font-size: 13px; letter-spacing: 1px; color: var(--muted, #8a86a0); text-transform: uppercase; }
  #mproto-settings-body .hud-cb-label { display: flex !important; align-items: center; gap: 10px; font-size: 17px; }
  #mproto-settings-body .hud-btn { display: block; width: 100%; padding: 12px; font-size: 17px; text-align: left; }
  .mproto-close-x {
    background: none; border: 1px solid var(--border, #3a3550);
    color: var(--bone, #e8dcc0); width: 34px; height: 34px;
    border-radius: 6px; font-size: 23px; line-height: 1; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
  }

  /* ── Building placement banner ────────────────────────────────────────── */
  /* Pinned to the top of the MAP, not the top of the SCREEN. It used to sit at the viewport edge,
     which is where the resources bar and the info bar live, so the one moment you most need to read
     your wood and stone was the one moment the banner covered them. There is no fixed HUD height to
     subtract — #top-bar wraps to as many rows as the resources need — so mobile_ui.js measures the
     canvas each poll and publishes the edge as --mmap-top. The safe-area inset is already inside the
     top bar's own padding, so it must not be added a second time here. */
  /* It belongs to the MAP, so it sits in the map's own chrome band (3850ish) and BELOW the sheets
     (3900) and the sheet head (4100). At 4400 it floated over the Jobs and Souls sheets: placing a
     building does not close them, so opening a tab mid-placement left a banner with a cancel cross
     hanging over a panel it has nothing to do with. Being behind them is also self-correcting — a
     sheet is opaque and full screen, so the banner is simply not on screen while one is up, and it
     is there again the moment you close it and can actually see the ground you are placing on. */
  #mproto-place-banner {
    position: fixed; left: 8px; right: 8px;
    top: calc(var(--mmap-top, 60px) + 6px);
    z-index: 3860; display: none; align-items: center; gap: 8px;
    padding: 8px 10px; border-radius: 9px;
    background: rgba(20,17,30,0.96); border: 1px solid var(--gold, #6a5a2e);
    color: var(--bone, #e8dcc0); font-size: 15.5px;
    box-shadow: 0 3px 14px rgba(0,0,0,.55);
  }
  body.mproto-placing #mproto-place-banner { display: flex; }
  #mproto-place-banner .mpb-text { flex: 1; line-height: 1.25; min-width: 0; }
  #mproto-place-banner .mpb-title { font-weight: 700; }
  /* WHAT THE BUILDING DOES, on its own line under the gesture hint. Its own line rather than
     appended to the first, because the name plus the hint plus a description is three phrases and
     they wrap unpredictably against a 320 screen; stacked, each half owns a whole line and the
     block is a fixed two rows whatever the building. The green matches the same sentence on the
     desktop build card (#a8d488), so it reads as the same fact in both places.
     IT WRAPS RATHER THAN CLIPPING, and the budget is TWO rows at 320, not one. Measured: the text
     column there is 244px and VT323 averages ~8.7px a glyph, so a row holds about 28 characters —
     less than "+1 smelter slot (stone into metal)". Squeezing every description under 28 would cost
     exactly the parentheticals that make them worth reading. An ellipsis was the other option and is
     the one this file already rules out: it leaves the string whole in the DOM and just makes the
     box too short. So the description owns up to two rows, balance_tests holds it to that, and
     giving up the standing gesture line above paid for both of them and then some. */
  #mproto-place-banner .mpb-desc {
    display: block; color: #a8d488; font-size: 14.5px; margin-top: 1px;
  }
  #mproto-place-banner .mpb-fert { font-weight: 700; }
  #mproto-place-banner .mpb-cancel {
    flex: 0 0 auto; background: none; border: 1px solid var(--border, #3a3550);
    color: var(--bone, #e8dcc0); width: 30px; height: 30px; border-radius: 6px; font-size: 20.5px;
  }
  /* While placing, the build sheet is closed and taps go to the map. */
  body.mproto-placing #mproto-build-fab { display: none !important; }

  /* ── "Saved." belongs to the MAP, not to the bar it was covering ───────────
     Desktop pins it at top:48px left:278px, which is under that build's HUD and clear of it. Here
     #top-bar wraps to as many rows as the resource chips need and there is no fixed height to
     subtract, so a viewport-relative offset lands somewhere different on every phone and on this
     one it landed on the chips — the reading you most want intact is the one a save notice sat on.
     Same cure as the placement banner, the boss strip and the toast lane: take the edge from
     --mmap-top, the live top of the canvas that mobile_ui.js stamps each poll, and sit in the
     map's own chrome band (3850) rather than the HUD's (4000). Top RIGHT, because the left of that
     band is the toast lane and the Build chip's reserved column.
     Every value is !important: the desktop rule is an inline <style> in the document head, which
     comes after this file's <link>, so at equal specificity it would win on all four properties. */
  #save-indicator {
    top: calc(var(--mmap-top, 56px) + 6px) !important;
    left: auto !important;
    right: 10px !important;
    z-index: 3850 !important;
  }

  /* ── No expand button on the research tree ─────────────────────────────────
     #research-expand-btn lifts the tree out of the sidebar into a full-screen overlay. Every panel
     on this build is ALREADY a full-screen sheet, so the control's whole promise is something the
     player has by default, and pressing it can only rearrange a view that was not cramped. It is
     display:none rather than removed because index.html is generated from the desktop source and
     _rtreeToggleExpand still writes the glyph back onto the node when it exists. */
  #research-expand-btn { display: none !important; }

  /* ── Road blueprint bar: the cost gets a LINE, not an ellipsis ─────────────
     #road-blueprint-bar is injected by ui.js (ensureRoadBlueprintBar) with its own <style> in the
     head, for both builds at once, and #rbb-msg there is nowrap + ellipsis. That is a desktop
     assumption: measured with a Range against the content box, "Paved Road · 148 tiles · 14800
     stone, 7400 food" is 395px of text in a 210px box at 390 wide, 181 at 360 and 144 at 320, and
     the longest line the bar can produce ("⚠ … — tap the map to redraw") is 499px. So on every
     phone, at every width, the cost was cut off — which is the whole point of the bar.

     The message takes the bar's FULL width on its own line and the two buttons drop below it,
     rather than the message merely wrapping in the ~180px the buttons leave: at that width the
     same string needs three lines, and at full width it needs two. Everything here is !important
     because the injected <style> is appended to the head AFTER this file's <link>, so at equal
     specificity it would otherwise win.

     The bar also has to be told its WIDTH. It is position:fixed with no width, so it is shrink to
     fit, and a flex item with `flex-basis: 100%` contributes almost nothing to a shrink to fit
     max-content size: measured, the bar collapsed to 160px and the same sentence came out over five
     lines. `max-width: 94vw` was never a width, only a ceiling. */
  #road-blueprint-bar { width: 94vw !important; flex-wrap: wrap !important; row-gap: 8px !important; }
  #rbb-msg {
    flex: 1 1 100% !important;
    white-space: normal !important;
    overflow: visible !important;
    text-overflow: clip !important;
    line-height: 1.3 !important;
    /* No clamp. A clamp is an ellipsis wearing a different hat: the string stays whole in the DOM
       and the box is simply too short, which is exactly the failure being fixed. */
  }
  /* The two-line budget above was measured at the bar's ORIGINAL 14px. The VT323 compensation pass
     took the injected style to 17px, which is fine at 390 and spills the longest string (499px at
     14px) onto a third line at 360 and 320. Held at the measured size on those widths only, so the
     wider phones keep the larger type. Third instance of the same rule in this file: where a budget
     was measured against a type size, the type size is part of the budget. */
  @media (max-width: 389px) {
    #rbb-msg { font-size: 14px !important; }
  }
  /* Build sits at the right end of the second line, next to the ✕, so the pair read as one control
     group under the sentence they act on rather than as a stray button on the left. RAZE takes the
     same slot — it is the confirm for the other tool that draws two points on the map and shares
     this bar, so it must sit exactly where the hand already goes. */
  #rbb-build, #rbb-raze { margin-left: auto !important; }
  /* The construction readout is pinned bottom-right and the bar is centred and near full width, so
     the two share the same band — and now that the bar is two lines tall they genuinely overlap.
     Same call as the sheets make about it (see #bottom-build-progress below): a road you are
     actively drawing is what you opened this for, and the progress line is still there afterwards. */
  body.road-mode-active #bottom-build-progress { display: none !important; }

  /* ── Combat: placement thumb-buttons (reposition the real elements) ────── */
  /* Hide the city Build button whenever we're in a battle flow. */
  body[data-mproto-state="ATTACK_WARNING"] #mproto-build-fab,
  body[data-mproto-state="PLACEMENT"] #mproto-build-fab,
  body[data-mproto-state="COMBAT"] #mproto-build-fab,
  body[data-mproto-state="VICTORY"] #mproto-build-fab,
  body[data-mproto-state="DEFEAT"] #mproto-build-fab { display: none !important; }

  /* ── ONE row: Fortifications, Squads, Hold the Pass ─────────────────────
     The BAR ITSELF is the flex row. Everything else it contains is display:none on this build, so
     only the three buttons and the stack wrapper are laid out — and the wrapper is display:contents,
     which lifts its two buttons up to be siblings of Hold the Pass inside this row.
     This replaces two earlier shapes, both of which pinned each button by id with position:fixed:
       * Squads left / Fortifications SCREEN CENTRE / Hold the Pass right — Fortifications overlapped
         Hold the Pass by 21px on a 390px phone, and no cap fits a 138px label 38px from the centre.
       * the same two on their own row ABOVE Hold the Pass — no overlap, but two rows of chrome.
     A flex row needs no per-button magic, and `space-between` covers BOTH shapes with one
     declaration: with Fortifications unlocked it puts Squads left, Fortifications in the middle and
     Hold the Pass right; with it hidden (body.mproto-no-forts) the two that remain go to the two
     ends, which is what they should do anyway. */
  #placement-bar:not(.hidden) {
    display: flex !important; flex-direction: row; align-items: center;
    gap: 7px; justify-content: space-between;
    position: fixed; left: 10px; right: 10px;
    bottom: calc(var(--mnav-h) + var(--mseason-h) + env(safe-area-inset-bottom, 0px) + 10px);
    top: auto; width: auto; height: auto; min-height: 0; padding: 0; margin: 0;
    background: none; border: none; box-shadow: none;
    z-index: 3850; pointer-events: none;   /* the gaps between them must not eat map taps */
  }
  #placement-bar:not(.hidden) #placement-units,
  #placement-bar:not(.hidden) .pb-label-2l,
  #placement-bar:not(.hidden) #placement-hint,
  #placement-bar:not(.hidden) #structures-hint { display: none !important; }

  #placement-bar:not(.hidden) #placement-placed {
    position: fixed; left: 50%; transform: translateX(-50%);
    bottom: calc(var(--mnav-h) + var(--mseason-h) + env(safe-area-inset-bottom, 0px) + 58px);
    z-index: 3850; font-size: 14.5px; color: var(--bone, #e8dcc0);
    background: rgba(12,11,18,.82); padding: 3px 12px; border-radius: 11px; white-space: nowrap;
  }

  /* display:contents so the wrapper lays out nothing of its own and its two buttons become direct
     flex items of the bar, alongside Hold the Pass. On the DESKTOP bar this same wrapper is a flex
     COLUMN, which is why its children carry `.pb-board { width: 100% }` — see the width undo below. */
  #placement-bar:not(.hidden) #pb-board-stack { display: contents !important; }
  #placement-bar:not(.hidden) #squads-mode-btn,
  #placement-bar:not(.hidden) #structures-mode-btn,
  #placement-bar:not(.hidden) #begin-battle-btn {
    /* RELATIVE, never static. Hold the Pass carries its "click me next" glint as an ::after that is
       absolutely positioned and swept across the button — which only stays ON the button while the
       button is its containing block. Setting these static handed that job to the nearest positioned
       ancestor, which is now the bar itself, so the sweep ran across all three buttons at once.
       (`overflow: hidden` below is the other half of clipping it, and is why the desktop rule sets
       both together.) */
    position: relative !important; pointer-events: auto;
    /* !important on the three that decide the box, because the desktop `.pb-board` rule sets
       font-size / padding / line-height with !important of its own (for the stacked column it makes
       there) and would otherwise win over these no matter how specific the selector is. Without it
       the two board buttons kept 13px type and 2px padding while Hold the Pass took 12px and 7px, so
       the row was three different heights and the narrow-screen tightening below reached only one of
       them. */
    font-family: inherit; font-weight: 700; letter-spacing: .5px;
    font-size: 14.5px !important; padding: 7px 11px !important; line-height: 1.15 !important;
    border-radius: 20px; cursor: pointer; margin: 0 !important;
    box-shadow: 0 3px 12px rgba(0,0,0,.5); animation: mproto-pop .22s ease;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    /* Undo `.pb-board { width: 100% }`, which makes the pair read as one COLUMN on the desktop bar.
       Width resolves against the containing block, and while these were pinned position:fixed that
       meant the VIEWPORT — Squads came out a full screen wide and sat on top of Hold the Pass. */
    width: auto !important;
    /* Shrink before overflowing. min-width lets a flex item go below its content size, which is what
       makes the ellipsis reachable at all; without it the row would simply run off a narrow phone. */
    flex: 0 1 auto; min-width: 3.4em;
  }
  /* Left to right: Squads, Fortifications, Hold the Pass. The DOM has the two board buttons the other
     way round (the desktop stack reads Fortifications ABOVE Squads), so the row is ordered here
     rather than by moving them — combat.js binds its handlers to those elements and the desktop
     column wants its own order. */
  #placement-bar:not(.hidden) #squads-mode-btn     { order: 1; }
  #placement-bar:not(.hidden) #structures-mode-btn { order: 2; }
  /* Hold the Pass gives up its width LAST — it is the one button that ends the screen, and
     "Hold the Pa…" is worse than a shortened Fortifications. */
  #placement-bar:not(.hidden) #begin-battle-btn    { order: 3; flex-shrink: 0; }
  /* Measured, not guessed: the three at full size need ~360px of the 370 a 390px phone leaves, so at
     390 and up they all fit whole. Below that the row would start eating labels ("Fortificat…",
     "Squa…"), so the type and padding tighten just enough to keep all three words intact — 320px, the
     narrowest phone worth supporting, is the case this is sized for. */
  @media (max-width: 389px) {
    #placement-bar:not(.hidden) { gap: 5px; left: 7px; right: 7px; }
    #placement-bar:not(.hidden) #squads-mode-btn,
    #placement-bar:not(.hidden) #structures-mode-btn,
    #placement-bar:not(.hidden) #begin-battle-btn {
      font-size: 12.5px !important; padding: 7px 8px !important; letter-spacing: 0;
    }
  }
  @media (max-width: 340px) {
    #placement-bar:not(.hidden) #squads-mode-btn,
    #placement-bar:not(.hidden) #structures-mode-btn,
    #placement-bar:not(.hidden) #begin-battle-btn {
      font-size: 11.5px !important; padding: 7px 6px !important;
    }
  }
  #placement-bar:not(.hidden) #squads-mode-btn {
    background: var(--panel, #211c30); border: 1px solid #4a6a8a; color: #b8ccdd;
  }
  #placement-bar:not(.hidden) #structures-mode-btn {
    background: var(--panel, #211c30); border: 1px solid var(--gold, #6a5a2e); color: #e8dcc0;
  }
  @keyframes mproto-pop { from { transform: scale(.6); opacity: 0; } to { opacity: 1; } }

  /* ── The onboarding spark has to win here ────────────────────────────────
     The block above sets `animation: mproto-pop` and a box-shadow on these three ids at specificity
     (2,1,0). `.onb-spark` (css/onboarding.css) is (0,1,0), so on the touch build it lost BOTH
     properties and the pulse never ran — the one build where a beat saying "open the Squads panel"
     most needs to point at the button. It worked on desktop only because desktop sets no animation
     on them at all.
     Restated at matching specificity, per id, so the entry pop still plays on the unsparked case and
     a sparked button pulses instead. The keyframes themselves stay in css/onboarding.css (the mobile
     build does load it), so there is still ONE definition of what a spark looks like. */
  #placement-bar:not(.hidden) #squads-mode-btn.onb-spark,
  #placement-bar:not(.hidden) #structures-mode-btn.onb-spark,
  #placement-bar:not(.hidden) #begin-battle-btn.onb-spark {
    animation: onb-spark 1.1s ease-in-out infinite;
  }
  /* And the same for anyone who asked the OS for less motion: still clearly marked, just not moving.
     The box-shadow needs restating too — here it is a plain declaration competing with the base
     rule's own, not an animated value that would outrank it. */
  @media (prefers-reduced-motion: reduce) {
    #placement-bar:not(.hidden) #squads-mode-btn.onb-spark,
    #placement-bar:not(.hidden) #structures-mode-btn.onb-spark,
    #placement-bar:not(.hidden) #begin-battle-btn.onb-spark {
      animation: none;
      box-shadow: 0 0 0 2px rgba(255,224,140,0.9), 0 0 12px 2px rgba(232,196,96,0.5);
    }
  }
  /* structures button drops out when there is no fortification effort available */
  body.mproto-no-forts #placement-bar:not(.hidden) #structures-mode-btn { display: none !important; }

  /* ── Squad / structures / combat panels → bottom sheets ───────────────── */
  #squad-panel:not(.hidden),
  #structures-panel:not(.hidden) {
    position: fixed !important; left: 0 !important; right: 0 !important;
    top: auto !important; bottom: 0 !important;
    transform: none !important; margin: 0 !important;   /* reset desktop translateX(-50%) */
    width: auto !important;
    z-index: 4360 !important; overflow-y: auto !important; -webkit-overflow-scrolling: touch;
    background: var(--deep, #12101a) !important;
    border-top: 2px solid var(--gold, #6a5a2e) !important;
    /* ── THE SHEET IS A LID OVER THE DEPLOYMENT GRID, SO ITS HEIGHT IS A COST ────────────────
       It is fixed to the bottom of the screen, and the bottom rows of the placement grid are
       underneath it — so every pixel this sheet is tall is a row the player cannot deploy in.
       Measured at 390x844 it was 259px, 31% of the screen, and 44 of those were EMPTY: a top
       padding reserving a whole line for the floating ✕, which is absolutely positioned and needs
       no line of its own. The ✕ keeps its corner and the first row of tabs gives up the width
       under it instead (see .squad-tabs below).
       74vh was never a real ceiling either — it is 624px here, more than the sheet has ever
       needed. Half the screen is the honest cap, and past it the sheet SCROLLS (overflow-y is
       already auto) rather than pushing further over the field. */
    max-height: 50vh !important;
    padding: 8px 10px calc(env(safe-area-inset-bottom, 0px) + 10px) !important;
    /* The desktop board is a floating panel with `min-width: 380px` under it, and left/right: 0
       cannot beat a min-width — the sheet simply became 380 wide inside a 320 phone and everything
       past the fold went off the right edge. Measured at 320: panel 380, with "+ Empty", "3Sh" and
       "2K" ending at 335, 331 and 365. A stretched sheet has no use for a floor at all. */
    min-width: 0 !important;
  }
  /* ── Fortifications sheet ─────────────────────────────────────────────────
     Same bottom-sheet shell as the squad board, laid out in three bands rather than one undivided
     run of identical buttons: a header (Effort left + what a tap does), the DEFENCES you can place
     two to a row and thumb-sized, and the ACTIONS (save / load / reset / rotate) smaller and four
     across, so a loadout button never reads like another wall to lay. The selected defence is
     unmistakable, because on touch there is no hover to fall back on. */
  #structures-panel:not(.hidden) {
    display: flex !important; flex-wrap: wrap !important; gap: 8px !important; align-content: flex-start;
  }
  /* The desktop "Defences:" caption is replaced by the injected header. */
  #structures-panel:not(.hidden) > .place-label { display: none !important; }
  #mproto-struct-head {
    flex: 1 1 100%; display: flex; justify-content: space-between; align-items: baseline;
    gap: 10px; padding: 0 2px 4px; margin: 0;
    border-bottom: 1px solid var(--border, #2a2740);
    font-size: 14.5px; line-height: 1.3; color: var(--muted, #8a86a0);
  }
  #mproto-struct-effort { color: var(--soul, #9060c0); font-weight: 700; font-size: 15.5px; white-space: nowrap; }
  #mproto-struct-hint { flex: 1; text-align: right; }

  #structures-panel:not(.hidden) .struct-btn[data-struct] {
    flex: 1 1 44% !important; padding: 14px 6px !important; font-size: 15.5px !important;
    border-radius: 8px;
  }
  /* combat.js already stamps .active on the selected one (updatePlacementInfo). */
  #structures-panel:not(.hidden) .struct-btn[data-struct].active {
    background: linear-gradient(180deg, #f4d878, #c89a36) !important;
    border-color: #5a4410 !important; color: #2a1e06 !important; font-weight: 700;
    box-shadow: 0 0 0 1px #5a4410, 0 2px 8px rgba(200,154,54,.35);
  }
  /* Zero-height full-width item (injected by mobile_ui) that forces the actions onto their own row
     instead of letting Save flow up beside the last defence tile. */
  #mproto-struct-break { flex: 1 1 100%; height: 0; margin: 2px 0 0; border-top: 1px solid var(--border, #2a2740); }
  #structures-panel:not(.hidden) .struct-btn:not([data-struct]) {
    flex: 1 1 20% !important; padding: 9px 3px !important; font-size: 13px !important;
    border-radius: 6px; opacity: .9;
  }
  /* The sheet already has the shared floating X; a second Close inside it is just a wider row. */
  #structures-panel:not(.hidden) #close-structures-btn { display: none !important; }

  /* ── The squad board on glass ─────────────────────────────────────────────
     Everything here was mouse sized: the tabs, the unit badges and the editor row all measured
     27 to 28px tall on a 320 phone, against the 32px this build uses as its fingertip floor
     everywhere else. They are also the controls a player presses most in the one screen that
     decides a battle. Raised by PADDING rather than a fixed height, so a wrapped label still
     grows the button instead of spilling out of it.

     The unit badges keep their own smaller type — they carry two characters ("3Sh") and a row of
     nine of them has to fit — but they take the same vertical size as the rest. */
  #squad-panel:not(.hidden) .squad-tab,
  #squad-panel:not(.hidden) .squad-editor button,
  #squad-panel:not(.hidden) .squad-editor select,
  #squad-panel:not(.hidden) .squad-editor input {
    padding-top: 7px !important; padding-bottom: 7px !important;
    font-size: 16px !important; min-height: 32px; box-sizing: border-box;
  }
  #squad-panel:not(.hidden) .squad-unit-btn {
    padding: 7px 7px !important; font-size: 15.5px !important;
    min-width: 34px; min-height: 32px; box-sizing: border-box;
  }
  /* The name field is a text input, not a button: it must not eat the row's width on a phone. */
  #squad-panel:not(.hidden) .squad-editor input[type="text"] { width: 74px !important; }
  /* Both rows wrap on a phone, so give the wrapped lines a gap that reads as rows rather than a
     single dense block. Tight, because this sheet's height is a cost (see the panel rule above). */
  #squad-panel:not(.hidden) .squad-tabs,
  #squad-panel:not(.hidden) .squad-editor { gap: 5px !important; row-gap: 5px !important; }
  /* The floating ✕ sits in this row's top-right corner rather than on a line of its own, so the
     first row of tabs gives up the width under it. Only the FIRST row: the reserve is a padding on
     the flex container, so wrapped rows below still use the full width. */
  #squad-panel:not(.hidden) .squad-tabs { padding-right: 34px; }
  /* A squad tab carries its composition after its name ("Army 6W 4B 3Sh 2K"), which is fine at two
     unit types and 180px wide at nine — enough to push the tab row onto a second and third line.
     Cap it and let the tail ellipsis: the NAME is what you pick a tab by, and the full composition
     is on the editor row directly below as badges you can actually press. */
  #squad-panel:not(.hidden) .squad-tab { max-width: 48%; }
  #squad-panel:not(.hidden) .squad-tab > span:last-child {
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  #squad-panel:not(.hidden) .squad-editor { margin-top: 5px; padding-top: 5px; }
  /* ── Every pixel of WIDTH on this row is a pixel of HEIGHT ──────────────────
     The row wraps, so a control 20px too wide costs a whole 37px line of sheet — and that line is
     a line of deployment grid. Measured at 390: the controls came to 412 against 366 of room, so
     it always sat on two. These three trims are worth ~65 and put an early roster on one line;
     a full nine-type roster still wraps, which is correct, and the sheet scrolls past its cap. */
  #squad-panel:not(.hidden) .squad-editor .sq-formation-select { max-width: 88px; }
  #squad-panel:not(.hidden) .squad-unit-btn { min-width: 30px; padding: 7px 4px !important; }
  #squad-panel:not(.hidden) .squad-editor button { padding-left: 6px !important; padding-right: 6px !important; }
  /* The add/take toggle is an ELEVENTH thing on a row that a nine-type roster already filled to
     339.7 of 366. It carries one glyph rather than a count and an abbreviation, so it honestly needs
     less width than a badge, and one pixel off each gap pays for the rest: 372.7 -> ~356, which
     keeps the full roster on the line it was on and costs the deployment grid nothing. */
  #squad-panel:not(.hidden) .sq-dir-btn { min-width: 22px; padding: 7px 2px !important; }
  #squad-panel:not(.hidden) .squad-unit-btns { gap: 2px; }
  /* The footer is ONE line. Both halves are live feedback ("All units assigned", "That spot is
     taken") and short by construction now, so they share a row and ellipsis rather than wrapping
     into a second — which on this sheet is another row of field the player cannot reach. */
  #squad-panel:not(.hidden) .squad-footer {
    margin-top: 5px !important; gap: 8px !important;
    flex-wrap: nowrap !important;
    font-size: 14px; line-height: 1.25;
  }
  #squad-panel:not(.hidden) #squad-unassigned,
  #squad-panel:not(.hidden) #squad-hint {
    font-size: 14px !important;
    min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .mproto-panel-x {
    position: absolute; top: 8px; right: 8px; z-index: 2;
  }

  #combat-bar:not(.hidden) {
    position: fixed !important; left: 0; right: 0;
    bottom: calc(var(--mnav-h) + var(--mseason-h) + env(safe-area-inset-bottom, 0px)) !important;
    z-index: 3850 !important; display: flex !important; flex-wrap: wrap; gap: 6px; justify-content: center;
    padding: 7px 6px !important; background: rgba(12,11,18,.9) !important; border-top: 1px solid var(--border, #2a2740) !important;
  }
  #combat-bar:not(.hidden) #combat-status { width: 100%; text-align: center; font-size: 14.5px !important; margin: 0 !important; }
  /* FOUR ORDERS, ONE ROW. They wrapped three-and-one, which reads as though the fourth belongs to
     something else — and a wrapped row is also taller, so the bar ate another strip of map during
     the one state where the map matters most. The row no longer wraps at all: the buttons share it
     equally (flex 1 1 0) and give up their side padding instead, and the label is allowed to
     shrink because the drawn mark carries the meaning on its own.
     Measured budget at 320px, the narrowest phone: 308 usable, 12 of padding, 3 gaps of 5 = 281
     across four buttons, 70 each. "Advance" is the longest label. */
  #combat-bar:not(.hidden) #combat-global-controls {
    margin: 0 !important; display: flex; gap: 5px; flex-wrap: nowrap;
    justify-content: center; width: 100%; min-width: 0;
  }
  #combat-bar:not(.hidden) .combat-global-btn {
    font-size: 14.5px !important; padding: 9px 4px !important;
    flex: 1 1 0; min-width: 0; justify-content: center; gap: 4px;
  }
  #combat-bar:not(.hidden) .combat-global-btn .cgb-t {
    overflow: hidden; text-overflow: clip; white-space: nowrap;
  }
  @media (max-width: 360px) {
    #combat-bar:not(.hidden) .combat-global-btn { font-size: 13px !important; padding: 9px 2px !important; gap: 3px; }
  }

  /* ── Building popup (tap a building; reuses the desktop hover-tooltip content) ── */
  /* Hover tooltips don't fit touch and floated over the map mid-placement — hide them.
     Their innerHTML is still built on mousemove, which the popup borrows. */
  #building-tooltip, #tooltip { display: none !important; }

  /* The bulk chip, docked at the top of whichever sheet uses it (see placeBulkChip in mobile_ui.js).
     Sticky so it stays visible while the panel under it scrolls — the Soul Forge and the Cairn are
     long lists, and a mode you cannot see while you press Buy is the bug this move exists to fix. */
  #mproto-bulk-host {
    position: sticky; top: 0; z-index: 5;
    display: flex; align-items: center; gap: 8px;
    padding: 6px 2px 7px; margin-bottom: 4px;
    background: var(--panel, #14121c);
    border-bottom: 1px solid var(--border-dim, #2a2740);
  }
  /* When the chip IS the panel's top strip (jobs, cairn) it has to behave like the other two
     sticky heads: take the padding the panel gave up, and BLEED across the panel's side padding
     so rows scrolling under it are clipped instead of peeking past its ends. Scoped to a direct
     child of the panel, so the two re-parented seats below (the Souls reserve bar, the Smithy
     title) are untouched — there the chip is a control inside someone else's bar. */
  .tab-panel > #mproto-bulk-host {
    padding: 8px 12px 7px; margin: 0 -10px 4px;
  }
  #mproto-bulk-host .mbh-label {
    font-family: 'VT323','Courier New',monospace; font-size: 17px; color: var(--ghost, #a49cb8);
  }
  /* ── The bulk chip ────────────────────────────────────────────────────────
     Built as a twin of the speed chip in the season strip (.mproto-speed-ctl), because it is the
     same kind of control: a numeric mode you cycle by tapping. Same VT323 numeral, same #1b1826
     panel fill, same 1px border off --border-dim, same 6px radius, same bone text — so it reads as
     a fitting of this chrome rather than as something dropped into the sheet afterwards.

     It used to carry .mproto-auto-tgl, whose rules are all scoped under #mproto-auto-toggles. The
     chip has not lived in that bar since it moved into the sheets, so NONE of them applied: it was
     painting as a bare browser button, with only the width and padding below reaching it. */
  #mproto-bulk-host #mproto-bulk-tgl {
    margin-left: auto; min-width: 58px;
    display: inline-flex; align-items: baseline; justify-content: center; gap: 1px;
    padding: 4px 11px; line-height: 1.1; cursor: pointer;
    font-family: 'VT323','Courier New',monospace;
    background: #1b1826; color: var(--bone, #e8dcc0);
    border: 1px solid var(--border-dim, #35314a); border-radius: 6px;
  }
  /* The numeral is what you read; the × and the % are punctuation on it. Typeset that way rather
     than as one flat string, which is what made '×10' and '10%' read as two unrelated widths. */
  #mproto-bulk-host #mproto-bulk-tgl .mbt-num { font-size: 20.5px; letter-spacing: .5px; }
  #mproto-bulk-host #mproto-bulk-tgl .mbt-sym { font-size: 15.5px; opacity: .62; }
  #mproto-bulk-host #mproto-bulk-tgl:active { filter: brightness(1.25); }
  /* GOLD when armed, not the green of the three automation chips: those are set-and-forget, this one
     changes what the very next tap does, and it must not read as one of them. */
  #mproto-bulk-host #mproto-bulk-tgl.on {
    background: #3a3016; border-color: var(--gold, #6a5a2e); color: #e8c46a;
  }
  #mproto-bulk-host #mproto-bulk-tgl.on .mbt-sym { opacity: .8; }
  /* In the SOULS sheet the host rides inside the sticky reserve bar, left of Sell (mobile_ui.js
     re-parents it there): two stacked sticky bars at top:0 fought for the same edge, and the chip
     scrolled the Sell button half out of reach. Inside the bar it sheds its own bar chrome and
     label; Sell gives up its auto margin so the pair sit together on the right. */
  .sf-sb-head #mproto-bulk-host {
    position: static; display: inline-flex;
    padding: 0; margin: 0 0 0 auto;
    background: none; border-bottom: none;
  }
  .sf-sb-head #mproto-bulk-host .mbh-label { display: none; }
  #tab-souls .sf-sb-head .sf-sb-sell { margin-left: 8px; }

  #mproto-bld-popup {
    position: fixed; z-index: 4500; display: none;
    /* Widened with the desktop tooltip, and for the same measured reason: the longest upgrade cost
       line the game can produce carries five resources and did not fit. The two panels show the
       SAME markup (loadBldInfo reads #building-tooltip's innerHTML) so they must not disagree about
       how much of it is readable. The viewport term is what keeps a 360px phone whole: the popup is
       centred on the tap and clamped to 8px margins, so it can never be wider than the screen. */
    min-width: 200px; max-width: min(360px, calc(100vw - 20px));
    /* The flavour and cost lines no longer clamp to two lines, so a tall building (the Smithy's long
       effect text, the Temple's quote) can outgrow a short phone. Scroll rather than run off. */
    max-height: calc(100vh - 96px); overflow-y: auto;
    background: rgba(18,16,26,0.98); border: 1px solid var(--gold, #6a5a2e);
    border-radius: 9px; padding: 10px 12px 12px;
    color: var(--bone, #e8dcc0); font-size: 15.5px;
    box-shadow: 0 4px 16px rgba(0,0,0,.6);
  }
  #mproto-bld-popup.show { display: block; }
  #mproto-bld-popup .mbp-x {
    position: absolute; top: 6px; right: 6px; z-index: 1;
    background: none; border: none; color: var(--muted, #8a86a0);
    font-size: 20.5px; line-height: 1; cursor: pointer; padding: 2px 4px;
  }
  #mproto-bld-popup .mbp-info { padding-right: 18px; line-height: 1.4; }
  #mproto-bld-popup .mbp-info > div:first-child { font-weight: 700; font-size: 17px; margin-bottom: 3px; }
  #mproto-bld-popup .mbp-info .tt-detail { margin: 3px 0; color: #cfc7dd; }
  #mproto-bld-popup .mbp-info .tt-upgrade { color: #c8a040; }
  #mproto-bld-popup .mbp-info .tt-up-block {
    margin-top: 7px; padding-top: 6px; border-top: 1px solid rgba(200,160,64,0.3);
  }
  /* "Click to upgrade" is the DESKTOP affordance, where a click on the building is the only way to
     do it. Here the popup carries a real Upgrade button, which says the same thing and can be
     pressed, so the line would be a second instruction contradicting the first. */
  #mproto-bld-popup .mbp-info .tt-click-hint { display: none; }
  #mproto-bld-popup .mbp-upgrade {
    width: 100%; margin-top: 10px; padding: 11px; border-radius: 6px; cursor: pointer;
    background: linear-gradient(180deg, #f4d878, #c89a36); border: 1px solid #5a4410;
    color: #2a1e06; font-family: inherit; font-weight: 700; font-size: 17px;
  }
  #mproto-bld-popup .mbp-upgrade.mbp-cant { background: #35314a; color: #8a86a0; border-color: #2a2740; cursor: default; }

  /* Shared fixed close button for the squad / structures bottom sheets */
  #mproto-panel-close { position: fixed; right: 10px; z-index: 4380; display: none; }

  /* The X actually hides the sheet, so the field + Hold-the-Pass button underneath become reachable. */
  #squad-panel.mproto-hide, #structures-panel.mproto-hide { display: none !important; }

  /* ── Smithy sheet: use the height it has ─────────────────────────────────
     The panel is laid out for a short desktop sidebar: a fixed 134px forge stage, tight rows, and a
     footer pushed to the bottom. On a phone sheet that is roughly twice as tall, everything bunched
     into the top third and ~300px sat empty between the MIGHT row and the STRIKE button. The forge
     stage now GROWS into that space (so the four paper-doll slots sit at the corners of a big anvil
     instead of a small one) and the rows around it breathe. */
  /* .sm2 is the real container — #tab-smithy holds exactly one child, so flexing the panel alone
     moved nothing. The column lives on .sm2 and the forge stage is what grows inside it.
     DO NOT set `display` on #tab-smithy here: `.tab-panel { display:none }` is what hides an inactive
     tab and `.tab-panel.active` already supplies the flex column, so a `display:flex !important` on
     the id beat the hide and left the Smithy painted permanently over every other tab. Guarded by
     balance_tests ("mobile.css never sets display on a tab-panel id"). */
  #tab-smithy .sm2 { display: flex !important; flex-direction: column; flex: 1 1 auto; min-height: 0;
                     overflow-y: auto; -webkit-overflow-scrolling: touch; }
  /* ── Why nothing here may shrink but the anvil ─────────────────────────────
     Every child of a flex column can shrink by default, and a box shrinking does NOT shrink the
     content inside it: the content simply spills out the bottom, over whatever comes next. That is
     the whole of the "things are in front of the bottom option" bug. On a 360x640 sheet the column
     asked for more height than it had, .sm2-top was shrunk to fit, and its last block (the GRADE and
     MIGHT rows and the variant buttons) painted straight over the gain readout and the STRIKE button
     — measured: MIGHT overlapped STRIKE by 244x38px and hit-tested to STRIKE, so pressing MIGHT
     pressed STRIKE instead. Nothing about that is visible in a size test, which is why the previous
     pass measured all-green.
     So: exactly ONE item in the column may give, the anvil stage, and it has a floor of its own
     (two rows of slot buttons). Everything else is flex: 0 0 auto. Past the anvil's floor there is
     nothing left to give and .sm2 scrolls instead — a scroll is reachable, an overlap is not. */
  #tab-smithy .sm2 > *, #tab-smithy .sm2-top > * { flex: 0 0 auto; }
  /* .sm2-bottom (the forge readout + STRIKE) is bottom-anchored, so ALL the slack landed between the
     two halves as one dead block. .sm2-top takes it instead and passes it down to the forge stage.
     min-height: min-content is the floor that turns an over-full column into a scroll: it stops
     .sm2-top being squeezed below the sum of its own children, which is what let them spill. */
  #tab-smithy .sm2-top {
    flex: 1 1 auto; display: flex; flex-direction: column; min-height: min-content;
  }
  /* The anvil grows, but only so far. Uncapped it took EVERY spare pixel of a phone sheet — measured
     at ~500px of anvil above a 35px MIGHT row — so the two controls the panel exists to drive were
     squeezed into the last inch above the STRIKE button. Capped, the anvil is still the biggest thing
     on the sheet and the slack goes to the controls instead (see the auto margin on .sm2-gmv).
     The floor is two 46px slot rows plus their 10px of inset, so the paper doll is never crushed. */
  #tab-smithy .sm2-forge {
    flex: 1 1 auto !important; height: auto !important;
    min-height: 104px; max-height: 320px; margin: 6px 10px !important;
  }
  #tab-smithy .sm2-anvil { height: 100% !important; object-fit: contain; }
  #tab-smithy .sm2-slot { width: 52px !important; height: 46px !important; }
  #tab-smithy .sm2-slot-ico { width: 46px !important; height: 40px !important; }
  #tab-smithy .sm2-stats { margin: 0 10px 8px !important; padding: 6px 4px !important; }
  #tab-smithy .sm2-row { padding: 5px 10px !important; }

  /* ── The bulk chip's home on this tab ─────────────────────────────────────
     Inside the title line, which costs no height at all. As a stray first child of #tab-smithy it
     both broke the "one child, and .sm2 is the flex column" contract above and spent a 42px band on
     a sheet that had none to give. See placeBulkChip in mobile_ui.js for why it must NOT re-arm to
     ×1 every time this panel repaints itself. */
  #tab-smithy .sm2-title {
    display: flex; align-items: center; justify-content: space-between;
    gap: 8px; padding: 4px 10px 4px !important;
  }
  #tab-smithy .sm2-title #mproto-bulk-host {
    position: static; flex: 0 0 auto;
    padding: 0; margin: 0; background: none; border-bottom: none;
  }
  #tab-smithy .sm2-title #mproto-bulk-host .mbh-label { font-size: 15.5px; }
  #tab-smithy .sm2-title #mproto-bulk-host #mproto-bulk-tgl { margin-left: 6px; }
  /* THE WORD "SMITHY" IS ALREADY ON THIS SCREEN — the sheet head above the panel says it, and here
     the title is a second copy of a label spending the width the stock strip needs. Dropped on
     touch only; the desktop sidebar has no sheet head and keeps it. That hands the strip the whole
     row minus the bulk chip, so all four goods fit on one line without the row growing. */
  #tab-smithy .sm2-title .sm2-title-t { display: none; }
  /* WRAP RATHER THAN CLIP. The four goods need 213px and a 320px phone gives the strip 150 once the
     bulk chip has its share, so at that width they take a second line — measured, and the panel
     scrolls past its cap anyway. At 390 they fit on one and nothing moves. Self-adjusting, so there
     is no breakpoint to get wrong: a fifth good or a bigger number simply wraps sooner. */
  #tab-smithy .sm2-title { align-items: center; }
  #tab-smithy .sm2-res { justify-content: flex-start; gap: 7px; font-size: 15px;
                         flex-wrap: wrap; row-gap: 2px; overflow: visible; }
  #tab-smithy .sm2-res .res-icon-sm { width: 12px; height: 12px; }

  /* ── Tap targets ──────────────────────────────────────────────────────────
     Everything below is a fingertip size, measured on a 390px phone. Before: the unit tabs were
     35×22, the variant buttons 40×15, the two mode rows 35 tall with 12px stars. Those are mouse
     sizes; on glass they are a lottery, and the mode rows are the whole of what the panel asks you
     to choose between. */
  #tab-smithy .sm2-utabs { padding: 2px 10px 8px !important; gap: 6px !important; flex-wrap: wrap; }
  #tab-smithy .sm2-utab {
    font-size: 21.5px !important; padding: 7px 0 !important;
    min-width: 48px; min-height: 40px; text-align: center;
  }
  /* Both mode rows carry a visible stone border, not just the selected one: an unselected row with a
     transparent border reads as a caption, and the player could not tell MIGHT was pressable at all. */
  /* The desktop puts the variant buttons in a narrow column beside the two mode rows. On a phone that
     column is 52px wide, which clips "Heavy" and stacks three 34px buttons down the right edge — the
     exact strip that ended up under the STRIKE button on a short sheet. They get their own full width
     row underneath instead: three real buttons, and the mode rows get the whole width back. */
  #tab-smithy .sm2-gmv {
    margin-top: auto; padding-top: 8px !important;
    flex-direction: column; align-items: stretch !important; gap: 6px !important;
  }
  #tab-smithy .sm2-gm { display: flex; flex-direction: column; gap: 6px; }
  #tab-smithy .sm2-moderow {
    padding: 9px 10px !important; min-height: 50px; margin: 0 6px !important;
    border-radius: 4px; border-color: #2f2942; background: #16131d;
  }
  #tab-smithy .sm2-moderow.modesel { border-color: #c8a040; background: rgba(200,160,64,0.12); }
  #tab-smithy .sm2-lbl { width: 58px !important; font-size: 19px; }
  #tab-smithy .sm2-gems { font-size: 26.5px !important; letter-spacing: 5px !important; }
  #tab-smithy .sm2-stars { font-size: 23px !important; letter-spacing: 2px !important; }
  #tab-smithy .sm2-vars {
    flex-direction: row !important; gap: 6px !important; justify-content: stretch;
    padding: 0 6px !important;
  }
  #tab-smithy .sm2-var {
    flex: 1 1 0; min-width: 0; font-size: 18px !important; padding: 6px 4px !important;
    min-height: 36px; text-align: center;
  }

  /* ── The gain readout ─────────────────────────────────────────────────────
     "Forge → Grade 1   +0.03 ATK" is the one line that says what the money buys, and it was 15px of
     dim green pressed against the right edge with three pixels of padding, directly under a block
     that overlapped it. It gets a stone of its own, above the STRIKE button, and the VALUE is the
     biggest thing on the line. flex-wrap so the value drops to a second line rather than being
     squeezed when a bulk preview names three stats. */
  #tab-smithy .sm2-gain {
    display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 10px;
    margin: 8px 10px 6px; padding: 7px 10px;
    background: #14111c; border: 1px solid #2f2942; border-radius: 4px;
    font-size: 20.5px; line-height: 1.25; color: #c8b888;
  }
  #tab-smithy .sm2-gainv {
    margin-left: auto; font-size: 23px; font-weight: bold; color: #a8e070;
  }
  /* The forge/strike block is the thing you press; keep it off the bottom nav. */
  #tab-smithy .sm2-strike { padding: 12px 8px !important; font-size: 19px !important; }

  /* ── Short sheets ─────────────────────────────────────────────────────────
     The comfortable column above measures 619px of content. The sheet is the viewport less about
     142px of chrome, so anything under an ~760px viewport cannot hold it, and both a 640px phone and
     a 390x844 phone whose browser bar has taken its share are under that. Rather than let it scroll,
     the whole column tightens by the ~50px it is short: the flavour note under the stat strip goes
     (the strip above it says the same thing in figures), the anvil floor comes down to the two rows
     of slot buttons, and every band gives a few pixels back. The breakpoint is set with room to
     spare, because the cost of being wrong on the tight side is a slightly bigger anvil and the cost
     of being wrong on the loose side is a scrollbar over the controls.
     Every tap target stays at a fingertip size: slots 44x40, mode rows 44, variants 34.

     THIS BLOCK IS EXEMPT FROM THE VT323 SIZE COMPENSATION, and the exemption is the point of the
     block. Everything else in the interface was scaled x1.20 to undo VT323's short x-height; doing
     it here too pushed the panel 14px past the sheet, and 32px after a forge, with STRIKE landing
     outside its own panel — the exact overlap the notes above exist to prevent. This block's job is
     to make a dense panel fit a short screen, so its numbers are a vertical BUDGET and not a
     readability choice, the same way the info bar row is a width budget. Phones taller than 800px
     never enter this branch and get the larger type. */
  @media (max-height: 800px) {
    #tab-smithy .sm2-title { font-size: 18px !important; padding: 2px 10px !important; }
    #tab-smithy .sm2-note { display: none !important; }
    #tab-smithy .sm2-stats { font-size: 13px !important; padding: 3px 4px !important; margin-bottom: 5px !important; }
    #tab-smithy .sm2-utabs { padding: 2px 10px 4px !important; }
    #tab-smithy .sm2-utab { font-size: 16px !important; padding: 3px 0 !important; min-height: 34px; }
    /* 4px inset + two 40px slots + 6px inset. Any less and the two rows of the paper doll meet. */
    #tab-smithy .sm2-forge { min-height: 90px; margin: 4px 10px !important; }
    #tab-smithy .sm2-slot { width: 44px !important; height: 40px !important; }
    #tab-smithy .sm2-slot-ico { width: 38px !important; height: 34px !important; }
    #tab-smithy .sm2-gmv { padding-top: 4px !important; gap: 4px !important; }
    #tab-smithy .sm2-gm { gap: 4px; }
    #tab-smithy .sm2-moderow { min-height: 44px; padding: 6px 10px !important; }
    /* The two COLUMN WRAPPERS carry the font-size every child without its own size inherits, so
       they were quietly re-inflating rows that have no entry in this block. Pinning them here is
       what closes the last few pixels, and it does it once instead of chasing each descendant. */
    #tab-smithy .sm2-top, #tab-smithy .sm2-bottom { font-size: 14px !important; }
    #tab-smithy .sm2-row { font-size: 15px !important; }
    /* .sm2-lbl sets the LINE HEIGHT of every mode row, so its 16 -> 19 was worth the last 3px of
       overflow on its own, and it was the only base smithy size with no entry in this block. */
    #tab-smithy .sm2-lbl { font-size: 16px !important; }
    #tab-smithy .sm2-title #mproto-bulk-host .mbh-label { font-size: 13px !important; }
    /* The bulk toggle lives INSIDE .sm2-title, so its type sets that row's height. Measured, this
       one number was the whole remaining 3px: the title went 32.69 -> 36.55 and nothing else moved. */
    #tab-smithy .sm2-title #mproto-bulk-tgl .mbt-num { font-size: 17px !important; }
    #tab-smithy .sm2-title #mproto-bulk-tgl .mbt-sym { font-size: 13px !important; }
    #tab-smithy .sm2-gems { font-size: 19px !important; letter-spacing: 4px !important; }
    #tab-smithy .sm2-stars { font-size: 17px !important; }
    #tab-smithy .sm2-var { min-height: 34px; font-size: 14px !important; }
    #tab-smithy .sm2-gain { margin: 4px 10px 2px; padding: 4px 8px; font-size: 16px; }
    #tab-smithy .sm2-gainv { font-size: 17px; }
    #tab-smithy .sm2-strike { padding: 7px 6px !important; font-size: 15px !important; }
    #tab-smithy .sm2-fbar { margin: 3px 10px 1px !important; }
    /* The bank's one line STAYS VISIBLE on the tight budget, unlike .sm2-note above. Hover never
       fires on touch, so on this build it is the only surface anywhere that states what the forge
       bar is worth — hiding it would take the number off the screen entirely rather than merely
       shortening it. Paid for in type and margin instead: 13px against 15, ~15px of row. */
    #tab-smithy .sm2-fnote { font-size: 13px !important; margin: 1px 10px 2px !important; }
  }

  /* Raze-on indicator (bottom-right, above the season strip + nav; JS toggles display) */
  #mproto-raze-btn {
    position: fixed; right: 12px;
    bottom: calc(var(--mnav-h) + var(--mseason-h) + env(safe-area-inset-bottom, 0px) + 10px);
    z-index: 3850; align-items: center; gap: 6px;
    padding: 11px 16px; border-radius: 24px; cursor: pointer;
    font-family: inherit; font-weight: 700; letter-spacing: .5px; font-size: 17px;
    background: #3a1c1c; border: 1px solid #a04040; color: #f0b0b0;
    box-shadow: 0 3px 12px rgba(0,0,0,.5);
  }
  body.mproto-overlay #mproto-raze-btn { display: none !important; }

  /* Mobile close X on the achievements overlay */
  .mproto-overlay-x {
    position: fixed; top: calc(env(safe-area-inset-top, 0px) + 8px); right: 10px;
    z-index: 2147483000;
  }

  /* ── Tutorial guides: a non-blocking sheet pinned to the bottom ──────────
     Desktop shows a centred modal that dims the screen and points a canvas arrow at its target. On a
     panned/zoomed phone that arrow lands nowhere and the modal drifts off-screen, trapping you ("can't
     tap wood, the tip keeps covering the map"). On mobile the backdrop goes click-through (the map
     stays usable, so a gated tip can still be completed), the panel is pinned above the nav, and the
     arrow + spotlight dims are hidden. Dismiss with the X (top-right of the panel) or Understood. */
  #guide-overlay:not(.hidden) { background: transparent !important; pointer-events: none !important; }
  #guide-overlay:not(.hidden) #guide-panel {
    position: fixed !important;
    left: 8px !important; right: 8px !important; top: auto !important;
    bottom: calc(var(--mnav-h) + var(--mseason-h) + env(safe-area-inset-bottom, 0px) + 10px) !important;
    width: auto !important; max-width: none !important; margin: 0 !important;
    /* Grow up to the FULL map height (top touches the resources bar) so a long tip needs no scroll;
       short tips stay content-sized. Only a tip longer than the whole map still scrolls. */
    max-height: calc(100vh - var(--mnav-h) - var(--mseason-h) - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 80px);
    overflow-y: auto; pointer-events: auto !important;
  }
  #guide-pointer-arrow, .guide-pointer-arrow, .guide-canvas-arrow,
  .guide-dim, .guide-spot-ring { display: none !important; }
  /* Guides sit ABOVE the bottom menu. #guide-overlay carries the base `.overlay` z-index (100),
     which is far BELOW the bottom chrome (season strip 3700 / fab 3800 / combat bars 3850 / nav
     4000), so the menu covered the tip. Lift it above the whole bottom-menu band (but below the
     full-screen job/defeat/victory overlays at 5000). */
  #guide-overlay:not(.hidden) { z-index: 4820 !important; }
  #tutorial-overlay { z-index: 4820 !important; }

  /* The interactive first-run tutorial (#tutorial-overlay, tutorial.js) positions itself at the
     forest/river on the canvas — meaningless once the map is panned. Pin it to the bottom too, and
     hide its arrow. (mobile_ui.js relabels its Skip and adds a close X.) */
  #tutorial-overlay {
    position: fixed !important;
    left: 8px !important; right: 8px !important; top: auto !important;
    bottom: calc(var(--mnav-h) + var(--mseason-h) + env(safe-area-inset-bottom, 0px) + 10px) !important;
    width: auto !important; max-width: none !important;
    transform: none !important;              /* cancel the centred translate(-50%,-50%) of the Start screen */
    /* Full map height so long first-run steps don't scroll (top touches the resources bar). */
    max-height: calc(100vh - var(--mnav-h) - var(--mseason-h) - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 80px);
    overflow-y: auto;
  }
  #tutorial-arrow { display: none !important; }
  /* Tutorial popups (guides + the first-run box) on mobile: no X — "Skip all tutorials" sits bottom-left,
     a "Next" button bottom-right advances instead. */
  #guide-close { display: none !important; }
  .mproto-skip-all { order: -1; margin-right: auto !important; }
  #tutorial-continue { display: none !important; }             /* replaced by the mobile Next button */
  #tutorial-btns #tutorial-skip { order: -1; margin-right: auto !important; }
  #mproto-tut-next {
    background: #c8a040; color: #1a141e; border: none; border-radius: 4px;
    padding: 6px 16px; cursor: pointer; font-family: inherit; font-size: 17px; font-weight: 700; letter-spacing: .5px;
  }

  /* Guide archive ("view tutorials back", opened from Settings) as a bottom sheet. */
  #guide-archive-overlay:not(.hidden) #guide-archive-panel {
    position: fixed !important; left: 8px !important; right: 8px !important;
    bottom: calc(var(--mnav-h) + env(safe-area-inset-bottom, 0px) + 10px) !important; top: auto !important;
    max-width: none !important; width: auto !important; margin: 0 !important;
    max-height: 74vh; overflow-y: auto;
  }

  /* ── Floating event messages (right side, just above the menu) ─────────────
     Replaces the hidden desktop event log. Each message fades in on the right, drifts down and fades
     out over ~10s; they pile up; no panel, no scrollback. mobile_ui.js feeds them off logMessage(). */
  /* THE BUILD BUTTON'S LANE IS RESERVED, NOT GUESSED. This column sits at the same bottom offset
     as #mproto-build-fab and used to run to `max-width: 82%`, which on a 390 phone is 320px and
     reaches back to x=70 — straight through a button whose right edge is around x=118. So the
     lowest toast, the one you are actually reading, had its first word under the Build chip:
     "Victory" arrived half hidden.
     --mfab-w is stamped from the FAB's own measured width (stampFabWidth in mobile_ui.js), the
     same pattern as --mmap-top, so a relabel or a type change cannot reopen this. The lane is held
     even while the FAB is hidden, deliberately: the alternative is every line reflowing the moment
     the button appears or goes. Lines wrap and stay right-aligned, which is what was asked for. */
  /* THE LANE IS THE MAP, AND NOTHING ABOVE IT. It was bottom-anchored with no top at all, so a
     tall stack grew straight up through the info bar and the boss strip — chrome the player reads
     numbers off. --mmap-top is the live top of the canvas (stampMapTop), the same source the
     placement banner and the boss bars use, so this lane can never disagree with where the map
     actually starts. overflow: hidden makes it a hard edge rather than a hope, and
     justify-content: flex-end keeps the line at the bottom of that lane, just above the menu,
     which is the stable spot the eye already knows. */
  #mproto-events {
    position: fixed; right: 10px;
    left: calc(12px + var(--mfab-w, 112px) + 12px);
    top: calc(var(--mmap-top, 56px) + 4px);
    bottom: calc(var(--mnav-h) + var(--mseason-h) + env(safe-area-inset-bottom, 0px) + 8px);
    z-index: 3600; display: flex; flex-direction: column;
    align-items: flex-end; justify-content: flex-end;
    overflow: hidden; pointer-events: none;
  }
  /* A REAL OUTLINE, NOT FOUR SHADOWS. VT323 is a thin pixel face and these lines are read against
     grass, water, sprites and fire, whatever the map happens to be doing underneath. Blurred
     shadows only darken what is behind the glyph; a painted stroke gives it an actual edge.
     paint-order puts the stroke UNDER the fill, without which the stroke eats the stem and a thin
     face turns to mush. The shadows stay behind it for depth on a busy background. */
  .mproto-event {
    max-width: 100%; padding: 1px 3px; text-align: right;
    color: var(--bone, #e8dcc0); font-size: 15px; font-weight: 700; line-height: 1.3;
    paint-order: stroke fill;
    -webkit-text-stroke: 2.5px rgba(0, 0, 0, 0.92);
    text-shadow: 0 0 4px #000, 0 1px 3px #000, 1px 0 3px #000, -1px 0 3px #000;
    animation: mproto-tick-in 0.30s ease forwards; will-change: opacity, transform;
  }
  .mproto-event-warning, .mproto-event-danger { color: #f2b0a0; }
  .mproto-event-success { color: #a8d868; }
  .mproto-event-sacrifice { color: #c79fda; }
  /* In on its own, out only when nothing is queued behind it — the ticker replaces rather than
     fades whenever there is a backlog, which is what keeps a ten line train to one line on screen.
     mproto-tick-out must match TOAST_FADE_MS in mobile_ui.js. */
  @keyframes mproto-tick-in {
    from { opacity: 0; transform: translateY(-6px); }
    to   { opacity: 1; transform: translateY(0); }
  }
  @keyframes mproto-tick-out {
    from { opacity: 1; transform: translateY(0); }
    to   { opacity: 0; transform: translateY(10px); }
  }
  .mproto-event.mproto-tick-out { animation: mproto-tick-out 0.6s ease forwards; }

  /* ── Boss health, pinned to the top of the SCREEN ─────────────────────────
     The canvas bars live in world space above the mobile viewport clamp, so the touch build never
     saw them (see updateBossBars in mobile_ui.js). This strip is their screen-fixed mirror.
     z 3750: above the season strip (3700) so nothing in the bottom band can be confused with it,
     and below the sheets (3900) and every overlay, because a boss's health is never the thing you
     opened a menu to read. JS sets `top` from the live bottom of the info bar. */
  /* ── THREE BOSSES MUST NOT COST A THIRD OF THE SCREEN ───────────────────────────────────────
     Measured before: 43px a row, 139px for three, on top of the info bar and the season strip —
     enough that a three-boss wave was reading as chrome rather than as a fight. It is a READOUT
     over the action, so it gets the smallest type in the chrome that is still a number you can
     read at a glance, and the rows sit close enough to read as one stack. */
  #mproto-boss-bars {
    position: fixed; left: 8px; right: 8px; top: 60px;
    z-index: 3750; display: none; flex-wrap: wrap; gap: 3px;
    pointer-events: none;
  }
  #mproto-boss-bars.show { display: flex; }
  /* One column up to three bosses; two columns past that, so six cost three rows and not six.
     `calc(50% - 3px)` is half the wrap minus the gap — flex-basis, not width, so a single odd
     row still fills the line. */
  .mbb-row { flex: 1 1 100%; }
  #mproto-boss-bars.cols2 .mbb-row { flex: 1 1 calc(50% - 3px); min-width: 0; }
  /* The row IS the bar: one line a boss, name inside it at the left, numerals at the right. */
  .mbb-bar {
    position: relative; height: 16px;
    background: rgba(12,11,18,.92); border: 1px solid #3a3550; border-radius: 3px;
    overflow: hidden;
  }
  .mbb-fill { position: absolute; left: 0; top: 0; bottom: 0; width: 100%; transition: width .15s linear; }
  .mbb-name {
    position: absolute; left: 5px; top: 0; bottom: 0; right: 48%;
    display: flex; align-items: center;
    font-size: 11px; font-weight: 700; letter-spacing: .3px; color: #f0e2b4;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    text-shadow: 0 0 3px #000, 0 1px 0 #000, 1px 0 0 #000, -1px 0 0 #000;
  }
  /* VT323 for the numerals, like every other number in this chrome. Centred over the fill, with a
     hard shadow so it stays readable against both the filled and the empty half of the bar. */
  /* Numerals RIGHT, so they never collide with the name however long it is (the name ellipsises
     into its own half). Centred was fine when the name had a line above; sharing one line it is not. */
  .mbb-num {
    position: absolute; right: 5px; top: 0; bottom: 0; display: flex; align-items: center;
    font-family: 'VT323','Courier New',monospace; font-size: 13px; line-height: 1;
    color: #e8e4f0; text-shadow: 0 0 3px #000, 0 1px 0 #000, 1px 0 0 #000, -1px 0 0 #000;
  }
  .mbb-tick { position: absolute; top: 0; bottom: 0; width: 1px; background: #c8a040; }
  .mbb-more {
    font-size: 11px; color: var(--muted, #8a86a0); text-align: right; padding-right: 2px;
  }
  /* It is a readout, so anything you deliberately opened wins over it. */
  body.mproto-overlay #mproto-boss-bars,
  body.mproto-sheet-open #mproto-boss-bars,
  body.mproto-build-open #mproto-boss-bars,
  body.mproto-settings-open #mproto-boss-bars { display: none !important; }

  /* ── Building-progress readout ("Raising: The Farm 45%") ──────────────────
     Pin it into the persistent bottom-menu band, ABOVE the build sheet (z 3900), so opening the
     build menu to raise ANOTHER building never hides the current construction's progress. It's part
     of the bottom menu, so it lives just above the season strip and stays out from under the sheet.
     Empty (nothing under construction) → no box. */
  #bottom-build-progress {
    position: fixed !important;
    left: auto; right: 10px; max-width: 64%;
    bottom: calc(var(--mnav-h) + var(--mseason-h) + env(safe-area-inset-bottom, 0px) + 6px) !important;
    z-index: 4010 !important;
    text-align: right; font-size: 14.5px !important;
    background: rgba(12,11,18,.92); border: 1px solid var(--border, #2a2740);
    border-radius: 6px; padding: 4px 10px; pointer-events: none;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  #bottom-build-progress:empty { display: none !important; }
  /* City-map only, like the Build button (so it never sits over the combat/deploy UI), and it's
     pinned to the RIGHT so it never covers the bottom-LEFT Build button. Hidden under overlays. */
  body.mproto-overlay #bottom-build-progress,
  body[data-mproto-state="ATTACK_WARNING"] #bottom-build-progress,
  body[data-mproto-state="PLACEMENT"] #bottom-build-progress,
  body[data-mproto-state="COMBAT"] #bottom-build-progress,
  body[data-mproto-state="VICTORY"] #bottom-build-progress,
  body[data-mproto-state="DEFEAT"] #bottom-build-progress { display: none !important; }
  /* ── …and out from over an OPEN SHEET ────────────────────────────────────
     z 4010 was chosen to clear the build sheet (3900) so raising a second building never hid the
     first one's progress. It clears every OTHER sheet too, which was not the intent: the readout
     floated over the Jobs list, the Soul Forge and the Cairn, and it is pinned bottom-right where
     those panels put their own controls. The hide list above covers overlays and the combat states
     but was never extended to the sheets. A sheet is something you deliberately opened; the progress
     line is waiting for you underneath it and will still be there when you close it.
     #mproto-events is the same argument, and the same omission. */
  body.mproto-sheet-open #bottom-build-progress,
  body.mproto-build-open #bottom-build-progress,
  body.mproto-settings-open #bottom-build-progress,
  body.mproto-sheet-open #mproto-events,
  body.mproto-build-open #mproto-events,
  body.mproto-settings-open #mproto-events { display: none !important; }

  /* ── Season progress bar (lives in the season strip; 4 seasons / 3 separators) ── */
  #mproto-season-progress {
    position: relative; flex: 1 1 auto; height: 12px; min-width: 40px;
    margin: 0 6px; border-radius: 3px; overflow: hidden;
    background: #26222f; border: 1px solid var(--border-dim, #23202f);
  }
  #mproto-season-progress .msp-fill {
    position: absolute; left: 0; top: 0; bottom: 0; width: 0%;
    background: linear-gradient(90deg, #5f8438, #8bbf4a);
    transition: width .25s linear;
  }
  #mproto-season-progress .msp-sep {
    position: absolute; top: 0; bottom: 0; width: 2px; margin-left: -1px;
    background: rgba(6,6,10,.7);
  }

  /* ── The job dropdown, in the game's own hand ─────────────────────────────
     A native <select> draws its OPEN list with the operating system, and no stylesheet reaches it —
     so the one control the Jobs panel is built around opened as a slab of Android chrome in the
     middle of the settlement. mobile_ui.js lays a transparent catcher over the select and opens the
     sheet below instead. The select itself stays exactly where it was, still styled by the game's own
     rules as the closed face, still the value holder syncPriorityFromDOM reads, and still a real box
     for the onboarding spark to point at. */
  .mprio-wrap { position: relative; flex: 1 1 auto; min-width: 0; display: flex; }
  /* appearance:none is what removes the BROWSER's own arrow. The select keeps its job as the value
     holder and the closed face, and .mprio-chev below draws the arrow in the game's gold — but
     without this the native chevron was still painted beside it, so the row carried two arrows,
     one of them a stock white triangle from the platform's widget set. Nothing was left over from
     an old icon: it was the control's own, and it had been there the whole time under the assumption
     that styling the box also styled the marker. It does not. */
  .mprio-wrap .prio-select {
    flex: 1 1 auto; min-width: 0; padding-right: 20px !important;
    -webkit-appearance: none; -moz-appearance: none; appearance: none;
    background-image: none;
  }
  .mprio-tap {
    position: absolute; inset: 0; z-index: 2;
    background: none; border: none; padding: 0 6px 0 0; margin: 0;
    display: flex; align-items: center; justify-content: flex-end;
    cursor: pointer; -webkit-tap-highlight-color: transparent;
  }
  .mprio-chev { font-family: 'VT323','Courier New',monospace; font-size: 18px; color: var(--gold, #c8a040); line-height: 1; }
  .mprio-tap:active + .prio-select, .mprio-wrap:active .prio-select { border-color: var(--gold, #c8a040); }

  #mproto-prio-sheet { position: fixed; inset: 0; z-index: 4850; display: none; }
  #mproto-prio-sheet.show { display: block; }
  #mproto-prio-sheet .mprio-scrim { position: absolute; inset: 0; background: rgba(6,5,10,0.62); }
  #mproto-prio-sheet .mprio-panel {
    position: absolute; left: 0; right: 0; bottom: 0;
    max-height: 72vh; display: flex; flex-direction: column;
    background: var(--panel, #14121c);
    border-top: 2px solid var(--gold-dim, #6a5a2e);
    box-shadow: 0 -6px 22px rgba(0,0,0,.6);
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
  #mproto-prio-sheet .mprio-head {
    display: flex; align-items: center; justify-content: space-between;
    padding: 10px 12px; border-bottom: 1px solid var(--border-dim, #23202f);
    font-family: 'VT323','Courier New',monospace; font-size: 24px;
    letter-spacing: 1px; color: var(--bone, #e8dcc0);
  }
  #mproto-prio-sheet .mprio-list { overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 6px 0 10px; }
  /* One row per trade, in the same pixel-bevelled stone as every other pressable thing in this
     chrome: inset light on the top left, hard shadow on the bottom right, no radius, VT323. */
  #mproto-prio-sheet .mprio-opt {
    display: flex; align-items: center; gap: 10px;
    margin: 4px 10px; padding: 11px 12px; min-height: 46px;
    font-family: 'VT323','Courier New',monospace; font-size: 23px; line-height: 1.1;
    color: var(--bone, #e8dcc0); background: #1c1a26;
    border: 1px solid #07060b;
    box-shadow: inset 1px 1px 0 rgba(255,255,255,0.06), inset -1px -1px 0 rgba(0,0,0,0.5), 0 2px 0 #07060b;
    cursor: pointer;
  }
  #mproto-prio-sheet .mprio-opt:active { transform: translateY(2px); box-shadow: inset 1px 1px 0 rgba(0,0,0,0.4); }
  #mproto-prio-sheet .mprio-opt.sel { color: #e8c46a; background: #241f12; border-color: var(--gold-dim, #6a5a2e); }
  #mproto-prio-sheet .mprio-opt.off { color: var(--ghost, #6f6a86); }
  #mproto-prio-sheet .mprio-mark { flex: 0 0 auto; color: var(--gold, #c8a040); font-size: 19px; }
  #mproto-prio-sheet .mprio-opt:not(.sel) .mprio-mark { color: #3a3550; }

  /* ── Hide ALL mobile chrome while a full-screen overlay is up (loading / AFK) ── */
  body.mproto-overlay #sidebar-tabs,
  body.mproto-overlay #sidebar-tab-area,
  body.mproto-overlay #left-sidebar,
  body.mproto-overlay #mproto-season-bar,
  body.mproto-overlay #mproto-build-fab,
  body.mproto-overlay #mproto-settings-btn,
  body.mproto-overlay #mproto-place-banner,
  body.mproto-overlay #mproto-bld-popup,
  body.mproto-overlay #mproto-events,
  body.mproto-overlay #mproto-sheet-head,
  body.mproto-overlay #mproto-prio-sheet,
  body.mproto-overlay #placement-bar:not(.hidden) #squads-mode-btn,
  body.mproto-overlay #placement-bar:not(.hidden) #structures-mode-btn,
  body.mproto-overlay #placement-bar:not(.hidden) #begin-battle-btn,
  body.mproto-overlay #placement-bar:not(.hidden) #placement-placed,
  body.mproto-overlay #combat-bar:not(.hidden) { display: none !important; }
}

/* The custom job listbox is a TOUCH decoration only. Its sheet lives in the DOM on every build, so
   its hidden state has to be declared OUTSIDE the phone media query — inside it, a desktop-width
   window matched no rule at all and the bare panel rendered down the page. The .show rule in the
   media block outranks this one on specificity, so ordering does not matter. */
#mproto-prio-sheet { display: none; }
