:root {
  --bg: #0d0f0b;
  --panel: #16190f;
  --line: #2c3123;
  --ink: #e6e2cd;
  --muted: #9aa08a;
  --accent: #c8a24a;
  --safe: #4f9d5d;
  --danger: #c1443a;
  --barricade: #e0913f;
  --food: #82c45c;
  --materials: #d7b049;
  --odds-good: #4f9d5d;
  --odds-mid: #d9a441;
  --odds-bad: #c1443a;
  /* The trail's own colour (see COLORS.goop in view/board-layers.mjs) — a
     marked wall carries the same intense purple the goop itself is drawn
     in, so the smear on the wall reads as the same thing as the trail
     leading to it. */
  --goop: #a238ff;

  /* Health. Medicine takes a cool teal because every other resource on the
     board is a warm earth tone — the point of the third larder row is that it
     is not interchangeable with the other two, and the palette should say so
     before the label does.

     Injured borrows the barricade amber deliberately: it is the same "this is
     costing you, but it still works" register the wall wear already uses.
     Critical is the danger red the game reserves for things that end runs. */
  --medicine: #5cbfb4;
  --injured: #e0913f;
  --critical: #c1443a;

  /* A placement piece says two things at once: the initial is who, the rim
     colour is what job. Four of these reuse marks the board already makes
     elsewhere (the pin blue, the barricade amber, the scout cyan, the rescue
     gold) so a piece matches the job it is standing on. Repair is the one new
     hue — a rose that nothing else on an olive/amber/blue board uses, so it
     can't be read as the barricade amber it sits next to conceptually. */
  --job-scavenge: #7fb2e5;
  --job-farm: #7fce68;
  --job-scout: #5fe3d6;
  --job-barricade: #e0913f;
  --job-repair: #c9799e;
  --job-rescue: #e6d38a;
  --job-outpost: #c8a24a;
  /* The two jobs run on somebody else's behalf. Both sit deliberately close to
     --job-rescue, because that is what they are for: an errand is a looking job
     so it leans toward the scout cyan, a delivery is a carrying job so it
     leans toward the gold, and neither becomes a colour the player has to
     learn separately from the people they are about. */
  --job-errand: #b8cf9c;
  --job-delivery: #d9b877;
  --job-idle: #6f7566;
  /* The night's two. Defend is a pale bone rather than another hue: at dusk the
     board is dark and the point is that these are *our* pieces, standing out
     against the horde red rather than competing with the day's job colours.
     Threat reuses COLORS.horde from board-layers, so the branch on the road and
     the number at the end of it are the same red. */
  --job-defend: #e8d9a8;
  --job-threat: #c1443a;
}

/* --- Icon + value grammar: a quantity is an icon next to a number, reused --- */
/* identically at every surface (top bar, tooltip, backlog, ledger, report). */
.icon {
  display: inline-block;
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  flex-shrink: 0;
}

.icon-food {
  color: var(--food);
}

.icon-materials {
  color: var(--materials);
}

.icon-medicine {
  color: var(--medicine);
}

.resource-chit {
  display: inline-grid;
  width: 30px;
  height: 30px;
  place-items: center;
  border: 1px solid color-mix(in srgb, currentColor 70%, #fff 18%);
  border-radius: 8px;
  color: var(--resource-color, var(--text));
  background:
    radial-gradient(circle at 35% 28%, rgba(255, 255, 255, 0.42), rgba(255, 255, 255, 0) 32%),
    rgba(16, 21, 17, 0.92);
  box-shadow:
    0 3px 10px rgba(0, 0, 0, 0.32),
    0 0 0 1px rgba(0, 0, 0, 0.22);
}

.resource-chit.food {
  --resource-color: #74d84f;
}

.resource-chit.materials {
  --resource-color: var(--materials);
}

.resource-chit.medicine {
  --resource-color: var(--medicine);
}

.resource-chit .icon {
  width: 26px;
  height: 26px;
  color: var(--resource-color);
  filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.4));
}

.resource-chit-sm {
  width: 20px;
  height: 20px;
  border-radius: 6px;
  box-shadow:
    0 2px 6px rgba(0, 0, 0, 0.26),
    0 0 0 1px rgba(0, 0, 0, 0.18);
}

.resource-chit-sm .icon {
  width: 17px;
  height: 17px;
}

.icon-people {
  color: var(--ink);
}

.icon-healthy {
  color: var(--safe);
}

.icon-injured {
  color: var(--injured);
}

.icon-critical {
  color: var(--critical);
}

.icon-time {
  color: var(--muted);
}

.icon-odds {
  color: var(--muted);
}

.icon-base {
  color: var(--accent);
}

.icon-threat {
  color: var(--danger);
}

.icon-scouted,
.icon-scout,
.icon-search,
.icon-wall,
.icon-repair,
.icon-rescue {
  color: var(--muted);
}

.stat {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-variant-numeric: tabular-nums;
}

.stat-num {
  font-weight: 600;
}

.stat.good,
.stat.good .stat-num {
  color: var(--safe);
}

.stat.bad,
.stat.bad .stat-num {
  color: var(--danger);
}

.stat.bad .icon-base {
  color: var(--danger);
}

/* A number that is a risk rather than a quantity. Amber and not red: it has not
   gone wrong yet, it is the price of something the player is about to buy. */
.stat.warn,
.stat.warn .stat-num {
  color: var(--injured);
}

/* The weight band on a street's hover card, in the trail's own colour — so
   the words on the card and the band drawn on the map are visibly the same
   fact. Capitalised in CSS rather than in the string so the band names stay
   the plain lowercase keys the domain uses. */
.stat.goop-stat .stat-num {
  color: var(--goop);
  text-transform: capitalize;
  /* "Heavy goop" is one label, not two words to break between. */
  white-space: nowrap;
}

.stat.odds-good .icon {
  color: var(--odds-good);
}

.stat.odds-mid .icon {
  color: var(--odds-mid);
}

.stat.odds-bad .icon {
  color: var(--odds-bad);
}

.stat.odds-good .stat-num {
  color: var(--odds-good);
}

.stat.odds-mid .stat-num {
  color: var(--odds-mid);
}

.stat.odds-bad .stat-num {
  color: var(--odds-bad);
}

.tip-detail {
  color: var(--muted);
  font-size: 10px;
}

.tip-state {
  color: var(--muted);
}

.tip-state.bad {
  color: var(--barricade);
}

.progress {
  display: inline-block;
  width: 34px;
  height: 6px;
  margin-right: 4px;
  background: #1c2016;
  border-radius: 3px;
  overflow: hidden;
  vertical-align: middle;
}

.progress-fill {
  display: block;
  height: 100%;
  background: var(--barricade);
}

/* A wall's HP as pips: how much is left reads at a glance, same colour ramp */
/* as the wall's own line on the map (healthy vs worn at the 60% threshold). */
.hp-pips {
  display: inline-flex;
  gap: 2px;
  vertical-align: middle;
}

.hp-pip {
  width: 7px;
  height: 7px;
  border-radius: 1px;
  background: #1c2016;
  border: 1px solid var(--line);
}

/* A barricade is a consumable, not a verdict, so its pips wear rather than
   score. Amber matches the wall's own colour on the board; red is the last
   third, where it is about to stop being a wall at all. See pipsMarkup. */
.hp-pips.wear-standing .hp-pip.filled {
  background: var(--barricade);
  border-color: var(--barricade);
}

.hp-pips.wear-failing .hp-pip.filled {
  background: var(--odds-bad);
  border-color: var(--odds-bad);
}

/* The same pips, worn by the wall itself. Smaller than the tooltip's, because on
   the board they have to sit inside a street rather than inside a card, and
   rotated by a child so MapLibre's own transform on the marker root survives. */
.wall-pips {
  pointer-events: none;
}

/* The row is exactly as long as the wall — createWallPips writes that width from
   the projected ends of the same tick the GL layer draws from — so the pips divide
   the wall instead of sitting near it. No horizontal padding for the same reason:
   the row has to start and stop where the wall does. */
.wall-pips-row {
  display: flex;
  gap: 1px;
  padding: 1px 0;
  border-radius: 2px;
  background: rgba(10, 12, 8, 0.45);
  transform-origin: center;
}

/* pipsMarkup wraps the pips in .hp-pips, so the row's own flex lays out that
   wrapper and not the pips — the wrapper has to be told to fill the row before
   the pips inside it have any width to share. */
.wall-pips-row .hp-pips {
  display: flex;
  width: 100%;
  gap: 1px;
}

/* Pips share out whatever the wall's length currently is, so ten of them always
   span it exactly.

   A spent pip has to read as *spent*, not as absent. The tooltip's version sits
   on a dark card where a near-black pip is visible enough; on the board it sits on
   a dark road under a red tick and disappeared completely, which made a wall at
   7 of 10 look like pips that only covered the first two thirds of it — exactly
   the "why doesn't it span the wall" the spanning was meant to answer. So the
   empty ones get a visible grey, and the full ten always read as ten. */
.wall-pips-row .hp-pip {
  flex: 1 1 0;
  width: auto;
  min-width: 0;
  height: 5px;
  border-radius: 1px;
  background: rgba(150, 158, 140, 0.3);
  border-color: rgba(150, 158, 140, 0.45);
}

/* Zoomed far enough out that ten pips inside one wall would be a smear rather
   than a count. The tick's own colour and width still carry wear at this range,
   which is what that ramp was always for. */
.wall-pips.too-small {
  display: none;
}

/* Nobody is near enough to reach this one tonight. The ledger has always said how
   many walls are in that state and never which, and which is the decision. Drawn
   outside the rotated row so it stays upright however the street runs. */
.wall-pips.unheld::after {
  content: "!";
  position: absolute;
  top: -13px;
  left: 50%;
  transform: translateX(-50%);
  color: var(--job-threat);
  font-size: 13px;
  font-weight: 800;
  line-height: 1;
  text-shadow: 0 0 3px rgba(10, 12, 8, 0.95);
}

/* Marked at dusk means visited at night — the smear that says which wall the
   swarm actually found. A glow on the row itself, not another glyph
   competing with the "!" above: this wall is the one to watch, and both can
   be true of it at once (marked and nobody near enough to hold it). */
.wall-pips.marked .wall-pips-row {
  box-shadow: 0 0 11px 3px var(--goop);
}

/* The moment a wall takes a hit. Brightness only, and not because a scale would
   not read better: `transform` on this element is carrying the rotation that puts
   the pips across the street, and a keyframe touching transform would flatten it
   back to horizontal mid-hit. (The marker root is not an option either — MapLibre
   rewrites that transform every frame.) */
.wall-pips-row.struck {
  animation: wall-struck 260ms ease-out;
}

@keyframes wall-struck {
  0% {
    filter: brightness(2.4);
  }
  40% {
    filter: brightness(1.6);
  }
  100% {
    filter: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .wall-pips-row.struck {
    animation: none;
  }
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--ink);
  font: 13px/1.45 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  overflow: hidden;
}

.main-menu {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  align-items: center;
  justify-items: center;
  padding: clamp(18px, 4vw, 44px);
  background:
    linear-gradient(180deg, rgba(7, 9, 7, 0.84), rgba(8, 10, 7, 0.52) 48%, rgba(7, 9, 7, 0.9)),
    radial-gradient(circle at 16% 14%, rgba(200, 162, 74, 0.18), transparent 34%),
    var(--bg);
  color: var(--ink);
}

.main-menu[hidden] {
  display: none;
}

.main-menu::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  opacity: 0.24;
  background-image:
    linear-gradient(rgba(230, 226, 205, 0.055) 1px, transparent 1px),
    linear-gradient(90deg, rgba(230, 226, 205, 0.045) 1px, transparent 1px);
  background-size: 44px 44px;
  mask-image: radial-gradient(circle at center, #000, transparent 76%);
}

.main-menu-head {
  align-self: start;
  text-align: center;
}

.main-menu h1 {
  margin: 0;
  color: #f4f0dc;
  font-size: clamp(36px, 8vw, 92px);
  line-height: 0.88;
  font-weight: 950;
  letter-spacing: 0;
  text-transform: uppercase;
  text-shadow: 0 10px 34px rgba(0, 0, 0, 0.72);
}

.main-menu-panel {
  align-self: center;
  width: min(168px, calc(100% - 32px));
  display: grid;
  gap: 18px;
  padding-bottom: min(7vh, 58px);
}

.main-menu-action {
  position: relative;
  display: block;
}

.main-menu-action-note {
  position: absolute;
  top: 50%;
  left: calc(100% + 8px);
  width: clamp(56px, calc((100vw - 216px) / 2), 190px);
  transform: translateY(-50%);
  color: #9f9a82;
  font-size: 14px;
  font-weight: 550;
  line-height: 1.2;
  text-align: left;
}

.main-menu-button {
  width: 100%;
  min-height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0;
  border: 1px solid #5f5630;
  border-radius: 6px;
  background: #252818;
  color: var(--ink);
  font: inherit;
  font-size: 15px;
  font-weight: 850;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
}

.main-menu-button:hover,
.main-menu-button:focus-visible {
  background: #31351f;
  border-color: var(--accent);
  outline: none;
}

.main-menu-button.primary {
  min-height: 52px;
  background: #d8bd62;
  border-color: #ffe69a;
  color: #15120a;
  font-size: 16px;
}

.main-menu-button.primary:hover,
.main-menu-button.primary:focus-visible {
  background: #ecd176;
}

.legacy-menu {
  display: grid;
  gap: 8px;
  margin-top: 10px;
}

.legacy-menu h2 {
  margin: 0 0 2px;
  color: var(--muted);
  font-size: 11px;
  font-weight: 850;
  letter-spacing: 0.08em;
  text-align: center;
  text-transform: uppercase;
}

.main-menu-version {
  align-self: end;
  justify-self: end;
  color: var(--muted);
  font-size: 12px;
  font-weight: 750;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

#app {
  display: grid;
  grid-template-columns: 1fr 330px;
  height: 100%;
}

#board-wrap {
  position: relative;
  overflow: hidden;
}

/* The MapLibre container. The ground style's background is the same colour as
   the board, so there is no seam where the tiles end. */
#board {
  position: absolute;
  inset: 0;
}

/* A wall taking a hit, felt rather than read.
   Shaking #board and not a marker is deliberate and it does not break the rule
   in the .token-marker block below: that rule is about MapLibre rewriting a
   marker root's own transform every frame. #board is their *ancestor*, nothing
   else writes a transform to it, and the canvas and every marker are inside it —
   so the whole board moves together, in screen space, like a knock to the table.
   Whole pixels and translate only; a scale would resample the canvas. MapLibre
   reads pointer coordinates from this element's rect, so hit-testing is out by up
   to 3px while it runs — harmless, since nothing is clickable during the night. */
#board.shaking {
  animation: board-shake 220ms ease-out;
}

@keyframes board-shake {
  0% { transform: translate(0, 0); }
  15% { transform: translate(-3px, 1px); }
  35% { transform: translate(2px, -2px); }
  55% { transform: translate(-2px, -1px); }
  75% { transform: translate(1px, 1px); }
  100% { transform: translate(0, 0); }
}

/* Dusk, as a vignette rather than a dimmer: the middle stays readable and the
   edges close in, so the board feels smaller at night without the pieces going
   muddy. Capped well below opaque for the same reason. */
#night-wash {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  opacity: 0;
  transition: opacity 600ms ease;
  background:
    radial-gradient(ellipse at center, rgba(6, 10, 18, 0) 35%, rgba(6, 10, 18, 0.55) 100%),
    linear-gradient(rgba(10, 16, 30, 0.22), rgba(10, 16, 30, 0.22));
}

#night-wash.on {
  opacity: 1;
}

/* Damage, where the damage happened. The number rises off the thing it cost and
   fades; the animation is on the child so the marker root's transform survives. */
.map-floater {
  pointer-events: none;
}

.map-floater-value {
  display: block;
  font-size: 15px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  color: var(--job-threat);
  text-shadow:
    0 0 4px rgba(8, 10, 6, 0.95),
    0 1px 0 rgba(8, 10, 6, 0.9);
  animation: floater-rise 900ms ease-out forwards;
}

@keyframes floater-rise {
  0% {
    transform: translateY(2px);
    opacity: 0;
  }
  25% {
    transform: translateY(-3px);
    opacity: 1;
  }
  100% {
    transform: translateY(-20px);
    opacity: 0;
  }
}

.map-resource-pop {
  width: 34px;
  height: 42px;
}

.map-resource-pop-value {
  position: absolute;
  left: 50%;
  bottom: 0;
  transform: translate(-50%, 0) scale(0.72);
  opacity: 0;
  animation: resource-pop 1.45s cubic-bezier(0.2, 0.9, 0.2, 1) forwards;
  animation-delay: var(--resource-pop-delay, 0s);
}

@keyframes resource-pop {
  0% {
    transform: translate(-50%, 4px) scale(0.55);
    opacity: 0;
  }
  16% {
    transform: translate(-50%, -6px) scale(1.1);
    opacity: 1;
  }
  38% {
    transform: translate(-50%, -10px) scale(1);
    opacity: 1;
  }
  100% {
    transform: translate(-50%, -34px) scale(0.86);
    opacity: 0;
  }
}

/* Reduced motion loses the flourishes and keeps the information: the beats still
   run, the wash still tells you it is night, the number still appears. */
@media (prefers-reduced-motion: reduce) {
  #board.shaking {
    animation: none;
  }

  #night-wash {
    transition: none;
  }

  .map-floater-value {
    animation: floater-hold 900ms steps(1, end) forwards;
  }

  .map-resource-pop-value {
    animation: resource-pop-hold 1.2s steps(1, end) forwards;
  }

  @keyframes floater-hold {
    0%, 99% { opacity: 1; }
    100% { opacity: 0; }
  }

  @keyframes resource-pop-hold {
    0%, 99% { opacity: 1; transform: translate(-50%, 0) scale(1); }
    100% { opacity: 0; transform: translate(-50%, 0) scale(1); }
  }
}

/* --- Top-left chrome (ported from v4) ------------------------------------ */

.topbar {
  position: absolute;
  top: 12px;
  left: 14px;
  right: 14px;
  z-index: 6;
  display: flex;
  align-items: center;
  gap: 10px;
  pointer-events: none;
}

.topbar-brand {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  pointer-events: auto;
}

.topbar-brand .ad-menu {
  width: 42px;
  height: 34px;
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  margin: 0;
  padding: 0;
  border: 1px solid #5a4620;
  border-radius: 4px;
  background: #f3c849;
  color: #171207;
  font-size: 18px;
  line-height: 1;
  font-weight: 900;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.36);
}

.topbar-brand .ad-menu:hover:not(:disabled) {
  background: #ffd95c;
}

.topbar-brand .day-marker {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  height: 34px;
  padding: 4px 9px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: rgba(15, 18, 12, 0.88);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.28);
}

.topbar-brand .day-marker .label {
  color: var(--muted);
  font-size: 9px;
  font-weight: 800;
  letter-spacing: 0.06em;
  line-height: 1;
  text-transform: uppercase;
}

.topbar-brand .day-marker b {
  color: var(--text);
  font-size: 15px;
  line-height: 1;
}

.topbar-brand .people-marker .icon {
  width: 13px;
  height: 13px;
  color: var(--muted);
}

.topbar-brand .people-marker {
  position: relative;
  cursor: pointer;
}

.topbar-brand .people-marker:focus-visible {
  border-color: var(--accent);
  outline: none;
}

.topbar-brand .people-marker .people-card {
  top: calc(100% + 8px);
  left: 50%;
}

.topbar-brand .people-marker:hover .people-card,
.topbar-brand .people-marker:focus-visible .people-card,
.topbar-brand .people-marker:focus-within .people-card {
  opacity: 1;
  transform: translate(-50%, 0);
}

/* Growth is the game's first instruction, so the number moving is the payoff
   for following it — worth a beat of its own rather than a line of dawn prose. */
.topbar-brand .people-marker.grew {
  animation: people-grew 900ms ease-out;
}

.topbar-brand .people-marker.grew .icon,
.topbar-brand .people-marker.grew b {
  color: var(--safe);
}

@keyframes people-grew {
  0% { transform: scale(1); }
  25% { transform: scale(1.18); box-shadow: 0 0 0 3px rgba(79, 157, 93, 0.35); }
  100% { transform: scale(1); }
}

.topbar-menu {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  display: grid;
  gap: 5px;
  width: 190px;
  padding: 6px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: rgba(15, 18, 12, 0.96);
  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.5);
}

.topbar-menu[hidden] {
  display: none;
}

.topbar-menu button {
  width: 100%;
  min-height: 30px;
  margin: 0;
  text-align: left;
}

/* --- Map markers --------------------------------------------------------- */

.base-marker svg {
  display: block;
  width: 24px;
  height: 24px;
  /* Dark outline under the fill, as v4 draws it: the house has to stay legible
     against both the pale held-ground fill and the dark ground. */
  stroke: #10151a;
  stroke-width: 1.6;
  paint-order: stroke fill;
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.7));
}

/* --- Placement pieces ----------------------------------------------------- */
/* The token that is set down on an action space in the morning and walks the
   route in the afternoon — the same DOM node in both halves of the day.

   NEVER transition or animate `transform` on .token-marker itself. MapLibre
   writes the position transform onto the marker root, so transitioning it would
   animate every pan and zoom and leave the piece lagging the map. It would also
   cut corners in screen space during playback, which is the very thing
   following the road is meant to avoid. The "set down" feel comes from a
   one-shot keyframe on a *child*, which fires when the element is created and
   never again.

   The marker's box is the dot and nothing else, because MapLibre centres the
   whole element on the coordinate: with the name in flow the box was 40px tall
   and the dot rode 11px above where the piece actually was. The name is lifted
   out of flow above the dot instead. */
.token-marker {
  width: 18px;
  height: 18px;
  /* A piece sits on top of the pin it was placed on, and the pin is selected
     through map.on("click") — a token that swallowed clicks would make its own
     action space unclickable. Phase 2 grants pointer-events back to the pieces
     that can actually be picked up. */
  pointer-events: none;
}

.token-marker .token-dot {
  position: relative;
  display: grid;
  place-items: center;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #eaeadf;
  color: #0d0f0b;
  font-size: 10px;
  font-weight: 600;
  /* Rim carries the job; the dark ring outside it keeps the piece separate from
     whatever building fill it happens to be standing on. */
  box-shadow:
    0 0 0 2px var(--rim, var(--job-idle)),
    0 0 0 3.5px rgba(0, 0, 0, 0.55);
  animation: token-place 140ms ease-out;
}

/* What is coming, and how many of them.
   Wider than a survivor piece and not round, so at a glance the board reads as
   "our people are circles, that thing is not". The icon and the number sit
   together as one mark — the same icon+number grammar the panel uses, put on the
   board where the thing it counts actually is. */
/* Big, because the number on it is the one thing on the board the player has to
   read while it is moving. At the old size it was a 12px digit crossing a dark
   map at speed, which is not a thing anybody reads — it is a thing they squint
   at and then miss. */
.token-marker.threat .token-dot {
  width: auto;
  min-width: 58px;
  height: 38px;
  padding: 0 10px 0 6px;
  gap: 4px;
  grid-auto-flow: column;
  border-radius: 7px;
  background: #1b1210;
  color: var(--job-threat);
  box-shadow:
    0 0 0 2px var(--job-threat),
    0 0 0 4px rgba(0, 0, 0, 0.6),
    0 0 18px rgba(193, 68, 58, 0.5);
}

.token-marker.threat .icon {
  width: 22px;
  height: 22px;
  color: var(--job-threat);
}

.token-marker.threat .token-threat-value {
  font-variant-numeric: tabular-nums;
  font-size: 21px;
  font-weight: 700;
  line-height: 1;
}

/* Dread, as two properties.

   The complaint this answers is that the night was over before you noticed it
   had started, and that nothing on the board made you look at the thing walking
   at your wall. The answer is not a camera move — somebody who has panned
   somewhere is looking at something — it is that the thing itself gets harder to
   ignore the closer it gets.

   Two mechanisms, deliberately different in kind:

   - `--menace` is a number, 0 to 1, written from the frame loop as they close.
     It only ever feeds *interpolated* values inside the keyframes below, which
     the compositor recomputes for free.
   - the rate is a class, because writing animation-duration on a running
     animation re-phases it and reads as a stutter. It changes exactly twice in a
     night: `closing` at seven tenths of the approach, `striking` when the
     exchange starts.

   Transform and opacity only, so every one of these stays off the main thread.
   See applyMenace in view/tokens.mjs for the writing side. */
.token-marker.threat {
  --pulse: 1200ms;
}
.token-marker.threat.closing {
  --pulse: 640ms;
}
.token-marker.threat.striking {
  --pulse: 380ms;
}

/* The piece itself does NOT pulse.

   It used to, and the scale was on the same element as the number — so the one
   value the player has to read while it crosses the map was breathing between
   two sizes the whole way in. Menace is worth nothing if it costs the read. All
   of the motion lives on the ring below, which is a sibling that contains no
   text and can throb as hard as it likes. */
.token-marker.threat .token-dot {
  animation: token-place 140ms ease-out;
}

/* The ring that comes off them, widening and fading — now carrying the whole
   pulse on its own, so it is drawn heavier than when it was only half of it.
   Absolutely positioned inside the piece so it adds nothing to the layout, and
   pointer-events: none so it never eats a click meant for the board. */
.token-marker.threat .token-threat-ring {
  position: absolute;
  inset: -7px;
  border: 2px solid var(--job-threat);
  border-radius: 10px;
  opacity: 0;
  pointer-events: none;
  animation: threat-ring var(--pulse) ease-out infinite;
}

@keyframes threat-ring {
  0% {
    transform: scale(0.9);
    opacity: calc(0.18 + var(--menace, 0) * 0.55);
  }
  70% {
    transform: scale(calc(1.3 + var(--menace, 0) * 0.45));
    opacity: 0;
  }
  100% {
    opacity: 0;
  }
}

/* Somebody at a wall the moment it is struck. A nudge back towards the piece's
   own centre rather than a move on the map: the road they ran down is drawn from
   their real route, and shifting the coordinate would draw a road they did not
   take. Re-triggered per round by the markup rebuild the round's new number
   causes, which is exactly when the flinch should land. */
.token-marker.job-defend.lunge .token-dot {
  animation: defend-lunge 260ms ease-out;
}

@keyframes defend-lunge {
  0% {
    transform: scale(1);
  }
  35% {
    transform: scale(1.18);
  }
  100% {
    transform: scale(1);
  }
}

/* Reduced motion still has to say "this is dangerous", so the ring stays and
   hardens as they close instead of pulsing. Kept next to the rules it overrides
   rather than in the block further down, for the same reason .wall-pips-row
   .struck keeps its own. */
@media (prefers-reduced-motion: reduce) {
  .token-marker.job-defend.lunge .token-dot {
    animation: token-place 140ms ease-out;
  }

  .token-marker.threat .token-threat-ring {
    animation: none;
    opacity: calc(0.25 + var(--menace, 0) * 0.55);
  }
}

@keyframes token-place {
  from {
    transform: scale(1.35);
    opacity: 0;
  }
  to {
    transform: none;
    opacity: 1;
  }
}

/* An order is a promise, not a fact: until Dispatch is pressed the piece is
   standing somewhere it has not actually walked to yet. A ghost says "this is
   where I am sending them" in the same breath as "they are not there". */
.token-marker.ghost .token-dot {
  background: rgba(234, 234, 223, 0.32);
  color: rgba(13, 15, 11, 0.8);
  box-shadow: 0 0 0 3.5px rgba(0, 0, 0, 0.45);
}

.token-marker.ghost .token-dot::after {
  content: "";
  position: absolute;
  inset: -2px;
  border: 1.5px dashed var(--rim, var(--job-idle));
  border-radius: 50%;
}

/* Whoever the next click on the map will be given to. The board used to keep
   this in a gold border on a list row, where it read as "selected" rather than
   as a promise about the next thing you do. */
.token-marker.up-next .token-dot {
  box-shadow:
    0 0 0 2px var(--rim, var(--job-idle)),
    0 0 0 3.5px rgba(0, 0, 0, 0.55),
    0 0 0 5.5px var(--accent);
}

/* The multi-day dial, drawn inside the piece so it stays correctly framed at
   every zoom — see the note in view/tokens.mjs. */
.token-marker .token-ring {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 26px;
  height: 26px;
  transform: translate(-50%, -50%);
}

.token-marker .token-ring circle {
  fill: none;
  stroke-width: 2;
}

.token-marker .token-ring-track {
  stroke: rgba(0, 0, 0, 0.55);
}

.token-marker .token-ring-fill {
  stroke: var(--rim, var(--job-idle));
  stroke-linecap: round;
  transform: rotate(-90deg);
  transform-origin: 50% 50%;
}

/* A standing order re-seats itself every dawn, so the piece is never picked up.
   The badge says so without a word. */
.token-marker .token-badge {
  position: absolute;
  right: -6px;
  bottom: -5px;
  display: grid;
  place-items: center;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: #0d0f0b;
  box-shadow: 0 0 0 1px var(--rim, var(--job-idle));
  color: var(--rim, var(--job-idle));
  font-size: 8px;
}

.token-marker .token-badge .icon {
  width: 8px;
  height: 8px;
  vertical-align: 0;
}

/* Positioned against the marker root, which MapLibre already absolutely
   positions — so no `position` override here to fight it. */
.token-marker .token-name {
  position: absolute;
  bottom: calc(100% + 5px);
  left: 50%;
  transform: translateX(-50%);
  white-space: nowrap;
  color: #eaeadf;
  font-size: 11px;
  font-weight: 600;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
}

.token-marker.ghost .token-name {
  color: var(--muted);
}

/* Dispatch, in three moments on one piece: the order goes solid and swells —
   it is a fact now, not a promise — settles, and then clears off the board,
   because what happens next is the person themselves walking out from home.
   Timed against COMMIT_MS in main.mjs.

   As everywhere else here, this animates a *child*: MapLibre writes the marker
   root's own transform on every frame of every pan. */
.token-marker.committing .token-dot {
  animation: token-commit 460ms ease-out forwards;
}

.token-marker.committing .token-name {
  animation: token-commit-name 460ms ease-out forwards;
}

@keyframes token-commit {
  0% {
    transform: scale(1);
    opacity: 1;
  }
  16% {
    transform: scale(1.24);
    opacity: 1;
  }
  40% {
    transform: scale(1);
    opacity: 1;
  }
  100% {
    transform: scale(0.9);
    opacity: 0;
  }
}

@keyframes token-commit-name {
  0%,
  40% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

/* The tail of a rack too long to lay out, drawn as one piece carrying the
   remainder. Offset plates behind the dot so it reads as a stack of pieces
   rather than as one bigger piece. */
.token-marker.token-cluster .token-dot {
  /* Positioned and lifted, because the plates below are absolutely positioned
     too and would otherwise paint straight over the count. */
  position: relative;
  z-index: 1;
  width: 20px;
  height: 20px;
  /* "+11" is three glyphs in a 20px circle, so this is a size down from the
     single initial a normal piece carries. */
  font-size: 9.5px;
  letter-spacing: -0.02em;
  background: #d7d5c6;
}

.token-marker.token-cluster::before,
.token-marker.token-cluster::after {
  content: "";
  position: absolute;
  z-index: 0;
  top: 50%;
  left: 50%;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: #23261d;
  box-shadow: 0 0 0 1.5px rgba(0, 0, 0, 0.5);
}

.token-marker.token-cluster::before {
  transform: translate(-50%, -50%) translate(3px, -3px);
}

.token-marker.token-cluster::after {
  transform: translate(-50%, -50%) translate(-3px, 3px);
}

/* MapLibre's own chrome, dressed for the board. */
.maplibregl-ctrl-group {
  background: rgba(15, 18, 12, 0.9);
  border: 1px solid var(--line);
}

.maplibregl-ctrl-group button + button {
  border-top: 1px solid var(--line);
}

.maplibregl-ctrl-group button .maplibregl-ctrl-icon {
  filter: invert(0.85);
}

.maplibregl-ctrl-attrib {
  background: rgba(15, 18, 12, 0.8) !important;
}

.maplibregl-ctrl-attrib a {
  color: var(--muted);
}

.topbar-playback-controls {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-left: auto;
  pointer-events: auto;
}

.topbar-playback-controls[hidden] {
  display: none;
}

.topbar-playback-controls.planning #speed {
  opacity: 0.58;
}

#map-hint {
  position: absolute;
  bottom: 12px;
  left: 14px;
  right: 14px;
  z-index: 3;
  color: var(--muted);
  font-size: 11px;
  pointer-events: none;
}

/* --- The log box ----------------------------------------------------------
   Short by default and tall on demand. Click-latched rather than hover-opened:
   this is something you read, and a panel that closes because the pointer
   drifted is not readable. */
#log-box {
  position: absolute;
  top: 12px;
  right: 14px;
  z-index: 7;
  width: 300px;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: rgba(13, 15, 11, 0.9);
  overflow: hidden;
  pointer-events: auto;
}

#log-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 5px 9px;
  border: none;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  background: transparent;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
}

#log-toggle .chev {
  transition: transform 120ms ease;
}

#log-box:not(.collapsed) #log-toggle .chev {
  transform: rotate(180deg);
}

#log-box #log {
  margin: 0;
  padding: 6px 9px;
  list-style: none;
  overflow-y: auto;
  max-height: 340px;
  transition: max-height 140ms ease;
}

/* Three lines rather than two: the collapsed box is a glance at the last thing
   that happened, and the last thing that happened is usually more than one
   sentence long. */
#log-box.collapsed #log {
  max-height: 66px;
}

/* --- The settlement cycler ------------------------------------------------
   The name of where you are, with the neighbours showing through greyed either
   side so the list reads as a ring rather than a dropdown. */
#settlement-bar {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 9px 10px;
  border-bottom: 1px solid var(--line);
  background: rgba(0, 0, 0, 0.2);
}

#settlement-bar[data-single="true"] button,
#settlement-bar[data-single="true"] .settlement-ghost,
#settlement-bar[data-single="true"] #settlement-count {
  display: none;
}

#settlement-bar button {
  padding: 2px 7px;
  border: none;
  background: transparent;
  color: var(--muted);
  font-size: 16px;
  line-height: 1;
}

#settlement-bar button:hover {
  color: var(--accent);
}

.settlement-names {
  flex: 1;
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 6px;
  min-width: 0;
}

#settlement-label {
  margin: 0;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The hint at what is either side. Deliberately allowed almost no room — it is
   a suggestion that there is more, not a second label to read. */
.settlement-ghost {
  flex: 0 1 auto;
  max-width: 52px;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: 10px;
  color: var(--muted);
  opacity: 0.45;
}

#settlement-count {
  font-size: 10px;
  color: var(--muted);
}

/* The standing order for this larder, sitting directly on top of the ledger it
   moves — so switching to planks and watching the food row stop growing is the
   explanation of what the control does. Shares the ledger's card treatment and
   loses its bottom radius, so the two read as one block hinged together. */
#settlement-priority {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  padding: 7px 10px;
  background: #11140d;
  border: 1px solid var(--line);
  border-bottom: none;
  border-radius: 5px 5px 0 0;
}

#settlement-priority:not([hidden]) + #ledger {
  border-radius: 0 0 5px 5px;
}

#settlement-priority[hidden] {
  display: none;
}

.priority-label {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
}

.priority-options {
  display: flex;
  gap: 4px;
}

.priority-option {
  display: grid;
  width: 30px;
  height: 30px;
  padding: 0;
  place-items: center;
  background: #11140d;
  border: 1px solid var(--line);
  border-radius: 5px;
  cursor: pointer;
  color: var(--muted);
}

.priority-option .resource-chit {
  pointer-events: none;
}

.priority-option:hover {
  color: var(--ink);
}

.priority-option.active {
  border-color: var(--accent);
  background: #1e2317;
  color: var(--ink);
}

/* What is inside a building doubles as the way to ask for one of them by name.
   The mark is the same one the readout already used — this only makes it look
   pressable, so the row reads as information first and a control second. */
.inside-pick {
  display: inline-flex;
  align-items: center;
  padding: 1px 4px;
  margin: -1px 0;
  background: none;
  border: 1px solid transparent;
  border-radius: 4px;
  cursor: pointer;
  color: inherit;
  font: inherit;
}

.inside-pick:hover {
  border-color: var(--line);
  background: #11140d;
}

.inside-pick.active {
  border-color: var(--accent);
  background: #1e2317;
}

/* A one-off, marked as one. The point of the tag is that the player can tell
   at a glance they changed this run and not the settlement's standing order. */
.once,
#tip-scavenge .cost.once {
  color: var(--accent);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 9px;
}

#panel {
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--panel);
  border-left: 1px solid var(--line);
}

#status {
  padding: 12px 14px;
  border-bottom: 1px solid var(--line);
}

/* Flex rather than three fixed columns. The stores are two boxes until
   medicine unlocks and three after, and a fixed grid left the empty third
   track sitting there as a gap the eye kept reading as a missing readout. */
.status-grid {
  display: flex;
  gap: 6px;
  margin-top: 8px;
}

/* Direct children only: the boxes are the grid's own cells, and anything
   nested inside one (the food hover card and its rows) must not inherit the
   cell's column layout, padding and border. */
.status-grid > div {
  display: flex;
  flex: 1 1 0;
  min-width: 0;
  flex-direction: column;
  align-items: center;
  padding: 6px 2px;
  background: #11140d;
  border: 1px solid var(--line);
  border-radius: 5px;
}

.status-grid > div.tutorial-hidden,
.base-row.tutorial-hidden {
  display: none;
}

/* The house, over the stores rather than among them. A full-width row on
   purpose: the grid below it is things you go out and get, and the condition of
   the place you sleep in is not one of them — sat in that grid it read as a
   fourth resource. */
.base-row {
  display: flex;
  align-items: baseline;
  gap: 7px;
  width: 100%;
  padding: 7px 9px;
  background: #11140d;
  border: 1px solid var(--line);
  border-radius: 5px;
  color: var(--ink);
  font-size: 12px;
  text-align: left;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.base-row > .icon {
  align-self: center;
  width: 15px;
  height: 15px;
}

/* The count keeps its own typography: a fraction is a number, not a word, and
   it has to line up with itself as it falls. */
.base-row #base-hp {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
}

/* The cue is the affordance as much as the advice, so it sits at the far end
   where a chevron would and stays quiet until there is something to do. */
.base-cue {
  margin-left: auto;
  color: var(--muted);
  font-size: 10px;
}

.base-row.good {
  color: var(--safe);
  border-color: #2f5a37;
}

/* Red rather than the walls' amber. Amber is a cost you are allowed to pay;
   an open door is the one state on this row that gets somebody killed. */
.base-row.bad {
  color: var(--danger);
  border-color: #6b2f2a;
}

.base-row.bad .base-cue {
  color: var(--danger);
}

.base-row.hurt #base-hp {
  color: var(--barricade);
}

.base-row:hover {
  border-color: var(--ink);
}

.status-grid > div > span {
  font-size: 16px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.status-grid > div > .icon {
  width: 16px;
  height: 16px;
  margin-bottom: 2px;
}

.status-grid .resource-chit {
  margin-bottom: 5px;
}

.status-grid small {
  color: var(--muted);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.status-grid.starving > div > span {
  color: var(--danger);
}

/* --- Emergency alerts ----------------------------------------------------- */
/* The county's broadcast, in the register of the thing that makes every phone
   in a valley scream at once. Institutional and terse: the format is the
   discipline, and it is why these need no comic voice. */

#alert-banner {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 42;
  width: min(720px, calc(100% - 32px));
  transform: translate(-50%, -50%);
  animation: alert-in 260ms ease-out;
}

#alert-callouts {
  position: fixed;
  inset: 0;
  z-index: 41;
  width: 100vw;
  height: 100vh;
  overflow: visible;
  pointer-events: none;
}

#alert-callouts line {
  stroke: var(--accent);
  stroke-width: 2;
  stroke-linecap: round;
  filter: drop-shadow(0 0 5px rgba(216, 189, 98, 0.45));
}

.alert-tutorial-target {
  position: relative;
  z-index: 43;
  border-color: var(--accent) !important;
  animation: alert-target-pulse 1800ms ease-in-out infinite;
  box-shadow:
    0 0 0 1px var(--accent),
    0 0 18px rgba(216, 189, 98, 0.36);
}

@keyframes alert-target-pulse {
  0%,
  100% {
    box-shadow:
      0 0 0 1px var(--accent),
      0 0 14px rgba(216, 189, 98, 0.28);
  }

  50% {
    box-shadow:
      0 0 0 2px var(--accent),
      0 0 24px rgba(216, 189, 98, 0.52);
  }
}

@media (prefers-reduced-motion: reduce) {
  .alert-tutorial-target {
    animation: none;
  }
}

#street-hint-callout.alert-tutorial-target {
  display: inline-block;
  padding: 2px 5px;
  border: 1px solid var(--accent);
  border-radius: 4px;
  background: rgba(17, 20, 13, 0.9);
  color: var(--ink);
}

@keyframes alert-in {
  from { opacity: 0; transform: translate(-50%, calc(-50% - 8px)); }
  to { opacity: 1; transform: translate(-50%, -50%); }
}

.alert-card {
  padding: 22px 24px;
  border: 2px solid #6d5a1d;
  border-left: 8px solid var(--accent);
  border-radius: 6px;
  background: rgba(28, 24, 10, 0.97);
  box-shadow: 0 18px 48px rgba(0, 0, 0, 0.56);
}

.alert-heading {
  color: var(--accent);
  font-size: 17px;
  font-weight: 800;
  letter-spacing: 0.08em;
}

.alert-lines {
  margin-top: 14px;
  color: var(--ink);
  font-size: 23px;
  line-height: 1.25;
}

.alert-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 18px;
}

.alert-source {
  color: var(--muted);
  font-size: 13px;
  letter-spacing: 0.06em;
}

#alert-dismiss {
  padding: 8px 18px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: #11140d;
  color: var(--ink);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}

#alert-dismiss:hover {
  border-color: var(--accent);
}

@media (max-width: 520px) {
  .alert-card {
    padding: 16px 17px;
  }

  .alert-heading {
    font-size: 14px;
  }

  .alert-lines {
    font-size: 17px;
    line-height: 1.28;
  }

  .alert-source,
  #alert-dismiss {
    font-size: 12px;
  }
}

/* The log, shown in the report modal: every instruction still standing. */
.alert-log-entry {
  padding: 8px 0;
  border-bottom: 1px solid var(--line);
  list-style: none;
}

.alert-log-entry:last-child {
  border-bottom: 0;
}

.alert-log-entry .alert-heading {
  font-size: 10px;
}

.alert-log-entry .alert-lines {
  margin-top: 4px;
  font-size: 12px;
}

/* --- Days of food, and the sum behind it --------------------------------- */

.status-grid .food-box {
  position: relative;
  cursor: help;
}

.food-days {
  margin-top: 2px;
  color: var(--muted);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
}

.status-grid.starving .food-days {
  color: var(--danger);
}

/* A panel hover card, deliberately not the map tooltip (#tip): that one is
   anchored to a coordinate and re-positions on every camera move. */
.hover-card {
  position: absolute;
  top: calc(100% + 6px);
  left: 50%;
  z-index: 30;
  display: grid;
  gap: 3px;
  width: max-content;
  max-width: 230px;
  padding: 7px 9px;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: rgba(12, 15, 9, 0.97);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
  opacity: 0;
  transform: translate(-50%, -3px);
  transition: opacity 120ms ease, transform 120ms ease;
  pointer-events: none;
}

.food-box:hover .hover-card,
.food-box:focus-visible .hover-card,
#medicine-box:hover .hover-card,
#medicine-box:focus-visible .hover-card {
  opacity: 1;
  transform: translate(-50%, 0);
}

/* --- Medicine ------------------------------------------------------------- */

.status-grid #medicine-box {
  position: relative;
  cursor: help;
}

.status-grid #medicine-box > span {
  color: var(--medicine);
}

/* Fewer doses than there are people here waiting on one. The only medicine
   state worth colouring, because it is the only one that is a decision: it
   means somebody is not getting treated today. */
.status-grid #medicine-box.short > span {
  color: var(--injured);
}

.hover-card .card-row.bad b {
  color: var(--danger);
}

.hover-card .card-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  font-size: 11px;
  line-height: 1.35;
  text-transform: none;
  letter-spacing: 0;
}

.hover-card .card-row span {
  font-size: 11px;
  font-weight: 400;
}

.hover-card .card-row b {
  font-variant-numeric: tabular-nums;
}

.hover-card .card-row.total {
  margin-top: 2px;
  padding-top: 4px;
  border-top: 1px solid var(--line);
}

.hover-card .card-note {
  color: var(--muted);
  font-size: 10px;
  line-height: 1.35;
  text-transform: none;
  letter-spacing: 0;
}

.block {
  padding: 10px 14px;
  border-bottom: 1px solid var(--line);
  min-height: 0;
}

.block.grow {
  flex: 1;
  overflow-y: auto;
  border-bottom: none;
}

.block h2 {
  margin: 0 0 6px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
}

.block h2 small {
  text-transform: none;
  letter-spacing: 0;
  color: var(--muted);
}

.hint {
  margin: 0 0 8px;
  color: var(--muted);
  font-size: 11px;
}

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Each settlement's block on the dawn card, once there is more than one. */
.summary-place {
  margin: 8px 0 3px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

.summary-place:first-child {
  margin-top: 0;
}

/* Kept quiet until it has something to say, and quiet after that: the count is
   the point, not the button. */
#memorial {
  float: right;
  padding: 0 6px;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  cursor: pointer;
}

#memorial:hover {
  color: var(--ink);
  border-color: #3d442f;
}

#memorial[hidden] {
  display: none;
}

.memorial-entry {
  padding: 6px 0;
  border-bottom: 1px solid var(--line);
}

.memorial-name {
  font-weight: 600;
  color: var(--ink);
}

.memorial-how {
  color: var(--muted);
  font-size: 11px;
}

/* --- The roster: one row per person, carrying their whole morning ---------- */

/* The per-settlement heading that used to sit inside this list is gone — the
   list is one settlement now, and the cycler above it is the heading. */

/* A settlement nobody has moved into yet. Not a row about a person, so it does
   not wear a row's card — it is the list saying why it is empty. */
#roster li.roster-empty {
  display: block;
  background: none;
  border: 1px dashed var(--line);
  padding: 10px 8px;
  text-align: center;
  line-height: 1.5;
}

#roster li.roster-empty .tip-detail {
  display: block;
  margin-top: 3px;
}

#roster li {
  padding: 6px 8px;
  margin-bottom: 4px;
  background: #11140d;
  border: 1px solid var(--line);
  border-radius: 5px;
  cursor: pointer;
}

#roster li:hover {
  border-color: #3d442f;
}

#roster li.staffed {
  border-color: var(--safe);
}

#roster li.selected {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}

/* Deliberate rest reads as quiet rather than as a warning: it is a choice that
   has been made, not one still outstanding. */
#roster li.skipping {
  opacity: 0.72;
}

#roster li.locked {
  cursor: not-allowed;
  opacity: 0.75;
}

#roster li.dead {
  opacity: 0.4;
  cursor: default;
  text-decoration: line-through;
}

#roster .row-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-height: 18px;
}

#roster .who {
  font-weight: 600;
}

/* --- Health and traits on the roster ---------------------------------------- */

/* Both chips sit between the name and the verbs, and take the middle of the row
   rather than the end: they belong to the person, not to the actions. Most rows
   carry neither, so an ordinary roster looks exactly as it did before either
   feature landed — which is what makes a row that carries one worth reading.
   The wrapper owns the auto margin, so the pair can be one, the other or both
   without the row re-balancing itself. */
#roster .row-chips {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-right: auto;
  min-width: 0;
}

.health-chip {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 1px 6px;
  border: 1px solid transparent;
  border-radius: 999px;
  font-size: 10px;
  letter-spacing: 0.02em;
  white-space: nowrap;
  cursor: help;
}

.health-chip .icon {
  width: 11px;
  height: 11px;
}

.health-chip b {
  font-variant-numeric: tabular-nums;
  opacity: 0.75;
}

.health-chip.injured {
  border-color: color-mix(in srgb, var(--injured) 45%, transparent);
  background: color-mix(in srgb, var(--injured) 14%, transparent);
  color: var(--injured);
}

.health-chip.critical {
  border-color: color-mix(in srgb, var(--critical) 55%, transparent);
  background: color-mix(in srgb, var(--critical) 18%, transparent);
  color: var(--critical);
}

/* A person on a countdown to dying should not sit still on the list. Slow
   enough to read as a pulse rather than an alarm — the row is permanent until
   it resolves one way or the other, and a fast blink would be unbearable. */
@keyframes health-critical-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.55; }
}

.health-chip.critical .icon {
  animation: health-critical-pulse 2.4s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .health-chip.critical .icon {
    animation: none;
  }
}

/* The explanation. Hover or keyboard focus, same as the top bar's cards — the
   status has to be able to say what it means, because none of these four words
   carry their own rules. */
.health-card {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 30;
  display: grid;
  gap: 4px;
  width: max-content;
  max-width: 240px;
  padding: 7px 9px;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: rgba(12, 15, 9, 0.97);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
  color: var(--ink);
  font-size: 11px;
  font-weight: 400;
  line-height: 1.4;
  letter-spacing: 0;
  text-transform: none;
  white-space: normal;
  opacity: 0;
  transform: translateY(-3px);
  transition: opacity 120ms ease, transform 120ms ease;
  pointer-events: none;
}

.health-chip:hover .health-card,
.health-chip:focus-visible .health-card {
  opacity: 1;
  transform: translateY(0);
}

.health-card strong {
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.health-card .health-clock {
  padding-top: 4px;
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: 10px;
}

/* --- Trait pills ------------------------------------------------------------ */

/* Quieter than the health chip on purpose. A trait is a standing fact about
   somebody and health is a thing happening to them right now, so when a builder
   is also bleeding the row has to read the injury first: outlined, no fill, and
   the word carries the colour rather than a wash behind it. */
.trait-chip {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 1px 6px;
  border: 1px solid color-mix(in srgb, var(--trait-ink) 40%, transparent);
  border-radius: 999px;
  color: var(--trait-ink);
  font-size: 10px;
  letter-spacing: 0.02em;
  white-space: nowrap;
  cursor: help;
}

.trait-chip .icon {
  width: 11px;
  height: 11px;
}

/* Glyph only, for rows that are also carrying a health chip. Squarer than the
   full pill so it reads as a badge rather than a word that failed to load. */
.trait-chip.compact {
  padding: 1px 4px;
}

.trait-chip.compact .trait-word {
  display: none;
}

.trait-chip.construction {
  --trait-ink: var(--barricade);
}

.trait-chip.medical {
  --trait-ink: var(--medicine);
}

.trait-chip.electrical {
  --trait-ink: var(--accent);
}

/* A trait the game does not use yet is still shown — the player found a real
   person and will want them later — but it is dimmed, so it never competes with
   the two that change what today's board can do. */
.trait-chip.inert {
  opacity: 0.55;
}

/* Same card as health's, and deliberately not a shared class: the two are
   positioned off different chips and will drift apart the first time one of
   them needs a second row. */
.trait-card {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 30;
  display: grid;
  gap: 4px;
  width: max-content;
  max-width: 240px;
  padding: 7px 9px;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: rgba(12, 15, 9, 0.97);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
  color: var(--ink);
  font-size: 11px;
  font-weight: 400;
  line-height: 1.4;
  letter-spacing: 0;
  text-transform: none;
  white-space: normal;
  opacity: 0;
  transform: translateY(-3px);
  transition: opacity 120ms ease, transform 120ms ease;
  pointer-events: none;
}

.trait-chip:hover .trait-card,
.trait-chip:focus-visible .trait-card {
  opacity: 1;
  transform: translateY(0);
}

.trait-card strong {
  color: var(--trait-ink);
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.trait-card .trait-inert-note {
  padding-top: 4px;
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: 10px;
}

/* The row itself, so the state is readable without finding the chip. Kept to a
   left edge rather than a background wash: the roster already uses background
   for "selected" and "staffed", and health has to stack with both. */
#roster li.hurt-injured {
  box-shadow: inset 2px 0 0 color-mix(in srgb, var(--injured) 70%, transparent);
}

#roster li.hurt-critical {
  box-shadow: inset 2px 0 0 var(--critical);
}

/* Treatment the player cannot perform yet. Shown rather than hidden, because
   the missing piece — a survivor with medical training — is a thing they will
   eventually be able to go and find, and an absent button teaches nothing. */
.row-action.medicine {
  color: var(--medicine);
}

.row-action.unavailable {
  color: var(--muted);
  opacity: 0.5;
  cursor: not-allowed;
}

.row-action.unavailable:hover,
.row-action.unavailable:focus-visible {
  background: none;
  color: var(--muted);
}

.row-action.medicine .row-tip {
  white-space: normal;
  width: max-content;
  max-width: 200px;
  text-align: right;
}

#roster .row-job {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 1px;
  color: var(--muted);
  font-size: 11px;
}

/* Nobody has been given anything to do, which is a whole person-day about to be
   spent on nothing — the one roster state worth colouring. */
#roster .tip-state.idle {
  color: var(--barricade);
}

#roster li .kind {
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  margin-right: 2px;
}

#roster li .kind.scavenge {
  background: var(--job-scavenge);
}

#roster li .kind.barricade {
  background: var(--job-barricade);
}

#roster li .kind.repair {
  background: var(--job-repair);
}

/* Held ground, matching the green the block is painted in on the map. */
#roster li .kind.farm {
  background: var(--job-farm);
}

#roster li .kind.scout {
  background: var(--job-scout);
}

#roster li .kind.rescue {
  background: var(--job-rescue);
}

#roster li .kind.errand {
  background: var(--job-errand);
}

#roster li .kind.delivery {
  background: var(--job-delivery);
}

#roster li .kind.outpost {
  background: var(--job-outpost);
}

#roster .yield {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--muted);
  font-style: normal;
}

/* --- Held ground ----------------------------------------------------------- */

#held-ground {
  list-style: none;
  margin: 0;
  padding: 0;
}

#held-ground li {
  padding: 6px 8px;
  border: 1px solid var(--line);
  border-radius: 4px;
  margin-bottom: 4px;
  cursor: pointer;
}

#held-ground li:hover {
  border-color: var(--accent);
}

/* Ground you hold with nobody working it is the one thing this list exists to
   catch, so it is the only row that raises its voice. */
#held-ground li.idle-block {
  border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
}

#held-ground .row-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
}

#held-ground .row-job {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 3px;
  font-size: 11px;
  color: var(--muted);
}

#held-ground .yield {
  margin-left: auto;
  font-style: normal;
}

/* The bag on a standing farm order: a number you can click, so it reads as part
   of the row rather than as a button bolted onto it. The dotted underline is the
   only hint that it is live — a full button here would compete with Skip and
   Cancel, which are the row's real verbs. */
#roster .repack {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0;
  background: none;
  border: 0;
  border-bottom: 1px dotted var(--line);
  font: inherit;
  color: inherit;
  cursor: pointer;
}

#roster .repack:hover,
#roster .repack:focus-visible {
  border-bottom-color: var(--accent);
}

/* --- Row verbs ------------------------------------------------------------- */

#roster .row-actions {
  display: flex;
  align-items: center;
  gap: 2px;
  flex-shrink: 0;
}

.row-action {
  position: relative;
  display: grid;
  place-items: center;
  width: 20px;
  height: 20px;
  padding: 0;
  border: 0;
  border-radius: 4px;
  background: none;
  color: var(--muted);
  cursor: pointer;
}

.row-action .icon {
  width: 11px;
  height: 11px;
}

.row-action:hover,
.row-action:focus-visible {
  background: #1e2317;
  color: var(--ink);
  outline: none;
}

.row-action.cancel:hover,
.row-action.cancel:focus-visible {
  color: var(--danger);
}

/* The word behind the glyph. Delayed rather than instant: these sit in a list
   you scan down, and a label that fires under the cursor on the way past is
   noise. Leaving is not delayed — only arriving. */
.row-tip {
  position: absolute;
  right: calc(100% + 4px);
  top: 50%;
  transform: translateY(-50%);
  padding: 2px 6px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: #1a1e13;
  color: var(--ink);
  font-size: 10px;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity 90ms ease;
  z-index: 3;
}

.row-action:hover .row-tip,
.row-action:focus-visible .row-tip {
  opacity: 1;
  transition-delay: 500ms;
}

.stat.standing .icon,
.stat.standing .stat-num {
  color: var(--muted);
}

.stat.standing .icon {
  width: 11px;
  height: 11px;
}

#controls {
  padding: 10px 14px;
  border-bottom: 1px solid var(--line);
}

#ledger {
  margin-bottom: 8px;
  padding: 8px 10px;
  background: #11140d;
  border: 1px solid var(--line);
  border-radius: 5px;
  font-size: 11px;
}

.ledger-guide {
  display: grid;
  grid-template-columns: repeat(3, minmax(34px, max-content)) minmax(34px, 1fr);
  column-gap: 10px;
  align-items: center;
  padding: 0 0 4px;
  color: var(--muted-2);
  font-size: 9px;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.ledger-guide strong {
  justify-self: end;
  color: var(--muted-2);
}

.ledger-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 3px 0;
  color: var(--muted);
}

.ledger-row:not(.rule) {
  display: grid;
  grid-template-columns: repeat(3, minmax(34px, max-content)) minmax(34px, 1fr);
  column-gap: 10px;
}

.ledger-row.rule {
  margin-top: 3px;
  padding-top: 6px;
  border-top: 1px solid var(--line);
}

.ledger-terms {
  display: flex;
  align-items: center;
  gap: 10px;
}

.ledger-row:not(.rule) .ledger-terms {
  display: contents;
}

.ledger-slot {
  display: flex;
  align-items: center;
  min-width: 34px;
}

.ledger-slot-night {
  gap: 6px;
}

.ledger-row strong {
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.ledger-row:not(.rule) strong {
  justify-self: end;
}

.ledger-row strong.good {
  color: var(--safe);
}

.ledger-row strong.bad {
  color: var(--danger);
}

/* The one thing on this row that is not arithmetic: somebody dies if you press
   the button. It gets a pulse because a colour alone reads as just another
   number in a panel full of them. */
.stat.starves {
  animation: starve-pulse 1.6s ease-in-out infinite;
}

@keyframes starve-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.45; }
}

@media (prefers-reduced-motion: reduce) {
  .stat.starves { animation: none; }
}

/* The building tooltip: what it is on hover, what you can do with it on click. */
#tip {
  position: absolute;
  z-index: 5;
  min-width: 190px;
  max-width: 280px;
  padding: 8px 10px;
  transform: translate(-50%, -100%);
  background: rgba(17, 20, 13, 0.97);
  border: 1px solid var(--line);
  border-radius: 6px;
  pointer-events: none;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
}

#tip.open {
  pointer-events: auto;
  border-color: var(--accent);
}

/* A refused click: the same tooltip in the same place, saying no. Red rather
   than the accent gold, because gold means "here is what you can do here" and
   this is the opposite of that. */
#tip.refused {
  border-color: var(--danger);
}

#tip.refused #tip-title {
  color: var(--danger);
}

#tip[hidden] {
  display: none;
}

#tip-title {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  font-weight: 700;
  font-size: 12px;
  margin-bottom: 3px;
}

.tip-title-main {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}

.tip-title-main .icon {
  color: var(--muted);
}

.tip-kind {
  color: var(--muted);
  font-weight: 400;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

#tip-body {
  color: var(--muted);
  font-size: 11px;
  line-height: 1.5;
}

.tip-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 1px 0;
}

/* Why the card opened, when the player did not open it. Sits above the numbers
   and wraps, because it is a sentence rather than a readout. */
.tip-row.tip-note {
  display: block;
  margin-bottom: 4px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--line);
  color: var(--danger);
  font-size: 11px;
  line-height: 1.35;
}

#tip-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin-top: 8px;
}

#tip-actions[hidden] {
  display: none;
}

/* Sized to what they say, not to an equal share of the row.

   `flex: 1` cut the bar into equal columns whatever was in them, so a card
   offering three verbs gave "Shore up the door" exactly the same width as
   "Carry here" and wrapped it down four lines beside a one-line neighbour.
   Growing from a floor instead lets a long label take the room it needs, and
   letting the bar wrap puts a third verb on a second row rather than crushing
   all three onto one. */
#tip-actions button {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  flex: 1 1 auto;
  min-width: 104px;
  padding: 5px 7px;
  font-size: 11px;
}

/* `display: flex` above is an author rule, so it beats the browser's own
   [hidden] { display: none } and every button stays on screen no matter how the
   attribute is set. The container two rules up already needed exactly this
   override; the buttons need it for the same reason. */
#tip-actions button[hidden] {
  display: none;
}

#tip-actions button:disabled {
  opacity: 0.4;
  cursor: default;
  border-color: var(--line);
}

/* Offered, but with the consequence on the face of it — moving somebody into
   an empty larder starves them that night. */
#tip-actions button.warn {
  border-color: var(--danger);
}

#tip-actions button .cost {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  margin-left: 3px;
  padding: 1px 5px;
  background: rgba(0, 0, 0, 0.25);
  border-radius: 3px;
  font-size: 10px;
  color: var(--muted);
}

button {
  font: inherit;
  color: var(--ink);
  background: #1e2317;
  border: 1px solid var(--line);
  border-radius: 5px;
  padding: 6px 10px;
  cursor: pointer;
}

button:hover {
  border-color: var(--accent);
}

#go {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 8px;
  width: 100%;
  padding: 10px;
  font-weight: 700;
  letter-spacing: 0.03em;
  background: #24301c;
  border-color: #3c5130;
}

/* Unspent hands, priced on the button that spends the day. Amber rather than
   red: it is a cost, not a refusal — you are still allowed to press it. */
#go.has-idle {
  border-color: #6b4f24;
}

.go-idle {
  color: var(--barricade);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0;
}

/* Red, and the button's frame with it: this one is not a price you may choose
   to pay. Pressing it opens the card that fixes it instead of spending the
   day — see unboardedHome in ui/tips.mjs. */
#go.blocked {
  border-color: #6b2f2a;
}

.go-blocked {
  color: var(--danger);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0;
}

#go:disabled {
  opacity: 0.5;
  cursor: default;
}

/* Explicit, because the `display: flex` below beats the browser's own rule for
   [hidden] and the night hides this control outright. Same reason
   .topbar-playback-controls has one. */
#speed[hidden] {
  display: none;
}

#speed {
  display: flex;
  height: 34px;
  gap: 0;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: rgba(13, 15, 11, 0.78);
}

#speed button {
  min-width: 42px;
  padding: 0 8px;
  border: 0;
  border-radius: 0;
  border-right: 1px solid var(--line);
  background: transparent;
  font-size: 12px;
  font-weight: 800;
  line-height: 1;
}

#speed button:disabled {
  cursor: default;
}

#speed button:last-child {
  border-right: 0;
}

#speed button.active {
  background: rgba(224, 183, 91, 0.16);
  color: var(--accent);
}

/* The one live control over a night. Deliberately not dimmed with the speed
   buttons beside it (see .topbar-playback-controls.planning) — those are off,
   this one works, and dimming both would say the same thing about two controls
   that mean opposite things. */
#skip-night {
  display: flex;
  align-items: center;
  gap: 6px;
  height: 34px;
  padding: 0 10px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: rgba(13, 15, 11, 0.78);
  color: var(--muted);
  font-size: 12px;
  font-weight: 800;
  line-height: 1;
  white-space: nowrap;
}

#skip-night[hidden] {
  display: none;
}

#skip-night:hover {
  border-color: var(--accent);
  color: var(--accent);
}

#skip-night kbd {
  padding: 2px 5px;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: rgba(255, 255, 255, 0.05);
  font: inherit;
  font-size: 10px;
  font-weight: 700;
  color: var(--muted);
}

#log li {
  padding: 4px 0;
  border-bottom: 1px solid #1c2016;
  color: var(--muted);
  font-size: 11px;
}

#log li.day-break {
  color: var(--accent);
  font-weight: 700;
  border-bottom: none;
  padding-top: 10px;
}

#log li.bad {
  color: var(--danger);
}

#log li.good {
  color: var(--safe);
}

#report {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  background: rgba(8, 10, 7, 0.72);
}

#report[hidden] {
  display: none;
}

.report-card {
  width: min(460px, 90vw);
  max-height: 76vh;
  overflow-y: auto;
  padding: 20px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 8px;
}

.report-card h2 {
  margin: 0 0 12px;
  font-size: 18px;
}

/* One .ledger-row per store, each reading `+in −out → after`. The card answers
   the commit ledger, so it borrows the ledger's rows outright rather than
   restating them in a second set of nearly-identical rules. */
.report-summary {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding-bottom: 12px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--line);
  font-size: 15px;
}

.report-summary:empty {
  display: none;
}

.report-summary .icon {
  width: 15px;
  height: 15px;
}

.report-summary .resource-chit-sm .icon {
  width: 17px;
  height: 17px;
}

.report-card li {
  padding: 6px 0;
  border-bottom: 1px solid #1c2016;
}

.report-card li.bad {
  color: var(--danger);
}

.report-card li.good {
  color: var(--safe);
}

#report-close {
  margin-top: 16px;
  width: 100%;
  padding: 10px;
  font-weight: 700;
}

/* --- Dev tools ------------------------------------------------------------- */

.dev-tools-modal {
  position: fixed;
  inset: 0;
  z-index: 220;
  display: grid;
  place-items: center;
  padding: min(5vh, 42px) min(4vw, 42px);
  background: rgba(5, 6, 4, 0.78);
}

.dev-tools-modal[hidden] {
  display: none;
}

.dev-tools-panel {
  width: min(880px, 100%);
  max-height: 92vh;
  display: grid;
  grid-template-rows: auto auto auto minmax(0, 1fr) auto;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.6);
  overflow: hidden;
}

/* Big, red, impossible to miss: this panel edits live numbers with no undo. */
.dev-tools-warning {
  padding: 10px 16px;
  background: rgba(193, 68, 58, 0.22);
  border-bottom: 2px solid var(--danger);
  color: #ff5548;
  font-size: 20px;
  font-weight: 900;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-align: center;
}

.dev-tools-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--line);
}

.dev-tools-head h2 {
  margin: 0;
  font-size: 15px;
  color: var(--ink);
}

.dev-tools-head button {
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  margin: 0;
  padding: 0;
  font-size: 18px;
  line-height: 1;
}

.dev-tools-tabs {
  display: flex;
  gap: 4px;
  padding: 8px 12px;
  border-bottom: 1px solid var(--line);
  background: rgba(0, 0, 0, 0.2);
}

.dev-tools-tab {
  min-height: 30px;
  margin: 0;
  border: 0;
  border-radius: 5px;
  background: transparent;
  color: var(--muted);
  font-weight: 800;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.dev-tools-tab.active {
  color: #10130d;
  background: var(--accent);
}

.dev-tools-body {
  overflow: auto;
  padding: 14px 16px 18px;
}

.dev-tools-section[hidden] {
  display: none;
}

.dev-setting-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 10px;
}

.dev-setting {
  display: grid;
  gap: 7px;
  min-width: 0;
  padding: 10px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: rgba(0, 0, 0, 0.22);
}

.dev-setting label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  color: var(--ink);
  font-weight: 700;
  font-size: 12.5px;
}

.dev-setting label span {
  color: var(--accent);
  font-size: 11px;
  font-weight: 700;
  white-space: nowrap;
}

.dev-setting input[type="range"] {
  width: 100%;
  accent-color: var(--accent);
}

.dev-setting input[type="number"] {
  width: 100%;
  height: 32px;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: var(--bg);
  color: var(--ink);
  padding: 5px 8px;
}

.dev-setting .setting-pair {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 70px;
  gap: 8px;
  align-items: center;
}

.dev-setting-plain input[type="number"] {
  height: 34px;
}

/* A switch rather than a dial: the label sits beside its box instead of being
   pushed apart from a readout, which is what .dev-setting label does for the
   slider rows. */
.dev-setting-toggle label {
  justify-content: flex-start;
  align-items: center;
  gap: 9px;
  cursor: pointer;
}

.dev-setting-toggle input[type="checkbox"] {
  width: 15px;
  height: 15px;
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}

.dev-survivors-actions {
  margin-bottom: 12px;
}

.dev-survivor-list {
  display: grid;
  gap: 6px;
  list-style: none;
  margin: 0;
  padding: 0;
}

.dev-survivor-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: rgba(0, 0, 0, 0.22);
  color: var(--ink);
  font-size: 13px;
}

.dev-survivor-row.dead {
  color: var(--muted);
}

/* The name takes the slack so the two dropdowns line up down the list rather
   than each row placing them wherever its name ends. */
.dev-survivor-row > span {
  flex: 1;
  min-width: 0;
}

.dev-survivor-row button {
  color: var(--danger);
  border-color: var(--danger);
  padding: 4px 10px;
  font-size: 11px;
}

.dev-tools-foot {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 12px 16px;
  border-top: 1px solid var(--line);
  background: rgba(0, 0, 0, 0.2);
}

.dev-tools-foot button {
  margin: 0;
}
