/**
 * MOBILE STYLES
 * =============
 * Everything here is scoped to `body.mobile` and is inert on desktop.
 *
 * WHY A SEPARATE FILE  (2026-08-25, user-directed)
 * ---------------------------------------------------------------------------
 * The alternative was threading mobile rules through style.css's 2,100 lines
 * behind media queries. That file has no breakpoints at all today, and the
 * changes mobile needs are STRUCTURAL rather than sizing -- the sidebar stops
 * being a column and becomes a sheet, the menu bar stops existing and becomes
 * two separate controls. Rules that reshape a layout read badly interleaved
 * with the rules establishing it, and they are far easier to reason about (and
 * to delete, if a direction doesn't work out) when they live together.
 *
 * The cost is some duplication of selectors between the two files. That is
 * accepted deliberately.
 *
 * TARGET  (user-directed): phones, portrait. Tablets and landscape are
 * explicitly out of scope for now -- they mostly want the desktop layout with
 * a narrower sidebar, which is a different and much smaller problem.
 *
 * THE GATE
 * ---------------------------------------------------------------------------
 * `body.mobile` is set once at startup by main.js's detectMobile(), from a
 * coarse-pointer check AND a viewport width test -- not from a user-agent
 * sniff, which is unreliable and ages badly. Every rule below is scoped to it
 * rather than to a bare media query, so that:
 *   - a desktop browser resized narrow does NOT silently become the phone
 *     build mid-session (the layout swaps under you, and canvas sizing and
 *     input handling would have to re-negotiate), and
 *   - the mode can be forced for testing (`?mobile` / `?desktop`) without
 *     having to fake a device.
 *
 * STATUS: phase 0. This file currently carries only the foundations -- the
 * layout inversion (bottom sheet, FAB, status pill) is phase 1 and lands here.
 */

/* --------------------------------------------------------------------------
   FOUNDATIONS
   -------------------------------------------------------------------------- */

body.mobile {
  /* A phone browser will happily bounce the whole document when a gesture
     starts at a screen edge, which drags the game out of frame. The game is a
     fixed-viewport app, not a scrolling page. */
  overscroll-behavior: none;
  position: fixed;
  inset: 0;
  width: 100%;
  /* Belt and braces with .game-screen's own dvh rule in style.css. */
  height: 100dvh;
  overflow: hidden;

  /* Stops iOS inflating body text in portrait, which would silently break
     every panel measured in ems. */
  -webkit-text-size-adjust: 100%;
}

/* Double-tap-to-zoom is a browser behavior we want replaced by our own
   double-tap-to-zoom-the-MAP (phase 3), and its ~300ms click delay makes
   every button feel unresponsive until then. */
body.mobile button,
body.mobile .menu-btn,
body.mobile .menu-dropdown-btn {
  touch-action: manipulation;
}

/* The blue-grey flash a mobile browser paints over a tapped element. It reads
   as a rendering artifact rather than as feedback; real press states are
   defined per-control. */
body.mobile * {
  -webkit-tap-highlight-color: transparent;
}

/* Long-press on the map is OUR gesture (it opens the radial menu -- see
   input.js). Left alone, the browser also fires its own text-selection and
   callout behavior on the same press, which puts a selection handle or a
   share sheet on top of the ring. Deliberately NOT applied to dialogs or the
   knowledge base, where selecting text is legitimate. */
body.mobile .map-area,
body.mobile #map-canvas {
  -webkit-touch-callout: none;
  user-select: none;
  -webkit-user-select: none;
}

/* Notches, rounded corners and the home indicator. Anything anchored to a
   screen edge has to sit inside the safe area or it is partly unreachable.
   Uses the padding on the shell rather than on each control, so phase 1's
   sheet and FAB inherit it. Requires `viewport-fit=cover` on the viewport
   meta tag (index.html) -- without that, env() resolves to 0 everywhere. */
body.mobile .game-screen {
  padding-top: env(safe-area-inset-top, 0px);
  padding-bottom: env(safe-area-inset-bottom, 0px);
  padding-left: env(safe-area-inset-left, 0px);
  padding-right: env(safe-area-inset-right, 0px);
  box-sizing: border-box;
}

/* No cursor affordances on a device with no cursor. */
body.mobile #map-canvas {
  cursor: default;
}

/* --------------------------------------------------------------------------
   PHASE 1 — LAYOUT INVERSION

   Desktop wraps the map in permanent furniture: a menu strip above, a 280px
   inspector beside. Measured on a 375px-wide phone that leaves the map 95
   CSS pixels of width, which is the whole argument for doing this.

   So the map takes the entire screen and everything else is summoned over
   it. Nothing MOVES in the DOM to achieve that -- these rules only reposition
   what index.html already contains, precisely so that every handler main.js
   binds by id keeps working. See index.html's mobile-chrome block.
   -------------------------------------------------------------------------- */

body.mobile .game-body {
  display: block;   /* not the desktop flex row */
  position: relative;
}

/* Map to full bleed, underneath everything else. */
body.mobile .map-area {
  position: absolute;
  inset: 0;
}

/* The desktop menu strip becomes a drawer behind the hamburger. Kept in the
   DOM (and so keeps all its wiring) but slid off-screen until summoned. */
body.mobile .menu-bar {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  z-index: 60;
  width: min(82vw, 20rem);
  flex-direction: column;
  /* Desktop's base .menu-bar rule (css/style.css) sets `flex-wrap: wrap` for
     its own horizontal button row, and this override never reset it back to
     nowrap for the column layout below. A `flex-direction: column` +
     `flex-wrap: wrap` container that overflows its height doesn't scroll --
     it starts a NEW COLUMN for whatever didn't fit, and on at least one
     real device (2026-08-31, user-reported) that second column rendered at
     the same origin as the first, visually merging "Reports" into "File".
     Root-caused by resizing the emulated viewport short enough to overflow
     by only a few pixels -- reproduced here, not on the real device, but
     the mechanism is confirmed: without this line, .menu-bar's own
     scrollHeight never exceeds its clientHeight even when content plainly
     doesn't fit, because the overflow was being absorbed by a phantom
     column instead of the `overflow-y: auto` this rule asks for. */
  flex-wrap: nowrap;
  align-items: stretch;
  gap: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--bg-panel, #14161b);
  padding: calc(env(safe-area-inset-top, 0px) + 3.5rem) 0 2rem;
  box-shadow: 2px 0 24px rgba(0, 0, 0, .5);
  transform: translateX(-102%);
  transition: transform .26s cubic-bezier(.32, .72, 0, 1);
  visibility: hidden;
}
body.mobile.m-menu-open .menu-bar {
  transform: translateX(0);
  visibility: visible;
}
/* In the drawer, each desktop dropdown becomes a collapsible SECTION of one
   settings list -- File, Interface, Audio, Knowledge, Reports.

   This reuses main.js's setupMenuBar wiring exactly as it stands: it already
   closes every other dropdown before opening the tapped one, and already puts
   an `.active` class on the open header. On desktop that reads as a menu bar;
   here the identical behavior reads as an accordion. So there is no mobile
   menu controller -- only different clothes on the same one. */
body.mobile .menu-bar .menu-item {
  width: 100%;
  position: static;
}
body.mobile .menu-bar .menu-btn {
  width: 100%;
  text-align: left;
  padding: .95rem 1.1rem;
  font-size: .95rem;
  font-weight: 600;
  letter-spacing: .02em;
  border: 0;
  border-bottom: 1px solid rgba(255, 255, 255, .07);
  border-radius: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 50px;
}
/* Disclosure caret, rotating on the .active class the existing JS sets. */
body.mobile .menu-bar .menu-btn::after {
  content: "";
  width: .48rem;
  height: .48rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  opacity: .5;
  transform: rotate(-45deg);
  transition: transform .18s ease;
  margin-right: .2rem;
}
body.mobile .menu-bar .menu-btn.active::after { transform: rotate(45deg); }

/* The open section. Left as the inline display the JS sets (flex) -- not
   forced open, or every section would be expanded at once and the drawer
   would be one unnavigable scroll. */
body.mobile .menu-bar .menu-dropdown {
  position: static;
  width: 100%;
  min-width: 0;
  border: none;
  border-bottom: 1px solid rgba(255, 255, 255, .07);
  border-radius: 0;
  box-shadow: none;
  background: rgba(0, 0, 0, .26);
  flex-direction: column;
  padding: .25rem 0;
}

/* Grouped-list rows. 48px min: comfortably past the 44px floor, and it makes
   the whole drawer scan as one rhythm rather than as ported desktop bits. */
body.mobile .menu-bar .menu-dropdown-btn,
body.mobile .menu-bar .audio-panel-row {
  min-height: 48px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .9rem;
  padding: .55rem 1.1rem;
  font-size: .94rem;
  width: 100%;
  text-align: left;
  border: 0;
  border-radius: 0;
  background: none;
}
body.mobile .menu-bar .menu-dropdown-btn + .menu-dropdown-btn,
body.mobile .menu-bar .audio-panel-row + .audio-panel-row {
  border-top: 1px solid rgba(255, 255, 255, .05);
}
body.mobile .menu-bar .fow-panel-label {
  padding: .8rem 1.1rem .3rem;
  font-size: .74rem;
  letter-spacing: .09em;
  text-transform: uppercase;
  opacity: .55;
}

/* Checkbox -> switch. A 22px tick box is a desktop control shrunk onto a
   phone; a switch is what a settings row is supposed to look like here, and
   it doubles the hit area. Radios keep their own shape -- they mean "one of
   these", and a row of switches would say the wrong thing. */
body.mobile .menu-bar input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  flex: 0 0 auto;
  width: 50px;
  height: 30px;
  margin: 0;
  border-radius: 999px;
  background: rgba(255, 255, 255, .17);
  border: 1px solid rgba(255, 255, 255, .16);
  position: relative;
  transition: background-color .2s ease;
  cursor: pointer;
}
body.mobile .menu-bar input[type="checkbox"]::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: #f2efe6;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .45);
  transition: transform .2s cubic-bezier(.32, .72, 0, 1);
}
body.mobile .menu-bar input[type="checkbox"]:checked {
  background: #3f7a4b;
  border-color: rgba(150, 220, 160, .5);
}
body.mobile .menu-bar input[type="checkbox"]:checked::after {
  transform: translateX(20px);
}

body.mobile .menu-bar input[type="radio"] {
  appearance: none;
  -webkit-appearance: none;
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  margin: 0;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, .35);
  background: transparent;
  position: relative;
}
body.mobile .menu-bar input[type="radio"]:checked {
  border-color: #7fbf8c;
}
body.mobile .menu-bar input[type="radio"]:checked::after {
  content: "";
  position: absolute;
  inset: 4px;
  border-radius: 50%;
  background: #7fbf8c;
}

/* Sliders need a thumb a finger can actually catch. */
body.mobile .menu-bar input[type="range"] {
  flex: 1 1 auto;
  min-width: 0;
  height: 30px;
  background: transparent;
  appearance: none;
  -webkit-appearance: none;
}
body.mobile .menu-bar input[type="range"]::-webkit-slider-runnable-track {
  height: 5px;
  border-radius: 3px;
  background: rgba(255, 255, 255, .2);
}
body.mobile .menu-bar input[type="range"]::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  width: 26px;
  height: 26px;
  margin-top: -10px;
  border-radius: 50%;
  background: #f2efe6;
  box-shadow: 0 1px 4px rgba(0, 0, 0, .5);
}
body.mobile .menu-bar input[type="range"]::-moz-range-track {
  height: 5px;
  border-radius: 3px;
  background: rgba(255, 255, 255, .2);
}
body.mobile .menu-bar input[type="range"]::-moz-range-thumb {
  width: 26px;
  height: 26px;
  border: 0;
  border-radius: 50%;
  background: #f2efe6;
}

body.mobile .menu-bar select {
  min-height: 40px;
  font-size: .92rem;
  padding: .3rem .55rem;
  border-radius: 8px;
  max-width: 55%;
}
body.mobile .menu-bar .now-playing-value {
  max-width: 55%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  opacity: .7;
}

/* Dismiss layer for the drawer. */
body.mobile .m-scrim {
  position: fixed;
  inset: 0;
  z-index: 55;
  background: rgba(0, 0, 0, .5);
  border: 0;
}

/* --- the sheet ---------------------------------------------------------- */

/* The sidebar, re-homed. Three resting heights, driven by data-detent.
   Peek/full are the two states #m-sheet-toggle-btn drives; half no longer
   happens automatically (an earlier revealSheetForSelection auto-raise was
   removed, 2026-08-26, user-reported: it "bounced" the sheet into view on
   every map selection, which read as broken/unwanted once a manual show/hide
   button existed) but the detent itself is kept for the status-pill
   shortcut (see setupMobileShell's `m-status` click listener) and for
   forward compatibility.

   --m-sheet-peek is how much stays on screen at rest -- exactly half of
   #m-sheet-toggle-btn's own 46px (2026-08-26, user-directed), so that
   button's bottom edge lands flush on the viewport's own bottom edge, with
   only the gilded top border and a sliver of background showing above it.
   Deliberately ignores env(safe-area-inset-bottom) at this one detent (the
   half/full transforms below never used it either) -- the user's own call,
   trading the home-indicator clearance the peek strip used to keep for a
   button that's flush with the true screen edge. #m-sheet-toggle-btn is a
   plain, independent position:fixed sibling, NOT nested in a shared
   wrapper -- an earlier cut tried sharing one transform via a
   display:contents wrapper so the button would ride along "for free," but
   position:fixed descendants of a display:contents ancestor didn't reliably
   repaint on attribute changes in testing (a real, documented class of
   browser bug). Instead the button tracks the sheet via its own `bottom`,
   driven by the SAME data-detent value (duplicated onto it in
   setSheetDetent) -- see its rule block further down, whose three bottom
   values must stay numerically in sync with the three transforms here. */
body.mobile #sidebar {
  --m-sheet-peek: 23px;
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  top: auto;
  width: auto;
  height: 78vh;
  z-index: 30;
  border-radius: 16px 16px 0 0;
  /* The gilded per-race frame is this game's identity and worth keeping, but
     a four-sided border-image around a sheet reads as a box floating in the
     wrong place. Top edge only: the art still themes to the kingdom, and it
     doubles as the sheet's lip. */
  border-width: 12px 0 0 0;
  overflow: hidden;
  box-shadow: 0 -6px 28px rgba(0, 0, 0, .45);
  transform: translateY(calc(100% - var(--m-sheet-peek)));
  transition: transform .28s cubic-bezier(.32, .72, 0, 1);
}
body.mobile #sidebar[data-detent="half"] { transform: translateY(52%); }
body.mobile #sidebar[data-detent="full"] { transform: translateY(0); }

/* Sheet contents scroll internally. */
body.mobile #sidebar > * {
  max-height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding-top: 1.15rem;
  -webkit-overflow-scrolling: touch;
}

/* The sidebar's own End Turn is redundant once the FAB exists, and having
   two of the same control on one screen is worse than having one. The button
   stays in the DOM -- the FAB forwards taps to it. */
body.mobile #sidebar .end-turn-btn { display: none; }

/* Tab strip -> horizontally scrolling chips. Selector matches sidebar.js's
   actual markup (.tile-tabs/.tile-tab, see renderTabStrip) -- without this,
   the desktop `flex-wrap: wrap` rule (style.css) was still active here, so
   tabs wrapped onto a second row on a narrow phone. That second row then
   got clipped by the generic `#sidebar > *` overflow-y:auto/max-height:100%
   rule just above (each direct child of the sheet scrolls independently),
   reading as the tab strip's bottom edge being overlapped by the panel
   content immediately below it in the flow. */
body.mobile #sidebar .tile-tabs {
  flex-wrap: nowrap;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  margin-bottom: 8px;
}
body.mobile #sidebar .tile-tabs::-webkit-scrollbar { display: none; }
body.mobile #sidebar .tile-tab {
  flex: 0 0 auto;
  min-height: 38px;
}

/* --- floating chrome ---------------------------------------------------- */

/* Top row: menu button, research progress, status pill, all inside ONE
   fixed flex row (2026-08-27) rather than three independently
   position:fixed elements -- see index.html's own comment on #m-topbar
   for why (the middle slot needs to flex-fill whatever room the other
   two's own natural widths leave, not a guessed fixed width). */
body.mobile .m-topbar {
  position: fixed;
  z-index: 45;
  top: calc(env(safe-area-inset-top, 0px) + .6rem);
  left: calc(env(safe-area-inset-left, 0px) + .6rem);
  right: calc(env(safe-area-inset-right, 0px) + .6rem);
  display: flex;
  align-items: flex-start;
  gap: .5rem;
}

/* Astronomical clock, docked on the SLIDE-UP SHEET's own top edge --
   mobile's equivalent of the desktop panel, not the End Turn FAB (2026-09-09
   -11, user-directed; went through two homes before landing here. First
   "move the clock to the top of the toolbar / FAB" put it above the FAB.
   Corrected the same week -- "it should be pinned to the top of it, the
   same way it is pinned to the top of the desktop browser version" --
   which read at the time as "pin it to the FAB more tightly," and got a
   z-index/overlap pass for exactly that. Corrected AGAIN once it became
   clear "it" meant the sidebar/toolbar all along -- "just as the clock is
   at the top of the toolbar on desktop browser, it should be at the top of
   the slide up toolbar on mobile, moving with that toolbar up and down."
   The FAB was never the right target: on desktop the dial docks on
   #sidebar's own top border, and #sidebar IS the slide-up sheet on mobile
   -- same element, same id, CSS just repositions it into a bottom sheet.
   The FAB was the wrong analogy from the start; this is the literal one.)

   SAME GEOMETRY AS DESKTOP: dial's own top edge lines up with the sheet's
   own top edge, extending DOWN into the panel rather than floating above
   it with a gap -- see .daynight-clock-docked's comment for the identical
   relationship on the sidebar. z-index 31 (just above the sheet's own 30)
   so the dial paints IN FRONT of the sheet at the seam, exactly like the
   desktop dial sits in front of the sidebar's border instead of vanishing
   behind it -- the reverse of the FAB-era version, which deliberately kept
   the clock BEHIND the button to protect its label; here the clock is the
   thing meant to be seen.

   CENTRED (2026-09-11, user-directed, reversing the earlier "right-aligned,
   not centred" call below). That earlier choice was to dodge the sheet's own
   drag-handle (.m-sheet-toggle), which sits dead centre on this exact same
   top edge -- centring the dial there too puts them on the same spot. Safe
   to do anyway: the toggle's z-index (45) beats the dial's (31), so the
   handle stays fully visible and tappable ON TOP of the dial wherever they
   overlap, the same protection the FAB-era version leaned on for the
   button's own label (see that note, still accurate, just for a different
   element now). Confirmed live rather than assumed -- the handle reads
   clearly against the dial at every detent.

   TRACKS THE SHEET'S THREE DETENTS -- see #sidebar[data-detent=...] above
   -- but NOT by copying its transform percentages verbatim: those (52%,
   calc(100% - 23px)) are relative to #SIDEBAR'S OWN 78vh height, and this
   element is a completely different size, so the same percentage would
   move it a completely different distance and desync the two immediately.
   Every value below is the ABSOLUTE shift those percentages actually
   resolve to (52% of 78vh = 40.56vh -- if #sidebar's height or that 52%
   ever changes, this needs updating by hand alongside it), which keeps the
   two locked together regardless of either element's own height. Applied
   via a mirrored data-detent attribute (main.js's setSheetDetent sets this
   alongside the toggle's own copy) rather than the FAB's old
   --m-fab-bottom custom property -- that property is set directly on
   #sidebar and does not inherit to a sibling living outside its subtree,
   which is also, incidentally, why the FAB itself has never actually
   changed position when the sheet opens; a separate, pre-existing bug this
   fix doesn't touch.

   Every translateY below is paired with a translateX(-50%) for the
   centring -- one `transform` property can't be split across two
   declarations, so the horizontal half has to be repeated on all three
   rules rather than factored out once.

   `bottom`'s 80.5px is the dial's own rendered height at mobileWidth 161
   (config.js's daynightClock block) -- R = 161/2, and the container's own
   box height is exactly R (see daynight-clock.js's geometry()). Lines the
   dial's TOP up with the sheet's top edge at rest (shift 0, "full" detent),
   the same relationship desktop's dial has with the sidebar. Update this
   by hand alongside mobileWidth if that ever changes again -- it is NOT
   read from config, just kept in sync with it.

   AT PEEK the sheet's own top edge sits almost entirely below the screen
   (only its 23px lip shows), so the dial is mostly off-screen too -- by
   design, not a bug: "visible when expanded... sliding up along with it"
   describes a clock that reveals itself as the sheet rises, the same way
   the sheet's own header content does, not one that must stay fully
   visible at rest. Same transition timing as the sheet's own transform, so
   the two read as one physical unit in motion rather than two animations
   that happen to fire from the same tap. */
body.mobile .daynight-clock {
  top: auto;
  left: 50%;
  right: auto;
  bottom: calc(78vh - 80.5px);
  transform: translateX(-50%) translateY(calc(78vh - var(--m-sheet-peek, 23px)));
  transition: transform .28s cubic-bezier(.32, .72, 0, 1);
  z-index: 31;
}
body.mobile .daynight-clock[data-detent="half"] { transform: translateX(-50%) translateY(40.56vh); }
body.mobile .daynight-clock[data-detent="full"] { transform: translateX(-50%) translateY(0); }
/* The desktop build reserves ~62px at the top of the sidebar for the clock
   docked over its border (see .sidebar's own padding-top in style.css).
   Mobile now docks the SAME clock on the sheet's own top edge too (see
   .daynight-clock above), so this needs the equivalent headroom -- the
   mobile dial's own rendered height (80.5px at mobileWidth 161, kept in
   sync by hand alongside that config value and .daynight-clock's `bottom`,
   same as that rule's own note), plus a little breathing room before the
   sheet's first real line of content, the same proportions desktop's own
   comment describes for its taller dial. Was 14px, back when this space
   really was dead (the clock lived over by the FAB instead); if the clock
   ever moves again, drop this back down rather than leaving unexplained
   empty space at the top of every sheet. */
body.mobile #sidebar {
  padding-top: 87px;
}

body.mobile .m-menu-btn,
body.mobile .m-status,
body.mobile .m-research,
body.mobile .m-fab {
  font: inherit;
  color: var(--text, #e8e4da);
  border: 1px solid rgba(255, 255, 255, .16);
  background: rgba(18, 20, 26, .82);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: 0 2px 12px rgba(0, 0, 0, .38);
}
body.mobile .m-fab { position: fixed; z-index: 45; }

body.mobile .m-menu-btn {
  width: 44px;
  height: 44px;
  border-radius: 12px;
  display: grid;
  place-items: center;
  padding: 0;
  flex: 0 0 auto;
}
/* The in-game hamburger (#m-menu-btn) gets its position from being a flex
   item of #m-topbar; the title screen's own copy (#title-m-menu-btn,
   2026-08-27) has no such row to sit in, so it's independently fixed in
   the same spot #m-topbar itself is anchored to. */
body.mobile #title-m-menu-btn {
  position: fixed;
  z-index: 45;
  top: calc(env(safe-area-inset-top, 0px) + .6rem);
  left: calc(env(safe-area-inset-left, 0px) + .6rem);
}
/* Three bars from one element: the box is the middle bar, the two
   pseudo-elements are the outer pair. */
body.mobile .m-menu-bars,
body.mobile .m-menu-bars::before,
body.mobile .m-menu-bars::after {
  content: "";
  display: block;
  width: 18px;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
  position: relative;
}
body.mobile .m-menu-bars::before { position: absolute; top: -6px; }
body.mobile .m-menu-bars::after  { position: absolute; top: 6px; }

/* Research progress (2026-08-27, user-directed) -- hidden via the
   [hidden] attribute (main.js's updateMobileStatus toggles it against
   civ.currentResearch) rather than always showing an empty bar.
   flex:1 fills exactly whatever's left between the menu button and the
   status pill's own natural widths -- see #m-topbar's own comment. */
body.mobile .m-research {
  flex: 1 1 auto;
  min-width: 0;
  height: 44px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 4px;
  padding: 0 .8rem;
  border-radius: 12px;
}
body.mobile .m-research[hidden] { display: none; }
body.mobile .m-research-label {
  font-size: .72rem;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
body.mobile .m-research-track {
  height: 5px;
  border-radius: 3px;
  background: rgba(255, 255, 255, .16);
  overflow: hidden;
}
/* Width transitions once per turn at most (research only ever
   progresses), so this is a gentle one-directional fill, not the
   flashing/strobing the project's own no-flashing rule is about. */
body.mobile .m-research-fill {
  height: 100%;
  width: 0%;
  background: var(--accent, #c9a857);
  border-radius: 3px;
  transition: width .3s ease;
}

body.mobile .m-status {
  height: 44px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 1px;
  padding: 0 .8rem;
  border-radius: 12px;
  line-height: 1.15;
  font-variant-numeric: tabular-nums;
  flex: 0 0 auto;
  /* Pinned to the row's right edge even when .m-research is [hidden] and
     removed from flow entirely (flex:1 on .m-research alone only pushes
     this over WHILE that sibling is actually present). */
  margin-left: auto;
}
body.mobile .m-status-turn { font-size: .76rem; opacity: .72; }
body.mobile .m-status-res  { font-size: .82rem; font-weight: 600; white-space: nowrap; }
body.mobile .m-status-res .resource-icon {
  width: 11px;
  height: 11px;
  vertical-align: -1px;
  color: var(--accent);
}

/* End Turn: the most-pressed control in the game, so it gets the bottom-right
   thumb arc and sits above the sheet's peek height. */
body.mobile .m-fab {
  right: calc(env(safe-area-inset-right, 0px) + .85rem);
  bottom: calc(var(--m-fab-bottom, 5.6rem) + env(safe-area-inset-bottom, 0px));
  width: 66px;
  height: 66px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  text-align: center;
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .02em;
  padding: 0;
  transition: transform .18s ease, background-color .2s ease;
}
body.mobile .m-fab:active { transform: scale(.93); }
/* Nothing left to order: the turn is ready to end, and the button says so
   with color and weight rather than by animating. Static on purpose -- a
   pulsing control would violate the project's no-flashing rule. */
body.mobile .m-fab.m-ready {
  background: rgba(58, 122, 74, .9);
  border-color: rgba(140, 220, 150, .5);
}
/* Other kingdoms taking their turn (2026-08-27, user-directed): reads
   "Wait" and goes visibly inert while viewState.turnBanner is up -- see
   main.js's updateMobileStatus, which also sets the native `disabled`
   attribute so it stops responding to taps and long-presses, not just
   looks like it does. Grey, not the ready green or the default gold --
   neither "go" color belongs on a control that currently does nothing. */
body.mobile .m-fab:disabled {
  background: rgba(60, 62, 68, .82);
  border-color: rgba(255, 255, 255, .1);
  color: rgba(232, 228, 218, .55);
}
body.mobile .m-fab-badge {
  position: absolute;
  top: -3px;
  right: -3px;
  min-width: 22px;
  height: 22px;
  padding: 0 5px;
  border-radius: 11px;
  background: #b4451f;
  color: #fff;
  font-size: .72rem;
  font-weight: 700;
  display: grid;
  place-items: center;
  border: 2px solid rgba(18, 20, 26, .9);
}
body.mobile .m-fab-badge[hidden] { display: none; }

/* Sheet show/hide toggle: centered ON the sheet's top border, half above
   (over the map) and half below (over the collapsed strip) -- a small tab
   poking out of it. Independent position:fixed, not nested with #sidebar --
   see that rule block's own comment for why -- so it tracks the sheet via
   `bottom` instead of sharing one transform. 0 / 37.44vh (= 78vh * (1 - 52%))
   / 78vh below MUST stay numerically in sync with #sidebar's own
   --m-sheet-peek / 52% / 0 transform targets just above -- at peek this
   button's own 46px is exactly double --m-sheet-peek, so `bottom: 0` alone
   already centers it on the border AND lands its bottom edge flush on the
   screen (2026-08-26, user-directed); 23px in the other two states is half
   this button's own 46px, centering it on the line rather than sitting
   above it. */
body.mobile .m-sheet-toggle {
  position: fixed;
  left: 50%;
  bottom: 0;
  transform: translateX(-50%);
  z-index: 45;
  width: 46px;
  height: 46px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  padding: 0;
  font: inherit;
  color: var(--text, #e8e4da);
  border: 1px solid rgba(255, 255, 255, .16);
  background: rgba(18, 20, 26, .82);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: 0 2px 12px rgba(0, 0, 0, .38);
  transition: bottom .28s cubic-bezier(.32, .72, 0, 1), transform .18s ease;
}
body.mobile .m-sheet-toggle[data-detent="half"] { bottom: calc(37.44vh - 23px); }
body.mobile .m-sheet-toggle[data-detent="full"] { bottom: calc(78vh - 23px); }
body.mobile .m-sheet-toggle:active { transform: translateX(-50%) scale(.93); }

/* Chevron: a bordered corner, not a Unicode arrow character (2026-08-26,
   user-reported: the old ↑/↓ glyphs were too faint to see) -- a font
   glyph's stroke weight isn't reliably controllable via font-weight, where
   a border's width is exact and as bold as this sets it. Points up
   (collapsed, "tap to raise") by default; setSheetDetent toggles
   .m-sheet-toggle-open on the BUTTON when the sheet is up, flipping it to
   point down ("tap to lower") via the second rule below. currentColor picks
   up the button's own --text color, so this needs no color of its own. */
body.mobile .m-sheet-toggle-arrow {
  width: 10px;
  height: 10px;
  border-style: solid;
  border-color: currentColor;
  border-width: 0 3px 3px 0;
  transform: rotate(-135deg);
  transition: transform .18s ease;
}
body.mobile .m-sheet-toggle.m-sheet-toggle-open .m-sheet-toggle-arrow {
  transform: rotate(45deg);
}

/* Desktop keeps none of this. .m-menu-btn/.m-status/.m-research no longer
   need their own entries here -- they're always-rendered children of
   .m-topbar now, and that parent's display governs their visibility same
   as any display:none subtree. */
.m-topbar, .m-fab, .m-sheet-toggle, .m-scrim { display: none; }
body.mobile .m-topbar { display: flex; }
body.mobile .m-fab,
body.mobile .m-sheet-toggle { display: grid; }
body.mobile .m-scrim { display: block; }
body.mobile .m-scrim[hidden] { display: none; }

/* The zoom buttons are redundant once pinch works, and they sit exactly where
   the status pill goes. */
body.mobile .zoom-controls { display: none; }
/* The desktop status/research pills (2026-08-27) live inside .menu-bar,
   which already becomes the mobile drawer wholesale -- without this they'd
   show up a second time in there, redundant with the always-visible
   #m-status/#m-research pills this same content already gets via
   #m-topbar. */
body.mobile .d-status, body.mobile .d-research { display: none; }

/* --------------------------------------------------------------------------
   PHASE 2 — DESTINATIONS

   Tech tree, Reports, Knowledge, Shortcuts. These are places you GO, not
   settings you flip, and they already exist as full-screen overlays with
   their own scroll containers -- so they want re-dressing, not rebuilding.

   Deliberately excluded: #game-dialog-modal. It carries real decisions (keep
   or raze a captured city, confirm ending a turn), and a modal you can
   swipe away by accident is the wrong shape for a choice that matters. Those
   keep their explicit buttons. See main.js's setupSwipeToDismiss.
   -------------------------------------------------------------------------- */

/* The knowledge overlay is inset by exactly the desktop sidebar's 280px so it
   never covers it (.knowledge-overlay-ingame in style.css). On a phone there
   IS no sidebar beside the map -- it's the bottom sheet now -- so that inset
   just squeezed the panel into a 95px column, the same failure the layout
   inversion exists to fix. */
body.mobile .knowledge-overlay.knowledge-overlay-ingame {
  right: 0;
}

body.mobile .techtree-modal,
body.mobile .reports-modal,
body.mobile .knowledge-modal {
  width: 100vw;
  max-width: 100vw;
  height: 100dvh;
  max-height: 100dvh;
  border-radius: 0;
  margin: 0;
  padding-top: calc(env(safe-area-inset-top, 0px) + 1.5rem);
  padding-bottom: env(safe-area-inset-bottom, 0px);
  transition: transform .26s cubic-bezier(.32, .72, 0, 1);
}
/* Following the finger during a dismiss drag -- no easing, same reasoning as
   the sheet. */
body.mobile .m-swiping { transition: none !important; }

/* Grab affordance, matching the bottom sheet's so the two read as the same
   language. Pseudo-element again: these modals have their content replaced
   wholesale by their renderers. */
body.mobile .techtree-modal::before,
body.mobile .reports-modal::before,
body.mobile .knowledge-modal::before {
  content: "";
  position: absolute;
  top: calc(env(safe-area-inset-top, 0px) + .5rem);
  left: 50%;
  width: 2.4rem;
  height: .25rem;
  margin-left: -1.2rem;
  border-radius: 999px;
  background: currentColor;
  opacity: .3;
  z-index: 3;
  pointer-events: none;
}

body.mobile .techtree-close-btn {
  width: 44px;
  height: 44px;
  font-size: 1.7rem;
  top: calc(env(safe-area-inset-top, 0px) + .35rem);
  right: .4rem;
  z-index: 4;
}

/* Knowledge base (Units/Conditions/Stats) list+profile layout (2026-08-27,
   user-reported: unreadable on a phone) -- .kb-list-pane's desktop-fixed
   240px column left almost nothing for .kb-profile-pane once the modal
   itself has shrunk to fit a ~350px-wide screen. Stacked instead: the list
   on top, height-capped and independently scrollable, profile pane below
   it at full width. */
body.mobile .kb-body {
  flex-direction: column;
  gap: 10px;
}
body.mobile .kb-list-pane {
  flex: 0 0 auto;
  width: 100%;
  max-height: 34vh;
  border-right: none;
  border-bottom: 1px solid var(--border);
  padding-right: 0;
  padding-bottom: 10px;
}
body.mobile .kb-profile-pane { padding-right: 0; }

/* Tech tree's 4-column civic/building/military/mystic grid (2026-08-27,
   user-reported: same unreadable problem, worse -- this grid's own comment
   already admits it "crams badly" at 800px, let alone a phone). Collapsed
   to one column per layer. The shared header row (.techtree-header) only
   had room to print each category's name ONCE, at the very top, which is
   useless once a single-column layout has you scrolled a screen past it --
   so each .techtree-column instead grows its own inline label from the
   data-column-label techtree.js now stamps on it, matching the header's
   text exactly without a second hand-copied list to keep in sync. */
body.mobile .techtree-layer {
  grid-template-columns: 1fr;
  gap: 4px;
}
body.mobile .techtree-header { display: none; }
body.mobile .techtree-layer-label {
  padding-top: 10px;
  font-size: 13px;
  min-height: 44px;
  width: 100%;
}
body.mobile .techtree-column:empty { display: none; }
body.mobile .techtree-column:not(:empty)::before {
  content: attr(data-column-label);
  display: block;
  color: var(--accent);
  font-size: 11px;
  font-weight: bold;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  margin-top: 4px;
}

/* --------------------------------------------------------------------------
   PHASE 4 — FEELS-INSTALLED

   manifest.json's orientation:"portrait-primary" locks rotation for an
   INSTALLED app on Android/Chrome, but that hint is invisible to the far
   more common case of someone just opening the game in an ordinary mobile
   browser tab. This is that case's fallback -- see index.html's own comment
   on #m-rotate-notice for why it's dismissible per-rotation rather than
   forever.
   -------------------------------------------------------------------------- */
.m-rotate-notice {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 200;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1.1rem;
  padding: 2rem;
  text-align: center;
  background: #0d0e11;
  color: #e8e4da;
}
body.mobile.m-landscape .m-rotate-notice { display: flex; }
.m-rotate-icon {
  font-size: 2.6rem;
  line-height: 1;
  opacity: .85;
  /* A single settle, not a loop -- the project's no-flashing rule covers
     repeating motion as much as it covers repeating light, and a spinning
     glyph here would be exactly that. */
  animation: m-rotate-settle .5s ease-out;
}
@keyframes m-rotate-settle {
  from { transform: rotate(-90deg); opacity: 0; }
  to   { transform: rotate(0deg);   opacity: .85; }
}
@media (prefers-reduced-motion: reduce) {
  .m-rotate-icon { animation: none; }
}
.m-rotate-notice p {
  max-width: 26rem;
  margin: 0;
  font-size: 1rem;
  line-height: 1.5;
  color: #b6bcc7;
}
.m-rotate-dismiss {
  min-height: 44px;
  padding: 0 1.3rem;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, .22);
  background: rgba(255, 255, 255, .08);
  color: inherit;
  font: inherit;
  font-size: .92rem;
}

/* Wide content inside a destination scrolls in its own box rather than
   forcing the page sideways -- reports tables and the tech tree are both far
   wider than a phone. */
body.mobile .techtree-modal-scroll,
body.mobile #techtree-content,
body.mobile #reports-content,
body.mobile #knowledge-content {
  overflow-x: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

/* Touch-sized rows in the knowledge base's own navigation. */
body.mobile .knowledge-modal button,
body.mobile #reports-content button,
body.mobile .knowledge-nav-item {
  min-height: 44px;
}

/* Report text (2026-08-31, user-reported: "the font is very small and hard
   to read" on mobile) -- desktop's own report typography (css/style.css)
   sits at 10-12px throughout, sized for a mouse-distance monitor. Every
   size below is that same desktop value bumped roughly 25-30%, not a fresh
   scale, so relative hierarchy (chart axis labels smaller than the AI
   action log's own body text, etc.) is preserved exactly, just legible at
   arm's length. The SVG chart's own axis/legend text (js/ui/reports.js)
   is set via inline font-size="10"/"11" attributes -- an SVG presentation
   attribute carries the lowest possible specificity, so this plain
   selector overrides it with no !important needed. */
body.mobile .reports-chart-wrap svg text { font-size: 13px !important; }
body.mobile .reports-subtitle { font-size: 15px; }
body.mobile .reports-legend-item { font-size: 15px; }
body.mobile .reports-tooltip { font-size: 14px; }
body.mobile .ai-action-log-controls,
body.mobile .ai-action-log-controls select,
body.mobile .ai-action-log-controls button { font-size: 15px; }
body.mobile .ai-action-log-summary { font-size: 13px; }
body.mobile .ai-action-log-row { font-size: 14px; line-height: 1.5; }

/* --------------------------------------------------------------------------
   PHASE 3 — THE RING GESTURE

   On desktop the ring is a right-click convenience. On a phone it becomes
   the PRIMARY way to give orders: long-press opens it, sliding the same
   finger across the pills previews each one, and lifting commits whichever
   is under the finger when it leaves the glass -- one continuous gesture
   from intent to action, thumb never leaving the screen. See input.js's
   ring-drag handling in attach() for the touch-move/haptic side of this;
   these rules are its visual half.

   ringmenu.js's layout() already grows PILL_H to 44px under body.mobile, so
   the geometry and this height agree without either module knowing about
   the other's number directly -- both just read the same mobile flag.

   TYPE AND PADDING (2026-08-26, user-reported: "the text of the ring menu
   often appears truncated within the menu option, making it unreadable")
   The mobile pill used to be set LARGER than the desktop one -- 13px type
   in 14px of side padding -- while the space available to it is a fraction
   of a desktop window's. Every px of padding is a px the label doesn't get,
   and a single nowrap line then ellipsized most real labels ("Build Road to
   This Tile", "Gather More Resources") down to nothing useful.

   Three changes, in order of how much each buys:
     - the label wraps to TWO lines instead of ellipsizing on one. A 44px
       touch target has room for two 11px lines with air to spare, so this
       roughly doubles what fits at no cost in target size.
     - smaller type (11px) and tighter padding (9px), which is a further
       ~30% more characters per line.
     - the keyboard-shortcut badge is hidden outright -- a phone has no
       keyboard, so it was pure width tax.
   -------------------------------------------------------------------------- */

body.mobile .map-ring-item {
  height: 40px;
  padding: 0 9px;
  font-size: 11px;
  border-radius: 20px;
}
/* -webkit-box + line-clamp is the only widely-supported way to cap wrapped
   text at N lines; it's implemented in every engine this game runs on
   (including Firefox since 68), and `overflow: hidden` on the pill keeps
   the failure mode graceful anywhere it isn't. The flex parent still
   vertically centres this box inside the 44px pill. */
body.mobile .map-ring-item-label {
  white-space: normal;
  line-height: 1.2;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  text-overflow: clip;
}
body.mobile .map-ring-item-shortcut { display: none; }
/* Same reasoning for the hamburger menu's "Enter Full Screen -- F11" row:
   a keyboard shortcut hint is noise on a device with no keyboard. */
body.mobile .menu-shortcut { display: none; }
/* The Interface menu's "Keyboard Shortcuts" entry itself, not just its
   hints (2026-08-27, user-directed): a phone has no keyboard, so the whole
   reference window it opens is dead weight, not just the badges above. */
body.mobile #keyboard-shortcuts-btn { display: none; }
/* The listed fallback gets the same treatment, and drops the desktop
   min-width -- on a phone the list is sized by applyList's own maxWidth
   against the map area, and a 170px floor could push it past that. */
body.mobile .map-ring-item-listed { min-width: 0; }

/* The pill currently under the dragging finger -- input.js toggles this as
   the finger crosses from one pill to the next, WITHOUT firing a real
   pointer/hover event on the button (pointer capture keeps every event
   routed to the canvas throughout the gesture, by design -- see input.js).
   Bright rather than the desktop hover treatment: a mouse hover is passive
   feedback, but this state means "lift now and this fires", which needs to
   read as more committed than a hover ever does. */
body.mobile .map-ring-item-armed {
  background: var(--accent, #d9b565);
  border-color: var(--accent, #d9b565);
  color: #16181c;
  transform: translateY(-50%) scale(1.06);
  box-shadow: 0 3px 14px rgba(0, 0, 0, .55);
}
/* A LISTED pill (style.css's .map-ring-item-listed, the "too many options"
   fallback -- see ringmenu.js's layout()) sits in normal document flow, not
   ring-positioned. The lift/scale transform above assumes the latter; on a
   listed row it visually detaches the pill from its own flow slot and
   overlaps the row above it while arming. Two classes beat .map-ring-item-
   listed's one on specificity alone, so this has to explicitly win back the
   transform reset rather than relying on source order. Background/border/
   box-shadow keep working exactly as before -- only the geometry-breaking
   transform is dropped. */
body.mobile .map-ring-item-listed.map-ring-item-armed {
  transform: none;
}
