/* 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,
 *      and `--finger` below: a fingertip does not scale with a dial).
 */

: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;

  /* THE FINGER. The one length in this sheet that is NOT an interface pixel,
     and the third deliberate exception to rule 6 beside the two the header
     names: a fingertip is the same size whatever the dial says, so the
     smallest thing a touch must be able to land on is a fixed 40 CSS pixels
     -- the ~40 the owner's audit was held to -- and it does NOT follow `--ui`.
     Read only under `body.onpad` (shared/ui.css, client/index.html): a desk
     never sees it. */
  --finger: 40px;

  /* 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 iOS APP DRAWS EDGE TO EDGE, and an iPhone's edge has a notch in it and a
   home indicator along the bottom. `html.iosapp` is stamped by ios/web/app.js,
   the file the iOS app writes at the top of every page it carries and that the
   website never serves -- so none of this exists on the web, on Android or on
   a PC. (The `env()` insets are zero there anyway.)

   The PAD is made a box of its own: the safe area as its size, and a transform,
   which makes it the containing block of its `position: fixed` controls. Every
   stick and key is then measured from the safe edge with not one of its own
   rules changed, and pad.js reads its rings with getBoundingClientRect, so the
   touch arithmetic follows. The HUD cannot be done that way -- the nameplates
   and the crosshair are placed in the 3D view's own coordinates and must stay
   there -- so each piece that hugs an edge is moved by `translate`, which adds
   to whatever `transform` it already wears instead of replacing it. */
html.iosapp #pad {
  inset: env(safe-area-inset-top) env(safe-area-inset-right)
    env(safe-area-inset-bottom) env(safe-area-inset-left);
  transform: translateZ(0);
}
html.iosapp :is(#chatlog, #chatbar, #menubtn) {
  translate: env(safe-area-inset-left) calc(-1 * env(safe-area-inset-bottom));
}
html.iosapp :is(#debug, #unseenchip) {
  translate: env(safe-area-inset-left) env(safe-area-inset-top);
}
html.iosapp :is(#compass, #helpbtn) {
  translate: calc(-1 * env(safe-area-inset-right)) env(safe-area-inset-top);
}
html.iosapp :is(#points, #ammo) {
  translate: calc(-1 * env(safe-area-inset-right)) calc(-1 * env(safe-area-inset-bottom));
}
html.iosapp :is(#health, #hotbar, #vitals, #draughts) {
  translate: 0 calc(-1 * env(safe-area-inset-bottom));
}
/* On a touch screen every window opens full screen (`body.onpad .win` in
   client/index.html), edge to edge -- so its own edge is given the insets, as a
   TRANSPARENT border: the window's background shows through it, and no
   window's own padding is touched. `!important` because each panel dresses
   its border under its own id. */
html.iosapp body.onpad .win {
  border-style: solid !important;
  border-color: transparent !important;
  border-width: env(safe-area-inset-top) env(safe-area-inset-right)
    env(safe-area-inset-bottom) env(safe-area-inset-left) !important;
}
/* The door, the lobby and the console are documents: their content simply
   stays inside the safe area. */
html.iosapp:not([data-page="world"]) body {
  padding: env(safe-area-inset-top) env(safe-area-inset-right)
    env(safe-area-inset-bottom) env(safe-area-inset-left);
}

/* 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); }

/* ---- A fingertip is not a cursor -------------------------------------------
 *
 * Three controls in this skin are drawn smaller than a finger: the X that
 * closes a window, the `?` beside a title, and the footer's `help`. Measured
 * inside the Android app, where the dial is 1.2: the X and the `?` were 24
 * pixels square on the door and in the lobby (30 on the game's full-screen
 * panels), and `help` was a 28x13 sliver of underlined text. A phone wants
 * something nearer 44 under a thumb.
 *
 * The DRAWING does not change -- every one of them is the size it was, in the
 * place it was -- only the part that answers a touch grows, and only on a
 * phone (`body.onpad`, hung by the one predicate in client/src/pad.js):
 *
 *   - the X keeps its 20u square but stands in a transparent border, the frame
 *     redrawn as an inset shadow at the old edge, and a negative margin of the
 *     same widths gives the room back to the bar. Its two bars are centred by
 *     the box model (`inset: 0; margin: auto`, above), so they follow the
 *     padding box and do not move. The border is NOT the same on every side,
 *     and each width is a measurement of the bar around it: up and down it
 *     reaches 2u past the bar's 4u padding; to the right it stops at the bar's
 *     own 5u edge, because on a phone the window scrolls (`overflow: auto`)
 *     and a hair past that edge would hand the window a sideways scrollbar;
 *     and to the left it takes 2u, half the 4u gap, so it never sits on the
 *     operator console's enlarge box beside it.
 *   - the `?` grows an invisible `::after` around itself; it has no other.
 *   - `help` is padded out and pulled back by its own negative margin, so the
 *     line it sits in is laid out exactly as before.
 *
 * SIZED OFF THE FINGER, not off the dial. « fait tout » took the app's
 * windows down to its own 1.2 (client/index.html), where a 20u square is 24
 * pixels and a fixed 6u of extra reach left the X at 32x38. So each reach is
 * what is left of `--finger` once the drawn square is paid for -- never less
 * than it was -- and the X now answers across a finger at every dial:
 *
 *   - up and down: half of what the finger is short, or 6u, whichever is more;
 *   - right: the same, but never past the bar's own 5u edge (the window
 *     scrolls, and a hair past the edge is a sideways scrollbar);
 *   - left: the same half, so two buttons side by side -- the operator
 *     console's minimise, enlarge and close -- split the gap between them
 *     instead of overlapping; and that gap opens to a whole finger's spare on
 *     a phone (the `:has` rule below), so all three reach a finger across.
 *     A close button that is the only BUTTON in its strip (every other window,
 *     and the decorative squares beside it take no touch) reaches left for
 *     everything the right side could not.
 *
 * Only real buttons: the door's and the lobby's decorative squares are spans
 * that take no touch at all (`span.winbtn` above). */
body.onpad button.winbtn {
  --hit-x: max(calc(0 * var(--u)), calc((var(--finger) - 20 * var(--u)) / 2));
  --hit-y: max(calc(6 * var(--u)), var(--hit-x));
  --hit-r: var(--hit-x);
  --hit-l: var(--hit-x);
  width: calc(20 * var(--u) + var(--hit-l) + var(--hit-r));
  height: calc(20 * var(--u) + 2 * var(--hit-y));
  flex-basis: calc(20 * var(--u) + var(--hit-l) + var(--hit-r));
  margin: calc(-1 * var(--hit-y)) calc(-1 * var(--hit-r)) calc(-1 * var(--hit-y)) calc(-1 * var(--hit-l));
  border-style: solid; border-color: transparent;
  border-width: var(--hit-y) var(--hit-r) var(--hit-y) var(--hit-l);
  background-clip: padding-box;
  box-shadow: inset 0 0 0 var(--b1) var(--ink);
}
body.onpad .winbtns > button.winbtn:last-child {
  --hit-r: min(var(--hit-x), calc(5 * var(--u)));
}
body.onpad .winbtns > button.winbtn.close:only-of-type {
  --hit-l: max(calc(2 * var(--u)), calc(var(--finger) - 20 * var(--u) - var(--hit-r)));
}
body.onpad .winbtns:has(> button.winbtn + button.winbtn) {
  gap: max(calc(4 * var(--u)), calc(var(--finger) - 20 * var(--u)));
}
body.onpad .whatbtn { position: relative; }
/* 9u from the PADDING box, which is 7u past the drawn edge once the button's
   own 2u outline is paid for -- measured with a grid of hit tests, not assumed:
   a 24-pixel `?` answers a touch across 41 at the app's dial. */
body.onpad .whatbtn::after { content: ''; position: absolute; inset: calc(-9 * var(--u)); }
body.onpad .helplink {
  display: inline-block;
  padding: calc(12 * var(--u)) calc(8 * var(--u));
  margin: calc(-12 * var(--u)) calc(-8 * var(--u));
}

/* --- 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); }
/* ...and not the phone's either. Chrome on Android lays a translucent wash
   over any control that is tapped -- a soft glow by another name, and on the
   touch pad a second "pressed" laid over the pad's own lit state. Rule 3
   already says what pressed looks like here; the browser's second opinion is
   turned off. Nothing on a desk has a tap to highlight, so nothing there moves. */
* { -webkit-tap-highlight-color: transparent; }
/* 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;
  /* ONE line, which the ellipsis above always assumed and nothing said: without
     `nowrap` the realm name broke in two and the box grew a row. */
  white-space: nowrap; }
/* THE REVERSE WORLD on one line. Measured: at the row's own 9u and tracking it
   is 109 pixels of type in a 103-pixel cell (the app's dial; the box is 118u
   wide at every dial, so the ratio is the same on a desk). A notch smaller and
   untracked it is about 91, with room to spare -- still the row's own face,
   and still read at a glance. Only a REALM is set this way: a climate's name is
   short, and its distance needs the cell this one gives up. See `update` in
   client/src/compass.js, which toggles the class. */
#compass .cline.realm b { font-size: calc(8 * var(--u)); letter-spacing: 0; }
#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; }

/* --- Safety: the players here, the rules, the account (`client/src/safety.js`) ---
 * Rows of the same list, but not switches: the row is not a click target (the
 * buttons are), so it keeps the list's look and drops the pointer. A player's
 * row carries REPORT and BLOCK hard right, where a switch would sit. */
.setplayers { display: flex; flex-direction: column; gap: var(--b1); }
.setrow.plain { cursor: default; }
.setrow.plain:hover { background: var(--paper2); }
.setrow.plain > button { margin-left: auto; flex: 0 0 auto; font-size: var(--fs-s);
  padding: calc(4 * var(--u)) calc(8 * var(--u)); }
.setrow.plain > button + button { margin-left: calc(4 * var(--u)); }
.settext b { overflow-wrap: anywhere; }
.ruleslink { color: var(--violet); font-weight: 700; overflow-wrap: anywhere; }

/* The report window and the menu a line of chat opens: the confirm dialog's box
 * (`.confirm` below), with an ordinary window's violet bar -- a question about
 * somebody else, not a warning about something you are about to lose. */
.confirm.safety .winbar { background: var(--violet); }
.confirm.safety select, .confirm.safety textarea { width: 100%; box-sizing: border-box; }
.confirm.safety textarea { font-family: var(--mono); font-size: var(--fs-m); resize: vertical; }
.confirm.safety .quoted { font-style: italic; overflow-wrap: anywhere; color: var(--ink); }
.confirm.safety .opcheck { margin: 0; display: flex !important; align-items: center; }
/* `.confirm input` is the full-width typed name; a checkbox is not that. */
.confirm.safety .opcheck input[type=checkbox] { width: calc(15 * var(--u)); height: calc(15 * var(--u));
  flex: 0 0 auto; }
.confirm.safety .opcheck b { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }

/* --- 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; }
/* On a phone the keyboard list comes LAST, under the message button and its
   note (see `mount` in client/src/help.js), and wants its own gap back. */
.helpbody .note + .keys { margin-top: calc(12 * var(--u)); }
.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));
}
