/* Astral Codex — RPG overlay UI (inventory / character / skills / hp bar) */

.rpg-ui { position: fixed; inset: 0; z-index: 40; pointer-events: none;
  font-family: var(--font-body); color: var(--ink); }
.rpg-ui * { box-sizing: border-box; }

/* backdrop shown behind an open panel (cosmic dim) */
.rpg-backdrop { position: absolute; inset: 0; opacity: 0; transition: opacity .18s;
  background: radial-gradient(120% 90% at 50% 10%, rgba(41,47,107,.55), rgba(7,9,26,.82));
  backdrop-filter: blur(2px); pointer-events: none; }
.rpg-backdrop.is-open { opacity: 1; pointer-events: auto; }

/* ---- The Ledger's page furniture -------------------------------------------------------------
   Governed by docs/art/VIL.md. The interface is the Adventurer's Ledger, and a panel is a PAGE —
   enchanted vellum over ivory board, framed with bronze. Two consequences are load-bearing here:

   1. A page is a SURFACE, never an opening. So a panel is opaque and does not blur what is behind
      it. (The `.rpg-backdrop` above is the exception and stays: it is the world being set aside
      while you read, not a panel you look through.)
   2. Purple is the ground and gold is the punctuation. Gold is a LINE AND POINT material — rules,
      hairlines, brackets, bezels — and is never an area fill. That is VIL's Colour rule, and it is
      why there is no gold background anywhere below.

   Built once because nine surfaces share it. The reference plate shows the current UI has already
   drifted into per-surface chrome, which is what a shared layer prevents.
   Geometry is MEASURED, not eyeballed — see docs/art/reference-measurements.md
   § UI CHROME CONSTRUCTION for the source of every number in this block. */

/* THE PAGE, AS A BEVELLED FITTING.
   The first version of this was a 1px border and a flat two-stop fill — a DIAGRAM of the reference
   rather than the artwork. Held against the plate at 5x (docs/superpowers/screenshots/ref-zoom.png)
   the differences are specific and none of them are "more ornament":

     · the frame is a STACK, not a line: dark outer contour, warm gold band, dark inner channel;
     · there is a LIGHT CATCH along the top edge and a shadow along the bottom, which is the whole
       reason the gold reads as metal. brandStyles.ts already states this — "it reads as METAL
       because it is dark with a lit edge, not because it is bright" — and this file ignored it;
     · the fill is not flat: it is darker at the rim and lifts toward the upper third, so the page
       reads as a surface catching light rather than as a rectangle of colour.

   All of it is box-shadow layers on one element. No extra DOM, no images. */
.v-page { position: relative; border-radius: var(--r-4, 12px);
  background:
    /* the lift, so the middle of the page is not the same value as its rim */
    radial-gradient(120% 90% at 50% 8%, rgba(120,100,200,.20), transparent 62%),
    linear-gradient(180deg, #2e2358 0%, #221a46 42%, #180f36 100%);
  border: 1px solid #0d0a20;                        /* the dark contour the gold sits inside */
  box-shadow:
    0 0 0 1px rgba(198,160,104,.85),                /* the gold band */
    0 0 0 2px rgba(13,10,32,.9),                    /* dark channel outside the band */
    inset 0 1px 0 rgba(255,236,198,.22),            /* LIGHT CATCH along the top bevel */
    inset 0 -1px 0 rgba(0,0,0,.55),                 /* the underside, in shadow */
    inset 0 0 0 1px rgba(154,134,232,.10),          /* a cool inner hairline off the violet */
    inset 0 14px 28px -18px rgba(255,236,198,.16),  /* soft sheen down from the top edge */
    inset 0 -22px 34px -26px rgba(0,0,0,.75),       /* weight in the lower half */
    0 18px 48px -14px rgba(0,0,0,.85);              /* the page lifting off the world */
  color: var(--ink, #f4ecdc); }

/* CORNER FLOURISHES.
   The plate's corners are CURVED SCROLLWORK in a hairline of warm gold — a drawn flourish that
   turns in on itself. They were 3px straight L-brackets here, which is both the wrong shape and
   roughly three times the weight, and that single mismatch is most of why the panels read as
   generated rather than made.
   One SVG, used four times and mirrored, so all four corners are the same piece of art and a change
   to the curve lands everywhere at once. On a child element so callers keep their own pseudos. */
.v-page__corners { position: absolute; inset: 5px; pointer-events: none; }
.v-page__corners::before, .v-page__corners::after,
.v-page__corners > i::before, .v-page__corners > i::after {
  content: ''; position: absolute; width: 34px; height: 34px;
  background-repeat: no-repeat; background-size: 100% 100%;
  /* Two nested arcs (the double hairline the plate draws), a short spur along each axis, and a
     small terminal bead where the scroll ends. Stroke widths are ~1px at native size. */
  background-image: url("data:image/svg+xml;utf8,\
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 34 34'>\
<g fill='none' stroke='%23c9a46e' stroke-linecap='round'>\
<path d='M1 33 L1 13 C1 6.4 6.4 1 13 1 L33 1' stroke-width='1.4'/>\
<path d='M5.5 33 L5.5 14.5 C5.5 9.5 9.5 5.5 14.5 5.5 L33 5.5' stroke-width='.85' opacity='.65'/>\
<path d='M1 24 C6 23 9 20 10 15' stroke-width='.9' opacity='.9'/>\
<path d='M24 1 C23 6 20 9 15 10' stroke-width='.9' opacity='.9'/>\
</g>\
<circle cx='10.6' cy='10.6' r='1.5' fill='%23efdcbb'/>\
</svg>");
  filter: drop-shadow(0 0 3px rgba(201,164,110,.35)); }
.v-page__corners::before { top: 0; left: 0; }
.v-page__corners::after { top: 0; right: 0; transform: scaleX(-1); }
.v-page__corners > i::before { bottom: 0; left: 0; transform: scaleY(-1); }
.v-page__corners > i::after { bottom: 0; right: 0; transform: scale(-1); }

/* The title rule with a gem at its centre — the logo's divider motif, and the reference's. */
.v-rule2 { display: flex; align-items: center; gap: 10px; margin: 10px 20px 14px; }
.v-rule2::before, .v-rule2::after { content: ''; flex: 1; height: 1px;
  background: linear-gradient(90deg, transparent, rgba(201,164,110,.55)); }
.v-rule2::after { background: linear-gradient(90deg, rgba(201,164,110,.55), transparent); }
/* A gem is SET, not printed. The plate's gems sit in a bronze bezel with a lit facet on the upper
   left and the stone darkening away from it — which is what makes a 12px diamond read as a cut
   stone rather than as a rotated square. The bezel is a ring (box-shadow) so the setting stays one
   element. */
.v-gem { width: 12px; height: 12px; flex: none; transform: rotate(45deg); border-radius: 2px;
  background:
    linear-gradient(135deg, #e7ddff 0%, var(--violet-hi, #c9bdff) 22%, #6f5cc4 62%, var(--violet-deep, #3c3178) 100%);
  box-shadow:
    0 0 0 1px rgba(198,160,104,.95),        /* the bezel */
    0 0 0 2px rgba(13,10,32,.75),           /* the shadow the setting casts into the page */
    inset 1px 1px 1px rgba(255,255,255,.55),/* facet catching the key light */
    inset -1px -1px 2px rgba(20,10,50,.75), /* the stone's far side */
    0 0 9px rgba(154,134,232,.55); }        /* the glow it throws */

/* ---- ORNAMENT TIERS ---------------------------------------------------------------------------
   VIL · Hierarchy asks how a primary surface differs from a secondary one WITHOUT using size alone.
   The answer the plate gives is ORNAMENT COMPLETENESS: the hotbar and the open panels are heavier
   assemblies - a second inner rail, full-size flourishes, gems in settings - while the tracker card
   and other glanceable furniture carry a plain frame and no gems at all.
   Uniform ornament everywhere is itself a tell: it says one style was applied, not that a set was
   designed. `--quiet` is the secondary tier. */
.v-page--quiet .v-page__corners { inset: 4px; }
.v-page--quiet .v-page__corners::before, .v-page--quiet .v-page__corners::after,
.v-page--quiet .v-page__corners > i::before, .v-page--quiet .v-page__corners > i::after {
  width: 20px; height: 20px; opacity: .72; filter: none; }
.v-page--quiet { box-shadow:
    0 0 0 1px rgba(198,160,104,.55),
    inset 0 1px 0 rgba(255,236,198,.14),
    inset 0 -1px 0 rgba(0,0,0,.45),
    0 12px 30px -12px rgba(0,0,0,.8); }

/* The PRIMARY tier's second, inner rail — set in from the frame, the way the plate's hotbar and
   panels are. This is the single clearest "this surface matters more" signal that is not size. */
.v-page--primary::before, .rpg-panel::before, .hud-hotbar::before {
  content: ''; position: absolute; inset: 4px; border-radius: 9px; pointer-events: none;
  border: 1px solid rgba(198,160,104,.30);
  box-shadow: inset 0 1px 0 rgba(255,236,198,.10); }

/* An inset cell — where a recorded value sits on the page. Contents, not chrome.
   CUT INTO the page rather than drawn on it: the light catch is on the BOTTOM inner edge and the
   shadow on the top, which is the inverse of the page's own bevel and is what makes a recess read
   as a recess. A flat dark rectangle with a violet outline was the single most "generated"-looking
   element in the set, because nothing in a real Ledger is a flat rectangle of colour. */
.v-slot { border-radius: var(--r-2, 8px);
  background: linear-gradient(180deg, #120e2a 0%, #1a1439 70%, #221a48 100%);
  border: 1px solid #0c0920;
  box-shadow:
    inset 0 2px 5px rgba(0,0,0,.75),                /* the lip casting into the well */
    inset 0 -1px 0 rgba(198,160,104,.22),           /* light catching the far inner edge */
    inset 0 0 0 1px rgba(154,134,232,.14),
    0 1px 0 rgba(198,160,104,.10); }                /* the page surface just below the cut */

/* The circular button family: nav, close, minimap controls. Gold as a RIM, never as a fill. */
.v-round { display: grid; place-items: center; border-radius: 50%; cursor: pointer;
  background: linear-gradient(180deg, #2b2360, #16113a);
  border: 1px solid var(--gold-deep, #7d5a2e); color: var(--gold-hi, #efdcbb);
  transition: border-color .15s, box-shadow .15s, color .15s; }
.v-round:hover { border-color: var(--gold, #c9a46e); color: #fff4de;
  box-shadow: 0 0 14px -2px var(--gold-glow, rgba(201,164,110,.4)); }
.v-round.is-active { border-color: var(--gold, #c9a46e); color: var(--gold, #c9a46e);
  box-shadow: 0 0 14px var(--gold-glow, rgba(201,164,110,.4)); }

/* THE INDEX — the row of tabs down the edge of the Ledger.
   RIGHT-ALIGNED, beside the minimap. It was moved to top-CENTRE on the argument that centre is the
   one region nothing else claims; that was tried in play and reverted, because centre is also
   directly above the character you are looking at, so the index sat in the middle of the thing it
   is meant to be peripheral to.
   The 196px clears the minimap assembly with 14px to spare: the assembly is MINIMAP_SIZE (168) at
   right:14px, so its left edge is at 182. The zoom controls hang at right:-2px — OUTWARD, into the
   margin — so they widen the assembly to the right, not toward this bar. Recompute this number if
   MINIMAP_SIZE changes; it is 14 + MINIMAP_SIZE + 14.
   Each button is a glyph with its LABEL beneath: five abstract glyphs in a row is a memory test,
   and the one thing an index must never be is a puzzle (VIL — the Ledger reveals, it does not
   make you guess). */
.rpg-actionbar { position: absolute; top: 14px; right: 196px; left: auto; bottom: auto;
  transform: none;
  display: flex; gap: 14px; align-items: flex-start; pointer-events: auto; }
.rpg-actionbar .rpg-btn { width: 46px; padding: 0; border: 0; background: none; cursor: pointer;
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  color: var(--ink-soft); transition: color .15s; }
/* The disc is the glyph's own box, so the label sits OUTSIDE the circle rather than inside it. */
.rpg-actionbar .rpg-btn-glyph { width: 44px; height: 44px; display: grid; place-items: center;
  border-radius: 50%; font-family: var(--font-banner); font-size: 19px; line-height: 1;
  background: linear-gradient(180deg, #2b2360, #16113a);
  border: 1px solid var(--gold-deep, #7d5a2e); color: inherit;
  /* NO backdrop-filter: a page is a surface, not an opening (VIL · Substance). The old square
     buttons blurred the world through themselves. */
  transition: border-color .15s, box-shadow .15s, transform .1s; }
.rpg-actionbar .rpg-btn-label { font-family: var(--font-banner); font-size: 10px; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase; white-space: nowrap; opacity: .78; }
.rpg-actionbar .rpg-btn:hover { color: var(--gold-hi); }
.rpg-actionbar .rpg-btn:hover .rpg-btn-glyph { border-color: var(--gold);
  box-shadow: 0 0 14px -2px var(--gold-glow); transform: translateY(-2px); }
.rpg-actionbar .rpg-btn.is-active { color: var(--gold); }
.rpg-actionbar .rpg-btn.is-active .rpg-btn-glyph { border-color: var(--gold);
  box-shadow: 0 0 14px var(--gold-glow); }
.rpg-actionbar .rpg-btn.is-active .rpg-btn-label { opacity: 1; }

/* The account button's old square-glass treatment is GONE: GameUI now builds it with the same
   glyph+label markup as the other index tabs, and this rule kept painting the retired 42px glass
   square behind the round glyph — the "old menu item" peeking out behind Account. The generic
   .rpg-actionbar .rpg-btn rules above are the whole treatment now. */

/* ---- The quest log ---------------------------------------------------------------------------
   An indexed register: one line per obligation accepted, the unfinished above the finished. */
.rpg-quests { display: flex; flex-direction: column; gap: 12px; }
.rpg-quest { padding: 9px 12px; border-radius: var(--r-2);
  background: rgba(7,9,26,.42); border: 1px solid rgba(154,134,232,.16); }
.rpg-quest-head { display: flex; align-items: center; gap: 8px; }
.rpg-quest-name { flex: 1 1 auto; font-family: var(--font-display); font-size: 15px;
  color: var(--ink); }
.rpg-quest-progress { font-family: var(--font-mono); font-size: 12px;
  font-variant-numeric: tabular-nums; color: var(--ink-mute); }
.rpg-quest-obj { margin: 5px 0 0 20px; font-size: 13px; color: var(--ink-soft); }
/* READY is the one state worth a colour: the objective is met and the player has to walk back to
   somebody, which no number on this row can say. */
.rpg-quest.is-ready { border-color: rgba(201,164,110,.5); }
.rpg-quest.is-ready .rpg-quest-progress { color: var(--gold-hi); }
/* Done stays on the page rather than vanishing — a record, not a task queue — but recedes. */
.rpg-quest.is-done { opacity: .5; }
.rpg-quests-empty { margin: 4px 2px; font-size: 13px; color: var(--ink-mute); font-style: italic; }
/* account button + its drop-down. `position: relative` on the wrapper so the menu hangs off the
   BUTTON rather than off the action bar, which would put it under the minimap. */
.rpg-account { position: relative; display: grid; place-items: center; }
.rpg-account-menu { position: absolute; top: 48px; right: 0; min-width: 168px; display: none;
  padding: 5px; border-radius: var(--r-2); background: var(--glass-deep);
  border: 1px solid var(--glass-stroke); backdrop-filter: blur(8px); z-index: 30; }
.rpg-account-menu.is-open { display: block; }
.rpg-account-item { width: 100%; padding: 9px 11px; text-align: left; border: 0; border-radius: var(--r-2);
  background: none; color: var(--ink-soft); font-size: 13px; cursor: pointer; white-space: nowrap; }
.rpg-account-item:hover { background: rgba(255,255,255,.08); color: var(--gold-hi); }
/* top-left status: name, level, gold, HP bar, XP bar */
/* 234px -> 306px. The medallion is 62px plus its gap, so at the old width the three bars would have
   lost a third of their length to make room for it — and a bar's LENGTH is what makes a fraction
   readable at a glance. The reference's own vitals cluster measures ~425px wide; 306 is the
   conservative step that keeps the bars at their current length rather than matching the plate. */
.rpg-status { position: absolute; top: 14px; left: 14px; width: 306px; pointer-events: none;
  border: 2px solid transparent; border-radius: var(--r-2);
  background:
    var(--frame-corners-sm),
    /* A GRADIENT, not the bare colour. In the `background` shorthand a <color> is only legal in the
       FINAL layer, so `var(--chrome) padding-box,` in the middle invalidated the entire declaration
       and this panel painted NOTHING — it has been fully transparent since it was written, which
       nobody noticed because the corner brackets and the gold inner glow still drew and read as a
       frame. Over a green meadow the "fill" was the meadow, which is what made it olive.
       `linear-gradient(c, c)` is the standard way to spend a colour as an image layer. */
    linear-gradient(var(--chrome), var(--chrome)) padding-box,
    var(--frame-trim-soft) border-box;
  padding: var(--sp-2) var(--sp-3);
  /* NO backdrop-filter. It was here, and VIL forbids it: the interface is the Adventurer's Ledger,
     a panel is a PAGE, and a page is a surface you read — never an opening you look through. A
     blur-through is the single clearest way to make a panel read as glass, which is the thing the
     Ledger is not. Removed on VIL's first application to real code. */
  box-shadow: var(--frame-inner-glow), 0 6px 18px -6px rgba(0,0,0,.55);
  /* Two columns now: the medallion, then the name-and-bars column. */
  display: flex; gap: 10px; align-items: flex-start; }

/* PORTRAIT MEDALLION.
   This block used to carry a comment saying there was "deliberately no portrait — the space it
   would have taken is given to the name and the two bars instead." That decision is REVERSED, with
   cause: the north-star plate leads with the player's face, and the Ledger is a record of a person,
   so its first page identifies its owner. The old note is replaced rather than left to contradict
   the code beneath it.
   The canvas is a placeholder gradient until the live CharacterView preview is wired (see GameUI). */
.rpg-portrait { position: relative; width: 62px; height: 62px; flex: none; }
.rpg-portrait-canvas { width: 100%; height: 100%; border-radius: 50%; display: block;
  background: radial-gradient(circle at 50% 34%, #3a2f6e, #17123a); }
/* The ring is a separate element rather than a border on the canvas: a seal sits ON the rim, and
   an element gives the badge something to overlap. Gold as a RIM — never a fill (VIL · Colour). */
.rpg-portrait-ring { position: absolute; inset: -3px; border-radius: 50%; pointer-events: none;
  border: 2px solid var(--gold, #c9a46e); box-shadow: 0 0 12px -2px var(--gold-glow); }
.rpg-portrait-lvl { position: absolute; left: -5px; bottom: -3px; width: 21px; height: 21px;
  display: grid; place-items: center; border-radius: 50%;
  background: linear-gradient(180deg, #2b2360, #16113a);
  border: 1px solid var(--gold, #c9a46e); color: var(--gold-hi, #efdcbb);
  /* Digits belong to the HUD voice — see the .lvl note below for why an old-style serif is wrong
     for anything containing a number. */
  font-family: var(--font-mono); font-size: 11px; font-variant-numeric: tabular-nums;
  line-height: 1; }
.rpg-statuscol { flex: 1 1 auto; min-width: 0; }

/* identity: the character's name, with level and gold beneath it. */
.rpg-ident { display: flex; flex-direction: column; gap: 3px; margin-bottom: 8px; }
.rpg-ident .name { font-family: var(--font-display); font-size: 20px; line-height: 1.05;
  color: var(--ink); letter-spacing: .01em;
  /* A name is player-typed and can be long; the block has a fixed width, so it must clip rather
     than push the gold count off the panel. */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rpg-status-row { display: flex; align-items: baseline; justify-content: space-between; }
/* The CHROME face, because this line contains a NUMBER. It used to be set in the prose face and
   read "LV I" — an old-style serif draws '1' as a stem that is indistinguishable from an I at
   12px. Anything with digits in it belongs to the HUD voice, whose figures are unambiguous. */
.rpg-status .lvl { font-family: var(--font-mono); font-weight: 600; letter-spacing: .1em;
  text-transform: uppercase; font-size: 12px; color: var(--gold-hi); }
.rpg-status .gold { font-family: var(--font-mono); font-size: 13px; color: var(--ink); }
.rpg-status .gold i { color: var(--gold); font-style: normal; margin-left: 1px; }
/* HP row. Server-owned and polled by GameUI.frame(); no mana row beside it because the game has
   no mana (see the seam note in GameUI). */
/* Bar + tag. The bar flexes, the tag is a fixed gutter so all three tags align down the block. */
.rpg-bar-row { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
.rpg-bar-row > :first-child { flex: 1 1 auto; min-width: 0; }
.rpg-bar-tag { flex: 0 0 30px; font-family: var(--font-mono); font-weight: 500; font-variant-numeric: tabular-nums; font-size: 11px; letter-spacing: .04em;
  color: var(--ink-soft); text-shadow: 0 1px 2px #000; }

.rpg-hprow { position: relative; height: 14px; border-radius: var(--r-pill); overflow: hidden;
  background: rgba(7,9,26,.6); border: 1px solid var(--glass-stroke); }
.rpg-hprow .fill { position: absolute; inset: 0; width: 100%; border-radius: var(--r-pill);
  background: linear-gradient(90deg, var(--hp-deep), var(--hp)); transition: width .15s; }
.rpg-hprow .hp-label { position: absolute; inset: 0; display: grid; place-items: center;
  font-family: var(--font-mono); font-weight: 500; font-variant-numeric: tabular-nums; font-size: 11px; letter-spacing: .02em; color: var(--ink);
  text-shadow: 0 1px 2px #000; }
.rpg-hprow.is-low { border-color: var(--ember); box-shadow: 0 0 10px rgba(255,122,82,.35); }

/* The XP bar is the ONE part of the status block that takes the pointer, because it swaps its
   label on hover. Scoped to the bar and not to `.rpg-status`, which stays pointer-events:none —
   the whole block is ~90px tall and would otherwise swallow world clicks in that corner. */
.rpg-xpbar { position: relative; height: 14px; border-radius: var(--r-pill); overflow: hidden;
  background: rgba(7,9,26,.6); border: 1px solid var(--glass-stroke); pointer-events: auto; }
.rpg-xpbar:hover { border-color: var(--glass-stroke-hi); }
.rpg-xpbar .fill { position: absolute; inset: 0; width: 0%; border-radius: var(--r-pill);
  background: linear-gradient(90deg, var(--xp-deep), var(--xp)); transition: width .2s; }
.rpg-xpbar .xp-label { position: absolute; inset: 0; display: grid; place-items: center;
  font-family: var(--font-mono); font-weight: 500; font-variant-numeric: tabular-nums; font-size: 11px; letter-spacing: .02em; color: var(--ink);
  text-shadow: 0 1px 2px #000; }
/* Percentage normally, exact numbers on hover. Exactly one of the pair is ever displayed, so the
   grid always has a single centred child. */
.rpg-xpbar .xp-num { display: none; }
.rpg-xpbar:hover .xp-pct { display: none; }
.rpg-xpbar:hover .xp-num { display: block; }

/* WXP — the gathering track. Same shape as the XP bar, deliberately a different HUE: the two fill
   from completely different actions and sit one above the other, so colour is what tells a player
   at a glance which of them just moved. Green/teal, matching the worker floats and the Trades panel. */
.rpg-wxpbar { position: relative; height: 14px; border-radius: var(--r-pill); overflow: hidden;
  background: rgba(7,9,26,.6); border: 1px solid var(--glass-stroke); pointer-events: auto; }
.rpg-wxpbar:hover { border-color: var(--glass-stroke-hi); }
.rpg-wxpbar .fill { position: absolute; inset: 0; width: 0%; border-radius: var(--r-pill);
  background: linear-gradient(90deg, #2f7d5c, #8ae0b0); transition: width .2s; }
.rpg-wxpbar .xp-label { position: absolute; inset: 0; display: grid; place-items: center;
  font-family: var(--font-mono); font-weight: 500; font-variant-numeric: tabular-nums; font-size: 11px; letter-spacing: .02em; color: var(--ink);
  text-shadow: 0 1px 2px #000; }
.rpg-wxpbar .wxp-num { display: none; }
.rpg-wxpbar:hover .wxp-pct { display: none; }
.rpg-wxpbar:hover .wxp-num { display: block; }

/* panel window */
/* THE MODAL PAGE. Same fitting as .v-page — see the long note there for why the frame is a stack
   with a light catch rather than a line, and why the corners are curved scrollwork rather than
   L-brackets.
   The flourishes arrive as BACKGROUND LAYERS rather than as a child element, because these panels
   are built in six separate files and threading an extra <span> through all of them to carry four
   pseudo-elements is a worse trade than four background images. One SVG, mirrored per corner by an
   internal transform, so the curve is authored once. */
.rpg-panel { position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%);
  width: min(880px, 92vw); max-height: 86vh; overflow: auto; pointer-events: auto;
  border: 1px solid #0d0a20; border-radius: var(--r-4); padding: var(--sp-6);
  background:
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 34 34'><g transform='translate(0,0)'><g fill='none' stroke='%23c9a46e' stroke-linecap='round'><path d='M1 33 L1 13 C1 6.4 6.4 1 13 1 L33 1' stroke-width='1.4'/><path d='M5.5 33 L5.5 14.5 C5.5 9.5 9.5 5.5 14.5 5.5 L33 5.5' stroke-width='.85' opacity='.65'/><path d='M1 24 C6 23 9 20 10 15' stroke-width='.9' opacity='.9'/><path d='M24 1 C23 6 20 9 15 10' stroke-width='.9' opacity='.9'/></g><circle cx='10.6' cy='10.6' r='1.5' fill='%23efdcbb'/></g></svg>") left 5px top 5px / 34px 34px no-repeat,
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 34 34'><g transform='translate(34,0) scale(-1,1)'><g fill='none' stroke='%23c9a46e' stroke-linecap='round'><path d='M1 33 L1 13 C1 6.4 6.4 1 13 1 L33 1' stroke-width='1.4'/><path d='M5.5 33 L5.5 14.5 C5.5 9.5 9.5 5.5 14.5 5.5 L33 5.5' stroke-width='.85' opacity='.65'/><path d='M1 24 C6 23 9 20 10 15' stroke-width='.9' opacity='.9'/><path d='M24 1 C23 6 20 9 15 10' stroke-width='.9' opacity='.9'/></g><circle cx='10.6' cy='10.6' r='1.5' fill='%23efdcbb'/></g></svg>") right 5px top 5px / 34px 34px no-repeat,
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 34 34'><g transform='translate(0,34) scale(1,-1)'><g fill='none' stroke='%23c9a46e' stroke-linecap='round'><path d='M1 33 L1 13 C1 6.4 6.4 1 13 1 L33 1' stroke-width='1.4'/><path d='M5.5 33 L5.5 14.5 C5.5 9.5 9.5 5.5 14.5 5.5 L33 5.5' stroke-width='.85' opacity='.65'/><path d='M1 24 C6 23 9 20 10 15' stroke-width='.9' opacity='.9'/><path d='M24 1 C23 6 20 9 15 10' stroke-width='.9' opacity='.9'/></g><circle cx='10.6' cy='10.6' r='1.5' fill='%23efdcbb'/></g></svg>") left 5px bottom 5px / 34px 34px no-repeat,
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 34 34'><g transform='translate(34,34) scale(-1,-1)'><g fill='none' stroke='%23c9a46e' stroke-linecap='round'><path d='M1 33 L1 13 C1 6.4 6.4 1 13 1 L33 1' stroke-width='1.4'/><path d='M5.5 33 L5.5 14.5 C5.5 9.5 9.5 5.5 14.5 5.5 L33 5.5' stroke-width='.85' opacity='.65'/><path d='M1 24 C6 23 9 20 10 15' stroke-width='.9' opacity='.9'/><path d='M24 1 C23 6 20 9 15 10' stroke-width='.9' opacity='.9'/></g><circle cx='10.6' cy='10.6' r='1.5' fill='%23efdcbb'/></g></svg>") right 5px bottom 5px / 34px 34px no-repeat,
    radial-gradient(120% 90% at 50% 8%, rgba(120,100,200,.20), transparent 62%),
    linear-gradient(180deg, #2e2358 0%, #221a46 42%, #180f36 100%);
  box-shadow:
    0 0 0 1px rgba(198,160,104,.85),
    0 0 0 2px rgba(13,10,32,.9),
    inset 0 1px 0 rgba(255,236,198,.22),
    inset 0 -1px 0 rgba(0,0,0,.55),
    inset 0 0 0 1px rgba(154,134,232,.10),
    inset 0 14px 28px -18px rgba(255,236,198,.16),
    inset 0 -22px 34px -26px rgba(0,0,0,.75),
    0 24px 80px rgba(0,0,0,.65);
  display: none; }
.rpg-panel.is-open { display: block; }
/* SETTINGS. Narrower than the ledgers: it is a short list of switches, and a switch stranded on the
   far side of 880px of parchment reads as unrelated to its own label. */
.rpg-panel--settings { width: min(560px, 92vw); }
.rpg-settings { display: flex; flex-direction: column; gap: var(--sp-4); margin-top: var(--sp-4); }
/* The row is the LABEL, so the whole thing is a hit target — a 13px checkbox is not one. */
.rpg-setting { display: flex; gap: var(--sp-3); align-items: flex-start; cursor: pointer;
  padding: var(--sp-3); border-radius: var(--r-3); border: 1px solid transparent; }
.rpg-setting:hover { border-color: rgba(201,164,110,.35); background: rgba(255,240,200,.04); }
.rpg-setting-box { margin-top: 3px; width: 16px; height: 16px; accent-color: var(--gold-hi, #ffd76a);
  cursor: pointer; flex: none; }
.rpg-setting-text { display: flex; flex-direction: column; gap: 2px; }
.rpg-setting-label { font: 600 15px var(--font-body); color: var(--ink, #f4ecdc); }
/* The hint is quieter than the label but NOT decorative — it is the only place the effect of the
   switch is written down, so it stays at a readable size rather than shrinking to a caption. */
.rpg-setting-hint { font: 13px var(--font-body); color: var(--ink-soft, #cbc2de); line-height: 1.45; }
.rpg-settings-note { font: italic 13px var(--font-quill); color: var(--ink-quill, #b9aed2);
  margin: var(--sp-5) 0 0; }
/* A SPEED ROW has no checkbox, so its text starts where a toggle's text does — the labels line up
   in one column and the rows read as a list rather than as two interleaved ones. */
.rpg-setting--speed { padding-left: calc(var(--sp-3) + 16px + var(--sp-3)); cursor: default; }
.rpg-setting--speed:hover { border-color: transparent; background: none; }
/* Greyed, not hidden, when the control it belongs to is off: a row that vanishes makes the panel
   jump, and the player loses the explanation of what they just turned off. */
.rpg-setting.is-disabled { opacity: .45; }
.rpg-setting-slider { display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-2); }
/* FIXED width, not `flex: 1`. Flexing made each slider as long as its own row allowed, and the
   rows differ because the hints differ — so two controls that do the same kind of thing were drawn
   at two lengths, which reads as them having different ranges. */
.rpg-setting-range { width: 240px; flex: none; accent-color: var(--gold-hi, #ffd76a); cursor: pointer; }
.rpg-setting.is-disabled .rpg-setting-range { cursor: not-allowed; }
/* Fixed width and tabular figures so the number does not shove the slider about as it changes. */
.rpg-setting-value { min-width: 62px; font: 600 13px var(--font-mono); font-variant-numeric: tabular-nums;
  color: var(--gold-hi, #ffd76a); }
.rpg-panel-title { font-family: var(--font-display); font-size: 30px; color: var(--gold-hi);
  margin: 0 0 var(--sp-2); letter-spacing: .02em; }
.rpg-panel-sub { font-family: var(--font-quill); font-style: italic; color: var(--ink-quill); margin: 0 0 var(--sp-5); }
.rpg-close { position: absolute; top: 14px; right: 16px; width: 30px; height: 30px; border-radius: 50%;
  background: var(--glass-hi); border: 1px solid var(--glass-stroke); color: var(--ink-soft); cursor: pointer; }
.rpg-close:hover { color: var(--ember); border-color: var(--ember); }

/* inventory */
.rpg-inv { display: grid; grid-template-columns: 300px minmax(0, 1fr); gap: var(--sp-5);
  align-items: start; min-width: 0; }
/* PAPERDOLL: the character stands in the middle with its slots arranged around the edges — head
   above, the two columns flanking, tools underneath. Explicit grid areas rather than source order,
   so the arrangement is stated in one place and the markup stays a flat list of containers. */
.rpg-doll { display: grid; gap: var(--sp-2);
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-areas:
    ".     head   ."
    "left  stage  right"
    "tools tools  tools"; }
.rpg-doll-head  { grid-area: head; display: grid; justify-content: center; }
.rpg-doll-left  { grid-area: left; }
.rpg-doll-right { grid-area: right; }
.rpg-doll-left, .rpg-doll-right { display: grid; gap: var(--sp-2); align-content: center; }
/* Tools sit CENTRED under the figure rather than spread across the full width — as three `1fr`
   columns the fixed-width slots left-aligned in their cells and the row read as unrelated to the
   character above it. */
.rpg-doll-tools { grid-area: tools; display: flex; justify-content: center; gap: var(--sp-2);
  margin-top: var(--sp-1); }
/* The slots are a fixed size so the figure — not the grid — takes the slack when the panel
   resizes; `1fr` on the columns instead made the slots stretch into rectangles. */
.rpg-doll .rpg-slot { width: 52px; }
/* The stage is the 3D canvas host. `min-height` rather than a fixed height so it grows with the
   flanking columns; the renderer resizes to whatever it ends up as. */
/* An inset alcove for the figure to stand in, so the canvas reads as part of the panel rather
   than a bare rectangle pasted onto it. */
.rpg-doll-stage { grid-area: stage; position: relative; min-height: 244px; min-width: 0;
  border-radius: var(--r-3); overflow: hidden;
  border: 1px solid var(--glass-stroke);
  background:
    radial-gradient(72% 54% at 50% 88%, rgba(126,138,255,.16), transparent 70%),
    linear-gradient(180deg, rgba(9,11,30,.55), rgba(9,11,30,.15));
  box-shadow: inset 0 1px 0 rgba(255,255,255,.05); }
.rpg-doll-figure { position: absolute; inset: 0; }
.rpg-doll-figure canvas { display: block; width: 100%; height: 100%; }
.rpg-group { display: grid; gap: var(--sp-2); min-width: 0; }
.rpg-group-title { margin: 0; font-family: var(--font-banner); font-size: 11px; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase; color: var(--ink-mute);
  display: flex; align-items: center; gap: var(--sp-2); }
/* Hairline rule trailing the group name — cheap way to read as a titled section rather than a
   floating word, and it absorbs the width difference between the two column headings. */
.rpg-group-title::after { content: ''; flex: 1; height: 1px; background: var(--glass-stroke); }
.rpg-group-count { font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  letter-spacing: .02em; color: var(--ink-soft); }
/* THE LIGHT PLATE, everywhere an item can sit. #e6e4ed on every slot — bag cells, paperdoll and
   tool slots, and the item card's art box below — so the item ground is one colour across the
   whole window, occupied or not. Rarity is allowed to touch ONLY the outer border colour. */
.rpg-slot, .rpg-cell { background: #e6e4ed; border: 2px solid var(--glass-stroke);
  /* The plate stops at the CONTENT box: the 2px padding leaves a dark channel between the border
     and the light plate, so a rarity border reads against dark on both of its sides instead of
     vanishing into the pale ground. */
  background-clip: content-box; padding: 2px;
  border-radius: var(--r-2); aspect-ratio: 1; position: relative; display: grid; place-items: center;
  cursor: pointer; user-select: none; min-width: 0;
  transition: border-color .13s, box-shadow .13s, transform .1s; }
.rpg-slot:hover, .rpg-cell:hover { border-color: var(--gold); transform: translateY(-1px); }
.rpg-slot { border-style: dashed; }
/* An OCCUPIED gear slot goes solid — dashed is the "nothing here yet" affordance, and a worn item
   framed in dashes read as still-empty. data-rarity is set exactly when the slot holds an item. */
.rpg-slot[data-rarity] { border-style: solid; }
.rpg-slot .rpg-slot-tag { position: absolute; bottom: 2px; left: 0; right: 0; text-align: center;
  font-size: 10px; letter-spacing: .05em; text-transform: uppercase; color: #4a4460; pointer-events: none; }
/* An OCCUPIED slot hides its label: the icon says what it is, and at these sizes the tag was
   overlapping the artwork. It comes back on hover for players still learning the layout. */
.rpg-slot[data-rarity] .rpg-slot-tag { opacity: 0; transition: opacity .13s; }
.rpg-slot[data-rarity]:hover .rpg-slot-tag { opacity: 1; }
/* `align-content: start` — without it the bag's rows STRETCH to fill whatever height the taller
   equipment column forces on the row, which is what spread the satchel into widely separated
   bands of icons with dead space between them. */
.rpg-bag { display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: var(--sp-2);
  align-content: start; min-width: 0; }
.rpg-cell.drop-hover { border-color: var(--violet); box-shadow: 0 0 10px var(--violet-glow); }

/* The split-stack prompt: a small floating box on the drop cell. */
.rpg-split { position: absolute; z-index: 5; width: 168px; padding: 10px 12px;
  border-radius: var(--r-2); border: 1px solid var(--gold, #c9a46e);
  background: linear-gradient(180deg, #2b2360, #16113a);
  box-shadow: 0 10px 26px -8px rgba(0,0,0,.8); }
.rpg-split-title { font-family: var(--font-banner); font-size: 10px; font-weight: 600;
  letter-spacing: .13em; text-transform: uppercase; color: var(--gold-hi); margin-bottom: 7px; }
.rpg-split-row { display: flex; gap: 5px; align-items: center; margin-bottom: 8px; }
.rpg-split-step { width: 26px; height: 26px; padding: 0; border-radius: var(--r-1, 5px);
  border: 1px solid var(--glass-stroke); background: var(--glass-hi); color: var(--ink);
  font-size: 15px; line-height: 1; cursor: pointer; }
.rpg-split-step:hover { border-color: var(--gold); }
.rpg-split-n { flex: 1; min-width: 0; padding: 4px 6px; border-radius: var(--r-1, 5px);
  border: 1px solid var(--glass-stroke); background: rgba(7,9,26,.6); color: var(--ink);
  font: 600 14px var(--font-mono); text-align: center; outline: none; }
.rpg-split-n:focus { border-color: var(--gold); }
.rpg-split-actions { display: flex; gap: 6px; }
.rpg-split-ok { flex: 1; padding: 5px 0; border-radius: var(--r-1, 5px);
  border: 1px solid var(--gold); background: rgba(201,164,110,.16); color: var(--gold-hi);
  font-family: var(--font-banner); font-size: 11px; font-weight: 600; letter-spacing: .1em;
  text-transform: uppercase; cursor: pointer; }
.rpg-split-ok:hover { background: rgba(201,164,110,.28); }
.rpg-split-cancel { flex: 1; padding: 5px 0; border-radius: var(--r-1, 5px);
  border: 1px solid var(--glass-stroke); background: transparent; color: var(--ink-soft);
  font-family: var(--font-banner); font-size: 11px; letter-spacing: .1em;
  text-transform: uppercase; cursor: pointer; }
.rpg-split-cancel:hover { color: var(--ink); }

/* The bag search, above the category tabs. Same fitting as the craft window's search box. */
.rpg-bag-search { width: 100%; padding: 8px 11px; border-radius: var(--r-2); margin-bottom: 8px;
  border: 1px solid var(--glass-stroke); background: var(--glass-deep); color: var(--ink);
  font: 14px var(--font-body); outline: none; }
.rpg-bag-search:focus { border-color: var(--gold); }
.rpg-bag-search::placeholder { color: var(--ink-dim); }

/* ---- THE INVENTORY WINDOW, one size up ---------------------------------------------------------
   20% over the shared panel: the window itself (880 -> 1056), the paperdoll slots and stage, the
   bag tiles (they inherit the extra width through the 8-column grid) and the panel's text. Scoped
   to the bag so the other ledgers keep the shared measure. */
.rpg-panel--inv { width: min(1056px, 94vw); }
.rpg-panel--inv .rpg-inv { grid-template-columns: 360px minmax(0, 1fr); }
.rpg-panel--inv .rpg-doll .rpg-slot { width: 62px; }
.rpg-panel--inv .rpg-doll-stage { min-height: 293px; }
.rpg-panel--inv .rpg-panel-title { font-size: 36px; }
.rpg-panel--inv .rpg-panel-sub { font-size: 17px; }
.rpg-panel--inv .rpg-group-title { font-size: 13px; }
.rpg-panel--inv .rpg-tab { font-size: 12px; }
.rpg-panel--inv .rpg-tile { font-size: 29px; }
.rpg-panel--inv .rpg-slot .rpg-slot-tag { font-size: 10px; }
.rpg-panel--inv .rpg-count { font-size: 12px; line-height: 18px; min-width: 18px; }
.rpg-panel--inv .rpg-purse { font-size: 17px; }
.rpg-panel--inv .rpg-cap { font-size: 14px; }
.rpg-panel--inv .rpg-bag-search { font-size: 17px; }

/* EMPTY BAG. Forty blank cells is the biggest thing on the panel and, unlabelled, it reads as a
   grid that failed to load. The line is laid OVER the grid rather than replacing it, so the cells
   are still there to drop onto and the layout does not jump the moment the first item arrives.
   Quiet enough that it disappears the instant there is anything to look at. */
.rpg-bag { position: relative; }
.rpg-bag.is-empty::after {
  content: 'Nothing carried yet — loot, gather and trade to fill it.';
  position: absolute; inset: 0; display: grid; place-items: center; pointer-events: none;
  padding: 0 var(--sp-4); text-align: center;
  font-family: var(--font-quill, Georgia, serif); font-style: italic; font-size: 13px;
  color: var(--ink-mute); text-shadow: 0 1px 6px rgba(0,0,0,.85); }
/* The cells fade back so the sentence is the thing being read, not competing with 40 rectangles. */
.rpg-bag.is-empty .rpg-cell { opacity: .45; }
/* THE LIGHT PLATE — the item ground, and the actual fix for dark-on-dark icons.
 *
 * `references/base items 1.png`, which is the project's own item sheet, draws every item as ink-
 * contoured art on a WHITE ground with a soft cast shadow. The panel was doing the opposite:
 * transparent mesh renders on a near-black cell, with a CSS glow around each silhouette
 * compensating. That glow was a workaround for having no ground at all — it lit the cell AROUND the
 * item instead of separating the item from it, and it looked like what it was.
 *
 * Inverting the ground removes the problem rather than patching it: the nine icons measured under
 * 3:1 against the dark cell are dark-brown and near-black meshes, which is exactly what a pale plate
 * separates best. It is one rule for every icon, present and future, and nothing here is per-item —
 * the constraint that ruled out hand-tuned per-icon fixes still holds.
 *
 * ONLY OCCUPIED CELLS GET IT, and that falls out of the markup rather than needing a modifier:
 * `InventoryPanel` emits `.rpg-tile` only when there is an item, so forty empty cells stay dark and
 * the grid keeps reading as a dark panel with lit items in it, not as a wall of white squares.
 *
 * Cool grey rather than the reference's pure white: at full white, forty tiles glare against the
 * violet panel. This carries a little of the panel's own hue so the plate belongs to the UI, and it
 * is a gradient with an inset shade so the tile reads as a recessed plate rather than a flat block. */
.rpg-tile { position: absolute; inset: 4px; border-radius: var(--r-1); display: grid; place-items: center;
  font-size: 24px; }
/* Rendered gear icons (public/icons/*.png) — fill the tile, keep aspect, sit above the glass.
 *
 * LEGIBILITY RIM. The cell composites to rgb(14,10,33) (`--glass-deep` over the panel), and a
 * measured sweep of every icon in public/icons/ found nine whose mean contrast against it is under
 * 3:1 — the leather set worst at 1.10:1 (chest_leather), then boots/gloves 1.18, bramble_husk 1.24,
 * dagger_beaten 1.82, the fishing rod's shaft, wolf/vole pelt, the buckler and the bow. Those are
 * dark-brown and near-black meshes on a near-black cell; they vanish.
 *
 * The rim is stacked drop-shadows, NOT a change to the cell and NOT a change to the meshes:
 *  - Lightening the cell does not work and was measured: an item at L≈0.009 against a cell lifted
 *    to L≈0.012 gets a WORSE ratio, because the two move together. Only a near-grey plate reaches
 *    3:1, and that is a different panel.
 *  - Brightening the materials would change how the items look in the world too.
 *  - It reads from the icon's own ALPHA, so it survives `node tools/icon-shots.mjs` re-rendering
 *    every PNG from its mesh — nothing here is per-item, and there is nothing for a re-render to
 *    undo. That is the constraint a hand-tuned per-icon crop or curve would fail.
 * THE RIM IS GONE, AND SO IS THE PLATE. Three attempts lived here — a blurred halo, a hard 1px
 * stroke, then a pale plate behind every tile — and all three were the panel trying to rescue an
 * icon that had no edge of its own.
 *
 * The icons now carry a baked ink contour, rendered into the PNG from the mesh (see the icon branch
 * of `capture.ts`). That is how Ragnarok's inventory reads: the icon owns its outline, so it works
 * on any ground and the cell is free to be whatever the panel wants — which here means the dark
 * glass it always was. One rule, applied once at render time, for every item present and future.
 *
 * What is left is a soft CAST SHADOW, which sits the item on the cell instead of floating over it. */
/* BRIGHTNESS LIFT. The icons are rendered from the world meshes, and the dark sets (leather,
   iron, pelts) carry the world's shade with them — legible on the ground, murky at 40px. One
   global lift here rather than per-icon edits or brighter materials: it survives every
   `icon-shots.mjs` re-render and never touches how the items look in the world. */
.rpg-icon { width: 100%; height: 100%; object-fit: contain;
  filter: brightness(1.38) contrast(1.04) saturate(1.08) drop-shadow(0 2px 2px rgba(34, 26, 66, .38)); }
/* STACK COUNT. Sat at `right:4px; bottom:2px` as bare text, which put it exactly on the tile's
   rarity rim (an inset ring at the same 4px inset) — so on any item above common the digits
   collided with the coloured line and the last one looked clipped, worst on 3-digit stacks.
   It is now a pill that sits INSIDE the rim with its own backing, so it reads on any icon and at
   any rarity, and `line-height`/`padding` keep the glyphs clear of their own box. */
.rpg-count { position: absolute; right: 5px; bottom: 5px; z-index: 2; pointer-events: none;
  min-width: 15px; padding: 0 4px; border-radius: 999px;
  font-size: 10px; line-height: 15px; text-align: center;
  font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-weight: 600;
  color: var(--ink); background: rgba(6, 8, 20, .82);
  box-shadow: 0 0 0 1px rgba(255,255,255,.10), 0 1px 3px rgba(0,0,0,.6); }
.rpg-drag-ghost { position: fixed; z-index: 60; pointer-events: none; font-size: 26px;
  filter: drop-shadow(0 4px 6px rgba(0,0,0,.6)); transform: translate(-50%,-50%); }
.rpg-drag-ghost .rpg-icon { width: 38px; height: 38px; }
/* RARITY RIM. One `--rarity` custom property per tier, set on the cell, so the rim, the cell's own
   border and its glow all read from a single hue instead of three rules drifting apart — and so the
   equipped SLOTS get the same treatment the bag cells always had (an equipped legendary used to
   look identical to an equipped common). Hues come from items.css, which is the palette of record. */
.rpg-cell[data-rarity="uncommon"],  .rpg-slot[data-rarity="uncommon"]  { --rarity: var(--r-uncommon-c); }
.rpg-cell[data-rarity="rare"],      .rpg-slot[data-rarity="rare"]      { --rarity: var(--r-rare-c); }
.rpg-cell[data-rarity="epic"],      .rpg-slot[data-rarity="epic"]      { --rarity: var(--r-epic-c); }
.rpg-cell[data-rarity="legendary"], .rpg-slot[data-rarity="legendary"] { --rarity: var(--r-legendary-c); }
/* Rarity touches the OUTER BORDER COLOUR and nothing else. The old treatment stacked an inner
   ring on the tile plus a hue wash through the background, which read as a double border and a
   tinted cell; both are gone. One rule, one border, the light plate stays #e6e4ed for every tier. */
.rpg-cell[data-rarity], .rpg-slot[data-rarity] { border-color: var(--rarity, var(--glass-stroke)); }
.rpg-cell[data-rarity]:hover, .rpg-slot[data-rarity]:hover { border-color: var(--rarity); }

/* tooltip */
.rpg-tip { position: fixed; z-index: 61; max-width: 240px; pointer-events: none;
  background: var(--glass-deep); border: 1px solid var(--glass-stroke-hi); border-radius: var(--r-2);
  padding: var(--sp-3); font-size: 13px; backdrop-filter: blur(8px); }
.rpg-tip h4 { margin: 0 0 1px; font-family: var(--font-banner); color: var(--tip-accent, var(--gold-hi)); }
.rpg-tip .stat { color: var(--sage); font-family: var(--font-mono); font-size: 12px; }
.rpg-tip .flavor { color: var(--ink-quill); font-style: italic; margin-top: 4px; }
/* Rarity accent. ONE pair of custom properties per tier, taken from items.css so the bag, the shop
   and the concept sheets cannot disagree about what a tier looks like. Every child that wants the
   colour reads `--tip-accent`; nothing hard-codes a hex. */
.rpg-tip                          { --tip-accent: var(--r-common-c);    --tip-aura: var(--r-common-g); }
.rpg-tip[data-rarity="uncommon"]  { --tip-accent: var(--r-uncommon-c);  --tip-aura: var(--r-uncommon-g); }
.rpg-tip[data-rarity="rare"]      { --tip-accent: var(--r-rare-c);      --tip-aura: var(--r-rare-g); }
.rpg-tip[data-rarity="epic"]      { --tip-accent: var(--r-epic-c);      --tip-aura: var(--r-epic-g); }
.rpg-tip[data-rarity="legendary"] { --tip-accent: var(--r-legendary-c); --tip-aura: var(--r-legendary-g); }
.rpg-tip[data-rarity] { border-color: color-mix(in srgb, var(--tip-accent) 35%, var(--glass-stroke-hi)); }
.rpg-tip .rpg-tip-tier { display: flex; align-items: center; gap: 6px; margin-bottom: 6px;
  font-family: var(--font-banner); font-weight: 500; font-size: 11px; letter-spacing: .16em;
  text-transform: uppercase; color: var(--tip-accent); }
.rpg-tip .rpg-tip-tier::before { content: ""; width: 5px; height: 5px; border-radius: 50%;
  background: var(--tip-accent); box-shadow: 0 0 6px var(--tip-accent); }
/* Level gate — sits under a hairline like the item card's footer. Ember when the character is
   below it, which is the whole point of showing it. */
.rpg-tip .rpg-tip-req { margin-top: 6px; padding-top: 5px; border-top: 1px solid var(--glass-stroke);
  font-family: var(--font-mono); font-weight: 500; font-variant-numeric: tabular-nums; font-size: 11px; letter-spacing: .04em; color: var(--ink-soft); }
.rpg-tip .rpg-tip-req.is-locked { color: var(--ember); }

/* character sheet */
.rpg-stats { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-5); }
.rpg-statrow { display: flex; align-items: center; justify-content: space-between;
  padding: var(--sp-2) var(--sp-3); background: var(--glass); border-radius: var(--r-2); margin-bottom: var(--sp-2); }
.rpg-statrow .val { font-family: var(--font-mono); color: var(--ink); }
.rpg-plus { width: 24px; height: 24px; border-radius: 50%; border: 1px solid var(--gold-deep);
  background: transparent; color: var(--gold); cursor: pointer; }
.rpg-plus:disabled { opacity: .25; cursor: default; }
.rpg-chip { display: inline-block; font-size: 11px; font-family: var(--font-mono); color: var(--ink-mute);
  border: 1px solid var(--glass-stroke); border-radius: var(--r-pill); padding: 1px 8px; margin: 2px; }

/* skill tree */
.rpg-tree { position: relative; display: grid; gap: var(--sp-6) var(--sp-5); justify-content: center; }
.rpg-tree svg.rpg-links { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
/* Column headings, drawn as a row INSIDE the tree grid (row 2 — below the Crafting root, above the
   first discipline tier) rather than a separate grid stacked above the whole thing. See the note in
   WorkerPanel.ts for why: the old placement put the headings above the root, which then read as one
   of the branches instead of as what unlocks all of them. */
.rpg-tree-head { align-self: end; font-size: 10px; letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--gold); text-align: center; }
.rpg-node { position: relative; width: 64px; height: 64px; border-radius: 50%; z-index: 1;
  background: var(--glass-deep); border: 2px solid var(--glass-stroke); color: var(--ink-soft);
  display: grid; place-items: center; cursor: pointer; font-size: 22px; }
.rpg-node.has-ranks { border-color: var(--gold); color: var(--gold-hi); box-shadow: 0 0 12px var(--gold-glow); }
.rpg-node.is-locked { opacity: .4; cursor: not-allowed; }
.rpg-node .rank { position: absolute; bottom: -6px; right: -6px; font-size: 11px; font-family: var(--font-mono);
  background: var(--space-4); border-radius: var(--r-pill); padding: 0 6px; border: 1px solid var(--glass-stroke); }

/* player hp bar (projected under hero) */
.rpg-hpbar { position: fixed; z-index: 35; width: 84px; height: 9px; transform: translate(-50%,0);
  border: 1px solid rgba(7,9,26,.8); border-radius: var(--r-pill); background: rgba(7,9,26,.6);
  overflow: hidden; pointer-events: none; box-shadow: 0 2px 6px rgba(0,0,0,.5); }
.rpg-hpbar .fill { position: absolute; inset: 0; border-radius: var(--r-pill);
  background: linear-gradient(90deg, var(--hp-deep), var(--hp)); transition: width .15s; transform-origin: left; }
.rpg-hpbar.is-hidden { display: none; }

/* --- Shop overlay: full-viewport, big vendor image over the live scene + centred window --- */
.rpg-shop { position: fixed; inset: 0; z-index: 50; display: none; pointer-events: none; }
.rpg-shop.is-open { display: block; pointer-events: auto;
  background: radial-gradient(120% 90% at 50% 40%, rgba(7,9,26,.28), rgba(7,9,26,.55)); }
/* Large vendor portrait composited on the bottom-right, rising from the bottom, beside the window. */
.rpg-shop-vendor { position: absolute; right: 0; left: auto; bottom: 0; height: min(84vh, 760px);
  max-width: 46vw; object-fit: contain; object-position: bottom right; pointer-events: none;
  filter: drop-shadow(0 10px 26px rgba(0,0,0,.55)); z-index: 1; }
/* Window centred in the left ~62% of the viewport (clears the right-side portrait): wares (left) + checkout (right). */
.rpg-shop-window { position: absolute; left: 31%; top: 50%; transform: translate(-50%,-50%); z-index: 2;
  width: min(720px, 60vw); max-height: 88vh; display: flex; flex-direction: column;
  border: 2px solid transparent; border-radius: var(--r-4); padding: var(--sp-5);
  background:
    var(--frame-corners),
    linear-gradient(180deg, rgba(20,17,30,.96), rgba(12,10,22,.97)) padding-box,
    var(--frame-trim) border-box;
  box-shadow: var(--frame-inner-glow), 0 30px 90px rgba(0,0,0,.7); }
.rpg-shop-close { position: absolute; top: 12px; right: 14px; width: 30px; height: 30px; border-radius: 50%;
  background: var(--glass-hi); border: 1px solid var(--glass-stroke); color: var(--ink-soft); cursor: pointer; }
.rpg-shop-close:hover { color: var(--ember); border-color: var(--ember); }
.rpg-shop-title { margin-bottom: var(--sp-3); }
.rpg-shop-name { font-family: var(--font-display); font-size: 26px; color: var(--gold-hi); margin: 0; }
.rpg-shop-tag { font-family: var(--font-quill); font-style: italic; color: var(--ink-quill); margin: 2px 0 0; font-size: 13px; }
.rpg-shop-gold { font-family: var(--font-mono); font-size: 14px; color: var(--ink); margin-top: 6px; }
.rpg-shop-gold i { color: var(--gold); font-style: normal; }
.rpg-shop-tabs { display: flex; gap: 6px; margin-bottom: var(--sp-3); }
.rpg-shop-tab { padding: 6px 20px; cursor: pointer; border-radius: var(--r-2);
  border: 1px solid var(--glass-stroke); background: var(--glass-deep); color: var(--ink-soft); font-size: 13px; }
.rpg-shop-tab.is-active { border-color: var(--gold); color: var(--gold-hi); }
.rpg-shop-cols { display: grid; grid-template-columns: minmax(0,1fr) 260px; gap: var(--sp-4); min-height: 0; flex: 1; }
.rpg-shop-wares { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 6px;
  align-content: start; overflow: auto; padding-right: 4px; }
.rpg-shop-ware { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: var(--r-2);
  background: var(--glass-deep); border: 1px solid var(--glass-stroke); border-left: 3px solid var(--glass-stroke); }
.rpg-shop-ware-icon { width: 30px; height: 30px; flex: 0 0 auto; display: grid; place-items: center; font-size: 22px; }
.rpg-shop-ware-icon img { width: 30px; height: 30px; object-fit: contain; }
.rpg-shop-ware-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.rpg-shop-ware-meta .nm { font-size: 12px; color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rpg-shop-ware-meta .pr { font-family: var(--font-mono); font-weight: 500; font-variant-numeric: tabular-nums; font-size: 11px; color: var(--gold); }
.rpg-shop-ware-meta i, .rpg-shop-gold i, .rpg-shop-total i { font-style: normal; color: var(--gold); }
.rpg-shop-add { flex: 0 0 auto; padding: 4px 10px; border-radius: var(--r-2); border: 1px solid var(--glass-stroke);
  background: var(--glass-hi); color: var(--ink); cursor: pointer; font-size: 12px; }
.rpg-shop-add:disabled { opacity: .45; cursor: default; }
/* checkout column */
.rpg-shop-checkout { display: flex; flex-direction: column; min-height: 0;
  background: var(--glass-deep); border: 1px solid var(--glass-stroke); border-radius: var(--r-2); padding: var(--sp-3); }
.rpg-shop-checkout-head { font-family: var(--font-banner); font-size: 13px; letter-spacing: .05em;
  text-transform: uppercase; color: var(--gold-hi); margin-bottom: var(--sp-2); }
.rpg-shop-cart { list-style: none; margin: 0; padding: 0; overflow: auto; flex: 1; min-height: 40px; }
.rpg-shop-cart-line { display: grid; grid-template-columns: 26px 1fr auto; grid-template-areas: 'ic nm del' 'ic qty pr';
  gap: 2px 6px; align-items: center; padding: 4px 0; border-bottom: 1px solid var(--glass-stroke); }
.rpg-shop-cart-line .rpg-shop-ware-icon { grid-area: ic; width: 26px; height: 26px; font-size: 18px; }
.rpg-shop-cart-line .rpg-shop-ware-icon img { width: 26px; height: 26px; }
.rpg-shop-cart-name { grid-area: nm; font-size: 12px; color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rpg-shop-qty { grid-area: qty; display: flex; align-items: center; gap: 6px; }
.rpg-shop-qty button { width: 18px; height: 18px; line-height: 1; padding: 0; border-radius: 4px;
  border: 1px solid var(--glass-stroke); background: var(--glass-hi); color: var(--ink); cursor: pointer; }
.rpg-shop-qty .q { font-family: var(--font-mono); font-size: 12px; min-width: 14px; text-align: center; }
.rpg-shop-cart-price { grid-area: pr; font-family: var(--font-mono); font-weight: 500; font-variant-numeric: tabular-nums; font-size: 11px; color: var(--gold); text-align: right; }
.rpg-shop-cart-del { grid-area: del; justify-self: end; width: 18px; height: 18px; padding: 0; border-radius: 4px;
  border: 1px solid var(--glass-stroke); background: transparent; color: var(--ink-soft); cursor: pointer; font-size: 11px; }
.rpg-shop-checkout-foot { margin-top: var(--sp-2); }
.rpg-shop-total { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 6px; font-size: 12px; color: var(--ink-soft); }
.rpg-shop-total .amt { font-family: var(--font-mono); font-size: 15px; color: var(--ink); }
.rpg-shop-total .amt.is-short { color: var(--ember); }
.rpg-shop-confirm { width: 100%; padding: 8px 0; border-radius: var(--r-2); border: 1px solid var(--gold);
  background: linear-gradient(180deg, var(--gold), var(--gold-deep, #b8860b)); color: #14110a; font-weight: 600; cursor: pointer; }
.rpg-shop-confirm:disabled { opacity: .5; cursor: default; filter: grayscale(.4); }
.rpg-shop-clear { width: 100%; margin-top: 6px; padding: 5px 0; border-radius: var(--r-2);
  border: 1px solid var(--glass-stroke); background: transparent; color: var(--ink-soft); cursor: pointer; font-size: 12px; }
.rpg-shop-clear:disabled { opacity: .4; cursor: default; }
.rpg-shop-empty { color: var(--ink-quill); font-style: italic; padding: 10px; text-align: center; font-size: 12px; }

/* --- Dialogue overlay: a low, wide conversation box over the LIVE scene (no backdrop) ---
   Same rule as the shop: nothing opaque covers the world, so talking to someone reads as
   leaning in rather than leaving the game. Bottom-centred and clear of the toolbar. */
.rpg-dialogue { position: fixed; inset: 0; z-index: 50; display: none; pointer-events: none; }
.rpg-dialogue.is-open { display: block; pointer-events: auto; }
.rpg-dialogue-window { position: absolute; left: 50%; bottom: 96px; transform: translateX(-50%);
  width: min(720px, 84vw); pointer-events: auto;
  border: 2px solid transparent; border-radius: var(--r-4); padding: var(--sp-5) var(--sp-6);
  background:
    var(--frame-corners),
    linear-gradient(180deg, rgba(20,17,30,.96), rgba(12,10,22,.97)) padding-box,
    var(--frame-trim) border-box;
  box-shadow: var(--frame-inner-glow), 0 30px 90px rgba(0,0,0,.7); }
.rpg-dialogue-close { position: absolute; top: 12px; right: 14px; width: 30px; height: 30px; border-radius: 50%;
  background: var(--glass-hi); border: 1px solid var(--glass-stroke); color: var(--ink-soft); cursor: pointer; }
.rpg-dialogue-close:hover { color: var(--ember); border-color: var(--ember); }
.rpg-dialogue-speaker { font-family: var(--font-display); font-size: 24px; color: var(--gold-hi); }
.rpg-dialogue-quest { font-family: var(--font-banner); font-size: 11px; letter-spacing: .09em;
  text-transform: uppercase; color: var(--ink-soft); margin-top: 2px; }
.rpg-dialogue-text { font-family: var(--font-quill); font-size: 15px; line-height: 1.6;
  color: var(--ink); margin: var(--sp-3) 0 0; }
.rpg-dialogue-progress { font-family: var(--font-mono); font-size: 13px; color: var(--gold);
  margin-top: var(--sp-2); }
.rpg-dialogue-missing { font-family: var(--font-quill); font-size: 13px; color: var(--ink-soft);
  margin: var(--sp-2) 0 0; padding-left: 1.2em; }
.rpg-dialogue-actions { display: flex; gap: var(--sp-2); justify-content: flex-end; margin-top: var(--sp-4); }
.rpg-dialogue-act { padding: 8px 22px; border-radius: var(--r-2); border: 1px solid var(--gold);
  background: var(--glass-hi); color: var(--gold-hi); font-family: var(--font-banner);
  letter-spacing: .05em; font-size: 13px; cursor: pointer; }
.rpg-dialogue-act:hover { box-shadow: 0 0 14px var(--gold-glow); }
.rpg-dialogue-bye { padding: 8px 18px; border-radius: var(--r-2); border: 1px solid var(--glass-stroke);
  background: transparent; color: var(--ink-soft); font-family: var(--font-banner);
  letter-spacing: .05em; font-size: 13px; cursor: pointer; }
.rpg-dialogue-bye:hover { color: var(--ink); border-color: var(--ink-soft); }

/* ── CHAT ────────────────────────────────────────────────────────────────────────────────────
   Bottom-left furniture, NOT an overlay: it stays readable while you play, so it never takes the
   backdrop and never closes on Escape. `pointer-events` is on the panel itself rather than on a
   parent, so the empty screen around it still passes world clicks through. */
.rpg-chat { position: absolute; left: 14px; bottom: 14px; width: 330px; z-index: 20;
  pointer-events: auto; display: flex; flex-direction: column;
  border: 1px solid var(--glass-stroke); border-radius: var(--r-3);
  background: linear-gradient(180deg, rgba(18,14,46,.93), rgba(9,7,24,.95));
  box-shadow: 0 8px 26px -10px rgba(0,0,0,.7); font-family: var(--font-body); }

.rpg-chat-head { position: relative; display: flex; align-items: center; gap: 4px;
  padding: 4px 6px; border-bottom: 1px solid var(--glass-stroke); }
.rpg-chat-tabs { display: flex; gap: 3px; flex: 1 1 auto; }
.rpg-chat-tab { font: 600 10px/1 var(--font-body); letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink-soft); background: transparent; border: 1px solid transparent;
  border-radius: var(--r-pill); padding: 4px 8px; cursor: pointer; }
.rpg-chat-tab:hover { color: var(--ink); }
.rpg-chat-tab.is-active { color: var(--gold-hi); border-color: var(--glass-stroke-hi);
  background: rgba(230,196,109,.10); }

.rpg-chat-cog { flex: 0 0 auto; background: transparent; border: none; cursor: pointer;
  color: var(--ink-soft); font-size: 13px; line-height: 1; padding: 3px 4px; border-radius: 4px; }
.rpg-chat-cog:hover { color: var(--gold-hi); background: rgba(230,196,109,.10); }

.rpg-chat-menu { display: none; position: absolute; right: 4px; top: 100%; z-index: 2; margin-top: 4px;
  min-width: 168px; padding: 6px; border: 1px solid var(--glass-stroke); border-radius: var(--r-3);
  background: linear-gradient(180deg, var(--space-3), var(--space-2));
  box-shadow: 0 10px 24px -8px rgba(0,0,0,.75); }
.rpg-chat-menu.is-open { display: block; }
.rpg-chat-menu-row { display: flex; align-items: center; gap: 7px; padding: 4px 5px; cursor: pointer;
  font-size: 11px; color: var(--ink); border-radius: 4px; }
.rpg-chat-menu-row:hover { background: rgba(255,255,255,.05); }
/* Each switch is tinted like the lines it controls, so the menu is readable at a glance. */
.rpg-chat-menu-row.is-xp { color: #c5a8ff; }
.rpg-chat-menu-row.is-loot { color: #f2c46a; }
.rpg-chat-menu-row.is-stamps { color: #7d8496; }

/* Fixed height so the window never grows and starts covering the hotbar. */
.rpg-chat-log { height: 132px; overflow-y: auto; overflow-x: hidden; padding: 6px 8px;
  font-size: 11.5px; line-height: 1.42; color: var(--ink); }
.rpg-chat-log::-webkit-scrollbar { width: 6px; }
.rpg-chat-log::-webkit-scrollbar-thumb { background: var(--glass-stroke); border-radius: 3px; }
/* EMPTY CHANNEL. The log is a fixed-height box, so with nothing in it the bottom-left corner of the
   screen is a black rectangle for as long as the shard is quiet. The hint is supplied per tab by
   ChatPanel.render via `data-hint`, so Local, Global and Notices each explain themselves. */
.rpg-chat-log.is-empty::before {
  content: attr(data-hint);
  display: block; padding: 10px 4px 0; max-width: 30ch;
  font-family: var(--font-quill, Georgia, serif); font-style: italic; font-size: 12px;
  line-height: 1.5; color: var(--ink-mute); }
.rpg-chat-line { margin: 0 0 2px; word-wrap: break-word; overflow-wrap: anywhere; }
/* SPEAKER colours must not collide with the notice colours below, or a name reads as an XP line.
   Blue for local, pink for global; violet and gold are spoken for. */
.rpg-chat-line .who { color: #8fc9ff; font-weight: 600; }
.rpg-chat-line .who.is-global { color: #ff9ecb; }
/* Timestamps are deliberately the DIMMEST thing in the log: they are a reference you occasionally
   look for, never something to read past. Grey, and grey in every line type. */
.rpg-chat-line .stamp { color: #7d8496; font-family: var(--font-mono); font-weight: 500; font-variant-numeric: tabular-nums; font-size: 11px; }

/* Notices read as the game talking, not as a player talking — and the two KINDS are coloured
   apart, because XP and loot interleave constantly and one shared green made the log a wall of
   identical text. The same two colours label their switches in the cog menu.
   The kind is NAMESPACED (`is-kind-*`), so each kind must name its own colour here: the unprefixed
   class collided with the generic `is-system` marker and needed a :not() chain that would have
   silently coloured a future fourth kind like a refusal. */
.rpg-chat-line.is-kind-xp   { color: #c5a8ff; }   /* violet — matches the XP bar */
.rpg-chat-line.is-kind-loot { color: #f2c46a; }   /* gold — matches loot/gear */
/* Warm amber, deliberately NOT a reward colour: these lines are refusals ("Nowhere to land."), and
   a refusal drawn like a payout is skimmed past. */
.rpg-chat-line.is-kind-system { color: #ffb08a; }

.rpg-chat-entry { border-top: 1px solid var(--glass-stroke); padding: 5px 6px; }
.rpg-chat-input { width: 100%; box-sizing: border-box; background: rgba(7,9,26,.6);
  border: 1px solid var(--glass-stroke); border-radius: var(--r-pill); padding: 5px 9px;
  color: var(--ink); font: 400 11.5px/1.3 var(--font-body); outline: none; }
.rpg-chat-input:focus { border-color: var(--glass-stroke-hi); }
.rpg-chat-input::placeholder { color: var(--ink-dim); }

/* ── CURSOR ──────────────────────────────────────────────────────────────────────────────────
   A custom pointer, because the OS arrow is the one piece of chrome that never matched the game.
   Gold with an ink outline, so it stays readable over both the pale plaza and the dark forest
   floor — a single-colour cursor disappears against one or the other.

   The `, auto` fallback is REQUIRED, not decoration: a url() cursor with no keyword fallback is
   invalid and the whole declaration is dropped, silently. Hotspot is the arrow's own tip (0 0). */
.rpg-ui, canvas, body { cursor: url('../cursors/arrow.png') 0 0, auto; }
/* Anything clickable keeps a normal pointer affordance rather than the world arrow. */
.rpg-ui button, .rpg-btn, .rpg-node, .rpg-slot, .rpg-chat-tab, .rpg-chat-cog,
.rpg-chat-menu-row, .rpg-close { cursor: url('../cursors/interact.png') 0 0, pointer; }
.rpg-chat-input { cursor: text; }

/* ---- HUD quest tracker ------------------------------------------------------------------------
   The card under the minimap: what the world is asking of you, as against the status block on the
   left, which is what you are. Capped at MAX_TRACKED_QUESTS in GameHUD; the full log is one keypress
   away (Q), which is where a tracker should send you rather than trying to be the log itself.
   It carries `.v-page`, so its frame comes from the shared furniture rather than a fourth
   hand-rolled copy — the inline #3b3170 it used to use appears in no design token. */
/* Secondary tier: glanceable furniture, not a page you sit and read. See ORNAMENT TIERS. */
.hud-quests { position: absolute; right: 14px; top: 200px; z-index: 16; pointer-events: none;
  width: 208px; padding: 9px 12px 10px; }
.hud-quests-title { font-family: var(--font-banner); font-size: 10px; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase; color: var(--gold-hi);
  margin-bottom: 6px; }
.hud-quest-line { font-size: 12px; line-height: 1.35; color: var(--ink-soft);
  text-shadow: 0 1px 2px #000; }
.hud-quest-line + .hud-quest-line { margin-top: 5px; }

/* ---- HUD minimap ------------------------------------------------------------------------------
   A LIVING SURVEY bound into the Ledger, and a surveyor's plate is a disc drawn to a compass rose.
   The circle is not decoration: the crop is centred on the player and rotationally symmetric, so a
   round frame tells the truth about what is shown where a rectangle implies a heading the map does
   not have.
   The disc clips; the ring, the compass mark and the zoom controls all sit OUTSIDE that clip, or a
   circular overflow would cut them in half. */
.hud-minimap-disc { position: absolute; inset: 0; border-radius: 50%; overflow: hidden;
  background: #0a0b1a; }
.hud-minimap-disc > canvas { width: 100%; height: 100%; display: block; }
/* The bezel the plate is set into. Gold as a RIM, never a fill (VIL · Colour). */
.hud-minimap-ring { position: absolute; inset: -2px; border-radius: 50%; pointer-events: none;
  border: 2px solid var(--gold, #c9a46e);
  box-shadow: 0 0 14px -3px var(--gold-glow), inset 0 0 18px -6px rgba(0,0,0,.9); }
/* North. The one mark a survey cannot be read without, so it is the one mark that earns its place
   on the rim. The map is north-up (see drawMinimap), so this is a fact and not a decoration. */
.hud-minimap-n { position: absolute; top: -9px; left: 50%; transform: translateX(-50%);
  padding: 0 5px; border-radius: 3px; pointer-events: none;
  background: linear-gradient(180deg, #2b2360, #16113a);
  border: 1px solid var(--gold-deep, #7d5a2e); color: var(--gold-hi, #efdcbb);
  font-family: var(--font-banner); font-size: 10px; font-weight: 600; letter-spacing: .1em;
  line-height: 15px; }
/* Stacked against the disc's lower-right rim, which is where the plate puts them. NOT outside the
   assembly: it is anchored at right:14px, so a column hung past its right edge lands in the margin
   and walks off screen. Overlapping the rim also keeps the control within the same object it
   controls, rather than floating beside it. */
.hud-minimap-zoom { position: absolute; right: -2px; top: 56%;
  display: flex; flex-direction: column; gap: 6px; }
.hud-minimap-btn { width: 26px; height: 26px; padding: 0; line-height: 1;
  font-family: var(--font-banner); font-size: 14px; }

/* ---- HUD hotbar -------------------------------------------------------------------------------
   The rail of things you can reach for. Class-based so it shares the Ledger's page furniture rather
   than a fifth hand-rolled frame.
   NOTE ON XP: the reference plate carries an XP rail beneath this bar as well as an XP row in the
   status block. We deliberately have only the top one. The same number in two places is the
   duplication the restraint gate exists to catch: the bottom rail would earn its place only by
   being the primary readout, and the status block already is. */
/* The two rails sit in one bottom-centred row: keys 1-9 on the left, the mount cell on its own
   rail to the right. A GAP rather than a tenth cell, because the mount is not the same kind of
   thing as the nine — those are what is in your hands, this is what you arrive on. */
.hud-hotbar-row { position: absolute; left: 50%; bottom: 16px; transform: translateX(-50%);
  display: flex; align-items: flex-end; gap: 34px; z-index: 15; pointer-events: none; }
.hud-hotbar { position: relative;
  display: flex; gap: 6px; padding: 10px 13px; pointer-events: none;
  font: 600 13px var(--font-body); }
/* The mount rail: same furniture, one cell wide. */
.hud-hotbar--mount { padding: 10px; }
/* The ammo rail, left of the nine. Same furniture again, one cell wide — it belongs to the bar
   rather than floating beside it, which is the whole reason it is a rail and not a loose badge. */
.hud-hotbar--ammo { padding: 10px; }
/* How many are left, bottom-right of the cell, where a stack count sits everywhere else in the
   game. Its own class rather than the bag's `.rpg-count`: that one is measured for a Ledger cell. */
.hud-slot-count { position: absolute; right: 3px; bottom: 1px; font-family: var(--font-mono);
  font-size: 11px; font-weight: 600; color: var(--ink, #f4ecdc); text-shadow: 0 1px 2px #000; }
/* AMMO ARTWORK SIZE. `object-fit: contain` fits the FILE, not the ink, and `arrow.png` is a 512²
   frame whose arrow occupies only 95×365 of it (18.6% of the width, 71.3% of the height — measured
   from the alpha channel). At the old 30px box that drew a 21px-tall, 5.6px-wide sliver inside a
   46px cell, next to neighbours whose 26px SVGs fill their own viewBox — which is exactly the
   "far too small/thin" complaint.
   The box is now 42px, the same near-full-cell inset the bag's tile uses (52px cell, 4px inset),
   which brings the arrow to ~30px of ink and gives it the same visual weight as slot 1. Sizing the
   BOX rather than cropping the PNG is what survives `node tools/icon-shots.mjs`: a tighter crop
   would be per-icon and the next re-render would silently undo it. */
/* THE ARROW IS PUT ON THE CELL'S DIAGONAL, and scaled past the box.
 *
 * `object-fit: contain` fits the FILE, not the ink, and the arrow's ink is 95x365 in a 512 frame —
 * 18.6% of the width. So even a 42px box inside a 46px cell drew a shaft under 8px wide, which is
 * why raising the box twice did not fix it: the box was never the binding constraint, the empty
 * corners of the PNG were.
 *
 * A square cell's diagonal is ~1.41x its side, so turning the arrow onto it buys length for free,
 * and the scale then spends that on thickness. Done here rather than by cropping the PNG because
 * icons are RENDERED from meshes by tools/icon-shots.mjs — a hand-crop is per-icon and the next
 * regeneration would silently undo it, which is the trap this file has fallen into before. */
.hud-slot--ammo .rpg-icon {
  /* NO ROTATE, NO SCALE — both were compensating for the ICON, in the wrong place.
     `arrow.png` is a near-vertical arrow with wide empty margins either side, so `object-fit:
     contain` fits it by HEIGHT and leaves it looking thin, which is what was reported. The answer
     was a 1.5 scale and a 42-degree turn, and that worked on an unbounded dark cell — on the plate
     it is 63px of content in a 40px box, so it spills the tile.
     The real fix is to re-render the icon so the arrow FILLS its frame diagonally (RO's own icons
     are drawn corner to corner for exactly this reason); until then it simply fits, which the plate
     now makes legible without the size hack. */
  width: 100%; height: 100%; object-fit: contain;
}
.hud-slot--ammo .hud-slot-icon { overflow: visible; }
/* An empty saddle is a PLACEHOLDER, not a state — it reads as "there could be something here",
   which is what greying it says and what a lit icon would not. */
.hud-slot--empty-mount { color: #6a6480; opacity: .72; }
/* AN EMPTY QUIVER IS NOT AN EMPTY SADDLE. The saddle cell greys out because nothing is wrong; this
   one is the reason a bow just did nothing, so it is lit rather than dimmed — a red-hot cell with a
   struck-through quiver and a red 0. Loud enough to find in peripheral vision, static enough to sit
   there for a walk back to town without nagging (the BLINK is what says "just now").
   The cell's border/background/shadow are INLINE styles (see `makeAmmoCell`), so the alarm's are
   written inline too by `setAmmo` rather than fought over with `!important` here — which would also
   have beaten the blink's own keyframes, since `!important` outranks an animation and inline does
   not. This rule owns only what is not inline. */
.hud-slot--ammo-empty { color: #ff7a6a; }
.hud-slot--ammo-empty .hud-slot-count { color: #ff9c8c; }
/* THE BLINK: you tried to shoot with nothing loaded. It pulses while you keep trying and stops
   about a second after you stop (AMMO_BLINK_MS in GameHUD.ts) — a permanent flash is edited out by
   the eye and would still be going while the player is doing something else entirely. */
@keyframes hud-ammo-alarm {
  0%, 100% { box-shadow: inset 0 1px 0 rgba(255,190,170,.18), 0 2px 5px rgba(0,0,0,.45); }
  50%      { box-shadow: 0 0 0 2px rgba(255,120,100,.95), 0 0 18px 4px rgba(255,90,70,.7);
             background: linear-gradient(180deg, rgba(168,44,40,.98), rgba(112,26,30,.98)); } }
/* INFINITE, bounded by the CLASS rather than by an iteration count: the gate is asked every frame
   while a target is held, and a version that restarted the animation on each attempt reset it to
   0% sixty times a second and so never reached the bright half of the cycle at all — measured in
   the live game, where the cell sat visibly still while "flashing". `blinkAmmo` therefore leaves a
   running animation alone and only pushes its end out, so the pulse continues while the player
   keeps trying and stops ~1.1s after they stop. */
.hud-slot.is-ammo-alarm { animation: hud-ammo-alarm 380ms ease-in-out infinite; }
/* LOW AMMO: one amber pulse per shot. `forwards` is deliberately absent — the animation must end
   on the cell's own amber, which `setAmmo` writes inline, so the pulse decays back into the state
   rather than leaving a brighter cell behind it after the last shot. */
@keyframes hud-ammo-low-pulse {
  0%   { box-shadow: inset 0 1px 0 rgba(255,226,170,.20), 0 2px 5px rgba(0,0,0,.45); }
  35%  { box-shadow: inset 0 1px 0 rgba(255,238,200,.55), 0 0 14px 3px rgba(232,170,52,.85); }
  100% { box-shadow: inset 0 1px 0 rgba(255,226,170,.20), 0 2px 5px rgba(0,0,0,.45); }
}
.hud-slot.is-ammo-low-pulse { animation: hud-ammo-low-pulse 300ms ease-out 1; }
@media (prefers-reduced-motion: reduce) { .hud-slot.is-ammo-low-pulse { animation: none; } }
@media (prefers-reduced-motion: reduce) { .hud-slot.is-ammo-alarm { animation: none;
  outline: 2px solid rgba(255,120,100,.95); outline-offset: 1px; } }
.hud-slot svg { display: block; filter: drop-shadow(0 1px 1px rgba(0,0,0,.6)); }
/* The hotbar's inner rail comes from the PRIMARY tier rule above. */
/* Gems at the rail's four midpoints. A horizontal rail has no structure of its own, so these are
   what say where it begins and ends — the same divider motif as the panel titles and the logo. */
.hud-hotbar-gem { position: absolute; }
.hud-hotbar-gem--t { top: -6px; left: 50%; margin-left: -6px; }
.hud-hotbar-gem--b { bottom: -6px; left: 50%; margin-left: -6px; }
.hud-hotbar-gem--l { left: -6px; top: 50%; margin-top: -6px; }
.hud-hotbar-gem--r { right: -6px; top: 50%; margin-top: -6px; }

/* A hotbar cell is a SOCKET, not a swatch. Border colour, background and box-shadow are written
   inline by refreshToolbar/sit/mount, so the bevel here has to live somewhere those do not touch —
   hence ::after, an overlay that survives every state write and keeps the cut edge on all of them. */
.hud-slot { position: relative; width: 46px; height: 46px; border: 2px solid; border-radius: 7px;
  display: flex; align-items: center; justify-content: center; font-size: 26px; }
.hud-slot::after { content: ''; position: absolute; inset: 0; border-radius: 5px;
  pointer-events: none;
  box-shadow:
    inset 0 1px 0 rgba(255,236,198,.20),            /* top lip, lit */
    inset 0 -2px 4px rgba(0,0,0,.55),               /* floor of the socket */
    inset 0 0 0 1px rgba(10,8,26,.75); }            /* the dark line that separates cell from rail */
/* AN ARMED SPELL. The cell pulses in the spell's own ember, so "the key did something" and "the
   next click will cast" are the same signal. Distinct from the boost cell's cyan glow, which is a
   state that is running rather than a mode that is waiting. */
.hud-slot.is-armed { border-color: #ff8a2b !important;
  box-shadow: 0 0 14px rgba(255,138,43,.85), inset 0 1px 0 rgba(255,216,138,.35) !important;
  animation: hud-armed-pulse 1.1s ease-in-out infinite; }
@keyframes hud-armed-pulse {
  0%, 100% { box-shadow: 0 0 10px rgba(255,138,43,.6), inset 0 1px 0 rgba(255,216,138,.3); }
  50%      { box-shadow: 0 0 18px rgba(255,138,43,1), inset 0 1px 0 rgba(255,216,138,.45); } }
@media (prefers-reduced-motion: reduce) { .hud-slot.is-armed { animation: none; } }
/* The keybind, top-left, as the plate has it. Small and quiet: it is a reminder for a hand that
   already knows the slot, not a label for someone reading the bar for the first time. */
.hud-slot-key { position: absolute; top: 1px; left: 3px; font-family: var(--font-mono);
  font-size: 10px; color: var(--ink-soft, #cbc2de); text-shadow: 0 1px 1px #000; }

/* The crest above the rail — a gold hairline with a gem at its heart. */
.hud-hotbar-crest { position: absolute; left: 50%; bottom: 70px; transform: translateX(-50%);
  z-index: 15; pointer-events: none; display: flex; align-items: center; gap: 8px; }

/* ---- Bag: category tabs and footer ------------------------------------------------------------
   A tab strip is an INDEX into a page of the Ledger, so it is set as one: small caps, wide tracked,
   and the active tab marked by a lit gold underline rather than by a filled block. Gold is a line
   and point material (VIL Colour) — a filled gold tab would be the one thing the rule forbids. */
.rpg-tabs { display: flex; flex-wrap: wrap; gap: 2px; margin: 0 0 10px;
  border-bottom: 1px solid rgba(198,160,104,.22); }
.rpg-tab { position: relative; padding: 6px 11px 8px; border: 0; background: none; cursor: pointer;
  font-family: var(--font-banner); font-size: 10px; font-weight: 600; letter-spacing: .13em;
  text-transform: uppercase; color: var(--ink-mute); transition: color .15s; }
.rpg-tab:hover { color: var(--ink-soft); }
.rpg-tab.is-active { color: var(--gold-hi); }
/* The lit segment under the active tab, brighter at its centre — the plate's own treatment. */
.rpg-tab.is-active::after { content: ''; position: absolute; left: 6px; right: 6px; bottom: -1px;
  height: 2px; border-radius: 2px;
  background: linear-gradient(90deg, transparent, var(--gold, #c9a46e), transparent);
  box-shadow: 0 0 8px -1px var(--gold-glow); }

/* Out of the current tab. DIMMED, NOT REMOVED: the slot keeps its place and its index, so the bag
   never reflows under a drag and nothing can be dropped into the wrong cell. It also means a player
   can still see that the thing they are looking for is in there, just filed elsewhere. */
.rpg-cell.is-filtered { opacity: .18; filter: grayscale(1) brightness(.7); }

.rpg-bag-foot { display: flex; align-items: center; justify-content: space-between;
  margin-top: 12px; padding-top: 10px; border-top: 1px solid rgba(198,160,104,.22); }
.rpg-purse { display: flex; align-items: center; gap: 6px; font-family: var(--font-mono);
  font-size: 14px; color: var(--ink); }
.rpg-purse i { font-style: normal; color: var(--gold); text-shadow: 0 0 8px var(--gold-glow); }
.rpg-cap { font-family: var(--font-mono); font-size: 12px; font-variant-numeric: tabular-nums;
  color: var(--ink-mute); }

/* ---- The item card ----------------------------------------------------------------------------
   A leaf of the illustrated catalogue: a thing you READ, which is why it is a panel beside the bag
   rather than a label chasing the pointer. Primary tier — it carries the full flourish set. */
/* Anchored off the INVENTORY panel's edge: the window is min(1056px, 94vw) wide, so the card
   starts at half of that plus a 12px gutter — it moved out with the 20% resize. */
.rpg-itemcard { position: absolute; left: calc(50% + min(540px, 48vw)); top: 50%;
  transform: translateY(-50%); width: 260px; padding: 16px 18px 14px;
  pointer-events: none; display: none; z-index: 41; }
.rpg-itemcard.is-open { display: block; }
/* The tier hue for the card. `ItemDetailCard` writes data-rarity and every accent below reads
   `--r-c`, but nothing ever SET it — so the chip and name sat on their fallbacks for every tier.
   Hues come from items.css, the palette of record. Common keeps the fallback ink deliberately. */
.rpg-itemcard[data-rarity="uncommon"]  { --r-c: var(--r-uncommon-c); }
.rpg-itemcard[data-rarity="rare"]      { --r-c: var(--r-rare-c); }
.rpg-itemcard[data-rarity="epic"]      { --r-c: var(--r-epic-c); }
.rpg-itemcard[data-rarity="legendary"] { --r-c: var(--r-legendary-c); }
/* The rarity chip. A FILLED block is right here and nowhere else on the card: it is the one badge
   whose whole job is to be read before anything else, and it is not gold (VIL keeps gold to lines
   and points) — it takes the tier colour items.css already owns. */
.rpg-itemcard-chip { display: inline-block; padding: 2px 8px; border-radius: 3px;
  font-family: var(--font-banner); font-size: 9px; font-weight: 700; letter-spacing: .16em;
  color: var(--r-c, var(--ink)); border: 1px solid var(--r-c, var(--glass-stroke));
  background: color-mix(in srgb, var(--r-c, #6a5fae) 22%, transparent); }
.rpg-itemcard-name { margin: 9px 0 2px; font-family: var(--font-display); font-size: 20px;
  line-height: 1.15; color: var(--r-c, var(--ink)); }
.rpg-itemcard-type { font-size: 12px; color: var(--ink-mute); margin-bottom: 12px; }
/* The plate gives the illustration real room — it is the only picture on the card and the fastest
   way to recognise the item. Set into the page like a slot, for the same reason. */
/* The same light plate as the bag and equipment slots — one item ground across the window. */
.rpg-itemcard-art { height: 116px; display: grid; place-items: center; font-size: 54px;
  border-radius: var(--r-2); margin-bottom: 12px;
  background: #e6e4ed; border: 1px solid var(--glass-stroke);
  box-shadow: inset 0 2px 5px rgba(20,14,50,.22); }
.rpg-itemcard-art img { width: 88px; height: 88px; object-fit: contain;
  filter: drop-shadow(0 4px 8px rgba(0,0,0,.6)); }
.rpg-itemcard-row { display: flex; justify-content: space-between; align-items: baseline;
  padding: 4px 0; font-size: 13px; color: var(--ink-soft);
  border-bottom: 1px solid rgba(154,134,232,.10); }
.rpg-itemcard-row b { font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  color: var(--ink); }
.rpg-itemcard-row.is-locked b { color: var(--ember, #ff7a52); }
.rpg-itemcard-flavor { margin: 11px 0 0; font-family: var(--font-quill); font-style: italic;
  font-size: 13px; line-height: 1.45; color: var(--ink-quill); }
/* The split-gesture hint, shown only on a stack of more than one. Quieter than flavour text —
   it is chrome speaking, not the item. */
.rpg-itemcard-hint { display: none; margin: 9px 0 0; font-size: 11px; line-height: 1.4;
  color: var(--ink-mute); }
.rpg-itemcard-foot { display: flex; justify-content: space-between; align-items: center;
  margin-top: 13px; padding-top: 10px; border-top: 1px solid rgba(198,160,104,.22); }
.rpg-itemcard-sell { font-size: 12px; color: var(--ink-mute); }
.rpg-itemcard-price { font-family: var(--font-mono); font-size: 14px; color: var(--ink);
  display: flex; align-items: center; gap: 5px; }
.rpg-itemcard-price i { font-style: normal; color: var(--gold); text-shadow: 0 0 8px var(--gold-glow); }

/* ---- Character sheet: the headline trio -------------------------------------------------------
   Attack, Defense and Max HP answer "can I take this fight"; everything below answers "why". They
   were previously three rows of seven, set at the same size as Flee, which stated no hierarchy at
   all. Each sits in its own cut cell so the trio reads as one instrument. */
.rpg-headline { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 18px; }
.rpg-headline-fig { display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 12px 8px 10px; border-radius: var(--r-2);
  background: linear-gradient(180deg, #120e2a, #1c1540);
  box-shadow: inset 0 2px 6px rgba(0,0,0,.7), inset 0 -1px 0 rgba(198,160,104,.18); }
.rpg-headline-fig i { font-style: normal; font-size: 15px; color: var(--gold);
  text-shadow: 0 0 8px var(--gold-glow); line-height: 1; }
.rpg-headline-fig b { font-family: var(--font-mono); font-size: 26px; font-weight: 600;
  font-variant-numeric: tabular-nums; color: var(--ink); line-height: 1.1; }
.rpg-headline-fig span { font-family: var(--font-banner); font-size: 9px; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase; color: var(--ink-mute); }

/* ---- The workbench ----------------------------------------------------------------------------
   Two columns, the same split as the satchel: what you can choose from on the left, what you have
   chosen on the right. The bench slots reuse `.rpg-slot` and the bag stacks reuse `.rpg-cell`, so
   an item looks the same wherever the player meets it — a material that changed appearance between
   the bag and the bench would read as a different material. */
/* THREE COLUMNS: what you have, what you can make, and the one you picked. The ingredients used to
   sit in the middle column under the bench, which put a wall of cells between the search box and the
   recipe list — the two things a player alternates between. As its own rail it is a standing
   inventory of the bench, glanceable without pushing anything else down the page. */
/* The rail's width is a variable because the mats grid's HEIGHT is derived from it — see below. */
.rpg-craft { --ing-col: 208px;
  display: grid; grid-template-columns: var(--ing-col) minmax(0, 1fr) 300px; gap: var(--sp-5);
  align-items: start; }
.rpg-craft-ing { display: grid; gap: var(--sp-2); min-width: 0; }
.rpg-craft-left { display: grid; gap: var(--sp-2); min-width: 0; }
.rpg-craft-search { width: 100%; padding: 8px 11px; border-radius: var(--r-2);
  border: 1px solid var(--glass-stroke); background: var(--glass-deep); color: var(--ink);
  font: 14px var(--font-body); outline: none; }
.rpg-craft-search:focus { border-color: var(--gold); }
/* FOUR ACROSS, FOUR DEEP, then scroll. The height is pinned to exactly four rows so the rail does
   not grow and shrink as materials are gathered and spent — a column that changes height on every
   craft drags the recipe list up and down beside it. `auto-rows` keeps the cells square once the
   grid is scrolling and the tracks stop being sized by content. */
.rpg-craft-mats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  grid-auto-rows: min-content; gap: var(--sp-2);
  /* FOUR ROWS EXACTLY, and the arithmetic collapses to something worth knowing: with four square
     cells across a column of width W and gap g, a cell is (W - 3g)/4, so four rows plus three gaps
     is 4(W - 3g)/4 + 3g = W. The height of a 4x4 square grid is its own width. Writing it as the
     column width means the two can never drift — a hand-typed pixel height cut the fifth row in
     half the first time, which reads as a rendering fault rather than as "there is more below". */
  max-height: var(--ing-col); overflow-y: auto; align-content: start;
  padding-right: 2px; }
/* The empty line has to span the grid or it lays out as one 4-wide cell of text. */
.rpg-craft-mats .rpg-craft-empty { grid-column: 1 / -1; }
/* NOT clickable any more. The bench slots and the click-to-stage gesture are gone, so the rail is a
   readout of what you are carrying, and a pointer cursor on it would promise an action that no
   longer exists. `default` rather than omitting the rule, because `.rpg-cell` sets `pointer`. */
.rpg-craft-mat { cursor: default; }
.rpg-craft-mat:hover { transform: none; border-color: var(--glass-stroke); }
.rpg-craft-list { display: flex; flex-direction: column; gap: 6px; max-height: 38vh; overflow: auto; }
/* THE CLOCK, drawn rather than typed. An emoji here inherits whatever the player's system has and
   lands at a different size and baseline on every platform; a bordered circle with two hands is the
   same everywhere and takes the surrounding text colour. */
.rpg-craft-clock { display: inline-block; position: relative; width: 12px; height: 12px;
  margin-right: 6px; vertical-align: -1px;
  border: 1.5px solid currentColor; border-radius: 50%; opacity: .85; }
.rpg-craft-clock::before, .rpg-craft-clock::after { content: ''; position: absolute;
  background: currentColor; border-radius: 1px; }
/* Hour hand to the top, minute hand to the right — a clock reading two o'clock, which reads as a
   clock at 12px where a symmetrical pair reads as a plus sign. */
.rpg-craft-clock::before { left: 50%; top: 2px; width: 1.5px; height: 3.5px; transform: translateX(-50%); }
.rpg-craft-clock::after { left: 50%; top: 50%; width: 3.5px; height: 1.5px; transform: translateY(-50%); }
.rpg-recipe { display: flex; align-items: center; gap: 10px; padding: 7px 10px; cursor: pointer;
  border-radius: var(--r-2); border: 1px solid var(--glass-stroke); background: var(--glass-deep); }
.rpg-recipe:hover { border-color: var(--gold); }
.rpg-recipe.is-active { border-color: var(--gold); box-shadow: 0 0 10px var(--gold-glow); }
/* DIMMED, NOT HIDDEN: "what do I need for that?" is the question this window answers, and a recipe
   you cannot afford yet is exactly the one being asked about. */
.rpg-recipe.is-dim { opacity: .48; }
.rpg-recipe-glyph { width: 26px; height: 26px; display: grid; place-items: center; font-size: 18px; }
.rpg-recipe-glyph .rpg-icon { width: 26px; height: 26px; }
.rpg-recipe-name { flex: 1 1 auto; font-family: var(--font-display); font-size: 15px; color: var(--ink); }
.rpg-recipe-out { font-family: var(--font-mono); font-size: 12px; color: var(--ink-mute); }
.rpg-craft-detail { padding: var(--sp-4); border-radius: var(--r-2);
  border: 1px solid var(--glass-stroke); background: linear-gradient(180deg, #120e2a, #1c1540); }
.rpg-craft-out { display: flex; align-items: center; gap: 9px; margin: 0 0 var(--sp-3);
  font-family: var(--font-display); font-size: 18px; color: var(--gold-hi); }
.rpg-craft-out .rpg-tile { position: static; width: 30px; height: 30px; inset: auto; }
.rpg-craft-out .rpg-icon { width: 30px; height: 30px; }
.rpg-craft-costs { display: flex; flex-direction: column; gap: 5px; }
.rpg-craft-cost { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--ink-soft); }
.rpg-craft-cost .rpg-tile { position: static; width: 22px; height: 22px; inset: auto; font-size: 15px; }
.rpg-craft-cost .rpg-icon { width: 22px; height: 22px; }
.rpg-craft-cost-name { flex: 1 1 auto; }
.rpg-craft-cost-qty { font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  color: var(--ink); }
.rpg-craft-cost.is-short .rpg-craft-cost-qty { color: var(--ember, #ff7a52); }
.rpg-craft-time { margin: var(--sp-3) 0; font-family: var(--font-quill); font-style: italic;
  font-size: 13px; color: var(--ink-quill); }
.rpg-craft-note { margin: var(--sp-3) 0; font-family: var(--font-quill); font-style: italic;
  font-size: 12px; color: var(--ink-quill); }
.rpg-craft-go, .rpg-craft-cancel { width: 100%; padding: 9px 0; cursor: pointer;
  border-radius: var(--r-2); border: 1px solid var(--gold); background: rgba(201,164,110,.16);
  color: var(--gold-hi); font-family: var(--font-banner); font-size: 12px; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase; }
.rpg-craft-go:hover, .rpg-craft-cancel:hover { background: rgba(201,164,110,.28); }
.rpg-craft-go[disabled] { opacity: .4; cursor: not-allowed; }
.rpg-craft-cancel { border-color: var(--ember, #ff7a52); color: var(--ember, #ff7a52);
  background: rgba(255,122,82,.12); }
.rpg-craft-cancel:hover { background: rgba(255,122,82,.22); }
/* The bar is drawn from the SERVER's two numbers and clamps at full; see CraftPanel. */
.rpg-craft-bar { position: relative; height: 16px; border-radius: var(--r-pill); overflow: hidden;
  border: 1px solid var(--glass-stroke); background: rgba(10,8,26,.8); }
.rpg-craft-bar .fill { position: absolute; inset: 0; width: 0%; border-radius: var(--r-pill);
  background: linear-gradient(180deg, #e9c07a, #b7823c); transition: width .1s linear; }
 /* The label spans the whole bar, so it sits over the filled part for part of the craft and over
    the empty part for the rest. Light ink with a dark halo is legible on both; dark ink was legible
    on the gold and invisible on the well. */
.rpg-craft-bar .label { position: absolute; inset: 0; display: grid; place-items: center;
  font-family: var(--font-mono); font-size: 11px; color: #fff3da;
  text-shadow: 0 1px 2px rgba(0,0,0,.85); }
.rpg-craft-locked, .rpg-craft-empty { margin: var(--sp-3) 2px; font-family: var(--font-quill);
  font-style: italic; font-size: 13px; color: var(--ink-mute); }

/* WHERE DOES THIS GO? The equipped slot an item under the pointer — hovered, or being dragged —
   would fill. Gold like the hover border, but LIT rather than merely outlined: an empty gear slot
   is a dashed grey square, and against forty of them a border colour alone does not carry across
   the width of the window to wherever the player is actually looking.
   Turned solid for the moment it is lit, because the dashes are the "nothing here yet" affordance
   and this cell is, for now, spoken for. */
.rpg-slot.is-target { border-style: solid; border-color: var(--gold);
  box-shadow: 0 0 0 1px var(--gold) inset, 0 0 12px var(--gold-glow, rgba(201,164,110,.4));
  background-color: #fdf6e3; }
/* The label comes back while the slot is lit, even on an occupied one: the cue is answering
   "which slot is that?", and an unlabelled highlight makes the player work it out from position. */
.rpg-slot.is-target .rpg-slot-tag { opacity: 1; }
