/* ============================================================================
   ApogeaIdle.

   THE WIKI'S DESIGN LANGUAGE, NOT ITS TYPEFACE.  The palette, the bracketed
   frames, the torch-lit uppercase headers and the slot tiles are the wiki's own
   idiom -- the same tokens `sim.css` lifted out of atlas.css, so the fight and
   the rest of the game are visibly one program.  The FONT is deliberately not
   the wiki's: this is a different thing that shares a world, and a system stack
   also means no webfont round-trip on a page that is left open for hours.

   Dark, and only dark. This is a game window rather than a document, and the
   sprites are lit for a dark ground -- a light theme would mean two sets of art.

   image-rendering: pixelated everywhere a sprite is drawn. The art is 32px and
   96px cells; the browser's default smoothing turns pixel art to mud the moment
   it is scaled at all.
   ========================================================================== */
/* ── THREE FACES, BUNDLED ────────────────────────────────────────────────────
   The redesign's one new material. Two of them are bitmap-derived and break off
   their grid, which is why the sizes are fixed rather than fluid: Press Start 2P
   below 8px closes its counters (B reads as D, 5 as S), and Silkscreen is drawn
   for 8-9px. Jersey 25 carries every title, name and number.

   SERVED FROM DISK, NOT FROM A FONT CDN. The client is served from your own
   machine, and a page that needs the network to look right is a page that looks
   wrong on a train. All three are SIL Open Font License 1.1 -- the licence text
   travels with them in client/assets/fonts/LICENSE.txt.

   `swap` rather than `block`: a first paint in the fallback face is worse than
   nothing only if it never arrives, and these are 8-20 KB each. */
@font-face {
  font-family: "Jersey 25"; font-style: normal; font-weight: 400;
  font-display: swap; src: url("/assets/fonts/jersey25.woff2") format("woff2");
}
@font-face {
  font-family: "Press Start 2P"; font-style: normal; font-weight: 400;
  font-display: swap; src: url("/assets/fonts/pressstart2p.woff2") format("woff2");
}
@font-face {
  font-family: "Silkscreen"; font-style: normal; font-weight: 400;
  font-display: swap; src: url("/assets/fonts/silkscreen.woff2") format("woff2");
}
@font-face {
  font-family: "Silkscreen"; font-style: normal; font-weight: 700;
  font-display: swap; src: url("/assets/fonts/silkscreen-bold.woff2") format("woff2");
}

:root {
  /* The wiki's palette, token for token with sim.css. */
  --ink:     #0d1017;
  --panel:   #151a24;
  --panel-2: #1b2130;
  --line:    #2a3242;
  --ink-2:   #10141c;
  --text:    #d6dde8;
  --dim:     #7c8698;
  --torch:   #f0a53c;
  --torch-d: #a56c1f;

  --hp: #c0433f;
  --mp: #3f7fc0;
  --xp: #6ec06a;
  --bad: #ff8b7a;
  --good: #7fd18c;

  --radius: 6px;
  /* ── THE WINDOW COLUMN, AT THE WIDTH ITS CONTENTS ACTUALLY NEED ──────────
     276, not 300: four 58px slots and their gaps come to 247, and the rest was
     padding either side of a grid that had already stopped growing. Twenty-four
     pixels of town back. */
  --pane: 276px;

  /* ── THE RAISED-METAL CHROME ───────────────────────────────────────────────
     Not a new palette -- these are the shades the game's own HUD has always
     used for a bevelled slot (`.abslot`, `.hudbar`, `.stanceb`), named here so
     the rail, the plaques, the tray and the windows are demonstrably the same
     material as the thing they surround rather than a second one that happens
     to be a similar grey. */
  --edge:  #05070b;      /* the hard outer edge: 2px borders, bottom rules */
  --well:  #0a0d13;      /* the interior of anything recessed */
  --lift:  #2b3241;      /* the lit top of a raised surface */
  --mid:   #171d28;      /* its middle */
  --sink:  #0f141d;      /* its shaded bottom */
  --face:  #232a36;      /* tab and button body, top */
  --face-d:#12171f;      /* ... and bottom */
  --sub:   #8d97a8;      /* a label under a title */
  --faint: #5d6779;      /* quieter than --dim: a placeholder, a hint, a
                            row the list is only showing for completeness */
  --title: #e7ecf4;      /* a plaque's title */
  --brow:  #c3ccda;      /* an eyebrow over artwork */
  --lit:   #ffc46a;      /* the lit gate, the primary button's top */

  /* The three faces, so a rule says WHICH KIND of label it is setting rather
     than repeating a font stack. */
  --display: "Jersey 25", "Segoe UI Variable Display", system-ui, sans-serif;
  --loud:    "Press Start 2P", "Silkscreen", monospace;
  --label:   "Silkscreen", "Press Start 2P", monospace;

  /* The two header tiers, referred to by everything that has to sit under
     them. One number, changed in one place. */
  --rail: 66px;
  --tier: 30px;
  --head: 96px;

  /* Kept as aliases so nothing that referred to the old names breaks. */
  --bg: var(--ink);
  --muted: var(--dim);
  --accent: var(--torch);
  --gold: var(--torch);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--ink);
  color: var(--text);
  /* NOT the wiki's display face. A neutral UI stack, doing its work through
     case and letterspacing rather than through the letterforms. */
  font: 15px/1.5 "Inter", "Segoe UI Variable Text", "Segoe UI", system-ui,
        -apple-system, Roboto, "Helvetica Neue", sans-serif;
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
}

img, .sprite { image-rendering: pixelated; }
.hidden { display: none !important; }
.muted { color: var(--dim); font-size: 13px; }
.muted.off { opacity: .75; font-style: italic; }
.err { color: var(--bad); min-height: 1.4em; margin: 10px 0 0; font-size: 14px; }

/* Every heading in the game is a torch-lit rule with a diamond on it -- the
   wiki's own section marker. */
h2.dh {
  display: flex; align-items: center; gap: 8px;
  font-size: 12px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--torch); font-weight: 600;
  margin: 18px 0 10px;
}
h2.dh::before { content: "◆"; font-size: 9px; color: var(--torch-d); }
h2.dh::after {
  content: ""; flex: 1; height: 1px;
  background: linear-gradient(90deg, var(--line), transparent);
}
h2 { font-size: 13px; letter-spacing: .08em; text-transform: uppercase;
     color: var(--dim); margin: 0 0 12px; font-weight: 600; }
/* A verb sitting in a heading -- "Traits picked · Open the tree". `order: 1`
   puts it AFTER the rule rather than before it: h2.dh::after is the flex:1 line
   that fills the row, and a button in DOM order lands to its left, squashing
   the rule to nothing. */
.linkbtn { order: 1; flex: none; background: none; border: 0; padding: 0 0 0 8px;
           color: var(--dim); font: inherit; font-size: 10px;
           letter-spacing: .1em; text-transform: uppercase; cursor: pointer; }
.linkbtn:hover { color: var(--torch); }

button, input, select { font: inherit; color: inherit; }

/* ── the panel frame ─────────────────────────────────────────────────────────
   THE TORCH-COLOURED CORNER BRACKETS ARE GONE.  The wiki corners its panels
   with four short amber brackets and they were lifted along with everything
   else, but the wiki draws two or three panels on a page and this draws six at
   once, permanently, stacked down both sides of the screen. Twenty-four amber
   marks around the edge of every screen is not an accent -- it is a border made
   of punctuation, and it competes with the one thing the colour is actually for
   here, which is telling you something is worth looking at.

   `position: relative` stays: panels inside these are positioned against
   them. */
.frame { position: relative; background: var(--panel); border: 1px solid var(--line); }

button.primary {
  background: var(--torch); color: #1a1205; border: 0; border-radius: var(--radius);
  padding: 10px 16px; font-weight: 700; cursor: pointer;
  letter-spacing: .04em;
}
button.primary:hover { filter: brightness(1.1); }
button.primary:disabled { opacity: .4; cursor: not-allowed; filter: none; }

button.ghost {
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  border-radius: var(--radius); padding: 7px 12px; cursor: pointer;
}
button.ghost:hover:not(:disabled) { border-color: var(--torch); }
button.ghost:disabled { opacity: .35; cursor: not-allowed; }
button.ghost.wide { width: 100%; margin-top: 10px; }
button.buy {
  background: transparent; border: 1px solid var(--torch-d); color: var(--torch);
  border-radius: var(--radius); padding: 5px 12px; cursor: pointer;
  font-size: 13px; letter-spacing: .04em; text-transform: uppercase; font-weight: 600;
}
button.buy:hover:not(:disabled) { background: var(--torch); color: #1a1205; }
button.buy:disabled { opacity: .3; cursor: not-allowed; border-color: var(--line); color: var(--dim); }

button.link {
  background: none; border: 0; color: var(--dim); cursor: pointer;
  text-decoration: underline; padding: 4px 6px; font-size: 13px;
}
button.link:hover { color: var(--torch); }

input, select {
  background: var(--ink-2); border: 1px solid var(--line); color: var(--text);
  border-radius: var(--radius); padding: 9px 11px; width: 100%;
}
input:focus, select:focus { outline: 0; border-color: var(--torch-d); }

/* ── signed out / picker ─────────────────────────────────────────────────── */
.gate { min-height: 100vh; display: grid; place-items: center; padding: 24px; overflow: auto; }
.gate-card { width: 380px; max-width: 100%; padding: 28px; border-radius: 10px; }
/* WIDE ENOUGH FOR THREE. Three 168px cards, two 12px gaps and 28px of padding
   either side is 616; 720 leaves the row breathing and still fits a laptop. */
.gate-card.wide { width: 720px; }
.brand { font-size: 30px; margin: 0 0 6px; letter-spacing: .02em; color: var(--text); }
.brand span { color: var(--torch); }
.sub { color: var(--dim); font-size: 14px; margin: 0 0 20px; }
.form { display: grid; gap: 12px; }
.form.row { grid-template-columns: 1fr 1fr auto; align-items: end; }
.form label { display: grid; gap: 5px; font-size: 13px; color: var(--dim); }
.form small { color: var(--dim); font-size: 12px; }
.discord { display: block; text-align: center; margin-top: 14px; color: var(--dim); font-size: 14px; }

/* ── THE ROSTER ──────────────────────────────────────────────────────────────
   > *"Let's remake the character selection screen to look better, right now it
   > looks horrible."*

   IT LOOKED HORRIBLE BECAUSE IT HAD NEVER BEEN STYLED. The rules that used to
   be here were `.char`, and the element the client builds is `.charcard` -- the
   class was renamed in app.mjs and the stylesheet was left behind, so every row
   rendered as a browser-default `<button>`: light grey, centre-aligned, with
   the character's own name in pale grey on white. Dead rules under the old name
   and a live element with no rule at all.

   A ROW OF THREE, because three is the limit -- a vertical list of three is a
   list you read and a row of three is a roster you choose from. A floor and
   `auto-fill` rather than a fixed `repeat(3, 1fr)`: on a narrow window they
   stack themselves without a media query having to know how many there are.

   `auto-FILL`, NOT `auto-fit`, AND THE DIFFERENCE IS VISIBLE. auto-fit
   collapses the tracks nothing is in, so an account with one character got one
   card stretched across the whole width -- and the dashed "new character" slot
   on an empty account filled the row like a banner. auto-fill keeps the empty
   tracks, so a card is always a card and the row always reads as three slots.

   THE WHOLE CARD IS THE BUTTON. It was a button before too, which is why the
   default styling was so loud; now it looks like the thing it is. */
.charlist { display: grid; gap: 12px; margin-bottom: 18px;
            grid-template-columns: repeat(auto-fill, minmax(168px, 1fr)); }
.charcard {
  display: grid; gap: 0; padding: 0; overflow: hidden;
  background: linear-gradient(var(--panel-2), var(--ink-2));
  border: 1px solid var(--line); border-radius: 6px;
  cursor: pointer; text-align: left; color: inherit; font: inherit;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.05);
  transition: border-color .12s, transform .12s, box-shadow .12s;
}
.charcard:hover { border-color: var(--torch); transform: translateY(-2px);
                  box-shadow: 0 6px 18px rgba(0,0,0,.45),
                              inset 0 1px 0 rgba(255,255,255,.07); }
.charcard:focus-visible { outline: 2px solid var(--torch); outline-offset: 2px; }

/* ── THE FIGURE, AT A SIZE YOU CAN RECOGNISE ─────────────────────────────────
   46px of a whole standing person is a person you cannot make out; the rail's
   medallion already learned that and crops to head and shoulders because it has
   no room. Here there IS room, so the whole figure stands in a lit well.

   `object-fit: contain` and a fixed box: the flat renders and the real portrait
   have different proportions -- the portrait is fitted to the figure -- and a
   card that resized when the picture upgraded would be a row that jumps. */
.ccface {
  position: relative; height: 150px;
  background: radial-gradient(120% 90% at 50% 18%, #2b3446 0%, var(--well) 70%);
  border-bottom: 1px solid var(--line);
  display: grid; place-items: end center;
}
.ccface img, .ccface .sprite {
  height: 142px; width: 100%; object-fit: contain; object-position: bottom center;
}
.ccface img.rendered { image-rendering: auto; }
/* Stamped on the frame, the same shape and the same corner the medallion uses:
   one language for "how far along is this character". */
.cclvl {
  position: absolute; left: 0; bottom: 0; padding: 1px 5px;
  font: 400 11px var(--label); color: var(--ink); background: var(--torch);
}
/* On the PICTURE, because where the character is standing is a fact about the
   character -- the facts below are all about what it owns. */
.ccpill {
  position: absolute; right: 4px; top: 4px; padding: 1px 6px;
  font: 400 9px var(--label); letter-spacing: .05em; text-transform: uppercase;
  color: var(--torch); background: rgba(10,13,19,.82);
  border: 1px solid var(--torch-d); border-radius: 2px;
}

.ccbody { padding: 9px 10px 10px; display: grid; gap: 2px; }
.ccname { font: 400 17px var(--display); color: var(--text);
          overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cccls { display: flex; align-items: center; gap: 5px;
         color: var(--dim); font-size: 12px; }
.cccls .clsico { image-rendering: pixelated; }

/* TWO TILES IN THE SAME PLACE ON EVERY CARD, which is what makes them
   comparable -- the run-on line they replace ("Level 34 Mage · 12,400 gold ·
   26,770 kills") put three numbers where none of them lined up with the
   character beside it. */
.ccfacts { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-top: 7px; }
.ccfact { padding: 5px 6px; background: rgba(0,0,0,.25);
          border: 1px solid var(--line); border-radius: 3px; }
.ccfv { font: 400 14px var(--display); color: var(--text);
        font-variant-numeric: tabular-nums; }
.ccfl { font: 400 9px var(--label); letter-spacing: .06em; text-transform: uppercase;
        color: var(--faint); margin-top: 1px; }

/* ── THE EMPTY SLOT ──────────────────────────────────────────────────────────
   Dashed, quiet, and the same size as the cards beside it, so "three slots, one
   free" is the picture rather than a sentence under it. It focuses the creation
   form below rather than replacing it -- see refreshPicker. */
.charcard.new {
  place-content: center; place-items: center; gap: 4px;
  min-height: 150px; padding: 20px 12px;
  background: none; border-style: dashed; box-shadow: none;
}
.charcard.new:hover { background: rgba(240,165,60,.05); transform: none;
                      box-shadow: none; }
.ccplus { font: 400 26px var(--display); color: var(--torch-d); line-height: 1; }
.charcard.new .ccname { font-size: 14px; color: var(--dim); }
.ccfree { font: 400 9px var(--label); letter-spacing: .06em; text-transform: uppercase;
          color: var(--faint); }
.pickcap { margin: -8px 0 16px; }

/* ── THE SHELL: TWO HEADER TIERS AND THE GAME UNDER THEM ─────────────────────
   There were three columns: a 300px character sheet, the town, and a 300px bag.
   Two thirds of the screen was interface about the game rather than the game,
   permanently, and the only way out was a grip on the inner edge that nobody
   found. The town in the middle was a picture with six cards drawn on top of
   it -- a menu with scenery behind it.

   Now the screen is the game and the chrome is a rail across the top. The stage
   is one column; the sheet, the gear and the bag are windows over it. */
/* ── FULL BLEED ──────────────────────────────────────────────────────────────
   No padding and no gap. The stage used to be a padded grid with the HUD as a
   ROW UNDER the art, which left a dead black strip across the bottom of the
   screen with the hotbar sitting in it -- the picture stopping short of its own
   frame for no reason. The art fills the frame now and the HUD floats on it,
   which is where a game puts a hotbar. */
.stage {
  position: relative; z-index: 1;
  height: calc(100vh - var(--head));
  display: grid; grid-template-columns: minmax(0, 1fr);
  gap: 0; padding: 0;
}

/* ── THE RAIL ────────────────────────────────────────────────────────────────
   66px, and it is a raised metal bar: lit along the top edge, shaded into its
   own bottom, with the hard 2px rule under it that everything in this game uses
   to say "this surface ends here". */
.rail {
  position: relative; z-index: 20;
  height: var(--rail); display: flex; align-items: center; gap: 14px;
  padding: 0 14px;
  background: linear-gradient(var(--lift), var(--mid) 62%, var(--sink));
  border-bottom: 2px solid var(--edge);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.10),
              inset 0 -8px 18px rgba(0,0,0,.5);
}
.railgap { flex: 1; }
.raildiv { width: 2px; height: 40px; flex: none;
           background: linear-gradient(transparent, var(--edge), transparent); }

/* ── THE MEDALLION ───────────────────────────────────────────────────────────
   Who you are, at a glance, and a door to the rest of it. The portrait is
   deliberately cropped to head and shoulders -- the class art is a full figure
   and a 46px square of a whole person is a person you cannot make out. */
.medallion {
  display: flex; align-items: center; gap: 11px; flex: none;
  padding: 4px 12px 4px 4px; cursor: pointer;
  background: linear-gradient(#1b2130, var(--ink-2));
  border: 1px solid var(--edge); border-radius: 4px;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.08);
  color: inherit; font: inherit; text-align: left;
}
.medallion:hover { border-color: var(--torch-d); }
/* ── THE WELL IS PART OF THE HEADER, NOT A HOLE IN IT ────────────────────────
   > *"And paint that image background the same color of the header
   > background."*

   It was `--well`, the near-black used for anything RECESSED -- a depot slot, an
   empty bag square, the inside of a bar. That is the right colour for a hole and
   the wrong one for a picture frame: a portrait cut out of the rail and dropped
   onto black reads as a photograph with the lights off behind it. The rail's own
   gradient, at the rail's own angle, makes the figure look like it is standing
   ON the header rather than in a box cut into it. */
.mwell {
  position: relative; width: 46px; height: 46px; flex: none;
  background: linear-gradient(var(--lift), var(--mid) 62%, var(--sink));
  border: 2px solid var(--torch-d); border-radius: 3px;
  overflow: hidden; display: grid; place-items: center;
}
/* ── AND THE FIGURE IS BIG ENOUGH TO RECOGNISE ───────────────────────────────
   > *"On the header character picture, can we move the character slightly up?
   > the reason for this is that you can barely see the character."*

   The note above always SAID this crops to head and shoulders. It did not: at
   54px tall, nudged DOWN seven pixels, the top third of the square was empty and
   the person was a 20-pixel doll in the corner of it with their feet cut off.
   Measured rather than eyeballed -- the first thirteen rows of the picture had
   nothing in them at all.

   72 AND MINUS SIX is the crop the note describes: the head lands near the top
   edge and the square fills with the head, the shoulders and the chest, which is
   what tells two of your characters apart at 46 pixels. Chosen by rendering six
   crops side by side and looking at them; the alternatives were a whole figure
   too small to read and a head clipped by the frame. */
.mwell img { height: 72px; margin-top: -6px; }
/* THE REAL RENDER IS NOT PIXEL ART. The flat outfit renders are drawn
   nearest-neighbour (they are small and sharp-edged); the portrait comes out of
   the arena's own renderer at more than twice the size it is shown at, and
   pixelating THAT is throwing the resolution away. Same rule the podium and the
   profile head already use -- see `.pod .art.rendered`. */
.mwell img.rendered { image-rendering: auto; }
/* Overhanging by a pixel, so it reads as stamped onto the frame rather than
   floating inside the picture. */
.mlvl {
  position: absolute; left: -1px; bottom: -1px; padding: 0 3px;
  font: 400 9px var(--label); color: var(--ink);
  background: var(--torch);
}
/* 104, NOT 150. The rail carries twelve tabs now and every pixel the
   medallion is not using is a pixel that keeps them on one line. 104 is the
   width of a tab, which is not a coincidence: the readout and the buttons
   beside it are on the same grid. */
.mtext { display: flex; flex-direction: column; gap: 4px; min-width: 104px; }
.mname { display: flex; align-items: center; gap: 6px;
         font: 400 15px var(--display); letter-spacing: .08em;
         color: var(--torch); text-transform: uppercase; }
.mname .clsico { image-rendering: pixelated; }
.mxp { display: flex; align-items: center; gap: 6px; }
/* A PLAIN BAR, NOT SEGMENTED. Segments imply steps you reach; this is a
   continuous distance to the next level. */
.mxp .xpline { display: block; width: 96px; height: 7px; flex: none;
               background: var(--well); border: 1px solid var(--edge); }
.mxp .xpline i { display: block; height: 100%; width: 0;
                 background: linear-gradient(#8fe08a, #3e7c3c); }
.xppct { font: 400 9px var(--label); color: var(--dim); }

/* ── THE TWELVE TABS ─────────────────────────────────────────────────────────
   ONE WIDTH. They were `padding: 9px 13px` and therefore as wide as their
   words, which on a row of six is a ragged but tolerable rhythm and on a row of
   twelve is a picket fence. 104px is what the longest label needs; every tab
   gets it, the icons line up down the row, and the whole nav is a grid rather
   than a sentence. */
.places { display: flex; gap: 4px; }
.placetab {
  /* MIN, NOT FIXED. 104 is what the longest English label needs, and at that
     width every tab is the same and the icons line up down the row -- which is
     the whole point of the grid. It is a floor rather than a wall because the
     rail is translated: `EQUIPAMENTO` is longer than `EQUIPMENT`, and a tab
     that clipped its own word to keep the rhythm would be choosing the rhythm
     over the label. The grid is exact in English and near-exact elsewhere. */
  min-width: 104px; flex: none;
  display: flex; align-items: center; justify-content: center; gap: 7px;
  padding: 9px 6px;
  cursor: pointer; background: linear-gradient(var(--face), var(--face-d));
  border: 1px solid var(--edge); border-radius: 3px; color: var(--sub);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.07);
  font: 400 9px var(--label); letter-spacing: .08em; text-transform: uppercase;
  position: relative; white-space: nowrap;
}
/* IN THE CORNER, NOT IN THE ROW. A count that sits after the label is a count
   that pushes the label sideways -- and on a tab with a floor width, pushes it
   out of the tab. Stamped on the corner instead, where it overlaps nothing that
   has to be read. */
.placetab .railbadge { position: absolute; right: 2px; top: 2px; }
/* THE GAME'S OWN SPRITES, at the size they were drawn for. Nearest-neighbour,
   because everything else on this screen that came out of the atlas is. */
.placetab .ico { width: 20px; height: 20px; flex: none; display: block;
                 background: center / contain no-repeat;
                 image-rendering: pixelated; }
/* The plaques still use the line glyphs, so the stroked rule stays. Nothing in
   the rail draws one any more -- the cog was the last, and it is `cog.png`. */
.placetab svg { width: 20px; height: 20px; flex: none; stroke: currentColor;
                fill: none; stroke-width: 1.6;
                stroke-linecap: round; stroke-linejoin: round; }
.placetab:hover { color: var(--text); border-color: var(--torch-d); }
.placetab.on {
  background: linear-gradient(#f0a53c22, #241c10);
  border-color: var(--torch-d); color: var(--torch);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.10);
}
/* A room that is in town, while you are not. Dimmer than a normal tab and
   plainly not lit -- but still pressable, because "why can I not shop" deserves
   an answer and a dead button gives none. */
.placetab.away { opacity: .5; }
.placetab.away .ico { filter: grayscale(.7); }

/* ── DRAWN, AND DARK ────────────────────────────────────────────────────────
   Quests has no system behind it. It holds its place in the row so the header
   does not rearrange itself the day it arrives, and it is unmistakably off:
   flat, dim, no hover, no pointer. */
.placetab.soon {
  opacity: .38; cursor: default; background: linear-gradient(#1b2130, #12171f);
  box-shadow: none;
}
.placetab.soon:hover { color: var(--sub); border-color: var(--edge); }
.placetab.soon .ico { filter: grayscale(1); }

/* WHERE THE TOWN ENDS AND YOU BEGIN. This is all that is left of the 200px gap
   that used to separate the places from your own windows -- and it says the
   same thing without breaking the row in half. */
.railsep { width: 1px; height: 26px; flex: none; align-self: center;
           margin: 0 5px;
           background: linear-gradient(transparent, #2a3242 30%,
                                       #2a3242 70%, transparent); }
/* The count on Traits when there are points to spend, and on Chat when
   somebody has said something. Both flicker, because both want to be noticed
   and neither should shout. */
.railbadge {
  min-width: 15px; padding: 0 3px; font: 400 9px var(--label);
  line-height: 14px; text-align: center; color: var(--ink);
  background: var(--torch); border-radius: 2px;
  animation: torchflick 1.6s ease-in-out infinite;
}
@keyframes torchflick { 0%, 100% { opacity: .85; } 50% { opacity: 1; } }

/* ── `railmine` IS GONE, AND THE NAMING LESSON IT CARRIED IS NOT ─────────────
   The five narrow pills at the far end of the rail are ordinary tabs now, in
   the same nav as the six places, so `.railmine` and `.minebtn` have no
   elements left to style.

   The comment that lived here is worth keeping, because the bug it records will
   happen again to somebody else. `.railmine` was `.mine` once, which is a STATE
   WORD, and the arena already owned it: the distance strip wears `.pl-gap.mine`
   when the creature is inside your reach, and a bare `.mine { display: flex }`
   in this file turned that strip into a flex container -- collapsing its 560px
   field to the width of its own two borders, which is what made the range
   readout disappear. `.who` and `.dragging` were the same mistake.

   THE RULE: a bare adjective is a state, and states belong to whatever owns
   them. A component in this file gets a component's name. */

/* ── THE STATUS TIER ─────────────────────────────────────────────────────────
   The one question an idle game must answer without being asked: how is it
   going. In town that is the last trip; hunting, it is this one -- and the tier
   turns warm-black, which is the only thing on the whole screen that says you
   are not in town. That is its entire job, so it is worth a colour of its own. */
.statustier {
  position: relative; z-index: 19;
  height: var(--tier); display: flex; align-items: center; gap: 0;
  padding: 0 14px; overflow: hidden;
  background: linear-gradient(#141922, #0b0f15);
  border-bottom: 2px solid var(--edge);
  box-shadow: 0 3px 10px rgba(0,0,0,.6);
}
.statustier.hunting { background: linear-gradient(#2a1a12, #140b07); }
.stwhere { flex: none; font: 400 8px var(--loud); letter-spacing: .12em;
           color: var(--torch); text-transform: uppercase; }
.statustier.hunting .stwhere { color: var(--bad); }
.stsep { width: 1px; height: 14px; flex: none; background: var(--line);
         margin: 0 12px; }
.statustier.hunting .stsep { background: #4a2f22; }
.stline { font: 400 9px var(--label); letter-spacing: .10em; color: var(--dim);
          text-transform: uppercase; white-space: nowrap;
          overflow: hidden; text-overflow: ellipsis; }
.stright { flex: none; padding-left: 12px; }
.stline .n { color: var(--text); }
.stline .xp { color: var(--xp); }
.stline .go { color: var(--torch); }

/* ── THE TOWN, AS A PLACE ────────────────────────────────────────────────────
   The art is the screen. It is `cover`ed rather than letterboxed, and the
   plaques are positioned in the IMAGE's coordinate space through the three
   custom properties placeTown() sets -- so a plaque stays on its building at
   every window size instead of drifting off it the moment the aspect changes.
   That arithmetic cannot be done in CSS, so it is done where it can be, once,
   on resize. */
.townspace { flex: 1; min-height: 0; position: relative; }
.townart {
  position: absolute; inset: 0; overflow: hidden;
  --tx: 0px; --ty: 0px; --ts: 1;
  background: url("/assets/town/basile.jpg") center / cover no-repeat;
  image-rendering: pixelated;
}
.townvig { position: absolute; inset: 0; pointer-events: none;
           background: radial-gradient(130% 105% at 50% 45%,
                       transparent 40%, rgba(9,11,16,.55) 100%); }
.plaques { position: absolute; inset: 0; }

/* ── A PLAQUE ────────────────────────────────────────────────────────────────
   A label, a wire and a lit pin. The pin is the anchor -- it sits ON the
   building -- and the label hangs off it, above or below depending on whether
   there is room above. Two variants of one component, children reordered. */
.plaque {
  position: absolute; display: flex; flex-direction: column; align-items: center;
  padding: 0; background: none; border: 0; cursor: pointer; color: inherit;
  font: inherit; text-align: left;
  /* ── AND `--pdx` IS HOW FAR IT HAD TO STEP ASIDE ─────────────────────────
     > *"Move the set sail button slightly to the left so if the player have the
     > inventory open, the button doesn't sit behind the inventory panel."*

     Zero almost always: a plaque sits on its building. It goes negative only
     while a side window is open over the town and the plaque would otherwise be
     underneath it, and it is set by keepPlaquesClear() in app.mjs because the
     answer needs the plaque's own WIDTH and CSS cannot read that. */
  left: calc(var(--tx) + var(--px) * var(--ts) * 1px + var(--pdx, 0px));
  top:  calc(var(--ty) + var(--py) * var(--ts) * 1px);
}
.plaque .pbox {
  display: flex; align-items: center; gap: 9px; padding: 7px 12px;
  background: linear-gradient(#2f3745, #151a22);
  border: 2px solid var(--edge); border-radius: 3px;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.14), 0 6px 18px rgba(0,0,0,.85);
  transition: border-color .12s, box-shadow .12s, transform .12s;
}
.plaque svg { width: 22px; height: 22px; flex: none; stroke: var(--torch);
              fill: none; stroke-width: 1.6;
              stroke-linecap: round; stroke-linejoin: round; }
.plaque .ptitle { font: 400 14px var(--display); letter-spacing: .10em;
                  color: var(--title); text-transform: uppercase; }
.plaque .psub { font: 400 9px var(--label); letter-spacing: .06em;
                color: var(--sub); text-transform: uppercase; }
.plaque .psub.good { color: var(--xp); }
.plaque .pwire { width: 2px; height: 24px; flex: none; }
.plaque .ppin { width: 9px; height: 9px; flex: none; background: var(--torch);
                border: 1px solid var(--edge);
                box-shadow: 0 0 8px rgba(240,165,60,.9); }
/* Label above the pin: the wire fades downward into it. */
.plaque.above .pwire { background: linear-gradient(var(--edge), transparent); }
/* Label below: the same wire, the other way up. */
.plaque.below .pwire { background: linear-gradient(transparent, var(--edge)); }
.plaque:hover .pbox { border-color: var(--torch);
                      box-shadow: inset 0 1px 0 rgba(255,255,255,.18),
                                  0 0 20px rgba(240,165,60,.35),
                                  0 6px 18px rgba(0,0,0,.85);
                      transform: translateY(-3px); }

/* THE ONE LIT PLAQUE. The gate is where the game actually happens, so it is the
   only one that glows -- and it is a different weight of the same component
   rather than a different component. */
.plaque.gate .pbox {
  gap: 10px; padding: 10px 14px;
  background: linear-gradient(#f0a53c2e, #3a2f1c 50%, #20180d);
  border-color: var(--torch-d);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.18),
              0 0 22px rgba(240,165,60,.3), 0 6px 18px rgba(0,0,0,.85);
}
.plaque.gate svg { width: 26px; height: 26px; stroke: var(--lit); }
.plaque.gate .ptitle { font-size: 16px; color: #ffe6bd; }
.plaque.gate .psub { color: var(--torch); }
.plaque.gate .ppin { background: var(--lit);
                     box-shadow: 0 0 10px rgba(240,165,60,1); }
.plaque.gate.below .pwire { height: 26px;
                            background: linear-gradient(transparent, var(--lit)); }

@media (prefers-reduced-motion: reduce) {
  .railbadge { animation: none; }
  .plaque:hover .pbox { transform: none; }
}

/* ── THE WINDOW LAYER ────────────────────────────────────────────────────────
   What used to be the two permanent columns. They keep their old corners so the
   habit survives -- sheet left, gear and bag stacked right -- and only the
   permanence goes.

   The SIDES take no mouse and the WINDOWS do, or a closed window would leave an
   invisible column standing over the town swallowing plaque clicks. */
.winside {
  position: fixed; top: calc(var(--head) + 10px); bottom: 10px;
  width: var(--pane); z-index: 15; pointer-events: none;
  display: flex; flex-direction: column; gap: 10px;
}
.winside.left { left: 10px; }
.winside.right { right: 10px; }
.win {
  pointer-events: auto; min-height: 0; display: flex; flex-direction: column;
  background: linear-gradient(#1d2330, #12171f);
  border: 2px solid var(--edge); border-radius: 4px;
  box-shadow: inset 0 0 0 1px var(--line), 0 20px 50px rgba(0,0,0,.85);
  overflow: hidden;
}
.win[hidden] { display: none; }
.win.grow { flex: 1; }
.winbar {
  display: flex; align-items: center; gap: 8px; flex: none;
  padding: 6px 8px 6px 11px;
  background: linear-gradient(#39424f, #1b2130);
  border-bottom: 1px solid var(--edge);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.10);
}
.wintitle { font: 400 9px var(--label); letter-spacing: .12em;
            text-transform: uppercase; color: var(--brow); }
/* The count belongs beside the name, not inside the body: "8 of 10 worn" and
   "17 stacks" are what the window IS, and a line of it at the top of the
   contents was a line the contents had to make room for. */
/* ONE LINE. The bar is a title, and a title that wraps is a title bar that
   changes height with its contents. */
.winsub { font: 400 9px var(--label); letter-spacing: .06em; color: var(--dim);
          white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
          min-width: 0; }
/* Above the scroll, so the one number that decides whether you can pick
   anything else up does not leave the screen when you look at the bottom of
   your bag. */
/* ── THE FIXED HALF OF THE WINDOW ────────────────────────────────────────────
   The doll, the equip pips and the weight bar. None of the three may scroll:
   the doll is what the bag is FOR, and the weight is the one number that
   decides whether you can pick anything else up -- it used to be the first
   thing in the scrolling body, so it left the screen the moment you looked at
   the bottom of your bag. */
.winband { flex: none; padding: 9px 11px 0;
           display: flex; flex-direction: column; gap: 8px; }
.winband .loadbar { margin: 0; height: 14px; }
.winband .dollwrap { margin: 0; }
.winband .eqpips { margin: 0; }
.winshut {
  width: 20px; height: 20px; padding: 0; flex: none; cursor: pointer;
  background: rgba(9,11,16,.6); border: 1px solid var(--line);
  border-radius: 2px; color: var(--text); font-size: 11px; line-height: 1;
}
.winshut:hover { color: var(--torch); border-color: var(--torch-d); }
.winbody {
  flex: 1; min-height: 0; padding: 12px 12px 14px;
  overflow-x: hidden; overflow-y: auto;
  scrollbar-width: thin; scrollbar-color: var(--line) transparent;
}
/* Long words -- an item name, a trait, a spell -- wrap rather than widen. */
.winbody, .winbody * { min-width: 0; overflow-wrap: anywhere; }
.winbody::-webkit-scrollbar { width: 8px; }
.winbody::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px; }

/* `position: relative` because the chat is pinned to this column's bottom-left
   corner -- see the note there. It is the column and not the window, so the
   corner it finds is the corner of the play area rather than of the browser. */
.middle { position: relative; min-width: 0; min-height: 0; display: flex;
          flex-direction: column; gap: 0; }

/* ── who you are ─────────────────────────────────────────────────────────── */
.who { display: flex; gap: 12px; align-items: flex-start; }
.portrait {
  width: 62px; height: 76px; flex: none; overflow: hidden;
  background: var(--ink-2); border: 1px solid var(--line); border-radius: 4px;
  display: grid; place-items: center;
}
.portrait img { max-width: 100%; max-height: 100%; }
.whotext { min-width: 0; flex: 1; }
.cname { color: var(--torch); font-size: 15px; font-weight: 700;
         letter-spacing: .07em; text-transform: uppercase;
         overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.xpline { height: 5px; background: var(--ink-2); border: 1px solid var(--line);
          border-radius: 3px; overflow: hidden; }
.xpline i { display: block; height: 100%; background: var(--torch); width: 0; }
.xptext { font-size: 11px; color: var(--dim); margin-top: 3px;
          font-variant-numeric: tabular-nums; }

.vitals { display: grid; gap: 6px; margin-top: 12px; }
.bar { position: relative; height: 20px; background: var(--ink-2);
       border: 1px solid var(--line); border-radius: 4px; overflow: hidden; }
.bar i { position: absolute; inset: 0 auto 0 0; width: 0; }
.bar.hp i { background: linear-gradient(180deg, #d4544f, #8d2b28); }
.bar.mp i { background: linear-gradient(180deg, #4f8fd4, #27548d); }
.bar span { position: absolute; inset: 0; display: grid; place-items: center;
            font-size: 12px; font-variant-numeric: tabular-nums;
            text-shadow: 0 1px 2px rgba(0,0,0,.8); }

/* ── the class button and its menu ───────────────────────────────────────── */

.classmenu {
  position: fixed; z-index: 70; left: 18px; top: 190px; width: 264px;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 8px;
  padding: 10px; box-shadow: 0 14px 40px rgba(0,0,0,.7); display: grid; gap: 5px;
}
.cmhead { font-size: 11px; letter-spacing: .12em; text-transform: uppercase;
          color: var(--dim); margin-bottom: 2px; }
.cmrow { display: flex; align-items: center; gap: 9px; padding: 7px 9px; cursor: pointer;
         background: var(--ink-2); border: 1px solid var(--line);
         border-radius: 5px; color: var(--text); text-align: left; }
.cmrow .grow { flex: 1; }
.cmrow:hover:not(:disabled) { border-color: var(--torch); }
.cmrow.on { border-color: var(--torch-d); }
.cmrow:disabled { opacity: .45; cursor: not-allowed; }

/* ── THE SHEET'S OWN ROOM ────────────────────────────────────────────────────
   Two columns: what you ARE on the left -- what the build does, and where every
   attribute came from -- and what you CHOSE on the right, traits and spells.
   1.5fr to 1fr because the left column carries bars and arithmetic and the
   right carries sentences. */
/* ── THE SHEET FITS, OR IT IS NOT A SHEET ────────────────────────────────────
   > *"The character sheet is to long, it giving players a vertical scrollbar,
   > let's compact it so the player doesn't have to scroll."*

   A character sheet is a thing you take in at a glance and compare against
   itself -- Health against Mana, this build against the one you had before. A
   scrollbar breaks exactly that: two attributes you want to compare are never
   on screen together, and the answer to "what did that trait do" is above the
   fold. Everything below is one budget, spent to get thirteen attributes, five
   verdict tiles, the traits and the spells into one screenful. */
/* 1.9 AND NOT 1.5. The spells moved out of the right column and into the left
   (see `VIEWS.sheet`), so the split that balanced two loaded columns now leaves
   one carrying everything and the other carrying the traits. The wider left
   column is also what keeps the spell grid to one row and the ATTRIBUTES
   heading to one line -- both of which were costing the sheet a scrollbar. */
.sheetgrid { display: grid; grid-template-columns: minmax(0, 1.9fr) minmax(0, 1fr);
             gap: 16px; align-items: start; }
/* THE BUDGET IS THE NARROW CASE. The sheet has to fit with your bag open, and
   an open window takes 276px off each side of the card -- which is what turns
   the five verdict tiles into two rows and makes every gap in this column count
   twice. Everything here is sized for that case, not for an empty screen. */
/* 5px and not 6: this column carries seven sections now and the sheet is
   measured to the pixel -- see the note on `.overlay`'s bottom edge. */
.shcol { min-width: 0; display: flex; flex-direction: column; gap: 5px; }
/* The verdict and what you are currently digesting, across the whole sheet --
   both are judgments on the whole character rather than on one of its columns,
   and across the sheet the five tiles are one row instead of two. */
.shband { grid-column: 1 / -1; min-width: 0;
          display: flex; flex-direction: column; gap: 6px; }
/* Everything in a column is a section with a heading; the gap above belongs to
   the heading, so a section's contents sit under their own title. */
.shcol > .shhead { margin-bottom: -8px; }

/* One heading, five places, so they cannot drift apart: a diamond, the words,
   whatever the section keeps in its corner, and a rule running off right. */
.shhead { display: flex; align-items: center; gap: 8px; margin: 0;
          font-size: 11px; letter-spacing: .14em; text-transform: uppercase;
          color: var(--torch); font-weight: 600; }
.shhead .d { font-size: 9px; color: var(--torch-d); }
.shhead .rule { order: 9; flex: 1; height: 1px;
                background: linear-gradient(90deg, var(--line), transparent); }
.shhead .shnote { font: 400 9px var(--label); letter-spacing: .06em;
                  color: var(--torch); text-transform: none; }
.shhead .linkbtn { text-transform: uppercase; letter-spacing: .1em; font-size: 10px; }

/* ── WHAT IT DOES ────────────────────────────────────────────────────────────
   Five tiles, and not one of them is an attribute. `auto-fit` rather than a
   fixed five, so the row reflows to two lines on a narrow window instead of
   squeezing each number into 60px. */
/* FIVE ACROSS, NOT TEN DOWN. `minmax(112px, …)` wrapped the row into two lines
   on a real character -- one long label was all it took -- and a wrapped verdict
   row costs seventy pixels the sheet does not have. 96 is what the narrowest of
   the five needs, and `auto-fit` still folds them on a genuinely narrow
   window. */
.verdict { display: grid; grid-template-columns: repeat(auto-fit, minmax(104px, 1fr));
           gap: 6px; }
.vtile { padding: 4px 9px; background: var(--ink-2); border: 1px solid #202836;
         border-radius: 5px; box-shadow: inset 0 1px 0 rgba(255,255,255,.03); }
.vtile .l { font: 400 8px var(--label); letter-spacing: .1em;
            text-transform: uppercase; color: var(--dim);
            white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vtile .v { font: 400 21px var(--display); line-height: 1.1; margin-top: 1px;
            color: #e7ecf4; }
.vtile .v.torch { color: var(--torch); }
.vtile .v.good  { color: #7fd18c; }
.vtile .v.mana  { color: #6fa8dc; }
/* `.s` is gone from the markup -- the reasoning is on the tile's hover. The
   rule stays for nothing else to inherit; the tile is a label and a number. */

/* ── ATTRIBUTES, AS COMPOSITION ──────────────────────────────────────────────
   Name and class multiplier, a bar in four colours, the total, a plus, a caret.
   Fixed columns rather than flex, because the whole point is that the bars line
   up down the page: a bar that starts where the longest name ends is a bar you
   cannot compare with the one above it. */
/* IN THE HEADING, NOT UNDER IT. Four colour chips and a hint were their own
   17px line plus a 14px gap above it, to say something that fits in the space
   the "ATTRIBUTES" rule was already wasting. */
/* ── AND IT DOES NOT PUSH THE HEADING ONTO A SECOND LINE ─────────────────────
   The legend rides in the ATTRIBUTES heading, and at the width this column has
   with a window open it was wrapping -- twenty-seven pixels of heading spent
   saying nothing new, on a sheet measured to the pixel. Tighter gaps and a
   smaller swatch keep it on the line; `flex-wrap: nowrap` means that when it
   genuinely cannot fit, the RULE gives way (it is `flex: 1` and last) rather
   than the four words folding under themselves. */
.shlegend { display: flex; align-items: center; gap: 9px; flex-wrap: nowrap;
            font-size: 10px; color: var(--dim); }
.shlegend .swatch { display: inline-flex; align-items: center; gap: 5px;
                    white-space: nowrap; }
.shlegend .swatch i { width: 12px; height: 8px; border-radius: 2px;
                      display: inline-block; flex: none; }
.shlegend .hint { margin-left: auto; }
.g-base  { background: #4a5670; }
.g-pts   { background: var(--torch); }
.g-gear  { background: #5fbf6f; }
.g-trait { background: #b07fd9; }

.statsbox { display: flex; flex-direction: column; }
/* THIRTEEN OF THESE, so every pixel of padding is thirteen pixels of scrollbar.
   8px became 3 and the total dropped five points; nothing was removed from the
   row, it is simply drawn at the size a list of thirteen wants rather than the
   size a list of four would. */
.statrow { display: grid; align-items: center;
           grid-template-columns: minmax(0, 205px) minmax(0, 1fr) 74px 26px 16px;
           gap: 12px; padding: 2px 6px; font-size: 13px;
           border-bottom: 1px solid rgba(42, 50, 66, .45); }
.statrow.can { cursor: pointer; }
.statrow.can:hover { background: rgba(240, 165, 60, .05); }
/* `.statrow.nil` no longer dims. Everything else on this screen that is greyed
   is greyed because you cannot use it, so a faded Magic row said "not yours to
   spend on" about the row a point would change most. The empty bar says the
   number is zero without making a claim about permission. */
.statrow .who { display: flex; align-items: center; min-width: 0;
                color: var(--text); }
.statrow .who > img, .statrow .who > .ph { margin-right: 9px; flex: none; }
.statrow .lbl { min-width: 0; overflow: hidden; text-overflow: ellipsis;
                white-space: nowrap; }
.statrow .compbar { display: flex; height: 10px; border-radius: 3px;
                    overflow: hidden; background: rgba(42, 50, 66, .5); }
.statrow .compbar i { height: 100%; }
.statrow .tot { text-align: right; font: 400 15px var(--display);
                font-weight: 400; color: #e7ecf4;
                font-variant-numeric: tabular-nums; }
.statrow .plus { width: 24px; height: 18px; padding: 0; line-height: 1;
                 font-size: 13px; }
/* HIDDEN, NOT REMOVED. A plus that disappears when the last point is spent
   would take 26px out of every row and shuffle the totals sideways. */
.statrow .plus.off { visibility: hidden; }
.statrow .caret { justify-self: end; width: 0; height: 0; border-style: solid;
                  border-width: 5px 0 5px 7px;
                  border-color: transparent transparent transparent var(--dim);
                  transition: transform .12s ease; }
.statrow.open .caret { transform: rotate(90deg); }

/* The arithmetic, when a row is opened. A sibling of the row rather than a
   child, so it spans the whole grid instead of living in one of its columns. */
/* ── THE ARITHMETIC, IN COLUMNS ──────────────────────────────────────────────
   > *"Align the stat multipliers vertically."*

   It was `flex` with a 22px gap, so every term was as wide as its own numbers
   and no two open rows agreed about where anything was: Base under Class under
   Points, sliding a little further right on each row as the figures got longer.
   Five fixed columns and the total pinned to the end, so opening three rows
   gives you three lines you can read DOWN -- which is the only reason to open
   three rows at once.

   `minmax(0, 1fr)` rather than a fixed width, because the widest term is not
   the same one on every character and a hard number would clip somebody's
   Points on a level 40 build. */
.statmath { display: grid; align-items: baseline;
            /* THE LAST COLUMN IS FIXED, and that is what makes the other five
               line up ACROSS rows. Each open row is its own grid, so `1fr`
               columns only agree if every grid has the same space to divide --
               and an `auto` total column is as wide as its own digits, which
               differ from row to row. 84px fits five figures and a sign. */
            grid-template-columns: repeat(5, minmax(0, 1fr)) 84px;
            gap: 4px 14px;
            padding: 8px 6px 9px 40px; font-size: 12.5px;
            border-bottom: 1px solid rgba(42, 50, 66, .45);
            background: rgba(13, 16, 23, .45); }
.statmath.hidden { display: none; }
.statmath .term { display: inline-flex; align-items: baseline; gap: 6px;
                  min-width: 0; }
.statmath .term.eq { justify-self: end; color: var(--dim); }
.statmath .tl { color: var(--dim); }
.statmath .tv { font-variant-numeric: tabular-nums; }
.statmath .tv.up { color: #5fbf6f; }
.statmath .tv.down { color: #e06a6a; }
.statmath .tv.pts { color: var(--torch); }
.statmath .tv.gear { color: #5fbf6f; }
.statmath .tv.trait { color: #b07fd9; }

/* The four things you do TO a character rather than with one. */
.shadmin { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.shadmin .ghost { font-size: 11px; padding: 5px 9px; }
.shadmin .ghost.armed { color: var(--torch); border-color: var(--torch); }
.shadmin .grow { flex: 1; }
.pointsleft { color: var(--torch); font-size: 12px; margin: 0 0 8px; }

/* ── spells ──────────────────────────────────────────────────────────────────
   A row is an icon and a block: the name with its element, then the numbers,
   then what it asks of you, then the wiki's own formula. It is also a DRAG
   HANDLE for the action bar, which is why the whole row lifts on hover rather
   than only the icon. */
/* ── THE SPELLS YOU KNOW, AS A TRAY ──────────────────────────────────────────
   Twelve five-line rows -- name, damage, cost, cooldown, two requirement chips,
   the wiki's formula and a line about the bar -- was most of a screenful of
   prose about spells you already know how to cast, and it was half the reason
   this panel needed a scrollbar. It is a tray of icons now, and everything the
   rows said is on the hover card, which has more room than the column ever did.

   The icons are 40px because that is where the game's own spell art stops being
   a smudge, and the cells are square so the grid reads as a rack rather than as
   a paragraph of pictures. */
/* ── TWO ROWS, AND THE REST SCROLLS INSIDE ITSELF ────────────────────────────
   The grid is in the sheet's left column now, under the attributes, which means
   its height is added to the tallest thing on the sheet rather than balanced
   against it. A character with twenty spells would have given the whole PANEL a
   scrollbar -- the thing this sheet was laid out twice to avoid.

   Bounded here instead. At the width this column has, two rows is eighteen to
   twenty-six spells before anything scrolls, and what scrolls is a picker
   rather than the page. */
.spellsbox { display: grid; gap: 6px; align-content: start;
             max-height: 102px; overflow-y: auto;
             scrollbar-width: thin; scrollbar-color: var(--line) transparent;
             grid-template-columns: repeat(auto-fill, minmax(48px, 48px)); }
.spellsbox::-webkit-scrollbar { width: 8px; }
.spellsbox::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px; }
/* The "you know none yet" line is a sentence, not a spell, so it takes the
   whole row rather than being squeezed into a 48px cell one word at a time. */
.spellsbox > p { grid-column: 1 / -1; margin: 0; }
.spellcell { position: relative; width: 48px; height: 48px;
             display: grid; place-items: center; cursor: grab;
             background: linear-gradient(#232a36, #151a24);
             border: 1px solid var(--edge); border-radius: 4px;
             box-shadow: inset 0 1px 0 rgba(255,255,255,.07); }
.spellcell:hover { border-color: var(--torch-d); }
/* A spell you have not the Magic for. Not the same statement as "not on the
   bar": this one is permanent until the build changes. */
.spellcell.cant { opacity: .5; cursor: default; }
.spellcell.dragging { opacity: .4; }
/* NOT PLACED, AND THE GRID SAYS SO WITHOUT WORDS. The row used to spend a line
   on "not on the bar -- drag it down"; a dashed edge says it at a glance and
   costs no height. */
.spellcell.unplaced { border-style: dashed; border-color: #3a4457; }
/* WHICH KEY IT IS -- the same torch square the bar draws its own numbers in. */
.spellcell .sslot { position: absolute; right: -3px; top: -3px;
                    min-width: 15px; height: 15px; padding: 0 2px;
                    display: grid; place-items: center;
                    font: 400 9px var(--label); color: var(--ink);
                    background: var(--torch); border-radius: 3px; }

.spellico { image-rendering: pixelated; flex: none; border-radius: 3px; }
/* The stand-in until `npm run sync` has copied the art across. See spellIcon(). */
span.spellico.ph { display: grid; place-items: center; background: var(--ink-2);
                   border: 1px solid var(--line); color: var(--dim);
                   font-size: 13px; font-weight: 700; }

/* ── equipment doll and inventory ────────────────────────────────────────── */
 /* ── THE DOLL IS ONE OBJECT ──────────────────────────────────────────────────
   THE SLOTS ARE A FIXED 58, NOT A FRACTION OF THE WINDOW. `repeat(3, 1fr)`
   stretched the three columns to whatever the panel was, which pushed the hands
   a third of the window apart from the chest -- so the figure read as three
   columns of equipment standing in a row rather than as a person wearing
   things. A fixed cell keeps the hands beside the chest at every window width,
   and the whole doll sits on ONE recessed plate so it is visibly a single
   object with a shape rather than twelve squares that happen to be adjacent. */
.dollwrap { display: flex; justify-content: center; }
.doll { display: grid; grid-template-columns: repeat(3, var(--cell));
        grid-template-rows: repeat(4, var(--cell)); gap: 5px; padding: 6px;
        background: rgba(8, 10, 15, .55); border: 1px solid #202836;
        border-radius: 5px; box-shadow: inset 0 1px 4px rgba(0,0,0,.6); }
.bag  { display: grid; grid-template-columns: repeat(4, var(--cell)); gap: 5px;
        justify-content: center; }

/* ── THE EQUIPMENT BUDGET, AS PIPS ───────────────────────────────────────────
   "Equip size 8 of 10." was a sentence, and a sentence is a thing you read.
   Ten pips is a thing you see: how much room is left is the question you are
   asking WHILE deciding whether to put something on, and the answer wants to
   arrive in the same glance as the doll it is under. The count stays, because
   at ten pips "how many exactly" is still a fair question. */
.eqpips { display: flex; align-items: center; gap: 7px; margin-top: 10px; }
.eqpips .l { font: 400 8px var(--label); letter-spacing: .1em;
             text-transform: uppercase; color: var(--dim); flex: none; }
.eqpips .pips { display: flex; gap: 3px; flex: 1; }
.eqpips .pips i { flex: 1; height: 9px; border-radius: 2px;
                  background: rgba(42, 50, 66, .6); }
.eqpips .pips i.on { background: var(--torch); }
/* Over budget is not a thing the server allows, but a doll that could not say
   so would be a doll that lies at the one moment it matters. */
.eqpips .pips i.over { background: #d4544f; }
.eqpips .n { font-size: 12px; color: var(--text); flex: none;
             font-variant-numeric: tabular-nums; }

/* Sized for the art inside it -- see SLOT_ART in app.mjs, which draws coins and
   items at the same scale so a gold pile and a sword read as the same kind of
   object. */
/* ONE CELL SIZE FOR THE WHOLE GAME, declared once. The doll and the bag are the
   same kind of square and they used to be two different sizes because each was
   a fraction of a different box. */
#game { --cell: 58px; }
.slot {
  position: relative; width: var(--cell); height: var(--cell);
  background: var(--ink-2); border: 1px solid var(--line); border-radius: 4px;
  display: grid; place-items: center; cursor: pointer; overflow: hidden;
}
.slot.empty { cursor: default; }
.slot:not(.empty):hover { border-color: var(--torch); }
.slot img.ph { width: 60%; opacity: .18; }
.slot .qty {
  position: absolute; right: 2px; bottom: 1px; font-size: 11px; font-weight: 700;
  color: var(--text); text-shadow: 0 1px 3px #000, 0 0 3px #000;
  font-variant-numeric: tabular-nums;
}
/* `.slot .tier` is gone with the depot's corner letter (2026-09-04). The `.t-*`
   colours below stay: they are shared with the auction rows, the item card's
   header and the context menu, which spell the rarity out as a WORD. */
.t-common { color: var(--dim); } .t-uncommon { color: #6ec06a; }
.t-rare { color: #4f9fe0; } .t-epic { color: #b07fe8; } .t-legendary { color: var(--torch); }
.slot.gap { visibility: hidden; }
/* An empty bag tile is a HOLE, not a slot with nothing in it: the doll's empty
   slots are places something goes and say so with a silhouette, and the bag's
   are simply the room left. Drawing them the same made a full bag and an empty
   one look like the same picture. */
/* THE DEPOT'S GRIDS DRAW THE SAME HOLE, from this rule rather than from a copy
   of its three values: both panes are "what is in it plus one spare row", and a
   spare row that did not look like the bag's would read as a slot you are
   allowed to fill. */
.bag .slot.empty, .dpgrid .slot.empty {
                   background: rgba(9, 11, 16, .5); border-color: #1d2431;
                   box-shadow: inset 0 1px 3px rgba(0,0,0,.55); }

/* ── RARITY SHINE ────────────────────────────────────────────────────────────
   THE GAME GLOWS THE SPRITE, NOT THE BOX, and that distinction is the whole
   technique. An `outline` or a `box-shadow` draws a rounded RECTANGLE around
   the tile, which is a dyed border by another name and reads as a UI state
   rather than as a special item.

   Stacked `drop-shadow()` follows the element's own ALPHA instead, so the four
   1px shadows trace a hard edge around the item's silhouette and the two
   blurred ones bloom outwards from it. Each filter in the chain runs on the
   previous result, which is what thickens the outline without a fifth and sixth
   copy of it. Lifted from the wiki's atlas.css, tier colours included.

   THE ELEMENT MUST BE TRANSPARENT for this to work -- a background colour or a
   border gives the filter a rectangle to trace and you are back where you
   started. `.sprite.item` is a bare span with a background-IMAGE and nothing
   else, so the filter goes straight on it; the wiki needed a pseudo-element
   only because its own icon keeps a frame.

   COMMON DOES NOT GLOW. In game only uncommon and up are outlined, and a white
   halo on every ordinary item is noise that would make "has rarity tiers" look
   like "is special". */
.rar-glow {
  filter:
    drop-shadow(1px 0 0 var(--rg, #9aa4b6)) drop-shadow(-1px 0 0 var(--rg, #9aa4b6))
    drop-shadow(0 1px 0 var(--rg, #9aa4b6)) drop-shadow(0 -1px 0 var(--rg, #9aa4b6))
    drop-shadow(0 0 5px var(--rg, #9aa4b6)) drop-shadow(0 0 11px var(--rg, #9aa4b6));
  transition: filter .15s;
}
.rarity-glow-uncommon  { --rg: #5fbf6f; }
.rarity-glow-rare      { --rg: #5aa9e6; }
.rarity-glow-epic      { --rg: #b07fd9; }
.rarity-glow-legendary { --rg: #f0a53c; }
/* Needs the doubled class to outrank .rar-glow, which shares its specificity
   and comes first. */
.rar-glow.rarity-glow-common { filter: none; }

/* A legendary is the one tier that MOVES. Four tiers of static halo start to
   read as a colour code rather than as a ladder, and the top of a ladder should
   be visible from across the panel. Slow, and only the outer bloom breathes --
   a pulsing hard edge would shimmer. */
@keyframes rarpulse {
  0%, 100% { filter:
    drop-shadow(1px 0 0 var(--rg)) drop-shadow(-1px 0 0 var(--rg))
    drop-shadow(0 1px 0 var(--rg)) drop-shadow(0 -1px 0 var(--rg))
    drop-shadow(0 0 5px var(--rg)) drop-shadow(0 0 9px var(--rg)); }
  50% { filter:
    drop-shadow(1px 0 0 var(--rg)) drop-shadow(-1px 0 0 var(--rg))
    drop-shadow(0 1px 0 var(--rg)) drop-shadow(0 -1px 0 var(--rg))
    drop-shadow(0 0 7px var(--rg)) drop-shadow(0 0 16px var(--rg)); }
}
.rar-glow.rarity-glow-legendary { animation: rarpulse 2.4s ease-in-out infinite; }
/* Somebody who asked their machine to stop moving things meant this too. */
@media (prefers-reduced-motion: reduce) {
  .rar-glow.rarity-glow-legendary { animation: none; }
}

/* ── sprites ─────────────────────────────────────────────────────────────────
   One rule per sheet. The cell size and column count come from the wiki's own
   index files, so a re-exported sheet changes the JSON and not this; what CSS
   supplies is only the image and the fact that a span with a width has to be
   inline-block to have one. */
.sprite { display: inline-block; background-repeat: no-repeat; vertical-align: middle;
          flex: none; }
.sprite.mob  { width: 96px; height: 96px; background-image: url(/assets/atlas/mob_sprites.png); }
.sprite.item { width: 32px; height: 32px; background-image: url(/assets/atlas/item_atlas.png); }
.sprite.stat { width: 16px; height: 16px; background-image: url(/assets/atlas/stat_icons.png);
               background-size: 256px 256px; }
.sprite.mob.sm { width: 48px; height: 48px; background-size: 768px auto; }
.sprite.ph { flex: none; }

/* ── the town ────────────────────────────────────────────────────────────────
   The background is PRE-BLURRED at build time rather than blurred in CSS. A
   `filter: blur()` on a full-screen 1.3 MB image is a compositing cost paid on
   every frame of every animation for as long as the window is open, and the
   result is identical -- blur destroys detail, so there was never any reason to
   ship the detail. saved_imgs/town.png is the source; the recipe is in the
   patch notes and reruns from there. */
#townbg {
  position: fixed; inset: 0; z-index: 0;
  background: var(--ink) center / cover no-repeat;
  transition: background-image .25s linear;
}
/* ── IT IS THE HUNTING GROUND, AND ONLY THAT ─────────────────────────────────
   This used to be a blurred photograph of the town, permanently, because the
   town was a backdrop for a menu. The town is a PLACE now -- drawn sharp, at
   full size, with plaques on its buildings (see `.townart`) -- so there is
   nothing left for a blurred copy of it to be behind, and this box is empty
   until a hunt starts.

   Out of town it carries the ground, sharp: the town was blurred because it was
   scenery behind panels, and a hunting ground is the thing you are watching. */
#townbg::after {
  content: ""; position: absolute; inset: 0;
  background: radial-gradient(120% 100% at 50% 40%,
              transparent 30%, rgba(9,11,16,.72) 100%);
}

/* ── the overlay ─────────────────────────────────────────────────────────── */
/* ── the overlay ─────────────────────────────────────────────────────────────
   NO SCRIM. There used to be a dark wash over the whole column behind the
   panel, which showed as a second rectangle around the card -- a plate under a
   plate. The card is opaque and carries a deep shadow; the blurred town behind
   it is already a backdrop, which is what it was blurred for. */
/* ── A PANEL WEARS ITS OWN ROOM ──────────────────────────────────────────────
   Every one of these used to open as the same grey card with a 15px title on
   it -- one room with a different list in it each time, which is why the whole
   thing read as a wiki. Each panel carries a photograph of the PLACE it is now:
   the merchant square, the vault's chests, the red-carpet hall. You are in the
   depot because you can see the depot.

   INSET, NOT FULL-BLEED. It leaves the hotbar showing at the bottom, because
   the bar is the bar in every screen of this game and a panel that covered it
   would be a panel you had to close to press 3. */
/* THE INSETS ARE THE OPEN WINDOWS. A panel is the focused thing -- but not at
   the expense of your bag, which is what you opened the merchant list to shop
   for. paintWindows() sets these two; with nothing open they are zero. */
#game { --winL: 0px; --winR: 0px; }

/* ── IT IS A CARD, NOT A WALL ────────────────────────────────────────────────
   The first version stretched edge to edge, which on a wide monitor meant a list
   of five-word rows with a metre of empty card beside each one. Nothing in any
   of these panels wants 1900px: they are lists and forms, and a line you have to
   move your head to read is a line laid out wrong.

   So the card is CAPPED AND CENTRED. The overlay is still the full frame --
   that is what catches the click-outside -- and the card sits in the middle of
   it at a width the content actually uses. */
/* ── AND IT ENDS WHERE THE HOTBAR BEGINS, MEASURED ──────────────────────────
   `bottom: 92px` was a guess at the height of the HUD, written down once. It
   was wrong in both directions: in a fight the cluster is three lines and about
   130px, so a panel ran a good thirty pixels UNDER the hotbar; and in town,
   now that the hotbar has gone (see `hudShown`), it reserved ninety-two pixels
   for nothing -- which is most of the room the character sheet needed for the
   nine slots that moved into it.

   `--hudh` is the real number and has been published on `.middle` all along;
   nothing had ever read it. The 40px floor is the CHAT: its tab sits in the
   bottom-left corner at z-index 12, above any panel, so a card that ran to the
   bottom of the screen would have a chat handle lying across its last row. */
.overlay { position: absolute; z-index: 4; display: flex;
           justify-content: center; align-items: stretch;
           top: 16px; bottom: max(40px, calc(12px + var(--hudh, 92px)));
           left: calc(14px + var(--winL)); right: calc(14px + var(--winR));
           transition: left .14s ease, right .14s ease; }
/* ── AND THE WORLD DIMS BEHIND IT ────────────────────────────────────────────
   An earlier note here said no scrim, on the grounds that the blurred town was
   already a backdrop. The town is not blurred any more -- it is the game, drawn
   sharp -- so an open panel now floats over a fully lit picture with plaques and
   buildings competing with it. The wash puts it back in the background where a
   backdrop belongs, and it is what makes "click anywhere out here to leave"
   look like an offer rather than a dead zone. */
.overlay::before {
  content: ""; position: fixed; inset: 0; z-index: -1;
  background: rgba(6, 8, 12, .62);
  backdrop-filter: blur(2px);
}
.ovcard {
  width: min(1120px, 100%); min-width: 0;
  display: flex; flex-direction: column; overflow: hidden;
  background: linear-gradient(#1d2330, #12171f);
  border: 2px solid var(--edge); border-radius: 4px;
  box-shadow: inset 0 0 0 1px var(--line), 0 20px 50px rgba(0,0,0,.85);
}
/* The trait tree is the one that genuinely wants the room: it is a MAP, and a
   map in a column is a map you pan twice as much. */
.overlay.wide .ovcard { width: 100%; }

/* ── THE CARVED TITLE ────────────────────────────────────────────────────────
   150px of the room, two scrims over it, and the name cut into the bottom-left
   corner. The 3px hard offset under the letters IS the carve -- a soft shadow
   would be text floating over a photograph, and this has to read as a sign
   fixed to the wall. The second scrim is the one that matters structurally: it
   takes the art down into the card's own colour so the two meet without a
   seam. */
/* ── ONE HEIGHT, EVERY PANEL ─────────────────────────────────────────────────
   The three with a photograph were 150px and the three without were 96px, so
   moving between them made the whole list jump up and down the screen. They are
   all 118px now: enough for the carved title and its eyebrow, and the same
   distance from the top of the card whichever room you are in. */
.ovhead {
  position: relative; height: 118px; flex: none;
  border-bottom: 2px solid var(--edge); overflow: hidden;
}
.ovhead .ovart {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: center 60%; image-rendering: pixelated;
}
.ovhead::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(90deg, rgba(9,11,16,.9), rgba(9,11,16,.35) 55%,
                                     rgba(9,11,16,.75)),
              linear-gradient(rgba(9,11,16,0) 40%, rgba(29,35,48,.95));
}
/* NO PHOTOGRAPH, NO PROBLEM. A panel with no picture of its own gets the
   card's own colour and keeps the carved title -- the layout does not depend on
   the art being there. */
.ovhead.bare { background: linear-gradient(#232a36, #12171f); }
/* ── THE SHEET'S PICTURE IS A PERSON, NOT A ROOM ─────────────────────────────
   Every other header is a photograph of somewhere in Basile, `cover`ed across
   the whole band. The class art is a standing figure on a transparent ground,
   so covering it would crop it to a knee: it keeps its own proportions, stands
   at the left where the name is, and sits at half strength so the name is the
   thing you read. */
.ovhead.figure { background: linear-gradient(#232a36, #12171f); }
.ovhead.figure .ovart { width: auto; right: auto; opacity: .5;
                        object-position: center 18%; }
.ovwords { position: absolute; left: 20px; bottom: 14px; z-index: 2; }
.ovhead h2 {
  margin: 0; font: 400 34px var(--display); letter-spacing: .14em;
  color: var(--torch); text-transform: uppercase;
  text-shadow: 0 3px 0 #4a2f0c, 0 4px 18px rgba(0,0,0,.95);
}
.oveyebrow {
  font: 400 8px var(--loud); letter-spacing: .14em; color: var(--brow);
  text-transform: uppercase; text-shadow: 0 1px 5px #000; margin-top: 5px;
}
/* The minidock and the note ride the top-right corner of the art, over the
   scrim, so the picture keeps its whole bottom edge for the title. */
.ovtools { position: absolute; right: 12px; top: 12px; z-index: 2;
           display: flex; align-items: center; gap: 8px; }
/* ── THE DOOR TO THE NEIGHBOUR ───────────────────────────────────────────────
   The sheet and the trait tree are two halves of one decision -- what you have,
   and what to spend next -- and each carries a button to the other, so moving
   between them is not a trip out to the rail and back. Lit on the sheet,
   because "go spend the points" is the offer; plain on the tree, because "go
   look at what it did" is a return. */
.ovextra { display: flex; align-items: center; gap: 6px; }
.ovbtn { height: 26px; padding: 0 9px; cursor: pointer;
         background: rgba(9,11,16,.75); border: 1px solid var(--line);
         border-radius: 2px; color: var(--text);
         font: 400 9px var(--label); letter-spacing: .08em; }
.ovbtn.lit { border-color: var(--torch-d); color: var(--torch); }
.ovbtn:hover:not(:disabled) { border-color: var(--torch); color: var(--torch); }
/* ── AND THE RESET WEARS IT TOO ──────────────────────────────────────────────
   > *"Make the reset attributes button look like the other ones please, that
   > looks very bad right now"*

   It was `button.ghost sm` -- the BODY's button, a 13px sentence in a tall box
   -- left over from when it sat in a row at the bottom of the panel. On the
   header rail between Trait tree and Change outfit it was a foreign object.
   Two states of its own: greyed when you cannot afford it, and lit while it is
   asking whether you meant it. */
.ovbtn:disabled { opacity: .38; cursor: not-allowed; }
.ovbtn.armed { border-color: var(--torch); color: var(--torch);
               background: rgba(255, 196, 106, .12); }
.ovhead .spacer { flex: 1; }
.minidock { display: flex; gap: 4px; }
.minibtn { width: 28px; height: 28px; padding: 0; cursor: pointer;
           display: grid; place-items: center; color: var(--brow);
           background: rgba(9,11,16,.7); border: 1px solid var(--line);
           border-radius: 2px; }
.minibtn svg { width: 17px; height: 17px; stroke: currentColor; fill: none;
               stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.minibtn:hover { color: var(--torch); border-color: var(--line); }
.minibtn.on { color: var(--torch); border-color: var(--torch-d);
              background: var(--panel-2); }
.ovclose { width: 26px; height: 26px; padding: 0; cursor: pointer;
           background: rgba(9,11,16,.75); border: 1px solid var(--line);
           border-radius: 2px; color: var(--text); font-size: 13px; line-height: 1; }
.ovclose:hover { color: var(--torch); border-color: var(--torch-d); }
#ovNote { font: 400 9px var(--label); letter-spacing: .08em; color: var(--brow);
          text-shadow: 0 1px 5px #000; }
/* POSITIONED, because the trait tree fills it with `position: absolute; inset:
   0`. Without this the tree anchors to the next positioned ancestor -- the
   whole middle column -- and lies invisibly across the panel's own header,
   swallowing every click on the nav inside it. */
/* `--ovpad` is DECLARED rather than written twice, because the market's sticky
   search bar has to cancel it exactly -- see .mkbar. */
.ovbody { --ovpad: 14px;
          position: relative; flex: 1; min-height: 0; overflow: auto;
          padding: var(--ovpad) 16px 16px;
          scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
.ovbody::-webkit-scrollbar { width: 8px; }
.ovbody::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px; }
/* The trait tree brings its own scrolling and wants every pixel. */
.ovbody.bare { padding: 0; overflow: hidden; }

/* ── carried weight ──────────────────────────────────────────────────────── */
.loadbar { position: relative; height: 16px; margin-bottom: 8px;
           background: var(--ink-2); border: 1px solid var(--line);
           border-radius: 4px; overflow: hidden; }
.loadbar i { position: absolute; inset: 0 auto 0 0; width: 0;
             background: linear-gradient(180deg, #6b7890, #414d63); }
.loadbar.full i { background: linear-gradient(180deg, #d4544f, #8d2b28); }
.loadbar span { position: absolute; inset: 0; display: grid; place-items: center;
                font-size: 11px; font-variant-numeric: tabular-nums;
                text-shadow: 0 1px 2px rgba(0,0,0,.8); }

.soundbtn { width: 30px; height: 26px; padding: 0; cursor: pointer; font-size: 15px;
            background: transparent; border: 1px solid var(--line);
            border-radius: 5px; color: var(--torch); line-height: 1; }
.soundbtn.off { color: var(--dim); }
.soundbtn.off::after { content: ""; position: absolute; }
.soundbtn:hover { border-color: var(--torch-d); }
.soundbtn.off { text-decoration: line-through; }

/* THE PURSE'S EMBLEM, painted as a background on an empty span. */
.coin { width: 14px; height: 14px; display: inline-block; vertical-align: -2px;
        background: url(/assets/ui/gold.png) center/contain no-repeat; margin-right: 5px; }
/* AND THE PILE, which is a real <img> whose src says which of the twelve coin
   sprites the amount deserves. It must NOT be `.coin`: wearing that class
   painted gold.png behind the img's own picture and drew two overlapping coins
   in one square. */
.coinico { image-rendering: pixelated; flex: none; }

/* ── traits picked ───────────────────────────────────────────────────────────
   The wiki planner's list, kept: branch heading with a count, then one row per
   trait with its icon, a tier numeral and the rank you have over the rank there
   is. `--tc` is the node's own colour, straight off traits.json. */
/* ── WHAT YOU HAVE TAKEN ─────────────────────────────────────────────────────
   Grouped by branch, deepest investment first, each branch in its own colour --
   the same colour the tree draws those nodes in, so the list and the board
   agree about what "Shields" looks like.

   AND EVERY ROW SAYS WHAT IT DOES. The sentence used to be on the hover, which
   made a list of nine traits nine acts of pointing to find out what your own
   character is. */
.traitsbox { display: flex; flex-direction: column; }
.tbranch { display: flex; align-items: center; gap: 8px;
           margin: 14px 0 9px; padding-bottom: 6px;
           font: 400 9px var(--label); letter-spacing: .1em;
           text-transform: uppercase; border-bottom: 1px solid var(--line);
           color: var(--tc, var(--dim)); }
.traitsbox > .tbranch:first-child { margin-top: 0; }
.tbranch .n { color: var(--dim); order: 2; font-variant-numeric: tabular-nums; }
.tbranch::after { content: ""; flex: 1; order: 1; }
/* Named areas rather than a wrapper div: the row's four parts are flat
   children, and the title / tier / rank line and the sentence under it are a
   layout fact rather than a reason to add a box. */
/* ONE LINE, NOT TWO. The sentence under the title went back to the hover, so
   the second grid row went with it -- and the icon dropped from 30px to 24 and
   centred itself, because it was sized to stand beside a paragraph. Nine traits
   used to be nine paragraphs; they are nine lines. */
.trow { display: grid; column-gap: 7px; align-items: center;
        grid-template-columns: 24px minmax(0, 1fr) auto auto;
        grid-template-areas: "ico name tier rank";
        margin-bottom: 5px; padding: 2px 0 2px 9px;
        border-left: 2px solid var(--tc, var(--line)); font-size: 13.5px; }
.trow:hover { background: rgba(240, 165, 60, .05); }
.trow .tico { grid-area: ico; width: 24px; height: 24px;
              image-rendering: pixelated; }
.trow .tname { grid-area: name; color: var(--text); min-width: 0;
               overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.trow .ttier { grid-area: tier; font-size: 10px; color: var(--dim);
               border: 1px solid var(--line); border-radius: 4px; padding: 0 4px;
               font-variant-numeric: tabular-nums; }
.trow .trank { grid-area: rank; font-size: 12px; color: var(--text);
               font-variant-numeric: tabular-nums; }
/* `.teff` is gone with the second row. The wiki's sentence is on the hover, and
   on the tree, which is one button away in this panel's own header. */

/* ── the tab strip ───────────────────────────────────────────────────────── */
.tabs { display: flex; align-items: center; gap: 6px; flex: none; }
.tabs .spacer { flex: 1; }
.tab {
  background: var(--panel); border: 1px solid var(--line); color: var(--dim);
  padding: 8px 16px; border-radius: var(--radius); cursor: pointer;
  font-size: 13px; letter-spacing: .06em; text-transform: uppercase;
}
.tab:hover { color: var(--text); }
.tab.on { color: var(--torch); border-color: var(--torch-d); background: var(--panel-2); }
.tabs.sm { margin-bottom: 16px; }
.purse { color: var(--dim); font-size: 13px; }
.purse b { color: var(--torch); font-size: 16px; font-variant-numeric: tabular-nums; }

#views { flex: 1; min-height: 0; display: flex; }
/* The overlay and the hunt are absolutely placed against THIS box rather than
   against the whole middle column, so the action bar underneath is never
   covered by an open tab. */
.stagearea { position: relative; flex: 1; min-height: 0;
             display: flex; flex-direction: column; }
.view { flex: 1; min-height: 0; overflow: auto; scrollbar-width: thin;
        scrollbar-color: var(--line) transparent; }
.view::-webkit-scrollbar { width: 8px; }
.view::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px; }

.panel { background: var(--panel); border: 1px solid var(--line);
         border-radius: 8px; padding: 16px; margin-bottom: 10px; }

/* ── the market ──────────────────────────────────────────────────────────── */
/* ── the market's search bar ─────────────────────────────────────────────────
   It is STICKY, so it needs an opaque background or the rows scroll through it
   -- but that background was `--ink`, a darker shade than the panel it sits in,
   which drew a black band across the top of the list that looked like a
   rendering fault. It is the panel's own colour now, and it is pulled up into
   the header's padding so the two meet instead of leaving a seam. */
/* ── AND `top: 0` IS NOT THE TOP ─────────────────────────────────────────────
   A sticky box is pinned against its scroll container's CONTENT box, not
   against the edge of the scrollport -- so `top: 0` inside a body with 14px of
   padding pins it fourteen pixels down, and rows scrolled past it in that strip
   in full view. That is the gap between the header and the search bar: not a
   margin, a sticky offset landing in the wrong place.

   The negative margin already pulled the bar up into that padding at rest; the
   offset has to pull it up by the same amount once it sticks, or the bar drops
   back down the moment you scroll. Both read the one declaration, so they
   cannot come apart again. */
.mkbar { display: flex; gap: 8px; align-items: center; flex: none;
         position: sticky; top: calc(-1 * var(--ovpad)); z-index: 2;
         background: var(--panel);
         margin: calc(-1 * var(--ovpad)) -16px 8px; padding: 12px 16px 10px;
         border-bottom: 1px solid var(--line); }
.mkbar input { flex: 1; }
.mkbar select { width: auto; min-width: 150px; }
.mkcount { color: var(--dim); font-size: 12px; white-space: nowrap; }

.mkrow {
  display: grid; grid-template-columns: 40px 1fr 120px 90px 78px;
  align-items: center; gap: 10px; padding: 7px 10px;
  border-bottom: 1px solid var(--line);
}
.mkrow:hover { background: var(--panel); }
.mkrow .nm { min-width: 0; }
/* ── THE NAME IS THE DISPLAY FACE, THE MERCHANT IS PROSE ─────────────────────
   Jersey 25 for the thing itself and for its price, because those are the two
   you scan; the merchant behind it and the category are genuine prose and stay
   on the system stack, where a name like "Bessie · Dorosam" is legible at
   11.5px in a way a display face is not. */
.mkrow .nm b { font: 400 15px var(--display); letter-spacing: .02em;
               color: var(--text); }
.mkrow .sub { color: var(--dim); font-size: 11.5px;
              overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mkrow .cat { color: var(--dim); font-size: 12px; }
.mkrow .price { color: var(--torch); font: 400 17px var(--display);
                font-variant-numeric: tabular-nums; text-align: right; }
.mkrow .price.cant { color: var(--dim); }
.mkempty { color: var(--dim); padding: 26px 10px; text-align: center; }

/* ── grounds ─────────────────────────────────────────────────────────────── */
/* ONE SIZE FOR EVERY CARD.  `grid-auto-rows: 1fr` makes every row the height of
   the tallest card in the grid rather than the tallest in its own row, so the
   wall is flat instead of stepped -- a ground whose pool is "Rat" no longer
   sits a centimetre shorter than the one whose pool is six long names.

   The card is a column and the Hunt button is pushed to the bottom of it, which
   is the half that actually makes them look alike: equal boxes with the buttons
   at different heights read as MORE ragged than unequal boxes, because the
   button is the thing your eye lines up on. */
/* ── THE STAMPED PLATE ───────────────────────────────────────────────────────
   Artboard 1a. The old card led with a name and put the one thing you came to
   find out -- whether the place kills you -- in a small tag near the bottom.
   Thirty of those is thirty cards you read one at a time.

   The verdict is a stamp across the head of the plate now, so a wall of cards
   reads as a COLUMN OF VERDICTS. `--vc` is the verdict's colour, set on the
   card by the painter, and everything on the plate that says "how did it go"
   reads it -- the stamp, the wash behind it, the Health-floor fill and the left
   edge. One value, four places, and they cannot disagree. */
.grounds { display: grid; grid-template-columns: repeat(auto-fill, minmax(238px, 1fr));
           grid-auto-rows: 1fr; gap: 10px; padding-bottom: 10px; }
.ground {
  --vc: #7c8698;
  display: flex; flex-direction: column; overflow: hidden;
  background: linear-gradient(var(--face), var(--ink-2));
  border: 1px solid var(--edge); border-radius: 3px;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.06);
}
/* A lair is a different KIND of place, so it wears the torch on its frame --
   and it keeps it whatever the verdict says, because "this is a boss" is true
   before anything has been measured. */
.ground.boss { border-color: var(--torch-d); }

/* ── THE STAMP ───────────────────────────────────────────────────────────── */
.gstamp { display: flex; align-items: center; gap: 6px; padding: 6px 9px 5px;
          border-bottom: 1px solid var(--edge);
          background: color-mix(in srgb, var(--vc) 11%, transparent); }
.gstamp .w { font: 400 9px var(--loud); letter-spacing: .08em;
             text-transform: uppercase; color: var(--vc); }
.gstamp .grow { flex: 1; }
.gstamp .s { font: 400 8px var(--label); letter-spacing: .06em; color: var(--sub);
             white-space: nowrap; }
/* Nothing measured yet. Deliberately colourless: a card that guessed a verdict
   and then corrected itself would be worse than one that waited. */
.ground.v-unknown .gstamp .w { animation: torchflick 1.6s ease-in-out infinite; }

/* ── THE HEALTH FLOOR, AS A BAR ──────────────────────────────────────────────
   "Lowest Health 4%" is a number you have to picture; a bar four percent full
   IS the picture. Coloured by the verdict rather than by the HUD's red -- a
   full red bar under the word SUSTAINABLE reads as an alarm for a place that
   never touched you. */
.gfloor { display: flex; align-items: center; gap: 7px; padding: 8px 9px 7px; }
.gfloor .t { position: relative; flex: 1; height: 11px; background: var(--well);
             border: 1px solid var(--edge); border-radius: 3px; overflow: hidden;
             box-shadow: inset 0 1px 0 rgba(255,255,255,.09),
                         inset 0 -1px 0 rgba(0,0,0,.8); }
.gfloor .t i { position: absolute; inset: 1px auto 1px 1px; display: block;
               border-radius: 2px; background: var(--vc); }
.gfloor .l { font: 400 8px var(--label); color: var(--dim); white-space: nowrap; }

.gbody { padding: 0 11px 10px; display: flex; flex-direction: column; flex: 1; }
.gname { display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap; }
.gname .n { font: 400 22px var(--display); letter-spacing: .04em;
            color: var(--title); line-height: 1.1; }
.gname .chip { font: 400 7px var(--loud); letter-spacing: .1em; color: var(--torch);
               border: 1px solid var(--torch-d); border-radius: 2px; padding: 3px 4px; }
/* DEEP is a warning, not a badge, so it is the bad colour rather than the
   torch: the ground holds something auto-attack cannot farm. */
.gname .chip.deep { color: var(--bad); border-color: #8d3a37; }
.gqual { font: 400 8px var(--label); letter-spacing: .08em; text-transform: uppercase;
         color: var(--sub); margin: 3px 0 0; }

.gmobs { display: flex; flex-wrap: wrap; gap: 3px; margin: 9px 0 10px; }
.gmob { width: 57px; flex: none; display: flex; flex-direction: column;
        align-items: center; gap: 3px; }
.gmob .mn { font: 400 8px var(--label); line-height: 1.3; color: var(--sub);
            text-align: center; word-break: break-word;
            display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2;
            -webkit-box-orient: vertical; overflow: hidden; }
/* The one in the pool that makes the ground deep. A ring rather than a label:
   the name is already under it, and what this has to say is "that one". */
.gmob.hz .mob { box-shadow: 0 0 0 1px #8d3a37, 0 0 8px rgba(217,112,122,.35);
                border-radius: 3px; }
.gmob.hz .mn { color: var(--bad); }

/* ── WHAT IT PAYS ────────────────────────────────────────────────────────────
   Two tiles rather than a sentence. They are the two numbers a player compares
   between grounds, and a number you compare wants to be in the same place on
   every card -- which a sentence cannot promise and a tile can. */
.grates { margin-top: auto; display: flex; gap: 1px; background: var(--edge);
          border: 1px solid var(--edge); border-radius: 3px; overflow: hidden; }
.grates .gr { flex: 1; min-width: 0; background: var(--well); padding: 6px 8px;
              display: flex; align-items: center; gap: 6px; }
.grates .gr img { image-rendering: pixelated; flex: none; }
.grates .gr b { font-size: 15px; color: var(--text); line-height: 1.15;
                font-variant-numeric: tabular-nums; min-width: 0;
                overflow: hidden; text-overflow: ellipsis; }
.grates .gr .u { font: 400 7px var(--label); letter-spacing: .08em;
                 text-transform: uppercase; color: var(--dim); }

.gcta { margin-top: 9px; width: 100%; padding: 7px 12px; cursor: pointer;
        background: linear-gradient(var(--face), #151a24);
        border: 1px solid var(--well); border-radius: 4px;
        box-shadow: inset 0 1px 0 rgba(255,255,255,.07);
        color: var(--text); font: inherit; font-size: 13px; }
.gcta:hover:not(:disabled) { border-color: var(--torch-d); color: var(--torch); }
.gcta:disabled { opacity: .5; cursor: default; }

/* ── THE SECTION HEADING ─────────────────────────────────────────────────────
   A diamond, a word and a rule that runs out to the edge. The rule used to be
   making room for a four-button sort strip; the cards are in one fixed order
   now (see the note above VIEWS.grounds in app.mjs), so the strip's three rules
   and its label went with it rather than being left to style nothing. Their
   class names are deliberately not written here: check.mjs greps this file for
   them, and a name in a comment keeps that assertion green forever. */
.gsechead { display: flex; align-items: center; gap: 8px; margin: 2px 0 10px; }
.gsechead .d { font-size: 9px; color: var(--torch-d); }
.gsechead .t { font-size: 12px; letter-spacing: .14em; text-transform: uppercase;
               color: var(--torch); font-weight: 600; }
.gsechead .rule { flex: 1; height: 1px;
                  background: linear-gradient(90deg, var(--line), transparent); }

/* The ladder and grounds both live in the middle column, which scrolls, so
   they get their own breathing room rather than a panel box. */
#view-ladder, #view-grounds, #view-market { padding-right: 4px; }
#view-ladder h2.dh:first-child, #view-grounds h2.dh:first-child { margin-top: 2px; }

/* ── the ladder ──────────────────────────────────────────────────────────── */
/* ── THE LADDER ──────────────────────────────────────────────────────────────
   It was a seven-column table, which is the right shape for a database dump and
   the wrong one for the only screen in this game where you look at somebody who
   is not you. Three things carry the redesign: the top three are a podium, a
   row wears its class, and a row is a door. */


/* ── THE PODIUM ──────────────────────────────────────────────────────────────
   Second, first, third -- the way a podium is arranged, so the winner is in the
   middle and taller rather than merely first in reading order. `align-items:
   end` is what makes the heights read as a podium instead of as three cards of
   different sizes. */
/* ── SMALLER THAN IT WAS ─────────────────────────────────────────────────────
   The first version stood three near-full-height cards across the top of the
   screen, and a leaderboard whose top three take the whole window is a
   leaderboard you have to scroll to read. The proportions are unchanged -- the
   winner is still centre and still taller -- everything is simply about a third
   shorter, and the list underneath is visible without scrolling. Capped in
   width too, so a wide window spreads the three apart instead of inflating
   them. */
/* ── SMALLER AGAIN ───────────────────────────────────────────────────────────
   Cut by about a third once already, and it was still taking the top half of
   the window -- so when the ladder opened you saw three portraits and two rows
   of the thing you came for. The proportions are unchanged: the winner is still
   centre and still taller, and the medal, the name, the class and the xp are
   all still on the card. It is simply the size of a header rather than the size
   of the page. */
.podium { display: grid; grid-template-columns: 1fr 1.12fr 1fr; gap: 8px;
          align-items: end; margin: 0 auto 12px; max-width: 430px; }
.pod { display: flex; flex-direction: column; align-items: center; gap: 2px;
       padding: 6px 6px 5px; background: var(--panel-2);
       border: 1px solid var(--line); border-radius: 6px;
       font: inherit; color: inherit; cursor: pointer; text-align: center; }
.pod:hover { border-color: var(--torch); }
.pod.p1 { padding-top: 8px; padding-bottom: 7px; border-color: var(--torch-d);
          background: linear-gradient(rgba(240,165,60,.10), var(--panel-2) 60%); }
.pod.me { box-shadow: inset 0 0 0 1px var(--torch); }
.pod .art { height: 40px; width: auto; image-rendering: pixelated;
            filter: drop-shadow(0 4px 7px rgba(0,0,0,.55)); }
/* ── AND WHEN IT IS THE REAL ONE ─────────────────────────────────────────────
   `figureArt` swaps the flat grey render for a `portrait()` of the same person
   in their own colours. It needs no new size: the still is cropped to the
   figure at 7:8, which is what `client/assets/outfits` is cropped to as well,
   so the two occupy the same box. It is not pixel art, though -- it came out of
   the arena's renderer at twice the size it is shown at, and nearest-neighbour
   on a smooth render is the one way to make it look worse. */
.pod .art.rendered, .pfhead .art.rendered { image-rendering: auto; }
.pod.p1 .art { height: 52px; }
.pod .nm { color: var(--text); font-weight: 700; font-size: 12.5px;
           max-width: 100%; overflow: hidden; text-overflow: ellipsis;
           white-space: nowrap; }
.pod .sub { display: flex; align-items: center; justify-content: center; gap: 4px;
            color: var(--dim); font-size: 10.5px; }
/* SIZED EXPLICITLY. The wiki's class emblems are 48px PNGs, and without this
   they drew at their natural size -- a badge bigger than the name under it. */
.pod .sub .clsico { width: 13px; height: 13px; flex: none;
                    image-rendering: pixelated; }
.pod .xp { color: var(--torch); font-size: 11px;
           font-variant-numeric: tabular-nums; }
/* The medal is the rank, not a decoration beside it. */
.pod .crown { font-size: 10px; font-weight: 700; letter-spacing: .06em;
              padding: 0 7px; border-radius: 999px; border: 1px solid; }
.pod.p1 .crown { font-size: 11px; }
.crown.gold   { color: #f0c05a; border-color: #f0c05a;
                background: rgba(240,192,90,.12); }
.crown.silver { color: #cdd6e4; border-color: #93a0b4;
                background: rgba(205,214,228,.10); }
.crown.bronze { color: #d09468; border-color: #a9714a;
                background: rgba(208,148,104,.12); }

/* ── THE LIST ────────────────────────────────────────────────────────────────
   One grid, so the columns line up down the page without a table -- and the row
   is a button, because it is a door.

   A LIST, NOT A STACK OF CARDS. Every row used to carry its own border, its own
   background and its own rounded corners, inside a bordered panel, inside the
   overlay's card. Three frames deep for a table, and each row cost 4px of gap
   and 2px of border to say nothing -- which is a row and a half of the list
   traded for boxes around the rows of the list. Hairlines now, and the row
   still lights on hover because it is still a door. */
.ldlist { display: flex; flex-direction: column; }
/* ── ONE LINE PER PLAYER ─────────────────────────────────────────────────────
   > *"Condense the ladder rows, they are too big."*

   A ladder is a list you read DOWN -- the whole point of it is where you sit
   among fifty other people -- and at 44px a row you could see eleven of them.
   Nothing was removed: the rank, the emblem, the name and its subtitle, the
   level, the kills and deaths and the xp bar are all still there, drawn at the
   size a list of fifty wants rather than the size a card of one would. */
.ldrow {
  display: grid; align-items: center; gap: 10px;
  grid-template-columns: 30px 18px minmax(90px, 1.4fr) 30px minmax(90px, 1fr) auto;
  width: 100%; padding: 2px 10px; text-align: left;
  background: none; border: 0; border-bottom: 1px solid rgba(42, 50, 66, .45);
  font: inherit; color: inherit; cursor: pointer;
}
.ldrow:hover { background: rgba(240, 165, 60, .06); }
/* No hairline under the last one: a separator with nothing after it is a line
   under the list rather than between two rows. */
.ldrow:last-child { border-bottom: 0; }
/* YOUR OWN ROW IS THE ANSWER TO "WHERE AM I", and it is the whole answer -- the
   banner that used to say it in words above the list is gone. So it has to be
   findable at a glance down a page of fifty: a torch edge and a tint, not a
   slightly different border. */
.ldrow.me { background: rgba(240,165,60,.09);
            box-shadow: inset 3px 0 0 var(--torch); }
.ldrow.me:hover { background: rgba(240,165,60,.14); }
/* The break before a rank that is nowhere near the fifty above it. */
.ldgap { text-align: center; color: var(--muted); letter-spacing: .3em;
         padding: 6px 0 2px; font-size: 11px; }
.ldrow .rk { font-variant-numeric: tabular-nums; font-weight: 700;
             color: var(--dim); text-align: center; font-size: 12px; }
.ldrow .rk.gold, .ldrow .rk.silver, .ldrow .rk.bronze {
  border-radius: 999px; border: 1px solid; padding: 1px 0; font-size: 12px; }
.ldrow .clsico { width: 18px; height: 18px; flex: none; image-rendering: pixelated; }
.ldrow .nm { min-width: 0; }
.ldrow .nm b { color: var(--text); display: block; overflow: hidden;
               text-overflow: ellipsis; white-space: nowrap;
               font-size: 13px; line-height: 1.25; }
/* The subtitle rides UNDER the name at a size that does not add a line's worth
   of height to fifty rows. */
/* MARGIN ZERO, AND THAT IS THE WHOLE FIX. The page-level `.sub` carries
   `margin: 0 0 20px` -- it is the paragraph under a panel heading -- and this
   one borrowed the class for its colour. Twenty pixels of nothing then sat
   under the class name INSIDE a cell the row centres, so 28px of text was
   centred in a 48px box and every ladder row read as top-aligned. The row was
   also 20px taller than it had anything in it for.

   The same margin, borrowed by the same class name, cost the houses panel 115
   rows of the same 20px. See "A borrowed class brings its margins" in
   CLAUDE.md: reset the inherited box wherever the class is borrowed. */
.ldrow .nm .sub { color: var(--muted); font-size: 10px; line-height: 1.2; margin: 0;
                  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The level as a badge rather than a column: it is the number the ladder sorts
   on and it should look like the answer, not like a cell. */
.ldrow .lv { text-align: center; font-weight: 700; font-size: 14px;
             color: var(--torch); font-variant-numeric: tabular-nums; }
.ldrow .kd { display: flex; gap: 5px; font-size: 11px; min-width: 72px;
             justify-content: flex-end; font-variant-numeric: tabular-nums; }
.ldrow .kd .k { color: #6ec06a; }
.ldrow .kd .sep { color: var(--muted); }
.ldrow .kd .d { color: #d9707a; }

/* HOW FAR THROUGH THE LEVEL, which a lifetime total cannot say. Drawn empty
   rather than guessed for rows whose progress the server has not published --
   see xpBar: an invented proportion would be subtly wrong for everybody but
   you, which is worse than an honest blank. */
.ldxp { display: flex; align-items: center; gap: 7px; min-width: 0; }
.ldxp .t { flex: 1; min-width: 40px; height: 6px; border-radius: 3px;
           background: rgba(0,0,0,.45); border: 1px solid var(--line);
           overflow: hidden; }
.ldxp .t i { display: block; height: 100%; width: 0;
             background: linear-gradient(90deg, #3e7c3c, #6ec06a); }
.ldxp .n { color: var(--dim); font-size: 10px;
           font-variant-numeric: tabular-nums; }
.ldxp.wide { width: 100%; margin-top: 6px; }

/* ── SOMEBODY ELSE'S CHARACTER ───────────────────────────────────────────────
   Borrowing the item menu's box, because it already knows how to sit above
   everything and dismiss on a click outside. Wider, and centred rather than
   dropped at the pointer: it is a page about a person, not a menu of verbs. */
.itemmenu.profile { width: min(520px, 92vw); max-width: none; padding: 14px;
                    max-height: 86vh; overflow-y: auto; }
.pfhead { position: relative; display: flex; gap: 14px; align-items: center; }
/* The X sits in the card's corner rather than in the header's flow, so it is in
   the same place here as it is on every overlay -- and so a long character name
   cannot push it off the end of the row. */
.pfhead .ovclose { position: absolute; right: -4px; top: -4px; }
.pfhead .art { height: 104px; width: auto; flex: none; image-rendering: pixelated;
               filter: drop-shadow(0 6px 10px rgba(0,0,0,.55)); }
/* NOT `.who` -- the character panel owns that name and it is a flex row. */
.pfhead .pfwho { flex: 1; min-width: 0; }
.pfhead .nm { color: var(--text); font-size: 20px; font-weight: 700; }
.pfhead .sub { display: flex; align-items: center; gap: 6px;
               color: var(--dim); font-size: 13px; margin-top: 2px; }
.pfhead .sub .clsico { width: 18px; height: 18px; image-rendering: pixelated; }
.pfhead .rank { color: var(--torch); font-size: 12px; margin-top: 4px; }

.pffacts { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px;
           margin: 14px 0; }
.pffacts .f { background: var(--ink-2); border: 1px solid var(--line);
              border-radius: 6px; padding: 7px 8px; text-align: center; }
.pffacts .f b { display: block; color: var(--text); font-size: 15px;
                font-variant-numeric: tabular-nums; }
/* THE VERDICT CARD. Three of these four are history and one is what the build
   is worth, so one of them is torch-coloured -- the same colour the swing line
   under the stats wears, because they are the same claim at two levels of
   detail. A row of four identical tiles makes you read all four to find the one
   you came for. */
.pffacts .f.hot b { color: var(--torch); }
.pffacts .f.hot { border-color: var(--torch-d); background: linear-gradient(#f0a53c14, var(--ink-2)); }
.pffacts .f span { color: var(--muted); font-size: 10px;
                   text-transform: uppercase; letter-spacing: .06em; }

.pfcols { display: grid; grid-template-columns: auto 1fr; gap: 16px;
          align-items: start; }
@media (max-width: 560px) { .pfcols { grid-template-columns: 1fr; } }
/* The same three-wide doll the character panel draws, at a size that fits
   beside a stat sheet. */
/* Its own cell size rather than a max-width on the slot: the slot is a square
   sized from `--cell` now, and capping only its width would have left a 46-wide
   58-tall rectangle. */
.pfdoll { --cell: 46px; display: grid; grid-template-columns: repeat(3, var(--cell));
          gap: 4px; }
/* ── A STATE WORD IS NOT A COMPONENT NAME, AND NEITHER IS A GOOD ONE ─────────
   `.statrow` is the sheet's own five-column composition row now, and the ladder
   profile was borrowing the name for a plain "icon, label, number" line -- which
   would have laid a stranger's Health out across a grid built for bars, a plus
   and a caret. Fourth time this exact collision has cost an afternoon, so:
   the profile's row is `.pfstat`. */
.pfsheet .pfstat { display: flex; align-items: center; gap: 8px;
                    padding: 3px 0; font-size: 13px; }
.pfsheet .pfstat .lbl { flex: 1; color: var(--dim); }
.pfsheet .pfstat .val { color: var(--text); font-weight: 600;
                         font-variant-numeric: tabular-nums; }
.pfsheet .derived { color: var(--torch); font-size: 12px; margin: 8px 0 0; }

/* `.pfhead2`, `.pfbranches`, `.bchip`, `.pfspells` and `.schip` are gone with
   the trait tally and the spell list they styled. What somebody ahead of you is
   WEARING is gear you could go and get and a number you can compare; a branch
   count and a list of spell names are the parts of a build you cannot copy by
   shopping, and they pushed the doll and the stats out of the first screenful
   to say so. */

/* ── the fight fills the middle column ───────────────────────────────────── */
.hunt { flex: 1; min-height: 0; display: flex; }
/* The fight is drawn on the page's own scene, so it takes the whole column. */
.hunt > .pl-sim { border-radius: 0; }
.hunt > * { flex: 1; min-height: 0; }

/* ── toast and tooltip ───────────────────────────────────────────────────── */
/* ── ABOVE THE HUD, AND THE HUD SAYS HOW TALL IT IS ─────────────────────────
   > *"The toast message after something was saved, should be above the bars and
   > buttons, so it doesn't block the vision."*

   84px was the answer on the day it was written and stopped being one when the
   strip grew a stance row and a kite field. `--hudh` is the real height, it is
   published by placeChat() on every layout, and this is the second time in this
   project that a written-down number has stood next to a measurement nobody was
   reading -- see "A published measurement nobody reads is a constant with extra
   steps". The fallback is only for the frame before the first measurement.

   AT ZERO IT IS 22px OFF THE FLOOR, which is the town: no HUD, nothing to clear,
   and a toast that floated a hundred pixels up for no reason would be the same
   bug pointing the other way. */
/* ABOVE THE ACTION BAR. At 26px the toast sat exactly on top of slots 4 to 6,
   so "You learn Fireball" covered the bar the spell had just been placed on. */
.toast {
  position: fixed; left: 50%; bottom: calc(var(--hudh, 92px) + 22px);
  transform: translateX(-50%) translateY(20px);
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  padding: 10px 18px; border-radius: 999px; opacity: 0; pointer-events: none;
  transition: opacity .18s, transform .18s; z-index: 50; font-size: 14px;
}
.toast.on { opacity: 1; transform: translateX(-50%) translateY(0); }
.toast.bad { border-color: var(--bad); color: var(--bad); }

/* Item tooltip. Follows the pointer, so it is positioned from script. */
.tip {
  /* 260 for a sentence; a drop table asks for a little more by class rather
     than by widening every tooltip in the game. */
  /* ── AND ALWAYS ON TOP, BECAUSE IT IS NEVER THE SUBJECT ──────────────────
     > *"when hovering spells, the tooltip is behind the window, so i can't read
     > it."*

     It was 60, which is also `.itemmenu`'s -- and the slot editor IS an
     `.itemmenu`, so two fixed boxes at the same z-index were left to DOM order
     and the menu comes later. The tooltip for a spell in the picker painted
     underneath the picker.

     90 is above the highest thing on the page (`.outfitwin`, 80) on purpose
     rather than 61: a tooltip takes no mouse, dies when the pointer moves and
     is by definition an explanation of something else, so there is no state in
     which something should cover it. Picking "one more than the box that
     happened to cover it" is how this comes back the next time a box is
     added. */
  position: fixed; z-index: 90; pointer-events: none; max-width: 260px;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px;
  padding: 8px 10px; font-size: 13px; box-shadow: 0 8px 24px rgba(0,0,0,.6);
}
/* ── ONE COLUMN, A LITTLE WIDER ──────────────────────────────────────────────
   It used to be 430px, because past ten rows the table split into two columns
   and each of them needed room for an item name. One column needs the width
   once rather than twice, and 300 is what stops "Small Health Potion ×3" from
   ellipsising -- which is the only thing the extra pixels are buying. */
.tip:has(.beastloot) { max-width: 300px; }
.tip b { color: var(--torch); }
.tip .l { color: var(--dim); font-size: 12px; }

/* ── the depot ───────────────────────────────────────────────────────────────
   THE GAME'S OWN TILES, TWICE. This was two lists of text rows; it is the same
   58px grid the bag draws, on both sides, because a stack is the same object
   wherever it is standing and it should not change shape when it crosses the
   panel. Every tile rule here is the bag's -- `.slot`, `.slot .qty`,
   `.slot .tier`, `.rar-glow` -- and what is added is only the STATES this panel
   has that the bag does not: hovered, armed, ticked.

   THE STATES ARE DRAWN FROM STATE, NOT FROM `:hover`. A tile lights for three
   different reasons here and only one of them is the pointer, so the panel puts
   the classes on and the stylesheet does as it is told. Which means the bag's
   own `.slot:not(.empty):hover` has to be answered rather than inherited -- it
   would otherwise outrank `.armed` and turn the armed tile's border back to
   plain torch the moment the pointer settled on it. That is what the
   `:not(.empty)` on each of these is buying: the same specificity, and the
   order below decides. */
.dpbody { padding: 11px 16px 12px; display: flex; flex-direction: column; gap: 9px;
          /* The panes are the height of the body and their grids scroll on
             their own, so the body does not -- and it must NOT clip, because
             the stepper on a second-row tile opens upwards and is allowed to
             stand over the carved header. The card's own overflow bounds it. */
          overflow: visible; }
.dpbar { flex: none; display: flex; align-items: center; gap: 8px; }
.dpsearch { position: relative; width: 224px; flex: none; }
.dpsearch svg { position: absolute; left: 9px; top: 9px; color: var(--faint);
                pointer-events: none; }
.dpsearch input { width: 100%; background: var(--well); border: 1px solid var(--line);
                  border-radius: 5px; color: var(--text); padding: 5px 9px 5px 27px;
                  font-size: 13px; }
.dpsearch input:focus { outline: 0; border-color: var(--torch-d); }
/* `width: auto` CANCELS THE GLOBAL. `input, select { width: 100% }` is right for
   a form and wrong in a toolbar: without this the two dropdowns eat the row and
   the two chips are squeezed into three lines of eight-pixel capitals. The
   market bar carries the same cancel for the same reason. */
.dpsel { width: auto; background: var(--well); border: 1px solid var(--line);
         border-radius: 5px; color: var(--text); padding: 5px 7px; font-size: 12.5px; }
.dpgap { flex: 1; }
.dpchip { height: 28px; padding: 0 10px; cursor: pointer; border-radius: 4px;
          white-space: nowrap;
          font: 400 8px var(--label); letter-spacing: .08em; text-transform: uppercase;
          background: var(--panel-2); border: 1px solid var(--line); color: var(--brow); }
.dpchip:hover { border-color: var(--torch-d); color: var(--torch); }
.dpchip.on { background: rgba(240, 165, 60, .14); border-color: var(--torch-d);
             color: var(--torch); }

.dpvaults { flex: 1; min-height: 0; display: grid; grid-template-columns: 1fr 1fr;
            gap: 14px; }
.dppane { display: flex; flex-direction: column; min-width: 0;
          background: var(--panel); border: 1px solid var(--line);
          border-radius: 8px; padding: 10px; }
/* THE RECEIVING PANE BORDERS IN TORCH while a stack from the other side is in
   the air. The doll's dashed green outline is the wrong mark here: there are
   exactly two targets on the screen and the question is never "which one takes
   it", so a dashed box around half the panel reads as an error rather than as
   an offer. `.drop-no` is the same-side pane refusing its own stack back, and
   it stays quiet -- nothing happened, and nothing went wrong. */
.dppane.drop-can { outline: none; animation: none; border-color: var(--torch-d); }
.dppane.drop-ok, .dppane.drop-no { outline: none; }
.dppane.drop-no { border-color: var(--line); }

.dphead { flex: none; display: flex; align-items: baseline; gap: 8px;
          padding-bottom: 7px; margin-bottom: 9px; border-bottom: 1px solid var(--line); }
.dphead .t { font: 400 9px var(--label); letter-spacing: .1em; text-transform: uppercase;
             color: var(--sub); }
.dphead .s { min-width: 0; overflow: hidden; text-overflow: ellipsis;
             white-space: nowrap; font-size: 11.5px; color: var(--faint); }
.dphead .n { margin-left: auto; flex: none; font: 400 9px var(--label);
             letter-spacing: .08em; color: var(--torch); }

.dpgrid { display: grid; grid-template-columns: repeat(8, var(--cell)); gap: 5px;
          align-content: start; min-height: 0; overflow-y: auto;
          scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
.dpgrid::-webkit-scrollbar { width: 8px; }
.dpgrid::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px; }
.dpgrid .slot:not(.empty) { cursor: grab; }
.dpgrid .slot:not(.empty):hover { border-color: var(--line); }
.dpgrid .slot.hot:not(.empty) { border-color: var(--torch-d); }
.dpgrid .slot.picked:not(.empty) { border-color: var(--torch);
                                   box-shadow: 0 0 0 2px rgba(240, 165, 60, .22); }
.dpgrid .slot.armed:not(.empty) { border-color: var(--torch);
                                  box-shadow: 0 0 0 2px rgba(240, 165, 60, .35); }
.dpcheck { position: absolute; right: 1px; top: 1px; width: 13px; height: 13px;
           display: grid; place-items: center; background: var(--torch);
           color: #1a1205; border-radius: 2px; font-size: 9px; font-weight: 700; }
/* `[hidden] { display: none }` lives in the UA sheet and every author rule that
   sets `display` beats it -- so a class that sets one has to carry its own. */
.dpcheck[hidden] { display: none; }
/* The spare row is breathing ROOM, not slots you may fill: the limit here is a
   weight and the pane head says it in ounces. */
.dpfill { flex: 1; }

.dpinsp { flex: none; display: flex; align-items: center; gap: 10px; margin-top: 9px;
          padding-top: 9px; border-top: 1px solid var(--line); min-height: 52px; }
.dpinsp .sprite { flex: none; }
.dpinsp .w { min-width: 0; flex: 1; }
.dpinsp .w .n { display: block; font: 400 17px var(--display); letter-spacing: .03em;
                color: var(--title); overflow: hidden; text-overflow: ellipsis;
                white-space: nowrap; }
.dpinsp .w .s { display: block; font-size: 11.5px; color: var(--dim);
                overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dpinsp .v { flex: none; text-align: right; }
.dpinsp .oz { display: block; font: 400 16px var(--display); color: var(--text);
              font-variant-numeric: tabular-nums; }
.dpinsp .oz.bad { color: var(--bad); }
.dpinsp .g { display: block; font-size: 11.5px; color: var(--torch);
             font-variant-numeric: tabular-nums; }
.dpinsp .hint { font-size: 12px; color: var(--faint); }

.dpfoot { flex: none; display: flex; align-items: center; gap: 10px; min-height: 32px; }
.dpflash { flex: 1; min-width: 0; font-size: 12px; color: var(--good); }
.dpflash.bad { color: var(--bad); }
/* NO `display` ON THESE THREE, deliberately: Undo and the batch button are
   shown by clearing `hidden`, and an author `display` would beat the UA rule
   that hides them. */
/* NOWRAP ON ALL THREE. They are fixed-height buttons with a sentence in them --
   "Store all loot", "Move 3 stacks", "Took 10 Health Potion" -- and a fixed
   height with wrapping text is a button with its own words falling out of it. */
.dpundo { flex: none; height: 28px; padding: 0 11px; cursor: pointer; white-space: nowrap;
          background: transparent; border: 1px solid var(--line); border-radius: 4px;
          color: var(--sub); font-size: 12px; }
.dpundo:hover { border-color: var(--torch-d); color: var(--torch); }
.dpgo { flex: none; height: 28px; padding: 0 13px; cursor: pointer; white-space: nowrap;
        background: var(--torch); border: 0; border-radius: 4px; color: #1a1205;
        font-weight: 700; font-size: 12.5px; }
.dpgo:hover { filter: brightness(1.1); }
.dpghost { flex: none; height: 28px; padding: 0 9px; cursor: pointer; white-space: nowrap;
           background: transparent; border: 1px solid var(--line); border-radius: 4px;
           color: var(--sub); font-size: 12px; }
.dpghost:hover { color: var(--text); }

/* ── THE STEPPER, AND THE SAME QUESTION ON A DROP ───────────────────────────
   The popover is placed by script against the tile it belongs to and carries no
   animation, which is not a style choice: a running animation whose keyframes
   mention `transform` overrides an inline one for as long as it runs, and this
   panel positions with `left`/`top` for the same reason. */
.dpstep { position: absolute; z-index: 30; width: 196px; background: var(--panel-2);
          border: 1px solid var(--torch-d); border-radius: 6px; padding: 9px;
          box-shadow: 0 12px 30px rgba(0, 0, 0, .75); }
.dpstep .l { font: 400 8px var(--label); letter-spacing: .1em; text-transform: uppercase;
             color: var(--sub); margin-bottom: 7px; }
.dpstep .r { display: flex; gap: 5px; align-items: center; margin-bottom: 6px; }
.dpstep .r .s { width: 26px; height: 26px; flex: none; cursor: pointer;
                background: var(--ink-2); border: 1px solid var(--line);
                border-radius: 4px; color: var(--text); font-size: 15px; line-height: 1; }
.dpstep .r .s:hover { border-color: var(--torch); color: var(--torch); }
.dpstep .v { flex: 1; min-width: 0; text-align: center; background: var(--well);
             border: 1px solid var(--line); border-radius: 4px; color: var(--torch);
             padding: 3px 4px; font: 400 16px var(--display);
             font-variant-numeric: tabular-nums; }
.dpstep .v:focus { outline: 0; border-color: var(--torch-d); }
.dpstep .q { display: flex; gap: 4px; margin-bottom: 8px; }
.dpstep .q button { flex: 1; height: 23px; cursor: pointer; background: var(--ink-2);
                    border: 1px solid var(--line); border-radius: 4px; color: var(--sub);
                    font: 400 8px var(--label); letter-spacing: .06em;
                    text-transform: uppercase; }
.dpstep .q button:hover { border-color: var(--torch-d); color: var(--torch); }
.dpstep .w { font-size: 11px; color: var(--sub); margin-bottom: 8px; }
.dpstep .w.bad { color: var(--bad); }
.dpstep .a { display: flex; gap: 5px; }
.dpstep .a .dpgo { flex: 1; padding: 0; letter-spacing: .03em; }

.dpscrim { position: absolute; inset: 0; z-index: 40; display: grid; place-items: center;
           background: rgba(6, 8, 12, .6); }
.dpask { width: 264px; background: var(--panel-2); border: 1px solid var(--torch-d);
         border-radius: 6px; padding: 13px; box-shadow: 0 18px 44px rgba(0, 0, 0, .8); }
.dpask .h { display: flex; align-items: center; gap: 9px; margin-bottom: 11px; }
.dpask .h .sprite { flex: none; }
.dpask .h .t { min-width: 0; }
.dpask .h .n { display: block; font: 400 18px var(--display); letter-spacing: .03em;
               color: var(--title); }
.dpask .h .l { display: block; font: 400 8px var(--label); letter-spacing: .08em;
               text-transform: uppercase; color: var(--sub); }
.dpask .r { display: flex; gap: 6px; align-items: center; margin-bottom: 7px; }
.dpask .r .s { width: 28px; height: 28px; flex: none; cursor: pointer;
               background: var(--ink-2); border: 1px solid var(--line); border-radius: 4px;
               color: var(--text); font-size: 16px; line-height: 1; }
.dpask .r .s:hover { border-color: var(--torch); color: var(--torch); }
.dpask .v { flex: 1; min-width: 0; text-align: center; background: var(--well);
            border: 1px solid var(--line); border-radius: 4px; color: var(--torch);
            padding: 3px 4px; font: 400 19px var(--display);
            font-variant-numeric: tabular-nums; }
.dpask .v:focus { outline: 0; border-color: var(--torch-d); }
/* The slider is the drop's own control and the stepper has none: a drop is how
   a stack of eight hundred arrows is moved, and stepping to four hundred is not
   a gesture. */
.dpslide { width: 100%; accent-color: var(--torch); margin: 0 0 9px; }
.dpask .w { font-size: 11.5px; color: var(--sub); margin-bottom: 10px; }
.dpask .w.bad { color: var(--bad); }
.dpask .a { display: flex; gap: 6px; }
.dpask .a .dpgo { flex: 1; height: 30px; padding: 0; font-size: 13px; }
.dpask .a .dpghost { height: 30px; padding: 0 11px; font-size: 12.5px; }

/* ── houses ──────────────────────────────────────────────────────────────────
   TWO ROOMS IN ONE PANEL. "What is for sale" is a list of 115 and "the one I
   live in" is a floor plan; they are different shapes and a panel that showed
   both at once would be a list with a map squeezed beside it. Tabs, and which
   opens first is decided by whether you own anything.

   The furniture, the tiles and the amount popover all wear the DEPOT'S clothes
   -- `.slot`, `.dpstep`, `.dpgo` -- because they are the same objects and the
   same question. A second visual language for putting a chair down would be a
   second thing to learn for no reason. */
.hsbody { padding: 11px 16px 12px; display: flex; flex-direction: column; gap: 9px;
          overflow: visible; }
.hstabs { flex: none; display: flex; gap: 6px; }
.hstab { height: 28px; padding: 0 14px; cursor: pointer; border-radius: 4px;
         white-space: nowrap; font: 400 9px var(--label); letter-spacing: .1em;
         text-transform: uppercase; background: var(--panel-2);
         border: 1px solid var(--line); color: var(--brow); }
.hstab:hover:not(:disabled) { border-color: var(--torch-d); color: var(--torch); }
.hstab.on { background: rgba(240, 165, 60, .14); border-color: var(--torch-d);
            color: var(--torch); }
.hstab:disabled { color: var(--faint); cursor: default; }
/* THE BODY DOES NOT SCROLL, THE LIST INSIDE IT DOES. Both room screens are a
   fixed row that has to fill the body exactly: a body that scrolled would put
   the bottom of the floor lift below the fold, and the lift is pinned to the
   middle of a box whose height it would then be arguing with. */
.hsmain { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 9px;
          overflow: hidden; }
/* ── THE FOOT SAYS TWO THINGS AND THEY DO NOT COLLIDE ──────────────────────
   The flash is the server's last sentence and is empty most of the time; the
   hint is a standing instruction and is never empty. They are the same row
   because they are the same kind of small print, and the spacer between them
   is what keeps a long refusal from pushing the hint off the panel.

   THE HINT USED TO BE A PARAGRAPH IN THE RAIL, four lines deep at the bottom
   of a 128px column, and it was the single largest thing on that side of the
   panel. One line, in a row that was already there and already empty. */
.hsfoot { flex: none; min-height: 20px; display: flex; align-items: center;
          gap: 10px; }
.hsflash { font-size: 12px; color: var(--good); }
.hsflash.bad { color: var(--bad); }
.hsgrow { flex: 1; }

/* ── THE TAB ROW IS THE HEAD BAND NOW ──────────────────────────────────────
   The `.hshead` panel is gone. It was 70px of card carrying a name, a rent, two
   buttons and a checkbox whose 33-character label wrapped to three lines -- and
   every one of those pixels came off the room, which is the only thing on this
   screen that is short of height. What was in it is beside the tabs (the name)
   or down the side (everything else), and the tab row costs 28px.

   The hairline only earns its place when there is something after it, so it is
   drawn by the panel and hidden when the head is empty. */
.hstop { flex: none; display: flex; align-items: center; gap: 12px; }
.hstopsep { width: 1px; height: 20px; flex: none; background: var(--line); }
.hstopsep.hidden { display: none; }
.hstopx { flex: 1; min-width: 0; display: flex; align-items: center; gap: 12px; }
.hstopx.empty { display: none; }
/* The name of the house you are standing in, on the tab row rather than in a
   card of its own. Baseline-aligned, because a 21px display face and an 11.5px
   sub line centred on each other read as two rows. */
.hsname { flex: 1; min-width: 0; display: flex; align-items: baseline; gap: 9px; }
.hsname b { font: 400 21px var(--display); letter-spacing: .03em;
            color: var(--title); font-weight: 400; }
.hsname .sub { display: flex; align-items: center; gap: 5px; min-width: 0;
               margin: 0; font-size: 11.5px; color: var(--dim);
               overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hsname .sub svg { flex: none; color: var(--faint); }

.hsbar { flex: none; display: flex; align-items: center; gap: 8px; }
/* The filter chip carries this panel's own glyph, so it needs the gap every
   other icon-and-word control in the redesign has. `.dpchip` is a word on its
   own everywhere else and is left alone. */
.hstopx .dpchip { display: flex; align-items: center; gap: 6px; }
/* A BOX WITH A DRAWN GLYPH IN IT, the auction rail's own arrangement -- the
   sprite sheets have no magnifier, so it is inline SVG on `currentColor`. */
.hsfind { position: relative; width: 238px; flex: none; }
.hsfind svg { position: absolute; left: 9px; top: 50%;
              transform: translateY(-50%); color: var(--faint);
              pointer-events: none; }
.hsfind input { width: 100%; background: var(--well);
                border: 1px solid var(--line); border-radius: 5px;
                color: var(--text); padding: 5px 9px 5px 28px; font-size: 13px; }
.hsfind input:focus { outline: 0; border-color: var(--torch-d); }
.hsrule { font-size: 11.5px; color: var(--faint); }

/* THE LIST IS WHAT SCROLLS, not the panel. `.hsmain` used to scroll as one
   page, which was right when the body was a stack; the room screens are now a
   fixed row that must fill the body exactly, and a body that scrolls would take
   the floor lift off the bottom of the room. */
.hslist { flex: 1; min-height: 0; display: grid; gap: 3px;
          align-content: start; overflow: auto;
          scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
.hslist::-webkit-scrollbar { width: 8px; }
.hslist::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px; }
/* TIGHT, BECAUSE THERE ARE 115 OF THEM. At the depot's row height six houses
   fill the panel and finding out what exists is a minute of scrolling; at this
   one it is a page and a half. The name and the price carry the row and the
   rest is small print. */
.hsrow { display: grid; grid-template-columns: 1fr auto 150px; align-items: center;
         gap: 12px; padding: 5px 10px; border-radius: 5px;
         background: var(--panel); border: 1px solid transparent; }
/* THE ROW IS A DOOR AND ITS HOVER SHOULD SAY SO. It used to go to `--line`,
   which is quieter than a tab's hover -- the one thing on this screen you are
   meant to click was the one thing that barely answered the pointer. */
.hsrow:hover { border-color: var(--torch-d); }
/* A house somebody lives in is not for sale and should read that way at a
   glance -- dimmed rather than hidden, because "who lives there" is worth
   knowing and a list with holes in it is a list you cannot count. */
.hsrow.taken { opacity: .55; }
.hsrow.mine { border-color: var(--torch-d); background: rgba(240, 165, 60, .07); }
/* THE ROW IS THE DOOR -- "I Should be able to click the house to inspect it
   before buying" -- so a row you can walk into says so with the pointer, and a
   row whose owner shut the door does not pretend otherwise. */
.hsrow.peek { cursor: pointer; }
.hsrow.shut { cursor: default; }
.hsrow.shut:hover { border-color: transparent; }
/* The viewing tab only exists while a viewing does; `hidden` is set on it. */
.hsvisittab[hidden] { display: none; }
.hsrow .nm { min-width: 0; }
.hsrow .t { display: flex; align-items: baseline; gap: 7px; }
.hsrow .t b { color: var(--title); font-weight: 500; font-size: 14px; }
/* `margin: 0` IS NOT DECORATION HERE. The page-level `.sub` carries
   `margin: 0 0 20px` -- it is a subtitle under a heading -- and this row has
   worn that class since it was written, so every one of the 115 rows has been
   carrying twenty pixels of nothing under its second line. At the row height
   that buys, the list was a third longer than it had any reason to be. */
.hsrow .sub { display: flex; align-items: center; gap: 5px; margin: 0;
              font-size: 11.5px; color: var(--dim); }
.hsrow .sub svg { flex: none; color: var(--faint); }
.hstag { font: 400 8px var(--label); letter-spacing: .08em; text-transform: uppercase;
         color: var(--torch-d); border: 1px solid var(--torch-d); border-radius: 3px;
         padding: 1px 4px; }
/* ── THE FRONT DOOR, AS A CHIP ─────────────────────────────────────────────
   > *"At my house, I should have 'Let other people see the inside' checkbox."*

   Same server bit, same tooltip, same disabled rules -- a different shape. As a
   native checkbox with that sentence beside it, the label wrapped to THREE
   LINES in the head row and pushed the whole band to about 70px; the room paid
   for it. A chip says the state it is IN rather than the state it would set,
   which is what an eye with a line through it is for, and it fits on one line
   in a 212px column. */
.hsdoor { display: flex; align-items: center; justify-content: center; gap: 7px;
          width: 100%; height: 28px; padding: 0 10px; cursor: pointer;
          border-radius: 4px; font-size: 11.5px; white-space: nowrap;
          background: var(--ink-2); border: 1px solid var(--line);
          color: var(--dim); }
.hsdoor svg { flex: none; }
.hsdoor.on { background: rgba(240, 165, 60, .10); border-color: var(--torch-d);
             color: var(--torch); }
.hsdoor:hover:not(:disabled) { border-color: var(--torch-d); color: var(--torch); }
.hsdoor:disabled { cursor: default; opacity: .55; }

.hsrow .pr { text-align: right; }
.hsrow .pr .v { display: flex; align-items: center; justify-content: flex-end;
                gap: 5px; font: 400 15px var(--display); color: var(--torch);
                font-variant-numeric: tabular-nums; line-height: 1.1; }
.hsrow .pr .v .coin { width: 12px; height: 12px; margin: 0; }
.hsrow .pr .rent { display: block; font-size: 11px; color: var(--faint);
                   font-variant-numeric: tabular-nums; }
.hsrow .go { display: flex; justify-content: flex-end; }
.hsmine { font: 400 9px var(--label); letter-spacing: .08em; text-transform: uppercase;
          color: var(--torch); }
.hstaken { font-size: 11.5px; color: var(--dim); overflow: hidden;
           text-overflow: ellipsis; white-space: nowrap; }

/* ── the house you live in ───────────────────────────────────────────────
   THE FACTS GO IN A COLUMN AND THE HOUSE GETS THE REST. Four things used to be
   crammed into one head row -- the rent, `Pay the rent`, `Give it up` and the
   door -- and the two buttons that do OPPOSITE things sat 5px apart. In a
   column they are 330px apart with a rule between them, and the head band that
   held them all is 28px of tab row instead of 70px of card.

   On the LEFT, not the right (which is where the exploration put it): the
   floor lift rides on the room, and a column on the same side as the lift
   reads as one ragged stack of controls again -- which is the thing being
   fixed. */
.hscol { flex: 0 0 212px; display: flex; flex-direction: column; gap: 10px;
         min-height: 0; }
.hscard { display: flex; flex-direction: column; gap: 8px; padding: 10px;
          background: var(--panel); border: 1px solid var(--line);
          border-radius: 8px; }
.hscolend { padding-top: 9px; border-top: 1px solid var(--line); }
.hsgroup { display: flex; flex-direction: column; gap: 4px; }
.hsgl { font: 400 8px var(--label); letter-spacing: .1em;
        text-transform: uppercase; color: var(--sub); }
/* Full width, because everything in the column is. `.dpgo`/`.dpghost` are
   `flex: none` and 28px, which is what they should be in a ROW. */
.hswide { display: flex; align-items: center; justify-content: center; gap: 7px;
          width: 100%; }
.hscard .hswide.dpgo { height: 30px; }
/* GIVING IT UP IS NOT `PAY THE RENT`'S NEIGHBOUR any more, and it wears the
   reds on hover so that the thing it does is legible before it is pressed. */
.hsdrop { color: var(--faint); }
.hsdrop:hover:not(:disabled) { border-color: var(--hp); color: var(--bad); }
.hsdrop.warn { background: rgba(255, 139, 122, .10); border-color: var(--hp);
               color: var(--bad); }
.hsback:hover:not(:disabled) { border-color: var(--torch-d); color: var(--torch); }

/* ── THE RENT IS THE NUMBER THAT TAKES THE HOUSE AWAY ──────────────────────
   So it is a plate recessed into the wall rather than text right-aligned in a
   row it shares with two buttons. Inside the last day the plate itself goes
   red -- the label and the border, not the number: the amount owed has not
   changed, the time left has. */
.hsplaque { display: flex; flex-direction: column; gap: 2px;
            padding: 6px 10px 7px; background: var(--well);
            border: 1px solid var(--line); border-radius: 6px; }
.hsplaque .l { display: flex; align-items: center; gap: 5px;
               font: 400 8px var(--label); letter-spacing: .1em;
               text-transform: uppercase; color: var(--sub); }
.hsplaque .v { display: flex; align-items: center; gap: 6px;
               font: 400 26px var(--display); color: var(--torch);
               font-variant-numeric: tabular-nums; line-height: 1.05; }
.hsplaque .v .coin { width: 17px; height: 17px; margin: 0; }
.hsplaque .n { font-size: 11px; color: var(--dim); }
.hsplaque.soon { border-color: var(--hp); }
.hsplaque.soon .l { color: var(--bad); }

/* ── FLOORS ARE A LIFT, AND IT RIDES ON THE ROOM ───────────────────────────
   `.hsfloors` was a wrapping flex row in a 128px rail, so a four-floor house
   came out two and two: a stack of buttons whose shape had nothing to do with
   the shape of the house. One fixed row per floor, one per line, top floor at
   the top -- it cannot wrap at any floor count.

   ON the room because the floor you are standing on is a fact about the
   PICTURE, and on the room's RIGHT edge because the facts column is on the
   left. Over the canvas, so it needs its own stacking level and its own drag
   listeners -- see `wire()` in app.mjs. */
.hslift { position: absolute; right: 12px; top: 50%; z-index: 3;
          transform: translateY(-50%); display: flex; flex-direction: column;
          gap: 3px; padding: 5px; background: rgba(9, 11, 16, .82);
          border: 1px solid var(--line); border-radius: 7px; }
.hsliftcap { display: grid; place-items: center; height: 18px; margin-bottom: 1px;
             border-bottom: 1px solid var(--line); color: var(--faint); }
.hsliftb { width: 30px; height: 26px; display: grid; place-items: center;
           cursor: pointer; border-radius: 4px; font-size: 12px;
           font-variant-numeric: tabular-nums; background: transparent;
           border: 1px solid transparent; color: var(--sub); }
.hsliftb:hover { color: var(--torch); }
.hsliftb.on { background: rgba(240, 165, 60, .16); border-color: var(--torch-d);
              color: var(--torch); }

/* ── ROOM / PLAN AND THE TURN PAIR ─────────────────────────────────────────
   Two joined controls instead of four loose 24px scraps in a column. One
   border round the pair says they are one choice; four separate boxes said
   there were four things to decide. */
.hsseg { display: flex; height: 26px; background: var(--ink-2);
         border: 1px solid var(--line); border-radius: 4px; overflow: hidden; }
.hssegb { flex: 1; display: flex; align-items: center; justify-content: center;
          gap: 5px; cursor: pointer; border: 0; background: transparent;
          color: var(--sub); font-size: 11.5px; }
.hssegb + .hssegb { border-left: 1px solid var(--line); }
.hssegb:hover { color: var(--torch); }
.hssegb.on { background: rgba(240, 165, 60, .14); color: var(--torch); }
.hsspin .hssegb:hover { background: rgba(240, 165, 60, .10); }
.hsspin .hssegb:active { background: var(--ink); }

/* ── THE ROOM READS AS A ROOM ──────────────────────────────────────────────
   A bare grid of dark squares on a dark panel is a scatter of squares; the
   frame is what makes eight of them a floor. Sized to its contents rather than
   to the panel, so a two-by-four hut does not draw a hall around itself. */
/* THE PLAN LIVES IN THE SAME BOX THE ROOM DOES. It used to be a frame of its
   own, sized to its contents -- which meant the panel changed shape when you
   switched view, and the floor lift had nothing to stand on. One box, two ways
   of filling it, and the grid centred in it.

   `safe center` and not plain `center`: a floor plan bigger than the box has to
   scroll, and centred overflow puts the first row and the first column on the
   wrong side of the scroll origin, permanently out of reach. The unprefixed
   pair is the fallback for anything that does not know the keyword. */
.hsplanpad { position: absolute; inset: 0; overflow: auto; padding: 10px;
             display: flex;
             align-items: center; align-items: safe center;
             justify-content: center; justify-content: safe center;
             scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
.hsgrid { display: grid; gap: 3px; justify-content: start; }
/* The tile is the game's own slot at the size a ROOM wants rather than the size
   a bag wants -- see HOUSE_CELL. */
.hscell { position: relative; width: 40px; height: 40px; }
.hscell.slot { width: 40px; height: 40px; }
/* A HOUSE IS NOT A RECTANGLE. Every cell of the bounding box that the floor
   plan does not name is a hole with nothing in it, which is what makes an
   L-shaped room read as an L. `.slot.gap` already means this in the bag and is
   deliberately not reused: that one is hidden, and this one must not be, or the
   grid loses its shape. */
.hscell.gap { background: none; border: 0; box-shadow: none; }
.hsgrid .slot.empty { background: rgba(9, 11, 16, .5); border: 1px solid #1d2431;
                      box-shadow: inset 0 1px 3px rgba(0,0,0,.55); cursor: pointer; }
.hsgrid .slot.empty:hover { border-color: var(--torch-d); }
/* ONE LINE, IN THE FOOTER. It was a four-line paragraph pinned to the bottom of
   the rail (`margin-top: auto`) and it was the biggest thing on that side of
   the panel -- a standing instruction taking more room than the controls it
   described. The `@media (max-width: 720px)` rule that hid it went with it:
   there is nothing left to hide. */
.hshint { font-size: 12px; color: var(--faint); margin: 0;
          overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── THE CONTROLS BESIDE THE ROOM ──────────────────────────────────────────
   A room in this projection comes out very nearly square, in a panel three
   times wider than it is tall. Every row of buttons above it takes height the
   room has none of and leaves width nobody is using; in a column at the side
   they are free. Under 900px the column would leave the room too narrow to
   read, so the two stack and the column goes back to being a row. */
.hsroomrow { display: flex; align-items: stretch; gap: 12px;
             flex: 1 1 auto; min-height: 0; }
@media (max-width: 900px) {
  .hsroomrow { flex-direction: column; }
  .hscol { flex: none; flex-direction: row; align-items: center;
           flex-wrap: wrap; }
  /* NEVER DROP A CONTROL to make room -- the auction's narrow rule tried to
     hide the item rail and would have left that panel with no navigation at
     all. Here the spacer goes (a spacer in a wrapping row is a line break) and
     the hairline goes, and `Give it up` stays in the row with the rest. */
  .hscol .hsgrow { display: none; }
  .hscol .hscolend { padding: 0; border: 0; }
  .hscard { flex-direction: row; align-items: center; }
  .hswide, .hsdoor, .hsseg { width: auto; }
}

/* The turn POPOVER keeps the old small square button -- it wears the depot's
   clothes and is out of this redesign's scope. The pair beside `Room`/`Plan`
   is `.hsspin .hssegb` above. */
.hsturn { width: 28px; height: 24px; padding: 0; cursor: pointer;
          background: var(--ink-2); border: 1px solid var(--line); border-radius: 4px;
          color: var(--sub); font-size: 14px; line-height: 1; }
.hsturn:hover { border-color: var(--torch-d); color: var(--torch); }
.hsturn:active { background: var(--ink); }

/* ── AND THE ROOM ITSELF ───────────────────────────────────────────────────
   A BOX WITH A SIZE, because a WebGL canvas measures the box it is in and a box
   that sizes itself to its canvas measures nothing: both settle at zero and the
   room never draws. So the height is set here and the canvas fills it.

   16 / 10 rather than square: the camera is turned 45 degrees, so a room that
   is square on the floor is WIDER than it is tall on screen -- an isometric
   picture in a square box is a picture with two empty corners. */
/* IT TAKES THE SLACK, it does not set the height. An `aspect-ratio` box here
   was 558px tall inside a 485px panel, so the panel scrolled -- and what
   scrolled off the top was the rent, the Pay button and the Room/Plan switch.
   A room you have to scroll up from to pay your rent is a worse room than a
   slightly shorter one. */
.hsroomwrap { position: relative; flex: 1 1 auto; min-height: 240px;
              align-self: stretch; min-width: 0;
              overflow: hidden; background: var(--panel);
              border: 1px solid var(--line); border-radius: 8px; }
/* `inset: 0` would over-constrain this and the aspect-ratio would be ignored;
   pinned top and bottom and centred by transform, it keeps its shape. */
.hsshape { position: absolute; top: 0; bottom: 0; left: 50%;
           transform: translateX(-50%);
           aspect-ratio: 2 / 1; max-width: 100%; }
.hsroom { position: absolute; inset: 0; cursor: pointer; }
/* ── THE ROOM SAYS YES AND NO WITH THE TILE, NOT WITH A BOX ROUND THE HOUSE ──
   > *"can we remove that green/red border around the house? that looks weird"*

   Every drop target in the game outlines itself green or red under a drag, and
   everywhere else that is exactly right: a bag, a depot pane, an auction column
   are each one thing, and the outline is the whole of the answer. A room is not
   one thing -- it is twenty tiles -- so outlining it says "somewhere in here",
   which is both true and useless, and it does it by drawing a box round a
   picture of a house. The tile under the pointer already answers, in the right
   place and in the same two colours: amber for the tile it will land on, red
   for one that is taken. The handles are drop targets too now, so they are
   silenced with it -- an outline round a chair would be worse. */
.hsroom.drop-can, .hsroom.drop-ok, .hsroom.drop-no,
.hsgrab.drop-can, .hsgrab.drop-ok, .hsgrab.drop-no,
.hsmark.drop-can, .hsmark.drop-ok, .hsmark.drop-no {
  outline: none; animation: none; }
.hs-stage { display: block; width: 100%; height: 100%; }

/* ── NO TARGETS ON EMPTY TILES ─────────────────────────────────────────────
   > *"no circles please, the item should snap to the tile hovered during the
   > drag and highlight the tile that it will be dropped onto."*

   There was a ring on every tile and it was a thing to AIM AT: a drag had to
   land on one, and forty drawn at once were a doily over the floor. The tile
   itself is lit on the floor now, in the room's own perspective, by the
   renderer -- so the whole tile is the target and there is nothing to miss.

   What is left in the DOM is an invisible handle over anything you can pick up,
   because starting an HTML5 drag needs an element to start it from. */
.hsgrab { position: absolute; width: 42px; height: 36px;
          transform: translate(-50%, -68%); pointer-events: auto; cursor: grab; }

.hsgrab.dragging, .hsmark.dragging { opacity: .35; }

/* THE THINGS ON THE FLOOR ARE DOM, hung over the canvas at the screen position
   the renderer gives for their tile -- see the note at the top of house3d.mjs.
   `pointer-events: none` on the layer and back on for each sprite, so the floor
   underneath still takes the clicks that are not on a thing. */
/* THE LAYER IS TRANSPARENT TO THE POINTER and each handle is not, so the floor
   underneath still answers "which tile is this" everywhere except directly over
   something you could pick up -- which is the one place the answer is already
   known. */
.hsmarks { position: absolute; inset: 0; pointer-events: none; }
.hsmark { position: absolute; transform: translate(-50%, -78%);
          pointer-events: auto; cursor: pointer; line-height: 0;
          filter: drop-shadow(0 2px 3px rgba(0, 0, 0, .75)); }
.hsmark:hover { filter: drop-shadow(0 0 4px var(--torch)); }
.hsmark .qty { position: absolute; right: -2px; bottom: -2px; line-height: 1;
               padding: 0 3px; border-radius: 3px; background: rgba(9, 11, 16, .85);
               color: var(--text); font-size: 10px;
               font-variant-numeric: tabular-nums; }

/* ── WHAT IS WAITING ON THE ANVIL ────────────────────────────────────────────
   > *"It will drop one after the other (one on top of the other above the
   > forge)"*

   An anvil is a MODEL: its handle in the room is invisible and has no size of
   its own to sit inside, so the waiting item is lifted clear of the anvil's own
   picture rather than laid over it. Sitting ON it would be indistinguishable
   from a second thing standing on the tile, which is the one reading it must
   not have.

   `pointer-events: none` on purpose: the whole point of the tile is that the
   SECOND drop lands on it, and a sprite in the middle of the target that
   answered drags itself would be exactly the hole in the floor the handles were
   taught not to be. */
/* ── WHAT IS WAITING, SITTING ON THE ANVIL ────────────────────────────────
   > *"Make sure the item above the anvil is sitting nicely above the anvil,
   > aligned with the center of it."*

   THE HANDLE IS THE ANVIL'S CENTRE LINE. `.hsgrab` is placed at the tile's
   projected point and pulled back by half its own width, so its horizontal
   middle IS the middle of the model standing on that tile -- which makes
   `left: 50%` with a -50% pull the alignment asked for, and it stays aligned
   when the camera turns, because the handle moves with it.

   `bottom: 100%` puts the sprite's FOOT on the handle's top edge, which is
   24px above the tile's point -- clear of the anvil's own silhouette without
   floating off into the room. The gap is 4px: at 0 the sprite's shadow touched
   the anvil's top face and the two read as one object. */
.hsanvil { position: absolute; left: 50%; bottom: 100%;
           transform: translate(-50%, -4px); line-height: 0;
           pointer-events: none;
           filter: drop-shadow(0 0 6px var(--torch))
                   drop-shadow(0 3px 4px rgba(0, 0, 0, .8)); }

/* AND IN THE PLAN IT IS A CORNER BADGE, NOT SOMETHING ABOVE.  A plan cell is
   40px with the anvil already drawn in the middle of it, so there is no "above"
   to put anything in -- the first version hung the sword over the top edge,
   where it overlapped the anvil, clipped against the panel on the top row, and
   read as a second thing standing on the tile. So it goes where the plan
   already puts a fact about a stack: a badge in a corner, the opposite one
   from `qty`. */
.hscell .hsanvil { left: 1px; top: 1px; bottom: auto; transform: none;
                   border-radius: 3px; padding: 1px;
                   background: rgba(9, 11, 16, .72);
                   outline: 1px solid var(--torch); filter: none; }

/* ── AND WHAT HAPPENED, THROWN OVER THE FORGE ─────────────────────────────
   > *"Instead a green/red text at the rail of the window, make the forge
   > success or fail, a floating popup above the forge, with an animation of
   > fading it and out."*

   TWO ELEMENTS, and the reason is the rule this project has paid for three
   times: **an element positioned by script must not be animated on the same
   property.** A running animation whose keyframes mention `transform` beats an
   inline `style.transform` for as long as it runs, so the box that carries the
   position carries no animation at all and the child inside it does the rising
   and the fading.

   `position: fixed`, off a rectangle measured before the round trip: the room
   is rebuilt by the reply, and this is a thing that HAPPENED rather than a
   thing that is true, so it must not be rebuilt with it. */
.hsfloatbox { position: fixed; z-index: 60; pointer-events: none;
              width: 0; height: 0; }
.hsfloat {
  position: absolute; left: 0; bottom: 6px; translate: -50% 0;
  display: grid; justify-items: center; gap: 1px; white-space: nowrap;
  padding: 5px 11px; border-radius: 6px;
  background: rgba(9, 11, 16, .93); border: 1px solid var(--line);
  box-shadow: 0 6px 16px rgba(0, 0, 0, .6);
  animation: hsFloatUp 1.9s ease-out forwards;
}
/* CENTRED WITH `translate`, NEVER WITH `transform` -- the rise owns transform
   for the whole 1.9s and would throw a translateX away on its first keyframe.
   Same fix, same reason, as the arena's damage numbers. */
/* `--label` and `--text`, which are the tokens this stylesheet actually has.
   The first draft said `var(--pixel)` and `var(--txt)` -- neither exists, and an
   unknown custom property does not fall back, it makes the whole declaration
   invalid at computed-value time and the rule quietly does nothing. Grep the
   stylesheet for a token before using it, the same way you grep it for a class
   name before naming one. */
.hsfloat .t { font-family: var(--label); font-size: 12px; letter-spacing: .5px; }
.hsfloat .s { font-size: 11px; color: var(--faint); }
.hsfloat.win  { border-color: #4f9d69; }
.hsfloat.win  .t { color: #6ee7a0; }
.hsfloat.half { border-color: #9d7a3a; }
.hsfloat.half .t { color: var(--torch); }
.hsfloat.lost { border-color: #9d4f4f; }
.hsfloat.lost .t { color: #ff8080; }
.hsfloat.hold .t { color: var(--text); }

@keyframes hsFloatUp {
  0%   { opacity: 0; transform: translateY(10px) scale(.9); }
  14%  { opacity: 1; transform: translateY(0) scale(1); }
  70%  { opacity: 1; transform: translateY(-14px) scale(1); }
  100% { opacity: 0; transform: translateY(-34px) scale(1); }
}
/* REDUCED MOTION IS A FADE, NOT NOTHING. The node is removed on `animationend`,
   so an animation of `none` would leave every float ever thrown on the page --
   the same trap the arena's effects had to be taught. */
@media (prefers-reduced-motion: reduce) {
  .hsfloat { animation: hsFloatFade 1.9s ease-out forwards; }
  @keyframes hsFloatFade { 0% { opacity: 0 } 12% { opacity: 1 }
                           70% { opacity: 1 } 100% { opacity: 0 } }
}

/* The bag, inside the popover that asks what goes on a tile. */
.hsput { width: 232px; }
.hspick { display: grid; grid-template-columns: repeat(5, 38px); gap: 4px;
          max-height: 168px; overflow: auto; margin-bottom: 8px; }
.hspick .slot { width: 38px; height: 38px; }

/* ── the trait tree ──────────────────────────────────────────────────────────
   THE BOARD AND THE PANEL BESIDE IT. The tree used to be the whole frame with a
   hover card floating over it, and a hover card is gone the moment you move the
   pointer to think -- so comparing two traits meant remembering the first one.
   The inspector stays put: what you clicked, every rank it has, what it needs,
   and the button that spends the point. */
.treeroom { position: absolute; inset: 0; display: flex; overflow: hidden; }
.treewrap {
  position: relative; flex: 1; min-width: 0; overflow: hidden; cursor: grab;
  background: radial-gradient(60% 60% at 50% 50%, #131924, #0b0e15);
  touch-action: none;
  /* ── DRAGGING THE TREE, NOT THE PICTURE ────────────────────────────────────
     The pan is pointer events, and the browser's own two gestures were fighting
     it: a drag that starts on one of the 141 node ICONS begins a native image
     drag -- the "no drop" cursor, and a tree that does not move -- and a drag
     that starts anywhere else selects the labels underneath. Both are switched
     off here rather than in the handler, because they are decisions about what
     this surface IS. */
  user-select: none; -webkit-user-select: none;
}
.treewrap img, .treewrap .tnode {
  -webkit-user-drag: none; user-drag: none;
}
/* ── AND NOTHING IN HERE PAINTS A SELECTION ──────────────────────────────────
   The belt to the handler's braces. `user-select: none` says a selection cannot
   START here, and the handler clears any that an earlier press left behind --
   but a selection anchored anywhere can still be PAINTED across this surface as
   the pointer crosses it, and the board then washes pale blue while you drag,
   as if you had selected it. There is no text in here anybody wants to select,
   so a highlight over it is only ever a lie. */
.treewrap ::selection { background: transparent; color: inherit; }
.treewrap ::-moz-selection { background: transparent; color: inherit; }
/* `panning`, not `dragging`: the global `.dragging` means "being carried away
   from here" and is 40% opacity, which took the whole board down with it for as
   long as the mouse was held. A board being pushed around is not a thing being
   carried somewhere -- two gestures, two names. */
.treewrap.panning { cursor: grabbing; }
.treepan { position: absolute; top: 0; left: 0; transform-origin: 0 0; }
.treelines { position: absolute; top: 0; left: 0; pointer-events: none; }
.treelines line { stroke: #313c52; stroke-width: 6; }
.treelines line.lit { stroke: var(--torch-d); stroke-width: 8; }

.tnode {
  position: absolute; width: 86px; height: 86px; margin: -43px 0 0 -43px;
  border: 3px solid var(--tc); border-radius: 10px;
  background: #10141c; display: grid; place-items: center;
  cursor: pointer; opacity: .5; filter: grayscale(.85);
  transition: opacity .12s, filter .12s, box-shadow .12s;
}
/* Three states, and they have to be tellable apart across a whole board at
   once: SHUT is dim and grey, OPEN is lit and colourful because it is the thing
   you are looking for, TAKEN carries a torch ring nothing else has. */
.tnode.open { opacity: 1; filter: none; box-shadow: 0 0 14px rgba(255,255,255,.06); }
.tnode.on   { opacity: 1; filter: none; box-shadow: 0 0 0 3px rgba(240,165,60,.35),
                                                    0 0 26px rgba(240,165,60,.28); }
.tnode:hover { opacity: 1; filter: none; }
.tnode.shut { cursor: not-allowed; opacity: .38; }
.tnico { width: 64px; height: 64px; image-rendering: pixelated; }
/* The initials only show when the icon could not be loaded, so a tree without
   `npm run sync` is still a tree you can read and click. */
.tninit { display: none; font-size: 26px; font-weight: 700; color: var(--tc);
          letter-spacing: .04em; }
.tnode.noico .tninit { display: block; }
.tnrank { position: absolute; right: -4px; bottom: -8px; font-size: 15px;
          font-weight: 700; color: #10141c; background: var(--torch);
          border-radius: 9px; padding: 0 6px; font-variant-numeric: tabular-nums; }
.tnode:not(.on) .tnrank { display: none; }

/* ── THE NAME UNDER THE TILE ─────────────────────────────────────────────────
   Drawn in BOARD space, so it scales with the tile rather than staying a fixed
   size while the picture shrinks away from it. It is allowed to overhang its
   own node either side -- `left: -52px; right: -52px` -- because trait names
   are longer than 86px and a name that wrapped to three lines under every tile
   would be a board made of text.

   The stroke is three shadows rather than one: a hard black offset for weight,
   then two blurs to lift it off whatever line or tile is behind it. */
.tnname { position: absolute; left: -52px; right: -52px; top: 94px;
          text-align: center; font: 400 24px var(--label); line-height: 1.2;
          letter-spacing: .02em; color: #8b95a8; pointer-events: none;
          text-shadow: 0 3px 0 #05070b, 0 0 10px #05070b, 0 0 18px #05070b;
          display: none; }
/* Shown when the zoom can carry them -- see NAME_Z -- and always on the one you
   have actually asked about. */
.treewrap.named .tnname, .tnode.sel .tnname { display: block; }
.tnode.open .tnname { color: var(--text); }
.tnode.on .tnname { color: #ffd479; }

/* SELECTED IS ITS OWN STATE, and it has to beat both "taken" and "shut" -- you
   can select a node you cannot afford, and the whole point is that it then
   looks picked rather than dim. */
.tnode.sel { opacity: 1; filter: none;
             box-shadow: 0 0 0 3px #ffc46a, 0 0 0 7px rgba(255,196,106,.28),
                         0 0 34px rgba(255,196,106,.45); }

/* ── ZOOM, WITH A PICTURE ────────────────────────────────────────────────────
   The wheel was the only way in, which is the same fault the volume had: a
   gesture nothing on the screen mentions. The wheel still works. */
.treezoom { position: absolute; left: 12px; bottom: 12px; display: flex;
            gap: 4px; z-index: 3; }
.treezoom button { height: 28px; min-width: 28px; padding: 0 8px; cursor: pointer;
                   background: rgba(9,11,16,.8); border: 1px solid var(--line);
                   border-radius: 2px; color: var(--brow);
                   font: 400 9px var(--label); letter-spacing: .08em;
                   line-height: 1; }
.treezoom button:hover { color: var(--torch); border-color: var(--torch-d); }

/* ── THE INSPECTOR ───────────────────────────────────────────────────────────
   330px, its own column, and it does not move. */
.treeaside { width: 330px; flex: none; display: flex; flex-direction: column;
             background: linear-gradient(#1a2029, #101520);
             border-left: 2px solid var(--edge);
             box-shadow: inset 1px 0 0 var(--line); }
.asnone { flex: 1; display: grid; place-items: center; padding: 24px;
          text-align: center; }
.asnone .t { font: 400 20px var(--display); letter-spacing: .1em;
             text-transform: uppercase; color: #3f4a5e; }
.asnone p { margin: 8px auto 0; font-size: 12px; line-height: 1.5;
            color: var(--dim); max-width: 200px; }

.ashead { flex: none; display: flex; gap: 11px; align-items: flex-start;
          padding: 14px 14px 12px; border-bottom: 1px solid var(--line);
          background: linear-gradient(rgba(240,165,60,.06), transparent); }
.asico { position: relative; width: 58px; height: 58px; flex: none;
         display: grid; place-items: center; border: 3px solid var(--tc);
         border-radius: 10px; background: var(--ink-2); }
.asico img { width: 44px; height: 44px; image-rendering: pixelated; }
.asico .init { font-size: 20px; font-weight: 700; color: var(--tc); }
.aswho { min-width: 0; flex: 1; }
.aswho .t { font: 400 22px var(--display); letter-spacing: .06em;
            text-transform: uppercase; color: var(--torch); line-height: 1.1; }
.aswho .s { display: flex; align-items: center; gap: 7px; margin-top: 5px;
            font: 400 9px var(--label); letter-spacing: .06em; color: var(--dim); }
.aswho .tier { border: 1px solid var(--line); border-radius: 3px; padding: 1px 5px; }
.aswho .branch { color: var(--tc); }
.asrank { flex: none; text-align: right; }
.asrank .n { font: 400 26px var(--display); color: #e7ecf4; line-height: 1; }
.asrank .l { font: 400 8px var(--label); letter-spacing: .1em;
             text-transform: uppercase; color: var(--dim); }

.asbody { flex: 1; min-height: 0; overflow-y: auto; padding: 12px 14px;
          scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
.asbody::-webkit-scrollbar { width: 8px; }
.asbody::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px; }
.askicker { font: 400 8px var(--label); letter-spacing: .1em;
            text-transform: uppercase; color: var(--dim); margin: 12px 0 7px; }
.asbody > .askicker:first-child { margin-top: 0; }
.aslevels { display: flex; flex-direction: column; gap: 4px; }
.aslv { display: flex; gap: 9px; align-items: flex-start; padding: 6px 8px;
        border-radius: 4px; font-size: 13px; line-height: 1.4;
        border: 1px solid #202836; color: #8b95a8; }
.aslv .n { flex: none; width: 16px; height: 16px; display: grid;
           place-items: center; border-radius: 3px; font: 400 9px var(--label);
           background: #232a36; color: var(--dim); }
.aslv .t { min-width: 0; text-wrap: pretty; }
/* HAVE is settled and NEXT is the offer, so they are green and torch rather
   than two shades of the same colour: "what I bought" and "what one more point
   buys" are the two things this list exists to tell apart. */
.aslv.have { background: rgba(95,191,111,.07); border-color: rgba(95,191,111,.25);
             color: var(--text); }
.aslv.have .n { background: #5fbf6f; color: var(--ink); }
.aslv.next { background: rgba(240,165,60,.10); border-color: var(--torch-d);
             color: var(--text); }
.aslv.next .n { background: var(--torch); color: var(--ink); }
/* ── THE NUMBER THAT MOVES ───────────────────────────────────────────────────
   Five near-identical sentences whose whole difference is two characters. The
   mark is what makes the list a comparison instead of five paragraphs to diff
   by eye -- see markRanks, which works out WHICH numbers move rather than
   lighting up every digit.

   One treatment in all three row states. A rank you have, the rank you would
   buy and a rank far off are already told apart by the row itself; giving the
   number a second colour per state would mean the eye cannot follow one value
   down the column, which is the entire job. */
.aslv .lvn { font-weight: 700; color: #ffd479; background: rgba(240,165,60,.16);
             border-radius: 3px; padding: 0 2px;
             font-variant-numeric: tabular-nums; }
.asdetail { margin: 12px 0 0; padding: 9px 11px;
            background: rgba(176,127,217,.08);
            border: 1px solid rgba(176,127,217,.28); border-radius: 4px;
            font-size: 12px; line-height: 1.5; color: #c9b6de;
            white-space: pre-line; text-wrap: pretty; }
.asreq { font-size: 12px; color: var(--brow); }

/* ── WHAT A TRAIT CASTS, AND THE SHAPE IT MAKES ──────────────────────────────
   > *"on the trait description, let's also have the AOE demonstration of the
   > spells the way that the wiki have."*

   Eighteen nodes grant a spell, and for most of them the spell IS the trait --
   "Attacking has a 35% chance of doing a special effect based on your
   Ammunition type" tells you nothing about whether that effect covers one tile
   or nine. The card is the wiki's own layout: the words on the left, the
   footprint on the right, wrapping under when the diagram is too wide for the
   column (a beam is twenty-three cells across).

   THE CELL SIZE IS SET IN JS, not here, because it depends on how many columns
   the diagram has -- the same reason the wiki sizes its own in `aoeHTML`. */
.asspell { display: flex; flex-wrap: wrap; gap: 9px; align-items: flex-start;
           margin-top: 8px; padding: 9px 11px;
           background: rgba(90,150,220,.07);
           border: 1px solid rgba(90,150,220,.24); border-radius: 4px; }
.asspell .asspl { flex: 1 1 150px; min-width: 0; }
.asspell .n { font: 400 13px var(--display); letter-spacing: .04em;
              color: #cfe0f5; }
.asspell .d { font-size: 11px; line-height: 1.45; color: #93a6bd;
              margin-top: 2px; text-wrap: pretty; }
.asspell .f { font-size: 11px; color: var(--xp); margin-top: 3px;
              font-variant-numeric: tabular-nums; }
.asspell .e { font-size: 11px; color: #c9b6de; margin-top: 3px; }
.asspell .m { font: 400 8px var(--label); letter-spacing: .08em;
              text-transform: uppercase; color: var(--dim); margin-top: 5px; }

/* THE DIAGRAM. `--n` columns of `--c` pixels, both handed over by the painter.
   Four cell states and they are the game's own reading of the grid the engine
   sends: nothing, caught, the tile it is centred on, and the caster. */
.asaoe { flex: none; display: grid; gap: 2px;
         grid-template-columns: repeat(var(--n, 5), var(--c, 8px));
         align-content: start; }
.asaoe .ac { width: var(--c, 8px); height: var(--c, 8px); border-radius: 1px;
             background: rgba(255,255,255,.05);
             box-shadow: inset 0 0 0 1px rgba(255,255,255,.04); }
.asaoe .ac.a { background: rgba(240,165,60,.42);
               box-shadow: inset 0 0 0 1px rgba(240,165,60,.55); }
.asaoe .ac.t { background: var(--torch);
               box-shadow: inset 0 0 0 1px #ffd479; }
/* THE CASTER'S OWN TILE, and a different colour on purpose: a diagram where
   "where I stand" and "what I hit" are the same swatch is a diagram that has
   to be counted rather than read. */
.asaoe .ac.me { background: rgba(120,190,255,.55);
                box-shadow: inset 0 0 0 1px rgba(160,215,255,.8); }

.asfoot { flex: none; padding: 12px 14px 14px; border-top: 1px solid var(--line);
          background: rgba(9,11,16,.45); }
.aswhy { display: flex; align-items: center; gap: 7px; margin-bottom: 9px;
         font-size: 12px; color: var(--dim); }
.aswhy .g { font-size: 10px; }
.aswhy.can { color: var(--torch); }
.aswhy.done { color: #7fd18c; }
.aswhy.no { color: #ff8b7a; }
.asbtns { display: flex; gap: 7px; }
.asspend { flex: 1; padding: 10px 12px; cursor: pointer; border: 0;
           border-radius: 6px; font: inherit; font-weight: 700;
           letter-spacing: .04em; background: var(--torch); color: #1a1205; }
.asspend:disabled { background: #232a36; color: var(--dim); opacity: .55;
                    cursor: not-allowed; }
.asbtns .ghost { flex: none; padding: 10px 12px; font-size: 13px; }

/* Left padding clears the zoom controls, which share this corner. */
.treehint { position: absolute; left: 0; right: 0; bottom: 0;
            padding: 7px 14px 7px 150px;
            font-size: 12px; color: var(--dim); text-align: center;
            background: linear-gradient(transparent, rgba(9,11,16,.9) 55%);
            pointer-events: none; }

/* ── narrow windows ──────────────────────────────────────────────────────────
   Below 1100px there is not room for three columns and a readable market, so
   the panels stack above and below the middle rather than squeezing it. */
@media (max-width: 1100px) {
  body { overflow: auto; }
  .stage { height: calc(100vh - var(--head)); }
  .middle { min-height: 0; }
  /* THE WINDOWS STOP BEING COLUMNS. On a narrow screen a 300px window on each
     side is most of the screen, so they become one centred sheet over the town
     -- still dismissible, still remembered, just no longer pinned to an edge
     that is not there. */
  .winside { left: 10px; right: 10px; width: auto; }
  .winside.right { left: 10px; }
  .win { max-height: 60vh; }
  /* The rail wraps rather than scrolling its labels away: the tabs are the
     only way to get anywhere. */
  .rail { height: auto; min-height: var(--rail); flex-wrap: wrap; padding: 6px 10px; gap: 8px; }
  .places { flex-wrap: wrap; }
  .mtext { min-width: 0; }
  .mxp .xpline { width: 90px; }
  .mkrow { grid-template-columns: 34px 1fr 84px 70px; }
  .mkrow .cat { display: none; }
}


/* ── TWELVE TABS ON A LAPTOP ─────────────────────────────────────────────────
   Twelve 104px tabs, their gaps, the hairline and the medallion come to roughly
   1540px. That is the width the artboard was drawn at and it is wider than most
   screens this will be opened on, so the row gives things up in the order they
   are worth least.

   FIRST THE EVEN WIDTH. The grid was the nicety; the labels are the content, so
   the tabs go back to being as wide as their words before anything is hidden.

   THEN THE LABELS. Below 1400 the words go and the icons stay, which is what
   the sprites were chosen for -- a chest is a chest at 20px. The name survives
   on the hover and in `aria-label`, so nothing is lost to a screen reader and
   nothing is lost to a moment's pointing. */
@media (max-width: 1560px) {
  .placetab { min-width: 0; padding: 9px 9px; }
}
@media (max-width: 1400px) {
  .placetab { min-width: 0; width: 40px; padding: 9px 0; }
  .placetab .lbl { display: none; }
  /* Outside the corner rather than on it: with the label gone the tab is the
     icon, and a badge stamped inside would sit on top of the picture that is
     now the only thing naming the tab. */
  .placetab .railbadge { right: -4px; top: -5px; }
  .railsep { margin: 0 4px; }
}

/* ── dragging ────────────────────────────────────────────────────────────────
   A drop target answers before the drop: green where it will work, red where it
   will not. The red matters more than the green -- "nothing happened" is the
   worst possible answer to a drag, and this is that answer said out loud. */
.dragging { opacity: .4; }
/* ── AND WHERE IT COULD GO, BEFORE YOU GET THERE ─────────────────────────────
   `drop-can` is on every target that would accept what is currently in the air,
   from the instant the drag starts -- so picking up a helmet lights the one
   square it belongs in rather than making you tour the doll to find it.

   QUIETER THAN THE HOVER, on purpose. This is on several slots at once and it
   has to read as "these are the candidates", not as six simultaneous
   confirmations; the solid outline stays for the one under the cursor, which is
   the slot that will actually take it. The pulse is what carries it at a
   glance -- a still tint on six squares is easy to miss over artwork. */
.drop-can { outline: 1px dashed rgba(95,191,111,.75); outline-offset: -2px;
            background-color: rgba(95,191,111,.10);
            animation: dropCan 1.15s ease-in-out infinite; }
.drop-can.drop-ok, .drop-can.drop-no { animation: none; }
@keyframes dropCan {
  0%, 100% { background-color: rgba(95,191,111,.07); }
  50%      { background-color: rgba(95,191,111,.20); }
}
@media (prefers-reduced-motion: reduce) { .drop-can { animation: none; } }
.drop-ok { outline: 2px solid #5fbf6f; outline-offset: -2px; }
.drop-no { outline: 2px solid #a3454f; outline-offset: -2px; }

/* ── the item menu ───────────────────────────────────────────────────────────
   Opened by either mouse button on any item, anywhere. Its rows are actions the
   server will accept; the ones it would refuse are shown greyed with the reason
   beside them, because a row that is missing explains nothing. */
.itemmenu {
  position: fixed; z-index: 60; min-width: 214px; max-width: 300px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 6px;
  box-shadow: 0 10px 30px rgba(0,0,0,.55); padding: 5px; display: grid; gap: 1px;
}
.itemmenu.hidden { display: none; }

/* ── THE AUTOCAST RULE EDITOR ────────────────────────────────────────────────
   A SENTENCE, NOT A FORM. Four controls on one line reading left to right --
   "Health / is under / 40 / %" -- because what is being written has to be
   legible at a glance from the bar afterwards, and a rule laid out as four
   labelled fields down a column reads as configuration rather than as the
   condition it is.

   AND IT SAYS WHETHER IT IS TRUE RIGHT NOW. The footer carries the live reading
   of whichever stat is selected, which is the difference between writing a rule
   and guessing at one: "Health is 147 of 150 — the rule does not hold" answers
   the question the player actually has, which is whether they have just
   switched the slot off by accident. */
/* ── THE SLOT'S OWN RACK ─────────────────────────────────────────────────────
   Everything you could put in this slot, as pictures. It borrows `.spellcell`
   from the sheet on purpose: the rack you drag FROM and the rack you choose IN
   should be the same object, or they are two different lists of the same
   spells. */
.slotrack { display: grid; gap: 5px; padding: 8px 4px 4px;
            grid-template-columns: repeat(auto-fill, minmax(42px, 42px)); }
.slotrack .spellcell { width: 42px; height: 42px; cursor: pointer; }
/* A spell you know but cannot cast yet is still placeable -- the server's rule
   is what you have LEARNED -- so it is dimmed rather than withheld, and the
   hover says what it is short of. */
.slotrack .spellcell.cant { opacity: .5; }
.slotrack .spellcell.on { border-color: var(--torch-d);
                          background: linear-gradient(#f0a53c22, #241c10);
                          box-shadow: inset 0 0 0 1px rgba(240,165,60,.25); }
.slotrack > p { grid-column: 1 / -1; margin: 4px 4px 8px; }
/* The chosen thing's own header, under the rack -- quieter than the box's own
   title so the two do not read as two panels stacked. */
.rulebox .imhead.sub { border-top: 1px solid var(--line); padding-top: 8px;
                       margin-top: 4px; }

.itemmenu.rulebox { width: 330px; max-width: 92vw; padding: 8px; gap: 0; }
.rulelbl { padding: 8px 4px 6px; color: var(--dim); font-size: 11px;
           text-transform: uppercase; letter-spacing: .08em; }

/* ── THE SWITCH ──────────────────────────────────────────────────────────────
   The dot from the slot's corner, given room to say what it means. A real
   checkbox underneath -- so it is focusable, space toggles it, and a screen
   reader gets a checkbox rather than a div somebody decorated. */
.ruleswitch { display: flex; align-items: center; gap: 9px; cursor: pointer;
              padding: 8px 4px 2px; }
.ruleswitch input { position: absolute; opacity: 0; width: 0; height: 0; }
.ruleswitch .track {
  position: relative; width: 34px; height: 18px; flex: none; border-radius: 999px;
  background: var(--well); border: 1px solid var(--edge);
  box-shadow: inset 0 1px 2px rgba(0,0,0,.7);
  transition: background .14s;
}
.ruleswitch .track i {
  position: absolute; left: 2px; top: 2px; width: 12px; height: 12px;
  border-radius: 50%; background: #6c7a90;
  transition: left .14s, background .14s;
}
.ruleswitch input:checked + .track { background: linear-gradient(#3a2f1c, #241c10);
                                     border-color: var(--torch-d); }
.ruleswitch input:checked + .track i { left: 18px; background: var(--torch);
                                       box-shadow: 0 0 6px rgba(240,165,60,.8); }
.ruleswitch input:focus-visible + .track { outline: 2px solid var(--torch-d);
                                           outline-offset: 2px; }
.ruleswitch .say { font-size: 12px; color: var(--dim); }
.ruleswitch input:checked ~ .say { color: var(--text); }
.ruleline { display: flex; align-items: center; gap: 4px; padding: 0 2px; }
.rulesel, .ruleval {
  min-width: 0; padding: 4px 5px; font: inherit; font-size: 12px;
  background: var(--ink-2); border: 1px solid var(--line); border-radius: 4px;
  color: var(--text);
}
/* The operator carries the longest words in the sentence ("is at or under"),
   so it gets the slack rather than sharing it evenly and clipping. */
.rulesel { flex: 1 1 0; }
.ruleline > .rulesel:nth-of-type(2) { flex: 1.5 1 0; }
.ruleline .ruleunit { flex: none; }
.ruleline .ruleunit .rulesel { width: 68px; }
.ruleval { width: 54px; flex: none; text-align: right;
           font-variant-numeric: tabular-nums; }
.rulesel:focus, .ruleval:focus { outline: none; border-color: var(--torch-d); }
.ruleunit.hidden { display: none; }
/* ── A CONDITION THAT IS A STATE IS ONE CONTROL ──────────────────────────────
   "When hungry" has no operator, no number and no unit -- see redraw() in
   app.mjs -- so the line is a single select and it should look like one that
   was meant, rather than like three that failed to load. */
.ruleline.bare > .rulesel { flex: 1 1 auto; }
.rulefoot { padding: 9px 4px 4px; margin-top: 8px; border-top: 1px solid var(--line); }
.rulesay { color: var(--torch); font-size: 12.5px; }
.rulenow { color: #6ec06a; font-size: 11.5px; margin-top: 3px;
           font-variant-numeric: tabular-nums; }
.rulenow.off { color: var(--muted); }
.rulebtns { display: flex; justify-content: flex-end; gap: 6px; padding: 8px 2px 2px; }
.rulebtns button { padding: 5px 12px; font: inherit; font-size: 12px;
                   border-radius: 4px; cursor: pointer; }
.rulebtns .ghost { background: none; border: 1px solid var(--line); color: var(--dim); }
.rulebtns .ghost:hover { color: var(--text); border-color: var(--torch-d); }
.rulebtns .primary { background: linear-gradient(#3a2f1c, #241c10);
                     border: 1px solid var(--torch-d); color: var(--torch); }
.rulebtns .primary:hover { border-color: var(--torch); }
.imhead { display: flex; align-items: center; gap: 7px; padding: 5px 7px 7px;
          border-bottom: 1px solid var(--line); margin-bottom: 4px;
          font-size: 13px; color: var(--text); }
.imhead .grow { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* WHERE THE STACK IS STANDING, said once at the top instead of in eight notes.
   Only ever on a stored stack: the bag is where a menu is normally opened and
   labelling the ordinary case is noise. */
.imhead .imwhere { flex: none; font: 400 8px var(--label); letter-spacing: .08em;
                   text-transform: uppercase; color: var(--torch);
                   border: 1px solid var(--torch-d); border-radius: 3px;
                   padding: 2px 5px; }
.imrow { display: flex; align-items: center; gap: 10px; width: 100%;
         padding: 6px 8px; background: none; border: 0; border-radius: 4px;
         color: var(--text); font: inherit; font-size: 13px; text-align: left;
         cursor: pointer; }
.imrow:hover:not(:disabled) { background: var(--panel-2); color: var(--torch); }
.imrow .muted { font-size: 11px; }
.imrow.off { color: var(--muted); cursor: default; }
/* ── THE MARK IN FRONT OF EACH ROW ───────────────────────────────────────────
   A FIXED COLUMN, not an inline icon. Every row reserves the same width whether
   or not it has a picture, so the labels start on one line -- a menu whose text
   steps in and out by twenty pixels is harder to read than one with no icons at
   all. A row that fails to load its art hides the image and keeps the box. */
.imrow .imico {
  flex: none; width: 20px; height: 20px; display: grid; place-items: center;
  font-size: 13px; line-height: 1; color: var(--dim); overflow: hidden;
}
.imrow .imico img { image-rendering: pixelated; opacity: .85; }
.imrow:hover:not(:disabled) .imico img { opacity: 1; }
.imrow.off .imico img { opacity: .5; }
/* The doll art is a full-bleed square where a glyph is a character on a
   baseline; a hair of inset stops it out-weighing the label it introduces. */
.imrow .imico img.art { width: 18px; height: 18px; }

/* ── THE HUD ─────────────────────────────────────────────────────────────────
   One centred stack at the bottom of the screen, the way the game itself does
   it: a small strip, the numbered icons, then Health and Mana under them.

   THE THREE LINES ARE ONE OBJECT. Centred together and sized to each other, so
   the bars are exactly as wide as the row of icons above them -- which is what
   makes it read as a HUD rather than as three controls that happen to be near
   each other. The middle line is the SAME action bar the town has; only the
   outer two are about a fight. */
/* ── THE HUD FLOATS ON THE ART ───────────────────────────────────────────────
   It was a row under the stage, which meant the picture had to stop short of the
   bottom of the screen to leave room for it -- a black strip with a hotbar in
   it. Pinned to the frame instead: the art runs to the true bottom edge and the
   cluster sits on top of it, centred, the way a game draws a hotbar.

   The stage keeps its whole height either way, so nothing in the arena has to
   know this changed. */
/* ── THE CLUSTER CENTRES IN THE ROOM IT HAS, NOT IN THE SCREEN ───────────────
   With the bag open the hotbar was centred on the whole frame, which put its
   right-hand slots underneath the window -- so slot 8 and slot 9 were behind a
   panel you had opened deliberately, which is the one case where "the game is
   under the interface" is easiest to avoid. The cluster now centres in the
   space beside whatever is open, the same two numbers the panels and the chat
   already read.

   IT MOVES ONLY WHEN YOU MOVE IT. The rule from the walk-home still holds --
   nothing may slide the bars under a reading hand -- and this does not break
   it: the only thing that changes these numbers is you opening or closing a
   window, which is a deliberate act, and the transition makes it read as the
   room changing shape rather than as the buttons jumping. */
/* ── THE CLUSTER OWNS THE MIDDLE OF THE SCREEN ───────────────────────────────
   > *"It should always be horizontally centered, it should not slide when
   > openning inventory or equipments."*

   It used to inset itself between the two window columns -- `left: var(--winL);
   right: var(--winR)` -- so opening your bag slid nine keys and the Health bar
   a hundred and thirty pixels sideways, with a 140ms animation on it. Everything
   else on this screen steps aside for an open window because everything else is
   READING material. The hotbar is not read, it is AIMED AT: a hand goes to slot
   4 because slot 4 was there a second ago, and the muscle memory that makes a
   hotbar worth having is memory of a POSITION.

   So it is pinned to the frame and it does not move for anything. The windows
   are 276px columns at the edges and the cluster is about 460px wide in the
   middle; they do not collide, and where they would the window is the thing
   that is over it, because the window is the thing you just opened.

   `left: 0; right: 0` and not `left: 50%; transform` -- the children centre
   themselves in a full-width flex column, so the bar's own centre is the
   frame's centre no matter how many slots or bars are in it. */
.hudwrap { position: absolute; left: 0; right: 0;
           bottom: 14px; z-index: 6;
           display: flex; flex-direction: column; align-items: center;
           gap: 5px; pointer-events: none; }
.hudwrap[hidden] { display: none; }
.hudwrap > * { pointer-events: auto; }
.hudstrip, .hudvitals { display: flex; align-items: center; gap: 6px; }
.hudstrip.hidden, .hudvitals.hidden { display: none; }

/* ── the action bar ──────────────────────────────────────────────────────────
   Nine slots, present in town and in a fight. Sized so the row fits a narrow
   window without wrapping: wrapping would put slot 6 under slot 1 and break the
   one thing a number key relies on, which is that the ninth slot is where you
   last saw it. */
/* ── THE BAR IS A TRAY ───────────────────────────────────────────────────────
   Nine slots floating on the ground read as nine separate objects that happen
   to be in a row. One raised plate under them makes them a HOTBAR -- a single
   thing, with a lit top edge and a shadow beneath, which is what the game's own
   bar is and what makes it the obvious place for a hand to go. */
.abar { display: flex; gap: 5px; padding: 7px; flex: none; justify-content: center;
        background: linear-gradient(var(--lift), #131821);
        border: 2px solid var(--edge); border-radius: 5px;
        box-shadow: inset 0 1px 0 rgba(255,255,255,.10), 0 8px 24px rgba(0,0,0,.8); }
/* ── THE SAME BAR, INSIDE THE SHEET ──────────────────────────────────────────
   > *"Move the spells and put that hotbar on the left side of the character
   > sheet, under the attributes"*

   IT IS IN A PANEL NOW, NOT OVER ONE. The floating bar carries a heavy drop
   shadow and a two-pixel frame because it stands on top of a 3D scene and has
   to be told apart from it; inside a section of the sheet, with a heading above
   it, both of those read as a box drawn round a box. The slots keep every bit
   of their own styling -- they are the thing you recognise.

   AND IT WRAPS RATHER THAN OVERFLOWS. Nine 46px keys are 472px, and the sheet's
   left column is narrower than that whenever a window is open beside it. A row
   that runs off the edge would put slots 8 and 9 out of reach; two rows of
   five and four are still nine keys you can drop on. Left-aligned, because a
   centred second row would put slot 6 under slot 3. */
.abar.sheetbar {
  flex-wrap: wrap; justify-content: flex-start; padding: 0;
  background: none; border: 0; border-radius: 0; box-shadow: none;
}
/* A SHADE SMALLER THAN THE FIGHT'S. The floating bar is a target you hit
   without looking; this one is a thing you arrange while reading, and the six
   pixels it gives back are six pixels the sheet does not have to scroll for.
   The art inside is unchanged -- 34px in a 40px key rather than in a 46px one. */
.abar.sheetbar .abslot { width: 40px; height: 40px; }

/* THE FRAME IS THE STYLE. The game's own slots are a bevelled dark square with
   the art inset and the hotkey in the corner -- a lip of light along the top
   edge and a dark line under it, which is what makes a flat square read as a
   pressed key rather than as a div. */
.abslot {
  position: relative; width: 46px; height: 46px; flex: none;
  background: linear-gradient(#232a36, #151a24);
  border: 1px solid #0a0d13; border-radius: 4px;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.09),
              inset 0 -1px 0 rgba(0,0,0,.6), 0 1px 2px rgba(0,0,0,.5);
  display: grid; place-items: center; cursor: pointer; overflow: hidden;
}
/* An empty slot is a recess rather than a dashed outline: it is somewhere a
   thing could go, which a hole in the panel says better than a border does. */
.abslot.empty { cursor: default; background: rgba(9,11,16,.55);
                box-shadow: inset 0 1px 3px rgba(0,0,0,.7); }
.abslot:not(.empty):hover { border-color: var(--torch);
                            box-shadow: inset 0 1px 0 rgba(255,255,255,.12),
                                        0 0 8px rgba(240,165,60,.35); }
.abslot.dragging { opacity: .4; }
/* A SPELL YOU CANNOT YET CAST STAYS ON THE BAR and says so, rather than being
   quietly removed the level before you could use it. */
.abslot.cant { opacity: .5; }
/* Bottom-left, where the game puts it, and bright enough to read over art. */
.abkey { position: absolute; left: 3px; bottom: 1px; font-size: 11px;
         font-weight: 700; color: #cfd8e6; font-variant-numeric: tabular-nums;
         text-shadow: 0 1px 2px #000, 0 0 3px #000; pointer-events: none; }
/* THE AUTOCASTER IS A DOT. Nine checkboxes along the bottom of the screen is a
   settings page; this has to be readable while something is killing you. */
/* ── ONE GEAR, WHERE THE DOT WAS ─────────────────────────────────────────────
   The dot toggled autocast and a ring in the other corner opened the rule: two
   10px glyphs on a 46px square, doing related things, neither saying which was
   which. The gear replaces both -- it opens the settings, and the settings hold
   the switch.

   QUIET UNTIL WANTED. It sits at 45% and comes up on hover or when the slot has
   a condition on it, because nine gears at full strength across the bottom of
   the screen is a settings page where a hotbar should be. */
.abgear {
  position: absolute; right: 1px; top: 1px; width: 15px; height: 15px;
  padding: 0; display: grid; place-items: center; cursor: pointer;
  background: none; border: 0; color: #cfd8e6; opacity: .62;
  text-shadow: 0 1px 2px #000;
  transition: opacity .12s, color .12s;
}
/* THE SPRITE, NOT A GLYPH. `currentColor` no longer paints it, so the two
   states this button had -- dim, and lit when it carries a rule -- are carried
   by opacity and a drop-shadow in the torch instead. A pixel sprite tinted by
   a CSS filter goes muddy; a sprite with a coloured glow under it does not. */
.abgear .cog { width: 14px; height: 14px; display: block;
               filter: drop-shadow(0 1px 1px rgba(0,0,0,.9)); }
.abslot:hover .abgear { opacity: 1; }
.abgear:hover { opacity: 1; }
.abgear:hover .cog { filter: drop-shadow(0 0 3px var(--torch)); }
/* A slot carrying a condition wears its gear lit, because a rule you cannot see
   from the bar is a rule you forget you wrote and then blame the caster for. */
.abgear.ruled { opacity: 1; }
.abgear.ruled .cog { filter: drop-shadow(0 0 3px var(--torch))
                             drop-shadow(0 0 1px var(--torch)); }

/* ── AND THE BORDER SAYS IT IS ARMED ─────────────────────────────────────────
   Dota's answer, and it is the right one: a lit dot in a corner is a thing you
   have to look FOR, and a moving border on the icon is a thing you read from the
   corner of your eye while something is trying to kill you.

   TWO LAYERS, because one cannot do it. `::before` is the steady rim -- a slot
   whose animation is switched off must still be obviously armed -- and `::after`
   is the travelling sheen, a conic gradient turning inside a masked ring. The
   ring is drawn OUTSIDE the slot's own border (`inset: -2px`) so it reads as
   something added to the square rather than as the square changing colour, which
   is how `.broke` and `.cooling` speak. */
/* INSIDE THE SQUARE, NOT AROUND IT. The slot is `overflow: hidden` -- it has to
   be, or the cooldown sweep and the artwork spill out of it -- so a ring drawn
   at `inset: -2px` was clipped away to a grey sliver. Drawn just inside the
   border instead, which is also where Dota's own sits: on the icon. */
.abslot.auto::before {
  content: ""; position: absolute; inset: 0; border-radius: 4px;
  border: 2px solid rgba(240,165,60,.9);
  box-shadow: inset 0 0 7px rgba(240,165,60,.35);
  pointer-events: none; z-index: 3;
}
.abslot.auto::after {
  content: ""; position: absolute; inset: 0; border-radius: 4px;
  padding: 2px; pointer-events: none; z-index: 4;
  background: conic-gradient(from var(--spin, 0deg),
              transparent 0deg, #ffe6bd 28deg, transparent 62deg,
              transparent 180deg, #ffe6bd 208deg, transparent 242deg,
              transparent 360deg);
  /* The mask is what turns a filled square into a RING: paint the padding box
     out of the border box and only the 2px rim survives. */
  -webkit-mask: linear-gradient(#000 0 0) content-box exclude,
                linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box exclude,
                linear-gradient(#000 0 0);
  animation: abspin 2.4s linear infinite;
}
/* Animating a registered custom property rather than `transform`, so the sheen
   travels round the rim instead of the whole ring rotating -- a rotating square
   ring is visibly a square being turned. */
@property --spin { syntax: "<angle>"; inherits: false; initial-value: 0deg; }
@keyframes abspin { to { --spin: 360deg; } }
/* Somebody who asked their machine to stop moving things keeps the rim, which is
   the half that carries the meaning. */
@media (prefers-reduced-motion: reduce) {
  .abslot.auto::after { animation: none; opacity: 0; }
}
.abslot.cooling .spellico { filter: grayscale(.8) brightness(.6); }
/* A CONSUMABLE'S PICTURE IS A SPRITE, NOT A SPELL ICON, and a potion is on a
   cooldown now -- so `.cooling` has to reach both or a slot on the shared
   bottle clock draws its number over a picture that still looks pressable.
   Same filter, because it is the same statement. */
.abslot.consumable.cooling .sprite { filter: grayscale(.8) brightness(.6); }

/* ── A SPELL YOU CANNOT PAY FOR RIGHT NOW ────────────────────────────────────
   Not the same statement as `.cant`, which is a spell you have not the Magic
   for and never will until you level. This is Mana, and it comes back in a few
   seconds -- so it reads as SWITCHED OFF rather than as broken: knocked back,
   desaturated and washed blue, the colour of the thing it is short of. A red or
   grey slot would say "this is wrong"; blue says "not yet". */
.abslot.broke .spellico {
  filter: grayscale(.55) brightness(.55) sepia(1) hue-rotate(175deg) saturate(2.2);
}
.abslot.broke {
  border-color: #1d3050;
  background: linear-gradient(#151d2c, #0c1220);
  box-shadow: inset 0 1px 0 rgba(120,160,220,.09), inset 0 0 12px rgba(40,80,150,.35);
}
.abslot.broke .abkey { color: #5b7ba8; }

.abcdn { position: absolute; inset: 0; display: grid; place-items: center;
         font-size: 15px; font-weight: 700; color: var(--text);
         text-shadow: 0 1px 3px #000, 0 0 4px #000;
         font-variant-numeric: tabular-nums; pointer-events: none; }

@media (max-width: 1100px) {
  .abslot { width: 40px; height: 40px; }
}

/* ── THE MOVEMENT STRIP ──────────────────────────────────────────────────────
   The line above the icons, where the game keeps its second and smaller row of
   buttons. Four glyphs laid out the way the fight is -- you are on the left, so
   retreating is leftward -- and the labels drop out on a narrow window, because
   the glyph is the thing you learn and the word is the thing you learn it by. */
.stancerow { display: flex; gap: 3px; }
.stanceb {
  display: flex; align-items: center; gap: 5px; padding: 3px 9px;
  background: linear-gradient(#232a36, #151a24);
  border: 1px solid #0a0d13; border-radius: 4px;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.07);
  color: var(--dim); font: inherit; font-size: 11px; line-height: 1.4;
  cursor: pointer; white-space: nowrap;
}
.stanceb:hover { color: var(--text); }
.stanceb.on { color: var(--torch); border-color: var(--torch-d);
              background: linear-gradient(#3a2f1c, #241c10);
              box-shadow: inset 0 1px 0 rgba(255,255,255,.10),
                          0 0 7px rgba(240,165,60,.30); }
.stanceb .sg { font-size: 9px; opacity: .85; }
@media (max-width: 900px) { .stanceb .sl, .stanceb .kitelbl { display: none; } }
/* ── AND THE ONE THAT IS NOT A STANCE ────────────────────────────────────────
   "Ignore summons" wears the strip's button skin because it belongs to the same
   decision, but it is a SWITCH and not a fifth stance: it does not turn the
   other four off and they do not turn it off. The real checkbox is hidden and
   the `.on` class carries the state, which is the same thing every other
   control on this strip does -- and the input stays in the DOM so the keyboard
   and the label still work. */
.ignoreb { cursor: pointer; user-select: none; }
.ignoreb input { position: absolute; opacity: 0; width: 0; height: 0; }
.ignoreb input:focus-visible + .sg { outline: 1px solid var(--torch); outline-offset: 2px; }

/* ── AND HOW MANY WALK IN AT ONCE ────────────────────────────────────────────
   > *"the player can choose it lure up to a maximum of 5 monsters"*

   THE SAME SLOT AS THE OTHER FOUR AND THE SWITCH, because it is the same kind
   of decision -- where to stand, how far away, what to swing at, how many of
   them. It is not lit like a stance: the four buttons are a choice of one, and
   this is a number that is always set to something, so lighting it would put a
   fifth thing on permanently and make the row read as five stances.

   GREYED, NOT HIDDEN, where it does not apply. A lair holds one boss whatever
   this says, and a control that disappeared there would be a control the player
   thinks they broke -- so it dims and the tooltip says why. Pointer events stay
   on the wrapper so the tooltip still answers the question. */
.lureb { cursor: pointer; user-select: none; gap: 4px; }
.lureb .luresel {
  /* The strip's own two colours, literal like the button they sit on: the
     stance buttons do not use tokens for their gradient either, and a token
     invented here would be one nothing else defines -- which `check` caught the
     first time this was written with `var(--slot)`. */
  background: #151a24; color: var(--text);
  border: 1px solid #0a0d13; border-radius: 3px;
  font: inherit; font-size: 11px; padding: 0 2px; cursor: pointer;
}
.lureb .luresel:focus-visible { outline: 1px solid var(--torch); outline-offset: 1px; }
.lureb.off { opacity: .45; }
.lureb.off .luresel { cursor: default; }
@media (max-width: 900px) { .lureb .sl { display: none; } }

/* ── AND THE GAP A KITE HOLDS ────────────────────────────────────────────────
   > *"The movement option "Kite" should have an option for the player to set
   > up the range they are trying to maintain"*

   ONE LINE UNDER THE STRIP, and only while Kite is the stance -- see paintKite
   in app.mjs, which sets `hidden`. It is deliberately quieter than the four
   buttons above it: those are the decision and this is a parameter of one of
   them, so it reads as a detail of the lit button rather than as a fifth
   control competing with it.

   THE SENTENCE IS THE POINT OF THE ROW. A number on its own does not tell you
   that you have just walked out of your own reach; `.warn` is what says so, and
   it borrows `--bad` because it is the same "this is costing you something"
   colour the rest of the HUD uses. */
/* The strip is a flex ROW, so the buttons and this share one column of it --
   see `stanceCol` in app.mjs. Without that they sit side by side and this
   row's `margin-top` does nothing at all. */
/* `.stancecol` is gone (2026-09-04). It existed to stack the gap control under
   the four buttons; the box is on the Kite button now and the sentence that sat
   under the strip is gone, so there is nothing left to stack -- and a row whose
   height changed depending on which stance was pressed was the thing being
   fixed. */
/* ── THE RANGE BOX LIVES ON THE KITE BUTTON ─────────────────────────────────
   > *"Add the Field that the player can type the range to 'hold at' during
   > kite, on the kite button and let's rename the button to: Kite at <input
   > field> <range icon>"*

   It was a row of its own under the strip, which is why `.kiterow` was a block
   with a label in small capitals -- it had to name itself. On the button it IS
   the button's label (`Kite at [ 34 ] <range>`), so the label takes the same
   type as the other three and the capitals go.

   `display: contents` rather than a flex box of its own: the three parts then
   sit in the BUTTON's gap, in line with the glyph beside them, instead of in a
   second box with its own spacing inside the first. */
.kiterow { display: contents; }
.kitelbl { color: inherit; }
/* NARROW ON PURPOSE. Three digits is the whole range of legal values, and a box
   wide enough for more invites a number the server will refuse. */
.kitenum {
  width: 40px; padding: 1px 4px; text-align: right;
  background: #0e131c; border: 1px solid var(--line); border-radius: 3px;
  color: var(--torch); font: inherit; font-size: 11px;
}
.kitenum:focus { outline: none; border-color: var(--torch-d);
                 box-shadow: 0 0 6px rgba(240,165,60,.25); }
/* The spinners are 20px of chrome on a 52px box, and the value is typed or
   dragged from the sheet rather than nudged one unit at a time. */
.kitenum::-webkit-inner-spin-button, .kitenum::-webkit-outer-spin-button {
  -webkit-appearance: none; margin: 0;
}
.kitenum { -moz-appearance: textfield; appearance: textfield; }
/* ── PAST YOUR OWN REACH, SAID ON THE BOX ───────────────────────────────────
   > *"can we remove the sayings: 'your own range - <value> units', and stop
   > moving those buttons around when select kite?"*

   It was a line under the strip, drawn only while Kite was the stance -- so
   picking Kite grew the strip and moved the four buttons, and every other
   stance left the space empty. A row of controls that changes height depending
   on which of them is pressed is a row you cannot aim at.

   The fact it carried is worth keeping and does not need a line: a gap wider
   than your own Range is allowed (standing at spell range is the reason to want
   one) and it costs you the auto-attack. That is a property of the NUMBER, so
   it is drawn on the box the number is in. The explanation is in the tooltip,
   where a paragraph belongs. */
.kitenum.far { border-color: var(--bad); color: var(--bad); }

/* ── HEALTH AND MANA ─────────────────────────────────────────────────────────
   Two long bars with the number in the middle of them, directly under the
   buttons you would press about them. They used to be an 18px sliver each,
   inside a bordered plate, floating over the top-left corner of the fight --
   opposite an identical plate for the creature. Two dashboards over a picture
   is the cramped half of "clunky and cramped", and the one number you watch
   most was the smallest thing on the screen.

   FRAMED, NOT FLAT: a dark rim, a lip of light on the inside top edge, and the
   fill sitting in the well. That frame is the whole of the reference's look and
   it costs three declarations. */
.hudstack { display: flex; flex-direction: column; gap: 3px; }
.hudbar {
  position: relative; width: min(430px, 46vw); height: 19px;
  background: #0a0d13; border: 1px solid #05070b; border-radius: 3px;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.10),
              inset 0 -1px 0 rgba(0,0,0,.8), 0 1px 2px rgba(0,0,0,.55);
  overflow: hidden;
}
.hudbar i { position: absolute; inset: 1px auto 1px 1px; display: block;
            border-radius: 2px; transition: width .12s linear; }
.hudbar b {
  position: relative; display: block; text-align: center; font-weight: 600;
  font-size: 12px; line-height: 19px; color: #fff;
  text-shadow: 0 1px 2px rgba(0,0,0,.95), 0 0 3px rgba(0,0,0,.9);
  font-variant-numeric: tabular-nums;
}
/* THE LITERALS ARE THE FALLBACK, not the colour. simBar() sets `--f0`/`--f1`
   from how hurt you are -- green above 80%, yellow at 50%, red below 20%, with a
   ramp between -- and these two are what draws if nobody ever sets them. */
.hudbar.hp i { background: linear-gradient(var(--f0, #5fd06a), var(--f1, #2f8a38)); }
.hudbar.mp i { background: linear-gradient(#5b8fe0, #2a4f9e); }
/* ── AND NOTHING BUT YOUR OWN TWO BARS LIVES UNDER THE BAR ───────────────────
   > *"when a new entity enters the combat, it should have it's own health bar
   > above it, with it's stats, like a normal creatures, no health bars under
   > the player."*

   There was a `.petplate` here and a `.packplate` under it -- your summon's
   health and the creature's pack, stacked below your Health and Mana -- and
   both are gone. They were written when the arena drew exactly two figures and
   a third body had nowhere else to be; it draws five now, so a body's health
   belongs on the body. See `.pl-simE` in sim.css.

   THE RULE THEY BROKE IS WHY THEY WENT and not only the tidiness: nothing may
   appear or vanish in this strip while a hand is reading the Health bar, and
   three plates arriving and leaving every thirty seconds were the one thing
   here that did. */

/* ── THE WALK HOME, PINNED TO THE FRAME ──────────────────────────────────────
   It was a 44x41 end cap on the end of the Health bar. Two things were wrong
   with that, and the second is the one that decided it:

   IT IS THE BIGGEST DECISION ON THE SCREEN and it was the smallest control on
   it -- a button the size of a spell slot for the only thing here that ends the
   hunt.

   AND IT MOVED THE BARS. Inside the flex row it changed the row's width when it
   appeared, again when its label became a countdown, and a third time when the
   countdown dropped from two digits to one -- each of which slid the Health bar
   sideways under a hand that was reading it. Out here, against the frame, the
   bars own the exact centre of the screen for the whole fight and nothing this
   button does can reach them. DO NOT PUT IT BACK IN THE ROW. */
/* AND IT STEPS INSIDE AN OPEN WINDOW, the same way the panel does: the bag on
   the right is a window you keep open while you fight, and a walk-home button
   underneath it is a walk-home button you cannot press. */
.walkwrap { position: absolute; bottom: 14px; z-index: 7;
            right: calc(16px + var(--winR)); }
.walkwrap[hidden] { display: none; }
.walkhome {
  display: flex; align-items: center; gap: 10px;
  height: 85px; padding: 0 14px; cursor: pointer;
  background: linear-gradient(#3a2f1c, #20180d);
  border: 2px solid var(--torch-d); border-radius: 4px;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.16),
              0 0 14px rgba(240,165,60,.22), 0 3px 0 var(--edge);
  color: var(--torch); font: inherit; text-align: left;
}
.walkhome:hover:not(:disabled) { border-color: var(--torch);
                                 box-shadow: inset 0 1px 0 rgba(255,255,255,.20),
                                             0 0 20px rgba(240,165,60,.38),
                                             0 3px 0 var(--edge); }
.walkhome .g { font-size: 30px; line-height: 1; flex: none; }
.walkhome .lbl { font: 400 8px var(--loud); letter-spacing: .02em; color: #ffe6bd;
                 text-transform: uppercase; }
.walkhome .n { font: 400 34px var(--display); line-height: 1.05; color: var(--torch);
               font-variant-numeric: tabular-nums; }
.walkhome .n:empty { display: none; }
.walkhome .note { font: 400 9px var(--label); letter-spacing: .04em; color: var(--brow);
                  text-transform: uppercase; }
/* ARMED. Brighter, and the countdown appears -- the button stays the same size
   because the number's row was already there, empty. */
.walkhome.going { background: linear-gradient(#4a3a1e, #241c10);
                  border-color: var(--torch);
                  box-shadow: inset 0 1px 0 rgba(255,255,255,.16),
                              0 0 18px rgba(240,165,60,.35), 0 3px 0 var(--edge); }
/* WALKING HOME IS NOT "NOTHING IS HAPPENING". A disabled button at 35% over a
   sharp background is invisible, and this one is counting down the ten seconds
   the fight is still running for -- the one moment you most want to read it. */
.walkhome:disabled { opacity: 1; cursor: default; }

/* ── the meal, and its clock ────────────────────────────────────────────────── */
/* Its own margin on top of the column's gap was 10px of nothing between two
   things that belong together: what you ate is part of what is true about you
   right now, which is what the tiles above it are. */
.buffbox { margin: 0; }
.buffbox.hidden { display: none; }
.buffrow { display: flex; align-items: center; gap: 8px;
           padding: 4px 8px; background: var(--panel-2);
           border: 1px solid var(--line); border-left: 2px solid #5fbf6f;
           border-radius: 4px; font-size: 12px; }
.buffrow .bn { color: var(--text); }
.buffrow .bw { color: var(--dim); font-size: 11px; }
.buffrow .bleft { color: #5fbf6f; font-variant-numeric: tabular-nums;
                  font-weight: 700; flex: none; }
/* THE LAST HALF-MINUTE IS THE ONE YOU MIGHT ACT ON, so it stops looking like
   ordinary furniture. No animation: a thing that blinks in the corner of a
   panel you are reading is a thing you turn off. */
.buffrow.ending { border-left-color: var(--torch); }
.buffrow.ending .bleft { color: var(--torch); }

/* ── consumables on the action bar ──────────────────────────────────────────
   The same slot as a spell, because at the moment it matters they are the same
   decision. What differs is the count: a potion you have run out of is not an
   error -- the bar is a plan -- but it must not look pressable. */
.abslot .abqty { position: absolute; right: 3px; bottom: 1px; font-size: 11px;
                 font-weight: 700; color: var(--text);
                 text-shadow: 0 1px 3px #000, 0 0 3px #000;
                 font-variant-numeric: tabular-nums; pointer-events: none; }
.abslot.none .sprite { filter: grayscale(1) brightness(.5); }
.abslot.none .abqty { color: var(--muted); }


/* ── the account strip, at the top of the character panel ────────────────────
   Sound and character-switching moved here off the town bar: neither is about
   the town, and both are about the session you are in. */
/* IN THE SHEET WINDOW NOW. The sound moved to the rail, where it is reachable
   with a panel open; what is left is the two things you do to a CHARACTER, and
   they wrap rather than squeezing each other into two ragged lines. */


/* ── locked stacks ───────────────────────────────────────────────────────────
   The padlock says Sell loot will not take THIS stack. It is about the stack, it
   is small, and there is nowhere else to put it.

   THE LOOT FILTER USED TO MARK THE TILE TOO -- a dimmed sprite and a dashed
   border -- and that was wrong in two ways at once. An item you are carrying
   should look like an item you are carrying; and a bag with a third of its tiles
   faded reads as a rendering fault rather than as a setting somebody chose. The
   filter is a rule about the NEXT one you walk past, not a property of this one,
   so it belongs on the card and not on the picture. */
.slot .lockmark { position: absolute; right: 2px; top: 0; font-size: 10px;
                  line-height: 1.2; pointer-events: none;
                  text-shadow: 0 1px 3px #000; }

/* ── the sell confirmation ───────────────────────────────────────────────────
   The same popup the item menu uses. It exists so that emptying a bag is one
   decision AND fully legible before you make it. */
.selllist { max-height: 260px; overflow-y: auto; margin: 2px 0 4px;
            scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
.sellrow { display: flex; align-items: center; gap: 7px; padding: 2px 8px;
           font-size: 12px; color: var(--text); }
.sellrow .grow { flex: 1; min-width: 0; overflow: hidden;
                 text-overflow: ellipsis; white-space: nowrap; }
.sellrow .n { font-variant-numeric: tabular-nums; color: var(--torch); }
/* Sell loot takes the whole bag now, so this list has to read as a warning and
   not only as a receipt. A tail among forty tails needs no label; a sword or an
   unread spellbook in that column is the thing your eye should catch before the
   button is pressed. */
.selltag { flex: none; font-size: 9.5px; letter-spacing: .06em;
           text-transform: uppercase; color: var(--torch);
           border: 1px solid var(--torch-d); border-radius: 3px;
           padding: 0 4px; line-height: 14px; }
/* Spared, and shown rather than silently missing: "why is my Ironsword not in
   this list" is a question the list should answer. */
.sellrow.off { color: var(--muted); }
.sellrow.off .n { color: var(--muted); }
.sellfoot { display: flex; align-items: center; gap: 6px; padding: 6px 8px;
            border-top: 1px solid var(--line); margin-bottom: 3px;
            font-size: 13px; }
.sellfoot .grow { flex: 1; color: var(--dim); }
.sellfoot b { color: var(--torch); font-variant-numeric: tabular-nums; }
.imrow.go { color: var(--torch); }

/* ── THE TWO VERBS SIT IN THE FRAME, NOT IN THE SCROLL ───────────────────────
   Artboard 2a, `Redesign ideas/split-plinth-handoff.html`.

   They were the last two children of .winbody, which made them part of the
   BAG: a body button in a rounded 6px box, moving down the window every time a
   stack was added and scrolling off the bottom on a full one. A window's verbs
   belong to the WINDOW. This is a plinth at the foot of the frame, cut from the
   same metal as .winbar at the top of it, and it does not move. */
.winfoot {
  flex: none; display: flex;
  background: linear-gradient(#39424f, #1b2130);
  border-top: 1px solid var(--edge);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.10);
}
.winfoot button {
  flex: 1; display: flex; align-items: center; justify-content: center; gap: 7px;
  height: 34px; padding: 0 6px; cursor: pointer;
  background: none; border: 0; color: var(--brow);
  font: 400 9px var(--label); letter-spacing: .09em; text-transform: uppercase;
  white-space: nowrap;
}
/* ONE GROOVE, NOT TWO BUTTONS. A dark edge with a lit pixel beside it is the
   bevel the rest of the chrome is drawn with; a border round each half would
   put two boxes back on the ledge, which is what this replaces. */
.winfoot button + button {
  border-left: 1px solid var(--edge);
  box-shadow: inset 1px 0 0 rgba(255,255,255,.06);
}
.winfoot button:hover { background: rgba(214,221,232,.08); color: var(--title); }
/* EQUAL SIZE, DIFFERENT TEMPERATURE. Emptying the bag is the verb and the
   filter is a setting, and that is the whole of the hierarchy here: same plate,
   same width, one of them lit. */
.winfoot .sell { color: var(--lit); }
.winfoot .sell:hover { background: rgba(240,165,60,.14); color: #ffd9a0; }
.winfoot .fico { width: 14px; height: 14px; flex: none;
                 background: url(/assets/ui/gold.png) center/contain no-repeat; }
.winfoot .fdia { font-size: 9px; color: var(--dim); flex: none; }
.winfoot .swords { display: flex; flex-direction: column;
                   align-items: flex-start; gap: 1px; }
.winfoot .swhy { font-size: 8px; letter-spacing: .06em; color: var(--faint); }
/* ── DIMMED, WITH THE REASON ON IT ───────────────────────────────────────────
   Selling is a town door, and town doors in this game DIM rather than disable --
   see .setswap.off. The button keeps its click, because pressing it is how you
   find out why; the difference is that now it says so first. */
.winfoot .sell.off { color: var(--faint); }
.winfoot .sell.off .fico { filter: grayscale(1); opacity: .4; }
.winfoot .sell.off:hover { background: rgba(214,221,232,.05); color: var(--dim); }

/* ── the loot filter panel ───────────────────────────────────────────────── */
/* ── THE TWO LOOT MODES ──────────────────────────────────────────────────────
   Named for what they are and dyed for what they do. Pressing the wrong one
   inverts the whole filter and the punishment arrives thirty kills later, with
   nothing on screen connecting the two -- so the two buttons are made as
   unalike as two buttons in a pair can be: the blacklist refuses, and is red;
   the whitelist admits, and is green. Only the SELECTED one is saturated. An
   unselected tab that shouted its colour would be a filter that looks switched
   on when it is not, which is the same failure the other way round. */
.lootmode { display: flex; gap: 8px; margin: 4px 0 14px; }
.lootmode .tab { display: flex; flex-direction: column; align-items: flex-start;
                 gap: 2px; padding: 7px 12px; text-align: left; }
.lootmode .lmname { font: 400 11px var(--label); letter-spacing: .08em;
                    text-transform: uppercase; }
.lootmode .lmsays { font-size: 10.5px; color: var(--dim); }
.lootmode .tab.lm-blacklist.on {
  background: linear-gradient(rgba(192,67,63,.22), rgba(58,20,18,.9));
  border-color: #8d3a37; color: #ff9f93; }
.lootmode .tab.lm-blacklist.on .lmsays { color: #d9887f; }
.lootmode .tab.lm-whitelist.on {
  background: linear-gradient(rgba(95,191,111,.20), rgba(19,45,25,.9));
  border-color: #3f7c47; color: #8fe08a; }
.lootmode .tab.lm-whitelist.on .lmsays { color: #7fb587; }
/* Off, they keep a hint of it -- enough to tell them apart before you read. */
.lootmode .tab.lm-blacklist:not(.on):hover { border-color: #8d3a37; color: #ff9f93; }
.lootmode .tab.lm-whitelist:not(.on):hover { border-color: #3f7c47; color: #8fe08a; }

.lootrow { display: flex; align-items: center; gap: 9px; padding: 6px 8px;
           background: var(--panel-2); border: 1px solid var(--line);
           border-radius: 4px; margin-bottom: 5px; }
/* ── THE ROW YOU JUST PUT THERE ──────────────────────────────────────────────
   Adding something from its own menu opens this list, and the list is useless
   as an answer unless it points at the row that changed -- a filter with
   fourteen entries and one new one looks exactly like a filter with fourteen.
   Torch edge and a fade, and it stops after a couple of seconds: a row still lit
   next time you open the panel is lit about nothing. */
.lootrow.justadded { border-color: var(--torch-d);
                     box-shadow: inset 3px 0 0 var(--torch);
                     animation: lootnew 2.6s ease-out forwards; }
@keyframes lootnew {
  0%   { background: rgba(240,165,60,.24); }
  100% { background: var(--panel-2); }
}
@media (prefers-reduced-motion: reduce) {
  .lootrow.justadded { animation: none; background: rgba(240,165,60,.14); }
}
.lootrow .grow { flex: 1; min-width: 0; }
.lootrow .tiers { display: flex; gap: 4px; flex-wrap: wrap; }
.lootrow .tchip { font-size: 11px; padding: 1px 7px; border-radius: 10px;
                  border: 1px solid var(--line); color: var(--dim);
                  background: var(--ink); cursor: pointer; }
.lootrow .tchip.on { color: var(--text); border-color: currentColor; }
.tchip.t-common.on { color: #d6dde8; }
.tchip.t-uncommon.on { color: #5fbf6f; }
.tchip.t-rare.on { color: #6fa8dc; }
.tchip.t-epic.on { color: #b07fd9; }
.tchip.t-legendary.on { color: var(--torch); }


/* ── what a ground is worth to you ───────────────────────────────────────────
   Two lines on every card you can actually walk into: the rates, and the
   verdict. The verdict is the one that decides things -- a place that pays well
   and kills you is not a place you leave running -- so it gets the colour. */
/* `.scout`, `.srate` and `.sverdict` are gone with the old card: the rates are
   two tiles on the plate and the verdict is the stamp across its head. The
   `.v-*` classes survive on the CARD, where they set `--vc`. */
.ground.v-sustainable { --vc: #5fbf6f; }
.ground.v-risky       { --vc: #f0a53c; }
.ground.v-deadly      { --vc: #d9707a; }
.ground.v-stalemate   { --vc: #7c8698; }
.ground.v-unknown     { --vc: #7c8698; }


/* ── a creature's sheet, on hover ────────────────────────────────────────────
   The same stat icons the attributes panel uses, so a creature's Armour and
   yours are the same picture and comparing them needs no reading. */
.tip .beaststats { display: grid; grid-template-columns: repeat(4, auto);
                   gap: 3px 12px; margin: 5px 0 2px; }
.tip .bs { display: flex; align-items: center; gap: 4px; }
.tip .bs .n { font-variant-numeric: tabular-nums; color: var(--text); }
/* ── AND THE SAME BAND ON ANY CARD ───────────────────────────────────────────
   `.beasthead` was named for the one card that had sections; the spell card has
   two ("Requires", "Formula") and wants the identical band. Added to the
   SELECTOR rather than copied into a second rule -- two rules that are meant to
   look the same are two rules that stop looking the same. */
.tip .beasthead, .tip .tiphead { margin-top: 7px; padding-top: 5px; font-size: 11px;
                  letter-spacing: .1em; text-transform: uppercase;
                  color: var(--dim); border-top: 1px solid var(--line); }

/* ── A SPELL'S CARD ──────────────────────────────────────────────────────────
   > *"let's show the requirements, formula, the calculated damage range, range
   > distance required, and costs. on that tooltip on a nice way with the
   > icons."*

   NOT `.beaststats`, and the difference is real rather than cosmetic: a
   creature's is a four-column stat SHEET read down, and a spell's is a short
   ROW of four facts -- damage, cost, cooldown, reach -- that should sit
   together and wrap when the window is narrow. Borrowing the grid would have
   spread four cells across a fixed four columns and left a two-cell spell
   looking like a table with holes in it. */
.tip .tipstats { display: flex; flex-wrap: wrap; gap: 4px 14px; margin: 6px 0 2px; }
.tip .ts { display: flex; align-items: baseline; gap: 5px; }
.tip .ts .n { font-variant-numeric: tabular-nums; color: var(--text); }
/* THE UNIT, QUIETLY. An icon is a fast reminder and not an explanation, and
   four bare numbers in a row is what the first render of this card actually
   looked like. The word is dim and small so the number still reads first. */
.tip .ts .u { color: var(--dim); font-size: 11px; }
.tip .ts .sprite, .tip .ts .elico { align-self: center; flex: none; }
/* WHAT IT HITS FOR IS THE HEADLINE and everything else is the price of it. */
.tip .scbig { margin: 7px 0 1px; }
.tip .scbig .n { font-size: 17px; color: var(--torch); }
.tip .scsub { display: flex; align-items: center; gap: 5px; }
.tip .scsub .elico { image-rendering: pixelated; }
/* One per line, because each is a comparison to read rather than a fact to
   glance at: what it asks for, and what you have. */
.tip .screq { margin-top: 2px; }
.tip .screq.short .n { color: var(--bad); }
.tip .scformula { color: var(--dim); font-size: 12px; margin-top: 3px; }
/* THE SERVER'S OWN SENTENCE for why it is not in the fight, and the same colour
   the bar draws an unusable slot in. */
.tip .scwhy { color: var(--bad); font-size: 12px; margin-top: 4px; }
/* 320 rather than 300 so the three costs sit on ONE line for the widest spell
   in the game -- measured, not guessed: at 300 a 3-digit cost and a 2-digit
   reach wrapped `60 range` onto a line of its own. */
.tip:has(.tipstats) { max-width: 320px; }
/* ── EVERY DROP, AND NONE OF IT SCROLLS ──────────────────────────────────────
   The list was cut at twelve with "and N more" under it, and the tail a cut
   throws away is always the RARE end -- the reason to read a drop table at all.

   A SCROLLBAR WOULD HAVE BEEN THE SAME BUG WEARING A CONTROL. This card follows
   the cursor and is dismissed by `mouseleave`, so anything below the fold is
   visible and permanently unreachable: you cannot move the mouse to the
   scrollbar without dismissing the thing you are trying to scroll.

   So a long table goes to TWO COLUMNS instead. The longest in the game is 24
   rows (the Thug), which is twelve a side -- shorter than the cut-down list
   was, with nothing left out. */
/* ── THE DROP TABLE IS ONE COLUMN, HOWEVER LONG ──────────────────────────────
   > *"when it's monster that drops a lot of loot, the table is being split into
   > 2 columns, leave it always as a one column."*

   A drop table is read in ORDER -- likeliest at the top, the line you came for
   at the bottom -- and splitting it put the rarest drop halfway down the right
   instead of at the end.

   The height that split was buying is paid back here instead: a 16px sprite
   rather than 18, a 1px gap rather than 2, and a line-height that does not
   round up. That is about 4px a row, which over a Thug's twenty-four rows is
   the difference between a card that fits in the window and one that does not
   -- and a hover card that does not fit is content you can see and never
   reach. check-client measures the tallest one against the viewport. */
.tip .beastloot { display: grid; gap: 1px; margin-top: 3px; line-height: 1.25; }
.tip .bl { display: flex; align-items: center; gap: 6px; }
.tip .bl .sprite { flex: none; }
.tip .bl .grow { flex: 1; min-width: 0; overflow: hidden;
                 text-overflow: ellipsis; white-space: nowrap; }
.tip .bl .pc { font-variant-numeric: tabular-nums; }
/* The wiki's own frequency words, coloured the way rarity is elsewhere. */
.tip .f-common { color: #d6dde8; }
.tip .f-semirare { color: #5fbf6f; }
.tip .f-rare { color: #6fa8dc; }
/* ── THE RAREST LINES ARE THE ONES YOU CAME FOR ──────────────────────────────
   Purple was borrowed from the gear-rarity scale, which is a different thing
   entirely -- loot frequency is not item power, and nothing connects them. The
   torch is this game's colour for "the thing on this screen worth your
   attention", and on a drop table that is the bottom of the list.

   TWO TIERS, because the wiki has two: `veryrare` runs 5% down to 2% and
   `ultrarare` is everything below, to 0.01%. The second is the brighter, which
   is the only ordering that can be read without a key. */
.tip .f-veryrare { color: var(--torch); font-weight: 700;
                   text-shadow: 0 0 6px rgba(240,165,60,.35); }
.tip .f-ultrarare { color: var(--lit); font-weight: 700;
                    text-shadow: 0 0 8px rgba(255,196,106,.55); }
/* A creature card is wider than a three-line item tooltip. */
.tip:has(.beaststats) { max-width: 340px; }


/* ── AN ITEM'S CARD ──────────────────────────────────────────────────────────
   Laid out like the game's own tooltip, in our type and with our icons: a
   centred head that says WHAT the thing is -- sprite, name, rarity, kind, and
   the line the game writes about it -- and then a stat list underneath, one per
   line, label on the left and value in its own right-aligned column.

   THE HEAD IS CENTRED AND THE STATS ARE NOT, deliberately. Centring is what
   makes the head read as a title block rather than as the first three rows of
   the list; and centred numbers cannot be compared down a column, which is the
   whole reason for putting them in one. */
.itemcard { display: block; min-width: 178px; }
.ic-head { display: grid; justify-items: center; text-align: center;
           gap: 1px; padding-bottom: 7px; margin-bottom: 6px;
           border-bottom: 1px solid var(--line); }
.ic-head .sprite { margin-bottom: 3px; }
/* ── A PIECE OF FURNITURE'S OWN PICTURE ─────────────────────────────────────
   The renderer's snapshot of the model, rendered at 72px and drawn at 56 so it
   is sharp on a normal screen and still crisp on a doubled one -- the same
   bargain the doll's portrait makes. It is a 3D render and NOT a sprite, so it
   does not wear `image-rendering: pixelated`: nearest-neighbour on a rendered
   image is the one thing that would make it look worse than the sprite it
   replaced. */
.ic-head .hsmodelart { width: 56px; height: 56px; margin-bottom: 3px;
                       image-rendering: auto; }
.ic-name { color: var(--text); font-weight: 700; font-size: 14px; }
.ic-tier { font-size: 11px; letter-spacing: .06em; text-transform: lowercase; }
.ic-kind { color: var(--muted); font-size: 11px; }
/* The game writes these in green and there is no reason to argue with it: it
   separates the sentence about the item from the numbers of the item at a
   glance, which is the only job the colour has here. */
.ic-desc { color: #7fc08a; font-size: 12px; margin-top: 3px; max-width: 210px; }

.ic-stats { display: grid; gap: 2px; }
.ic-row { display: grid; grid-template-columns: 17px 1fr auto;
          align-items: center; gap: 7px; }
.ic-ico { display: grid; place-items: center; width: 17px; }
/* THE DRAWN CLOCK TAKES THE LABEL'S COLOUR. It is an inline SVG on
   `currentColor` (see clockIcon in app.mjs) rather than a cell of the stat
   sheet -- there is no clock in the game's art -- and left to inherit it would
   sit in full --text and shout over the sprite icons beside it, which are the
   things you are actually meant to read. */
.ic-ico svg { color: var(--dim); display: block; }
.ic-lbl { color: var(--dim); font-size: 12px; }
.ic-val { color: var(--text); font-size: 13px; font-weight: 600;
          font-variant-numeric: tabular-nums; }
/* A bonus is green and a penalty is red, so an Ale Mug's Movespeed reads as a
   cost without being read. Weight carries neither: it is what the thing weighs,
   not something it does to you. */
/* ── WHAT A TRAIT IS GIVING THIS PIECE ───────────────────────────────────────
   Its own block under the item's own stats, with a rule above it, because the
   two are different kinds of fact: the rows above belong to the item and go
   with it when you sell it, and these stop the moment you untake the trait.
   Run together they would read as "this Cloth Vest has 12 Mana on it". */
.ic-traits { margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--line);
             display: grid; gap: 2px; }
.ic-tlbl { font: 400 8px var(--label); letter-spacing: .1em; text-transform: uppercase;
           color: var(--torch-d); margin-bottom: 2px; }
.ic-timg { width: 17px; height: 17px; object-fit: contain; image-rendering: pixelated; }
.ic-val.up { color: #6ec06a; }
.ic-val.down { color: #d9707a; }
.ic-val.dim { color: var(--dim); font-weight: 500; }
.tip:has(.itemcard) { max-width: 300px; }
.tip:has(.itemcard) .l { margin-top: 3px; }


/* ── the volume ──────────────────────────────────────────────────────────────
   A short slider beside the mute button. Styled rather than left native because
   a chrome-grey slider in an otherwise dark panel is the one control that looks
   like it belongs to a different program. */
/* ── ONE CONTROL FOR SOUND ───────────────────────────────────────────────────
   A speaker that mutes and a track that sets the level, in a single pill. The
   previous version was a musical note and a floating ball with nothing tying
   them together or saying what they were for -- two controls where there is
   one decision with two parts.

   THE TRACK CARRIES ITS OWN FILL, painted from `--fill` as the slider moves, so
   the level is legible at a glance rather than only from where the handle sits.
   NO THUMB AT ALL -- not even on hover. The fill IS the readout: a ball riding
   a 5px track is a second thing to look at that says what the fill already
   said, and it read as a stray dot rather than as a control. */
.sound { display: flex; align-items: center; gap: 0;
         background: var(--ink-2); border: 1px solid var(--line);
         border-radius: 999px; padding: 2px 10px 2px 2px; }
.sound .soundbtn { width: 24px; height: 24px; padding: 0; flex: none;
                   background: none; border: 0; border-radius: 50%;
                   display: grid; place-items: center; font-size: 13px;
                   line-height: 1; cursor: pointer; color: var(--text); }
.sound .soundbtn:hover { color: var(--torch); }
.sound.muted { opacity: .62; }
.sound.muted .soundbtn { color: var(--muted); }

/* ── THE SETTINGS BOX ────────────────────────────────────────────────────────
   Two settings, because this client has two: what language it speaks and how
   loud it is. It is the same floating panel the item menu and the autocast
   editor use, opened under the tab that owns it -- one kind of transient box on
   this screen, one way to dismiss it.

   THE SCROLL-WHEEL SHORTCUT DID NOT COME WITH IT. Over a speaker showing a
   percentage the wheel was at least guessable; over a button that says SETTINGS
   it would be a hidden gesture that changes something else. The slider is
   inside, and it has a picture. */
.itemmenu.setbox { min-width: 268px; padding: 8px; gap: 0; }
.setbox .imhead { font: 400 9px var(--label); letter-spacing: .12em;
                  text-transform: uppercase; color: var(--brow); }
/* The name of a group, not a heading -- quieter than the box's own title, and
   it is what separates language from sound without a rule between them. */
.setsec { margin: 10px 4px 6px; font: 400 8px var(--loud); letter-spacing: .12em;
          text-transform: uppercase; color: var(--dim); }
.setsec:first-of-type { margin-top: 6px; }

/* ── LANGUAGE ────────────────────────────────────────────────────────────────
   A flag and the language's own name for itself, side by side. NOT a dropdown:
   there are two, and a select box you have to open to see two options is one
   click more than the options are worth. Each language names itself in its own
   language, because "Portuguese" is the word for somebody who already reads
   English. */
.langrow { display: flex; gap: 6px; }
.langbtn {
  flex: 1; display: flex; align-items: center; gap: 8px; padding: 7px 8px;
  cursor: pointer; text-align: left;
  background: linear-gradient(var(--face), var(--face-d));
  border: 1px solid var(--edge); border-radius: 3px; color: var(--sub);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.07);
}
.langbtn:hover { color: var(--text); border-color: var(--torch-d); }
.langbtn.on { background: linear-gradient(#f0a53c22, #241c10);
              border-color: var(--torch-d); color: var(--torch); }
.langbtn .flag { width: 24px; height: 16px; flex: none; display: block;
                 border: 1px solid var(--edge); border-radius: 1px; }
.langbtn .lwords { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.langbtn .lname { font: 400 12px var(--display); letter-spacing: .06em;
                  text-transform: uppercase; color: inherit; }
.langbtn .lsub { font: 400 8px var(--label); letter-spacing: .06em;
                 color: var(--dim); text-transform: uppercase;
                 white-space: nowrap; }

/* The way out of this character. Full width and plain -- it is the only control
   in this box that takes the screen away, so it does not compete with the two
   above it for a glance. */
.setswap { display: flex; width: 100%; padding: 7px 9px; cursor: pointer;
           background: linear-gradient(var(--face), var(--face-d));
           border: 1px solid var(--edge); border-radius: 3px; color: var(--sub);
           box-shadow: inset 0 1px 0 rgba(255,255,255,.07);
           font: 400 10px var(--label); letter-spacing: .08em;
           text-transform: uppercase; text-align: left; }
.setswap:hover { color: var(--text); border-color: var(--torch-d); }
.setswap .grow { flex: 1; }
/* ── DIMMED WHILE YOU ARE OUT HUNTING ────────────────────────────────────────
   Switching character is a town door now, and town doors in this game DIM
   rather than disable: pressing one is how you find out why. So this is a
   colour and a reason, not `[disabled]` -- see the note on `switchCharacter`. */
.setswap.off { color: var(--faint); }
.setswap.off:hover { color: var(--dim); border-color: var(--line); }
.setswap .setwhy { color: var(--faint); font-size: 10px; text-transform: uppercase;
                   letter-spacing: .05em; align-self: center; }
.volrow { display: flex; align-items: center; gap: 9px; padding: 10px 4px 4px; }
.volpct { flex: none; min-width: 34px; text-align: right; color: var(--text);
          font: 400 15px var(--display); font-variant-numeric: tabular-nums; }

.vol { -webkit-appearance: none; appearance: none; width: 100%; flex: 1;
       height: 5px; border-radius: 3px; outline: none; cursor: pointer;
       background: linear-gradient(90deg,
                    var(--torch) 0 var(--fill, 70%),
                    var(--line) var(--fill, 70%) 100%); }
/* Muted, the fill goes grey -- so a slider sitting at 70% with nothing coming
   out of it says why rather than looking broken. */
.setbox .ruleswitch input:not(:checked) ~ .say { color: var(--dim); }
#settingsPop:has(.ruleswitch input:not(:checked)) .vol {
  background: linear-gradient(90deg, var(--dim) 0 var(--fill, 70%),
                                     var(--line) var(--fill, 70%) 100%); }
/* NO HANDLE AT ALL.  The thumb is the only part of a range input that cannot be
   removed by declaration -- unstyled it draws the platform's own ball -- so it
   is declared away: full track height, zero width, no paint. The input still
   takes clicks and drags across its whole width, and the FILL is the readout. */
.vol::-webkit-slider-thumb { -webkit-appearance: none; appearance: none;
       width: 0; height: 5px; border: 0; background: none; cursor: pointer; }
.vol::-moz-range-thumb { width: 0; height: 5px; border: 0; background: none;
       cursor: pointer; }
.vol:focus-visible { box-shadow: 0 0 0 2px var(--torch-d); }

/* The class emblem in the character's subtitle. */


/* ── the class multiplier ────────────────────────────────────────────────────
   Context for the number beside it, not a number of its own -- so it is small,
   dim, and only present where the class actually changes something. */
/* ── AND THEY LINE UP ────────────────────────────────────────────────────
   > *"The class multipliers that should be vertically aligned are the ones
   > that appears right after the stat name"*

   They rode directly after the label, so thirteen of them landed at thirteen
   different x's -- ×2.0 after "Mana" at 239 and ×0.75 after "Health Regen" at
   292 -- and a column of numbers that is not a column is a column you have to
   read one row at a time. `margin-left: auto` in the flex row pushes every one
   of them to the end of the name cell, which is a fixed 205px: they line up
   with each other and land just before the bars, without a width written down
   anywhere that a longer label or another language could break. */
.statrow .mult { font-size: 10px; font-variant-numeric: tabular-nums;
                 color: var(--muted); letter-spacing: .02em;
                 margin-left: auto; padding-left: 10px; flex: none; }
.statrow .mult.up { color: #4f7a58; }
.statrow .mult.down { color: #7a5155; }

/* ── A CREATURE DRAWN FROM ITS OWN PICTURE ───────────────────────────────────
   Eight creatures have no cell in the sprite sheet and are drawn from the
   wiki's separate per-monster PNGs instead (see mobSprite). They arrive at
   whatever proportions the artist used, so they are CONTAINED rather than
   stretched: the box is the same square the sheet would have filled, the
   picture fits inside it, and a wall of ground cards keeps its grid. */
.sprite.mob.art { object-fit: contain; image-rendering: pixelated;
                  background: transparent; }
/* ── AND A RENDERED ONE IS NOT PIXEL ART ─────────────────────────────────────
   > *"let's update the monster pictures in there, to actually show a snapshot
   > of their idle pose"*

   A ground card's creature comes out of the arena's own renderer at twice the
   size it is shown at -- smooth edges, not a sprite sheet -- so `pixelated`
   would put a staircase on every silhouette. Same rule, same word, as the
   portrait medallion and the profile head above. */
.sprite.mob.art.rendered { image-rendering: auto; }

/* ── the auction house ───────────────────────────────────────────────────────
   Artboard "Auction House Reworked", from `Redesign ideas/Auction house`.

   THE OLD PANEL HAD NO SHELVES. Its whole navigation was a search box over a
   datalist: to find out whether anybody was selling a Garnet Ring you had to
   already know the phrase, spell it the item table's way, and type it. There
   was no browsing, no "what is cheap today", and no way at all to learn that
   the thing you wanted has never once been listed.

   So the room is THREE REGIONS and each scrolls on its own -- which is why the
   auction is in BARE_VIEWS and the card's body stops being one scrolling page.
   The rail on the left is the whole item table; the board on the right is the
   two sides of a price; the strip between them is the one sort that governs
   both. Nothing here moves when something else scrolls. */
.aucroom { position: absolute; inset: 0; display: grid;
           grid-template-columns: 262px minmax(0, 1fr);
           grid-template-rows: minmax(0, 1fr);
           gap: 12px; padding: 12px 14px 14px; }

/* ── the item rail ───────────────────────────────────────────────────────────
   ALL 548 ROWS, with the ones somebody has posted floated to the top. The grey
   four hundred are not clutter: "nobody is selling this" is a real answer and
   the only place to get it is a list that contains the item. */
.aucrail { display: flex; flex-direction: column; min-width: 0;
           background: var(--panel); border: 1px solid var(--line);
           border-radius: 8px; overflow: hidden; }
.aucrailhead { flex: none; padding: 10px 11px;
               border-bottom: 1px solid var(--line);
               background: linear-gradient(var(--panel-2), var(--panel)); }
.aucrailhead .arh { display: flex; align-items: center;
                    justify-content: space-between; margin-bottom: 8px; }
.aucrailhead .lbl { font: 400 9px var(--label); letter-spacing: .1em;
                    color: var(--sub); text-transform: uppercase; }
.aucrailhead .arh .n { font-size: 11px; color: var(--dim);
                       font-variant-numeric: tabular-nums; }
/* A box with a drawn glyph inside it: the sprite sheets have no magnifier and
   no head, so both are inline SVG on `currentColor` -- the same call the item
   card's clock makes. */
.aucfind { position: relative; }
.aucfind svg { position: absolute; left: 9px; top: 50%;
               transform: translateY(-50%); color: var(--faint);
               pointer-events: none; }
.aucfind input { width: 100%; background: var(--well);
                 border: 1px solid var(--line); border-radius: 5px;
                 color: var(--text); padding: 6px 9px 6px 27px; font-size: 13px; }
.aucfind input:focus { outline: 0; border-color: var(--torch-d); }
.arline { display: flex; gap: 6px; margin-top: 7px; }
.arline select { flex: 1; min-width: 0; background: var(--well);
                 border: 1px solid var(--line); border-radius: 5px;
                 color: var(--text); padding: 5px 6px; font-size: 12px; }
.aucrails { flex: 1; min-height: 0; overflow: auto; padding: 6px;
            scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
.aucrails::-webkit-scrollbar { width: 8px; }
.aucrails::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px; }

.aucrailrow { display: flex; align-items: center; gap: 9px;
              padding: 5px 8px; margin-bottom: 3px; border-radius: 5px;
              cursor: pointer; border: 1px solid transparent; }
.aucrailrow:hover { border-color: var(--torch-d); }
.aucrailrow.on { background: var(--panel-2); border-color: var(--torch-d); }
.aucrailrow.all { padding: 7px 8px; margin-bottom: 4px; }
.aucrailrow.all.on .nm .n { color: var(--torch); }
.aucrailrow .ico { width: 30px; height: 30px; flex: none; display: grid;
                   place-items: center; background: var(--well);
                   border: 1px solid var(--line); border-radius: 4px;
                   overflow: hidden; }
.allmark { font: 400 13px var(--display); letter-spacing: .06em;
           color: var(--torch-d); }
.aucrailrow .nm { min-width: 0; flex: 1; }
.aucrailrow .nm .n { display: block; font: 400 14px var(--display);
                     letter-spacing: .02em; white-space: nowrap;
                     overflow: hidden; text-overflow: ellipsis;
                     color: var(--text); }
.aucrailrow .nm .sub { display: block; font-size: 10.5px; color: var(--sub); }
/* AN ITEM NOBODY HAS POSTED IS STILL A ROW, and it says so by being quieter
   rather than by being absent -- see the note at the top. */
.aucrailrow.quiet .nm .n { color: #6b7488; }
.aucrailrow.quiet .nm .sub { color: #4f5867; }
.aucrailrow .pr { flex: none; font: 400 14px var(--display);
                  color: var(--torch); font-variant-numeric: tabular-nums; }
.aucrailrow .pr.none { color: var(--faint); font-size: 11px;
                       font-family: inherit; }
.aucrailmore { padding: 10px 8px; text-align: center; font-size: 11px;
               color: var(--faint); }

/* ── the board ─────────────────────────────────────────────────────────────── */
.aucmain { display: flex; flex-direction: column; min-width: 0; min-height: 0;
           overflow: hidden; gap: 9px; }

/* ── THE COMPARISON, DONE ────────────────────────────────────────────────────
   Cheapest and best offer are the top of the left column and the top of the
   right one. Side by side up here, the answer to "is this worth selling" does
   not depend on the reader scanning two lists and holding one figure in their
   head. */
.auchero { flex: none; display: flex; align-items: center; gap: 12px;
           padding: 9px 12px; background: var(--panel);
           border: 1px solid var(--line); border-radius: 8px; }
.auchero .ico { width: 44px; height: 44px; flex: none; display: grid;
                place-items: center; background: var(--well);
                border: 1px solid var(--line); border-radius: 5px;
                overflow: hidden; }
.auchero .ico .allmark { font-size: 15px; }
.auchero .nm { flex: 1; min-width: 0; }
.auchero .t { display: flex; align-items: baseline; gap: 8px; }
.auchero .t b { font: 400 22px var(--display); letter-spacing: .03em;
                color: var(--title); white-space: nowrap; overflow: hidden;
                text-overflow: ellipsis; }
.auchero .t .tier { flex: none; font: 400 8px var(--label);
                    letter-spacing: .08em; text-transform: uppercase; }
.auchero .sub { display: block; font-size: 11.5px; color: var(--dim); }
.auchint { flex: none; font-size: 11.5px; color: var(--faint);
           text-align: right; max-width: 210px; }
.aucfacts { flex: none; display: flex; align-items: stretch;
            border: 1px solid var(--line); border-radius: 6px;
            overflow: hidden; background: var(--ink-2); }
.aucfacts .f { padding: 5px 12px; text-align: right; }
.aucfacts .f + .f { border-left: 1px solid var(--line); }
.aucfacts .l { display: block; font: 400 8px var(--label); letter-spacing: .06em;
               color: var(--sub); text-transform: uppercase; }
.aucfacts .v { display: block; font: 400 18px var(--display);
               font-variant-numeric: tabular-nums; }
/* TORCH IS WHAT YOU PAY AND GREEN IS WHAT YOU ARE PAID, here and in both
   columns and on both buttons. One rule, so a colour never has to be read. */
.aucfacts .v.sell { color: var(--torch); }
.aucfacts .v.buy { color: var(--good); }

.aucfilters { flex: none; display: flex; align-items: center; gap: 10px;
              flex-wrap: wrap; }
.aucfilters .lbl { font: 400 8px var(--label); letter-spacing: .09em;
                   color: var(--faint); text-transform: uppercase; }
.aucfilters .grow { flex: 1; }
.aucsorts { display: flex; gap: 5px; }
.aucchip { padding: 5px 10px; cursor: pointer; border-radius: 4px;
           font-size: 11.5px; background: var(--ink-2);
           border: 1px solid var(--line); color: var(--sub); }
.aucchip:hover { border-color: var(--torch-d); }
.aucchip.on { background: rgba(240,165,60,.14); border-color: var(--torch-d);
              color: var(--torch); }
.aucchip.big { padding: 8px 14px; font: 400 9px var(--label);
               letter-spacing: .08em; text-transform: uppercase; }
.aucfind.who { flex: none; width: 136px; }
.aucfind.who input { padding: 5px 8px 5px 25px; font-size: 12px; }
.aucrarity { flex: none; width: 112px; background: var(--well);
             border: 1px solid var(--line); border-radius: 5px;
             color: var(--text); padding: 5px 6px; font-size: 12px; }

/* Both sides at once. Somebody wanting one and somebody selling one are the two
   halves of a price, and showing them in sequence would mean scrolling between
   the two numbers you are comparing. */
.auccols { flex: 1; min-height: 0; display: grid;
           grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
           grid-template-rows: minmax(0, 1fr); gap: 10px; }
/* ── AND ON A NARROW WINDOW THEY STACK ───────────────────────────────────────
   Side by side is the point, but each column carries a 36px sprite, a name, a
   price and a button, and below about 760px of card the name has nowhere left
   to go. One over the other keeps every row readable; the two numbers are then
   a scroll apart, which is worse than side by side and much better than four
   ellipsised words.

   THE RAIL NARROWS AND DOES NOT LEAVE. Folding it away would save the most room
   and would take the panel's only navigation with it -- no way to pick an item,
   so no prices, no posting, and a search box with nothing to search. A layout
   that drops a control is a layout that drops a feature. */
@media (max-width: 860px) {
  .aucroom { grid-template-columns: 196px minmax(0, 1fr); gap: 8px; padding: 10px; }
  .aucrailrow .pr { font-size: 12px; }
  .auccols { grid-template-columns: minmax(0, 1fr);
             grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); }
}
.auccol { display: flex; flex-direction: column; min-width: 0; min-height: 0;
          background: var(--panel); border: 1px solid var(--line);
          border-radius: 8px; overflow: hidden; }
.acol { flex: none; display: flex; align-items: center; gap: 8px;
        padding: 7px 11px; border-bottom: 1px solid var(--line);
        background: var(--ink-2); }
.acol .dot { width: 5px; height: 5px; border-radius: 50%; }
.acol .t { font: 400 9px var(--label); letter-spacing: .1em;
           text-transform: uppercase; }
.acol .n { font-size: 11px; color: var(--dim);
           font-variant-numeric: tabular-nums; }
.acol .grow { flex: 1; }
.acol .why { font-size: 10.5px; color: var(--faint); }
.auccol.sell .acol .dot { background: var(--torch); }
.auccol.sell .acol .t { color: var(--torch); }
.auccol.buy .acol .dot { background: var(--good); }
.auccol.buy .acol .t { color: var(--good); }
.acolbody { flex: 1; min-height: 0; overflow: auto;
            scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
.acolbody::-webkit-scrollbar { width: 8px; }
.acolbody::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px; }
.aucempty { padding: 38px 10px; text-align: center; color: var(--dim);
            font-size: 12.5px; }

.aucrow { display: grid; grid-template-columns: 36px minmax(0,1fr) 78px 58px;
          align-items: center; gap: 9px; padding: 7px 10px;
          border-bottom: 1px solid rgba(42,50,66,.55); }
.auccol.sell .aucrow:hover { background: rgba(240,165,60,.05); }
.auccol.buy .aucrow:hover { background: rgba(127,209,140,.05); }
/* Your own offers are marked in place rather than moved or hidden: "am I the
   best price" is the question you are actually asking when you look at a board
   you have posted to, and it cannot be answered from a list your own row has
   been taken out of. */
.aucrow.mine { background: rgba(240,165,60,.06); }
.auccol.sell .aucrow.mine { box-shadow: inset 2px 0 0 var(--torch); }
.auccol.buy .aucrow.mine { box-shadow: inset 2px 0 0 var(--good); }
.aucrow .ico { width: 34px; height: 34px; display: grid; place-items: center;
               background: var(--well); border: 1px solid var(--line);
               border-radius: 4px; overflow: hidden; }
.aucrow .nm { min-width: 0; }
.aucrow .nm .t { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
.aucrow .nm .t b { font: 400 16px var(--display); letter-spacing: .02em;
                   color: var(--title); white-space: nowrap; overflow: hidden;
                   text-overflow: ellipsis; }
.aucrow .nm .tier { flex: none; font: 400 8px var(--label);
                    letter-spacing: .06em; text-transform: uppercase; }
.aucrow .nm .sub { display: block; font-size: 10.5px; white-space: nowrap;
                   overflow: hidden; text-overflow: ellipsis; color: var(--sub); }
.aucrow .nm .sub .who { color: var(--brow); }
.aucrow .nm .sub .you { color: var(--torch); }
.aucrow .pr { text-align: right; }
.aucrow .pr .v { display: block; font: 400 18px var(--display);
                 font-variant-numeric: tabular-nums; line-height: 1.05; }
.auccol.sell .aucrow .pr .v { color: var(--torch); }
.auccol.buy .aucrow .pr .v { color: var(--good); }
.aucrow .pr .all { display: block; font-size: 10px; color: var(--dim);
                   font-variant-numeric: tabular-nums; }
.aucgo { width: 100%; padding: 5px 0; cursor: pointer; border-radius: 4px;
         background: transparent; font: 400 9px var(--label);
         letter-spacing: .06em; text-transform: uppercase; }
.aucgo.sell { border: 1px solid var(--torch-d); color: var(--torch); }
.aucgo.sell:hover { background: var(--torch); color: #1a1205; }
.aucgo.buy { border: 1px solid #4f7a58; color: var(--good); }
.aucgo.buy:hover { background: var(--good); color: #1a1205; }
.aucgo.cancel { border: 1px solid var(--line); color: var(--text); }
.aucgo.cancel:hover { background: var(--line); }

/* ── posting ─────────────────────────────────────────────────────────────────
   Under the board, not over it: you write a price after reading the ones
   already there. */
.aucpost { flex: none; padding: 11px 12px; background: var(--panel-2);
           border: 1px solid var(--torch-d); border-radius: 8px; }
.aucpost .aph { display: flex; align-items: center; gap: 10px; margin-bottom: 9px; }
.aucpost .aptitle { font: 400 18px var(--display); letter-spacing: .03em;
                    color: var(--title); }
.aucpost .aph .grow { flex: 1; }
/* ── THE TWO SIDES ARE THE TWO COLUMNS' OWN COLOURS ──────────────────────────
   A dropdown reading "I am selling" is a line of prose you have to parse. Two
   tabs in torch and green are the two columns above, and the lit one is the
   column your offer is about to land in. */
.apsides { display: flex; gap: 0; border: 1px solid var(--line);
           border-radius: 6px; overflow: hidden; }
.apside { padding: 6px 13px; cursor: pointer; background: var(--ink-2);
          border: 0; color: var(--sub); font-size: 12.5px; }
.apside + .apside { border-left: 1px solid var(--line); }
.apside:hover { color: var(--text); }
/* `s-sell` / `s-buy` rather than `sell` / `buy`: `button.buy` is a global (the
   merchant list's Buy button, uppercase and bordered) and a bare class here
   picked it up, so the two tabs rendered as two different kinds of control. */
.apside.s-sell.on { background: rgba(240,165,60,.16); color: var(--torch); }
.apside.s-buy.on { background: rgba(127,209,140,.16); color: var(--good); }
.apline { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; }
.apline .primary { align-self: flex-end; }
.apline .primary.p-buy { background: var(--good); }
.aucpost select { flex: none; min-width: 168px; background: var(--well);
                  border: 1px solid var(--line); border-radius: 5px;
                  color: var(--text); padding: 5px 8px; font-size: 12.5px; }
.aucpost select:disabled { color: var(--faint); }
.aucfield { display: flex; flex-direction: column; gap: 3px; }
.aucfield span { color: var(--dim); font-size: 10.5px; }
.aucfield input { width: 96px; background: var(--well);
                  border: 1px solid var(--line); border-radius: 5px;
                  color: var(--text); padding: 5px 8px; font-size: 12.5px; }
/* A COLUMN THAT WILL TAKE THE DROP SAYS SO before the cursor arrives — the
   same green/red the doll and the depot use, so the gesture is one gesture
   everywhere in the game. */
.auccol.drop-ok { border-color: var(--good);
                  box-shadow: inset 0 0 0 1px var(--good); }
.auccol.drop-no { border-color: var(--bad); }
/* THE FEE BEFORE THE BUTTON. A cost you find out about afterwards is a cost
   people learn to fear, and the fifty-gold floor surprises in exactly the
   direction that annoys -- small postings. */
.aucfee { margin-top: 8px; font-size: 12px; color: var(--text); }
.aucfee.muted, .aucfee .muted { color: var(--dim); }

.aucfoot { flex: none; display: flex; align-items: center; gap: 12px;
           font-size: 11.5px; color: var(--dim); }
.aucfoot .note { min-width: 0; flex: 1; }
.aucfoot .cap { flex: none; font-variant-numeric: tabular-nums; }

/* ── the two resets ──────────────────────────────────────────────────────────
   Shown only when there is something to undo, and each asks once in place: a
   confirmation dialogue over the panel you are reading is a heavier
   interruption than "200 gold, and your gear stays put" deserves. */
/* A row that destroys something says so before it does it. */
.imrow.warn { color: #d9707a; }
.imrow.warn .imico { color: #d9707a; }

/* ── WHERE IT GOES ───────────────────────────────────────────────────────────
   The paper doll in miniature with the legal slots lit, instead of the sentence
   "goes in your left hand or right hand". The grid is DOLL's own shape, so the
   little one cannot come to disagree with the big one about where a ring sits.

   Four rows of three, at eight pixels a cell: small enough to read as a symbol
   rather than as a control you might try to click, big enough that "both hands"
   and "one hand" are different pictures. */
.ic-where { display: flex; align-items: center; gap: 10px;
            margin-top: 8px; padding-top: 7px; border-top: 1px solid var(--line); }
.ic-doll { display: grid; grid-template-columns: repeat(3, 9px);
           gap: 2px; flex: none; }
.ic-doll .d-cell { display: block; width: 9px; height: 9px; border-radius: 2px;
                   border: 1px solid var(--line); background: var(--ink); }
.ic-doll .d-cell.gap { border-color: transparent; background: none; }
/* Lit, and lit the way the torch lights everything else that is available. */
.ic-doll .d-cell.on { border-color: var(--torch); background: var(--torch-d);
                      box-shadow: 0 0 5px rgba(240,165,60,.55); }
.ic-wherelbl b { color: var(--text); font-size: 12px; font-weight: 600; }
.ic-wherelbl .sub { color: var(--muted); font-size: 11px; }

/* ── WHAT THE FILTER DOES WITH THIS ──────────────────────────────────────────
   Two facts and not one: which way the filter is pointing at this item, and
   which rarities it applies to. A row with no rarities means every rarity, and
   it says so rather than leaving an empty space to be interpreted.

   Coloured by the OUTCOME rather than by the mode -- green when one of these
   comes home, red when it is left -- because "am I picking these up" is the
   question, and "which mode am I in" is only how the answer is arrived at. */
.ic-filter { margin-top: 8px; padding-top: 7px; border-top: 1px solid var(--line); }
.ic-filter .fl-head { display: flex; align-items: center; gap: 6px;
                      font-size: 12px; }
.ic-filter .fl-ico { font-size: 11px; }
.ic-filter.keep .fl-head { color: #6ec06a; }
.ic-filter.leave .fl-head { color: #d9707a; }
.ic-filter .fl-tiers { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 5px;
                       padding-left: 17px; }
.ic-filter .fl-any { color: var(--dim); font-size: 11px; }

/* ── AND HOW TO GET AT THE REST ──────────────────────────────────────────────
   In the torch colour with a mark on it, because it is the one line on the card
   that is an instruction rather than a fact -- and a grey instruction among grey
   facts is a line nobody reads. */
.tip .ic-more { display: flex; align-items: center; gap: 6px; margin-top: 7px;
                color: var(--torch); font-size: 11px; letter-spacing: .02em; }
.tip .ic-more .i { display: grid; place-items: center; width: 13px; height: 13px;
                   border: 1px solid var(--torch-d); border-radius: 50%;
                   font-size: 9px; line-height: 1; flex: none; }

/* ── WHAT A TRAIT DOES ───────────────────────────────────────────────────────
   The board is 141 pictures; this is the only place any of them says what it
   is. Every rank rather than the one you are on, because a trait's shape is
   "1 / 2 / 3" or "and the third adds a proc" -- and you have to judge that from
   the first rank, which is when you decide whether to start down a branch. */
.traitcard { min-width: 230px; }
.tc-head { display: flex; align-items: center; gap: 9px; padding-bottom: 7px;
           margin-bottom: 7px; border-bottom: 1px solid var(--line); }
.tc-ico { width: 34px; height: 34px; flex: none; image-rendering: pixelated;
          border: 1px solid var(--line); border-radius: 6px; background: #10141c; }
.tc-who { flex: 1; min-width: 0; }
.tc-name { color: var(--text); font-weight: 700; font-size: 14px; }
.tc-sub { display: flex; gap: 7px; color: var(--muted); font-size: 11px; }
.tc-tier { color: var(--torch-d); }
.tc-rank { color: var(--torch); font-weight: 700; font-size: 14px; flex: none;
           font-variant-numeric: tabular-nums; }

.tc-levels { display: grid; gap: 3px; }
.tc-lv { display: flex; gap: 7px; font-size: 12px; color: var(--muted);
         line-height: 1.35; }
.tc-lv .n { flex: none; width: 14px; text-align: center; border-radius: 3px;
            background: var(--ink); color: var(--muted); font-size: 10px;
            line-height: 15px; }
/* THREE STATES ON ONE LIST: what you already have, what the next point buys,
   and what is further up the trait. The middle one is the decision. */
.tc-lv.have { color: var(--dim); }
.tc-lv.have .n { background: var(--torch-d); color: var(--ink); }
.tc-lv.next { color: var(--text); }
.tc-lv.next .n { background: var(--torch); color: var(--ink); font-weight: 700; }

/* The wiki's own paragraph about the spell a trait grants. Kept as prose,
   newlines and all, because it was written as prose. */
/* One rank line -- the one the next point buys -- because the hover is a peek
   and the inspector is the read. */
.tc-peek { margin-top: 7px; font-size: 12.5px; line-height: 1.4; color: var(--text);
           text-wrap: pretty; }
.tc-detail { margin-top: 7px; padding-top: 6px; border-top: 1px solid var(--line);
             color: #8fa9d0; font-size: 11.5px; line-height: 1.4;
             white-space: pre-line; }

.tc-foot { display: flex; align-items: center; gap: 6px; margin-top: 8px;
           padding-top: 6px; border-top: 1px solid var(--line);
           font-size: 11px; color: #d9707a; }
.tc-foot.can { color: #6ec06a; }
.tc-foot .g { font-size: 10px; }
.tip:has(.traitcard) { max-width: 330px; }

/* ── GLOBAL CHAT ─────────────────────────────────────────────────────────────
   Bottom-left of the middle column, between the ground and the HUD, in town
   and in a fight both.
   SHUT UNTIL YOU OPEN IT, because a chat box is the one panel that must not be
   in the way of the game beside it -- collapsed it is a tab with a count of
   what was said while it was closed, since a silent room and a room you are not
   listening to look identical. */
/* ── IN THE CORNER, AND NOTHING ELSE MOVES ─────────────────────────────────
   Two wrong shapes came before this one and both are worth keeping written
   down, because the right shape is the one that avoids both.

   IT HUNG OFF A ZERO-HEIGHT ANCHOR AND GREW UPWARD, which put an opaque panel
   over the character and over the combat log -- the two things the screen is
   for. Then it became a real row in the column, which fixed that by making the
   ground shorter -- and pushed the entire fight HUD up every time somebody
   opened it. "Only the chat box should slide" is the correction, and it is the
   right one: the HUD is where your hands are, and a panel that shoves it is a
   panel that moves the buttons you were about to press.

   So it FLOATS AGAIN, but in the corner the HUD does not use. The bottom-left
   of the column is empty in town and in a fight both -- the action bar and the
   bars are centred -- and that is where this lives now, pinned by its BOTTOM
   edge rather than by its top.

   THAT PIN IS THE WHOLE ANIMATION. Shut, the object is just the handle, so the
   handle is what sits in the corner. Open, the object is the handle plus the
   room, still pinned at the bottom -- so the handle rides up and the room drops
   out from underneath it, and folding it away puts the handle back in the
   corner. One rule, both directions, and nothing outside the box is touched. */
.chatdock { position: absolute; left: 0; bottom: 0; width: 100%; height: 0;
            z-index: 12; pointer-events: none; }
/* `bottom` IS SET FROM JS -- see placeChat(). The corner is the resting place
   and this is the fallback for the moment before the first measurement.

   THE BOX ITSELF CATCHES NOTHING. Only the handle and the open room take the
   mouse: shut, the rest of this element is 235px of transparent nothing, and a
   transparent nothing over the action bar is three buttons that stop working. */
.chat { position: absolute; left: calc(14px + var(--winL)); bottom: 6px;
        pointer-events: none;
        width: min(340px, 42%); display: flex; flex-direction: column;
        align-items: stretch; }
.chattab, .chatbody { pointer-events: auto; }
.chat.shut .chatbody { pointer-events: none; }

/* THE SHUTTER. Height is animated on the wrapper and the contents keep their
   own size inside it, so the lines are clipped on the way down rather than
   reflowed -- a log that re-wraps itself while it opens reads as a glitch.
   `visibility` is what actually takes it out of the page when it is shut: a
   clipped box is still a box, and a box you can tab into is a box that is not
   really closed. It is switched at the END of the fold and at the START of the
   unfold, so it never eats the animation. */
.chatbody { overflow: hidden; max-height: 210px; opacity: 1;
            transition: max-height .22s ease, opacity .16s ease .04s,
                        visibility 0s; }
.chatbody-in { display: flex; flex-direction: column; gap: 5px;
               padding-top: 5px; }
.chat.shut .chatbody { max-height: 0; opacity: 0; visibility: hidden;
                       transition: max-height .22s ease, opacity .14s ease,
                                   visibility 0s linear .22s; }
/* Points the way the next click sends it: down to open, up to close. */
.chattab .caret { margin-left: 1px; font-size: 9px; color: var(--dim);
                  transition: transform .22s ease; }
.chat:not(.shut) .chattab .caret { transform: rotate(180deg); }
.chattab { align-self: flex-start;
           display: flex; align-items: center; gap: 6px; padding: 3px 10px;
           background: linear-gradient(#232a36, #151a24);
           border: 1px solid #0a0d13; border-radius: 4px;
           box-shadow: inset 0 1px 0 rgba(255,255,255,.07);
           color: var(--dim); font: inherit; font-size: 11px; cursor: pointer; }
.chattab:hover { color: var(--text); }
.chattab .g { font-size: 11px; }
.chattab .badge { min-width: 15px; padding: 0 4px; border-radius: 999px;
                  background: var(--torch); color: var(--ink);
                  font-size: 10px; font-weight: 700; line-height: 15px;
                  text-align: center; }
.chattab .badge.hidden { display: none; }

/* SHORT ENOUGH TO STAY IN THE CORNER. Every line this grows by is a line of
   arena it stands in front of while it is open. */
.chatlines { width: 100%; height: 116px; overflow-y: auto;
             background: rgba(9,11,16,.82); border: 1px solid var(--line);
             border-radius: 6px; padding: 6px 9px; font-size: 12.5px;
             line-height: 1.35; backdrop-filter: blur(2px); }
.chatline { color: var(--text); padding: 1px 0; word-break: break-word; }
.chatline b { color: var(--torch); font-weight: 600; }
/* Anything the game says about the room rather than a person saying it. */
.chatline.sys { color: var(--muted); font-style: italic; }

.chatform { display: flex; gap: 5px; width: 100%; }
.chatform input { flex: 1; min-width: 0; padding: 4px 8px; font-size: 12.5px;
                  background: rgba(9,11,16,.86); border: 1px solid var(--line);
                  border-radius: 4px; color: var(--text); }
.chatform input:focus { outline: none; border-color: var(--torch-d); }
/* THE COOLDOWN IS ON THE BUTTON. Five seconds is the server's rule and this is
   a courtesy -- so you can see the wait rather than discover it by being
   refused. It counts down; it does not merely grey out. */
.chatsend { flex: none; min-width: 52px; padding: 4px 10px; font: inherit;
            font-size: 11px; background: linear-gradient(#232a36, #151a24);
            border: 1px solid #0a0d13; border-radius: 4px; color: var(--torch);
            cursor: pointer; font-variant-numeric: tabular-nums; }
.chatsend:disabled { color: var(--muted); cursor: default; }

@media (max-width: 900px) { .chat { width: 60%; } }

/* ── THE WARDROBE ────────────────────────────────────────────────────────────
   Two controls, and they do different jobs. The SEX is a two-way switch and the
   OUTFIT is a grid of pictures, because there are two of the first and
   twenty-two of the second and a control's shape should say how many answers it
   has.

   PICTURES AND NOT NAMES.  The list is "Knight Level 3", "Paladin Level 1",
   "Vanguard Level 2" -- forty-four rows of two words each, and nobody knows
   what any of them look like. The wiki ships the game's own render of every one
   of them, so the card IS the answer and the words underneath are the label for
   the thing you have already recognised.

   It appears in two places -- the character-creation form and the Settings box
   -- and it is one function in app.mjs (`wardrobe`) so the second cannot drift
   from the first. The `.compact` variant is the popup's: smaller cards, and a
   height cap so twenty-two of them scroll instead of pushing "Switch character"
   off the bottom of the screen. */
.wardrobe { display: flex; flex-direction: column; gap: 8px; }
.wdsexes { display: flex; gap: 6px; }
.wdsex {
  flex: 1; padding: 6px 10px; cursor: pointer;
  background: var(--panel-2); color: var(--dim);
  border: 1px solid var(--line); border-radius: 4px;
  font: 400 9px var(--loud); letter-spacing: .1em; text-transform: uppercase;
}
.wdsex:hover { color: var(--text); border-color: var(--line); }
.wdsex.on { color: var(--torch); border-color: var(--torch);
            background: rgba(255, 196, 106, .10); }

.wdgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(92px, 1fr));
          gap: 6px; }
.wdcard {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 6px 4px 7px; cursor: pointer;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 5px;
  color: var(--dim); font: 400 10px var(--label);
}
.wdcard:hover { border-color: var(--line); color: var(--text); }
/* THE CHOSEN ONE IS LIT, not merely outlined: this grid is scrolled past and a
   1px border change is not findable in a wall of cards. */
.wdcard.on { border-color: var(--torch); color: var(--torch);
             background: rgba(255, 196, 106, .10);
             box-shadow: 0 0 0 1px rgba(255, 196, 106, .35) inset; }
.wdcard b { font-weight: 700; }
.wdcard .lv { font-size: 9px; color: var(--muted); letter-spacing: .04em; }
.wdcard.on .lv { color: rgba(255, 196, 106, .8); }
/* The renders are drawn on transparent and are taller than they are wide; the
   box keeps a wall of them lining up. */
.outfitart { display: block; object-fit: contain; image-rendering: auto; }
.wdcard .outfitart { height: 74px; width: auto; max-width: 100%; }

/* The popup's variant. `max-height` and not a row count: the number of outfits
   is the wiki's and it will change. */
.wardrobe.compact { }
.wardrobe.compact .wdgrid {
  grid-template-columns: repeat(3, 1fr); gap: 5px;
  max-height: 232px; overflow-y: auto; padding-right: 2px;
}
.wardrobe.compact .wdcard { padding: 4px 2px 5px; font-size: 9px; }
.wardrobe.compact .wdcard .outfitart { height: 52px; }
.wardrobe.compact .wdcard .lv { font-size: 8px; }

/* The creation form is a column now rather than a row -- it grew a wardrobe --
   so its buttons need their own row back. */
#newChar { flex-direction: column; align-items: stretch; gap: 10px; }
#newChar .row { display: flex; align-items: center; gap: 12px; }
#newChar .wdgrid { max-height: 300px; overflow-y: auto; }

/* ── THE WARDROBE'S OWN FIGURE ───────────────────────────────────────────────
   > *"My idea here is that the player can choose their colors for the outfit."*

   A colour picker with nothing to look at is a colour picker nobody can use, so
   the real model stands above the swatches and turns. The box is sized here and
   nowhere else -- the renderer measures whatever it is mounted into. */
.wdstage {
  width: 100%; height: 230px; border-radius: 10px;
  background: radial-gradient(120% 100% at 50% 15%,
              rgba(58,66,84,.55), rgba(12,15,22,.85));
  border: 1px solid rgba(226,232,244,.10);
  overflow: hidden; position: relative;
}
.wardrobe.compact .wdstage { height: 170px; }

/* ── AND THE SWATCHES ────────────────────────────────────────────────────────
   One row per region, top of the body downwards, because that is the order
   people describe a character in. The label is fixed-width so four rows of
   swatches line up as a grid rather than as four ragged rows. */
.wdpaints { display: flex; flex-direction: column; gap: 6px; }
.wdpaint { display: flex; align-items: center; gap: 8px; }
/* SIX ROWS NOW, one of them twenty-four swatches wide, so both the label and
   the swatches are a little tighter than they were -- the popup is 280px and
   the alternative to tightening is a colour picker you scroll. */
.wdplbl {
  flex: 0 0 46px; font: 400 10px var(--loud); letter-spacing: .1em;
  text-transform: uppercase; color: #8e99ad; cursor: help;
}
.wdswatches { display: flex; flex-wrap: wrap; gap: 3px; flex: 1 1 auto; }
/* A SWATCH IS ITS OWN COLOUR AND NOTHING ELSE -- no label, no border of its
   own, because a grid of eighteen colours is read by comparing them and any
   chrome between them is something else to compare. The chosen one is marked
   by a ring OUTSIDE its edge, so being chosen does not change the amount of
   colour on screen. */
.wdsw {
  width: 18px; height: 18px; border-radius: 4px; border: 0; padding: 0;
  cursor: pointer; box-shadow: inset 0 0 0 1px rgba(0,0,0,.45);
  transition: transform .08s ease;
}
.wdsw:hover { transform: scale(1.14); }
.wdsw.on {
  box-shadow: inset 0 0 0 1px rgba(0,0,0,.45),
              0 0 0 2px var(--bg, #0d1017), 0 0 0 3.5px #e8b45a;
}

/* ── THE OFF HAND, MIRRORED ──────────────────────────────────────────────────
   The paperdoll is a mirror of somebody facing you, so its two hand slots are
   opposite hands and an item drawn the same way in both is two right hands. The
   wiki's character planner flips it; so does this. Equipment panel only -- the
   arena has real fists and nothing to mirror. */
.slot .sprite.offhand { transform: scaleX(-1); }

/* ── THE CHARACTER'S OWN PICTURE, ON THEIR OWN HEADER ────────────────────────
   > *"we going to add the character picture to the character sheet header (you
   > can get the first frame of the 3d render)"*

   It is a render and not a file: one frame of the game's own model, painted in
   this character's colours (see `portrait` in arena3d.mjs), which is the only
   way a picture in here can show the paint at all.

   IT STANDS AT THE LEFT AND THE NAME MOVES OVER. A portrait in the middle of
   the band would be a portrait behind the title -- so the figure takes the left
   hundred pixels, the carved name starts after it, and the two read as one
   plate rather than as a picture with writing on top of it. */
.ovface {
  position: absolute; left: 14px; top: 0; bottom: 0; width: 96px; margin: 0;
  z-index: 2; display: grid; place-items: end center; pointer-events: none;
}
/* AND `hidden` HAS TO MEAN HIDDEN. The browser's own `[hidden] { display: none }`
   lives in the UA sheet, which every author rule beats -- so a `display: grid`
   up there quietly un-hides the element the markup says is hidden, and the
   character stands in the corner of the market. */
.ovface[hidden] { display: none; }
.ovface img {
  max-width: 100%; max-height: 116px; object-fit: contain; display: block;
  /* A DROP SHADOW AND NOT A BOX. The render arrives on transparent ground, so
     there is nothing to frame -- what makes it sit on the header rather than
     float above it is the shade under the boots. */
  filter: drop-shadow(0 3px 6px rgba(0,0,0,.75));
}
/* THE GHOST GOES WHEN THE PORTRAIT ARRIVES. `.figure` puts the flat class art
   behind the name at half strength; with a real figure standing in the corner
   that is the same person drawn twice. It stays for a browser that cannot draw
   the portrait -- which is exactly the case `.hasface` is not added in.

   AND IT IS NOT CALLED `.portrait`, which is what it was called for about ten
   minutes. There is already a `.portrait` in this file -- the ladder's little
   round pictures of other players -- and it carries `display: grid`, so naming
   this one the same thing turned the panel HEADER into a grid: 62 pixels wide,
   with the minidock and the close button somewhere off the left of the screen.
   A bare class name in a stylesheet is a global, and this one was taken. */
.ovhead.hasface .ovart { display: none; }
.ovhead.hasface .ovwords { left: 122px; }

/* ── AND THE PREVIEW WHEN THE ARENA CANNOT BE LENT ───────────────────────────
   Mid-hunt the wardrobe's stage holds a still instead of the live model -- one
   context, and the fight has it. Same box, same framing; it simply does not
   turn. */
.wdstill { width: 100%; height: 100%; object-fit: contain; display: block; }

/* ── THE OUTFIT WINDOW ───────────────────────────────────────────────────────
   > *"on click should open the select outfit window with all our options, with
   > 2 buttons: Save, discard"*

   Above everything, including the panel that opened it, because it is a
   decision taken ON the sheet rather than a second place to be. The wash behind
   it is the same one the overlay uses, so the gesture people already know --
   click the dark part to leave -- works here too.

   TALLER THAN IT IS WIDE, and the grid is the part that scrolls: the figure and
   the six colour rows are the controls, and controls that scroll away while you
   are using them are the reason this was not a good fit for the settings popup
   in the first place. */
.outfitwin {
  position: fixed; inset: 0; z-index: 80; display: flex;
  align-items: center; justify-content: center; padding: 24px;
  background: rgba(5, 7, 11, .72);
}
.owcard {
  width: min(420px, 100%); max-height: 100%;
  display: flex; flex-direction: column; border-radius: 4px;
  box-shadow: 0 24px 60px rgba(0,0,0,.7);
}
.owhead {
  display: flex; align-items: center; gap: 10px; flex: none;
  padding: 12px 12px 10px 16px; border-bottom: 1px solid var(--line);
}
.owhead h2 {
  margin: 0; flex: 1; font: 400 18px var(--display); letter-spacing: .12em;
  color: var(--torch); text-transform: uppercase;
}
.owbody { padding: 14px 16px; overflow-y: auto; flex: 1 1 auto; }
.owbody .wdgrid {
  grid-template-columns: repeat(3, 1fr); gap: 6px;
  max-height: 260px; overflow-y: auto; padding-right: 2px;
}
.owbody .wdstage { height: 200px; }
/* ── TWO BUTTONS AND THEY ARE NOT THE SAME WEIGHT ────────────────────────────
   Save is what you came here to do and it is lit; Discard is the way out and it
   is plain. A pair of identical buttons is a pair you have to read. */
.owfoot {
  display: flex; justify-content: flex-end; gap: 8px; flex: none;
  padding: 11px 16px; border-top: 1px solid var(--line);
}
.owbtn {
  height: 30px; padding: 0 16px; cursor: pointer; border-radius: 2px;
  background: rgba(9,11,16,.75); border: 1px solid var(--line);
  color: var(--text); font: 400 10px var(--label); letter-spacing: .1em;
  text-transform: uppercase;
}
.owbtn:hover { border-color: var(--line); }
.owbtn.go { border-color: var(--torch-d); color: var(--torch); }
.owbtn.go:hover { border-color: var(--torch); }
.owbtn[disabled] { opacity: .5; cursor: default; }

/* ── AN ITEM THAT IS WEARING OFF ─────────────────────────────────────────────
   > *"Temporary items needs to be actually temporary [...] When a temporary
   > item is equipped, it's sprite change for their 'Active' version."*

   The sprite change is the art's own doing -- the atlas ships a switched-on
   twin for all thirteen. What this adds is the CLOCK, because a sprite that
   changed is a thing you notice once and a ring that vanishes in four minutes
   is a thing you have to plan around.

   BOTTOM RIGHT, where the bag already puts a stack count: it is a number about
   the tile and the tile has one corner left. Amber, and warmer still under half
   a minute, which is the same two-step the meal's countdown uses. */
.slot.temp { position: relative; }
.slot .tleft {
  position: absolute; right: 2px; bottom: 1px;
  font-size: 9px; font-weight: 700; line-height: 1;
  color: var(--lit); text-shadow: 0 1px 2px rgba(0,0,0,.9);
  pointer-events: none;
}
.slot .tleft.low { color: var(--torch); }


/* ── A DUNGEON CARD ──────────────────────────────────────────────────────────
   > *"there are hunting grounds, dungeons and Lairs"*

   The same card shell the grounds use, with the walk down where a ground puts
   its pool. What it does NOT have is a verdict stamp, and the absence is
   deliberate: the scout measures five minutes in one place, and a descent is
   not one place -- a stamp there would be describing the first room and calling
   it the dungeon. */
.gcard.locked { opacity: .62; }
.dfloors { display: flex; flex-direction: column; gap: 2px; margin: 6px 0 8px; }
.dfloor {
  display: flex; align-items: center; gap: 6px;
  padding: 2px 4px; border-radius: 3px; background: rgba(0,0,0,.18);
  font-size: 11px; color: var(--dim);
}
/* The floor number, which IS the number that walks in -- see `dx`. */
.dfloor .dn {
  flex: none; width: 16px; text-align: center;
  color: var(--torch); font-variant-numeric: tabular-nums;
}
.dfloor .mn { flex: 1 1 auto; color: var(--text); }
.dfloor .dx { flex: none; color: var(--dim); font-variant-numeric: tabular-nums; }
/* The thing at the bottom, marked rather than merely last: a player scanning
   the card should not have to count rows to find where it ends. */
.dfloor.bottom { background: rgba(120,20,20,.22); }
.dfloor.bottom .dn { color: #e08a8a; }
.dfloor.bottom .mn { color: #f0d6d6; }


/* ── THE STAIR ───────────────────────────────────────────────────────────────
   In the hunt header beside the name of the place, because "where am I" and
   "can I go deeper" are the same question asked twice. HIDDEN rather than
   disabled outside a dungeon: there is no stair in a sheep pasture, and a
   button that is permanently dead is furniture. */
.stair { display: flex; align-items: center; gap: 8px; margin-left: 10px; }
.stair .sw { font-size: 11px; color: var(--torch); white-space: nowrap; }
.stairb {
  padding: 3px 10px; font: inherit; font-size: 11px; cursor: pointer;
  background: linear-gradient(#232a36, #151a24);
  border: 1px solid #0a0d13; border-radius: 4px; color: var(--dim);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.07);
}
.stairb:hover:not(:disabled) { border-color: var(--torch-d); color: var(--torch); }
.stairb:disabled { opacity: .5; cursor: default; }

/* ── THE QUEST LOG ───────────────────────────────────────────────────────────
   Two hundred rows in eleven groups, and the only control on the screen is
   Claim. Everything else is a readout of something the server measured.

   THE CARD IS THE SAME SHAPE WHICHEVER KIND IT IS -- a quest and a task differ
   by a chip and by what is in their goals, and giving them two layouts would
   have made "what can I hand in" two questions instead of one.

   Every colour here is a token that already exists. A `var(--x)` with no
   fallback naming something undefined does not fall back to a default: it
   deletes the whole declaration, silently, and takes the rest of a shorthand
   with it. check.mjs asserts every one of them resolves. */
.qbody { display: flex; flex-direction: column; gap: 10px; }

/* The head band: what to show, and the two numbers only a quest can move. */
.qband {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding-bottom: 8px; border-bottom: 1px solid var(--line);
}
.qfilter { display: flex; gap: 4px; }
.qfb {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 9px; border: 1px solid var(--line); border-radius: var(--radius);
  background: linear-gradient(var(--face), var(--face-d));
  color: var(--sub); font: 400 10px var(--label); cursor: pointer;
  text-transform: uppercase; letter-spacing: .06em;
}
.qfb:hover { color: var(--text); border-color: var(--lift); }
.qfb.on { color: var(--lit); border-color: var(--torch-d); }
.qfb .n { color: var(--faint); font-family: var(--label); }
.qfb.on .n { color: var(--torch); }
/* A COUNT OF ZERO IS NOT NEWS. `Ready` only lights when there is something in
   it, so the one tab worth glancing at is the one that changes colour. */
.qfb.lit .n { color: var(--good); }

.qprof { display: flex; gap: 10px; margin-left: auto; }
.qpt {
  display: inline-flex; align-items: baseline; gap: 5px;
  padding: 3px 8px; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--well); cursor: help;
}
.qpt .w { color: var(--sub); font: 400 9px var(--label); text-transform: uppercase; }
.qpt .n { color: var(--lit); font: 400 12px var(--loud); }

/* The section heading, cut from the same metal as the grounds panel's. */
.qsechead { display: flex; align-items: center; gap: 8px; margin-top: 6px; }
.qsechead .d { color: var(--torch-d); font-size: 9px; }
.qsechead .t { color: var(--title); font: 400 15px var(--display); letter-spacing: .02em; }
.qsechead .s { color: var(--faint); font-size: 11px; }
.qsechead .rule { flex: 1; height: 1px; background: var(--line); }
.qsechead .qready {
  color: var(--good); font: 400 9px var(--label); text-transform: uppercase;
  letter-spacing: .06em;
}

.qlist {
  display: grid; gap: 8px;
  /* `auto-fill` AND NOT `auto-fit`. auto-fit collapses the empty tracks and
     lets the survivor stretch, so a group with one quest in it draws a card the
     width of the panel while the group under it draws three. Same trap the
     character picker fell into. */
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}

.qcard {
  display: flex; flex-direction: column; gap: 6px;
  padding: 10px 12px 12px;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: linear-gradient(var(--panel-2), var(--panel));
}
.qcard.ready { border-color: var(--torch-d); box-shadow: inset 0 0 0 1px rgba(240,165,60,.12); }
.qcard.shut { opacity: .55; }
.qcard.done { opacity: .5; background: var(--ink-2); }

.qhead { display: flex; align-items: baseline; gap: 7px; }
.qname { color: var(--title); font: 400 15px var(--display); }
.qkind {
  color: var(--faint); font: 400 8px var(--label); text-transform: uppercase;
  letter-spacing: .08em; border: 1px solid var(--line); border-radius: 3px;
  padding: 1px 4px;
}
.qlvl { color: var(--sub); font: 400 9px var(--label); }
.qlvl.far { color: var(--bad); cursor: help; }
.qwiki { color: var(--torch-d); font-size: 8px; cursor: help; }
.qsp { flex: 1; }
.qdone { color: var(--good); font: 400 9px var(--label); text-transform: uppercase; }

/* THE MARGIN IS RESET ON PURPOSE. `p` carries a block margin from the page and
   a paragraph inside a dense card does not want it -- the same trap `.sub` set
   in the houses list, where a borrowed page-level class made every one of a
   hundred and fifteen rows 70px tall for 38px of content. */
.qtell { margin: 0; color: var(--dim); font-size: 11.5px; line-height: 1.45; }

.qgoal { display: flex; align-items: center; gap: 8px; }
.qgtext { color: var(--text); font-size: 11px; flex: 1; min-width: 0; }
.qgoal.met .qgtext { color: var(--good); }
.qbar {
  width: 72px; height: 6px; flex: none;
  background: var(--well); border: 1px solid var(--edge); border-radius: 3px;
  overflow: hidden;
}
.qbarfill { display: block; height: 100%; background: var(--torch); }
.qgoal.met .qbarfill { background: var(--good); }
.qnum { color: var(--faint); font: 400 9px var(--label); flex: none; min-width: 62px;
        text-align: right; }

.qgive { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 2px; }
.qchip {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 6px; border: 1px solid var(--line); border-radius: 3px;
  background: var(--well); color: var(--sub); font-size: 10.5px;
}
.qchip img { image-rendering: pixelated; }
.qchip.xp .w { color: var(--xp); }
.qchip.gold .w { color: var(--lit); }
.qchip.prof .w { color: var(--mp); text-transform: capitalize; }
/* A COST IS NOT A REWARD and must not read as one: it is the only chip in the
   row that takes something away. */
.qchip.cost { border-color: var(--torch-d); }
.qchip.cost .w { color: var(--bad); }

.qclaim {
  margin-top: 4px; align-self: flex-start;
  padding: 5px 14px; border: 1px solid var(--line); border-radius: var(--radius);
  background: linear-gradient(var(--face), var(--face-d));
  color: var(--faint); font: 400 10px var(--label);
  text-transform: uppercase; letter-spacing: .08em;
  /* A fixed-height button with a sentence in it wants this for the reason the
     depot's two verbs do. */
  white-space: nowrap;
}
.qclaim.on {
  color: var(--ink); border-color: var(--torch-d); cursor: pointer;
  background: linear-gradient(var(--lit), var(--torch));
}
.qclaim.on:hover { background: linear-gradient(#ffd48c, var(--lit)); }
.qclaim:disabled { cursor: default; }
.qclaim.on:disabled { opacity: .55; }
/* The horizon chip sits with the filters and is not one: it does not select a
   kind, it lifts a fold. Same metal, quieter until it is on. */
.qfb.qlater { border-style: dashed; }
.qfb.qlater.on { border-style: solid; color: var(--text); }
