/* MP interface skin: Y2K / pixel retro-computing.
 *
 * One sheet, two pages. `client/index.html` (game HUD, operator console, weapon
 * workshop) and `viewer/index.html` (retargeting bench) both link this, so a
 * panel added to either one is already dressed. Both servers already serve
 * `.css` from the repo root, so `/shared/ui.css` resolves on 8175 and on 8174
 * alike -- and it is a local file, like everything else here: no CDN, no font
 * download, no network needed to run the game.
 *
 * The five rules. Break one and a panel stops belonging to the set:
 *
 *   1. EVERYTHING is outlined in black. `--b1` on anything you click or type
 *      into, `--b2` on a window. That outline is the whole signature -- it is
 *      what keeps the HUD readable over a noon sky and over a midnight cave,
 *      because an opaque fill inside a black line does not care what is behind
 *      it.
 *   2. Radius is 0. Always. There is not one rounded corner in this file.
 *   3. Flat fills only. No gradient, no blur, no translucent panel. Depth is a
 *      HARD shadow: `var(--drop) var(--drop) 0 var(--ink)`, and pressing a
 *      button moves it into its own shadow rather than dimming it.
 *   4. Monospace, uppercase for anything that names a thing (window titles,
 *      section headings, buttons). Antialiasing off: pixel type wants hard
 *      edges.
 *   5. Saturated accents on off-white or pale lavender. Colour carries meaning
 *      (violet = selected, orange = the one commit button, magenta = focus,
 *      red = refused, green = accepted) -- it is not decoration.
 *
 * And one mechanical rule that matters more than it looks:
 *
 *   6. NO BARE PIXEL VALUE. Every length is `calc(N * var(--u))`, where `N` is
 *      what the number would have been on a 1x interface. `--ui` below is the
 *      single dial for how big the whole thing is; nothing else needs touching
 *      to go bigger or smaller. The exceptions are deliberate and each one says
 *      why it is one (the crosshair, and the viewport caps in the client page).
 */

:root {
  /* ------------------------------------------------------------------ *
   *  THE DIAL. 1 = the original small interface, 3 = three times that.  *
   *  Change this one number and every panel, control, outline, shadow   *
   *  and type size follows. Nothing else in the project is allowed to   *
   *  hard-code a pixel.                                                 *
   * ------------------------------------------------------------------ */
  --ui: 2;
  /* One interface pixel. Write `calc(11 * var(--u))`, never `11px`. */
  --u: calc(1px * var(--ui));

  /* -------------------------------------------------------------------
   *  INSIDE THE ANDROID APP the same dial turns down: a phone holds the
   *  screen at arm's length and the 2x interface reads as furniture.
   *  `html.inapp` is stamped before first paint by the inline script at
   *  the top of each page's <body>, off the one signal only the installed
   *  app can send (an android-app:// referrer). ON THE ROOT ELEMENT, not
   *  a descendant: `--u` substitutes `--ui` where it is DECLARED, so only
   *  an override on html itself re-resolves it -- the measured trap five
   *  lines under `body.onpad` in this file.
   * ------------------------------------------------------------------- */

  /* Ink. Not pure black: a little violet in it so it sits with the accents. */
  --ink: #14101c;
  --ink-soft: #4a4160;          /* secondary text ON a light fill; never a border */

  /* Interiors */
  --paper: #fbf7f0;             /* off-white: panels, fields */
  --paper2: #e4dbfa;            /* pale lavender: rails, inert chips, stripes */
  --screen: #1b1426;            /* the dark "screen" inside a frame (3D previews) */

  /* Accents */
  --lav: #b9a3f2;
  --violet: #7b3fe4;
  --orange: #ff8b26;
  --magenta: #ff4d9d;
  --turq: #25d0c0;
  --gold: #ffd23f;
  --red: #e8402a;
  --green: #2fb85f;

  /* Metrics, all in interface pixels */
  --b1: calc(2 * var(--u));
  /* outline: buttons, fields, chips, swatches */
  --b2: calc(3 * var(--u));
  /* outline: windows */
  --drop: calc(3 * var(--u));   /* hard shadow offset */

  /* ------------------------------------------------------------------ *
   *  THE STACK. Every layer of the interface, in order, in ONE place.   *
   * ------------------------------------------------------------------ *
   *
   * There were nine different `z-index` numbers spread between this file,
   * `client/index.html`, `client/src/loading.js` and `client/src/impact.js`,
   * and nowhere that said what the order was. So each new window got a number
   * picked by eye against whatever its author happened to remember -- and
   * `#craft` got none at all, stacked at `auto`, and the help button sat on top
   * of the recipe list. The owner saw a `?` floating over his recipes.
   *
   * That is the same shape of defect as a guard written into nine key handlers
   * and forgotten in nine others. The cure is the same: one list, and every
   * rule names a LAYER instead of choosing a number.
   *
   * Bottom to top:
   *
   *   (auto)      the canvas, then the HUD furniture -- crosshair, debug strip,
   *               health, hotbar, chat log, points, price tags, compass. All at
   *               `auto`, stacking in DOM order, and deliberately so: none of
   *               them ever needs to cover another, and giving each one a number
   *               would create a dozen stacking contexts to no purpose. The one
   *               exception is `.slot.on`, which is a bump INSIDE the hotbar so
   *               the selected cell's outline is not clipped by its neighbour --
   *               a local affair, not a page layer.
   *   --z-pad     the touch pad. Over the HUD it is drawn on, UNDER the flash --
   *               a blow tints the whole screen and the sticks are part of the
   *               screen. Phones only; see client/src/pad.js.
   *   --z-flash   the full-screen hit flash. Over the HUD (it tints it), under
   *               everything you can click.
   *   --z-hudbtn  the corner buttons: `?` and MENU.
   *   --z-hudchip the reconnecting chip, which must clear those buttons.
   *   --z-invite  #install, the offer to add the game to a home screen. The
   *               LOWEST thing on the page that is still a window, on purpose:
   *               nobody asked for it, so anything you actually opened -- help,
   *               a dialog, the loading curtain -- covers it.
   *   --z-win     A WINDOW YOU OPEN OUTRANKS THE HUD IT COVERS. #craft.
   *   --z-win-2   ...and a window opened FROM a window outranks it. #craftall.
   *   --z-panel   the centred panels: inventory, encyclopedia, settings.
   *   --z-panel-2 the big ones: operator console, help.
   *   --z-dialog  the dialogs: pause, and the help form riding on the help
   *               window. The two can never be open at once, so they share.
   *   --z-fatal   #lost, alone. NOTHING may cover the sentence that says the
   *               session ended -- not the pause dialog, not the help form.
   *   --z-turn    "turn your phone sideways". Over EVERYTHING in play, including
   *               the fatal notice -- a sentence you cannot read is not made
   *               readable by outranking the thing on top of it, and this is
   *               the one overlay that is about the DEVICE rather than the
   *               game. Under the door, which is not in play.
   *   --z-door    the sign-in card, which covers the whole game.
   *   --z-curtain the loading curtain, which covers the sign-in card too.
   *   --z-landfall the atmospheric-entry white, which covers even the
   *               curtain: an arrival is one unbroken sentence and the
   *               machinery loads BEHIND it.
   *
   * The gaps are on purpose: room to slip a layer in without renumbering.
   */
  --z-pad: 4;
  --z-flash: 5;
  --z-hudbtn: 6;
  --z-hudchip: 7;
  --z-invite: 8;
  --z-win: 10;
  --z-win-2: 12;
  --z-panel: 14;
  --z-panel-2: 15;
  --z-dialog: 16;
  --z-fatal: 17;
  --z-turn: 18;
  --z-door: 20;
  --z-curtain: 60;
  --z-landfall: 64;

  /* Type */
  --fs-s: calc(10 * var(--u));  /* small print, tool strips */
  --fs-m: calc(11 * var(--u));  /* body, buttons, headings */
  --fs-l: calc(12 * var(--u));  /* fields, running text */

  /* ------------------------------------------------------------------ *
   *  THE FACE. SquareFont is the interface font: every panel, button,   *
   *  HUD chip and editor inherits it through `--mono`. It is SERVED     *
   *  FROM THIS PROJECT (/fonts/Square.ttf) — never a CDN, so the game   *
   *  still runs with the network unplugged. See `fonts/README.md`.      *
   *                                                                     *
   *  The stack behind it is the real fallback, not decoration: it does  *
   *  the work whenever a glyph is missing. SquareFont is Latin-1 only,  *
   *  so the arrows, box-drawing and emoji the editors and menus use     *
   *  (→ ✓ ⌫ ⊙ ⛏ ─ │ 🗑) all come out of the fonts below, per character. *
   *  Drop the fallback and those turn into blank boxes.                 *
   * ------------------------------------------------------------------ */
  --mono: 'SquareFont', ui-monospace, "DejaVu Sans Mono", "Liberation Mono", "Courier New", monospace;

  /* The same stack WITHOUT SquareFont, for text where telling two
     characters apart matters more than the look. SquareFont is a square
     display face: `S` and `5` are the same shape, and so are `Z` and `2`.
     `server/maps.mjs` already drops I/O/0/1 from the operator-code
     alphabet "because it gets read aloud" — but S, 5, Z and 2 are all
     still IN that alphabet, so an operator code set in SquareFont cannot
     be read off a screen reliably. That, and only that, is what this is
     for. Measured, not guessed: see the specimen in the font note. */
  --mono-code: ui-monospace, "DejaVu Sans Mono", "Liberation Mono", "Courier New", monospace;
}

/* The app's own scale -- see the note beside `--ui` above. 1.2 against the
   desktop's 2: the owner tried 1.5 on his own phone and asked for smaller
   still -- the number below is his eye, not a guess. */
html.inapp {
  --ui: 1.2;
}

/* The interface font. `swap` on purpose: the text draws immediately in the
   fallback and re-draws when the file lands, so a slow or missing
   /fonts/Square.ttf can never leave the interface blank. */
@font-face {
  font-family: 'SquareFont';
  src: url('/fonts/Square.ttf') format('truetype');
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

* { box-sizing: border-box; }

body {
  font-family: var(--mono);
  /* Rule 4: pixel type, not smoothed type. */
  -webkit-font-smoothing: none;
  -moz-osx-font-smoothing: grayscale;
  font-smooth: never;
}

/* --- Type ---------------------------------------------------------------- */

h1, h2, h3 {
  font-family: var(--mono);
  text-transform: uppercase;
  letter-spacing: .12em;
  font-weight: 700;
}

.muted { color: var(--ink-soft); }
.note { font-size: var(--fs-m); line-height: 1.55; max-width: 72ch; color: var(--ink-soft); }
.val {
  font-size: var(--fs-m); color: var(--ink);
  background: var(--paper2); border: var(--b1) solid var(--ink);
  padding: calc(1 * var(--u)) calc(4 * var(--u));
  min-width: calc(42 * var(--u)); text-align: center; white-space: nowrap;
}

/* --- Windows ------------------------------------------------------------- */

.win {
  background: var(--paper);
  border: var(--b2) solid var(--ink);
  box-shadow: calc(5 * var(--u)) calc(5 * var(--u)) 0 var(--ink);
}

/* Full-colour title bar: name on the left in small caps, the three squares on
 * the right. This belongs to panels you OPEN -- never to the permanent HUD. */
.winbar {
  display: flex; align-items: center; gap: calc(8 * var(--u));
  background: var(--violet); color: var(--paper);
  border-bottom: var(--b2) solid var(--ink);
  padding: calc(4 * var(--u)) calc(5 * var(--u)) calc(4 * var(--u)) calc(8 * var(--u));
}
.winbar h1, .winbar h2 { margin: 0; font-size: var(--fs-m); color: var(--paper); }
.winbtns { display: flex; gap: calc(4 * var(--u)); margin-left: auto; }

/* A key hint sitting in a title bar, so a panel says how to dismiss itself
 * without growing a button that does what the key already does. Lived in
 * client/index.html until the help panel arrived: that one is mounted on the
 * door and in the lobby too, and a rule that dresses a shared panel cannot sit
 * on one page. The crafting and settings windows wear the same one. */
.keycap { margin-left: auto; background: var(--paper); color: var(--ink);
  border: var(--b1) solid var(--ink); padding: 0 calc(5 * var(--u));
  font-size: var(--fs-s); line-height: calc(18 * var(--u)); }

/* Each control in its own outlined square, drawn in CSS: no glyph, no font to
 * download, and the same shape at every scale. */
.winbtn {
  position: relative;
  width: calc(20 * var(--u)); height: calc(20 * var(--u));
  flex: 0 0 calc(20 * var(--u)); padding: 0;
  background: var(--lav); border: var(--b1) solid var(--ink);
  box-shadow: none; cursor: pointer;
}
.winbtn:hover { background: var(--turq); }
.winbtn:active { transform: translate(var(--u), var(--u)); box-shadow: none; }
/* A window that has nothing to reduce or close still wants the frame. Written
 * as a <span> rather than a <button>, it is chrome and cannot be clicked: a
 * dead button is worse than an honest ornament. */
span.winbtn { pointer-events: none; }
.winbtn.close { background: var(--magenta); }
.winbtn.close:hover { background: var(--red); }
/* CENTRED BY THE BOX MODEL, never by an offset measured by hand.
 *
 * Every one of these used to carry a `left` and a `top` tuned by eye -- and they
 * were tuned against a padding box that is NOT `20 * var(--u)`. The button
 * scales with `--u`; its border is `--b1`, which does not. So the room inside it
 * shrinks by a different amount than the glyph does, and a number that looked
 * right at one scale drifts at another.
 *
 * Measured, at the operator console's own `--u` of 1.6: the enlarge box sat
 * 3.6 px from the left and -0.4 from the right -- overflowing -- and flat
 * against the floor. All three were off-centre by 1.6 px or more.
 *
 * `inset: 0` with `margin: auto` and a definite size centres a positioned box
 * exactly, whatever the border around it is doing. There is no number to keep in
 * step because there is no number. */
.winbtn::before, .winbtn::after {
  content: '';
  position: absolute;
  background: var(--ink);
}
/* ...and the centring goes on the ones that HAVE a size, never on the shared
 * rule above. `inset: 0` on a box with `width: auto` does not centre it, it
 * STRETCHES it -- and `min` and `max` each have an `::after` with no size at
 * all, which was invisible only because nothing had ever positioned it. Put on
 * the shared rule, it filled both buttons solid black. Measured by looking. */
.winbtn.min::before,
.winbtn.max::before,
.winbtn.close::before,
.winbtn.close::after { inset: 0; margin: auto; }
/* reduce: a bar on the floor. Centred across, and pushed down to the floor by
 * the one margin that is not `auto`. */
.winbtn.min::before {
  width: calc(10 * var(--u));
  height: calc(2 * var(--u));
  margin-bottom: calc(3 * var(--u));
}
/* enlarge: an empty box, dead centre */
.winbtn.max::before {
  width: calc(9 * var(--u));
  height: calc(9 * var(--u));
  background: none;
  border: var(--b1) solid var(--ink);
}
/* close: two bars crossed, both centred, so the cross is centred */
.winbtn.close::before, .winbtn.close::after {
  width: calc(13 * var(--u));
  height: calc(2 * var(--u));
}
.winbtn.close::before { transform: rotate(45deg); }
.winbtn.close::after { transform: rotate(-45deg); }

/* --- Buttons ------------------------------------------------------------- */

button {
  font-family: var(--mono); font-size: var(--fs-m); line-height: 1.3;
  text-transform: uppercase; letter-spacing: .06em;
  background: var(--lav); color: var(--ink);
  border: var(--b1) solid var(--ink); border-radius: 0;
  padding: calc(5 * var(--u)) calc(9 * var(--u)); cursor: pointer;
  box-shadow: var(--drop) var(--drop) 0 var(--ink);
}
button:hover { background: var(--turq); }
/* Rule 3: pressed is a real displacement, not a soft glow. */
button:active { transform: translate(var(--drop), var(--drop)); box-shadow: none; }
button:disabled { background: var(--paper2); color: var(--ink-soft); box-shadow: none; cursor: default; }
button.on { background: var(--violet); color: var(--paper); }
button.primary { background: var(--orange); font-weight: 700; }
/* The one button in this project that destroys something. It is the DANGER red
 * of `#lost`'s bar rather than an accent, it is never `primary`, and it is
 * never the biggest thing in its dialog -- see `.confirm` below and the rule
 * the pause dialog settled: the safe half gets the reflex press. */
button.danger { background: var(--red); color: var(--paper); font-weight: 700; }
button.danger:hover { background: var(--magenta); color: var(--ink); }
button.danger:disabled { background: var(--paper2); color: var(--ink-soft); box-shadow: none; }
button.tiny { padding: calc(2 * var(--u)) calc(6 * var(--u)); box-shadow: var(--b1) var(--b1) 0 var(--ink); }
button.tiny:active { transform: translate(var(--b1), var(--b1)); }

/* --- Fields -------------------------------------------------------------- */

input, select, textarea {
  font-family: var(--mono); font-size: var(--fs-l); line-height: 1.3;
  background: var(--paper); color: var(--ink);
  border: var(--b1) solid var(--ink); border-radius: 0;
  padding: calc(5 * var(--u)) calc(7 * var(--u));
}
input:focus, select:focus, textarea:focus {
  outline: var(--b1) solid var(--magenta); outline-offset: 0;
}
input::placeholder { color: var(--ink-soft); }

/* The caret is drawn, like everything else. */
select {
  appearance: none; -webkit-appearance: none;
  padding-right: calc(22 * var(--u)); cursor: pointer;
  background-color: var(--paper);
  background-image:
    linear-gradient(45deg, transparent 50%, var(--ink) 50%),
    linear-gradient(135deg, var(--ink) 50%, transparent 50%);
  background-position:
    calc(100% - 14 * var(--u)) calc(50% + 1 * var(--u)),
    calc(100% - 9 * var(--u)) calc(50% + 1 * var(--u));
  background-size: calc(5 * var(--u)) calc(5 * var(--u)), calc(5 * var(--u)) calc(5 * var(--u));
  background-repeat: no-repeat;
}

input[type=number] { padding-right: calc(2 * var(--u)); }

input[type=color] {
  padding: var(--b1); width: calc(34 * var(--u)); height: calc(26 * var(--u));
  background: var(--paper); cursor: pointer;
}
input[type=color]::-webkit-color-swatch-wrapper { padding: 0; }
input[type=color]::-webkit-color-swatch { border: var(--u) solid var(--ink); }

/* Checkbox: an outlined square that fills in, with a drawn tick. */
input[type=checkbox] {
  appearance: none; -webkit-appearance: none;
  position: relative; flex: 0 0 auto;
  width: calc(15 * var(--u)); height: calc(15 * var(--u)); padding: 0;
  background: var(--paper); border: var(--b1) solid var(--ink); cursor: pointer;
}
input[type=checkbox]:checked { background: var(--magenta); }
input[type=checkbox]:checked::after {
  content: ''; position: absolute; left: calc(3 * var(--u)); top: 0;
  width: calc(4 * var(--u)); height: calc(8 * var(--u));
  border: solid var(--ink); border-width: 0 calc(3 * var(--u)) calc(3 * var(--u)) 0;
  transform: rotate(42deg);
}

/* Slider: outlined rail, solid square handle. Rule 1 applies to controls too. */
input[type=range] {
  appearance: none; -webkit-appearance: none;
  background: transparent; border: none; padding: 0;
  height: calc(18 * var(--u)); cursor: pointer;
}
input[type=range]::-webkit-slider-runnable-track {
  height: calc(10 * var(--u)); background: var(--paper2); border: var(--b1) solid var(--ink);
}
input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: calc(12 * var(--u)); height: calc(18 * var(--u)); margin-top: calc(-6 * var(--u));
  background: var(--violet); border: var(--b1) solid var(--ink);
}
input[type=range]:active::-webkit-slider-thumb { background: var(--magenta); }
input[type=range]::-moz-range-track {
  height: calc(10 * var(--u)); background: var(--paper2); border: var(--b1) solid var(--ink);
}
input[type=range]::-moz-range-thumb {
  width: calc(12 * var(--u)); height: calc(18 * var(--u)); border-radius: 0;
  background: var(--violet); border: var(--b1) solid var(--ink);
}

/* --- Scrollbars ---------------------------------------------------------- */

* { scrollbar-color: var(--violet) var(--paper2); scrollbar-width: auto; }
::-webkit-scrollbar { width: calc(14 * var(--u)); height: calc(14 * var(--u)); }
::-webkit-scrollbar-track { background: var(--paper2); border: var(--b1) solid var(--ink); }
::-webkit-scrollbar-thumb { background: var(--violet); border: var(--b1) solid var(--ink); }
::-webkit-scrollbar-thumb:hover { background: var(--magenta); }
::-webkit-scrollbar-corner { background: var(--paper2); }

/* --- Shared component classes -------------------------------------------- */
/* These names are load-bearing: `opmenu.js`, `softedit.js` and the bench all
 * build DOM with them. Change what they LOOK like here; do not rename them. */

.oprow { display: flex; gap: calc(6 * var(--u)); align-items: center; margin: calc(5 * var(--u)) 0; }
.oprow.wrap { flex-wrap: wrap; }
/* Fills a narrow pane, but a slider a metre long in a full-width console reads
 * as a progress bar, not as a control. */
.oprow input[type=range] {
  flex: 1 1 calc(120 * var(--u)); min-width: calc(60 * var(--u)); max-width: calc(260 * var(--u));
}
/* `min-width: auto` is the flex default, and a number input's own minimum is
 * about twenty characters wide -- which is why these boxes used to be three
 * times their declared size. */
.oprow input.num, input[type=number] { min-width: 0; }
.oprow input.num { flex: 0 0 calc(66 * var(--u)); width: calc(66 * var(--u)); }
/* Same trap, other end: a bare <input> is about twenty characters wide by
 * default, which at this scale is wider than the pane it sits in. Text fields
 * share the row rather than setting its width. */
.oprow input:not([type]), .oprow input[type=text], .oprow input[type=search] {
  flex: 1 1 calc(90 * var(--u)); min-width: 0;
}
.oprow select { min-width: 0; max-width: 100%; }

/* A label that behaves like a checkbox chip. */
.opcheck {
  display: inline-flex !important; gap: calc(5 * var(--u)); align-items: center;
  background: var(--paper2); border: var(--b1) solid var(--ink);
  padding: calc(3 * var(--u)) calc(7 * var(--u)) calc(3 * var(--u)) calc(5 * var(--u));
  margin: 0 !important;
  font-size: var(--fs-m) !important; color: var(--ink) !important; cursor: pointer;
}
.opcheck:hover { background: var(--turq); }
.opcheck.tight { flex: 0 0 auto; }
.opcheck span.muted { color: var(--ink-soft); font-size: var(--fs-m); }

/* The verdict line: silence, then green or red, never both. */
.opreport {
  margin-top: calc(8 * var(--u)); font-size: var(--fs-m); line-height: 1.55;
  border: var(--b1) solid var(--ink); background: var(--paper2);
  padding: calc(5 * var(--u)) calc(7 * var(--u)); color: var(--ink);
}
.opreport.good { background: var(--green); color: var(--paper); }
.opreport.bad { background: var(--red); color: var(--paper); }

/* A colour chip in a palette. Square, outlined, and it grows a frame when picked. */
.swatch {
  width: calc(26 * var(--u)); height: calc(26 * var(--u)); padding: 0; flex: 0 0 auto;
  border: var(--b1) solid var(--ink); border-radius: 0;
  box-shadow: none; cursor: pointer;
}
.swatch:hover { transform: none; }
.swatch:active { transform: translate(var(--u), var(--u)); box-shadow: none; }
.swatch.on { box-shadow: 0 0 0 var(--b1) var(--magenta), 0 0 0 calc(2 * var(--b1)) var(--ink); }

/* A tool strip: buttons packed tight, the armed one filled violet. */
.edtools { gap: calc(5 * var(--u)); }
.edtools button {
  font-size: var(--fs-s); padding: calc(4 * var(--u)) calc(7 * var(--u));
  box-shadow: var(--b1) var(--b1) 0 var(--ink);
}
.edtools button:active { transform: translate(var(--b1), var(--b1)); box-shadow: none; }

/* The avatar editor's bone tree (`rigedit.js`): an outlined list box, one full
 * row per bone, indented in the row's own padding. Dressed here rather than in
 * that file so it follows `--ui` like everything else. */
.rigtree {
  display: flex; flex-direction: column; gap: 0;
  background: var(--paper); border: var(--b1) solid var(--ink);
  max-height: calc(240 * var(--u)); overflow: auto; margin: calc(6 * var(--u)) 0;
}
.rigtree .rignode {
  text-align: left; text-transform: none; letter-spacing: 0;
  background: var(--paper); border: 0; border-bottom: var(--u) solid var(--ink);
  box-shadow: none; padding-top: calc(4 * var(--u)); padding-bottom: calc(4 * var(--u));
}
.rigtree .rignode:last-child { border-bottom: 0; }
.rigtree .rignode:hover { background: var(--turq); }
.rigtree .rignode:active { transform: none; }
.rigtree .rignode.on { background: var(--violet); color: var(--paper); }
.rigtree .rignode.on .muted { color: var(--paper2); }
/* A socket is a role, so it reads as a badge and not as more of the name. */
.rigsock {
  font-style: normal; background: var(--turq); color: var(--ink);
  border: var(--u) solid var(--ink); padding: 0 calc(3 * var(--u));
  margin-left: calc(4 * var(--u)); font-size: var(--fs-s);
}

/* A small square item/avatar icon, outlined like everything else. Used instead
 * of a bare identifier wherever a thing is listed. */
.ico {
  display: inline-block; flex: 0 0 auto;
  width: calc(14 * var(--u)); height: calc(14 * var(--u));
  border: var(--b1) solid var(--ink); background: var(--lav);
}

/* The sound chip (`#sound`, `.sndbtn`) lived here, bottom-left of the HUD,
 * until the owner asked for the corner back. The volume is a row in the
 * settings screen now (see `.setrow` below); `M` still mutes without a menu. */

/* --- The vitals gauges (`client/src/gauges.js`) --------------------------- */
/* Breath and magical flight, stacked directly over the health bar, which is
 * their model in every respect: same width, same `--b1` outline, same hard
 * shadow, same flat fill ticked in ten so it can be counted rather than
 * estimated. Three bars read at a glance have to be one shape in three colours.
 *
 * The numbers come from the server (`server/vitals.mjs`); this file only
 * decides what they look like. `--slot` is the hotbar's own size, declared in
 * `client/index.html`, so the whole stack keeps its proportions on the one
 * `--ui` dial and on the viewport cap alike -- there is not a pixel here.
 *
 * `display` is toggled per bar by `gauges.js`, not by a media query: a full
 * breath bar on dry land is furniture, and furniture on a HUD is noise. */

#vitals {
  position: fixed; left: 50%; bottom: calc(var(--slot) * 1.55);
  transform: translateX(-50%);
  display: flex; flex-direction: column; gap: calc(3 * var(--u));
}
#vitals .gauge {
  width: calc(var(--slot) * 3.8); height: calc(var(--slot) * 0.2);
  background: var(--paper2);
  border: var(--b1) solid var(--ink); box-shadow: var(--b1) var(--b1) 0 var(--ink);
  overflow: hidden;
}
#vitals .gauge i { display: block; height: 100%; width: 100%; }
/* Air: the only cyan in the palette, which is what water gets to be here. */
#vitals .air i {
  background: repeating-linear-gradient(90deg,
    var(--turq) 0 calc(var(--slot) * 0.34), #17998e calc(var(--slot) * 0.34) calc(var(--slot) * 0.38));
}
/* Magic: violet, the accent this skin already spends on "chosen" -- and flight
 * is the one state you choose to be in. */
#vitals .fly i {
  background: repeating-linear-gradient(90deg,
    var(--violet) 0 calc(var(--slot) * 0.34), #5327a5 calc(var(--slot) * 0.34) calc(var(--slot) * 0.38));
}
/* The last quarter. A flat swap to the palette's "refused" red, not a fade:
 * rule 3, and a colour that already means trouble everywhere else in the set. */
#vitals .gauge.low i {
  background: repeating-linear-gradient(90deg,
    var(--red) 0 calc(var(--slot) * 0.34), #a82a1c calc(var(--slot) * 0.34) calc(var(--slot) * 0.38));
}

/* --- Rounds left (`client/src/guns.js`) ----------------------------------- */
/* To the RIGHT of the quick bar rather than above it, and deliberately not in
 * the stack with the two gauges: breath and flight are fractions and read as
 * bars, a magazine is a COUNT and reads as a number. Putting a number in that
 * column would make three unlike things look like one family.
 *
 * Built by the module, like the bars, so `client/index.html` gains no empty div
 * for a feature a map can switch off — and it is hidden outright whenever the
 * hand holds anything but a firearm. */

#ammo {
  position: fixed; right: calc(var(--slot) * 0.6); bottom: calc(var(--slot) * 1.5);
  display: flex; align-items: baseline; gap: calc(4 * var(--u));
  padding: calc(4 * var(--u)) calc(8 * var(--u));
  background: var(--paper2);
  border: var(--b1) solid var(--ink); box-shadow: var(--b1) var(--b1) 0 var(--ink);
}
#ammo b {
  font-size: calc(var(--slot) * 0.42); line-height: 1;
  font-variant-numeric: tabular-nums;    /* 30 -> 9 must not shuffle the row */
  color: var(--ink);
}
#ammo i { font-style: normal; font-size: calc(var(--slot) * 0.2); color: var(--ink-soft); }
/* The last fifth, and empty. The same flat swap to the palette's "refused" red
 * the gauges make, for the same reason: rule 3, and a colour that already means
 * trouble everywhere else in the set. */
#ammo.low b { color: var(--red); }

/* --- What has been drunk (`client/src/potions.js`) ------------------------ */
/* One chip per running draught, in a row above the two bars. Built by the
 * module rather than written into index.html, for the reason the bars are: one
 * `--ui` dial, one place, nothing to keep in sync.
 *
 * The effect's own colour arrives as `--tint` and `--glow` -- from
 * shared/potion.js, which is where an effect's identity lives -- so a chip is
 * the same shape in seven colours rather than seven rules here. Rule 1 still
 * holds: outlined in black, hard shadow, no gradient on the card itself. */

#draughts {
  position: fixed; left: 50%; bottom: calc(var(--slot) * 2.05);
  transform: translateX(-50%);
  display: none; gap: calc(3 * var(--u));
}
#draughts .draught {
  display: flex; align-items: center; gap: calc(2 * var(--u));
  padding: calc(2 * var(--u)) calc(4 * var(--u));
  background: var(--paper2);
  border: var(--b1) solid var(--ink); box-shadow: var(--b1) var(--b1) 0 var(--ink);
  font-family: var(--mono); font-size: var(--fs-s);
  text-transform: uppercase; letter-spacing: .06em; white-space: nowrap;
}
/* The bead: the one place the effect's colour is spent, so seven chips in a row
 * are told apart by a dot rather than by seven coloured cards shouting. */
#draughts .draught i {
  width: calc(var(--slot) * 0.16); height: calc(var(--slot) * 0.16);
  background: var(--glow); border: var(--b1) solid var(--ink);
}
#draughts .draught b { color: var(--tint); }

/* --- The inventory panel (`client/src/inventory.js`) ---------------------- */
/* That file builds its own DOM rather than writing markup into the page, so its
 * dressing lives here with every other panel's -- one place, one `--ui` dial,
 * and nothing to keep in sync in `client/index.html`.
 *
 * The bag and the quick bar are drawn as ONE outlined box with black gaps
 * showing between the cells, rather than as 36 floating cards: rule 1 says
 * everything is outlined in black, and a grid of separate outlines would read
 * as confetti. The gap IS the outline -- the container is inked and the cells
 * sit on top of it. */

#inventory {
  /* One cell. Nine columns of it, so the bag is exactly as wide as the bar
   * underneath and "this column feeds that cell" needs no explaining. The
   * viewport term is the same idea as the HUD's, one notch narrower because the
   * panel also pays for its own frame. */
  --cell: min(calc(44 * var(--u)), 7vw);
  /* Dropped clear of the debug strip, because a title bar you cannot read is
   * not a title bar. It is NOT held above the HUD quick bar, and that is a
   * decision rather than an oversight: while this panel is open its own bar
   * strip is that bar -- same nine cells, same gold, same digits -- so keeping
   * both on screen buys a duplicate at the price of the four rows this panel
   * exists to show. The bag scrolls if even this is not enough. */
  position: fixed; left: 50%; top: calc(64 * var(--u)); transform: translateX(-50%);
  z-index: var(--z-panel); display: none; flex-direction: column;
  max-width: 96vw; max-height: calc(100vh - 88 * var(--u));
}
#inventory.open { display: flex; }

/* The body itself never scrolls: the bag does. Whatever the screen height, the
 * quick bar and the line that explains it stay put, because a panel that hides
 * the thing it is for is worse than a panel that scrolls. */
.invbody {
  padding: calc(12 * var(--u)); background: var(--paper); overflow: hidden;
  display: flex; flex-direction: column; align-items: flex-start; min-height: 0;
}
.invbag { overflow: auto; min-height: 0; flex: 0 1 auto; max-width: 100%; }
.invbody h3 {
  margin: calc(7 * var(--u)) 0 calc(4 * var(--u));
  padding: calc(2 * var(--u)) calc(8 * var(--u)); font-size: var(--fs-m);
  background: var(--gold); color: var(--ink); border: var(--b1) solid var(--ink);
}
/* Small print, and it earns the space it takes: it is the only text that says
 * the bar is something you choose rather than something you are given. */
.invbody .note {
  margin: calc(6 * var(--u)) 0 0; font-size: var(--fs-s); line-height: 1.5;
  max-width: calc(var(--cell) * 9);
}

.invgrid, .invbar {
  display: grid; grid-template-columns: repeat(9, var(--cell));
  gap: var(--b1); padding: var(--b1);
  background: var(--ink); border: var(--b2) solid var(--ink);
}

.invslot, .invcell {
  position: relative; height: calc(var(--cell) * 0.93); min-width: 0;
  background: var(--paper2); color: var(--ink);
  display: flex; flex-direction: column; justify-content: flex-end;
  cursor: pointer; overflow: hidden;
}
/* A stack that answers to a quick-bar key wears a gold band, exactly like the
 * active console tab: "which of my things are within reach" is answered by
 * looking, not by counting. */
.invslot.bound, .invcell.bound { box-shadow: inset 0 calc(3 * var(--u)) 0 var(--gold); }
/* Gold fill = the one in hand, the same signal the HUD bar uses. */
.invslot.on, .invcell.on { background: var(--gold); }
/* Violet = picked up and waiting for somewhere to go (rule 5). */
.invslot.picked, .invcell.picked { background: var(--violet); color: var(--paper); }
/* Turquoise = the hover colour of every other control here. */
.invslot.over, .invcell.over { background: var(--turq); }

.invslot b, .invcell b {
  flex: 1; display: flex; align-items: center; justify-content: center;
  font-size: calc(var(--cell) * 0.30);
}
.invslot .n, .invcell .n {
  display: block; padding: 0 calc(2 * var(--u));
  font-size: calc(var(--cell) * 0.16); line-height: 1.6; text-align: center;
  border-top: var(--b1) solid var(--ink); color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The key that holds this stack, printed on the stack. It is the same badge on
 * both grids, which is what ties a cell to the slot it points at. */
.invslot i, .invcell i {
  position: absolute; left: 0; top: 0; font-style: normal;
  padding: 0 calc(3 * var(--u)); font-size: var(--fs-s); line-height: 1.5;
  background: var(--ink); color: var(--paper);
}
.invslot i:empty, .invcell i:empty { display: none; }

/* --- The picture of the thing (`client/src/itemshot.js`) ------------------ *
 *
 * « J'aimerais que dans les items de l'inventaire et quickbar, on ait des images
 *   des objets. Pas sur fond noir comme l'encyclopédie. Mais sur fond
 *   transparent. »
 *
 * There is no fill anywhere in this block, and that is the whole of the ask.
 * The PNG the painter hands over is transparent everywhere the object is not,
 * so what shows behind a pickaxe is `--paper2` in an ordinary cell, `--gold` in
 * the one in hand, `--violet` under a pick-up and `--turq` under a drag -- four
 * states the object goes on being legible in because it never brought a
 * rectangle with it. `.dexpic` next door does the opposite on purpose, and it is
 * right to: a bestiary tile is a photograph and wants a plate under it. A cell
 * is paper that changes colour, and wants nothing.
 *
 * IN FLOW, not laid over the cell. The picture takes the space the big centred
 * count used to have, which is exactly the space between the badge and the name
 * strip -- so nothing has to know how tall the strip is, and the strip can go on
 * deriving its own size from `--cell` without a second copy of that arithmetic
 * living here to keep the picture clear of it. `order: -1` is what puts it above
 * the strip in a column that is otherwise laid out from the bottom up.
 *
 * ONE rule for both grids and for the bar drawn across the world, which is the
 * CSS half of "one painter, not two". Where a rule below has to reach into
 * `#hotbar .slot` it is written with the id in front: that row is dressed in
 * `client/index.html`, which is linked after this sheet, so a bare class cannot
 * win against it. */
.invpic {
  order: -1; flex: 1 1 0; min-height: 0; min-width: 0; width: 100%;
  object-fit: contain;
  /* The cell owns every gesture there is here -- click to pick, double-click to
   * eat, drag to move. An image that took a click would swallow the first two,
   * and one the browser thought was draggable would offer a PNG to the desktop
   * instead of offering this panel a stack. */
  pointer-events: none;
}
/* A cell that shows its object no longer needs to shout its count across the
 * middle of it. The number moves to the far corner from the quick-bar badge and
 * wears the same ink chip, so the two read as a pair: on the left the key that
 * reaches this stack, on the right how many of it there are. */
.invslot.pic > b, .invcell.pic > b, #hotbar .slot.pic > b {
  position: absolute; right: 0; top: 0; flex: none;
  padding: 0 calc(3 * var(--u));
  font-size: var(--fs-s); line-height: 1.5;
  background: var(--ink); color: var(--paper);
}
/* ...which needs a corner to be absolute against. `.invslot` has always had one;
 * the HUD slot only had one while it was the selected cell. */
#hotbar .slot { position: relative; }

/* ---- A cell on a phone, where a cell is 24 pixels ------------------------ *
 *
 * Everything printed in a cell is sized off `--cell`, and on a desktop that is
 * exactly right: `.invgrid` is `repeat(9, var(--cell))`, so `--cell` IS the
 * width of the square and `calc(var(--cell) * 0.16)` is sixteen percent of it.
 *
 * The touch bag is TWELVE columns of `1fr` (client/index.html, and the note
 * there says why twelve). `--cell` still describes a ninth of the panel, so
 * every one of those fractions is a THIRD too big for the square it is drawn
 * in: on a 375-pixel phone the cell measures 24.5 and the name strip is sized
 * for 36.8, which leaves eleven pixels for the picture. The quick-bar badge is
 * worse -- it is `--fs-s`, a fixed twenty pixels, in a twenty-four pixel square,
 * and it was already like that before there was anything behind it to hide.
 *
 * So a cell becomes a CONTAINER and what is inside it is sized off the cell.
 * `inline-size` rather than `size`: the width is what all of this was ever a
 * fraction of, and asking for size containment would additionally require every
 * cell to have a definite height, which is a promise about a layout this sheet
 * does not own. Nothing on a desktop moves by a pixel -- there `1cqw` and
 * `var(--cell) / 100` are the same length -- which is why these are scoped to
 * `body.onpad` rather than replacing the rules above.
 *
 * The two chips are capped rather than scaled outright, so the badge keeps the
 * `--fs-s` it has always had wherever there is room for it. */
body.onpad .invslot, body.onpad .invcell, body.onpad #hotbar .slot {
  container-type: inline-size;
}
body.onpad .invslot > b, body.onpad .invcell > b { font-size: 30cqw; }
body.onpad .invslot > .n, body.onpad .invcell > .n { font-size: 16cqw; }
body.onpad .invslot > i, body.onpad .invcell > i,
body.onpad .invslot.pic > b, body.onpad .invcell.pic > b,
body.onpad #hotbar .slot.pic > b {
  font-size: min(var(--fs-s), 24cqw);
}
/* ...AND THE BAR ACROSS THE WORLD GIVES UP ITS LABEL FOR THE PICTURE.
 *
 * `--slot` is capped at `8vw`, so on a 375-pixel phone a quick-bar cell is 30
 * pixels wide with a six-pixel border on each side: eighteen across and sixteen
 * down to hold everything. The name printed across the bottom of it is
 * four-and-a-half pixel type -- it has never been readable there, on any phone,
 * and it was taking eleven of those sixteen pixels. Measured: the picture was
 * left five pixels tall.
 *
 * The strip keeps its COLOUR and gives up its text, which is the right half to
 * keep: the colour is the part that was still doing work at this size -- a plant
 * wears its crown there, and that is the whole signal a forager reads a species
 * by -- while the word it was printed under says nothing. The object above it
 * now says what the word was for. Only where there IS an object: an item with no
 * geometry keeps its label, because the label is all it has. */
body.onpad #hotbar .slot.pic > .n {
  font-size: 0; line-height: 0; height: 16cqw; padding: 0;
}

/* --- The encyclopedia (`client/src/dex.js`) ------------------------------- */
/* Same arrangement as the inventory above: that file builds its own DOM, so its
 * dressing lives here rather than in `client/index.html`, and follows the one
 * `--ui` dial like everything else.
 *
 * The panel has to hold four hundred squares without becoming a spreadsheet, so
 * three decisions carry it:
 *
 *   - the family tabs sit ON the title bar's rail, wearing their own tally.
 *     That is the operator console's idiom (`#opTabs` in client/index.html) and
 *     it is here for the same reason: on a four-section panel, "which section am
 *     I in" and "how far along am I" must both be answerable without reading.
 *   - the wall of tiles is ONE inked box with the gaps showing through, exactly
 *     like the bag: rule 1 says everything is outlined, and four hundred
 *     separate outlines would read as confetti.
 *   - a found tile is paper with its family's colour banded across the top; a
 *     locked one is flat lavender with a single '?'. The difference has to be
 *     visible at a glance from across the grid, because that contrast IS the
 *     thing that makes somebody go looking. */

#dex {
  /* Dropped clear of the debug strip, for the same reason the inventory is: a
   * title bar you cannot read is not a title bar. */
  position: fixed; left: 50%; top: calc(64 * var(--u)); transform: translateX(-50%);
  z-index: var(--z-panel); display: none; flex-direction: column;
  width: min(calc(940 * var(--u)), 96vw);
  /* A DEFINITE height, not a cap, and that is the whole layout.
   *
   * A percentage max-height only resolves against a containing block whose own
   * height is definite, so with `max-height` here the two panes below had
   * nothing to be a percentage OF -- they grew to four hundred tiles tall and
   * the page beside them went with them. Giving the window a real height makes
   * `max-height: 100%` on the wall and the page mean "the row you are in", which
   * is what puts a scrollbar on each of them instead of on the panel.
   *
   * It is also right on its own terms: a bag is small and may be content-sized,
   * a wall of four hundred squares wants the room whether or not it is full. The
   * second term stops it from becoming absurd on a very tall screen. */
  height: min(calc(100vh - 88 * var(--u)), calc(520 * var(--u)));
}
#dex.open { display: flex; }

/* Tabs, not buttons: no bottom edge, no drop shadow, and the active one is the
 * only paper-coloured thing on a violet bar. Kept in step with `#opTabs`. */
.dextabs {
  /* Grows into the bar's free space rather than being squeezed by the title:
   * four tabs that wrap onto a second row make the title bar twice as tall, and
   * this panel needs that height for the wall. */
  display: flex; align-items: flex-end; align-self: flex-end; flex: 1 1 auto;
  gap: calc(3 * var(--u)); flex-wrap: wrap; min-width: 0;
  margin-bottom: calc(-4 * var(--u));
}
/* Small print on the tabs, and tight: four of them plus a title have to sit on
 * ONE row of the bar, or the panel loses a seventh of its height to chrome. */
.dextabs button {
  display: flex; align-items: baseline; gap: calc(4 * var(--u));
  font-size: var(--fs-s);
  box-shadow: none; background: var(--lav); border-bottom: 0;
  padding: calc(5 * var(--u)) calc(6 * var(--u)) calc(6 * var(--u));
}
.dextabs button:hover { background: var(--turq); }
.dextabs button:active { transform: none; }
.dextabs button.on {
  background: var(--paper); color: var(--ink); font-weight: 700;
  padding-bottom: calc(10 * var(--u));
  box-shadow: inset 0 calc(4 * var(--u)) 0 var(--gold);
}
/* The tally is the point of the tab, so it is gold and it is not the label. */
.dextabs button b {
  font-size: var(--fs-s); background: var(--ink); color: var(--gold);
  padding: 0 calc(4 * var(--u));
}

.dexbody {
  padding: calc(12 * var(--u)); background: var(--paper);
  display: flex; flex-direction: column; flex: 1 1 auto;
  min-height: 0; overflow: hidden;
}
.dextools { margin: 0 0 calc(10 * var(--u)); }
.dextools .val { margin-left: auto; }

/* The wall and the page, side by side -- and the page WRAPS under the wall when
 * there is no room for both.
 *
 * Flex bases in `--u` rather than a pixel media query, which is the same choice
 * the weapon workshop made and for the same reason: the breakpoint is "does a
 * usable page still fit", it is expressed in the interface's own unit, and it
 * follows `--ui` with nothing to keep in sync. */
/* The bases are what decide the breakpoint, and they are small on purpose: two
 * panes side by side is the arrangement this panel is for, so they only wrap
 * once a usable page genuinely stops fitting. `.dexmain` takes the scroll for
 * that wrapped case; side by side, each pane scrolls in its own box. */
.dexmain {
  /* Takes every pixel the search row and the footnote do not: those two are
   * fixed furniture, the wall is what the panel is for. */
  display: flex; flex-wrap: wrap; gap: calc(10 * var(--u)); flex: 1 1 auto;
  min-height: 0; overflow: auto;
}

.dexgrid {
  flex: 3 1 calc(220 * var(--u)); min-width: 0;
  display: grid; gap: var(--b1); padding: var(--b1);
  grid-template-columns: repeat(auto-fill, minmax(calc(64 * var(--u)), 1fr));
  align-content: start;
  background: var(--ink); border: var(--b2) solid var(--ink);
  /* `100%` of the row, not a slice of the viewport: side by side, each pane
   * scrolls inside its own box and BOTH stay on screen. A viewport cap here let
   * the two panes add up to more than the panel and pushed the card out of it. */
  overflow: auto; min-height: 0; max-height: 100%;
}
.dexgrid .note { grid-column: 1 / -1; background: var(--paper); margin: 0; padding: calc(8 * var(--u)); }
/* The book, shelved by WORLD: one full-width rule per world with its name on
 * it, drawn only when the book actually spans more than one. See the grouping
 * in client/src/dex.js and `p` on a wire entry in server/dex.mjs. */
.dexworld {
  grid-column: 1 / -1; background: var(--paper); color: var(--ink);
  margin: 0; padding: calc(3 * var(--u)) calc(8 * var(--u));
  font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase;
  font-size: calc(11 * var(--u));
}

/* Packed from the top with a gap, NOT `space-between`: a button clips its own
 * content box, and a column that distributes space it does not have pushes its
 * first child out through the top edge -- which read as every name on the wall
 * being sliced in half. */
.dextile {
  position: relative; display: flex; flex-direction: column;
  justify-content: flex-start; gap: calc(3 * var(--u));
  min-width: 0; text-align: left;
  text-transform: none; letter-spacing: 0; font-size: var(--fs-s);
  background: var(--paper); color: var(--ink);
  border: 0; border-top: calc(4 * var(--u)) solid var(--fam, var(--lav));
  box-shadow: none; padding: calc(5 * var(--u)) calc(6 * var(--u));
}
.dextile:hover { background: var(--turq); }
.dextile:active { transform: none; }
.dextile.on { background: var(--violet); color: var(--paper); }
.dexname {
  display: block; flex: 0 0 auto;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--fs-s); line-height: 1.4;
}
/* --- The ground a creature is standing on --------------------------------- */
/* « J'aimerais une gradient radius (foncé central à pâle sur les bords) pour
 *   montrer les monstres, l'item, etc. [...] Selon son environnement -- désert,
 *   la couleur du dégradé devrait être celle du sable ; neige blanc ; gazon
 *   vert ; forêt vert ; eau bleu ; cave noir ; lave orange, etc. » -- the owner.
 *
 * WHICH colour is `shared/habitat.js`'s answer, deduced from where the species
 * says it lives and read out of the live block registry; `client/src/dex.js`
 * puts it on the element as `--hab` and nothing else. This rule owns only the
 * SHAPE of the light, which is dressing and belongs in the stylesheet with
 * every other panel's.
 *
 * -- and it is the other way round from the sentence above ------------------
 *
 * Rendered both ways and looked at, side by side, on six species from six
 * countries at both sizes. Dark-in-the-middle loses the picture twice over:
 *
 *   - most of the bestiary is a dark animal. A boar, a cave bear and a howler
 *     ape all sit in the darkest part of their own plate and stop having a
 *     silhouette -- which is the one thing a 64-pixel tile has to give you.
 *   - the CAST SHADOW disappears. It is a black ellipse, so it needs light
 *     under the feet to be a shadow at all; on a dark centre there is nothing
 *     for it to darken, and the depth the owner asked for goes with it.
 *
 * Turned round, both come back: the subject stands in the brightest, most
 * saturated patch of its own country, its shadow reads as a shadow, and the rim
 * falls away so the corners recede. That is the effect he described -- « pour
 * donner l'effet de profondeur et de 3D réel » -- and it is what his sentence
 * gets you by being inverted. His reading is not lost either: the darkest thing
 * in the middle of the picture is the shadow, and it is right under the animal.
 *
 * Two translucent layers over one flat colour, so the falloff is written once
 * and the only thing that varies per species is `--hab`. It costs NOTHING per
 * frame: this is the element's background, painted by the compositor, and not a
 * single pixel of it goes through the renderer -- which is what lets the wall
 * keep the "one canvas, one draw per body, ever" argument dex.js opens with. */
.dexpic, .dexshot {
  background:
    radial-gradient(ellipse 74% 72% at 50% 38%,
      rgba(255, 255, 255, .10) 0%, rgba(255, 255, 255, 0) 38%),
    radial-gradient(ellipse 74% 72% at 50% 38%,
      rgba(0, 0, 0, 0) 40%, rgba(0, 0, 0, .50) 78%, rgba(0, 0, 0, .88) 100%),
    var(--hab, var(--screen));
}

/* The portrait. A found square carries the creature itself; the wall is meant to
 * be read by picture first and by name second, which is the whole difference
 * between a collection and a list.
 *
 * What it sits on is the rule above: the ground of the country it lives in, or
 * -- with no `--hab` to read, which is a book opened before the registry has
 * landed -- the dark screen every 3D view in this project sits on, so an empty
 * one and a full one are still the same object in two states. */
.dexpic {
  display: block; flex: 0 0 auto; width: 100%; aspect-ratio: 1 / 1;
  border: var(--u) solid var(--ink);
  overflow: hidden;
}
.dexpic img { display: block; width: 100%; height: 100%; }

/* Locked: an EMPTY BOX. Same size, same shape, same frame as a found one -- the
 * wall is complete from the first minute and what changes as you play is that
 * the boxes fill with animals. It must not look like a creature whose picture
 * failed to load, so it carries no name and no family colour. */
.dextile.locked {
  background: var(--paper2); border-top-color: var(--ink-soft);
  cursor: default;
}
.dextile.locked .dexpic { background: var(--paper); border-style: dashed; }
.dextile.locked .dexname { color: var(--ink-soft); font-weight: 700; text-align: center; }

/* The portrait mill: one small surface every tile picture is drawn on, parked
 * off-screen. NOT `display: none` -- the renderer measures its element, and a
 * canvas that is not laid out measures zero. See `drawThumb` in dex.js. */
.dexmill {
  position: absolute; left: calc(-400 * var(--u)); top: 0;
  width: calc(64 * var(--u)); height: calc(64 * var(--u));
  pointer-events: none;
}

/* The verb marks: one letter per way in, filled when it has happened.
 *
 * Wraps, because how many there are is the CREATURE's business, not the tile's:
 * a moth offers one mark, a monster offers three, and a save file written under
 * the old sighting rule can carry two more that nobody can earn again. A row
 * that cannot wrap pushes the last stamp out through the edge of the square. */
.dexmarks {
  display: flex; flex: 0 0 auto; flex-wrap: wrap;
  gap: calc(3 * var(--u)); margin-top: auto;
}
.dexmarks i {
  font-style: normal; font-size: var(--fs-s); line-height: 1.4;
  padding: 0 calc(4 * var(--u));
  background: var(--paper2); color: var(--ink-soft); border: var(--u) solid var(--ink);
}
.dexmarks i.on { background: var(--gold); color: var(--ink); }

.dexcard {
  flex: 1 1 calc(190 * var(--u)); min-width: 0;
  background: var(--paper2); border: var(--b2) solid var(--ink);
  padding: calc(10 * var(--u)); overflow: auto; min-height: 0; max-height: 100%;
}
.dexcard h3 {
  display: inline-block; margin: calc(10 * var(--u)) 0 calc(6 * var(--u));
  padding: calc(3 * var(--u)) calc(8 * var(--u)); font-size: var(--fs-m);
  background: var(--gold); color: var(--ink); border: var(--b1) solid var(--ink);
}
.dexcard h3:first-of-type { margin-top: calc(8 * var(--u)); }
/* The card's own screen, framed like the workshop's, and standing on the same
 * ground the tile does -- see the shared rule above. Wider than tall, and
 * capped: a square preview at this width ate the whole card, and the creature's
 * NAME and marks have to be readable without scrolling to them. */
.dexshot {
  display: block; width: 100%; aspect-ratio: 5 / 4;
  max-height: calc(96 * var(--u));
  border: var(--b2) solid var(--ink);
}
.dexchips { margin: calc(4 * var(--u)) 0; gap: calc(4 * var(--u)); }
.dexchip {
  font-size: var(--fs-s); text-transform: uppercase; letter-spacing: .06em;
  background: var(--paper); color: var(--ink-soft);
  border: var(--b1) solid var(--ink); padding: calc(2 * var(--u)) calc(6 * var(--u));
}
.dexchip.on { background: var(--gold); color: var(--ink); font-weight: 700; }
.dexmet { margin: calc(6 * var(--u)) 0; font-size: var(--fs-s); color: var(--ink); }

/* The facts: a label column and a value column, ruled like a card in a folder. */
.dexfacts {
  display: grid; grid-template-columns: auto 1fr; gap: 0 calc(8 * var(--u));
  margin: calc(6 * var(--u)) 0; font-size: var(--fs-s);
  border-top: var(--u) solid var(--ink);
}
.dexfacts dt {
  color: var(--ink-soft); text-transform: uppercase; letter-spacing: .08em;
  padding: calc(3 * var(--u)) 0; border-bottom: var(--u) solid var(--ink);
}
.dexfacts dd {
  margin: 0; color: var(--ink); text-align: right;
  padding: calc(3 * var(--u)) 0; border-bottom: var(--u) solid var(--ink);
}

.dexdrops { display: flex; flex-direction: column; gap: calc(3 * var(--u)); }
.dexdrop {
  display: flex; align-items: center; gap: calc(5 * var(--u)); font-size: var(--fs-s);
  background: var(--paper); border: var(--b1) solid var(--ink);
  padding: calc(2 * var(--u)) calc(5 * var(--u));
}
.dexdrop b { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.dexbody > .note { margin: calc(8 * var(--u)) 0 0; font-size: var(--fs-s); }



/* --- The compass (`client/src/compass.js`) -------------------------------- */
/* Top right, and small: it is on screen for every frame of the game forever, so
 * it is built to be glanced at and otherwise ignored. It never competes with the
 * crosshair (which is white-on-black in the middle of the screen) and it never
 * touches the hotbar's corner.
 *
 * Three bands, in the order you need them: the sliding tape of the eight points,
 * then the land the well would send you to if you jumped facing this way, then
 * the hall. Same skin as every other permanent chip -- flat lavender fill, black
 * outline, hard shadow, monospace, no chrome and no title bar (rule 5: chrome
 * belongs to panels you open).
 *
 * Two colours carry meaning rather than decoration, and both are borrowed rather
 * than invented: the land arrow is the well's arcane VIOLET, because that row is
 * literally the well's promise; the hall arrow is the sanctuary's TURQUOISE, the
 * colour of the veil at its doors. Nothing here is a seventh accent.
 *
 * `pointer-events: none`: it is read, never clicked, and it sits in the corner
 * the crafting panel opens into. */

#compass {
  position: fixed; right: calc(10 * var(--u)); top: calc(10 * var(--u));
  width: calc(118 * var(--u)); pointer-events: none;
  background: var(--paper2); color: var(--ink);
  border: var(--b1) solid var(--ink); box-shadow: var(--drop) var(--drop) 0 var(--ink);
  padding: calc(4 * var(--u)) calc(5 * var(--u));
  display: flex; flex-direction: column; gap: calc(2 * var(--u));
}

/* The tape. `overflow: hidden` is the whole trick: twenty-four letters are laid
 * out once at mount, three copies of the eight points at -360/0/+360 degrees,
 * and the heading slides the strip under this window with one transform. There
 * is no layout in a frame of this. */
#compass .tape {
  position: relative; overflow: hidden;
  height: calc(17 * var(--u)); width: calc(104 * var(--u));
  background: var(--paper); border: var(--u) solid var(--ink);
}
#compass .strip { position: absolute; left: 50%; top: 0; height: 100%; will-change: transform; }
#compass .strip b {
  position: absolute; top: 50%; transform: translate(-50%, -50%);
  font-family: var(--mono); line-height: 1; white-space: nowrap;
}
/* The four that matter, and the four between them. A diagonal you can read but
 * would not mistake for a cardinal is what makes the tape legible at a glance. */
#compass .strip b.main { font-size: calc(11 * var(--u)); font-weight: 700; color: var(--ink); }
#compass .strip b.half { font-size: calc(8 * var(--u)); color: var(--ink-soft); }
/* The notch: a drawn triangle biting down into the middle of the tape, gold,
 * because gold is already "the one in hand" everywhere else in this skin. No
 * glyph, no font -- the same border trick the speaker and the arrows use. */
#compass .tape::after {
  content: ''; position: absolute; left: 50%; top: 0;
  margin-left: calc(-4 * var(--u));
  width: 0; height: 0; border: solid transparent;
  border-width: calc(6 * var(--u)) calc(4 * var(--u)) 0;
  border-top-color: var(--gold);
}

/* A line: arrow, name, distance in blocks hard against the right edge.
 *
 * `cline`, not `row`: the login card in `client/index.html` owns a global `.row`
 * with a margin on it, and an id-scoped rule wins the properties it names while
 * inheriting the ones it does not. That margin made this box a quarter taller
 * than it was written to be. */
#compass .cline {
  display: flex; align-items: center; gap: calc(4 * var(--u));
  font-size: calc(9 * var(--u)); line-height: 1.5;
  text-transform: uppercase; letter-spacing: .04em;
}
#compass .cline b { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
#compass .cline .d { flex: 0 0 auto; color: var(--ink-soft); }
/* Standing in the hall: the row keeps its place, the arrow stops pretending to
 * point at something you are inside of. */
#compass .cline.here .dir { visibility: hidden; }

/* The arrow's box, so the triangle can spin about its own centre without the
 * row reflowing around it. */
#compass .dir {
  position: relative; flex: 0 0 calc(11 * var(--u)); height: calc(11 * var(--u));
}
#compass .dir i {
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: 0; height: 0; border: solid transparent;
  border-width: 0 calc(4 * var(--u)) calc(8 * var(--u));
}
#compass .land i { border-bottom-color: var(--violet); }
#compass .home i { border-bottom-color: var(--turq); }

/* --- the player's settings screen (client/src/settings.js) ------------------
 *
 * Dressed here rather than in client/index.html for the reason the inventory
 * gives above: that file builds its own DOM, so its dressing belongs with every
 * other panel's, on the one `--ui` dial and in the one face.
 *
 * A LIST, not a form. Every row is the same shape -- what it is on the left,
 * the switch hard right -- so a screen with one row and a screen with nine read
 * the same way and nothing has to be re-laid-out when the next setting lands.
 * The rows are separated by black rules rather than by gaps, which is the same
 * move the inventory grid makes: the outline IS the separator (rule 1). */

#settings {
  position: fixed; left: 50%; top: calc(64 * var(--u)); transform: translateX(-50%);
  z-index: var(--z-panel); display: none; flex-direction: column;
  width: min(calc(380 * var(--u)), 92vw); max-height: calc(100vh - 88 * var(--u));
}
#settings.open { display: flex; }
#settings .winbar { background: var(--turq); }
#settings .winbar h2 { color: var(--ink); }

.setbody {
  padding: calc(12 * var(--u)); background: var(--paper);
  display: flex; flex-direction: column; min-height: 0; overflow: hidden;
}
/* The list is the part allowed to scroll, so the line that says where a setting
 * is kept stays on screen however many rows there are one day. */
.setlist {
  display: flex; flex-direction: column; gap: var(--b1);
  background: var(--ink); border: var(--b2) solid var(--ink);
  overflow: auto; min-height: 0;
}

/* A heading inside the list. It is a ROW of the list, not a bar above it, so it
 * scrolls with what it names -- with eleven display switches under one heading,
 * a heading pinned outside the scroller would be a label for whatever happened
 * to be off screen. Ink on the dark rail the rows are separated by, which is
 * the one surface in this panel that is not already a row. */
.setgroup {
  margin: 0; padding: calc(5 * var(--u)) calc(10 * var(--u));
  background: var(--ink); color: var(--lav);
  font-size: var(--fs-s); font-weight: 700;
}

/* A whole row is the click target: the switch is small and hunting for it with a
 * mouse in a game is not what a settings screen is for. */
.setrow {
  display: flex; align-items: center; gap: calc(10 * var(--u));
  padding: calc(8 * var(--u)) calc(10 * var(--u));
  background: var(--paper2); cursor: pointer;
}
.setrow:hover { background: var(--turq); }
.settext { display: flex; flex-direction: column; gap: calc(2 * var(--u)); min-width: 0; }
.settext b {
  font-size: var(--fs-m); text-transform: uppercase; letter-spacing: .06em;
}
/* The explanation. Sentence case and not uppercase — rule 4 puts uppercase on
 * things that NAME something, and this one is a sentence about a thing. */
.settext span { font-size: var(--fs-s); line-height: 1.5; color: var(--ink-soft); }
/* A row this machine cannot honour. Greyed and unclickable, and it says the
 * word rather than pretending: see `disable` in client/src/settings.js. */
.setrow.off { cursor: default; opacity: .55; }
.setrow.off:hover { background: var(--paper2); }
.settext .no {
  font-size: var(--fs-s); font-weight: 700; color: var(--red); text-transform: uppercase;
}

/* Hard right, and it stays there whatever the label does. */
.setrow input[type=checkbox] { margin-left: auto; }
/* The volume slider sits where the switches sit, and does not shrink: a rail
 * short enough to be a guess is worse than no rail. */
.setrow input[type=range] { margin-left: auto; flex: 0 0 calc(96 * var(--u)); width: calc(96 * var(--u)); }

.setbody .note { margin: calc(8 * var(--u)) 0 0; font-size: var(--fs-s); line-height: 1.5; }

/* --- The pause dialog (`client/src/pause.js`) ----------------------------- */
/* Escape, with nothing else open. Same construction as the settings screen
 * above -- the file builds its own DOM, the dressing lives here -- but it is a
 * DIALOG, not a panel: three ways out of a session and nothing to browse, so it
 * sits dead centre and its buttons run full width, biggest target first. */

#pause {
  position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%);
  z-index: var(--z-dialog); display: none; flex-direction: column;
  width: min(calc(300 * var(--u)), 92vw);
}
#pause.open { display: flex; }

/* The MENU button: the visible half of the first Escape. Bottom-left -- the
 * corner the volume chip vacated -- and only ON when the mouse is free in play;
 * client/src/main.js decides (`menuButtonWanted`), client/src/pause.js draws.
 * The sheet's own button dressing does the rest: SquareFont, uppercase, the
 * hard shadow and the pressed displacement. */
#menubtn {
  position: fixed; left: calc(10 * var(--u)); bottom: calc(14 * var(--u));
  z-index: var(--z-hudbtn); display: none;
  padding: calc(6 * var(--u)) calc(12 * var(--u));
  font-size: var(--fs-m); font-weight: 700;
}
#menubtn.on { display: block; }

.pausebody {
  padding: calc(12 * var(--u)); background: var(--paper);
  display: flex; flex-direction: column; gap: calc(8 * var(--u));
}
.pausebody button { width: 100%; padding: calc(9 * var(--u)); font-size: var(--fs-m); }
.pausebody .note { margin: 0; font-size: var(--fs-s); line-height: 1.5; }

/* --- Before something irreversible (`client/src/confirm.js`) --------------- */
/* The pause dialog's box and body again, and for the third time on purpose: the
 * same centred window, the same full-width buttons, the same "biggest target is
 * the safe one". Two things are its own. The bar is the DANGER red, borrowed
 * from `#lost` and for a related reason -- that one is the dialog you did not
 * ask for, this one is the dialog you must not answer by reflex. And it is a
 * CLASS, not an id: the lobby page and the game page each mount their own, and
 * an id would have made the second one silently the first one's twin.
 *
 * `--z-fatal` rather than `--z-dialog`: it can be opened from the operator
 * console, which is a panel, and a question about destroying a world may not be
 * covered by the panel that asked it. It still sits under nothing, because the
 * only thing above it is the sentence saying the session ended -- and if that
 * appears, this question has already been answered by events. */

.confirm {
  position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%);
  z-index: var(--z-fatal); display: none; flex-direction: column;
  width: min(calc(340 * var(--u)), 94vw);
}
.confirm.open { display: flex; }
.confirm .winbar { background: var(--red); }
.confirm .winbar h2 { color: var(--paper); }
.confirm .pausebody { max-height: 84vh; overflow-y: auto; }
/* The name, said once and largest: a dialog that does not say WHICH world
 * cannot be checked before it is obeyed. `--mono-code` because a name can be
 * read back against what is typed below it, and SquareFont draws S and 5 alike
 * -- see fonts/README.md. */
.confirm .confirmname {
  margin: 0; font-family: var(--mono-code); font-size: var(--fs-l);
  color: var(--ink); word-break: break-word;
}
.confirm input { width: 100%; font-family: var(--mono-code); }
.confirm label { font-size: var(--fs-s); color: var(--ink-soft); }

/* --- The end of a session (`client/src/lost.js`) --------------------------- */
/* The pause dialog's own dressing, borrowed on purpose: same family of problem
 * -- this page can no longer talk to this server -- so the same box, the same
 * body and the same full-width buttons. Two differences, and both are the
 * meaning of the thing: it sits ABOVE everything including the pause dialog
 * (`--z-fatal`, the layer above `--z-dialog`; see the stack in `:root`), because
 * nothing may cover the sentence that says
 * the game ended; and its bar is the DANGER red rather than the sheet's ink,
 * because it is the one dialog in the game you did not ask for. */

#lost {
  position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%);
  z-index: var(--z-fatal); display: none; flex-direction: column;
  width: min(calc(340 * var(--u)), 94vw);
}
#lost.open { display: flex; }
#lost .winbar { background: var(--red); }
#lost .winbar h2 { color: var(--paper); }
/* It says more than the pause dialog does -- what happened, what the server
 * answered when asked again, and what rejoining costs -- so on a short window
 * it scrolls INSIDE its own box rather than running off the bottom with the two
 * buttons on it. The bar stays put: the headline is the half you need first. */
#lost .pausebody { max-height: 78vh; overflow-y: auto; }

/* The warning chip: the seconds of silence, counted up, before any of that.
 * Top centre and read-only -- it is the one place on screen that says the link
 * has gone quiet while there is still every chance it comes back, and a link
 * that comes back takes it away again. It never overlaps the compass (top
 * right) and it sits above the crosshair, not on it. */
#hush {
  position: fixed; left: 50%; top: calc(10 * var(--u)); transform: translateX(-50%);
  z-index: var(--z-hudchip); display: none; pointer-events: none;
  background: var(--red); color: var(--paper);
  border: var(--b1) solid var(--ink); box-shadow: var(--drop) var(--drop) 0 var(--ink);
  padding: calc(3 * var(--u)) calc(8 * var(--u));
  font-family: var(--mono); font-size: var(--fs-s); font-weight: 700;
}
#hush.on { display: block; }

/* ...and its twin, for the state you can forget you are in.
 *
 * "Walk into worlds unseen" lives on the ACCOUNT, so a super-admin who set it
 * once arrives hidden every time, in every world, with nothing on screen to say
 * so -- and a person who has forgotten they are invisible will wonder why
 * nobody answers them. So it is said, permanently, in the one corner that is
 * already the game's "something about this session is not normal" line.
 *
 * Violet rather than the hush chip's red: this is a state somebody CHOSE, not a
 * warning, and the two must not read as the same kind of news.
 *
 * TOP LEFT, which is the one corner of this HUD that holds nothing: the compass
 * is top right, the menu button bottom left, the points bottom right, and the
 * quick bar and its health strip run along the bottom centre. Bottom centre was
 * tried first and drawn ON the quick bar, over two of its slots -- caught by
 * looking at it rather than by any number. Top centre belongs to the hush chip,
 * and a link that has gone quiet while you are unseen must show both. */
#unseenchip {
  position: fixed; left: calc(10 * var(--u)); top: calc(10 * var(--u));
  z-index: var(--z-hudchip); display: none; pointer-events: none;
  background: var(--violet); color: var(--paper);
  border: var(--b1) solid var(--ink); box-shadow: var(--drop) var(--drop) 0 var(--ink);
  padding: calc(3 * var(--u)) calc(8 * var(--u));
  font-family: var(--mono); font-size: var(--fs-s); font-weight: 700;
}
#unseenchip.on { display: block; }

/* --- Help: the controls, and the message form (`client/src/help.js`) --------
 *
 * `.keys` is the box the key list is drawn into, and it is dressed HERE rather
 * than on any one page because four surfaces draw the same box: the card before
 * the world (client/index.html), the `?` panel in game, and the help dialog
 * behind the small `help` in the footer of the door and of the lobby. The list
 * inside it is `shared/controls.js` and nothing writes a key anywhere else.
 *
 * A section heading and paragraphs, not a wall of <br>: the list is forty keys
 * now and the headings are what make it scannable rather than a page you give
 * up on at row six. */

.keys { margin-top: calc(12 * var(--u)); padding: calc(9 * var(--u)) calc(10 * var(--u));
  font-size: var(--fs-m); line-height: 1.85;
  background: var(--paper2); border: var(--b1) solid var(--ink); color: var(--ink); }
.keys kbd { display: inline-block; background: var(--paper); border: var(--b1) solid var(--ink);
  box-shadow: var(--b1) var(--b1) 0 var(--ink); padding: 0 calc(4 * var(--u));
  margin: 0 var(--u); font-family: var(--mono); font-size: var(--fs-s); }
/* The one thing that NAMES something in this box, so the one thing in caps —
 * rule 4. Ruled off above rather than spaced, the way every list here is. */
.keys h4 { margin: calc(10 * var(--u)) 0 calc(4 * var(--u)); font-size: var(--fs-s);
  text-transform: uppercase; letter-spacing: .08em; color: var(--ink-soft);
  border-top: var(--b1) solid var(--ink); padding-top: calc(6 * var(--u)); }
.keys h4:first-child { margin-top: 0; border-top: 0; padding-top: 0; }
.keys p { margin: 0 0 calc(3 * var(--u)); }

/* The `?`, top right. Opposite corner to the MENU button and for the opposite
 * reason: MENU appears only when it can be clicked, this one is always there,
 * because it is how somebody learns the help exists at all. With the pointer
 * captured it cannot be clicked -- no DOM element can -- which is exactly why
 * `H` does the same thing; see the keydown handler in client/src/main.js.
 *
 * BESIDE the compass, not on it. The corner is already occupied: `#compass`
 * above sits at right/top 10 and is 118 wide with a 3 shadow, so this is
 * parked just left of it -- measured against those numbers rather than nudged
 * until it looked right, which is what makes it still correct at another
 * `--ui`. It does NOT move when the compass hides under a panel: a button that
 * changes corner depending on what else is open is a button you hunt for. */
/* `--mono-code` on the label below, not the interface font: SquareFont draws `?`
   at this size in a way that reads as a capital P -- the owner reported it as
   "le bouton P" floating over his crafting window, the same S/5 and Z/2
   confusion fonts/README.md already documents. A button whose label is misread
   is a button nobody presses. The comment sits OUTSIDE the rule because a test
   reads `right:` within 200 characters of the brace. */
#helpbtn {
  font-family: var(--mono-code);
  position: fixed; right: calc(140 * var(--u)); top: calc(10 * var(--u));
  z-index: var(--z-hudbtn); width: calc(26 * var(--u)); height: calc(26 * var(--u));
  padding: 0; font-size: var(--fs-l); font-weight: 700; line-height: 1;
}

/* --- The world from above, which `/spawnme` opens ------------------------- *
 *
 * A centred panel like the encyclopedia, on `--z-panel` with them: it is a
 * window you open, it covers the HUD, and nothing opens on top of it. See
 * client/src/atlas.js. */

#atlas {
  position: fixed; left: 50%; top: calc(40 * var(--u)); transform: translateX(-50%);
  z-index: var(--z-panel); display: none; flex-direction: column;
  width: min(calc(520 * var(--u)), 94vw); max-height: calc(100vh - 64 * var(--u));
}
#atlas.open { display: flex; }
#atlas .winbar { background: var(--turq); }
#atlas .winbar h2 { color: var(--ink); }

.atlasbody {
  padding: calc(10 * var(--u)); background: var(--paper);
  display: flex; flex-direction: column; gap: calc(7 * var(--u));
  min-height: 0; overflow: auto;
}

/* Square, and never taller than the screen it is drawn on -- the same shape and
 * the same reasoning as the editors' pixel grid. `touch-action: none` so a drag
 * pans the map instead of scrolling the panel out from under it. */
.atlasmap {
  width: 100%; max-width: min(100%, 62vh); aspect-ratio: 1 / 1;
  display: block; margin: 0 auto; touch-action: none;
  background: var(--screen); border: var(--b2) solid var(--ink);
  cursor: crosshair;
}

/* What is under the pointer. `--mono-code` and not `--mono`: this line is read
 * as COORDINATES, and a reader who cannot tell 5 from S cannot type them into
 * /tp afterwards. See fonts/README.md. */
.atlasread {
  margin: 0; font-family: var(--mono-code); font-size: var(--fs-s);
  padding: calc(4 * var(--u)) calc(6 * var(--u));
  background: var(--ink); color: var(--paper);
}

.atlasbar { display: flex; flex-wrap: wrap; gap: calc(4 * var(--u)); }
.atlasbar button { padding: calc(5 * var(--u)) calc(7 * var(--u)); font-size: var(--fs-s); }

.atlaskey { display: flex; flex-wrap: wrap; gap: calc(4 * var(--u)) calc(10 * var(--u)); }
.atlaskeyitem { display: flex; align-items: center; gap: calc(4 * var(--u));
  font-size: var(--fs-s); }
.atlasswatch { width: calc(12 * var(--u)); height: calc(12 * var(--u));
  border: var(--b1) solid var(--ink); }

.atlasnote { margin: 0; font-size: var(--fs-s); line-height: 1.5; }

#help {
  position: fixed; left: 50%; top: calc(40 * var(--u)); transform: translateX(-50%);
  z-index: var(--z-panel-2); display: none; flex-direction: column;
  width: min(calc(560 * var(--u)), 94vw); max-height: calc(100vh - 64 * var(--u));
}
#help.open { display: flex; }
#help .winbar { background: var(--gold); }
#help .winbar h2 { color: var(--ink); }
.helpbody {
  padding: calc(12 * var(--u)); background: var(--paper);
  display: flex; flex-direction: column; min-height: 0; overflow: auto;
}
.helpbody .keys { margin-top: 0; }
.helpbody #helpAsk { margin-top: calc(10 * var(--u)); padding: calc(9 * var(--u));
  font-size: var(--fs-m); background: var(--orange); }
.helpbody .note { margin: calc(6 * var(--u)) 0 0; font-size: var(--fs-s); line-height: 1.5; }

/* The form. ON TOP of the help window -- higher z-index, and Escape peels it
 * first -- the same relationship the View All window has with the crafting one.
 *
 * `--z-dialog`, which is the pause dialog's layer: the two can never be open at
 * once (Escape only reaches the pause fall-through when nothing is open, and
 * this window is something), so sharing is free. It is deliberately NOT
 * `--z-fatal`: that belongs to `#lost` alone, and the sentence saying the
 * session ended must not be coverable by anything, including this. */
#helpform {
  position: fixed; left: 50%; top: calc(30 * var(--u)); transform: translateX(-50%);
  z-index: var(--z-dialog); display: none; flex-direction: column;
  width: min(calc(440 * var(--u)), 94vw); max-height: calc(100vh - 48 * var(--u));
}
#helpform.open { display: flex; }
#helpform .winbar { background: var(--orange); }
.helpformbody {
  padding: calc(12 * var(--u)); background: var(--paper);
  display: flex; flex-direction: column; min-height: 0; overflow: auto;
}
/* The fields, as one block that goes away together. `[hidden]` needs saying
 * out loud here: this rule sets `display: flex`, and a class rule outranks the
 * attribute's own `display: none` — the same trap `.row[hidden]` in
 * client/index.html already carries a note about. */
.helpfields { display: flex; flex-direction: column; }
.helpfields[hidden] { display: none; }
.helpformbody label { display: block; color: var(--ink-soft); font-size: var(--fs-s);
  text-transform: uppercase; letter-spacing: .08em; margin-top: calc(10 * var(--u));
  margin-bottom: calc(4 * var(--u)); }
.helpformbody input, .helpformbody textarea { width: 100%; box-sizing: border-box; }
.helpformbody textarea { font-family: var(--mono); font-size: var(--fs-m);
  line-height: 1.5; resize: vertical; }
/* Readonly and not disabled: this field exists to be READ. Greyed enough to say
 * "not yours to change", legible enough to be the point of the row. */
.helpformbody input[readonly] { background: var(--paper2); color: var(--ink-soft); cursor: default; }
.helpformbody .note { margin: calc(6 * var(--u)) 0 0; font-size: var(--fs-s); line-height: 1.5; }
.helpformbody .note.tiny { font-size: calc(9 * var(--u)); line-height: 1.45; }
.helpformbody #helpSend { margin-top: calc(12 * var(--u)); padding: calc(9 * var(--u));
  font-size: var(--fs-m); background: var(--orange); }

/* The three choices. All three visible at once, because which one is picked
 * changes what the form DOES -- a bug attaches a picture of your screen -- and
 * a choice with a consequence does not belong behind a dropdown. */
.helppicks { display: flex; gap: calc(6 * var(--u)); flex-wrap: wrap; }
.helppicks button { flex: 1 1 auto; padding: calc(6 * var(--u)) calc(10 * var(--u));
  font-size: var(--fs-m); }
.helppicks button.on { background: var(--violet); color: var(--paper); font-weight: 700; }

.helpsaid { margin-top: calc(8 * var(--u)); font-size: var(--fs-s); line-height: 1.5;
  background: var(--green); color: var(--paper);
  border: var(--b1) solid var(--ink); padding: calc(5 * var(--u)) calc(8 * var(--u)); }
.helpsaid[hidden] { display: none; }
.helperr { color: var(--red); font-size: var(--fs-m); margin-top: calc(6 * var(--u));
  min-height: calc(16 * var(--u)); }
.helpgate { display: flex; flex-direction: column; gap: calc(8 * var(--u));
  align-items: flex-start; }
.helpgate[hidden] { display: none; }
/* Dressed as the one thing to do here, rather than left as the browser's blue
 * underline: rule 5 says the orange is the one commit button, and on this
 * screen going to the door IS the commit. */
.helpgate a { background: var(--orange); color: var(--ink); text-decoration: none;
  border: var(--b1) solid var(--ink); box-shadow: var(--drop) var(--drop) 0 var(--ink);
  padding: calc(6 * var(--u)) calc(12 * var(--u));
  font-size: var(--fs-m); font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em; }
.helpgate a:active { transform: translate(var(--drop), var(--drop)); box-shadow: none; }

/* The footer link, on the door and in the lobby. Discreet: it sits inside a
 * line that is already small and soft, and it must not out-shout the © line
 * it is beside. */
.helplink { color: var(--ink-soft); text-decoration: underline; }
.helplink:hover { color: var(--ink); }

/* --- What is this: the prose of a page, folded (`client/src/what.js`) -------
 *
 * The lobby explained itself in paragraphs and the owner asked for a list of
 * controls instead: every explanatory box folds to a small `?` beside the
 * title it explained, and the words wait in ONE dialog. The entry card before
 * a world folds its key list the same way. The words stay in each page
 * (`#whatentries`); the button and the window are dressed here, once, so the
 * two pages cannot drift. */

/* The `?` itself. `--mono-code` and not the interface face, for the reason
 * `#helpbtn` gives: SquareFont draws `?` as something close to a capital P,
 * and a button whose label is misread is a button nobody presses. Sized and
 * mannered like a `.winbtn`, because it lives beside them in title bars. */
.whatbtn {
  font-family: var(--mono-code); font-weight: 700; font-size: var(--fs-s);
  width: calc(20 * var(--u)); height: calc(20 * var(--u));
  flex: 0 0 auto; padding: 0; line-height: 1;
  box-shadow: none;
}
.whatbtn:active { transform: translate(var(--u), var(--u)); box-shadow: none; }

/* A short functional title with its `?` after it: what remains on the page
 * where a folded box used to stand. Dressed like the labels around it, so the
 * page reads as one list of controls. */
.whatline { display: flex; align-items: center; gap: calc(6 * var(--u));
  margin: calc(10 * var(--u)) 0 0; font-size: var(--fs-s); color: var(--ink-soft);
  text-transform: uppercase; letter-spacing: .08em; }

/* The entries, waiting to be adopted. Undrawn even before the script runs —
 * and STILL undrawn on a page whose script failed, which is the honest half:
 * prose out of reach beats prose flooding back. */
#whatentries { display: none; }

/* The one dialog. Same construction as `#help`: a centred `win` on the dialog
 * layer, `display: none` until `.open` — and mounted INSIDE `#login` on the
 * entry card, because the door is its own stacking context at `--z-door` and
 * a dialog left on `body` would open under the card that opened it. */
#what {
  position: fixed; left: 50%; top: calc(40 * var(--u)); transform: translateX(-50%);
  z-index: var(--z-dialog); display: none; flex-direction: column;
  width: min(calc(560 * var(--u)), 94vw); max-height: calc(100vh - 64 * var(--u));
}
#what.open { display: flex; }
#what .winbar { background: var(--turq); }
#what .winbar h2 { color: var(--ink); }
.whatbody {
  padding: calc(12 * var(--u)); background: var(--paper);
  display: flex; flex-direction: column; min-height: 0; overflow: auto;
}
.whatbody .note { margin: calc(8 * var(--u)) 0 0; }
.whatbody section > :first-child { margin-top: 0; }
.whatbody .keys { margin-top: 0; }

/* The veil: a click-catcher, not a wash. Rule 3 forbids a translucent panel,
 * so it is not drawn at all — it exists so that a click anywhere OUTSIDE the
 * window says the same sentence the X and Escape say: close. Painted before
 * the window in the same layer, so the window stacks on top of it. */
#whatveil { position: fixed; inset: 0; z-index: var(--z-dialog); display: none;
  background: transparent; }
#whatveil.open { display: block; }

/* --- The offer to install (`client/src/install.js`) ------------------------ */
/* A strip at the foot of the door, and deliberately NOT the pause dialog's
 * centred box: this is the one window in the project that nobody asked for, so
 * it may not sit in the middle of the page somebody came here to use. It has no
 * backdrop, it disables nothing, and the sign-in card behind it keeps working
 * the whole time it is up. `--z-invite` is one step above the HUD chips and far
 * below anything you open yourself; see the stack in `:root`.
 *
 * The bottom is where it belongs on both platforms it is shown on. On an iPhone
 * Safari's own toolbar -- the one holding the Share button these directions
 * point at -- is at the bottom of the screen, so the card sits directly above
 * the thing it is talking about.
 *
 * There is deliberately no `env(safe-area-inset-bottom)` here. It would be dead
 * weight: those insets are zero unless the page asks for `viewport-fit=cover`,
 * and none of this project's pages does -- the browser already keeps the page
 * out from under the notch and the home indicator. It would also be the one
 * bare pixel in the sheet, which rule six forbids and section 56 measures. */

/* Room to scroll out from under it, put on the page while the card is up and
 * taken away with it (`open` and `close` in client/src/install.js).
 *
 * This is measured, not decorative. The door on a phone is barely taller than
 * the screen -- more so since the "why a code and not a password" section stops
 * being drawn there -- so with a strip fixed to the bottom, `send me a code`
 * sat behind it and there was no scroll left to bring it out. Half a screen of
 * empty page below the card is enough to lift the whole form clear of it, and
 * it is invisible: it is page background, and it exists only while there is
 * something to scroll clear OF. */
body.invited { padding-bottom: 50vh; }

#install {
  position: fixed; left: 50%;
  bottom: calc(10 * var(--u));
  transform: translateX(-50%);
  z-index: var(--z-invite);
  display: flex; flex-direction: column;
  width: min(calc(380 * var(--u)), 94vw);
  /* A ceiling, not a size, and it is only ever reached on purpose. Measured on
   * a 385-wide phone at the game's own `--ui: 2`: the card OPENS at 41% of the
   * screen -- one sentence and a row of two buttons -- and only grows towards
   * this when somebody has pressed `show me how`. The number is the confirm
   * dialog's own 84vh, borrowed knowingly: at that point this is a window
   * somebody asked for, and it is allowed to behave like one. */
  max-height: 84vh;
}
/* The gold bar the section headings on the door and in the lobby wear, rather
 * than the violet every other window has: those are windows you opened, this is
 * a notice, and the two should not read as the same kind of thing. */
#install .winbar { background: var(--gold); }
#install .winbar h2 { color: var(--ink); }

/* The pause dialog's body, borrowed for the fourth time -- the same padding,
 * the same paper, the same gap between the things in it. Scrolls rather than
 * growing, because the directions plus the note run long on a narrow phone and
 * a card taller than the screen is a card with no way out at the bottom. */
.installbody { min-height: 0; overflow: auto; }
.installbody .note { margin: 0; }
/* One tap high, and no higher. `.pausebody button` is padded for a dialog that
 * owns the screen; this one is sharing a phone with the form underneath it. */
.installbody button { padding: calc(7 * var(--u)); }

/* The two answers on ONE row, which is the whole of the override: the pause
 * dialog's `button { width: 100% }` stacks them, and that is right for a dialog
 * that owns the screen and wrong for a strip whose entire job is to be short --
 * two rows of button is a hundred pixels of a phone spent on yes and no.
 *
 * They do not share it evenly. The orange is the one commit button on this
 * sheet, and the rule the rest of the project follows is that the target you
 * are meant to hit is the bigger one, so yes is half again the width of no. */
.installact { display: flex; gap: calc(6 * var(--u)); }
.installact button { flex: 1 1 0; width: auto; min-width: 0; }
.installact button.primary { flex: 1.5 1 0; }

/* The directions. Numbered, because they are a sequence and the order is the
 * whole content -- the share sheet is not open until the first one has
 * happened. `ol` for that reason and not for decoration. */
.installsteps {
  margin: 0; padding-left: calc(16 * var(--u));
  display: flex; flex-direction: column; gap: calc(4 * var(--u));
  /* The small print's size, like the notes above them: three numbered lines at
   * body size is most of a phone. */
  font-size: var(--fs-s); line-height: 1.5; color: var(--ink);
}
.installsteps li { margin: 0; }

/* The share glyph, drawn in code by `shareGlyph` in client/src/install.js and
 * sitting inside the sentence that names it. Sized off the text so it scales
 * with the `--ui` dial and with nothing else, and `currentColor` inside the SVG
 * means it is the same ink as the words on either side of it. */
.shareglyph {
  width: 1.15em; height: 1.15em;
  vertical-align: -0.22em;
  margin: 0 calc(1 * var(--u));
}
