/* ==========================================================================
   Evrima Portal - phosphor-terminal design system
   One stylesheet, no build step, no framework. Dark only, by design; there is
   no light theme and no prefers-color-scheme block. Every colour is explicit.

   THE FOUR RULES THIS FILE IS BUILT ON
   ---------------------------------------------------------------------------
   1. CORNERS ARE SQUARE. An instrument panel has no rounded boxes, so
      --radius is 0 and stays 0. The only curved things on the page are the
      status lamps (.dot, .chip__dot), because a lamp is round and a box is
      not. Nothing else gets a radius - not cards, not chips, not buttons.
   2. UPPERCASE MEANS "THIS IS THE NAME OF A VALUE, NOT THE VALUE".
      Exactly five rules set it: .stat__label, .kv__k, .label, .table th and
      .flash__title. Headings are sentence case in the display face. If you
      find yourself uppercasing a sixth thing, you are decorating.
   3. COLOUR IS INFORMATION. Green carries structure and healthy state; amber
      is attention and food; red is failure and predation; aqua is water;
      violet means "derived, not measured". Prose is --text, never --phos
      (AGENTS.md 4). A hue that means nothing does not go on the page.
   4. DENSE IN THE DATA, GENEROUS AT THE HEADINGS. Tables and key/value rows
      run tight and tabular; page and section titles get room and scale.

   COMPONENT INDEX - the vocabulary sibling pages code against
   ---------------------------------------------------------------------------
   Shell      .site-header .site-header__inner .wordmark .wordmark__mark
              .nav .nav__link (.is-active) .conn .conn__item .conn__label
              .account .account__avatar .page .page-head .page-title
              .page-sub .site-footer .site-footer__inner .skip-link
   Layout     .stack (--tight --loose) .row (--tight --wrap) .spread
              .grid (--2 --3 --4) .sidebar-layout .divider .grow .sr-only
   Surface    .card (--ok --warn --crit --flush) .card__head .card__title
              .card__meta .card__body .card__foot .card__actions
              .panel .section-title
   Data       .stat-grid .stat (--ok --warn --crit) .stat__label .stat__value
              .stat__unit .stat__note
              .table-wrap .table (--compact --wide) th td
              .num .data .kv .kv__k .kv__v
              .meter .meter__fill (--warn --crit) .trunc
   State      .chip (--ok --warn --crit --info --idle --derived) .chip__dot
              .dot (--ok --warn --crit --idle) .badge (--derived) .pill-row
              [data-diet] [data-intent]  - semantic hooks, see section 7
   Controls   .btn (--primary --danger --ghost --sm --block) .btn-row
              .field (--error) .label .input .select .textarea .hint
              .field-error .form-grid .check .check__box
   Feedback   .empty .empty__title .empty__body .empty__action
              .flash (--ok --warn --crit) .flash__title
              .toasts .toast (--ok --warn --crit) .toast__body .toast__close
              .htmx-indicator .is-busy .skeleton
   Utility    .muted .strong .accent .is-derived .nowrap .mt-0 .mb-0 .prose
   Ambience   .fx-backdrop .fx-rain .fx-sweep .fx-overlay  - mounted once in
              base.html, driven by static/fx.js, killed by html.fx-off
              .console .console__screen .console__lines .console__line
              (.is-ok .is-warn .is-crit .is-dim .is-derived)
              .console__time .console__tag .console__msg .console__caret
   Map        .map .map-art .map-raster .map-scale
              .map-marker .map-dot (--leader --player --self)
              .map-zone (--major) .map-zone__name
              .map-water .map-water__name
              .map-pop .map-pop__title .map-pop__row .map-pop__k .map-pop__v
              button.chip  - the diet filter, /map only, see 11.
              Markers read --diet from the same section 7 hook as the tables
   ---------------------------------------------------------------------------
   Sibling spacing is done with flex/grid `gap` (.stack, .row, .grid), not with
   per-element margins. Modifiers are always `.block.block--mod`, one class more
   specific than the base, so nothing cancels out.
   ========================================================================== */

/* --------------------------------------------------------------------------
   0. Faces - local files, no third-party origin
   -------------------------------------------------------------------------- */
/* These three used to arrive from a third-party CSS host on every page, /mod
   and /audit included. They are served from static/fonts/ now (SR-13): one
   origin for the whole document, `font-src 'self'` with no exception, and the
   portal keeps its typeface on a LAN with no internet. Bytes, hashes, licence
   and the recipe for refetching are in static/fonts/SOURCE.txt.

   src URLs are relative to this stylesheet, so /static lives in one place.

   `font-weight: 400 500` is a variable file's own range, not a hint to
   synthesise: Plex and JetBrains each answer both weights from one file. The
   unicode-range values are the css2 response's, so latin-ext is fetched only
   for a name that needs it, and anything outside both ranges falls back to the
   stack in section 1 - as everything does while a face loads (`swap`), and if
   a file ever goes missing.

   Two rules ask for 700, .flash__title and .map-zone--major .map-zone__name.
   No 700 file is loaded and the browser synthesises it, which is exactly what
   the remote stylesheet did. */

@font-face {
  font-family: "Martian Mono";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("fonts/martian-mono-600-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Martian Mono";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("fonts/martian-mono-600-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url("fonts/jetbrains-mono-400-500-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url("fonts/jetbrains-mono-400-500-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url("fonts/ibm-plex-sans-400-500-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url("fonts/ibm-plex-sans-400-500-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* No rule in this file sets font-style: italic. These two are here so an <em>
   renders in Plex instead of a sloped fake; nothing fetches them otherwise. */
@font-face {
  font-family: "IBM Plex Sans";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/ibm-plex-sans-italic-400-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "IBM Plex Sans";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/ibm-plex-sans-italic-400-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */
:root {
  /* Ground and surfaces - near-black biased green, never pure #000. */
  --ground:      #0A0F0C;
  --surface:     #111815;
  --surface-2:   #16201B;
  --rule:        #1E2B24;
  --rule-bright: #2A6B47;

  /* Phosphor - structure, healthy state, herbivores. 11.4:1 on --ground. */
  --phos:        #4FE08C;
  --phos-dim:    #2A6B47;

  --text:        #C8D5CD;
  --text-strong: #E6F0EA;
  --text-muted:  #7A8C82;

  /* Attention and food (omnivore, foraging intents). 8.9:1 on --ground. */
  --warn:        #E0A44F;
  /* Failure and predation (carnivore, encounter intents). 5.8:1. */
  --crit:        #E06666;

  /* Water - piscivores, water-seeking behaviour, thirst. 9.9:1 on --ground,
     9.2:1 on --surface, 8.5:1 on --surface-2. Same value family as --phos,
     rotated to cyan, so it sits beside the green rather than fighting it. */
  --aqua:        #4FC8E0;

  /* Derived, not measured. The two figures on this site that are inferred
     rather than read: the life stage banded off growth, and the HUD-scaled
     coordinates whose axis mapping is still unverified. 7.0:1 on --ground,
     6.5:1 on --surface, 6.0:1 on --surface-2. */
  --violet:      #A98CE6;
  --violet-dim:  #4A3D6B;

  /* Washes: the tokens above at low alpha, for fills and glows. */
  --phos-wash:   rgba(79, 224, 140, 0.10);
  --phos-wash-2: rgba(79, 224, 140, 0.18);
  --warn-wash:   rgba(224, 164, 79, 0.10);
  --crit-wash:   rgba(224, 102, 102, 0.10);
  --crit-wash-2: rgba(224, 102, 102, 0.20);
  --aqua-wash:   rgba(79, 200, 224, 0.10);
  --violet-wash: rgba(169, 140, 230, 0.10);

  --font-display: "Martian Mono", ui-monospace, "Cascadia Mono", "Segoe UI Mono", Consolas, monospace;
  --font-data:    "JetBrains Mono", ui-monospace, "Cascadia Mono", Consolas, "Courier New", monospace;
  --font-prose:   "IBM Plex Sans", "Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;

  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-7: 48px;

  /* Rule 1. These stay 0. They exist so a sibling page that reaches for
     `var(--radius)` out of habit still gets a square corner. */
  --radius:    0;
  --radius-lg: 0;

  --header-h:  52px;
  --page-max:  1220px;

  --shadow-1: 0 1px 0 rgba(0, 0, 0, 0.6);
  --shadow-2: 0 8px 24px rgba(0, 0, 0, 0.55);
  --speed: 110ms;

  /* One scanline texture, reused. 3.5% phosphor at a 3px pitch: visible as
     grain on a large surface, invisible as banding on a small one. */
  --scanlines: repeating-linear-gradient(
      0deg,
      rgba(79, 224, 140, 0.035) 0px,
      rgba(79, 224, 140, 0.035) 1px,
      transparent 1px,
      transparent 3px);
}

/* --------------------------------------------------------------------------
   2. Reset and base
   -------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  background: var(--ground);
  color: var(--text);
  font-family: var(--font-prose);
  font-size: 15px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  /* The page body never scrolls sideways; wide content scrolls inside
     .table-wrap instead. */
  overflow-x: hidden;
}

h1, h2, h3, h4 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.22;
  letter-spacing: -0.015em;
  color: var(--text-strong);
}
h1 { font-size: 1.55rem; }
h2 { font-size: 1.00rem; }
h3 { font-size: 0.88rem; }
h4 { font-size: 0.80rem; }

p { margin: 0; }

a {
  color: var(--phos);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: color var(--speed) linear, border-color var(--speed) linear;
}
a:hover { border-bottom-color: var(--rule-bright); }

/* Links inside body copy stay readable rather than turning a paragraph green:
   the underline carries the affordance, not the hue. */
.prose a { color: var(--text-strong); border-bottom-color: var(--rule-bright); }
.prose a:hover { color: var(--phos); }

code, kbd, samp, pre { font-family: var(--font-data); font-size: 0.88em; }
code {
  padding: 1px 5px;
  background: var(--surface-2);
  border: 1px solid var(--rule);
  color: var(--text-strong);
}

hr { border: 0; border-top: 1px solid var(--rule); margin: 0; }

img { max-width: 100%; height: auto; }

:focus-visible {
  outline: 2px solid var(--phos);
  outline-offset: 2px;
}
/* Only drop the default ring where :focus-visible has replaced it. */
:focus:not(:focus-visible) { outline: none; }

::selection { background: var(--phos-wash-2); color: var(--text-strong); }

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: var(--ground); }
::-webkit-scrollbar-thumb { background: var(--rule); }
::-webkit-scrollbar-thumb:hover { background: var(--rule-bright); }

/* --------------------------------------------------------------------------
   3. Shell - header, nav, connection indicator, page frame, footer

   The header is a status bar, not a navbar: scanlined, hairlined in phosphor,
   with the live daemon lamps pushed to the right where an operator expects the
   readout to be.
   -------------------------------------------------------------------------- */
.skip-link {
  position: absolute;
  left: var(--sp-2);
  top: -100px;
  z-index: 100;
  padding: var(--sp-2) var(--sp-3);
  background: var(--surface-2);
  border: 1px solid var(--phos);
  color: var(--text-strong);
}
.skip-link:focus { top: var(--sp-2); }

.site-header {
  position: sticky;
  top: 0;
  z-index: 30;
  background: var(--scanlines), var(--surface);
  border-bottom: 1px solid var(--rule-bright);
  box-shadow: var(--shadow-1);
}

.site-header__inner {
  max-width: var(--page-max);
  margin: 0 auto;
  min-height: var(--header-h);
  padding: var(--sp-2) var(--sp-4);
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-5);
}

.wordmark {
  display: inline-flex;
  align-items: baseline;
  gap: var(--sp-2);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.90rem;
  letter-spacing: 0.01em;
  color: var(--text-strong);
  border-bottom: 0;
}
.wordmark:hover { border-bottom: 0; color: var(--text-strong); }
.wordmark__mark { color: var(--phos); }

.nav {
  display: flex;
  align-items: center;
  gap: 2px;
  flex-wrap: wrap;
}
.nav__link {
  display: inline-block;
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 0;
  font-family: var(--font-data);
  font-size: 0.78rem;
  font-weight: 400;
  color: var(--text-muted);
}
.nav__link:hover { background: var(--surface-2); color: var(--text-strong); border-bottom: 0; }
.nav__link.is-active {
  color: var(--phos);
  background: var(--phos-wash);
  box-shadow: inset 0 -2px 0 var(--phos);
}

/* Live subsystem health. One item per daemon: a lamp plus a label. */
.conn {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  margin-left: auto;
  font-variant-numeric: tabular-nums;
}
.conn__item {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-family: var(--font-data);
  font-size: 0.72rem;
  color: var(--text-muted);
  white-space: nowrap;
}
.conn__label { color: var(--text); }

.account {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-family: var(--font-data);
  font-size: 0.76rem;
  color: var(--text);
}
.account__avatar {
  width: 24px;
  height: 24px;
  border: 1px solid var(--rule-bright);
  display: block;
}

.page {
  position: relative;
  z-index: 1;
  width: 100%;
  flex: 1 0 auto;
  max-width: var(--page-max);
  margin: 0 auto;
  padding: var(--sp-6) var(--sp-4) var(--sp-7);
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
}

.page-head {
  display: flex;
  align-items: baseline;
  gap: var(--sp-3);
  flex-wrap: wrap;
  padding-bottom: var(--sp-3);
  border-bottom: 1px solid var(--rule-bright);
}

/* Generous, because rule 4 says the headings get the room. */
.page-title {
  font-size: clamp(1.5rem, 3.4vw, 2.05rem);
  font-weight: 600;
  letter-spacing: -0.025em;
  color: var(--text-strong);
}
.page-title::before { content: "> "; color: var(--phos); }
.page-title::after {
  content: "";
  display: inline-block;
  width: 0.5em;
  height: 0.9em;
  margin-left: 0.18em;
  vertical-align: -0.08em;
  background: var(--phos);
  animation: blink 1.15s steps(2, start) infinite;
}
@keyframes blink { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }

.page-sub {
  font-family: var(--font-prose);
  font-size: 0.90rem;
  color: var(--text-muted);
  max-width: 66ch;
}

/* A status line, not a footer: mono, tabular, one row of separated facts. */
.site-footer {
  position: relative;
  z-index: 1;
  border-top: 1px solid var(--rule-bright);
  background: var(--scanlines), var(--surface);
  padding: var(--sp-3) var(--sp-4);
  font-family: var(--font-data);
  font-size: 0.70rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}
.site-footer__inner {
  max-width: var(--page-max);
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-5);
  justify-content: space-between;
}

/* --------------------------------------------------------------------------
   4. Layout primitives - gap-based, never per-element margins

   The two page-level grids are deliberately unequal. A screen with four
   identically sized panels reads as a template; a screen with one dominant
   column and a narrower rail reads as an instrument.
   -------------------------------------------------------------------------- */
.stack { display: flex; flex-direction: column; gap: var(--sp-4); }
.stack.stack--tight { gap: var(--sp-2); }
.stack.stack--loose { gap: var(--sp-5); }

.row { display: flex; align-items: center; gap: var(--sp-3); }
.row.row--tight { gap: var(--sp-2); }
.row.row--wrap { flex-wrap: wrap; }

.spread {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--sp-3);
}

.grid { display: grid; gap: var(--sp-4); grid-template-columns: 1fr; }
.grid.grid--2 { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
.grid.grid--3 { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.grid.grid--4 { grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); }

/* First child leads. Every .grid--2 in this app puts the thing that matters
   first, so give it the extra width instead of splitting the row down the
   middle. */
@media (min-width: 980px) {
  .grid.grid--2 { grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); }
}

.sidebar-layout { display: grid; gap: var(--sp-5); grid-template-columns: 1fr; }

@media (min-width: 980px) {
  .sidebar-layout { grid-template-columns: minmax(0, 2.4fr) minmax(250px, 1fr); }

  /* The rail. Its panels lose their chrome and become an aligned list in a
     gutter - the density contrast that stops both columns reading as the same
     component repeated. */
  .sidebar-layout > :last-child {
    border-left: 1px solid var(--rule);
    padding-left: var(--sp-5);
  }
  .sidebar-layout > :last-child > .card { background: none; border: 0; }
  .sidebar-layout > :last-child > .card > .card__head {
    background: none;
    padding: 0 0 var(--sp-2);
  }
  .sidebar-layout > :last-child > .card > .card__body { padding: var(--sp-3) 0 0; }
  .sidebar-layout > :last-child > .card > .card__foot { background: none; padding: var(--sp-3) 0 0; }
}

.divider { height: 1px; background: var(--rule); border: 0; }

.grow { flex: 1 1 auto; min-width: 0; }

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* --------------------------------------------------------------------------
   5. Surfaces - cards and panels
   -------------------------------------------------------------------------- */
.card {
  position: relative;
  /* --level is the card's state colour. Unset means no stripe at all: a
     severity stripe on every card is decoration, not information. */
  background: var(--surface);
  border: 1px solid var(--rule);
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.card.card--ok   { --level: var(--phos); }
.card.card--warn { --level: var(--warn); }
.card.card--crit { --level: var(--crit); }
.card.card--ok,
.card.card--warn,
.card.card--crit { border-left: 2px solid var(--level); }

.card__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface-2);
  border-bottom: 1px solid var(--rule);
}
/* Sentence case in the display face. This used to be uppercase phosphor, which
   made every panel shout the same note (rule 2). */
.card__title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.90rem;
  letter-spacing: -0.01em;
  color: var(--text-strong);
}
.card__meta {
  font-family: var(--font-data);
  font-size: 0.70rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}

.card__body {
  padding: var(--sp-4);
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  min-width: 0;
  /* Takes the slack when a grid stretches this card to match a taller
     neighbour, so .card__foot stays welded to the bottom edge instead of
     leaving a band of empty surface under it. */
  flex: 1 1 auto;
}
.card.card--flush > .card__body { padding: 0; gap: 0; }

.card__foot {
  padding: var(--sp-2) var(--sp-4);
  border-top: 1px solid var(--rule);
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-3);
  font-size: 0.76rem;
  color: var(--text-muted);
}
.card__actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; }

.panel {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-left: 2px solid var(--phos-dim);
  padding: var(--sp-3) var(--sp-4);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  min-width: 0;
}

/* A page-level heading with a rule running out to the margin - the artifact's
   own h2 treatment, which is where this whole page's structure comes from. */
.section-title {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  font-family: var(--font-display);
  font-size: 0.92rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--phos);
}
.section-title::after {
  content: "";
  flex: 1 1 auto;
  height: 1px;
  background: var(--rule-bright);
}

/* --------------------------------------------------------------------------
   6. Data - stat tiles, tables, key/value, meters
   -------------------------------------------------------------------------- */
.stat-grid {
  display: grid;
  gap: var(--sp-3);
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}

.stat {
  background: var(--surface);
  border: 1px solid var(--rule);
  padding: var(--sp-3) var(--sp-4);
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.stat.stat--ok   { --level: var(--phos); }
.stat.stat--warn { --level: var(--warn); }
.stat.stat--crit { --level: var(--crit); }
.stat.stat--ok,
.stat.stat--warn,
.stat.stat--crit { border-left: 2px solid var(--level); }

.stat__label {
  font-family: var(--font-data);
  font-size: 0.64rem;
  font-weight: 500;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.stat__value {
  font-family: var(--font-data);
  font-weight: 500;
  font-size: 1.65rem;
  line-height: 1.05;
  letter-spacing: -0.02em;
  color: var(--text-strong);
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}
.stat__unit { font-size: 0.78rem; color: var(--text-muted); margin-left: 2px; }
.stat__note {
  font-family: var(--font-data);
  font-size: 0.68rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}

/* THE HERO STRIP. The summary row directly under a page title is the one
   element that gets to dominate: hairline-welded into a single instrument
   panel rather than four floating boxes, with the first figure at display
   scale. Nested stat grids (inside a card) keep the plain treatment. */
.page > .stat-grid {
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
}
.page > .stat-grid > .stat {
  border: 0;
  padding: var(--sp-3) var(--sp-4) var(--sp-4);
}
.page > .stat-grid > .stat.stat--ok,
.page > .stat-grid > .stat.stat--warn,
.page > .stat-grid > .stat.stat--crit { box-shadow: inset 2px 0 0 var(--level); }

@media (min-width: 860px) {
  /* The lead figure carries the strip. Same track width as its neighbours -
     the scale and the grain are what make it the headline, not extra room. */
  .page > .stat-grid > .stat:first-child { background: var(--scanlines), var(--surface); }
  .page > .stat-grid > .stat:first-child .stat__value { font-size: 2.6rem; }
}

/* The page body must never scroll sideways - a wide table scrolls in here. */
.table-wrap {
  overflow-x: auto;
  overflow-y: hidden;
  max-width: 100%;
  border: 1px solid var(--rule);
  background: var(--surface);
}
/* Inside a flush card the card already draws the border. */
.card.card--flush .table-wrap { border: 0; }

.table {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--font-data);
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
}
.table.table--wide { min-width: 720px; }

.table th {
  position: sticky;
  top: 0;
  z-index: 1;
  text-align: left;
  padding: var(--sp-2) var(--sp-3);
  background: var(--surface-2);
  border-bottom: 1px solid var(--rule-bright);
  font-weight: 500;
  font-size: 0.63rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-muted);
  white-space: nowrap;
}
.table td {
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--rule);
  color: var(--text);
  vertical-align: top;
}
.table tbody tr:last-child td { border-bottom: 0; }
.table tbody tr:hover td { background: var(--surface-2); }
/* A cursor bar on the row under the pointer, the way a TUI marks selection. */
.table tbody tr:hover td:first-child { box-shadow: inset 2px 0 0 var(--phos); }
.table.table--compact td { padding: var(--sp-1) var(--sp-3); }

/* Digits that line up in a column. Works on a <td> or a bare <span>. */
.num {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}
.data {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  color: var(--text-strong);
}

/* Ledger rows: name left, value right, a hairline under each pair. Reads as a
   printout rather than as a list of floating labels. */
.kv {
  display: grid;
  grid-template-columns: minmax(88px, auto) minmax(0, 1fr);
  gap: 0 var(--sp-4);
  margin: 0;
  align-items: baseline;
}
.kv__k,
.kv__v {
  padding: var(--sp-2) 0;
  border-bottom: 1px solid var(--rule);
}
.kv > :nth-last-child(-n + 2) { border-bottom: 0; padding-bottom: 0; }
.kv__k {
  font-family: var(--font-data);
  font-size: 0.64rem;
  font-weight: 500;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.kv__v {
  margin: 0;
  font-family: var(--font-data);
  font-size: 0.80rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-strong);
  overflow-wrap: anywhere;
}

/* Segmented, like a hardware gauge: the fill is ticked at a 4px pitch so a
   glance reads roughly how full it is without the number. */
.meter {
  height: 7px;
  background: var(--surface-2);
  border: 1px solid var(--rule);
  overflow: hidden;
}
.meter__fill {
  display: block;
  height: 100%;
  width: 0;
  background: var(--phos);
  background-image: repeating-linear-gradient(
      90deg, transparent 0 3px, rgba(10, 15, 12, 0.55) 3px 4px);
  transition: width var(--speed) linear;
}
.meter.meter--warn .meter__fill { background-color: var(--warn); }
.meter.meter--crit .meter__fill { background-color: var(--crit); }

.trunc { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 28ch; }

/* --------------------------------------------------------------------------
   7. State - chips, lamps, badges, and the two semantic hooks

   [data-diet] and [data-intent] are how colour stops being decoration. Both
   read the game's own vocabulary straight out of the telemetry feed, so the
   hue on screen is the thing the animal is, or the thing it is doing.
   -------------------------------------------------------------------------- */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 2px var(--sp-2);
  border: 1px solid var(--rule-bright);
  background: var(--surface-2);
  font-family: var(--font-data);
  font-size: 0.70rem;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  white-space: nowrap;
}
.chip.chip--ok      { border-color: var(--phos-dim);   background: var(--phos-wash);   color: var(--phos); }
.chip.chip--warn    { border-color: var(--warn);       background: var(--warn-wash);   color: var(--warn); }
.chip.chip--crit    { border-color: var(--crit);       background: var(--crit-wash);   color: var(--crit); }
.chip.chip--info    { border-color: var(--rule-bright); background: var(--surface-2);  color: var(--text-strong); }
.chip.chip--idle    { border-color: var(--rule);       background: var(--surface);     color: var(--text-muted); }
.chip.chip--derived { border-color: var(--violet-dim); background: var(--violet-wash); color: var(--violet); }

/* Diet. Carnivore is the red already on the page for danger, herbivore the
   phosphor already there for healthy, omnivore the amber for mixed - and
   piscivore gets the water hue, which is the reason aqua exists. Anything the
   feed invents that is not in this list falls back to the neutral rail. */
[data-diet]                 { --diet: var(--rule-bright); }
[data-diet="carnivore"]     { --diet: var(--crit); }
[data-diet="herbivore"]     { --diet: var(--phos); }
[data-diet="omnivore"]      { --diet: var(--warn); }
[data-diet="piscivore"]     { --diet: var(--aqua); }
[data-diet="unknown"]       { --diet: var(--text-muted); }

.chip[data-diet] {
  border-color: var(--diet);
  color: var(--diet);
  background: none;
}
tr[data-diet] > td:first-child { box-shadow: inset 2px 0 0 var(--diet); }
.table tbody tr[data-diet]:hover td:first-child { box-shadow: inset 2px 0 0 var(--diet); }
/* A diet's share bar is drawn in that diet's own colour, so the breakdown is
   readable as a picture before it is read as a list. */
[data-diet] .meter__fill { background-color: var(--diet); }

/* Movement intent, matched on the mod's own reason strings by substring so a
   new one it invents still lands in the right family:
     survival-water, drinking-*   -> water
     survival-food,  eating-*     -> food
     encounter-*, retaliation-*,
     predator-*, *-threat         -> danger
     herd-*, protecting-*         -> social
   Everything else (roaming, resting) stays neutral, which is most of them. */
[data-intent]                    { --intent: var(--text-muted); }
[data-intent*="water"],
[data-intent*="drink"]           { --intent: var(--aqua); }
[data-intent*="food"],
[data-intent*="eat"]             { --intent: var(--warn); }
[data-intent*="encounter"],
[data-intent*="retaliation"],
[data-intent*="predator"],
[data-intent*="threat"],
[data-intent*="attack"]          { --intent: var(--crit); }
[data-intent*="herd"],
[data-intent*="protect"]         { --intent: var(--phos); }

.chip[data-intent] {
  border-color: var(--intent);
  color: var(--intent);
  background: none;
}

.chip__dot,
.dot {
  width: 7px;
  height: 7px;
  /* Rule 1's single exception. A lamp is round; a box is not. */
  border-radius: 50%;
  background: var(--text-muted);
  flex: 0 0 auto;
  display: inline-block;
}
.dot.dot--ok {
  background: var(--phos);
  box-shadow: 0 0 6px var(--phos-wash-2);
  animation: pulse 2.4s ease-in-out infinite;
}
.dot.dot--warn { background: var(--warn); }
.dot.dot--crit { background: var(--crit); }
.dot.dot--idle { background: var(--text-muted); }

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

.badge {
  display: inline-block;
  padding: 1px var(--sp-2);
  border: 1px solid var(--rule-bright);
  background: var(--surface-2);
  font-family: var(--font-data);
  font-size: 0.66rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}
.badge.badge--derived {
  border-color: var(--violet-dim);
  background: var(--violet-wash);
  color: var(--violet);
}

.pill-row { display: flex; flex-wrap: wrap; gap: var(--sp-2); }

/* --------------------------------------------------------------------------
   8. Controls - buttons and forms
   -------------------------------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4);
  min-height: 32px;
  border: 1px solid var(--rule-bright);
  background: var(--surface-2);
  color: var(--text-strong);
  font-family: var(--font-data);
  font-size: 0.76rem;
  font-weight: 500;
  cursor: pointer;
  text-decoration: none;
  transition: background var(--speed) linear, border-color var(--speed) linear,
              color var(--speed) linear;
}
.btn:hover { background: var(--phos-wash); border-color: var(--phos-dim); color: var(--text-strong); }
.btn:active { background: var(--phos-wash-2); }

.btn.btn--primary { background: var(--phos-wash); border-color: var(--phos); color: var(--phos); }
.btn.btn--primary:hover { background: var(--phos-wash-2); color: var(--phos); }

.btn.btn--danger { background: var(--crit-wash); border-color: var(--crit); color: var(--crit); }
.btn.btn--danger:hover { background: var(--crit-wash-2); border-color: var(--crit); color: var(--crit); }

.btn.btn--ghost { background: transparent; border-color: var(--rule); color: var(--text-muted); }
.btn.btn--ghost:hover { background: var(--surface-2); border-color: var(--rule-bright); color: var(--text-strong); }

.btn.btn--sm { min-height: 24px; padding: 2px var(--sp-3); font-size: 0.70rem; }
.btn.btn--block { width: 100%; }

.btn[disabled],
.btn[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; pointer-events: none; }

.btn-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }

.field { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }

.label {
  font-family: var(--font-data);
  font-size: 0.64rem;
  font-weight: 500;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.input,
.select,
.textarea {
  width: 100%;
  padding: var(--sp-2) var(--sp-3);
  min-height: 32px;
  background: var(--ground);
  border: 1px solid var(--rule-bright);
  color: var(--text-strong);
  font-family: var(--font-data);
  font-size: 0.80rem;
  transition: border-color var(--speed) linear, background var(--speed) linear;
}
.textarea { min-height: 84px; resize: vertical; line-height: 1.5; }
.select { cursor: pointer; }

.input::placeholder,
.textarea::placeholder { color: var(--text-muted); opacity: 1; }

.input:hover,
.select:hover,
.textarea:hover { border-color: var(--phos-dim); }

.input:focus,
.select:focus,
.textarea:focus { border-color: var(--phos); background: var(--surface); }

.input[disabled],
.select[disabled],
.textarea[disabled] { opacity: 0.5; cursor: not-allowed; }

.hint { font-size: 0.74rem; color: var(--text-muted); }
.field-error { font-size: 0.74rem; color: var(--crit); }
.field.field--error .input,
.field.field--error .select,
.field.field--error .textarea { border-color: var(--crit); }

.form-grid { display: grid; gap: var(--sp-4); grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }

.check { display: inline-flex; align-items: center; gap: var(--sp-2); cursor: pointer; font-size: 0.80rem; }
.check__box { accent-color: var(--phos); width: 15px; height: 15px; }

/* --------------------------------------------------------------------------
   9. Feedback - empty states, inline flashes, toasts, busy
   -------------------------------------------------------------------------- */
/* Left-aligned and hatched. "Nothing here" is a state of the data, so it gets
   a surface that looks like absence rather than a centred invitation. */
.empty {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-2);
  padding: var(--sp-4);
  border: 1px solid var(--rule);
  background:
    repeating-linear-gradient(135deg,
      transparent 0 7px, rgba(42, 107, 71, 0.10) 7px 8px),
    var(--surface);
  color: var(--text-muted);
}
.empty__title {
  font-family: var(--font-display);
  font-size: 0.86rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text-strong);
}
.empty__body { font-size: 0.84rem; max-width: 60ch; color: var(--text-muted); }
.empty__action { display: flex; gap: var(--sp-2); flex-wrap: wrap; }

/* A callout, not a box: one coloured edge and a wash, the artifact's own
   treatment. --level is what the modifier sets, so the three variants are
   three token swaps rather than three rule blocks. */
.flash {
  --level: var(--rule-bright);
  --level-wash: var(--surface-2);
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--sp-3) var(--sp-4);
  border: 0;
  border-left: 2px solid var(--level);
  background: var(--level-wash);
  font-size: 0.84rem;
  color: var(--text);
}
.flash.flash--ok   { --level: var(--phos); --level-wash: var(--phos-wash); }
.flash.flash--warn { --level: var(--warn); --level-wash: var(--warn-wash); }
.flash.flash--crit { --level: var(--crit); --level-wash: var(--crit-wash); }
/* Rule 2's fifth and last uppercase: this genuinely is the name of the note. */
.flash__title {
  font-family: var(--font-data);
  font-size: 0.64rem;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--level);
}

.toasts {
  position: fixed;
  right: var(--sp-4);
  bottom: var(--sp-4);
  z-index: 60;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  width: min(360px, calc(100vw - 2 * var(--sp-4)));
  pointer-events: none;
}
.toast {
  --level: var(--rule-bright);
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface-2);
  border: 1px solid var(--rule);
  border-left: 2px solid var(--level);
  box-shadow: var(--shadow-2);
  font-size: 0.82rem;
  color: var(--text);
  animation: toast-in 160ms ease-out;
}
.toast.toast--ok   { --level: var(--phos); }
.toast.toast--warn { --level: var(--warn); }
.toast.toast--crit { --level: var(--crit); }
.toast__body { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.toast__close {
  flex: 0 0 auto;
  padding: 0 var(--sp-1);
  background: none;
  border: 0;
  color: var(--text-muted);
  font-family: var(--font-data);
  font-size: 0.90rem;
  line-height: 1;
  cursor: pointer;
}
.toast__close:hover { color: var(--text-strong); }

@keyframes toast-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}

/* htmx puts .htmx-request on the element that issued the request. */
.htmx-indicator {
  opacity: 0;
  font-family: var(--font-data);
  font-size: 0.72rem;
  transition: opacity var(--speed) linear;
}
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator { opacity: 1; }

.is-busy { opacity: 0.55; pointer-events: none; }

.skeleton {
  display: block;
  min-height: 1em;
  background: var(--surface-2);
  animation: pulse 1.6s ease-in-out infinite;
}

/* --------------------------------------------------------------------------
   10. Utilities
   -------------------------------------------------------------------------- */
.muted  { color: var(--text-muted); }
.strong { color: var(--text-strong); }
.accent { color: var(--phos); }
/* "This number was computed from a scale nobody has verified." */
.is-derived { color: var(--violet); }
.nowrap { white-space: nowrap; }
.mt-0   { margin-top: 0; }
.mb-0   { margin-bottom: 0; }

.prose  {
  font-family: var(--font-prose);
  color: var(--text);
  max-width: 66ch;
  font-size: 0.94rem;
  line-height: 1.65;
}
.prose ul { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: var(--sp-1); }
.prose li { position: relative; padding-left: 20px; }
.prose li::before {
  content: "\25B8";
  position: absolute;
  left: 0;
  color: var(--phos-dim);
  font-family: var(--font-data);
}

/* --------------------------------------------------------------------------
   10a. Ambient CRT layers - .fx-*
   ---------------------------------------------------------------------------
   Two fixed, pointer-events-free layers that no page has to know about; both
   are mounted once in base.html and neither ever contains content.

     .fx-backdrop  z-index 0, above the body ground and behind page content. Holds the island rain canvas and the
                   sweep bar. Masked so it is strong at the viewport edges and
                   gone behind the content column - the rain must never be
                   behind a paragraph.
     .fx-overlay   z-index 50, ABOVE the page but below .toasts (60), because a
                   CRT's scanlines are on the glass, in front of the phosphor.
                   Scanlines + vignette only; 3.5% alpha, no motion.

   `html.fx-off` kills every moving part. static/fx.js sets it from the saved
   reader preference and from prefers-reduced-motion.
   -------------------------------------------------------------------------- */
.fx-backdrop,
.fx-overlay {
  position: fixed;
  inset: 0;
  pointer-events: none;
  /* Its own stacking context, so nothing inside can escape the layer. */
  isolation: isolate;
}

html.fx-off .fx-backdrop { display: none; }
html.fx-off .fx-sweep { animation: none; }

.fx-backdrop {
  /* A negative layer sits behind body's opaque background and is therefore
     invisible. Page chrome establishes z-index: 1; this layer sits between
     that content and the body ground. */
  z-index: 0;
  overflow: hidden;
  /* Cards, panels and the header are all opaque --surface, so the rain is only
     ever visible in the gutters and in the gaps between them - no card text is
     ever read against it. The mask is vertical only, taking the layer to
     nothing before the footer so the page ends quiet. */
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 62%, transparent 100%);
  mask-image: linear-gradient(180deg, #000 0%, #000 62%, transparent 100%);
}

.fx-rain {
  display: block;
  width: 100%;
  height: 100%;
  /* The canvas draws at full strength; the whole layer is dimmed here so one
     number controls how loud the effect is. */
  opacity: 0.28;
  filter: blur(0.55px);
}

/* A single 7s pass of the beam. One compositor-only transform, no repaint. */
.fx-sweep {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 26vh;
  background: linear-gradient(
      180deg,
      transparent 0%,
      rgba(79, 224, 140, 0.05) 55%,
      rgba(79, 224, 140, 0.12) 88%,
      rgba(79, 224, 140, 0.22) 97%,
      transparent 100%);
  will-change: transform;
  animation: fx-sweep 7.5s linear infinite;
}
@keyframes fx-sweep {
  from { transform: translate3d(0, -30vh, 0); }
  to   { transform: translate3d(0, 100vh, 0); }
}

.fx-overlay {
  z-index: 50;
  background:
    /* Vignette first so the scanlines sit on top of it. */
    radial-gradient(120% 90% at 50% 42%, transparent 52%, rgba(0, 0, 0, 0.42) 100%),
    var(--scanlines);
}

/* --------------------------------------------------------------------------
   10b. Console - the live feed tail
   ---------------------------------------------------------------------------
   A public island log: player and AI activity comes from the anonymous-tier
   activity wire, while meaningful link changes come from the shared /events
   stream. Neither source contains a position or a Steam ID.

   Markup contract (templates/partials/console.html):
     .console                  a .card.card--flush
       [data-console-screen]   the scrolling viewport
       [data-console-lines]    the <ol> that fx.js appends .console__line to
       [data-console-caret]    the block cursor at the tail
       [data-fx-toggle]        button that flips html.fx-off
   -------------------------------------------------------------------------- */
.console__screen {
  position: relative;
  height: 15rem;
  overflow-y: auto;
  overscroll-behavior: contain;
  background:
    linear-gradient(180deg, rgba(79, 224, 140, 0.04), transparent 120px),
    var(--ground);
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  scrollbar-width: thin;
  scrollbar-color: var(--phos-dim) transparent;
}
.console__screen::-webkit-scrollbar { width: 8px; }
.console__screen::-webkit-scrollbar-thumb { background: var(--phos-dim); }
.console__screen::-webkit-scrollbar-track { background: transparent; }

.console__lines {
  list-style: none;
  margin: 0;
  padding: var(--sp-3) var(--sp-4);
  display: flex;
  flex-direction: column;
  gap: 1px;
  font-family: var(--font-data);
  font-size: 0.76rem;
  line-height: 1.5;
}

/* Three columns: when, what subsystem, what happened. The message wraps under
   itself rather than under the timestamp. */
.console__line {
  display: grid;
  grid-template-columns: 8ch 8ch 1fr;
  gap: var(--sp-2);
  color: var(--text);
  word-break: break-word;
}
.console__time { color: var(--text-muted); font-variant-numeric: tabular-nums; }
/* Rule 2 exception is deliberate: these are log tags, not labels - a tag is
   lowercase in every log this portal's operators already read. */
.console__tag  { color: var(--phos-dim); }

.console__line.is-ok      .console__tag { color: var(--phos); }
.console__line.is-warn    .console__tag,
.console__line.is-warn    .console__msg { color: var(--warn); }
.console__line.is-crit    .console__tag,
.console__line.is-crit    .console__msg { color: var(--crit); }
.console__line.is-dim     .console__msg { color: var(--text-muted); }
.console__line.is-derived .console__msg { color: var(--violet); }
.console__msg .strong { color: var(--text-strong); }

/* Sits on the last line, not on its own row, so the feed reads as one stream. */
.console__caret {
  display: inline-block;
  width: 0.55em;
  height: 0.95em;
  margin-left: 0.15em;
  vertical-align: -0.12em;
  background: var(--phos);
  animation: blink 1.15s steps(2, start) infinite;
}

.console__foot { font-family: var(--font-data); font-size: 0.72rem; }

/* --------------------------------------------------------------------------
   11. Map - /map only

   WHY THIS SECTION EXISTS. Two things above cannot express it. Leaflet injects
   its own DOM with its own class names and a light-theme default skin, and no
   .card or .chip rule can reach a `.leaflet-*` node - left alone it puts white
   boxes and a grey void on a phosphor terminal. And a marker is a positioned
   point on a canvas, which is not what .dot is: .dot is a 7px inline lamp
   carrying a state, while .map-dot is sized by rank, stroked so it survives
   over a bright raster, and coloured from --diet.

   IT ADDS NO COLOUR. Every hue below is an existing token, and every marker
   takes its own from the [data-diet] hook in section 7 - the same hook the AI
   tables on /server use, so the map and the tables cannot disagree and nothing
   in Rust maps a diet to a colour (AGENTS.md 4).
   -------------------------------------------------------------------------- */
.map {
  height: clamp(360px, 66vh, 760px);
  background: var(--ground);
}

/* The rasters are scaled to the viewport - relief a cell to a pixel, heat
   already resampled to a fifth of the base art. Let the browser smooth both:
   these are derived surfaces, not pixel art, and nearest-neighbour would
   assert a precision the aggregate does not have. */
.map-raster { image-rendering: auto; }

/* The base art is a photographic composite dropped onto a phosphor terminal.
   Taking a little saturation and light out of it is what lets the hotspot
   layer above it read as the foreground, which is the whole point of the
   page. The image itself is untouched on disk. */
.map-art { filter: saturate(0.72) brightness(0.82) contrast(1.05); }

/* Leaflet's own furniture, brought onto the palette.

   Every selector below is qualified with `.map`, and that is load-bearing
   rather than tidiness: leaflet.css is linked from templates/map/map.html and
   therefore lands AFTER app.css in the cascade, so a bare `.leaflet-container`
   here loses to leaflet.css's own `.leaflet-container { background: #ddd }` at
   equal specificity. One extra class wins it without an !important and without
   editing the vendored file. */
.map.leaflet-container {
  background: var(--ground);
  font-family: var(--font-data);
  font-size: 0.74rem;
  color: var(--text);
}
.map.leaflet-container a { color: var(--phos); }
.map .leaflet-bar,
.map .leaflet-control-layers {
  border: 1px solid var(--rule-bright);
  border-radius: 0;              /* rule 1 */
  box-shadow: var(--shadow-1);
  background: var(--surface);
}
.map .leaflet-bar a,
.map .leaflet-bar a:hover {
  background: var(--surface-2);
  color: var(--text-strong);
  border-bottom-color: var(--rule);
  border-radius: 0;
}
.map .leaflet-bar a:hover { background: var(--phos-wash); color: var(--phos); }
.map .leaflet-control-layers-expanded {
  padding: var(--sp-2) var(--sp-3);
  color: var(--text);
  background: var(--surface);
}
.map .leaflet-control-layers-list label { display: block; line-height: 1.7; cursor: pointer; }
.map .leaflet-control-layers-selector { accent-color: var(--phos); margin-right: var(--sp-2); }

.map .leaflet-popup-content-wrapper,
.map .leaflet-popup-tip {
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--rule-bright);
  border-radius: 0;
  box-shadow: var(--shadow-2);
}
.map .leaflet-popup-content { margin: var(--sp-3); }
.map .leaflet-popup-close-button { color: var(--text-muted) !important; }

/* The base map's credit. Required by static/map/SOURCE.txt, so it is styled
   to be readable rather than hidden. */
.map .leaflet-control-attribution {
  background: var(--surface);
  border: 1px solid var(--rule);
  padding: 1px var(--sp-2);
  font-family: var(--font-data);
  font-size: 0.62rem;
  color: var(--text-muted);
}
.map .leaflet-control-attribution a { color: var(--text-muted); }

/* Markers. .map-marker is only the divIcon wrapper - it must carry no box of
   its own, or every dot gets Leaflet's default white square behind it. */
.map .map-marker { background: none; border: 0; }

.map-dot {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 50%;            /* rule 1's lamp exception: a marker is a point */
  background: var(--diet, var(--rule-bright));
  /* A dark ring, so a herbivore stays legible over pale high ground. */
  box-shadow: 0 0 0 1px rgba(10, 15, 12, 0.85), 0 0 6px rgba(10, 15, 12, 0.6);
}
/* The leader is the animal the herd is following, so it is the one that reads
   as a centre rather than as one more dot. */
.map-dot--leader { border: 2px solid var(--ground); box-shadow: 0 0 0 1px var(--diet, var(--phos)); }

/* A player is not an animal and does not get a diet: RCON reports a class and
   the class list is open (PLAN.md 10). Square, so an unrecognised species is
   still a marker rather than a gap. */
.map-dot--player {
  border-radius: 0;
  background: var(--text-strong);
  transform: rotate(45deg);
}
.map-dot--self { background: var(--phos); box-shadow: 0 0 0 2px var(--ground), 0 0 10px var(--phos); }

/* The diet filter. A chip has never been a control before, so it needs the
   three rules a button always needs and nothing else: no UA chrome, a
   pointer, and a dimmed state for the four it is not. */
button.chip {
  -webkit-appearance: none;
  appearance: none;
  cursor: pointer;
  line-height: 1.5;
}
button.chip[aria-pressed="false"] { opacity: 0.4; }
button.chip:disabled { opacity: 0.18; cursor: default; }

/* The density legend: the ramp map.js is actually painting with, so the two
   cannot disagree. */
.map-scale { display: inline-flex; align-items: center; gap: var(--sp-2); }
.map-scale i {
  width: 88px;
  height: 8px;
  border: 1px solid var(--rule);
  background: var(--surface-2);
}

.map-zone,
.map-water { background: none; border: 0; white-space: nowrap; }
/* Place names sit on satellite photography, not on the portal's own ground, so
   they break the palette deliberately: white with a black stroke is the one
   combination legible over both a bright sandbank and dark water. A phosphor
   caption vanished against the map's own greens. Rule 2 still holds - a place
   name is not uppercased, so this adds no sixth text-transform.

   `paint-order: stroke fill` draws the outline UNDER the glyph; without it the
   stroke eats into the letterforms and thin type turns to mush. The text-shadow
   is the fallback for anything that does not support -webkit-text-stroke. */
.map-zone__name,
.map-water__name {
  position: absolute;
  left: 0;
  top: -0.6em;
  font-family: var(--font-data);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.03em;
  color: #FFFFFF;
  -webkit-text-stroke: 2.5px #000000;
  paint-order: stroke fill;
  text-shadow:
     0  1px 2px rgba(0, 0, 0, 0.9),
     0 -1px 2px rgba(0, 0, 0, 0.9),
     1px 0  2px rgba(0, 0, 0, 0.9),
    -1px 0  2px rgba(0, 0, 0, 0.9);
}
.map-water__name {
  color: var(--aqua);
  transform: translateX(-50%);
}
/* Two ranks, by how much of the population the zone holds - a map with six
   identical captions has no hierarchy. Size and weight carry it now that hue
   is fixed at white for legibility. */
.map-zone--major .map-zone__name {
  font-family: var(--font-display);
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: 0.01em;
  -webkit-text-stroke: 3px #000000;
}

.map-pop { display: grid; gap: 2px; }
.map-pop__title { font-family: var(--font-display); font-size: 0.78rem; color: var(--text-strong); }
.map-pop__row { display: flex; gap: var(--sp-3); justify-content: space-between; }
.map-pop__k { color: var(--text-muted); }
.map-pop__v { font-variant-numeric: tabular-nums; color: var(--text); }

/* --------------------------------------------------------------------------
   12. Motion preference - last, so it wins over every rule above
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  .page-title::after { animation: none; opacity: 1; }
  .console__caret { animation: none; opacity: 1; }
  .fx-backdrop { display: none; }
}

/* --------------------------------------------------------------------------
   13. Narrow viewports
   -------------------------------------------------------------------------- */
@media (max-width: 720px) {
  .site-header__inner { gap: var(--sp-2) var(--sp-3); }
  .conn { order: 3; width: 100%; margin-left: 0; gap: var(--sp-3); }
  .page { padding: var(--sp-5) var(--sp-3) var(--sp-6); }
  .stat__value { font-size: 1.35rem; }
  .kv { grid-template-columns: 1fr; gap: 0; }
  .kv__k { border-bottom: 0; padding-bottom: 0; }

  /* A phone is the one device where this costs battery for an effect nobody
     can see: the content is full-bleed, so there are no gutters left to rain
     in. fx.js reads the same 720px and never starts the loop. */
  .fx-backdrop { display: none; }

  /* The world is roughly square, so at a phone's width the map already fits
     it across; 66vh would only add empty ground under it. */
  .map { height: clamp(300px, 56vh, 480px); }

  .console__screen { height: 12rem; }
  /* The tag column goes; the timestamp and the message are what matter at this
     width. */
  .console__line { grid-template-columns: 8ch 1fr; }
  .console__tag { display: none; }
}
.map-population-marker { background: transparent; border: 0; }
.map-population-count {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border: 2px solid rgba(255,255,255,.9);
  border-radius: 50%;
  background: rgba(7,18,16,.88);
  color: #fff;
  font: 700 12px/1 var(--font-mono);
  box-shadow: 0 2px 10px rgba(0,0,0,.65);
}
