/* ============================================================
   ANTHILLS.IO — UI shell
   Warm near-black, a faint checkerboard under everything, and a
   single amber accent. Two shells share one markup: a full-screen
   title before a map exists, a slim rail beside the map after.
   ============================================================ */
:root{
  --bg:        #131010;
  --bg-2:      #1a1512;
  --panel:     rgba(16,13,11,.94);
  --line:      #2b241e;
  --line-soft: #221c17;
  --ink:       #efe7dc;
  --ink-dim:   #8d8177;
  --ink-off:   #5c534b;
  --amber:     #e2a63a;
  --amber-2:   #f7c85f;
  --red:       #e8564c;
  --green:     #6fc16a;
  --blue:      #5aa9d6;
  --check:     34px;
  --radius:    9px;
  /* one height for both footer bubbles, so the clock and the sun slider match */
  --foot-h:    23px;
  --font: "Segoe UI", Inter, system-ui, -apple-system, sans-serif;
  --mono: ui-monospace, "Cascadia Mono", Consolas, monospace;
}

*{ box-sizing:border-box; }
html,body{ height:100%; margin:0; }
body{
  background:var(--bg);
  color:var(--ink);
  font-family:var(--font);
  overflow:hidden;
  /* ALL THREE, AND THE UNPREFIXED ONE ON ITS OWN WAS NOT ENOUGH.

     A press and hold is the gesture that trips both of the others. WebKit has
     wanted -webkit-user-select since long before it took the plain name, and
     -webkit-touch-callout is a separate thing again — it is what stops iOS
     answering a long press with the magnifier and the copy bubble. Neither was
     here.

     It surfaced the moment holding a brood row became a way to buy ants:
     holding one selected its text instead, and a selection starting mid-press
     takes the pointer stream with it, so the hold died on the spot. The same
     press that was supposed to keep buying was being read as the start of
     `select this word`. */
  -webkit-user-select:none;
  user-select:none;
  -webkit-touch-callout:none;
  -webkit-font-smoothing:antialiased;
}

/* THE HEIGHT IS svh, AND THAT ONE WORD IS BOTH iOS BUGS.

   On iOS Safari 100vh is the LARGE viewport - the height this page WOULD have
   if the toolbar were retracted - so #app has always been taller than the
   screen by the height of Safari's bar, with its last inch living behind the
   chrome. And it is worse than "sometimes": html/body above are height:100%
   with overflow:hidden, so this document has no scrollable overflow at all,
   and Safari only retracts its toolbar in response to a document scroll. The
   toolbar therefore never retracts here. 100vh is not occasionally too tall,
   it is permanently too tall, on every load, by the full height of the bar.

   Everything in this file is `position:absolute; inset:0` inside #app - the
   front door, the settings dialog, both sheets, the endgame card, #viewport
   itself - so every one of them inherited the overhang, and so did their
   scrollers. .sheet-body scrolls perfectly well to an end that is physically
   off the glass; .title scrolls the front-door card down into a band nobody
   can look at. The FAQ's missing last line and the landscape card's missing
   MAP ID are the same bug wearing two hats, which is why there is no
   per-panel patch and no new breakpoint anywhere below.

   svh IS THE SMALL VIEWPORT: the height with every retractable piece of
   browser chrome showing. dvh is the fashionable answer and it is the wrong
   one HERE. On this page the two are the same number - nothing can scroll, so
   nothing can retract - so dvh buys no pixels; what it costs is that it is
   DEFINED to track the viewport, and every time it moved it would relayout
   #viewport, fire the resize listener in js/main.js, and land in Gfx.resize(),
   which reassigns canvas.width with no same-size guard and so throws the WebGL
   drawing buffer away every time. svh cannot move, so it cannot thrash - and
   that holds without my having to be right about what iOS does during a
   toolbar animation or when the keyboard opens over the SEED field, which is
   the point on a device I cannot test. What svh gives up: if some future iOS
   ever does retract the bar here, the freed strip stays body background, which
   is --bg, the same near-black the game already is. Letterbox, not flash.

   TWO height DECLARATIONS, IN THIS ORDER, ARE THE WHOLE FALLBACK. A browser
   that has never heard of svh drops the second at parse time and keeps the
   first - exactly today's behaviour, so nothing regresses. Fallback first,
   modern second; the order is the mechanism, and no @supports is involved.
   svh is Safari 15.4, Chrome 108, Firefox 101.

   MEDIA QUERIES ARE NOT EVALUATED AGAINST THIS. They do not track retracting
   chrome, so the max-height:640 and max-height:560 ladders below still measure
   the LARGE viewport while this box measures the small one. On a phone the gap
   is the toolbar's height and both thresholds are far away - landscape is
   ~390 either way, portrait is 745+ either way - so nothing changes today.
   Anything added near a threshold has to account for it.

   WIDTH IS 100%, and that half is housekeeping, not a fix - 100vw was never
   part of either symptom, and today it is even the same number, because body's
   overflow:hidden propagates to the viewport so there is no classic scrollbar
   gutter for 100vw to include. It is a trap armed and silent rather than a
   bug: the day anything makes the root scrollable, a 100vw box is wider than
   the content area. #app is a block child of a margin:0 body, so 100% is the
   number that was meant. */
#app{ position:relative; height:100vh; height:100svh; width:100%; overflow:hidden; }

/* ============================ VIEWPORT ============================ */
/* deliberately NOT transitioned: the canvas is sized from this element's
   measured rect, and animating it means every resize during the animation
   measures a width that is about to change — which stretches the picture */
#viewport{ position:absolute; inset:0; overflow:hidden; background:#0b0908; }
/* The canvas owns every gesture that lands on it. Without this the browser
   keeps scrolling and pinching the PAGE under the game, and on a phone that is
   the difference between panning the map and dragging the whole site about. */
#gl{ touch-action:none; }
/* AND THE UI OWNS ITS OWN DOUBLE TAP. The canvas was covered by the rule above;
   the panels were not, so two quick taps on the brood rows — which is exactly
   how you buy two ants — were handed to Safari as zoom-the-page. `manipulation`
   is the narrow one: it turns off the double-tap and the 300ms click delay that
   comes with it, and leaves scrolling and pinching alone, which the settings
   sheet still needs.

   On the CONTAINERS, not on every descendant. The effective touch-action for a
   touch is the intersection all the way up from whatever it lands on, so one
   rule per panel covers every button inside it — and the range inputs keep
   their own stricter touch-action:none, because none intersects to none. */
/* .settings and .endgame are on this list for a reason worth stating: they are
   NOT inside #viewport. They are direct children of #app, siblings of the
   viewport, so #gl's touch-action above never covered them and neither did the
   first version of this line. Both are panels you tap the same spot in twice —
   a stepper walked from 1 to 3, a segmented switch cycled by re-tapping the lit
   half, STATS toggling to HIDE STATS in place — which is precisely the gesture
   iOS reads as zoom. On the hosts and not the cards, so the backdrop is covered
   too. */
#hud, .brood, #ctx, #radial, #btnRail, .title-inner, .sheet,
#settings, #endgame{ touch-action:manipulation; }
#gl{ position:absolute; inset:0; width:100%; height:100%; display:block; }
#overlay{ position:absolute; inset:0; width:100%; height:100%; pointer-events:none; }
#worldLabels{ position:absolute; inset:0; pointer-events:none; overflow:hidden; }

/* ============================ TITLE / RAIL ============================ */
/* The checkerboard, blended out by a gradient rather than tiling flat to every
   edge. The first layer is the topmost, so it is the panel's own base colour
   washing back over the weave: full strength at the wordmark, gone by the foot.
   Painted as a background layer instead of a mask because .title is a scroll
   container in game mode, and masking the element would take its text with it.
   rgb(19,16,16) is --bg — a var cannot be interpolated to alpha 0 here. */
.title{
  position:absolute; z-index:10;
  background-color:var(--bg);
  background-image:
    linear-gradient(165deg, rgba(19,16,16,0) 0%, rgba(19,16,16,.35) 42%,
                            rgba(19,16,16,.88) 84%, rgb(19,16,16) 100%),
    linear-gradient(45deg, rgba(255,255,255,.024) 25%, transparent 25%, transparent 75%, rgba(255,255,255,.024) 75%),
    linear-gradient(45deg, rgba(255,255,255,.024) 25%, transparent 25%, transparent 75%, rgba(255,255,255,.024) 75%);
  background-size:
    100% 100%,
    calc(var(--check)*2) calc(var(--check)*2),
    calc(var(--check)*2) calc(var(--check)*2);
  background-position: 0 0, 0 0, var(--check) var(--check);
  background-repeat: no-repeat, repeat, repeat;
}

/* --- full screen, before START ---
   THE FRONT DOOR IS A SHEET OF GLASS OVER A LIVE MAP. It used to be an opaque
   panel with the game's checkerboard on it, which was right when there was
   nothing behind it: the map was not built until GENERATE and there was nothing
   to look at. Now a world is built at boot and left to the AI, so the menu's
   whole job is to be readable over a war it must not hide.

   ONE LAYER DOES IT NOW, AND THAT IS THE POINT. There were three: a vignette
   over the middle of the screen, a card behind the controls, and a backdrop
   blur under the card. Two have gone and the notes on .title and .title-inner
   below record why, each arriving at the same answer — contrast belongs to the
   thing that has to be READ, not to everything behind it. What is left is the
   card: nearly opaque, carrying its own contrast, and small enough that the war
   goes on around it instead of behind a smear of it.

   AND IT SCROLLS, which it did not, and that was not a small bug. The card was
   815px tall; on a 900x560 laptop it ran 279px past the bottom of the window
   with `overflow:visible` above it, so START — the only thing on the page that
   matters — could not be reached at all. Any screen shorter than about 840px
   was a front door nobody could open.

   `safe center` is the whole fix and it is worth knowing why plain `center` is
   not: a centred flex item taller than its container overflows EQUALLY at both
   ends, and the top half goes to negative scroll offsets that no scrollbar can
   reach. `safe` falls back to flex-start the moment it would overflow, so a
   tall card starts at the top and scrolls down like anything else. */
#app.mode-title .title{
  inset:0; width:100%;
  display:flex; flex-direction:column;
  justify-content:safe center; align-items:center;
  padding:24px;
  overflow-y:auto;
  background-color:transparent;
  /* NO VIGNETTE. There was a radial wash here to lift the card off the map, and
     it cost the map: a live world dimmed towards the edges reads as a still
     with something laid over it, which is the exact impression a live backdrop
     exists to avoid. The card carries its own contrast now, which is the right
     place for it — contrast belongs to the thing that has to be read, not to
     everything behind it. */
  background-image:none;
}
#app.mode-title .title-inner{
  /* ONLY AS BIG AS THE MENU IT HOLDS. 560px of card with 26px of padding
     was a slab across the middle of the screen, and it cost twice: it hid
     the map, which is the whole argument for a live backdrop, and it sat on
     the sun's arc, which is the one control on this screen. The contents
     are one button, one disabled button, three links and a fold-out — none
     of which wanted 560px. */
  width:min(400px, 100%); flex:0 0 auto;
  background:rgba(13,11,10,.92);
  border:1px solid var(--line);
  border-radius:14px;
  padding:18px 20px 15px;
  /* AND THE SHADOW IS SMALL, which is that same argument a third time. It was
     `0 40px 90px -34px #000`: a 90px pure-black blur, which on a desktop is a
     drop shadow and on a 375px phone is a wash reaching 56px past both sides of
     a 327px card. Measured against a noon sky it visibly dulled the ground on
     either side — the vignette again, drawn with a different property, on the
     screens with the least room to give away. A card at 92% opacity with its own
     hairline only needs enough shadow to sit ON the map rather than in it. */
  box-shadow:0 14px 34px -16px rgba(0,0,0,.7), 0 0 0 1px rgba(0,0,0,.4);
  /* NO BACKDROP BLUR EITHER. It was doing the same job as the vignette by
     another route — smearing the world so the text would sit on something
     quiet — and it is the more expensive of the two: a blur of the live scene
     is recomputed every frame the scene moves, which is every frame. The card
     is nearly opaque instead. Solid enough to read against a noon sky, still
     glass enough to see the map is alive underneath it. */
}
/* DEV TOOLS IS NOT FRONT-DOOR FURNITURE. It is 146px of the card, which is the
   single biggest thing standing between a short screen and the START button —
   and it is a cheat menu with CONQUER ALL in it, sitting on the page a stranger
   sees first. It belongs to a game in progress, so it appears with the rail and
   not before. */
#app.mode-title .dev{ display:none; }
/* THE MAP IS NOT TREATED AT ALL. It was dimmed to a third once, then lifted to
   `brightness(.86) saturate(.92)` when it became a live backdrop, and it is now
   left exactly as the game draws it. Every one of those was a way of making the
   card easier to read by making the game worse to look at, and the card no
   longer needs the help. */
#app.mode-title #viewport{ opacity:1; filter:none; }

/* THE CLOCK IS NOT AT THE DOOR. The readout and its slider are how you tell and
   set the time inside a game; on the title screen the sky itself is the control
   — the sun and the moon can be picked up and moved — and a slider next to it
   would be a second way to do the same thing, sitting below the fold, out of
   sight of the gesture it duplicates. It comes back with the rail. */
#app.mode-title .foot-row{ display:none; }
/* ...and the gear goes with it. It was the second control in that footer, and
   with the clock gone it is a lone SETTINGS button sitting under a SETTINGS
   link that opens the same panel. One door, one way through it. */
#app.mode-title #btnSettings{ display:none; }

/* GRABBING THE SKY. Two cursors and nothing else: the disc is the affordance,
   and a phase this small does not need an outline drawn round it to be found.
   The class goes on #app because the thing under the cursor is a canvas the
   size of the window — there is no element to hang :hover on. */
#app.mode-title.sky-near #gl,
#app.mode-title.sky-near .title{ cursor:grab; }
#app.mode-title.sky-hold #gl,
#app.mode-title.sky-hold .title,
#app.mode-title.sky-hold .title-inner{ cursor:grabbing; }

/* A SHORT WINDOW GIVES UP PADDING BEFORE IT GIVES UP CONTROLS, in the same
   order the settings panel does: the gaps go first, the card's own padding
   next, and only then does anything scroll. */
@media (max-height:640px){
  #app.mode-title .title{ padding:12px; }
  #app.mode-title .title-inner{ padding:16px 18px 14px; border-radius:12px; }
}



/* --- slim rail, once the map is up --- */
#app.mode-game .title{
  left:0; top:0; bottom:0; width:330px;
  border-right:1px solid var(--line);
  overflow-y:auto; padding:16px 20px 12px;
}

/* ---- rail compaction ----
   Everything below is scoped to mode-game on purpose: the full-screen title has
   room to spare and is left exactly as it was. The rail does not. Nine summon
   buttons put the content 60px past the window, and the point of the rail is
   that the whole of it is in front of you — so the space comes out of the parts
   with slack (a brand mark that is decorative once you are in the game, and
   padding on rows that were sized for a full-screen menu) rather than out of the
   controls themselves. The sticky footer stays as the backstop for a window
   short enough that even this does not fit. */
#app.mode-game .opts{ margin-top:10px; gap:9px; }
#app.mode-game .opt label{ margin-bottom:4px; }
#app.mode-game .dev{ margin-top:9px; padding-top:8px; }
#app.mode-game .dev-head{ margin-bottom:4px; }
/* eleven of these, so a pixel off each is worth more than a pixel anywhere else.
   18px still leaves a comfortable target — the range inputs beside them are 17. */
#app.mode-game .dev-btn{ padding:3px 6px; }
#app.mode-game .title-foot{ padding-top:6px; }
#app.mode-game #viewport{ left:330px; }
#app.mode-game .title-inner{ width:100%; }

#app.no-menu .title{ display:none; }
#app.no-menu #viewport{ left:0; }

.title::-webkit-scrollbar{ width:8px; }
.title::-webkit-scrollbar-thumb{ background:#2b241e; border-radius:8px; }

/* ============================ WORDMARK ============================ */
.wordmark{
  margin:0 0 30px; display:flex; flex-direction:column;
  font-weight:900; letter-spacing:-.5px; line-height:.86;
  text-transform:uppercase;
}
.wm-top{ color:#f6f1e8; text-shadow:0 6px 26px rgba(0,0,0,.85); }
.wm-bot{ color:var(--amber); text-shadow:0 6px 26px rgba(0,0,0,.85); }

/* SIZED BY BOTH AXES. clamp(54px, 13vw, 96px) knew nothing about how TALL the
   window was, so a wide-but-short laptop got the full 96px — 165px of card, a
   fifth of the height, for the logo, on exactly the screens with none to spare.
   Taking the smaller of a width figure and a height figure keeps it large where
   there is room and lets it shrink where there is not. */
#app.mode-title .wordmark{
  /* It was up to 96px, which on a 400px card is most of the card. The
     wordmark still has to be the first thing read — it just does not have to
     be the only thing on screen. */
  /* svh HERE TOO, TO MATCH #app. After the change at the top of this file the
     box these sit in is the SMALL viewport, and type measured against the LARGE
     one is type measured against a box taller than the one it is inside - which
     is the exact mistake the note above this rule was written about, just on
     the other axis. The whole declaration is doubled rather than only the unit:
     one unknown unit anywhere inside clamp()/min() invalidates the entire
     declaration, so there would be nothing for a per-unit fallback to fall back
     to. On desktop svh and vh are the same number, so this changes nothing
     there. */
  font-size:clamp(28px, min(8.5vw, 9.5vh), 60px);
  font-size:clamp(28px, min(8.5vw, 9.5svh), 60px);
  margin-bottom:min(18px, 2.4vh);
  margin-bottom:min(18px, 2.4svh);
}
/* tightened so the rail still fits DEV TOOLS and the clock without scrolling —
   and tightened again when the summon buttons arrived. In the rail this is a
   brand mark rather than a title; the controls under it are what matters. */
#app.mode-game  .wordmark{ font-size:25px; margin-bottom:7px; }

/* ============================ ROWS ============================ */
/* The fold's two wrappers are a front-door device only. Everywhere else —
   which means the rail in mode-game — they are removed from the layout
   entirely, so .rows and .opts are the direct children they have always
   been and nothing about the menu changes. */
.door-fold, .door-fold-in{ display:contents; }
.rows{ display:flex; flex-direction:column; border-top:1px solid var(--line-soft); }

.row{
  position:relative; width:100%;
  border:0; border-bottom:1px solid var(--line-soft);
  background:transparent; color:var(--ink);
  font-family:var(--mono); font-size:13px; letter-spacing:3.4px;
  text-transform:uppercase; text-align:left;
  padding:17px 6px; display:flex; align-items:center; gap:12px;
  transition:background .16s, color .16s, padding-left .16s;
}
#app.mode-game .row{ padding:9px 4px; font-size:11.5px; letter-spacing:2.4px; }

/* GENERATE / OBSERVE / START carry 5px more left padding than the seed row above
   them, because they are the only rows with the hover tick. That mark sits at
   left:0 and runs 8px wide, and the rail's 4px padding had it overlapping the
   first letter — the tick and the G of GENERATE sharing the same pixels. The
   text now starts clear of it in both shells. */
.row-act{ cursor:pointer; padding-left:11px; }
.row-act:hover:not(:disabled){ background:rgba(226,166,58,.07); color:var(--amber-2); padding-left:19px; }
/* Restated at ID strength, and it has to come AFTER `#app.mode-game .row`: that
   rule sets padding with the shorthand, which carries a padding-left of its own,
   and an ID beats a bare class. Without this the rail would flatten these three
   rows straight back to 4px and put the tick under the text again. */
#app.mode-game .row-act{ padding-left:9px; }
.row-act:disabled{ color:var(--ink-off); cursor:not-allowed; }
/* the amber tick that slides in on hover */
.row-act::before{
  content:''; position:absolute; left:0; top:50%; width:0; height:2px;
  background:var(--amber); transform:translateY(-50%); transition:width .16s;
}
.row-act:hover:not(:disabled)::before{ width:8px; }

.row-seed label{
  color:var(--ink-off); font-size:11px; letter-spacing:3px; flex:0 0 auto;
}
#seed{
  flex:1; min-width:0; background:transparent; border:0; outline:none;
  color:var(--ink); font-family:var(--mono); font-size:13px; letter-spacing:2.4px;
  text-transform:uppercase; padding:0;
}
#seed::placeholder{ color:var(--ink-off); text-transform:lowercase; letter-spacing:2px; }
.seedbtn{
  flex:0 0 auto; background:transparent; border:0; color:var(--ink-off);
  cursor:pointer; display:grid; place-items:center; padding:2px; transition:.18s;
}
.seedbtn:hover{ color:var(--amber); }
.dice:hover{ transform:rotate(-14deg); }

/* copy: the tick sits on top of the clipboard and swaps in once it has copied */
#btnCopy{ position:relative; }
#btnCopy .tick{ position:absolute; inset:0; margin:auto; opacity:0; transform:scale(.5); transition:.18s; }
#btnCopy.copied{ color:var(--green); }
#btnCopy.copied svg:not(.tick){ opacity:0; }
#btnCopy.copied .tick{ opacity:1; transform:scale(1); }

/* Rolling: lit amber and spinning while it cycles through seeds, so the churn
   in the field reads as the dice working rather than the box glitching. */
.dice.rolling{ color:var(--amber-2); animation:diceRoll .42s linear infinite; }
.dice.rolling:hover{ transform:none; }
@keyframes diceRoll{ to{ transform:rotate(360deg); } }
.row-seed.rolling #seed{ color:var(--amber); opacity:.75; }

/* ============================ OPTIONS ============================ */
.opts{ margin-top:16px; display:flex; flex-direction:column; gap:12px; }
.opt label{
  display:block; font-family:var(--mono); font-size:10px; letter-spacing:2.6px;
  color:var(--ink-off); margin-bottom:7px; text-transform:uppercase;
}
.opt label b{ color:var(--amber); float:right; letter-spacing:1px; font-weight:600; }

input[type=range]{
  -webkit-appearance:none; appearance:none; width:100%; height:18px; background:transparent; cursor:pointer;
  /* ---- touch-action:none ON EVERY DEVICE, and it used to be on some ----

     This lived in the (pointer: coarse) block below, which reads as exactly right
     — it is a touch problem, so gate it on touch — and it left a hole the shape of
     an iPad. Reported as: dragging the ants-per-order slider SWITCHES BROWSER TABS.

     touch-action does not inherit, but the effective value for a gesture is the
     intersection down the tree, and #hud above sets manipulation — which permits
     panning. So on any device the coarse query does not match, a horizontal drag
     beginning on a slider is a pan as far as the browser is concerned, and Safari
     is entitled to take it as a swipe. Taking it as a swipe between tabs is a
     reasonable thing for a browser to do with a gesture nobody claimed.

     NOTHING ON ANY POINTER NEEDS TO PAN BY DRAGGING A SLIDER, so the query was
     never buying anything. js/ui.js has driven these from pointer and touch events
     since the day the native gesture stopped working on iOS; the declarative half
     should say so unconditionally rather than asking first. The coarse block keeps
     the part that IS about fingers: the taller hit box and the bigger thumb. */
  touch-action:none;
  /* --thumb is READ BY ui.js, which drives these from pointer events rather
     than leaving them to the native gesture. The travel of a range input is
     its width less one thumb, so the drag maths needs this number and there
     must not be a second copy of it in the script to drift out of step with
     the ::-webkit-slider-thumb rules below. */
  --thumb:13px;
}
input[type=range]::-webkit-slider-runnable-track{ height:2px; background:#3a3029; }
input[type=range]::-webkit-slider-thumb{
  -webkit-appearance:none; width:13px; height:13px; margin-top:-5.5px; border-radius:50%;
  background:var(--amber); box-shadow:0 0 0 3px rgba(226,166,58,.16);
}
input[type=range]::-moz-range-track{ height:2px; background:#3a3029; }
input[type=range]::-moz-range-thumb{ width:12px;height:12px;border:0;border-radius:50%;background:var(--amber); }

.hint{ font-family:var(--mono); font-size:10px; letter-spacing:1.4px; color:var(--ink-off); }

/* ============================ FOOTER CLOCK ============================ */
.title-foot{
  margin-top:14px; padding-top:10px; border-top:1px solid var(--line-soft);
  display:flex; flex-direction:column; gap:5px;
}
/* the time is a row of two: a readout that cannot be dragged, and a slider that
   can. The clock is a fixed width and the slider takes what is left, so nothing
   in here moves as the day runs. */
.foot-row{ display:flex; align-items:center; justify-content:space-between; gap:10px; }
/* Pinned to the foot of the rail rather than sitting at the end of its content.
   The rail scrolls once DEV TOOLS is full, and the clock and the sun slider are
   controls, not the tail of a list — they used to scroll away out of reach. */
#app.mode-game .title-foot{
  margin-top:auto; position:sticky; bottom:0; z-index:2;
  background:var(--bg); padding-bottom:2px;
}
/* A readout, not a control: no pointer affordance, and the lit background that
   used to be the :hover state is simply always on. */
/* Fixed width, not shrink-to-fit. The label runs from "—" to "NIGHT 180s", and
   letting the bubble size itself meant the slider beside it grew and shrank by
   65px as the countdown ticked past 100s and 10s. 108px clears the longest
   label; the countdown now changes only what is inside the bubble. */
/* THE GEAR, under the time and in the middle of the rail. On its own row rather
   than tucked at one end of the time row: it is the only control down here that
   is not about the clock, and a thing on the end of a row reads as part of that
   row. Centred because there is nothing to line it up with.

   It SPINS on hover — a gear is the one icon in the game that has an obvious
   thing to do when you point at it, and a cog that turns says "this opens the
   works" better than any tooltip. The turn is on the SVG and not on the button,
   so the box, its border and its background hold still while the cog goes
   round; spinning the button would smear its rounded corners. Slow, because a
   fast one reads as a loading spinner — which is a thing you wait for rather
   than a thing you press. */
.gear{
  flex:0 0 auto; align-self:center; height:21px;
  display:flex; align-items:center; gap:6px; padding:0 10px 0 7px; cursor:pointer;
  background:rgba(226,166,58,.08); border:1px solid transparent; border-radius:7px;
  color:var(--ink-dim); transition:color .16s, background .16s, border-color .16s;
  font-family:var(--mono); font-size:9px; letter-spacing:2.2px;
}
.gear span{ line-height:1; }
.gear:hover{ color:var(--amber); border-color:#3d3227; background:rgba(226,166,58,.14); }
.gear svg{ display:block; }
.gear:hover svg{ animation:gearSpin 2.6s linear infinite; }
@keyframes gearSpin{ to{ transform:rotate(360deg); } }
/* the colour still answers the pointer; only the turning stops */
@media (prefers-reduced-motion: reduce){ .gear:hover svg{ animation:none; } }

/* AND THE GEAR ITSELF IS THE HANDLE INTO ALL OF THAT, at 21px tall.

   Every control INSIDE the settings panel was given a coarse size — the close
   at 44, the steppers at 40, the segmented switches taller — and the one thing
   the sweep stepped over was the button that opens it. Which makes it the
   narrowest part of the path rather than an incidental one: during a running
   game this is the ONLY way in on a touchscreen, because the door's own
   SETTINGS link is display:none outside mode-title and the key needs a keyboard.
   Adding CHUNK GRID to that panel made it carry more, not less.

   NO NEGATIVE MARGIN HERE, which is what .sheet-x and .set-x use. Those sit in
   headers with slack above and below. This does not: #timeWrap's live area
   begins five pixels above it and scrubs the sky on POINTERDOWN, so a tap that
   misses high does not do nothing — it jumps the time of day. The footer is
   given real height instead, and only where there is a finger. */
@media (pointer: coarse){
  .gear{ height:44px; padding:0 14px 0 11px; }
  .title-foot{ gap:10px; }
}

.clock{
  display:flex; align-items:center; gap:8px; cursor:default;
  flex:0 0 108px; height:var(--foot-h); overflow:hidden; white-space:nowrap;
  font-family:var(--mono); font-size:10.5px; color:var(--ink-dim); letter-spacing:1.4px;
  padding:5px 8px; border-radius:7px;
  background:rgba(226,166,58,.08);
}
.clock .dot{ flex:0 0 auto; }
.clock.night{ background:rgba(111,134,216,.10); }
.clock .dot{ width:7px; height:7px; border-radius:50%; background:var(--amber); box-shadow:0 0 9px var(--amber); transition:.6s; }
.clock.night .dot{ background:#6f86d8; box-shadow:0 0 9px #6f86d8; }

/* ---- time of day, beside the clock ----
   The border is a live gradient of the sky: --sky-lo is the horizon, --sky-hi
   the zenith, both written from the running sun. Done with the two-background
   trick — panel fill clipped to the padding box, gradient to the border box —
   because a border cannot take a gradient directly. */
/* #timeWrap, not the input, is what the pointer talks to — see the scrubbing
   block in ui.js. touch-action:none so a touch drag scrubs instead of scrolling
   the rail, and so pointermove keeps arriving once the finger leaves the pill. */
.timewrap{
  flex:1; min-width:0; height:var(--foot-h);
  cursor:pointer; touch-action:none;
  padding:2px 10px; border:1px solid transparent; border-radius:7px;
  background:
    /* the same sky inside the pill as around it — fill and border in step */
    linear-gradient(100deg, var(--sky-lo, #26364f), var(--sky-hi, #7fb2e5)) padding-box,
    linear-gradient(100deg, var(--sky-lo, #26364f), var(--sky-hi, #7fb2e5)) border-box;
}

/* Every rule below is written input[type=range].timeslider rather than a bare
   .timeslider. The generic range styling further up is `input[type=range]`,
   specificity (0,1,1), which silently beat a lone class at (0,1,0) — so the
   track kept the generic 2px #3a3029 rail no matter how many times it was set
   transparent here. (The night thumb worked only because .timewrap.is-night
   .timeslider reaches (0,2,0) and does clear the bar.) */
/* padding and margin zeroed explicitly: Chrome's UA sheet gives a range input
   its own padding, which shifted the track off the middle of the box and took
   the sun with it. With these at 0 the 2px track centres in the 17px input, and
   the thumb's -6.5px margin then centres the disc on the track. */
/* pointer-events:none is load-bearing, not cosmetic. A range input clamps at its
   ends, so it can never be dragged past midnight, and correcting the value after
   the fact loses — the browser recomputes it from the pointer every move. Taking
   the input out of hit-testing leaves nothing native to fight, and #timeWrap
   drives it instead. The input is still focusable, so the keyboard is unaffected. */
input[type=range].timeslider{ display:block; width:100%; height:17px; padding:0; margin:0;
  pointer-events:none; }
input[type=range].timeslider::-webkit-slider-runnable-track{ height:2px; background:transparent; }
input[type=range].timeslider::-moz-range-track{ height:2px; background:transparent; border:0; }

/* The thumb IS the body in the sky, and it is driven from the live sun rather
   than approximated: --sun-col is the disc colour the sky shader is using this
   frame, so the handle warms to orange at dusk exactly as the sun does. The
   glow is that same colour, so it matches the halo around it too. */
input[type=range].timeslider::-webkit-slider-thumb{
  -webkit-appearance:none; width:15px; height:15px; margin-top:-6.5px; border-radius:50%;
  background:
    radial-gradient(circle at 42% 38%, #fffdf4 0%, var(--sun-col, #f7c85f) 52%,
                    color-mix(in srgb, var(--sun-col, #f7c85f) 72%, #000) 100%);
  box-shadow:0 0 0 1.5px rgba(0,0,0,.45),
             0 0 10px 2px color-mix(in srgb, var(--sun-col, #f7c85f) 70%, transparent);
  cursor:pointer;
}
input[type=range].timeslider::-moz-range-thumb{
  width:15px; height:15px; border:0; border-radius:50%;
  background:
    radial-gradient(circle at 42% 38%, #fffdf4 0%, var(--sun-col, #f7c85f) 52%,
                    color-mix(in srgb, var(--sun-col, #f7c85f) 72%, #000) 100%);
  box-shadow:0 0 0 1.5px rgba(0,0,0,.45),
             0 0 10px 2px color-mix(in srgb, var(--sun-col, #f7c85f) 70%, transparent);
  cursor:pointer;
}

/* After dark it becomes the moon, and carries the SAME PHASE the sky does. The
   shape is built the way the shader builds it — a dark body of equal size
   riding in front — so --moon-off is simply that offset in pixels: 0 covers the
   disc for a new moon, a full diameter clears it for a full one. */
.timewrap.is-night input[type=range].timeslider::-webkit-slider-thumb{
  background:
    radial-gradient(circle 7.6px at calc(50% + var(--moon-off, 0px)) 50%,
                    rgba(9,11,26,.97) 0 99%, rgba(0,0,0,0) 100%),
    radial-gradient(circle at 40% 36%, #f6f8ff 0%, #ccd6fa 55%, #9aa8e0 100%);
  box-shadow:0 0 0 1.5px rgba(0,0,0,.45),
             0 0 10px 2px rgba(150,168,255,var(--moon-glow, .5));
}
.timewrap.is-night input[type=range].timeslider::-moz-range-thumb{
  background:
    radial-gradient(circle 7.6px at calc(50% + var(--moon-off, 0px)) 50%,
                    rgba(9,11,26,.97) 0 99%, rgba(0,0,0,0) 100%),
    radial-gradient(circle at 40% 36%, #f6f8ff 0%, #ccd6fa 55%, #9aa8e0 100%);
  box-shadow:0 0 0 1.5px rgba(0,0,0,.45),
             0 0 10px 2px rgba(150,168,255,var(--moon-glow, .5));
}

/* No border and no button chrome: this is a scrubber like the clock, not
   something to click. The chevrons are the affordance — the cursor alone only
   shows up once you are already over it. */
/* ============================ WORLD LABELS ============================ */
.wlabel{
  position:absolute; transform:translate(-50%,-100%);
  font-size:9.5px; font-family:var(--mono); letter-spacing:1px;
  padding:2px 7px; border-radius:5px; white-space:nowrap;
  background:rgba(10,8,7,.78); border:1px solid rgba(255,255,255,.12);
  text-shadow:0 1px 2px #000;
}

/* ============================ PLANET LABELS ============================ */
.plabel{
  position:absolute; transform:translate(-50%,-190%);
  font-size:8px; font-family:var(--mono); letter-spacing:1.6px;
  color:#cdd6ee; text-shadow:0 1px 3px #000, 0 0 6px rgba(0,0,0,.9);
  white-space:nowrap; pointer-events:none;
}
#planetLabels{ position:absolute; inset:0; pointer-events:none; overflow:hidden; z-index:5; }

/* ============================ DEV TOOLS ============================ */
/* Sits in the rail under the options, as another section of the menu. */
.dev{
  margin-top:9px; padding-top:7px; border-top:1px solid var(--line-soft);
  display:flex; flex-direction:column; gap:2px;
}
/* The head carries the hint for the decks rather than a line of its own doing
   it: a rail that scrolls to tell you how to use the thing that stopped it
   scrolling has undone itself. */
.dev-head{
  display:flex; align-items:baseline; justify-content:space-between; gap:8px;
  font-family:var(--mono); font-size:10px; letter-spacing:2.6px;
  color:var(--ink-off); margin-bottom:3px; text-transform:uppercase;
}
.dev-head i{
  font-style:normal; font-size:7.5px; letter-spacing:.9px; opacity:.62;
  text-transform:none; white-space:nowrap;
}
.dev-btn{
  display:flex; align-items:center; gap:7px; width:100%;
  background:transparent; border:1px solid transparent; border-radius:6px;
  padding:4px 7px; cursor:pointer; color:var(--ink-dim);
  font-family:var(--mono); font-size:9px; letter-spacing:1.2px; text-align:left;
  transition:background .14s, color .14s, border-color .14s;
}
.dev-btn:hover{ background:rgba(226,166,58,.08); border-color:#3d3227; color:var(--ink); }
.dev-btn .dev-ico{ flex:0 0 auto; width:9px; text-align:center; font-size:10px; }
.dev-btn .dev-dot{
  flex:0 0 auto; width:7px; height:7px; border-radius:50%;
  background:#3a3029; transition:background .16s, box-shadow .16s;
}
/* Two columns, for the two one-offs. Everything that comes in a family is a
   deck below this instead — nine summons stacked full width pushed the clock and
   the sun slider off the bottom of the rail. */
.dev-grid{ display:grid; grid-template-columns:1fr 1fr; gap:2px; margin-top:2px; }
/* Size was set by the LONGEST label, back when "SUMMON SHOOTING STAR" shared
   this grid: twenty characters in a half-width button. The tracking had to come
   almost all the way out to buy the room. The long labels live on full-width
   deck cards now and have the space, but the two that are left keep these
   metrics so the row matches the cards under it. The ellipsis below is the
   backstop either way: these are set in whatever monospace the machine has, and
   the fallbacks are not all the same width. */
.dev-grid .dev-btn{ gap:4px; padding:4px 5px; font-size:9.5px; letter-spacing:.2px; min-width:0; }
/* min-width and display:block are what make the ellipsis work at all — overflow
   does not apply to an inline box, so on a bare span the rule is inert and the
   text just runs out of the button instead. */
.dev-grid .dev-btn span:last-child{
  display:block; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.dev-grid .dev-ico{ width:9px; font-size:10px; }
.dev-grid .dev-dot{ width:7px; height:7px; }
/* the worker and the soldier are the same glyph — size is what tells them apart,
   exactly as in the brood panel */
.dev-ico.ant-n{ font-size:8px; }
.dev-ico.ant-s{ font-size:12px; }

/* the toggle lights up when it is on */
.dev-btn[aria-pressed="true"]{ color:var(--amber); }
.dev-btn[aria-pressed="true"] .dev-dot{ background:var(--amber); box-shadow:0 0 8px var(--amber); }

/* ---- DECKS: one control, a family of actions behind it ----
   Three things that come in threes — the sky, the weather, the castes. A deck is
   the front one plus the others poking out underneath, and the wheel brings
   another to the front.

   THE PEEK HAS TO PAY FOR ITSELF. Twelve buttons in two columns was six rows;
   three decks is three, which saves five rows — and a deck is taller than a
   button by exactly the peek it shows. At 12px apiece that was 36px spent to
   save about 30, and the rail grew a scrollbar it had never had. 8px is still
   two clear slivers and it comes out ahead: 159px of rail before, 183 at 12px,
   148 now.

   The back cards are DELIBERATELY VISIBLE rather than tucked away. A control
   that hides what else it can do is a control nobody finds the rest of, and the
   two slivers under the front card are the only thing saying there is anything
   to scroll for. The hint line under the decks says it in words as well, because
   a sliver is a hint and not an instruction.

   The stack is built from transforms alone — no layout changes, so nothing
   reflows when a card moves and three cards animate for the price of one. */
.dev-decks{ display:flex; flex-direction:column; gap:2px; margin-top:2px; }
/* 21px card + the 8px the back one is dropped by. Every pixel of this is rail,
   and the rail has to end above the clock. */
.deck{ position:relative; height:29px; }
.deck .deck-card{
  position:absolute; left:0; right:0; top:0;
  gap:4px; padding:4px 5px; font-size:9.5px; letter-spacing:.2px; min-width:0;
  color:var(--ink-dim);
  /* THE RAIL'S OWN COLOUR, not a panel colour. SPIN MOON and CONQUER ALL above
     these are plain .dev-btn — transparent background, transparent border — and a
     stack that looked like a different kind of control sat oddly two rows under
     them. Painting var(--bg) LOOKS like transparent against the rail and IS
     opaque, which is the one thing a card in a pile has to be. */
  background:var(--bg); border:1px solid transparent;
  /* the overshoot at the end is what makes it read as a card being PUT DOWN on
     the top of the pile rather than a box sliding into position */
  transition:transform .28s cubic-bezier(.22,1.15,.36,1),
             background .18s ease, border-color .18s ease, color .18s ease;
}
.deck .deck-card span:last-child{
  display:block; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
/* DEPTH IS SHADE, NOT OPACITY, and the first version had it the other way
   round. `opacity` fades the whole element, background included, so a back card
   went see-through: you read the rail and the card under it straight through the
   one in front, and mid-turn two half-transparent cards overlapped into a smear.
   That is not what a card further down a pile looks like. A card is opaque and
   simply sits in less light.

   The FRONT card is a plain .dev-btn and nothing else — same colour as the rail,
   no border — so the stack does not read as a different species of control from
   the two buttons above it. The two behind it are that same card a shade up
   with their edge drawn, which is all a sliver needs to read as an edge rather
   than as nothing. Every one of them is solid: nothing shows through anything,
   and the animation has one less property to carry. */
.deck .deck-card.p0{ transform:none; z-index:3; }
.deck .deck-card.p1{ transform:translateY(4px) scale(.972); z-index:2; background:#1a1514; border-color:#2e2720; }
.deck .deck-card.p2{ transform:translateY(8px) scale(.944); z-index:1; background:#171312; border-color:#262019; }
.deck .deck-card:not(.p0){ pointer-events:none; }
/* WHOSE — EVERY COLONY ON THE MAP, on the cards that make something owned. It
   rides the DECK rather than the card so it is written once instead of on all
   three faces, and it sits over the front card at a higher z-index, which is
   also what keeps a click on it from reaching the summon underneath. The colours
   are the colonies' own, the same swatches the map labels and the war log use.

   A row rather than one cycling pip: a pip made you click through the roster to
   find out what the roster was, and eight of these cost 70px of a card whose
   label needs about 110. */
.deck-team{
  position:absolute; right:5px; top:0; bottom:0; z-index:4;
  display:flex; align-items:center; gap:3px;
}
.deck-team.hidden{ display:none; }
.deck-team .tp{
  width:7px; height:7px; padding:0; border-radius:50%; cursor:pointer;
  background:var(--tc); border:1px solid transparent;
  opacity:.34; transition:opacity .14s, transform .14s, box-shadow .14s;
}
.deck-team .tp:hover{ opacity:.8; transform:scale(1.18); }
/* the chosen one, and it has to be obvious at seven pixels: full strength, a
   ring of its own colour around it, and bigger than its neighbours */
.deck-team .tp.on{ opacity:1; transform:scale(1.28); box-shadow:0 0 0 1.5px rgba(0,0,0,.55), 0 0 7px -1px var(--tc); }
/* a colony that has fallen keeps its place and goes out. The gap would be the
   lie: COLONY C is still the third colony after it dies. */
.deck-team .tp.gone{ opacity:.12; filter:grayscale(1); cursor:default; transform:none; box-shadow:none; }
/* Opaque on hover too, and this is .dev-btn:hover WORKED OUT RATHER THAN
   COPIED: rgba(226,166,58,.08) over #131010 composites to #241c13, so the card
   lands on the same pixels the plain buttons do without ever being a tint you
   could see the rail through. An rgba() wash here was the other half of the
   opacity bug — the front card was solid until you pointed at it. */
.deck .deck-card.p0:hover{ background:#241c13; border-color:#3d3227; color:var(--ink); }
@media (prefers-reduced-motion: reduce){
  .deck .deck-card{ transition:background .18s, border-color .18s, color .18s; }
}

/* ============================ WAR LOG ============================ */
.log{ position:absolute; right:14px; top:14px; width:264px; display:flex; flex-direction:column; gap:6px; pointer-events:none; z-index:6; }
.log .entry{
  background:rgba(12,9,8,.9); border-left:2px solid var(--amber);
  border-radius:0 7px 7px 0; padding:7px 10px; font-size:11px; color:#d6c9bb;
  box-shadow:0 8px 24px -14px #000; animation:slideIn .28s ease both; line-height:1.35;
}
@keyframes slideIn{ from{ opacity:0; transform:translateX(20px);} to{ opacity:1; transform:none; } }
.log .entry.fade{ animation:fadeOut .5s ease forwards; }
@keyframes fadeOut{ to{ opacity:0; transform:translateX(20px); } }

/* ============================ DIAGNOSTICS ============================ */
/* Top-left of the viewport, opposite the war log. Deliberately plain and
   deliberately narrow: it is a readout over a live game, so it has to be
   legible against anything the map puts behind it and has to stay out of the
   way of the thing it is measuring. */
.probe{
  position:absolute; left:14px; top:14px; width:252px; z-index:7;
  background:rgba(10,8,7,.93); border:1px solid var(--line); border-radius:9px;
  padding:8px 10px 8px; pointer-events:auto;
  font-family:var(--mono); font-size:9px; letter-spacing:.4px; color:var(--ink-dim);
  box-shadow:0 14px 40px -22px #000;
}
.probe.hidden{ display:none; }
.pb-head{
  font-size:8.5px; letter-spacing:2.4px; color:var(--ink-off);
  padding-bottom:6px; margin-bottom:6px; border-bottom:1px solid var(--line-soft);
}
.pb-head b{ float:right; color:var(--green); letter-spacing:1px; }
.pb-head b.warn{ color:var(--amber); }
.pb-head b.bad{ color:var(--red); }

.pb-row{ display:flex; align-items:center; gap:6px; line-height:1.7; }
.pb-k{ flex:0 0 48px; color:var(--ink-off); }
/* the bar is share of the frame, not milliseconds against a number picked out
   of the air — see the note in probe.js */
.pb-bar{ flex:1; height:4px; background:#241e19; border-radius:3px; overflow:hidden; }
.pb-bar i{ display:block; height:100%; background:var(--amber); }
.pb-v{ flex:0 0 34px; text-align:right; color:var(--ink); }
.pb-v.dim{ flex:0 0 32px; color:var(--ink-off); }
.pb-note{ color:var(--ink-off); margin-top:5px; line-height:1.5; }
.pb-note.warn{ color:var(--amber); }

.pb-list{ margin-top:7px; padding-top:6px; border-top:1px solid var(--line-soft);
  max-height:190px; overflow-y:auto; }
.pb-list::-webkit-scrollbar{ width:6px; }
.pb-list::-webkit-scrollbar-thumb{ background:#2b241e; border-radius:6px; }
.pb-clean{ color:var(--green); }
.pb-hit{ display:flex; align-items:baseline; gap:6px; flex-wrap:wrap; margin-bottom:4px; }
.pb-id{ flex:0 0 auto; color:var(--red); }
.pb-n{
  flex:0 0 auto; min-width:16px; padding:0 4px; border-radius:7px; text-align:center;
  background:#3a1f1c; color:#f0b0a8; font-size:8px; line-height:13px;
}
.pb-d{ flex:1 0 100%; color:var(--ink-off); line-height:1.45; }

.pb-foot{ display:flex; gap:6px; margin-top:8px; padding-top:7px; border-top:1px solid var(--line-soft); }
.pb-foot button{
  flex:1; background:transparent; border:1px solid #33291f; border-radius:6px;
  color:var(--ink-off); font-family:var(--mono); font-size:8px; letter-spacing:1.4px;
  padding:5px 4px; cursor:pointer; transition:.14s;
}
.pb-foot button:hover{ border-color:var(--amber); color:var(--amber); }

/* ============================ HUD ============================ */
/* a column, so nothing has to be positioned into a gap that a narrow
   viewport does not actually have */
.hud{
  position:absolute; left:0; right:0; bottom:0; padding:0 16px 12px; z-index:6;
  pointer-events:none; display:flex; flex-direction:column; align-items:stretch; gap:9px;
}
.hud.hidden{ display:none; }

/* --- comets a colony is holding ---
   A box whose BORDER IS THE CLOCK. The rim is one SVG rect using its own
   perimeter as the dash pattern, so pushing the offset undraws it a side at a
   time and the glow retreats round the box as the 45 seconds burn down. No
   number on it: how much border is left is how much time is left.
   Every colour on it comes from --cc, which ui.js sets to the owning colony's,
   so a badge and the comet overhead are visibly the same thing. */
.comets{
  position:absolute; left:0; right:0; top:12px; z-index:8;
  display:flex; gap:7px; justify-content:center; flex-wrap:wrap;
  pointer-events:none;
}
.comets.empty{ display:none; }
.comet{
  position:relative; width:30px; height:30px; flex:0 0 auto;
  animation:cometIn .3s ease both;
  /* the box itself is faintly the colony's colour, so a bare rim still reads */
  background:color-mix(in srgb, var(--cc) 12%, rgba(10,8,7,.86));
  border-radius:6px;
}
@keyframes cometIn{ from{ opacity:0; transform:scale(.55) translateY(6px);} to{opacity:1;transform:none;} }
.comet svg{ position:absolute; inset:0; overflow:visible; }
.comet .rim{
  fill:none; stroke:var(--cc); stroke-width:2.4; stroke-linecap:butt;
  /* the glow is the same colour as the stroke, so the rim reads as burning
     rather than as a plain outline */
  filter:drop-shadow(0 0 3px var(--cc)) drop-shadow(0 0 7px var(--cc));
}
.comet .ico{
  position:absolute; inset:0; display:grid; place-items:center;
  font-size:13px; line-height:1; color:var(--cc);
  text-shadow:0 0 6px var(--cc), 0 1px 2px #000;
}
/* the player's own is the one that matters, so it is the one that is solid */
.comet.mine{ background:color-mix(in srgb, var(--cc) 22%, rgba(10,8,7,.9)); }
.comet:not(.mine){ opacity:.72; transform:scale(.88); }
/* about to lapse */
.comet.going .rim{ animation:cometGo .7s ease-in-out infinite; }
@keyframes cometGo{ 50%{ opacity:.35; } }

/* THE ROW LETS CLICKS THROUGH; THE CHIPS DO NOT. `.hud` is pointer-events:none
   and its children opt back in, and this one opted in as a CONTAINER — which
   stretches the full width of the HUD because the column stretches its items. So
   a band the height of a chip and the width of the screen was swallowing clicks
   either side of the three visible ones, and the map underneath could not be
   right-clicked anywhere along it. Nothing looked wrong, because the box that
   was catching them is invisible.
   The chips take their own clicks instead — they are the only part of this that
   has anything to answer. */
.cooldowns{ display:flex; gap:8px; justify-content:center; flex-wrap:wrap; pointer-events:none; order:2; }
.cd{
  pointer-events:auto; cursor:pointer;
  position:relative; width:92px; height:50px; border-radius:8px; overflow:hidden;
  background:rgba(14,11,9,.92); border:1px solid var(--line); text-align:center;
}
.cd .cd-water{
  position:absolute; left:0; right:0; bottom:0; height:0%; z-index:1;
  background:linear-gradient(180deg, rgba(226,166,58,.30), rgba(226,166,58,.07));
  border-top:1.5px solid rgba(247,200,95,.9);
  transition:height .09s linear, opacity .3s;
}
.cd .cd-inner{ position:absolute; left:0; right:0; top:6px; z-index:2; pointer-events:none; }
.cd .cd-inner.desat{ filter:saturate(.04) brightness(.5); }
.cd .cd-inner.sat{ clip-path:inset(100% 0 0 0); }
.cd .cd-ico{ font-size:14px; line-height:1; height:16px; }
.cd .cd-ico svg{ vertical-align:top; }
.cd .cd-name{ font-size:8.5px; letter-spacing:1.5px; margin-top:3px; color:#b3a596; }
.cd .cd-time{
  position:absolute; left:0; right:0; bottom:4px; z-index:3;
  font-size:9px; font-family:var(--mono); color:var(--ink-off); letter-spacing:.5px;
}
.cd.ready{ border-color:#4a3c2c; }
.cd.ready .cd-time{ color:var(--amber); }
/* How many ants are out on this order — and therefore how many clicking it
   brings home. Top-right so it clears the icon and the label underneath. */
.cd .cd-count{
  position:absolute; right:5px; top:4px; z-index:3;
  min-width:15px; height:15px; padding:0 3px; border-radius:8px;
  font-family:var(--mono); font-size:9px; font-weight:700; line-height:15px;
  text-align:center; color:#1a1208; background:var(--amber);
  box-shadow:0 0 0 1px rgba(0,0,0,.45);
}
.cd .cd-count:empty{ display:none; }
/* only worth pressing when somebody is out on it */
.cd.manned{ cursor:pointer; }
.cd.manned:hover{ border-color:var(--amber); background:rgba(226,166,58,.10); }
/* ARMED: the selected chunk is carrying this order, so pressing the chip takes
   that one off rather than recalling every order of its kind. Without a state
   here the second route in is invisible — nothing would say the chip had
   changed meaning under the cursor. */
.cd.armed{
  cursor:pointer; border-color:var(--amber);
  background:rgba(226,166,58,.12);
  box-shadow:0 0 0 1px rgba(226,166,58,.45), 0 0 14px -4px var(--amber);
}
.cd.armed .cd-name{ color:var(--amber-2); }
.cd.armed::after{
  content:'CANCEL'; position:absolute; left:0; right:0; bottom:4px; z-index:4;
  font-family:var(--mono); font-size:8px; letter-spacing:1.4px; color:var(--amber);
}
/* the countdown would sit under the CANCEL label; the chip is not about time
   while it is armed */
.cd.armed .cd-time{ opacity:0; }

.hud-bar{
  display:flex; align-items:center; gap:16px; flex-wrap:wrap; order:3;
  background:var(--panel); border:1px solid var(--line); border-radius:10px;
  padding:9px 16px; pointer-events:auto;
}
.ant-slider{ flex:1; }
.ant-slider label{ display:block; font-size:9.5px; letter-spacing:2px; text-transform:uppercase; color:var(--ink-off); margin-bottom:1px; font-family:var(--mono); }
.ant-slider label b{ color:var(--amber); float:right; letter-spacing:0; }
.hud-stat{ font-size:10.5px; font-family:var(--mono); color:var(--ink-dim); letter-spacing:.5px; white-space:nowrap; min-width:128px; }
.hud-stat i{ display:inline-block; width:9px; height:9px; border-radius:2px; margin-right:6px; vertical-align:-1px; }

/* --- brood priority --- */
/* ---- SLIDING THE PANELS OUT OF THE WAY ----

   Both of these exist because the HUD is 245px of a 667px phone even after
   being compacted, and the two biggest pieces of it are things you look at in
   bursts rather than continuously: the brood panel when you are spending
   larvae, the order size when you are changing it. The chips are the only part
   that is wanted every minute, so the chips are the part that never leaves.

   THE BROOD PANEL GOES SIDEWAYS AND ITS HANDLE DOES NOT. translateX(100%) is
   exactly its own width, so the panel clears the screen edge and the handle,
   hanging 26px off its left, ends up sitting ON that edge. Nothing is
   positioned in screen coordinates and nothing has to know how wide the screen
   is — the handle is placed relative to the thing it moves with.

   THE ORDER SIZE GOES DOWN, and the chips follow it down. That is what a grid
   track collapsing does in a bottom-anchored flex column: the row stops taking
   height, and everything above it drops into the space. 0fr to 1fr rather than
   max-height for the reason the door fold gives — a track interpolates to the
   content height, so there is no number to keep in step when a row is added. */
.brood{
  position:relative; transition:transform .28s cubic-bezier(.22,.61,.36,1);
  width:212px; margin-left:auto; order:1;
  background:var(--panel); border:1px solid var(--line); border-radius:10px;
  padding:9px 10px 10px; pointer-events:auto;
}
/* --- the threat line ---
   Always on, and the only thing in the panel that is allowed to shout. Four
   rungs, each louder than the last; SAFE is deliberately quiet, because a line
   you stop noticing is exactly what makes the other three land. */
.brood.away{ transform:translateX(100%); }
.brood-grab{
  position:absolute; left:-26px; top:6px; z-index:2;
  width:24px; height:46px; padding:0; cursor:pointer; pointer-events:auto;
  display:grid; place-items:center;
  background:rgba(14,11,9,.92); border:1px solid var(--line);
  border-radius:8px 0 0 8px; border-right:0;
  color:var(--ink-off); font-size:15px; line-height:1; transition:color .14s, border-color .14s;
}
.brood-grab:hover{ color:var(--ink); border-color:#3d3227; }
.brood.away .brood-grab{ border-radius:8px; border-right:1px solid var(--line); }

/* THE EDGE IS THE ALARM WHILE THE PANEL IS AWAY. Same three rungs and the same
   two keyframes the threat line uses, so there is one vocabulary for "something
   is wrong" and not two. Only while away: with the panel open the line itself is
   already saying it, and two things flashing the same fact is one too many. */
/* The edge FLASHES at warn where the threat line only changes colour, and that
   is a deliberate difference rather than an inconsistency: the line is a
   sentence you can read, and this is 24px on the rim of the screen with the
   panel that would have explained it pushed off. It has to catch an eye that is
   not looking at it. Slower than bad, so the rungs still rank by urgency. */
.brood.away .brood-grab.warn{
  color:#ff9522; background:rgba(255,140,26,.16); border-color:#a85c14;
  animation:threatPulse 2.2s ease-in-out infinite;
}
.brood.away .brood-grab.bad{
  color:#ff503c; background:rgba(255,60,40,.20); border-color:#b02a1c;
  animation:threatPulse 1.5s ease-in-out infinite;
}
.brood.away .brood-grab.dire{ animation:threatStrobe .7s step-end infinite; }

.cd-row{ display:flex; align-items:center; justify-content:center; gap:8px; order:2; pointer-events:none; }

/* THE LIP, AND IT IS THE WHOLE WIDTH even though only 38px of it is painted:
   what you are asked to hit should be the strip, not the mark on it. The mark is
   a pseudo-element for the same reason — a 3px bar is not a glyph anybody should
   have to aim at. Transparent, so with the drawer shut the HUD ends in a rule
   over the map rather than in another panel. */
.hud-grab{
  order:0; flex:0 0 auto; pointer-events:auto; cursor:pointer;
  width:100%; height:20px; padding:0; display:grid; place-items:center;
  background:transparent; border:0;
}
.hud-grab::before{
  content:''; display:block; width:38px; height:3px; border-radius:2px;
  background:var(--line); transition:background .14s, width .14s;
}
.hud-grab:hover::before{ background:var(--ink-dim); width:46px; }

.hud-fold{ order:3; display:flex; flex-direction:column; }
.hud-fold-in{
  display:grid; grid-template-rows:1fr;
  transition:grid-template-rows .28s cubic-bezier(.22,.61,.36,1);
}
.hud-fold-in.away{ grid-template-rows:0fr; }
/* IT LEAVES, RATHER THAN BEING SQUEEZED. Clipping the track alone was read as
   the bar shrinking on itself: the box got shorter, and because the bar is
   pinned to the bottom of it the top edge crept down the face of something that
   was otherwise standing still. The brood panel next door simply goes — it
   translates off the edge and back — and the two are the same gesture on the
   same HUD, so they should read the same way.

   So the bar now TRANSLATES down out of the clip on the same curve the track
   closes on. The track still collapses, because that is what gives the map back
   the space; the translate is what makes it look like leaving instead of
   wilting. align-items:flex-end stays: it keeps the bar's own height while the
   box around it shrinks, so the thing sliding is a whole bar and not a stub. */
.hud-clip{ overflow:hidden; min-height:0; display:flex; align-items:flex-end; }
.hud-clip > .hud-bar{
  width:100%;
  transition:transform .28s cubic-bezier(.22,.61,.36,1);
}
.hud-fold-in.away .hud-bar{ transform:translateY(100%); }

@media (prefers-reduced-motion: reduce){
  .hud-clip > .hud-bar{ transition:none; }
  .brood{ transition:none; }
  .hud-fold{ transition:none; }
  .brood.away .brood-grab.warn{ animation:none; }
  .brood.away .brood-grab.bad{ animation:none; }
  .brood.away .brood-grab.dire{
    animation:none; color:#2a0a06; background:#ff3c28; border-color:#ffb0a2;
  }
}

.brood-threat{
  font-family:var(--mono); font-size:8.5px; letter-spacing:2.2px; text-align:center;
  padding:4px 6px; margin:-2px -3px 7px; border-radius:6px;
  border:1px solid transparent;
}
/* SIX STATES, AND EACH HAZARD RUNG IS LOUDER THREE WAYS AT ONCE — hue, box and motion.
   Any one of them alone is too close to call at a glance on a panel this small,
   and the whole point of the line is that it is read without being looked at.

     safe   the larvae's own amber, flat, still     a readout, not a whisper
     near   the same amber, but the BOX comes up     they are at the fence
     warn   hazard orange, lit border               caution
     bad    hazard red, glowing, slow pulse         alarm
     dire   STROBING red, hard square wave          the hill itself
     gone   flat grey, wide tracking                nothing left to do

   The second rung changes the box and not the hue, deliberately. An enemy
   standing just outside your ground is worth noticing and is not yet worth a
   colour — keeping amber for both and lighting the border instead is what makes
   the step up to orange mean "they are ON it".

   The top rung strobes rather than pulses: a brightness ramp reads as a glow
   and a hard alternation reads as a beacon, and the difference is exactly the
   difference between "something is wrong" and "it is at your door". */
.brood-threat.safe{ color:var(--amber); background:rgba(255,255,255,.03); }
.brood-threat.near{
  color:var(--amber-2); background:rgba(226,166,58,.13); border-color:#6a5124;
  text-shadow:0 0 7px rgba(247,200,95,.45);
}

.brood-threat.warn{
  color:#ff9522; background:rgba(255,140,26,.16); border-color:#a85c14;
  text-shadow:0 0 8px rgba(255,149,34,.55);
}
.brood-threat.bad{
  color:#ff503c; background:rgba(255,60,40,.20); border-color:#b02a1c;
  text-shadow:0 0 9px rgba(255,80,60,.65);
  box-shadow:0 0 14px -6px rgba(255,60,40,.9);
  animation:threatPulse 1.5s ease-in-out infinite;
}
@keyframes threatPulse{ 50%{ filter:brightness(1.5); } }

/* step-end is what makes this a STROBE and not a fade: each keyframe holds
   until the next one rather than being interpolated towards it, so the two
   states snap. 0.7s a cycle is about 1.4 flashes a second — unmistakable, and
   comfortably under the three-a-second that photosensitivity guidance draws the
   line at. */
.brood-threat.dire{
  animation:threatStrobe .7s step-end infinite;
  font-weight:700;
}
@keyframes threatStrobe{
  0%{
    color:#2a0a06; background:#ff3c28; border-color:#ffb0a2;
    text-shadow:none; box-shadow:0 0 18px -2px rgba(255,60,40,.95);
  }
  50%{
    color:#ff6b5c; background:rgba(58,10,6,.95); border-color:#8c2a1c;
    text-shadow:0 0 10px rgba(255,107,92,.8); box-shadow:none;
  }
}

/* It is over. Sombre rather than loud: a strobe on a colony that no longer
   exists would be shouting about something nobody can do anything about. */
.brood-threat.gone{
  color:var(--ink-dim); background:rgba(120,110,100,.10); border-color:#3a332c;
  letter-spacing:4px;
}

/* Anything that flashes gets a way out. The colours still climb, so the ladder
   is intact without a single thing moving. */
@media (prefers-reduced-motion: reduce){
  .brood-threat.bad{ animation:none; }
  .brood-threat.dire{
    animation:none;
    color:#2a0a06; background:#ff3c28; border-color:#ffb0a2;
    box-shadow:0 0 18px -2px rgba(255,60,40,.95);
  }
}

.brood-head{
  display:flex; align-items:center; gap:8px; margin-bottom:7px;
  font-family:var(--mono); font-size:8.5px; letter-spacing:2.4px; color:var(--ink-off);
}
.brood-head .bh-name{ flex:0 0 auto; white-space:nowrap; }
/* the count rides with the word it qualifies — BROOD x3 — rather than taking a
   label of its own, which is what buys the slider its width */
.brood-head .bh-name i{
  font-style:normal; color:var(--amber); letter-spacing:0; margin-left:5px; font-weight:600;
}
/* Slimmer than a range input anywhere else in the game: it is sharing a row
   with 8.5px type, and the stock ClickTarget would stand a head above it. */
.brood-head .bh-qty{ flex:1 1 auto; min-width:40px; height:12px; --thumb:9px; }
.brood-head .bh-qty::-webkit-slider-runnable-track{ height:2px; }
.brood-head .bh-qty::-webkit-slider-thumb{
  width:9px; height:9px; margin-top:-3.5px; box-shadow:0 0 0 2px rgba(226,166,58,.16);
}
.brood-head .bh-qty::-moz-range-thumb{ width:9px; height:9px; }

/* A FINGER IS NOT A CURSOR, and these two sliders were drawn for a cursor.
   Measured on a 375x667 phone in a live game: #broodQty was 67x12 and
   #antLimit 175x18. Apple asks for 44px and Google for 48dp; twelve is not a
   near miss, it is a control you cannot put a thumb on.

   THE HIT AREA GROWS AND THE LAYOUT DOES NOT. Simply setting height:44px
   works and costs 58px of a screen that is already giving the HUD 47% of
   itself — so the box is grown and then pulled back with equal negative
   margins, which leaves the row exactly the height it was and the tappable
   area three times bigger. Checked afterwards that the enlarged boxes do not
   swallow the caste rows either side of them: all three still hit-test to
   themselves.

   (pointer: coarse) rather than a width query, because this is about what is
   pointing at the screen and not how wide it is — a tablet gets it, a narrow
   desktop window does not, and the desktop keeps the tighter look it wants. */
@media (pointer: coarse){
  /* THE SIZE, AND NOT THE GESTURE. touch-action moved up to the base rule: it
     was here, which meant it only applied where this query matched, and the
     device that suffered for it was the iPad. See the note beside it. What is
     left here is what genuinely is about having a finger rather than a mouse. */
  input[type=range]{
    height:38px; margin-top:-10px; margin-bottom:-10px;
    --thumb:20px;
  }
  input[type=range]::-webkit-slider-thumb{ width:20px; height:20px; margin-top:-9px; }
  input[type=range]::-moz-range-thumb{ width:20px; height:20px; }
  .brood-head .bh-qty{ height:38px; margin-top:-13px; margin-bottom:-13px; --thumb:18px; }
  .brood-head .bh-qty::-webkit-slider-thumb{ width:18px; height:18px; margin-top:-8px; }
  .brood-head .bh-qty::-moz-range-thumb{ width:18px; height:18px; }

  /* THE TWO HANDLES, AT FINGER SIZE. Both were drawn against a cursor, which
     can hit a 24px tab on the rim of the screen and a 20px strip without
     thinking about it. A thumb cannot, and the brood tab was reported as hard
     to hit — it is the one control on the edge of the glass, where a thumb is
     least accurate and the bezel crowds it. Bigger in both directions, and the
     tab moves out with its own width so it still sits flush against the panel. */
  .brood-grab{ left:-38px; width:36px; height:66px; font-size:18px; }
  .hud-grab{ height:30px; }
  .hud-grab::before{ width:52px; height:4px; }
  /* The way out of HOW TO PLAY and the FAQ wants the same treatment, but it
     cannot live here: .sheet-x is defined several hundred lines BELOW this
     block, a media query adds no specificity, and equal specificity is settled
     by source order — so this would lose to the plain rule every time. It sits
     next to that rule instead. Measured 34px when it was written here. */
}
/* both ends the same number: there is nothing to drag, so the row is just the
   two labels again */
.brood-head.flat .bh-qty{ display:none; }


/* the stock, at the right end of the header — same treatment as the value beside
   a map option, so the one number that moves reads as the live one. No float:
   the header is a flex row now and the order in the markup is the order on
   screen. */
.brood-head b{ flex:0 0 auto; color:var(--amber); letter-spacing:1px;
  font-weight:600; white-space:nowrap; }
.brood-rows{ display:flex; flex-direction:column; gap:3px; }
.brood-row{
  display:flex; align-items:center; gap:8px; width:100%;
  background:transparent; border:1px solid transparent; border-radius:7px;
  padding:5px 7px; cursor:pointer; color:var(--ink);
  font-family:var(--mono); font-size:10px; letter-spacing:1.2px;
  transition:background .14s, border-color .14s;
}
.brood-row:hover{ background:rgba(226,166,58,.08); border-color:#3d3227; }
.brood-row .rank{
  flex:0 0 auto; width:17px; height:17px; border-radius:5px; display:grid; place-items:center;
  font-size:9px; font-weight:700; background:#2a231d; color:var(--ink-dim);
}
/* The chip is the PRICE now, not a rank, so it says the same thing on every row
   whatever is selected — what changes is which row is lit. */
.brood-row.sel{ border-color:#5a4527; background:rgba(226,166,58,.07); }
.brood-row.sel .rank{ background:var(--amber); color:#1a1208; }
/* A caste the stock will not cover. Dimmed rather than disabled: picking it is
   how you start saving for it, and the colony takes it the moment it can. */
.brood-row.broke:not(.sel){ opacity:.42; }
.brood-row.broke .rank{ color:var(--ink-off); }
.brood-row.broke.sel .rank{ background:#4a3c2c; color:var(--ink-dim); }
/* Sized by caste rather than drawn at one size: the soldier and the worker
   are the same glyph, and the scale is what tells them apart — biggest for the
   soldier, smallest for the worker, the flyer between them. The row keeps a
   fixed width so the names below still line up. */
.brood-row .glyph{ flex:0 0 auto; width:22px; text-align:center; line-height:1; }
.brood-row[data-k="worker"]   .glyph{ font-size:9px; }
.brood-row[data-k="flying"]  .glyph{ font-size:13px; }
.brood-row[data-k="soldier"] .glyph{ font-size:13px; }
.brood-row .nm{ flex:1; text-align:left; }
.brood-row .pct{ flex:0 0 auto; color:var(--ink-off); font-size:9px; }
.brood-row .ct{ flex:0 0 auto; color:var(--amber); font-size:9px; min-width:18px; text-align:right; }

/* ============================ CONTEXT MENU ============================ */
.ctx{
  position:absolute; z-index:20; min-width:196px;
  background:rgba(14,11,9,.97); border:1px solid var(--line); border-radius:10px;
  box-shadow:0 24px 60px -20px #000; padding:7px;
  /* ---- HELD BACK A BEAT, SO A DOUBLE TAP NEVER SEES IT ----

     On a touchscreen a single tap opens this menu and a DOUBLE tap gives the
     order directly. Both start the same way, so the first tap of a double tap
     opened the menu and the second killed it again: reported as "double tapping
     opens the menu and then it does the attack". The gap between two taps of one
     gesture is roughly 60 to 200ms against a 130ms fade, so what you saw was the
     menu caught halfway in and snatched away.

     The delay costs a single tap 90ms before the menu BEGINS to appear, which is
     inside the window people read as immediate, and buys a double tap a clean
     gesture with nothing flashing in the middle of it.

     THE FILL MODE IS WHAT MAKES IT WORK. Without both, an animation with a delay
     leaves the element at its NORMAL state until the delay elapses — so the menu
     would sit there fully opaque for 90ms and then fade in from nothing, which is
     worse than no delay at all. both holds the from-state through the wait.

     Not a cure for every case — a slow double tap still catches the tail of the
     fade — but it turns the common one from a flash into nothing at all. */
  animation:ctxIn .13s ease .09s both;
}
.ctx.hidden{ display:none; }
@keyframes ctxIn{ from{ opacity:0; transform:scale(.95) translateY(-4px);} to{opacity:1;transform:none;} }
.ctx-head{
  font-size:9px; letter-spacing:2px; color:var(--ink-off); text-transform:uppercase;
  padding:4px 9px 7px; border-bottom:1px solid var(--line-soft); margin-bottom:6px; font-family:var(--mono);
}
.ctx-group{ display:flex; flex-direction:column; gap:2px; }
.ctx-group:empty{ display:none; }
.ctx-sep{ height:1px; background:var(--line-soft); margin:6px 2px; }
.ctx-group:empty + .ctx-sep{ display:none; }
.ctx-item{
  display:flex; align-items:center; gap:10px; width:100%;
  background:transparent; border:0; color:var(--ink); font-family:var(--mono);
  padding:8px 10px; border-radius:6px; font-size:11px; letter-spacing:1.4px;
  cursor:pointer; text-align:left; transition:background .12s;
}
.ctx-item:hover:not(:disabled){ background:rgba(226,166,58,.09); }
.ctx-item .k{ font-size:13px; width:18px; text-align:center; display:grid; place-items:center; }
.ctx-item .k svg{ display:block; }
.ctx-item .meta{ margin-left:auto; font-size:9px; color:var(--ink-off); }
.ctx-item:disabled{ opacity:.34; cursor:not-allowed; }
.ctx-item.attack .k{ color:var(--red); }
.ctx-item.defend .k{ color:var(--blue); }
.ctx-item.patrol .k{ color:var(--amber); }
.ctx-item.fob .k{ color:var(--green); }
.ctx-item.demolish .k,
.ctx-item.surrender .k{ color:var(--red); }
.ctx-item.cancel .k{ color:var(--ink-off); }

/* ============================ RADIAL ORDER MENU ============================ */
/* The same orders as the list, ringed around the cursor. Three layers, in one
   place so they cannot drift apart: the SVG wedges are the hit targets, the caps
   ride over them carrying the icon and the name, and the hub in the middle is
   the readout and the cancel.
   The host itself is pointer-transparent — only the wedges and the hub take a
   click, so the square corners of the box do not swallow presses meant for the
   map underneath. */
.radial{
  position:absolute; z-index:20; width:224px; height:224px;
  transform:translate(-50%,-50%); pointer-events:none;
  animation:radIn .14s ease both;
}
.radial.hidden{ display:none; }
@keyframes radIn{
  from{ opacity:0; transform:translate(-50%,-50%) scale(.86); }
  to  { opacity:1; transform:translate(-50%,-50%) scale(1); }
}
/* overflow visible so the lit wedge's glow is not cut off at the rim */
.rad-svg{ position:absolute; inset:0; width:100%; height:100%; overflow:visible; }
/* SEE-THROUGH ALL ROUND, at the alpha the lit wedge always had. The ring used
   to be a near-solid disc with one translucent slice in it, so pointing at a
   wedge did two things at once: it lit up AND it opened a window onto the map.
   Only one of those was meant to be the feedback. At a matching .15 everywhere
   the ground you are giving an order about stays visible while you choose, and
   the hot wedge is told apart by its HUE and its stroke rather than by being the
   one piece you can see through.

   The strokes and the text keep their full strength on purpose. Transparency is
   for the fills; an outline you can barely see is not a lighter ring, it is a
   ring that has stopped saying where the targets are. */
.rad-wedge{
  pointer-events:auto; cursor:pointer;
  fill:rgba(14,11,9,.15); stroke:var(--line); stroke-width:1;
  transition:fill .12s, stroke .12s;
}
.rad-wedge.hot{ fill:rgba(226,166,58,.15); stroke:var(--amber); }
/* An order that would be refused keeps its wedge and its hover: the hub is where
   the reason is, and a wedge that vanished when it went cold would move every
   other one out from under the cursor. */
/* A refused wedge is LESS present, not more. Both of these sat above the
   resting fill when the ring was near-solid and it did not show; against a
   see-through ring, hovering a dead wedge darkening it would read as the wedge
   answering. It stays inside the same band: dimmer at rest, a hair up on hover
   so the pointer is still acknowledged, and never the amber of a live one. */
.rad-wedge.off{ cursor:not-allowed; fill:rgba(10,8,7,.10); }
.rad-wedge.off.hot{ fill:rgba(14,11,9,.17); stroke:var(--line); }

.rad-caps{ position:absolute; inset:0; pointer-events:none; }
.rad-cap{
  position:absolute; transform:translate(-50%,-50%); width:66px;
  display:flex; flex-direction:column; align-items:center; gap:3px; text-align:center;
  font-family:var(--mono); font-size:8.5px; letter-spacing:1.1px; color:var(--ink);
  text-shadow:0 1px 3px #000;
}
.rad-cap .k{ font-size:15px; line-height:1; }
.rad-cap .k svg{ display:block; }
.rad-cap .nm{ white-space:nowrap; }
.rad-cap.off{ opacity:.34; }
.rad-cap.hot{ color:var(--amber-2); }
/* the same colour coding the list items and the cooldown chips use */
.rad-cap.attack .k{ color:var(--red); }
.rad-cap.defend .k{ color:var(--blue); }
.rad-cap.patrol .k{ color:var(--amber); }
.rad-cap.fob    .k{ color:var(--green); }
.rad-cap.demolish .k,
.rad-cap.surrender .k{ color:var(--red); }
.rad-cap.cancel .k{ color:var(--ink-off); }

/* Sized to sit inside the inner rim with a ring of map still showing through
   the gap — 84 against an inner radius of 46. */
.rad-hub{
  position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
  width:84px; height:84px; border-radius:50%; padding:0 7px;
  display:flex; flex-direction:column; align-items:center; justify-content:center; gap:3px;
  background:rgba(14,11,9,.15); border:1px solid var(--line);
  pointer-events:auto; cursor:pointer; text-align:center;
  font-family:var(--mono); transition:border-color .12s, background .12s;
}
.rad-hub:hover{ border-color:var(--red); background:rgba(30,12,10,.3); }
.rad-hub span{ display:block; max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* the hub has no solid backing left, so the text carries its own */
.rad-a{ font-size:9px; letter-spacing:1.2px; color:var(--ink); text-shadow:0 1px 3px #000, 0 0 6px #000; }
.rad-b{ font-size:7.5px; letter-spacing:.8px; color:var(--ink-off); text-shadow:0 1px 3px #000, 0 0 6px #000; }
.rad-hub.hot .rad-a{ color:var(--red); }

/* ============================ SETTINGS ============================ */
/* FLEX, NOT GRID, AND THAT IS THE WHOLE FIX. This centred the card with
   `display:grid; place-items:center` for a long time and it looked identical —
   until the panel grew past the height of a laptop screen, at which point the
   card ran off the bottom with no scrollbar anywhere to get it back.

   The reason is that `max-height:100%` on the card needs its containing block
   to have a DEFINITE height, and in a grid the card's containing block is its
   grid AREA. With no explicit rows the row is `auto`, sized to the card, so the
   percentage is being resolved against the very thing it is meant to constrain.
   A browser is required to treat that as `none` — the cap is silently dropped,
   the card grows to its content, and `.set-body` is never squeezed, so it never
   scrolls either. Measured at 563px of window: a 1165px card, 626px of it below
   the fold and unreachable.

   A flex item's percentage height resolves against the flex CONTAINER instead,
   and this one is `inset:0` inside a `100svh` #app — definite, always. So the
   cap bites, the card stops at the window, and the body takes what is left. */
.settings{
  position:absolute; inset:0; z-index:40;
  display:flex; align-items:center; justify-content:center; padding:24px;
  background:rgba(6,5,4,.78);
  animation:setIn .16s ease both;
}
.settings.hidden{ display:none; }
@keyframes setIn{ from{ opacity:0; } to{ opacity:1; } }
/* A short window gives the padding up before it gives up any settings. */
@media (max-height:560px){ .settings{ padding:10px; } }
/* OPAQUE, not --panel. Every other panel in the game floats over the map and is
   meant to: a HUD reads better with the world showing faintly behind it. This
   one is a dialog over a moving picture, and three percent of a lit terrain
   chunk sliding about behind a keybinding row is three percent too much — a
   GPU that composites its canvas in a layer of its own can make it far more
   than three. Solid, so it reads the same on every machine. */
.set-card{
  width:min(430px,100%); max-height:100%; min-height:0;
  display:flex; flex-direction:column;
  background:#100d0b; border:1px solid var(--line); border-radius:12px;
  box-shadow:0 30px 80px -24px #000;
}
/* The head and the foot keep their size; the list in the middle is the part
   that gives. Without flex:0 0 auto a tall list would squeeze the title and the
   RESET ALL button instead of scrolling, which is the wrong thing to lose. */
.set-head{
  flex:0 0 auto;
  display:flex; align-items:center; justify-content:space-between;
  padding:13px 16px 11px; border-bottom:1px solid var(--line-soft);
  font-family:var(--mono); font-size:11px; letter-spacing:3.4px; color:var(--ink);
}
.set-x{
  background:transparent; border:0; color:var(--ink-off); cursor:pointer;
  font-size:13px; line-height:1; padding:2px 4px; transition:color .14s;
}
.set-x:hover{ color:var(--amber); }

/* min-height:0 because a flex item will not shrink below its content otherwise,
   and a body that refuses to shrink pushes the card off the screen again. */
.set-body{ flex:1 1 auto; min-height:0; overflow-y:auto; padding:10px 12px 12px; }
.set-body::-webkit-scrollbar{ width:8px; }
.set-body::-webkit-scrollbar-thumb{ background:#2b241e; border-radius:8px; }
.set-sec{
  font-family:var(--mono); font-size:9.5px; letter-spacing:2.6px; color:var(--ink-off);
  padding:10px 4px 6px; text-transform:uppercase;
}
.set-sec:first-child{ padding-top:2px; }
.set-note{
  font-family:var(--mono); font-size:9px; line-height:1.6; letter-spacing:.4px;
  color:var(--ink-off); padding:2px 5px 4px;
}

.set-row{
  display:flex; align-items:center; gap:9px;
  padding:5px 5px; border-radius:7px; min-height:32px;
}
.set-row:hover{ background:rgba(255,255,255,.022); }
.set-label{
  flex:1; min-width:0; font-family:var(--mono); font-size:10px; letter-spacing:1.4px;
  color:var(--ink-dim); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
/* A row that no longer matches what it shipped with. The amber pip is the cue
   that its own reset is now live — the ⟲ beside it is otherwise invisible, so
   the panel never carries eight dead buttons. */
.set-row.changed .set-label{ color:var(--ink); }
.set-row.changed .set-label::before{
  content:''; display:inline-block; width:4px; height:4px; border-radius:50%;
  background:var(--amber); margin-right:7px; vertical-align:2px;
}

.set-key{
  flex:0 0 auto; min-width:96px; text-align:center;
  background:#221c17; border:1px solid #33291f; border-radius:6px; color:var(--ink);
  font-family:var(--mono); font-size:10px; letter-spacing:1.2px;
  padding:6px 10px; cursor:pointer; transition:.14s;
}
.set-key:hover{ border-color:var(--amber); color:var(--amber-2); }
.set-key.unbound{ color:var(--ink-off); }
/* listening: the row has the keyboard and nothing else does */
.set-key.listening{
  border-color:var(--amber); color:#1a1208; background:var(--amber);
  animation:setPulse .9s ease-in-out infinite;
}
@keyframes setPulse{ 50%{ box-shadow:0 0 0 4px rgba(226,166,58,.18); } }

.set-clear{
  display:none; flex:0 0 auto;
  background:transparent; border:1px solid #4a2c28; border-radius:6px; color:#d08a80;
  font-family:var(--mono); font-size:8.5px; letter-spacing:1.2px; padding:5px 7px; cursor:pointer;
}
.set-clear:hover{ background:rgba(232,86,76,.12); color:var(--red); }
.set-row[data-cap="1"] .set-clear{ display:block; }

/* Segmented choice — the one control that is not a key. */
.set-seg{ flex:0 0 auto; display:flex; gap:2px; padding:2px; background:#1c1713; border-radius:7px; }
.set-seg button{
  background:transparent; border:0; border-radius:5px; color:var(--ink-off); cursor:pointer;
  font-family:var(--mono); font-size:9px; letter-spacing:1.4px; padding:5px 11px; transition:.14s;
}
.set-seg button:hover{ color:var(--ink); }
.set-seg button.on{ background:var(--amber); color:#1a1208; }

/* A number, as a stepper. Same footprint as the segmented control beside it,
   so the rows line up whichever kind they are. */
.set-step{
  flex:0 0 auto; display:flex; align-items:center; gap:2px;
  padding:2px; background:#1c1713; border-radius:7px;
}
.set-step button{
  width:22px; height:22px; padding:0; display:grid; place-items:center;
  background:transparent; border:0; border-radius:5px; color:var(--ink-dim);
  font-family:var(--mono); font-size:13px; line-height:1; cursor:pointer; transition:.14s;
}
.set-step button:hover:not(:disabled){ background:rgba(226,166,58,.14); color:var(--amber); }
/* an end that cannot move says so rather than clicking and doing nothing */
.set-step button:disabled{ color:var(--ink-off); opacity:.4; cursor:default; }
.set-step b{
  min-width:26px; text-align:center; font-family:var(--mono); font-size:11px;
  font-weight:600; color:var(--amber);
}

/* Per-row reset. Held in the layout rather than removed, so a row does not
   jump sideways the moment it is changed. */
.set-undo{
  flex:0 0 auto; width:22px; height:22px; visibility:hidden;
  display:grid; place-items:center; padding:0; cursor:pointer;
  background:transparent; border:1px solid transparent; border-radius:6px;
  color:var(--ink-off); font-size:12px; line-height:1; transition:.14s;
}
.set-row.changed .set-undo{ visibility:visible; }
.set-undo:hover{ color:var(--amber); border-color:#3d3227; background:rgba(226,166,58,.10); }

.set-foot{
  flex:0 0 auto;
  display:flex; align-items:center; justify-content:space-between; gap:12px;
  padding:10px 14px 12px; border-top:1px solid var(--line-soft);
}
.set-hint{
  flex:1; min-width:0; font-family:var(--mono); font-size:8.5px; line-height:1.5;
  letter-spacing:.4px; color:var(--ink-off);
}
.set-reset{
  flex:0 0 auto; background:transparent; border:1px solid #4a3c2c; border-radius:7px;
  color:var(--amber); font-family:var(--mono); font-size:9.5px; letter-spacing:1.8px;
  padding:7px 12px; cursor:pointer; transition:.14s;
}
.set-reset:hover:not(:disabled){ background:rgba(226,166,58,.12); border-color:var(--amber); }
/* nothing has been changed: there is nothing for it to undo */
.set-reset:disabled{ color:var(--ink-off); border-color:var(--line-soft); cursor:default; }

/* ============================ THE DOOR ============================ */
/* The front door's own controls. Present in the markup always, shown only in
   mode-title — the same one-markup-two-shells rule the order menus follow, so
   there is no second copy of anything to keep in step.

   ONE OBVIOUS THING TO PRESS. Everything else on this card is quieter than
   PLAY on purpose: a stranger should not have to read a menu to find the verb.
   The seed and the sliders are not gone, they are folded away, because a
   first-time visitor has no opinion about map size and should not be asked for
   one before they have seen an ant. */
.door{ display:none; }
#app.mode-title .door{ display:flex; flex-direction:column; gap:8px; }
/* ...and in the rail the old rows are the menu again, exactly as they were. */
/* CUSTOM GAME OPENS BY GROWING THE CARD, rather than by the contents simply
   being there on the next frame.

   It used to be display:none to display:block, which cannot be animated at
   all: the card jumped from one height to another and the seed field, the
   sliders and the options appeared fully formed, with nothing connecting them
   to the button that had just been pressed. Growing the card says where they
   came from.

   GRID 0fr TO 1fr, not max-height. max-height needs a number larger than the
   content will ever be, and then the easing is wrong at both ends — it races
   through the real height and spends the rest of the duration animating empty
   space, which is why every max-height accordion feels slightly broken. A grid
   track interpolates to the CONTENT's height, so there is no magic number to
   guess and nothing to keep in step when a row is added later.

   The inner wrapper is the single grid row that trick requires, and it is what
   does the clipping: min-height:0 because a grid item will not shrink below
   its content without it, which would leave the fold stuck open at 0fr.

   THE SKY FOLLOWS IT FOR FREE. There is already a ResizeObserver on
   .title-inner calling fitSky — see js/main.js — so the sun's arc lifts to
   clear the growing card on every frame of this transition rather than
   snapping to the new shape once it lands. */
#app.mode-title .door-fold{
  display:grid;
  grid-template-rows:0fr;
  transition:grid-template-rows .34s cubic-bezier(.22,.61,.36,1);
}
#app.mode-title .title-inner.custom .door-fold{ grid-template-rows:1fr; }
#app.mode-title .door-fold-in{
  display:block; overflow:hidden; min-height:0;
  /* Faded as well as clipped, so the text does not slide up the card as a
     solid block while the height is still moving under it. */
  opacity:0;
  /* visibility, transitioned with a delay, is what keeps the seed field and
     the sliders out of the tab order while the fold is shut. A clipped element
     is still focusable, so without this the first Tab from CUSTOM GAME landed
     somewhere nobody could see. The delay holds it visible for the whole
     collapse and drops it exactly at the end. */
  visibility:hidden;
  transition:opacity .18s ease, visibility 0s linear .34s;
}
#app.mode-title .title-inner.custom .door-fold-in{
  opacity:1; visibility:visible;
  /* Opening, the fade waits a moment for the card to have somewhere to put
     the text; closing, it goes first. */
  transition:opacity .26s ease .08s, visibility 0s linear 0s;
}
/* Unchanged from the display-toggle version: at the door these are blocks,
   NOT the flex columns they are in the rail. .opts carries gap:12px as a flex
   column and must not carry it here, or every option gains twelve pixels the
   moment this stops being display:block. */
#app.mode-title .door-fold-in .rows,
#app.mode-title .door-fold-in .opts{ display:block; }
#app.mode-title .door-fold-in .rows{ margin-top:10px; }

/* The chevron turns over with it, so the button says which way it is going. */
.door-more .chev{ display:inline-block; transition:transform .34s cubic-bezier(.22,.61,.36,1); }
#app.mode-title .title-inner.custom .door-more .chev{ transform:rotate(180deg); }

/* ...and none of it for somebody who has asked the operating system for less
   movement. The fold still opens and closes; it simply arrives. */
@media (prefers-reduced-motion: reduce){
  #app.mode-title .door-fold,
  #app.mode-title .door-fold-in,
  #app.mode-title .title-inner.custom .door-fold-in,
  .door-more .chev{ transition:none; }
}
/* PLAY replaces START at the door; START stays in the rail where it belongs. */
#app.mode-title #btnStart{ display:none; }

.door-act{
  display:flex; align-items:center; justify-content:center; gap:10px;
  width:100%; padding:13px 14px; cursor:pointer;
  background:transparent; border:1px solid #3a3127; border-radius:10px;
  color:var(--ink); font-family:var(--mono);
  font-size:14.5px; letter-spacing:4px; transition:.15s;
}
.door-act:hover:not(:disabled){ border-color:var(--amber); color:var(--amber); background:rgba(226,166,58,.10); }
.door-act.go{
  background:rgba(226,166,58,.14); border-color:#6a5124; color:var(--amber-2);
  font-size:17.5px; padding:15px 14px; letter-spacing:5px;
}
.door-act.go:hover{ background:var(--amber); border-color:var(--amber); color:#1a1208; }
/* DISABLED SAYS WHY. A dead control with no explanation reads as a broken
   page; the tag is what turns "this does not work" into "not yet". */
.door-act:disabled{ opacity:.45; cursor:default; border-style:dashed; }
.door-act i{
  font-style:normal; font-size:9px; letter-spacing:2px; padding:3px 7px;
  border:1px solid currentColor; border-radius:20px; opacity:.8;
}

.door-links{ display:flex; gap:8px; margin-top:2px; }
/* THE ADD-TO-HOME-SCREEN LINE. Quieter than the door links, because it is
   advice rather than a control - the only thing on this card that is not a
   verb. It carries its own dismiss, because a suggestion you cannot decline
   stops being a suggestion the second time you see it. */
.door-a2hs{
  display:flex; align-items:center; gap:8px; margin:2px 0 0;
  padding:8px 4px 0; border-top:1px solid var(--line-soft);
  color:var(--ink-off); font-family:var(--mono);
  font-size:10px; letter-spacing:.6px; line-height:1.5;
}
.door-a2hs b{ color:var(--ink-dim); font-weight:600; }
.door-a2hs span{ flex:1; min-width:0; }
.door-a2hs-x{
  flex:0 0 auto; width:22px; height:22px; padding:0; cursor:pointer;
  background:transparent; border:1px solid transparent; border-radius:6px;
  color:var(--ink-off); font-size:15px; line-height:1; transition:.14s;
}
.door-a2hs-x:hover{ color:var(--ink); border-color:var(--line); }

.door-link{
  flex:1; padding:11px 4px; cursor:pointer;
  background:transparent; border:1px solid transparent; border-radius:8px;
  color:var(--ink-dim); font-family:var(--mono);
  /* WAS 8.5px, UNDER A 96px WORDMARK. Small is how a control says it is
     secondary, and past a point it stops saying secondary and starts being
     unreadable — these three are the only route to the tutorial and the FAQ,
     which is not a thing to hide from the person who needs it most. */
  font-size:11.5px; letter-spacing:1.9px; transition:.14s;
}
.door-link:hover{ color:var(--amber); border-color:#3d3227; background:rgba(226,166,58,.08); }

.door-more{
  margin-top:2px; padding:7px 4px; cursor:pointer;
  display:flex; align-items:center; justify-content:center; gap:7px;
  background:transparent; border:0; border-top:1px solid var(--line-soft); border-radius:0;
  color:var(--ink-off); font-family:var(--mono); font-size:10px; letter-spacing:2.4px;
  transition:color .14s;
}
.door-more:hover{ color:var(--ink-dim); }
/* NO TRANSITION HERE. This used to restate `transition:transform .18s`, which is
   the same trap the #btnRail note further down warns about: a media query adds no
   specificity, so a plain rule written afterwards beats one inside it. It beat
   both the .34s matched to the fold ABOVE — so the chevron finished turning while
   the card was still growing — and the prefers-reduced-motion block between them,
   which meant a visitor who had asked their system for less movement still got a
   chevron spinning through 180 degrees. The opt-out was dead for as long as this
   line existed. Size only. */
.door-more .chev{ font-size:9px; }
.door-more[aria-expanded="true"] .chev{ transform:rotate(180deg); }

/* A NARROW CARD CANNOT AFFORD THE SAME LETTER-SPACING. At 11.5px with 1.9px
   of tracking, HOW TO PLAY needs about 97px and the button is 91px on a
   390px phone, so it wrapped to two lines and made one of three equal links
   taller than its neighbours. The tracking goes first, then the size — the
   same order the card gives up its padding in. */
@media (max-width: 480px){
  .door-link{ font-size:10px; letter-spacing:1.1px; padding:10px 2px; }
  .door-act{ font-size:13px; letter-spacing:3px; }
  .door-act.go{ font-size:16px; letter-spacing:4px; }
}

/* A SHORT WINDOW HAS TO GIVE THE SKY A BAND. On a 416px-tall screen the card
   was 326px of it, leaving 45px top and bottom — and the sun's arc cannot fit
   in 45px, so the one control on this screen was behind the card for most of
   the day however hard fitSky pushed. The wordmark goes first (it is the
   largest thing and the least load-bearing), then the gaps, then the padding.
   Nothing is removed: a short window gets the same menu, closer together. */
@media (max-height: 560px){
  /* doubled for the same reason as the base rule - see the note there */
  #app.mode-title .wordmark{ font-size:clamp(22px, 7.2vh, 34px); margin-bottom:8px; }
  #app.mode-title .wordmark{ font-size:clamp(22px, 7.2svh, 34px); }
  #app.mode-title .title-inner{ padding:12px 16px 10px; border-radius:11px; }
  #app.mode-title .door{ gap:6px; }
  .door-act{ padding:9px 14px; font-size:13px; }
  .door-act.go{ padding:11px 14px; font-size:15px; }
  .door-link{ padding:8px 4px; }
  .door-more{ padding:5px 4px; }
}

/* ============================ LOBBY ============================ */
/* Rooms live in the same card HOW TO PLAY and the FAQ use, because they are
   the same kind of thing: a page over the door. Only the contents differ, so
   only the contents get rules — .sheet and .sheet-card are already right. */
#app.mode-game #lobby{ display:none; }   /* a game is not a place to browse rooms */

.lobby-me{ display:flex; align-items:center; gap:10px; margin:14px 0 4px; }
.lobby-me label{ font-family:var(--mono); font-size:9px; letter-spacing:2.4px; color:var(--ink-off); }
.lobby-me input, .lobby-join input{
  flex:1; min-width:0; padding:9px 11px;
  background:#0d0b09; border:1px solid var(--line); border-radius:8px;
  color:var(--ink); font-family:var(--mono); font-size:12.5px; letter-spacing:1.6px;
}
.lobby-me input:focus, .lobby-join input:focus{ outline:none; border-color:var(--amber); }
/* The value in this one is always uppercase — the field folds it as it is typed —
   so the hint has to be told to be lowercase or it reads as content rather than
   as a shape. Exactly what #seed::placeholder does, for exactly the same reason. */
#lobbyCodeIn::placeholder{ color:var(--ink-off); text-transform:lowercase; letter-spacing:2px; }

.lobby-make{ display:flex; gap:8px; margin:12px 0 6px; }
.lobby-btn{
  flex:1; padding:11px 12px; cursor:pointer;
  background:transparent; border:1px solid #3a3127; border-radius:9px;
  color:var(--ink); font-family:var(--mono); font-size:11.5px; letter-spacing:2.6px;
  transition:.15s;
}
.lobby-btn:hover:not(:disabled){ border-color:var(--amber); color:var(--amber); background:rgba(226,166,58,.10); }
.lobby-btn:disabled{ opacity:.4; cursor:default; }
.lobby-btn.go{ background:rgba(226,166,58,.14); border-color:#6a5124; color:var(--amber-2); }
.lobby-btn.go:hover:not(:disabled){ background:var(--amber); border-color:var(--amber); color:#1a1208; }

.lobby-hint{ font-size:11.5px; color:var(--ink-off); margin:6px 0 2px; line-height:1.6; }

/* THE LIST IS BUTTONS, not rows with a button in them: the whole row is the
   target, which is the difference between a list you can use on a phone and
   one you aim at. */
.lobby-list{ display:flex; flex-direction:column; gap:6px; margin:8px 0 4px; }
.lobby-row{
  display:flex; align-items:center; gap:12px; width:100%;
  padding:10px 12px; cursor:pointer; text-align:left;
  background:#14100e; border:1px solid var(--line-soft); border-radius:9px;
  color:var(--ink); font-family:var(--mono); font-size:12px;
  transition:.14s;
}
.lobby-row:hover{ border-color:var(--amber); background:rgba(226,166,58,.09); }
.lobby-code{ color:var(--amber); letter-spacing:2.4px; }
.lobby-who{ flex:1; min-width:0; color:var(--ink-dim); overflow:hidden;
            text-overflow:ellipsis; white-space:nowrap; }
.lobby-n{ color:var(--ink-off); font-size:11px; }
.lobby-empty{ color:var(--ink-off); font-size:11.5px; padding:10px 2px; }

/* ---- the feedback form ---- */
.fb-kinds{ display:flex; gap:6px; margin:10px 0 4px; flex-wrap:wrap; }
.fb-kind{
  flex:1; min-width:96px; padding:9px 8px; cursor:pointer;
  background:transparent; border:1px solid #3a3127; border-radius:8px;
  color:var(--ink-off); font-family:var(--mono); font-size:10.5px; letter-spacing:1.8px;
  transition:.15s;
}
.fb-kind:hover{ border-color:var(--amber); color:var(--ink); }
.fb-kind.on{ border-color:var(--amber); color:var(--amber); background:rgba(226,166,58,.10); }
.fb-label{
  display:block; margin:12px 0 5px;
  font-family:var(--mono); font-size:9.5px; letter-spacing:2px; color:var(--ink-off);
}
.fb-label i{ font-style:normal; opacity:.6; }
.fb-text, .fb-reply{
  width:100%; box-sizing:border-box; padding:9px 11px;
  background:#0d0b09; border:1px solid var(--line); border-radius:8px;
  color:var(--ink); font-family:var(--font); font-size:13px; line-height:1.5;
  resize:vertical;
}
.fb-text:focus, .fb-reply:focus{ outline:none; border-color:var(--amber); }
.fb-diag{ margin:12px 0 4px; }
.fb-diag summary{
  cursor:pointer; font-family:var(--mono); font-size:10px; letter-spacing:1.6px;
  color:var(--ink-off); padding:4px 0;
}
.fb-diag summary:hover{ color:var(--amber); }
.fb-diag-body{
  margin:6px 0 0; padding:9px 11px; max-height:190px; overflow:auto;
  background:#0d0b09; border:1px solid var(--line-soft); border-radius:8px;
  color:var(--ink-dim); font-family:var(--mono); font-size:10.5px; line-height:1.55;
  white-space:pre-wrap; word-break:break-word;
}
.fb-acts{ display:flex; gap:8px; margin:14px 0 2px; }
/* A finger gets the same 44 everything else here gets. */
@media (pointer: coarse){
  .fb-kind{ padding:12px 8px; }
  .fb-acts .lobby-btn{ min-height:44px; }
}

.lobby-join{ display:flex; gap:8px; margin:8px 0 4px; }
.lobby-join .lobby-btn{ flex:0 0 auto; padding:9px 18px; }

.lobby-head{ display:flex; align-items:center; gap:12px; margin:14px 0 10px; }
.lobby-bigcode{ font-family:var(--mono); font-size:24px; letter-spacing:5px; color:var(--amber); }
.lobby-pill{
  font-family:var(--mono); font-size:8.5px; letter-spacing:2px; padding:3px 9px;
  border:1px solid currentColor; border-radius:20px; color:var(--ink-off);
}
.lobby-pill.private{ color:var(--amber); }

.lobby-seats{ display:flex; flex-direction:column; gap:5px; margin:4px 0 8px; }
.lobby-seat{
  display:flex; align-items:center; gap:11px; padding:9px 12px;
  background:#14100e; border:1px solid var(--line-soft); border-radius:9px;
  font-family:var(--mono); font-size:12px;
}
.lobby-seat.away{ opacity:.55; }
.lobby-seatn{ color:var(--ink-off); font-size:10px; min-width:12px; }
.lobby-name{ flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.lobby-tag{ font-size:8.5px; letter-spacing:1.8px; color:var(--amber); }
.lobby-seat.away .lobby-tag{ color:var(--ink-off); }

.lobby-note{ min-height:18px; margin-top:10px; font-size:11.5px; color:var(--ink-off); }
.lobby-note.bad{ color:#ff9b8c; }

/* ============================ SHEETS ============================ */
/* HOW TO PLAY and the FAQ: pages of text over the game. Same shape as the
   settings panel, for the same reason — flex rather than grid so that
   `max-height:100%` on the card resolves against something definite, and
   `safe center` so a card taller than the window starts at the top and
   scrolls instead of hiding its own first line at a negative offset. Both of
   those were bugs here before they were rules. */
/* ---- AND THE FEEDBACK SHEET SITS ABOVE THE END PANEL ----

   .sheet is 42 and .endgame is 45, which is right for every other sheet — the end
   panel is modal and a stray HOW TO PLAY has no business over it. But FEEDBACK is
   opened FROM that panel, by a button inside it, so at 42 it would have rendered
   behind the thing that launched it: a click that appears to do nothing.

   On the id rather than on .sheet, so the rule that keeps every other sheet under
   the end panel is left exactly as it was. */
#feedback{ z-index:48; }

.sheet{
  position:absolute; inset:0; z-index:42;
  display:flex; flex-direction:column;
  justify-content:safe center; align-items:center;
  padding:24px; overflow-y:auto;
  background:rgba(6,5,4,.86);
  animation:setIn .16s ease both;
}
.sheet.hidden{ display:none; }
.sheet-card{
  width:min(620px,100%); max-height:100%; min-height:0; flex:0 0 auto;
  display:flex; flex-direction:column;
  background:#100d0b; border:1px solid var(--line); border-radius:12px;
  box-shadow:0 30px 80px -24px #000;
}
.sheet-head{
  flex:0 0 auto;
  display:flex; align-items:center; justify-content:space-between;
  padding:13px 16px 11px; border-bottom:1px solid var(--line-soft);
  font-family:var(--mono); font-size:11px; letter-spacing:3.4px; color:var(--ink);
}
/* A CLOSE BUTTON HAS TO BE HITTABLE, and 19x17 is not.

   It was sized as a glyph rather than as a target — unremarkable under a
   cursor, about a sixth of a finger's worth on a tablet, and a tablet is
   exactly where the other way out does not exist, because Escape needs a
   keyboard. Tapping the darkness outside the card closes it too and always
   has, but nobody should have to discover that to put a page down.

   THE GLYPH DOES NOT CHANGE SIZE — the box grows around it. The sheets look
   exactly as they did and the negative margin keeps the header the height it
   was; all that is different is how much of it answers to a thumb. */
.sheet-x{
  background:transparent; border:0; color:var(--ink-off); cursor:pointer;
  font-size:13px; line-height:1; transition:color .14s;
  display:grid; place-items:center;
  min-width:34px; min-height:34px; padding:0; margin:-8px -8px -8px 0;
}
.sheet-x:hover{ color:var(--amber); }
/* FINGER-SIZED WHEREVER THERE IS A FINGER, and keyed on the POINTER rather
   than on the width, because a tablet is not narrow. It reports a desktop-sized
   viewport, takes every width-based rule the wide layout has, and still has
   nothing but a thumb to close a sheet with — no Escape, and until now a 19x17
   target. Immediately after the rule it overrides, for the reason the comment
   in the narrow block gives. */
@media (pointer: coarse){
  .sheet-x{ min-width:44px; min-height:44px; margin:-12px -10px -12px 0; }
}
.sheet-body{
  flex:1 1 auto; min-height:0; overflow-y:auto;
  padding:4px 20px 20px; font-size:12.5px; line-height:1.68; color:var(--ink-dim);
}
.sheet-body::-webkit-scrollbar{ width:8px; }
.sheet-body::-webkit-scrollbar-thumb{ background:#2b241e; border-radius:8px; }
.sheet-body h3{
  margin:22px 0 6px; font-family:var(--mono); font-size:9.5px; letter-spacing:2.6px;
  color:var(--amber); text-transform:uppercase; font-weight:400;
  padding-bottom:5px; border-bottom:1px solid var(--line-soft);
}
.sheet-body p{ margin:9px 0; }
.sheet-body b{ color:var(--ink); font-weight:600; }
.sheet-body ol, .sheet-body ul{ margin:9px 0; padding-left:20px; }
.sheet-body li{ margin:7px 0; }
.sheet-body .lede{
  margin:14px 0 4px; font-size:13.5px; color:var(--ink); line-height:1.6;
}
.sheet-body .more{
  margin-top:20px; padding-top:12px; border-top:1px solid var(--line-soft);
  font-size:11px; color:var(--ink-off);
}
@media (max-height:560px){ .sheet{ padding:10px; } }

/* ========================= END OF THE RUN ========================= */
/* ONE PANEL, TWO ENDINGS. A win and a loss want the same thing said in the same
   place — here is how it went — so they are the same card with the word and the
   colour swapped and one button hidden. Two panels would be two layouts to keep
   in step, and they would drift the first time either was touched.

   Flex-centred and capped, for the reason the settings panel is — see the note
   in README on `max-height:100%` inside a grid row. Above the settings panel,
   so ending the game with the settings open still reads as the game ending. */
.endgame{
  position:absolute; inset:0; z-index:45;
  display:flex; align-items:center; justify-content:center; padding:24px;
  background:rgba(6,4,4,.82);
  animation:setIn .22s ease both;
}
.endgame.hidden{ display:none; }
.endgame.won{ background:rgba(5,6,4,.82); }
.end-card{
  position:relative;
  width:min(380px,100%); max-height:100%; min-height:0; overflow-y:auto;
  padding:26px 26px 20px; text-align:center;
  background:#120d0c; border:1px solid #4a2019; border-radius:12px;
  box-shadow:0 30px 80px -24px #000, 0 0 60px -30px var(--red);
}
.endgame.won .end-card{ background:#0d120c; border-color:#2c5a2c; box-shadow:0 30px 80px -24px #000, 0 0 60px -30px var(--green); }
.end-x{
  position:absolute; top:8px; right:9px;
  background:transparent; border:0; color:var(--ink-off); cursor:pointer;
  font-size:13px; line-height:1; padding:3px 5px; transition:color .14s;
}
.end-x:hover{ color:var(--amber); }

/* ================== THE REST OF THE CONTROLS, FOR A THUMB ==================

   Three panels in this game have a close button and they were written as three
   separate classes, so fixing .sheet-x fixed exactly one of them. Measured, at
   a coarse pointer, before this block existed:

     .set-x   19x17    the SETTINGS panel
     .end-x   21x19    the END OF THE RUN panel — and that one is a modal you
                       have to dismiss. OBSERVE and REGENERATE do close it, but
                       both take an action you may not want, there is no
                       tap-outside handler on it, and Escape needs a keyboard.
                       So on a tablet the only way to simply put it down was a
                       twenty-one pixel target.
     .set-step button  22x22, the + and − on every number row

   WHY THIS BLOCK IS HERE AND NOT WITH THE OTHER COARSE RULES. A media query
   adds no specificity, so an override only wins if it comes AFTER the rule it
   overrides. The coarse block up at the range inputs sits above every selector
   below, and anything put there for these would lose silently — which already
   happened once, to .sheet-x, and read 34px where 44 was written. This sits
   after the last of the rules it touches.

   ON THE POINTER, NOT ON THE WIDTH. A tablet is not narrow: it reports a
   desktop viewport, takes the wide layout, and still has only a thumb.

   The GLYPHS do not change size in any of these — the boxes grow around them,
   and the negative margins hold the headers at the height they were, so none
   of these panels looks any different under a cursor. */
@media (pointer: coarse){
  .set-x, .end-x{
    display:grid; place-items:center;
    min-width:44px; min-height:44px; padding:0;
  }
  .set-x{ margin:-12px -10px -12px 0; }
  /* .end-x is absolutely positioned rather than in a flex header, so it is
     nudged back into the corner instead of having a margin pulled out of one. */
  .end-x{ top:2px; right:3px; }

  /* The steppers are the only genuinely square targets in SETTINGS, and at
     22x22 they were the hardest thing in the game to hit twice in a row —
     which is exactly what a number row asks you to do. */
  .set-step button{ width:40px; height:40px; font-size:16px; }
  /* ...and the segmented choices beside them, which are wide enough already
     but only twenty pixels tall. Taller, not wider: the row has to keep
     fitting a label beside it. */
  .set-seg button{ padding:11px 13px; }
}
/* The one word, in the same inverted red the threat line uses at its top rung —
   the panel has been shouting it in 8px for as long as it took to get here. */
.end-mark{
  display:inline-block; padding:7px 16px; border-radius:7px;
  background:#ff3c28; color:#2a0a06; border:1px solid #ffb0a2;
  font-family:var(--mono); font-size:13px; font-weight:700; letter-spacing:5px;
  box-shadow:0 0 26px -6px rgba(255,60,40,.85);
}
/* ...and the same word in green when it went the other way. Inverted either
   way: whatever it says, it is the loudest thing that has been on the screen. */
.endgame.won .end-mark{
  background:#4ad46a; color:#08210d; border-color:#c8f5d2;
  box-shadow:0 0 26px -6px rgba(74,212,106,.85);
}
.end-line{
  margin:16px 0 20px; font-size:12.5px; line-height:1.65; color:var(--ink);
}
.end-acts{ display:flex; gap:8px; }
.end-btn{
  flex:1; padding:11px 10px; cursor:pointer; transition:.14s;
  background:transparent; border:1px solid #4a3c2c; border-radius:8px;
  color:var(--ink); font-family:var(--mono); font-size:10.5px; letter-spacing:2.2px;
}
.end-btn:hover{ border-color:var(--amber); color:var(--amber); background:rgba(226,166,58,.10); }
/* STATS leads, because it is the question the panel raises. */
.end-btn.go{ border-color:var(--amber); color:var(--amber-2); background:rgba(226,166,58,.13); }
.end-btn.go:hover{ background:var(--amber); color:#1a1208; }
.end-btn.hidden{ display:none; }
/* The run, read off the colony the moment it ended. Hidden until asked for: a
   panel that opens with a dozen numbers on it buries the buttons under them. */
.end-stats{
  margin-top:14px; padding-top:11px; border-top:1px solid var(--line-soft);
  text-align:left; font-family:var(--mono); font-size:9.5px; letter-spacing:.6px;
}
.end-stats.hidden{ display:none; }
.end-stats div{
  display:flex; justify-content:space-between; gap:10px; padding:3px 2px;
  color:var(--ink-off);
}
.end-stats div:nth-child(odd){ background:rgba(255,255,255,.022); border-radius:4px; }
.end-stats b{ color:var(--amber); font-weight:600; }
.end-note{
  margin:14px 0 0; font-family:var(--mono); font-size:8.5px; line-height:1.6;
  letter-spacing:.4px; color:var(--ink-off);
}
@media (max-height:560px){ .endgame{ padding:10px; } .end-card{ padding:18px 20px 14px; } }

/* ============================ CURTAIN ============================ */
/* Cloud cover over a map being generated, which then breaks up and drifts
   apart to leave the world behind it.

   TWO LAYERS, and both are needed. `b` is a flat wash that guarantees COVER —
   at t=0 not one pixel of terrain may show, and seven soft-edged blobs cannot
   promise that at every aspect ratio. The blobs are what make it read as
   CLOUD rather than as a fade: the wash goes first and quickly, and by the
   time it is gone the blobs have already started to separate, so the eye sees
   weather parting rather than opacity ramping.

   Sized in vmax so a tall phone and a wide monitor both get blobs bigger than
   the screen. Positioned in percentages so they stay spread however it is
   shaped. */
.curtain{
  position:absolute; inset:0; z-index:9; overflow:hidden;
  pointer-events:none;                 /* it is scenery: clicks go to the map */
}
.curtain.hidden{ display:none; }
.curtain b{
  position:absolute; inset:-10%;
  background:linear-gradient(178deg, #cfd6dc 0%, #dfe3e5 40%, #cdd3d8 100%);
}
.curtain i{
  position:absolute; left:var(--x); top:var(--y);
  width:var(--s); height:var(--s); margin:calc(var(--s) / -2);
  border-radius:50%;
  /* A cloud is not one circle. Three offset lobes with soft stops read as a
     mass with an edge; a single gradient reads as a spotlight. */
  background:
    radial-gradient(circle at 38% 42%, #f2f4f5 0%, rgba(242,244,245,.92) 28%, rgba(238,241,243,0) 62%),
    radial-gradient(circle at 66% 56%, #e8ecef 0%, rgba(232,236,239,.88) 26%, rgba(232,236,239,0) 60%),
    radial-gradient(circle at 50% 70%, #dde3e7 0%, rgba(221,227,231,.85) 30%, rgba(221,227,231,0) 66%);
  filter:blur(10px);
  will-change:transform, opacity;      /* one 2s animation; give it a layer */
}
/* THE SEVEN BLOBS, moved out of the markup. They were seven style="" attributes
   carrying nothing but custom properties, and they were the only reason a
   Content-Security-Policy for this site would have needed style-src
   'unsafe-inline'. Static values belong in a stylesheet; the cost of leaving
   them inline was a CSP weaker than the page actually needs. */
.curtain i:nth-of-type(1){ --x:12%; --y:18%; --s:46vmax; --d:.00s; --dx:-26%; --dy:-22%; }
.curtain i:nth-of-type(2){ --x:74%; --y:10%; --s:52vmax; --d:.05s; --dx: 30%; --dy:-24%; }
.curtain i:nth-of-type(3){ --x:44%; --y:38%; --s:60vmax; --d:.02s; --dx:  2%; --dy:-14%; }
.curtain i:nth-of-type(4){ --x: 6%; --y:70%; --s:48vmax; --d:.09s; --dx:-30%; --dy: 20%; }
.curtain i:nth-of-type(5){ --x:88%; --y:62%; --s:50vmax; --d:.06s; --dx: 32%; --dy: 16%; }
.curtain i:nth-of-type(6){ --x:32%; --y:92%; --s:44vmax; --d:.12s; --dx:-14%; --dy: 28%; }
.curtain i:nth-of-type(7){ --x:66%; --y:88%; --s:46vmax; --d:.10s; --dx: 18%; --dy: 26%; }


/* THE PARTING. Adding .part starts it; the curtain hides itself when the last
   blob is done. Each blob carries its own --dx/--dy so they leave in different
   directions — a curtain where everything drifts the same way is a pan, not a
   parting — and its own --d so they do not all let go on the same frame.

   The wash clears in a third of the time the blobs take. That gap IS the
   effect: terrain starts showing between the clouds while the clouds are still
   there to show between. */
.curtain.part b{ animation:curtainWash .62s ease-in forwards; }
.curtain.part i{ animation:curtainPart 1.9s cubic-bezier(.32,.06,.28,1) var(--d) forwards; }
@keyframes curtainWash{ to{ opacity:0; } }
@keyframes curtainPart{
  0%   { transform:translate(0,0) scale(1);    opacity:1; }
  35%  { opacity:.92; }
  100% { transform:translate(var(--dx), var(--dy)) scale(1.5); opacity:0; }
}
/* No drift and no scale, because that is the part that moves; the cover still
   clears, so the map is never left behind a white screen. */
@media (prefers-reduced-motion: reduce){
  .curtain.part b{ animation:curtainWash .5s linear forwards; }
  .curtain.part i{ animation:curtainWash .8s linear forwards; }
}

/* The rail button: ON AT EVERY WIDTH while a game is running, and never at the
   front door, where the menu IS the screen.

   It used to be the narrow layout's alone, on the reasoning that a wide window
   has Esc and a phone does not. That reasoning quietly assumed wide meant
   keyboard. A touchscreen laptop and a tablet in either orientation are both
   over 720px, so they took the wide layout — where the rail is a permanent 330px
   panel and Esc is the only thing that folds it — and there is no Esc. The menu
   was a one-way door on exactly the machines least able to get out of it, and it
   was reported from a tablet.

   Showing it on a desktop costs nothing and answers something else as well: Esc
   was the only way to fold the rail and nothing on screen said so.

   DEFINED BEFORE the media query below: a media query adds no specificity, so a
   block sitting after it silently wins. That trap already cost this handle once. */
#btnRail{
  /* THE SIZE IS A VARIABLE BECAUSE SOMETHING ELSE IS DERIVED FROM IT. The rule
     that parks this button inside the rail's right edge subtracts the button plus
     its margin, and that subtraction used to be the literal 48. Growing the button
     for a finger without growing the 48 to match would have slid it four pixels
     from the rail's edge and straight onto the rail's own scrollbar, which lives
     in the last eight. One number, derived once. */
  --rail-btn:38px;
  position:absolute; left:10px; top:10px; z-index:12;
  display:grid; place-items:center;
  width:var(--rail-btn); height:var(--rail-btn); padding:0; cursor:pointer;
  background:rgba(14,11,9,.82); border:1px solid var(--line); border-radius:9px;
  color:var(--ink); font-size:17px; line-height:1;
  box-shadow:0 10px 30px -14px #000;
  -webkit-backdrop-filter:blur(3px); backdrop-filter:blur(3px);
}
#btnRail:active{ background:rgba(226,166,58,.16); border-color:var(--amber); color:var(--amber); }
#app.mode-title #btnRail{ display:none !important; }
/* A FINGER GETS THE 44 EVERYTHING ELSE HERE GETS. Written AFTER the rule it
   overrides, because a media query adds no specificity and this file has been
   caught by that twice. */
@media (pointer: coarse){ #btnRail{ --rail-btn:44px; } }

/* ============================ NARROW ============================ */
/* PHONES, AND ANY WINDOW SHAPED LIKE ONE.

   The rail is 330px and the map got whatever was left. On a 375px phone that
   is 88% of the screen for the menu and a 45px strip of game — measured, and
   the screenshot of it is the reason this section exists.

   So below 720px the rail stops being a COLUMN and becomes a DRAWER: the map
   takes the whole width, the rail lies over it, and it starts out of the way.
   Nothing about the rail's own contents changes, which is the point — it is
   the same markup in the same order, sitting somewhere else. */
/* ...AND EVERY TOUCHSCREEN, WHATEVER WIDTH IT CLAIMS. This block was width-only,
   and a tablet is the case that breaks a width test: an iPad reports a
   desktop-sized viewport, so in portrait it took the 330px COLUMN layout and the
   map was left with 438px of a 768px screen — 43% of the device given over to a
   menu, with the game in the strip beside it. That is the same complaint the
   paragraph above describes on a phone, arriving on a bigger screen, and it reads
   to a player as the canvas failing to resize.

   pointer: coarse RATHER THAN A BIGGER NUMBER, for the reason this project has
   settled on everywhere else: a tablet's width is indistinguishable from a small
   laptop's and its INPUT is not. Raising the breakpoint to catch an iPad in
   portrait would hand the drawer to a windowed desktop that wants the column, and
   would still miss a 12.9 inch iPad in landscape.

   THE THREE PLACES THAT DECIDE THIS HAVE TO AGREE, and js/ui.js says so at length:
   this query, the no-menu class enterGame writes, and the matchMedia handler that
   clears it on rotation. The two in JavaScript read a single constant named
   DRAWER_Q, and it is written to match this line exactly. When they drifted before,
   the menu became unreachable on a device with no Esc key. */
@media (max-width: 720px), (pointer: coarse){
  #app.mode-game .title{
    width:min(330px, 86vw);
    z-index:30;                        /* over the map and the HUD, not beside them */
    box-shadow:0 0 70px -8px #000;     /* it is floating now, and should look it */
  }
  /* The map is always full width here. It is no longer making room for
     anything: the rail either covers part of it or is gone. */
  #app.mode-game #viewport{ left:0; }

  /* WHETHER THE BUTTON EXISTS is settled further up now, because a tablet needed
     it too and a media query was the wrong place to decide who has a keyboard.
     WHERE IT GOES WITH THE MENU OPEN still belongs here, and the reason is worth
     writing down because it is not obvious and it was got wrong once:

     #btnRail IS INSIDE #viewport, not inside #app. So left:10px is measured from
     the viewport's own left edge, and the viewport moves: left:0 here, left:330px
     at a wider width where the rail is a panel beside the map rather than a
     drawer over it. At a wide width that means left:10px already lands just
     inside the map, snug against the rail — exactly where it wants to be — and
     nothing needs moving. Try to share this rule with that width and the calc
     resolves against a viewport already pushed across by 330, putting the button
     282px into the middle of the map. Measured, not guessed.

     Here the rail is a drawer at z-index 30 over the map and the button is 12,
     so the drawer painted over the only control that closes it: a second tap on
     ☰ landed on the wordmark. Esc still worked, which is precisely the keyboard
     a phone does not have, so the menu was a one-way door — no tap-outside
     either, and the 52px of map still showing is not a dismiss target. Reload was
     the way out.

     It moves to the drawer’s own top-right rather than merely rising above it,
     because at left:10px it would sit on the wordmark. 48px is the 38px button
     plus the 10px margin it keeps everywhere else, so it hangs inside the
     drawer’s right edge and reads as that panel’s close. */
  #app.mode-game:not(.no-menu) #btnRail{
    left:calc(min(330px, 86vw) - (var(--rail-btn) + 10px));
    z-index:31;
  }

  /* The brood panel was 212px of a 375px screen. Letting it take the width it
     is given keeps the caste rows legible instead of squeezing the numbers off
     the end of them. */
  .brood{ width:min(212px, 56vw); padding:6px 8px 7px; }
  /* Three 92px chips plus gaps do not fit across 375px. They wrap already —
     this stops them wrapping into a second row that covers the map. */
  .cd{ width:min(92px, 29vw); }

  /* ---- AND THEN IT WAS STILL 47% OF THE SCREEN ----

     Measured in a live game at 375x667: #hud 311px of 667. The brood panel was
     158 of that, the chips 50, and the bottom bar 81. Reported from a phone as
     the UI taking up a lot of the screen, which at nearly half of it was fair.

     Almost all of the bar was layout rather than content. It holds three things
     — two stat readouts and the ants-per-order slider — and it wrapped them onto
     THREE lines, because the slider sits between the two readouts in the markup
     and takes a full line, so each readout was stranded on its own. Ordering the
     two readouts together puts them side by side (152px + 143px fits 359px) and
     leaves the slider one line of its own: 81px to 50px, without hiding a thing.

     The slider label was stacked over its track, 44px for one number. Beside it
     instead: 18px. The track keeps the 38px touch box the (pointer: coarse)
     block above gives it — that is a hit area built out of negative margins, so
     it survives being put in a flex row, and it was re-measured after this.

     The rest is padding and row gaps, taken a few pixels at a time from the
     brood panel and the chips. 311px -> 245px, 47% -> 37%, nothing removed. */
  .hud{ padding:0 8px 8px; gap:6px; }
  .hud-bar{ padding:6px 10px; gap:7px 12px; }
  .hud-stat{ min-width:0; font-size:9.5px; order:1; flex:1 1 auto; }
  .ant-slider{ order:2; display:flex; align-items:center; gap:9px; flex:1 1 100%; }
  .ant-slider label{ margin-bottom:0; white-space:nowrap; }
  .ant-slider input[type=range]{ flex:1; min-width:0; }
  /* THE CHIP IS SHORTER, SO ITS INSIDES HAVE TO BE TOO, and this is what I got
     wrong shrinking it. .cd-inner stacks the icon and the name from top:6px and
     .cd-time is pinned to bottom:4px — a layout that only clears at the 50px the
     chip used to be. At 40 the two met: READY was drawn straight through ATTACK.

     44 rather than 40, because at 40 the name and the time abut exactly and
     nothing is left for the font to disagree about. Still six pixels off the
     original, with four between the two lines. */
  .cd{ height:44px; }
  .cd .cd-inner{ top:4px; }
  .cd .cd-ico{ font-size:12px; height:14px; }
  .cd .cd-name{ font-size:8px; letter-spacing:1.2px; margin-top:1px; }
  .cd .cd-time{ bottom:3px; font-size:8px; }
  .brood-threat{ padding:3px 6px; margin:-1px -2px 5px; }
  .brood-rows{ gap:2px; }
  .brood-row{ padding:3px 7px; }
}

/* ============================ TOAST ============================ */
/* 56px rather than 26: the comet strip owns the top of the screen now, and a
   toast landing on top of a buff bar hides the one thing that is supposed to be
   readable at a glance for two minutes at a time. */
.toast{
  position:absolute; left:50%; top:56px; transform:translateX(-50%);
  /* A NOTICE, NEVER A TARGET. z-index 22 at top:56px is exactly the band the
     lifted sun arc lives in at the front door, so without this a toast
     silently ate the grab for as long as it was up. Nothing clicks a toast:
     ui.js only ever writes its text. */
  pointer-events:none;
  background:rgba(30,12,10,.95); border:1px solid #6a2a22; color:#ffdcd6;
  padding:10px 18px; border-radius:9px; font-size:12.5px; font-weight:600; letter-spacing:.4px;
  box-shadow:0 18px 44px -20px #000; z-index:22;
  animation:toastIn .2s ease both;
}
.toast.hidden{ display:none; }
.toast.info{ background:rgba(14,13,11,.95); border-color:#4a3c2c; color:#ecdcc2; }
.toast.good{ background:rgba(11,20,12,.95); border-color:#2c5a2c; color:#d5f2d3; }
@keyframes toastIn{ from{ opacity:0; transform:translate(-50%,-10px);} to{opacity:1;transform:translate(-50%,0);} }
.toast.out{ animation:toastOut .35s ease forwards; }
@keyframes toastOut{ to{ opacity:0; transform:translate(-50%,-10px); } }

/* ---- the double tap that could not ----
   A toast says something happened; this says why the thing you just asked
   for did not. ANCHORED AT THE FINGER rather than at the top of the screen,
   because it answers one tap on one chunk and reading it should not mean
   looking somewhere else — which on a phone is most of the map away.
   translate(-50%,-100%) puts the box ABOVE the point it is handed, so `top`
   is its BOTTOM edge — which is what lets ui.js flip it under the finger
   when there is no room over it. */
.costpop{
  position:absolute; z-index:24; pointer-events:none;
  max-width:min(250px, 64vw);
  background:rgba(30,12,10,.96); border:1px solid #6a2a22; color:#ffdcd6;
  padding:8px 12px; border-radius:9px;
  font-size:12px; font-weight:600; letter-spacing:.3px; line-height:1.4;
  box-shadow:0 18px 44px -20px #000;
  transform:translate(-50%,-100%);
  animation:popIn .16s ease both;
}
.costpop.hidden{ display:none; }
.costpop b{ color:#ffc0b0; }
@keyframes popIn{ from{ opacity:0; transform:translate(-50%,calc(-100% + 7px)); }
                    to{ opacity:1; transform:translate(-50%,-100%); } }
.costpop.out{ animation:popOut .3s ease forwards; }
@keyframes popOut{ to{ opacity:0; transform:translate(-50%,calc(-100% - 7px)); } }

.glerror{
  position:absolute; inset:0; display:grid; place-items:center; text-align:center;
  background:#0b0908; color:#e8968e; font-size:14px; padding:40px; z-index:30; line-height:1.6;
}
.glerror.hidden{ display:none; }

.hidden{ display:none !important; }
