/* ============================================================================================
   OVERBOSS UI LANGUAGE
   ============================================================================================

   Tokens and primitives. No screen-specific rules live here — those stay in theme.css and adopt
   these as each surface is converted.

   WHERE THIS CAME FROM
   --------------------
   Not invented, and not borrowed from Diablo 3. Sampled from the game's own painted plates
   (assets/world/*.png), which had already solved the problem the stylesheet had not.

   Clustering the four 3500x1500 plates put every dominant colour in one of two families:

     shadow / stone / sky    hue 200-230   lightness  7-38%
     torchlight / timber     hue  14- 46   lightness 32-59%

   The stylesheet's own tokens sat at hue 258-265 — violet, 30-60 degrees off every scene in the
   game. That gap is why the UI read as though it belonged to a different product. Everything below
   is pulled back onto the art's own axis: cold blue-grey stone carries the surfaces, warm amber
   carries light and attention, and the two never compete because the art never lets them.

   The vocabulary is the art's too: cut stone, black iron, aged gold, cyan rune inlay, amber
   brazier. Colour and material come from here.

   WHAT DIABLO 3 CONTRIBUTES
   -------------------------
   Structure and weight, not colour. Specifically, and corrected after checking rather than
   recalling:

     - The hero window is ONE window. Paper doll in the middle with equipment slots arranged
       around the model, backpack below it, stats beside it behind a Details toggle. Splitting
       "character" and "inventory" into two panels is not a thing Diablo does.
     - No filled title bars anywhere. Panels are near-black behind a heavy metal frame and the
       title floats centred between two rules. A coloured header bar over a grey slab is Windows
       XP's Luna, which is exactly what the first attempt produced.
     - Rarity is light, not outline: a legendary glows from underneath rather than wearing a
       coloured border.
     - Chrome heavy enough to read while you are busy dying.

   See the dock layer at the bottom of this file.

   SCALES
   ------
   The stylesheet had 31 font sizes, 59 paddings, 53 shadows, 26 gaps and 15 radii. That is a
   palette with no scale, and it is why nothing lines up between screens. Each scale below is
   deliberately short — if a value is missing, the answer is usually the neighbouring step, not a
   new token.
   ============================================================================================ */

:root {
  /* Runtime PNGs are exported from assets/ui/cursors/cursor-sheet.png. */
  --cursor-navigate: url('/assets/ui/cursors/runtime/cursor-navigate.png') 3 1, default;
  --cursor-interact: url('/assets/ui/cursors/runtime/cursor-interact.png') 3 1, pointer;
  --cursor-target: url('/assets/ui/cursors/runtime/cursor-target.png') 16 15, crosshair;
  --cursor-hostile: url('/assets/ui/cursors/runtime/cursor-hostile.png') 16 15, crosshair;
  --cursor-heal: url('/assets/ui/cursors/runtime/cursor-heal.png') 16 17, crosshair;
  --cursor-pick-up: url('/assets/ui/cursors/runtime/cursor-pick-up.png') 16 23, grab;
  --cursor-carry: url('/assets/ui/cursors/runtime/cursor-carry.png') 16 19, grabbing;
  --cursor-unavailable: url('/assets/ui/cursors/runtime/cursor-unavailable.png') 3 1, not-allowed;

  /* ---- Stone. Surfaces, in the order they stack out of the dark. ------------------------- */
  --stone-abyss:    #05101a;  /* behind everything; the night beyond the plate */
  --stone-deep:     #0b1726;  /* sunken wells: slot backgrounds, inset tracks */
  --stone-base:     #17223a;  /* the default panel slab */
  --stone-raised:   #212e4a;  /* raised within a panel: rows, headers, hover */
  --stone-edge:     #404761;  /* mortar line, the lit top edge of cut stone */
  --stone-highlight:#52576f;  /* the catch of light on a bevel */

  /* ---- Iron. Chain, bracket, cage — the hard edges between stone and air. ---------------- */
  --iron-dark:      #0f131a;
  --iron:           #2a3040;
  --iron-light:     #4a5163;

  /* ---- Banner. Navy cloth carrying a gold sigil; the plates hang these everywhere. ------- */
  --banner:         #193264;
  --banner-lit:     #204a9c;

  /* ---- Light. Amber is the only warm source in the world and the only one here. ---------- */
  --torch:          #c3844b;  /* brazier flame at rest */
  --torch-bright:   #e8a765;  /* flame core, hover, focus */
  --gold-aged:      #7f6d2f;  /* sigil metal in shadow */
  --gold-lit:       #d4af52;  /* sigil metal catching the fire */
  /* The same colour as r,g,b, which is the only form that can take an alpha: a property
     holding #d4af52 cannot. Twenty-eight rules across these two files wrote it out as
     rgba(var(--gold-rgb), …) by hand, so changing the gold changed twenty-seven of them. */
  --gold-rgb:       212, 175, 82;

  /* ---- Rune. The cyan inlay glowing in the arena stone. Interaction and focus only. ------ */
  --rune:           #3683e5;
  --rune-bright:    #6fb3ff;

  /* ---- Signal. Reserved for meaning, never decoration. ----------------------------------- */
  --ember:          #d74315;  /* danger, damage taken, destructive action */
  --vitality:       #3f9f5f;  /* restoration: healing numbers, heal-target affordances */

  /* Health itself is crimson, on every bar that shows it — yours, your party's, the boss's. It was
     green for heroes and crimson for the boss, which made the single most-read shape on the screen
     mean two different things depending on whose it was. --vitality stays green and keeps the jobs
     that are about HEALING rather than health: the floating heal number, the healable-ally rim.

     Three stops rather than one because a bar this size reads as metal: a catch of light along the
     top, the body, and the shadow it sits in. */
  --health-lit:     #b8354a;
  --health:         #8c1528;
  --health-deep:    #530a19;
  --warning:        #d9a227;

  /* ---- Ink. ------------------------------------------------------------------------------ */
  --ink:            #e8eef7;  /* primary text on stone */
  --ink-dim:        #93a3bd;  /* secondary */
  --ink-faint:      #5e6d85;  /* disabled, watermark */
  --ink-on-light:   #10151f;

  /* ---- Rarity. Kept on the art's axis: amber and gold lead, cyan for magic. --------------
     These carry game meaning, so they stay distinguishable at a glance and in peripheral
     vision — the reason legendary is the brightest thing on any screen. */
  --rarity-common:    #9fb0c4;
  --rarity-rare:      #4aa3e8;
  --rarity-epic:      #a86fd6;
  --rarity-legendary: #e08a2e;
  --rarity-mythic:    #e0484f;

  /* ---- Type. Cinzel for anything carved or proclaimed; Inter for anything read. ----------
     The old UI had ten sizes below 12px. A step below --fs-sm is not a design decision, it is a
     legibility bug, so the scale simply stops there. */
  --font-display: 'Cinzel', 'Trajan Pro', serif;
  --font-body: 'Inter', system-ui, sans-serif;
  --font-numeric: 'Inter', system-ui, sans-serif;

  --fs-xs:   12px;  /* the floor. sub-labels, units, timers */
  --fs-sm:   13px;  /* secondary body */
  --fs-base: 15px;  /* body */
  --fs-lg:   18px;  /* panel headers, item names */
  --fs-xl:   24px;  /* screen titles */
  --fs-2xl:  34px;  /* boss names, the reward moment */

  --lh-tight: 1.15;
  --lh-base: 1.45;
  --tracking-wide: 0.08em;   /* small caps labels */
  --tracking-display: 0.04em;

  /* ---- Space. A 4px base; every gap and pad is one of these. ----------------------------- */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-7: 48px;

  /* ---- Radius. Cut stone is not round. Small, deliberate, mostly square. ------------------ */
  --r-sharp: 2px;   /* stone, slots, panels */
  --r-soft:  5px;   /* buttons, chips */
  --r-round: 999px; /* orbs, pips only */

  /* ---- Depth. Three ways light lands on stone, and one way it leaves. -------------------- */
  --bevel-up:   inset 0 1px 0 rgba(255,255,255,.10), inset 0 -1px 0 rgba(0,0,0,.55);
  --bevel-down: inset 0 2px 5px rgba(0,0,0,.72), inset 0 -1px 0 rgba(255,255,255,.05);
  --cast:       0 6px 18px rgba(0,0,0,.55);
  --cast-deep:  0 14px 40px rgba(0,0,0,.72);
  --glow-rune:  0 0 14px rgba(54,131,229,.45);
  --glow-torch: 0 0 18px rgba(195,132,75,.40);

  --edge-hair: 1px solid var(--stone-edge);
  --edge-iron: 1px solid var(--iron);

  --t-fast: .12s ease;
  --t-base: .2s ease;

  /* How wide a rail is. One definition, used by the width, by the closed transform and by the
     media query that decides when a rail simply takes the screen. */
  /* GEAR IS ONE SIZE EVERYWHERE, AND THE BAG DECIDES WHAT THAT SIZE IS.
     Used by the bag, the paper doll, the Forge's picker, the coffer and the comparison tooltip.
     They had four icon sizes between them (44, 42, 46, 58) and the bag's socket was not even
     square — its width came from a 1fr grid track, so the same sword measured 68x58 at one rail
     width and 51x58 at another.

     The value here is only a floor. A row of squares can never divide a fluid rail evenly and
     SOMETHING has to absorb the remainder: widen the gaps and the bag reads as scattered, push it
     to one end and the well has a bald patch down one side. So the socket absorbs it, and
     fitItemSockets() in ui/SocketGrid.js writes the measured size back here — to the document,
     because the doll holds the same object and has to hold it at the same size. The tile is always
     the socket less its seat, so one number moves both. */
  --item-socket: 56px;
  --item-seat:   10px;
  --bag-gap:     4px;
  --item-icon:   calc(var(--item-socket) - var(--item-seat));

  --rail-w: clamp(320px, 27vw, 460px);

  /* The close tab protruding from a rail's inner edge. A token because three rules need it and one
     of them is the closed transform — a rail that slides out by its own width alone leaves the tab
     poking out past the screen edge. */
  --rail-tab-w: 24px;

  /* How much of the bottom edge a rail leaves clear.
     Rails overlay the screen and no longer push the edges aside, so without this the hero panel
     would bury the one row that is nothing but controls — chat, the Hero button, the menu. Sized
     to that row: 16px inset + 8px pad + a 38px button + 8px pad, rounded up. */
  --edge-reserve-bottom: 80px;
}

/* ============================================================================================
   TYPE
   ============================================================================================ */

.ui-display { font-family: var(--font-display); font-weight: 700; letter-spacing: var(--tracking-display); line-height: var(--lh-tight); }
.ui-title   { font-family: var(--font-display); font-weight: 700; font-size: var(--fs-xl); letter-spacing: var(--tracking-display); color: var(--ink); }
.ui-heading { font-family: var(--font-display); font-weight: 600; font-size: var(--fs-lg); color: var(--ink); }
.ui-body    { font-family: var(--font-body); font-size: var(--fs-base); line-height: var(--lh-base); color: var(--ink); }
.ui-muted   { color: var(--ink-dim); }

/* Small caps label. The one place tracking is wide — it is what makes a 12px label read as a
   label rather than as body text that someone shrank. */
.ui-label {
  font-family: var(--font-body);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--ink-dim);
}

/* Any number that changes while you watch it. Tabular figures stop the layout twitching. */
.ui-numeric { font-family: var(--font-numeric); font-variant-numeric: tabular-nums; font-weight: 600; }

/* ---- Scrollbars ---------------------------------------------------------------------------
   The last piece of default browser chrome on screen: a light grey capsule on a rounded track,
   sitting inside panels made of black iron. Same material as everything else now — a sunken
   channel with a bar of iron in it, catching the same light as a .ui-slot does.

   Two syntaxes, and they must not both apply. Chromium now supports scrollbar-width/color as well
   as ::-webkit-scrollbar, and setting either standard property makes it DROP the pseudo-elements
   entirely, whatever the order — `scrollbar-width: thin` alone turned this into a 2px hairline with
   none of the detail below. So the standard properties are fenced behind a test only Firefox
   passes: it has scrollbar-width and no ::-webkit-scrollbar. */
::-webkit-scrollbar { width: 12px; height: 12px; }
::-webkit-scrollbar-track {
  background: var(--stone-abyss);
  box-shadow: inset 1px 0 3px rgba(0,0,0,.8);
}
::-webkit-scrollbar-thumb {
  background: linear-gradient(90deg, var(--iron), var(--iron-light));
  border: 1px solid #000;
  border-radius: var(--r-sharp);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.12);
}
::-webkit-scrollbar-thumb:hover { background: linear-gradient(90deg, var(--iron-light), var(--stone-highlight)); }
::-webkit-scrollbar-corner { background: var(--stone-abyss); }

/* Firefox: no pseudo-elements to draw, so it gets the colours and a narrow bar. */
/* THE THIN SCROLL — for anything that scrolls INSIDE a panel.
   The 12px iron bar above is furniture: right for a window, far too heavy for a column within one.
   A spread carrying a heavy bar down its index and a thin one down its page reads as two panels
   that happen to be touching.
   It takes the panel's own --accent, so the book's gold, the Forge's fire and the Spire's violet
   each keep their own, and no panel has to state a colour to get the right one. */
.ui-thin-scroll::-webkit-scrollbar { width: 6px; height: 6px; }
.ui-thin-scroll::-webkit-scrollbar-track { background: transparent; }
.ui-thin-scroll::-webkit-scrollbar-thumb {
  border-radius: 3px;
  background: color-mix(in srgb, var(--accent, var(--iron-light)) 26%, transparent);
}
.ui-thin-scroll::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--accent, var(--iron-light)) 46%, transparent); }

@supports (scrollbar-width: thin) and (not selector(::-webkit-scrollbar)) {
  * { scrollbar-width: thin; scrollbar-color: var(--iron-light) var(--stone-abyss); }
  .ui-thin-scroll { scrollbar-color: color-mix(in srgb, var(--accent, var(--iron-light)) 46%, transparent) transparent; }
}


/* Text for a screen reader and nothing else. Not display:none and not visibility:hidden — both of
   those take it out of the accessibility tree as well, which is the one place it needs to be. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* ============================================================================================
   PANEL — a dark slab in a bronze frame
   ============================================================================================

   Rebuilt. The first attempt put a filled blue gradient bar across the top of a grey-blue slab,
   which is Windows XP's Luna, not Diablo. Diablo has no filled title bars anywhere: panels are
   near-black with a heavy metal frame, and the title floats centred between two rules. Contrast
   does the work a coloured header was doing badly.
   ============================================================================================ */

.ui-panel {
  position: relative;
  /* Near-black, vignetted so the middle sits back and the frame catches the light. */
  background:
    radial-gradient(ellipse at 50% 0%, rgba(64,71,97,.20), transparent 62%),
    linear-gradient(180deg, #0c1420 0%, #070d16 55%, #05090f 100%);
  border: 2px solid var(--iron);
  border-radius: var(--r-sharp);
  box-shadow:
    inset 0 0 0 1px rgba(var(--gold-rgb), .22),
    inset 0 2px 20px rgba(0,0,0,.9),
    var(--cast-deep);
  color: var(--ink);
}

/* Corner brackets. Drawn rather than art so they hold at any size; frame art replaces them later
   without the markup changing. */
.ui-panel::before, .ui-panel::after,
.ui-panel__body::before, .ui-panel__body::after {
  content: ''; position: absolute; width: 16px; height: 16px; pointer-events: none;
  border: 2px solid var(--gold-aged);
}
.ui-panel::before { top: 4px; left: 4px; border-right: 0; border-bottom: 0; }
.ui-panel::after  { top: 4px; right: 4px; border-left: 0; border-bottom: 0; }
.ui-panel__body::before { bottom: 4px; left: 4px; border-right: 0; border-top: 0; }
.ui-panel__body::after  { bottom: 4px; right: 4px; border-left: 0; border-top: 0; }

/* Title: centred gold caps between two rules. No fill behind it. */
.ui-panel__header {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5) var(--sp-2);
  font-family: var(--font-display);
  font-weight: 700; font-size: var(--fs-lg);
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--gold-lit);
  text-shadow: 0 2px 4px #000, 0 0 16px rgba(var(--gold-rgb), .25);
  background: none; border: 0;
}
.ui-panel__header::before, .ui-panel__header::after {
  content: ''; flex: 1; height: 1px;
  background: linear-gradient(90deg, transparent, var(--gold-aged), transparent);
}

/* A rule with a turned lozenge in the middle of it, for breaking one stretch of a panel from the
   next where a heading would be too much. Same aged gold as a panel header's rules, but each half
   runs toward the mark rather than away from it, so the join is the brightest part instead of the
   faintest. */
.ui-flourish {
  display: flex; align-items: center; gap: var(--sp-2);
  margin: var(--sp-3) 0 var(--sp-2);
}
.ui-flourish::before, .ui-flourish::after { content: ''; flex: 1; height: 1px; }
.ui-flourish::before { background: linear-gradient(90deg, transparent, var(--gold-aged)); }
.ui-flourish::after  { background: linear-gradient(90deg, var(--gold-aged), transparent); }
.ui-flourish__mark {
  width: 7px; height: 7px;
  transform: rotate(45deg);
  border: 1px solid var(--gold-aged);
  background: rgba(var(--gold-rgb), .18);
}

.ui-panel__body { position: relative; padding: var(--sp-3) var(--sp-5) var(--sp-5); }

.ui-well {
  background: rgba(0,0,0,.45);
  border: 1px solid rgba(0,0,0,.8);
  border-radius: var(--r-sharp);
  box-shadow: var(--bevel-down), inset 0 0 0 1px rgba(var(--gold-rgb), .08);
}

/* ============================================================================================
   SLOT — a recessed socket
   ============================================================================================

   Rarity is a glow behind the item, the way Diablo lights a legendary from underneath. The first
   attempt used a coloured border, which made a full bag read as a spreadsheet with conditional
   formatting.
   ============================================================================================ */

.ui-slot {
  position: relative;
  display: grid; place-items: center;
  /* A socket is usually a <button>, and the UA's own padding is not decoration: it becomes the
     element's min-content width, which silently sets the floor for any grid track holding one.
     A 32px icon in an unreset button measures 46px and overflows a five-column bag. */
  padding: 0; margin: 0; font: inherit; color: inherit; appearance: none; -webkit-appearance: none;
  width: var(--item-socket); height: var(--item-socket);
  background: linear-gradient(180deg, #05090f, #0a111c);
  border: 1px solid #000;
  border-radius: var(--r-sharp);
  box-shadow: inset 0 2px 6px rgba(0,0,0,.95), inset 0 -1px 0 rgba(255,255,255,.05);
  transition: box-shadow var(--t-fast);
  cursor: var(--cursor-interact);
}
.ui-slot:hover { box-shadow: inset 0 2px 6px rgba(0,0,0,.95), 0 0 0 1px var(--gold-aged); }
.ui-slot:focus-visible { outline: none; box-shadow: inset 0 2px 6px rgba(0,0,0,.95), 0 0 0 2px var(--rune-bright), var(--glow-rune); }
.ui-slot--empty { cursor: var(--cursor-navigate); }
/* The art and socket hover already identify items. A steady arrow avoids hand/arrow
   flicker as the pointer crosses the narrow gutters of the inventory grid. */
.bag-cell, .doll-slot:not(.ui-slot--empty) { cursor: var(--cursor-navigate); }
.ui-slot--empty:hover { box-shadow: inset 0 2px 6px rgba(0,0,0,.95), inset 0 -1px 0 rgba(255,255,255,.05); }

.ui-slot[data-rarity]::before {
  content: ''; position: absolute; inset: 0; border-radius: var(--r-sharp); pointer-events: none;
  background: radial-gradient(ellipse at 50% 100%, var(--rarity-glow, transparent), transparent 72%);
}
.ui-slot[data-rarity="common"]    { --rarity-glow: rgba(159,176,196,.20); --rarity-lit: var(--rarity-common); }
.ui-slot[data-rarity="rare"]      { --rarity-glow: rgba(74,163,232,.42); --rarity-lit: var(--rarity-rare); }
.ui-slot[data-rarity="epic"]      { --rarity-glow: rgba(168,111,214,.45); --rarity-lit: var(--rarity-epic); }
.ui-slot[data-rarity="legendary"] { --rarity-glow: rgba(224,138,46,.62); --rarity-lit: var(--rarity-legendary); }
.ui-slot[data-rarity="mythic"]    { --rarity-glow: rgba(224,72,79,.62); --rarity-lit: var(--rarity-mythic); }

/* The glow burns INSIDE the tile.
   The tile is opaque and fills most of the socket, so the radial above survives only in the few
   pixels of recess around it — not enough to tell an epic from a rare across a full bag. Thrown
   outwards it did read, but it spilled over the 4px gap and pooled with its neighbours, so a row
   of epics lit the whole row rather than each item lighting itself. Inset, it stays in its own
   square: a coloured haze around the inside of the art, which reads as the item glowing instead of
   the socket being tinted.

   Still not a border — no hard edge, nothing at full opacity — and Common is left dark on
   purpose, because it is the default and the eye should only catch what is worth catching. */
.ui-slot[data-rarity] .item-art {
  box-shadow: var(--bevel-up),
              inset 0 0 11px 1px color-mix(in srgb, var(--rarity-lit, transparent) 70%, transparent),
              0 1px 3px rgba(0,0,0,.6);
}
.ui-slot[data-rarity="common"] .item-art {
  box-shadow: var(--bevel-up), 0 1px 3px rgba(0,0,0,.6);
}

/* The tile that goes in the socket.
   Framed like an ability icon (.ability-socket) and for the same reason: they are the same kind of
   object seen in the same kind of hole — a painted square you can pick up — and a player should
   not have to learn two vocabularies for it. Raised, so it reads as sitting IN the recess rather
   than being painted on the bottom of it.

   No rarity colour on this edge. Rarity is the glow underneath the socket; a coloured border is
   the spreadsheet-with-conditional-formatting the note above warns about, and it was on this tile
   at 2px in one panel, 1px in another and absent in a third. */
.item-art {
  position: relative; z-index: 1; flex: none;
  width: var(--item-icon); height: var(--item-icon);
  background-color: #070d16;
  background-repeat: no-repeat;
  border: 1px solid var(--iron-light);
  border-radius: var(--r-sharp);
  box-shadow: var(--bevel-up), 0 1px 3px rgba(0,0,0,.6);
}

.ui-slot__count {
  position: absolute; right: 3px; bottom: 1px; z-index: 1;
  font-family: var(--font-numeric); font-variant-numeric: tabular-nums;
  font-size: var(--fs-xs); font-weight: 700; color: var(--ink);
  text-shadow: 0 1px 2px #000;
}

/* What a socket on a vendor's shelf costs. Same corner and same numerals as the stack count, in
   gold, because the two never appear on the same socket: one is a thing you own, the other is not. */
.ui-slot__price {
  position: absolute; right: 3px; bottom: 1px; z-index: 1;
  font-family: var(--font-numeric); font-variant-numeric: tabular-nums;
  font-size: var(--fs-xs); font-weight: 800; color: var(--gold-lit);
  text-shadow: 0 1px 2px #000;
}

/* ============================================================================================
   PORTRAIT — the hero's own sprite, in DOM
   ============================================================================================

   No new art and no PixiJS instance in a panel: the runtime hero atlas is a 1280x512 sheet of
   256x256 frames at known offsets, so a background-position crop is the whole implementation.
   --portrait-x / --portrait-y come straight from sprites.json.

   The frames carry transparent margin, which is what the art extents baked into sprites.json
   measure. Scaling the sheet up slightly and nudging it lets the figure fill the frame instead of
   floating in a box of nothing.
   ============================================================================================ */

.ui-portrait {
  position: relative;
  aspect-ratio: 1 / 1;
  background-image: url('/assets/sprites/runtime/heroes-sheet.png');
  background-repeat: no-repeat;
  /* The sheet is 5 frames wide and 2 tall, so one frame fills the box at 500% / 200%.
     Percentage background-position resolves against (container - image), NOT as a plain offset:
     at 500% width the image is 5x the box, so the span is -4x and frame n needs n/4 of it. Same
     reasoning gives n/1 down a 2-row sheet. Passing pixel offsets here silently shows frame 0. */
  background-size: 500% 200%;
  background-position: calc(var(--portrait-col, 0) / 4 * 100%) calc(var(--portrait-row, 0) * 100%);
  image-rendering: auto;
  border: 1px solid rgba(0,0,0,.7);
  border-radius: var(--r-sharp);
  box-shadow: inset 0 2px 14px rgba(0,0,0,.9);
  /* Lit from below like everything else the braziers touch. */
  background-color: #070d16;
}
.ui-portrait::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(ellipse at 50% 108%, rgba(195,132,75,.22), transparent 62%);
}

/* ============================================================================================
   BUTTON
   ============================================================================================ */

/* `hidden` is display:none from the browser's own sheet, which any class that sets `display` wins
   against — so every `btn.hidden = true` in the app was quietly doing nothing here. .btn already
   carried this line for the same reason (theme.css); .ui-btn was missing it, which is why Ascend,
   Create New Hero and the Tavern's Recruit button all stayed on screen after being switched off. */
.ui-btn[hidden] { display: none; }

/* A raised metal face seated in a black iron frame. Variant tokens also drive hover,
   so a primary/danger button keeps its material in every interaction state. */
.ui-btn {
  --btn-top: #354057; --btn-bottom: #141e30;
  --btn-rim: #647085; --btn-light: #a6b4c9; --btn-glow: rgba(111,179,255,.12);
  position: relative; isolation: isolate; box-sizing: border-box;
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
  min-height: 38px; padding: var(--sp-2) var(--sp-4);
  font-family: var(--font-body); font-size: var(--fs-sm); font-weight: 700;
  letter-spacing: var(--tracking-wide); text-transform: uppercase;
  color: var(--ink); text-shadow: 0 1px 2px #000;
  border: 1px solid #080c12; border-radius: var(--r-sharp);
  background:
    linear-gradient(110deg, transparent 20%, rgba(255,255,255,.055) 46%, transparent 70%),
    linear-gradient(180deg, var(--btn-top), var(--btn-bottom));
  box-shadow: inset 0 0 0 1px var(--btn-rim), inset 0 2px 0 var(--btn-light),
    inset 0 4px 0 rgba(255,255,255,.07), inset 0 -3px 0 rgba(0,0,0,.5),
    0 3px 0 #05080d, 0 5px 8px rgba(0,0,0,.45);
  cursor: var(--cursor-interact); touch-action: manipulation;
  transition: filter var(--t-fast), transform var(--t-fast), box-shadow var(--t-fast);
}
/* An inset hairline leaves a broad bevel around the face without extra markup. */
.ui-btn::before {
  content: ""; position: absolute; inset: 4px; border: 1px solid rgba(0,0,0,.35);
  border-bottom-color: rgba(255,255,255,.09); border-radius: 1px; pointer-events: none;
}
.ui-btn:hover:not(:disabled):not([aria-disabled="true"]) {
  filter: brightness(1.18);
  box-shadow: inset 0 0 0 1px var(--btn-rim), inset 0 2px 0 var(--btn-light),
    inset 0 4px 0 rgba(255,255,255,.09), inset 0 -3px 0 rgba(0,0,0,.5),
    0 3px 0 #05080d, 0 5px 12px rgba(0,0,0,.5), 0 0 14px var(--btn-glow);
}
.ui-btn:active:not(:disabled):not([aria-disabled="true"]) {
  transform: translateY(2px); filter: brightness(.94);
  box-shadow: inset 0 0 0 1px var(--btn-rim), inset 0 3px 6px rgba(0,0,0,.7),
    inset 0 -1px 0 rgba(255,255,255,.12), 0 1px 0 #05080d;
}
.ui-btn:focus-visible { outline: 2px solid var(--rune-bright); outline-offset: 3px; }
.ui-btn--primary {
  --btn-top: #886031; --btn-bottom: #412b19;
  --btn-rim: var(--gold-aged); --btn-light: var(--torch-bright); --btn-glow: rgba(195,132,75,.28);
  color: #fff0cb; font-family: var(--font-display);
}
.ui-btn--danger {
  --btn-top: #813a29; --btn-bottom: #391912;
  --btn-rim: #9a5037; --btn-light: #d68c65; --btn-glow: rgba(215,67,21,.2);
  color: #ffe9e2;
}
.ui-btn--ghost {
  --btn-top: #18212c; --btn-bottom: #101720;
  --btn-rim: #35404f; --btn-light: #505b68; --btn-glow: transparent;
  color: var(--ink-dim);
}
.ui-btn:disabled, .ui-btn[aria-disabled="true"] {
  color: #7d899b; background: #151d29; border-color: #0a1018;
  box-shadow: inset 0 0 0 1px #303947, inset 0 2px 4px rgba(0,0,0,.4);
  text-shadow: none; cursor: var(--cursor-unavailable);
}
.ui-btn--sm { min-height: 30px; padding: var(--sp-1) var(--sp-3); font-size: var(--fs-xs); }
.ui-btn--lg { min-height: 48px; padding: var(--sp-3) var(--sp-5); font-size: var(--fs-base); }
@media (prefers-reduced-motion: reduce) { .ui-btn { transition: none; } }

/* ============================================================================================
   TAG — the world's station markers and any inline status
   ============================================================================================ */

.ui-tag {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  padding: 3px var(--sp-2);
  font-family: var(--font-body); font-size: var(--fs-xs); font-weight: 700;
  letter-spacing: var(--tracking-wide); text-transform: uppercase;
  color: var(--ink-dim);
  background: rgba(11,23,38,.85);
  border: 1px solid var(--iron);
  border-radius: var(--r-soft);
}
.ui-tag--rune  { color: var(--rune-bright); border-color: rgba(54,131,229,.55); }
.ui-tag--torch { color: var(--torch-bright); border-color: rgba(195,132,75,.55); }
.ui-tag--gold  { color: var(--gold-lit); border-color: var(--gold-aged); }
.ui-tag--ember { color: #ffb4a0; border-color: rgba(215,67,21,.6); }
/* A tag is inline-flex, which beats the hidden attribute on its own. Anything that toggles one
   with .hidden = true needs this or the tag simply never goes away. */
.ui-tag[hidden] { display: none; }
/* The optional glyph that leads a tag. It takes the tag's colour, so a variant only has to say
   the colour once. */
.ui-tag i { font-style: normal; font-size: 1.15em; line-height: 1; }
/* For a tag that has to be noticed rather than merely read — a state that is hurting you right
   now. It is the only motion a tag is allowed, and a row where several pulse at once is a row
   where none of them do, so keep it to the state that changes what you press next. */
.ui-tag.is-urgent { animation: ui-tag-urgent 1s infinite; }
@keyframes ui-tag-urgent { 0%, 100% { opacity: 1; } 50% { opacity: .5; } }

/* ============================================================================================
   BAR — health, resource, experience, cast
   ============================================================================================ */

.ui-bar {
  position: relative; overflow: hidden;
  height: 18px;
  background: var(--stone-deep);
  border: 1px solid var(--iron);
  border-radius: var(--r-sharp);
  box-shadow: var(--bevel-down);
}
.ui-bar__fill {
  position: relative; z-index: 1;
  height: 100%; width: 0;
  /* Crimson by default, because the unmodified bar is a health bar and health is crimson on every
     bar in the game that shows it. --vitality is green and keeps the jobs that are about HEALING
     rather than health. Anything that is not health takes a variant. */
  background: linear-gradient(180deg, var(--health-lit), var(--health) 55%, var(--health-deep));
  box-shadow: inset 0 1px 0 rgba(255,255,255,.22);
  transition: width var(--t-base);
}

/* THE TRAIL — what a bar leaves behind when it drops.
   ------------------------------------------------------------------------------------------
   A second fill, behind the real one, given the same target width but slower to reach it. Only a
   drop is ever visible: on the way back up the fill is in front and covers it.

   Every bar in the game that shows health carries one. A hit that takes a tenth of somebody is the
   loudest thing that happens in a fight, and without this the bar is simply a different length the
   next time you look at it — the event itself never reaches the eye.

   The track has to be position:relative and the trail has to come BEFORE the fill in the markup.
   Setting the width is setBarWidth's job (ui.js); nothing should write one of the two alone.

   A bar is three layers and they are numbered, because a fill given z-index to clear the trail
   also clears anything whose z-index is auto — which is every label ever written over a bar:
       0  trail
       1  fill
       2  anything readable: a label, a name, a number. */
.ui-trail {
  position: absolute; inset: 0 auto 0 0; width: 0;
  z-index: 0;
  pointer-events: none;
  background: rgba(255,186,176,.42);
  transition: width .5s ease .22s;
}
.ui-bar__label {
  position: absolute; inset: 0; z-index: 2;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-numeric); font-variant-numeric: tabular-nums;
  font-size: var(--fs-xs); font-weight: 700; color: var(--ink);
  text-shadow: 0 1px 3px #000;
}
.ui-bar--torch .ui-bar__fill { background: linear-gradient(180deg, var(--torch-bright), var(--torch)); }
.ui-bar--rune  .ui-bar__fill { background: linear-gradient(180deg, var(--rune-bright), var(--rune)); }
.ui-bar--lg { height: 26px; }

/* ============================================================================================
   ORB — the bottom-corner globes
   ============================================================================================ */

.ui-orb {
  position: relative; overflow: hidden;
  width: 104px; height: 104px;
  border-radius: var(--r-round);
  background: radial-gradient(circle at 34% 28%, var(--stone-raised), var(--stone-abyss) 72%);
  border: 3px solid var(--iron);
  box-shadow: var(--cast-deep), inset 0 4px 12px rgba(0,0,0,.8);
}
.ui-orb__fill {
  position: absolute; left: 0; right: 0; bottom: 0;
  height: 60%;
  background: linear-gradient(180deg, #4fbf76, #1f6b3a);
  box-shadow: inset 0 3px 10px rgba(255,255,255,.28);
  transition: height var(--t-base);
}
/* The glass catch-light that makes it read as a globe rather than a filled circle. */
.ui-orb::after {
  content: ''; position: absolute; inset: 0; border-radius: var(--r-round); pointer-events: none;
  background: radial-gradient(ellipse at 34% 24%, rgba(255,255,255,.30), transparent 46%);
}
.ui-orb__value {
  position: absolute; inset: 0; display: grid; place-items: center;
  font-family: var(--font-numeric); font-variant-numeric: tabular-nums;
  font-size: var(--fs-lg); font-weight: 700; color: var(--ink);
  text-shadow: 0 2px 4px #000; z-index: 1;
}
.ui-orb--resource .ui-orb__fill { background: linear-gradient(180deg, var(--torch-bright), #8a4f1d); }

/* ============================================================================================
   DOCKS — the Diablo 3 contribution
   ============================================================================================

   Every HUD element used to choose its own corner and offset: three of them stacked down the
   right edge at top 16 / 74 / 95px, two of those at right:16px and the third at right:20px. There
   was no concept of a region, which is why side panels could not simply be added.

   A surface now declares which dock it belongs to and the shell places it. Rails open from the
   edges and can be open together over the stage, which is the layout the whole request is about.
   ============================================================================================ */

.ui-rail {
  /* The containing block for its close tab, so the component works wherever it is put rather than
     only where an id rule happens to have made it absolute. */
  position: relative;
  width: var(--rail-w);
  margin: var(--sp-3);
  display: flex; flex-direction: column; gap: var(--sp-3);
  /* Visible, not auto: the close tab hangs outside the rail box on purpose, and a scroll container
     clips it. The panel inside takes the overflow instead — see .ui-rail > .ui-panel below. */
  overflow: visible;
  transition: transform var(--t-base), opacity var(--t-base);
}
.ui-rail--left  { grid-area: left; }
.ui-rail--right { grid-area: right; }
/* Some panels genuinely need more than a rail. The Notice Board is five columns of standings, and
   a wide variant is a better answer than the Board being the one station that is still a modal. */
.ui-rail--wide { width: min(58vw, 820px); }
/* A spread needs an index, a gutter and a page of prose beside a plate of art. At the Board's width
   the page runs to one cramped column and the art has nowhere to sit.

   Two sizes of spread, and the difference is what the PAGE has to hold, never the index — that is
   288px in both. The book prints a boss at full height with a kit list beside it and wants the
   room; Maeve's manuals and a vault's briefing are prose and a few rows, and at the book's width
   they run to a measure nobody can read a paragraph across. */
.ui-rail--wide[data-wide="page"]   { width: min(74vw, 1120px); }
.ui-rail--wide[data-wide="spread"] { width: min(60vw, 870px); }
.ui-rail--wide[data-wide="talents"] { width: min(80vw, 1200px); }
.ui-rail[hidden] { display: none; }

/* Far enough out that the tab goes with it, and inert while it is out there: opacity:0 alone
   leaves an invisible 24px button sitting against the screen edge, which is worse than a visible
   one. */
.ui-rail.is-closed { pointer-events: none; }
.ui-rail--left.is-closed  { transform: translateX(calc(-100% - var(--sp-3) - var(--rail-tab-w))); opacity: 0; }
.ui-rail--right.is-closed { transform: translateX(calc(100% + var(--sp-3) + var(--rail-tab-w))); opacity: 0; }

/* ---- Close tab ---------------------------------------------------------------------------
   Diablo dismisses a panel with a stub on its inner edge rather than by finding the button that
   opened it. That matters more here than it does there: rails now sit ON TOP of the bottom bar
   instead of pushing it aside, so the Hero button underneath an open hero rail is no longer
   reachable. The tab is how you get out.

   Same frame as .ui-panel minus the border it shares with the panel, so the two read as one piece
   of metal rather than a button parked beside a window. */
/* Dark red, and the only thing on a panel that is. Gold is the frame's own colour, so a gold tab
   read as more frame; a control that dismisses something should look like the one part of the panel
   that acts ON the panel. Deliberately NOT palette tokens — Signal is reserved for game meaning,
   and closing a window is not one — but declared once and shared, because a second control wearing
   the same red by hand is how two reds end up almost matching. Worn by the rail's close tab and by
   the Tavern's dismiss-a-hero button. */
.ui-close-tab, .tavern-seat__dismiss {
  --dismiss-ink: #e8a294;
  --dismiss-face: linear-gradient(180deg, #4a1512, #2a0b09);
  --dismiss-edge: #6b201a;
  --dismiss-ink-lit: #ffd9d0;
  --dismiss-face-lit: linear-gradient(180deg, #6d201b, #3d100d);
  --dismiss-edge-lit: #9c3128;
}

/* Anything that covers the screen is closed by this and by nothing else — a rail, a dialog. One
   shape in one place means the way out never has to be looked for, which is the entire job of a
   control whose only purpose is to be found when someone wants out.

   Never a small X floating in a panel's top-right corner. That corner already belongs to the
   header, so the X either crowds the title or sits over it, and at 26px it is the smallest target
   on a panel while being the one every player reaches for first. */
.ui-close-tab {
  position: absolute; top: var(--sp-6); z-index: 2;
  width: var(--rail-tab-w); height: 44px;
  display: grid; place-items: center;
  padding: 0; appearance: none; cursor: var(--cursor-interact);
  font-size: 13px; line-height: 1;
  color: var(--dismiss-ink);
  background: var(--dismiss-face);
  border: 2px solid var(--dismiss-edge);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.08), var(--cast);
  transition: color var(--t-fast), background var(--t-fast), border-color var(--t-fast);
}
/* Which edge it grows out of. It drops the border it shares with the panel, so the two read as one
   piece of metal rather than a button parked beside a window.

   An absolute offset is measured from the containing block's PADDING box, so whatever border the
   host carries has to be added here or the tab comes to rest ON that border instead of against it.
   --tab-host-border is 0 for a rail, which is an unbordered box holding a panel, and the panel's
   own border for anything that IS the panel. */
.ui-close-tab { --tab-out: calc((var(--rail-tab-w) + var(--tab-host-border, 0px)) * -1); }
.ui-close-tab--east { right: var(--tab-out); border-left: 0;  border-radius: 0 var(--r-sharp) var(--r-sharp) 0; }
.ui-close-tab--west { left:  var(--tab-out); border-right: 0; border-radius: var(--r-sharp) 0 0 var(--r-sharp); }
.ui-panel > .ui-close-tab { --tab-host-border: 2px; }
.ui-close-tab:hover { color: var(--dismiss-ink-lit); background: var(--dismiss-face-lit); border-color: var(--dismiss-edge-lit); }
.ui-close-tab:focus-visible { outline: none; color: var(--dismiss-ink-lit); border-color: var(--rune-bright); box-shadow: var(--cast), var(--glow-rune); }

/* Phones and short windows: rails take the screen rather than squeezing the stage to nothing. */
@media (max-width: 900px) {
  .ui-rail { position: absolute; top: 0; bottom: 0; width: min(92vw, var(--rail-w)); margin: 0; z-index: 20; }
  .ui-rail--left { left: 0; } .ui-rail--right { right: 0; }
  /* Nothing left to protrude into, so the tab sits inside the panel's own edge. */
  .ui-close-tab--west { left: 0; }
  .ui-close-tab--east { right: 0; }
}


/* ============================================================================================
   HERO WINDOW — the right rail's contents
   ============================================================================================

   Slot layout follows the 13 physical slots in Game/Models/Enums.cs, balanced four and four either
   side of the portrait with the five paired/held slots beneath it. Not a grid of named areas: the
   columns are flex, so a slot can be added without re-authoring a template.
   ============================================================================================ */

.hero-window { display: flex; flex-direction: column; min-height: 0; }
.hero-window__body { flex: 1; display: flex; flex-direction: column; gap: var(--sp-3); min-height: 0; }

/* The figure is centred rather than top-aligned, because the two are not the same height and never
   can be: the columns are four sockets tall and the portrait is as wide as what is left, and it is
   square. Hung from the top, the difference collects underneath it as a hole; centred, the same
   pixels read as the space a figure stands in. */
.doll { display: grid; grid-template-columns: auto 1fr auto; gap: var(--sp-2); align-items: center; }
/* The portrait and the weapon share the middle cell, stacked. */
.doll__figure, .doll-slot--weapon { grid-column: 2; grid-row: 1; }
/* Bottom of the middle cell, which is the line Belt and Pet sit on — so the weapon reads as part
   of the ring of slots around the figure rather than as the first of the held row, where it was
   one of five identical sockets and the hardest of them to find. It overlaps the lower part of the
   portrait, which is what puts it IN the picture instead of beside it. */
.doll-slot--weapon { align-self: end; justify-self: center; z-index: 2; }
.doll__col { display: flex; flex-direction: column; gap: var(--sp-2); }
.doll__hands { display: flex; justify-content: center; gap: var(--sp-2); }
/* The figure grows and shrinks with the sockets flanking it: it is the 1fr between two fixed
   columns, so the doll stays proportionate at any socket size without a second rule. */
.doll__figure { min-width: 150px; }

/* A short window has a fixed budget and one flexible part, the bag well, which bottoms out at its
   own min-height and then the panel overflows. The doll is the largest rigid block in it — four
   sockets tall plus the row of held slots — so it is what yields. 54 -> 48 buys 30px, which is what
   a 720-tall viewport is short by now that the rail stops above the bottom bar.

   The cap lives in fitItemSockets(), not here — the socket is measured, so a media query setting it
   would be overwritten on the next render and would only ever apply for the frame before one. */

/* An empty slot still says what it is. Two lines at 12px would not fit a 44px socket, so the label
   is the one place the type scale yields — it is a watermark, not text you read. */
.doll-slot__label {
  font-family: var(--font-body); font-size: 9px; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink-faint); text-align: center; line-height: 1.1;
  padding: 0 2px; pointer-events: none;
}

.bag-well { flex: 1 1 auto; min-height: 120px; overflow-y: auto; padding: var(--sp-2); }
.hero-window__foot { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); }

/* Gear is read-only in the portal lobby (inventory.js, atPortal). Keyed off the screen itself so
   the hint can never disagree with the lock, and the sockets drop their grab cursor to match. */
.hero-window__locked { display: none; margin: 0; font-size: var(--fs-sm); color: var(--ink-dim); text-align: center; }
body:has(#screen-queue.active) .hero-window__locked { display: block; }
body:has(#screen-queue.active) #railRight .ui-slot { cursor: var(--cursor-navigate); }

/* The rails sit over the world rather than beside it, so the canvas beneath never resizes. They
   stop short of the bottom edge because they no longer push it aside — see --edge-reserve-bottom. */
#railLeft, #railRight { position: absolute; top: 0; bottom: var(--edge-reserve-bottom); z-index: 30; }

/* The rift's bottom edge is not a row of small buttons, it is the ability dock: four 104px ability
   tiles, an action row, the vitals and whatever banner is firing. A fixed reserve would either eat
   a third of the rail or let the rail cover the buttons you are pressing, and the dock's height is
   not a constant — it grows with the number of bound abilities and when the loadout panel opens
   inside it. So rift.js measures it and writes --rift-reserve-bottom; this is the fallback for the
   frame before the first measurement lands. */
.app:has(#screen-rift.active) { --edge-reserve-bottom: var(--rift-reserve-bottom, 268px); }
/* The panel fills the rail; the bag inside it takes the slack. Without this the panel is only as
   tall as its contents and the world shows through beneath it. */
/* A rail panel is always a header, sometimes a tab row, and a body that takes whatever is left.
   Without the column here the body is only as tall as its contents and the panel keeps a slab of
   empty stone under it — the hero window said this itself and every new panel had to repeat it. */
.ui-rail > .ui-panel { flex: 1; min-height: 0; overflow: hidden; display: flex; flex-direction: column; }
/* The line above is `display: flex`, which beats the `hidden` attribute's own `display: none` —
   so without this every panel in the rail is live at once, stacked, and the one on top eats the
   clicks meant for the one you think you are looking at. */
.ui-rail > .ui-panel[hidden] { display: none; }
/* The rail stopped being the scroller when the close tab needed to hang outside it, so a rail panel
   scrolls its own body instead.

   This has to be `auto` and not `hidden`, including for the hero window, whose bag well is
   otherwise the designated scroller. A short window can outgrow the whole column — 1024x600 with
   the selected-item panel open overflows it by 250px — and pressing a button in there moves focus,
   which makes the browser scroll the focused element into view. It will do that to a clipped box
   exactly as readily as to a scrolling one: scrollTop went to 152, the paper doll left the top of
   the panel, and there was no scrollbar, no wheel and no way back. Content you cannot scroll to is
   content that is gone. */
/* flex:1 so the body reaches the bottom of the panel. The two bottom corner brackets hang off the
   BODY, so a body that stops early leaves them floating mid-panel — the Details sheet ended at 371
   of 808px and its brackets sat there with it. */
.ui-rail .ui-panel__body { flex: 1; min-height: 0; overflow-y: auto; }
#railLeft { left: 0; } #railRight { right: 0; }


/* ============================================================================================
   TABS — one panel, several faces
   ============================================================================================

   The rail holds one panel at a time, so a panel that covers more than one subject has to do it
   inside itself. Three of them did already (Forge, Shrine, Notice Board) and each had written its
   own: three data attributes, three click loops, three notions of "active". The Board's
   [data-tab] even matched the Forge's buttons, because nothing scoped them.

   Drawn as tabs cut into the panel's top edge rather than as pills floating above it: the active
   one joins the body it belongs to, which is what makes a tab read as a tab and not as a button
   that happens to be selected. See bindTabs in js/ui/LeftRail.js.
   ============================================================================================ */

.ui-tabs {
  display: flex; gap: 2px;
  padding: 0 var(--sp-5);
  margin-top: var(--sp-2);
  border-bottom: 1px solid var(--iron);
}

.ui-tab {
  flex: 1 1 0; min-width: 0;
  padding: var(--sp-2) var(--sp-2) calc(var(--sp-2) + 1px);
  margin-bottom: -1px;
  appearance: none; cursor: var(--cursor-interact);
  font-family: var(--font-body); font-size: var(--fs-xs); font-weight: 700;
  letter-spacing: var(--tracking-wide); text-transform: uppercase;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  color: var(--ink-faint);
  background: linear-gradient(180deg, rgba(255,255,255,.02), transparent);
  border: 1px solid transparent; border-bottom-color: var(--iron);
  border-radius: var(--r-sharp) var(--r-sharp) 0 0;
  transition: color var(--t-fast), background var(--t-fast), border-color var(--t-fast);
}
.ui-tab:hover { color: var(--ink-dim); background: rgba(255,255,255,.04); }
/* The active tab's bottom border is the panel's own background, which is what cuts the notch in
   the rule and joins the tab to the body underneath it. */
.ui-tab.is-active {
  color: var(--accent, var(--gold-lit));
  background: linear-gradient(180deg, rgba(255,255,255,.06), transparent);
  border-color: var(--iron); border-bottom-color: #070d16;
}
.ui-tab:focus-visible { outline: none; color: var(--ink); border-color: var(--rune-bright); }


/* ============================================================================================
   PANEL ACCENTS — a station you can name before you read its title
   ============================================================================================

   Every panel is the same black stone in the same iron frame, which is right: they are all part of
   one building. What tells them apart is the light in them. One variable per panel, and every
   accented part of the panel reads it, so a new station is one line rather than a stylesheet.
   ============================================================================================ */

.ui-panel { --accent: var(--gold-lit); --accent-dim: var(--gold-aged); }
.ui-panel--forge  { --accent: var(--torch-bright); --accent-dim: #8a4f1d; }
.ui-panel--shrine { --accent: var(--rune-bright);  --accent-dim: #2a5c96; }
.ui-panel--board  { --accent: var(--gold-lit);     --accent-dim: var(--gold-aged); }
/* Lamp-gold rather than the Forge's fire-orange. Both rooms are lit by something burning, and they
   should be: the difference between a forge and a tavern is not the colour family, it is that one
   of them is a working fire and the other is a lamp over a table. */
.ui-panel--tavern { --accent: #e8b06a;             --accent-dim: #7a4e22; }
/* The one panel deliberately off the art's axis, and the reason is the subject. Everything else in
   this stylesheet was pulled back onto the plates' two families (cold stone, warm torch) because a
   violet UI over amber art read as a different product. A rift is the one thing in the game that is
   NOT of this world — a tear with something else showing through it — so the light in this panel
   comes from somewhere the other stations' light does not. Epic rarity's violet, which is already
   in the palette carrying meaning rather than decoration. */
.ui-panel--rifts  { --accent: #b48ae4;             --accent-dim: #5b3f86; }

.ui-panel--forge .ui-panel__header,
.ui-panel--rifts .ui-panel__header,
.ui-panel--tavern .ui-panel__header,
.ui-panel--shrine .ui-panel__header { color: var(--accent); }
.ui-panel--forge .ui-panel__header::before, .ui-panel--forge .ui-panel__header::after,
.ui-panel--rifts .ui-panel__header::before, .ui-panel--rifts .ui-panel__header::after,
.ui-panel--tavern .ui-panel__header::before, .ui-panel--tavern .ui-panel__header::after,
.ui-panel--shrine .ui-panel__header::before, .ui-panel--shrine .ui-panel__header::after {
  background: linear-gradient(90deg, transparent, var(--accent-dim), transparent);
}
/* The light each station works by, washing up from the bottom of the panel. The Forge is a fire,
   so it glows from under the anvil; the Shrine is rune inlay, so it glows from the stone.
   The bottom-left bracket stays. It was dropped here on the theory that it "yields to the wash",
   which was wrong twice over: the frame is the one thing every panel shares, and three corners with
   a bracket and one without does not read as restraint, it reads as a missing corner. */
.ui-panel--forge, .ui-panel--shrine, .ui-panel--rifts, .ui-panel--tavern {
  background:
    radial-gradient(ellipse 120% 40% at 50% 100%, var(--accent-wash, transparent), transparent 70%),
    radial-gradient(ellipse at 50% 0%, rgba(64,71,97,.20), transparent 62%),
    linear-gradient(180deg, #0c1420 0%, #070d16 55%, #05090f 100%);
}
.ui-panel--forge  { --accent-wash: rgba(195,132,75,.16); }
.ui-panel--shrine { --accent-wash: rgba(54,131,229,.14); }
.ui-panel--rifts  { --accent-wash: rgba(139,92,214,.16); }
.ui-panel--tavern { --accent-wash: rgba(224,167,101,.13); }

/* A body that holds its own scroller (a tab pane with a well in it) must not also scroll, or the
   panel gets two bars. panel-body--flush hands the height down instead. */
.panel-body--flush { flex: 1; display: flex; flex-direction: column; min-height: 0; overflow: hidden; padding-top: var(--sp-3); }
.panel-body--flush > [data-pane] { flex: 1; display: flex; flex-direction: column; gap: var(--sp-3); min-height: 0; }
.panel-body--flush > [data-pane][hidden] { display: none; }
.panel-blurb { font-size: var(--fs-xs); margin: 0; }


/* ============================================================================================
   BAG — a grid of sockets, and the verbs for the one you picked
   ============================================================================================ */

/* Fixed square tracks, and as many as fit.
   It was five fluid columns, which made the socket's WIDTH a function of the rail: 68px at one
   size and 51px at another, against a fixed 58px height, so the one square in the game that holds
   a square picture was the only thing on screen that was not square. The count of columns is what
   varies now; the socket never does. The slack goes into the gaps rather than into the cells. */
.bag-grid {
  display: grid;
  grid-template-columns: repeat(var(--bag-columns, 5), minmax(0, 1fr));
  gap: var(--bag-gap);
}
/* The socket fills its track exactly and squares itself off that, so the row runs edge to edge
   with no remainder anywhere: not in the gaps, not against one side. The column COUNT is what
   varies with the rail; see fitItemSockets(). */
.bag-grid .ui-slot { width: 100%; height: auto; aspect-ratio: 1 / 1; }

/* An unfilled space in the bag. It is the same recessed socket as a filled one and carries nothing
   — a bag with room left in it should look like a bag with room left in it, which is also why
   there is no longer a sentence here saying so. See padWithEmptySockets() in ui/SocketGrid.js. */
.bag-hole { pointer-events: none; }

/* The bag holds what you are CARRYING. Worn gear was in here too, marked with a badge, on the
   argument that you want to see it beside the candidates — but the paper doll is directly above the
   grid and shows exactly that, so the badge bought a duplicate and the duplicate is what made
   equipping feel like nothing had happened. An item now leaves the bag when you put it on. */
.bag-cell__set {
  position: absolute; z-index: 2; pointer-events: none;
  font-size: 9px; font-weight: 800; line-height: 1;
  padding: 1px 3px; border-radius: var(--r-sharp);
  text-shadow: 0 1px 2px #000;
}
.bag-cell__set { right: 1px; bottom: 1px; background: rgba(0,0,0,.8); color: var(--rune-bright); }

/* The item's level, top-left, over the art. A socket is otherwise a picture and nothing else, and
   two Runic Crowns look identical whether one dropped from Tier 3 and the other from Tier 18 — the
   number you actually compare them by was only in the tooltip. Tabular so a column of them lines
   up, and dark enough behind that it reads over any art. */
.ui-slot__ilvl {
  position: absolute; left: 1px; top: 1px; z-index: 2; pointer-events: none;
  padding: 0 3px; border-radius: var(--r-sharp);
  font-family: var(--font-numeric); font-variant-numeric: tabular-nums;
  font-size: 9px; font-weight: 800; line-height: 1.4;
  color: var(--ink); background: rgba(0,0,0,.72);
  text-shadow: 0 1px 2px #000;
}
.bag-cell.is-locked { opacity: .45; }
/* What the Forge is working on. The bag lost its selection when the selected-item panel went, and
   this went with it — so the Forge had a bench full of an item with nothing on screen saying which
   socket it came out of. */
.bag-cell.is-selected {
  box-shadow: inset 0 2px 6px rgba(0,0,0,.95), 0 0 0 2px var(--accent, var(--gold-lit)), var(--glow-torch);
}


/* ============================================================================================
   DOCKS — a corner owns its contents
   ============================================================================================

   The world screen had seven floats at seven hand-picked offsets (top 16 / 76 / 88 / 160, and a
   button at a literal top:205px). A dock owns the corner and its contents stack inside it, so a
   new readout never has to find a free coordinate, and two of them can never be given the same one.

   Docks hold their position when a rail slides in. A rail is laid over them, not beside them —
   see the .ui-edge rule below for why that is worth insisting on.
   ============================================================================================ */

/* An edge is ONE row with three slots. Corners positioned independently on the same edge can
   always overlap at some width, and did: a 360px chat form and a centred action bar collided at
   520px, and the later one in the DOM simply covered the other. Flex slots push each other
   instead, so the collision cannot be expressed.

   Edges DO NOT step aside for rails. They used to, via a --rail-l/--rail-r pair driven by :has(),
   and the result was that opening the hero window shoved the world-boss strip and the hero's own
   name off to one side — a readout you are watching mid-fight, moved because you opened a bag.
   A rail is a sheet of metal laid over the screen: the centre lane stays centred on the SCREEN, and
   the rail simply covers what it covers. Three things follow, and all three are load-bearing:
   the rails outrank the edges (z 30 > 20), they stop above the bottom control row, and anything
   that must never be covered goes in .alert-lane, which outranks both. */
.ui-edge {
  position: absolute; z-index: 20;
  left: var(--sp-4);
  right: var(--sp-4);
  display: flex; gap: var(--sp-3);
  pointer-events: none;
}
.ui-edge--top { top: var(--sp-4); align-items: flex-start; }
.ui-edge--bottom { bottom: var(--sp-4); align-items: flex-end; }

/* start and end take equal shares of the free space, which is what keeps the centre slot actually
   centred no matter how wide either side's contents are. min-width:0 lets a slot shrink rather
   than force the row wider than the stage. */
.ui-edge__start, .ui-edge__end, .ui-edge__center {
  display: flex; flex-direction: column; gap: var(--sp-2); min-width: 0;
}
.ui-edge__start { flex: 1 1 0; align-items: flex-start; }
.ui-edge__end   { flex: 1 1 0; align-items: flex-end; }
.ui-edge__center { flex: 0 1 auto; align-items: center; max-width: min(760px, 100%); }

.ui-edge__start > *, .ui-edge__end > *, .ui-edge__center > * { pointer-events: auto; max-width: 100%; }

/* Below 900px a rail is 92vw, so the edges underneath it are a sliver of half-covered controls
   rather than a layout. Take them away for as long as the rail is open. */
@media (max-width: 900px) {
  /* .app, not .screen: the rails are siblings of the screens now rather than children of the world
     one, because the rift has panels of its own and two rails cannot share an id. */
  .app:has(.ui-rail:not(.is-closed)) .ui-edge { opacity: 0; pointer-events: none; }
}

/* ---- Alert lane ---------------------------------------------------------------------------
   Above everything, rails included. Announcements are the one class of surface that must reach the
   player during whatever else they are doing, and they are temporary, so covering an open panel for
   a few seconds costs nothing.

   It is its OWN element rather than a slot in the top edge because z-index does not work the way
   the old markup assumed: .ui-edge is positioned with z-index 20, which makes it a stacking
   context, so the announcement's z-index 80 only ever ranked it against its siblings inside that
   edge. It was painting UNDER the rails the whole time. */
.alert-lane {
  position: absolute; z-index: 90;
  /* Clear of the top-centre lane rather than on top of it. Pinned to the edge, an announcement
     landed exactly on the world-boss strip — the one readout the centre lane exists for — and hid
     it for as long as it showed. Below that stack it overlays the world instead, which is where an
     interruption belongs and what every game that does this already does. */
  top: clamp(118px, 14vh, 200px); left: var(--sp-4); right: var(--sp-4);
  display: flex; flex-direction: column; align-items: center; gap: var(--sp-2);
  pointer-events: none;
}
.alert-lane > * { max-width: min(760px, 100%); }
/* The lane is click-through so it never eats a click meant for the world underneath. Anything in it
   that has a control on it has to opt back in, or the control renders and does nothing. */
.alert-lane button { pointer-events: auto; }

/* An alert you have to answer, as opposed to one you only have to read.
   Same lane and the same few seconds as .server-announcement, but it carries a verb, so it gets a
   frame rather than the announcement's banner fill: a button on a gradient reads as decoration. */
.alert-banner {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  background: linear-gradient(180deg, rgba(12,20,32,.95), rgba(5,9,15,.97));
  border: 1px solid var(--iron-light);
  border-radius: var(--r-sharp);
  box-shadow: inset 0 0 0 1px rgba(var(--gold-rgb), .18), var(--cast-deep);
  color: var(--ink);
  animation: announcement-in .35s ease-out;
}
.alert-banner[hidden] { display: none; }
.alert-banner__mark { font-size: 18px; line-height: 1; color: var(--ember); filter: drop-shadow(0 0 8px rgba(215,67,21,.7)); }
.alert-banner__body { display: flex; flex-direction: column; min-width: 0; }
.alert-banner__body b {
  font-family: var(--font-display); font-size: var(--fs-sm);
  letter-spacing: var(--tracking-display); color: var(--gold-lit); white-space: nowrap;
}
/* Tabular: this is a countdown ticking in place, and proportional digits make the row twitch. */
.alert-banner__body span {
  font-family: var(--font-numeric); font-variant-numeric: tabular-nums;
  font-size: var(--fs-xs); color: var(--ink-dim); white-space: nowrap;
}

/* ---- Bottom bar ------------------------------------------------------------------------- */
.ui-bar-group {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2);
  background: linear-gradient(180deg, rgba(12,20,32,.92), rgba(5,9,15,.96));
  border: 1px solid var(--iron);
  border-radius: var(--r-sharp);
  box-shadow: inset 0 0 0 1px rgba(var(--gold-rgb), .18), var(--cast);
}

/* ---- Menu ------------------------------------------------------------------------------- */
.ui-menu { position: relative; }
.ui-menu__list {
  position: absolute; bottom: calc(100% + var(--sp-2)); right: 0;
  display: flex; flex-direction: column; gap: var(--sp-2);
  min-width: 170px; padding: var(--sp-3);
  z-index: 40;
}
.ui-menu__list[hidden] { display: none; }


/* ============================================================================================
   ITEM TOOLTIP — the paired card Diablo shows
   ============================================================================================

   Two cards side by side: what you are wearing, dimmed and labelled, then what the cursor is on
   with the differences marked. It replaces a delta-only list of "before -> after" rows that never
   showed either item, which is the wrong way round — the point of comparing gear is seeing both.
   ============================================================================================ */

.ui-tip-pair {
  position: fixed; z-index: 200;
  display: flex; align-items: flex-start; gap: var(--sp-2);
  pointer-events: none;
}
.ui-tip-pair[hidden] { display: none; }

.ui-item-tip {
  width: 260px;
  padding: var(--sp-3) var(--sp-4) var(--sp-4);
  background: linear-gradient(180deg, #0c1420, #05090f);
  border: 1px solid var(--iron);
  border-top: 2px solid var(--rarity-color, var(--iron));
  border-radius: var(--r-sharp);
  box-shadow: inset 0 0 0 1px rgba(var(--gold-rgb), .16), var(--cast-deep);
  color: var(--ink);
  font-size: var(--fs-sm);
}
/* The worn piece, marked gold all the way round, including over the rarity stripe. Gold means
   "you are wearing this" everywhere else in this game and it should mean it here.

   It used to be faded instead — opacity .72 and desaturated — so the eye would land on the
   candidate. The candidate still wins that, by being the card under the cursor and the only one
   carrying deltas; what the fade actually did was make the thing you are comparing AGAINST harder
   to read than the thing you are comparing. */
.ui-item-tip.is-equipped {
  border-color: var(--gold);
  border-top-color: var(--gold);
  box-shadow: inset 0 0 0 1px rgba(var(--gold-rgb), .24), 0 0 12px rgba(251,191,36,.25), var(--cast-deep);
}

.tip-label {
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: var(--tracking-wide);
  text-transform: uppercase; color: var(--ink-faint); margin-bottom: var(--sp-2);
}
.tip-head { display: flex; gap: var(--sp-3); align-items: flex-start; }
.tip-title { min-width: 0; }
.tip-name {
  font-family: var(--font-display); font-weight: 700; font-size: var(--fs-lg);
  line-height: var(--lh-tight); overflow-wrap: anywhere;
}
.tip-kind { font-size: var(--fs-xs); color: var(--ink-dim); margin-top: 2px; }
.tip-role {
  display: inline-block; margin-top: 4px; padding: 2px 6px;
  border: 1px solid currentColor; border-radius: 4px;
  background: color-mix(in srgb, currentColor 12%, transparent);
  font-size: var(--fs-xs); font-weight: 700;
}
.tip-role--bulwark { color: #42d7a3; }
.tip-role--slayer { color: #d4e568; }
.tip-role--mender { color: #f094c6; }
.tip-role--universal { color: #e8d5a6; }

.tip-primary {
  display: flex; align-items: baseline; gap: var(--sp-2);
  margin: var(--sp-3) 0; padding-bottom: var(--sp-2);
  border-bottom: 1px solid rgba(var(--gold-rgb), .18);
}
.tip-primary b { font-family: var(--font-numeric); font-variant-numeric: tabular-nums; font-size: var(--fs-xl); }
.tip-primary span { font-size: var(--fs-xs); color: var(--ink-dim); letter-spacing: var(--tracking-wide); text-transform: uppercase; }

.tip-stats { display: flex; flex-direction: column; gap: 3px; }
.tip-stat { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-2); }
.tip-stat > span { color: var(--rune-bright); }
/* A stat the candidate does not have at all still gets a line, so an upgrade cannot hide what it
   costs you by simply omitting it. */
.tip-stat.is-absent > span { color: var(--ink-faint); }
.tip-stat.is-legacy > span { color: var(--ink-faint); }

.tip-comparison { display: flex; flex-direction: column; gap: 3px; margin-top: var(--sp-3); padding-top: var(--sp-2); border-top: 1px solid rgba(var(--gold-rgb), .18); }
.tip-comparison-title { color: var(--ink-dim); font-size: var(--fs-xs); font-weight: 700; }
.tip-comparison-row { font-size: var(--fs-xs); font-weight: 700; font-variant-numeric: tabular-nums; }
.tip-comparison-row.gain { color: #4ec77a; }
.tip-comparison-row.loss { color: #e0603f; }

.tip-sockets { display: flex; flex-direction: column; gap: 4px; margin-top: var(--sp-3); }
.tip-sockets-title { font-size: var(--fs-xs); font-weight: 700; color: var(--ink-dim); text-transform: uppercase; }
.tip-socket { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-xs); }
.tip-socket-circle { width: 16px; text-align: center; font-size: 16px; line-height: 1; }
.tip-socket.is-empty { color: var(--ink-faint); }
.tip-socket.is-filled { color: var(--rune-bright); }

.tip-delta {
  font-style: normal; font-weight: 700; font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.tip-delta.gain { color: #4ec77a; }
.tip-delta.loss { color: #e0603f; }

/* Who a piece was made for. Always present when the item has a class, so the line means "this is
   class gear" rather than "you are the wrong class" — the mark carries that second fact, and it
   carries it without relying on the colour. */
.tip-restriction { margin-top: var(--sp-3); font-size: var(--fs-xs); font-weight: 700; color: var(--gold-lit); }
.tip-restriction::before { content: '✓ '; }
.tip-restriction.is-locked { color: var(--ember); }
.tip-restriction.is-locked::before { content: '✕ '; }
/* The half of a card that belongs to things which are not gear. What it does is the only number
   here, so it carries the accent the rest of the UI gives numbers; where it may be used is a rule
   about the item and sits back. */
.tip-plain { display: flex; flex-direction: column; gap: 4px; margin-top: var(--sp-2); }
.tip-grants {
  font-family: var(--font-display);
  font-size: var(--fs-sm);
  color: var(--accent);
  letter-spacing: .02em;
}
.tip-use { font-size: var(--fs-xs); color: var(--ink-dim); font-style: italic; }

.tip-foot { margin-top: var(--sp-3); font-size: var(--fs-xs); color: var(--ink-faint); }
.tip-foot b { color: var(--gold-lit); }

/* The Shift prompt on a ring or trinket. Below the rule, because it is about the tooltip rather
   than about the item — it tells you the card can show you something else. */
.tip-hint {
  margin-top: var(--sp-2); padding-top: var(--sp-2);
  border-top: 1px solid var(--iron);
  font-size: var(--fs-xs); color: var(--ink-faint);
}
.tip-hint b {
  display: inline-block; padding: 0 5px;
  border: 1px solid var(--iron-light); border-radius: var(--r-sharp);
  color: var(--ink-dim); font-size: 10px; font-weight: 700;
}

.ui-item-tip .item-special { margin-top: var(--sp-3); font-size: var(--fs-xs); }

/* ============================================================================================
   DRAGGING GEAR
   ============================================================================================ */

body.is-dragging-item { cursor: var(--cursor-carry); user-select: none; }
body.is-dragging-item * { cursor: var(--cursor-carry) !important; }

/* The socket an item was lifted out of, while it is on the cursor. Dimmed rather than emptied: the
   item has not gone anywhere yet, and a socket that blanks on the way to being put straight back
   reads as though the click destroyed what was in it. */
.ui-slot.is-lifted { opacity: .35; }
.ui-slot.is-lifted::after {
  content: ''; position: absolute; inset: 0;
  border: 1px dashed var(--gold-aged); border-radius: inherit;
}

/* The ghost follows the cursor and must never be what elementFromPoint returns, or every drop
   target would be the ghost itself. */
/* What follows the cursor during a drag. Shared by gear and by the loadout board: the two are the
   same gesture, and a second copy of these four lines is how two drags end up feeling different. */
.item-ghost, .lo-ghost {
  position: fixed; z-index: 300; pointer-events: none;
  transform: translate(-50%, -50%) scale(1.08);
  opacity: .9;
  box-shadow: 0 8px 22px rgba(0,0,0,.7), 0 0 20px rgba(var(--gold-rgb), .35);
}

.is-drop-target {
  box-shadow: inset 0 2px 6px rgba(0,0,0,.95), 0 0 0 2px var(--gold-lit), var(--glow-torch);
}
.bag-well.is-drop-target { box-shadow: var(--bevel-down), inset 0 0 0 2px var(--gold-lit); }

.ui-slot { touch-action: none; }   /* so a drag on a touch screen is a drag, not a scroll */


/* ============================================================================================
   WORLD BOSS STRIP — one wide readout in the centre lane
   ============================================================================================

   Replaces a top-right panel plus a separate centred alert that duplicated most of it. Horizontal
   because the centre lane is wide and short: the same information in a third of the vertical space,
   and it no longer holds a whole screen corner hostage.
   ============================================================================================ */

/* Folded is what STANDS here: a skull and one number. Open is what pointing at it gives back, and
   a hover is the whole of that — nothing latches, because a readout that stays open once opened is
   the permanent panel with an extra step in front of it.

   This is the one HUD element that takes pointer events across its whole body rather than only on
   its buttons, and the fold is what pays for it: a pill at the top edge costs a sliver of the Wilds
   you can click, a strip costs the width of the screen. Keep it narrow for that reason. */
.world-boss-bar {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-3);
  background: linear-gradient(180deg, rgba(36,12,14,.94), rgba(16,6,8,.96));
  border: 1px solid rgba(215,67,21,.55);
  border-radius: var(--r-sharp);
  box-shadow: inset 0 0 0 1px rgba(224,138,46,.18), var(--cast);
  color: var(--ink);
  pointer-events: auto;
}
.world-boss-bar:hover { gap: var(--sp-4); padding: var(--sp-2) var(--sp-4); }
.world-boss-bar[hidden] { display: none; }

/* What is left when it is folded. The claim button stays: it is the only thing on this strip you
   can press, and burying an expiring payout behind a hover is hiding the one part that matters. */
.world-boss-bar:not(:hover) .wb-id,
.world-boss-bar:not(:hover) .wb-share,
.world-boss-bar:not(:hover) .wb-bar,
.world-boss-bar:not(:hover) .wb-chest,
.world-boss-bar:not(:hover) .wb-respawn__label,
.world-boss-bar:not(:hover) .terrak-force-spawn { display: none; }
.world-boss-bar:not(:hover) .wb-skull { font-size: 15px; }
.world-boss-bar:not(:hover) .wb-health,
.world-boss-bar:not(:hover) .wb-countdown { flex: 0 0 auto; min-width: 0; }

/* He is in this part of the Wilds: the strip picks up the same red the screen edge is pulsing. */
.world-boss-bar.is-nearby {
  border-color: var(--ember);
  box-shadow: inset 0 0 0 1px rgba(224,138,46,.3), 0 0 26px rgba(215,67,21,.45);
}
.world-boss-bar.is-dormant { border-color: var(--iron); background: linear-gradient(180deg, #0c1420, #05090f); }

.wb-skull { font-size: 26px; color: var(--ember); filter: drop-shadow(0 0 8px rgba(215,67,21,.8)); line-height: 1; }
.world-boss-bar.is-dormant .wb-skull { color: var(--ink-faint); filter: none; }

.wb-id { display: flex; flex-direction: column; min-width: 0; }
.wb-id b { font-family: var(--font-display); font-size: var(--fs-base); white-space: nowrap; }
.wb-id small {
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: var(--tracking-wide);
  text-transform: uppercase; color: var(--ink-dim); white-space: nowrap;
}
.world-boss-bar.is-nearby .wb-id small { color: #ffb4a0; }

.wb-health { flex: 1; display: flex; align-items: center; gap: var(--sp-2); min-width: 120px; }
.wb-bar {
  flex: 1; height: 12px; overflow: hidden;
  background: var(--stone-abyss);
  border: 1px solid rgba(0,0,0,.8);
  border-radius: var(--r-sharp);
  box-shadow: var(--bevel-down);
}
/* Crimson like every other health bar. The strip around it is the ember panel; the bar inside it
   is still health. */
.wb-bar i { display: block; height: 100%; background: linear-gradient(180deg, var(--health-lit), var(--health) 55%, var(--health-deep)); transition: width .35s ease; }
.wb-pct { font-family: var(--font-numeric); font-variant-numeric: tabular-nums; font-size: var(--fs-sm); font-weight: 700; }

.wb-countdown { flex: 1; display: flex; align-items: center; gap: var(--sp-3); font-size: var(--fs-xs); color: var(--ink-dim); }
.wb-countdown b { font-family: var(--font-numeric); font-variant-numeric: tabular-nums; color: var(--gold-lit); }
.wb-chest { color: var(--gold-lit); font-weight: 700; }

.wb-share { display: flex; align-items: baseline; gap: var(--sp-2); white-space: nowrap; }
.wb-share[hidden] { display: none; }
.wb-share small { font-size: var(--fs-xs); letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--ink-faint); }
.wb-share.is-eligible small { color: var(--vitality); }
.wb-share em { font-style: normal; font-size: 10px; font-weight: 800; letter-spacing: .1em; color: var(--vitality); }

@media (max-width: 900px) {
  .wb-share small { display: none; }
  .wb-id b { font-size: var(--fs-sm); }
}


/* ============================================================================================
   BOON STRIP — what the fountain gave you, parked under the world boss
   ============================================================================================

   Moved out of the top-left corner, where it sat beneath the hero's name and had nothing to do
   with it, and restyled off the violet it was authored in. The centre lane is where the things
   that change while you are standing still already live, and a boon is one of those: it is on a
   clock and you check it the same way you check his health.

   Amber rather than the boss strip's red — the two share a lane and must not read as one bar.
   ============================================================================================ */



/* ============================================================================================
   NEW IN THE BAG
   ============================================================================================

   Loot arrives from nine places — boss rewards, loot rolls, ground pickups, forge crafts, gambles
   — and all of it lands in the same grid of sockets, sorted by slot, indistinguishable from the
   thirty things that were already there. This is the mark that says which one is the one you just
   picked up.

   A notch and an outline rather than a tint: rarity already owns colour in a socket, so a "new"
   colour would read as a sixth rarity. Cyan for the same reason focus is cyan — it is the game
   pointing at something, not a property of the item — and it goes away once you have looked.
   ============================================================================================ */

.bag-cell.is-new { outline: 1px solid var(--rune-bright); outline-offset: 1px; }
.bag-cell.is-new::after {
  content: ''; position: absolute; top: 0; right: 0; z-index: 3; pointer-events: none;
  border: 7px solid transparent;
  border-top-color: var(--rune-bright);
  border-right-color: var(--rune-bright);
  animation: bag-new-pulse 1.7s ease-in-out infinite;
}
@keyframes bag-new-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .3; } }
@media (prefers-reduced-motion: reduce) { .bag-cell.is-new::after { animation: none; } }


/* ============================================================================================
   SELECT — a dropdown made of the same stone as everything else
   ============================================================================================

   A native <select> can be styled closed and not open: the popup belongs to the operating system,
   so whatever the button looked like, choosing from it meant a white system menu over a near-black
   panel. This is the control rebuilt out of elements we own. See js/ui/Select.js for the keyboard
   behaviour that had to be written back in, and for why the list is a child of <body>.
   ============================================================================================ */

.ui-select { position: relative; flex: 1; min-width: 0; display: flex; }

.ui-select__button {
  flex: 1; min-width: 0;
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-2);
  appearance: none; cursor: var(--cursor-interact);
  font-family: var(--font-body); font-size: var(--fs-xs); font-weight: 700;
  letter-spacing: var(--tracking-wide); text-transform: uppercase;
  color: var(--ink-dim);
  text-align: left;
  background: linear-gradient(180deg, #0a111c, #05090f);
  border: 1px solid var(--iron);
  border-radius: var(--r-sharp);
  box-shadow: var(--bevel-down);
  transition: color var(--t-fast), border-color var(--t-fast);
}
.ui-select__button:hover { color: var(--ink); border-color: var(--iron-light); }
.ui-select__button:focus-visible { outline: none; color: var(--ink); border-color: var(--rune-bright); box-shadow: var(--bevel-down), var(--glow-rune); }

.ui-select__value { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Drawn rather than a glyph, so it is the same triangle at every size and can be turned. */
.ui-select__caret {
  flex: none; width: 0; height: 0;
  border: 4px solid transparent;
  border-top: 5px solid currentColor;
  margin-top: 2px;
  transition: transform var(--t-fast);
}
.ui-select.is-open .ui-select__caret { transform: rotate(180deg); margin-top: -3px; }

/* Filtered: the control stops reading as chrome, because the bag now looks half empty. */
.ui-select.is-set .ui-select__button { color: var(--gold-lit); border-color: var(--gold-aged); }

/* Fixed, and on <body> — see Select.js. z-index above the rails so a dropdown in a rail panel is
   not clipped behind the rail beside it. */
.ui-select__list {
  position: fixed; z-index: 120;
  margin: 0; padding: var(--sp-1); list-style: none;
  /* Sized to its content, never to a scroll box: every option has to be reachable without the
     player discovering there are more of them. Twelve kinds plus "All items" is the most this can
     ever hold (inventory.js, CATEGORY_ORDER) and place() keeps the whole of it on screen, so the
     viewport cap below is a floor against absurdly short windows, not a layout. */
  max-height: calc(100vh - 16px); overflow-y: auto;
  background: linear-gradient(180deg, #0c1420, #05090f);
  border: 1px solid var(--iron-light);
  border-radius: var(--r-sharp);
  box-shadow: inset 0 0 0 1px rgba(var(--gold-rgb), .16), var(--cast-deep);
}
.ui-select__list[hidden] { display: none; }

.ui-select__option {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4);
  padding: var(--sp-2) var(--sp-3);
  font-family: var(--font-body); font-size: var(--fs-xs); font-weight: 700;
  letter-spacing: var(--tracking-wide); text-transform: uppercase;
  color: var(--ink-dim);
  border-radius: var(--r-sharp);
  cursor: var(--cursor-interact);
  white-space: nowrap;
}
/* One highlight, driven by the component, so the mouse and the arrow keys cannot disagree about
   which row is the live one — :hover plus a keyboard cursor gives you two. */
.ui-select__option.is-active { color: var(--ink); background: var(--stone-raised); }
.ui-select__option.is-selected { color: var(--gold-lit); }
.ui-select__option.is-selected.is-active { color: var(--gold-lit); background: var(--stone-raised); }
.ui-select__count { font-family: var(--font-numeric); font-variant-numeric: tabular-nums; color: var(--ink-faint); }
.ui-select__option.is-active .ui-select__count { color: var(--ink-dim); }


/* ============================================================================================
   BAG FILTER
   ============================================================================================

   Was a chip per owned slot. That is up to eleven buttons, which wrapped to three rows in a rail
   and pushed the bag itself off the bottom of the panel — the filter took more space than the
   thing it filtered. One control costs one line whatever you own, and the clear button beside it
   keeps the escape hatch that clicking the active chip used to provide.
   ============================================================================================ */

.inv-filters { display: flex; align-items: center; gap: var(--sp-2); }
.inv-filters[hidden] { display: none; }

.inv-filter-clear {
  flex: none; width: 24px; height: 24px;
  display: grid; place-items: center;
  padding: 0; appearance: none;
  font-size: 13px; line-height: 1; color: var(--ink-faint);
  background: transparent; border: 0; border-radius: var(--r-soft);
  cursor: var(--cursor-interact);
  transition: color var(--t-fast), background var(--t-fast);
}
.inv-filter-clear:hover { color: var(--ink); background: rgba(255,255,255,.06); }
.inv-filter-clear:focus-visible { outline: none; color: var(--ink); box-shadow: 0 0 0 1px var(--rune-bright); }
/* Hidden rather than disabled: with no filter set there is nothing to clear, and a permanently
   greyed button beside a control is just noise. */
.inv-filters:not(.is-filtered) .inv-filter-clear { visibility: hidden; }


/* ============================================================================================
   THE FORGE
   ============================================================================================

   The bag in the right rail holds the candidates. The left rail is the worktop: one seated piece
   and the operations the smith can perform on it.
   ============================================================================================ */

.forge-pane .bag-well { flex: 1; min-height: 96px; }
.forge-worktop {
  flex: none; display: flex; flex-direction: column; align-items: center; gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-2) var(--sp-2);
  border-bottom: 1px solid rgba(224,167,101,.22);
  background: radial-gradient(ellipse 66% 70% at 50% 58%, rgba(194,100,38,.16), transparent 78%);
}
.forge-worktop__eyebrow {
  font: 800 10px var(--font-body); letter-spacing: var(--tracking-wide); text-transform: uppercase;
  color: var(--torch-bright);
}
.forge-item-slot {
  position: relative; display: grid; place-items: center; isolation: isolate;
  width: 132px; height: 132px; padding: 0; cursor: var(--cursor-interact);
  border: 0; border-radius: 50%; color: #a88459;
  background:
    url('/assets/ui/forge-anvil-v1.png') center/contain no-repeat,
    radial-gradient(circle, #080b10 0 36%, transparent 37%);
  filter: drop-shadow(0 7px 4px #000a) drop-shadow(0 0 16px #a758253b);
  transition: filter var(--t-fast), transform var(--t-fast);
}
.forge-item-slot:hover, .forge-item-slot.is-drop-target {
  filter: brightness(1.17) drop-shadow(0 9px 5px #000b) drop-shadow(0 0 20px #df8d4970);
}
.forge-item-slot:focus-visible { outline: 2px solid var(--rune-bright); outline-offset: 5px; }
.forge-item-slot.is-seating { animation: forge-seat .34s cubic-bezier(.16,.7,.2,1) both; }
.forge-item-slot.is-struck { animation: forge-strike .55s ease-out both; }
@keyframes forge-seat {
  0% { transform: scale(.88); filter: brightness(1.5) drop-shadow(0 0 24px #ee9e57); }
  70% { transform: scale(1.035); }
  100% { transform: none; }
}
@keyframes forge-strike {
  0% { filter: brightness(2.1) drop-shadow(0 0 30px #f3b270); transform: scale(1.04); }
  100% { filter: drop-shadow(0 7px 4px #000a) drop-shadow(0 0 16px #a758253b); transform: none; }
}
.forge-item-slot__empty { font: 38px var(--font-display); text-shadow: 0 0 12px #e88c4666; }
.forge-item-slot__item {
  width: var(--item-socket); height: var(--item-socket);
  border: 1px solid #a67b4d; border-radius: var(--r-sharp);
  background: #0a0d13; box-shadow: var(--bevel-down), 0 5px 14px #000b;
}
.forge-item-slot__remove {
  position: absolute; top: 7px; right: 7px; display: grid; place-items: center;
  width: 22px; height: 22px; border-radius: 50%;
  font: 700 18px/1 var(--font-body); color: #e9c894;
  background: #0a0b0ed9; border: 1px solid #9a6a3c;
  box-shadow: 0 2px 5px #000b;
}
.forge-worktop__hint {
  min-height: 18px; margin: 0; text-align: center;
  font: var(--fs-xs)/var(--lh-base) var(--font-body); color: var(--ink-dim);
}
.forge-worktop.has-item .forge-worktop__hint { display: none; }
@media (prefers-reduced-motion: reduce) {
  .forge-item-slot.is-seating, .forge-item-slot.is-struck { animation: none; }
}

/* The bench: what the selected piece is, and the three things the smith can do to it. */
.forge-bench {
  flex: 1 1 auto; min-height: 0;
  padding: var(--sp-2);
  background: rgba(0,0,0,.5);
  border: 1px solid rgba(224,167,101,.28);
  border-radius: var(--r-sharp);
  box-shadow: var(--bevel-down);
  overflow-y: auto;
}
.forge-bench[hidden] { display: none; }
.forge-bench__head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3); }
.forge-bench__name { font-family: var(--font-display); font-size: var(--fs-base); }
.forge-bench__meta { font-size: var(--fs-xs); color: var(--ink-dim); margin-top: 2px; }
.forge-bench__affix { display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-3); margin-top: var(--sp-2); font-size: var(--fs-xs); color: var(--rune-bright); }

/* One row per verb: name, price, and the button that does it. */
.forge-op { margin-top: var(--sp-3); padding-top: var(--sp-3); border-top: 1px solid rgba(255,255,255,.06); }
.forge-op:first-of-type { border-top: 0; padding-top: 0; }
.forge-bench .forge-op { margin-top: var(--sp-2); padding-top: var(--sp-2); }
.forge-bench__affix + .forge-op { padding-top: 0; border-top: 0; }
.forge-bench .forge-op__desc { margin-bottom: var(--sp-1); }
.forge-op__head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-2); }
.forge-op__head b { font-size: var(--fs-sm); color: var(--accent); }
.forge-op__cost { font-family: var(--font-numeric); font-variant-numeric: tabular-nums; font-size: var(--fs-xs); color: var(--gold-lit); }
.forge-op__desc { margin: var(--sp-1) 0 var(--sp-2); font-size: var(--fs-xs); color: var(--ink-dim); line-height: var(--lh-base); }
.forge-op .ui-btn { width: 100%; }

.forge-locks { display: flex; flex-wrap: wrap; gap: var(--sp-1); margin-bottom: var(--sp-2); }
.lock-chip {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 3px var(--sp-2); appearance: none; cursor: var(--cursor-interact);
  font-family: var(--font-body); font-size: 11px; font-weight: 700;
  color: var(--ink-faint);
  background: var(--stone-deep);
  border: 1px solid var(--iron); border-radius: var(--r-soft);
  transition: color var(--t-fast), border-color var(--t-fast);
}
.lock-chip:hover { color: var(--ink-dim); border-color: var(--iron-light); }
.lock-chip.locked { color: var(--gold-lit); border-color: var(--gold-aged); background: rgba(127,109,47,.18); }

.forge-pity { font-size: var(--fs-xs); color: var(--ink-dim); }
.forge-pity b { color: var(--torch-bright); font-family: var(--font-numeric); font-variant-numeric: tabular-nums; }

.gamble-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--sp-2); overflow-y: auto; }
.gamble-card {
  display: flex; flex-direction: column; gap: var(--sp-1);
  padding: var(--sp-3);
  background: linear-gradient(180deg, rgba(36,24,12,.75), rgba(10,7,4,.85));
  border: 1px solid rgba(224,167,101,.26);
  border-radius: var(--r-sharp);
  box-shadow: var(--bevel-down);
}
.gamble-card__name { font-family: var(--font-display); font-size: var(--fs-sm); color: var(--torch-bright); }
.gamble-card__blurb { flex: 1; font-size: var(--fs-xs); color: var(--ink-dim); line-height: var(--lh-base); }
.gamble-card .ui-btn { width: 100%; margin-top: var(--sp-2); }


/* ---- The coffer -----------------------------------------------------------------------------
   Drag a piece in and it is gone, with no dialog. The gesture is the confirmation: you had to pick
   the thing up and carry it across the panel, which is more deliberate than clicking Yes, and
   clearing a bag of thirty commons a dialog at a time is the actual hostile design. */
.sell-coffer {
  flex: none;
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: linear-gradient(180deg, rgba(40,31,10,.7), rgba(12,9,4,.85));
  border: 1px dashed rgba(var(--gold-rgb), .45);
  border-radius: var(--r-sharp);
  transition: border-color var(--t-fast), background var(--t-fast), box-shadow var(--t-fast);
}
/* A whole counter rather than a bar across the top of one.
   itemDrag.js sells on a drop anywhere inside .sell-coffer, and what a player aims at is the shelf —
   which is most of the pane. A coffer smaller than the thing it sits above is a drop target you have
   to find. */
.sell-coffer--counter {
  flex: 1 1 auto; flex-direction: column; align-items: stretch; gap: var(--sp-3);
  min-height: 0;
}

/* Two shelves on one counter: what is for sale, and what the vendor bought off you. The second is
   absent rather than empty until you have sold something — a titled empty grid reads as a thing that
   is broken rather than a thing you have not used. */
.trade-shelf { display: flex; flex-direction: column; gap: var(--sp-1); min-height: 0; }
.trade-shelf:last-child { flex: 1 1 auto; }
.trade-shelf .bag-well { min-height: 78px; }

.sell-coffer__mark { font-size: 22px; line-height: 1; color: var(--gold-aged); transition: color var(--t-fast); }
.sell-coffer__text { display: flex; flex-direction: column; min-width: 0; }
.sell-coffer__text b { font-family: var(--font-display); font-size: var(--fs-sm); color: var(--gold-lit); }
.sell-coffer__text span { font-size: var(--fs-xs); color: var(--ink-dim); }
/* Lit while something is over it, so the drop is never a guess. */
.sell-coffer.is-drop-target {
  border-style: solid; border-color: var(--gold-lit);
  background: linear-gradient(180deg, rgba(88,68,18,.8), rgba(24,18,6,.9));
  box-shadow: var(--glow-torch);
}
.sell-coffer.is-drop-target .sell-coffer__mark { color: var(--gold-lit); }
/* The gold flying off a sale. Short, and it does not move the layout. */
@keyframes coffer-take { 0% { opacity: 0; transform: translateY(6px); } 30% { opacity: 1; } 100% { opacity: 0; transform: translateY(-14px); } }
.sell-coffer__take {
  position: absolute; right: var(--sp-4);
  font-family: var(--font-numeric); font-variant-numeric: tabular-nums;
  font-size: var(--fs-sm); font-weight: 800; color: var(--gold-lit);
  text-shadow: 0 1px 3px #000; pointer-events: none;
  animation: coffer-take 1s ease-out forwards;
}
.sell-coffer { position: relative; }


/* ============================================================================================
   MAEVE'S ADVICE
   ============================================================================================

   The Book of Bosses' spread, and for the Book of Bosses' reason: an index down the gutter and the
   page beside it. This began as a row of six portrait cards across the top, which is a shape that
   reads beautifully at six and not at all at twelve — a fixed-count row either wraps into a ragged
   second line or shrinks the faces to thumbnails, and both get worse with every class added. An
   index does not care how long it is. The portrait comes with it, at gutter size.

   It is warm where the book is gold. The book is vellum in a quiet room; this is a woman behind a
   bar at the end of a long night, so the panel sits in ale-light and her two lines on each page are
   set as pulled quotes rather than as body text.

   The page tints to the class's OWN colour — the red a Warrior's selection ring is drawn in, the
   cyan a Mage's is (CLASS_TINT in js/guide.js, mirroring heroFactory's CLASS_COLORS). Same idea as
   the boss pages tinting to the creature printed on them, except the palette already exists and
   nothing has to be sampled.
   ============================================================================================ */

.ui-panel--guide {
  --accent: #dba263; --accent-dim: #7a4f28; --accent-wash: rgba(198,140,78,.12);
  /* Replaced per page by js/guide.js. The tavern's amber is the resting value, so a panel opened
     before the fetch lands is still warm rather than broken: rgba() with an unset custom property
     is an invalid declaration, not a transparent one. */
  --class-tint: 198, 140, 78;
  overflow: hidden;
  background:
    radial-gradient(ellipse 120% 44% at 50% 100%, var(--accent-wash), transparent 70%),
    radial-gradient(ellipse at 50% 0%, rgba(120,78,44,.16), transparent 62%),
    linear-gradient(180deg, #16100a 0%, #0c0807 55%, #060504 100%);
}
.ui-panel--guide .ui-panel__header { color: var(--accent); }
.ui-panel--guide .ui-panel__header::before, .ui-panel--guide .ui-panel__header::after {
  background: linear-gradient(90deg, transparent, var(--accent-dim), transparent);
}

/* The paper. A child of the PANEL so it reaches the rounded frame rather than stopping dead under
   the title bar — the lesson .codex-wash had to learn twice. */
.guide-wash {
  position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: hidden;
}
.guide-wash::before {
  content: ''; position: absolute; inset: 0;
  background:
    radial-gradient(ellipse 64% 44% at 50% -4%, rgba(var(--class-tint), .20), transparent 70%),
    radial-gradient(ellipse 90% 60% at 104% 104%, rgba(var(--class-tint), .16), transparent 74%);
  transition: background var(--t-slow);
}
.ui-panel--guide > .ui-panel__header,
.ui-panel--guide > .ui-panel__body { position: relative; z-index: 1; }
.ui-panel--guide > .ui-panel__body { display: flex; flex-direction: column; min-height: 0; }

/* ---- The index ----------------------------------------------------------------------------- */


/* A face needs more room than a numeral; the open row's colour is the class's own, which the row
   sets as --entry-tint when it is built. */
.guide-entry { --entry-mark: 34px; }
.guide-entry__face { width: 34px; }

/* ---- The page ------------------------------------------------------------------------------ */

/* .ui-detail carries the column and the gutter margin; this is only what the LEAF is. */
.guide-leaf { position: relative; }
.guide-empty { margin: auto; font-size: var(--fs-xs); color: var(--ink-faint); font-style: italic; }
.guide-empty[hidden] { display: none; }

.guide-page {
  flex: 1 1 auto; min-height: 0;
  /* Explicitly, not left to compute: overflow-y:auto with overflow-x:visible promotes x to auto,
     which is how the book ended up with a horizontal scrollbar across the foot of every page. */
  overflow-y: auto; overflow-x: hidden;
  padding: var(--sp-4) var(--sp-2) var(--sp-5) 0;
}
.guide-page[hidden] { display: none; }

/* The prose on a page that is not a manual: who she is, what the crate under the bar is. A wider
   measure than a kit list and set as body text, because it is meant to be read rather than scanned. */
.guide-lede { max-width: 58ch; margin: var(--sp-4) 0 0; font-size: var(--fs-sm); line-height: var(--lh-base); color: var(--ink-dim); }
.guide-lede b { color: var(--ink); }

/* Her voice, twice a page: the line at the top and the secret at the bottom. Set as a pulled quote
   so it never reads as instructions — everything between them is the manual, and she is the person
   handing it to you. */
.guide-epigraph {
  margin: 0 0 var(--sp-4); padding-left: var(--sp-4);
  border-left: 2px solid rgba(var(--class-tint), .45);
}
.guide-epigraph p {
  margin: 0; font-family: var(--font-display); font-size: var(--fs-base); line-height: 1.5;
  font-style: italic; color: var(--ink);
}
.guide-epigraph cite,
.guide-secret cite {
  display: block; margin-top: var(--sp-1);
  font-style: normal; font-size: var(--fs-xs); color: var(--ink-faint);
  letter-spacing: var(--tracking-wide); text-transform: uppercase;
}

.guide-plate { display: grid; grid-template-columns: 76px 1fr; gap: var(--sp-4); align-items: start; margin-bottom: var(--sp-4); }
.guide-plate__face { width: 76px; }
.guide-plate__who { min-width: 0; }
.guide-plate__who b { display: block; font-family: var(--font-display); font-size: var(--fs-lg); line-height: 1.2; }
.guide-plate__who p { margin: var(--sp-2) 0 0; font-size: var(--fs-sm); line-height: 1.6; color: var(--ink-dim); }

.guide-role {
  display: inline-block; margin-top: 2px; padding: 1px var(--sp-2);
  font-size: 10px; font-weight: 800; letter-spacing: var(--tracking-wide); text-transform: uppercase;
  color: rgb(var(--class-tint));
  background: rgba(var(--class-tint), .12);
  border: 1px solid rgba(var(--class-tint), .35);
  border-radius: var(--r-sharp);
}

/* The same rule the book uses, because it is the one piece of typography that makes a panel read as
   a page rather than as a form. */
.guide-rule { display: flex; align-items: center; gap: var(--sp-3); margin: var(--sp-5) 0 var(--sp-3); }
.guide-rule span {
  flex: none; font-size: 10px; font-weight: 800; letter-spacing: var(--tracking-wide);
  text-transform: uppercase; color: var(--accent);
}
.guide-rule::after {
  content: ''; flex: 1 1 auto; height: 1px;
  background: linear-gradient(90deg, rgba(var(--class-tint), .42), transparent);
}

.guide-prose { margin: 0; font-size: var(--fs-sm); line-height: 1.6; color: var(--ink-dim); }

/* ---- The gauge ----------------------------------------------------------------------------- */

/* Which spells move it, in both directions. Derived server-side off the same fields the caster
   checks, so this is the one part of a resource explainer that can never go stale. */
.guide-flows { display: flex; flex-direction: column; gap: var(--sp-2); margin-top: var(--sp-3); }
.guide-flow { display: grid; grid-template-columns: 78px 1fr; gap: var(--sp-3); align-items: baseline; }
.guide-flow__label {
  font-size: 10px; font-weight: 800; letter-spacing: var(--tracking-wide); text-transform: uppercase;
  text-align: right;
}
.guide-flow.is-up .guide-flow__label { color: #7fd6a2; }
.guide-flow.is-down .guide-flow__label { color: #e08a72; }
.guide-flow__names { display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-2); }
.guide-flow__names i {
  font-style: normal; font-size: var(--fs-xs); color: var(--ink-dim);
  padding: 1px var(--sp-2);
  background: rgba(255,255,255,.04);
  border: 1px solid var(--iron);
  border-radius: var(--r-sharp);
}

/* ---- The kit ------------------------------------------------------------------------------- */

/* Pictures rather than paragraphs.

   Every spell used to be a bordered card carrying its whole sentence — nine or ten boxes of prose
   on a page that already has prose on it, so the kit stopped being something you could take in and
   became something you read past on the way to the Weave. The game already ships one painted icon
   per ability and the action bar carries no names at all, which only works if the picture is
   learnable; a picture is learnable where it turns up beside its name often enough to stick. This
   is one more of those places. The sentence is on hover.

   Two shapes, because the groups are different questions: the fixed slots are yours whether you
   like it or not and only need recognising, and the pool is a choice, which wants a line. */
.guide-slots { display: flex; flex-wrap: wrap; gap: var(--sp-4); }
.guide-slot { display: flex; flex-direction: column; align-items: center; gap: 2px; width: 84px; text-align: center; }
.guide-slot b {
  margin-top: var(--sp-1);
  font-family: var(--font-display); font-size: var(--fs-xs); line-height: 1.25; color: var(--ink);
}
.guide-slot > span {
  font-size: 9px; font-weight: 800; letter-spacing: var(--tracking-wide); text-transform: uppercase;
  color: var(--accent-dim);
}
.guide-slot .ability-socket { --socket-size: 46px; border-color: rgba(var(--class-tint), .45); }

.guide-kit__note {
  margin: var(--sp-4) 0 var(--sp-2);
  font-size: var(--fs-xs); font-style: italic; color: var(--ink-faint);
}

.guide-picks { display: grid; grid-template-columns: repeat(auto-fill, minmax(238px, 1fr)); gap: var(--sp-1) var(--sp-4); }
.guide-pick { display: grid; grid-template-columns: 34px 1fr; align-items: center; gap: var(--sp-2); padding: var(--sp-1) 0; }
.guide-pick__body { display: flex; flex-direction: column; min-width: 0; }
.guide-pick__body b {
  font-family: var(--font-display); font-size: var(--fs-sm); line-height: 1.3;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.guide-pick__body > span {
  font-family: var(--font-numeric); font-variant-numeric: tabular-nums;
  font-size: var(--fs-xs); color: var(--ink-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---- The weave ----------------------------------------------------------------------------- */

/* Numbered, because the ORDER is the entire mechanic. These rows spent months as prose describing
   spells that did not exist, and nothing ever rendered them. */
.guide-weave {
  padding: var(--sp-3) var(--sp-4);
  background: rgba(var(--class-tint), .06);
  border: 1px solid rgba(var(--class-tint), .28);
  border-radius: var(--r-soft);
}
.guide-weave__name { font-family: var(--font-display); font-size: var(--fs-base); color: rgb(var(--class-tint)); }
.guide-weave__steps { margin: var(--sp-3) 0 0; padding: 0; list-style: none; counter-reset: weave; }
.guide-weave__steps li {
  position: relative; counter-increment: weave;
  padding: 0 0 var(--sp-3) var(--sp-6);
  font-size: var(--fs-sm); line-height: 1.6; color: var(--ink-dim);
}
.guide-weave__steps li:last-child { padding-bottom: 0; }
.guide-weave__steps li::before {
  content: counter(weave);
  position: absolute; left: 0; top: 0;
  width: 20px; height: 20px; display: grid; place-items: center;
  font-family: var(--font-numeric); font-size: 11px; font-weight: 800;
  color: rgb(var(--class-tint));
  background: rgba(var(--class-tint), .12);
  border: 1px solid rgba(var(--class-tint), .4);
  border-radius: 50%;
}
/* The step's own name — Primer, Link, Detonator — run in ahead of the sentence the way a printed
   recipe sets a stage name. */
.guide-weave__steps li span {
  display: block;
  font-size: 10px; font-weight: 800; letter-spacing: var(--tracking-wide); text-transform: uppercase;
  color: var(--ink-faint);
}
.guide-weave__payoff {
  margin: var(--sp-3) 0 0; padding-top: var(--sp-3);
  border-top: 1px solid rgba(var(--class-tint), .22);
  font-size: var(--fs-sm); line-height: 1.6; color: var(--ink-dim);
}
.guide-weave__payoff b { color: rgb(var(--class-tint)); font-family: var(--font-display); }

/* ---- The drink ----------------------------------------------------------------------------- */

.guide-secret {
  padding: var(--sp-4);
  background: linear-gradient(180deg, rgba(198,140,78,.10), rgba(0,0,0,.25));
  border: 1px solid rgba(198,140,78,.32);
  border-radius: var(--r-soft);
}
.guide-secret b { display: block; font-family: var(--font-display); font-size: var(--fs-base); color: var(--accent); }
.guide-secret p { margin: var(--sp-2) 0 0; font-size: var(--fs-sm); line-height: 1.65; color: var(--ink); }
.guide-secret__tease { color: var(--ink-faint) !important; font-style: italic; }
.guide-secret .ui-btn { margin-top: var(--sp-3); }
.guide-secret__short { display: block; margin-top: var(--sp-2); font-size: var(--fs-xs); color: var(--ink-faint); }
/* What one does, as against what it is like. Read off the effect it grants (ClassGuide.Grants), so
   it is a number and belongs in the accent the rest of the UI gives numbers - never italic, which
   is the tease's job. */
.guide-secret__grants {
  color: var(--accent) !important;
  font-family: var(--font-display);
  font-size: var(--fs-sm);
  letter-spacing: .02em;
}
/* Bought. Warmer, and framed as something she said rather than something you unlocked. */
.guide-secret.is-told {
  background: linear-gradient(180deg, rgba(198,140,78,.16), rgba(0,0,0,.28));
  border-color: rgba(198,140,78,.5);
}

/* ============================================================================================
   THE BOOK OF BOSSES
   ============================================================================================

   A spread: an index down the gutter and a plate beside it. It began as a list with a bench under
   it — the shape the Forge uses — and that shape is wrong here. The Forge's picker is a grid of
   sockets you glance at; this index is eighteen named rows you read, and stacked above a page of
   prose the two fought over one column.

   The art is the page, not a thumbnail on it. Each boss's own atlas frame sits behind the text,
   bled off the right edge and faded back, and the page is tinted to the colour of the creature
   printed on it — sampled from that same frame (spriteTint in js/spriteAssets.js), so a repainted
   boss brings its page with it and nobody maintains a palette.

   Entries are set the way a printed bestiary sets them: a seal, a heading, a hairline, then the
   figures run in on one line. They were a coloured bar with stacked label-over-value chips, which
   is how a tooltip looks — the panel read as a form that happened to be inside a book.
   ============================================================================================ */

.ui-panel--codex {
  --accent: #c8a45c; --accent-dim: #6b5220; --accent-wash: rgba(200,164,92,.10);
  /* Replaced per page by js/codex.js from the sprite's own dominant colour. The book's gold is the
     resting value, so an unopened or unpainted page is still warm rather than broken: rgba() with
     an unset custom property is an invalid declaration, not a transparent one. */
  --boss-tint: 200, 164, 92;
}
.ui-panel--codex .ui-panel__header { color: var(--accent); }
.ui-panel--codex .ui-panel__header::before, .ui-panel--codex .ui-panel__header::after {
  background: linear-gradient(90deg, transparent, var(--accent-dim), transparent);
}
/* Aged vellum over the same black stone every panel is cut from — warm rather than rune-blue,
   because this is the one station in town that is a book instead of a working surface. */
.ui-panel--codex {
  /* The wash bleeds to the panel's own edges, and the panel has a rounded frame it does not
     otherwise clip to. */
  overflow: hidden;
  background:
    radial-gradient(ellipse 120% 40% at 50% 100%, var(--accent-wash), transparent 70%),
    radial-gradient(ellipse at 50% 0%, rgba(97,84,64,.18), transparent 62%),
    linear-gradient(180deg, #14110c 0%, #0b0908 55%, #060505 100%);
}

/* ============================================================================================
   THE INDEX — a list you pick from, beside the thing you picked
   ============================================================================================

   Three rooms are this shape: the Book of Bosses, the Nether-Spire, and Maeve's roster. A column
   of things down one side, what the chosen one IS beside it, and the one button that acts on your
   choice docked ABOVE the column where scrolling cannot take it away from you.

   The dock is above the list rather than under the detail on purpose: it belongs to the choice,
   not to the description of it, and a button at the foot of a page is a button you have to reach
   the bottom of the page to find.

   A room with too few things to be worth rolling up takes .ui-index without .ui-spread and lists
   them flat — five heroes do not need three headings. The parts are the same either way.
   ============================================================================================ */

/* One width for every gutter in the game. A room is not more important because its list is wider,
   and three panels that are the same shape at three widths read as three shapes. */
.ui-spread { position: relative; flex: 1; min-height: 0; display: grid; grid-template-columns: var(--index-w, 288px) 1fr; }

/* The gutter. One rule down it, the way a bound book has one. */
.ui-index { display: flex; flex-direction: column; min-height: 0; }
.ui-spread > .ui-index { padding-right: var(--sp-4); border-right: 1px solid var(--index-rule); }
.ui-spread > .ui-detail { min-height: 0; display: flex; flex-direction: column; padding-left: var(--sp-5); }
/* Every room that has one tints its rule with its own accent rather than naming a colour. */
.ui-index, .ui-detail { --index-rule: color-mix(in srgb, var(--accent, var(--iron)) 28%, transparent); }

/* The list's own label, and it sits UNDER the main pages for that reason: it counts what is in the
   list, so putting it at the very top made it look like a heading for the whole rail. */
.ui-index__head {
  flex: none; padding: 0 0 var(--sp-2);
  font-size: 10px; font-weight: 800; letter-spacing: var(--tracking-wide); text-transform: uppercase;
  color: var(--ink-faint);
}

/* The pages that belong to no shelf: a book's frontispiece, Maeve herself, the crate under her bar.
   A rail reads top to bottom as MAIN PAGES, then the list — and nothing else goes up here. In
   particular no action: a button that DOES something belongs on the page it acts on, and one parked
   above a list makes the section mean two things at once. Gone rather than blank when a room has no
   such pages. */
.ui-index__pages {
  flex: none;
  display: flex; flex-direction: column; gap: var(--sp-2);
  padding-bottom: var(--sp-3); margin-bottom: var(--sp-2);
  border-bottom: 1px solid var(--index-rule);
}
.ui-index__pages:empty { display: none; }

/* A way to a page the list cannot hold: the book's frontispiece, Maeve herself. Set as a row like
   the entries under it rather than as a button, because it IS an entry — one that happens to sit
   above the shelves because it belongs to no shelf. Quieter than a .ui-btn, which would read as
   the thing to press and make the list beneath it look like the small print. */
.ui-index__link {
  display: flex; align-items: center; gap: var(--sp-2);
  width: 100%; padding: var(--sp-1) var(--sp-2);
  appearance: none; cursor: var(--cursor-interact); text-align: left;
  font-family: var(--font-display); font-size: var(--fs-sm);
  color: var(--ink-dim);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-sharp);
  transition: color var(--t-fast), background var(--t-fast), border-color var(--t-fast);
}
.ui-index__link::before { content: '¾f'; font-size: 10px; color: var(--accent-dim); }
.ui-index__link:hover { color: var(--ink); background: rgba(255,255,255,.05); }
.ui-index__link:focus-visible { outline: none; border-color: var(--rune-bright); box-shadow: var(--glow-rune); }
.ui-index__link.is-open {
  color: var(--ink);
  background: rgba(var(--entry-tint, 200, 164, 92), .15);
  border-color: rgba(var(--entry-tint, 200, 164, 92), .45);
}
.ui-index__link.is-open::before { color: var(--accent); }

/* Scrolls on its own, so the list never pushes the dock off the top or the panel off the bottom. */
.ui-index__list { flex: 1 1 auto; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 2px; padding-right: var(--sp-1); }

/* ---- The shelves ---------------------------------------------------------------------------
   One per group. A heading and a rule, not a panel inside a panel — an index is a gutter down the
   side of a room and a boxed section in it reads as a second window. A shelf you can roll up is
   how the column gets shorter once you are done with a set. */
.ui-shelf { display: flex; flex-direction: column; gap: 2px; }
.ui-shelf + .ui-shelf { margin-top: var(--sp-3); }
.ui-shelf__head {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-2) var(--sp-1) 0;
  cursor: var(--cursor-interact); list-style: none;
  border-bottom: 1px solid var(--index-rule);
  font-family: var(--font-display); font-size: var(--fs-xs); font-weight: 700;
  letter-spacing: var(--tracking-wide); text-transform: uppercase;
  color: var(--accent-dim);
  transition: color var(--t-fast), border-color var(--t-fast);
}
.ui-shelf__head::-webkit-details-marker { display: none; }
.ui-shelf__head:hover { color: var(--accent); }
.ui-shelf__head:focus-visible { outline: none; color: var(--accent); border-color: var(--rune-bright); box-shadow: 0 1px 0 var(--rune-bright); }
/* Drawn rather than the browser's marker, so it TURNS rather than swapping glyph — and so it sits
   on the baseline of a Cinzel heading instead of wherever the UA decides. */
.ui-shelf__head::before {
  content: ''; flex: none;
  width: 0; height: 0;
  border: 4px solid transparent; border-left-color: currentColor;
  margin-left: 2px;
  transition: transform var(--t-fast);
}
[open] > .ui-shelf__head::before { transform: rotate(90deg) translateX(1px); }
.ui-shelf__name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* How much of THIS shelf you have. The room's own progress line counts everything; this is what
   tells you which set you have unfinished business in. */
.ui-shelf__count { flex: none; font-family: var(--font-numeric); font-variant-numeric: tabular-nums; color: var(--ink-faint); }

/* ---- The entries -----------------------------------------------------------------------------
   One row, wherever a list is picked from. A mark on the left saying WHICH one this is — a rank, a
   face, a glyph — then who it is, then an optional word on the right for its state.

   --entry-mark is the only thing a room may change about the shape, because a portrait needs more
   room than a numeral. --entry-tint is what marks the open one, as "r, g, b": the book's gold, the
   Spire's violet, or in Maeve's index the CLASS's own colour, so which page you are reading is
   legible from the gutter without reading a word of it. */
.ui-entry {
  --entry-mark: 26px;
  display: grid; grid-template-columns: var(--entry-mark) 1fr auto; align-items: center; gap: var(--sp-2);
  width: 100%; padding: var(--sp-1) var(--sp-2);
  appearance: none; cursor: var(--cursor-interact); text-align: left;
  color: var(--ink);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-sharp);
  transition: color var(--t-fast), background var(--t-fast), border-color var(--t-fast);
}
.ui-entry:hover:not(:disabled):not([aria-disabled="true"]) { background: rgba(255,255,255,.05); }
.ui-entry:focus-visible { outline: none; border-color: var(--rune-bright); box-shadow: var(--glow-rune); }
/* The open page, marked the way a ribbon marks a book. */
.ui-entry.is-open {
  background: rgba(var(--entry-tint, 200, 164, 92), .15);
  border-color: rgba(var(--entry-tint, 200, 164, 92), .45);
}
.ui-entry__mark {
  font-family: var(--font-numeric); font-variant-numeric: tabular-nums;
  font-size: var(--fs-xs); font-weight: 800; text-align: right; color: var(--accent-dim);
}
.ui-entry.is-open .ui-entry__mark { color: var(--accent); }
.ui-entry__who { display: flex; flex-direction: column; min-width: 0; }
.ui-entry__who b { font-family: var(--font-display); font-size: var(--fs-sm); line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ui-entry__who span { font-size: var(--fs-xs); color: var(--ink-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The state word, if a room has one. Uppercase and quiet: it is the answer to "can I?", which you
   scan for, not something you read. */
.ui-entry__aside {
  font-size: var(--fs-xs); font-weight: 700;
  letter-spacing: var(--tracking-wide); text-transform: uppercase;
  color: var(--ink-faint);
}
/* Something you have not reached. Still listed — a list with gaps is a reason to keep going, and
   a list that hides them is just a shorter list. */
.ui-entry.is-locked { cursor: var(--cursor-unavailable); opacity: .38; }
.ui-entry.is-locked .ui-entry__who b { font-family: var(--font-body); font-style: italic; font-weight: 400; color: var(--ink-faint); }

/* The paper the whole panel is printed on.

   A child of the PANEL, not of the body inside it. Hung off the body it stopped at the body's box,
   which drew a dead straight edge along the bottom of the title bar and another one inside each
   margin — the one thing a wash must never have. Against the panel it runs to the rounded frame,
   which is the only edge here that is meant to be seen, and .ui-panel--codex clips it to that. */
.codex-wash {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  overflow: hidden;
}
/* Above the paper. Positioned so they paint over a positioned sibling regardless of source order. */
.ui-panel--codex > .ui-panel__header,
.ui-panel--codex > .ui-panel__body { position: relative; z-index: 1; }
/* Two pools rather than a band. The foot of the page was a 180deg linear gradient, which is the
   same colour all the way across and therefore ends in a hard vertical line against the index.
   Anchored past the bottom-right corner instead, it has faded out long before the gutter. */
.codex-wash::before {
  content: ''; position: absolute; inset: 0;
  background:
    radial-gradient(ellipse 56% 52% at 96% 0%, rgba(var(--boss-tint), .26), transparent 72%),
    radial-gradient(ellipse 86% 74% at 100% 102%, rgba(var(--boss-tint), .30), transparent 76%);
}

.codex-leaf { position: relative; }

/* The book's rows are the shared .ui-entry and nothing besides. */

/* ---- The plate ----------------------------------------------------------------------------- */

/* The leaf is just the column the page scrolls in now — the plate and the tint live on the wash
   behind the whole spread, because neither belongs to one column of it. */
/* .ui-detail carries the column and the gutter margin. This is only what the LEAF is: the paper a
   page scrolls over. */
.codex-empty { margin: auto; font-size: var(--fs-xs); color: var(--ink-faint); font-style: italic; }
.codex-empty[hidden] { display: none; }

/* The text, scrolling over the paper. Transparent on purpose — everything behind it is the leaf. */
.codex-page {
  position: relative; z-index: 1; flex: 1 1 auto; min-height: 0;
  /* overflow-x explicitly, not left to compute: a box with overflow-y:auto and overflow-x:visible
     has the x axis promoted to auto, so the plate bleeding past the right edge put a HORIZONTAL
     scrollbar across the foot of every page. The bleed lives on the clipped leaf now, and this is
     the belt to that braces. */
  overflow-y: auto; overflow-x: hidden;
}
.codex-page[hidden] { display: none; }
/* A hairline gutter rather than the chrome default sitting on top of the plate. */

/* The creature, printed at the head of the leaf and bled off the right edge the way a plate faces
   its page.

   SQUARE, and that is not a style choice. spriteCssCrop sets background-size as an inline style, so
   it wins over anything here — and a percentage background-size crops correctly only in a box whose
   aspect matches the frame's. Every atlas frame is square; this box was a wide rectangle, and every
   boss came out stretched. If the frames ever stop being square this needs the frame's own ratio,
   not a guess. */
.codex-art {
  position: absolute; top: 0; right: 0; width: 420px; height: 420px;
  background-repeat: no-repeat;
  opacity: .22;
  pointer-events: none;
  /* Into the paper rather than onto it, on every side, so no edge of the crop is ever visible as an
     edge. The top fade matters now that the plate runs up under the title bar: without it the
     painting stopped dead against the panel's top border. */
  -webkit-mask-image: linear-gradient(to left, #000 0%, #000 38%, transparent 96%),
                      linear-gradient(to bottom, transparent 0%, #000 22%, #000 58%, transparent 100%);
  -webkit-mask-composite: source-in;
  mask-image: linear-gradient(to left, #000 0%, #000 38%, transparent 96%),
              linear-gradient(to bottom, transparent 0%, #000 22%, #000 58%, transparent 100%);
  mask-composite: intersect;
}



.codex-leaf__body { position: relative; padding-right: var(--sp-4); }

.codex-plate { display: flex; flex-direction: column; gap: 1px; }
.codex-plate__tier {
  font-size: 10px; font-weight: 800; letter-spacing: var(--tracking-wide); text-transform: uppercase;
  color: var(--accent-dim);
}
.codex-plate__name {
  font-family: var(--font-display); font-size: 30px; line-height: 1.08; color: var(--accent);
  text-shadow: 0 2px 10px rgba(0,0,0,.85);
}
.codex-plate__title { font-size: var(--fs-base); color: var(--ink-dim); font-style: italic; }
/* Tiers 13-18 have no painted likeness yet. Said plainly on the page rather than left as a blank
   space, and better than borrowing another boss's portrait. */
.codex-plate__note {
  margin-top: var(--sp-2);
  font-size: 10px; font-weight: 700; letter-spacing: var(--tracking-wide); text-transform: uppercase;
  color: var(--ink-faint);
}

/* The frontispiece's tally. Figures large enough to read across the room, because that is the whole
   content of the page — how much of the book you have written. */
.codex-front { display: flex; gap: var(--sp-6); margin-top: var(--sp-5); }
.codex-front__stat { display: flex; flex-direction: column; }
.codex-front__stat b { font-family: var(--font-display); font-size: 30px; line-height: 1; color: var(--accent); }
.codex-front__stat span {
  margin-top: 2px;
  font-size: 10px; font-weight: 800; letter-spacing: var(--tracking-wide); text-transform: uppercase;
  color: var(--ink-faint);
}

.codex-page__flavor {
  max-width: 60ch;
  margin: var(--sp-4) 0 0;
  font-size: var(--fs-sm); color: var(--ink-dim); line-height: var(--lh-base); font-style: italic;
  /* A hanging rule down the left, the way a printed book sets an epigraph apart from the body. */
  padding-left: var(--sp-3);
  border-left: 2px solid rgba(var(--boss-tint), .45);
}

/* The phases a fight has, stated once at the head of the page.

   Two thresholds mean three phases, and a mechanic entry reading "At 66% health, 33% health" makes
   the reader work that out. A fight that changes gear is the first thing you want to know about it,
   so it is set as a strip under the epigraph rather than buried nine entries down. */
.codex-phases {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-1) var(--sp-3);
  margin-top: var(--sp-3); padding-left: var(--sp-3);
}
.codex-phases__label {
  font-size: 10px; font-weight: 800; letter-spacing: var(--tracking-wide); text-transform: uppercase;
  color: var(--accent-dim);
}
.codex-phase {
  display: inline-flex; align-items: baseline; gap: 5px;
  padding: 2px var(--sp-2);
  font-family: var(--font-numeric); font-variant-numeric: tabular-nums;
  font-size: var(--fs-xs); color: var(--ink-dim);
  background: rgba(var(--boss-tint), .12);
  border: 1px solid rgba(var(--boss-tint), .30);
  border-radius: var(--r-sharp);
}
.codex-phase i {
  font-style: normal; font-family: var(--font-display); font-size: var(--fs-xs); font-weight: 700;
  color: var(--accent);
}

/* A section rule with its label sitting in the break, which is the one piece of typography that
   makes a panel read as a page rather than as a form. */
.codex-rule { display: flex; align-items: center; gap: var(--sp-3); margin: var(--sp-5) 0 var(--sp-4); }
.codex-rule::before, .codex-rule::after { content: ''; flex: 1; height: 1px; background: linear-gradient(90deg, transparent, rgba(200,164,92,.34), transparent); }
.codex-rule span {
  flex: none;
  font-size: 10px; font-weight: 800; letter-spacing: var(--tracking-wide); text-transform: uppercase;
  color: var(--accent-dim);
}

/* ---- The entries --------------------------------------------------------------------------- */
.codex-mechs { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: var(--sp-6); align-items: start; }

.codex-mech { min-width: 0; }
.codex-mech__head {
  display: grid; grid-template-columns: auto 1fr auto; align-items: baseline; gap: var(--sp-2);
  padding-bottom: var(--sp-1);
  border-bottom: 1px solid rgba(200,164,92,.18);
}
/* The seal: a small lozenge in the mechanic's own telegraph colour, which is the whole of the
   colour coding now that the heavy bar down the side is gone. */
.codex-mech__seal {
  width: 7px; height: 7px; align-self: center;
  background: var(--mech-colour, var(--iron-light));
  transform: rotate(45deg);
  box-shadow: 0 0 8px var(--mech-colour, transparent);
}
.codex-mech__head b {
  font-family: var(--font-display); font-size: var(--fs-base); line-height: 1.2;
  color: var(--mech-colour, var(--ink));
}
.codex-mech__family {
  font-size: 9px; font-weight: 700; letter-spacing: var(--tracking-wide); text-transform: uppercase;
  color: var(--ink-faint); white-space: nowrap;
}
/* What you see, then what you do. Different weights on purpose: under pressure a player reads the
   second one and nothing else, so it has to be findable without reading the first. */
.codex-mech__tell { margin: var(--sp-2) 0 0; font-size: var(--fs-xs); color: var(--ink-faint); line-height: var(--lh-base); font-style: italic; }
.codex-mech__counter { margin: var(--sp-1) 0 0; font-size: var(--fs-sm); color: var(--ink); line-height: var(--lh-base); }

/* The figures, run in the way a printed bestiary sets measurements — not a row of stacked
   label-over-value chips, which is a stat block and reads as one.

   Wrapping rather than one nowrap line: several of these values are sentences ("140, split between
   everyone inside"), and holding them on one line ran them off the column and straight through the
   entry beside it. The gap does the separating that a middot would, and survives a line break. */
.codex-mech__numbers {
  display: flex; flex-wrap: wrap; gap: 2px var(--sp-4);
  margin: var(--sp-3) 0 0; font-size: var(--fs-xs); color: var(--ink-dim); line-height: 1.5;
}
.codex-number { font-family: var(--font-numeric); font-variant-numeric: tabular-nums; color: var(--gold-lit); }
.codex-number i {
  font-style: normal; font-family: var(--font-body); font-size: 10px; font-weight: 700;
  letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--ink-faint);
  margin-right: 5px;
}

/* A narrow window has no room for a gutter, so the index becomes a strip above the plate. */
@media (max-width: 1100px) {
  /* No room for two columns, so the spread becomes what it always was underneath: a list with the
     thing you picked under it. */
  .ui-spread { grid-template-columns: 1fr; grid-template-rows: minmax(0, 34%) 1fr; }
  .ui-spread > .ui-index { padding-right: 0; padding-bottom: var(--sp-3); border-right: 0; border-bottom: 1px solid var(--index-rule); }
  .ui-spread > .ui-detail { padding-left: 0; padding-top: var(--sp-3); }
  .codex-art { width: 260px; height: 260px; opacity: .16; }
  .codex-plate__name { font-size: 24px; }
}


/* ============================================================================================
   THE TAVERN
   ============================================================================================

   The roster is a shelf of people. It was a list of names with a rank beside each — which is a
   save-file picker, and the thing you are actually choosing between is six characters who look
   nothing like each other. .ui-portrait crops the runtime hero sheet, so the card shows the same
   figure that walks around Havenstead: no art was drawn for this panel and no PixiJS runs in it.

   The class cards underneath are cut the same way on purpose. Choosing which hero to play and
   choosing which hero to make are the same gesture over the same shelf, one seat further along.
   ============================================================================================ */

.tavern-pane { overflow: hidden; }
.tavern-pane .bag-well { flex: 1; min-height: 96px; }

/* `flex: 0 1 auto` rather than `1`: with two heroes on it the shelf takes the height of two heroes
   and the Recruit button sits under them, and with six it takes what is left and scrolls, with the
   button still under them. `flex: 1` would park that button at the foot of an empty panel. */
/* A shelf of portraits, so two across rather than the index's single column. `flex: 0 1 auto`
   rather than `1`: with two heroes on it the shelf is two heroes tall, and with six it takes what
   is left and scrolls. */
.tavern-roster {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-2);
  flex: 0 1 auto; align-content: start;
}

/* Two views, one tab. Which of my heroes do I play, and who am I adding, are different questions;
   stacked, they made a panel you scrolled past six portraits to reach a name field. */
/* The recruitment form is the tab's OTHER view, not a bench under this one — so the roster, its
   dock and the purse all step aside for it rather than being scrolled past. */
.ui-panel--tavern.is-recruiting .tavern-roster,
.ui-panel--tavern.is-recruiting #tavernActions,
.ui-panel--tavern.is-recruiting .tavern-purse { display: none; }

.tavern-hero, .tavern-class {
  display: flex; flex-direction: column; gap: 2px;
  min-width: 0; padding: var(--sp-2);
  appearance: none; cursor: var(--cursor-interact); text-align: center;
  color: var(--ink);
  background: linear-gradient(180deg, rgba(23,34,58,.9), rgba(5,9,15,.94));
  border: 1px solid var(--iron);
  border-radius: var(--r-sharp);
  box-shadow: var(--bevel-up), var(--cast);
  transition: border-color var(--t-fast), box-shadow var(--t-fast);
}
/* Lit under the pointer, never raised. A seat's two controls are pinned to the SEAT while the card
   is what would move, so a rise of 2px is the card sliding out from under the button being
   travelled to — and a shelf where only half the cards do that is worse than either. */
.tavern-hero:hover:not(:disabled), .tavern-class:hover:not(:disabled) {
  border-color: var(--stone-highlight);
}
.tavern-hero:focus-visible, .tavern-class:focus-visible {
  outline: none; border-color: var(--rune-bright); box-shadow: var(--bevel-up), var(--glow-rune);
}

/* The figure, and the two things written over it. Both are corner marks rather than lines of their
   own: a card this narrow has room for a name and a rank and nothing else. */
.tavern-hero__art { display: block; position: relative; }
.tavern-hero__art .ui-portrait { width: 100%; }

.tavern-hero__deed {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 3px 4px;
  font-size: 9px; font-weight: 800;
  letter-spacing: var(--tracking-wide); text-transform: uppercase;
  color: var(--ink-dim);
  background: linear-gradient(180deg, rgba(4,7,12,0), rgba(4,7,12,.88) 55%);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tavern-hero__asc {
  position: absolute; top: 3px; right: 3px;
  padding: 1px 4px;
  font-family: var(--font-numeric); font-variant-numeric: tabular-nums;
  font-size: 9px; font-weight: 800;
  color: var(--gold-lit);
  background: rgba(4,7,12,.82);
  border: 1px solid var(--gold-aged); border-radius: var(--r-soft);
}

.tavern-hero__name {
  margin-top: var(--sp-1);
  font-family: var(--font-display); font-size: var(--fs-sm); line-height: 1.15;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tavern-hero__rank { font-size: var(--fs-xs); color: var(--ink-dim); }

/* The one you are already playing. Not greyed out like a disabled control — it is the most
   important card on the shelf, it just is not a thing to press. */
.tavern-hero.is-you {
  cursor: var(--cursor-navigate);
  border-color: var(--gold-aged);
  box-shadow: inset 0 0 0 1px rgba(var(--gold-rgb), .5), var(--glow-torch);
}
.tavern-hero.is-you .tavern-hero__deed { color: var(--gold-lit); }

/* ---- The seat -------------------------------------------------------------------------------
   The card you press to play and the controls that act on it cannot be the same element — a button
   inside a button is not something the browser will build — and they must not carry the same weight
   either. Both are hidden and unclickable until the seat is hovered or something in it has focus,
   so a shelf at rest is a row of people and no hazards. */
.tavern-seat { position: relative; display: flex; min-width: 0; }
.tavern-seat > .tavern-hero { flex: 1; min-width: 0; }

/* Both of the seat's controls: same socket, same corner, same appearing-on-hover, because they are
   the two things you do TO a hero rather than with them. Only the colour differs, and it is the
   whole difference — so each one declares its own --seat-* set and nothing else is written twice. */
.tavern-seat__dismiss, .tavern-seat__rename {
  position: absolute; top: 4px;
  width: 22px; height: 22px;
  display: grid; place-items: center;
  padding: 0; appearance: none; cursor: var(--cursor-interact);
  font-size: 11px; line-height: 1;
  color: var(--seat-ink);
  background: var(--seat-face);
  border: 1px solid var(--seat-edge);
  border-radius: var(--r-soft);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.08), var(--cast);
  /* pointer-events too, not opacity alone: an invisible destructive button over the top-left
     corner of a card you click constantly is a trap that only springs occasionally. */
  opacity: 0; pointer-events: none;
  transition: opacity var(--t-fast), color var(--t-fast), background var(--t-fast), border-color var(--t-fast);
}
/* The close tab at socket size — same red, same plate, 1px of edge instead of 2 because 2 on a
   22px square is mostly border. A control that destroys a hero and a control that shuts a window
   should not need telling apart by colour. */
.tavern-seat__dismiss {
  left: 4px;
  --seat-ink: var(--dismiss-ink); --seat-face: var(--dismiss-face); --seat-edge: var(--dismiss-edge);
  --seat-ink-lit: var(--dismiss-ink-lit); --seat-face-lit: var(--dismiss-face-lit); --seat-edge-lit: var(--dismiss-edge-lit);
}
/* Iron rather than red, in the ghost button's own colours (.ui-btn--ghost): renaming is the one
   thing on this card a player may do twice, and it must not read as the neighbouring hazard. */
.tavern-seat__rename {
  left: 30px;
  --seat-ink: var(--ink-dim); --seat-face: linear-gradient(180deg, #18212c, #101720); --seat-edge: #35404f;
  --seat-ink-lit: var(--ink); --seat-face-lit: linear-gradient(180deg, #222d3b, #161f2b); --seat-edge-lit: #505b68;
}
.tavern-seat:hover .tavern-seat__dismiss, .tavern-seat:focus-within .tavern-seat__dismiss,
.tavern-seat:hover .tavern-seat__rename, .tavern-seat:focus-within .tavern-seat__rename { opacity: 1; pointer-events: auto; }
.tavern-seat__dismiss:hover, .tavern-seat__rename:hover {
  color: var(--seat-ink-lit); background: var(--seat-face-lit); border-color: var(--seat-edge-lit);
}
.tavern-seat__dismiss:focus-visible, .tavern-seat__rename:focus-visible {
  opacity: 1; pointer-events: auto; outline: none;
  color: var(--seat-ink-lit); border-color: var(--rune-bright); box-shadow: var(--cast), var(--glow-rune);
}

/* ---- Recruitment ---------------------------------------------------------------------------
   The same bench shape the Forge uses: it opens under the roster rather than over it, so the
   heroes you already have stay on screen while you decide what you are missing. */
.tavern-recruit {
  flex: 1; min-height: 0;
  display: flex; flex-direction: column; gap: var(--sp-2);
}
/* No sunken plate around it any more. It was a bench under a shelf and needed its own edge to read
   as a separate thing; it is the whole view now, and a frame inside the panel's frame is just a
   second frame. */
.tavern-recruit[hidden] { display: none; }
/* The grid scrolls; the name field above it and the two buttons below it do not, so the answer and
   the way out are never scrolled off. */
.tavern-recruit__actions { flex: none; display: flex; gap: var(--sp-2); }
.tavern-recruit__actions .ui-btn { flex: 1; }

.tavern-class-grid {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-2); margin: 0;
  flex: 0 1 auto; min-height: 0; overflow-y: auto; align-content: start;
}
/* Chosen reads as lit rather than merely outlined — this is the one decision in the form. */
.tavern-class.selected { border-color: var(--gold-lit); box-shadow: inset 0 0 0 1px rgba(var(--gold-rgb), .5), var(--glow-torch); }
/* A class you already have. Dimmed but still drawn, with the reason written on it: a grid that
   silently shortens itself is a rule the player has to infer. */
.tavern-class.is-taken { opacity: .45; cursor: var(--cursor-unavailable); }
.tavern-class.is-taken .ui-portrait { filter: grayscale(.8); }
/* A roster card's ribbon is a two-word verdict and stays on one line. The class grid's is a
   sentence, and clipping it to "Already on this accou…" makes the one card that needs a reason
   the one card that does not give you one. */
.tavern-class .tavern-hero__deed { white-space: normal; line-height: 1.2; }

/* Four paintings and nothing else, tight enough to stay on one row inside a rail-width card. */
.tavern-class__kit { display: flex; justify-content: center; gap: 2px; margin-top: var(--sp-1); }
.tavern-class__kit .ability-socket { --socket-size: 24px; }
.tavern-class.is-taken .tavern-class__kit { filter: grayscale(.75); }


/* ============================================================================================
   THE SHRINE — an actual tree
   ============================================================================================

   Talents were a flat grid of three cards, three cards, two cards, with a heading over each row
   saying "Tier 1", "Tier 2", "Tier 3". Nothing on screen said the rows were connected, which is
   the only interesting fact about a talent tree: tier 2 is closed until tier 1 has been paid into.
   The connectors carry that now, and they light as the tier behind them opens.
   ============================================================================================ */

.shrine-pane { overflow-y: auto; }
.shrine-points { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }

.skill-tree { display: flex; flex-direction: column; align-items: center; }
.skill-tier { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-2); width: 100%; }
.skill-tier--capstone { grid-template-columns: repeat(2, minmax(0, 1fr)); width: 78%; }

/* The rung between two tiers: a rail with a drop into each node below it. Drawn rather than art so
   it holds at any rail width. Dim until the tier it feeds is actually reachable. */
.skill-link {
  position: relative; width: 100%; height: 26px;
  --link: var(--iron);
}
.skill-link.is-open { --link: var(--rune); }
.skill-link::before {
  content: ''; position: absolute; left: 16%; right: 16%; top: 50%; height: 2px;
  background: var(--link);
  box-shadow: 0 0 8px color-mix(in srgb, var(--link) 60%, transparent);
}
/* The drops into the three nodes below, so the rung reads as feeding them rather than as a rule
   between two unrelated rows. */
.skill-link i {
  position: absolute; top: 50%; bottom: 0; width: 2px; background: var(--link);
}
.skill-link i:nth-child(1) { left: 16%; }
.skill-link i:nth-child(2) { left: 50%; transform: translateX(-1px); }
.skill-link i:nth-child(3) { left: calc(84% - 2px); }
/* And the stem up out of the tier above. */
.skill-link::after {
  content: ''; position: absolute; left: 50%; top: 0; height: 50%; width: 2px;
  transform: translateX(-1px);
  background: var(--link);
}

.skill-node {
  position: relative;
  display: flex; flex-direction: column; gap: 2px;
  padding: var(--sp-2);
  text-align: center;
  background: linear-gradient(180deg, #101a2a, #070d16);
  border: 1px solid var(--iron);
  border-radius: var(--r-sharp);
  box-shadow: var(--bevel-down);
  cursor: var(--cursor-interact);
  transition: border-color var(--t-fast), box-shadow var(--t-fast);
}
.skill-node:hover:not([aria-disabled="true"]) { border-color: var(--rune); box-shadow: var(--bevel-down), var(--glow-rune); }
/* Dimmed only when it is LOCKED. A maxed talent is not dimmed — it is finished, which is the
   opposite of unavailable — and one you simply cannot afford right now still reads at full
   strength, because tomorrow you can. */
.skill-node.is-locked { opacity: .45; }
.skill-node[aria-disabled="true"] { cursor: var(--cursor-unavailable); }
.skill-node:focus-visible { outline: none; border-color: var(--rune-bright); box-shadow: var(--bevel-down), var(--glow-rune); }
/* Anything with a point in it is lit from inside — the tree should be readable at a glance as
   "where have I spent". */
.skill-node.is-invested { border-color: var(--rune); background: linear-gradient(180deg, #14233c, #081020); }
.skill-node.is-maxed { border-color: var(--gold-lit); box-shadow: var(--bevel-down), var(--glow-torch); }
.skill-node__name { font-family: var(--font-display); font-size: var(--fs-xs); color: var(--ink); line-height: var(--lh-tight); }
.skill-node.is-maxed .skill-node__name { color: var(--gold-lit); }
.skill-node__rank { font-family: var(--font-numeric); font-variant-numeric: tabular-nums; font-size: 10px; font-weight: 800; color: var(--ink-faint); }
.skill-node.is-invested .skill-node__rank { color: var(--rune-bright); }
/* Pips rather than "3 / 10": at socket size the number is the same width as the bar and says less
   about how much is left. */
.skill-node__pips { display: flex; justify-content: center; gap: 2px; flex-wrap: wrap; }
.skill-node__pips i { width: 4px; height: 4px; border-radius: var(--r-round); background: var(--iron-light); }
.skill-node__pips i.on { background: var(--rune-bright); box-shadow: 0 0 4px var(--rune-bright); }
.skill-node.is-maxed .skill-node__pips i.on { background: var(--gold-lit); box-shadow: 0 0 4px var(--gold-lit); }

.skill-tier-label { font-family: var(--font-body); font-size: 10px; font-weight: 700; letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--ink-faint); margin: var(--sp-2) 0 var(--sp-1); }

.shrine2-trees { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-3); align-items: start; }
.shrine2-active-spec { margin: var(--sp-1) 0; color: var(--rune-bright); }
.shrine2-trees[data-tree-count="1"] { grid-template-columns: minmax(0, 1fr); }
.shrine2-trees[data-tree-count="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.shrine2-trees[data-tree-count="1"] .shrine2-tier__nodes,
.shrine2-trees[data-tree-count="2"] .shrine2-tier__nodes { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.shrine2-tree { min-width: 0; border: 1px solid var(--iron); background: rgba(7, 13, 22, .58); padding: var(--sp-2); }
.shrine2-tree__header { display: flex; justify-content: space-between; align-items: baseline; gap: var(--sp-2); border-bottom: 1px solid var(--iron); margin-bottom: var(--sp-2); }
.shrine2-tree__header h3 { margin: 0 0 var(--sp-2); color: var(--rune-bright); font-family: var(--font-display); }
.shrine2-tree__header span, .shrine2-tier__gate, .shrine2-capstone-rule { color: var(--ink-faint); font-size: var(--fs-xs); }
.shrine2-tree__header .ui-tag { color: var(--ink-dim); vertical-align: middle; }
.shrine2-tier { min-height: 94px; margin-bottom: var(--sp-2); }
.shrine2-tier__gate { margin: 0 0 var(--sp-1); }
.shrine2-tier__nodes { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-2); }
.shrine2-node { min-height: 57px; width: 100%; position: relative; }
.shrine2-node.is-available { border-color: var(--rune-bright); box-shadow: var(--bevel-down), inset 0 0 0 1px rgba(76, 157, 241, .42), var(--glow-rune); }
.skill-node.shrine2-node--keystone, .skill-node.shrine2-node--capstone { min-height: 76px; font-size: 1.08em; border: 2px solid var(--gold-lit); box-shadow: var(--bevel-down), inset 0 0 0 1px rgba(var(--gold-rgb), .32); }
.shrine2-node__summary { font-size: 10px; color: var(--ink-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.shrine2-node__tradeoff { color: #ed7777; }
.shrine2-node.is-excluded { opacity: .3; filter: grayscale(1); }
.shrine2-capstone-rule { color: var(--gold-lit); margin-bottom: var(--sp-1); }
.shrine2-coming { min-height: 150px; display: grid; place-items: center; color: var(--ink-faint); }
.shrine2-controls { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; font-size: var(--fs-xs); }
.shrine2-board { border-top: 1px solid var(--iron); padding-top: var(--sp-3); }
.shrine2-board h3 { margin: 0 0 var(--sp-2); color: var(--rune-bright); }
.shrine2-board [data-pane="loadout"] { display: block; }
.shrine2-board .lo-keys { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); }
.shrine2-pages { display: flex; gap: var(--sp-2); flex-wrap: wrap; width: 100%; }
.shrine2-controls { margin-bottom: var(--sp-3); }
.shrine2-page-loadout { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: var(--sp-2); }
.shrine2-slot { min-width: 0; display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2); }
.shrine2-slot select { width: 100%; min-width: 0; background: var(--void); color: var(--ink); border: 1px solid var(--iron); }
.shrine2-choice-picker { grid-column: 1 / -1; display: flex; gap: var(--sp-2); flex-wrap: wrap; align-items: center; padding: var(--sp-2); border: 1px solid var(--gold-lit); }
.shrine2-prereq { color: var(--ink-faint); position: absolute; top: 2px; right: 4px; }
.shrine2-prereq.is-open { color: var(--rune-bright); }
.shrine2-pending { font-size: var(--fs-xs); color: var(--gold-lit); }
.shrine2-board .lo-card { min-width: 0; }
@media (max-width: 900px) {
  .ui-rail--wide[data-wide="talents"] { width: calc(100vw - 2 * var(--sp-3) - var(--rail-tab-w)); }
  .shrine2-trees { grid-template-columns: minmax(0, 1fr); }
  .shrine2-trees[data-tree-count="2"] { grid-template-columns: minmax(0, 1fr); }
  .shrine2-trees[data-tree-count="1"] .shrine2-tier__nodes,
  .shrine2-trees[data-tree-count="2"] .shrine2-tier__nodes { grid-template-columns: minmax(0, 1fr); }
  .shrine2-board .lo-keys { grid-template-columns: minmax(0, 1fr); }
  .shrine2-page-loadout { grid-template-columns: minmax(0, 1fr); }
}


/* ---- The ability socket -------------------------------------------------------------------
   The action bar has no lettering on it: a tile is a painting and a key cap. That only works if
   the painting is learnable, and nothing is learnable from the one place you are too busy to look
   at it. So the socket is a primitive, and it goes anywhere a spell is named at rest — the class
   you are choosing, the Shrine's two lists, the hero sheet's key map, the rift's swap panel.

   Deliberately the same frame as .ability-btn (iron edge, stone gradient, top bevel, 2px corner)
   rather than a lighter list-friendly version of it. The sockets have to read as the same object
   in every panel or they teach nothing; a spell you met in the Shrine has to be recognisably the
   same thing on the bar.

   No tone tint on the frame. The bar colours its border by damage school, and repeating that here
   would put six hues down a list that is already carrying the art's own colour — and the art was
   painted to flood its tile, which is exactly the thing a coloured edge would fight. */
.ability-socket {
  --socket-size: 34px;
  display: block; position: relative; flex: none;
  width: var(--socket-size); height: var(--socket-size);
  border: 1px solid var(--iron-light);
  border-radius: var(--r-sharp);
  background: linear-gradient(180deg, var(--stone-base), var(--stone-abyss));
  box-shadow: var(--bevel-up);
  overflow: hidden;
  transition: border-color var(--t-fast);
}
.ability-socket--sm { --socket-size: 26px; }
.ability-socket--md { --socket-size: 34px; }
.ability-socket--lg { --socket-size: 44px; }
.ability-socket__img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* Two different absences, and they have to look different.

   is-missing is a spell whose art has not been drawn yet — the socket holds _missing.png, a
   desaturated question mark, and only has to stop claiming to be a finished tile. The icon set is
   a fixed 58 files and the ability catalog is not, so this is a state the game will reach.

   is-blank is nothing at all: an unassigned key, or a socket whose placeholder is missing too. An
   empty cut well says that, and a broken-image mark says the page is broken. Both are set by
   AbilityIcon.js from a capture-phase error; is-blank is also asked for directly. */
.ability-socket.is-missing { border-color: var(--iron); }
.ability-socket.is-blank {
  background: var(--stone-deep);
  box-shadow: var(--bevel-down);
}
.ability-socket.is-blank::after {
  content: '✦';
  position: absolute; inset: 0; display: grid; place-items: center;
  font-size: calc(var(--socket-size) * .4); line-height: 1;
  color: var(--ink-faint); opacity: .45;
}

/* The row lights its own socket, so the art picks up the hover rather than sitting dead inside a
   row that moved. */
.spell-row:hover .ability-socket,
.lo-card:hover .ability-socket,
.ability-sheet__row:hover .ability-socket { border-color: var(--stone-highlight); }

/* ---- Spells: loadout and mastery ---------------------------------------------------------- */
.spell-list { display: flex; flex-direction: column; gap: var(--sp-2); }
.spell-row {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  background: linear-gradient(180deg, #101a2a, #070d16);
  border: 1px solid var(--iron);
  border-radius: var(--r-sharp);
  box-shadow: var(--bevel-down);
}
.spell-row__body { flex: 1; min-width: 0; }
.spell-row__name { font-family: var(--font-display); font-size: var(--fs-sm); color: var(--ink); }
.spell-row__desc { font-size: var(--fs-xs); color: var(--ink-dim); line-height: var(--lh-base); }
/* The sentence carries the spell's number inside it; this line carries the two facts that are not
   part of a sentence — what it does in three words, and how long until you can do it again. Set in
   the numeric face and a shade brighter than the prose, because it is the line a player compares
   between two rows, and prose is the line they read once. */
.spell-row__facts {
  margin-top: 2px;
  font-family: var(--font-numeric); font-size: 11px; font-weight: 700;
  letter-spacing: .02em; color: var(--ink);
}
.spell-row.is-picked .spell-row__facts { color: var(--rune-bright); }
.spell-row.is-maxed .spell-row__facts { color: var(--gold-lit); }
.spell-row.is-picked { border-color: var(--rune); background: linear-gradient(180deg, #14233c, #081020); }
/* Keys full, so this spell cannot be taken right now. The row used to dim as a whole, which was
   right when a row was two lines of text and wrong the moment it carried a painting: at .5 over a
   dark panel the darker icons stopped being readable at all, and recognising the picture is the
   only reason it is in this list. So the words dim and the art does not — the disabled button
   already says the row is unavailable, in the vocabulary every other disabled control uses. */
.spell-row.is-locked .spell-row__body { opacity: .5; }
.spell-row.is-locked .ability-socket { opacity: .85; }
/* Mastery on the frame rather than only in the sentence. A spell you have paid into is worth
   picking out of a list of ten at a glance, and the metal is the game's own vocabulary for it:
   rune inlay once it is started, aged gold once it is finished. Same two colours the talent pips
   use, for the same reason. */
.spell-row.is-picked .ability-socket { border-color: var(--rune); box-shadow: var(--bevel-up), var(--glow-rune); }
/* Finished, not merely started — the whole row turns, not just the socket. Rank 3 is the end of a
   560g investment and it should be findable by colour in a list of thirteen; with only the socket
   edge changed it was indistinguishable from rank 1 at a glance. */
.spell-row.is-maxed { border-color: var(--gold-aged); background: linear-gradient(180deg, #1f1a0e, #0c0a06); }
.spell-row.is-maxed .ability-socket { border-color: var(--gold-lit); box-shadow: var(--bevel-up), var(--glow-torch); }

/* ---- The loadout board -----------------------------------------------------------------------
   Every spell a class owns is the same card, whether it is on a number key or waiting in the pool.
   That is the whole design: the thing you pick up has to look like the thing you drop it onto, or
   the gesture reads as filing an object into a different kind of container rather than as swapping
   two of a kind. Before this, the bound spells and the available ones were two different shapes
   with two different sets of buttons, and moving a spell between them was a third verb again.

   The key cap is the only part that is not on both cards, because it is the only fact on a card
   that is not about the spell. See js/ui/LoadoutBoard.js for the rule the board runs on. */
.lo-keys, .lo-pool { display: flex; flex-direction: column; gap: var(--sp-2); }
.lo-head {
  display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between;
  gap: 0 var(--sp-2);
}
/* The label never reflows: when the status beside it grows into a sentence, it is the status that
   takes a line of its own. A heading that rewraps because something next to it changed reads as
   the screen shifting under you. */
.lo-head > .ui-label { flex: none; }
.lo-status { flex: 1 1 auto; text-align: right; }

.lo-card {
  display: flex; align-items: center; gap: var(--sp-3);
  width: 100%; padding: var(--sp-2) var(--sp-3);
  text-align: left; appearance: none; cursor: var(--cursor-navigate);
  /* So a drag on a touch screen is a drag and not a scroll — the same reason .ui-slot has it. */
  touch-action: none;
  background: linear-gradient(180deg, #101a2a, #070d16);
  border: 1px solid var(--iron);
  border-radius: var(--r-sharp);
  box-shadow: var(--bevel-down);
  transition: border-color var(--t-fast), box-shadow var(--t-fast), transform var(--t-fast), opacity var(--t-fast);
}
.lo-card:hover { border-color: var(--iron-light); }
.lo-card:active { cursor: var(--cursor-carry); }

/* Bound, so it carries the rune the rest of the game uses for "this is yours and it is live". */
.lo-card--key { border-color: var(--rune); background: linear-gradient(180deg, #14233c, #081020); }

/* A proper key cap, not a chip: the number you are going to press under pressure is the thing this
   half of the board is ABOUT, so it leads the card and it is lettered like a key. */
.lo-card__key {
  flex: none; width: 34px; height: 34px;
  display: grid; place-items: center;
  font-family: var(--font-numeric); font-size: 17px; font-weight: 800; line-height: 1;
  color: var(--ink-on-light);
  background: linear-gradient(180deg, var(--gold-lit), var(--gold-aged));
  border: 1px solid #2a2208;
  border-radius: var(--r-soft);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.4), inset 0 -2px 0 rgba(0,0,0,.35), 0 1px 2px rgba(0,0,0,.6);
}

.lo-card__body { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.lo-card__name { font-family: var(--font-display); font-size: var(--fs-sm); color: var(--ink); }
.lo-card__name em { font-style: normal; font-size: var(--fs-xs); color: var(--rune-bright); }
.lo-card__desc { font-size: var(--fs-xs); color: var(--ink-dim); line-height: var(--lh-base); }
/* The sentence carries the spell's number inside it; this line carries the two facts that are not
   part of a sentence. Same treatment as .spell-row__facts, because it is the same line. */
.lo-card__facts {
  margin-top: 2px;
  font-family: var(--font-numeric); font-size: 11px; font-weight: 700;
  letter-spacing: .02em; color: var(--ink);
}
.lo-card--key .lo-card__facts { color: var(--rune-bright); }

/* Mastery on the frame, in the game's own metals: rune inlay once a spell has been paid into, aged
   gold once it is finished. The same two colours the talent pips and the mastery list use. */
.lo-card.is-mastered .ability-socket { border-color: var(--rune); box-shadow: var(--bevel-up), var(--glow-rune); }
.lo-card.is-maxed .ability-socket { border-color: var(--gold-lit); box-shadow: var(--bevel-up), var(--glow-torch); }

/* The one thing on the card that is not information. With the buttons gone, nothing else says the
   card is a thing you take hold of. */
.lo-card__grip {
  flex: none; align-self: stretch; display: grid; place-items: center;
  font-size: 15px; line-height: 1; color: var(--ink-faint);
  transition: color var(--t-fast);
}
.lo-card:hover .lo-card__grip { color: var(--ink-dim); }

.lo-card.is-dragging { opacity: .35; }
/* Lifted by click or by keyboard: the same state a drag ghost puts the card in, held still so it
   is obvious what is in your hand while you look for where to put it. */
.lo-card.is-lifted {
  border-color: var(--gold-lit);
  box-shadow: var(--bevel-up), var(--glow-torch);
  transform: translateY(-1px);
}
/* .is-drop-target is the shared primitive — see the gear drag above. */

/* What replaced the Save button.
   Removing a commit control without putting anything in its place leaves the player unsure whether
   what they did took, so the line says so instead: it sits beside the keys where the change
   happens, it is a live region, and it is quiet until it has something to report. */
.lo-status {
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: var(--tracking-wide);
  text-transform: uppercase; color: var(--ink-faint);
  transition: color var(--t-fast);
}
.lo-status.is-saving { color: var(--ink-dim); }
.lo-status.is-saved { color: var(--rune-bright); }
.lo-status.is-lifted { color: var(--gold-lit); text-transform: none; letter-spacing: 0; font-weight: 600; }
.lo-status.is-failed { color: var(--ember); text-transform: none; letter-spacing: 0; }


/* ============================================================================================
   RIFTS — one list, two bands
   ============================================================================================

   Was two modals, one of which opened the other. The list scrolls and the bench does not: the
   thing you have chosen stays put while you look past it at the others, which is the whole reason
   this is a panel and not a stack of dialogs.
   ============================================================================================ */


/* The Spire's rows are .ui-entry like every other list. What is its own: the tier numeral is cut
   into stone rather than set as a figure — that is what makes the list read as a ladder — and the
   four states each have to be readable at a glance from the far side of the panel, because which
   vault is next is the only question this list answers. */
.rift-row { --entry-mark: 32px; --entry-tint: 180, 138, 228; }
.rift-row__tier {
  display: grid; place-items: center;
  width: 32px; height: 32px;
  font-family: var(--font-display); font-size: var(--fs-base); font-weight: 700;
  color: var(--ink-dim);
  background: var(--stone-deep);
  border: 1px solid var(--iron);
  border-radius: var(--r-sharp);
  box-shadow: var(--bevel-down);
}
.rift-row.is-cleared   .rift-row__status { color: var(--vitality); }
.rift-row.is-cleared   .rift-row__tier   { color: var(--ink); border-color: rgba(63,159,95,.45); }
.rift-row.is-available .rift-row__status { color: var(--accent); }
.rift-row.is-available .rift-row__tier {
  color: var(--ink); border-color: var(--accent-dim);
  box-shadow: var(--bevel-down), 0 0 12px rgba(180,138,228,.35);
}
.rift-row.is-gated     .rift-row__status { color: var(--warning); }
.rift-row.is-gated     .rift-row__tier   { border-color: rgba(217,162,39,.4); }
.rift-row.is-locked    { cursor: var(--cursor-unavailable); }
.rift-row.is-locked:hover { transform: none; background: linear-gradient(90deg, rgba(255,255,255,.035), transparent); }

/* Selected wins over every status colour above it: it is the one row the bench is talking about. */
.rift-row.is-selected {
  background: linear-gradient(90deg, rgba(180,138,228,.18), transparent);
  border-color: var(--accent-dim); border-left-color: var(--accent);
}

/* ---- the bench: the tier you picked, and the way in ---------------------------------------- */

/* ---- The vault, as a page ---------------------------------------------------------------------
   The Spire is six rooms with something living in each, and it was a list with a bench under it —
   which describes the rooms without ever showing you one. It is printed like the Book of Bosses now
   and for the same reason: what is waiting in there is the whole point of choosing it.

   --boss-tint is sampled off the occupant's own sprite (rifts.js applyVaultArt), so the room is lit
   by whoever is in it, and repainting a boss relights his vault without anybody writing a colour
   down. The Spire's violet is the resting value, for the page with nothing chosen on it. */
.ui-panel--rifts { --boss-tint: 180, 138, 228; }

/* The Spire is a three-part workbench: choose a vault, read and prime its seal, then choose a
   stone from the satchel. Each part owns its own height, so adding keystones never pushes the
   socket or the way in below the viewport. */
.rift-spread {
  grid-template-columns: minmax(210px, 240px) minmax(370px, 1fr) minmax(190px, 230px);
}
.rift-spread > .rift-detail {
  position: relative;
  padding-right: var(--sp-4);
  border-right: 1px solid var(--index-rule);
}
.rift-inventory {
  min-width: 0; min-height: 0;
  display: flex; flex-direction: column;
  padding-left: var(--sp-4);
  overflow-y: auto;
  --index-rule: color-mix(in srgb, var(--accent, var(--iron)) 28%, transparent);
}
.rift-inventory[hidden] { display: none; }
#riftKeystoneInventory { min-height: 0; display: flex; flex-direction: column; gap: var(--sp-3); }
.rift-inventory .keystone-pack-label {
  flex-direction: column; align-items: flex-start; gap: 3px;
  padding-bottom: var(--sp-2);
  border-bottom: 1px solid rgba(180,138,228,.24);
  font-family: var(--font-display); font-size: var(--fs-sm); color: #e3cfa5;
}
.rift-inventory .keystone-pack-label > span { font-family: var(--font-body); }
.rift-inventory .keystone-tray { display: flex; flex-direction: column; gap: var(--sp-2); }
.rift-inventory .keystone-stone { width: 100%; }

.rift-wash { position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
/* Above the paper. Positioned so they paint over a positioned sibling regardless of source order. */
.ui-panel--rifts > .ui-panel__header,
.ui-panel--rifts > .ui-panel__body { position: relative; z-index: 1; }
.rift-wash::before {
  content: ''; position: absolute; inset: 0;
  background:
    radial-gradient(ellipse 54% 50% at 96% 0%, rgba(var(--boss-tint), .24), transparent 72%),
    radial-gradient(ellipse 88% 72% at 100% 102%, rgba(var(--boss-tint), .28), transparent 76%);
  transition: background var(--t-base);
}

/* Printed INTO the page, never onto it: masked away on every side so no edge of the crop is ever
   visible as an edge, which is what separates a printed illustration from a pasted cut-out. */
.rift-art {
  position: absolute; top: 0; right: 0; width: 360px; height: 360px;
  background-repeat: no-repeat;
  opacity: .20;
  pointer-events: none;
  -webkit-mask-image: radial-gradient(ellipse 70% 70% at 62% 44%, #000 42%, transparent 78%);
          mask-image: radial-gradient(ellipse 70% 70% at 62% 44%, #000 42%, transparent 78%);
}
.rift-art[hidden] { display: none; }
.rift-detail > .rift-art { right: var(--sp-4); }

/* The page beside the index: what this vault is, what bars it, and what is socketed into it. No
   box around it — it IS the right-hand side of the spread, and a bordered card inside a column
   that already has a rule down its edge reads as a panel inside a panel. */
.rift-bench {
  position: relative;
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  display: flex; flex-direction: column; gap: var(--sp-2);
  padding-bottom: 80px;
  -webkit-mask-image: linear-gradient(to bottom, #000 0, #000 calc(100% - 72px), transparent 100%);
          mask-image: linear-gradient(to bottom, #000 0, #000 calc(100% - 72px), transparent 100%);
}
.rift-bench[hidden] { display: none; }
/* The way in stays at the foot of the detail column while the selected vault page scrolls behind
   it. The fade keeps the footer legible without making it look like a separate toolbar. */
.rift-detail__footer {
  position: absolute; z-index: 3; left: var(--sp-5); right: var(--sp-4); bottom: 0;
  padding: var(--sp-3) 0 var(--sp-1);
}
.rift-detail__footer[hidden] { display: none; }
.rift-enter { width: min(260px, 100%); }

/* The headline, set like the book's plate so a vault and its occupant's page are plainly about the
   same thing — rank in small caps, the name carved, the epithet under it. */
.rift-plate { display: flex; flex-direction: column; gap: 1px; }
.rift-plate__rank {
  font-size: 10px; font-weight: 800; letter-spacing: var(--tracking-wide); text-transform: uppercase;
  color: var(--accent-dim);
}
.rift-plate__name {
  font-family: var(--font-display); font-size: 30px; line-height: 1.08; color: var(--accent);
  text-shadow: 0 2px 10px rgba(0,0,0,.85);
}
.rift-plate__title { font-size: var(--fs-base); color: var(--ink-dim); font-style: italic; }
/* Stacked, not a name-left/title-right row. Tier names run from "Ignis" to "The Gravewarden", and
   at the long end the name wrapped to two lines with its own subtitle floating beside the break. */
.rift-bench__head { display: flex; flex-direction: column; gap: 2px; padding-right: min(28%, 180px); }
.rift-bench__flavor {
  max-width: 62ch;
  margin: var(--sp-4) 0 var(--sp-2);
  padding-left: var(--sp-3);
  border-left: 2px solid rgba(var(--boss-tint), .45);
  font-size: var(--fs-sm); font-style: italic; color: var(--ink-dim); line-height: 1.65;
}
.rift-bench__status { margin: var(--sp-1) 0 0; font-size: var(--fs-sm); line-height: var(--lh-base); color: var(--ink); }
.rift-bench__status.is-gated { color: var(--warning); }
.rift-bench__status.is-locked { color: var(--ink-faint); }
.rift-bench__status.is-cleared { color: var(--vitality); }

/* ---- THE PORTAL CARD ------------------------------------------------------------------------
   The rift you are standing at, pinned to it.

   Twelve tears are painted into the Wilds and you walk to the one you want. A side panel listing
   all twelve asked that question a second time in a form where the place you had walked to did not
   matter — so the walk, and the scene it crossed, stopped counting for anything.

   Deliberately NOT a modal: no backdrop, nothing dims, the world keeps moving behind it and you can
   still walk. Walking away is what closes it, because it belongs to a place. Fixed rather than
   absolute so it can be placed against the viewport and clamped to it; world.js re-places it every
   frame, since the camera follows the hero and a card placed once would slide off its own portal.

   Above the edge docks, below the rails: a rail is the thing you deliberately opened on top of
   everything, and this is part of the world underneath it. */
.portal-card {
  position: fixed; z-index: 25;
  width: 264px; display: flex; flex-direction: column; gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-3) var(--sp-3);
  color: var(--ink);
  background:
    radial-gradient(ellipse 120% 60% at 50% 0%, rgba(139,92,214,.22), transparent 70%),
    linear-gradient(180deg, rgba(12,20,32,.97), rgba(5,9,15,.97));
  border: 1px solid var(--iron-light);
  border-radius: var(--r-sharp);
  box-shadow: var(--bevel-up), 0 14px 34px rgba(0,0,0,.6), 0 0 24px rgba(139,92,214,.22);
  animation: portal-card-in .16s ease-out;
}
.portal-card[hidden] { display: none; }
/* The stem points at the tear the card is about — down when the card hangs above it, up when a
   portal near the top of the window makes it flip below. */
.portal-card::after {
  content: ''; position: absolute; left: 50%; margin-left: -7px;
  border: 7px solid transparent;
  top: 100%; border-top-color: var(--iron-light);
}
.portal-card.is-below::after { top: auto; bottom: 100%; border-top-color: transparent; border-bottom-color: var(--iron-light); }
@keyframes portal-card-in { from { opacity: 0; transform: translateY(6px); } }

.portal-card__close {
  position: absolute; top: 4px; right: 4px;
  width: 20px; height: 20px; display: grid; place-items: center;
  padding: 0; font-size: 12px; line-height: 1; cursor: var(--cursor-interact);
  color: var(--ink-faint); background: transparent; border: 0;
}
.portal-card__close:hover { color: var(--ink); }

.portal-card__tier {
  font-family: var(--font-display); font-size: var(--fs-xs); font-weight: 700;
  letter-spacing: var(--tracking-wide); color: #b48ae4;
}
.portal-card__name { font-family: var(--font-display); font-size: var(--fs-lg); line-height: var(--lh-tight); }
.portal-card__title { font-size: var(--fs-xs); color: var(--ink-dim); }
.portal-card__flavor { margin: 0; font-size: var(--fs-xs); font-style: italic; color: var(--ink-dim); line-height: var(--lh-base); }
.portal-card__flavor:empty { display: none; }
.portal-card__status { margin: 0; font-size: var(--fs-xs); color: var(--ink); }
.portal-card__status.is-gated { color: var(--warning); }
.portal-card__status.is-locked { color: var(--ink-faint); }
.portal-card__status.is-cleared { color: var(--vitality); }
.portal-card__enter { width: 100%; }
.t1-hardmode-enter { width:100%; }
.t1-hardmode-enter[hidden] { display:none; }
.t1-hardmode-controls { display:flex; align-items:center; gap:7px; flex-wrap:wrap; margin-top:8px; font-size:12px; }
.t1-hardmode-controls[hidden] { display:none; }
.t1-hardmode-controls select { background:#080d14; color:var(--ink); border:1px solid var(--iron-light); padding:5px; }
.t1-hardmode-status { color:#e9d5ff; font-weight:700; min-width:170px; }

@media (max-width:620px) {
  .portal-card { width: min(86vw, 264px); }
}

/* The seal is a painted piece of machinery; the stone remains a live, movable object. */
.rift-keystone { container-type: inline-size; display: flex; flex-direction: column; gap: 12px; padding: 20px 0 4px; border-top: 1px solid #514332; }
.rift-keystone[hidden] { display: none; }
.keystone-heading { text-align: center; display: grid; gap: 5px; }
.keystone-heading > span { color: var(--torch); font: 700 10px var(--font-body); letter-spacing: .22em; text-transform: uppercase; }
.keystone-heading > b { color: #e3cfa5; font: 700 19px var(--font-display); letter-spacing: .06em; }
.keystone-door { display: flex; flex-direction: column; align-items: center; gap: 0; }
.keystone-altar { position: relative; flex: none; display: grid; place-items: center; width: min(100%, 290px); aspect-ratio: 1; isolation: isolate; }
.keystone-altar__ring { position: absolute; inset: 0; pointer-events: none; background: url('/assets/ui/nether-socket-v1.png') center/cover; mask-image: radial-gradient(ellipse, #000 56%, transparent 73%); filter: brightness(.82); transition: filter .25s; }
.keystone-altar__runes {
  position: absolute; left: 50%; top: 46%; translate: -50% -50%;
  width: 64%; aspect-ratio: 1; border-radius: 50%;
  pointer-events: none; opacity: 0;
  box-shadow: 0 0 25px #b68aff66, inset 0 0 18px #c59cff44;
  border: 1px solid #cfaeff77;
  transition: opacity .25s, transform .25s;
}
.keystone-altar.is-receptive .keystone-altar__ring { filter: brightness(1.12); }
.keystone-altar.is-drop-target .keystone-altar__runes, .keystone-altar.is-awakened .keystone-altar__runes { opacity: .7; }
.keystone-altar.is-drop-target .keystone-altar__runes { opacity: 1; transform: scale(1.04); }
.keystone-altar.is-drop-target { box-shadow: none; }
.keystone-altar.is-drop-target .keystone-socket__empty { color: #d2adeb; text-shadow: 0 0 16px #bd8aff; }
.keystone-socket { position: absolute; left: 50%; top: 46%; translate: -50% -50%; display: grid; place-items: center; width: 32%; aspect-ratio: 1; padding: 0; background: transparent; border: 0; color: #b08bc5; cursor: var(--cursor-interact); transition: background .2s, box-shadow .2s; }
.keystone-socket:disabled { cursor: var(--cursor-unavailable); }
.keystone-socket__empty { font-size: 30px; color: #54425d; text-shadow: 0 2px 3px #000; }
.keystone-socket:focus-visible { outline: 2px solid var(--rune-bright); outline-offset: 6px; }
.keystone-gem { position: relative; display: grid; place-items: center; width: 64px; height: 72px; flex: none; filter: drop-shadow(0 5px 3px #000b); transition: transform .18s, filter .18s; }
.keystone-gem::before { content: ''; position: absolute; inset: -5%; background: url('/assets/ui/nether-keystone-v1.png') center/contain no-repeat; }

.keystone-gem > b { z-index: 1; color: #fff0d3; font: 700 22px var(--font-display); text-shadow: 0 2px 3px #09040e, 0 0 8px #1b0b2c; }
.keystone-socket .keystone-gem { width: 100%; height: 100%; }
.keystone-socket:hover:not(:disabled) .keystone-gem { transform: translateY(-3px); filter: drop-shadow(0 8px 5px #000) brightness(1.15); }
.keystone-door__say { text-align: center; display: grid; gap: 5px; margin-top: -7px; z-index: 1; }
.keystone-state { color: #ac92c7; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .16em; }
.keystone-title { font: 700 17px var(--font-display); color: #e5d6b4; }
.keystone-instruction { margin: 0; color: var(--ink-dim); font: 12px/1.6 var(--font-body); }
.keystone-stats { display: flex; justify-content: center; gap: 24px; margin-top: 8px; }
.keystone-stats > span { display: grid; gap: 3px; font: 10px var(--font-body); text-transform: uppercase; letter-spacing: .08em; color: var(--ink-dim); }
.keystone-stats b { font: 700 18px var(--font-numeric); color: #e7ce9b; }
.keystone-difficulty { font-size: 12px; color: #baa3d2; }
.rift-keystone__affix { display: flex; justify-content: center; gap: 10px; padding: 10px; border-top: 1px solid #3c3348; border-bottom: 1px solid #3c3348; background: linear-gradient(90deg, transparent, #9c73c00c, transparent); font-size: 12px; }
.rift-keystone__affix > span { color: var(--ink-dim); }
.rift-keystone__affix > b { color: #c7a2e9; }
.keystone-pack-label { display: flex; justify-content: space-between; color: #bfa57b; font: 700 11px var(--font-body); text-transform: uppercase; letter-spacing: .08em; }
.keystone-pack-label > span { color: var(--ink-faint); font-weight: 400; text-transform: none; letter-spacing: 0; }
.keystone-tray { display: grid; grid-template-columns: repeat(auto-fit, minmax(155px, 1fr)); gap: 8px; }
.keystone-stone { display: flex; gap: 10px; align-items: center; min-width: 0; padding: 10px; text-align: left; cursor: var(--cursor-pick-up); touch-action: none; appearance: none; border: 1px solid #443b34; border-radius: 2px; color: var(--ink); background: linear-gradient(145deg, #24212c, #0b101a 65%); box-shadow: inset 0 1px #9f8c6133, inset 0 -2px #0009, 0 2px 3px #0008; transition: border-color .15s, background .15s; }
.keystone-stone:not(.is-here) { cursor: var(--cursor-navigate); }
.keystone-stone .keystone-gem { width: 42px; height: 48px; }
.keystone-stone .keystone-gem > b { font-size: 16px; }
.keystone-stone__copy { display: grid; gap: 3px; min-width: 0; }
.keystone-stone__copy > b { color: #ddc49c; font: 700 12px var(--font-display); }
.keystone-stone__copy > span { color: var(--ink-dim); font: 11px var(--font-body); }
.keystone-stone__copy > small { color: #ae91c8; font: 10px var(--font-body); }
.keystone-stone:hover:not(:disabled) { border-color: #a18a64; background: linear-gradient(145deg, #332b3d, #10131d); }
.keystone-stone:hover:not(:disabled) .keystone-gem { transform: translateY(-3px) rotate(-3deg); }
.keystone-stone:focus-visible { outline: 2px solid var(--rune-bright); outline-offset: 2px; }
.keystone-stone.is-here { border-color: #9873ba; cursor: var(--cursor-interact); }
.keystone-stone.is-carried { opacity: .3; }
.keystone-stone:disabled { cursor: wait; opacity: .5; }
.keystone-feedback { margin: 0; font: 12px/1.5 var(--font-body); color: #d3bddf; text-align: center; }
.keystone-feedback:not(.sr-only) { min-height: 18px; }
.keystone-ghost { position: fixed; z-index: 400; pointer-events: none; transform: translate(-50%, -55%) rotate(-8deg); filter: drop-shadow(0 14px 10px #000a); }
.keystone-altar.is-binding .keystone-altar__runes { opacity: 1; }
.keystone-altar.is-seating .keystone-gem { animation: keystone-seat .5s cubic-bezier(.16,.7,.2,1) both; }
.keystone-altar.is-seating .keystone-altar__runes { animation: keystone-seal .7s ease-out both; }
@keyframes keystone-seat { 0% { transform: translateY(-22px) scale(1.25); filter: brightness(1.8) drop-shadow(0 12px 8px #000); } 65% { transform: translateY(2px) scale(.97); } 100% { transform: none; } }
@keyframes keystone-seal { 0% { opacity: 0; transform: scale(.8); } 40% { opacity: 1; box-shadow: 0 0 50px #d7b6ffa0, inset 0 0 26px #c99bff; } 100% { opacity: .7; transform: scale(1); } }
@container (min-width: 420px) {
  .keystone-door { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: 16px; }
  .keystone-altar { width: 100%; max-width: 290px; }
  .keystone-door__say { text-align: left; margin-top: 0; }
  .keystone-stats { justify-content: flex-start; }
}
@media (max-width: 1100px) {
  .rift-spread {
    grid-template-columns: minmax(0, 1fr) minmax(190px, 230px);
    grid-template-rows: minmax(150px, 30%) minmax(0, 1fr);
  }
  .rift-spread > .ui-index { grid-column: 1 / -1; }
  .rift-spread > .rift-detail {
    grid-column: 1; grid-row: 2;
    padding-right: var(--sp-4);
    border-right: 1px solid var(--index-rule);
  }
  .rift-detail__footer { left: 0; }
  .rift-inventory { grid-column: 2; grid-row: 2; padding-top: var(--sp-3); }
}
@media (max-width: 700px) {
  .rift-spread { display: flex; flex-direction: column; overflow-y: auto; }
  .rift-spread > .ui-index { flex: 0 0 180px; }
  .rift-spread > .rift-detail { flex: none; min-height: 520px; padding-right: 0; border-right: 0; }
  .rift-detail__footer { right: 0; }
  .rift-inventory { flex: none; padding: var(--sp-4) 0 0; overflow: visible; }
  .rift-inventory .keystone-tray { flex-direction: row; overflow-x: auto; padding-bottom: var(--sp-2); }
  .rift-inventory .keystone-stone { flex: 0 0 170px; }
}
@media (prefers-reduced-motion: reduce) { .keystone-altar *, .keystone-stone, .keystone-gem { animation: none !important; transition: none !important; } }


/* ============================================================================================
   THE NOTICE BOARD
   ============================================================================================ */

.board-list { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 2px; }
.board-row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4);
  padding: var(--sp-2) var(--sp-3);
  background: linear-gradient(90deg, rgba(255,255,255,.03), transparent);
  border-left: 2px solid var(--iron);
  border-radius: 0 var(--r-sharp) var(--r-sharp) 0;
}
.board-row:nth-child(odd) { background: linear-gradient(90deg, rgba(255,255,255,.055), transparent); }
/* The top three carry the gold; below that a rank is just a number. */
.board-row:nth-child(1) { border-left-color: var(--gold-lit); }
.board-row:nth-child(2) { border-left-color: var(--ink-dim); }
.board-row:nth-child(3) { border-left-color: var(--torch); }
.board-row__who { display: flex; flex-direction: column; min-width: 0; }
.board-row__name { font-size: var(--fs-sm); color: var(--ink); }
.board-row__sub { font-size: var(--fs-xs); color: var(--ink-faint); }
.board-rank {
  display: inline-block; min-width: 34px; margin-right: var(--sp-2);
  font-family: var(--font-numeric); font-variant-numeric: tabular-nums; font-weight: 800;
  font-size: var(--fs-xs); color: var(--gold-lit);
}
.board-row__stat { font-family: var(--font-numeric); font-variant-numeric: tabular-nums; font-size: var(--fs-sm); color: var(--ink-dim); white-space: nowrap; }
.board-row.is-live { border-left-color: var(--vitality); }
.board-row.is-live .board-rank { color: var(--vitality); }


/* ============================================================================================
   TOOLTIP — for everything that is not gear
   ============================================================================================

   Two tooltips, two jobs. .ui-tip-pair above draws gear: two cards and the differences between
   them. This one draws a sentence about whatever you are pointing at, and it exists because all of
   those were using the browser's `title` attribute — which appears after a second of stillness, in
   the operating system's font on its own beige slab, cannot be styled, truncates, never appears on
   a touch screen and never appears for a keyboard user at all. A talent whose entire explanation
   lived in one was a talent most players never read.
   ============================================================================================ */

.ui-tip {
  position: fixed; z-index: 320; pointer-events: none;
  display: flex; flex-direction: column; gap: 2px;
  max-width: 280px;
  padding: var(--sp-2) var(--sp-3);
  background: linear-gradient(180deg, #0c1420, #05090f);
  border: 1px solid var(--iron-light);
  border-radius: var(--r-sharp);
  box-shadow: inset 0 0 0 1px rgba(var(--gold-rgb), .16), var(--cast-deep);
  font-family: var(--font-body); font-size: var(--fs-xs); line-height: var(--lh-base);
  color: var(--ink-dim);
}
.ui-tip[hidden] { display: none; }
.ui-tip__title {
  font-family: var(--font-display); font-size: var(--fs-sm); font-weight: 700;
  letter-spacing: var(--tracking-display); color: var(--gold-lit);
}
/* The figures, lit so a card can be scanned rather than read.
   The body is --ink-dim on purpose, so a value only has to step up to full ink to separate itself.
   It takes no colour: every colour in this language already means something, and "this is the
   number" is not one of those meanings. Tabular figures so two tooltips compared one after the
   other line up. See markValue() in js/ui/AbilityText.js for why this is fenced text rather than
   markup. */
.ui-tip__value {
  font-family: var(--font-numeric); font-variant-numeric: tabular-nums;
  font-weight: 700; color: var(--ink);
}
.ui-tip__line:empty { height: var(--sp-1); }
.ui-tip__tradeoff { color: #ed7777; font-weight: 700; }
/* The line that says why you cannot have it yet. Last line by convention — see skillNode(). */
.ui-tip__line:last-child:not(:only-child) { color: var(--ink-faint); }


/* ============================================================================================
   THE HERO SHEET — what wearing all of it adds up to
   ============================================================================================

   Six stat chips and a tier-set box, behind a toggle you had to press every time. It opens with the
   hero window now, because it is the other half of the same question: the right rail is what you
   are wearing, this is what it comes to. And it carries what it always should have — who you are,
   how far you have got, and which spells are actually on your keys.
   ============================================================================================ */

.sheet-label { display: block; margin: var(--sp-4) 0 var(--sp-2); }
.sheet-label:first-child { margin-top: 0; }

.hero-identity { display: flex; flex-direction: column; gap: var(--sp-2); }
.hero-identity__name { font-family: var(--font-display); font-size: var(--fs-lg); color: var(--gold-lit); }
.hero-identity__sub { font-size: var(--fs-xs); color: var(--ink-dim); }

/* The experience bar. A number on its own ("1420 / 2600") is arithmetic; the bar is the answer. */
.hero-xp { display: flex; flex-direction: column; gap: 3px; }
.hero-xp__track {
  height: 10px; overflow: hidden;
  background: var(--stone-abyss);
  border: 1px solid rgba(0,0,0,.8);
  border-radius: var(--r-sharp);
  box-shadow: var(--bevel-down);
}
.hero-xp__track i { display: block; height: 100%; background: linear-gradient(90deg, var(--rune), var(--rune-bright)); transition: width .35s ease; }
.hero-xp__label { display: flex; justify-content: space-between; font-family: var(--font-numeric); font-variant-numeric: tabular-nums; font-size: var(--fs-xs); color: var(--ink-faint); }

/* The counts that are not combat stats: purse, materials, ascensions, unspent points. Unspent
   points get the gold, because they are the only one of these you can act on right now. */
.hero-tally { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-1) var(--sp-3); }
.hero-tally div { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-2); font-size: var(--fs-xs); color: var(--ink-dim); }
.hero-tally b { font-family: var(--font-numeric); font-variant-numeric: tabular-nums; color: var(--ink); }
.hero-tally .is-actionable b { color: var(--gold-lit); }

/* Which spell is on which key, read off the same loadout the rift binds. It used to be knowable
   only by opening the Shrine and counting down the list. */
.ability-sheet { display: flex; flex-direction: column; gap: var(--sp-1); }
.ability-sheet__row {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-2);
  background: linear-gradient(90deg, rgba(255,255,255,.04), transparent);
  border-left: 2px solid var(--iron);
  border-radius: 0 var(--r-sharp) var(--r-sharp) 0;
  font-size: var(--fs-xs);
}
.ability-sheet__key {
  flex: none; min-width: 20px; height: 18px; padding: 0 4px;
  display: grid; place-items: center;
  font-family: var(--font-numeric); font-size: 11px; font-weight: 800;
  color: var(--ink-on-light); background: var(--gold-lit); border-radius: var(--r-sharp);
}
/* Always-bound spells carry a word rather than a number, and iron rather than gold: there is no
   key to press and nothing to choose. */
.ability-sheet__row.is-fixed { border-left-color: var(--iron-light); }
.ability-sheet__row.is-fixed .ability-sheet__key {
  background: transparent; color: var(--ink-faint); border: 1px solid var(--iron-light);
  font-family: var(--font-body); font-size: 9px; letter-spacing: var(--tracking-wide); text-transform: uppercase;
}
.ability-sheet__name { flex: 1; min-width: 0; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ability-sheet__rank { flex: none; font-size: 10px; font-weight: 800; letter-spacing: .06em; color: var(--rune-bright); }
.ability-sheet__row.is-empty { opacity: .5; }
.ability-sheet__row.is-empty .ability-sheet__name { color: var(--ink-faint); font-style: italic; }

/* ============================================================================================
   CONTEXT MENU
   ============================================================================================

   The right button's menu. Fixed and on <body> — see ui/ContextMenu.js — so it is never clipped
   by the rail or the canvas it was opened over. Above the rails and the Select dropdown, below the
   dragged item's ghost: the ghost follows the cursor and must stay the topmost thing on screen. */
.ctx-menu {
  position: fixed; z-index: 200;
  min-width: 150px; padding: var(--sp-1);
  display: flex; flex-direction: column;
  background: linear-gradient(180deg, #0c1420, #05090f);
  border: 1px solid var(--iron-light);
  border-radius: var(--r-sharp);
  box-shadow: inset 0 0 0 1px rgba(var(--gold-rgb), .16), var(--cast-deep);
}
.ctx-menu__item {
  appearance: none; text-align: left;
  padding: var(--sp-2) var(--sp-3);
  background: transparent; border: 0; border-radius: var(--r-sharp);
  color: var(--ink); font-family: var(--font-body); font-size: var(--fs-sm);
  cursor: var(--cursor-interact); transition: background var(--t-fast), color var(--t-fast);
}
.ctx-menu__item:hover:not(:disabled),
.ctx-menu__item:focus-visible { background: var(--stone-raised); color: var(--gold-lit); outline: none; }
.ctx-menu__item:disabled { color: var(--ink-faint); cursor: var(--cursor-unavailable); }
.ctx-menu__item.is-danger:hover:not(:disabled) { color: var(--ember); }
.ctx-menu__rule { height: 1px; margin: var(--sp-1) var(--sp-2); background: var(--iron); }



/* ============================================================================================
   FLOAT — a readout laid over the play area
   ============================================================================================

   Not .ui-panel. A panel is a thing you stop and read: two millimetres of iron, a gold inset ring,
   corner brackets and a carved title between two rules. That weight is right for a rail, a dialog
   and the hero window, and wrong for anything that has to sit ON the fight — over a painted plate
   the frame competes with the art, and the title is louder than the numbers underneath it.

   A float is the same stone one layer thinner: a near-solid slab, one hairline of iron, a soft cast
   shadow, and no title at all — what a float holds says what it is, and a word above it is a word
   in the way. The log dock and the damage meter are both this.
   ============================================================================================ */

.ui-float {
  position: relative;
  padding: var(--sp-2);
  background: linear-gradient(180deg, rgba(10,17,28,.93), rgba(4,8,14,.96));
  border: 1px solid var(--iron);
  border-radius: var(--r-sharp);
  box-shadow: var(--cast);
  color: var(--ink);
}

/* Goes out of the scene entirely once nothing has happened in it for a while, and comes back for
   the next arrival or when you reach for it. Only for a float fed by DISCRETE arrivals — the log.
   A readout that changes continuously, like the meter, has no "between", so it would either never
   recede or flicker trying to.

   `.is-awake` is written by js/ui/Log.js, which owns the quiet clock. Hover is not among the wake
   signals because the log is click-through and can never be hovered; reaching for it means putting
   the caret in the chat box.

   Out slowly and back fast. A panel that vanishes the moment it goes quiet reads as a fault, and
   one that crawls back reads as lag on the line you are trying to read. */
.ui-float--recede { opacity: 0; transition: opacity 900ms ease; }
.ui-float--recede.is-awake,
.ui-float--recede:focus-within { opacity: 1; transition-duration: 120ms; }

/* ============================================================================================
   THE LOG — the one scrolling stream
   ============================================================================================

   Chat in town, combat in the rift. One dock, one treatment: they never appear together, so they
   were never two things.

   A log is read at a GLANCE, by someone being hit. So it is built to be scanned down two columns
   rather than read as prose: a glyph gutter gives each family a silhouette you learn without
   reading it, and the amounts are tabular and right-aligned, so a run of numbers stacks into a
   shape whose size can be judged without parsing a single one.

   What a line may carry is LogEntryDto (Game/RiftDtos.cs): a kind, the sentence, whose line it is,
   an amount, and whether it crit or was guarded. Crit and guard are MARKS here, never words inside
   the sentence — a log that writes "CRIT!" into the text cannot align it, colour it, or let a
   player find it.

   THE ONE DOCK THAT TAKES CLICKS. Everything else laid over the play area is a frame and not a
   target (see the pointer-events block in theme.css) — this is the exception, because it holds
   history you scroll and an edge you drag. The cost is real and is the price of both: a click in
   the bottom-left corner of the arena lands on the panel instead of moving your hero.
   ============================================================================================ */

/* The log's own frame. Fixed height, because a panel that grows as the fight does moves the thing
   you were reading, and resizable from its top-right corner, because it is anchored bottom-left and
   that is the corner free to move. --log-w/--log-h are written by initLogPanels in js/ui/Log.js and
   remembered per browser. The slab, the border and the receding are .ui-float, above. */
/* The corner it sits in is a corner of the WORLD, and a click there moves your hero. So the dock
   passes clicks through, and the two things you can actually operate take the pointer back for
   themselves: the grip and the scrollbar thumb. The wheel is bridged by hand in js/ui/Log.js —
   an element that takes no pointer events receives no wheel event either.

   And it is barely a slab. Chat is read over the scene rather than through a window laid on top of
   it, so the frame is a wash and a hairline, and the lines earn their legibility from their own
   shadow instead (.ui-log__line). */
.ui-log-dock {
  width: var(--log-w, 400px);
  pointer-events: none;
  background: linear-gradient(180deg, rgba(10,17,28,.22), rgba(4,8,14,.34));
  border-color: rgba(232,238,247,.07);
  box-shadow: none;
}
.ui-log-dock .ui-log { height: var(--log-h, 220px); padding-right: 9px; }

/* The grip. Top-right, 14px, and invisible until the pointer is in the panel — a permanent handle
   on a readout is a control asking to be used, and this one is used once. */
.ui-log-grip {
  position: absolute; top: 3px; right: 3px; z-index: 2;
  width: 13px; height: 13px;
  /* A <button>, so the UA gives it a slab background and a border that bury anything drawn on it. */
  appearance: none; -webkit-appearance: none;
  padding: 0; background: none; border: 0;
  border-top: 2px solid var(--gold-aged);
  border-right: 2px solid var(--gold-aged);
  cursor: nesw-resize;
  opacity: 0;
  transition: opacity var(--t-fast), border-color var(--t-fast);
}
/* One of the two things on this panel that takes the pointer back from the world. Shown while the
   dock is awake, because a handle on something faded out is a handle on nothing. */
.ui-log-grip { pointer-events: auto; }
.ui-log-dock.is-awake .ui-log-grip { opacity: .5; }
.ui-log-grip:hover,
.ui-log-grip:focus-visible { opacity: .9; border-color: var(--gold-lit); }
.ui-log-grip:focus-visible { outline: 1px solid var(--gold-lit); outline-offset: 1px; }

/* Town stacks the log above the thing you type into; the edge slot is already a column, so nothing
   here has to place it. There is no empty-frame case to hide: a dock with nothing in it has had no
   arrival, so it is faded out anyway, and putting the caret in the chat box is exactly when you
   want the empty frame to appear. */

.ui-log {
  display: flex;
  flex-direction: column;
  gap: 1px;
  overflow-y: auto;
  overscroll-behavior: contain;
  /* Bottom-anchored AND scrollable. `justify-content: flex-end` gives the first of those and
     silently breaks the second: a flex column justified to its end overflows its START, and
     block-start overflow is not part of the scrollable region, so the lines above simply cannot be
     reached. Pushing the first line down with an auto margin bottom-anchors a short log and leaves
     a long one scrolling normally. */
}
.ui-log > :first-child { margin-top: auto; }

/* THE BAR IS DRAWN, NOT THE BROWSER'S.
   Chromium and Edge ship overlay scrollbars: they reserve no gutter, they cannot be turned back
   into a classic bar by styling ::-webkit-scrollbar, and they appear only for the moment you are
   actively scrolling — never on hover. Measured on a bare unstyled div, not assumed. So the native
   one is hidden and js/ui/Log.js positions this from the log's own scroll metrics.
   It is also the only way this reads as part of the panel rather than as part of Windows. */
.ui-log { scrollbar-width: none; }
.ui-log::-webkit-scrollbar { width: 0; height: 0; }

.ui-log-bar {
  position: absolute; top: var(--sp-2); bottom: var(--sp-2); right: 3px;
  width: 5px;
  border-radius: var(--r-round);
  background: rgba(0,0,0,.45);
  opacity: 0;
  transition: opacity var(--t-fast);
  pointer-events: none;
}
/* Only where there is something to scroll, and only while the dock is awake: a full-length thumb
   on a half-empty log is a control that does nothing, drawn at the exact moment it has nothing to
   say. The other half of the pair that takes the pointer back — see .ui-log-dock. */
.ui-log-dock.is-scrollable.is-awake .ui-log-bar,
.ui-log-dock.is-scrollable:focus-within .ui-log-bar { opacity: 1; pointer-events: auto; }

.ui-log-bar__thumb {
  position: absolute; left: 0; right: 0;
  border-radius: var(--r-round);
  background: var(--iron-light);
  cursor: var(--cursor-pick-up);
  transition: background var(--t-fast);
}
.ui-log-bar__thumb:hover { background: var(--stone-highlight); }
.ui-log-bar__thumb.is-dragging { background: var(--gold-aged); cursor: var(--cursor-carry); }

/* Newest at the bottom, the way every chat and every message feed reads. While there is history
   above the top edge, that edge softens instead of cutting, so the line the panel is halfway
   through dissolves rather than being sliced.
   Only while there IS history above it: a fade at a fixed offset would dim the first line of a
   fight for no reason, and a quiet town is always in that state. js/ui/Log.js sets the class from
   the scroll position, so scrolling to the top takes the fade away with it. */
.ui-log.is-clipped {
  mask-image: linear-gradient(180deg, transparent 0, #000 20px);
  -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 20px);
}

.ui-log__line {
  display: grid;
  grid-template-columns: 13px 1fr auto;
  align-items: baseline;
  gap: var(--sp-2);
  margin: 0;
  padding: 1px var(--sp-1) 1px var(--sp-1);
  border-left: 2px solid transparent;
  font-size: var(--fs-xs);
  line-height: var(--lh-tight);
  color: var(--ink-dim);
  /* What makes the line readable, not the panel behind it: the dock is a wash, so a line can find
     itself over torchlit stone or over a lava shelf and has to hold up against both. */
  text-shadow: 0 1px 2px rgba(0,0,0,.95), 0 0 6px rgba(0,0,0,.7);
  /* The gutter is the tone's own mark, so a line states its family in the markup and the language
     decides what that looks like. */
  --log-mark: '\00b7';
  --log-tone: var(--ink-dim);
}

.ui-log__line::before {
  content: var(--log-mark);
  color: var(--log-tone);
  font-size: 11px;
  line-height: inherit;
  text-align: center;
  opacity: .8;
}

/* The sentence. Whoever the line is about is picked out inside it rather than repeated in a
   column of its own — a name column would be six characters of padding on every line about the
   boss, which is most of them. */
.ui-log__text { min-width: 0; }
.ui-log__text b { color: var(--log-tone); font-weight: 700; }

/* The amount, and the only reason this is a grid. Right-aligned tabular numerals turn the column
   into a bar chart you can read without reading. */
.ui-log__amount {
  font-family: var(--font-numeric);
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  color: var(--log-tone);
  justify-self: end;
}

/* ---- Tone. Six families, each one a signal token already in the language. ------------------- */

/* You hit something. Deliberately the quietest: it is the line that arrives most often, and a log
   where the routine case shouts has no room left to raise its voice. */
.ui-log__line--strike { --log-mark: '\203A'; --log-tone: var(--ink); }
/* Something hit you. */
.ui-log__line--harm   { --log-mark: '\2739'; --log-tone: #ff9d84; }
/* Healing and revival. */
.ui-log__line--mend   { --log-mark: '\271A'; --log-tone: #6fd38f; }
/* A buff landed, a cast was interrupted, a blow was dodged — the fight going your way. */
.ui-log__line--ward   { --log-mark: '\25C8'; --log-tone: var(--rune-bright); }
/* Something is coming. The only tone that is about the future, so it is the only one that moves. */
.ui-log__line--omen   { --log-mark: '\25B2'; --log-tone: var(--warning); }
/* A class combo detonating: the loudest line the log ever prints. */
.ui-log__line--surge  { --log-mark: '\2726'; --log-tone: var(--gold-lit); }
/* Someone said something. */
.ui-log__line--say    { --log-mark: '\275D'; --log-tone: var(--ink); }

.ui-log__line--omen .ui-log__text { color: var(--warning); }
.ui-log__line--surge .ui-log__text { color: #f0dda6; }

/* ---- Marks. What the DTO knows, shown rather than spelled. ---------------------------------- */

/* A crit is the number, brighter and struck with a spark — not the word CRIT after the sentence,
   which reads last and scans as more prose. */
.ui-log__line.is-crit .ui-log__amount { color: var(--gold-lit); text-shadow: 0 0 10px rgba(var(--gold-rgb), .5); }
.ui-log__line.is-crit .ui-log__amount::after { content: ' \2726'; font-size: 9px; vertical-align: .1em; }
/* Guarded: the blow landed but you ate it properly, so the number is stated and set back. */
.ui-log__line.is-guarded .ui-log__amount { color: var(--ink-faint); }
.ui-log__line.is-guarded .ui-log__amount::after { content: ' \25E7'; color: var(--rune-bright); font-size: 10px; }

/* Your own line, in a stream of six people's. A rule in the gutter rather than a colour, because
   every colour here already means something else. Every line reserves the border so marking one
   cannot shift the gutter it sits in. */
.ui-log__line.is-mine { border-left-color: var(--gold-lit); background: linear-gradient(90deg, rgba(var(--gold-rgb), .09), transparent 42%); }

/* ---- The herald. A phase turning, a boss falling, a party wiping. --------------------------- */

/* Not a row: these are the four or five lines in a fight that are about the FIGHT rather than
   about a hit, and they earn the full width and the display face to say so. */
.ui-log__line--herald {
  display: block;
  margin: var(--sp-1) 0;
  padding: 2px 0;
  text-align: center;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--gold-lit);
  border-top: 1px solid rgba(var(--gold-rgb), .18);
  border-bottom: 1px solid rgba(var(--gold-rgb), .18);
}
.ui-log__line--herald::before { content: none; }
.ui-log__line--herald.is-grim { color: #ff9d84; border-color: rgba(215,67,21,.28); }

/* ---- Arrival. -------------------------------------------------------------------------------
   A line that just landed is lit for a moment and settles. The log is in peripheral vision most of
   the time, and motion at the bottom edge is how a player learns something arrived without
   watching for it. */
/* No transform. A translated line contributes its TRANSFORMED box to the scrollable overflow
   region, so a log measuring whether its top line is cut would be measuring this animation for as
   long as it runs — and every arriving line would report the log as clipped. Brightness and opacity
   say "this just landed" without moving anything. */
@keyframes uiLogArrive {
  from { opacity: 0; filter: brightness(1.7); }
  to   { opacity: 1; filter: none; }
}
.ui-log__line.is-new { animation: uiLogArrive .34s ease-out; }
@media (prefers-reduced-motion: reduce) {
  .ui-log__line.is-new { animation: none; }
}


/* What you type into, directly under what everyone said. It kept the pre-language palette longest:
   a 10px-radius violet-bordered field with a 10px backdrop blur, floating on its own over the
   corner of the world. It is the same well as every other sunken thing here. */
.ui-log-say { width: var(--log-w, 400px); max-width: 100%; }
.ui-log-say input {
  width: 100%;
  padding: var(--sp-2) var(--sp-3);
  background: rgba(0,0,0,.45);
  border: 1px solid var(--iron);
  border-radius: var(--r-sharp);
  box-shadow: var(--bevel-down);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--fs-xs);
  transition: border-color var(--t-fast), box-shadow var(--t-fast);
}
.ui-log-say input::placeholder { color: var(--ink-faint); }
.ui-log-say input:focus {
  outline: none;
  border-color: var(--gold-aged);
  box-shadow: var(--bevel-down), var(--glow-torch);
}
/* The field is the one thing on this dock that is always a target, so it says so even while the
   panel around it is sitting back. */
.ui-log-say input:focus,
.ui-log-say:hover input { background: rgba(0,0,0,.62); }

.rift-requirements { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.rift-requirements[hidden], .rift-bench__status[hidden] { display: none; }
.admin-panel {
  position: absolute; z-index: 110; top: 54px; right: 16px;
  width: min(390px, calc(100vw - 32px)); max-height: min(76vh, 760px);
  display: flex; flex-direction: column;
  overflow: hidden; pointer-events: auto;
}
.admin-panel[hidden], #adminBtn[hidden] { display: none !important; }
.admin-panel__head {
  flex: none;
  display: flex; align-items: center; justify-content: space-between;
  padding: 16px; border-bottom: 1px solid rgba(255,255,255,.17);
}
#adminPanelBody { min-height: 0; overflow-y: auto; padding: 0 16px 16px; }
.admin-panel__section { border-top: 1px solid rgba(255,255,255,.17); padding: 12px 0; }
.admin-panel__section h3 { margin: 0 0 8px; font-size: 15px; }
.admin-panel__row { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 6px 0; }
.admin-panel__row input, .admin-panel__row select { min-width: 0; flex: 1; }
.admin-panel__row label { flex: 0 0 96px; }
.admin-panel__actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.admin-panel__flag { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 3px 0; }
.admin-panel__flag button { min-width: 52px; }
.admin-roll-results { display: grid; gap: 5px; margin-top: 8px; }
.admin-roll-item { width: 100%; border: 1px solid rgba(255,255,255,.2); border-radius: 4px;
  background: rgba(255,255,255,.06); color: inherit; padding: 6px 8px; text-align: left; cursor: help; }
.admin-roll-item:hover, .admin-roll-item:focus-visible { border-color: #bca776; }

/* Expansion 3 shares the existing rail, typography and panel frame. */
.ui-rail--wide[data-wide="conduit"] { width: min(650px, calc(100vw - 36px)); }
.astral-entry-body { overflow: auto; min-height: 0; padding: 16px; }
.astral-entry-list { display: grid; gap: 14px; }
.astral-entry-card { padding: 14px; border: 1px solid rgba(194,164,113,.35); background: rgba(8,14,23,.55); }
.astral-entry-card h3 { margin: 0 0 10px; font-size: 17px; }
.astral-entry-details { min-width: 0; overflow-wrap: anywhere; }
.astral-entry-facts { display: flex; flex-wrap: wrap; gap: 7px; }
.astral-entry-facts .ui-tag { white-space: normal; }
.astral-lock-reasons, .astral-party-notes, .astral-party-members { margin: 10px 0; padding-left: 20px; line-height: 1.5; }
.astral-lock-reasons, .astral-lock-reason { color: #f0aa90; }
.astral-lock-reason { display: block; }
.astral-entry-ready { color: #b6d8b1; }
.forge2-result { overflow-wrap: anywhere; min-height: 1.5em; }
#panelForge .forge-op__head { flex-wrap: wrap; gap: 6px; }
#panelForge .forge-op__cost, #panelForge .forge-bench__name { min-width: 0; overflow-wrap: anywhere; }
#panelForge .forge-op__desc, #panelAlchemist .alchemist-quote { overflow-wrap: anywhere; }
#modalCrucible .modal { max-height: calc(100dvh - 42px); }
.set-summary-head b { min-width: 0; overflow-wrap: anywhere; }
#panelForge .forge2-result { flex: none; margin: 0; }
#panelForge .forge2-result:empty { display: none; }
#panelForge [data-pane="enhance"] .bag-well { flex: none; max-height: 120px; overflow-y: auto; }
#panelRifts.has-astral-entry .rift-spread { grid-template-columns: minmax(210px, 240px) minmax(370px, 1fr); }
#panelRifts.has-astral-entry .rift-bench { padding-bottom: var(--sp-3); -webkit-mask-image: none; mask-image: none; }
#panelRifts.has-astral-entry .rift-detail__footer { position: relative; left: auto; right: auto; bottom: auto; flex: none; }
#panelRifts.has-astral-entry .rift-detail__footer { display: flex; flex-wrap: wrap; align-items: end; gap: var(--sp-3); }
#riftBossDifficultyLabel { display: flex; flex-direction: column; gap: var(--sp-1); font-size: var(--fs-xs); color: var(--ink-dim); }
#riftBossDifficulty { width: 140px; }
#panelRifts.has-astral-entry .rift-detail { display: grid; grid-template-rows: minmax(0, 1fr) auto; }
#panelRifts.has-astral-entry .rift-bench { display: block; overflow-y: auto; scroll-padding-block: var(--sp-3); }
#panelRifts.has-astral-entry .rift-bench > * { margin-bottom: var(--sp-3); }
#panelRifts.has-astral-entry .rift-detail__footer { grid-row: 2; background: var(--panel-bg, #0d1320); }
