/* SuperVirus interface — pixel-game styling.
 *
 * -- The reference ------------------------------------------------------------
 *
 * A pixel-art mobile RPG kit: flat teal panels on a dark teal ground, pale sage
 * list rows with dark text, a coral primary action, a yellow progress bar, hard
 * one-pixel borders, and small white squares at the corners of a floating
 * window. No gradients, no blur, no soft shadows, no fourteen-pixel radii.
 *
 * Everything below is that, and the rules that matter are:
 *
 *   FLAT      one colour per surface. The only gradient in the file is the
 *             four-corner handle mark, and that is geometry, not shading.
 *   HARD      borders are 2px and opaque; radii are 0 to 3px. A rounded card
 *             with a blurred backdrop is the look this replaces. Two, not the
 *             one the reference kit draws: the world behind this interface is
 *             rendered at a quarter resolution and magnified, so a one-pixel
 *             rule is half a pixel of the thing it sits next to and reads as a
 *             hairline from a different program.
 *   TWO INKS  light text on the dark panels, DARK text on the pale sage rows.
 *             The sage rows are the ones you act on, and the inversion is what
 *             makes them read as actionable rather than as more background.
 *   TWO ACCENTS  coral is "do this", yellow is "how much". They never swap.
 *
 * -- Why the strain colour survives -------------------------------------------
 *
 * `--v` is the colour the player chose for their pathogen in the laboratory, and
 * it is a mechanic: infected people, cough clouds, contaminated doorknobs and
 * infected countries are all drawn in it. So it stays, but it is now used only
 * for things that ARE the strain -- the counter, the route bars, the release
 * meter -- and the chrome around them is the fixed palette. Before this, a
 * player who picked a violet pathogen got a violet interface, which meant the
 * interface had no identity of its own.
 */

/* The two faces, served from this project's own `fonts/`, never from a CDN.
 *
 * VT323 is the pixel voice: titles, buttons, labels, anything the eye reads as
 * the game speaking. JetBrains Mono carries the NUMBERS, because VT323 is a
 * terminal face with a small x-height and thin strokes and a column of
 * populations and percentages set in it is measurably harder to read. Both come
 * from the author's Layercast project with their SIL Open Font Licences beside
 * them in `fonts/`.
 *
 * `font-display: block` on the pixel face: a title that renders in Helvetica for
 * two hundred milliseconds and then snaps to pixels looks broken. Better to wait.
 */
@font-face {
  font-family: 'VT323';
  src: url('../fonts/vt323.ttf') format('truetype');
  font-display: block;
}
@font-face {
  font-family: 'JBMono';
  src: url('../fonts/JetBrainsMono-Regular.ttf') format('truetype');
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: 'JBMono';
  src: url('../fonts/JetBrainsMono-Bold.ttf') format('truetype');
  font-weight: 700;
  font-display: swap;
}

:root {
  /* the fixed kit */
  --ink0: #2b3d44;          /* app ground */
  --ink1: #394f57;          /* panel */
  --ink2: #446668;          /* panel row, inset */
  --ink3: #749593;          /* border, muted rule */
  /* The panel colour at half alpha, for the chrome that floats ON the world
     rather than beside it: the bottom edge of the top bar, and every button in
     the band along the bottom of the screen. One variable so those two agree,
     and it has to be written as a literal rgba because `--ink1` is a hex and
     there is no arithmetic in this file. Keep the three channels equal to
     `--ink1` above if that ever changes. */
  --ink1-50: rgba(57, 79, 87, 0.5);
  /* The frost behind that half-alpha. A readability aid and a material, not an
     effect: three pixels quietens the scene under a label without hiding what
     it is, and the world stays recognisable through every panel that wears it.
     One number, so the whole floating layer is one material.

     Deliberately small for a second reason. The renderer presents at a pixel
     factor of four, so what is being blurred is a grid of big square blocks
     rather than a photograph, and a wide radius would dissolve exactly the look
     the game is drawn for. At three it softens the block EDGES and leaves the
     blocks, which is the effect worth having.

     Where `backdrop-filter` is unsupported the declaration is dropped and the
     panel is a flat 50%, which is what it was before the frost and is legible
     on its own -- the alpha is doing the work, the blur is helping it. */
  --frost: 3px;

  /* The height of a SMALL CONTROL, and the reason it is a token.
   *
   * Three things sit side by side in the laboratory's header -- the budget
   * badge, the gear and the reset arrow -- and two more in the corner of the
   * title screen: the language switch and the sound toggle. Every one of them
   * used to size itself from its own text: a font, a line-height, a padding and
   * a border, four numbers each, five boxes, no two of them the same sum. They
   * agreed where they happened to agree. On a wide screen the language switch
   * and the sound button both came to 36.8px and looked deliberate; at 375px the
   * same two were 40.8 and 35.2, and the badge beside the gear was 45 against
   * 35.2.
   *
   * So the height is declared here and every one of them takes it. `min-height`
   * rather than `height`, so a control whose text somehow outgrows the box still
   * shows all of it rather than clipping -- but each of those five has had its
   * vertical padding trimmed to sit inside this number, so in practice the token
   * decides and the text does not.
   *
   * Thirty-eight, because it is just above the tallest natural height in the
   * group (the badge's 38.3 at 23px) and comfortably above the 34px the small
   * buttons were using as a touch floor.
   *
   * It CASCADES, which is the point: a context that wants a bigger control
   * redefines it once on its own container and the whole group follows. The
   * wizard's header does exactly that -- see `#lab.lab-wizard .lab-head`.
   *
   * There are no exceptions left. The top bar's speed strip had one -- a 34px
   * floor that was never binding, so the strip settled at 36.95 beside a 38px
   * gear -- and it now takes the token like everything else. */
  --control-h: 38px;
  /* The "where am I" line's height, for the same reason `--control-h` is a
     token: something that is not the line has to know it. The camera pad floats
     above the bottom band and the band is a row taller when this line is in it,
     so the pad's clearance is an addition over these numbers rather than a
     figure copied out of a measurement. Enforced by `.where-line`'s padding and
     line-heights; measured at 40.0 at 375. */
  --where-h: 40px;
  /* The display cutout, horizontally.
   *
   * The vertical insets are used in thirteen places already; these two were
   * never used anywhere, and the Android build draws into the cutout on purpose
   * (`LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS`), so a device held with the camera
   * on a side edge can put a hole over the interface.
   *
   * Named here rather than written out at each site for the reason every other
   * token in this file exists: there are two dozen edge-anchored boxes and the
   * next one should be a `max()` against a name, not a fresh `env()` somebody
   * has to remember the fallback for.
   *
   * ALWAYS read through `max()`, never added: `max(14px, var(--safe-l))` keeps
   * the fourteen pixels every device without a cutout has always had, where
   * `calc(14px + ...)` would push everything inward on the phones that report a
   * couple of pixels for a curved edge. On a screen with no cutout both resolve
   * to 0px and every rule below computes exactly what it computed before. */
  --safe-l: env(safe-area-inset-left, 0px);
  --safe-r: env(safe-area-inset-right, 0px);
  /* How far a floating pad sits above the button that opened it. One number for
     every pad in the game -- the ground view's, the running globe's and the
     release screen's -- so the three clear their openers by the same gap rather
     than by three tens that happen to agree. It doubles as the bottom band's own
     row and column gap, which is where it started. */
  --band-gap: 10px;
  /* The release screen's eye, which is the one control on that screen with
     nothing else to size itself against: no band track, no dock. Something
     outside it needs the number -- the pad floats directly above it -- so it is
     a token and the button takes its height FROM the token, rather than the pad
     being told a height the button merely happens to have. Forty-four is the
     touch floor `.tool-btn` already uses; below the breakpoint it becomes the
     band's own button size, so the eye is the same square there as the eye in a
     running game. */
  --release-eye-h: 44px;
  --sage: #b9cbc9;          /* pale row, light surface */
  --sage-dim: #9db2b0;      /* pale row, pressed */
  --plum: #525162;          /* modal, secondary window */
  --white: #ffffff;
  --coral: #fd9978;         /* primary action */
  --coral-dark: #d97654;
  --gold: #fec837;          /* quantity, progress, value */

  --text: #e6efed;          /* on dark */
  --text-dim: #a8c0bd;
  --text-faint: #7d9a97;
  --text-dark: #1f2e32;     /* on sage */
  --text-dark-dim: #40585c;

  --bad: #ef6a5a;
  --good: #7fd3c8;

  /* the pathogen's own colour, set from the laboratory */
  --v: #5ef05a;
  --v-dim: #2c7a2a;
  --v-glow: rgba(94, 240, 90, 0.28);

  /* `--pixel` is the voice, `--mono` is the data. Never the other way round. */
  --pixel: 'VT323', ui-monospace, "DejaVu Sans Mono", monospace;
  --mono: 'JBMono', ui-monospace, "SF Mono", "DejaVu Sans Mono", Menlo, Consolas, monospace;
  --sans: var(--pixel);
  color-scheme: dark;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  background: var(--ink0);
  color: var(--text);
  /* Eighteen pixels, which reads like about thirteen in a normal face: VT323's
   * glyphs sit small in their em box, so every size in this file is the size it
   * would have been times 1.4. The numbers, set in JetBrains Mono, are the
   * exception and carry their own smaller sizes. */
  font-family: var(--pixel);
  font-size: 21px;
  letter-spacing: 0.02em;
  -webkit-font-smoothing: antialiased;
  user-select: none;
  overscroll-behavior: none;
}

/* `touch-action: none` is what makes the globe draggable with a finger. Without
 * it the browser claims the gesture first -- one-finger drag scrolls the page,
 * two fingers zoom it -- and the orbit handler sees a pointermove or two and
 * then nothing. It costs a mouse nothing, which is why it is not in a media
 * query. `overscroll-behavior` on the body is the same fight one level up: a
 * downward drag near the top of a phone screen is pull-to-refresh otherwise. */
#view { position: fixed; inset: 0; display: block; width: 100vw; height: 100vh; touch-action: none; }
#ui { position: fixed; inset: 0; pointer-events: none; }
#ui > * { pointer-events: auto; }
/* Every dialog in the game, and it is a SIBLING of `#ui` rather than a box
 * inside it.
 *
 * `#ui` is `position: fixed`, which makes it a stacking context, so a z-index
 * written on anything inside it is a rank among its own siblings and nothing
 * more. The region panel is a full screen on a phone at rank 44; dialogs were at
 * rank 40 and inside the same context, so the help button on that panel opened
 * its dialog UNDERNEATH the panel that spawned it. It looked like a dead button,
 * so people pressed it repeatedly, and each press added another invisible sheet.
 *
 * Raising 40 above 44 would have moved the race to the next panel. Out here
 * there is no race: the whole interface paints, then this does. */
#modals { position: fixed; inset: 0; z-index: 100; pointer-events: none; }
#modals > * { pointer-events: auto; }

button, input, select { font: inherit; letter-spacing: inherit; color: inherit; }

/* ------------------------------------------------------------- the pointer */

/* The cursor is drawn, like the icons, and for the same reason: the arrow was
 * the last thing on this screen that was the operating system's artwork rather
 * than the game's.
 *
 * The IMAGES are not here. `client/src/icons.js` holds the grids and rasterises
 * them to PNG data URIs -- generated, never a file -- and `ui.js` writes the
 * finished values onto the root as `--cur-default`, `--cur-pointer` and the
 * rest. This file only says WHICH cursor goes WHERE.
 *
 * Every use is written `var(--cur-x, <keyword>)`, and the keyword half is what
 * makes this safe. On a touch device nothing is installed -- `ui.js` asks
 * `(pointer: coarse)`, the same question `appshell.js` asks before going full
 * screen -- so every one of these falls back to the plain keyword it replaced
 * and the sheet behaves exactly as it did. The same is true for the moment
 * between first paint and the module loading, and on any browser that refuses
 * the image.
 *
 * The `!important` rules below are not a shortcut. `main.js` writes
 * `style.cursor` DIRECTLY on two things -- the world canvas, which switches
 * between "a country is under you" and "nothing is", and the measure rows, which
 * go dead when a measure is already running -- and an inline declaration beats
 * every ordinary rule in a stylesheet. `!important` is the one thing that does
 * not, so it is what translates those two keywords into the drawn cursor. The
 * attribute selector is reading the inline style the script just wrote; it is
 * not a class waiting to be invented, and `audio.js` reads the same inline value
 * to decide whether a row makes a sound, so the keyword has to stay a keyword. */
html { cursor: var(--cur-default, auto); }

input[type="text"], textarea { cursor: var(--cur-text, text); }

[style*="cursor: pointer"] { cursor: var(--cur-pointer, pointer) !important; }
[style*="cursor: default"] { cursor: var(--cur-default, default) !important; }

/* The world is a thing you drag -- the globe orbits, the ground pans -- so its
 * resting cursor says so, and the press closes the hand. The rules are ordered:
 * the ID beats the bare attribute selector above whatever the order, and among
 * these three the last one wins on equal specificity, which is why "pressed"
 * comes after "over a country". Holding the button IS a drag, even when the drag
 * started on something clickable. */
#view { cursor: var(--cur-grab, grab) !important; }
#view[style*="cursor: pointer"] { cursor: var(--cur-pointer, pointer) !important; }
#view:active { cursor: var(--cur-grabbing, grabbing) !important; }

/* --------------------------------------------------------------- drawn icons */

.vx-wrap {
  display: inline-block;
  width: 1.2em;
  height: 1.2em;
  vertical-align: -0.2em;
  line-height: 0;
  flex: none;
}
.vx-wrap svg.vx { display: block; width: 100%; height: 100%; }
.vx-wrap.vx-fallback { width: auto; height: auto; line-height: 1; }
.vx-wrap + * { margin-left: 2px; }
.side .icon .vx-wrap { width: 1.3em; height: 1.3em; vertical-align: middle; }
.card .em .vx-wrap, .class-opt .em .vx-wrap, .shop-item .em .vx-wrap {
  width: 1.1em; height: 1.1em;
}
.news .item .vx-wrap, .tag .vx-wrap { width: 1.3em; height: 1.3em; }

/* ------------------------------------------------------------------ surfaces */

/* The corner marks from the reference: four small white squares at the corners
 * of a floating window. One background-image, four gradients, no extra markup. */
.handles {
  background-image:
    linear-gradient(var(--white), var(--white)), linear-gradient(var(--white), var(--white)),
    linear-gradient(var(--white), var(--white)), linear-gradient(var(--white), var(--white));
  background-size: 4px 4px;
  background-position: -2px -2px, calc(100% + 2px) -2px, -2px calc(100% + 2px), calc(100% + 2px) calc(100% + 2px);
  background-repeat: no-repeat;
}

.panel {
  background: var(--ink1);
  border: 2px solid var(--ink3);
  border-radius: 2px;
}

/* ------------------------------------------------------------------- buttons */

.btn {
  appearance: none;
  border: 2px solid var(--ink3);
  background: var(--ink2);
  color: var(--text);
  padding: 8px 14px;
  border-radius: 2px;
  cursor: var(--cur-pointer, pointer);
  font-weight: 500;
  letter-spacing: 0.04em;
  transition: background .1s steps(2), color .1s steps(2);
}
.btn:hover { background: var(--ink3); color: var(--white); }
/* Two pixels, like the borders. The world behind this interface is magnified
   four times, so a one-pixel nudge on press is a quarter of the smallest thing
   on screen and reads as nothing happening. */
.btn:active { transform: translateY(2px); }
.btn:disabled { opacity: .45; cursor: var(--cur-no, not-allowed); }

.btn.primary {
  background: var(--coral);
  border-color: var(--coral);
  /* The depth under the primary action: it has to stay visibly thicker than the
     2px the other three sides carry, or the button stops looking pressable. */
  border-bottom: 4px solid var(--coral-dark);
  color: #2b1a12;
  font-weight: 700;
  padding-bottom: 6px;
}
.btn.primary:hover { background: #ffb193; border-color: #ffb193; }
/* The two pixels the bottom border loses are the two the top margin gains, so
   the row the button sits in does not jump when it is pressed. */
.btn.primary:active { border-bottom-width: 2px; margin-top: 2px; }
.btn.primary:disabled { background: var(--ink2); border-color: var(--ink3); color: var(--text-faint); }

.btn.ghost { background: transparent; }
.btn.danger { border-color: var(--bad); color: var(--bad); }
/* The token, not a height of its own -- see `--control-h`. A native button
   centres its own content, so a min-height is all this needs. */
.btn.small { padding: 4px 9px; font-size: 18.5px; min-height: var(--control-h); }

/* A column of full-width buttons. Labels stay on one line in both languages
   instead of wrapping mid-phrase, which a pixel face makes very visible. */
.btn-stack { display: flex; flex-direction: column; gap: 8px; }
.btn-stack .btn { width: 100%; white-space: nowrap; }

/* ------------------------------------------------------------------- screens */

.screen {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(28, 42, 47, 0.88);
  /* Above the bar (4) and the phone's band (3): the end card is appended after
     the HUD but those are ranked, and on a phone the card's title was under
     the three rows of the bar. */
  z-index: 10;
}
.screen.hidden, .hidden { display: none !important; }

/* ------------------------------------------------------- the heading scale
 *
 * One ladder, worn by every screen in the game: the laboratory, the release,
 * the settings, the region panel, every dialog and the end card.
 *
 * It exists because the laboratory's header had no hierarchy at all. The
 * screen's own name, the step's name, the research budget and the titles of the
 * cards below were all within two pixels of each other, so nothing led and you
 * had to read the bar left to right to find out where you were. Picking a bigger
 * number for that one heading would have fixed that one heading; the sizes were
 * ad hoc everywhere -- 22.5 in the laboratory, 24 in a dialog, 24.5 in the
 * region panel, 26 on the release sheet, all meaning "this is the title" -- so
 * the fix is a scale, once, and everything on it.
 *
 *   .screen-title       what this screen IS
 *   .screen-title.hero  the same, when the heading is the whole screen
 *   .screen-step        what you are doing on it, right now
 *
 * VT323 sits small in its em box, so these run about 1.6x what the same ladder
 * would be in a normal face: 32 and 24 read as roughly 20 and 15.
 */
.screen-title {
  margin: 0;
  font-size: 32px;
  line-height: 1.08;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--sage);
  /* A long country name is a title, and a title that widens its own panel is a
     panel with a sideways scrollbar. */
  min-width: 0;
  overflow-wrap: anywhere;
}
.screen-title.hero { font-size: 48.5px; letter-spacing: 0.08em; }
.screen-step {
  font-size: 24px;
  line-height: 1.2;
  letter-spacing: 0.04em;
  color: var(--white);
}

.title {
  font-size: 75px;
  font-weight: 700;
  letter-spacing: 0.06em;
  margin: 0 0 10px;
  color: var(--sage);
  text-shadow: 3px 3px 0 var(--ink0);
}
.title-accent { color: var(--coral); }
.subtitle {
  color: var(--text-dim);
  margin: 0 0 28px;
  font-size: 21px;
  line-height: 1.7;
  max-width: 560px;
}

.menu-card { width: min(940px, 92vw); display: flex; flex-direction: column; align-items: center; text-align: center; }

.side-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; width: 100%; }
/* The two cards the whole game starts from, on the floating material.
 *
 * `--ink1-50` and `--frost`, the same two tokens the top bar, the ground tools,
 * the animal count, the camera pad, the way out and the dock buttons all wear.
 * Not new values: the point of a material is that there is one of it.
 *
 * -- What it costs in contrast, measured ------------------------------------
 *
 * Nothing -- it GAINS. The menu sits on `.screen`, which is an 88% scrim over
 * the globe, so half of `--ink1` over that scrim composites DARKER than the flat
 * `--ink1` it replaces, not lighter. Against the brightest frame the globe can
 * produce (a fully clipped white pixel, which is the worst case by construction
 * rather than by sampling a lucky frame):
 *
 *   heading, `--white`      9.4:1   was 12.1:1 on flat --ink1
 *   body, `--text-dim`      4.9:1   was 4.5:1
 *
 * The body line is the one that matters: it was sitting a hundredth above the
 * 4.5 threshold and is now comfortably over it. So the rotating globe underneath
 * cannot take the text below AA at any point in its turn, which is the thing a
 * moving backdrop had to be checked for. No text shadow -- that repair has been
 * turned down twice and is not needed here.
 *
 * -- The stacking context ----------------------------------------------------
 *
 * `backdrop-filter` makes this element a containing block for fixed descendants
 * and a stacking context, which is what broke the settings screen once and hid
 * the settings popover once. Nothing inside a `.side` depends on either: its
 * only control is the `(?)`, and `infoButton` mounts its dialog in `#modals`, a
 * sibling of the whole interface, precisely so that it cannot be trapped. */
.side {
  border: 2px solid var(--ink3);
  border-radius: 2px;
  padding: 20px 22px;
  background: var(--ink1-50);
  -webkit-backdrop-filter: blur(var(--frost));
  backdrop-filter: blur(var(--frost));
  cursor: var(--cur-pointer, pointer);
  text-align: left;
  transition: background .1s steps(2), border-color .1s steps(2);
}
.side:hover { background: var(--ink2); border-color: var(--coral); }
.side h3 { margin: 0 0 8px; font-size: 28px; letter-spacing: 0.04em; color: var(--white); }
.side .icon { font-size: 34px; display: block; margin-bottom: 12px; }
.side p { margin: 0; color: var(--text-dim); font-size: 19.5px; line-height: 1.7; }

/* ------------------------------------------------------------------ language */

/* The switch and the sound button beside it are one pair and must be one
   height. The WRAPPER takes the token and the options stretch to fill it, which
   is the only arrangement that survives a change of font or padding: sizing the
   options and letting the wrapper add its padding and border on top is how this
   ended up 40.8px tall next to a 35.2px button. */
.lang {
  display: inline-flex; gap: 2px; padding: 2px;
  border: 2px solid var(--ink3); background: var(--ink1); border-radius: 2px;
  min-height: var(--control-h);
}
/* No vertical padding: the height comes from the wrapper above and the option
   centres its label in whatever it is given. */
.lang-opt {
  appearance: none; border: 0; background: transparent; color: var(--text-dim);
  padding: 0 11px; border-radius: 2px; cursor: var(--cur-pointer, pointer); font-size: 18.5px;
  display: inline-flex; align-items: center; justify-content: center;
}
.lang-opt:hover { color: var(--white); background: var(--ink2); }
.lang-opt.on { background: var(--sage); color: var(--text-dark); font-weight: 700; }
.lang-corner { position: absolute; right: max(18px, var(--safe-r)); top: 16px; }

/* ------------------------------------------------------------ the laboratory */

#lab {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-columns: 292px 1fr 336px;
  grid-template-rows: auto 1fr;
  background: var(--ink0);
  align-items: stretch;
  justify-items: stretch;
}

.lab-head {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 10px 18px;
  padding-left: max(18px, var(--safe-l));
  padding-right: max(18px, var(--safe-r));
  border-bottom: 2px solid var(--ink3);
  background: var(--ink1);
}
/* Size, weight, tracking and colour all come from `.screen-title` now. */
.lab-head h2 { margin: 0; }
.lab-head .spacer { flex: 1; }

/* The budget badge: what there is left to spend, wherever it is being spent.
 *
 * One component and one appearance, because there are three screens that show a
 * balance -- the laboratory, the mutation sheet and the response sheet -- and
 * until now only the first of them showed one at all. It takes a LIST of
 * figures, so the response side's two currencies and its sample count are the
 * same widget rather than a second one that looks similar.
 *
 * Sized to be read rather than found. It was nineteen pixels on a wide screen
 * and twelve in the wizard, tucked under the title, which for the one number the
 * whole screen is about is roughly the opposite of the right emphasis. */
.budget { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.budget-item {
  font-size: 23px;
  line-height: 1.1;
  /* The same token the gear and the reset arrow beside it take, and the padding
     trimmed by a pixel so the text sits inside it rather than pushing past. */
  min-height: var(--control-h);
  display: inline-flex; align-items: center;
  padding: 3px 11px;
  border-radius: 2px;
  border: 2px solid var(--gold);
  color: var(--gold);
  background: var(--ink0);
  white-space: nowrap;
}
.budget-item.broke { border-color: var(--bad); color: var(--bad); }

.lab-col {
  padding: 14px;
  overflow-y: auto;
  min-height: 0;
  border-right: 2px solid var(--ink3);
  background: var(--ink1);
}
.lab-col:last-child { border-right: 0; }
.lab-main {
  padding: 14px 18px; overflow-y: auto; min-height: 0; background: var(--ink0);
  /* The line between the middle column and the right-hand one. It lives here
     rather than as a `border-left` on the column it separates, because this
     project has no `border-left` anywhere and is not getting one back. */
  border-right: 2px solid var(--ink3);
}

.field { margin-bottom: 16px; }
.field label {
  display: block; font-size: 16px; text-transform: uppercase; letter-spacing: 0.12em;
  color: var(--text-faint); margin-bottom: 7px;
}
.field input[type=text] {
  width: 100%; padding: 8px 10px; border-radius: 2px; font-size: 21px;
  border: 2px solid var(--ink3); background: var(--ink0);
}
.field input[type=text]:focus { outline: none; border-color: var(--coral); }

/* pale rows — the things you act on */

.class-list { display: flex; flex-direction: column; gap: 4px; }
.class-opt {
  display: flex; gap: 10px; align-items: flex-start;
  padding: 9px 10px;
  border: 2px solid transparent;
  border-radius: 2px;
  cursor: var(--cur-pointer, pointer);
  background: var(--ink2);
  color: var(--text);
}
.class-opt:hover { background: var(--ink3); }
.class-opt.on { background: var(--sage); color: var(--text-dark); border-color: var(--white); }
/* `!important` because the perk line carries an inline colour of its own, and a
   green meant for a dark row is invisible on a pale one. The body drops to the
   full dark ink rather than the dimmed one: dimmed dark-on-pale reads as grey on
   grey, which is the state the selected card was actually in. */
.class-opt.on span { color: var(--text-dark) !important; }
.class-opt.on b { color: var(--text-dark); }
.class-opt .em { font-size: 19px; line-height: 1; }
.class-opt b { display: block; font-size: 20.5px; margin-bottom: 3px; letter-spacing: 0.03em; }
.class-opt span { font-size: 18px; color: var(--text-dim); line-height: 1.55; }

.swatches { display: flex; gap: 5px; flex-wrap: wrap; }
.swatch { width: 26px; height: 26px; border-radius: 2px; cursor: var(--cur-pointer, pointer); border: 2px solid transparent; }
.swatch:hover { border-color: var(--text-dim); }
.swatch.on { border-color: var(--white); }

/* tabs — flat, active one underlined in coral */

.tabs { display: flex; gap: 0; margin-bottom: 12px; flex-wrap: wrap; border-bottom: 2px solid var(--ink3); }
.tab {
  padding: 7px 14px;
  border: 0;
  border-bottom: 3px solid transparent;
  background: transparent;
  cursor: var(--cur-pointer, pointer);
  font-size: 19.5px;
  color: var(--text-faint);
  letter-spacing: 0.04em;
}
.tab:hover { color: var(--text); }
.tab.on { color: var(--white); border-bottom-color: var(--coral); background: var(--ink1); }

.grid-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(226px, 1fr)); gap: 6px; }

.card {
  border: 2px solid transparent;
  border-radius: 2px;
  padding: 10px 11px;
  background: var(--ink1);
  cursor: var(--cur-pointer, pointer);
}
.card:hover { background: var(--ink2); border-color: var(--ink3); }
.card.owned { background: var(--sage); color: var(--text-dark); }
.card.owned p { color: var(--text-dark-dim); }
.card.owned .cost { color: var(--text-dark); border-color: var(--text-dark-dim); }
.card.maxed { border-color: var(--white); }
.card.unaffordable { opacity: .42; }
.card .row { display: flex; align-items: center; gap: 8px; }
.card .em { font-size: 18px; }
.card b { font-size: 20.5px; flex: 1; letter-spacing: 0.03em; }
.card .cost {
  font-size: 17px; color: var(--gold);
  border: 2px solid var(--gold); padding: 0 6px; border-radius: 2px;
}
.card .cost.max { color: var(--text-faint); border-color: var(--text-faint); }
.card p { margin: 6px 0 0; font-size: 18px; color: var(--text-dim); line-height: 1.55; }
.card .pips { display: flex; gap: 3px; margin-top: 8px; }
.card .pip { width: 22px; height: 5px; background: rgba(0, 0, 0, .28); }
.card .pip.on { background: var(--gold); }
.card.owned .pip { background: rgba(0, 0, 0, .18); }
.card.owned .pip.on { background: var(--coral); }

/* dials — yellow track, teal knob, arrow ends, from the reference */

.dial { margin-bottom: 16px; }
.dial .head { display: flex; align-items: baseline; gap: 7px; margin-bottom: 4px; }
.dial .head b { font-size: 20.5px; flex: 1; letter-spacing: 0.03em; }
.dial .head .val { font-size: 17px; color: var(--gold); }
.dial .head .cost { font-size: 16px; color: var(--text-faint); }
.dial input[type=range] {
  width: 100%; appearance: none; height: 14px; background: transparent; cursor: var(--cur-pointer, pointer);
}
.dial input[type=range]::-webkit-slider-runnable-track {
  height: 6px; background: var(--ink0); border: 2px solid var(--gold);
}
.dial input[type=range]::-moz-range-track {
  height: 6px; background: var(--ink0); border: 2px solid var(--gold);
}
.dial input[type=range]::-webkit-slider-thumb {
  appearance: none; width: 12px; height: 12px; margin-top: -5px;
  background: var(--sage); border: 2px solid var(--white); border-radius: 50%;
}
.dial input[type=range]::-moz-range-thumb {
  width: 12px; height: 12px; background: var(--sage); border: 2px solid var(--white); border-radius: 50%;
}
.dial .ends { display: flex; justify-content: space-between; font-size: 16px; color: var(--text-faint); }
.dial p { margin: 4px 0 0; font-size: 17px; color: var(--text-dim); line-height: 1.5; }

.verdict {
  margin-top: 14px; padding: 12px; border-radius: 2px;
  border: 2px solid var(--ink3); background: var(--ink0); font-size: 19.5px; line-height: 1.6;
}
.verdict .line { color: var(--gold); font-weight: 700; margin-bottom: 9px; letter-spacing: 0.03em; }
/* The ONE place left in this file that keeps the data face, and the test is the
   one this file has always stated: is this a figure somebody reads DOWN a column
   and compares? Here it is, and only here -- the laboratory's read-out and the
   region panel are both a stack of label-and-number rows, and the digits have to
   line up between them. The top bar's counters and the end card's four tiles
   both used to be set in it too; they are read ACROSS, once, so they failed the
   test and have gone to the pixel face with everything else. */
.stat-row { display: flex; justify-content: space-between; font-size: 18.5px; padding: 2px 0; gap: 10px; }
.stat-row span:first-child { color: var(--text-dim); }
.stat-row span:last-child { font-family: var(--mono); font-size: 11px; color: var(--text); }

/* progress bars — yellow fill, hard track, from the reference */

.bars { margin-top: 12px; }
.bar-row { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; font-size: 18px; }
.bar-row .name { width: 118px; color: var(--text-dim); }
.bar-row .track { flex: 1; height: 7px; background: var(--ink0); border: 2px solid var(--ink3); }
.bar-row .fill { height: 100%; background: var(--gold); }

/* ----------------------------------------------------------------- the HUD */

#hud { position: absolute; inset: 0; pointer-events: none; }
#hud > * { pointer-events: auto; }

/* The bar, and the one gradient in this file that is not the corner handles.
 *
 * `FLAT` at the top of this stylesheet means one colour per surface, and this
 * still is one colour -- it is the ALPHA that ramps, from the panel colour at
 * the top edge to half of it at the bottom, so the city or the runway underneath
 * comes through where the bar meets the world and the bar stops reading as a lid
 * bolted over the game.
 *
 * The ramp runs the way it does because the two ends have different jobs. The
 * top edge is against the top of the screen where nothing is drawn behind it, so
 * it can be solid and give the numbers a ground to sit on; the bottom edge is
 * where the world starts and is where the seam would show. Everything that has
 * to be READ -- the date, the clock, the four counters -- sits in the upper two
 * thirds, and the row that lands in the thin half is the controls, every one of
 * which carries its own opaque button.
 *
 * The 2px rule underneath is doing more work than it was and stays fully opaque:
 * with a translucent plate above it, that line is the whole of what separates
 * the interface from the scene. */
/* `z-index: 4`, and it is the second half of the backdrop-filter fix.
 *
 * A `backdrop-filter` makes its element a stacking context, so the settings
 * popover's own `z-index: 20` became a rank INSIDE the bar and could no longer
 * reach past it. The bar itself is `z-index: auto` and is appended before the
 * meters, the feed and the response sheet, and among positioned boxes at the
 * same level DOM order decides -- so on a desktop the popover was painted
 * underneath the response panel it hangs over. Measured: `elementFromPoint` at
 * the middle of the open panel returned a row of the sheet behind it.
 *
 * Four, one above `.hud-band`'s three, which is the only other ranked thing in
 * the heads-up layer. It is deliberately far below the full-screen region panel
 * at 44 and `#modals` at 100: nothing is meant to paint over the top bar except
 * a sheet that is covering the whole game anyway. */
.topbar {
  position: absolute; top: 0; left: 0; right: 0; z-index: 4;
  display: flex; align-items: center; gap: 12px;
  padding: 7px 14px;
  /* The bar spans the whole width, so in landscape a side cutout lands on the
     strain's name at one end and the help mark at the other. */
  padding-left: max(14px, var(--safe-l));
  padding-right: max(14px, var(--safe-r));
  background: linear-gradient(to bottom, var(--ink1) 0%, var(--ink1) 38%, var(--ink1-50) 100%);
  -webkit-backdrop-filter: blur(var(--frost));
  backdrop-filter: blur(var(--frost));
  border-bottom: 2px solid var(--ink3);
}
.brand {
  display: flex; align-items: center; gap: 8px;
  font-weight: 700; letter-spacing: 0.05em; color: var(--sage);
  /* Truncate rather than push. A flex item's default min-width is its content,
     so a long strain name grew the bar and shoved every counter off the end
     instead of being cut. */
  min-width: 0; flex: 0 1 auto;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.brand .dot { flex: none; }
.stat .k { white-space: nowrap; }
.brand .dot { width: 9px; height: 9px; background: var(--v); }
.topbar .spacer { flex: 1; }

.stat { display: flex; flex-direction: column; align-items: flex-end; line-height: 1.25; min-width: 68px; }
.stat .k { font-size: 14.5px; text-transform: uppercase; letter-spacing: 0.12em; color: var(--text-faint); }
/* The pixel face, not the data face.
   The test this file already states for `--mono` is whether the figure is one
   somebody reads DOWN a column and compares. These five are a ROW -- read
   across, once, at a glance -- so they fail it, and they were the last thing in
   the bar quietly set in a second typeface. Twenty-one pixels of VT323 is about
   what fourteen of JetBrains was, so the bar does not grow on a wide screen. */
.stat .v { font-size: 21px; color: var(--text); }
/* The date and the clock share the date cell's caption. A row on a wide screen;
   the small-screen block turns it into the two columns the counters use. */
/* `nowrap`: the clock widened this cell, and on the narrowest wide window the
   date broke across two lines and took the height of the whole bar with it. The
   strain's name is the flexible thing on this row and already ellipsises. */
.bar-when { display: flex; align-items: baseline; gap: 10px; white-space: nowrap; }
/* The clock spins -- see `gameClock` -- so its width must not. VT323 is a
   terminal face and every glyph in it has the same advance, which makes `ch`
   exact here, but the fallback stack behind it is only nearly monospaced, so the
   tabular figures are asked for as well. Nine characters, not eight: the colons
   are narrower than the digits in some of the fallbacks and the extra one is
   cheaper than a bar that twitches. */
.bar-clock { font-variant-numeric: tabular-nums; min-width: 9ch; }
.stat.bad .v { color: var(--bad); }
.stat.good .v { color: var(--good); }
.stat.strain .v { color: var(--gold); }

/* The bar's three groups.
 *
 * `display: contents` on a wide screen: the wrappers vanish from the layout and
 * the bar is the single flat row it has always been, unchanged. Below the
 * breakpoint they become rows -- see the small-screen block -- which is what
 * takes the line break away from the browser. It used to choose, and it chose
 * mid-counter: the last figure and a leading middot orphaned on a second line
 * beside the speed buttons, and the break moved as the numbers grew. */
.topbar .bar-stats, .topbar .bar-controls, .topbar .bar-tools { display: contents; }

/* `flex: none`, and it is not a tidy-up.
 *
 * The bar is a flex row, and just above the breakpoint -- a 960px window with a
 * long strain name in it -- the row wants more than it has and every item on it
 * shrinks by its share. That took the equal pair below apart: a shrunk `1fr`
 * track cannot go under its own content, so `1:1` collapsed to 50.5 and `60:1`
 * stopped at 54.7, and the two buttons that are supposed to be identical were
 * four pixels different at exactly one range of window widths. The controls do
 * not shrink; the strain's name does, which is what its ellipsis is for. */
.speed { display: flex; align-items: stretch; gap: 2px; flex: none; }
.speed .btn { padding: 4px 9px; font-size: 19px; }
.speed .btn.on { background: var(--coral); border-color: var(--coral); color: #2b1a12; }

/* The two sustained speeds, the same width by construction.
 *
 * `1:1` is three characters and `60:1` is four, so left to their own content
 * they come out different and the strip reads ragged. Two equal `1fr` tracks in
 * a grid that is itself sized to its content: the wider label decides the track,
 * both buttons take it, and the number is never written down anywhere. A width
 * measured off one button and typed into the other is right until the next time
 * the font, the padding or a label changes -- which is the same failure this
 * file already fixed for the language switch against the sound button, and for
 * the laboratory's header, both with a shared token rather than a copied number.
 *
 * Pause is not in here: it wears a glyph, not a label, and there is nothing for
 * it to agree with. Nor is fast forward, which is deliberately bigger. */
.speed-steps { display: grid; grid-template-columns: 1fr 1fr; gap: 2px; }
.speed-steps .btn { width: 100%; }

/* Fast forward: the big one, and the only control in the game that is held.
 *
 * Bigger because it is where the waiting happens -- an incubation, a cure, a
 * border reopening -- and a control the player rests a thumb on for a minute at
 * a time cannot be the same size as one he taps once an hour.
 *
 * `touch-action: none` is not decoration: without it the browser waits to see
 * whether the press is the start of a scroll, and the hold either does not begin
 * or ends the moment the finger moves a pixel. The two `user-select` lines and
 * the callout stop a long press turning into a text selection or an iOS
 * share-sheet over the button, both of which swallow the `pointerup` that ends
 * the hold. */
.speed .btn.ff {
  /* Three times its own height, from the same token every control on this row
     takes its height from -- so "bigger" is a PROPORTION and not a measurement.
     It comes out at a little over twice the width of a speed step, which is the
     size difference that says primary without the strip looking lopsided, and
     it is the same button at every width: a control that grew to fill whatever
     the row had left was six pixels wider than a step on a desktop and four
     hundred and seventy on a tablet. */
  width: calc(var(--control-h) * 3);
  /* No padding, and a flex box to centre what is left. The drawing is nearly as
     tall as the button -- see the icon rule below -- so there is nothing for
     padding to do except push the button past `--control-h` and leave it a
     visible half-step taller than the gear beside it. Wider is the whole of
     "bigger" here; taller would only break the row. */
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}
/* Nearly the height of the button. This is the largest icon in the interface
   and it is meant to be: at arm's length on a phone the shape is what is read,
   not the button under it.
   1.55em, which is 29.45px of the nineteen this row is set in, and `snapSize`
   then rounds it to a whole number of DEVICE pixels per cell -- 32 device pixels
   square, two per cell, at every display scale measured (1.25 and 2 both land
   there). That rounding is why the size is not a round number: asking for 32 CSS
   pixels on a 1.25 display would snap to 40 and no longer fit the button, while
   1.55em lands on the same two-pixel cell either way. Cells of equal size are
   what keep one arm of the arrow from coming out thicker than the other. */
.speed .btn.ff .vx-wrap { width: 1.55em; height: 1.55em; }
/* --- the two fast-forward states, and why they do not merely differ in shade
 *
 * One button answers to two gestures and has to show which one is running,
 * WITHOUT the player waiting to see whether the clock keeps moving. Two tints of
 * the same coral would have been a difference you check rather than one you see,
 * so the pair is separated the way this file already separates a primary action
 * from a pressed one -- by GEOMETRY, in opposite directions:
 *
 *   LATCHED  raised. Flat coral and a four-pixel `--coral-dark` under-edge, the
 *            exact treatment `.btn.primary` wears: sitting proud, pressable,
 *            waiting for the tap that ends it.
 *   HELD     pressed. The darker coral, the under-edge collapsed to the two
 *            pixels of an ordinary border, and the whole button two pixels down
 *            -- what every button in this file does on `:active`.
 *
 * So the two states are on opposite sides of the row's baseline and four pixels
 * of edge apart, which reads at arm's length on a phone. Same accent for both,
 * because coral is still what says which speed is running; brightness inverted,
 * because the arrows are white and have to hold on either.
 *
 * The four-pixel edge is affordable exactly here: `--control-h` is 38, the top
 * border takes 2 and this takes 4, leaving 32 -- which is the icon's snapped
 * size, to the pixel, at every display scale it can land on. See the icon rule
 * above before changing either number. */
/* Three sides written out one at a time rather than `border-color` and then a
   `border-bottom` over it. Both spellings compute the same, but a shorthand
   holding a `var()` cannot be serialised once a later shorthand has overridden
   part of it, so the second one renders in devtools as `border-top-color: ;` --
   a rule that looks broken, is not, and will be "fixed" by whoever reads it. */
.speed .btn.ff.latched {
  background: var(--coral);
  border-top-color: var(--coral);
  border-right-color: var(--coral);
  border-left-color: var(--coral);
  border-bottom: 4px solid var(--coral-dark);
}
/* After `.latched` on purpose: holding a latched button looks held, so the two
   rules have to resolve in that order. */
.speed .btn.ff.held {
  background: var(--coral-dark);
  border-color: var(--coral);
  border-bottom-width: 2px;
  transform: translateY(2px);
}

/* meters */

.meters { position: absolute; left: max(14px, var(--safe-l)); bottom: 14px; width: 264px; display: flex; flex-direction: column; gap: 6px; }
.meter { background: var(--ink1); border: 2px solid var(--ink3); border-radius: 2px; padding: 9px 11px; }
/* `gap`, and not `justify-content` alone. The label carries `flex: 1` and eats
   every spare pixel, so `space-between` has nothing left to distribute and the
   figure ends up hard against the help button.
   The small-screen block already set `gap: 8px` on this row, which is why the
   crowding was only ever visible on a wide screen -- the phone had been quietly
   fixed and the desktop had not. Twelve here, because the wide panel has the
   room and the row it sits in is taller. */
.meter .lbl { display: flex; justify-content: space-between; gap: 12px; font-size: 18.5px; margin-bottom: 6px; align-items: center; }
.meter .lbl b { font-weight: 700; letter-spacing: 0.03em; flex: 1; min-width: 0; }
.meter .lbl .n { font-size: 17px; color: var(--gold); }
.meter .track { height: 8px; background: var(--ink0); border: 2px solid var(--ink3); }
.meter .fill { height: 100%; transition: width .2s steps(6); }
.meter.deploy .fill { background: var(--gold); }
.meter.cure .fill { background: var(--good); }
.meter.ready { border-color: var(--coral); }
.meter.ready .fill { background: var(--coral); }
.meter-hint { font-size: 16px; color: var(--text-faint); margin-top: 6px; line-height: 1.5; }

/* news */

/* The feed is never a free-floating column any more, at any width.
   On a phone it always lived in the dialog the dock's third button opens. On a
   wide screen it used to stand in the bottom-right corner under the response
   sheet, and the two fought for the same corner: the sheet stopped at a number
   chosen once against a feed that had since grown to `42vh`, and on a tall
   screen the headlines were under the measures. The owner's call was not to
   shorten the sheet but to give the wide screen the phone's answer -- the same
   button, the same badge, the same centred dialog, and the sheet gets its full
   height back. So this rule is only the feed's SHAPE; where it shows is decided
   by `.dock-body .news` further down, in both layouts. */
.news { display: none; flex-direction: column; gap: 3px; width: 322px; }
.news .item {
  background: var(--ink1);
  border: 2px solid var(--ink3);
  border-radius: 2px;
  padding: 7px 10px;
  font-size: 18.5px;
  line-height: 1.5;
}
.news .item .d { font-size: 16px; color: var(--text-faint); margin-right: 8px; }
.news .item.alert .d { color: var(--coral); }
.news .item.spread .d, .news .item.mutation .d { color: var(--gold); }
.news .item.cure .d { color: var(--good); }
.news .item.measure .d { color: var(--sage); }
.news .item.border .d { color: var(--coral-dark); }
.news .item.politics .d { color: var(--text-dim); }
/* `seed` is the rarest headline in the game -- the player spending the entire
   release meter to open a second outbreak -- and it was the one news kind with
   no rule of its own, so it fell through to the default grey and read as
   background chatter. The strain's own colour, because this is the one entry in
   the feed that is the player acting rather than the world reacting. */
.news .item.seed .d { color: var(--v); }

/* region and response panels */

/* `top` clears the bar rather than guessing at it. Sixty pixels is what the top
   bar measures on a wide screen now that its counters are set in the pixel face
   at twenty-one; the four anchors below all used to say 52 or 48, which was one
   size of that bar ago and had them tucked under it. */
/* Every panel in the game wears this, and every panel's geometry belongs HERE.
 *
 * If you are adding one: give it a class and a rule beside `.cure-panel` and
 * `.release-panel` below. An inline `style` object carrying `position`, `top`,
 * `right`, `left` or `width` cannot be beaten from this file, so a panel written
 * that way quietly opts out of every rule added afterwards -- which is how one
 * of them ended up 336px wide inside a 351px phone sheet, and the other pinned
 * under a landscape display cutout. See the note in `Hud.showRegion`. */
.region-panel {
  position: absolute; right: max(14px, var(--safe-r)); top: 66px; width: 322px;
  background: var(--ink1);
  border: 2px solid var(--ink3);
  border-radius: 2px;
  padding: 14px;
  /* The whole height under the top bar, less the same 14px inset the panel
     keeps at its side. Nothing shares this corner any more -- the feed is behind
     a button -- so nothing is subtracted for it. */
  max-height: calc(100vh - 66px - 14px);
  overflow-y: auto;
}
.region-panel h3 { margin: 0 0 3px; }
.region-panel .sub { color: var(--text-faint); font-size: 18px; margin-bottom: 12px; line-height: 1.5; }
/* The way out, which is the same boxed cross `modal()` uses -- there is one
   close button in the game now, in `ui.js`, and this only says where it sits.
   It was a bare glyph here: no border, no background, eighteen pixels, floating
   in the corner. It read as decoration and it was a quarter of the target. */
.region-panel .panel-head .dialog-x { position: absolute; right: 10px; top: 10px; }
/* Room for it, so a long country name does not run underneath. */
.region-panel .panel-head { padding-right: 56px; }
/* The chosen country, on the release screen. The spacing lives here rather than
   in an inline style because the small-screen arrangement puts the same node in
   a two-line strip at the top of the globe, where a 22px column of margin is a
   fifth of the strip. */
.region-panel .pick-label { margin: 8px 0 14px; }
/* The country card on the cure side, wide screen only -- `showRegion` adds the
   class and sets the two properties from the eye button's own rectangle, the
   same way the camera pad is hung (see `hangUnder` in hud.js). Right is
   released so `left` can take over; the fallbacks are the pad's old numbers so
   a card with nothing to hang from still lands under the strip. The response
   sheet owns the top-right corner on that side, and a card opened there sat on
   top of it. */
.region-panel.under-eye {
  right: auto; left: var(--card-x, 50%); top: var(--card-y, 118px);
  transform: translateX(-50%);
}
/* The cure side's response panel. These three were an inline style written by
   `buildCurePanel`, which meant no rule in this file could beat them without
   `!important` -- including the one that makes the panel fill the dialog it is
   moved into on a phone, where it stayed 336px wide inside a 351px sheet and
   pushed its rows past the edge. */
/* The width is a token because a second thing now has to clear it: the news
   button on a wide screen sits to the LEFT of this sheet, and a button that
   copied 336 would sit inside the sheet the day the sheet was widened. The
   region panel is 322 and is left alone -- narrower, so anything that clears
   this clears it too. */
:root { --side-panel-w: 336px; }
.cure-panel { right: max(14px, var(--safe-r)); top: 66px; width: var(--side-panel-w); }
/* The release screen's panel, for the same reason the one above is here: it was
   four values in an inline style, which no rule in this file could reach. It
   sits higher and wider than the running game's -- there is no top bar on that
   screen, so it starts at the same 18px inset it keeps at the sides. */
.release-panel {
  right: max(18px, var(--safe-r)); top: 18px; width: 352px;
  max-height: calc(100vh - 36px);
}
/* Head, body and foot. On a wide screen they are plain blocks and the panel
   scrolls as one, exactly as it always did; on a phone the panel is a screen and
   these are the three things that have to behave differently -- a heading that
   stays put, a middle that scrolls, and actions that cannot be scrolled off. */
.panel-head, .panel-body, .panel-foot { display: block; }
/* A small caps heading inside a panel, with room for its question mark. */
.panel-sub {
  display: flex; align-items: center; gap: 8px;
  margin: 13px 0 4px; font-size: 17px; letter-spacing: .08em; color: var(--text-faint);
}
/* The label, not the question mark beside it. Unscoped, this stretched the
   mark across the whole row: `.card-info` sets `flex: none`, and a rule one
   class deeper on the parent's own children beats it. */
.panel-sub > span:first-child { flex: 1; min-width: 0; }
.info-row > .card-info { flex: none; align-self: center; }

.tagline { display: flex; gap: 4px; flex-wrap: wrap; margin: 10px 0; }
.tag { font-size: 16px; padding: 2px 7px; border-radius: 2px; border: 2px solid var(--ink3); color: var(--text-dim); }
.tag.hot { border-color: var(--gold); color: var(--gold); }
.tag.warn { border-color: var(--coral); color: var(--coral); }
.tag.good { border-color: var(--good); color: var(--good); }

.measure-list { display: flex; flex-direction: column; gap: 2px; margin-top: 8px; }
.measure-row {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 9px; border-radius: 2px;
  background: var(--ink2);
  font-size: 18.5px;
  color: var(--text);
}
.measure-row:hover { background: var(--ink3); }
.measure-row.on { background: var(--sage); color: var(--text-dark); }
/* Cannot be paid for, or is waiting on something that has not been done yet.
   The same reading as an unaffordable mutation card, for the same reason: the
   ground colour says "not now" and the red price says which of the two numbers
   is the problem. Forty per cent opacity alone reads as "still loading". */
.measure-row.no { background: var(--ink0); color: var(--text-faint); }
.measure-row.no:hover { background: var(--ink0); }
.measure-row.no .lv { color: var(--bad); }
.measure-row.on .lv { color: var(--text-dark-dim); }
.measure-row .em { font-size: 15px; }
/* `min-width: 0`, and this is the whole horizontal-overflow bug on the response
   sheet. A flex item's automatic minimum is its CONTENT width, so `flex: 1`
   alone will not let a name shrink: "Gloves and protective gear" held the row
   wider than the panel, the panel's `overflow-y: auto` made the other axis
   `auto` too by the cascade rule, and the sheet grew a horizontal scrollbar with
   every question mark clipped off the right edge. Nothing in this game scrolls
   sideways. */
.measure-row .nm { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.measure-row .lv { font-size: 16px; color: var(--gold); }

/* toast, hint, the mutation shelf */

/* The transient message, on the panel treatment like every other surface.
 *
 * It was `--plum` behind a white rule, which is a different material from
 * anything else on screen: a purple-grey plate with a hard white edge over a
 * game whose panels are all dark teal. `--plum` is described in the palette as
 * "modal, secondary window" -- it is a SURFACE colour from the reference kit,
 * for the dialogs, and the dialogs moved to the ink family long ago. This was
 * the last thing still wearing it, which makes it a leftover rather than a
 * decision to stand out.
 *
 * And standing out is still required -- it is a message that appears for two
 * seconds and has to be read in one -- so the loudness is bought inside the
 * palette instead of outside it: the corner handles `toast()` already adds, which
 * mark it as a floating window and which no static panel in the game wears; the
 * middle of the screen, where nothing else is; white text rather than the
 * ordinary `--text`; and its own fade. Motion and position, not a hue nothing
 * else uses. */
.toast {
  position: absolute; left: 50%; top: 62px; transform: translateX(-50%);
  background: var(--ink1); border: 2px solid var(--ink3); border-radius: 2px;
  padding: 10px 18px; font-size: 21px; color: var(--white);
  /* Half the screen is all a `left: 50%` box has to grow into, so a sentence
     any longer than a few words wrapped into a tall narrow column. */
  max-width: min(560px, calc(100vw - 36px));
}

.hint {
  position: absolute; left: 50%; bottom: 14px; transform: translateX(-50%);
  font-size: 18px; color: var(--text-faint);
  background: var(--ink1); padding: 5px 12px; border-radius: 2px; border: 2px solid var(--ink3);
  transition: opacity .5s linear;
}
/* While the response sheet is up, the bottom row is not the whole width: the
   sheet takes the right, and the news button sits just left of it. Centred on
   the viewport the strip ran under that button -- measured, 856-920 against a
   strip at 320-960. So it is centred in the space that is actually free, the
   same answer the tools strip gives at the top, and capped so it cannot reach
   the button at a narrow desktop width: the sheet, its inset, the button and a
   gap on either side of it, all through the tokens they are made of. */
#hud:has(.cure-panel) .hint {
  left: calc((100% - var(--side-panel-w) - 24px) / 2);
  max-width: calc(100% - var(--side-panel-w) - 24px - 2 * (64px + 20px));
  text-align: center;
}
/* Faded, not removed: it still holds its space, so nothing under it jumps when
   it goes, and `pointer-events` goes with it so an invisible strip cannot eat a
   click on the map. */
.hint.gone { opacity: 0; pointer-events: none; }

/* The mutation sheet: a row of offers, and under it what has been marked.
   A column now rather than a bare row, because the marks need somewhere to be
   listed and the grid is only half the control. `overflow-x` moved off this box
   and on to the grid inside it -- a scroll container clips BOTH axes, and with
   it out here the picks list underneath was being clipped by a rule that exists
   for the row above it. */
.shop {
  position: absolute; left: 50%; bottom: 46px; transform: translateX(-50%);
  display: flex; flex-direction: column; gap: 6px;
  background: var(--ink1); border: 2px solid var(--ink3); border-radius: 2px;
  padding: 6px;
  /* Wide enough for six cards, and never wide enough to run under the news feed.
   *
   * -- Why this is arithmetic and not a round number --------------------------
   *
   * The strip is centred on the VIEWPORT, and the two panels it has to clear are
   * anchored to the edges: the feed is 322px at `max(14px, --safe-r)` on the
   * right, the meters 264px at the matching inset on the left. So the space a
   * centred box can occupy without touching either is the width minus twice the
   * wider panel, ITS OWN INSET, and a little air: 322 + 10, plus the inset the
   * feed is actually sitting at.
   *
   * That last term is the one worth stating, because leaving it out is a bug
   * that measures as almost-right. Reserving only 332 -- the panel and the air,
   * forgetting the fourteen pixels the panel is held off the edge by -- puts the
   * strip's edge exactly 4px past the feed's at EVERY width where the calc
   * binds, which looks like a rounding error and is a missing term. Measured at
   * 1280: strip to 948, feed from 944.
   *
   * `max(14px, ...)` and not the bare inset, because that is precisely what
   * `.news` resolves its own `right` to; the larger of the two insets, because a
   * centred box gives ground on both sides at once and only the worse one
   * matters.
   *
   * -- What it was doing before -----------------------------------------------
   *
   * `min(700px, 88vw)`, which at any laptop width resolved to a flat 700 and ran
   * straight under the feed. Measured: at 1180 the strip reached 887 against a
   * feed starting at 844, a 43px overlap; at 1440 it clears by 34. The crossover
   * is about 1372, so the strip was overlapping the feed at 1280 and at 1366 --
   * the two commonest laptop widths there are. It was logged as a curiosity in
   * the 901-980 band and it was never that.
   *
   * The 88vw is gone because the 700 cap already beats it above 795px, and below
   * 900 this whole box is `display: none` and the dock's dialog sets its own
   * width. It was doing nothing at either end.
   *
   * Inert from 1392 up -- 700 + 692 -- which is where the six-card design was
   * evidently tuned. Below that it gives up cards rather than position, and `.shop-grid`
   * scrolls, so nothing becomes unreachable -- three cards you can scroll past
   * beats six cards with a panel on top of them. */
  max-width: min(700px, calc(100vw - 2 * (332px + max(14px, var(--safe-l), var(--safe-r)))));
}
.shop-grid { display: flex; gap: 3px; overflow-x: auto; }
/* A real `button` element, and it has to look like one. It is the only control
   in the running game that spends points, and as a bordered-with-transparent
   div it read as a label on the panel behind it. */
.shop-item {
  appearance: none; font-family: inherit; letter-spacing: inherit;
  min-width: 88px; border: 2px solid var(--ink3); border-radius: 2px;
  padding: 7px 8px; text-align: center; cursor: var(--cur-pointer, pointer); font-size: 17px;
  background: var(--ink2); color: var(--text);
  /* The checkmark is positioned against this. */
  position: relative;
}
.shop-item:hover { background: var(--sage); color: var(--text-dark); border-color: var(--white); }
.shop-item:hover .c { color: var(--coral-dark); }
.shop-item:active { background: var(--sage-dim); color: var(--text-dark); transform: translateY(2px); }
.shop-item:active .c { color: var(--coral-dark); }
/* Cannot be paid for. Forty per cent opacity on its own reads as "still
   loading"; dropping to the ground colour and turning the price red says which
   of the two numbers is the problem. */
.shop-item.no { background: var(--ink0); border-color: var(--ink3); color: var(--text-faint); cursor: var(--cur-no, not-allowed); }
.shop-item.no .c { color: var(--bad); }
.shop-item.no:active { transform: none; background: var(--ink0); color: var(--text-faint); }
.shop-item .em { font-size: 18px; display: block; }
.shop-item .nm { display: block; margin: 3px 0; line-height: 1.3; }
.shop-item .c { font-size: 17px; color: var(--gold); font-weight: 700; }

/* --- marked, but not yet bought -------------------------------------------
 *
 * The owner asked for the count badges off the dock buttons -- "comme les
 * pastilles a numero" -- so this IS `.pip`, the same class and the same shape,
 * carrying a tick where those carry a number. Third scoped `.pip` in this file
 * after `.dock-btn`'s and `.card`'s, which is the house pattern for it.
 *
 * INSIDE the card, not hung off the corner the way the dock's is at
 * `top: -8px`. The grid it sits in is a horizontal scroller, and a scroll
 * container clips both axes, so a badge overhanging the card would have been
 * cut off along the top of every card on a desktop.
 *
 * The card changes too, and not only in the corner: at a glance across eight
 * cards a 26-pixel badge is what you count, but the coral border is what tells
 * you which one your thumb is over. Both, because they answer different
 * questions. */
.shop-item.picked { border-color: var(--coral); background: var(--ink1); }
.shop-item.picked .c { color: var(--coral); }
.shop-item .pip {
  position: absolute; top: 3px; right: 3px;
  /* Twenty-six square, which is the dock badge's own size: the owner asked for
     that badge, and a near-miss is worse than a difference. The sums: a 16px
     drawing, three pixels of padding each side, two of border each side. */
  min-width: 26px; height: 26px; padding: 0 3px; box-sizing: border-box;
  display: flex; align-items: center; justify-content: center;
  background: var(--coral); color: #2b1a12; border: 2px solid var(--coral-dark);
  font-size: 17px; font-weight: 700; line-height: 0;
}
/* The tick is a drawing, not a glyph -- see `icons.js` -- and it has to fit the
   badge rather than the text around it, which is why it is sized here at all.
   Fourteen asks for it; `snapSize` rounds that to 16 device pixels, one whole
   pixel a cell, at every display scale the badge is drawn at. */
.shop-item .pip .vx-wrap { width: 14px; height: 14px; }

/* --- what is about to be bought -------------------------------------------
 *
 * The space under the grid, which was empty. It holds the sheet's instructions
 * when nothing is marked and the list of marks when something is, so the dead
 * space is doing one of two useful jobs at all times rather than none. */
.shop-picks { display: flex; flex-direction: column; gap: 4px; }
/* The rows scroll and the total and the button do not, which is the whole
   reason they are wrapped: eight marks is eight rows, and on the desktop strip
   -- which floats over the globe rather than sitting in a dialog -- that took
   the panel past half the height of the window with the Buy button pushed off
   the bottom of it. */
.shop-pick-list {
  display: flex; flex-direction: column; gap: 4px;
  max-height: 132px; overflow-y: auto;
}
/* Not on the floating strip. It is the sheet's instructions and it belongs in
   the sheet, which is opened deliberately and closed again; on the desktop it
   would be forty pixels of standing text over the planet for the whole game.
   There it is the strip's tooltip instead -- see `showShop` -- and the first tap
   teaches the rest, since a tap now costs nothing to undo. */
.shop-hint { display: none; margin: 0; padding: 2px 2px 4px; color: var(--text-dim); font-size: 17px; }
.shop-picks-head {
  display: flex; justify-content: space-between; align-items: baseline; gap: 10px;
  border-top: 2px solid var(--ink3); padding-top: 6px; font-size: 17px;
}
.shop-picks-head .k { color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.12em; }
.shop-picks-head .n { color: var(--gold); font-weight: 700; white-space: nowrap; }
.shop-pick { display: flex; align-items: baseline; gap: 8px; font-size: 17px; }
.shop-pick .nm { flex: 1; min-width: 0; }
.shop-pick .who { display: block; }
/* The sentence a phone used to reach through a confirmation dialog. Hidden on
   the desktop strip, where the card's own tooltip already says it and where the
   strip is floating over the globe and should stay small. */
.shop-pick .why { display: none; color: var(--text-dim); line-height: 1.25; }
.shop-pick .c { color: var(--gold); font-weight: 700; }
.shop-commit { width: 100%; margin-top: 4px; }

/* The ground controls: the pin, the eye, and the live animal count.
 *
 * A bare flex row with no plate of its own -- each button wears its own border,
 * so a third one is an append and not a re-layout. It replaces the four-tab
 * strip, which on a phone was a two-by-two block plus a line of text pinned
 * across the bottom of the screen for as long as the player was on the ground.
 *
 * The buttons are translucent for the same reason the bar above is: they float
 * on the world rather than beside it. The border stays opaque and 2px. */
/* Centred, and the `max-width` is what keeps it centred SAFELY.
 *
 * On a wide screen this strip shares its row with the back button on the left,
 * which is 213px of "Retour à la carte". Centre a group of three things -- two
 * buttons and the animal count -- on a 901px window, the narrowest a wide screen
 * can be, and the group starts at 220 while the back button ends at 227: they
 * overlap, and the four-tab strip this replaces overlapped it worse. Clamping
 * the width puts the count on its own line instead, so what is centred is only
 * ever the two buttons and the group cannot reach back that far. */
.ground-tools {
  position: absolute; left: 50%; top: 66px; transform: translateX(-50%);
  display: flex; flex-wrap: wrap; justify-content: center; gap: 8px;
  align-items: center; max-width: 340px;
}
/* Under the buttons rather than in front of them -- see the clamp above. */
.ground-tools .place-note { order: 2; }
.tool-btn {
  appearance: none; cursor: var(--cur-pointer, pointer);
  display: inline-flex; align-items: center; gap: 8px;
  padding: 8px 14px; min-height: 44px;
  background: var(--ink1-50); border: 2px solid var(--ink3); border-radius: 2px;
  -webkit-backdrop-filter: blur(var(--frost));
  backdrop-filter: blur(var(--frost));
  color: var(--text); font-size: 18.5px; letter-spacing: 0.03em;
}
.tool-btn:hover { background: var(--ink2); color: var(--white); }
.tool-btn:active { background: var(--ink2); transform: translateY(2px); }
.tool-btn .em { font-size: 19px; line-height: 1; }
/* The count keeps a plate of its own: it is the one thing in this strip with no
   border round it, and dim text laid straight on a sunlit street is not text.
   Full `--text` rather than the `--text-faint` it had as a caption inside an
   opaque strip: on a half-transparent plate over a bright field the faint tone
   is the first thing to go, and this is live state the player is meant to read
   at a glance rather than a label on something else. */
.place-note {
  font-size: 16px; color: var(--text);
  /* A caption, not a control -- the same reason `.where-line` above carries
     this. It spans three of the band's five tracks at the bottom of a phone,
     which is exactly where a thumb starts a pan, and it was swallowing every
     drag that began on it. */
  pointer-events: none;
  background: var(--ink1-50); border: 2px solid var(--ink3); border-radius: 2px;
  -webkit-backdrop-filter: blur(var(--frost));
  backdrop-filter: blur(var(--frost));
  padding: 5px 10px; white-space: nowrap;
}

/* Where you are, permanently, once the dive title has finished saying it.
 *
 * -- Why it exists ------------------------------------------------------------
 *
 * The arrival title is enormous and then gone, and "après un certain temps à
 * être dedans on oublie où on est". This is the same two facts kept, small, in
 * the strip above the tools that was dead space.
 *
 * -- Why it looks like the title it comes from --------------------------------
 *
 * Same face, same two-part shape, same colours -- a white name and a coral
 * spaced-out place under it, here beside it -- so the small line reads as the
 * big one settling rather than as a new piece of interface. Smaller by a long
 * way: 24px is `.screen-step`, the rung of the heading ladder that answers
 * "what am I looking at", against the title's clamped 46-96.
 *
 * No text shadow. The title carries a hard 4px offset because it is white type
 * straight on the world with nothing behind it; this has a plate, and a second
 * shadow on top of a plate is the softening the owner has turned down twice.
 * The plate is the band's own material -- 50% ink, a 2px opaque border and the
 * one 3px frost -- so the line belongs to the floating chrome around it rather
 * than to a fourth kind of surface.
 *
 * One line, and the name is what gives way: a region name runs to "République
 * démocratique du Congo" and the place beside it is two words at most, so the
 * name ellipsises and the place is never the half that goes. */
.where-line {
  /* Above the buttons on a wide screen, where `.ground-tools` is a wrapping flex
     row. `order`, not DOM position: the animal count is already ordered BELOW
     the buttons from the same mechanism, and the three of them being placed by
     one property is what stops the strip depending on the order things were
     appended in. */
  order: -1; flex: 0 0 100%;
  display: flex; align-items: baseline; justify-content: center; gap: 8px;
  min-width: 0;
  /* It is a fact, not a control. `#hud > *` turns pointer events on for the
     band, so without this a 40px strip across the whole width would swallow
     every drag that started on it -- and the bottom of the screen is exactly
     where a thumb starts a pan. */
  pointer-events: none;
  /* The height is a TOKEN, not a consequence, because something else has to
     know it: the camera pad floats ten pixels above the bottom band, and the
     band is a row taller in a ground view than on the globe. Declared here,
     enforced by the padding and the two line-heights below, and read back in
     the `.cam-panel` rules -- the same arrangement `--band-btn` already has.
     Forty: 24px of VT323 at 1.15 is 27.6, plus 4px of padding each side and the
     2px border each side. Measured at 40.0 at 375. */
  min-height: var(--where-h);
  padding: 4px 10px;
  background: var(--ink1-50); border: 2px solid var(--ink3); border-radius: 2px;
  -webkit-backdrop-filter: blur(var(--frost));
  backdrop-filter: blur(var(--frost));
}
.where-line .wh-name {
  font-size: 24px; font-weight: 700; letter-spacing: 0.07em; color: var(--white);
  line-height: 1.15;
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The separator is drawn here rather than written into the markup: it is
   punctuation between two fields, not a word, so it has no language and no
   business in either string. */
.where-line .wh-sub::before { content: '·'; margin-right: 8px; color: var(--text-faint); }
.where-line .wh-sub {
  flex: none;
  font-size: 15px; letter-spacing: 0.3em; text-transform: uppercase;
  line-height: 1.15;
  color: var(--coral); white-space: nowrap;
}

/* The place menu: the four ground views as rows in a dialog.
 *
 * Rows rather than a grid of tiles, because each one now carries the three words
 * saying what is in it -- which the tabs could only put in a `title`, and a
 * phone has no tooltips. The mark on the one you are in is the pale row every
 * chosen thing in this game wears, and it is the same mark the lit tab had. */
.place-menu { display: flex; flex-direction: column; gap: 8px; }
.place-opt {
  appearance: none; cursor: var(--cur-pointer, pointer); text-align: left;
  display: flex; align-items: center; gap: 12px;
  padding: 10px 12px; min-height: 56px;
  background: var(--ink1); border: 2px solid var(--ink3); border-radius: 2px;
  color: var(--text); font-size: 20px; letter-spacing: 0.03em;
}
.place-opt:hover { background: var(--ink2); color: var(--white); }
.place-opt:active { transform: translateY(2px); }
.place-opt .em { font-size: 26px; line-height: 1; flex: none; }
.place-opt .nm { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.place-opt .nm b { font-weight: 700; }
.place-opt .sub { font-size: 17px; color: var(--text-faint); line-height: 1.3; }
.place-opt.on { background: var(--sage); border-color: var(--sage-dim); color: var(--text-dark); }
.place-opt.on .sub { color: var(--text-dark-dim); }
.place-opt.on:hover { background: var(--sage); color: var(--text-dark); }

/* The camera pad.
 *
 * Anchored to a corner and small, because it is the one sheet in the game that
 * must not cover what it acts on: the other dialogs are read or spent in, and
 * this one aims a view you have to be able to see while you aim it. Mounted in
 * `#modals` like every other dialog, so nothing in the interface can paint over
 * it -- it is only its SHAPE that differs. */
/* Top right on a wide screen, which is the one corner a ground view leaves
   empty: the meters hold the bottom left, the news feed the bottom right and the
   mutation strip the bottom middle. The small-screen block moves it down to sit
   over the button that opens it. */
/* Half-transparent like every other control that floats on the world, and
   legible for two reasons that stack.
   THE PLATE IS THINNED, THE CONTENTS ARE NOT -- the same trick the dock plays,
   where the button is 50% and its drawn icon and its badge are solid. Here the
   panel is the plate and everything with something to say, the nine buttons and
   the header strip, carries a second 50% of its own: half over half is three
   quarters, so an arrow sits on a firmer ground than a dock icon does, and the
   header sits on the arrows' ground rather than on the bare plate, where at noon
   over grass it fell to about 3.3:1 and stopped being text.
   AND THE FROST -- see `--frost` -- but only ONCE, on the panel. The buttons and
   the header carry the second 50% of ink and no filter of their own: their
   backdrop is the panel's already-frosted output, so a second blur would repaint
   what is already soft and cost eleven more compositor passes for it. The ink
   stacks, the blur does not need to.
   No shadow anywhere: a soft edge under a pixel face is the one repair this
   project will not make. */
.cam-panel {
  /* `--pad-x` is the centre of the button that opened it, in viewport pixels,
     set by `cameraPad` when it mounts. 50% is the fallback and it is not good
     enough on its own: the tools strip is centred in the space LEFT OF the
     response sheet, not in the viewport, so on a wide screen a screen-centred
     pad misses its own button by about sixty pixels. The pad is appended to the
     modal root rather than beside the button, so this is the only way the two
     can be told about each other.
     `--pad-y` is the same idea downward, and it earns its place: the eye sits on
     the strip's first row on the globe and its second row in a ground view, so a
     fixed top landed the ground pad on top of its own button. Both fallbacks are
     the globe's old numbers, so a pad opened with no anchor still lands where it
     always did. */
  position: absolute; left: var(--pad-x, 50%); top: var(--pad-y, 118px);
  transform: translateX(-50%);
  background: var(--ink1-50); border: 2px solid var(--ink3); border-radius: 2px;
  -webkit-backdrop-filter: blur(var(--frost));
  backdrop-filter: blur(var(--frost));
  padding: 8px;
}
/* A fill and no border: bordered, a full-width strip reads as a button, and the
   only thing in this row that can be pressed is the cross, which has its own. */
.cam-head {
  display: flex; align-items: center; gap: 8px;
  font-size: 19px; letter-spacing: 0.05em; margin-bottom: 5px;
  background: var(--ink1-50); border-radius: 2px; padding: 3px 4px 3px 6px;
}
/* Both pads hang under the eye that opens them, and that is now one rule.
 *
 * It used to be two. The ground pad sat top right, on the reasoning that a
 * ground view leaves that corner empty -- true when only the globe had a panel
 * anchored there. It stopped being true the day the response side got a sheet of
 * its own: in a ground view on the cure side the pad opened at the far right
 * edge, half a screen from the button that opened it and on top of the panel.
 *
 * So the ground pad does what the globe pad already did, and the special case is
 * gone rather than duplicated. The tools strip is centred at `top: 66px`, the
 * eye is a cell in it, and the pad hangs directly under it in both worlds --
 * which is also the answer to "where did that come from" for a player who has
 * just pressed a button in the middle of the top of the screen.
 *
 * `.cam-globe` survives as a hook, not as geometry: the small-screen block below
 * still needs to tell the two apart, because the globe's band is one row shorter
 * than a locality's and the pad clears whichever it is sitting above. */
/* The eye on the release screen, and the pad above it.
 *
 * That globe is the one in this game with no camera control at all, and it is
 * the one the player is asked to TAP a country on -- so it needed the control
 * most and had it least.
 *
 * Bottom left, at both widths, because it is the only corner every arrangement
 * of this screen leaves free: the method sheet takes the bottom on a phone, the
 * release panel takes the whole right side on a desktop, and the title strip
 * takes the top. It is deliberately not the running game's corner -- there the
 * eye is the last cell of the band's tools row, and this screen has no band.
 *
 * The pad opens directly above it, computed from the button's own height token
 * rather than from a measurement of it. */
.release-eye {
  position: absolute; left: max(18px, var(--safe-l)); bottom: 18px;
  min-height: var(--release-eye-h);
  /* The padding is trimmed so the token DECIDES the height rather than merely
     flooring it -- the same treatment `--control-h`'s five controls have. With
     `.tool-btn`'s own 8px the content came to 45.6 against a 44 token, so the
     pad above it cleared by 8.4 instead of 10: a near-miss of exactly the kind
     that gets reported as "not quite lined up". Four leaves 37.6 of content
     inside 44, so the box is the number. */
  padding-block: 4px;
}
.cam-panel.cam-release {
  left: max(18px, var(--safe-l)); right: auto; top: auto; transform: none;
  bottom: calc(18px + var(--release-eye-h) + var(--band-gap));
}
.cam-head b { flex: 1; font-weight: 700; white-space: nowrap; color: var(--white); }
.cam-head .em { font-size: 19px; line-height: 1; }
/* Three by three: turn on the top corners, zoom on the bottom corners, the move
   cross down the middle with recentre at its heart.
   The globe's pad is the same declaration with three children instead of nine,
   so it comes out as one row of the same squares at the same pitch. No second
   rule: a grid three columns wide fits three buttons on one line by itself, and
   a `.cam-pad-globe` would be a place for the two to drift. */
.cam-pad { display: grid; grid-template-columns: repeat(3, 48px); gap: 5px; }
.cam-btn {
  appearance: none; cursor: var(--cur-pointer, pointer);
  width: 48px; height: 48px; padding: 0;
  display: grid; place-items: center;
  background: var(--ink1-50); border: 2px solid var(--ink3); border-radius: 2px;
  color: var(--text);
}
.cam-btn:hover { background: var(--ink2); }
.cam-btn:active { background: var(--ink3); transform: translateY(2px); }
.cam-btn .em { font-size: 22px; line-height: 1; }

/* The way back to the planet, from a ground view.
 *
 * Deliberately not an entry in the place menu: the four places are one region at
 * four removes and this is not a fifth, it is leaving. So on a phone it is drawn
 * as a peer of the DOCK -- the same 64px square, the opposite bottom corner, on
 * the dock's row rather than the tools' row -- and on a wide screen it sits
 * under the top bar on the left, clear of the meters at the bottom and the pin
 * in the middle.
 *
 * Translucent like the rest of the floating chrome, and it keeps the white arrow
 * while the pin beside it is red: one of them leaves, the other relocates, and
 * they must not be told apart by their labels alone. */
.ground-back {
  position: absolute; left: max(14px, var(--safe-l)); top: 66px; z-index: 2;
  appearance: none; cursor: var(--cur-pointer, pointer);
  display: inline-flex; align-items: center; gap: 8px;
  padding: 8px 14px; min-height: 44px;
  background: var(--ink1-50); border: 2px solid var(--ink3); border-radius: 2px;
  -webkit-backdrop-filter: blur(var(--frost));
  backdrop-filter: blur(var(--frost));
  color: var(--text); font-size: 18.5px; letter-spacing: 0.03em;
}
.ground-back:hover { background: var(--ink2); color: var(--white); }
.ground-back:active { transform: translateY(2px); }
.ground-back .em { font-size: 19px; line-height: 1; }

/* research bubbles */

.bubble {
  position: absolute; pointer-events: auto;
  width: 40px; height: 40px; margin: -20px 0 0 -20px;
  border: 2px solid var(--gold);
  background: var(--ink1);
  cursor: var(--cur-pointer, pointer); display: grid; place-items: center;
  animation: bob 2.4s steps(8) infinite;
}
@keyframes bob { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.12); } }
.bubble:hover { background: var(--gold); }
/* The cure side's swab, on a carrier in the street. Same shape and the same bob
   as the pathogen's bubble -- it is the same gesture, and a player who has
   played one side recognises it on the other -- in the response's own colour
   rather than the strain's gold, because the two are never on screen together
   and the colour is what says which side you are playing. Slightly smaller: it
   hangs over a walking person at a close camera, not over a whole country. */
/* A note on a sheet: not a row, because nothing here can be pressed. It says
   what the block above it is waiting for and what to do about it, so it wears
   the response's own colour on its leading edge rather than the alarm red a
   blocked row would suggest -- the laboratory is not failing, it is waiting. */
.sheet-note {
  margin: 8px 0 2px; padding: 9px 11px;
  background: var(--ink1); border: 2px solid var(--ink3);
  border-left-width: 4px; border-left-color: var(--good);
  border-radius: 2px; line-height: 1.45;
}
.sheet-note .nm { color: var(--text); }
.sheet-note .dim { color: var(--text-faint); font-size: 17px; margin-top: 4px; }

.bubble.swab {
  width: 34px; height: 34px; margin: -17px 0 0 -17px;
  border-color: var(--good);
}
.bubble.swab:hover { background: var(--good); }

/* the dive title */

.dive-title {
  position: fixed; inset: 0; display: grid; place-content: center; text-align: center;
  pointer-events: none; z-index: 40; transition: opacity .5s steps(6);
}
.dive-title.out { opacity: 0; }
.dive-title .dive-name {
  font-size: clamp(46px, 8vw, 96px); font-weight: 700; letter-spacing: 0.07em;
  color: var(--white); text-shadow: 4px 4px 0 var(--ink0);
}
.dive-title .dive-sub {
  margin-top: 8px; font-size: 18px; letter-spacing: 0.35em; text-transform: uppercase;
  color: var(--coral); text-shadow: 2px 2px 0 var(--ink0);
}

/* the end card */

.end-card {
  width: min(660px, 92vw);
  background: var(--ink1); border: 2px solid var(--ink3); border-radius: 2px;
  padding: 28px 30px; text-align: center;
}
.end-head { display: flex; align-items: center; justify-content: center; gap: 12px; margin-bottom: 8px; }
/* Colour only: the size is `.screen-title.hero`. */
.end-card h2 { margin: 0; }
.end-summary { color: var(--text-dim); font-size: 20px; margin: 0 0 20px; line-height: 1.6; }
.end-actions { display: flex; gap: 10px; justify-content: center; }
.end-card.win h2 { color: var(--gold); }
.end-card.lose h2 { color: var(--bad); }
.end-card .why { color: var(--text-dim); margin-bottom: 22px; font-size: 21px; line-height: 1.7; }
.end-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; margin-bottom: 22px; }
.end-stats .s { background: var(--ink2); border-radius: 2px; padding: 11px 8px; }
.end-stats .s .k { font-size: 14.5px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-faint); }
/* Four standalone figures in four tiles, not a column: same test, same answer
   as the top bar above. */
.end-stats .s .v { font-size: 24px; margin-top: 3px; color: var(--gold); }

/* labels floating over the globe */

.map-label {
  /* The pixel face, not the data face, and big enough to read at a glance.
     These float over a moving planet and are the only thing telling you which
     country is under the cursor; at ten pixels of monospace they were a grey
     smudge and you had to stop and look for them. */
  position: absolute; transform: translate(-50%, -100%);
  font-size: 20px; font-family: var(--pixel); color: var(--text);
  letter-spacing: 0.04em; line-height: 1.15;
  background: var(--ink1); border: 2px solid var(--ink3); border-radius: 2px;
  padding: 2px 9px; white-space: nowrap; pointer-events: none;
  /* Belt and braces with the clamp in `updateLabels`: a label is centred on its
     country and a very long name near an edge could still reach past it. What
     used to survive the clip at the left edge was the tail of the chip -- a
     bordered box containing "· ?" -- which is exactly what the interface's own
     question-mark button looks like. */
  max-width: calc(100vw - 16px); overflow: hidden; text-overflow: ellipsis;
}
.map-label.hot { border-color: var(--gold); color: var(--gold); }

/* The chip over an aircraft at the airport.
 *
 * A sibling of `.map-label` and not a variant of it: that one is a country's
 * name on a moving planet and this is three fields over a thing on the ground.
 *
 * All of it in the pixel face, numbers included. The test for `--mono` is
 * whether the figure is one somebody reads DOWN A COLUMN and compares with the
 * one under it -- the region panel's label/value rows, which are the only place
 * left in this file that passes it. Two counts sitting inline on a chip beside
 * a destination are interface, not tabular data, and they were the fourth time
 * the data face had leaked into it. Sized against the chip's own 19px rather
 * than scaled from the 13px they had: that number was picked against JetBrains,
 * and VT323 sits small in its em box, so carrying it across gives a figure half
 * the height of the word next to it. The chip's own 0.03em tracking stands for
 * the same reason -- the override that zeroed it was there for the monospace. */
.air-label,
.place-label {
  position: absolute; transform: translate(-50%, -100%);
  display: flex; align-items: center; gap: 7px;
  font-size: 19px; font-family: var(--pixel); color: var(--text);
  letter-spacing: 0.03em; line-height: 1.1;
  background: var(--ink1); border: 2px solid var(--ink3); border-radius: 2px;
  padding: 2px 8px; white-space: nowrap; pointer-events: none;
  max-width: calc(100vw - 16px); overflow: hidden;
}
.air-label .af-dest { color: var(--white); }
.air-label .af-n, .air-label .af-k { font-size: 18px; }
.air-label .af-n { color: var(--text-dim); }
.air-label .af-k { color: var(--text-faint); }
/* A flight with carriers on it is the thing the player came to look at, so the
   count is boxed and painted in the strain's own colour -- set inline, because
   the colour belongs to the run and not to the stylesheet.
   Two pixels like every other border in this file: at a magnification of four a
   one-pixel rule is a quarter of the smallest thing on screen. */
.air-label .af-k.hot {
  color: var(--gold); border: 2px solid var(--gold); border-radius: 2px; padding: 0 4px;
}
.air-label.shut { border-color: var(--ink3); color: var(--text-faint); }
.air-label .af-shut { font-size: 17px; }

/* The chip over a civic building -- the hospital, the police station, the fire
 * station -- which is the aircraft chip with the aircraft taken out of it.
 *
 * It takes `.air-label`'s whole block above rather than restating it: the
 * position, the centring transform, the pixel face, the plate, the 2px border
 * and the clipping guard are the same object doing the same job over a
 * different thing on the same ground, and two copies of that is two places to
 * fix the next time a chip near the screen edge is clipped to a stub. What it
 * does NOT take is the row vocabulary -- `.af-dest`, `.af-n` and `.af-k` are a
 * destination, a load and a carrier count, which is exactly the hard-wiring
 * this one exists without.
 *
 * A building with nothing to report is the icon alone. That is a legitimate
 * shape here rather than a hole: at the arrival framing the label is not drawn
 * at all -- see the gate in `updatePlaceLabels` -- and a police station with no
 * quarantine in force genuinely has nothing to say. */
/* The rows live in a wrapper so they can be rebuilt without touching the icon
   beside them -- the icon is forty `<rect>` runs and is the expensive half. The
   wrapper must not become a layout box of its own or the chip's `gap` would
   apply once around the group instead of between each row, so it dissolves. */
.place-label .pl-rows { display: contents; }
.place-label .pl-icon { font-size: 19px; line-height: 1; }
.place-label .pl-row { font-size: 18px; color: var(--text-dim); }
/* Past capacity. The same treatment `.af-k.hot` wears, for the same reason: it
   is the number the player came to look at, so it is boxed and painted in the
   strain's own colour -- set inline, because the colour belongs to the run and
   not to this file. The gold here is the fallback for a chip drawn before a
   strain exists, which the test benches can reach. */
.place-label .pl-row.hot {
  color: var(--gold); border: 2px solid var(--gold); border-radius: 2px; padding: 0 4px;
}
/* The building itself is past capacity, not just one figure on it. The border
   takes the strain colour inline; this is what it falls back to. */
.place-label.hot { border-color: var(--gold); }

/* scrollbars, kept in the same language */

.scrollfade::-webkit-scrollbar { width: 10px; }
.scrollfade::-webkit-scrollbar-thumb { background: var(--ink3); }
.scrollfade::-webkit-scrollbar-track { background: var(--ink0); }
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-thumb { background: var(--ink3); }
*::-webkit-scrollbar-track { background: var(--ink0); }

@media (max-width: 980px) {
  #lab { grid-template-columns: 1fr; grid-template-rows: auto auto 1fr auto; }
  .lab-col { border-right: 0; border-bottom: 2px solid var(--ink3); }
  /* One column: the divider between two side-by-side things has nothing left
     to divide, and against the right edge of the screen it is just a stripe. */
  .lab-main { border-right: 0; }
  .news, .meters { width: min(290px, 44vw); }
}

/* --------------------------------------------------------------- the settings
 *
 * Appended as one block on purpose: this file was rewritten to match the
 * reference and is not the place for a second pass of small edits scattered
 * through it.
 *
 * The panel hangs off the gear at the right-hand end of the top bar. It is
 * `position: absolute` against the gear rather than fixed to the corner, so a
 * later setting that makes it taller has nowhere to go but downwards.
 */

.settings { position: relative; display: inline-flex; }
.settings-panel {
  display: none;
  position: absolute; right: 0; top: calc(100% + 7px);
  width: 280px; z-index: 20;
  /* A cap, because this one hangs off a gear and grows downwards with no floor
     under it. On a short window -- a laptop with the browser half height, or a
     phone in landscape above the breakpoint -- the quit button was simply past
     the bottom of the screen with no way to scroll to it. The phone variant
     solved this by becoming a screen with a pinned foot; the popover never got
     anything. `100vh` minus the bar it hangs from and a margin, and it scrolls
     inside itself once it reaches that. */
  max-height: calc(100vh - 88px);
  overflow-y: auto;
  background: var(--ink1); border: 2px solid var(--ink3); border-radius: 2px;
  padding: 12px 13px;
}
/* The class is on the PANEL, not on the wrapper around the gear.
 *
 * Below the breakpoint the panel is a full screen and is mounted in `#modals`
 * rather than in the top bar -- see `settingsControl` in `ui.js` for why -- so
 * `.settings.open .settings-panel` would be a descendant selector reaching
 * across a move that has already happened. One class on the thing itself works
 * from either home. */
.settings-panel.open { display: block; }
/* The desktop settings panel is a POPOVER hanging off a gear, not a screen, and
   the full scale on a 280px drop-down would be the loudest thing in the top bar.
   Below the breakpoint it genuinely is a screen and takes the ladder like every
   other one -- see the `.settings-bar` rule in the small-screen block. */
.settings-title { margin-bottom: 9px; }
.settings-panel .screen-title {
  font-size: 19px; letter-spacing: 0.12em; color: var(--text-faint);
}
.settings-rows { display: flex; flex-direction: column; gap: 12px; }
/* On a wide screen the popover carries the quit button and nothing else: it
   closes on a click anywhere outside it, as it always has, so a CANCEL under a
   dismiss-on-click-away panel would be a second word for the same gesture. The
   cross and the CANCEL are the small-screen pair, where there is no "outside"
   to click. */
.settings-foot { margin-top: 14px; }
.settings-row { display: flex; flex-direction: column; gap: 7px; }
.settings-head b { font-weight: 700; font-size: 19px; letter-spacing: 0.03em; }
.settings-note { font-size: 16px; color: var(--text-faint); line-height: 1.5; margin-top: 3px; }

/* A toggle row, for the preferences that are simply on or off. Nothing uses it
 * yet; it is here so that adding one is an entry in `settings.js` and no CSS. */
.settings-toggle {
  appearance: none; border: 0; background: transparent; color: inherit;
  padding: 0; cursor: var(--cur-pointer, pointer); text-align: left;
  display: flex; align-items: center; gap: 10px; width: 100%;
}
.settings-sw {
  flex: none; width: 34px; height: 20px; margin-left: auto;
  background: var(--ink0); border: 2px solid var(--ink3);
}
.settings-sw::after {
  content: ''; display: block; width: 12px; height: 12px; margin: 2px;
  background: var(--text-faint);
}
.settings-sw.on { border-color: var(--sage); }
.settings-sw.on::after { margin-left: 16px; background: var(--sage); }

/* ------------------------------------------------------------ small screens
 *
 * -- Where the line is and why -----------------------------------------------
 *
 * 900px, and the number was measured off this file rather than off a phone.
 * The laboratory's desktop grid is `292px 1fr 336px`, its middle column carries
 * 36px of padding, and its shop cards declare `minmax(226px, 1fr)`. So the three
 * columns hold each other up down to 292 + 336 + 226 + 36 = 890px and no
 * further: below that the shop is narrower than the card it was drawn for and
 * the two side columns have squeezed it out. 890 to the nearest ten is the line.
 *
 * The same number is written once in `ui.js` as `SMALL_SCREEN`, because the
 * wizard's step logic has to know it too. Change one and change the other.
 *
 * -- What changes ------------------------------------------------------------
 *
 * Above the line: NOTHING. The `@media (max-width: 980px)` block further up is
 * untouched and still owns the 900-980 band, so the screen the desktop has
 * today is the screen it keeps.
 *
 * Below it the laboratory is one decision per screenful with Back and Next, the
 * release is a globe and then a method sheet, and every control that a finger
 * has to hit is grown to about 44px. The wizard is markup the SCRIPT chooses --
 * `#lab.lab-wizard` and the `.wiz-*` classes only exist when `ui.js` agrees
 * this screen is small -- so these rules are inside the query as well, and a
 * disagreement between the two renders nothing rather than a wizard laid out in
 * three columns. */

@media (max-width: 900px) {

  /* --- the title screen and the end card --- */

  /* Block, not centred flex: a card taller than the viewport centred in a flex
     container has its TOP clipped and no scrollbar can reach it. */
  .screen {
    display: block; overflow-y: auto; padding: 16px 12px; padding-left: max(12px, var(--safe-l)); padding-right: max(12px, var(--safe-r));
  }
  /* The laboratory carries `.screen` too, and it is the one screen that is a
     full-bleed grid rather than a card floating on the globe: the padding above
     inset it from all four edges and floated it, and the wizard's footer stopped
     being at the bottom of the phone. */
  #lab { padding: 0; overflow: hidden; }
  .menu-card { width: 100%; }
  .title { font-size: 46px; }
  .subtitle { font-size: 19.5px; margin-bottom: 20px; }
  .side-grid { grid-template-columns: 1fr; }
  .side { padding: 14px 16px; }
  .side .icon { font-size: 28px; margin-bottom: 8px; }
  .side h3 { font-size: 24px; }
  .side p { font-size: 18.5px; }
  /* Out of the corner and into the flow: absolute at the top right, it sat on
     top of the title. */
  .lang-corner { position: static; justify-content: flex-end; margin-bottom: 14px; }
  .end-card { margin: 0 auto; padding: 20px 16px; }
  .end-card h2, .end-card .screen-title.hero { font-size: 36px; }
  .end-stats { grid-template-columns: repeat(2, 1fr); }

  /* --- things a finger has to hit --- */

  .btn { min-height: 44px; padding: 9px 14px; }
  /* 44px is the target for anything a thumb aims at. The `small` variants are
     the header furniture -- language, sound, reset -- which sit at the top of
     the screen away from the thumb and are pressed once a session: at 44px they
     forced the laboratory's header on to four rows and ate a fifth of a phone.
     They no longer carry a height of their own here: `--control-h` is 38, which
     is above the 34 this rule was asking for, and it is the SAME number the
     badge and the language switch beside them take. A second height written
     here is how the group came apart in the first place. */
  /* Horizontal only, for the same reason: the height belongs to `.lang`. */
  .lang-opt { padding: 0 8px; }
  .swatch { width: 38px; height: 38px; }
  .tab { min-height: 44px; padding: 9px 13px; }
  .class-opt { padding: 12px 11px; }
  .card { padding: 12px; }
  .measure-row { padding: 10px 9px; }

  /* --- the laboratory, one step at a time --- */

  /* Six controls and a step name do not fit across 375px in a face this size,
     so both header rows wrap rather than pretending otherwise -- and every
     number below is about how SHORT a wrapped row is, not about avoiding the
     wrap. Two tight rows cost less of the one screenful the step gets than one
     row of clipped labels. */
  /* `minmax(0, 1fr)`, not `1fr`. A grid item's automatic minimum is its
     min-content width, so the footer's two nowrap buttons widened the single
     column to 429px on a 375px phone and every step scrolled sideways. */
  #lab.lab-wizard { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto auto 1fr auto; }
  /* The wizard runs a taller rhythm, declared ONCE for the whole screen. It is
     a phone laboratory: every control in it is a thumb target rather than a
     mouse target, and the badge carries the biggest number on it. Raising the
     token raises the badge, the gear AND the reset arrow on the step bar
     together -- which is the whole point of it being a token, and which the
     header alone did not do: with this on `.lab-head` the two boxes in the
     header agreed at 44 and the reset one row below them stayed at 38, which is
     the same near-miss one level down. Before any of it: badge 45, gear 35.2,
     reset 38, three boxes, three sizes. */
  #lab.lab-wizard { --control-h: 44px; }
  #lab.lab-wizard .lab-head { flex-wrap: wrap; gap: 6px; padding: 6px 12px; }
  /* Bigger here than on a wide screen, which looks wrong written down and is
     right on the phone: this is the only number in the header, the title beside
     it has been given room by the two controls that moved into the gear, and it
     is the budget every step of the wizard spends from. */
  #lab.lab-wizard .budget-item { font-size: 26px; padding: 3px 12px; }

  .wiz-bar {
    grid-column: 1 / -1;
    display: flex; align-items: center; gap: 8px; row-gap: 5px; flex-wrap: wrap;
    padding: 5px 12px; padding-left: max(12px, var(--safe-l)); padding-right: max(12px, var(--safe-r));
    background: var(--ink1);
    border-bottom: 2px solid var(--ink3);
  }
  .wiz-bar .spacer { flex: 1; }
  .wiz-dots { display: flex; gap: 3px; flex: none; }
  .wiz-dot { width: 7px; height: 7px; background: var(--ink3); }
  .wiz-dot.done { background: var(--sage); }
  .wiz-dot.on { background: var(--coral); }
  /* Size, colour and tracking come from `.screen-step`. This rule used to set
     its own 18px and, being later in the file, quietly beat the scale. */
  .wiz-where { min-width: 0; }
  .wiz-count { font-size: 18px; color: var(--text-faint); }

  .wiz-body {
    grid-column: 1 / -1;
    padding: 14px 12px; padding-left: max(12px, var(--safe-l)); padding-right: max(12px, var(--safe-r));
    overflow-y: auto;
    min-height: 0;
    background: var(--ink0);
  }
  .wiz-foot {
    grid-column: 1 / -1;
    display: flex; gap: 8px;
    padding: 10px 12px calc(10px + env(safe-area-inset-bottom, 0px));
    padding-left: max(12px, var(--safe-l)); padding-right: max(12px, var(--safe-r));
    background: var(--ink1);
    border-top: 2px solid var(--ink3);
  }
  /* Next is the wide one. Back is the one you press by accident. Labels wrap
     rather than being held on one line: a two-line button is survivable, a
     button that widens the grid past the screen is not. */
  .wiz-foot .btn { flex: 1; min-width: 0; min-height: 48px; }
  /* Back takes exactly its label and no more, so Next gets the rest and reads
     as the way forward. A percentage here was narrower than "← Précédent". */
  .wiz-foot .btn.ghost { flex: 0 0 auto; }
  .wiz-foot .wiz-count { flex: none; align-self: center; }

  /* Selling, for a screen with no right button and no shift key. It only ever
     appears on a card that is already owned, which is a pale sage row, so it is
     drawn in the dark ink that row uses. */
  .card .refund {
    appearance: none; flex: none;
    width: 40px; height: 40px; margin: -8px -4px -8px 0;
    border: 2px solid var(--text-dark-dim); border-radius: 2px;
    background: var(--sage-dim); color: var(--text-dark);
    font-size: 24px; line-height: 1; cursor: var(--cur-pointer, pointer);
  }
  .card .refund:active { background: var(--sage); }

  /* A 14px-tall slider is a control a finger cannot land on, let alone drag. */
  .dial { margin-bottom: 20px; }
  .dial input[type=range] { height: 44px; }
  .dial input[type=range]::-webkit-slider-runnable-track { height: 10px; }
  .dial input[type=range]::-moz-range-track { height: 10px; }
  .dial input[type=range]::-webkit-slider-thumb { width: 26px; height: 26px; margin-top: -10px; }
  .dial input[type=range]::-moz-range-thumb { width: 26px; height: 26px; }

  /* --- the release, as two screens --- */

  /* Two lines at the top and the rest of the screen is globe. */
  .release-top {
    position: absolute; left: 0; right: 0; top: 0;
    pointer-events: auto;
    display: flex; flex-direction: column; gap: 4px;
    padding: calc(8px + env(safe-area-inset-top, 0px)) 12px 8px;
    padding-left: max(12px, var(--safe-l)); padding-right: max(12px, var(--safe-r));
    background: var(--ink1);
    border-bottom: 2px solid var(--ink3);
  }
  .release-top-row { display: flex; align-items: center; gap: 10px; }
  .release-top-row .spacer { flex: 1; }
  .release-top h3 { margin: 0; }
  .release-top .sub { font-size: 18px; color: var(--text-faint); line-height: 1.4; }
  .release-top .pick-label { font-size: 12px; text-align: right; }

  /* The method, on its own. It stops short of the top so the country the camera
     has just flown to stays visible: that strip of globe is the only
     confirmation the player gets before the button that cannot be undone. */
  .release-sheet {
    position: absolute; left: 0; right: 0; bottom: 0;
    pointer-events: auto;
    max-height: 82vh; overflow-y: auto;
    padding: 14px 12px calc(14px + env(safe-area-inset-bottom, 0px));
    padding-left: max(12px, var(--safe-l)); padding-right: max(12px, var(--safe-r));
    background: var(--ink1);
    border-top: 2px solid var(--ink3);
  }
  .release-sheet h3 { margin: 0 0 3px; }
  .release-sheet .sub { font-size: 18.5px; color: var(--text-faint); margin-bottom: 12px; line-height: 1.5; }

  /* --- the running game --- */

  /* `max-width` rather than `width`, because the help card sets its width
     inline and an inline value cannot be beaten from here without `!important`.
     A max-width has no inline rival, so it reaches every panel of this class. */
  /* The wide layout can hang the cure side's card under the eye. Here the card
     is a whole screen, so the three things that hanging sets are put back, by
     name, in case a window is resized across the breakpoint with a card open. */
  .region-panel.under-eye { left: auto; transform: none; }
  .region-panel { left: 8px; right: 8px; width: auto; max-width: calc(100vw - 16px); max-height: 60vh; }
  /* The top bar holds a brand, six counters, four speed buttons and the
     settings, and it wraps rather than pushing half of them off the end. The
     tightened padding is what keeps the wrap to two rows instead of four. */
  .topbar { flex-wrap: wrap; row-gap: 4px; gap: 8px; padding: 6px 10px; }
  /* Horizontal only. The settings panel lives inside the top bar, so this rule
     reaches the language switch in it, and a vertical padding here would put
     that switch back out of step with the sound button on its row. */
  .topbar .lang-opt { padding: 0 7px; }
  .stat { min-width: 0; }
}

/* Drawn icons, on whole pixels.
 *
 * `crispEdges` is what stops a sixteen-cell grid being anti-aliased into mud at
 * twenty-five pixels; `pixelated` covers the same markup when a browser decides
 * to raster the SVG and magnify it rather than redraw it, which is what happens
 * to anything inside a scaled layer. The sizes themselves are rounded to a whole
 * number of device pixels per cell in `icons.js` -- CSS cannot do that, because
 * it does not know what an em is worth until after layout. */
.vx-wrap svg.vx { shape-rendering: crispEdges; image-rendering: pixelated; }

/* --------------------------------------------------------------- dialogs */

/* A card on a phone is an icon and a name. Everything it used to say lives in
   here, behind the question mark, because six cards each carrying three lines
   is four screens of scrolling and choosing between things you cannot see side
   by side is not choosing. */

.dialog-wrap {
  position: fixed; inset: 0; z-index: 40;
  display: flex; align-items: center; justify-content: center;
  padding: 18px;
  background: rgba(28, 42, 47, 0.86);
}
.dialog {
  width: min(460px, 100%); max-height: min(80vh, 640px);
  display: flex; flex-direction: column;
  background: var(--ink1); border: 2px solid var(--ink3); border-radius: 2px;
}
.dialog.wide { width: min(680px, 100%); }
.dialog-head {
  display: flex; align-items: center; gap: 10px;
  padding: 12px 14px; border-bottom: 2px solid var(--ink3);
}
.dialog-head h3 { margin: 0; flex: 1; min-width: 0; }
/* The balance gets its own line under the title rather than a slot beside it:
   the response sheet carries three figures, and three badges, a title and a
   44px cross do not share 375 pixels. `order` keeps the cross on the top row. */
.dialog-head { flex-wrap: wrap; }
.dialog-head .dialog-x { order: 2; }
.dialog-head .budget { order: 3; flex: 1 0 100%; margin-top: 2px; }
.dialog-head .budget-item { font-size: 21px; padding: 3px 9px; }
.dialog-head .em { font-size: 22px; line-height: 1; }
.dialog-x {
  appearance: none; border: 2px solid var(--ink3); background: var(--ink2);
  color: var(--text); border-radius: 2px; cursor: var(--cur-pointer, pointer);
  width: 40px; height: 40px; font-size: 20px; flex: none;
}
.dialog-x:hover { background: var(--ink3); color: var(--white); }
.dialog-body { padding: 14px; overflow-y: auto; font-size: 20px; line-height: 1.55; }
.dialog-body p { margin: 0 0 10px; }
.dialog-body p:last-child { margin-bottom: 0; }
.dialog-foot { padding: 12px 14px; border-top: 2px solid var(--ink3); }
.dialog-foot .btn { width: 100%; }
/* A confirm and an abandon: half the width each, side by side, with the thing
   that happens on the right. A stacked pair puts the irreversible one under the
   thumb that was reaching for the other. */
.dialog-foot.two { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.shop-price { color: var(--gold); font-weight: 700; }

/* A named block inside a sheet.
 *
 * The response sheet is two lists that look alike and are not alike: the
 * programmes above are bought once for the WORLD out of the science budget, the
 * measures below are bought for ONE COUNTRY out of the operations budget. The
 * only thing saying so was a small grey line reading "MESURES — Thaïlande", set
 * at the weight of a label, and a player scrolling past it bought a national
 * measure believing it was global.
 *
 * Three things carry it now instead of one, because one was not enough:
 *   THE RULE     a 2px line, the same border every panel in this kit wears, so
 *                the two blocks are visibly two blocks before a word is read.
 *   THE NAME     the country, on `.screen-step` -- 24px and white, the ladder's
 *                second rung, the one that answers "what am I looking at". It is
 *                the important word and it now leads; "measures" was the loud
 *                half and is the generic one.
 *   THE KIND     underneath, small caps and faint, saying what sort of thing the
 *                list is. Both blocks have one, so the loud header has something
 *                to be louder than.
 * What was considered and left out: a different ground behind the measures.
 * The rows in that list go pale sage when they are ordered, and a tinted block
 * behind them would be a third surface arguing with the two the rows already
 * use. The rule does the same job and adds no colour. */
.sheet-section {
  margin: 16px 0 6px;
  padding-top: 12px;
  border-top: 2px solid var(--ink3);
}
/* The first block starts at the top of the sheet, where a rule would be a line
   drawn under the filter for no reason. */
.sheet-section.first { margin-top: 4px; padding-top: 0; border-top: 0; }
.sheet-where {
  text-transform: uppercase;
  /* "République démocratique du Congo" is in the region table and this is a
     322px panel. It wraps rather than widening the sheet or being cut. */
  min-width: 0; overflow-wrap: anywhere;
}
.sheet-kind {
  margin-top: 2px;
  font-size: 17px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-faint);
}

/* The response sheet wears the same filter, at every width rather than only
   below the breakpoint: its panel is 336px on a wide screen too, and six
   categories as a tab row is three rows of tabs above the list they filter. */
.cure-panel .tabs { margin-bottom: 10px; }
.cure-panel .filter-open { width: 100%; text-align: left; }

/* The filter, which replaces a row of tabs that wrapped to seven rows. */
.filter-list { display: flex; flex-direction: column; gap: 6px; }
.filter-list .btn { width: 100%; text-align: left; padding: 11px 14px; }

/* A section heading that IS its own question mark.
 *
 * As two things -- a small-caps label, and a button on the end of the row -- a
 * heading whose entire body was a question mark read as a section that had
 * failed to load. One control: the words and the mark are the same target. */
.info-row {
  appearance: none; width: 100%; padding: 0; cursor: var(--cur-pointer, pointer);
  border: 0; background: transparent; color: var(--text-faint);
  font-family: inherit; letter-spacing: .08em; text-align: left;
}
.info-row:hover { color: var(--text); }
.info-row:hover .card-info { background: var(--ink3); color: var(--white); }
/* Not a button: the row is. It only has to LOOK like the mark everywhere else,
   and it must not be a click target of its own inside one. */
.info-row .card-info { display: grid; place-items: center; pointer-events: none; }

/* The question mark itself. Square, quiet, and out of the way of the card's
   own job, which is to be pressed. */
.card-info {
  appearance: none; flex: none; cursor: var(--cur-pointer, pointer);
  width: 30px; height: 30px; padding: 0;
  border: 2px solid var(--ink3); background: var(--ink2);
  color: var(--text-dim); border-radius: 2px;
  font-family: var(--pixel); font-size: 20px; line-height: 1;
}
.card-info:hover { background: var(--ink3); color: var(--white); }
.class-opt .card-info { align-self: flex-start; }
.class-opt .card-text { flex: 1; min-width: 0; }
.dial .head .card-info { width: 26px; height: 26px; font-size: 18px; }

/* --------------------------------------------------- pixel range sliders */

/* Square, hard-edged, and the same two-pixel border everything else in the kit
   has. A rounded native thumb on a pixel-art panel is the one control that
   still looks like a web page. */

input[type=range] {
  appearance: none; -webkit-appearance: none;
  width: 100%; background: transparent; cursor: var(--cur-pointer, pointer);
}
input[type=range]::-webkit-slider-runnable-track {
  height: 10px; background: var(--ink0);
  border: 2px solid var(--ink3); border-radius: 0;
}
input[type=range]::-moz-range-track {
  height: 10px; background: var(--ink0);
  border: 2px solid var(--ink3); border-radius: 0;
}
input[type=range]::-webkit-slider-thumb {
  appearance: none; -webkit-appearance: none;
  width: 16px; height: 22px; margin-top: -8px;
  background: var(--coral); border: 2px solid var(--coral-dark); border-radius: 0;
}
input[type=range]::-moz-range-thumb {
  width: 16px; height: 22px;
  background: var(--coral); border: 2px solid var(--coral-dark); border-radius: 0;
}
input[type=range]:active::-webkit-slider-thumb { background: #ffb193; }
input[type=range]:active::-moz-range-thumb { background: #ffb193; }

@media (max-width: 900px) {
  /* A card keeps its icon and its name. The rest is behind the question mark.
     One rule, not the three scoped ones this replaces: those covered the
     laboratory and the title screen and missed the release sheet, the meters,
     the region panel and the end card, every one of which had grown a question
     mark whose text was still printed underneath it. */
  .card-more { display: none; }
  .lab-wizard .class-opt { align-items: center; }
  .lab-wizard .card .row { align-items: center; }
  .filter-open { width: 100%; text-align: left; }

  /* Bigger targets, and the thumb has to clear the track it sits in. */
  input[type=range] { height: 44px; }
  input[type=range]::-webkit-slider-runnable-track { height: 14px; }
  input[type=range]::-moz-range-track { height: 14px; }
  input[type=range]::-webkit-slider-thumb { width: 22px; height: 30px; margin-top: -10px; }
  input[type=range]::-moz-range-thumb { width: 22px; height: 30px; }
  .card-info { width: 36px; height: 36px; font-size: 22px; }
}

/* ------------------------------------------------- the game HUD on a phone */

/* Two buttons with a count on them, bottom right, each opening what it stands
   for. The three meters, the mutation shop and the news feed together covered
   more than half the globe, and the globe is the thing being played. */

/* On a wide screen the dock is ONE button: the feed's. The other two open
   sheets that are already on screen there -- the meters on the left, the shop
   or the response on the right -- so they stay hidden. The third is the phone's
   own news button, node for node: same badge written by the same frame loop,
   same handler, same centred dialog, same mark-as-read. It is `display: contents`
   and not `flex` for the reason given under `.hud-band`: the button positions
   against `#hud`, and a box in between would catch it.
   It sits at the bottom, to the left of the side sheet, clearing the sheet's
   width through the token and not through a copy of it. */
.hud-dock { display: contents; }
.hud-dock .dock-btn:nth-child(1), .hud-dock .dock-btn:nth-child(2) { display: none; }
.hud-dock .dock-btn:nth-child(3) {
  position: absolute; bottom: 14px;
  right: calc(max(14px, var(--safe-r)) + var(--side-panel-w) + 10px);
}
/* Inside the dialog the feed is a column again, at every width. The small-screen
   block below says the same for its own three sheets; this one is not inside
   that block because the feed is now dialog-only on a wide screen too. */
.dock-body .news { display: flex; }
/* On a wide screen the band dissolves and its three children go back to their
   own corners: the tools under the top bar in the middle, the way out on the
   left, the dock not drawn at all. `display: contents` and not `position:
   static` for a reason -- the children are absolutely positioned and must keep
   resolving against `#hud`, which they do only if nothing in between generates
   a box. `#hud > *` still matches the band, and `pointer-events` inherits from
   it, so the clicks its children need survive the dissolve. */
.hud-band { display: contents; }

/* Sixty-four, not fifty-six. These are the three buttons the whole running game
   is played through on a phone and they sit at the very bottom corner, which is
   where a thumb is least accurate.

   Half-transparent, like the back button and the ground tools they share the
   bottom band with: three opaque squares in the corner of a phone are three
   squares of the game you cannot see. Flat 50%, not a ramp -- a button is one
   surface and a gradient across a 64px square would read as a lighting effect.
   The drawn icon on top and the badge beside it are both fully opaque, which is
   what keeps the pair legible over anything: only the plate is thinned. */
.dock-btn {
  position: relative; appearance: none; cursor: var(--cur-pointer, pointer);
  width: 64px; height: 64px; padding: 0;
  background: var(--ink1-50); border: 2px solid var(--ink3); border-radius: 2px;
  -webkit-backdrop-filter: blur(var(--frost));
  backdrop-filter: blur(var(--frost));
  color: var(--text);
}
.dock-btn:active { background: var(--ink2); transform: translateY(2px); }
.dock-btn .em { font-size: 32px; line-height: 1; }
/* The badge, and the two things measurement changed here.
 *
 * IT NEVER CLIPPED, IT CROWDED. `min-width` lets the box grow, so nothing was
 * ever cut off -- but at four pixels of side padding a three-figure count filled
 * its content box exactly, glyphs touching the border on both sides, which is
 * what reads as overflowing. Six gives every realistic value air: the widest
 * these three ever hold is three characters -- a run that never spends peaks at
 * 181 DNA, the response side at 24 ops, the affordable-mutation count at the
 * size of the shop, and the feed is clamped to "99+" in `hud.js` -- and at six
 * the box holds four figures with room, which is more than the game can reach.
 *
 * Z-INDEX because the badge now MOVES. It already sat eight pixels outside its
 * button, into a ten-pixel gap; at the top of its bounce it reaches into the
 * next button along, and positioned siblings paint in document order, so the
 * middle badge would have gone under the button to its right for the length of
 * the animation. A badge is never the thing that should be hidden. */
.dock-btn .pip {
  position: absolute; top: -8px; right: -8px; min-width: 26px; height: 26px;
  padding: 0 6px; box-sizing: border-box; z-index: 1;
  display: flex; align-items: center; justify-content: center;
  background: var(--coral); color: #2b1a12; border: 2px solid var(--coral-dark);
  font-size: 18px; font-weight: 700;
}
.dock-btn .pip.zero { background: var(--ink2); color: var(--text-faint); border-color: var(--ink3); }
.dock-body { display: flex; flex-direction: column; gap: 10px; }
.dock-body .meters, .dock-body .news, .dock-body .shop, .dock-body .cure-panel {
  position: static; width: auto; max-width: none; max-height: none; inset: auto;
  transform: none; overflow: visible;
}
/* The cure side's response panel is a `region-panel` by class, so inside the
   dialog it has to give back the chrome that class carries -- otherwise it is a
   bordered box with its own padding inside a bordered box with its own padding. */
.dock-body .cure-panel {
  display: block; padding: 0; border-width: 0; background: transparent;
}
/* Its own heading, one line under the dialog's heading, saying the same word
   with the same icon. The dialog names what it is now. */
.dock-body .cure-panel > h3 { display: none; }

@media (max-width: 900px) {
  /* --- the bottom band: ONE grid, two rows, five tracks -------------------
   *
   * Everything a thumb reaches for stands in the same columns, because both
   * rows are cells of the same grid rather than two boxes anchored to opposite
   * corners and tuned until they looked right at 375px. They did not look right
   * anywhere else: the pin and the eye fell in the GAPS between the dock's
   * buttons, and the animal count's right edge landed in the middle of one.
   *
   * Five tracks for four buttons, and the second one is the point. It is the
   * BREAK -- the separation between leaving the ground and opening a panel,
   * which is a real distinction and was worth the seventy-one pixels it used to
   * get by accident. Declaring it as a track keeps it and aligns the rest: at
   * 375px the `1fr` resolves to exactly the 51px it always was, so the dock's
   * three buttons do not move at all, and on a wider phone the break GROWS
   * while every button stays 64px, which is the opposite of what two
   * independently anchored rows did.
   *
   * Add a fifth dock button and you add a track here, once, and both rows
   * follow. That is the whole reason this is one definition. */
  /* The band's four measurements, at the root rather than on the band itself.
   *
   * They were on `.hud-band` and inherited down to its cells, which was enough
   * until something OUTSIDE the band had to clear it: the camera pad is mounted
   * in `#modals`, a sibling of the whole interface, so it inherits nothing from
   * here and used to carry the sum as a typed 162. That number is now wrong in a
   * ground view -- the band grew the "where am I" row -- and a second copy of an
   * arithmetic is exactly what this file keeps refusing to have. See the two
   * `.cam-panel` rules further down, which add these up instead. */
  :root {
    --band-btn: 64px;
    --band-edge: 14px;
  }
  .hud-band {
    position: absolute;
    left: max(var(--band-edge), var(--safe-l));
    right: max(var(--band-edge), var(--safe-r));
    bottom: var(--band-edge);
    z-index: 3;
    display: grid;
    grid-template-columns:
      var(--band-btn) 1fr var(--band-btn) var(--band-btn) var(--band-btn);
    gap: var(--band-gap);
    align-items: center;
  }
  /* The two wrappers dissolve so their contents become cells of the band's
     grid. They still exist as nodes -- `hidePlaces` hides the tools by class,
     and the wide layout needs them back. */
  .ground-tools, .hud-dock { display: contents; }
  /* Row one: where you are, the whole width of the band. Row two: the count
     over the way out and the meters, the pin over the strain, the eye over the
     news. Row three: the way out in the first column, the dock from the third.
     The two dock buttons after the first flow into the tracks beside it, which
     is what an explicit start plus `grid-row` gives.
     The band is anchored by its BOTTOM, so a row added on top costs the rows
     under it nothing: the dock still ends at 14 and the tools row still ends at
     88, measured, at 375 and 390 and 412 alike. */
  .where-line { grid-row: 1; grid-column: 1 / -1; }
  .place-note { grid-row: 2; grid-column: 1 / 4; }
  .place-btn { grid-row: 2; grid-column: 4; }
  .cam-open { grid-row: 2; grid-column: 5; }
  /* `width: auto`, like the tool buttons: the track is the 64px, and a width
     repeated on the item is a second copy of that number free to drift. */
  .dock-btn { grid-row: 3; width: auto; height: var(--band-btn); }
  /* The wide layout shows only the dock's third button and pins it to a corner
     -- see `.hud-dock` above the breakpoint. Here all three are cells of the
     band, so both of those are undone by name: the two hidden ones come back,
     and the pinned one goes back into the grid. */
  .hud-dock .dock-btn:nth-child(1), .hud-dock .dock-btn:nth-child(2) { display: block; }
  .hud-dock .dock-btn:nth-child(3) { position: relative; right: auto; bottom: auto; }
  /* Every one placed by hand, and this is not verbosity. Auto-placement fills
     HOLES: it puts an item in the earliest free cell, so the second dock button
     dropped straight into the break track, and with the way out hidden -- which
     is every moment the player is on the globe -- the third would have gone to
     the first column and the dock would have scattered across the screen. Three
     lines that are true in every combination beat one that is true while
     nothing is hidden. A fifth button is a fourth line here and a sixth track
     above; they are eight lines apart on purpose. */
  .dock-btn:nth-child(1) { grid-column: 3; }
  .dock-btn:nth-child(2) { grid-column: 4; }
  .dock-btn:nth-child(3) { grid-column: 5; }
  /* Out of the way; they live in the dialogs the dock opens. */
  .meters, .news, .shop { display: none; }
  .dock-body .meters, .dock-body .news, .dock-body .shop { display: flex; }
  /* A tutorial line the player has read, parked over the map for the rest of
     the game. It says the same thing every meter's own label says. */
  .hint { display: none; }

  /* All the counters on ONE line, values only.
     The labels are what broke it: five two-word captions stacked over five
     numbers cannot be a single row at 375px, and letting the columns shrink to
     fit made them overlap into a jumble. The numbers keep their colours -- gold
     for the strain, red for the dead -- and the caption is in the tooltip. */
  /* Two rows, and which things share a row is the point.
     The five counters go on one line together and nothing is allowed to break
     them up; the controls take the row underneath. Everything on a single row
     needs about four hundred and fifty pixels and a phone has three hundred and
     seventy-five, so the choice was two rows or a bar that scrolls sideways,
     and a counter you have to drag into view is not a counter. */
  /* The five counters have to hold ONE line, and at day one they did: the row
     was measured with "1 Jan 2031 · 8.00 bn · 6 · 0 · 1/173" and fitted. Late in
     a run every one of them is at its full width -- "31 déc. 2032 · 7.94 Md ·
     1.23 Md · 410.0 M · 161/173" -- and that row wants 415px on a 375px phone,
     so the last counter dropped to a second line and the speed buttons with it.
     What pays for the difference is the separators, not the numbers: each dot
     was costing 21px of gap, glyph and margin, which is more than a third of the
     figure it separates. */
  /* Three rows, chosen rather than wrapped into.
     The date on its own, the four counters on their own, the controls under
     them -- and the owner would rather spend the pixels than cram, because these
     numbers are the game. Letting the bar wrap on its own put the break wherever
     the current figures happened to land: `· 173/173` orphaned on a second line
     next to the pause button, moving as the run went on. */
  .topbar {
    padding: 5px 8px calc(5px + env(safe-area-inset-top, 0px));
    padding-left: max(8px, var(--safe-l));
    padding-right: max(8px, var(--safe-r));
    gap: 3px 4px; flex-wrap: wrap;
  }
  /* The strain's name is on the title of both dock dialogs and in the settings;
     here it was eating the row the numbers need. */
  .topbar .brand { display: none; }
  .topbar .spacer { display: none; }
  .topbar .bar-date { flex: 1 0 100%; }
  /* Two by two, with the captions back.
     The captions were hidden because five stacked pairs could not be one line
     at 375px -- but the answer to that was never to strip the labels off the
     numbers the whole screen is about and separate them with middots. The row
     budget was the constraint, and the owner lifted it: spend a row, name every
     figure. A grid rather than a wrapping flex row so the split is 2/2 whatever
     the numbers are doing, instead of moving as they grow. */
  .topbar .bar-stats {
    display: grid; grid-template-columns: 1fr 1fr;
    flex: 1 0 100%; gap: 2px 12px; min-width: 0;
  }
  .topbar .bar-controls {
    display: flex; flex: 1 0 100%; align-items: center; gap: 6px; min-width: 0;
  }
  /* The gear and the help mark, hard against the far edge and a clear gap from
     the speed strip. Flush against it they read as a fifth and sixth speed. */
  .topbar .bar-tools {
    display: flex; align-items: center; gap: 6px; margin-left: auto;
  }
  .topbar .stat {
    flex: none; min-width: 0; flex-direction: column; align-items: flex-start;
  }
  .topbar .stat .k {
    display: block; font-size: 15px; letter-spacing: 0.10em;
    overflow: hidden; text-overflow: ellipsis; max-width: 100%;
  }
  /* The date is the exception: a date does not need to be told it is a date, and
     the caption would cost a whole line to say so. Caption and value share a
     baseline, and the value is the biggest single figure on the bar. */
  .topbar .bar-date { flex-direction: row; align-items: baseline; gap: 8px; }
  .topbar .bar-date .k { display: none; }
  /* The date row is two cells on the SAME grid the counters take: `1fr 1fr`
     with a 12px gutter, in a box that is `flex: 1 0 100%` exactly as
     `.bar-stats` is. So the clock's left edge is the left edge of INFECTÉS and
     PAYS underneath it, rather than merely somewhere right of the date. Copy
     the two numbers together if either ever changes. */
  /* `.stat.bar-date`, not `.bar-date`: the `.topbar .stat` rule below sets
     `flex: none` at the same specificity and later in the file, which quietly
     took the full-width basis away again -- the date row then shrank to fit its
     own text and the second column landed wherever that ended. */
  .topbar .stat.bar-date { flex: 1 0 100%; }
  .topbar .bar-date .bar-when {
    flex: 1; display: grid; grid-template-columns: 1fr 1fr; gap: 2px 12px;
  }
  /* Twelve pixels, for the four figures the whole screen is about. These are the
     sizes they should always have had; the row was only that small because all
     six controls were fighting over one line.
     The middot separators that used to sit between them are gone with the same
     change: they existed only because the captions did not, and a named figure
     needs no punctuation to say where the last one ended. */
  .topbar .stat .v { font-size: 24px; white-space: nowrap; }
  .topbar .bar-date .v { font-size: 24px; }
  /* `.speed` is already `flex: none` in the shared block, for a reason that
     applies at every width -- see the note there. It used to be re-stated here. */
  /* Fifteen pixels of VT323 is small enough to be mistaken for a different face
     -- it was reported as JetBrains Mono, which it never was. The face was right
     and the size was the bug.
     The height is the shared token, not a floor of its own: the 34 written here
     was never binding -- the content came to 36.95 -- so the speed strip sat a
     pixel under the gear beside it on the same row. A near-miss is worse than a
     difference, and the horizontal padding is the only thing this row actually
     needs to keep for itself. */
  /* The row went the other way this time: seven buttons back down to four --
     pause, 1:1, 60:1 and the held one -- because every speed above 1:1 strobed
     the day and night the sun now follows. There is room again, and most of it
     has gone to the big button; five pixels here rather than the four seven
     buttons had squeezed this to leaves the controls row at 341.4 against 359.2
     of usable bar at 375px. Every figure in that sum is fixed -- two glyphs, a
     four-character label and two icons, none of which change with the language
     or with the numbers -- so the eighteen pixels of slack stay slack. */
  .topbar .speed .btn { padding: 5px 5px; font-size: 19px; min-height: var(--control-h); }
  /* Fast forward keeps what the shared rule gives it: three control-heights
     wide and no padding at all. Without this line the rule above puts five
     pixels back on a button whose height is already full of icon, and the strip
     ends up half a step taller than the gear on the same row. */
  .topbar .speed .btn.ff { padding: 0; }
}

/* The region panel's own controls. Two halves and a way out under them. */
.panel-actions {
  display: grid; grid-template-columns: 1fr 1fr; gap: 7px; margin-top: 14px;
}
.panel-actions .btn { width: 100%; }
/* A third action -- a new outbreak -- takes the whole row under the two. */
.panel-actions .btn.primary { grid-column: 1 / -1; }
.panel-cancel { width: 100%; margin-top: 7px; }

@media (max-width: 900px) {
  /* The cross grows into something a thumb can hit, and the Cancel underneath
     is the one that is actually meant to be used. */

  .panel-actions .btn, .panel-cancel { padding: 12px 10px; }

  /* Every mutation on screen at once, as tiles, rather than a strip that
     scrolls sideways: a row you have to drag to see the end of is a row whose
     end nobody knows is there.
     Three across, not four. At four the column is 86px and the longest names --
     "Transmission by touch" -- run to three lines, so one tile in the row is
     half again as tall as its neighbours and the grid stops reading as a grid.
     The strip's own border and padding go: inside a dialog that already has a
     frame, they are a box drawn around the inside of a box. */
  /* The GRID is the grid, not the whole sheet: the sheet is that plus the list
     of what has been marked under it, and before this rule moved down one level
     the two of them were being laid out as two cells of the tile grid -- the
     picks list ninety-six pixels wide in the corner next to a mutation. */
  .dock-body .shop {
    display: flex; flex-direction: column; gap: 10px; overflow: visible;
    max-width: none; padding: 0; border-width: 0; background: transparent;
  }
  .dock-body .shop-grid {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
    gap: 6px; overflow: visible;
  }
  .dock-body .shop-item {
    padding: 10px 6px; text-align: center;
    font-size: 16px; line-height: 1.25;
  }
  .dock-body .shop-item .em { font-size: 24px; }
  .dock-body .shop-item .nm { margin: 5px 0 3px; }
  /* The room the sheet has and the strip has not: every marked mutation gets
     its sentence here. This is where the per-tap confirmation dialog's words
     went when a tap stopped spending -- the same text, at the point where the
     decision is actually made rather than one tile at a time. */
  /* The dialog has the whole screen; nothing in it needs its own scrollbar. */
  .dock-body .shop-pick-list { max-height: none; overflow: visible; }
  .dock-body .shop-pick { align-items: flex-start; font-size: 18px; }
  .dock-body .shop-pick .why { display: block; font-size: 16px; }
  .dock-body .shop-picks-head { font-size: 18px; }
  .dock-body .shop-hint { display: block; font-size: 18px; }
  /* One commit control, never two saying the same thing. Inside the dialog the
     foot's own Done buys the list; the sheet's button is for the desktop strip,
     which has no dialog around it. */
  .dock-body .shop-commit { display: none; }
}

@media (max-width: 900px) {
  /* Settings takes the screen and gives back a Back button. A drop-down hung
     off a gear in a forty-pixel bar puts half its rows past the edge. */
  /* A screen with a pinned foot, not a scrolling box.
     It used to be one `overflow-y: auto` column with the panel's own padding,
     and the bar bled out of it with a negative margin that had to match that
     padding exactly -- which it did not, so the settings screen had a horizontal
     scrollbar for one pixel. Three parts instead, each owning its own inset:
     a head that stays, a middle that scrolls, and a foot that cannot be scrolled
     away from. The settings are short and a phone is tall, so a foot that simply
     follows the last row would sit stranded halfway up the screen. */
  /* `inset: 0` means the VIEWPORT here, and it only does so because the panel
     has been moved out of `.topbar` into `#modals`. A `backdrop-filter` makes
     its element a containing block for fixed descendants, so while this lived
     inside the blurred bar it was drawn exactly the height of that bar. See
     `settingsControl` in `ui.js`. */
  .settings-panel.open {
    /* The screen carries its own control height, and it has to now that it is
       mounted in `#modals` rather than inside whatever opened it. It used to
       inherit: from the running game's top bar it got the root's 38, and from
       the laboratory's header it got the wizard's 44 -- the same screen, two
       sizes, depending on which button you pressed to reach it. Declared here,
       it is 44 in both, which is the number the wizard picked for the right
       reason: below the breakpoint every row on this screen is a thumb target. */
    --control-h: 44px;
    position: fixed; inset: 0; width: auto; max-height: none;
    /* 35, and it must stay BELOW `.dialog-wrap`'s 40.
       This screen is mounted in `#modals` so that its `inset: 0` resolves
       against the viewport rather than against the blurred top bar it used to
       live inside. That fixed one bug and created another: the Quit
       confirmation is a `.dialog-wrap`, also in `#modals`, and at 45 this panel
       painted on top of it -- the dialog opened underneath and the player was
       asked a question they could not see. A full-screen panel that outranks
       the dialogs it opens is a panel nothing can ever ask anything about. */
    border-width: 0; border-radius: 0; z-index: 35;
    background: var(--ink1);
    padding: 0;
    display: flex; flex-direction: column; overflow: hidden;
    /* Nothing floats here any more, so the four white marks that say it does
       would be four dots in the corners of the phone. */
    background-image: none;
  }
  .settings-bar {
    flex: none;
    display: flex; align-items: center; gap: 10px;
    padding: calc(10px + env(safe-area-inset-top, 0px)) 12px 10px;
    padding-left: max(12px, var(--safe-l)); padding-right: max(12px, var(--safe-r));
    border-bottom: 2px solid var(--ink3);
    background: var(--ink1);
  }
  /* The title leads the row; the cross is the thing pushed to the far end. */
  .settings-bar .settings-title { margin: 0; flex: 1; min-width: 0; }
  .settings-panel.open .settings-rows {
    flex: 1; min-height: 0; overflow-y: auto; padding: 12px; padding-left: max(12px, var(--safe-l)); padding-right: max(12px, var(--safe-r));
  }
  .settings-panel.open .settings-foot {
    flex: none; margin-top: 0;
    padding: 10px 12px calc(10px + env(safe-area-inset-bottom, 0px));
    padding-left: max(12px, var(--safe-l)); padding-right: max(12px, var(--safe-r));
    border-top: 2px solid var(--ink3);
  }
  .settings-panel .settings-bar .screen-title {
    font-size: 32px; letter-spacing: 0.07em; color: var(--sage);
  }
  .settings-bar .btn { padding: 10px 14px; }
}

@media (max-width: 900px) {
  /* Eight colours as a grid that fills the width, not eight small squares in a
     row that wraps one onto a second line. Four across, so the eight land as a
     tidy two rows and every one of them is a comfortable target. */
  .swatches {
    display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px;
  }
  .swatch { width: auto; height: 64px; border-width: 3px; }
}

/* The two menu cards: icon, name, and the question mark on the same line. */
.side-head { display: flex; align-items: center; gap: 10px; }
.side-head h3 { flex: 1; margin: 0; }
.side-head .icon { font-size: 1.6em; line-height: 1; }

@media (max-width: 900px) {
  /* The card-more rule that used to live here is the one that is now global. */
  .side { padding: 16px 14px; }
}

/* Buttons centre what is in them.
   A drawn icon followed by a label is two inline boxes sitting on a text
   baseline, so the icon rode high and a label that wrapped to two lines pushed
   itself off centre. Flex puts both in the middle on both axes, and the gap
   replaces the space character the markup no longer needs. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 0.45em;
  text-align: center; line-height: 1.25;
}
.btn .vx-wrap { flex: none; }

/* ------------------------------------------------ a phone: screens, not boxes
 *
 * The rule this whole section is built on, in the player's words: anything that
 * needs a scrollbar to be used becomes a SCREEN with a way out, rather than a
 * box inside a box. Every failure it fixes was the same shape. A panel pinned
 * under a top bar whose height changes with the language has its heading behind
 * the bar; a panel with `max-height` and its actions at the end of its own
 * scroll has its last button sliced in half by its own bottom edge; and a
 * switcher centred with a transform on a screen narrower than it is overflows
 * BOTH ways at once, so neither end can be reached.
 *
 * Above the breakpoint none of this applies and the wide layout is untouched.
 */

@media (max-width: 900px) {

  /* --- every dialog is the whole screen --- */

  .dialog-wrap { padding: 0; }
  .dialog {
    width: 100%; max-width: none;
    height: 100%; max-height: none;
    border-width: 0; border-radius: 0;
  }
  .dialog.wide { width: 100%; }
  .dialog-head { padding: calc(12px + env(safe-area-inset-top, 0px)) 12px 10px; padding-left: max(12px, var(--safe-l)); padding-right: max(12px, var(--safe-r)); }
  /* The body takes whatever is left so the way out is at the BOTTOM of the
     screen rather than wherever the content happened to stop. `min-height: 0`
     is what lets it be shorter than its content and scroll: a flex item's
     automatic minimum is its content height, so without this a long dialog
     grows past the viewport and pushes its own footer off the end.
     Small screens only -- on a wide one the dialog is a card that fits its
     content, and a flexed body would stretch every one of them to 80vh. */
  .dialog-body { padding: 14px 12px; padding-left: max(12px, var(--safe-l)); padding-right: max(12px, var(--safe-r)); flex: 1; min-height: 0; }
  .dialog-foot { padding: 12px 12px calc(12px + env(safe-area-inset-bottom, 0px)); padding-left: max(12px, var(--safe-l)); padding-right: max(12px, var(--safe-r)); }
  .dialog-x { width: 44px; height: 44px; font-size: 22px; }

  /* --- the region panel is the whole screen --- */

  /* Late in a run this carries a dozen figures and every measure in force. As a
     322px box pinned at `top: 52px` under a two-row top bar, the country's own
     name was behind the bar and Cancel was cut in half by the bottom edge. */
  .region-panel {
    position: fixed; inset: 0; z-index: 44;
    width: auto; max-width: none; max-height: none;
    border-width: 0; border-radius: 0; padding: 0;
    display: flex; flex-direction: column; overflow: hidden;
    background: var(--ink1);
  }
  .region-panel .panel-head {
    flex: none;
    padding: calc(12px + env(safe-area-inset-top, 0px)) 56px 10px 12px;
    padding-left: max(12px, var(--safe-l));
    /* The cross lives in this 56px, so the inset has to grow the gutter it sits
       in as well as move the cross itself -- see the rule further down. */
    padding-right: max(56px, calc(56px + var(--safe-r) - 10px));
    border-bottom: 2px solid var(--ink3);
  }
  .region-panel .panel-head .sub { margin-bottom: 0; }
  .region-panel .panel-body {
    flex: 1; min-height: 0; overflow-y: auto; padding: 12px; padding-left: max(12px, var(--safe-l)); padding-right: max(12px, var(--safe-r));
  }
  .region-panel .panel-foot {
    flex: none;
    padding: 10px 12px calc(10px + env(safe-area-inset-bottom, 0px));
    padding-left: max(12px, var(--safe-l)); padding-right: max(12px, var(--safe-r));
    border-top: 2px solid var(--ink3);
  }
  .region-panel .panel-foot .panel-actions { margin-top: 0; }
  /* The way out of a full-screen panel, and the one control that must never be
     under a cutout: there is a Cancel at the foot, but a cross you can see and
     cannot press is worse than no cross. */
  .region-panel .panel-head .dialog-x {
    top: calc(10px + env(safe-area-inset-top, 0px));
    right: max(10px, var(--safe-r));
  }
  /* Nothing left to scroll past the edges, so the marks that say "this window
     floats" are marks in the four corners of the phone. */
  .region-panel.handles { background-image: none; }

  /* The cure side's response panel wears `region-panel` too, and it must NOT
     become that screen: it lives behind its own docked button. This has to come
     after the rule above and not with the other three hidden panels, because
     both selectors are one class deep and the last one wins. */
  .cure-panel { display: none; }

  /* --- the ground-view switcher --- */

  /* The ground controls are the band's top row -- see `.hud-band` above for the
     columns they stand in. At the BOTTOM of the screen, not the top: they are
     controls a thumb works, and the top of the screen is a counter bar that is
     three rows tall here.

     The four-tab block this replaces was two rows of buttons plus a row of text,
     permanently across the bottom of a 375px screen. Two 64px squares and one
     line is the same information for a fifth of the space, and the three words
     that used to hide in each tab's tooltip are now readable in the menu. */
  /* A peer of the dock and of the back button: same 64px square, in the same
     column, so the bottom band reads as one set of controls. `width: auto` and
     let the track decide -- the track IS 64px, and a width written here would be
     a second copy of that number free to drift from it. */
  .tool-btn {
    width: auto; height: var(--band-btn); padding: 0;
    justify-content: center; gap: 0;
  }
  .tool-btn .nm { display: none; }
  .tool-btn .em { font-size: 32px; }
  /* Two buttons wide and one button tall, so the row is one band of equal boxes
     rather than two squares with a shorter label beside them.
     WIDTH: it spans the tracks that carry the way out and the meters, so its
     right edge lands exactly on the right edge of the meters button below it.
     That is three TRACKS, not two, because the break lies between those two
     buttons -- 2×64 + 10 would stop it in the middle of the break, which is a
     boundary of nothing. A span, not an arithmetic width, so it is still two
     buttons wide at 390 and 412 where the break is a different size.
     HEIGHT: the same `--band-btn` the squares take, with the text centred in
     it. The longest real string wraps to two lines -- "999 animaux · 999
     porteurs" is the worst case in either language -- and two lines at this
     size and leading come to 54px inside the 64. */
  .place-note {
    height: var(--band-btn);
    display: flex; align-items: center;
    margin-right: 0; white-space: normal; line-height: 1.25;
    max-width: none;
  }

  /* Over the button that opened it, ten pixels clear of the top of the band.
   *
   * Added up from the band's own tokens rather than typed, because the two pads
   * clear DIFFERENT bands: on the ground the band carries three rows -- the
   * "where am I" line, the tools, the dock -- and on the globe only the last
   * two, since the pin, the count and the line all belong to a locality. The
   * globe's sum comes to the 162 this rule used to say in one number, which is
   * the check that the arithmetic is the right arithmetic. */
  .cam-panel {
    right: max(var(--band-edge), var(--safe-r)); top: auto; left: auto; transform: none;
    bottom: calc(var(--band-edge) + 2 * var(--band-btn) + var(--where-h)
      + 3 * var(--band-gap));
  }
  .cam-panel.cam-globe {
    right: max(var(--band-edge), var(--safe-r)); top: auto; left: auto; transform: none;
    bottom: calc(var(--band-edge) + 2 * var(--band-btn) + 2 * var(--band-gap));
  }
  /* The release screen keeps its own corner on a phone. `.tool-btn` above has
     already turned the button into a square of `--band-btn` with its label
     hidden; the token follows so the pad's clearance follows with it, and the
     eye is the same 64px square as the one in a running game. */
  :root { --release-eye-h: var(--band-btn); }
  .release-eye {
    left: max(var(--band-edge), var(--safe-l));
    bottom: calc(var(--band-edge) + env(safe-area-inset-bottom, 0px));
    width: var(--release-eye-h);
  }
  .cam-panel.cam-release {
    left: max(var(--band-edge), var(--safe-l)); right: auto; top: auto; transform: none;
    bottom: calc(var(--band-edge) + env(safe-area-inset-bottom, 0px)
      + var(--release-eye-h) + var(--band-gap));
  }

  /* The band's first column, with the break to its right. Not a fourth view
     tab: the tabs are one region at four removes and this is leaving.
     `position: static` -- it is a cell of the grid now, not a box anchored to a
     corner, and the two must not both be true or the corner wins. */
  .ground-back {
    position: static; grid-row: 3; grid-column: 1;
    height: var(--band-btn); padding: 0;
    justify-content: center; gap: 0;
  }
  .ground-back .nm { display: none; }
  .ground-back .em { font-size: 32px; }

  /* --- things a finger still could not hit --- */

  /* A research bubble is a moving target on a rotating globe. */
  .bubble { width: 48px; height: 48px; margin: -24px 0 0 -24px; }
  .card-info { width: 44px; height: 44px; font-size: 24px; }
  /* Two exceptions, both because the question mark is the SECOND control in its
     row and the row itself is the target. In a dial's header a 44px square
     drags the baseline of the value beside it out of line; in a measure row it
     turns a list of nineteen into a list of nineteen sixty-pixel rows. */
  .dial .head .card-info { width: 40px; height: 40px; font-size: 22px; }
  .measure-row .card-info { width: 40px; height: 40px; font-size: 22px; }
  .meter .lbl { gap: 8px; }
  .measure-list { gap: 4px; }
  .news { gap: 4px; }
  /* The one column of figures a phone keeps, and eleven pixels of JetBrains is
     under the size anything on this screen is allowed to be. */
  .stat-row span:last-child { font-size: 13px; }

  /* A transient message, where the docstring on `toast()` says it belongs:
     the middle of the screen. At 62px from the top it was landing on the second
     row of a top bar that is two rows tall here -- and taller again in French,
     where the bar has no fixed height to dodge. */
  /* Centred by construction, which it was not.
     The base rule centres with `left: 50%` and a transform and caps the width at
     `100vw - 36px`. This one sets BOTH insets and drops the transform, and the
     two do not compose: with `left`, `right` and an auto width all set, the box
     is over-constrained, and the cap made it 12px narrower than the insets
     allow -- surplus that falls entirely to the right in a left-to-right box, so
     the message sat 12px from the left edge and 24 from the right.
     Two lines, and each earns its place. `max-width: none` gives the insets the
     job of deciding the width, which is what they were written to do; they are
     equal, so the box is centred. `margin-inline: auto` is what keeps it centred
     if a width ever comes back -- an over-constrained absolutely positioned box
     with auto margins splits the surplus equally instead of dropping it on one
     side. */
  .toast {
    left: max(12px, var(--safe-l)); right: max(12px, var(--safe-r)); top: 38%;
    max-width: none; margin-inline: auto;
    transform: none; text-align: center;
  }

  /* --- the end card --- */

  .end-stats .s .k { font-size: 16px; }
  .end-actions { display: grid; grid-template-columns: 1fr 1fr; }
}

/* ===========================================================================
 * Zpread — the dispatch interface, on the same kit.
 *
 * Everything above is the other game's stylesheet, kept whole so the buttons,
 * the bar, the dialogs and the labels are the same objects. Below is only what
 * this game adds: the orders column, the feed, the follow chip, the loading
 * bar and the where-line. Same tokens, same 2px rules, same two faces.
 * ======================================================================== */

/* The orders: a column of tools down the left, under the bar. */
/* Both columns hang under the bar, WHATEVER its height. The bar is one row on a
   wide screen and three below the breakpoint, and taller again in French; a
   number typed here was right for exactly one of those. `--bar-h` is measured
   off the bar itself by `hud.js`, four times a second. */
.zorders {
  position: absolute; left: max(14px, var(--safe-l)); top: calc(var(--bar-h, 56px) + 10px);
  display: flex; flex-direction: column; gap: 6px; width: 268px;
  /* 96: the follow chip stands in the corner under this column -- see `.zfollow`
     -- and the column stops short of it. */
  max-height: calc(100% - var(--bar-h, 56px) - 96px); overflow: hidden auto;
}
.zorders .ztitle {
  font-size: 15px; text-transform: uppercase; letter-spacing: 0.14em;
  color: var(--text-faint); padding: 0 4px 2px;
}
.zorder {
  appearance: none; cursor: var(--cur-pointer, pointer);
  display: flex; align-items: center; gap: 10px;
  padding: 6px 12px; min-height: 40px; width: 100%; text-align: left;
  background: var(--ink1-50); border: 2px solid var(--ink3); border-radius: 2px;
  -webkit-backdrop-filter: blur(var(--frost)); backdrop-filter: blur(var(--frost));
  color: var(--text); font-size: 19px; letter-spacing: 0.03em;
}
.zorder .em { font-size: 21px; line-height: 1; flex: none; }
.zorder .nm { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.zorder .cost { font-size: 17px; color: var(--gold); flex: none; }
/* The response time, beside the price: how long that service takes to reach
   the point under the cursor. The data face, because it is a figure read
   against the one above and below it. */
.zorder .eta { font-family: var(--mono); font-size: 12px; color: var(--text-dim); flex: none; min-width: 4.5ch; text-align: right; }
.zorder.on .eta { color: #2b1a12; }
.zorders .zsub { font-size: 14px; text-transform: uppercase; letter-spacing: 0.14em; color: var(--coral); padding: 6px 4px 0; }
.zorder.extreme { border-color: var(--coral-dark); }
.zorder:hover { background: var(--ink2); color: var(--white); }
.zorder:active { transform: translateY(2px); }
.zorder.on { background: var(--coral); border-color: var(--coral); color: #2b1a12; }
.zorder.on .cost { color: #2b1a12; }
.zorder.broke .cost { color: var(--bad); }
.zorder:disabled { opacity: .45; }

/* The restriction slider, in the same column. */
.zlock {
  display: flex; flex-direction: column; gap: 4px; padding: 8px 12px;
  background: var(--ink1-50); border: 2px solid var(--ink3); border-radius: 2px;
  -webkit-backdrop-filter: blur(var(--frost)); backdrop-filter: blur(var(--frost));
}
.zlock .row { display: flex; align-items: center; gap: 8px; font-size: 19px; }
.zlock .row .em { font-size: 21px; line-height: 1; }
.zlock .row .nm { flex: 1; }
.zlock .row .pct { color: var(--gold); font-size: 17px; }
.zlock input[type=range] { width: 100%; accent-color: var(--coral); cursor: var(--cur-pointer, pointer); margin: 2px 0 0; }
.zlock .hint2 { font-size: 15px; color: var(--text-faint); line-height: 1.4; }

/* The follow chip. It is a child of the bottom band, not of the orders column,
   because on a phone the column is behind a button and the way to stop
   following has to stay on screen. On a wide screen the band is
   `display: contents` (the kit's `.hud-band`) and the chip stands on its own in
   the corner under the orders; below the breakpoint it is the band's top row. */
.zfollow {
  position: absolute; left: max(14px, var(--safe-l)); bottom: 14px; width: 268px;
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px;
  padding: 7px 10px; background: var(--ink1); border: 2px solid var(--gold); border-radius: 2px;
  font-size: 18px; color: var(--text);
}
/* The name keeps at least 140px; where the two buttons do not fit beside that
   -- the 268px corner box -- they go under it, rather than the name being
   broken one syllable to a line beside them. In the phone band there is room
   and it stays one row. */
.zfollow .who { flex: 1 1 140px; min-width: 0; }
.zfollow .who b { color: var(--gold); display: block; }
.zfollow .who span { color: var(--text-dim); font-size: 16px; }
.zfollow .btn { padding: 3px 8px; font-size: 16px; }

/* The feed: last few headlines, top right under the bar. */
.zfeed {
  position: absolute; right: max(14px, var(--safe-r)); top: calc(var(--bar-h, 56px) + 10px); width: 322px;
  display: flex; flex-direction: column; gap: 3px; pointer-events: none;
}
.zfeed .item {
  background: var(--ink1-50); border: 2px solid var(--ink3); border-radius: 2px;
  -webkit-backdrop-filter: blur(var(--frost)); backdrop-filter: blur(var(--frost));
  padding: 6px 10px; font-size: 18px; line-height: 1.4; color: var(--text);
}
.zfeed .item .d { font-size: 15px; color: var(--text-faint); margin-right: 8px; font-family: var(--mono); }
.zfeed .item.alert { border-color: var(--coral); }
.zfeed .item.alert .d { color: var(--coral); }
.zfeed .item.measure .d { color: var(--sage); }
.zfeed .item.good { border-color: var(--good); }
.zfeed .item.good .d { color: var(--good); }
.zfeed .item.old { opacity: .55; }
/* A headline with a place on the map is a button to it. */
.zfeed .item.link { pointer-events: auto; cursor: var(--cur-pointer, pointer); }
.zfeed .item.link:hover { background: var(--ink2); color: var(--white); }

/* Where the camera is, and the eye beside it: the bottom strip. */
.zwhere {
  /* The bottom-right corner, not the middle: the hint sits in the middle and
     was covering the place name for the fourteen seconds it shows. */
  position: absolute; right: max(14px, var(--safe-r)); bottom: 14px;
  display: flex; align-items: center; gap: 8px;
}
.zwhere .tool-btn { min-height: 40px; padding: 6px 12px; }
.zwhere .wh { font-size: 19px; color: var(--text); padding: 0 6px; white-space: nowrap; text-align: right; }
.zwhere .wh .code { color: var(--gold); margin-left: 6px; font-family: var(--mono); font-size: 13px; }

/* The camera pad: the same nine keys, hung above the eye. */
.zpad {
  position: absolute; left: 50%; bottom: 66px; transform: translateX(-50%);
  background: var(--ink1); border: 2px solid var(--ink3); border-radius: 2px; padding: 8px;
  display: grid; grid-template-columns: repeat(3, 48px); gap: 5px;
}
.zpad .cam-btn, .zpad .btn { width: 48px; height: 44px; padding: 0; display: inline-flex; align-items: center; justify-content: center; }

/* Chips over units and fires. */
.zchip {
  position: absolute; transform: translate(-50%, -100%);
  display: flex; align-items: center; gap: 6px;
  font-size: 18px; color: var(--text); letter-spacing: 0.03em; line-height: 1.1;
  background: var(--ink1); border: 2px solid var(--ink3); border-radius: 2px;
  padding: 2px 8px; white-space: nowrap; pointer-events: none;
}
.zchip .em { font-size: 18px; line-height: 1; }
.zchip .n { color: var(--gold); }
.zchip.fire { border-color: var(--coral); }
.zchip.sel { border-color: var(--gold); }
.zchip.lost { opacity: .55; }
.zchip.bit { border-color: #c9b23e; }
.zchip.bit .n { color: #efd75a; }
.zchip.cull { border-color: var(--coral); }
.zchip.cull .n { color: var(--coral); }
.zorder .cost.free { color: var(--text-dim); }
.zorder.locked { opacity: .5; }
.zorder.locked .cost { color: var(--text-faint); font-size: 14px; letter-spacing: 0.08em; text-transform: uppercase; }
/* The accordion's two buttons: the phone dock's, drawn wide, with the same
   badge in the same corner. One section shows under them at a time. */
.ztabs { display: flex; gap: 8px; margin-bottom: 4px; }
.ztab {
  position: relative; flex: 1 1 0; min-width: 0; appearance: none; cursor: var(--cur-pointer, pointer);
  display: flex; align-items: center; justify-content: center; gap: 8px;
  min-height: 48px; padding: 4px 6px;
  background: var(--ink1-50); border: 2px solid var(--ink3); border-radius: 2px;
  -webkit-backdrop-filter: blur(var(--frost)); backdrop-filter: blur(var(--frost));
  color: var(--text); font-size: 15px; letter-spacing: 0.05em; text-transform: uppercase;
}
.ztab .em { font-size: 24px; line-height: 1; flex: none; }
/* The brigade's mark on its button: the game's own Z, in the brigade's red. */
.ztab .zlogo { font-size: 30px; font-weight: 700; color: var(--coral); line-height: 1; flex: none; }
.ztab.on .zlogo { color: #2b1a12; }
.dock-btn .zlogo { font-size: 40px; font-weight: 700; color: var(--coral); line-height: 1; }
/* Three dock buttons now, and the kit floats a third one on a wide screen. Not here. */
.hud-dock .dock-btn:nth-child(3) { display: none; }
.ztab .nm { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ztab:active { background: var(--ink2); transform: translateY(2px); }
.ztab.on { background: var(--coral); border-color: var(--coral); color: #2b1a12; }
.ztab .pip {
  position: absolute; top: -8px; right: -8px; min-width: 26px; height: 26px;
  padding: 0 6px; box-sizing: border-box; z-index: 1;
  display: flex; align-items: center; justify-content: center;
  background: var(--coral); color: #2b1a12; border: 2px solid var(--coral-dark);
  font-size: 18px; font-weight: 700; letter-spacing: 0; text-transform: none;
}
.ztab .pip.zero { background: var(--ink2); color: var(--text-faint); border-color: var(--ink3); }
.zsec { display: flex; flex-direction: column; gap: 6px; }
.zsec.hidden { display: none; }
/* The where-line: the country, small and spaced, over the district. */
.zwhere .wh .country { font-size: 14px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--text-faint); line-height: 1.1; }
.zwhere .wh .district { line-height: 1.15; }
.zchip.base { border-color: #d24a3a; }
.zchip.base .n { color: #f08c7e; }
.zchip.shelter { border-color: #4fc95a; }
.zchip.shelter .n { color: #8ef09a; }

/* The loading screen. */
.zload { width: min(560px, 90vw); display: flex; flex-direction: column; align-items: center; text-align: center; gap: 12px; }
.zload .track { width: 100%; height: 14px; background: var(--ink0); border: 2px solid var(--ink3); }
.zload .fill { height: 100%; background: var(--gold); transition: width .12s steps(3); }
.zload .sub { color: var(--text-dim); font-size: 19px; min-height: 1.4em; }

/* A toast lands above the bottom strip, not in the feed's corner. */
#hud .toast { top: auto; bottom: 76px; }

/* The scrim behind the menu, over a map instead of a globe. */

/* The speed strip: pause, the three steps and the big fast-forward, on ONE line
   at every width. The kit's `.speed-steps` is two equal tracks for the other
   game's two sustained speeds; this game has three, and on two tracks the third
   wrapped under the first. Same construction, one more track: the widest label
   decides the width and all three take it. The big button, its `latched` and
   `held` looks and its icon size are the kit's own rules (`.speed .btn.ff`). */
.speed-steps { grid-template-columns: repeat(3, 1fr); }

/* The clock is the LAST cell of the bar, after the gear and the help mark, at
   every width -- `hud.js` builds it last, and on a wide screen the flat row
   needs nothing more. It used to lead the bar, which on a phone is the top-left
   corner: under the punch-hole camera on the phones that have one there. The
   phone rules are in the block below. */

@media (max-width: 900px) {
  /* --- the bar: three rows at most, the clock last -------------------------
     The kit's phone bar stacks the counters two by two, which with this game's
     FIVE counters is three rows, and gives the date a row of its own at the
     top: five rows, 276px of a 700px window. Here: one row of counters, sized
     to fit; the speed strip with the gear and the help mark; and the clock
     LAST, against the right edge -- on the controls' row when it fits beside
     them (a tablet, a phone on its side) and on a row of its own when it does
     not. `justify-content: flex-end` is what puts a row of its own at the
     right; the other two rows fill their width and never see it. */
  .topbar { justify-content: flex-end; }
  .topbar .bar-stats { grid-template-columns: repeat(auto-fit, minmax(60px, 1fr)); gap: 2px 8px; }
  .topbar .stat .v, .topbar .bar-date .v { font-size: 21px; }
  .topbar .stat .k { font-size: 13.5px; }
  .topbar .bar-controls { flex: 1 1 auto; }
  /* The owner's phone order: the day at the top left and the hour at the top
     right, on a row of their own BEFORE everything else -- a camera notch sits
     in the middle of that row on some phones and both figures clear it. Then
     the counters, then the controls. `order: -1` puts the cell first whatever
     the markup's order, which is the clock-last order the desktop wants. */
  .topbar .stat.bar-date { flex: 1 0 100%; order: -1; display: block; }
  /* The controls row fits the phone: the strip gives before the gear does, the
     fast-forward is the part that gives, the help mark is in Réglages instead,
     and the gear is flush with the right edge -- the same edge the hour ends on. */
  .topbar .bar-controls { flex-wrap: nowrap; }
  .topbar .speed { flex: 1 1 auto; min-width: 0; }
  .topbar .speed .btn.ff { flex: 1 1 48px; min-width: 44px; }
  .topbar .bar-tools { flex: none; margin-left: auto; margin-right: 0; }
  .topbar .bar-tools .help { display: none; }
  .topbar .bar-date .bar-when .v:last-child { text-align: right; }
  .topbar .bar-date .bar-when { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; width: 100%; }
  .topbar .bar-date .bar-when .v { font-size: 23px; }

  /* --- the bottom band ------------------------------------------------------
     The kit's grid (`.hud-band`: anchored to the bottom, the band's gap and
     button tokens) with this game's rows on it. Four tracks: a button, the
     stretch, a button, a button. The follow chip across the top when there is
     one; the where-line with the pin and the eye; and the two docked columns'
     buttons in the corners -- orders on the left and news on the right, where
     the columns are on a wide screen. The band itself lets the map through:
     only the controls in it take the finger. (`#hud > *` turns pointer events
     on for every child of the interface at id strength, so the band is named
     the same way to turn them off again.) */
  .hud-band {
    grid-template-columns: var(--band-btn) var(--band-btn) 1fr var(--band-btn) var(--band-btn);
    grid-template-areas:
      "follow  follow  follow follow follow"
      "where   where   where  pin    eye"
      "orders  brigade .      .      news";
  }
  #hud > .hud-band { pointer-events: none; }
  .hud-band .zfollow, .hud-band .tool-btn, .hud-band .dock-btn { pointer-events: auto; }
  .zfollow { grid-area: follow; position: static; width: auto; }
  /* The where-line dissolves so its three parts stand on the band's own tracks:
     the name over the stretch, the pin over the gap, the eye over the news. */
  .zwhere { display: contents; }
  .zwhere .wh { grid-area: where; min-width: 0; overflow: hidden; text-overflow: ellipsis; padding: 0 2px; }
  /* The pin and the eye are the same square as the two dock buttons under
     them: four buttons in one band, one size. */
  .zwhere .tool-btn {
    padding: 0; justify-content: center;
    width: var(--band-btn); min-height: var(--band-btn); height: var(--band-btn);
  }
  .zwhere .tool-btn .em { font-size: 30px; }
  .zwhere .tool-btn.pin { grid-area: pin; }
  .zwhere .tool-btn.eye { grid-area: eye; }
  .zwhere .wh { align-self: center; }
  .hud-dock .dock-btn:nth-child(1) { grid-area: orders; }
  .hud-dock .dock-btn:nth-child(2) { grid-area: brigade; }
  .hud-dock .dock-btn:nth-child(3) { display: block; grid-area: news; position: static; width: auto; height: var(--band-btn); }
  .zwhere .wh { text-align: left; }
  /* The camera pad clears the band: the edge, a button, the gap, the
     where-line, the gap -- added up from the tokens rather than measured. */
  .zpad { bottom: calc(var(--band-edge) + var(--band-btn) + var(--band-gap) + var(--band-btn) + var(--band-gap)); }

  /* --- the two columns, in the sheets the dock opens -------------------------
     Out of the way on the map. Inside a sheet each is a plain column the width
     of the dialog: no anchoring, no cap, every headline, every price, and the
     feed takes the finger again. The sheet's own heading names the orders. */
  .zorders, .zfeed { display: none; }
  .dock-body .zorders, .dock-body .zfeed {
    display: flex; position: static; width: auto; max-height: none; inset: auto;
    overflow: visible; pointer-events: auto;
  }
  .dock-body .zorders .ztitle { display: none; }
}
