/* ═══════════════════════════════════════════════════════════════════════════
                        THE ILLUMINATED MANUSCRIPT
   ═══════════════════════════════════════════════════════════════════════════

   THE MOTIF: gilt struck into dark oiled leather. Everything in this interface
   is one object — a page of a war-chronicle, lit by one candle. There is no
   second visual language. A panel is a struck plate; a border is engraved gilt;
   a button is that plate pressed; a number is ink. When something needs to feel
   different it is the SAME plate under different light, never a new material.

   WHY TOKENS. Every value below was, until this pass, tuned at the point of use.
   That is how a design with one gold ends up with twelve of them — twelve
   near-identical yellows nobody chose, each right in its own rule and wrong
   against its neighbours. Twenty border radii. Eighteen easing curves, all of
   them trying to be the same curve. None of that reads as a bug; it reads as
   SOFTNESS — the HUD landing a half-step less deliberate than the canvas art it
   sits over.

   The rule from here: nothing downstream names a colour, a radius, a curve or a
   duration. It names a ROLE, and the role is defined once, here.
   ═══════════════════════════════════════════════════════════════════════════ */
:root {
  /* ── GILT ─────────────────────────────────────────────────────────────────
     Gold is not a colour in this game, it is a MATERIAL, so this is a specular
     ramp: seated shadow → unlit body → the face → the spark. Read it as "where
     is the candle", not "how yellow". Each step states the one job it may do;
     if a rule needs gold and none of these is the right gold, the rule is
     wrong, not the ramp. */
  --gilt-ink:    #2a1c06;  /* ink laid ON a gold face — hotkey caps, level pips. ONLY over a --gold* fill. */
  --gilt-seat:   #6d5117;  /* the engraved shadow a gilt line sits in. Borders + insets; never a fill under text. */
  --gilt-deep:   #7a5e20;  /* metal fully in shadow. Off-state rims, dividers, the quiet half of a bevel. */
  --gold-dk:     #9c7a2a;  /* gold at rest. Scrollbars, muted rims — gilt that is not being looked at. */
  --gilt-body:   #b8a06a;  /* the unlit body of the metal. Secondary labels, inactive gilt text. */
  --gold:        #d8b24a;  /* THE gold. Anything that IS gold — the treasury, the frame, the seal. */
  --gold-lt:     #f3d889;  /* gilt text on dark. The readable gold; the one most words are set in. */
  --gilt-lit:    #ffd86a;  /* the lit face of struck metal. Gradient tops, hover rims, active edges. */
  --gold-hi:     #ffe9a8;  /* specular highlight: the 1px top edge, lit pips. */
  --gold-bright: #fff3c8;  /* the spark. RULE: at most one element per screen may wear it. */
  --gold-rgb: 216, 178, 74;      /* the gold as channels, for washes: rgba(var(--gold-rgb), a) */
  --gilt-lit-rgb: 255, 216, 106; /* the lit face as a wash — the glow off an active edge */
  --lit-rgb: 255, 230, 170;      /* the candle itself — bevels, inner top edges, warm veils */
  --shade-rgb: 0, 0, 0;          /* the seat. All depth here is one black at varying depth. */

  /* ── INK & LEATHER ──────────────────────────────────────────────────────── */
  --bg: #0a0b08;           /* the leather the whole page is bound in */
  --ink: #ecdfc4;          /* the hand that wrote everything. Body copy, numbers. */
  --muted: #a99b7d;        /* the same hand, faded. Secondary prose, units, footnotes. */

  /* ── SIGNAL ───────────────────────────────────────────────────────────────
     The ONLY two colours in the interface that are not gilt, ink, or a
     kingdom's banner. They mean exactly one thing each and may not be borrowed
     for mood:
       ready — YOU CAN AFFORD THIS / THIS IS DONE. Never decorative.
       peril — this is being taken from you. Never merely "emphasis".
     Both are carried in value and shape as well as hue, because a good share of
     players cannot use the hue (see the grayscale rule at .build.ready). */
  --ready: #7fe09a;
  --ready-lt: #b4f7c6;
  --ready-dk: #3fb463;
  --ready-rgb: 127, 224, 154;
  --peril: #e0786a;
  --peril-rgb: 216, 50, 44;

  /* ── THE PLATE ────────────────────────────────────────────────────────────
     One panel material, used at every scale from a tooltip to the victory
     proclamation. Anything that is a surface is built from exactly these. */
  --panel-1: rgba(36, 30, 22, 0.94);   /* the lit top of the plate */
  --panel-2: rgba(18, 14, 10, 0.96);   /* its shadowed underside */
  --plate: linear-gradient(160deg, var(--panel-1), var(--panel-2));
  --edge: rgba(var(--gold-rgb), 0.45);         /* engraved gilt, at rest */
  --edge-strong: rgba(var(--gold-rgb), 0.72);  /* engraved gilt, under the candle */
  --edge-quiet: rgba(var(--gold-rgb), 0.22);   /* a divider INSIDE a plate, not around one */

  /* ── DEPTH ────────────────────────────────────────────────────────────────
     Four heights and no others. A thing that lifts moves UP the scale on hover;
     it does not grow a differently-shaped shadow. --bevel and --seat are the two
     halves of struck metal and travel together on every plate. */
  --sh-0: 0 1px 2px rgba(var(--shade-rgb), 0.60);       /* a hairline seat under a small mark — pips, caps */
  --sh-1: 0 2px 6px rgba(var(--shade-rgb), 0.40);       /* resting on the page — chips, badges */
  --sh-2: 0 4px 12px rgba(var(--shade-rgb), 0.45);      /* a control — build cards, buttons */
  --sh-3: 0 6px 22px rgba(var(--shade-rgb), 0.55);      /* a panel — the standing HUD furniture */
  --sh-4: 0 12px 34px rgba(var(--shade-rgb), 0.62);     /* over everything — tooltips, modals */
  --bevel: inset 0 1px 0 rgba(var(--lit-rgb), 0.08);    /* the candle catching the plate's top edge */
  --seat: inset 0 0 0 1px rgba(var(--shade-rgb), 0.45); /* the die-cut the plate is seated in */

  /* ── GEOMETRY ─────────────────────────────────────────────────────────────
     Eight radii, down from twenty. Corners here are CUT, not rounded — this is
     a struck plate — so the scale is tight, and its large end is reserved for
     things that are genuinely soft objects (a scroll, a pill, a wheel). */
  --r-xs: 3px;    --r-sm: 5px;    --r-md: 7px;    --r-lg: 9px;
  --r-xl: 12px;   --r-2xl: 16px;  --r-3xl: 20px;  --r-pill: 999px;

  /* ── RHYTHM ─────────────────────────────────────────────────────────────── */
  --sp-1: 3px;  --sp-2: 5px;  --sp-3: 8px;  --sp-4: 12px;  --sp-5: 16px;  --sp-6: 22px;
  --pad-plate: 9px 12px;      /* the inside of any standing panel */
  --gutter: 8px;              /* between two things of the same kind */

  /* ── MOTION ───────────────────────────────────────────────────────────────
     Two curves, because there are only two kinds of movement here: a thing
     SETTLING (hover, colour, opacity) and a thing ARRIVING (entrance, spawn,
     pop). Everything that arrives overshoots. That single rule does more for
     how struck-metal this feels than any amount of gloss: metal has mass, and
     mass does not stop dead. --e-in is the only exit curve; things leave by
     accelerating away. */
  --e-out:     cubic-bezier(.2, .8, .3, 1);      /* settle. The default for every transition. */
  --e-back:    cubic-bezier(.2, .85, .3, 1.35);  /* arrive. Anything that appears wears this. */
  --e-back-lg: cubic-bezier(.2, .85, .3, 1.5);   /* RULE: once per screen — the thing the screen is about. */
  --e-in:      cubic-bezier(.5, 0, .75, 0);      /* leave. */
  --t-1: 90ms;    /* a press answers instantly */
  --t-2: 160ms;   /* a state changes */
  --t-3: 280ms;   /* an element arrives */
  --t-4: 460ms;   /* a screen arrives */

  /* ── THE HAND ON THE FIELD ────────────────────────────────────────────────
     The most-looked-at object in the game was the one thing wearing none of its
     language: an OS crosshair over hand-inked terrain. It stays a CROSSHAIR —
     an RTS lives or dies on the hotspot being exactly where the eye thinks it
     is, and an ornate pointer over a hundred-unit melee is noise, not art. What
     changes is whose crosshair it is: four engraved gilt ticks, open at the
     centre so the thing being aimed at is never hidden by the thing aiming.
     Every stroke is laid twice — an ink halo, then the gilt — so it reads as a
     VALUE against grass, water and stone alike rather than a hue that happens
     to differ from the ground. That is also why it survives grayscale.
     --cur-target is the same reticle under more light, closed by the plate's own
     cut corner: the exact bracket the HUD panels are engraved with, at 8px
     instead of 800. One motif, two scales. Both are 32×32 with the hotspot dead
     centre, and both name the keyword they replace as a fallback. */
--cur-command: url("data:image/svg+xml,%3Csvg xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27 width%3D%2732%27 height%3D%2732%27 viewBox%3D%270 0 32 32%27%3E %3Cg fill%3D%27none%27 stroke-linecap%3D%27round%27%3E %3Cpath d%3D%27M16 4.5v6.5M16 21v6.5M4.5 16h6.5M21 16h6.5%27 stroke%3D%27%230a0b08%27 stroke-opacity%3D%27.82%27 stroke-width%3D%273.4%27%2F%3E %3Cpath d%3D%27M16 4.5v6.5M16 21v6.5M4.5 16h6.5M21 16h6.5%27 stroke%3D%27%23f3d889%27 stroke-width%3D%271.35%27%2F%3E %3C%2Fg%3E %3Ccircle cx%3D%2716%27 cy%3D%2716%27 r%3D%271.9%27 fill%3D%27%230a0b08%27 fill-opacity%3D%27.82%27%2F%3E %3Ccircle cx%3D%2716%27 cy%3D%2716%27 r%3D%27.85%27 fill%3D%27%23ffe9a8%27%2F%3E %3C%2Fsvg%3E") 16 16, crosshair;
  --cur-target: url("data:image/svg+xml,%3Csvg xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27 width%3D%2732%27 height%3D%2732%27 viewBox%3D%270 0 32 32%27%3E %3Cg fill%3D%27none%27 stroke-linecap%3D%27round%27 stroke-linejoin%3D%27round%27%3E %3Cpath d%3D%27M16 5v5M16 22v5M5 16h5M22 16h5M6.6 11.1V8.2a1.6 1.6 0 0 1 1.6 -1.6h2.9M25.4 11.1V8.2a1.6 1.6 0 0 0 -1.6 -1.6h-2.9M6.6 20.9V23.8a1.6 1.6 0 0 0 1.6 1.6h2.9M25.4 20.9V23.8a1.6 1.6 0 0 1 -1.6 1.6h-2.9%27 stroke%3D%27%230a0b08%27 stroke-opacity%3D%27.85%27 stroke-width%3D%273.4%27%2F%3E %3Cpath d%3D%27M16 5v5M16 22v5M5 16h5M22 16h5%27 stroke%3D%27%23ffd86a%27 stroke-width%3D%271.5%27%2F%3E %3Cpath d%3D%27M6.6 11.1V8.2a1.6 1.6 0 0 1 1.6 -1.6h2.9M25.4 11.1V8.2a1.6 1.6 0 0 0 -1.6 -1.6h-2.9M6.6 20.9V23.8a1.6 1.6 0 0 0 1.6 1.6h2.9M25.4 20.9V23.8a1.6 1.6 0 0 1 -1.6 1.6h-2.9%27 stroke%3D%27%23ffe9a8%27 stroke-width%3D%271.35%27%2F%3E %3C%2Fg%3E %3Ccircle cx%3D%2716%27 cy%3D%2716%27 r%3D%272%27 fill%3D%27%230a0b08%27 fill-opacity%3D%27.85%27%2F%3E %3Ccircle cx%3D%2716%27 cy%3D%2716%27 r%3D%271%27 fill%3D%27%23fff3c8%27%2F%3E %3C%2Fsvg%3E") 16 16, cell;
  /* ── THE HAND ─────────────────────────────────────────────────────────────
     Four faces, four jobs, no overlap. Cinzel is CARVED (headings, and figures
     you take in at a glance), Spectral is WRITTEN (prose you actually read),
     MedievalSharp is the chronicler's own voice — flavour only, never a
     control. */
  --font-logo: "Cinzel Decorative", "Cinzel", Georgia, serif;
  --font-display: "Cinzel", Georgia, serif;
  --font-flavor: "MedievalSharp", "Cinzel", Georgia, serif;
  --font-ui: "Spectral", Georgia, serif;
  /* aliases a few rules were already reaching for (they were undefined, so those
     `font:` shorthands silently fell back to the inherited face and size) */
  --font-title: var(--font-display);
  --font-body: var(--font-ui);
  /* EVERY figure in this game is a counter — gold ticking up, population, a
     muster clock. Proportional digits make a counter WOBBLE as it counts, which
     reads as the HUD being unstable rather than the number changing. Declared
     once here instead of remembered at 38 separate call sites. */
  --nums: tabular-nums lining-nums;
  /* THE WAR-RACK'S OWN SCALE, on top of --ui. The rack is the biggest thing on the
     battlefield and it is a shelf you skim rather than something you read, so it does
     not need the size the rest of the HUD is set at — at 1 it covered the bottom third
     of the field and the ground under it is ground you are choosing where to build ON.
     Everything about the rack rides this: the tiles, the type, the gaps, its offset off
     the Build cluster, and the height it reports to the panels that stack above it (see
     `--rackh` in refreshRack), so the one number moves all of it together and nothing
     drifts out of register with anything else. */
  --rack: 0.825;

  /* ── THE ONE SCALE ───────────────────────────────────────────────────────
     Every HUD panel below is drawn in fixed pixels against a ~1600×900 design
     canvas, and --ui is the number that makes those pixels mean the same thing
     on every machine: main.js measures the viewport against the design size on
     every resize and publishes the ratio here (clamped so panels stay readable
     on a phone and never cartoonish on a 4K wall). The top-level HUD panels
     each carry `zoom: var(--ui)`, which scales the panel, its offsets and its
     children in one stroke. Two rules of the road:
       · a viewport unit (vw/vh/dvh) INSIDE a zoomed panel is multiplied by the
         zoom when rendered — any cap meant to fit the real screen must divide
         by var(--ui) to compensate;
       · scripts that place a zoomed panel in screen px must divide their
         coordinates by window.UI_SCALE (the same number, published together).
     Falls back to 1 (today's look) before main.js runs or where zoom is
     unsupported. */
  --ui: 1;
  /* ---- THE MEASURE EVERY SCREEN IS SET TO ---------------------------------
     A screen with no cap on it runs to the glass, and on a 2560px monitor that
     means a guild roster four words wide with three feet of nothing either side
     of it, a chat line your eye cannot track back from, and a header whose Back
     button and title are at opposite ends of the desk. The menu and the Armory
     were each capped by hand and every other screen was not, so the game had two
     screens that looked composed and eight that looked like a webpage.
     One number, stated once. Screens that are genuinely a WIDE ARTEFACT — the
     Armory shelf, the ten-by-ten grid — override it deliberately; everything
     else takes this and is centred in it. */
  --screenw: min(1320px, 94vw);
  /* physical cutouts (notch, home indicator) — 0 everywhere but on the phones
     that have them; requires viewport-fit=cover in the meta tag to be nonzero */
  --sat: env(safe-area-inset-top, 0px);
  --sab: env(safe-area-inset-bottom, 0px);
  --sal: env(safe-area-inset-left, 0px);
  --sar: env(safe-area-inset-right, 0px);
}

/* engraved SVG iconography (icons.js) — 1em line art riding the text baseline,
   inked in currentColor so every icon takes the color of the label it sits in */
.ic { display: inline-block; vertical-align: -0.125em; flex: none; }
button .ic, .ric .ic { pointer-events: none; }

* { box-sizing: border-box; margin: 0; padding: 0; }

html, body {
  width: 100%; height: 100%; overflow: hidden;
  background: var(--bg); color: var(--ink);
  font-family: var(--font-display);
  user-select: none;
}

#game { position: fixed; inset: 0; display: block; cursor: var(--cur-command); }

.hidden { display: none !important; }

/* reusable ornate panel */
.panelish {
  background: var(--plate);
  border: 1px solid var(--edge);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-3), var(--seat), var(--bevel);
}

/* ----------------------------------------------------------- HUD */
#hud { position: fixed; inset: 0; pointer-events: none; }
/* Layout containers must NOT eat battlefield clicks — only real controls do.
   (The old `#hud > * { pointer-events:auto }` made full-width bars like #topbar
   swallow every click along the screen edges.) */
#hud > * { pointer-events: none; }
#resources, #factions, #buildbar, #leavebtn, #mapframe,
#topbar > * { pointer-events: auto; }

/* Three columns, so the middle one is centred on the SCREEN rather than on whatever
   room the other two happen to leave. The outer columns are minmax(0,1fr), which is
   what makes them equal and the centre exact; the scorecard's own width (see THE DUEL
   SCORECARD) is what keeps them wide enough to hold their contents. Absolute centring
   was tried and cannot work here: the card would have to overlap the topbar's other
   furniture on any screen under ~1900px.
   THE TWO OCCUPANTS NAME THEIR OWN COLUMNS. The left column used to hold the treasury
   banner, and auto-placement did the rest; when the treasury moved to the map frame it
   left a hole, and auto-placement obligingly walked everything one column to the left —
   the standings card ended up jammed against the left edge and Surrender floated in the
   dead centre of the screen. An empty column has to STAY empty for the centre to be the
   centre, and the only way to say that is to say it. */
#topbar {
  position: absolute; top: 0; left: 0; right: 0;
  display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: start;
  padding: 12px 16px; gap: 14px;
  zoom: var(--ui);
  /* a landscape notch eats one side; the padding steps around it (see --sal) */
  padding-left: calc(16px + var(--sal) / var(--ui));
  padding-right: calc(16px + var(--sar) / var(--ui));
  padding-top: calc(12px + var(--sat) / var(--ui));
}
#topbar > #factions { grid-column: 2; }
#topbar > #leavebtn { grid-column: 3; }

/* THE COMMANDER'S OWN NUMBERS LIVE ON THE MAP FRAME. They used to be a banner of
   their own in the opposite corner of the screen from the map — a second box, with a
   second border and a second shadow, saying things about the same side of the battle
   the map is showing you. Docked into the frame they are one object with it: no
   chrome of their own, exactly the map's width, a hairline of gilt between the
   register and the enemy read-out under it. */
#resources {
  width: var(--mmw); box-sizing: border-box;
  display: grid; grid-template-columns: 1.05fr 0.82fr 1.13fr;
  align-items: center; gap: 0;
  padding: 1px 0 6px; margin-bottom: 6px;
  border-bottom: 1px solid rgba(var(--gold-rgb),0.22);
  background: none; border-radius: 0; box-shadow: none; backdrop-filter: none;
  font-size: 17px;
}
#resources b { font-variant-numeric: var(--nums); letter-spacing: 0.5px; }
.res-gold { color: #e8cf9a; }
.res-gold b { color: var(--gold-lt); text-shadow: 0 0 9px rgba(255,200,90,0.4); }
.res-inc { color: var(--ready); font-size: 13px; }
.res-pop { color: #cdd9e0; }

#factions {
  display: grid; grid-template-columns: 1fr 1fr; gap: 5px 8px;
  background: var(--plate);
  border: 1px solid var(--edge); border-radius: var(--r-lg);
  box-shadow: var(--sh-3), var(--bevel);
  padding: 9px 12px; min-width: 360px; backdrop-filter: blur(3px);
}
.fac { display: flex; align-items: center; gap: 9px; font-size: 13px; line-height: 1.1; }
.fcrest { width: 22px; height: 26px; flex: none; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.6)); }
.fbody { display: flex; flex: 1; align-items: center; gap: 6px; }
.fac .fn { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.fac .fp { color: var(--muted); font-variant-numeric: var(--nums); font-size: 12px; }
.fac.me { font-weight: 700; }
.fac.me .fn { color: var(--gold-lt); }
.fac.dead { opacity: 0.4; }
.fac.dead .fn { text-decoration: line-through; }

/* Centred by auto margins between left:0 / right:0, NOT by left:50% + translate.
   With left:50% a `width: fit-content` box only ever sees half the screen as its
   available width, so the rack capped itself at 720px on a 1440px screen and wrapped
   into more ranks than it needed to. */
#buildbar {
  position: absolute; bottom: 12px; left: 0; right: 0; margin-inline: auto;
  display: flex; flex-wrap: nowrap; justify-content: flex-start; gap: 6px;
  max-width: min(97vw, 1300px); padding: 6px 8px 8px; overflow-x: auto; overflow-y: visible;
  scrollbar-width: thin; scrollbar-color: var(--gold-dk) transparent;
  background: linear-gradient(180deg, rgba(8,6,4,0), rgba(8,6,4,0.42)); border-radius: var(--r-xl);
}
#buildbar::-webkit-scrollbar { height: 7px; }
#buildbar::-webkit-scrollbar-thumb { background: var(--gold-dk); border-radius: var(--r-sm); }
.build {
  position: relative; flex: 0 0 auto;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  width: 106px; padding: 7px 7px 7px;
  background: var(--plate);
  color: var(--ink); border: 1px solid var(--edge); border-radius: var(--r-lg);
  box-shadow: var(--sh-2), var(--bevel);
  cursor: pointer; font-family: inherit;
  transition: transform 0.06s, border-color 0.14s, box-shadow 0.14s;
}
.build:hover { border-color: var(--gold-lt); transform: translateY(-3px); z-index: 2; box-shadow: var(--sh-3), 0 0 16px rgba(var(--gold-rgb),0.32); }
.build:active { transform: translateY(-1px); }
.build.isidle { opacity: 0.92; }
.build.isidle .bicon { filter: grayscale(0.5) drop-shadow(0 2px 3px rgba(0,0,0,0.5)); }
.build.ready { border-color: var(--ready); animation: bready 1.5s ease-in-out infinite; }
@keyframes bready { 0%,100% { box-shadow: var(--sh-2), 0 0 10px rgba(var(--ready-rgb),0.25); } 50% { box-shadow: var(--sh-2), 0 0 20px rgba(var(--ready-rgb),0.6); } }
.build.levelup { animation: blevelup 0.9s ease-out; }
@keyframes blevelup { 0% { border-color: var(--gold-hi); } 30% { box-shadow: 0 0 26px rgba(255,224,120,0.9); border-color: #ffe78a; transform: translateY(-5px) scale(1.05); } 100% { box-shadow: var(--sh-2); } }
.bicon { width: 44px; height: 44px; filter: drop-shadow(0 2px 3px rgba(0,0,0,0.5)); }
.bk { position: absolute; top: 3px; left: 4px; font-size: 9px; color: #1a1205; background: var(--gold); border-radius: var(--r-xs); padding: 0 4px; font-weight: 700; font-family: var(--font-display); z-index: 1; }
.blvl { position: absolute; top: 3px; right: 4px; font-size: 10px; font-weight: 800; color: #1a1205; background: linear-gradient(180deg, var(--gold-lt), var(--gold)); border-radius: var(--r-sm); padding: 0 5px; box-shadow: var(--sh-0); z-index: 1; }
.build.isidle .blvl { background: #4a4636; color: #cdbf9a; }
/* max-width was a fixed 100px against a card that is now fluid — at narrower widths
   the name badge stuck 2px out of each side. Relative, so it can never outgrow its card. */
.bn { font-size: 11.5px; font-weight: 700; white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
/* level dots */
.bdots { display: flex; gap: 2.5px; margin: 3px 0 1px; }
.bdots i { width: 6px; height: 6px; border-radius: 50%; background: #2c2618; border: 1px solid rgba(0,0,0,0.55); }
.bdots i.on { background: radial-gradient(circle at 40% 35%, var(--gold-hi), var(--gold)); border-color: var(--gold-dk); box-shadow: 0 0 4px rgba(var(--gold-rgb),0.75); }
/* cooldown to next spawn: bar drains, number counts down */
.bcd { display: flex; align-items: center; gap: 5px; width: 100%; padding: 0 1px; margin-top: 1px; }
.bcdbar { flex: 1; height: 6px; background: #0c0a07; border: 1px solid rgba(0,0,0,0.5); border-radius: var(--r-sm); overflow: hidden; }
.bcdbar i { display: block; height: 100%; width: 0%; background: linear-gradient(90deg, #4f93c8, #8fd0f0); transition: width 0.1s linear; }
.bcdnum { font-size: 10px; font-weight: 700; font-variant-numeric: var(--nums); color: #bcd4e6; min-width: 30px; text-align: right; white-space: nowrap; }
.build.isidle .bcdnum { color: var(--muted); }
/* two-line upgrade button so the action + cost always fit */
.bupg { margin-top: 5px; width: 100%; display: flex; flex-direction: column; align-items: center; gap: 0; line-height: 1.15; border-radius: var(--r-md); padding: 4px 2px; border: 1px solid transparent; }
.bupgw { font-size: 9.5px; font-weight: 800; letter-spacing: 0.3px; }
.bupgc { font-size: 11.5px; font-weight: 800; font-variant-numeric: var(--nums); }
.bupg.go { color: #07140b; background: linear-gradient(180deg, var(--ready), var(--ready-dk)); border-color: #2f8f54; box-shadow: 0 0 10px rgba(var(--ready-rgb),0.4); }
.bupg.cant { color: var(--muted); background: rgba(255,255,255,0.05); border-color: rgba(255,255,255,0.09); }
.bupg.cant .bupgc { color: var(--gold-lt); opacity: 0.75; }
.bupg.maxed { color: var(--gold-lt); background: rgba(var(--gold-rgb),0.14); border-color: var(--gold-dk); }

#tip {
  position: fixed; z-index: 30; pointer-events: none; width: 232px; padding: 9px 12px;
  zoom: var(--ui);
  background: linear-gradient(160deg, rgba(40,33,24,0.98), rgba(16,12,8,0.99));
  border: 1px solid var(--edge); border-radius: var(--r-lg);
  box-shadow: var(--sh-3);
}
.tname { font-size: 15px; font-weight: 700; color: var(--gold-lt); }
.tcat { font-size: 11px; color: var(--muted); margin: 2px 0 5px; font-family: var(--font-ui); }
.tstats { font-size: 11.5px; color: #cdd9e0; font-family: var(--font-ui); font-variant-numeric: var(--nums); }
/* .tdesc is gone: the in-battle tooltip no longer carries flavour prose. Mid-fight a
   tooltip is a REFERENCE — what it costs, what it kills, what kills it — and a line of
   description sat between the counters and the price on every single hover. The Armory
   and the muster still tell you what a thing IS; they are where you have time to read. */

/* The keybind strip that used to run along the very bottom is GONE. It spelled out
   eight controls in small type under the Build button, on every screen, forever — and
   the ? beside the battle log has carried the same reference in full the whole time.
   A permanent line of instructions is what a game shows when it does not trust its
   own controls; the reference is a keypress away for anyone who wants it. */

/* ---- THE CARTOUCHE -----------------------------------------------------
   The corner of the screen answers for YOUR side of the battle, and it is the
   one piece of standing furniture that sits directly on the map. So it is drawn
   as the thing a map puts in its corner: a CARTOUCHE — the titled block an old
   chart carries its legend and its scale in.
   Everything here is borrowed from the finish the battlefield itself is baked
   with (see the PARCHMENT FINISH in terrain.js): a warm vellum ground, foxing
   blooms in the corners, fine ink fibres running through the grain, and a
   double engraved rule with gilt lozenges struck at the corners. It is the same
   page as the ground under it, not a black glass rectangle floating over it. */
#mapframe {
  /* 24, not 16: the keybind strip runs along the bottom edge and the frame's lower
     rail was landing on its ascenders at narrower widths */
  position: absolute;
  bottom: calc(24px + var(--sab) / var(--ui));
  right: calc(16px + var(--sar) / var(--ui));
  padding: 9px;
  zoom: var(--ui);
  background:
    /* the foxing — two old-paper stains, in the corners where they always are */
    radial-gradient(120% 80% at 12% 4%, rgba(196,150,74,0.13), rgba(0,0,0,0) 60%),
    radial-gradient(90% 70% at 92% 96%, rgba(150,104,46,0.12), rgba(0,0,0,0) 62%),
    /* the grain — ink fibres, laid at the angle the plate is lit from */
    repeating-linear-gradient(112deg, rgba(var(--lit-rgb),0.030) 0 1px, rgba(0,0,0,0) 1px 5px),
    /* the vellum itself, still dark enough to hold gilt type over a bright map */
    linear-gradient(158deg, #33281a, #1b140d 62%, #0f0b07);
  border: 2px solid var(--gold-dk); border-radius: var(--r-lg);
  box-shadow: var(--sh-3), inset 0 0 0 1px rgba(0,0,0,0.5),
    /* the second rule of the engraved border, held off the first by the padding */
    inset 0 0 0 4px rgba(var(--gold-rgb),0.16), inset 0 0 0 5px rgba(0,0,0,0.35);
}
/* THE FOUR CORNER LOZENGES, ON ONE LAYER THAT TRACKS THE BOX. They used to be
   two 7px dots each casting a second dot 210px to its right — a hardcoded offset
   against a frame whose width is a variable. At --mmw:208 it landed 5px from the
   right rail instead of 4; at the 190 and 166 breakpoints the right-hand pair
   fell outside the frame entirely and the cartouche lost half its corners on
   every narrow screen. Four background positions cannot drift from the box they
   are painted on. */
#mapframe::before {
  content: ''; position: absolute; inset: 3px; border-radius: var(--r-md);
  pointer-events: none;
  background-image:
    radial-gradient(circle, var(--gold) 0 2.4px, rgba(var(--gold-rgb),0.35) 2.4px 3.4px, rgba(0,0,0,0) 3.6px),
    radial-gradient(circle, var(--gold) 0 2.4px, rgba(var(--gold-rgb),0.35) 2.4px 3.4px, rgba(0,0,0,0) 3.6px),
    radial-gradient(circle, var(--gold) 0 2.4px, rgba(var(--gold-rgb),0.35) 2.4px 3.4px, rgba(0,0,0,0) 3.6px),
    radial-gradient(circle, var(--gold) 0 2.4px, rgba(var(--gold-rgb),0.35) 2.4px 3.4px, rgba(0,0,0,0) 3.6px);
  background-size: 9px 9px; background-repeat: no-repeat;
  background-position: left top, right top, left bottom, right bottom;
  filter: drop-shadow(0 0 4px rgba(var(--gilt-lit-rgb),0.45));
}

/* ---- THE SKY, PINNED TO THE MAP ----------------------------------------
   It answers for the match's OMEN — the themed condition every battle is dealt,
   which applies to BOTH sides all game — and names the sky underneath it.
   Announced once at the muster and then never again is how you get "why is nothing
   shooting as far as it should" with nowhere to look.

   IT WAS A 27px DISC, AND THAT WAS THE REMAINING HALF OF THE BUG. A glyph with no
   label, whose modifier only appeared on hover, is an unsigned modifier in
   practice: hover does not exist on touch, and mid-battle nobody spends a
   deliberate act on a symbol they have not been given a reason to suspect. It is
   a NAMED STRIP now, the full width of the map it sits under, wearing the sky's
   own accent (--wxc, from the shared table), with the omen's name and its
   headline effect printed on its face — and hover promoted from "where you find
   out there IS a modifier" to "where you read all of it". It never hides, either:
   a quiet omen under a clear sky says so, because the muster a player learns this
   element from is every muster, not only the loud ones. */
/* The legend panel of the cartouche: an inlay of paler vellum set into the plate,
   so the omen reads as a card laid ON the register rather than another band of
   the same dark glass. It wears the sky's own accent on its top rule. */

/* the heading rank — the sky's mark struck into a roundel, and the omen named
   beside it. The name is allowed to WRAP: "THE GATHERING STOR…" is not a name,
   and a legend whose title has been cut off is a legend that has failed at the
   one job the title does. */

/* THE MODIFIER, ON THE FACE. Not in a tooltip. */

/* a weather that actually CHANGES the fight says so in ink, and breathes
   ON ITS OWN LAYER. This used to animate the strip's `box-shadow` directly,
   which is a property the browser cannot hand to the compositor: every frame of
   a 3.2s loop that never ends repainted the whole strip, all match, for a glow
   that fades in and out. The same breath drawn as the OPACITY of a pseudo-element
   is composited, so it costs nothing per frame and looks identical.
   `z-index: -1` keeps it above the strip's own plate and below its type — the
   badge is already a stacking context (position + z-index), so it cannot escape. */

@keyframes wxPulse { 0%, 100% { opacity: 0; } 50% { opacity: 1; } }
/* …and one that is not finished with you yet gets a second, slower mark. A turning
   omen is the only condition in the game whose numbers are going to change while you
   are playing around them, so it is worth being able to tell one at a glance.
   (It was a scaling RING when the glass was a 27px disc; on a full-width strip a
   scaled outline sweeps out over the minimap above it, so the same idea is drawn
   as a breathing outline that stays inside the strip's own footprint.) */
/* Opacity alone, for the same reason as above — an animated box-shadow SPREAD
   repaints the strip and its surroundings on every frame of a loop that runs for
   the length of the match, and the spread was fading to fully transparent at the
   halfway mark anyway, so it was paying for a ring nobody could see. */

@keyframes wxTurns { 0%, 100% { opacity: 0.8; } 50% { opacity: 0.24; } }
/* ---- THE FOOTNOTE, NOT A SECOND COPY OF THE CARD ------------------------
   Two faults, and they made each other worse. It was anchored `left: 0` on an
   element docked to the BOTTOM-RIGHT CORNER of the screen and then made wider
   than that element, so it grew off the right edge of the display — the one
   direction it had no room in. And what it grew out there to say was the omen's
   name, and then every rule restated as a sentence: both of which are now
   printed on the face of the card two centimetres below it. A tooltip that
   repeats the thing it is attached to is charging the player a hover to read
   what they were already looking at.
   So it hangs from the right rail, where the room is, and it carries only the
   two things the card cannot: the line of prose that says what the omen IS, and
   the sky it is fought under. */

/* the name is on the card an inch below; the mood line is gone entirely */

/* THE RULES, ONE PER ROW, IN FIXED COLUMNS. How much · what it moves · who it
   lands on. Scannable down the left edge for the figures and down the right for
   the troops, which a sentence of the same facts is not. */

/* …and the one fact the card genuinely does not carry: that it lands on everyone.
   Four words, rather than the paragraph that used to restate every chip. */

/* the sky it is fought under — the footnote line, since the glass is the OMEN's */

/* the landing, when the muster's last act folds the omen down into this strip */


/* enemy army composition read-out, docked above the minimap. It takes its width from
   the map below it (--mmw), so shrinking the minimap on small screens shrinks the
   whole frame instead of leaving a 230px-wide readout propping it open. */
:root { --mmw: 230px; }
#minimap { display: block; width: var(--mmw); height: var(--mmw); border-radius: var(--r-xs); }

/* ---- EMOTES: the wheel above the map ----------------------------------
   Docked to the map frame's top edge rather than the bottom utility stack, because
   this is a thing you reach for while WATCHING the map — the same corner your eye is
   already in — and the bottom-left stack is where the reference material lives. */
/* -22px, not -15px: the frame's top register is the resource read-out now, and at
   -15 the button's lower third sat over the Army figure's ascenders. It clears the
   border and the padding band and still straddles the rail. */
/* ---- A STUD IS A REAL SIZE, NOT A SCALED ONE ----------------------------
   These two ride inside #mapframe, which carries `zoom: var(--ui)` — so a flat
   30px here is 30px only at --ui 1, and --ui is the viewport measured against a
   1600×900 design. Measured on the shipped build: 36px on a 1080p desk, 26px on
   a 1366 laptop, 24px on a tablet and 20px on a 1024×640 touchscreen. March-All
   is the control that takes EVERY spawner in hand at once, and on a tablet it
   was a 24px dot — around half of any touch-target guidance, for one of the two
   most-reached-for controls in a battle.
   Dividing by --ui cancels the zoom, so the number below is the size on the
   GLASS, whatever the scale: 38px under a cursor, 46px under a finger. The 30px
   floor keeps it from growing past today's look where --ui is above 1 (a 4K wall),
   because a stud that is always exactly 46 real px would be a knob up there. */
#mapframe { --stud: max(30px, calc(38px / var(--ui))); --studgap: 6px; }
@media (pointer: coarse) { #mapframe { --stud: max(30px, calc(46px / var(--ui))); } }
#emotebtn { position: absolute; top: calc(var(--stud) * -0.73); right: 9px; z-index: 3;
  width: var(--stud); height: var(--stud); border-radius: 50%; cursor: pointer; line-height: 1;
  font: 700 17px var(--font-display); color: var(--gilt-ink);
  background: linear-gradient(180deg, var(--gold-hi), var(--gold)); border: 1.5px solid var(--gold-dk);
  box-shadow: var(--sh-2), inset 0 1px 0 rgba(255,255,255,0.55);
  transition: transform 0.12s, filter 0.12s; }
#emotebtn:hover { filter: brightness(1.08); transform: scale(1.08); }
#emotebtn.on { background: linear-gradient(180deg, var(--ready-lt), var(--ready-dk)); border-color: #2f8f54; }

/* MARCH ALL, on the same rail and to the emote button's left. 36px of gap is the
   button's own 30 plus a 6px breath, so the two read as a pair of controls rather
   than one control with a growth on it. Everything else is inherited from the
   emote button deliberately: they are the same KIND of thing — a round gilt stud
   on the frame's top edge — and two studs that differ in size or finish would
   imply a difference in rank that is not there. */
#marchallbtn { position: absolute; top: calc(var(--stud) * -0.73); right: calc(9px + var(--stud) + var(--studgap)); z-index: 3;
  width: var(--stud); height: var(--stud); border-radius: 50%; cursor: pointer; line-height: 1;
  display: grid; place-items: center;
  font: 700 15px var(--font-display); color: var(--gilt-ink);
  background: linear-gradient(180deg, var(--gold-hi), var(--gold)); border: 1.5px solid var(--gold-dk);
  box-shadow: var(--sh-2), inset 0 1px 0 rgba(255,255,255,0.55);
  transition: transform 0.12s, filter 0.12s; }
/* the mark rides the stud rather than sitting at a fixed size inside it, so the
   chevrons keep the same weight against the rim at every scale */
#marchallbtn .ic { width: calc(var(--stud) * 0.57); height: calc(var(--stud) * 0.57); display: block; }
#emotebtn { font-size: calc(var(--stud) * 0.57); }
#marchallbtn:hover { filter: brightness(1.08); transform: scale(1.08); }
/* ARMED IS A DIFFERENT STATE FROM PRESSED, and it lasts until you click the field
   — so it pulses rather than merely lighting up. A stud that is quietly a
   different colour is something a commander mid-battle will not notice they left
   on; one that is breathing is not. */
#marchallbtn.on { background: linear-gradient(180deg, var(--ready-lt), var(--ready-dk)); border-color: #2f8f54;
  animation: marchAllArmed 1.15s ease-in-out infinite; }
@keyframes marchAllArmed {
  0%, 100% { box-shadow: var(--sh-2), inset 0 1px 0 rgba(255,255,255,0.55), 0 0 0 0 rgba(63,191,99,0.55); }
  50%      { box-shadow: var(--sh-2), inset 0 1px 0 rgba(255,255,255,0.55), 0 0 0 6px rgba(63,191,99,0); }
}

/* A GRID, NOT A RADIAL WHEEL. Twenty tiles on a circle puts every one of them at a
   different angle and distance from the cursor, which is precisely the layout you
   cannot build muscle memory for; five across in four ranks means "well played" is
   always in the same place, and the name under each glyph is what makes it learnable
   in the first place. */
/* The sheet is 250px tall and it hangs ABOVE the map frame, which itself sits 24px off
   the bottom of a frame ~216px tall — so on a 450px-high window it ran 22px off the top
   of the screen and the first rank of emotes could not be clicked. `100%` inside the
   calc resolves against the map frame, so this is "whatever is left above the frame",
   and the sheet scrolls rather than escaping when that is less than it wants. */
#emotewheel { position: absolute; bottom: calc(100% + 12px); right: -2px; z-index: 40;
  display: grid; grid-template-columns: repeat(5, 1fr); gap: 5px;
  width: max-content; overflow-y: auto; overscroll-behavior: contain;
  max-height: calc(100vh / var(--ui) - 100% - 46px);
  max-height: calc(100dvh / var(--ui) - 100% - 46px);
  padding: 11px; border-radius: var(--r-xl);
  background: linear-gradient(165deg, rgba(46,37,23,0.98), rgba(16,12,7,0.99));
  border: 1px solid var(--edge-strong);
  box-shadow: var(--sh-4), inset 0 1px 0 rgba(var(--lit-rgb),0.14);
  animation: briefPop 0.22s var(--e-back); }
#emotewheel.hidden { display: none; }
/* ---- THE RACK ----------------------------------------------------------
   62px tiles carrying a 34px abstract gold device and an 8px label: a wall of
   near-identical brass shapes with captions too small to scan, so picking the emote
   you meant took a deliberate read every time and most players used two of the
   twenty. The tiles are bigger, the GLYPH is the thing you see first, the device
   sits behind it as the kingdom's watermark, and the label is finally readable. */
.emtile { display: flex; flex-direction: column; align-items: center; justify-content: flex-end; gap: 2px;
  width: 78px; height: 78px; padding: 7px 4px 6px; cursor: pointer; border-radius: var(--r-xl);
  background: linear-gradient(170deg, rgba(255,255,255,0.07), rgba(255,255,255,0.02));
  border: 1px solid rgba(var(--gold-rgb),0.22);
  transition: transform 0.12s var(--e-back), background 0.12s, border-color 0.12s, box-shadow 0.12s; }
.emtile:hover { transform: translateY(-4px) scale(1.09); background: rgba(var(--gold-rgb),0.2);
  border-color: var(--gold-lt); box-shadow: var(--sh-3), 0 0 0 1px rgba(var(--lit-rgb),0.25); z-index: 1; }
.emtile:active { transform: translateY(-1px) scale(0.96); }
/* the glyph, over the device — this is what a player actually reads */
.emtile .emglyph { position: absolute; top: 12px; left: 0; right: 0;
  font-size: 30px; line-height: 1; font-weight: 400; text-align: center;
  filter: drop-shadow(0 2px 3px rgba(0,0,0,0.8));
  transition: transform 0.12s var(--e-back-lg); pointer-events: none; }
.emtile:hover .emglyph { transform: scale(1.18) rotate(-5deg); }
/* ---- the rack of banners ----------------------------------------------
   Each tile carries the same hand-cut device that will fly over your Builder, in
   your own kingdom's colours — so what you pick and what the field sees are
   visibly the same object. They lean on a slow stagger, as cloth on a wall does,
   rather than bobbing in unison like a character map. */
/* the device, dropped to a watermark behind the glyph — it is the same cut that will
   fly over your Builder, so the tile and the field are visibly one object */
.emtile .emsig { position: absolute; top: 8px; left: 50%; margin-left: -21px;
  width: 42px; height: 42px; opacity: 0.34;
  animation: emBob 3.1s ease-in-out infinite; animation-delay: var(--d, 0s); transform-origin: 50% 8%; }
.emtile .emsig canvas { display: block; width: 42px; height: 42px; }
.emtile:hover .emsig { animation-duration: 0.9s; opacity: 0.62; }
@keyframes emBob { 0%,100% { transform: rotate(-4deg); } 50% { transform: rotate(4deg) translateY(-2px); } }
.emtile i { font-style: normal; font: 700 9.5px var(--font-ui); letter-spacing: 0.3px;
  color: #cbbd97; text-align: center; line-height: 1.15; max-width: 100%; }
.emtile:hover i { color: var(--gold-bright); }
/* the hotkey, shown only while the sheet is open — which is the only time it works */
.emtile u { position: absolute; top: 2px; right: 4px; text-decoration: none;
  font: 800 8px var(--font-ui); color: rgba(var(--gold-rgb),0.55); }
.emtile { position: relative; }
.emtile:hover u { color: var(--gold-bright); }
/* the cooldown: the sheet stays open and simply says "not yet" */
#emotewheel.cool .emtile { opacity: 0.42; pointer-events: none; }
@media (max-width: 620px) {
  .emtile { width: 60px; height: 62px; }
  .emtile .emglyph { font-size: 24px; top: 9px; }
  .emtile .emsig { width: 34px; height: 34px; margin-left: -17px; }
  .emtile .emsig canvas { width: 34px; height: 34px; }
  .emtile i { font-size: 8px; }
}
/* A short-and-wide window (a laptop held low, a phone turned sideways) has plenty of
   room across and none at all up: ten abreast makes the sheet two ranks instead of four,
   which fits above the frame with the labels intact rather than scrolling. */
@media (max-height: 640px) and (min-width: 700px) {
  #emotewheel { grid-template-columns: repeat(10, 1fr); gap: 4px; padding: 9px; }
  .emtile { width: 62px; height: 64px; padding: 5px 2px 4px; }
  .emtile .emglyph { font-size: 25px; top: 9px; }
  .emtile .emsig { width: 36px; height: 36px; margin-left: -18px; }
  .emtile .emsig canvas { width: 36px; height: 36px; }
  .emtile i { font-size: 8px; }
}

/* ----------------------------------------------------------- overlay */
#overlay {
  position: fixed; inset: 0; z-index: 20;
  display: flex; align-items: center; justify-content: center;
  background: radial-gradient(circle at 50% 24%, #241a12, #07090c 72%);
  overflow: hidden;
}
.vignette-bg {
  position: absolute; inset: -10%;
  background:
    radial-gradient(circle at 18% 80%, rgba(255,120,50,0.10), transparent 36%),
    radial-gradient(circle at 82% 78%, rgba(255,200,80,0.08), transparent 40%);
  animation: flicker 5s ease-in-out infinite alternate;
}
@keyframes flicker { from { opacity: 0.7; } to { opacity: 1; } }

.panel {
  position: relative; z-index: 1;
  width: min(660px, 94vw);
  background: linear-gradient(160deg, rgba(38,31,22,0.96), rgba(16,12,8,0.97));
  border: 1px solid var(--edge); border-radius: var(--r-2xl);
  padding: 30px 38px 34px; text-align: center;
  box-shadow: var(--sh-4), inset 0 0 0 1px rgba(0,0,0,0.5), inset 0 1px 0 rgba(var(--lit-rgb),0.1);
}
.panel h1 {
  font-size: 48px; font-weight: 800; letter-spacing: 2px; margin-bottom: 8px;
  background: linear-gradient(180deg, var(--gold-hi), var(--gold) 55%, var(--gold-dk));
  -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
  filter: drop-shadow(0 2px 6px rgba(0,0,0,0.6));
}
.tagline { color: var(--muted); margin-bottom: 20px; line-height: 1.5; font-family: var(--font-flavor); font-size: 16px; }

#crests {
  display: flex; justify-content: center; gap: 16px; margin: 6px 0 22px;
  flex-wrap: wrap;
}
.kcard { display: flex; flex-direction: column; align-items: center; gap: 4px; width: 96px; }
.kcard canvas { filter: drop-shadow(0 4px 10px rgba(0,0,0,0.6)); transition: transform 0.18s; }
.kcard:hover canvas { transform: translateY(-4px) scale(1.05); }
.kname { font-size: 14px; font-weight: 600; font-family: var(--font-flavor); }
.kblurb { font-size: 11px; color: var(--muted); font-family: var(--font-ui); }

.joinrow { display: flex; gap: 10px; }
#name {
  flex: 1; padding: 13px 15px; font-size: 16px; font-family: var(--font-flavor);
  background: #0c0a07; color: var(--ink);
  border: 1px solid var(--edge); border-radius: var(--r-lg);
}
#name:focus { outline: none; border-color: var(--gold-lt); box-shadow: 0 0 12px rgba(var(--gold-rgb),0.3); }
#play {
  padding: 13px 26px; font-size: 17px; font-weight: 700; font-family: var(--font-display);
  color: var(--gilt-ink); background: linear-gradient(180deg, var(--gold-lt), var(--gold) 60%, var(--gold-dk));
  border: 1px solid var(--gilt-deep); border-radius: var(--r-lg); cursor: pointer; letter-spacing: 0.5px;
  white-space: nowrap; box-shadow: var(--sh-2), inset 0 1px 0 rgba(255,255,255,0.4);
  transition: filter 0.12s, transform 0.05s;
}
#play:hover { filter: brightness(1.08); }
#play:active { transform: translateY(1px); }
#status { margin-top: 12px; font-size: 13px; color: var(--muted); min-height: 18px; font-family: var(--font-ui); }

.rules {
  text-align: left; margin-top: 22px; padding-top: 18px;
  border-top: 1px solid rgba(var(--gold-rgb),0.25); list-style: none;
  display: grid; grid-template-columns: 1fr 1fr; gap: 9px 22px;
}
.rules li { font-size: 12.5px; color: var(--muted); line-height: 1.45; padding-left: 20px; position: relative; font-family: var(--font-ui); }
.rules li::before { content: '⚜'; position: absolute; left: 0; color: var(--gold); }
.rules b { color: var(--ink); }

/* (the old #endbanner block is gone — nothing has built that element since the
   .resultbox proclamation took over the end of a battle) */

@media (max-width: 560px) {
  .rules { grid-template-columns: 1fr; }
  .panel h1 { font-size: 38px; }
  #crests { gap: 10px; }
  .kcard { width: 72px; }
}

/* ===================================================================== */
/* ============================  MENU SHELL  =========================== */
/* ===================================================================== */
#screens { position: fixed; inset: 0; z-index: 20; }
#screens.hidden { display: none; }
.screen {
  position: absolute; inset: 0; opacity: 0; pointer-events: none;
  display: flex; flex-direction: column; transition: opacity 0.25s ease;
  overflow-y: auto; overflow-x: hidden;   /* short viewports: scroll instead of trapping PLAY off-screen */
  background: radial-gradient(circle at 50% 42%, rgba(8,11,15,0.5), rgba(5,8,11,0.8));
}
/* darker behind dense content screens so panels/text read clearly */
#scr-modes, #scr-armory, #scr-profile { background: radial-gradient(circle at 50% 40%, rgba(8,11,15,0.78), rgba(5,8,11,0.9)); }
.screen.active { opacity: 1; pointer-events: auto; }

.chip {
  display: inline-flex; align-items: center; gap: 12px; padding: 9px 16px;
  background: var(--plate);
  border: 1px solid var(--edge); border-radius: var(--r-xl); font-size: 14px;
  box-shadow: var(--sh-3), var(--bevel);
}
.chip .gold { color: var(--gold-lt); font-weight: 700; }
/* the purse flashes when the Mint pays out */
.chip.minted { animation: mintFlash 0.75s ease-out; }
@keyframes mintFlash {
  0%   { box-shadow: var(--sh-3), 0 0 0 0 rgba(var(--gilt-lit-rgb),0.85); }
  40%  { box-shadow: var(--sh-3), 0 0 26px 6px rgba(var(--gilt-lit-rgb),0.55); }
  100% { box-shadow: var(--sh-3), 0 0 0 0 rgba(var(--gilt-lit-rgb),0); }
}

/* ---- BUY GOLD, and the Mint behind it ------------------------------------
   A shop that takes no money. It exists to be fun to press: the button sits
   beside the purse it fills, and every tier hands over its gold the moment it is
   clicked. Nothing is ever asked for — no card, no account, no form — and the
   copy says so twice, because a fake storefront that plays it straight is a
   different and much worse thing than one that is obviously having you on. */
.buygold {
  display: inline-flex; align-items: center; gap: 7px; margin-right: 10px;
  padding: 9px 15px; cursor: pointer; font-family: var(--font-display);
  font-size: 13px; font-weight: 800; letter-spacing: 0.6px; color: var(--gilt-ink);
  background: linear-gradient(180deg, var(--gold-lt), var(--gold) 62%, var(--gold-dk));
  border: 1px solid var(--gilt-deep); border-radius: var(--r-xl);
  box-shadow: var(--sh-3), inset 0 1px 0 rgba(255,255,255,0.5);
  transition: transform 0.12s ease, filter 0.12s ease;
}
.buygold:hover { filter: brightness(1.07); transform: translateY(-2px); }
.buygold:active { transform: translateY(0); }
.buygold .bg-ic { font-size: 15px; animation: bgSpin 3.4s ease-in-out infinite; }
@keyframes bgSpin { 0%,72%,100% { transform: rotateY(0); } 84% { transform: rotateY(180deg); } }

.mintlay {
  position: fixed; inset: 0; z-index: 60; display: flex; align-items: center; justify-content: center;
  padding: 20px; background: rgba(6,7,10,0.72); backdrop-filter: blur(5px);
  opacity: 0; transition: opacity 0.18s ease;
}
.mintlay.in { opacity: 1; }
.mintlay.out { opacity: 0; }
.mintbox {
  position: relative; width: min(880px, 96vw); max-height: 92vh; overflow: auto;
  padding: 20px 22px 16px; border-radius: var(--r-2xl); text-align: center;
  background: linear-gradient(170deg, rgba(44,36,23,0.98), rgba(13,11,8,0.99));
  border: 1px solid var(--edge); border-top: 3px solid var(--gold);
  box-shadow: var(--sh-4), 0 0 40px rgba(var(--gold-rgb),0.14);
  transform: translateY(10px) scale(0.99); transition: transform 0.22s var(--e-out);
}
.mintlay.in .mintbox { transform: none; }
.mintx { position: absolute; top: 10px; right: 12px; width: 28px; height: 28px; cursor: pointer;
  background: rgba(0,0,0,0.35); color: var(--gold-lt); border: 1px solid var(--edge);
  border-radius: var(--r-lg); font-size: 13px; }
.mintx:hover { border-color: var(--gold-lt); }
.mint-t { font: 800 24px var(--font-display); color: var(--gold-lt); letter-spacing: 1px; }
/* THE MINT READS PLAINLY. Its three quiet lines — the Mintmaster's greeting, each
   tier's aside and the small print — were set in italic MedievalSharp, a display
   face whose lowercase is close enough to handwriting that at 10px it stops being
   type and becomes texture. This is the one screen in the game with prices on it,
   so it is the last place that can afford a font you have to decode. Same words,
   same voice, in the UI serif everything else legible is set in. */
.mint-s { font: 500 13px var(--font-ui); color: #bcae90; margin-top: 3px; letter-spacing: 0.2px; }
.mint-joke { margin: 10px auto 14px; max-width: 620px; padding: 7px 12px; border-radius: var(--r-lg);
  font: 600 11.5px var(--font-ui); color: var(--ready); line-height: 1.45;
  background: rgba(var(--ready-rgb),0.09); border: 1px solid rgba(var(--ready-rgb),0.26); }
.mint-joke b { color: #d8ffe2; }

.mintgrid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; }
@media (max-width: 780px) { .mintgrid { grid-template-columns: repeat(2, 1fr); } }
.minttier {
  position: relative; display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 16px 8px 10px; cursor: pointer; overflow: hidden;
  background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(0,0,0,0.28));
  border: 1px solid var(--edge); border-radius: var(--r-xl);
  transition: transform 0.14s ease, border-color 0.14s ease, box-shadow 0.14s ease;
}
.minttier:hover { transform: translateY(-4px); border-color: var(--gold-lt);
  box-shadow: var(--sh-3), 0 0 18px rgba(var(--gold-rgb),0.25); }
.minttier.hot { border-color: rgba(var(--gold-rgb),0.65); background: linear-gradient(180deg, rgba(var(--gilt-lit-rgb),0.12), rgba(0,0,0,0.3)); }
.mt-flag { position: absolute; top: 0; left: 0; right: 0; padding: 2px 0;
  font: 800 8.5px var(--font-ui); letter-spacing: 0.8px; color: var(--gilt-ink);
  background: linear-gradient(90deg, var(--gold-lt), var(--gold)); }
.mt-emo { font-size: 30px; margin-top: 6px; filter: drop-shadow(0 3px 6px rgba(0,0,0,0.6)); }
.mt-amt { font: 800 18px var(--font-display); color: var(--gold-lt); font-variant-numeric: var(--nums); }
.mt-lbl { font: 700 9px var(--font-ui); letter-spacing: 1.4px; color: var(--muted); }
.mt-note { font: 500 10.5px var(--font-ui); color: #a89a7c; margin: 4px 2px 7px; line-height: 1.34; min-height: 26px; }
.mt-buy { width: 100%; padding: 5px 0; border-radius: var(--r-md);
  font: 800 12px var(--font-display); color: var(--gilt-ink);
  background: linear-gradient(180deg, var(--gold-lt), var(--gold) 62%, var(--gold-dk)); }
.minttier.paid { border-color: var(--ready); cursor: default; transform: none; }
.minttier.paid .mt-buy { background: linear-gradient(180deg, var(--ready), var(--ready-dk)); color: #0e2a17; }
.minttier.paid:hover { box-shadow: none; }

.mintfoot { margin-top: 13px; font: 500 11px var(--font-ui); color: #8b8069; }
.mintfoot.paidfoot { color: var(--ready); font-weight: 600; }
.mintfoot b { color: #d8ffe2; }

/* the payout itself: coin thrown off the tier you pressed */
.mintcoin { position: fixed; z-index: 61; pointer-events: none; font-size: 19px;
  animation: mintToss 1.2s var(--e-out) forwards; }
@keyframes mintToss {
  0%   { opacity: 0; transform: translate(0,0) rotate(0) scale(0.6); }
  12%  { opacity: 1; }
  100% { opacity: 0; transform: translate(var(--dx), var(--dy)) rotate(var(--rot)) scale(1.05); }
}

/* ---- main menu ----
   EVERY vertical measure on this screen is fluid in vh, because the home page has to
   fit — crest, title, four kingdoms, PLAY, Armory and Commander — on whatever screen
   it lands on. Height breakpoints could not do it: they step, and between two steps
   there is always a viewport that overflows (1280x700 pushed the bottom row 24px past
   the fold). Sizing continuously means the column shrinks with the window instead of
   waiting for the next breakpoint, so there is no height at which it spills.

   The column is as wide as the heraldry row needs, not as wide as the prose wants:
   at 700px the seven kingdoms could not fit one rank, so they wrapped 6+1 and that
   second rank alone (144px) pushed PLAY and the nav row clean off the bottom of a
   1440x900 window — worse on bigger screens, because the cards grow with vw while
   the column did not.

   There is no prose on this screen any more. Two paragraphs — one under the title,
   one under the banners — explained a game the title plate and seven banners already
   show, and between them they took a third of the fold to do it. What is left is the
   lockup, the choice, and the button: the vertical they were holding went to the
   banners, which is where it says something. */
/* Buy Gold sits immediately left of the purse it fills — `align-items` so the two
   line up on their centres rather than the button stretching to the row's height. */
.topbarmenu { display: flex; justify-content: flex-end; align-items: center; padding: clamp(6px, 1.4vh, 16px) 20px; }
.menucenter { margin: auto; text-align: center; max-width: min(1040px, 94vw); padding: clamp(4px, 1.4vh, 20px) 20px; }
.bigbtn {
  font-size: clamp(15px, 2.1vh, 22px); font-weight: 800; letter-spacing: 2px; font-family: var(--font-display);
  padding: clamp(9px, 1.6vh, 16px) clamp(32px, 5vw, 64px); color: var(--gilt-ink); cursor: pointer;
  background: linear-gradient(180deg, var(--gold-lt), var(--gold) 60%, var(--gold-dk));
  border: 1px solid var(--gilt-deep); border-radius: var(--r-xl);
  box-shadow: var(--sh-3), 0 0 30px rgba(var(--gold-rgb),0.25), inset 0 1px 0 rgba(255,255,255,0.45);
  transition: transform 0.08s, filter 0.12s;
}
.bigbtn:hover { filter: brightness(1.08); transform: translateY(-2px); }
.bigbtn:active { transform: translateY(0); }
/* ---- THE BATTLES: one shape, chosen between ---------------------------
   Both modes are the same button — same width, same height, same two lines —
   because they are the same kind of choice. Only the colour ranks them: the
   headline battle is gilt, the other is dark. They used to differ in font size,
   padding, width AND structure (one carried a subtitle, the other did not), which
   made the pair read as an offer and a footnote instead of as two battles. */
.bigbtn.modebtn {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  width: clamp(300px, 34vw, 420px); box-sizing: border-box; margin: 0 auto;
  padding: clamp(13px, 2.1vh, 19px) 22px; font-size: clamp(15px, 1.9vh, 19px); letter-spacing: 1.4px;
}
.bigbtn.modebtn .bttl { display: block; white-space: nowrap; }
/* Button wrapper — one per battle, so the next battle's wrapper starts a
   clean margin below rather than the button-to-button rule it used to be.
   Position:relative so the square toggle (see .rankcheck) has this wrap's
   own box, not the whole stack, to measure its left edge from — and
   width:fit-content so that box IS the button's own width rather than the
   full centered column's, or the toggle anchors to a phantom edge dozens
   of pixels from the button it's meant to sit against. */
.modewrap { position: relative; display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 0 auto; }
.modewrap + .modewrap { margin-top: 12px; }
/* the second battle: same box, quieter livery */
.bigbtn.alt {
  color: var(--ink, var(--ink));
  background: linear-gradient(160deg, rgba(56,46,30,0.92), rgba(38,31,20,0.92));
  border: 1px solid var(--gilt-deep); border-radius: var(--r-xl);
  box-shadow: var(--sh-3), inset 0 1px 0 rgba(255,255,255,0.08);
  animation: none;
}
.bigbtn.alt::after { display: none; }
.bigbtn.alt:hover { border-color: var(--gold); filter: brightness(1.12); }
/* THE SQUARE TOGGLE BESIDE THE BATTLE IT CHANGES. It has lived under each
   button, then as a single word-and-tick pill on the stack's corner — this
   puts a plain square check back beside each button, one per battle, both
   wired to the same `Rank.on` flag so ticking either arms both. The word
   sits above its own square rather than as a tooltip, and the pair sits
   close against the button, not off in the margin beside it. */
.rankcheck {
  position: absolute; top: 50%; right: 100%; margin-right: 8px; transform: translateY(-50%);
  display: inline-flex; flex-direction: column; align-items: center; gap: 4px; cursor: pointer;
}
.rankcheck input { position: absolute; opacity: 0; width: 0; height: 0; }
.rankcheck-word {
  font: 700 10px var(--font-display); letter-spacing: 1px; text-transform: uppercase;
  color: var(--muted); white-space: nowrap; transition: color var(--t-2) var(--e-out);
}
.rankcheck.on .rankcheck-word { color: var(--gold-lt); }
.rankcheck-tick { width: 36px; height: 36px; border-radius: var(--r-sm);
  border: 1px solid var(--gilt-deep); background: rgba(var(--shade-rgb), 0.45); box-shadow: var(--sh-2), var(--seat);
  display: grid; place-items: center; font: 900 20px var(--font-display); color: transparent;
  transition: all var(--t-2) var(--e-out); }
.rankcheck:hover .rankcheck-tick { border-color: var(--gold); }
.rankcheck.on .rankcheck-tick { background: linear-gradient(180deg, var(--gold-lt), var(--gold) 60%, var(--gold-dk));
  border-color: var(--gilt-deep); color: var(--gilt-ink); box-shadow: 0 0 8px rgba(var(--gold-rgb), 0.5); }

.menubtn {
  padding: clamp(7px, 1.2vh, 11px) clamp(15px, 2vw, 24px); font-size: clamp(12px, 1.5vh, 15px);
  color: var(--ink); font-family: inherit; cursor: pointer;
  background: var(--plate);
  border: 1px solid var(--edge); border-radius: var(--r-lg); transition: border-color 0.12s, transform 0.08s;
}
.menubtn:hover { border-color: var(--gold-lt); transform: translateY(-2px); }
/* ---- THE HOME ROW: SEVEN EQUAL DOORS ------------------------------------
   `.menubtn` is a shared class — the guild hall's sidebar, the search screen's
   cancel button and every confirm dialog use it too, all as a plain horizontal
   pill — so the tile format below is scoped to `.menurow` rather than rewriting
   the class itself. A grid, not a flex row of intrinsic widths: the whole point
   is that "Deeds" and "Challenge Grid" come out the same size. Seven columns on
   a desktop-width menu; narrower breakpoints re-column it (see the 620px query). */
.menurow {
  display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 10px;
  margin-top: clamp(9px, 2.1vh, 24px);
}
.menurow .menubtn {
  position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px;
  min-height: 64px; padding: clamp(8px, 1.2vh, 12px) 6px; font-size: clamp(11px, 1.3vh, 12.5px);
  text-align: center;
}
.menurow .menubtn .mb-ic { display: flex; width: 20px; height: 20px; }
.menurow .menubtn .mb-ic svg { width: 100%; height: 100%; }
.menurow .menubtn .mb-tx { line-height: 1.2; letter-spacing: 0.2px; }
/* THE COUNT IS A CORNER CHIP, NOT PART OF THE LABEL. It used to sit inline
   after the word, which made whichever tile happened to be counting anything
   wider than its neighbours — "Challenge Grid 100" beside a bare "Deeds".
   Pinned to the corner instead, it can say the same number without being
   able to change the tile's shape. */
.menurow .menubtn .navbadge { position: absolute; top: -7px; right: -7px; margin-left: 0; }

/* ---- shared screen header ---- */
/* The header takes the same measure as the body under it (see `--screenw`), so Back,
   the title and the purse stand over the panel they belong to rather than at the far
   edges of a 2560px desk with the content stranded in the middle. */
.scrhdr { display: flex; align-items: center; gap: 14px; padding: 16px 22px;
  width: 100%; max-width: var(--screenw); margin-inline: auto; }
.backbtn { padding: 9px 16px; font-family: inherit; color: var(--ink); cursor: pointer; background: var(--plate); border: 1px solid var(--edge); border-radius: var(--r-lg); }
.backbtn:hover { border-color: var(--gold-lt); }
.scrtitle { flex: 1; font-size: 26px; font-weight: 800; letter-spacing: 2px; color: var(--gold-lt); }
/* Buy Gold sits immediately left of the header purse, exactly as it does beside the
   home screen's — `align-items` so the button and the chip line up on their centres
   rather than the button stretching to the header's height. */
.hdrright { display: flex; align-items: center; gap: 10px; }
.hdrright .buygold { margin-right: 0; }

/* ---- mode select ----
   `min-height: 0` on every one of these flex-child scrollers is load-bearing: without
   it a flex item cannot shrink below its content, so the panel stayed content-tall,
   the screen behind it overflowed by ~12px, and you got a second scrollbar outside
   the real one — two scrollbars, and the outer one moving the header off the top. */
.modegrid { flex: 1; min-height: 0; display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; padding: 6px 26px 26px; align-content: start; overflow: auto; }
.modecard {
  text-align: left; padding: 20px; cursor: pointer; color: var(--ink); font-family: inherit;
  background: linear-gradient(160deg, rgba(36,30,22,0.92), rgba(16,12,8,0.95));
  border: 1px solid var(--edge); border-radius: var(--r-2xl);
  transition: transform 0.1s, border-color 0.14s, box-shadow 0.14s;
}
.modecard:hover { transform: translateY(-4px); border-color: var(--gold-lt); box-shadow: var(--sh-4), 0 0 22px rgba(var(--gold-rgb),0.18); }
.micon { font-size: 38px; }
.mname { font-size: 22px; font-weight: 800; margin-top: 6px; }
.mtag { display: inline-block; font-size: 11px; letter-spacing: 1px; color: var(--gilt-ink); background: var(--gold); border-radius: var(--r-sm); padding: 2px 8px; margin: 7px 0; font-weight: 700; }
.mdesc { color: var(--muted); font-size: 13px; line-height: 1.5; min-height: 56px; }
.mplay { margin-top: 10px; color: var(--gold-lt); font-weight: 700; display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.mseats { font-size: 11px; font-weight: 600; color: var(--muted); letter-spacing: 0.4px; }
/* which battle this muster is FOR — the kingdom you want for a duel is not always
   the kingdom you want holding a lane on a warfront */
.mustermode { text-align: center; font-size: 12.5px; color: var(--muted); letter-spacing: 0.4px; padding: 0 20px 8px; }
.mustermode b { color: var(--gold-lt); font-family: var(--font-display); letter-spacing: 0.6px; }
@media (max-width: 820px) { .modegrid { grid-template-columns: repeat(2, 1fr); } }

/* ---- matchmaking ---- */
#scr-search { align-items: center; justify-content: center; }
.searchbox { text-align: center; padding: 40px; }
.spinner { width: 72px; height: 72px; margin: 0 auto 26px; border: 5px solid rgba(var(--gold-rgb),0.18); border-top-color: var(--gold); border-radius: 50%; animation: spin 0.9s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.searchtitle { font-size: 30px; font-weight: 800; letter-spacing: 5px; color: var(--gold-lt); }
.searchmode { font-size: 18px; color: var(--ink); margin: 12px 0; }
.searchstatus { color: var(--muted); font-size: 15px; margin-bottom: 26px; min-height: 22px; }
.searchstatus b { color: var(--gold-lt); }
.searchbtns { display: flex; gap: 12px; justify-content: center; }
.menubtn.primary { color: var(--gilt-ink); background: linear-gradient(180deg, var(--gold-lt), var(--gold) 60%, var(--gold-dk)); border-color: var(--gilt-deep); font-weight: 700; }
.menubtn.primary:hover { filter: brightness(1.08); }

/* ---- armory ---- */
.armgrid { flex: 1; min-height: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(148px, 1fr)); gap: 12px; padding: 6px 26px 26px; overflow: auto; align-content: start; }
.armcard { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 12px 8px; position: relative; background: var(--plate); border: 1px solid var(--edge); border-radius: var(--r-xl); }
.armcard.owned { border-color: rgba(95,211,95,0.42); }
.armcard.level { opacity: 0.72; }
.armicon { width: 64px; height: 64px; filter: drop-shadow(0 3px 4px rgba(0,0,0,0.5)); }
.armcard.level .armicon, .armcard.gold .armicon { filter: grayscale(0.7) brightness(0.65); }
.aname { font-weight: 700; font-size: 14px; }
.acat { font-size: 11px; color: var(--muted); }
.afoot { margin-top: 4px; min-height: 28px; display: flex; align-items: center; }
.owned { color: var(--ready); font-weight: 700; font-size: 13px; }
.locked { color: var(--muted); font-size: 12px; }
.buybtn { padding: 6px 14px; font-size: 13px; font-weight: 700; font-family: inherit; color: var(--gilt-ink); cursor: pointer; background: linear-gradient(180deg, var(--gold-lt), var(--gold)); border: 1px solid var(--gilt-deep); border-radius: var(--r-lg); }
.buybtn:hover:not(:disabled) { filter: brightness(1.08); }
.buybtn.dim, .buybtn:disabled { background: #3a352a; color: #8a8270; border-color: #2a2620; cursor: not-allowed; }

/* ---- profile ---- */
/* A centred column, like every other screen — see the measure block at the foot of
   this file. `--screenw` is the shared one so the record does not sit at a width no
   other screen uses. */
.profbody { flex: 1; min-height: 0; padding: 10px 26px 26px; overflow: auto; text-align: center;
  width: 100%; max-width: var(--screenw); margin-inline: auto; }
/* Whose record this is, over the top of it. The page is reached by clicking your own
   name, and a page you opened by name should say the name back. */
.pf-who { margin: 6px auto 2px; }
.pf-name { font-family: var(--font-display); font-size: clamp(22px, 3.4vw, 34px); font-weight: 900;
  letter-spacing: 1px; color: var(--gold-lt); line-height: 1.1; }
.pf-title { font-size: 12.5px; font-style: italic; color: var(--muted); margin-top: 3px; }
/* The treasury, where the level circle used to be. Gold is the only progression
   there is now, so it gets the headline. */
.purse { max-width: 430px; margin: 18px auto 24px; padding: 18px 22px; border-radius: var(--r-2xl);
  background: radial-gradient(circle at 50% 20%, rgba(58,47,29,0.9), rgba(18,14,9,0.9));
  border: 2px solid var(--gold-dk); box-shadow: 0 0 32px rgba(var(--gold-rgb),0.18); }
.purseamt { font-size: clamp(34px, 5vw, 46px); font-weight: 800; color: var(--gold-lt); line-height: 1.05; }
.purselabel { font-size: 12px; letter-spacing: 3px; color: var(--muted); margin-top: 2px; }
/* `.pursesub` is gone — the treasury is a number, not a number with a motto under
   it. See buildProfile() in ui.js. */
.statgrid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; max-width: 580px; margin: 0 auto; }
.stat { background: var(--plate); border: 1px solid var(--edge); border-radius: var(--r-lg); padding: 16px; }
.stat b { display: block; font-size: 24px; color: var(--gold-lt); }
.stat span { font-size: 12px; color: var(--muted); }

/* ---- results ---- */
#scr-results { align-items: center; justify-content: center; }
/* THE PROCLAMATION. Two colourways off one skeleton — `--vc` is the verdict's colour
   and every ornament on the card reads from it, so victory is gold and hearth-warm and
   defeat is cold iron and ash without either being a different (or lesser) screen. */
.resultbox {
  --vc: var(--gilt-lit); --vc-dk: #a87f27; --vc-glow: 255,190,80;
  position: relative; overflow: hidden; text-align: center;
  padding: 26px 46px 28px; width: min(520px, 92vw);
  background: linear-gradient(168deg, rgba(46,37,23,0.98), rgba(16,12,8,0.99));
  border: 1px solid var(--edge); border-radius: var(--r-2xl);
  box-shadow: var(--sh-4), 0 0 60px rgba(var(--vc-glow), 0.12), inset 0 1px 0 rgba(255,236,190,0.14);
}
.resultbox.lose { --vc: #c9d2dc; --vc-dk: #5d6772; --vc-glow: 150,168,188;
  background: linear-gradient(168deg, rgba(31,32,38,0.98), rgba(11,11,14,0.99)); }
/* a slow shaft of light behind the crest — sunlight through the smoke after a field is
   taken, and a colder, slower version of the same when it is lost */
/* LIGHT HAS A SOURCE, AND IT DOES NOT PINWHEEL. What was here read as floating
   geometry rather than light, for three reasons worth naming because each is easy
   to repeat:

   · The cone was struck `at 50% 0` — apex at the top — but the element rotated
     about its own CENTRE, a third of the way down. Pivot and apex disagreed, so
     the whole fan swung bodily sideways across the card. That single mismatch is
     most of why it looked like shapes drifting past rather than beams.
   · Every stop was hard: transparent to 0.28 and back inside twelve degrees, under
     3px of blur. A hard-edged wedge is a triangle. Light has no edges.
   · It turned a full 360°, so rays swept out sideways and eventually pointed UP.

   So: the apex is the pivot, the stops are feathered over their whole width, and
   the fan only breathes — a few degrees each way, two layers on different clocks
   crossing each other, which is what makes shafts of light shimmer instead of
   spin. A mask fades them out with distance, because a real shaft dissolves in
   the air it is travelling through rather than stopping at the edge of a box. */
.resrays { position: absolute; top: -6%; left: 50%; width: 190%; height: 78%;
  transform: translateX(-50%); transform-origin: 50% 0;      /* pivot AT the light source */
  pointer-events: none; z-index: 0; opacity: 0.42; filter: blur(14px);
  /* fade down the shaft, and in from the sides, so nothing ever meets an edge */
  -webkit-mask-image: radial-gradient(120% 100% at 50% 0, #000 12%, rgba(0,0,0,0.55) 46%, transparent 82%);
  mask-image: radial-gradient(120% 100% at 50% 0, #000 12%, rgba(0,0,0,0.55) 46%, transparent 82%);
}
/* two fans, counter-swaying: one broad and dim, one narrower and brighter */
.resrays::before, .resrays::after {
  content: ''; position: absolute; inset: 0; transform-origin: 50% 0;
}
.resrays::before {
  background: conic-gradient(from 208deg at 50% 0,
    transparent 0deg, rgba(var(--vc-glow),0.05) 6deg, rgba(var(--vc-glow),0.22) 15deg, rgba(var(--vc-glow),0.05) 24deg, transparent 30deg,
    transparent 46deg, rgba(var(--vc-glow),0.04) 52deg, rgba(var(--vc-glow),0.16) 62deg, rgba(var(--vc-glow),0.04) 72deg, transparent 78deg,
    transparent 96deg, rgba(var(--vc-glow),0.05) 102deg, rgba(var(--vc-glow),0.2) 112deg, rgba(var(--vc-glow),0.05) 122deg, transparent 128deg);
  animation: resRaySwayA 19s ease-in-out infinite;
}
.resrays::after {
  background: conic-gradient(from 214deg at 50% 0,
    transparent 0deg, rgba(var(--vc-glow),0.03) 10deg, rgba(var(--vc-glow),0.13) 21deg, rgba(var(--vc-glow),0.03) 32deg, transparent 40deg,
    transparent 68deg, rgba(var(--vc-glow),0.03) 76deg, rgba(var(--vc-glow),0.15) 88deg, rgba(var(--vc-glow),0.03) 100deg, transparent 108deg);
  animation: resRaySwayB 27s ease-in-out infinite;
}
/* a few degrees, and a little breath in the brightness — never a full turn */
@keyframes resRaySwayA {
  0%, 100% { transform: rotate(-3.5deg); opacity: 0.75; }
  50%      { transform: rotate(3.5deg);  opacity: 1; }
}
@keyframes resRaySwayB {
  0%, 100% { transform: rotate(2.5deg);  opacity: 1; }
  50%      { transform: rotate(-2.5deg); opacity: 0.7; }
}
/* defeat gets the same light, thinner and colder — smoke rather than sun */
.resultbox.lose .resrays { opacity: 0.26; filter: blur(20px); }

/* WHAT IS STILL IN THE AIR OVER THE FIELD. Embers rising off a won battlefield, and the
   same motes falling as ash over a lost one — the one ornament that says the card is a
   place rather than a panel, and the only thing separating the two colourways that is
   not just a change of hue. Eight of them, drifting on different clocks so no two ever
   line up; `nth-child` sets each one's lane, delay and pace. */
.resmotes { position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
.resmotes i { position: absolute; bottom: -10px; width: 3px; height: 3px; border-radius: 50%;
  background: rgba(var(--vc-glow), 0.9); box-shadow: 0 0 7px rgba(var(--vc-glow), 0.75);
  opacity: 0; animation: resEmber 9s linear infinite; }
.resmotes i:nth-child(1) { left: 8%;  animation-duration: 8.5s;  animation-delay: 0.2s; }
.resmotes i:nth-child(2) { left: 21%; animation-duration: 11.5s; animation-delay: 2.4s; width: 2px; height: 2px; }
.resmotes i:nth-child(3) { left: 34%; animation-duration: 9.8s;  animation-delay: 4.1s; }
.resmotes i:nth-child(4) { left: 47%; animation-duration: 13s;   animation-delay: 1.1s; width: 2px; height: 2px; }
.resmotes i:nth-child(5) { left: 60%; animation-duration: 8.9s;  animation-delay: 5.6s; }
.resmotes i:nth-child(6) { left: 72%; animation-duration: 12.2s; animation-delay: 3.0s; width: 2px; height: 2px; }
.resmotes i:nth-child(7) { left: 85%; animation-duration: 10.4s; animation-delay: 6.8s; }
.resmotes i:nth-child(8) { left: 94%; animation-duration: 14s;   animation-delay: 0.9s; width: 2px; height: 2px; }
@keyframes resEmber {
  0%   { opacity: 0;    transform: translate(0, 0) scale(0.7); }
  12%  { opacity: 0.85; }
  70%  { opacity: 0.5; }
  100% { opacity: 0;    transform: translate(14px, -320px) scale(1.1); }
}
/* defeat: the same motes, falling, colder and slower — ash, not embers */
.resultbox.lose .resmotes i { bottom: auto; top: -10px; box-shadow: 0 0 5px rgba(var(--vc-glow), 0.4); }
@keyframes resAsh {
  0%   { opacity: 0;   transform: translate(0, 0) scale(0.9); }
  14%  { opacity: 0.5; }
  100% { opacity: 0;   transform: translate(-16px, 330px) scale(0.6); }
}
.resultbox.lose .resmotes i { animation-name: resAsh; }

.resultbox > *:not(.resrays):not(.resmotes) { position: relative; z-index: 1; }

/* THE PROCLAMATION IS READ OUT, NOT HANDED OVER. Every block below the verdict rises in
   on its own beat (the delay is written per-element by showResults), so the screen
   assembles in the order a herald would say it — whose result, what happened, what it
   was worth — and the purse lands last, which is where the count-up is waiting. */
.resbanner, .reshead, .resrule, .resblurb, .resmode, .rstat, .respurse, .levelup, .rescareer,
.resultbox .bigbtn { animation: resRise 0.5s var(--e-out) both; }
@keyframes resRise { from { opacity: 0; transform: translateY(11px); } to { opacity: 1; transform: none; } }

/* your own crest, crowned — the first thing the screen says is WHOSE result this is */
.resbanner { display: flex; flex-direction: column; align-items: center; gap: 3px; }
/* WHO THIS WAS. The herald's proclamation names the commander and the house
   under the kingdom's crest — the arms a commander designed in the Forge,
   finally on the screen that exists to make them feel good about a battle. */
.resid { margin-top: 4px; }
.resid .cmark { justify-content: center; }
.resid .cm-nm { font-family: var(--font-display); letter-spacing: 0.6px; }
.rescrest { width: 66px; height: auto;
  filter: drop-shadow(0 4px 12px rgba(0,0,0,0.8)) drop-shadow(0 0 16px rgba(var(--vc-glow),0.45));
  animation: crestFloat 5s ease-in-out infinite; }
.reskd { font: 800 11px var(--font-display); letter-spacing: 2.4px; text-transform: uppercase; color: #d8cbaa; }

.resverdict { margin-top: 4px; font-family: var(--font-logo); font-weight: 900;
  font-size: clamp(42px, 9vw, 74px); letter-spacing: 7px; line-height: 1;
  background: linear-gradient(180deg, #fffdf2 0%, var(--vc) 42%, var(--vc-dk) 100%);
  -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; color: transparent;
  filter: drop-shadow(0 3px 10px rgba(0,0,0,0.85)) drop-shadow(0 0 26px rgba(var(--vc-glow),0.55));
  animation: resStrike 0.6s var(--e-back) both; }
@keyframes resStrike { 0% { opacity: 0; transform: scale(1.35); filter: blur(9px); } 100% { opacity: 1; transform: scale(1); filter: blur(0); } }
.resrule { display: flex; align-items: center; justify-content: center; gap: 10px; margin: 7px auto 9px; width: 78%; }
.resrule i { flex: 1; height: 1px; background: linear-gradient(90deg, transparent, rgba(var(--vc-glow),0.75)); }
.resrule i:last-child { background: linear-gradient(90deg, rgba(var(--vc-glow),0.75), transparent); }
.resrule b { flex: none; font-size: 14px; color: var(--vc); text-shadow: 0 0 10px rgba(var(--vc-glow),0.8); }

/* THE HEADLINE, on the rule the ornament used to sit on. One battle, one thing said
   about it in the verdict's own colour — the rule was carrying a ⚜ that says nothing
   the word above it has not already said, and this is the same shape doing a job. */
.reshead { display: flex; align-items: center; justify-content: center; gap: 11px;
  margin: 8px auto 10px; width: 86%; }
.reshead i { flex: 1; height: 1px; background: linear-gradient(90deg, transparent, rgba(var(--vc-glow),0.7)); }
.reshead i:last-child { background: linear-gradient(90deg, rgba(var(--vc-glow),0.7), transparent); }
.reshead b { flex: none; font: 800 12px var(--font-display); letter-spacing: 2.2px; text-transform: uppercase;
  white-space: nowrap; color: var(--vc); text-shadow: 0 0 14px rgba(var(--vc-glow),0.55), 0 2px 4px rgba(0,0,0,0.8); }
.resblurb { font-family: var(--font-flavor); font-style: italic; font-size: 14.5px; line-height: 1.4;
  color: #e4d8ba; margin: 0 auto; max-width: 400px; }
.resmode { margin-top: 6px; font: 700 10.5px var(--font-display); letter-spacing: 1.6px;
  text-transform: uppercase; color: var(--muted); }

/* THE TALLY, AS MEDALLIONS. It was a definition list whose first row restated the word
   printed above it in sixty-four point. These are the numbers you actually earned. */
.resspoils { display: flex; justify-content: center; flex-wrap: wrap; gap: 10px; margin: 18px 0 16px; }
.rstat { flex: 1 1 96px; max-width: 132px; display: flex; flex-direction: column; align-items: center; gap: 1px;
  padding: 10px 8px 9px; border-radius: var(--r-xl);
  background: linear-gradient(180deg, rgba(255,255,255,0.055), rgba(0,0,0,0.3));
  border: 1px solid rgba(var(--vc-glow), 0.28); box-shadow: inset 0 1px 0 rgba(255,255,255,0.08); }
.rstat i { font-style: normal; font-size: 16px; line-height: 1; opacity: 0.9;
  filter: drop-shadow(0 0 7px rgba(var(--vc-glow),0.5)); }
.rstat b { font: 800 27px var(--font-display); color: #fff6de; line-height: 1.06;
  font-variant-numeric: var(--nums); text-shadow: 0 2px 8px rgba(0,0,0,0.7); }
.rstat span { font: 700 9px var(--font-ui); letter-spacing: 0.9px; text-transform: uppercase; color: var(--muted); }

/* THE PURSE, AS A PRIZE. A 30px number on a bare background was the least ceremonious
   thing on a screen whose whole job is ceremony. */
.respurse { position: relative; margin: 0 auto; padding: 13px 18px 12px; border-radius: var(--r-2xl); width: fit-content; min-width: 210px;
  background: radial-gradient(120% 130% at 50% 0, rgba(var(--vc-glow),0.2), rgba(0,0,0,0.34) 70%);
  border: 1px solid rgba(var(--vc-glow), 0.45);
  box-shadow: var(--sh-3), inset 0 1px 0 rgba(255,255,255,0.12); }
.rpttl { font: 800 10px var(--font-display); letter-spacing: 2px; text-transform: uppercase; color: var(--vc); }
/* THE COIN RUNS UP. Fixed cells (`tabular-nums`) and a min-width wide enough for the
   figure it is heading toward, because a counter that reflows on every digit it gains
   makes the box breathe in and out for a second and a half — the one moment on the
   screen that has to feel solid. It brightens when it lands. */
.rpnum { margin: 2px 0 1px; font: 900 40px var(--font-display); line-height: 1.02;
  font-variant-numeric: var(--nums); min-width: 5ch;
  background: linear-gradient(180deg, #fffdf2, var(--gold-hi) 45%, var(--gold));
  -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
  filter: drop-shadow(0 2px 8px rgba(0,0,0,0.7)) drop-shadow(0 0 20px rgba(255,200,90,0.5));
  transition: filter 0.35s ease; }
.rpnum.done { filter: drop-shadow(0 2px 8px rgba(0,0,0,0.7)) drop-shadow(0 0 34px rgba(255,210,110,0.85)); }
/* …and the coffer flares as the last coin drops in */
.respurse:has(.rpnum.done) { border-color: rgba(var(--vc-glow), 0.72);
  box-shadow: var(--sh-3), 0 0 34px rgba(var(--vc-glow),0.28), inset 0 1px 0 rgba(255,255,255,0.12); }
/* `.rpsub` — "carried into your next campaign" under the purse — is gone. It was
   true of every number this box has ever shown, so it said nothing the coin had
   not already said, directly under the one figure the screen most wants read. */

/* one quiet line placing this battle in a career, under everything else */
.rescareer { margin-top: 14px; font: 600 10.5px var(--font-ui); letter-spacing: 0.9px;
  text-transform: uppercase; color: #8c8067; }

.levelup { margin: 16px 0 4px; padding: 12px 13px; background: rgba(var(--ready-rgb),0.09);
  border: 1px solid rgba(var(--ready-rgb),0.34); border-radius: var(--r-xl);
  animation: pulse 1.9s ease-in-out infinite; }
.luttl { font: 800 11px var(--font-display); letter-spacing: 1.2px; text-transform: uppercase; color: var(--ready); }
.newunits { display: flex; flex-wrap: wrap; justify-content: center; align-items: flex-end; gap: 7px; margin-top: 9px; }
/* THE PORTRAIT, NOT THE WORD. A commander who has fought this kingdom all match
   knows a Berserker on sight — reading the word "Berserker" is slower than
   recognising it, and the canvas here is the same one the Armory card and the
   muster chip already draw it with, so it is a face already met, not a new one.
   Same bones as those chips: art on top, the name clipped to two lines under
   it, and a small hover lift because a fresh unlock is the one thing on this
   screen actually worth a second look. */
.nutroop { display: flex; flex-direction: column; align-items: center; gap: 2px;
  width: 46px; padding: 3px 2px 2px; border-radius: var(--r-lg); cursor: default;
  background: rgba(0,0,0,0.28); border: 1px solid rgba(var(--ready-rgb),0.28);
  transition: transform 0.12s var(--e-back), border-color 0.14s, background 0.14s; }
.nutroop:hover { transform: translateY(-2px); background: rgba(var(--ready-rgb),0.11); border-color: rgba(var(--ready-rgb),0.6); }
.nutroop canvas { width: 34px; height: 34px; filter: drop-shadow(0 2px 3px rgba(0,0,0,0.5)); }
.nutroop i { font: 600 8.5px 'Spectral', serif; font-style: normal; color: #cdeed9;
  text-align: center; line-height: 1.14; width: 100%; overflow-wrap: break-word; hyphens: auto;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.lumore { align-self: center; font: 700 11.5px var(--font-ui); color: var(--ready); padding: 2.5px 4px; }
.resultbox .bigbtn { margin-top: 18px; width: 100%; }
@keyframes pulse { 0%, 100% { box-shadow: 0 0 0 rgba(var(--ready-rgb),0); } 50% { box-shadow: 0 0 26px rgba(var(--ready-rgb),0.34); } }

/* ---- in-match additions ---- */
#factions { margin-top: 42px; }
/* third column of the topbar grid — see #topbar. It is placed by the grid, not by
   coordinates, so the column it occupies is what reserves the scorecard's clearance
   on the right; giving it back an absolute position collapses that column to nothing
   and the centre drifts. */
/* `min-height` for the same reason the corner chips carry one: the topbar is drawn
   through `zoom: var(--ui)`, so 8px of padding around a 13px word measured 23px tall
   on a 1024×640 window. Concede-the-match is not a control to make small by accident,
   and on a coarse pointer it has to be hittable without hitting the scorecard. */
#leavebtn { justify-self: end; z-index: 6; padding: 8px 14px; font-family: inherit; cursor: pointer; color: #ecc4bc; background: linear-gradient(160deg, rgba(58,28,28,0.92), rgba(28,14,14,0.95)); border: 1px solid rgba(200,90,80,0.4); border-radius: var(--r-lg);
  min-height: max(32px, calc(32px / var(--ui))); }
/* `min-height` for the same reason the corner chips carry one: the topbar is drawn
   through `zoom: var(--ui)`, so 8px of padding around a 13px word measured 23px tall
   on a 1024×640 window. Concede-the-match is not a control to make small by accident,
   and on a coarse pointer it has to be hittable without hitting the scorecard.
   NOT ON A PHONE, though: there this is one chip in a row of four on the top band,
   and a taller Surrender than the three switches beside it is a broken row, not a
   safer button. The phone's own band already sizes every one of them to a target. */
@media (pointer: coarse) { html:not(.phone) #leavebtn { min-height: max(32px, calc(44px / var(--ui))); } }
#leavebtn:hover { border-color: #e07a6a; }
/* armed: the second press is the one that concedes, so it should not look like the first */
#leavebtn.arm { color: #fff0ec; background: linear-gradient(160deg, rgba(150,42,36,0.96), rgba(96,22,20,0.97));
  border-color: #ff8f7d; box-shadow: 0 0 0 1px rgba(255,143,125,0.35), 0 0 16px rgba(224,110,90,0.5); }
#leavebtn:disabled { opacity: 0.65; cursor: default; }

/* ---- richer build tooltip ---- */
#tip { width: 196px; }
.tpow { display: flex; flex-wrap: wrap; gap: 3px 5px; margin: 6px 0 2px; }
.tpow span { font-size: 10.5px; color: #d7e0ea; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.09); border-radius: var(--r-sm); padding: 1px 5px; font-family: var(--font-ui); }
.tcounters { display: flex; flex-direction: column; gap: 2px; margin: 7px 0 2px; font-size: 11px; font-family: var(--font-ui); }
.tstrong { color: var(--ready); } .tweak { color: #e8a08f; }
.tupg { margin-top: 7px; padding-top: 6px; border-top: 1px solid rgba(var(--gold-rgb),0.25); font-size: 11px; color: var(--gold-lt); font-family: var(--font-ui); }
.tupg b { color: var(--ready); }

/* ---- AUTO: buy this spawner's next level the moment the gold is there ----
   Sits beside the manual Upgrade button wherever one is offered, so the choice always
   reads as "buy it now, or have it bought for me". Small and quiet when off; plainly
   lit when armed, because standing money is being spent on your behalf. */
.autobox { flex: none; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px;
  width: 44px; padding: 5px 2px; border-radius: var(--r-md); cursor: pointer; user-select: none;
  background: rgba(0,0,0,0.3); border: 1px solid rgba(var(--gold-rgb),0.28);
  transition: border-color 0.14s, background 0.14s, box-shadow 0.14s; }
.autobox:hover { border-color: var(--gold-lt); background: rgba(255,255,255,0.05); }
.autobox input { position: absolute; opacity: 0; width: 0; height: 0; }
.autobox i { display: block; width: 15px; height: 15px; border-radius: var(--r-sm); font-style: normal;
  font: 800 10px var(--font-display); line-height: 15px; text-align: center; color: transparent;
  background: rgba(0,0,0,0.45); border: 1px solid rgba(var(--gold-rgb),0.45); transition: 0.14s; }
.autobox span { font: 800 8.5px var(--font-display); letter-spacing: 0.5px; color: #9a8c6e; }
.autobox.on { border-color: rgba(var(--ready-rgb),0.6); background: rgba(90,200,130,0.12);
  box-shadow: 0 0 10px rgba(110,220,150,0.25); }
.autobox.on i { color: #08170d; background: linear-gradient(180deg, var(--ready), var(--ready-dk)); border-color: rgba(8,23,13,0.5); }
.autobox.on span { color: var(--ready); }

/* ---- selected-units panel ---- */
#selpanel {
  position: absolute; left: calc(64px + var(--sal) / var(--ui)); bottom: calc(var(--rackh, 200px) + 20px); max-width: 340px;
  zoom: var(--ui);
  background: var(--plate);
  border: 1px solid var(--edge); border-radius: var(--r-lg); padding: 8px 10px;
  box-shadow: var(--sh-3); backdrop-filter: blur(3px);
}
/* ---- the spawner widget: a control, not a curtain ----------------------
   It floats on the battlefield directly over the building you clicked, so every
   pixel of it is ground you cannot see. It used to be a five-block stack — title,
   pip row, HP line, a sentence explaining right-click, then the button — deep
   enough to blank the fight around the spawner it belonged to. Now it is one
   narrow strip, and you can see through it: translucent enough to read the battle
   underneath, and it steps down to a whisper while the pointer is elsewhere so it
   never sits opaque over a fight you are trying to watch. */
/* ---- AND YOU CAN MARCH THROUGH IT ------------------------------------
   The widget sits directly over the spawner it belongs to, which is exactly
   where a commander then clicks to send that spawner's troops — so the one
   control that opens the moment you select a building was standing on the one
   patch of ground you select a building in order to click. The march order
   went into the panel and nothing happened, and the only way to give it was to
   scroll the field so the building moved out from under its own widget.
   Nothing about the panel needs to catch a click except its BUTTONS: the name,
   the pips, the health and the cooldown bar are all read-only. So the plate is
   transparent to the pointer and the controls on it are not, and a march point
   can be dropped straight through the middle of it.
   (:hover still lights the panel up, because hovering a child that does take
   the pointer matches the parent — so it solidifies exactly when you reach for
   a control on it, which is the only time it needed to.) */
#selpanel.atworld {
  bottom: auto; max-width: 214px; min-width: 178px; transform: translate(-50%, -100%);
  pointer-events: none; z-index: 22; padding: 5px 7px 6px;
  background: linear-gradient(160deg, rgba(42,34,22,0.72), rgba(20,16,10,0.76));
  border-color: rgba(var(--gold-rgb),0.42); box-shadow: var(--sh-3);
  backdrop-filter: blur(2px);
  opacity: 0.82; transition: opacity 0.14s ease;
}
/* Its own entrance. The generic panel fade-in animates TRANSFORM, and this widget
   uses transform to sit itself over the building — so the shared animation yanked
   it off its anchor for a sixth of a second on every open, and finished at full
   opacity before snapping back to translucent. This one only touches opacity, and
   lands on the value the widget actually keeps. */
#selpanel.atworld:not(.hidden) { animation: bpIn 0.14s ease-out; }
@keyframes bpIn { from { opacity: 0; } to { opacity: 0.82; } }
/* Everything that can be operated stays operable. `label` matters as much as
   `button` here: the auto-upgrade toggles are checkboxes wrapped in labels, and
   a label whose pointer-events are off is a tick-box you can only hit dead
   centre on the box itself. */
#selpanel.atworld button, #selpanel.atworld a, #selpanel.atworld input,
#selpanel.atworld label, #selpanel.atworld select { pointer-events: auto; }
#selpanel.atworld:hover { opacity: 1; background: linear-gradient(160deg, rgba(44,36,23,0.94), rgba(20,16,10,0.96)); }
/* ---- THE WAY OFF THE FIELD PANEL ------------------------------------------
   `#selpanel.atworld` is pointer-events:none with everything operable opted back
   in above, so this button is already clickable by the rule that covers the
   Upgrade button beside it — and the panel's body still lets an order through to
   the ground underneath, which is the whole reason it is translucent.

   Only the world-anchored headers go flex. The plain #selpanel header is also
   worn by the multi-unit selection strip, which lays out its own aggregate span
   and is not what anybody is trying to shut. */
#selpanel.atworld .selhdr { display: flex; align-items: center; }
#selpanel .sp-x {
  margin-left: auto; flex: none; position: relative;
  width: 18px; height: 18px; padding: 0; display: flex;
  align-items: center; justify-content: center;
  font: 700 10px var(--font-ui); line-height: 1; cursor: pointer;
  color: var(--gilt-body); background: rgba(0,0,0,0.3);
  border: 1px solid rgba(var(--gold-rgb),0.34); border-radius: 50%;
}
#selpanel .sp-x:hover { color: var(--gold-bright); background: rgba(0,0,0,0.55);
  border-color: rgba(var(--gold-rgb),0.72); }
#selpanel .sp-x:active { transform: scale(0.92); }
/* 18px is a mouse target; the thumb gets the corner through a transparent
   outset, which brings the real reach to 44px without moving the strip. */
html.touch #selpanel .sp-x::after { content: ''; position: absolute; inset: -13px; }
/* one strip: name, level pips and health on a single line */
.bp-hdr { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
.bp-nm { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font: 700 11px var(--font-display, 'Cinzel', serif); color: var(--gold-lt); }
.bp-pips { flex: none; display: flex; gap: 2px; }
.bp-pip { width: 5px; height: 5px; border-radius: 50%; background: rgba(255,255,255,0.16); }
.bp-pip.on { background: var(--gold); box-shadow: 0 0 4px rgba(var(--gold-rgb),0.8); }
.bp-hp { flex: none; font: 700 10px 'Spectral', serif; font-variant-numeric: var(--nums); }
/* the action row */
/* THE MUSTER CLOCK. A spawner's whole job is a countdown and the panel never showed
   it — you were told the interval and left to guess where in it you were standing.
   A thin sweep under the header, with the seconds left printed on it, so "is one
   about to walk out of here" is answered before you decide to spend. */
.bp-cd { position: relative; height: 9px; margin: 0 0 5px; border-radius: var(--r-sm);
  background: #100e0a; box-shadow: inset 0 1px 2px rgba(0,0,0,0.7); overflow: hidden; }
.bp-cd i { display: block; height: 100%; width: 0%; border-radius: var(--r-sm) 0 0 var(--r-sm);
  background: linear-gradient(90deg, #6d5a2a, var(--gold) 70%, var(--gold-hi));
  transition: width 0.09s linear; }
.bp-cd s { position: absolute; inset: 0; text-decoration: none; text-align: center;
  font: 800 8px var(--font-ui); letter-spacing: 0.5px; line-height: 9px; color: #f6ecd0;
  text-shadow: 0 1px 2px rgba(0,0,0,0.95); font-variant-numeric: var(--nums); }
/* about to pop — the one moment the bar is worth looking at */
.bp-cd.near i { background: linear-gradient(90deg, #b9932f, var(--gold-hi) 60%, #fffdf2);
  box-shadow: 0 0 8px rgba(var(--gilt-lit-rgb),0.55); }

.bp-act { display: flex; align-items: center; gap: 6px; }
.bp-wait { flex: 1; color: #d6b478; font: 600 11px 'Spectral', serif; }
.bp-max { flex: 1; color: var(--gold); font: 700 10.5px var(--font-display, 'Cinzel', serif); }
.bpupg { flex: 1; cursor: pointer; border: 1px solid var(--gold); border-radius: var(--r-md); padding: 5px 7px;
  font: 700 10.5px var(--font-display, 'Cinzel', serif); color: #1a140c; text-align: left; line-height: 1.25;
  background: linear-gradient(180deg, var(--gold-lt), var(--gold)); }
.bpupg span { display: block; font-weight: 600; font-size: 9px; opacity: 0.82; }
.bpupg.poor { border-color: rgba(var(--gold-rgb),0.3); color: #9a8c6e; background: rgba(60,52,38,0.55); }
/* the march point, shown only when there is one — and it is its own clear button */
.bp-route { display: flex; align-items: center; justify-content: center; gap: 5px; margin-top: 4px;
  font: 600 9.5px 'Spectral', serif; color: #cfe0a0; }
.bp-route .bprclr { cursor: pointer; border: 1px solid rgba(var(--gold-rgb),0.4); border-radius: var(--r-sm);
  background: rgba(60,52,38,0.6); color: var(--ink); font: 700 9px var(--font-display, 'Cinzel', serif); padding: 0 5px; }
#selpanel.atworld::after {   /* little pointer toward the building */
  content: ''; position: absolute; left: 50%; bottom: -7px; width: 12px; height: 12px;
  transform: translateX(-50%) rotate(45deg);
  background: var(--panel-2); border-right: 1px solid var(--edge); border-bottom: 1px solid var(--edge);
}
.selhdr { font-size: 12px; color: var(--gold-lt); font-weight: 700; margin-bottom: 5px; letter-spacing: 0.5px; }
.selagg { margin-left: 6px; color: var(--muted); font-weight: 600; font-family: var(--font-ui); font-variant-numeric: var(--nums); }
.selicons { display: flex; flex-wrap: wrap; gap: 6px; }
.selchip { position: relative; display: inline-flex; }
.selchip canvas { width: 30px; height: 30px; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.6)); }
.selchip b { position: absolute; right: -3px; bottom: -3px; font-size: 10px; color: #fff; background: rgba(10,10,12,0.88); border-radius: var(--r-md); padding: 0 3px; font-variant-numeric: var(--nums); }
.selmore { align-self: center; font-size: 11px; color: var(--muted); }

/* ---- menu race showcase: the seven banners ----
   A kingdom's card IS its banner — cloth hung off a rod, cut to a swallowtail, dyed
   in that kingdom's own colour, with its crest at the head and three of its actual
   troops mustered at the foot. The host is the point. A name and a blurb tell you a
   kingdom EXISTS; three orcs, three knights or three drakes tell you what fighting
   as one looks like, which is the question this row is actually asking.

   The gold trim follows the swallowtail because the cloth is TWO stacked clips, not
   a border: a border-radius edge stops dead at the notch, and the one thing a banner
   cannot have is a hem that gives up halfway down. The inner clip is inset 2px and
   cut with the same polygon, which on a notch this shallow reads as an even hem to
   within a fifth of a pixel. */
/* ---- TEN BANNERS, ONE RANK -------------------------------------------------
   A wrapping flex row put seven up and hung the last three underneath, which read
   as two classes of kingdom rather than one rank of ten. A GRID of ten equal
   columns is the thing that cannot wrap: `minmax(0, 1fr)` lets every banner give
   ground together as the window narrows instead of one of them falling off the
   end, so the rank stays a rank all the way down.
   It also has to be WIDER than the column it sits in — ten banners do not fit in
   the 1040px the lockup and the nav row want — so it breaks out of `.menucenter`
   and re-centres itself on the viewport. `stretch`, so every banner hangs to the
   same hem whatever its blurb wraps to; ragged bottoms read as a bug, not cloth. */
.racerow {
  display: grid; grid-template-columns: repeat(10, minmax(0, 1fr));
  justify-content: center; align-items: stretch;
  gap: clamp(4px, 0.7vw, 13px);
  width: min(1420px, 96vw); margin-left: 50%; transform: translateX(-50%);
  margin-top: clamp(7px, 1.5vh, 18px); margin-bottom: clamp(8px, 1.7vh, 20px);
}

/* Ten banners, not seven. The card takes its width from its grid column now rather
   than stating one, so the rank divides the room it has evenly however wide it is. */
.racecard {
  --notch: 15px;
  position: relative; display: flex; flex-direction: column; align-items: center; gap: 2px;
  width: auto; min-width: 0; flex: none;
  padding: clamp(9px, 1.5vh, 15px) 7px calc(var(--notch) + clamp(4px, 0.8vh, 9px));
  background: none; border: 0; border-radius: 0; overflow: visible;
  font-family: inherit; color: var(--facl, var(--gold-lt)); cursor: pointer;
  filter: drop-shadow(0 11px 17px rgba(0,0,0,0.55));
  transition: transform 0.18s var(--e-out), filter 0.22s;
}
.racecard > * { position: relative; z-index: 3; }
.rc-cloth, .rc-sheen {
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--notch)), 50% 100%, 0 calc(100% - var(--notch)));
}
/* Gold braid with the kingdom's own thread run through the top of it — enough colour
   that seven hems are seven colours, not enough that any of them stops being metal. */
.rc-cloth { position: absolute; inset: 0; z-index: 0;
  background: linear-gradient(180deg, var(--gold-bright), color-mix(in srgb, var(--facl) 62%, var(--gold-lt)) 17%,
    var(--gold) 42%, #b08829 70%, var(--gilt-seat));
  transition: background 0.22s; }
.rc-cloth::before {           /* the field, and the weave in it */
  content: ''; position: absolute; inset: 2px; clip-path: inherit;
  background:
    repeating-linear-gradient(90deg, rgba(0,0,0,0.13) 0 1px, transparent 1px 4px),
    linear-gradient(180deg, rgba(255,255,255,0.16), rgba(255,255,255,0) 26%),
    linear-gradient(168deg, color-mix(in srgb, var(--fac) 44%, #17110a),
      color-mix(in srgb, var(--fac) 19%, #0b0907) 58%, #070605);
}
.rc-cloth::after {            /* the kingdom's own light, pooling at the head */
  content: ''; position: absolute; inset: 2px; clip-path: inherit; opacity: 0.55;
  background: radial-gradient(115% 55% at 50% 0, color-mix(in srgb, var(--fac) 72%, transparent), transparent 70%);
}
.rc-sheen { position: absolute; inset: 2px; z-index: 1; overflow: hidden; pointer-events: none; }
.rc-sheen::before { content: ''; position: absolute; top: -20%; left: -60%; width: 34%; height: 140%;
  background: linear-gradient(100deg, transparent, rgba(255,246,214,0.34), transparent);
  transform: skewX(-18deg); opacity: 0; }
.racecard:hover .rc-sheen::before { animation: rcSheen 0.85s ease-out; }
@keyframes rcSheen { 0% { left: -60%; opacity: 0; } 22% { opacity: 1; } 100% { left: 132%; opacity: 0; } }

/* the rod it hangs from — overhanging both edges, capped with finials */
.rc-rod { position: absolute; z-index: 2; left: -6px; right: -6px; top: -5px; height: 7px; border-radius: var(--r-sm);
  background: linear-gradient(180deg, var(--gold-bright), var(--gold) 46%, var(--gilt-deep));
  box-shadow: var(--sh-1), inset 0 1px 0 rgba(255,255,255,0.55); }
.rc-rod::before, .rc-rod::after { content: ''; position: absolute; top: 50%; width: 9px; height: 9px;
  border-radius: 50%; transform: translateY(-50%);
  background: radial-gradient(circle at 34% 30%, var(--gold-bright), var(--gold) 55%, var(--gilt-seat));
  box-shadow: var(--sh-1); }
.rc-rod::before { left: -6px; }
.rc-rod::after { right: -6px; }

.racecrest { width: clamp(34px, 4.4vh, 50px); height: auto; filter: drop-shadow(0 3px 7px rgba(0,0,0,0.75)); }
/* THE KINGDOM'S COLOUR LIVES IN THE GLOW, NOT IN THE LETTERS. Every banner is a field
   tinted with its own `--fac`, and both text lines were then PAINTED in that same
   colour — red words on red cloth, violet on violet — which is the one pairing
   guaranteed to have no contrast whatever the hue. The identity is still here: the
   braid, the field, the crest and the halo behind each line all carry it. What changed
   is that the words are now near-white and the colour sits behind them, which is how a
   banner has always worked. */
.racename { font-family: var(--font-display); font-weight: 700; text-transform: uppercase;
  font-size: clamp(10px, 1.35vh, 13.5px); line-height: 1.12; letter-spacing: 0.4px;
  color: color-mix(in srgb, var(--facl) 16%, #fff8ea);
  text-shadow: 0 1px 3px rgba(0,0,0,0.95), 0 0 14px color-mix(in srgb, var(--fac) 70%, transparent); }
/* `.rc-style` — the "BRUTAL · FAST · MONSTROUS" caption — is gone from all three places
   it was printed (banner, muster rail, dossier header). Seven kingdoms described in
   three adjectives each converge: ARMOURED appears twice, SWIFT three times, and by the
   fourth banner the line has stopped distinguishing anything. What it sat directly above
   on the banner — this kingdom's own three troops, drawn — says the same thing in a form
   you cannot mistake for another kingdom's. */

/* the muster line: this kingdom's own troops, stood on a dark step so a sprite still
   reads against cloth its own colour */
.rc-host { display: flex; align-items: flex-end; justify-content: center; gap: 1px;
  width: calc(100% + 2px); min-height: clamp(26px, 3.6vh, 40px);
  margin: clamp(3px, 0.7vh, 6px) 0 1px; padding: 2px 0 1px;
  border-top: 1px solid color-mix(in srgb, var(--fac) 42%, transparent);
  border-bottom: 1px solid color-mix(in srgb, var(--fac) 24%, transparent);
  background: linear-gradient(180deg, rgba(0,0,0,0.36), rgba(0,0,0,0.05)); }
/* No entrance on the troops. They used to fade up as each kingdom's bake landed,
   which was only ever an apology for arriving late — now they are cut before the
   banner is on screen, so they are simply standing there when it is. */
.rc-host canvas { width: clamp(24px, 3.4vh, 38px); height: auto;
  filter: drop-shadow(0 2px 3px rgba(0,0,0,0.85)); transition: filter 0.2s; }
.racecard:hover .rc-host canvas {
  filter: drop-shadow(0 2px 3px rgba(0,0,0,0.85)) drop-shadow(0 0 8px color-mix(in srgb, var(--fac) 70%, transparent)); }

.raceblurb { font-family: var(--font-flavor); font-style: italic; text-align: center;
  font-size: clamp(8px, 1.02vh, 10.5px); line-height: 1.25;
  color: color-mix(in srgb, var(--facl) 14%, #ded2b6); text-shadow: 0 1px 2px rgba(0,0,0,0.8); }

/* ---- nav badge (Armory unlock count) ---- */
.navbadge { display: inline-flex; align-items: center; justify-content: center; min-width: 20px; height: 20px; margin-left: 6px; padding: 0 5px; border-radius: var(--r-lg); background: linear-gradient(180deg, var(--ready), var(--ready-dk)); color: #07120b; font-size: 12px; font-weight: 800; box-shadow: 0 0 10px rgba(var(--ready-rgb),0.6); animation: badgepulse 1.5s ease-in-out infinite; }
/* TALK IS NOT AN ALARM. The green badge is the Armory's "you can afford
   something" and the guild's "a band is riding out NOW" — both are things that
   want acting on. Unread conversation is news, not a summons, so it wears the
   house's own gilt and does not pulse. */
.navbadge.talk { background: linear-gradient(180deg, var(--gold-lt), var(--gold-dk));
  color: var(--gilt-ink); box-shadow: 0 0 9px rgba(var(--gold-rgb), 0.45); animation: none; }
@keyframes badgepulse { 0%,100% { transform: scale(1); box-shadow: 0 0 8px rgba(var(--ready-rgb),0.5); } 50% { transform: scale(1.12); box-shadow: 0 0 16px rgba(var(--ready-rgb),0.9); } }

/* ---- armory summary + unlock cues ---- */
/* flex-START, not space-between: the tally and the "ready to unlock" prompt are
   one group now (see _armRows in ui.js), and space-between would fling the group's
   own end back out to the right edge it was moved off. */
.armbar { display: flex; align-items: center; justify-content: flex-start; gap: 12px; margin: 0 26px 8px; padding: 10px 16px; background: var(--plate); border: 1px solid var(--edge); border-radius: var(--r-lg); }
.armready { color: var(--ready); font-weight: 700; font-size: 15px; text-shadow: 0 0 12px rgba(var(--ready-rgb),0.4); }
.armdim { color: var(--muted); font-size: 14px; }
.armcount { color: var(--gold-lt); font-weight: 700; font-variant-numeric: var(--nums); }
.armcard { transition: transform 0.12s, border-color 0.14s, box-shadow 0.14s; }
.armcard.ready { border-color: var(--ready); box-shadow: 0 0 0 1px rgba(var(--ready-rgb),0.4), 0 0 18px rgba(var(--ready-rgb),0.3); animation: bready 1.7s ease-in-out infinite; }
.arole { font-size: 10.5px; color: var(--ready); font-family: var(--font-ui); text-align: center; }
/* `.lockprog` / `.lpbar` / `.lpnum` are gone with the shortfall readout they styled —
   see the armory footer in ui.js. The price alone now owns the row. */
.buybtn.go { background: linear-gradient(180deg, var(--ready), var(--ready-dk)); border-color: #2f8f54; color: #07140b; }
.buybtn.go:hover:not(:disabled) { filter: brightness(1.08); }

/* ===================================================================== */
/* ===========================  MENU GLOW-UP  ========================== */
/* ===================================================================== */
.menucenter { position: relative; }
/* With the plate gone, this warmth behind the title is the only thing separating the
   crest from the flat backdrop — turned up so the lockup reads as lit from within
   rather than merely sitting on the menu. */
.menucenter::before {
  content: ''; position: absolute; left: 50%; top: 22%; width: 130%; height: 66%;
  transform: translate(-50%, -50%); z-index: -1; pointer-events: none;
  background: radial-gradient(ellipse at center, rgba(var(--gilt-lit-rgb),0.22), rgba(var(--gold-rgb),0.1) 42%, transparent 70%);
  filter: blur(16px); animation: auraBreathe 7s ease-in-out infinite;
}
@keyframes auraBreathe { 0%,100% { opacity: 0.6; transform: translate(-50%,-50%) scale(0.97); } 50% { opacity: 0.9; transform: translate(-50%,-50%) scale(1.04); } }

/* heraldic logo lockup: crest achievement + engraved decorative title + flourish.
   No plate any more — the crest and the wordmark sit straight on the menu's own
   backdrop, at the size a title card earns rather than the size a box permitted.
   Embers still rise past it, a glint still crosses the shield, and the wordmark is
   still struck rather than merely coloured — the letters carry a four-step gold
   extrusion under them, so they read as stamped metal at any size. */
.logo {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  position: relative; width: fit-content; margin: 0 auto clamp(2px, 0.7vh, 6px);
}
/* corner brackets used to mark a plate's edges; there is no plate to mark. */
.lcorner { display: none; }
/* embers coming off the plate — five, slow, and staggered so they never pulse as one */
.lspark { position: absolute; bottom: 14%; left: var(--x); width: 3px; height: 3px; border-radius: 50%;
  background: var(--gold-hi); box-shadow: 0 0 8px 2px rgba(var(--gilt-lit-rgb),0.65); opacity: 0; pointer-events: none;
  animation: lspark 7s ease-in-out infinite; animation-delay: var(--d); }
@keyframes lspark {
  0% { opacity: 0; transform: translateY(0) scale(0.5); }
  14% { opacity: 0.85; }
  72% { opacity: 0.4; }
  100% { opacity: 0; transform: translateY(-70px) scale(0.25); }
}
/* four-point sparkle glints scattered across the lockup — twinkling STARS, not glow.
   Kept as their own small elements rather than more blur piled onto the wordmark's
   shadow, which is what turned the letters into a ghosted double-stroke last time. */
.lglint { position: absolute; left: var(--x); top: var(--y); width: var(--s, 13px); height: var(--s, 13px);
  color: #fff8e2; opacity: 0; pointer-events: none; z-index: 2;
  filter: drop-shadow(0 0 4px rgba(255,240,200,0.9));
  animation: lglintTwinkle 3.1s ease-in-out infinite; animation-delay: var(--d); }
@keyframes lglintTwinkle {
  0%, 100% { opacity: 0; transform: scale(0.15) rotate(0deg); }
  45% { opacity: 1; transform: scale(1) rotate(50deg); }
  58% { opacity: 0.9; transform: scale(0.8) rotate(58deg); }
}
/* min() of the two axes: the crest and title shrink for whichever dimension is the
   tighter one, so a short-and-wide laptop scales them down just as a narrow phone does.
   Sized to fill the room the plate used to spend on padding instead of on the crest. */
.logocrest { position: relative; z-index: 1; width: clamp(64px, min(17vw, 16vh), 210px); height: auto; margin-bottom: 2px;
  filter: drop-shadow(0 5px 13px rgba(0,0,0,0.72)) drop-shadow(0 0 16px rgba(var(--gilt-lit-rgb),0.45));
  animation: crestFloat 5s ease-in-out infinite; }
/* a soft radiant glow sitting behind the crest, not around the plate — the achievement
   now reads as the source of its own light rather than a badge printed on a box */
.crestaura { position: absolute; top: -6%; left: 50%; z-index: 0; pointer-events: none;
  width: clamp(90px, 24vw, 260px); height: clamp(90px, 24vw, 260px);
  transform: translate(-50%, 0) scale(0.94); border-radius: 50%;
  background: radial-gradient(circle, rgba(var(--gilt-lit-rgb),0.42), rgba(255,176,56,0.15) 55%, transparent 76%);
  filter: blur(9px); animation: crestAuraPulse 4s ease-in-out infinite; }
@keyframes crestAuraPulse {
  0%, 100% { opacity: 0.65; transform: translate(-50%, 0) scale(0.92); }
  50% { opacity: 1; transform: translate(-50%, 0) scale(1.06); }
}
/* the glint crosses the shield, not the whole crest — a highlight that runs over the
   crossed blades as well reads as a screen wipe rather than light on metal */
.crestglint { transform-box: view-box; transform-origin: 0 0; animation: crestGlint 5.5s ease-in-out infinite; }
@keyframes crestGlint {
  0%, 58% { transform: skewX(-16deg) translateX(0); }
  100% { transform: skewX(-16deg) translateX(215px); }
}
/* tiny sparkle glyphs pinned to the crown's gems — twinkle on their own timers so the
   crown reads as jewelled rather than merely gold. Positioned by an outer SVG
   `transform` ATTRIBUTE (translate/scale); the CSS animation only ever touches the
   inner `<g>`'s scale/rotate, because a CSS `transform` on the same element would
   replace the attribute's translate outright and fling the sparkle to the origin. */
.cbsparkle { transform-box: fill-box; transform-origin: center; animation: cbSparkleTwinkle 2.6s ease-in-out infinite; animation-delay: var(--d); }
@keyframes cbSparkleTwinkle {
  0%, 100% { opacity: 0; transform: scale(0.2) rotate(0deg); }
  50% { opacity: 1; transform: scale(1) rotate(90deg); }
}
.gametitle {
  display: flex; flex-direction: column; align-items: center; line-height: 0.9;
  font-family: var(--font-logo); font-weight: 900; text-transform: uppercase; letter-spacing: 5px;
}
/* The ramp never goes below #d9ae4a now. Its old ends sat at #8a6a22 — a dark bronze
   — and because the sheen SLIDES the ramp across the letters, those ends spent the
   whole loop dragging themselves through the wordmark: half the animation was the
   name of the game going dim. Bright metal end to end, with a white-hot band that
   travels across it, so the sheen now reads as a highlight passing over gold rather
   than a shadow passing under it. */
.gametitle span {
  background: linear-gradient(100deg, #e8bd5e 0%, #fff6d2 14%, #ffffff 25%, #fff0b8 36%, #ffdd8c 48%, #eec066 64%, #fff3c4 80%, #e8bd5e 100%);
  background-size: 230% auto;
  -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; color: transparent;
  filter: drop-shadow(0 3px 11px rgba(0,0,0,0.82)) drop-shadow(0 0 8px rgba(255,222,140,0.2));
  animation: titleSheen 5.5s linear infinite;
}
/* no plate to hold them down any more — the crest and wordmark are the whole opening
   move now, sized to actually fill the space the box used to spend on its own frame */
.gametitle span:first-child { font-size: clamp(34px, min(9.6vw, 10vh), 118px); }
.gametitle span:last-child { font-size: clamp(40px, min(11.8vw, 12vh), 140px); margin-top: -0.08em; }
.flourish { width: clamp(220px, 38vw, 460px); height: auto; color: var(--gold-lt);
  margin: clamp(3px, 1.1vh, 12px) auto clamp(1px, 0.4vh, 4px);
  filter: drop-shadow(0 1px 2px rgba(0,0,0,0.6)); }
@keyframes titleSheen { from { background-position: 210% center; } to { background-position: -30% center; } }

/* PLAY button: pulsing glow + periodic shine sweep */
.bigbtn { position: relative; overflow: hidden; animation: btnPulse 2.8s ease-in-out infinite; }
@keyframes btnPulse {
  0%,100% { box-shadow: var(--sh-3), 0 0 22px rgba(var(--gold-rgb),0.25), inset 0 1px 0 rgba(255,255,255,0.45); }
  50%     { box-shadow: var(--sh-4), 0 0 44px rgba(var(--gold-rgb),0.55), inset 0 1px 0 rgba(255,255,255,0.55); }
}
.bigbtn::after {
  content: ''; position: absolute; top: 0; left: -65%; width: 42%; height: 100%; pointer-events: none;
  background: linear-gradient(100deg, transparent, rgba(255,255,255,0.55), transparent);
  transform: skewX(-20deg); animation: btnShine 3.6s ease-in-out infinite;
}
@keyframes btnShine { 0% { left: -65%; } 55%,100% { left: 150%; } }

/* race crests gently float — staggered so the row breathes rather than pulsing as
   one block. Written per-child because the delay has to differ; `n+8` catches any
   kingdom added beyond the seven and simply reuses the first beat. */
.racecard:nth-child(1) .racecrest, .racecard:nth-child(n+11) .racecrest { animation: crestFloat 4.2s ease-in-out infinite; }
.racecard:nth-child(2) .racecrest { animation: crestFloat 4.2s ease-in-out infinite 0.4s; }
.racecard:nth-child(3) .racecrest { animation: crestFloat 4.2s ease-in-out infinite 0.8s; }
.racecard:nth-child(4) .racecrest { animation: crestFloat 4.2s ease-in-out infinite 1.2s; }
.racecard:nth-child(5) .racecrest { animation: crestFloat 4.2s ease-in-out infinite 1.6s; }
.racecard:nth-child(6) .racecrest { animation: crestFloat 4.2s ease-in-out infinite 2.0s; }
.racecard:nth-child(7) .racecrest { animation: crestFloat 4.2s ease-in-out infinite 2.4s; }
.racecard:nth-child(8) .racecrest { animation: crestFloat 4.2s ease-in-out infinite 2.8s; }
.racecard:nth-child(9) .racecrest { animation: crestFloat 4.2s ease-in-out infinite 3.2s; }
.racecard:nth-child(10) .racecrest { animation: crestFloat 4.2s ease-in-out infinite 3.6s; }

/* The fluid sizing above carries the menu down on its own — these are the last
   rungs, where a banner gives up what it SAYS so it never gives up what it SHOWS.
   The host outlasts the blurb: three troops is the thing a kingdom cannot be
   recognised without. Nothing below drops a kingdom, PLAY, Armory or Commander — the
   things you came here to press always stay. (The style caption used to be the rung
   at 680px; it is gone at every height now, so that step is just the logo's.) */
@media (max-height: 780px) {
  .raceblurb { display: none; }                            /* crest, name, host */
}
@media (max-height: 580px) {
  .rc-host { display: none; }                              /* the heraldry alone, as it was */
  .lspark { display: none; }
}
@media (max-height: 480px) {
  .logocrest, .flourish { display: none; }                 /* the wordmark alone still says what this is */
  /* the last few pixels: an 812x375 phone held sideways clears the fold by five */
  .racerow { margin: 4px 0 5px; }
  .racecard { padding-top: 6px; }
  .menucenter { padding-top: 2px; padding-bottom: 2px; }
}
@keyframes crestFloat { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-6px); } }

/* ---- juicier build cards: category stripe, count badge, press, coin-pop ---- */
.build::before { content: ''; position: absolute; top: 0; left: 9px; right: 9px; height: 3px; border-radius: 0 0 var(--r-xs) var(--r-xs); background: var(--cat, var(--gilt-body)); opacity: 0.9; box-shadow: 0 0 7px var(--cat, var(--gilt-body)); }
.build:active { transform: translateY(1px) scale(0.97); }
/* The role line was `nowrap` with nothing to stop it, so a long one ("SWARM · CHEAP
   BODIES, FAST") ran clean out of its card and printed over its neighbours. It wraps
   inside the card now, clamped to two lines — the full text is in the hover tip. */
.bcat { font-size: 8.5px; letter-spacing: 0.6px; text-transform: uppercase; color: var(--cat, var(--gilt-body)); opacity: 0.9; margin-top: -1px;
  white-space: normal; text-align: center; line-height: 1.25; max-width: 100%;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.bcount { position: absolute; top: 21px; left: 4px; font-size: 10px; font-weight: 800; color: #fff; background: rgba(10,10,12,0.82); border: 1px solid rgba(255,255,255,0.18); border-radius: var(--r-md); padding: 0 4px; font-variant-numeric: var(--nums); z-index: 1; }
.coinpop { position: fixed; transform: translate(-50%, 0); pointer-events: none; z-index: 50; font-size: 20px; color: var(--gold-lt); text-shadow: 0 1px 3px rgba(0,0,0,0.7); animation: coinFloat 0.85s ease-out forwards; }
@keyframes coinFloat { 0% { opacity: 0; transform: translate(-50%, 4px) scale(0.6); } 20% { opacity: 1; transform: translate(-50%, -6px) scale(1.15); } 100% { opacity: 0; transform: translate(-50%, -48px) scale(1); } }

/* ===================================================================== */
/* ============================  THEME PASS  ========================== */
/* ===================================================================== */

/* themed scrollbars everywhere */
* { scrollbar-width: thin; scrollbar-color: var(--gold-dk) rgba(0,0,0,0.25); }
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-track { background: rgba(0,0,0,0.28); border-radius: var(--r-md); }
*::-webkit-scrollbar-thumb { background: linear-gradient(180deg, var(--gold-dk), #6e561d); border: 2px solid rgba(0,0,0,0.35); border-radius: var(--r-md); }
*::-webkit-scrollbar-thumb:hover { background: linear-gradient(180deg, var(--gold), var(--gold-dk)); }

/* faint parchment/fibre texture behind the menu screens */
.screen::after {
  content: ''; position: absolute; inset: 0; z-index: -1; pointer-events: none; opacity: 0.5;
  background:
    radial-gradient(circle at 22% 28%, rgba(var(--gold-rgb),0.05), transparent 42%),
    radial-gradient(circle at 80% 72%, rgba(120,90,255,0.04), transparent 46%),
    repeating-linear-gradient(115deg, rgba(255,240,200,0.012) 0 2px, transparent 2px 6px);
}

/* engraved flourish under section titles */
.scrtitle { position: relative; font-family: var(--font-display); text-shadow: 0 2px 6px rgba(0,0,0,0.6); }
.scrtitle::after { content: ''; position: absolute; left: 1px; bottom: -6px; width: 62%; height: 2px; background: linear-gradient(90deg, var(--gold), transparent); border-radius: var(--r-xs); }

/* menu/back buttons: display font + warmer hover glow */
.menubtn, .backbtn { font-family: var(--font-display); letter-spacing: 0.5px; box-shadow: var(--sh-2), var(--bevel); }
.menubtn:hover, .backbtn:hover { box-shadow: var(--sh-3), 0 0 16px rgba(var(--gold-rgb),0.25), inset 0 1px 0 rgba(var(--lit-rgb),0.12); }

/* a consistent inset gold hairline on the common framed panels */
#resources, #factions, .modecard, .armcard, .stat, .chip, .armbar, #selpanel, #tip, .resultbox, .searchbox {
  box-shadow: var(--sh-3), inset 0 0 0 1px rgba(0,0,0,0.4), var(--bevel);
}
#resources { font-family: var(--font-display); }
#resources b { color: var(--gold-bright); }

/* ornate corner rivets on the hero panels */
.resultbox, .searchbox { position: relative; }
.resultbox::before, .resultbox::after, .searchbox::before, .searchbox::after { content: ''; position: absolute; width: 14px; height: 14px; border: 2px solid var(--gold); opacity: 0.85; }
.resultbox::before, .searchbox::before { top: 9px; left: 9px; border-right: 0; border-bottom: 0; }
.resultbox::after, .searchbox::after { bottom: 9px; right: 9px; border-left: 0; border-top: 0; }

/* victory / defeat polish. The shared framed-panel shadow two rules up lists
   `.resultbox` and would otherwise win over the verdict-tinted glow set with the rest of
   the proclamation, so the card's own lighting is restated here, after it. */
.resultbox {
  box-shadow: var(--sh-4), 0 0 60px rgba(var(--vc-glow), 0.14), inset 0 0 0 1px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,236,190,0.14);
}
.resultbox::before, .resultbox::after { border-color: var(--vc); opacity: 0.9;
  filter: drop-shadow(0 0 6px rgba(var(--vc-glow), 0.6)); z-index: 2; }
.resmode { font-family: var(--font-flavor); letter-spacing: 1px; }
.searchtitle { font-family: var(--font-display); }
/* a short window gives up the ceremony before it gives up the button */
@media (max-height: 720px) {
  .resultbox { padding: 18px 32px 20px; }
  .resverdict { font-size: clamp(34px, 7vw, 52px); letter-spacing: 5px; }
  .rescrest { width: 50px; }
  .rpnum { font-size: 32px; }
  .rstat b { font-size: 22px; }
  .resspoils { margin: 13px 0 12px; }
}

/* ===================================================================== */
/* ====================  WAR-RACK: BUILD CARDS  ====================== */
/* ===================================================================== */
/* the bar is a stone-and-timber armoury rack the muster tiles sit in */
#buildbar {
  background:
    linear-gradient(180deg, rgba(34,25,14,0) 0%, rgba(28,20,11,0.55) 55%, rgba(20,14,8,0.72) 100%),
    repeating-linear-gradient(90deg, rgba(0,0,0,0.14) 0 2px, transparent 2px 8px);
  border: 1px solid var(--edge); border-top: 2px solid var(--edge-strong);
  border-radius: var(--r-2xl) var(--r-2xl) var(--r-lg) var(--r-lg); padding: 10px 14px 12px;
  box-shadow: var(--sh-3), inset 0 1px 0 rgba(var(--lit-rgb),0.14), inset 0 -12px 26px rgba(0,0,0,0.42);
}

/* each card is a carved muster tile */
.build {
  width: 110px; padding: 9px 8px; gap: 1px;
  background:
    radial-gradient(130% 60% at 50% -8%, rgba(var(--lit-rgb),0.1), transparent 60%),
    linear-gradient(180deg, #2c2215 0%, #1a130b 70%, #120c06 100%);
  border: 1px solid var(--edge-strong); border-radius: var(--r-xl);
  box-shadow: var(--sh-2), inset 0 0 0 1px rgba(0,0,0,0.6), inset 0 0 0 3px rgba(var(--gold-rgb),0.1), inset 0 2px 0 rgba(var(--lit-rgb),0.14), inset 0 -10px 18px rgba(0,0,0,0.45);
  transition: transform 0.12s var(--e-back-lg), box-shadow 0.16s, border-color 0.16s, filter 0.16s;
}
.build:hover { transform: translateY(-7px) scale(1.035); border-color: var(--gold-lt); filter: brightness(1.06); z-index: 3;
  box-shadow: var(--sh-4), 0 0 24px rgba(var(--gold-rgb),0.42), inset 0 0 0 3px rgba(var(--gold-rgb),0.24), inset 0 2px 0 rgba(var(--lit-rgb),0.18); }
.build:active { transform: translateY(-2px) scale(0.99); }
/* ---- THE LIFT CANNOT PULL THE CARD OUT FROM UNDER THE CURSOR ----
   A hover that MOVES the thing being hovered is a loop waiting to happen, and this one
   found it: the lift takes the card's bottom edge up about five pixels (seven of
   translate, less the two the scale hands back), so a cursor resting on that last band
   of the card was left standing in the gap the moment the card rose — which un-hovered
   it, which dropped it back under the cursor, which hovered it again. Rest the pointer
   on the bottom edge of a tile and the whole card strobed.
   A skirt hung off the bottom edge closes it: while the card is up, its hit area still
   reaches down to where the card was standing. It exists ONLY on hover, so a card at
   rest never reaches into its neighbour's space, and at 7px it is exactly the rack's
   row gap — the band it covers is empty rack, never the tile on the rank below.
   Hung on `.bupg::after` for want of a free pseudo-element on the card itself (`::before`
   is the category ribbon, `::after` the hover wash). `.bupg` is unpositioned, so this is
   laid out against the CARD's padding box — which is the box that has to be covered. */
.build:hover .bupg::after { content: ''; position: absolute; left: 0; right: 0; top: 100%; height: 7px; }

/* the count-in holds the rack: read it and plan, but no ground is broken before
   CHARGE. Cards stay legible (and still answer a hover tip) — they just don't
   look armed, so a refused click reads as "not yet" rather than as a dead button. */
#buildbar.cdlock .build { filter: grayscale(0.55) brightness(0.78); cursor: not-allowed; }
#buildbar.cdlock .build:hover { transform: none; border-color: var(--edge-strong); filter: grayscale(0.4) brightness(0.9);
  box-shadow: var(--sh-2), inset 0 0 0 1px rgba(0,0,0,0.6), inset 0 2px 0 rgba(var(--lit-rgb),0.14); }
#buildbar.cdlock .build:active { transform: none; }
#buildbar.cdlock .bupg { opacity: 0.5; }

/* category gem ribbon across the top */
.build::before { left: 12px; right: 12px; top: -1px; height: 4px; border-radius: 0 0 var(--r-xs) var(--r-xs); background: linear-gradient(90deg, transparent, var(--cat,var(--gilt-body)) 22%, var(--cat,var(--gilt-body)) 78%, transparent); opacity: 0.95; box-shadow: 0 0 8px var(--cat,var(--gilt-body)); }

/* recessed gilded portrait medallion */
.bicon {
  width: 50px; height: 50px; margin: 4px auto 2px; padding: 0; border-radius: 50%; overflow: hidden;
  background: radial-gradient(circle at 50% 36%, #3c3120, #0e0a05 72%);
  box-shadow: inset 0 3px 7px rgba(0,0,0,0.75), inset 0 0 0 2px rgba(var(--gold-rgb),0.55), 0 1px 0 rgba(var(--lit-rgb),0.12), var(--sh-2);
}
.build:hover .bicon { box-shadow: inset 0 3px 7px rgba(0,0,0,0.7), inset 0 0 0 2px var(--gold-lt), 0 0 14px rgba(var(--gold-rgb),0.5); }
.build.isidle .bicon { filter: grayscale(0.55) brightness(0.72); }

/* engraved hotkey keycap + gilded level seal */
.bk { top: 4px; left: 5px; font-size: 9px; color: #241405; background: linear-gradient(180deg, var(--gold-lt), #c79a36); border: 1px solid var(--gilt-deep); border-radius: var(--r-sm); padding: 1px 5px; font-weight: 800; box-shadow: inset 0 1px 0 rgba(255,255,255,0.55), var(--sh-0); }
.blvl { top: 4px; right: 5px; font-size: 10px; color: #241405; background: radial-gradient(circle at 40% 30%, var(--gold-hi), #c79a36); border: 1px solid var(--gilt-deep); border-radius: var(--r-sm); padding: 0 5px; font-weight: 800; box-shadow: inset 0 1px 0 rgba(255,255,255,0.5), var(--sh-0); }
.build.isidle .blvl { background: #4a4636; color: #cdbf9a; border-color: #2a2620; box-shadow: none; }
.bcount { top: 25px; left: 5px; background: rgba(8,8,10,0.85); border-color: rgba(var(--gold-rgb),0.4); }

/* nameplate banner + gem category */
.bn { font-family: var(--font-display); font-size: 11.5px; font-weight: 700; color: var(--gold-lt); letter-spacing: 0; text-shadow: 0 1px 2px rgba(0,0,0,0.85); padding: 1px 3px; border-radius: var(--r-sm); background: linear-gradient(180deg, rgba(0,0,0,0.3), rgba(0,0,0,0.08)); box-shadow: inset 0 0 0 1px rgba(var(--gold-rgb),0.16); }
.bcat { font-size: 8px; letter-spacing: 1px; }

/* level dots as gilded studs */
.bdots { gap: 2px; margin: 4px 0 2px; }
.bdots i { width: 6px; height: 6px; background: radial-gradient(circle at 40% 35%, #4a4030, #181208); border: 1px solid rgba(0,0,0,0.6); box-shadow: inset 0 1px 1px rgba(0,0,0,0.6); }
.bdots i.on { background: radial-gradient(circle at 40% 30%, var(--gold-bright), var(--gold)); border-color: var(--gold-dk); box-shadow: 0 0 5px rgba(240,200,90,0.85), inset 0 1px 1px rgba(255,255,255,0.5); }

/* rune cooldown channel */
.bcdbar { height: 6px; background: linear-gradient(180deg, #060a0c, #0c1418); border: 1px solid rgba(0,0,0,0.6); border-radius: var(--r-sm); box-shadow: inset 0 1px 2px rgba(0,0,0,0.7); }
.bcdbar i { background: linear-gradient(180deg, #9bdcff, #3f8fc8); box-shadow: 0 0 6px rgba(120,200,255,0.6); }
.bcdnum { color: #aac8e0; }

/* carved action plate — emerald rune-seal when affordable, stone otherwise */
.bupg {
  margin-top: 6px; padding: 5px 2px; border-radius: var(--r-md); gap: 0; line-height: 1.08;
  background: linear-gradient(180deg, #3a2f1d, #1a130a); border: 1px solid var(--gold-dk); color: var(--gold-lt);
  box-shadow: inset 0 1px 0 rgba(var(--lit-rgb),0.16), inset 0 -3px 8px rgba(0,0,0,0.5), var(--sh-1);
}
.bupgw { font-family: var(--font-display); font-size: 9px; font-weight: 800; letter-spacing: 0.6px; text-shadow: 0 1px 1px rgba(0,0,0,0.7); }
.bupgc { font-size: 12px; font-weight: 800; text-shadow: 0 1px 1px rgba(0,0,0,0.6); }
.bupg.go { background: linear-gradient(180deg, #2f7046, #143620); border-color: var(--ready); color: #dffbe8; box-shadow: inset 0 1px 0 rgba(190,255,210,0.28), inset 0 -3px 8px rgba(0,0,0,0.45), 0 0 14px rgba(110,224,154,0.5); animation: upgGlow 1.7s ease-in-out infinite; }
@keyframes upgGlow { 0%,100% { box-shadow: inset 0 1px 0 rgba(190,255,210,0.28), 0 0 12px rgba(110,224,154,0.4); } 50% { box-shadow: inset 0 1px 0 rgba(190,255,210,0.32), 0 0 22px rgba(110,224,154,0.78); } }
.bupg.cant { background: linear-gradient(180deg, #241c12, #130d06); border-color: rgba(var(--gold-rgb),0.22); color: var(--muted); box-shadow: inset 0 -2px 6px rgba(0,0,0,0.5); }
.bupg.cant .bupgc { color: var(--gold-lt); opacity: 0.72; }
.bupg.maxed { background: linear-gradient(180deg, #4a3a1a, #241804); border-color: var(--gold); color: var(--gold-lt); }

/* the tile itself only whispers a glow when ready (the plate does the pulsing) */
.build.ready { border-color: rgba(var(--ready-rgb),0.55); animation: none;
  box-shadow: var(--sh-2), inset 0 0 0 3px rgba(var(--ready-rgb),0.16), 0 0 16px rgba(var(--ready-rgb),0.22); }

/* ===================================================================== */
/* ===================  HUD AS HERALDIC BANNERS  ===================== */
/* ===================================================================== */
#tip { position: relative; }
/* gilt corner brackets. The resource register lost them with the box they framed:
   brackets mark the corners of a panel, and it is not a panel any more — it is the
   map frame's top register, and the frame has its own rivets. */
#tip::before, #tip::after {
  content: ''; position: absolute; width: 11px; height: 11px; border: 2px solid var(--gold); opacity: 0.72; pointer-events: none;
}
#tip::before { top: 4px; left: 4px; border-right: 0; border-bottom: 0; }
#tip::after { bottom: 4px; right: 4px; border-left: 0; border-top: 0; }

.res-gold b, .res-inc b, .res-pop b { font-family: var(--font-display); }

/* war-council roster */
.fac { padding: 3px 6px; border-radius: var(--r-md); transition: background 0.15s; }
.fac.me { background: linear-gradient(90deg, rgba(var(--gold-rgb),0.16), transparent); box-shadow: inset 0 0 0 1px rgba(var(--gold-rgb),0.28); }
.fac:hover { background: rgba(255,255,255,0.04); }

/* nav / lobby buttons as aged ledger tabs */
.menubtn { background: linear-gradient(180deg, rgba(48,38,22,0.95), rgba(22,16,9,0.96)); border-left: 3px solid var(--gold-dk); }
.menubtn:hover { border-left-color: var(--gold-lt); }

/* retreat banner */
#leavebtn { font-family: var(--font-display); letter-spacing: 0.5px; box-shadow: var(--sh-2), inset 0 1px 0 rgba(255,200,190,0.12); }

/* inspect-scroll tooltip */
#tip { border-top: 2px solid var(--edge-strong); }
.tname { font-family: var(--font-display); }

/* ===================================================================== */
/* ==================  CLEARER SCORING & FEEDBACK  =================== */
/* ===================================================================== */
/* Each stat is a tiny 2-row grid — sigil and number on the first line, its caption
   under them — rather than one long row. Explicit grid placement, not wrapping, so
   every number in the register shares a baseline and every caption shares the one
   below it no matter how many digits the number runs to. Stacked also costs no height
   (the caption fits beside the sigil's ascender), which is what lets three stats sit
   across the map's 230px without either shrinking or spilling. */
#resources > span {
  display: grid; grid-template-columns: auto auto; grid-auto-rows: min-content;
  align-items: center; justify-items: center; column-gap: 3px; row-gap: 0; min-width: 0;
}
.ric { grid-row: 1; grid-column: 1; font-size: 12px; opacity: 0.9; }
#resources b { grid-row: 1; grid-column: 2; font-size: 15px; line-height: 1.15; }
#resources em { grid-row: 2; grid-column: 1 / -1; font-style: normal; font-size: 9px; line-height: 1.3; letter-spacing: 0.6px; text-transform: uppercase; color: var(--muted); font-family: var(--font-ui); }
.res-inc b { color: var(--ready); }


/* the standings scoreboard — 2×2 grid */
#factions { min-width: 360px; max-width: 440px; gap: 3px 8px; padding: 8px 11px 10px; }

/* =====================  THE DUEL SCORECARD  ==========================
   Both keeps face each other across a clock. The bars drain INWARD, toward the
   middle, so the two are read against each other rather than as two separate
   readouts stacked in a list. Everything on it moves during a fight. */
/* The card rides the topbar's middle column, so `relative` is both what the hover
   briefing needs to anchor to and what keeps the card in the grid that centres it.
   Its width is the topbar's whole balancing act: reserving 400px per side leaves the
   treasury banner (~355px) and the retreat button room to sit clear of it at any
   width down to the stacking breakpoint below. */
/* ===== THE SCORECARD IS A HANGING BANNER ===== */
/* The menu's kingdom cards and the muster rail are both cloth on a gilt rod with a
   notched hem, and this — the thing on screen for the entire match — was a rounded
   rectangle with a hairline border. It is the same object in the same game and it
   should look like it: a rod across the top with finials past both ends, a notched
   point at the bottom hem, and each half of the field tinted with the kingdom hanging
   there. Same construction as `.rc-cloth` / `.rc-rod` on the menu, so the two cannot
   drift apart. */
/* ===== AND IT IS A BANNER, NOT A BILLBOARD =====
   At the design size the card was 720 × 123 — 45% of the width and 15.6% of the
   height of the screen, hanging over the middle of the field, which is exactly
   where the fighting is. Everything on it earns its place; nothing on it earned
   THAT much room. Every number below is trimmed and none is removed: the keep
   bar is still the biggest thing on the card, the percentage is still readable
   across the room, and the whole thing now comes in at 600 × 93 — a quarter less
   height, and the top of the battlefield handed back.
   The phone is untouched by all of it. It overrides `--notch`, the padding, both
   gaps and every font in `html.phone .sc-*` further down, so these are desktop
   numbers only — which is what was asked for, and what keeps the hand-held
   layout from moving when the desktop one does. */
#factions.scorecard {
  position: relative; display: block; justify-self: center;
  width: clamp(400px, calc(100vw / var(--ui) - 800px), 600px); min-width: 0; max-width: none;
  padding: 0; background: none; border: none; box-shadow: none; backdrop-filter: none;
  margin-top: 3px; filter: drop-shadow(0 8px 18px rgba(0,0,0,0.6)); }
.sc-card { --notch: 10px; position: relative; display: grid; grid-template-columns: 1fr auto 1fr;
  gap: 10px; align-items: center; padding: 7px 18px calc(var(--notch) + 5px);
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--notch)), 50% 100%, 0 calc(100% - var(--notch)));
  background:
    repeating-linear-gradient(90deg, rgba(0,0,0,0.08) 0 1px, transparent 1px 5px),
    linear-gradient(180deg, rgba(255,255,255,0.07), rgba(255,255,255,0) 24%),
    linear-gradient(170deg, rgba(44,36,23,0.96), rgba(15,12,8,0.97));
  backdrop-filter: blur(4px); }
/* the gilt braid along the top, and the rod it hangs from */
.sc-card::before { content: ''; position: absolute; left: 0; right: 0; top: 0; height: 3px;
  background: linear-gradient(90deg, transparent, var(--gold-bright) 12%, var(--gold) 50%, var(--gold-bright) 88%, transparent); }
#factions.scorecard::before { content: ''; position: absolute; z-index: 2; left: -9px; right: -9px; top: -5px; height: 7px;
  border-radius: var(--r-sm); background: linear-gradient(180deg, var(--gold-bright), var(--gold) 46%, var(--gilt-deep));
  box-shadow: var(--sh-1), inset 0 1px 0 rgba(255,255,255,0.55); }
#factions.scorecard::after {                       /* the two finials capping the rod */
  content: ''; position: absolute; z-index: 3; left: -14px; top: -6px; width: 9px; height: 9px; border-radius: 50%;
  background: radial-gradient(circle at 34% 30%, var(--gold-bright), var(--gold) 55%, var(--gilt-seat));
  box-shadow: var(--sh-1), calc(100% + 19px) 0 0 -0px transparent; }
.sc-rodcap { position: absolute; z-index: 3; right: -14px; top: -6px; width: 9px; height: 9px; border-radius: 50%;
  background: radial-gradient(circle at 34% 30%, var(--gold-bright), var(--gold) 55%, var(--gilt-seat));
  box-shadow: var(--sh-1); }
/* a slow gilt sweep across the whole card — alive, not busy */
.sc-card::after { content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(105deg, transparent 38%, rgba(var(--lit-rgb),0.07) 50%, transparent 62%);
  background-size: 260% 100%; animation: scSheen 7s linear infinite; }
@keyframes scSheen { from { background-position: 160% 0; } to { background-position: -60% 0; } }

/* each half of the cloth carries the kingdom hanging on it — the same "own light
   pooling at the head" the menu banners use, so the two read as one family */
.sc-side { --fac: #d8b24a; --facl: #f0d27e; position: relative; min-width: 0;
  display: flex; flex-direction: column; gap: 3px; transition: filter 0.2s; }
.sc-side::before { content: ''; position: absolute; inset: -8px -12px -11px; z-index: -1; pointer-events: none;
  background: radial-gradient(105% 78% at 50% 0, color-mix(in srgb, var(--fac) 26%, transparent), transparent 72%); }
/* `.sc-r` is the EASTERN keep's panel, not "the enemy's" — the card is seated like
   the field, so either commander may be on either side. All these rules do is mirror
   the right-hand panel outward. */
.sc-r { align-items: flex-end; }
.sc-id { display: flex; align-items: center; gap: 7px; min-width: 0; }
.sc-r .sc-id { flex-direction: row-reverse; }
/* The crest is the tallest thing in this row, so it is the row: the canvas is
   still cut at 30 × 35 and only DISPLAYED smaller, which costs nothing and
   leaves the heraldry sharper than it was at 1:1. */
.sc-crest { flex: none; width: 25px; height: 29px; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.7)); }
/* The house beside the kingdom crest on a duel card — a 1v1 is where a house
   matters most and where there are fewest things competing to be looked at. */
.sc-arms { flex: none; width: 17px; height: 20px; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.75)); }
.sc-house {
  font: 800 8.5px var(--font-display); letter-spacing: 0.9px; text-transform: uppercase;
  color: var(--gilt-body); white-space: nowrap; opacity: 0.92;
}
.sc-sub { display: flex; align-items: center; gap: 5px; min-width: 0; }
.sc-r .sc-sub { flex-direction: row-reverse; }
.sc-rung { flex: none; display: inline-flex; }
.sc-rung .cm-rk { font-size: 8.5px; letter-spacing: 0.7px; padding: 0 5px; }
.sc-nm { min-width: 0; display: flex; flex-direction: column; }
.sc-r .sc-nm { align-items: flex-end; }
.sc-name { font: 800 13.5px var(--font-display); color: #f4e6c0; letter-spacing: 0.4px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 190px; }
/* The gold YOU pill under your own name is gone. Which side is yours is already said
   three louder ways — your kingdom's name, its crest and its colour, all of which the
   `.mine` panel is already wearing — and the pill spent a line of the topbar repeating
   the one thing on the card nobody has ever needed telling. */
/* `.sc-keep` — the kingdom's three-word style line — is gone from the topbar. It is a
   thing you read while CHOOSING a kingdom, not while fighting with one. */

/* THE KEEP BAR IS THE WIN CONDITION, so it is still unmistakably the biggest thing on
   the card — it just no longer needs 24px to say so. At 19px it is still half again
   the height of any text beside it and the percentage still reads from the far side
   of the screen, which was the whole point of making it big in the first place. */
.sc-bar { position: relative; height: 19px; border-radius: var(--r-xl); overflow: hidden;
  background: rgba(0,0,0,0.58); border: 1px solid rgba(0,0,0,0.65);
  box-shadow: inset 0 2px 6px rgba(0,0,0,0.7); }
.sc-bar i { position: absolute; inset: 0 auto 0 0; width: 100%;
  background: linear-gradient(180deg, var(--facl), var(--fac));
  box-shadow: 0 0 14px color-mix(in srgb, var(--fac) 55%, transparent);
  transition: width 0.32s var(--e-out); }
.sc-r .sc-bar i { inset: 0 0 0 auto; }            /* the eastern keep drains the other way */
.sc-bar s { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  text-decoration: none; font: 800 13px var(--font-display); color: #150f07; letter-spacing: 0.6px;
  text-shadow: 0 1px 0 rgba(255,255,255,0.32); }

.sc-stats { display: flex; gap: 12px; font: 600 11.5px 'Spectral', serif; color: #b6a889; }
.sc-stats b { font: 800 14px var(--font-display); color: #f2e7cd; margin-left: 4px;
  display: inline-block; }
/* NO GROW ON A RUNNING COUNTER. Army, spawners and kills all climb through the
   match, so animating each change left the readouts almost permanently swelling —
   and a number that is moving is a number you cannot read at a glance. The moment
   still lands where it happened: the kill floats over the body, the gold over the
   bounty. These just say what the count is. */
.sc-stats b.tick { animation: none; }
@keyframes scTick {
  0% { transform: scale(1.5); color: var(--gold-bright); text-shadow: 0 0 12px rgba(var(--gilt-lit-rgb),0.9); }
  100% { transform: scale(1); }
}

/* the middle: the clock, and what you are here to do */
.sc-mid { display: flex; flex-direction: column; align-items: center; gap: 1px; padding: 0 3px; }
.sc-clock { font: 800 17px var(--font-display); color: var(--gold-lt); letter-spacing: 1px; line-height: 1;
  text-shadow: 0 0 12px rgba(var(--gold-rgb),0.4); }
.sc-vs { font: 700 8px var(--font-display); color: #8d825f; letter-spacing: 0.8px; white-space: nowrap; }

/* ---- states ---- */
.sc-side.lead .sc-name { color: var(--gold-hi); text-shadow: 0 0 12px color-mix(in srgb, var(--fac) 60%, transparent); }
.sc-side.lead .sc-bar { border-color: color-mix(in srgb, var(--fac) 55%, transparent); }
/* THE BAR KEEPS ITS SIDE'S COLOUR ALL THE WAY DOWN. Under 35% this used to repaint
   the fill from the faction's own gradient to a fixed red-orange — so a keep in
   trouble stopped being that kingdom's bar and became a generic warning, and the two
   sides of the scorecard, which are read against each other at a glance, no longer
   agreed about what a colour meant: the Orcs' full bar and the Humans' failing one
   were near enough the same red. The alarm is carried by the PULSE instead, which was
   always doing the real work — it moves, and a moving thing on a still bar is seen
   sooner than a hue is. Colour says whose keep it is; brightness says how it fares. */
.sc-side.danger .sc-bar i { animation: scDanger 0.9s ease-in-out infinite; }
@keyframes scDanger { 0%, 100% { box-shadow: 0 0 10px rgba(226,80,50,0.5); } 50% { box-shadow: 0 0 22px rgba(255,120,80,0.95); } }
.sc-side.struck .sc-bar { animation: scStruck 0.42s ease-out; }
@keyframes scStruck {
  0% { transform: translateX(-4px) scaleY(1.22); box-shadow: 0 0 26px rgba(255,120,80,0.95); }
  35% { transform: translateX(3px) scaleY(1.1); }
  100% { transform: none; }
}
.sc-side.dead { filter: grayscale(0.85) brightness(0.6); }
.sc-side.dead .sc-bar s { color: var(--ink); }

/* …and the narrow-window step keeps stepping DOWN. Every number here has to stay
   under the base above it, or trimming the desktop card would have quietly made
   the cramped one bigger. */
@media (max-width: 1100px) {
  #factions.scorecard { min-width: 400px; }
  .sc-name { max-width: 120px; font-size: 12px; }
  .sc-stats { gap: 9px; font-size: 10.5px; }
  .sc-stats b { font-size: 12.5px; }
  .sc-crest { width: 21px; height: 25px; }
  .sc-bar { height: 17px; }
  .sc-bar s { font-size: 11.5px; }
}
.sb-hdr { grid-column: 1 / -1; order: -1; font-family: var(--font-display); font-size: 11px; letter-spacing: 0.5px; color: var(--gold-lt); text-align: center; margin-bottom: 4px; }
.sb-hdr b { color: var(--gold-bright); }
.sb-row { display: flex; align-items: center; gap: 7px; padding: 3px 4px; border-radius: var(--r-md); transition: background 0.15s, filter 0.15s; }
.sb-row.me { background: linear-gradient(90deg, rgba(var(--gold-rgb),0.18), transparent); box-shadow: inset 0 0 0 1px rgba(var(--gold-rgb),0.32); }
.sb-row.dead { opacity: 0.5; }
.sb-row.dead .sb-name { text-decoration: line-through; }
/* A row is hovered to open its kingdom's dossier (see `showDos` in main.js) — the
   same "more is one hover away" cursor the muster's own troop chips use. `filter`
   rather than `background`/`box-shadow`: those are already spoken for by `.me`
   and `.ally`, and a brightness lift stacks over whichever one is active instead
   of replacing it. */
.sb-row { cursor: help; }
.sb-row:hover { filter: brightness(1.08); }
.fcrest { width: 26px; height: 30px; flex: none; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.6)); }
/* THE CREST WEARS A MEDALLION IN ITS OWN KINGDOM'S COLOUR. A bare canvas floating
   in the row read as a small icon among the numbers; a plate under it — the same
   idea as a shield boss — gives eight different banners eight different faces at
   a glance, expanded, and doubles as the row's one hover flourish. `--fac` is set
   per row in JS (see the faction row loop), so this needs no per-kingdom CSS. */
.fcrestwrap { position: relative; flex: none; display: flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; border-radius: 50%; transition: transform 0.15s, box-shadow 0.15s;
  background: radial-gradient(circle at 35% 32%,
    color-mix(in srgb, var(--fac, #8a7550) 42%, #1c150c),
    color-mix(in srgb, var(--fac, #8a7550) 12%, #0a0806) 72%);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--fac, #8a7550) 52%, transparent), var(--sh-1); }
.fcrestwrap .fcrest { filter: drop-shadow(0 1px 2px rgba(0,0,0,0.65)); }
/* THE HOUSE ON THE BOARD. The arms hang off the bottom-right corner of the
   kingdom crest — small enough that eight rows of them still read as a list of
   kingdoms, large enough that a commander recognises their own. */
.sb-arms {
  position: absolute; right: -5px; bottom: -4px; width: 15px; height: 18px;
  filter: drop-shadow(0 1px 2px rgba(0,0,0,0.8));
  pointer-events: none;
}
#factions.alliance .sb-arms { width: 13px; height: 16px; right: -4px; bottom: -3px; }
.sb-house {
  font: 800 8.5px var(--font-display); letter-spacing: 0.9px; text-transform: uppercase;
  color: var(--gilt-body); border: 1px solid var(--edge-quiet); border-radius: var(--r-pill);
  padding: 0 4px; flex: none; white-space: nowrap; opacity: 0.9;
}
/* WHAT THE LADDER SAYS THEY ARE, on the board naming them. Vouched for by the
   server, so unlike the arms beside it this is a claim the game is making
   rather than one the commander is. Shrunk hard: eight rows of these still have
   to read as a list of kingdoms first. */
.sb-rung { flex: none; display: inline-flex; }
.sb-rung .cm-rk { font-size: 8px; letter-spacing: 0.6px; padding: 0 4px; }
.sb-row.lead .fcrestwrap { box-shadow: 0 0 0 1px color-mix(in srgb, var(--fac, #8a7550) 85%, transparent), 0 0 8px color-mix(in srgb, var(--fac, #8a7550) 55%, transparent), var(--sh-1); }
.sb-row:hover .fcrestwrap { transform: scale(1.08);
  box-shadow: 0 0 0 1px var(--fac, #8a7550), 0 0 10px color-mix(in srgb, var(--fac, #8a7550) 65%, transparent), var(--sh-1); }
.sb-body { flex: 1; min-width: 0; }
.sb-top { display: flex; justify-content: space-between; align-items: baseline; gap: 6px; }
.sb-name { font-family: var(--font-display); font-size: 12px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sb-row.me .sb-name { color: var(--gold-lt); }
.sb-out { font-size: 10px; color: var(--gold-lt); font-variant-numeric: var(--nums); white-space: nowrap; }
.sb-bar { position: relative; height: 12px; margin-top: 2px; background: #0c0a07; border: 1px solid rgba(0,0,0,0.55); border-radius: var(--r-md); overflow: hidden; box-shadow: inset 0 1px 2px rgba(0,0,0,0.6); }
.sb-bar i { display: block; height: 100%; width: 0%; border-radius: var(--r-md) 0 0 var(--r-md); transition: width 0.25s ease; box-shadow: 0 0 8px rgba(255,220,120,0.3); }
.sb-score { position: absolute; right: 5px; top: 50%; transform: translateY(-50%); font-size: 9.5px; font-weight: 800; color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,0.95); font-variant-numeric: var(--nums); }
.sb-rank { width: 18px; text-align: center; font-size: 13px; font-weight: 800; color: var(--gold-lt); font-variant-numeric: var(--nums); }
.sb-row.lead .sb-rank { filter: drop-shadow(0 0 5px rgba(255,220,120,0.85)); }

/* ---- THE ALLIANCE BOARD (4v4) ----
   Eight rows ranked into one list answers "who is winning" and nothing else; the
   question a 4v4 asks the topbar is "which LINE is breaking". So the two sides get
   a column each — yours on the left, always, whichever seat you drew — and each
   column ranks and crowns its own. An ally reads as one of yours without being
   mistaken for you: a cool banner down the edge against your own gold one. */
/* ---- THE ALLIANCE BOARD: eight keeps read as two lines ------------------
   Hung from the same gilt rod as the duel's card, because both answer the same
   question and there was no reason for one to be a war banner and the other a
   dark rounded box. What each column reports is the thing an alliance war is
   actually racing — keeps still standing, and wall left across the whole line. */
#factions.alliance {
  position: relative; display: block; justify-self: center;
  width: clamp(480px, calc(100vw / var(--ui) - 700px), 720px); min-width: 0; max-width: none;
  padding: 0; background: none; border: none; box-shadow: none; backdrop-filter: none;
  margin-top: 5px; filter: drop-shadow(0 10px 22px rgba(0,0,0,0.6));
}
.sb-card {
  --notch: 12px; position: relative; display: grid; grid-template-columns: 1fr 86px 1fr;
  column-gap: 0; align-items: start; padding: 10px 16px calc(var(--notch) + 7px);
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--notch)), 50% 100%, 0 calc(100% - var(--notch)));
  background:
    repeating-linear-gradient(90deg, rgba(0,0,0,0.08) 0 1px, transparent 1px 5px),
    linear-gradient(180deg, rgba(255,255,255,0.07), rgba(255,255,255,0) 24%),
    linear-gradient(170deg, rgba(44,36,23,0.96), rgba(15,12,8,0.97));
  backdrop-filter: blur(4px);
}
.sb-card::before { content: ''; position: absolute; left: 0; right: 0; top: 0; height: 3px;
  background: linear-gradient(90deg, transparent, var(--gold-bright) 12%, var(--gold) 50%, var(--gold-bright) 88%, transparent); }
/* the rod it hangs from, and its two finials */
#factions.alliance::before { content: ''; position: absolute; z-index: 2; left: -9px; right: -9px; top: -5px; height: 7px;
  border-radius: var(--r-sm); background: linear-gradient(180deg, var(--gold-bright), var(--gold) 46%, var(--gilt-deep));
  box-shadow: var(--sh-1), inset 0 1px 0 rgba(255,255,255,0.55); }
#factions.alliance::after { content: ''; position: absolute; z-index: 3; left: -14px; top: -6px; width: 9px; height: 9px; border-radius: 50%;
  background: radial-gradient(circle at 34% 30%, var(--gold-bright), var(--gold) 55%, var(--gilt-seat));
  box-shadow: var(--sh-1); }
#factions.alliance .sb-rodcap { position: absolute; z-index: 3; right: -14px; top: -6px; width: 9px; height: 9px; border-radius: 50%;
  background: radial-gradient(circle at 34% 30%, var(--gold-bright), var(--gold) 55%, var(--gilt-seat));
  box-shadow: var(--sh-1); }

#factions.alliance .sb-side { display: flex; flex-direction: column; gap: 3px; min-width: 0; padding: 0 11px; }
/* a gilt seam down the middle rather than a hairline border */
#factions.alliance .sb-side + .sb-side {
  border-left: 1px solid transparent; padding-left: 12px;
  border-image: linear-gradient(180deg, transparent, rgba(var(--gold-rgb),0.5) 22%, rgba(var(--gold-rgb),0.5) 78%, transparent) 1; }

/* ---- a line's own header: its name, its keeps, its remaining wall ---- */
.sb-sidettl { order: -1; display: flex; align-items: baseline; justify-content: space-between; gap: 6px;
  font-family: var(--font-display); font-size: 9.5px; letter-spacing: 1px;
  text-transform: uppercase; color: var(--muted); padding-bottom: 1px; }
.sb-sidenm { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#factions.alliance .sb-side.us .sb-sidettl { color: var(--gold-lt); }
.sb-sum { display: flex; align-items: baseline; gap: 5px; font-variant-numeric: var(--nums); white-space: nowrap; }
.sb-sum b { font-size: 12.5px; font-weight: 800; color: var(--gold-lt); letter-spacing: 0; }
/* one icon per figure, not one rule for every <b> — a second stat sharing the tag
   would have inherited the castle glyph the first one wears */
.sb-keeps::before { content: ''; display: inline-block; width: 10px; height: 10px; margin-right: 3px; opacity: 0.85;
  background: currentColor; vertical-align: -1px;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linejoin='round'%3E%3Cpath d='M5.4 20V9.4h2.7V7.4h2v2h3.8v-2h2v2h2.7V20z'/%3E%3C/svg%3E") center/contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linejoin='round'%3E%3Cpath d='M5.4 20V9.4h2.7V7.4h2v2h3.8v-2h2v2h2.7V20z'/%3E%3C/svg%3E") center/contain no-repeat; }
/* KILLS, alongside keeps and wall. It used to live only on the eight rows below —
   fold the board to the strip it opens as and it vanished with them, so the one
   figure an alliance war's fighting actually produces had nowhere to be read
   without unfolding. Same cell shape as keeps, but NOT gold — gold on this line
   means "the win condition", and kills is a tally of the fighting, not of it. It
   takes the same ink the per-row kill count already wears, so the two read as
   the same kind of number. `.sb-sum b` normally goes gold (brighter still on
   "your line"); this beats both on specificity rather than reaching for `!important`. */
#factions.alliance .sb-sum b.sb-kills,
#factions.alliance .sb-side.us .sb-sum b.sb-kills { color: var(--ink); }
.sb-kills::before { content: ''; display: inline-block; width: 10px; height: 10px; margin-right: 3px; opacity: 0.85;
  background: currentColor; vertical-align: -1px;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linejoin='round'%3E%3Cpath d='M12 4.4c-3.9 0-6.7 2.7-6.7 6.2 0 2.1 1 3.7 2.5 4.7v3h8.4v-3c1.5-1 2.5-2.6 2.5-4.7 0-3.5-2.8-6.2-6.7-6.2z'/%3E%3Ccircle cx='9.4' cy='10.8' r='1.5' fill='black' stroke='none'/%3E%3Ccircle cx='14.6' cy='10.8' r='1.5' fill='black' stroke='none'/%3E%3C/svg%3E") center/contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linejoin='round'%3E%3Cpath d='M12 4.4c-3.9 0-6.7 2.7-6.7 6.2 0 2.1 1 3.7 2.5 4.7v3h8.4v-3c1.5-1 2.5-2.6 2.5-4.7 0-3.5-2.8-6.2-6.7-6.2z'/%3E%3Ccircle cx='9.4' cy='10.8' r='1.5' fill='black' stroke='none'/%3E%3Ccircle cx='14.6' cy='10.8' r='1.5' fill='black' stroke='none'/%3E%3C/svg%3E") center/contain no-repeat; }
.sb-sum s { text-decoration: none; font-size: 10px; font-weight: 700; color: var(--ink); opacity: 0.8; }
#factions.alliance .sb-side.us .sb-sum b { color: var(--gold-bright); }
/* the line's total wall — one bar per alliance, the score of the whole war */
.sb-wallbar { order: -1; height: 4px; margin: 0 0 5px; border-radius: var(--r-xs); background: #0b0906;
  border: 1px solid rgba(0,0,0,0.5); overflow: hidden; box-shadow: inset 0 1px 2px rgba(0,0,0,0.65); }
.sb-wallbar i { display: block; height: 100%; width: 100%; transition: width 0.4s ease;
  box-shadow: 0 0 7px rgba(255,220,120,0.35); }
/* a line about to break says so */
#factions.alliance .sb-side.breaking .sb-sum b { color: #ff9c86; animation: sbPulse 1.1s ease-in-out infinite; }
@keyframes sbPulse { 0%,100% { opacity: 1; } 50% { opacity: 0.45; } }

.sb-row.ally { background: linear-gradient(90deg, rgba(110,168,216,0.15), transparent); box-shadow: inset 0 0 0 1px rgba(110,168,216,0.24); }

/* WHICH SIDE, BEFORE WHICH KINGDOM. Eight rows painted in eight kingdom colours
   answer "who is that" and never answer "are they with me" — which on a formation
   with no near half and far half is the only question being asked. The name is
   inked by SIDE and nothing else: blue for your line, red for theirs, and the same
   for a Dwarven ally as for a Dwarven enemy. The crest ring beside it still carries
   the kingdom's own hue, so the row says both things without either one shouting
   over the other. `.me` still overrules both — your own row is gold. */
.sb-row.ourside .sb-name { color: #8ec6f2; }
.sb-row.foeside .sb-name { color: #ff7a68; }
.sb-row.me .sb-name { color: var(--gold-lt); }
.sb-row.foeside { background: linear-gradient(90deg, rgba(216,96,80,0.13), transparent); box-shadow: inset 0 0 0 1px rgba(216,96,80,0.22); }
.sb-row.ourside { background: linear-gradient(90deg, rgba(110,168,216,0.15), transparent); box-shadow: inset 0 0 0 1px rgba(110,168,216,0.24); }
/* eight rows is a lot of topbar — the alliance board runs tighter than the ladder */
#factions.alliance .sb-row { padding: 2px 3px; gap: 6px; border-radius: var(--r-sm); }
#factions.alliance .fcrest { width: 20px; height: 23px; }
#factions.alliance .fcrestwrap { width: 24px; height: 24px; }
#factions.alliance .sb-name { font-size: 11px; }
#factions.alliance .sb-bar { height: 9px; border-radius: var(--r-sm); }
#factions.alliance .sb-bar i { border-radius: var(--r-sm) 0 0 var(--r-sm); }
#factions.alliance .sb-rank { width: 12px; font-size: 11px; }
#factions.alliance .sb-out { font-size: 9.5px; opacity: 0.85; }
/* the keep taking a hit is the reason to look up here at all */
#factions.alliance .sb-row.struck { background: linear-gradient(90deg, rgba(255,90,64,0.34), transparent); }
#factions.alliance .sb-row.struck .sb-bar { box-shadow: 0 0 0 1px rgba(255,120,90,0.75), inset 0 1px 2px rgba(0,0,0,0.6); }
#factions.alliance .sb-row.danger .sb-score { color: #ffbdae; }
#factions.alliance .sb-row.dead { opacity: 0.42; }


/* ---- the match clock, and the escalation it is counting toward ---- */
.sb-mid { display: flex; flex-direction: column; align-items: center; justify-content: flex-start;
  gap: 2px; padding: 0 4px; align-self: stretch; width: 86px; box-sizing: border-box; overflow: hidden; }
.sb-clock { font-family: var(--font-display); font-size: 17px; font-weight: 800; color: var(--gold-lt);
  letter-spacing: 0.5px; font-variant-numeric: var(--nums); text-shadow: 0 2px 5px rgba(0,0,0,0.8); line-height: 1.05; }
.sb-phase { font-family: var(--font-display); font-size: 8.5px; letter-spacing: 0.4px; text-transform: uppercase;
  white-space: nowrap; color: var(--muted); height: 10px; line-height: 10px; overflow: hidden; }
.sb-mid.weary .sb-clock { color: #ffc98a; }
.sb-mid.weary .sb-phase { color: #ffc98a; }
/* a hanging divider either side of the clock, so the board reads as one banner */
.sb-mid::after { content: ''; flex: 1; width: 1px; margin-top: 5px; min-height: 26px;
  background: linear-gradient(180deg, rgba(var(--gold-rgb),0.45), transparent); }


/* ---- a row whose cells never move ------------------------------------
   Every figure has its own fixed cell, right-aligned, in tabular figures. The
   name is the only elastic part and it clips rather than pushing anything. What
   was here grew with whatever it said — a long kingdom name shoved the numbers
   along, a kill tally crossing ten took another character, gold crossing a
   thousand took two more — so the board shifted under the eye all match. And the
   percentage was printed INSIDE the bar in 9px white-on-colour, which is the one
   number you actually look for and was the hardest thing on the panel to read. */
#factions.alliance .sb-top { display: flex; align-items: baseline; gap: 5px; }
#factions.alliance .sb-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Gold is gone. `.sb-g` reported gold EARNED this match, on all eight rows of a
   panel you read mid-battle — a number that says nothing about whether the line
   is holding. Kills is the one stat that survives, and it inherits the space:
   a small chip rather than a bare right-aligned figure, tinted with the row's
   own `--fac` so it picks up the same colour the crest ring wears. */
#factions.alliance .sb-k {
  flex: 0 0 auto; display: inline-flex; align-items: baseline; gap: 2px; justify-content: flex-end;
  min-width: 32px; padding: 1px 6px 1px 5px; border-radius: var(--r-3xl);
  font: 700 10px 'Spectral', serif; color: var(--muted);
  background: color-mix(in srgb, var(--fac, var(--muted)) 16%, transparent);
  border: 1px solid color-mix(in srgb, var(--fac, var(--muted)) 34%, transparent);
  font-variant-numeric: var(--nums); white-space: nowrap;
}
#factions.alliance .sb-k b { font-weight: 800; color: var(--ink); margin-left: 1px; }
/* the bar and its percentage, side by side — the number out in the open */
#factions.alliance .sb-barrow { display: flex; align-items: center; gap: 5px; margin-top: 2px; }
#factions.alliance .sb-barrow .sb-bar { flex: 1 1 auto; margin-top: 0; }
#factions.alliance .sb-barrow .sb-score {
  position: static; transform: none; flex: 0 0 auto; width: 30px; text-align: right;
  font: 800 10px 'Spectral', serif; color: var(--ink); text-shadow: none;
  font-variant-numeric: var(--nums);
}
#factions.alliance .sb-row.danger .sb-barrow .sb-score { color: #ffab97; }
#factions.alliance .sb-row.dead .sb-barrow .sb-score { color: #d99; }
/* the line summary keeps fixed cells too, for the same reason */
.sb-sum b { display: inline-block; min-width: 30px; text-align: right; }
.sb-sum s { display: inline-block; width: 30px; text-align: right; }


/* A figure that changes should be felt, not shoved. `transform` is ignored on an
   inline box, so these are inline-block — and a scale never reflows, where a font
   or padding change would push every cell after it along the row. */
#factions.alliance .sb-k b { display: inline-block; }
#factions.alliance .sb-k b.tick { animation: none; }

/* ---- THE FOLD: the board as a strip, which is how it opens ---------------
   Expanded, this card is eight commanders in full — and about 200px of panel hanging
   across the middle of the screen, which is the part of the screen you are trying to
   look THROUGH. Folded keeps exactly what you read continuously: each line's standing
   keeps, the wall left across it, and the clock. The eight rows are one chevron away.

   NO TRANSITION ON THE HEIGHT. A transition declaration outranks everything else in
   the cascade — inline styles included — so once the box had been clipped to zero the
   stuck transition went on reporting zero over the height applyFold() wrote, and the
   board would fold once and never open again. The height is switched outright and the
   rows fade, which is the part of the movement you actually read anyway.
   `flex: none` on the rows because a column flex item shrinks to fit a constrained
   box: mid-fold the four rows squashed to 4px each instead of vanishing cleanly. */
.sb-rows { display: flex; flex-direction: column; gap: 3px; min-width: 0;
  overflow: hidden; transition: opacity 0.16s ease; }
.sb-rows > .sb-row { flex: none; }
#factions.alliance.folded .sb-rows { opacity: 0; }
/* folded, the line summary IS the score rather than a caption over one, so it is
   read at the size that job deserves — and the wall bar gets some presence back */
#factions.alliance.folded .sb-sidettl { font-size: 10.5px; }
#factions.alliance.folded .sb-sum b { font-size: 14px; }
#factions.alliance.folded .sb-sum s { font-size: 11.5px; }
#factions.alliance.folded .sb-wallbar { height: 6px; margin-bottom: 0; }

/* ---- UNFOLDING IS AN ENTRANCE, NOT A REAPPEARANCE ----
   The rows used to just fade in together at whatever opacity the fold transition
   left them at — eight commanders arriving as one flat block. They now step in
   one after another instead, down each column, the same cascade the menu's mode
   and kingdom cards open with (see `.modegrid .modecard:nth-child`).

   `.unfold-anim` is a SHORT-LIVED class main.js's `setFold` adds only on an actual
   press of the chevron — not on every rebuild that happens to call `applyFold`
   while the board is already open, which would replay the cascade over rows that
   never went anywhere. */
#factions.alliance.unfold-anim .sb-rows > .sb-row {
  animation: sbRowIn 0.3s var(--e-out) both;
}
#factions.alliance.unfold-anim .sb-rows > .sb-row:nth-child(1) { animation-delay: 0.02s; }
#factions.alliance.unfold-anim .sb-rows > .sb-row:nth-child(2) { animation-delay: 0.07s; }
#factions.alliance.unfold-anim .sb-rows > .sb-row:nth-child(3) { animation-delay: 0.12s; }
#factions.alliance.unfold-anim .sb-rows > .sb-row:nth-child(4) { animation-delay: 0.17s; }
@keyframes sbRowIn { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; transform: none; } }

/* the middle column lies down: clock and hinge abreast, the hanging divider gone
   (it divides nothing once the rows are away), and the escalation caption only
   taking a line when it has something to say */
#factions.alliance.folded .sb-mid { flex-direction: row; flex-wrap: wrap;
  justify-content: center; align-items: center; gap: 0 6px; }
#factions.alliance.folded .sb-mid::after { display: none; }
#factions.alliance.folded .sb-clock { order: 1; }
#factions.alliance.folded .sb-tog { order: 2; }
#factions.alliance.folded .sb-phase { order: 3; width: 100%; text-align: center; }
#factions.alliance.folded .sb-phase:empty { display: none; }

/* the hinge itself */
.sb-tog { flex: none; display: flex; align-items: center; justify-content: center;
  width: 22px; height: 13px; padding: 0; margin: 0; cursor: pointer;
  font: 800 8px var(--font-display); line-height: 1; color: var(--gold-lt);
  background: rgba(var(--gold-rgb),0.10); border: 1px solid rgba(var(--gold-rgb),0.30);
  border-radius: var(--r-sm); transition: background 0.15s, border-color 0.15s, color 0.15s; }
.sb-tog:hover { background: rgba(var(--gold-rgb),0.26); border-color: rgba(var(--gold-rgb),0.62); color: var(--gold-bright); }
.sb-tog:active { transform: translateY(1px); }
.sb-tog:focus-visible { outline: 1px solid var(--gold-lt); outline-offset: 1px; }

@media (max-width: 1180px) { #factions.alliance { width: clamp(430px, calc(100vw / var(--ui) - 480px), 580px); } }

/* event herald toasts */
#herald { position: absolute; top: 92px; left: 50%; transform: translateX(-50%); z-index: 8; display: flex; flex-direction: column; align-items: center; gap: 6px; pointer-events: none; width: max-content; max-width: 90vw; }
.herald-msg { font-family: var(--font-flavor); font-size: 15px; color: var(--ink); padding: 7px 18px; border-radius: var(--r-lg); white-space: nowrap; background: linear-gradient(180deg, rgba(38,31,22,0.96), rgba(16,12,8,0.97)); border: 1px solid var(--gold-dk); box-shadow: var(--sh-3), inset 0 1px 0 rgba(var(--lit-rgb),0.1); animation: heraldIn 0.4s var(--e-back); }
.herald-msg b { color: var(--gold-lt); }
.herald-msg.good { border-color: var(--ready); box-shadow: var(--sh-3), 0 0 18px rgba(var(--ready-rgb),0.4); }
.herald-msg.bad { border-color: #e0786a; box-shadow: var(--sh-3), 0 0 18px rgba(224,120,106,0.4); }
.herald-msg.warn { border-color: #e0b96a; box-shadow: var(--sh-3), 0 0 18px rgba(224,185,106,0.4); }
.hl-row.warn { color: #eed9a4; }
.herald-msg.out { animation: heraldOut 0.6s ease forwards; }

/* connection lost — the reconnect window, with a way out of it */
#netlost { position: fixed; inset: 0; z-index: 60; display: flex; align-items: center; justify-content: center; background: rgba(6,4,2,0.72); backdrop-filter: blur(3px); }
#netlost.hidden { display: none; }
.nl-card { text-align: center; padding: 34px 44px; border-radius: var(--r-2xl); background: linear-gradient(180deg, rgba(38,31,22,0.98), rgba(16,12,8,0.99)); border: 1px solid var(--gold-dk); box-shadow: var(--sh-4); max-width: calc(90vw / var(--ui)); zoom: var(--ui); }
.nl-spin { width: 46px; height: 46px; margin: 0 auto 18px; border: 4px solid rgba(var(--gold-rgb),0.18); border-top-color: var(--gold); border-radius: 50%; animation: spin 0.9s linear infinite; }
.nl-ttl { font-family: var(--font-title); font-size: 22px; color: var(--gold-lt); letter-spacing: 1px; }
.nl-sub { font-family: var(--font-flavor); font-size: 14px; color: var(--ink); margin-top: 8px; }
.nl-sub b { color: var(--gold-lt); }
.nl-quit { margin-top: 20px; font-family: var(--font-ui); font-size: 12px; letter-spacing: 0.5px; color: #e0a89a; background: rgba(224,120,106,0.1); border: 1px solid rgba(224,120,106,0.45); border-radius: var(--r-md); padding: 8px 16px; cursor: pointer; }
.nl-quit:hover { background: rgba(224,120,106,0.22); color: #ffd8cd; }
/* a cast vote reads as cast — the button stops inviting a second press */
.menubtn.waiting { opacity: 0.72; cursor: default; filter: saturate(0.7); }
@keyframes heraldIn { from { opacity: 0; transform: translateY(-12px) scale(0.92); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes heraldOut { to { opacity: 0; transform: translateY(-10px); } }

/* ===================================================================== */
/* =====================  GUIDED OPENING / CLARITY  ================== */
/* ===================================================================== */

/* ═══════════════════════════════════════════════════════════════════════════
   THE MUSTER — A COLD OPEN
   ═══════════════════════════════════════════════════════════════════════════
   The economy runs from the instant you're seated, but nothing may be RAISED
   until this reaches CHARGE — so neither side can steal a march off the opening
   frame. Those ten seconds are also the ONLY ten a commander ever gets to look
   at the ground they were dealt, so they are staged rather than merely waited
   out: letterbox closes, the field is revealed wide under a slow push, and three
   acts run over it — the ground, the country, and the sky, ending on the one
   fact that changes how the next twenty minutes are fought.

   Everything here is laid OVER the open field, never on a plate nailed across
   it; a dispatch that hides the ground it describes is a poster, not a briefing.
   Contrast is earned with layered shadow under bright ink, so it reads the same
   on snow, ash, jade or bog. See main.js `startBattleCountdown`.
   Below the first-time briefing's z-index so that modal still wins if both ever
   show. See #buildbar.cdlock for the held rack. */
#battlecountdown { --wxc: #e8cf8a; --wxc-dim: rgba(232,207,138,0.2); position: fixed; inset: 0; z-index: 12; pointer-events: none; }

/* ---- THE HEAD BAR. The cheapest signal that what you are watching is composed
   rather than merely happening: it slides down over 0.7s at the muster and snaps
   back up on CHARGE, and the frame widening is the starting gun.

   THERE IS NO MATCHING BOTTOM BAR, and it is not an oversight. A true letterbox
   needs the bottom eighth of the screen, and that is exactly where the map frame
   lives — so the bar sat directly over the omen glass, which is the thing the
   muster's last act FLIES INTO. The briefing was covering its own destination:
   the card dropped out of sight behind a black band and the landing happened
   underneath it. Symmetry is not worth losing the one beat that connects the
   briefing to the HUD, so the composition is framed from the top alone and the
   whole lower half of the screen is left clear for the field and the fold. */
#battlecountdown .cbar {
  position: absolute; left: 0; right: 0; top: 0; height: 8.5vh; z-index: 2;
  background: linear-gradient(180deg, #050403 62%, rgba(5,4,3,0.86));
  box-shadow: var(--sh-3);
  animation: cbarIn 0.72s var(--e-out) both;
}
@keyframes cbarIn { from { transform: translateY(-100%); } to { transform: none; } }
/* CHARGE: the frame flies open, and the widening IS the starting gun */
#battlecountdown .cbar.out { animation: cbarOut 0.5s var(--e-in) both; }
@keyframes cbarOut { to { transform: translateY(-100%); } }

/* a whisper of the sky's own colour into the frame while the briefing runs — the
   composition is tinted by the weather before the weather has even been named,
   so the reveal lands as a confirmation.

   TOP-WEIGHTED, for the same reason there is no bottom bar. This used to be a
   centred vignette reaching 0.42 black at every extreme, and the darkest place
   on screen was therefore the bottom-right corner — which is the map frame, the
   omen glass, and the exact spot the muster's last act flies into. It falls off
   to nothing below the midline now, so the landing happens in clear air. */
#battlecountdown .cwash {
  position: absolute; inset: 0; z-index: 1; opacity: 0;
  background:
    linear-gradient(180deg, rgba(0,0,0,0.34), rgba(0,0,0,0) 46%),
    linear-gradient(180deg, var(--wxc-dim), rgba(0,0,0,0) 38%);
  animation: cwashIn 1.6s ease-out 0.3s both;
}
@keyframes cwashIn { from { opacity: 0; } to { opacity: 1; } }

/* THE NUMERAL IS A MARK, NOT A CENTREPIECE — until the last three seconds,
   when it becomes the only thing on screen that matters. */
#battlecountdown .cdmini {
  position: absolute; left: 50%; top: calc(8.5vh + 14px); transform: translate(-50%, 0); z-index: 3;
  display: flex; align-items: baseline; gap: 9px;
  transition: transform 0.6s var(--e-out), opacity 0.4s ease;
}
#battlecountdown .cdlabel {
  font: 800 11px var(--font-display); letter-spacing: 3.4px; text-transform: uppercase;
  color: rgba(240,222,176,0.72); text-shadow: 0 2px 8px rgba(0,0,0,0.95);
  transition: opacity 0.5s ease;
}
#battlecountdown .cdnum {
  display: inline-block; font: 900 32px var(--font-logo); line-height: 1; min-width: 1.3em; text-align: center;
  background: linear-gradient(100deg, var(--gold-dk) 0%, var(--gold-hi) 18%, var(--gold-bright) 28%, var(--gold-lt) 40%, var(--gold) 58%, var(--gold-dk) 100%);
  background-size: 230% auto; -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
  filter: drop-shadow(0 2px 8px rgba(0,0,0,0.85)) drop-shadow(0 0 12px rgba(var(--gold-rgb),0.45));
  animation: cdPop 0.5s var(--e-out) both;
  transition: font-size 0.5s var(--e-out);
}
/* the last three seconds: the count steps forward and swells */
#battlecountdown .cdmini.final { transform: translate(-50%, 2vh); }
#battlecountdown .cdmini.final .cdlabel { opacity: 0; }
#battlecountdown .cdmini.final .cdnum { font-size: 72px; }

/* ---- CHARGE -------------------------------------------------------------- */
/* ═══ CHARGE ══════════════════════════════════════════════════════════════
   The muster no longer speaks, so this is the only thing it says — and it gets
   the whole budget the three spoken acts used to spend between them. Six parts,
   all CSS off one element, so the simulation pays nothing for it and reduced
   motion can take the lot away in one rule at the foot of this block. */
#battlecountdown .cdgo {
  position: absolute; left: 50%; top: 42%; z-index: 3;
  display: block; font: 900 clamp(46px, 9vw, 96px) var(--font-logo); white-space: nowrap;
  animation: cdPopCenter 0.55s var(--e-back) both;
}
/* THE LETTERS SLAM. They used to fade up already at their spacing; now they
   arrive out of a wide track and close, which is the difference between a title
   appearing and a word landing. Overshoots tight, settles at 6. */
#battlecountdown .cdgo b {
  display: inline-block; font-weight: inherit;
  background: linear-gradient(100deg,
    #a8761f 0%, #c08f26 14%, #ffd66a 30%, #fffbe8 40%, #ffe9a8 50%, #c08f26 66%, #a8761f 100%);
  background-size: 280% 100%; background-position: 128% 0;
  -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; color: transparent;
  filter: drop-shadow(0 3px 14px rgba(0,0,0,0.95)) drop-shadow(0 0 36px rgba(255,200,90,0.8));
  animation: cdSlam 0.62s var(--e-back) both, cdSheen 1.05s 0.16s var(--e-out) both;
}
/* IN `em`, NOT PIXELS. The face is clamp(46px, 9vw, 96px), so a fixed 46px of
   tracking is a tenth of the word at full size and most of a phone's width at
   the small end — measured, it took CHARGE to 1252px inside a 1280px viewport
   on the way in. Proportional tracking opens by the same amount relative to the
   letters at every size, and cannot outgrow the screen it is landing on. */
@keyframes cdSlam {
  0%   { opacity: 0; letter-spacing: 0.42em; }
  58%  { opacity: 1; letter-spacing: 0.03em; }
  100% { opacity: 1; letter-spacing: 0.065em; }
}
/* the struck-metal sweep: one pass of the bright band down the whole word */
@keyframes cdSheen { from { background-position: 128% 0; } to { background-position: -28% 0; } }

/* TWO SHOCKWAVES, A BEAT APART — one ring is a circle, two are an impact. */
#battlecountdown .cdgoring {
  position: absolute; left: 50%; top: 50%; width: 40px; height: 40px; margin: -20px 0 0 -20px;
  border: 3px solid rgba(var(--gilt-lit-rgb),0.9); border-radius: 50%;
  animation: cdRing 0.85s var(--e-out) both;
}
#battlecountdown .cdgoring.two {
  border-color: rgba(255,236,176,0.55);
  animation: cdRing 1.05s 0.14s var(--e-out) both;
}
@keyframes cdRing {
  from { opacity: 0.95; transform: scale(0.4); border-width: 5px; }
  to   { opacity: 0; transform: scale(26); border-width: 0.5px; }
}

/* THE RAYBURST. Struck spokes turning behind the word, masked to a soft disc so
   it never reaches the edge of the screen and becomes wallpaper. */
#battlecountdown .cdgoburst {
  position: absolute; left: 50%; top: 50%; width: 150vmin; height: 150vmin;
  margin: -75vmin 0 0 -75vmin; border-radius: 50%; z-index: -1;
  background: repeating-conic-gradient(from 0deg,
    rgba(255,228,158,0.15) 0deg 2.6deg, rgba(255,228,158,0) 2.6deg 10deg);
  -webkit-mask-image: radial-gradient(closest-side, rgba(0,0,0,0.95), rgba(0,0,0,0) 66%);
          mask-image: radial-gradient(closest-side, rgba(0,0,0,0.95), rgba(0,0,0,0) 66%);
  animation: cdBurst 1.15s var(--e-out) both;
}
@keyframes cdBurst {
  0%   { opacity: 0; transform: scale(0.32) rotate(0deg); }
  18%  { opacity: 1; }
  100% { opacity: 0; transform: scale(1.12) rotate(10deg); }
}

/* CROSSED BLADES, STRUCK OUT EITHER SIDE. They travel outward from under the
   word, so the mark reads as the word throwing them rather than as furniture
   that was always there. */
#battlecountdown .cdgoblade {
  position: absolute; top: 50%; font-size: clamp(24px, 4.4vw, 52px);
  color: rgba(var(--gilt-lit-rgb),0.92); line-height: 1;
  filter: drop-shadow(0 2px 8px rgba(0,0,0,0.9)) drop-shadow(0 0 16px rgba(255,200,90,0.55));
}
#battlecountdown .cdgoblade svg { width: 1em; height: 1em; display: block; }
#battlecountdown .cdgoblade.l { right: 50%; animation: cdBladeL 0.72s 0.1s var(--e-back) both; }
#battlecountdown .cdgoblade.r { left: 50%;  animation: cdBladeR 0.72s 0.1s var(--e-back) both; }
@keyframes cdBladeL {
  0%   { opacity: 0; transform: translate(2.2em, -50%) scale(0.6) rotate(-40deg); }
  100% { opacity: 1; transform: translate(-0.55em, -50%) scale(1) rotate(0deg); }
}
@keyframes cdBladeR {
  0%   { opacity: 0; transform: translate(-2.2em, -50%) scale(0.6) rotate(40deg); }
  100% { opacity: 1; transform: translate(0.55em, -50%) scale(1) rotate(0deg); }
}

/* …and a rule drawn out under the whole thing, last, so the beat closes */
#battlecountdown .cdgorule {
  position: absolute; left: 50%; top: calc(50% + 0.66em); height: 2px; width: 0;
  transform: translateX(-50%);
  background: linear-gradient(90deg, rgba(0,0,0,0), rgba(var(--gilt-lit-rgb),0.95), rgba(0,0,0,0));
  animation: cdRule 0.66s 0.14s var(--e-out) both;
}
@keyframes cdRule { from { width: 0; opacity: 0; } to { width: min(760px, 86vw); opacity: 1; } }

/* A COMMANDER WHO ASKED FOR STILLNESS STILL GETS THE WORD. Everything that
   travels, turns or sweeps is dropped; CHARGE itself lands, gold and legible. */
@media (prefers-reduced-motion: reduce) {
  #battlecountdown .cdgoburst,
  #battlecountdown .cdgoring { display: none; }
  #battlecountdown .cdgo { animation: none; transform: translate(-50%, -50%); }
  #battlecountdown .cdgo b { animation: none; letter-spacing: 0.065em; background-position: 42% 0; }
  #battlecountdown .cdgoblade.l { animation: none; transform: translate(-0.55em, -50%); }
  #battlecountdown .cdgoblade.r { animation: none; transform: translate(0.55em, -50%); }
  #battlecountdown .cdgorule { animation: none; width: min(760px, 86vw); }
}

/* ---- THE ACTS ------------------------------------------------------------ */
#battlecountdown .cdstage {
  position: absolute; left: 50%; top: 47%; transform: translate(-50%, -50%); z-index: 3;
  width: min(760px, 88vw); text-align: center;
}

/* the eyebrow: a small ruled caption, with the rules DRAWING OUT from the words */


/* the title — the biggest thing the muster says */

/* the blurb: the sentence that says what KIND of war this seed dealt */
#battlecountdown .cdsub {
  max-width: 46ch; font: italic 500 15px var(--font-ui); line-height: 1.45;
  color: rgba(238,230,212,0.9); text-shadow: 0 2px 8px rgba(0,0,0,1), 0 0 18px rgba(0,0,0,0.9);
}
/* the country — a quieter register than the name that dealt it */


/* ---- ACT III: THE OMEN, WEARING ITS SKY ----------------------------------
   The only beat that touches the numbers gets the whole screen and the sky's own
   colour (--wxc, from the shared weather table). The struck plate carries
   `omenTell` — generated from the rules the server is running, never written by
   hand — and it already ends "on both sides", which is the sentence's real work:
   the commonest misreading of a map condition is that the other commander is
   doing it to you. */

@keyframes skyGlyph { 0%,100% { transform: scale(1); } 50% { transform: scale(1.06); } }
/* the sky, reduced to a TAG on the omen's card. It used to be a second headline
   in its own right — an all-caps cry plus the sky's name on its own line — which
   made the beat read as two announcements competing for the same moment. The
   omen owns the rules, so the omen owns the headline; the sky rides along as a
   small stamped label, which is also exactly how it appears in the glass this
   card is about to become. */


/* ---- THE SCOREBOARD ------------------------------------------------------
   The numbers were a sentence on a dark plate. Nobody reads a sentence in a
   two-and-a-half-second window over a battlefield, and the plate itself was a
   black box floating over the field — the least game-looking object in the game.

   They are TOKENS now: the value large enough to be taken in without reading,
   the stat under it, and who it lands on under that. Generated from the same
   rules the simulation ran, so the scoreboard and the prose could not disagree.
   Colour is by DIRECTION, which is the fastest sort
   an eye can do — and because up/down could be misread as "good/bad for ME",
   the row says `Both armies` underneath, once, in words. */

/* they land one after another, left to right, like a hand of cards dealt out */

@keyframes omChipIn {
  from { opacity: 0; transform: translateY(12px) scale(0.9); }
  to   { opacity: 1; transform: none; }
}
/* THE NUMBER. This is the object on the whole card that has to survive being
   glanced at, so it is set larger than the stat it belongs to. */


/* ---- THE TAGS: what this is CALLED, at the size a name deserves -----------
   The omen's name used to be the 44px headline of the whole muster and the sky
   was a tag under it. They are peers now, and neither is the headline — a name
   is a handle for talking about a battle afterwards, not something a commander
   can open on. Same pill, same weight, same type: the omen's carries the sky's
   colour at full strength, the sky's a step quieter, because the omen is the one
   that owns the rules. */
#battlecountdown .omtags {
  display: flex; flex-wrap: wrap; justify-content: center; align-items: center;
  gap: var(--gutter); margin-top: var(--sp-3);
}

/* THE SCOREBOARD IS THE HEADLINE NOW, so it is set at headline scale. It also
   gets the room to wrap to a second rank without the card fighting it — an omen
   that moves five numbers is telling you five things, and shrinking them to fit
   one line is how a briefing becomes decoration. */


/* ---- WHAT IT BECOMES ------------------------------------------------------
   A turning omen used to say only that it turns, and when — a chip reading
   IT TURNS 6:00. That is a warning with the actionable half removed: you were
   told to expect a change in the one window where knowing WHICH change would
   have shaped your opening build. The second phase is drawn in full, in the same
   tokens, under its own clock, one step quieter so the rank you are fighting
   under right now still reads first. */
#battlecountdown .omwhen {
  display: flex; align-items: center; gap: var(--sp-4);
  margin-top: var(--sp-5); width: min(94vw, 560px);
  font: 800 11px var(--font-display); letter-spacing: 4px; text-transform: uppercase;
  color: var(--wxc); opacity: 0.92; text-shadow: 0 2px 6px rgba(var(--shade-rgb), 1);
  animation: omChipIn 0.42s var(--e-back) 0.34s both;
}
#battlecountdown .omwhen i { flex: 1; height: 1px; background: linear-gradient(90deg, transparent, var(--wxc-dim)); }
#battlecountdown .omwhen i:last-child { background: linear-gradient(90deg, var(--wxc-dim), transparent); }
/* the clock on a turning omen — the one token that is neither gain nor loss,
   because "it changes at 2:45" is a schedule, not a modifier */

@keyframes omTurn {
  0%, 100% { box-shadow: var(--sh-3), 0 0 0 0 rgba(232,207,138,0.3); }
  50%      { box-shadow: var(--sh-3), 0 0 26px 0 rgba(232,207,138,0.28); }
}
/* Still Air: the roll where the field is simply the field. It gets a token too,
   rather than an absence — "NONE" read at a glance is information; a blank space
   where the numbers usually are is just a commander wondering what they missed. */

/* said ONCE, under the row, because the commonest misreading of a map condition
   is that the other commander is doing it to you */


/* ---- ACT II: the country, as tokens too ---------------------------------- */

/* an act with nothing but tokens has no title to leave a gap for */

/* THE FOLD: everything but the glyph and the name clears away first, and THAT
   is what drops into the corner — so what lands in the glass is visibly the same
   object that just spoke */

/* mid-flight: the transform is driven by the Web Animations API (the target is
   the strip's actual on-screen rect, which CSS alone cannot read), so this only
   has to get out of the transition's way while that runs */


/* (There was a SKIP MARK in the lower bar. It is gone on purpose: the muster
   gate is the server's, so the ten seconds are being waited out whether or not
   anything is drawn over them, and a skip could never have returned a second to
   anyone — only traded the briefing for a blank field. See the header in
   main.js. The camera still yields to any input, which is the part a commander
   can actually have an intention about.) */

@keyframes cdPop {
  0% { opacity: 0; transform: scale(1.55); }
  55% { opacity: 1; transform: scale(0.96); }
  100% { opacity: 1; transform: scale(1); }
}
/* Same pop as `cdPop`, but for an element `.cdgo` centres with its own
   `translate(-50%,-50%)` — an animated `transform` replaces the property's value
   outright rather than composing with it, so `cdPop`'s bare `scale()` was wiping
   that centring out for the animation's whole run (and forever after, since
   `fill: both` freezes on the last keyframe). Baking the translate into every
   step keeps CHARGE centred through the pop instead of just before it. */
@keyframes cdPopCenter {
  0% { opacity: 0; transform: translate(-50%, -50%) scale(1.7); }
  55% { opacity: 1; transform: translate(-50%, -50%) scale(0.94); }
  100% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}

/* Landscape phones: the sky card is the tallest thing the muster draws — glyph,
   name, cry, struck modifier plate and a sentence — and 390px of height minus two
   letterbox bars is not enough to stand it up. Everything shrinks; nothing is
   dropped, because the modifier is the entire point of the beat. */
@media (max-height: 560px) {
  #battlecountdown .cbar { height: 6vh; }
  #battlecountdown .cdmini { top: calc(6vh + 8px); }
  #battlecountdown .cdmini.final .cdnum { font-size: 46px; }
  #battlecountdown .cdstage { top: 50%; }
  #battlecountdown .cdsub { font-size: 12.5px; max-width: 52ch; }
  /* the promoted scoreboard and the turning row have to come back DOWN on a short
     screen like everything else here — their selectors carry an extra class each,
     so without these the generic .omchip rules above are simply outranked and a
     landscape phone gets desktop-sized tokens */

  #battlecountdown .omtags { margin-top: 5px; gap: 6px; }
  #battlecountdown .omwhen { margin-top: 8px; font-size: 9.5px; letter-spacing: 3px; gap: 8px; }
  #battlecountdown .cdgo { font-size: clamp(34px, 7vw, 58px); }
}

/* A commander who asked their OS for reduced motion keeps every WORD of the
   muster and loses the moving parts: no push, no blur-in, no pulsing plate. */
@media (prefers-reduced-motion: reduce) {
  /* …and the glow that was breathing simply sits at half, so the two marks the
     glass carries are still readable as marks rather than disappearing with the
     motion that drew attention to them */

  /* the tokens still ARRIVE — they just arrive at once, without the dealt-hand
     stagger, because the scoreboard is the information and not the flourish */

  #battlecountdown .cdgoring { display: none; }
}
@keyframes briefPop { from { opacity: 0; transform: translateY(14px) scale(0.97); } to { opacity: 1; transform: none; } }


/* ---- full-screen impact flash (driven by JS via element.animate) ---- */
#fxflash {
  position: fixed; inset: 0; z-index: 36; pointer-events: none; opacity: 0;
  mix-blend-mode: screen; background: radial-gradient(circle at 50% 50%, rgba(255,240,190,0.0), rgba(255,240,190,0.0));
}

/* ---- the corner rail: sound, score, the reference ------------------------
   THREE CHIPS, ONE PITCH, ONE SIZE — and the size is a size on the GLASS. Each
   of these carries `zoom: var(--ui)` like every other HUD panel, so the flat 40
   (46 on a coarse pointer) they were written at is only that at --ui 1. On the
   shipped build they measured 27px on a 1024×640 window and 37px on a tablet —
   under any touch-target guidance, for the switch a player reaches for first when
   the music starts over the top of something else.
   Dividing by --ui cancels the zoom; the floor keeps them from growing past
   today's look where --ui runs above 1. The stack is derived from the chip rather
   than restated three times, because the two were drifting apart every time one
   of them changed: `bottom` was hand-tuned per media query and a 46px chip was
   still being stacked on a 46px pitch, so the chips touched. */
:root { --chip: max(40px, calc(40px / var(--ui))); --chipgap: 6px; }
@media (pointer: coarse) { :root { --chip: max(46px, calc(46px / var(--ui))); } }
#sfxtoggle {
  position: fixed; left: calc(14px + var(--sal) / var(--ui)); bottom: calc(12px + var(--sab) / var(--ui)); z-index: 45;
  width: var(--chip); height: var(--chip);
  zoom: var(--ui);
  display: flex; align-items: center; justify-content: center; font-size: 18px; cursor: pointer;
  color: var(--gold-lt); background: var(--plate);
  border: 1px solid var(--edge); border-radius: var(--r-lg);
  box-shadow: var(--sh-2), inset 0 1px 0 rgba(var(--lit-rgb),0.1); transition: border-color 0.14s, transform 0.08s, filter 0.12s;
}
#sfxtoggle:hover { border-color: var(--gold-lt); transform: translateY(-2px); filter: brightness(1.1); }
#sfxtoggle:active { transform: translateY(0); }
#sfxtoggle.muted { color: var(--muted); filter: grayscale(0.5); }
/* the score's switch, sitting on the same rail one chip up */
#musictoggle {
  position: fixed; left: calc(14px + var(--sal) / var(--ui));
  bottom: calc(12px + var(--chip) + var(--chipgap) + var(--sab) / var(--ui)); z-index: 45;
  width: var(--chip); height: var(--chip);
  zoom: var(--ui);
  display: flex; align-items: center; justify-content: center; font-size: 18px; cursor: pointer;
  color: var(--gold-lt); background: var(--plate);
  border: 1px solid var(--edge); border-radius: var(--r-lg);
  box-shadow: var(--sh-2), inset 0 1px 0 rgba(var(--lit-rgb),0.1); transition: border-color 0.14s, transform 0.08s, filter 0.12s;
}
#musictoggle:hover { border-color: var(--gold-lt); transform: translateY(-2px); filter: brightness(1.1); }
#musictoggle:active { transform: translateY(0); }
#musictoggle.muted { color: var(--muted); filter: grayscale(0.5); }
/* (the detail toggle that sat one chip further up this rail is gone — picture
   detail is always the frame governor's own live decision; see Renderer._pace) */

/* ---- bottom-left corner: the keybind reference (in-match only) ----------
   The battle log's button stood below this one. Both it and the panel it opened
   are gone — a scrollback of things that had already happened on screen, kept
   behind a button, over the ground the commander is trying to read. The
   reference drops into the slot it left rather than floating above a gap. */
#keybtn {
  position: absolute; left: calc(14px + var(--sal) / var(--ui)); z-index: 7;
  width: var(--chip); height: var(--chip); pointer-events: auto;
  zoom: var(--ui);
  display: flex; align-items: center; justify-content: center; font-size: 15px; cursor: pointer;
  font-family: var(--font-display); font-weight: 800; color: var(--gold-lt);
  background: var(--plate);
  border: 1px solid var(--edge); border-radius: var(--r-lg);
  box-shadow: var(--sh-2); transition: border-color 0.14s, transform 0.08s, filter 0.12s;
}
#keybtn:hover { border-color: var(--gold-lt); transform: translateY(-2px); filter: brightness(1.1); }
#keybtn { bottom: calc(12px + 2 * (var(--chip) + var(--chipgap)) + var(--sab) / var(--ui)); }

#heraldlog {
  position: absolute; left: 14px; bottom: 148px; z-index: 20; width: 300px; max-height: 320px;
  overflow-y: auto; pointer-events: auto; scrollbar-width: thin; scrollbar-color: var(--gold-dk) transparent;
  background: linear-gradient(165deg, rgba(30,24,16,0.98), rgba(12,9,6,0.99));
  border: 1px solid var(--edge-strong); border-radius: var(--r-lg);
  box-shadow: var(--sh-4); padding: 8px;
}
#heraldlog.hidden { display: none; }
.hl-hdr { font-family: var(--font-display); font-size: 11px; font-weight: 700; letter-spacing: 0.6px; color: var(--gold-lt); padding: 2px 4px 7px; border-bottom: 1px solid var(--edge); margin-bottom: 5px; }
.hl-row { font-size: 12px; line-height: 1.4; padding: 5px 4px; color: var(--ink); font-family: var(--font-ui); }
.hl-row + .hl-row { border-top: 1px solid rgba(255,255,255,0.05); }
.hl-row.good { color: #bfe6b0; }
.hl-row.bad { color: #f2a99a; }
.hl-empty { text-align: center; color: var(--muted); font-size: 12px; padding: 16px 6px; }

/* ---- keybind reference modal ---- */
#keymodal {
  position: fixed; inset: 0; z-index: 90; pointer-events: auto;
  display: flex; align-items: center; justify-content: center; padding: 20px;
  background: radial-gradient(circle at 50% 40%, rgba(8,6,3,0.55), rgba(4,3,1,0.82));
}
#keymodal.hidden { display: none; }
.km-card {
  zoom: var(--ui);
  width: min(560px, calc(94vw / var(--ui))); max-height: calc(86vh / var(--ui)); overflow-y: auto;
  background: linear-gradient(165deg, rgba(44,36,22,0.99), rgba(16,12,7,0.99));
  border: 1px solid var(--edge-strong); border-radius: var(--r-2xl);
  box-shadow: var(--sh-4), inset 0 1px 0 rgba(var(--lit-rgb),0.12);
  padding: 18px 22px 22px;
}
.km-head { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
.km-title { font-family: var(--font-logo); font-weight: 900; font-size: 19px;
  background: linear-gradient(180deg, var(--gold-hi), var(--gold) 60%, var(--gold-dk)); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
.km-x { margin-left: auto; flex: none; width: 30px; height: 30px; border-radius: var(--r-lg); cursor: pointer; font-size: 17px; line-height: 1; color: #d8c69a;
  background: rgba(0,0,0,0.3); border: 1px solid var(--edge); }
.km-x:hover { filter: brightness(1.2); border-color: var(--gold-lt); color: #fff; }
.km-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 16px; }
/* ---- THE PICTURE READOUT -------------------------------------------------
   Live numbers for what this machine is actually drawing: tier, render scale,
   canvas pixels against panel pixels, and where the frame time went. It lives on
   BOTH references, not just the phone's, because "pixelated when I go full
   screen" is a desktop report too and it is the same three numbers that settle
   it. `pre-line` because paceLine() returns two lines and the second one is a
   breakdown that must not reflow into the first. */
/* …FOLDED, THOUGH. Open by default it was the last thing — and the largest single
   block — on the sheet a commander opens to ask how to give an order: five lines of
   render tier, canvas budget, sample ratio and frame timings, in a panel whose job
   is to answer a beginner's question. One line at rest, the whole readout one tap
   away for the handset that needs reading. */
.km-diagwrap { margin-top: 14px; padding-top: 10px; border-top: 1px solid var(--edge-quiet); }
.km-diagwrap > summary {
  cursor: pointer; list-style: none; display: flex; align-items: center; gap: 6px;
  font: 700 10.5px var(--font-display); letter-spacing: 0.7px; text-transform: uppercase;
  color: var(--gilt-body); padding: 3px 2px; border-radius: var(--r-sm);
  transition: color 0.14s;
}
.km-diagwrap > summary::-webkit-details-marker { display: none; }
.km-diagwrap > summary::before { content: '▸'; font-size: 10px; transition: transform 0.16s var(--e-out); }
.km-diagwrap[open] > summary::before { transform: rotate(90deg); }
.km-diagwrap > summary:hover { color: var(--gold-lt); }
.km-diag {
  margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--edge-quiet);
  font: 600 10.5px var(--font-ui); letter-spacing: 0.2px; color: var(--muted);
  font-variant-numeric: var(--nums); text-align: center; line-height: 1.6;
  white-space: pre-line;
}
.km-grp-ttl { font-family: var(--font-display); font-weight: 800; font-size: 12px; letter-spacing: 0.6px; color: var(--gold-lt); margin-bottom: 6px; }
.km-row { display: flex; justify-content: space-between; gap: 10px; font-size: 12.5px; padding: 3px 0; color: var(--ink); font-family: var(--font-ui); }
/* THE TOUCH REFERENCE READS THE OTHER WAY ROUND. On a keyboard the question is
   "what is the key for X", so the description leads and the cap answers; with a
   gesture the question is "what does this do", so the gesture leads. Two columns
   rather than a `space-between` row, because a gesture chip is three words wide
   and a description is ten — pushed apart they read as two unrelated lists. */
/* Keyed off the GROUP rather than the sheet: a Surface or an iPad with a keyboard
   case is handed the gesture groups AND the keyboard groups on one sheet, and
   `.km-touch .km-row` would have laid the keyboard half out as gestures. */
.km-gest .km-row {
  display: grid; grid-template-columns: minmax(0, 8.5em) minmax(0, 1fr);
  gap: 4px 9px; align-items: start; padding: 4px 0; line-height: 1.3;
}
.km-gest .km-row kbd { white-space: normal; text-align: center; }
.km-row kbd { font-family: var(--font-display); font-weight: 700; color: #1a140c; background: linear-gradient(180deg, var(--gold-lt), var(--gold)); padding: 1px 7px; border-radius: var(--r-sm); font-size: 11px; white-space: nowrap; }
/* ===================================================================== */
/* ======================  THE ARMORY  ================================= */
/* 150 things on one flat shelf is a warehouse. This is a shop: a filter rail
   at the top, then one section per CATEGORY carrying that category's colour —
   the same colour it wears on the build cards in battle — so "what kind of
   thing is this" is answered by looking, not by reading. */

/* ---- headline: a real progress bar, not just a number ---- */
.armbar { align-items: center; }
.armprog { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 12px; flex: 1; min-width: 0; }
/* the bar gives up its stretch so the tally and the prompt sit next to it rather
   than being pushed to the far end of a 1900px header */
.armprog .armprogbar { flex: 0 1 340px; }
.armprogbar { flex: 1; max-width: 340px; height: 9px; border-radius: var(--r-sm); overflow: hidden;
  background: rgba(0,0,0,0.42); border: 1px solid var(--edge); }
.armprogbar i { display: block; height: 100%; border-radius: var(--r-sm);
  background: linear-gradient(90deg, var(--gold), var(--gold-lt)); box-shadow: 0 0 10px rgba(246,221,146,0.5); transition: width 0.35s ease; }
.armcount { color: var(--gold-lt); font-weight: 800; font-variant-numeric: var(--nums); white-space: nowrap; }
.armcount em { font-style: normal; color: var(--muted); font-weight: 600; }

/* ---- the filter rail ---- */
.armtools { margin: 0 26px 10px; display: flex; flex-direction: column; gap: 8px; }
.armrow { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.armsearch { display: flex; align-items: center; gap: 7px; flex: 1; min-width: 200px; max-width: 420px;
  padding: 7px 12px; background: rgba(0,0,0,0.34); border: 1px solid var(--edge); border-radius: var(--r-lg); }
.armsearch span { opacity: 0.6; font-size: 13px; }
.armsearch input { flex: 1; min-width: 0; background: none; border: none; outline: none;
  font-family: var(--font-ui); font-size: 13.5px; color: var(--ink); }
.armsearch input::placeholder { color: rgba(180,170,150,0.45); }
.armsearch:focus-within { border-color: var(--gold-dk); box-shadow: 0 0 0 2px rgba(var(--gold-rgb),0.14); }
.armstates { display: flex; gap: 5px; }
.armchip { --cat: var(--gold); cursor: pointer; white-space: nowrap;
  padding: 6px 12px; border-radius: var(--r-lg); font-family: var(--font-ui); font-size: 12.5px; font-weight: 600;
  color: var(--ink); background: rgba(255,255,255,0.045); border: 1px solid var(--edge); transition: 0.13s; }
.armchip:hover { background: rgba(255,255,255,0.1); border-color: var(--cat); }
.armchip b { font-variant-numeric: var(--nums); opacity: 0.55; font-weight: 700; margin-left: 3px; }
.armchip.on { color: #14100a; background: var(--cat); border-color: var(--cat); font-weight: 800; }
.armchip.on b { opacity: 0.7; color: inherit; }
.armchip.ready.on { background: var(--ready); border-color: var(--ready); }
/* The row of seven kingdom CHIPS (`.armkings` / `.armlbl`) is long gone. Choosing a
   kingdom is the rail beside the shelf now — see `.armlayout` below. */
/* `.armonly` — the ★ Exclusive toggle — is gone with the idea it filtered on. Every
   troop belongs to exactly one kingdom and the shelf already belongs to one, so it
   could never hide a unit; all it did was drop the shared structures. */

/* Grouping control — a plain select, because it is one choice from five and a rail of
   chips for it would compete with the two rails that matter more. */
.armsort { display: flex; align-items: center; gap: 6px; font: 700 10.5px var(--font-display);
  letter-spacing: 1.6px; text-transform: uppercase; color: var(--muted); }
.armsort select { font: 600 12px var(--font-body); color: var(--ink); cursor: pointer;
  background: rgba(0,0,0,0.34); border: 1px solid var(--edge); border-radius: var(--r-lg); padding: 6px 8px; outline: none; }
.armsort select:focus { border-color: var(--gold-dk); }

/* How much of the shelf you are actually looking at, and one press back to all of it */
.armresult { justify-content: space-between; }
.armshown { font-size: 12px; color: var(--muted); }
.armshown b { color: var(--gold-lt); font-variant-numeric: var(--nums); }
.armclear { cursor: pointer; font: 600 11.5px var(--font-body); color: var(--gold-lt);
  background: rgba(var(--gold-rgb),0.1); border: 1px solid var(--gold-dk); border-radius: var(--r-lg); padding: 5px 11px; }
.armclear:hover { background: rgba(var(--gold-rgb),0.2); }
.armempty .armclear { margin-top: 12px; }

/* The in-battle price, kept visually apart from the unlock button in the footer —
   they are different currencies of attention and must not read as one number. */
.afield { display: flex; align-items: baseline; gap: 5px; margin: 4px 0 2px; padding: 3px 7px;
  border-radius: var(--r-md); background: rgba(0,0,0,0.26); border: 1px solid rgba(var(--gold-rgb),0.16);
  font: 700 11px var(--font-ui); color: var(--gold-lt); }
.afield i { font-style: normal; font-size: 10px; opacity: 0.85; }
.afield em { margin-left: auto; font-style: normal; font-weight: 600; font-size: 9px;
  letter-spacing: 0.2px; color: var(--muted); }

/* ===================================================================== */
/* ==============  ARMORY STEP ONE: WHICH ARMY?  ====================== */
/* ===================================================================== */
/* ===================  THE ARMORY, LAID OUT LIKE THE MUSTER  ===================
   `.armkgrid` / `.armking` are gone. The shelf used to be reached THROUGH a page of
   seven kingdom cards, so comparing two armouries meant a round trip via a screen that
   showed neither — and the two places in the game that ask "which kingdom?" looked
   nothing alike. The Armory now borrows the muster's whole skeleton: the same rail of
   the same banners down the left, one deep panel beside it. Everything the rail tile
   needs is already in `.mtile`; what is added here is the collection's progress, which
   is the only thing this rail says that the muster's does not. */
/* …AND IT BORROWS THE MUSTER'S CEILING WITH THE REST OF THE SKELETON. Same screen,
   same three-part shape, same failure on an ultrawide: the rail went to one edge and
   the shelf ran to the other, so a category's four cards sat at the left of a track
   half a metre wide and the eye had to cross the whole monitor to compare two of
   them. Capped at the same 1900px the muster uses and centred in what is left, so
   the two screens are the same size as well as the same shape — you walk between
   them and nothing moves. Narrower windows are untouched. (See `.musterwrap`.) */
.armwrap { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; padding: 0 26px 18px;
  width: 100%; max-width: 1900px; margin-inline: auto; }
/* the progress bar above it is part of the same column, so it takes the same measure
   rather than running the full width of the screen over a centred shelf. Its cap is
   the wrap's LESS the wrap's own 26px gutters, because this bar has margins where the
   wrap has padding — matched any other way the bar overhangs the shelf by 26px a
   side. `100% - 52px` keeps those same gutters on every window under the cap, which
   is what the plain `margin: 0 26px` was doing before there was a cap at all.
   `width: 100%` is not decoration either: auto side margins stop a column-flex item
   being stretched to its container, so without a stated width the bar shrink-wraps to
   its own text and sits as a 412px tab in the middle of an empty header. */
.armbar { width: 100%; max-width: min(1848px, 100% - 52px); margin-inline: auto; }
.armlayout { flex: 1 1 auto; min-height: 0; display: grid;
  grid-template-columns: minmax(210px, 254px) 1fr; grid-template-rows: minmax(0, 1fr);
  gap: 14px; align-items: stretch; }
/* Same rail as the muster's, same floor under the tiles, same "share the column"
   behaviour — see .musterrail for why the tiles flex rather than huddling at the top. */
.armrail { --mtmin: 74px; display: flex; flex-direction: column; gap: 7px;
  min-height: 0; overflow-y: auto; padding-right: 4px; }
.armrail .mtile { flex: 1 1 auto; min-height: var(--mtmin); }
/* the panel the shelf lives in: its own scroller, so the rail stays put while you read */
.armdetail { display: flex; flex-direction: column; min-height: 0; overflow-y: auto; padding-right: 4px; }
.armdetail .armtools { margin: 0 0 10px; }

/* ---- what the rail tile adds: how far through this armoury you are ---- */
.at-bar { height: 5px; margin: 5px 0 3px; border-radius: var(--r-xs); overflow: hidden;
  background: rgba(0,0,0,0.5); box-shadow: inset 0 1px 2px rgba(0,0,0,0.7); }
.at-bar i { display: block; height: 100%; border-radius: var(--r-xs);
  background: linear-gradient(90deg, var(--gilt-deep), var(--gold-bright));
  box-shadow: 0 0 7px rgba(255,225,150,0.55); transition: width 0.3s ease; }
/* LIGHT INK ON DARK CLOTH. This line was #4a3a18 with a #2b2008 tally — brown on
   near-black — written for a pale gold field and then left there when the banner
   became a dark hanging one. It was not small so much as invisible. Same figures,
   in the ink the name beside them uses, and a stop larger so the rail can be read
   at a glance rather than leaned into. */
.at-num { display: flex; align-items: baseline; gap: 6px; font: 700 12px var(--font-ui); color: #e6dabc;
  text-shadow: 0 1px 2px rgba(0,0,0,0.9); }
.at-num b { font: 800 15px var(--font-display); color: var(--gold-bright); }
.at-num em { font-style: normal; opacity: 0.8; }
.at-num s { text-decoration: none; margin-left: auto; font: 800 10.5px var(--font-display);
  letter-spacing: 0.4px; text-transform: uppercase; white-space: nowrap; }
.at-ready { color: var(--ready-lt); text-shadow: 0 0 8px rgba(var(--ready-rgb),0.6); }
.at-done { color: var(--gold-hi); }
/* a kingdom with something buyable in it flags itself down the rail, the way the ready
   badge does everywhere else — this is a shopping screen, and that is the whole news */
.armtile.hasready .mt-rod { background: linear-gradient(180deg, #d9ffe6, var(--ready-dk) 46%, #1d6d3a); }
.armtile.hasready .mt-rod::before, .armtile.hasready .mt-rod::after {
  background: radial-gradient(circle at 34% 30%, #e8fff0, var(--ready-dk) 55%, #1d6d3a); }
.armtile.done .mt-tick { opacity: 1; transform: scale(1); }

@media (max-width: 900px) {
  .armlayout { grid-template-columns: 1fr; grid-template-rows: auto minmax(0, 1fr); }
  .armrail { flex-direction: row; overflow-x: auto; overflow-y: hidden; padding: 0 0 4px; }
  .armrail .mtile { flex: 0 0 190px; }
}

/* ---- whose shelf this is, and how it is ordered ---- */
.armwho { display: flex; align-items: center; gap: 11px; padding-bottom: 4px; }
/* `.armback` and `.armwhocrest` went with the step they served: there is no screen above
   this one to go back to, and the lit banner in the rail already says whose shelf it is
   in the kingdom's own colours. */
.armwhotxt { min-width: 0; display: flex; flex-direction: column; line-height: 1.15; }
.armwhotxt b { font: 800 17px var(--font-display); letter-spacing: 0.3px;
  color: color-mix(in srgb, var(--facl) 20%, #fff8ea); }
.armwhotxt em { font-style: normal; font: 600 11.5px var(--font-ui); color: #b3a68a; }
.armwho .armsort { margin-left: auto; }

/* ---- category sections ---- */
.armgrid { display: block; }
.armsec { --cat: var(--gold); margin-bottom: 22px; }
.armsechd { display: flex; align-items: center; gap: 10px; margin-bottom: 9px; padding-bottom: 7px;
  border-bottom: 1px solid rgba(255,255,255,0.07); }
/* `.armsecdot` is gone: the heading now carries the category's colour in the words and
   the rule beside them, the way the muster dossier does it, so a swatch saying the same
   thing a third time was one mark too many. See the shelf pass at the end of this file. */
.armsechd h3 { margin: 0; font-family: var(--font-display); font-size: 16px; font-weight: 800; letter-spacing: 0.4px; color: var(--cat); }
.armsecrole { flex: 1; min-width: 0; font-family: var(--font-ui); font-size: 11.5px; color: rgba(190,180,160,0.6);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.armsecnum { font-variant-numeric: var(--nums); font-size: 12px; font-weight: 700; color: var(--muted); }
.armsecbar { width: 74px; height: 5px; flex: none; border-radius: var(--r-xs); overflow: hidden; background: rgba(0,0,0,0.4); }
.armsecbar i { display: block; height: 100%; background: var(--cat); border-radius: var(--r-xs); }
.armcards { display: grid; grid-template-columns: repeat(auto-fill, minmax(176px, 1fr)); gap: 11px; }

/* ---- the card ---- */
/* The category stripe is a pseudo-element, NOT a border: the card's outline already
   carries its unlock STATE (green when owned or ready to buy), and a border-colour
   rule would win over the category and wipe the colour off exactly the cards you are
   most likely to be looking at. Two channels, two surfaces. */
.armcard { align-items: stretch; gap: 6px; padding: 13px 11px 11px; text-align: left; overflow: hidden; }
.armcard::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px;
  background: var(--cat); box-shadow: 0 0 9px color-mix(in srgb, var(--cat) 55%, transparent); }
.armhead { display: flex; align-items: center; gap: 9px; }
.armicon { width: 52px; height: 52px; flex: none; }
.armttl { min-width: 0; }
.aname { font-size: 13.5px; line-height: 1.15; }
.acatpill { display: inline-block; margin-top: 3px; padding: 1px 7px; border-radius: var(--r-3xl);
  font-family: var(--font-ui); font-size: 9.5px; font-weight: 700; letter-spacing: 0.3px;
  color: var(--cat); background: color-mix(in srgb, var(--cat) 16%, transparent); border: 1px solid color-mix(in srgb, var(--cat) 40%, transparent); }
.arole { font-size: 10.5px; color: var(--ready); text-align: left; line-height: 1.3; }
/* the stat row: icons above values, tabular so columns line up card to card */
.astats { display: flex; flex-wrap: wrap; gap: 4px 6px; padding: 6px 0 1px; border-top: 1px solid rgba(255,255,255,0.06); }
.astats span { display: inline-flex; align-items: center; gap: 3px; font-family: var(--font-ui);
  font-size: 11px; font-weight: 700; color: var(--ink); font-variant-numeric: var(--nums); }
.astats i { font-style: normal; opacity: 0.65; font-size: 10.5px; }
.amatch { display: flex; flex-direction: column; gap: 1px; font-family: var(--font-ui); font-size: 9.5px; line-height: 1.3; }
.astrong { color: var(--ready); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.aweak { color: #e2907f; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.afoot { margin-top: auto; padding-top: 6px; justify-content: center; }
.armcard .buybtn { width: 100%; }
.armempty { grid-column: 1/-1; display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 60px 20px; color: var(--muted); font-family: var(--font-ui); }
.armempty b { font-size: 17px; color: var(--ink); }

/* Structures wear the cool stone tint they carry in the build rack. */
.armcard.armemp { background: linear-gradient(168deg, rgba(30,38,50,0.92), rgba(13,17,24,0.95)); }

/* Divider between the troop spawners and the structures in the build rack: two very
   different bargains, so the rack marks the break instead of running them together.
   A RULE, not a caption — sideways text in the middle of a row of cards reads as
   something blocking the row rather than dividing it, and the cards say which is which
   perfectly well on their own (PLACE a troop, RAISE a building). Fades out at both
   ends so it sits in the rack rather than cutting it in half, with a small gilt lozenge
   at the middle to look intended. */
.buildsep {
  flex: 0 0 auto; align-self: stretch; position: relative;
  width: 1px; margin: 10px 13px; padding: 0; border: 0;
  background: linear-gradient(180deg,
    rgba(var(--gold-rgb),0) 0%, rgba(var(--gold-rgb),0.42) 26%,
    rgba(var(--gold-rgb),0.42) 74%, rgba(var(--gold-rgb),0) 100%);
}
.buildsep::after {
  content: ''; position: absolute; top: 50%; left: 50%;
  width: 5px; height: 5px; margin: -3px 0 0 -3px; transform: rotate(45deg);
  background: linear-gradient(180deg, var(--gold-lt), #b58f34);
  box-shadow: 0 0 7px rgba(var(--gold-rgb),0.55);
}
/* a structure card reads cooler than a troop card — stone and iron, not banners */
.build.emp { background: linear-gradient(168deg, rgba(30,38,50,0.94), rgba(13,17,24,0.96)); }
.build.emp .bcat { color: #9fc0e0; }

/* Camera preference, sat under the reference it belongs to. The card scrolls when the
   list outgrows the viewport, so this stays pinned to its foot — a setting is no use
   if you have to scroll to discover it. Negative margins carry it to the card's edges. */
/* ONE STICKY FOOTER, NOT TWO STICKY ROWS. Each setting used to pin itself to
   `bottom: -22px` on its own, and two elements pinned to the same offset in the
   same scroller land on top of each other: with both settings shown, Edge
   scrolling sat underneath Atmospheric haze and could not be reached at all. The
   pin belongs to the FOOTER — one box, however many rows are in it. */
.km-togs { position: sticky; bottom: -22px; z-index: 1;
  margin: 16px -22px -22px;
  border-top: 1px solid var(--edge);
  background: linear-gradient(180deg, rgba(32,26,15,0.97), rgba(16,12,7,0.995)); }
.km-togs .km-tog + .km-tog { border-top: 1px solid var(--edge-quiet); }
.km-tog {
  display: flex; align-items: flex-start; gap: 9px;
  padding: 11px 22px 12px;
  font-family: var(--font-ui); font-size: 12.5px; line-height: 1.45; color: var(--ink); cursor: pointer; }
.km-tog input { flex: none; width: 15px; height: 15px; margin-top: 1px; accent-color: var(--gold); cursor: pointer; }
.km-tog b { color: var(--gold-lt); }
.km-tog:hover b { color: #fff; }

/* ===================================================================== */
/* ==============  READABLE INSPECT-SCROLL TOOLTIP  =================== */
/* ===================================================================== */
/* a clean, sectioned parchment scroll — bigger type, clear hierarchy, strong
   contrast so a unit's stats/role read instantly without feeling like raw UI. */
/* ===== A TIP IS READ MID-BATTLE, WITH A CURSOR ALREADY MOVING =====
   This was 304px wide with a 60px portrait, a 19px name and 14px figures — a
   panel, sitting over the field, on a card you are sweeping past. Everything on
   it is kept and every measurement is taken in twice over: 304 to 252 to 208, and
   243px tall to about 170. It reads the same and covers a third of the ground.

   AND IT IS SEE-THROUGH. Two stops at 0.99 made it a solid card of brown laid
   over the battlefield — and the whole reason a tip is small is that it is
   floating over the thing you are trying to look at. A wash and a blur instead:
   the type stays legible over any country the seed deals, and the fight goes on
   being visible through it. */
#tip {
  width: 208px; padding: 0; overflow: hidden;
  background: linear-gradient(168deg, rgba(38,30,19,0.62), rgba(12,9,6,0.72));
  backdrop-filter: blur(10px) saturate(1.1);
  border: 1px solid rgba(var(--gold-rgb),0.34); border-radius: var(--r-lg);
  box-shadow: var(--sh-4), inset 0 1px 0 rgba(var(--lit-rgb),0.12);
}
/* A HEADER BAND, A STAT GRID, THEN THE READING. The tip used to be six paragraphs of
   different weights stacked with no structure, ending in a three-line footer that
   repeated the card's own price and re-taught veterancy on every troop in the game.
   Now it opens with the soldier's own portrait against a band in its category colour,
   the numbers sit in a labelled grid you can look straight into, and the last thing on
   it is the one line of flavour that says what the thing actually is. */
#tip .thead { display: flex; align-items: center; gap: 7px; padding: 6px 8px 5px;
  background: linear-gradient(100deg, color-mix(in srgb, var(--cat, var(--gilt-body)) 26%, transparent), rgba(0,0,0,0.18) 72%);
  border-bottom: 1px solid rgba(var(--gold-rgb),0.3); border-left: 4px solid var(--cat, var(--gilt-body)); }
#tip .tart { flex: none; width: 34px; height: 34px; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.75)); }
/* ---- THE WAY OUT OF THE TIP ----------------------------------------------
   #tip is `pointer-events: none` on purpose. It hangs over the battlefield, and
   a tooltip that swallowed a command mid-fight would be a worse bug than one
   that overstays its welcome — so the close button opts ITSELF back in, and is
   the only part of the tip that answers a press. Everything else still lets the
   order through to the ground underneath.

   It earns its place on touch, where the tip opens on a press-and-hold and the
   `mouseleave` that would have closed it is an event that is never coming. */
#tip .tclose {
  pointer-events: auto; position: relative; flex: none; align-self: flex-start;
  width: 20px; height: 20px; padding: 0; display: flex;
  align-items: center; justify-content: center;
  font: 700 11px var(--font-ui); line-height: 1; cursor: pointer;
  color: var(--gilt-body); background: rgba(0,0,0,0.3);
  border: 1px solid rgba(var(--gold-rgb),0.34); border-radius: 50%;
}
#tip .tclose:hover { color: var(--gold-bright); background: rgba(0,0,0,0.55);
  border-color: rgba(var(--gold-rgb),0.72); }
#tip .tclose:active { transform: scale(0.92); }
/* A 20px circle is a mouse target; a thumb wants the whole corner. The header
   must not reflow to give it one, so the reach is bought with a transparent
   outset — 44px of it, which is the size every other touch target here is held
   to, and it costs the layout nothing. */
html.touch #tip .tclose::after { content: ''; position: absolute; inset: -12px; }
#tip .tid { min-width: 0; flex: 1; }
#tip .tname { font-family: var(--font-display); font-size: 13px; font-weight: 800; color: var(--gold-bright);
  padding: 0; letter-spacing: 0.4px; line-height: 1.1; text-shadow: 0 2px 6px rgba(0,0,0,0.7); }
#tip .tsig { color: var(--gilt-lit); text-shadow: 0 0 8px rgba(var(--gilt-lit-rgb),0.9); }
#tip .trole { font: 600 9.5px 'Spectral', serif; color: #f0e2c2; margin-top: 1px; line-height: 1.15; }
#tip .tcat { display: flex; align-items: center; flex-wrap: wrap; gap: 4px;
  font-size: 9px; color: #c9bda0; padding: 0; margin: 2px 0 0; border: 0; }
#tip .tchip { font: 800 9px var(--font-display); letter-spacing: 0.6px; text-transform: uppercase;
  color: #100c06; background: color-mix(in srgb, var(--cat, var(--gilt-body)) 74%, #fff6dc);
  border-radius: var(--r-3xl); padding: 1px 7px; }
#tip .tonly { font: 700 10px var(--font-ui); letter-spacing: 0.4px; text-transform: uppercase; color: var(--gilt-lit); }
#tip .tworth { font: 600 9.5px var(--font-ui); color: #bdb195; font-variant-numeric: var(--nums); }

/* Six cells three across, then the muster clock on a row of its own. Seven cells
   in a three-column grid leaves an orphan in the last row; spanning the odd one
   out is both tidier and correct emphasis — how long until the first troop walks
   out is the number that decides whether a spawner is worth raising at all. */
#tip .tgrid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px;
  margin: 0; padding: 1px; background: rgba(var(--gold-rgb),0.16); }
#tip .tcell { display: flex; flex-direction: column; align-items: center; gap: 0;
  padding: 3px 2px 2px; background: rgba(10,7,4,0.55); }
#tip .tcell b { font: 800 11px var(--font-ui); color: #f2f7fc; font-variant-numeric: var(--nums); line-height: 1.05; white-space: nowrap; }
/* row-REVERSE, because a cell is authored value-then-label and stacked that reads
   fine — laid out in a row it came out "2:58 MUSTERS ONE EVERY", which is the
   sentence backwards. Reversing the row puts the label first without the markup
   having to know this one cell is laid out differently from the other six. */
#tip .tcell.twide { grid-column: 1 / -1; flex-direction: row-reverse; justify-content: center; gap: 6px; padding: 3px 3px; }
#tip .tcell.twide b { color: var(--gold-bright); font-size: 11.5px; }
#tip .tcell.twide i { align-self: center; }
#tip .tcell i { font-style: normal; font: 700 6.5px var(--font-ui); letter-spacing: 0.4px; text-transform: uppercase; color: #a2967c; }

#tip .tpow { padding: 5px 8px 0; gap: 2px 3px; }
#tip .tpow span { font: 600 9px var(--font-ui); color: #f2f6fa; background: rgba(255,255,255,0.1);
  border: 1px solid rgba(var(--gold-rgb),0.3); padding: 1px 5px; border-radius: var(--r-sm); }
#tip .tcounters { font-size: 9.5px; padding: 5px 8px 0; gap: 2px; }
#tip .tcounters span { display: flex; align-items: baseline; gap: 6px; }
#tip .tcounters i { flex: none; font-style: normal; font: 800 7.5px var(--font-display); letter-spacing: 0.4px;
  text-transform: uppercase; opacity: 0.85; }
#tip .tstrong { color: var(--ready); } #tip .tweak { color: #f2ab94; }
/* `.tdesc` and `.thave` are both gone — the tip ends on the counters now, and ends
   there for every card, so it is the same height all the way along the rack. See
   showTip() / showEmpTip() in main.js. */
#tip .tcounters { padding-bottom: 6px; }
/* the gilt corner brackets sit a touch inside the rounder frame */
#tip::before, #tip::after { opacity: 0.6; }

/* ===================================================================== */
/* ===================  WAR-RACK CARDS: CREATIVE EYE  ================= */
/* ===================================================================== */
/* give every muster tile a faint category-coloured wash so roles read as a
   family at a glance, a livelier hover, and a bolder, more legible cost. */
.build { position: relative; isolation: isolate; }
.build::after {
  content: ''; position: absolute; inset: 0; z-index: -1; pointer-events: none; border-radius: var(--r-xl);
  background: radial-gradient(125% 78% at 50% -6%, var(--cat, var(--gilt-body)), transparent 62%);
  opacity: 0.14; transition: opacity 0.16s;
}
.build:hover::after { opacity: 0.3; }
.build:hover { box-shadow: var(--sh-4), 0 0 22px rgba(var(--gold-rgb),0.4), inset 0 0 0 3px rgba(var(--gold-rgb),0.26), inset 0 2px 0 rgba(var(--lit-rgb),0.2); }
.build .bcat { color: var(--cat, var(--gilt-body)); opacity: 1; font-weight: 700; text-shadow: 0 1px 2px rgba(0,0,0,0.7); }
.build .bupgc { font-size: 13px; color: var(--gold-bright); }
.build .bupgw { color: var(--gold-lt); opacity: 0.95; }
/* the unit medallion gives a soft category-tinted halo on hover */
.build:hover .bicon { box-shadow: inset 0 3px 7px rgba(0,0,0,0.7), inset 0 0 0 2px var(--gold-lt), 0 0 16px var(--cat, rgba(var(--gold-rgb),0.6)); }

/* ---- war-rack: stack into wrapped rows, never a horizontal scrollbar ----
   an EXPLICIT width is required: an absolutely-positioned flex box otherwise
   shrink-to-fits to a narrow column and over-wraps (and bloats --rackh). */
#buildbar {
  flex-wrap: wrap;
  justify-content: center;
  /* flex-START, not flex-end. The rack is bottom-anchored and only as tall as its
     ranks, so end-packing never bought anything — but once max-height made it a
     scroller it became a trap: wrapped lines packed toward the end overflow past the
     TOP edge, and overflow above the start edge cannot be scrolled to. A 40-tile
     roster put 30 tiles at negative offsets with no way to reach them. */
  align-content: flex-start;
  overflow: visible;
  /* The rack is as wide as the tiles it holds, not as wide as the screen. Fixed at
     98vw it was a 1400px slab of stone carrying five 110px tiles, and it lay right
     across the minimap and the ?/log buttons — blocking clicks on both, since the
     rack takes pointer events for its whole footprint.
     The cap reserves the minimap's width (248 + margin) on BOTH sides, because the
     rack is centred: a one-sided reserve would just shift the collision. Past that
     the tiles wrap to another rank rather than reaching for the minimap, and past
     ~46vh of ranks the rack scrolls rather than swallowing the battlefield. */
  width: fit-content;
  /* …and the hard cap is what actually decides the shape. 1520px let a full
     roster lie in one rank across most of the screen; 900px is about eleven
     tiles, which turns the same roster into a compact two-rank block. Stated in
     layout px rather than vw on purpose: the rack is `zoom: var(--ui)`, so a
     fixed figure here holds a constant number of TILES PER RANK on every screen,
     which is the thing that decides whether it reads as a block or a bar. A vw
     cap would give a wide monitor more tiles per rank and put the slab back. */
  max-width: min(calc(98vw / var(--ui)), calc(100vw / var(--ui) - 544px), 930px);
  max-height: min(calc(46vh / var(--ui)), 420px);
  overflow-y: auto;
  padding-top: 16px;   /* headroom so a hovered tile's lift is not clipped away */
  gap: 7px 6px;
  /* a thin gilt rail, not nothing: the rack hid its scrollbar back when it could not
     scroll, and a silent scroller is a roster the player never learns is longer */
  scrollbar-width: thin;
  scrollbar-color: var(--gold-dk) transparent;
}
#buildbar::-webkit-scrollbar { width: 8px; height: 8px; }
#buildbar::-webkit-scrollbar-track { background: transparent; }
#buildbar::-webkit-scrollbar-thumb { background: var(--gold-dk); border-radius: var(--r-sm); border: 2px solid transparent; background-clip: content-box; }

/* ===================================================================== */
/* ================  TOP HUD: points centred · banner gone  =========== */
/* ===================================================================== */
/* drop the objective ribbon; the standings (the points) sit in the middle column of
   the topbar grid with slight transparency so the battlefield reads through them.
   This used to centre itself with position:absolute + translateX(-50%). It did not
   work: `#factions.scorecard` sets position:relative (the hover briefing anchors to
   it) and outranks a bare `#factions`, so the card dropped back into the topbar's
   flex row and then took `left: 50%` as a RELATIVE nudge on top of that — landing
   the whole scorecard, both keep bars and the clock included, about 420px past the
   right edge of a 1440px screen. The grid centres it structurally instead. */
#factions {
  margin-top: 0;
  background: linear-gradient(160deg, rgba(34,28,20,0.6), rgba(15,11,7,0.64));
  border: 1px solid rgba(var(--gold-rgb),0.42);
  box-shadow: var(--sh-3), var(--bevel);
  backdrop-filter: blur(4px);
}
/* keep event toasts clear of the now-centred standings */
#herald { top: 148px; }

/* ---- always-visible Build button (bottom centre); the rack opens above it ---- */
/* bottom control cluster: Build + Upgrades, centred above the very bottom edge */
/* The 26px was clearance for the keybind strip that used to run along the very bottom.
   The strip is gone, but the gap stays: it is what keeps the Build cluster off the
   screen edge, which is worth having on its own. */
#bottombtns {
  position: absolute; bottom: calc(26px + var(--sab) / var(--ui)); left: 50%; transform: translateX(-50%); z-index: 7;
  zoom: var(--ui);
  display: flex; align-items: flex-end; gap: 9px; pointer-events: none;
}
#bottombtns > button { pointer-events: auto; }
#buildbtn {
  display: flex; align-items: center; gap: 5px; padding: 9px 26px; cursor: pointer;
  font-family: var(--font-display); font-weight: 800; font-size: 14px; letter-spacing: 1.5px; color: var(--gilt-ink);
  background: linear-gradient(180deg, var(--gold-lt), var(--gold) 60%, var(--gold-dk));
  border: 1px solid var(--gilt-deep); border-radius: var(--r-xl);
  box-shadow: var(--sh-3), 0 0 16px rgba(var(--gold-rgb),0.3), inset 0 1px 0 rgba(255,255,255,0.45);
  transition: transform 0.08s, filter 0.12s;
}
#buildbtn:hover { filter: brightness(1.08); transform: translateY(-2px); }
#buildbtn:active { transform: translateY(0); }
/* secondary "stone" styling so Upgrades reads as a manage action, not the primary CTA */
/* GREEN, to match the + that marks an affordable upgrade on the field. Upgrading is
   one action with two entry points — the marker over the building and this button —
   and they were speaking in different colours, so nothing tied them together. */
#upgradebtn {
  display: flex; align-items: center; gap: 5px; padding: 9px 20px; cursor: pointer;
  font-family: var(--font-display); font-weight: 800; font-size: 13px; letter-spacing: 1.2px; color: var(--ready);
  background: linear-gradient(180deg, rgba(38,54,40,0.97), rgba(18,28,20,0.98));
  border: 1px solid #3f7d55; border-radius: var(--r-xl);
  box-shadow: var(--sh-3), inset 0 1px 0 rgba(150,255,190,0.14);
  transition: transform 0.08s, filter 0.12s, border-color 0.12s, box-shadow 0.12s;
}
#upgradebtn .upgarrow { color: #8fffb4; }
#upgradebtn:hover { filter: brightness(1.12); transform: translateY(-2px); border-color: var(--ready); }
#upgradebtn:active { transform: translateY(0); }
/* pulse the Upgrades button while at least one spawner can afford a level-up */
#upgradebtn.ready { border-color: #8fffb4; color: var(--ready-lt); animation: upgReady 1.8s ease-in-out infinite; }
/* …and grey it right down when there is nothing to upgrade at all — no spawner raised,
   or every one of them already at maximum. It still opens (the panel explains itself),
   but it should never look like a live button offering something it cannot give. The
   arrow dims further than the word, so the "there is a level waiting" cue goes first. */
#upgradebtn.none { color: #7c745f; border-color: rgba(150,126,74,0.34); filter: saturate(0.35) brightness(0.82);
  box-shadow: var(--sh-2), inset 0 1px 0 rgba(255,220,150,0.05); }
#upgradebtn.none:hover { filter: saturate(0.5) brightness(0.95); transform: none; border-color: rgba(150,126,74,0.5); }
#upgradebtn.none .upgarrow { opacity: 0.3; }
@keyframes upgReady {
  0%, 100% { box-shadow: var(--sh-3), 0 0 0 1px rgba(110,232,154,0.55), inset 0 1px 0 rgba(150,255,190,0.14); }
  50% { box-shadow: var(--sh-3), 0 0 16px 2px rgba(110,232,154,0.75), inset 0 1px 0 rgba(150,255,190,0.2); }
}
/* the war-rack sits just above the Build button instead of at the very bottom.
   Keep in step with #bottombtns' offset and with refreshRack() in main.js, which

   (The MYTHIC FINISH PASS restates this further down — change both.) */
/* …and the rack's own scale on top of it. The offset is divided by the same factor
   the zoom multiplies by, so the rack sits exactly where it did against the Build
   cluster — 86 layout px at --ui, whatever --rack does to the tiles above it. */
#buildbar { bottom: calc((86px + var(--sab) / var(--ui)) / var(--rack)); zoom: calc(var(--ui) * var(--rack)); }

/* ============ The Upgrades panel ============ */
#upgradewin {
  position: absolute; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%);
  z-index: 90; pointer-events: auto;
}
#upgradewin.hidden { display: none; }
/* ===== THE PANEL RUNS ACROSS, LIKE THE RACK UNDER IT =====
   This was a tall column of full-width rows: five spawners and a keep and it was a
   sheet of screen, in the middle of a battle, over the ground the battle is on. The
   rack directly beneath it has always been a row of small cards you skim sideways,
   and this is the same act — pick one of the things you own — so it is now the same
   shape. Nothing is dropped: the portrait, the name, the level, the ladder, the
   three gain chips, the price button and the auto toggle are all still on every
   card, stacked instead of strung out. */
.uw-card {
  /* AS WIDE AS WHAT IS IN IT. This was a flat `min(1180px, 96vw)`, which is a
     width chosen for the fullest the panel ever gets — six spawners and a keep —
     and then charged to every other state it is ever in. At the opening of a
     match, which is when a commander opens it for the first time, it holds ONE
     card: a 1400px sheet of nothing across the battlefield with a keep in the
     corner and a caption floating in the middle of the empty half. The rack
     directly beneath it settled this already (see `html.phone #buildbar`): size
     to the roster, cap at the screen, scroll past the cap. Same rule here, so
     the panel is a tool the size of its job and the ground it hides is only ever
     the ground it needs. The min-width is the header's own — the strap, the
     purse and the way out — below which the panel would be smaller than the line
     that describes it. */
  width: fit-content;
  /* …AND A FLOOR THAT IS A CONSTANT, not a measurement of the line above it. 536 is
     the header at full stretch — the strap whole, the ready pill, the widest purse
     anyone will hold and the way out (511.5 measured, plus room for the zoom) — so the
     strap is never cut short. And because it is a stated number rather than the
     header's own width, nothing that happens ON that line can move the panel. The old
     330 left the header deciding, and the header changes: arm AUTO and the "N ready to
     buy" pill goes, taking ninety pixels of panel with it while you are reading it.
     (See `.uw-head`, which no longer votes, and `.uw-gold b`, which was the same fault
     one line further in.) */
  min-width: min(536px, calc(92vw / var(--ui)));
  max-width: min(1180px, calc(96vw / var(--ui)));
  display: flex; flex-direction: column;
  max-height: calc(100vh / var(--ui) - 132px);
  max-height: calc(100dvh / var(--ui) - 132px);
  /* SEE-THROUGH, like the rack it sits above. This was two nearly-opaque stops —
     a slab of brown laid over the battlefield — and the panel is a tool for
     deciding what to do about the fight going on underneath it. The rack solved
     this long ago: a soft dark wash that grounds the cards without hiding the
     ground. Same treatment, plus a blur so the text stays legible over whatever
     colour of country happens to be behind it.
     THE WASH IS THE RACK'S, THOUGH, NOT A FAINTER ONE. It sat at 0.10 → 0.50
     against the rack's 0.55 → 0.78, and a tenth of black is not a wash, it is
     nothing: the strap and the empty-state caption were being read against
     whatever the terrain happened to be — spring grass, pale sand, a lit shrine —
     and over the bright half of a map they were not readable at all. Matched to
     the strip it opens above, which is still see-through and was never in doubt. */
  background: linear-gradient(180deg, rgba(8,6,4,0.55), rgba(8,6,4,0.8));
  backdrop-filter: blur(9px) saturate(1.08);
  border: 1px solid rgba(var(--gold-rgb),0.3); border-radius: var(--r-2xl);
  box-shadow: var(--sh-4), inset 0 1px 0 rgba(var(--lit-rgb),0.12);
  animation: briefPop 0.32s var(--e-back);
}
/* ONE THIN LINE, not a band. This was a two-rank header with its own field of
   background: a logo-type title on the first rank and the strap on the second,
   about 63px of panel to carry a name nobody needed, a purse and a ✕. The title
   is gone entirely and the rest sits on a single 22px line — the strap taking the
   width it does not need to be given, the purse and the way out at the end. */
/* THE HEADER DOES NOT GET A VOTE ON HOW WIDE THE PANEL IS. The card is sized to its
   content (see `.uw-card`), and this line — strap, ready pill, purse, way out — is
   wider at full stretch than the row of cards below it, so it was the thing deciding.
   That made the panel change width whenever anything on the line did: arm AUTO on your
   spawners and the "N ready to buy" pill goes away, because there is nothing left for
   you to buy by hand — and ninety pixels came off the panel, in the middle of a battle,
   while you were looking at it. (The purse used to do the same on every gold tick; see
   `.uw-gold b` for that half.)
   `width: 0` takes the header out of the intrinsic width calculation altogether while
   `min-width: 100%` keeps it filling whatever the cards ask for — so the panel is the
   width of the work in it, which is what `.uw-card`'s min-width was already reaching
   for, and the strap ellipsizes into whatever is left. */
.uw-head { display: flex; align-items: center; flex-wrap: nowrap; gap: 8px; padding: 3px 8px 3px 12px;
  width: 0; min-width: 100%;
  border-bottom: 1px solid var(--edge);
  background: linear-gradient(180deg, rgba(var(--gold-rgb),0.1), rgba(0,0,0,0.14)); }
/* `.uw-title` is gone with the markup that used it — see buildUpgradeShell. */
/* `flex: 1 0 100%` forces the header's wrap onto a second row and gives this the whole
   width, so it is one line at a readable size instead of two cramped ones. */
.uw-strap { flex: 1 1 auto; min-width: 0; margin: 0; font: 500 10.5px 'Spectral', serif; color: #ddd1b4;
  line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.uw-strap b { color: var(--gold-hi); font-weight: 700; }
/* how many levels the purse can buy RIGHT NOW — the number that decides whether you
   spend or keep saving, said once at the top instead of counted off the rows */
.uw-ready { flex: none; font: 800 9px var(--font-display); letter-spacing: 0.5px; text-transform: uppercase;
  color: #06150b; background: linear-gradient(180deg, var(--ready-lt), var(--ready-dk)); border-radius: var(--r-3xl); padding: 1px 8px;
  box-shadow: 0 0 12px rgba(var(--ready-rgb),0.55); animation: bready 1.9s ease-in-out infinite; white-space: nowrap; }
.uw-ready.hidden { display: none; }
.uw-ready b { font-size: 10.5px; }
.uw-gold { flex: none; display: inline-flex; align-items: baseline; gap: 4px;
  font-family: var(--font-display); font-weight: 800; font-size: 12px; color: #1c1508;
  background: linear-gradient(180deg, var(--gold-hi), var(--gold)); border: 1px solid var(--gold-dk); border-radius: var(--r-3xl);
  padding: 1px 9px; white-space: nowrap; box-shadow: inset 0 1px 0 rgba(255,255,255,0.5); }
.uw-gold i { font-style: normal; }
/* A CELL, NOT A RUN OF TEXT. Gold ticks up every frame of a match and this figure is
   rewritten with it, so its width is the width of the whole chip — and in a face with
   proportional digits that width changes on nearly every tick (`1,111` is ten pixels
   narrower than `8,888` at this size, measured, and no `tabular-nums` will close the
   gap because Cinzel has no tabular set to switch to). The cell is 6ch, which holds
   `888,888` — more gold than any match has ever ended with — so the box never resizes,
   and the number is set against its right edge so it grows leftward into its own slack
   instead of shoving the sigil about. */
.uw-gold b { min-width: 6ch; text-align: right; font-weight: 800; font-variant-numeric: var(--nums); }
.uw-x { flex: none; width: 21px; height: 21px; border-radius: var(--r-md); cursor: pointer; font-size: 13px; line-height: 1; color: #d8c69a;
  background: rgba(0,0,0,0.3); border: 1px solid var(--edge); transition: filter 0.12s, border-color 0.12s; }
.uw-x:hover { filter: brightness(1.2); border-color: var(--gold-lt); color: #fff; }
.uw-list { overflow-x: auto; overflow-y: hidden; padding: 6px 9px 7px; display: flex; flex-direction: row;
  align-items: stretch; gap: 7px; scrollbar-width: thin; scrollbar-color: var(--gold-dk) transparent; }
.uw-list::-webkit-scrollbar { height: 7px; }
.uw-list::-webkit-scrollbar-thumb { background: var(--gold-dk); border-radius: var(--r-sm); }
/* THE CAPTION IS A COLUMN OF THE LIST, so it has to say how wide it wants to be.
   `.uw-list` is a flex ROW — it holds cards side by side — and a bare block dropped
   into it is just another flex item: with the card sized to its content it was
   handed whatever was left beside the keep and set itself in a 110px gutter, one
   word per line, reading downwards. A stated measure makes the card grow to hold
   the sentence instead of wringing it out. */
/* …AND IT DOES NOT GET TO SET THE PANEL'S HEIGHT. `.uw-list` stretches its items to
   the tallest one, so this caption's own padding — 26px top and bottom, around three
   lines of 15px — was the tallest thing in the list at the one moment the list has
   the least in it, and it stretched the single keep card to match: a card with an
   inch of nothing under its button, which is exactly the state a commander opens
   this panel in for the first time. Trimmed to where the CARD is the tallest thing
   again and the caption sits centred against it.
   (`align-self: center` was already keeping it from being stretched itself; it was
   never about that. A flex item's own height counts toward the line's height whether
   it stretches or not.) */
.uw-empty { flex: 0 0 auto; align-self: center; width: min(300px, calc(70vw / var(--ui)));
  text-align: center; padding: 10px 16px; font-family: var(--font-flavor); font-size: 15px; color: var(--muted); line-height: 1.45; }
/* ---- HALF THE ROW ----
   Every row was a three-rank stack — the name, then the ladder of pips, then the
   gain chips — beside a portrait stretched to match it, which made a spawner cost
   about 115px of a panel that has to list every building you own in the middle of
   a battle. Four spawners and a keep filled the screen.
   Nothing here is deleted. The three ranks become ONE flow: `.uw-info` is a
   wrapping flex row, so the name, the ladder and the gains sit on a single line
   while there is width for them and fold to a second only when there is not. That
   is where the height goes — a row is now a line of text tall rather than three,
   and the portrait, which only ever stretched to fill whatever the text made, comes
   down with it. */
/* A CARD IN THREE RANKS — portrait BESIDE the name and ladder, gains on one line
   under them, price and standing order sharing the last. Stacked as five things in
   a column it came out 210px tall and the panel 313, which is a third of the screen
   over the ground the battle is on. The rank that cost the most was the one that
   held only a portrait: 54px of card to say which troop, when the name is right
   there. Nothing is dropped and the card is 90. */
.uw-row { flex: 0 0 auto; width: 186px; display: flex; flex-direction: column; align-items: stretch;
  gap: 3px; padding: 5px 7px; cursor: pointer;
  /* the cards keep a plate of their own, or they float on a transparent panel with
     nothing to read them against */
  background: linear-gradient(160deg, rgba(38,30,18,0.72), rgba(14,10,6,0.78));
  border: 1px solid var(--edge); border-radius: var(--r-lg);
  transition: border-color 0.12s, box-shadow 0.12s, transform 0.1s; }
/* HOVERING A CARD POINTS AT THE BUILDING. The panel lists things that are somewhere
   — and which of your six huts a row meant was a thing you could only find by
   clicking it and being flown there. Now the card lights and the building on the
   map lights with it (see `markBldId` in renderer.js), so the answer arrives before
   you have committed to anything. */
.uw-row:hover { border-color: var(--gold-lt); transform: translateY(-2px);
  box-shadow: 0 0 0 1px rgba(var(--gold-rgb),0.4), 0 6px 18px rgba(0,0,0,0.5), 0 0 22px rgba(var(--gold-rgb),0.28); }
/* THE WHOLE CARD IS THE BUTTON. The gold plate was the only live pixel on a
   152px tile — everything else was a picture of what you would be buying, and
   aiming at the plate is a smaller target than the thing that describes it. The
   card takes the click now, so a hover lights all of it rather than lighting the
   card and arming only its foot. */
.uw-row.can:hover { background: linear-gradient(160deg, rgba(72,56,26,0.86), rgba(28,20,10,0.86)); }
.uw-row.can:hover .uw-up.can { filter: brightness(1.09); }
.uw-row.can:active { transform: translateY(0) scale(0.985); }
/* …and a card with nothing to buy says so by not lifting. */
.uw-row:not(.can) { cursor: default; }
.uw-row:not(.can):hover { transform: none; }
/* ===== A STANDING ORDER IS VISIBLE ON THE THING IT WILL BUY =====
   AUTO said "this spawner will buy its own next level the moment the gold is
   there" on a 40px toggle, and the button that will actually be pressed on your
   behalf looked exactly like one that will not. The order now shows on the
   ORDER: the price plate wears the AUTO green and breathes, so a card that is
   going to spend your gold by itself is legible from across the panel.

   `:has()` rather than a class from JS, because the toggle's state is already
   painted onto the live checkbox every frame (see the note on upgPending) — a
   second copy of the same fact in a class name is a second thing to keep in
   step, and this needs none. */
.uw-act:has(.autobox.on) .uw-up { animation: uwAutoArmed 1.8s ease-in-out infinite; }
.uw-act:has(.autobox.on) .uw-up.poor {
  color: var(--ready-lt); border-color: rgba(var(--ready-rgb),0.55);
  background: linear-gradient(180deg, rgba(90,200,130,0.2), rgba(40,110,66,0.16)); }
@keyframes uwAutoArmed {
  0%, 100% { box-shadow: 0 0 0 1px rgba(var(--ready-rgb),0.45), 0 0 8px rgba(var(--ready-rgb),0.35); }
  50% { box-shadow: 0 0 0 1px rgba(var(--ready-rgb),0.8), 0 0 18px rgba(var(--ready-rgb),0.75); }
}
/* The keep leads the list and is not one of the spawners, so it says so: a warmer
   field and a rule under it separating it from the buildings you raised yourself. */
.uw-row.uw-keep { border-color: rgba(236,202,120,0.5);
  background: linear-gradient(160deg, rgba(var(--gold-rgb),0.16), rgba(120,90,40,0.06));
  margin-bottom: 4px; box-shadow: var(--sh-2), inset 0 1px 0 rgba(255,236,190,0.14); }
/* …and the rule that set the keep apart runs down its RIGHT edge now that the list
   goes sideways, not under it. */
.uw-row.uw-keep::after { content: ''; position: absolute; top: 12%; bottom: 12%; right: -4px; width: 1px;
  background: linear-gradient(180deg, transparent, rgba(var(--gold-rgb),0.4), transparent); }
.uw-row.uw-keep { margin-right: 4px; margin-bottom: 0; }
.uw-row.uw-keep { position: relative; }
.uw-row.uw-keep .uw-name { color: var(--gold-lt); }
/* THE PORTRAIT FILLS THE ROW. It was 42px in a row over 60 tall — the one picture on
   a panel made of numbers, drawn smaller than the words beside it, with dead space
   above and below. It now takes the row's full height, which is what the space was
   always for. */
/* It STRETCHES to the row rather than sitting at a fixed size in the middle of it:
   the rows are not all the same height (the keep carries two gain chips, a maxed
   spawner carries one line) and a fixed portrait leaves a different amount of dead
   air in each. Capped, because the keep's row is half again as tall as a spawner's
   and a 150px portrait would stop being a row and start being a poster. */
.uw-spr { flex: none; width: 38px; height: 38px; border-radius: var(--r-md);
  background: radial-gradient(circle at 50% 35%, rgba(255,255,255,0.1), rgba(0,0,0,0.32)); cursor: pointer; }
/* the "+" rides the portrait, so its wrapper has to stretch with it */
.uw-sprwrap { align-self: stretch; display: flex; }
/* the field's green "+" brought into the list, pinned to the portrait's shoulder —
   same mark, same meaning: there is still work to do on this one */
.uw-sprwrap { position: relative; flex: none; display: block; line-height: 0; }
.uw-plus { position: absolute; top: -4px; right: -5px; width: 17px; height: 17px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font: 800 13px/1 'Cinzel', Georgia, serif; color: #0b2a16;
  background: radial-gradient(circle at 40% 32%, var(--ready-lt), var(--ready) 60%, var(--ready-dk));
  border: 1.5px solid #13241a; box-shadow: 0 0 8px rgba(var(--ready-rgb),0.75);
  animation: uwPlusPulse 2.2s ease-in-out infinite; }
.uw-plus.dim { opacity: 0.4; box-shadow: none; animation: none; }
@keyframes uwPlusPulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.13); } }
/* EVERYTHING A STOP LARGER. With the shortfall pill out of the gains row there is a
   line's worth of height back on every row, and this panel is read at a glance in the
   middle of a battle — so the name, the level, the ladder and the gain chips all take
   the space the removed chip was using. */
/* the wrapping single flow described at `.uw-row`: name, ladder and gains in one
   line where they fit, folding rather than stacking where they do not */
.uw-top { display: flex; align-items: center; gap: 7px; min-width: 0; }
.uw-id { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.uw-act { display: flex; align-items: stretch; gap: 5px; }
.uw-info { flex: 1 1 auto; min-width: 0; width: 100%; display: flex; flex-direction: column;
  align-items: center; gap: 3px; text-align: center; }
/* ONE LINE, AND IT KEEPS IT. The tier chip used to sit beside the name as a second
   flex item, which is why this had to be a wrapping flex box at all: a chip is not
   text, so `text-overflow` could not ellipsize it — it was guillotined by the
   `overflow: hidden` and the keep card read "Unfortifiec". Wrapping saved the word
   and cost a whole line of height, on every card in the list rather than the one it
   happened to (see `.uw-list`, which stretches them all to the tallest). The chip
   has moved down to the ladder, where there was empty line to spare, so the name is
   plain text again and ellipsis does the job it is for. */
.uw-name { font-family: var(--font-display); font-weight: 800; font-size: 12px; color: #f6ecd2;
  flex: 0 0 auto; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.uw-lv { font-size: 9.5px; font-weight: 800; color: #1a140c; background: linear-gradient(180deg, var(--gold-hi), var(--gold)); padding: 1px 7px; border-radius: var(--r-lg); letter-spacing: 0.3px; }
/* `.uw-hp` is gone — a spawner's health is not something gold answers, and the
   scorecard reports the keep's continuously in a bar at the top of the screen. */

/* ---- the building tip: what that is, for whoever asks ----
   Small on purpose. It replaces a name plate that was painted under EVERY building
   on the field, so it has to be the opposite of that: one line, on one building,
   only while the cursor is on it, and never big enough to hide the ground it is
   floating over. */
#bldtip {
  position: fixed; z-index: 14; pointer-events: none; display: flex; align-items: baseline; gap: 8px;
  zoom: var(--ui);
  padding: 5px 10px; border-radius: var(--r-lg); white-space: nowrap;
  font: 700 12px var(--font-ui); color: #f0e6cd;
  background: linear-gradient(160deg, rgba(28,24,17,0.95), rgba(10,10,9,0.96));
  border: 1px solid rgba(var(--gold-rgb),0.42); box-shadow: var(--sh-3);
  backdrop-filter: blur(4px);
}
#bldtip.hidden { display: none; }
#bldtip.foe { border-color: rgba(232,90,72,0.5); }
#bldtip .bt-n { font-family: var(--font-display); font-weight: 800; font-size: 12.5px; }
#bldtip .bt-n b { color: var(--gold-lt); font-weight: 800; }
#bldtip .bt-w { font-size: 10.5px; letter-spacing: 0.3px; }
#bldtip .bt-h { font-size: 10.5px; color: #cdb; opacity: 0.8; font-variant-numeric: var(--nums); }
/* A LADDER, NOT A ROW OF DOTS. The rung one press away is outlined and pulsing, so the
   pips answer "how far can this still go" as well as "how far has it come". */
.uw-pips { margin-top: 0; display: flex; align-items: center; gap: 4px; flex: 0 0 auto; min-width: 0; }
/* the tier or level, at the head of the rungs it counts */
.uw-pips .uw-lv { flex: none; margin-right: 3px; }
.uw-pips i { display: block; width: 6px; height: 6px; border-radius: 50%;
  background: rgba(255,255,255,0.11); border: 1px solid rgba(255,255,255,0.08); }
.uw-pips i.on { background: linear-gradient(180deg, var(--gold-hi), var(--gold)); border-color: var(--gold-dk);
  box-shadow: 0 0 7px rgba(var(--gold-rgb),0.85); }
.uw-pips i.next { background: transparent; border: 1.5px dashed rgba(255,233,168,0.75);
  animation: uwPlusPulse 2.2s ease-in-out infinite; }

/* WHAT THE PRESS BUYS, beside the price rather than inside it. Three gain chips —
   cadence, health, damage — so the row states its case in the game's own terms.
   (`.uw-short`, the red "N short" chip, is gone: the price is on the button and the
   purse is in the header, so it was a third number that only ever restated the gap
   between the two — on every row you could not yet afford, which early in a match is
   all of them.) */
.uw-gains { margin-top: 0; display: flex; flex-wrap: nowrap; align-items: center; justify-content: space-between; gap: 3px; overflow: hidden; }
.uw-g { display: inline-flex; align-items: baseline; gap: 2px; font: 600 9px var(--font-ui); white-space: nowrap;
  color: #e2d7bb; font-variant-numeric: var(--nums); background: rgba(0,0,0,0.3);
  border: 1px solid rgba(var(--gold-rgb),0.24); border-radius: var(--r-sm); padding: 0 4px; }
.uw-g i { font-style: normal; font-size: 11px; opacity: 0.95; }
.uw-g b { color: var(--ready); font-weight: 800; }
.uw-gdim { font: 600 10px var(--font-ui); color: #a99a7a; text-align: center; }

.uw-up { flex: 1 1 auto; min-width: 0; cursor: pointer; border-radius: var(--r-md); padding: 3px 6px; text-align: center; line-height: 1.2;
  display: flex; align-items: center; justify-content: center; gap: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  font-family: var(--font-display); font-weight: 800; font-size: 10.5px; transition: filter 0.12s, transform 0.08s; }
.uw-up.can { color: #1a140c; background: linear-gradient(180deg, var(--gold-hi), var(--gold)); border: 1px solid var(--gold-lt);
  box-shadow: 0 3px 12px rgba(var(--gold-rgb),0.35), inset 0 1px 0 rgba(255,255,255,0.5); }
.uw-up.can:hover { filter: brightness(1.07); transform: translateY(-1px); }
.uw-up.can:active { transform: translateY(0); }
.uw-up.poor { color: #9a8c6e; background: rgba(60,52,38,0.5); border: 1px solid rgba(var(--gold-rgb),0.25); cursor: default; }
.uw-up .uwc { display: inline; font-weight: 800; font-size: 10px; opacity: 0.92; margin: 0; font-variant-numeric: var(--nums); }
.uw-up.busy { color: #d6b478; background: rgba(60,52,38,0.4); border: 1px solid var(--edge); cursor: default; }
/* a row you can act on now leans forward out of the list */
.uw-row.can { border-color: rgba(240,210,126,0.55);
  box-shadow: 0 0 0 1px rgba(var(--gold-rgb),0.14), var(--sh-3); }
/* On a card the AUTO toggle goes full width under the price rather than standing
   beside it — a 44px square floating in a 152px column is the one thing that would
   have looked left over from the old row. */
.uw-row .autobox { flex: none; width: 40px; flex-direction: row; gap: 3px; padding: 2px 3px; }
.uw-row .autobox i { width: 11px; height: 11px; }
.uw-row .autobox span { font-size: 7.5px; }
@media (max-width: 460px) {
  .uw-up { min-width: 100px; font-size: 11.5px; }
  .uw-name { font-size: 13px; }
  .uw-strap { display: none; }
}

.fortbtn:hover { filter: brightness(1.06); }

/* ===================================================================== */
/* ============  LOOK & FEEL POLISH — tactility + live juice  =========== */
/* ===================================================================== */

/* --- The Muster: choose your kingdom before the march --- */
/* ===================================================================== */
/* ===============  THE MUSTER — choose your kingdom  =================== */
/* A rail of four you skim, and one deep dossier on whichever you're weighing.
   Each kingdom carries its own banner colour (--fac) through the tile, the
   dossier rim and the march button, so the choice is felt as well as read. */
/* The dossier CLAIMS THE SCREEN. It used to be sized by its content and pinned to
   the top, so the roster scrolled inside a 250px box while a third of the page sat
   empty beneath it — reading a kingdom meant wheeling through a letterbox with all
   the room in the world going spare. Every link in the chain now grows: wrap → layout
   → detail → roster, each with min-height:0 so the flexbox actually lets them shrink. */
/* …UP TO A POINT. The chain above is about a panel that grows into the height it
   is given; this is the same screen's answer for WIDTH, which is a different
   question because the muster is not one object but three — a rail, a dossier and a
   stage — and only the middle one has anything to do with extra room. Left uncapped
   on an ultrawide (3700px is a real desktop now, and full-screen is where people
   put this) the rail and the stage were flung to opposite edges of the monitor with
   a three-foot dossier stranded between them, and the roster's fit answered the
   width honestly: three columns a thousand pixels wide, each holding one 140px
   troop and 850px of nothing. Every part was doing its job and the screen was
   unreadable — you cannot compare two kingdoms your eyes have to travel that far
   across.

   So the muster is given a WIDTH IT IS DESIGNED FOR and centred in whatever is
   left. 1900px is where the roster's fit still chooses seven columns — one role per
   column, which is the layout the whole panel is built around (see the media
   queries below, which start shedding zones at 1700) — and it is a ceiling rather
   than a size: every window narrower than it is untouched, and the screen goes on
   behaving exactly as it did before on every laptop it has ever been opened on. */
.musterwrap { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; gap: 10px; padding: 0 20px 18px; overflow-y: auto;
  width: 100%; max-width: 1900px; margin-inline: auto; }
/* `grid-template-rows: minmax(0, 1fr)` is what actually holds the dossier inside the
   screen. An implicit `auto` row sizes to its CONTENT, so the roster growing to fit
   every troop simply pushed the panel past the bottom and handed the whole muster a
   scrollbar — the min-height:0 chain above cannot help, because the row was never
   asking the panel to be smaller. A 1fr row with a zero floor pins it to the height
   available and lets the shrinking pass down the chain as intended. */
/* THREE COLUMNS: the rail you choose from, the army you are choosing, and the one
   troop out of it you are looking at. The stage is a fixed-width column on purpose —
   it holds a 200px portrait and five stat bars, and neither of those wants to be
   elastic — so all the slack goes to the roster, which is the only part whose
   contents actually scale. Below `--stage-gone` (see the media query at the foot of
   this block) it is dropped entirely and the hover tip takes the job back. */
.musterlayout { flex: 1 1 auto; min-height: 0; display: grid;
  grid-template-columns: minmax(196px, 236px) minmax(0, 1fr) minmax(330px, 396px);
  grid-template-rows: minmax(0, 1fr);
  gap: 14px; align-items: stretch; }
/* The rail is a full column of kingdoms and there are seven of them now. It SHARES the
   column between them: seven tiles at their natural height left a third of the left
   side empty on any normal screen, with the banners huddled at the top of a tall gap.
   They grow into it instead — `flex: 1 1 auto` hands the slack out evenly.
   The floor is what keeps the original promise. The tiles used to refuse to flex at all
   so that a short window could not squeeze them flat; now they refuse to go under
   `--mtmin` and the rail scrolls below that, which is the same guarantee written as a
   limit rather than as a refusal to grow. */
/* THE FLOOR CAME DOWN WHEN THE REALM GREW. At seven kingdoms a 62px minimum and
   a 7px gap wanted 476px and the rail had 560, so every tile grew into the slack
   and the list sat exactly full. At TEN it wants 683 in the same 560: the rail
   still scrolls, so nothing was broken, but the last two kingdoms in the realm
   were permanently below the fold on an ordinary window — which for the two
   newest banners in the game is the worst possible place to put them. 50px and a
   5px gap wants 545, so all ten fit again with room to spare, and 50 is still a
   30px crest with a two-line name beside it rather than a squeezed strip. */
.musterrail { --mtmin: 50px; display: flex; flex-direction: column; gap: 5px;
  min-height: 0; overflow-y: auto; padding-right: 4px; }
.musterrail .mtile { flex: 1 1 auto; min-height: var(--mtmin); }

/* ---- the seven tiles: the menu's banners, flown sideways ----
   Same construction as .racecard on the home screen — gold braid and the kingdom's
   cloth as TWO stacked clips, so the trim follows the swallowtail instead of stopping
   where a border-radius would. The notch is cut into the RIGHT edge here rather than
   the bottom, because these hang off a rod on the left and fly across the rail. */
.mtile { --fac: var(--gold); --notch: 15px;
  position: relative; display: flex; align-items: center; gap: 10px;
  text-align: left; cursor: pointer; color: var(--ink); font-family: inherit;
  padding: 10px calc(var(--notch) + 10px) 10px 16px;
  background: none; border: 0; border-radius: 0; overflow: visible;
  filter: drop-shadow(0 8px 14px rgba(0,0,0,0.5));
  transition: transform 0.14s var(--e-out), filter 0.18s; }
.mt-cloth, .mt-sheen {
  clip-path: polygon(0 0, 100% 0, calc(100% - var(--notch)) 50%, 100% 100%, 0 100%);
}
.mt-cloth { position: absolute; inset: 0; z-index: 0;
  background: linear-gradient(160deg, var(--gold-bright), color-mix(in srgb, var(--facl, var(--fac)) 60%, var(--gold-lt)) 18%,
    var(--gold) 46%, #b08829 72%, var(--gilt-seat));
  transition: background 0.2s; }
.mt-cloth::before {                    /* the field, and the weave in it */
  content: ''; position: absolute; inset: 2px; clip-path: inherit;
  background:
    repeating-linear-gradient(0deg, rgba(0,0,0,0.13) 0 1px, transparent 1px 4px),
    linear-gradient(180deg, rgba(255,255,255,0.14), rgba(255,255,255,0) 32%),
    linear-gradient(105deg, color-mix(in srgb, var(--fac) 42%, #17110a),
      color-mix(in srgb, var(--fac) 17%, #0b0907) 62%, #070605);
}
.mt-cloth::after {                     /* the kingdom's light, pooling at the rod */
  content: ''; position: absolute; inset: 2px; clip-path: inherit; opacity: 0.5;
  background: radial-gradient(70% 130% at 0 50%, color-mix(in srgb, var(--fac) 70%, transparent), transparent 72%);
}
.mt-sheen { position: absolute; inset: 2px; z-index: 1; overflow: hidden; pointer-events: none; }
.mt-sheen::before { content: ''; position: absolute; top: -25%; left: -55%; width: 30%; height: 150%;
  background: linear-gradient(100deg, transparent, rgba(255,246,214,0.32), transparent);
  transform: skewX(-16deg); opacity: 0; }
.mtile:hover .mt-sheen::before { animation: rcSheen 0.8s ease-out; }
/* the rod it hangs from, stood on end down the left edge */
.mt-rod { position: absolute; z-index: 2; left: 1px; top: -5px; bottom: -5px; width: 7px; border-radius: var(--r-sm);
  background: linear-gradient(90deg, var(--gold-bright), var(--gold) 46%, var(--gilt-deep));
  box-shadow: 2px 0 5px rgba(0,0,0,0.55), inset 1px 0 0 rgba(255,255,255,0.5); }
.mt-rod::before, .mt-rod::after { content: ''; position: absolute; left: 50%; width: 9px; height: 9px;
  border-radius: 50%; transform: translateX(-50%);
  background: radial-gradient(circle at 34% 30%, var(--gold-bright), var(--gold) 55%, var(--gilt-seat));
  box-shadow: var(--sh-1); }
.mt-rod::before { top: -5px; }
.mt-rod::after { bottom: -5px; }
.mtile > canvas, .mtile > div { position: relative; z-index: 3; }

.mtile:hover, .mtile:focus-visible { transform: translateX(4px);
  filter: drop-shadow(0 12px 20px rgba(0,0,0,0.58)) drop-shadow(0 0 16px color-mix(in srgb, var(--fac) 34%, transparent)); }
.mtile:focus-visible { outline: none; }
.mtile:hover .mt-cloth, .mtile:focus-visible .mt-cloth {
  background: linear-gradient(160deg, #fffbe8, color-mix(in srgb, var(--facl, var(--fac)) 70%, var(--gold-bright)) 15%,
    var(--gold-lt) 46%, var(--gold) 72%, var(--gold-dk)); }
.mtile.on { filter: drop-shadow(0 12px 22px rgba(0,0,0,0.55)) drop-shadow(0 0 18px color-mix(in srgb, var(--fac) 60%, transparent)); }
.mtile.on .mt-cloth {
  background: linear-gradient(160deg, #fff6dc, var(--facl, var(--fac)) 16%, var(--fac) 64%, var(--facd, var(--gilt-seat))); }
/* the crest grows with the tile it sits in — a 44px shield marooned in a 120px-tall
   banner reads as a mistake, not as breathing room */
.mt-crest { flex: none; width: clamp(40px, 5vh, 68px); height: auto;
  filter: drop-shadow(0 2px 4px rgba(0,0,0,0.6)); }
.mt-txt { min-width: 0; flex: 1; }
/* Bigger, on both rails. It is the one thing a tile has to say, and with the style
   line gone from the muster (see _armRail / the muster rail in ui.js) there is room
   for it to say it properly. */
.mt-name { font: 800 18px var(--font-display); color: #f4e6c0; line-height: 1.15;
  text-shadow: 0 2px 4px rgba(0,0,0,0.75); }
/* `.mt-style` — the three-word "what this kingdom plays like" line — is gone with
   the markup that carried it: at rail width it truncated mid-word on all seven, so
   what it printed was an ellipsis. The dossier beside the rail says it in full. */
.mt-tick { flex: none; width: 24px; height: 24px; border-radius: 50%; font-size: 13px; font-weight: 800; line-height: 24px; text-align: center;
  color: #08140b; background: linear-gradient(180deg, var(--ready-lt), var(--ready-dk));
  box-shadow: 0 0 0 1.5px rgba(10,26,14,0.6), 0 0 12px rgba(var(--ready-rgb),0.7);
  opacity: 0; transform: scale(0.5); transition: 0.16s; }
.mtile.on .mt-tick { opacity: 1; transform: scale(1); }

/* ---- the dossier ---- */
.musterdetail { --fac: var(--gold); display: flex; flex-direction: column; min-height: 0;
  padding: 15px 18px 16px; border-radius: var(--r-2xl); color: var(--ink);
  background:
    radial-gradient(120% 70% at 50% -10%, color-mix(in srgb, var(--fac) 12%, transparent), transparent 62%),
    linear-gradient(165deg, rgba(42,35,23,0.95), rgba(22,18,12,0.96));
  border: 1px solid var(--edge); border-top: 3px solid var(--fac);
  box-shadow: var(--sh-4), inset 0 1px 0 rgba(255,238,190,0.08); }
.musterdetail.preview { opacity: 0.92; }
/* THE SEAL. One ring of the kingdom's own light thrown out across the panel the
   instant a banner is chosen — the moment that decides the next twenty minutes used
   to be a class toggle and nothing else. It is drawn on a ::after so it costs the
   panel no box and cannot be hit by the mouse, and it runs ONCE per actual change
   (see `_pickMuster`), because a flourish that fires on every stray click is
   furniture rather than an event. */
.musterdetail { position: relative; overflow: hidden; }
.musterdetail::after { content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 5; opacity: 0;
  background: radial-gradient(closest-side circle at 22% 8%,
    color-mix(in srgb, var(--facl, var(--fac)) 55%, transparent), transparent 70%); }
.musterdetail.sealed::after { animation: mdSeal 0.62s var(--e-out) 1; }
@keyframes mdSeal {
  0%   { opacity: 0.85; transform: scale(0.12); }
  100% { opacity: 0;    transform: scale(2.6); }
}
/* ---- the banner band: who they are, how they win, and the numbers ----
   FOUR ZONES, ONE PINNED HEIGHT. The height has to be a constant across all seven
   kingdoms or the roster underneath is a different size on each of them — see the
   note at the markup in `_musterDetail`. So the band is given its height rather than
   taking one from its longest sentence, every zone inside it clips, and the prose
   line-clamps. What that buys is the thing the old screen could not do: the kingdom
   and its army on screen together, at the same time, in the same glance. */
/* The four widths are set by the LONGEST thing each zone has to hold across all
   seven kingdoms, not by eye: a 118-character doctrine, a 110-character strength
   line, four trait bars and three power lines of up to 85 characters. That is why
   the doctrine's column is the widest — it is the only zone carrying two blocks of
   running prose, and prose is the one thing here that cannot be shortened without
   losing what it says. */
/* FOUR ZONES BECAME THREE, and the two that stayed took the room. The tally chips,
   the doctrine plate and the racial trait bars are gone — between them they were more
   than half the band's width and none of them answered a question you ask while
   choosing a kingdom. What is left is WHO they are, what beats them and what they
   beat, and the powers worth picking them for, each at a size you can read across a
   room instead of three columns of 9px type competing for a glance.
   The height is still PINNED and still identical for all seven — the roster below is
   fitted to what the band leaves (see _rosterPx), so a band that breathed per kingdom
   would resize the army as you skimmed the rail. */
.md-head { flex: none; height: clamp(120px, 17.5vh, 176px); overflow: hidden;
  display: grid; grid-template-columns: minmax(238px, 1.15fr) minmax(250px, 1.2fr) minmax(250px, 1.35fr);
  align-items: stretch; gap: clamp(12px, 1.8vw, 26px);
  padding-bottom: clamp(7px, 1.3vh, 12px); margin-bottom: clamp(7px, 1.3vh, 12px);
  border-bottom: 1px solid rgba(255,255,255,0.08); }
.md-idcol { display: flex; align-items: center; gap: clamp(8px, 1.4vh, 13px); min-width: 0; }
.md-crest { flex: none; width: clamp(60px, 9.6vh, 100px); height: auto;
  filter: drop-shadow(0 4px 12px rgba(0,0,0,0.8)) drop-shadow(0 0 18px color-mix(in srgb, var(--fac) 45%, transparent));
  animation: mdCrest 0.5s var(--e-out) both; }
@keyframes mdCrest { from { opacity: 0; transform: translateX(-10px) scale(0.9); } to { opacity: 1; transform: none; } }
.md-id { min-width: 0; }
/* Capped at 22px, not 25. A kingdom's name is two words and the id column is the
   narrowest zone in the band, so at 25 the longer four wrapped to two lines — and
   two lines of 25px display type plus a blurb, a style line and a wrapped tally is
   more than the band has. It is still the largest type on the panel. */
.md-head h2 { margin: 0; font: 900 clamp(19px, 3.1vh, 30px) var(--font-logo); color: #fff6e0; letter-spacing: 0.4px; line-height: 1.02;
  text-shadow: 0 2px 14px color-mix(in srgb, var(--fac) 62%, transparent), 0 1px 2px rgba(0,0,0,0.8); }
.md-blurb { font: 500 clamp(12px, 1.95vh, 15px) 'Spectral', serif; color: #e8dcc0; font-style: italic; margin-top: 3px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.md-style { margin-top: 6px; font: 800 clamp(9px, 1.35vh, 11.5px) var(--font-ui); letter-spacing: 2px; text-transform: uppercase;
  color: color-mix(in srgb, var(--facl, var(--fac)) 78%, #f2e7cd);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The tally: how big the army is, how much of it is the kingdom's own, and how much
   of it you have. The third figure is the one nothing on this screen said before —
   the green ticks say it forty times over and never add up. */
/* Struck small on purpose. Three pills have to sit on ONE row inside the narrowest
   zone of the band — a second row is 20px, and the id column is already carrying a
   two-line name — so the padding, the gap and both type sizes are the smallest that
   still read. They may still wrap on the narrowest layouts, and the band has the
   room for that; they simply should not have to. */


/* the doctrine and the two lines that qualify it — how this kingdom wins, and the
   one thing that beats it. Both prose, both clamped, so a long weakness cannot take
   a pixel of height from the army below. */
.md-brief { min-width: 0; display: flex; flex-direction: column; justify-content: center; gap: clamp(3px, 0.7vh, 6px); }
/* THREE LINES, and the clamp is a backstop rather than the plan. The longest `how`
   in the game is 118 characters and this column is sized to take it in three at the
   font below — a two-line clamp cut the second sentence off five of the seven
   kingdoms, which is a worse failure than a band one line taller: the sentence that
   went was always the one saying how the kingdom LOSES. */

/* THE ONE QUESTION THIS SCREEN ASKS, ANSWERED TWICE. What this kingdom beats and what
   beats it were two clamped lines of 10px prose under a plate three times their size.
   They are the plate now: two cards, each with its arrow struck into a roundel in its
   own colour, at a size you read rather than squint at. They deal in on a stagger,
   because a thing that arrives is a thing you look at. */
.md-edge { display: flex; flex-direction: column; justify-content: center; gap: clamp(5px, 0.9vh, 9px); min-width: 0; }
.md-edge em { display: flex; align-items: flex-start; gap: clamp(7px, 0.8vw, 10px);
  padding: clamp(6px, 1vh, 10px) clamp(9px, 0.9vw, 13px); border-radius: var(--r-lg);
  font-style: normal; animation: mdEdge 0.42s var(--e-out) both; }
.md-edge em > i { flex: none; font-style: normal; display: inline-flex; align-items: center; justify-content: center;
  width: clamp(17px, 2.4vh, 22px); height: clamp(17px, 2.4vh, 22px); border-radius: 50%;
  font-size: clamp(9px, 1.2vh, 11px); font-weight: 900; }
.md-edge em > span { min-width: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; font: 500 clamp(11px, 1.6vh, 14px) 'Spectral', serif; line-height: 1.28; }
.md-good { color: #e2f0c2; background: linear-gradient(100deg, rgba(150,200,90,0.17), rgba(0,0,0,0.3));
  box-shadow: inset 0 0 0 1px rgba(170,214,110,0.32); animation-delay: 0.04s; }
.md-good > i { color: #16200a; background: linear-gradient(180deg, #cfe7a0, #93bb56); }
.md-bad { color: #f2cebf; background: linear-gradient(100deg, rgba(214,110,80,0.17), rgba(0,0,0,0.3));
  box-shadow: inset 0 0 0 1px rgba(224,140,110,0.32); animation-delay: 0.11s; }
.md-bad > i { color: #230d06; background: linear-gradient(180deg, #f0b49c, #cd7350); }
@keyframes mdEdge { from { opacity: 0; transform: translateX(-9px); } to { opacity: 1; transform: none; } }

/* the two measured columns — the racial trait bars and the kingdom's own powers.
   Both reuse the `.kd-*` build the in-match briefing draws, tightened to band height
   here rather than re-invented, so a trait can never be shown two different ways. */
.md-col { min-width: 0; display: flex; flex-direction: column; justify-content: center; gap: clamp(3px, 0.6vh, 5px); overflow: hidden; }
.md-col h6 { margin: 0 0 clamp(3px, 0.6vh, 6px); font: 800 clamp(9px, 1.3vh, 11.5px) var(--font-display);
  letter-spacing: 2px; text-transform: uppercase;
  color: color-mix(in srgb, var(--facl, var(--fac)) 58%, #f2e7cd); opacity: 0.95;
  display: flex; align-items: center; gap: 8px; }
.md-col h6::after { content: ''; flex: 1; height: 1px;
  background: linear-gradient(90deg, color-mix(in srgb, var(--fac) 45%, transparent), transparent); }
/* TIGHT ENOUGH FOR THE LONGEST KINGDOM. The Undead's three powers run to four
   lines between them — one is the sentence explaining how their whole army works —
   and at the first cut of this size they overflowed the band by 7px, which clips the
   last power's descenders and says nothing about why. Measured against that kingdom,
   not against a comfortable one. */
.md-pcol .kd-powers { gap: clamp(3px, 0.55vh, 5px); }
/* Three lines here too, for the same reason as the doctrine: the longest kingdom
   power in the game is 85 characters, which is three lines in this column, and it
   was losing its last clause — on the Undead, whose longest power is the one that
   explains how their whole army works. */
.md-pcol .kd-powers div { padding: clamp(3px, 0.55vh, 5px) clamp(8px, 0.8vw, 12px); border-radius: var(--r-md);
  background: linear-gradient(100deg, color-mix(in srgb, var(--fac) 14%, rgba(0,0,0,0.34)), rgba(0,0,0,0.3));
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fac) 24%, transparent);
  font: 500 clamp(10.5px, 1.5vh, 13px) 'Spectral', serif; line-height: 1.27; color: #ece1c6;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
  animation: mdPow 0.4s var(--e-out) both; animation-delay: calc(0.06s * var(--i, 0) + 0.06s); }
@keyframes mdPow { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.md-pcol .kd-powers .ic { color: var(--gilt-lit); }

/* THE BAND SHEDS ZONES, NEVER WORDS. Every one of these is a width at which the
   remaining zones stop being wide enough to hold their longest kingdom's prose in
   the lines they are clamped to — measured, not guessed, and measured against the
   width the BAND gets rather than the viewport's, which is two whole columns wider
   (the rail and the stage both come out of it first).

   IT TIGHTENS NOW; IT DOES NOT SHED. The old pair of rules fired at 1700 and 1400 and
   dropped the powers first, on the grounds that those three sentences are also in the
   Armory. That was true of a band carrying four zones — it is the wrong trade for one
   carrying three, because the powers are the thing a commander actually chooses a
   kingdom FOR, and "it is also somewhere else" is a poor reason to take it off the
   screen where the choice is being made.
   So all three zones survive every width the three-column layout exists at. The only
   squeeze is between 1200 (where the stage drops and the band gets its width back) and
   1360, and it is answered by narrower minimums and a step down in type rather than by
   deleting a zone. */
@media (max-width: 1360px) {
  .md-head { grid-template-columns: minmax(186px, 1fr) minmax(196px, 1.15fr) minmax(206px, 1.3fr);
    gap: clamp(9px, 1.2vw, 16px); }
  .md-head h2 { font-size: clamp(17px, 2.7vh, 24px); }
  .md-crest { width: clamp(54px, 8.4vh, 84px); }
  .md-edge em > span { font-size: clamp(10px, 1.45vh, 12.5px); }
  .md-pcol .kd-powers div { font-size: clamp(9.5px, 1.35vh, 11.5px); }
  /* The rail and the stage give ground before the army does. Both are fixed-width
     columns holding one object each — a banner and a portrait — and both read
     perfectly well fifty pixels narrower; the roster is the only column whose
     contents actually scale with the room it is given, so every pixel taken off
     these two comes back as a bigger troop. */
  .musterlayout { grid-template-columns: minmax(178px, 212px) minmax(0, 1fr) minmax(300px, 350px); }
}
/* …and on a SHORT viewport it sheds by height instead. At 720p the band is 118px and
   four stacked things do not go into it: the doctrine's three lines are the ones
   worth keeping, so the two summary lines go (the traits, the powers and the roster
   all say the same thing more precisely) and the style words go with them — they are
   on the banner in the rail, six inches to the left. */
/* …and a SHORT viewport tightens rather than sheds, for the same reason. This used to
   hide the EDGE lines and keep the doctrine plate, which was right when the plate was
   three lines of prose and the edge a two-line summary of it. The plate is gone, so
   the edge lines ARE the prose — and the powers stay, because they are what the choice
   is made on. Everything steps down instead: the style words go (they are on the
   banner in the rail, six inches to the left), the crest and the type come in, and the
   band still hands some height back to the army.
   THE POWERS KEEP THEIR THREE LINES, and the band is cut as fine as that allows. Every
   pixel here is a pixel off the army: measured at a 617px-tall pane, each 8px of band
   is a 4px step of chip size, and the chips are already down to the twenties there. So
   the height is the least that still holds the Undead's longest power — the sentence
   explaining how their whole army works, which a two-line clamp cuts the last clause
   off, and that is the failure the three-line clamp exists to prevent. */
@media (max-height: 800px) {
  .md-style { display: none; }
  .md-head { height: clamp(98px, 14.2vh, 130px); }
  .md-crest { width: clamp(48px, 7.4vh, 68px); }
  .md-head h2 { font-size: clamp(16px, 2.5vh, 22px); }
  .md-blurb { font-size: clamp(10.5px, 1.7vh, 12.5px); }
  .md-edge { gap: 4px; }
  .md-edge em { padding: 4px 8px; }
  .md-edge em > i { width: 16px; height: 16px; font-size: 9px; }
  .md-edge em > span { font-size: clamp(10px, 1.45vh, 12px); }
  .md-col h6 { margin-bottom: 2px; font-size: clamp(8.5px, 1.15vh, 10px); }
  .md-pcol .kd-powers { gap: 3px; }
  .md-pcol .kd-powers div { padding: 2px 8px; font-size: clamp(9.5px, 1.3vh, 11px); line-height: 1.25; }
}
/* …and below this the three-line promise finally cannot be kept: a 660px-tall pane
   leaves the powers about 69px and the Undead's three want 78. The choice is a clipped
   sentence or no powers at all, and a clipped sentence is the better of the two — the
   block stays, and the kingdom's headline effects are still on the screen where the
   choice is made. It is the only viewport in the game where a power is shortened. */
@media (max-height: 660px) {
  .md-pcol .kd-powers div { -webkit-line-clamp: 2; }
}
/* THE BODY IS ONE COLUMN NOW. It used to split into a 232px prose rail and the
   roster beside it — `grid-template-columns: 232px minmax(0, 1fr)` — so the rail's
   width was pinned exactly so it could never nudge the roster's columns between
   kingdoms. With the rail gone there is nothing left to pin against; the roster
   takes the frame's whole width, and `_rosterPx` (which measures the real box
   rather than assuming one) simply finds bigger chips fit in it now. */
.md-body { flex: 1 1 auto; min-height: 0; display: grid; grid-template-rows: minmax(0, 1fr); }
.md-h { flex: none; display: flex; align-items: center; gap: 8px; margin: clamp(6px, 1.5vh, 13px) 0 clamp(4px, 0.9vh, 7px);
  font: 800 clamp(10px, 1.65vh, 11.5px) var(--font-display);
  color: color-mix(in srgb, var(--facl, var(--fac)) 40%, #fff2d8); letter-spacing: 1px; text-transform: uppercase;
  text-shadow: 0 1px 2px rgba(0,0,0,0.8); }
.md-hn { font: 800 10.5px var(--font-display); color: #14100a;
  background: linear-gradient(180deg, color-mix(in srgb, var(--facl, var(--fac)) 70%, #fff3d8), var(--facl, var(--fac)));
  border-radius: var(--r-3xl); padding: 2px 9px; letter-spacing: 0.5px;
  box-shadow: 0 0 10px color-mix(in srgb, var(--fac) 45%, transparent); }

/* ---- the roster, in full ----
   The WHOLE army, on one screen, without a scrollbar. That is the requirement; it is
   how you compare two kingdoms at a glance, and a roster you have to scroll through is
   a roster you cannot read.

   This was a multi-column block (`column-width`), which is exactly the wrong box for
   the job: when a multicol container runs out of height it does not scroll, it lays
   out MORE COLUMNS to the right — so eight of the eleven categories were parked
   off-screen behind a horizontal scrollbar.

   A grid was the next attempt and still wrong: equal tracks size every category to the
   widest one, so a Levy of one sat in a box built for a Shock of five, and the wasted
   width pushed the set back off the bottom. These cards flow INLINE at their own width
   and wrap, which packs eleven uneven categories into three lines instead of six. */
/* Roster + structures, sharing what the dossier has left. This is the one part that
   is allowed to shrink, so everything above it must be `flex: none` and everything
   inside it must be free to give. */
.md-lower { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
/* A column grid, not a wrapping strip. BOTH the column count and the row track list are
   written inline by _fitRoster, and both are stated explicitly — `repeat(N, …)` rather
   than an auto-fill that has to infer N from a pinned width, because an inferred count
   can round to something the fit never measured, and then the row tracks describe a
   grid that is not the one on screen. The rule here is only what applies before the
   first measurement. Each row is pinned to the tallest role block ANY of the seven
   kingdoms puts in it, so a role holds the same cell — same column AND same row —
   whichever banner you are hovering. */
.md-roster { flex: 1 1 auto; min-height: 0; display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  align-content: start; gap: 13px 16px; overflow-y: auto; padding-right: 4px;
  /* THE GUTTER IS ALWAYS RESERVED. With `auto` the scrollbar appears only when the
     content overflows, and it takes ~10px out of the CONTENT width when it does — so
     three kingdoms whose blocks ran a hair past their track grew a bar, lost 10px, and
     every column after the first shifted left by two to eight pixels. Identical chip,
     identical tracks, identical box, and the roster still moved. Reserving the gutter
     unconditionally makes the content width the same number for all seven whether a bar
     is showing or not, which is the only way the columns can be guaranteed to line up. */
  scrollbar-gutter: stable; }
.md-cat { align-content: start; }
.md-roster::-webkit-scrollbar { width: 8px; }
.md-roster::-webkit-scrollbar-thumb { background: rgba(var(--gold-rgb),0.34); border-radius: var(--r-sm); }

/* One TITLED SECTION per role, laid into columns. The roles used to be labels wedged
   into one long running flow, so a category began and ended wherever the line happened
   to wrap — LEVY and RANGED sharing a row, ANTI-ARMOR stranded on the end of another.
   There was nothing to scan down and no group with a clear top. Columns give every
   role a heading, a rule and its own block, and they fill the panel's width instead of
   leaving the bottom third of it empty. */
.md-cat { --cat: var(--gilt-body); display: flex; flex-direction: column; gap: 5px;
  padding: 0; background: none; break-inside: avoid; }
/* A stop larger, and with the headcount gone the name has the rule to itself. These are
   the labels you navigate the whole panel by — thirteen of them, read at a glance while
   comparing kingdoms — so they were the wrong thing to have shrunk to 9px. */
.md-cath { display: flex; align-items: center; gap: 8px; margin: 0;
  font: 800 clamp(11px, 1.62vh, 13.5px) var(--font-display); letter-spacing: 1.3px; text-transform: uppercase;
  color: color-mix(in srgb, var(--cat) 55%, #f2e7cd); text-shadow: 0 1px 2px rgba(0,0,0,0.8); }
.md-cath i { font-style: normal; white-space: nowrap; }
/* an <s> purely as a rule — it carries the category's colour out across the block */
.md-cath s { flex: 1 1 auto; min-width: 10px; height: 2px; border-radius: var(--r-xs); text-decoration: none;
  background: linear-gradient(90deg, color-mix(in srgb, var(--cat) 62%, transparent), transparent); }
.md-catrow { display: flex; flex-wrap: wrap; gap: 5px 5px; align-items: flex-start; }
/* A ROLE THIS KINGDOM CANNOT MUSTER. It holds its slot so every other block keeps its
   place — and it is worth a slot: that the Dwarven Clans field no Cavalry at all is one
   of the defining things about them. Dimmed to about a third, so the eye reads the roles
   the kingdom HAS first and finds the holes second. */
.md-cat.md-catnone { opacity: 0.34; }
.md-none { font: italic 500 calc(var(--chip, 54px) * 0.2) 'Spectral', serif; color: #8b8069; padding: 2px 1px; }

/* The chip is sized off the VIEWPORT HEIGHT, because height is the scarce thing: a
   short window shrinks every portrait rather than pushing the last four categories
   off the bottom. Seeing the whole army matters more than the size of any one face. */
/* `--chip` is MEASURED, not guessed — see `_fitRoster`. A viewport-height clamp cannot
   answer "how big can a chip be so that forty of them still fit", because the answer
   depends on the roster, the panel width and whatever height the prose left over; it
   guessed low and left the roster crammed into the top half of an empty box. The fit
   searches for the largest size that still fits and stops there, so the chips are as
   big as the space genuinely allows. */
/* The chip is WIDER than its portrait. A square card is the wrong shape for a card
   whose second job is carrying a word: at chip-width, "Conscript" missed fitting by a
   pixel and the browser snapped the final "t" onto its own line. A third again of
   width costs about two chips per rank and buys nearly every name a single line. */
.md-troop { position: relative; display: flex; flex-direction: column; align-items: center;
  width: calc(var(--chip, 54px) * 1.34); gap: 1px;
  padding: 4px 3px 3px; border-radius: var(--r-lg); background: rgba(0,0,0,0.3);
  border: 1px solid color-mix(in srgb, var(--cat) 24%, transparent); cursor: default;
  transition: transform 0.11s var(--e-back), border-color 0.14s, box-shadow 0.14s, background 0.14s; }
.md-troop:hover { transform: translateY(-3px); background: rgba(255,255,255,0.06);
  border-color: var(--cat); box-shadow: var(--sh-3), 0 0 14px color-mix(in srgb, var(--cat) 34%, transparent); z-index: 2; }
/* `--us` is the per-unit scale — a Titan is drawn bigger than a levyman, the way the
   battlefield draws them. The CELL stays the same size for everyone so the column grid
   never goes ragged; only the portrait inside it grows. */
.md-troop canvas { width: calc(var(--chip, 54px) * 0.76 * var(--us, 1)); height: calc(var(--chip, 54px) * 0.76 * var(--us, 1)); }
/* The label is dropped only when the fit has already shrunk the chip past the point of
   carrying it — the portrait still identifies the troop, and every chip names itself
   and its price on hover whatever size it is. */
.md-roster.no-name .md-troop i { display: none; }
.md-roster.no-name .md-troop { padding: 3px 2px; }
/* On a narrow window the eleven category WORDS cost ~380px of inline width between
   them — more than six troops. The coloured count pill and the tinted left edge still
   say which category a group is, and the group still reads as a group. */
@media (max-width: 1150px) {
  .md-catname { width: auto; padding-right: 5px; }
  .md-catname i { display: none; }
}
/* Names WRAP rather than truncate. "Sharps…", "Spellbl…" and "Archm…" are not names —
   a label clipped to seven characters is worse than no label, because it looks like
   information while telling you nothing. Two lines, sized off the chip, and the fit
   above is what guarantees the two lines have somewhere to go.

   But `overflow-wrap: anywhere` breaks at whatever character the line happens to end
   on, which is what printed "Execution / er", "Sandwor / m" and "Plaguebri / nger" —
   a name snapped mid-syllable reads as damage, not as a label. `break-word` only
   breaks a word that cannot fit a line of its own, and `hyphens` (the page is lang=en)
   gets first refusal, so a long name comes apart at a syllable with a hyphen to say
   so. Multi-word names still break at the space, where they always should have. */
.md-troop i { flex: none; font: 600 calc(var(--chip, 54px) * 0.185) 'Spectral', serif; font-style: normal;
  color: #ded2b4; line-height: 1.14; text-align: center; width: 100%;
  overflow-wrap: break-word; hyphens: auto;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
/* the few very long single-word names ("Plaguebringer") sit down a notch rather than
   shed their last letter onto a line of its own */
.md-troop i.long { font-size: calc(var(--chip, 54px) * 0.163); letter-spacing: -0.2px; }
.md-troop canvas { flex: none; }
.md-troop.sig { border-color: rgba(var(--gilt-lit-rgb),0.55); background: linear-gradient(180deg, rgba(var(--gilt-lit-rgb),0.14), rgba(0,0,0,0.32));
  box-shadow: inset 0 0 0 1px rgba(var(--gilt-lit-rgb),0.12); }
.md-troop.sig::after { content: '★'; position: absolute; top: -4px; right: -2px; font-size: 10px; color: var(--gilt-lit);
  text-shadow: 0 0 6px rgba(var(--gilt-lit-rgb),0.95); }
.md-troop.sig i { color: var(--gold-lt); }
.md-troop.sig em { color: #d3b86e; }

/* OWNED — a green tick on every troop you can actually field. Top-LEFT, so it never
   collides with the gold exclusivity star in the opposite corner. */
.md-troop.own::before { content: '✓'; position: absolute; top: -6px; left: -5px; z-index: 2;
  width: 19px; height: 19px; border-radius: 50%; font: 800 12px var(--font-display); line-height: 19px; text-align: center;
  color: #06150b; background: linear-gradient(180deg, var(--ready-lt), var(--ready-dk));
  border: 1.5px solid rgba(6,21,11,0.72); box-shadow: var(--sh-0), 0 0 10px rgba(var(--ready-rgb),0.75); }
/* Not-owned troops are NOT dimmed. The tick alone carries the distinction — greying
   two thirds of a roster made the kingdom look impoverished and buried the artwork,
   when the only thing being said is "you haven't bought this one yet". The `.locked`
   class stays for the tooltip, which spells it out on hover. */
.md-counts .own { border-color: rgba(var(--ready-rgb),0.4); background: linear-gradient(180deg, rgba(var(--ready-rgb),0.1), rgba(0,0,0,0.4)); }
.md-counts .own b { color: var(--ready); text-shadow: 0 0 10px rgba(var(--ready-rgb),0.45); }
/* `.md-legown` — the green tick in the roster header's legend — went with the header
   itself. The tick on the chip is its own legend. */

.md-works { flex: none; display: flex; flex-wrap: wrap; gap: 8px; }
/* A STRUCTURE CHIP IS A TROOP CHIP NOW — same portrait-over-name shape, same green
   tick once it is yours, same rich panel one hover away (`showEmpTip`, forked off
   the same `data-t` the troops use). It used to be a coloured label with the
   category's NAME spelled out on it and nothing else — no picture, no hover, and a
   Tar Pit read no differently from a Blood Totem beyond which word was printed. The
   `.md-troop` base gives all of that back for free; `.md-work` only overrides the
   SIZE, because it does not share `.md-roster`'s fitted `--chip` — that variable is
   set directly on the roster grid to pack up to forty troops into whatever room the
   panel has, and `.md-works` is that grid's SIBLING, not its descendant, so the
   value would not reach it even if a shared size were wanted. There are rarely more
   than a dozen structures; they read better at one comfortable size than at
   whatever the busiest kingdom's roster happened to be squeezed down to. */
.md-work { width: 80px; }
.md-work canvas { width: 46px; height: 46px; }
/* THE COMMIT BUTTON IS A BUTTON, NOT A FLOOR. `width: 100%` stretched it across
   the entire roster — sixteen hundred pixels of gilt on a wide monitor — so the
   loudest object on a screen full of troops you are there to READ was the bar
   underneath them. It takes the room its own words need and sits centred, which
   is what every other commit control in this interface does. */
/* THE PRESS THAT STARTS A WAR should not be the same size as a menu item. It is the
   one irreversible control on the screen and the last thing a commander does here,
   so it is wider, louder, and it breathes — and a light runs across it on hover, the
   same sweep the dais gets, so the two ends of the screen answer each other. */
.md-march {
  flex: none; align-self: center; margin: clamp(13px, 2vh, 20px) auto 2px;
  width: auto; min-width: 320px; max-width: min(520px, 94%);
  font-size: clamp(15px, 1.9vh, 20px); letter-spacing: 2.4px;
  padding: clamp(11px, 1.6vh, 16px) clamp(30px, 4vw, 54px);
  border-radius: var(--r-lg);
  position: relative; overflow: hidden;
  animation: mdMarch 2.8s ease-in-out infinite;
}
@keyframes mdMarch {
  0%, 100% { box-shadow: var(--sh-2), 0 0 0 rgba(var(--gilt-lit-rgb),0); }
  50%      { box-shadow: var(--sh-2), 0 0 26px rgba(var(--gilt-lit-rgb),0.42); }
}
.md-march::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(104deg, rgba(255,255,255,0) 38%, rgba(255,255,255,0.34) 50%, rgba(255,255,255,0) 62%);
  transform: translateX(-120%);
}
.md-march:hover::after { animation: mdMarchSheen 0.62s var(--e-out) both; }
@keyframes mdMarchSheen { from { transform: translateX(-120%); } to { transform: translateX(120%); } }
/* Stillness, for anyone who asked for it: the word stays, the motion goes. */
@media (prefers-reduced-motion: reduce) {
  .md-march { animation: none; }
  .md-march:hover::after { animation: none; }
  .musterstage.in .ms-plinth::after { animation: none; opacity: 0; }
  .md-edge em, .md-pcol .kd-powers div, .md-crest { animation: none; }
}

/* THE TROOP ON THE STAGE, marked in the roster it came out of. A ring rather than a
   fill: the chip's own category colour already owns its border, and a second filled
   state next to forty unfilled ones reads as "selected for an action" — which this
   is not. It says WHICH of these forty the panel on the right is describing. */
.md-troop.staged { border-color: var(--gilt-lit); background: rgba(255,255,255,0.07);
  box-shadow: 0 0 0 1px rgba(var(--gilt-lit-rgb),0.55), 0 0 16px rgba(var(--gilt-lit-rgb),0.3); }
.md-troop.staged i { color: #fff6dc; }
.md-troop:focus-visible { outline: 2px solid var(--gold-lt); outline-offset: 2px; }

/* ═══════════════════════════════════════════════════════════════════════════
                        THE CHAMPION'S STAGE
   The third column. One troop out of the forty beside it, presented instead of
   listed: lit from above on a dais, in the kingdom's own colours, with every
   number it has drawn as a bar you can hold against the next troop's.

   It replaces a HOVER TIP — a panel that existed only while your hand was still
   and vanished the moment you looked away from it, on the one screen whose whole
   job is understanding an army before you commit to it. The tip is still there
   and still does this job below the breakpoint at the foot of this block, where
   there is no room for a third column.

   EVERY KINGDOM'S STAGE IS THE SAME STAGE. Same dais, same five bars in the same
   order, same eight medallions along the foot. Two armies cannot be compared
   through two layouts, so there is only one.
   ═══════════════════════════════════════════════════════════════════════════ */
.musterstage { --fac: var(--gold); --cat: var(--gilt-body);
  display: flex; flex-direction: column; gap: clamp(4px, 0.8vh, 8px); min-height: 0;
  padding: clamp(9px, 1.3vh, 14px) clamp(10px, 1vw, 15px) clamp(10px, 1.2vh, 14px);
  border-radius: var(--r-2xl); color: var(--ink); overflow: hidden auto; scrollbar-gutter: stable;
  background:
    radial-gradient(115% 55% at 50% 0%, color-mix(in srgb, var(--fac) 20%, transparent), transparent 66%),
    linear-gradient(165deg, rgba(40,33,21,0.96), rgba(17,14,9,0.97));
  border: 1px solid var(--edge); border-top: 3px solid var(--fac);
  box-shadow: var(--sh-4), inset 0 1px 0 rgba(255,238,190,0.08); }
.musterstage::-webkit-scrollbar { width: 8px; }
.musterstage::-webkit-scrollbar-thumb { background: rgba(var(--gold-rgb),0.34); border-radius: var(--r-sm); }
/* The whole column arrives when the troop changes — a swap you can see, so leafing
   through a roster feels like turning cards rather than re-rendering a table. The
   dais and the bars carry their own, longer entrances underneath this one. */
.musterstage.in { animation: msIn 0.26s var(--e-out) both; }
@keyframes msIn { from { opacity: 0; transform: translateY(7px); } to { opacity: 1; transform: none; } }
/* …AND A LIGHT CROSSES THE DAIS AS IT ARRIVES. One pass, on the entrance only —
   a shimmer that loops is a screensaver, and this column is looked at for as long
   as it takes to decide something. It is drawn over the heraldry and under the
   figure (z-index 2 sits between the back wall and the portrait's 3), so the troop
   is never washed out by its own presentation. */
.musterstage.in .ms-plinth::after {
  content: ''; position: absolute; z-index: 2; inset: 0; pointer-events: none;
  background: linear-gradient(104deg, rgba(255,242,206,0) 38%, rgba(255,242,206,0.19) 50%, rgba(255,242,206,0) 62%);
  animation: msSweep 0.72s 0.06s var(--e-out) both;
}
@keyframes msSweep { from { transform: translateX(-118%); } to { transform: translateX(118%); } }

.ms-hdr { flex: none; display: flex; align-items: center; gap: 8px;
  font: 800 clamp(9.5px, 1.35vh, 12px) var(--font-display); letter-spacing: 1.8px; text-transform: uppercase;
  color: color-mix(in srgb, var(--facl, var(--fac)) 46%, #f2e7cd); }
.ms-hdr > i { color: var(--gilt-lit); font-size: 16px; display: inline-flex; }
.ms-hdr > span { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* the tier ribbon — three steps off the same cost ladder the roster is sorted by */
.ms-tier { flex: none; padding: 3px 10px; border-radius: var(--r-pill); font: 800 10px var(--font-display); letter-spacing: 1.2px;
  color: #1a1205; background: linear-gradient(180deg, var(--gilt-body), var(--gold-dk)); box-shadow: var(--sh-0); }
.ms-tier.t2 { background: linear-gradient(180deg, var(--gold-lt), var(--gold)); }
.ms-tier.t3 { background: linear-gradient(180deg, var(--gold-hi), var(--gilt-lit));
  box-shadow: var(--sh-0), 0 0 12px rgba(var(--gilt-lit-rgb),0.55); }

/* ---- the dais ----
   A stage, not a thumbnail: the kingdom's heraldry ghosted on the back wall, a pool
   of its light from above, seven motes drifting through it, and the troop standing
   on a struck-gold ellipse. It is `flex: 1 1 auto` so it takes whatever the column
   has spare after the readouts — a tall window gets a taller stage. */
.ms-plinth { position: relative; flex: 1 1 auto; min-height: clamp(158px, 27vh, 286px);
  display: flex; align-items: flex-end; justify-content: center; overflow: hidden;
  border-radius: var(--r-xl); isolation: isolate;
  background:
    radial-gradient(80% 62% at 50% 100%, color-mix(in srgb, var(--fac) 26%, transparent), transparent 72%),
    linear-gradient(180deg, rgba(8,7,5,0.62), rgba(0,0,0,0.5));
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fac) 26%, transparent), inset 0 18px 34px rgba(0,0,0,0.55); }
.ms-heraldry { position: absolute; top: 6%; left: 50%; transform: translateX(-50%);
  width: clamp(72px, 12vh, 118px); height: auto; opacity: 0.14; filter: saturate(0.4) blur(0.4px); z-index: 0; }
/* the candle: one shaft of the kingdom's colour down the back of the stage */
.ms-light { position: absolute; inset: -18% 12% 30%; z-index: 1; pointer-events: none;
  background: radial-gradient(52% 68% at 50% 0%, color-mix(in srgb, var(--facl, var(--fac)) 34%, transparent), transparent 74%);
  filter: blur(6px); }
/* dust in the shaft. Seven, each with its own drift, because a stage with nothing
   moving on it is a photograph. */
.ms-motes { position: absolute; inset: 0; z-index: 2; pointer-events: none; }
.ms-motes i { position: absolute; width: 3px; height: 3px; border-radius: 50%; bottom: 12%;
  background: color-mix(in srgb, var(--facl, var(--fac)) 70%, #fff6dc); opacity: 0;
  box-shadow: 0 0 6px color-mix(in srgb, var(--facl, var(--fac)) 80%, transparent);
  animation: msMote 7s linear infinite; }
.ms-motes i:nth-child(1) { left: 18%; animation-duration: 8.5s; animation-delay: -0.4s; }
.ms-motes i:nth-child(2) { left: 31%; animation-duration: 6.8s; animation-delay: -2.7s; }
.ms-motes i:nth-child(3) { left: 44%; animation-duration: 9.4s; animation-delay: -5.1s; }
.ms-motes i:nth-child(4) { left: 57%; animation-duration: 7.6s; animation-delay: -1.3s; }
.ms-motes i:nth-child(5) { left: 68%; animation-duration: 8.1s; animation-delay: -6.2s; }
.ms-motes i:nth-child(6) { left: 79%; animation-duration: 6.4s; animation-delay: -3.6s; }
.ms-motes i:nth-child(7) { left: 88%; animation-duration: 9.9s; animation-delay: -4.8s; }
@keyframes msMote {
  0%   { transform: translateY(0) scale(0.7); opacity: 0; }
  14%  { opacity: 0.85; }
  78%  { opacity: 0.5; }
  100% { transform: translateY(-120px) scale(1.15); opacity: 0; }
}
/* THE TROOP. It stands ON the dais — bottom-aligned, lifted clear of the gold
   ellipse — and breathes, which is the whole difference between a portrait and an
   icon. `image-rendering` stays default: these are baked at 160–320px and drawn at
   200, so this is a DOWNSCALE and smoothing is what keeps the armour crisp. */
.ms-art { position: relative; z-index: 3; width: clamp(140px, 25vh, 262px); height: auto; margin-bottom: 6%;
  filter: drop-shadow(0 10px 14px rgba(0,0,0,0.72)) drop-shadow(0 0 22px color-mix(in srgb, var(--fac) 30%, transparent));
  animation: msBreathe 4.2s ease-in-out infinite, msArrive 0.4s var(--e-back) both; }
@keyframes msBreathe { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }
@keyframes msArrive { from { opacity: 0; transform: translateY(14px) scale(0.9); } to { opacity: 1; } }
/* the struck ellipse it stands on — gilt seen almost edge-on */
.ms-dais { position: absolute; z-index: 2; left: 50%; bottom: 5%; transform: translateX(-50%);
  width: 62%; height: 15px; border-radius: 50%;
  background: radial-gradient(closest-side ellipse at 50% 34%, var(--gold-hi), var(--gold) 42%, var(--gilt-seat) 78%, transparent);
  box-shadow: 0 0 22px color-mix(in srgb, var(--fac) 42%, transparent); opacity: 0.75; }
/* the two corner marks: what ROLE it is, and whether it is one of the kingdom's own */
.ms-badge { position: absolute; z-index: 4; top: 7px; left: 7px; padding: 2px 8px; border-radius: var(--r-pill);
  font: 800 8.5px var(--font-display); letter-spacing: 1.1px; text-transform: uppercase; color: #1a1205;
  background: linear-gradient(180deg, color-mix(in srgb, var(--cat) 78%, #fff3d8), var(--cat));
  box-shadow: var(--sh-0), 0 0 12px color-mix(in srgb, var(--cat) 45%, transparent); }
.ms-star { position: absolute; z-index: 4; top: 5px; right: 8px; font-size: 15px; color: var(--gilt-lit);
  text-shadow: 0 0 9px rgba(var(--gilt-lit-rgb),0.95); cursor: help; }

/* ---- the name plate ---- */
.ms-name { flex: none; margin-top: 3px; font: 900 clamp(20px, 3.2vh, 30px) var(--font-logo); line-height: 1.06;
  color: #f9efd6; letter-spacing: 0.3px; text-align: center;
  text-shadow: 0 2px 14px color-mix(in srgb, var(--fac) 50%, transparent); }
.ms-role { flex: none; text-align: center; font: 600 clamp(11px, 1.65vh, 14px) 'Spectral', serif; color: var(--gilt-body);
  display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }
.ms-role .ic { color: var(--gilt-lit); }
/* MIGHT — five pips off the cost ladder, which is the game's own estimate of how
   much troop this is. A rating rather than a number, because the number (its price)
   is not a decision anybody makes on this screen. */
.ms-might { flex: none; display: flex; align-items: center; gap: 4px; margin: 2px 0 1px;
  padding: 3px 8px; border-radius: var(--r-pill); background: rgba(0,0,0,0.3);
  box-shadow: inset 0 0 0 1px rgba(var(--gold-rgb),0.14); }
.ms-might s { text-decoration: none; font: 800 9.5px var(--font-display); letter-spacing: 1.4px; text-transform: uppercase; color: var(--muted); }
/* The pips SHARE what the row has left after the label and the upkeep figure, up to
   a stop. Pinned at 26px they would have been five rigid bars in a pill that has to
   hold two words as well — the first thing to overflow on a narrow stage — and a pip
   is a proportion, not a measurement, so it loses nothing by being a little shorter. */
.ms-might i { flex: 1 1 0; min-width: 8px; max-width: 34px; height: 7px; border-radius: var(--r-xs);
  background: rgba(255,255,255,0.09); box-shadow: inset 0 0 0 1px rgba(0,0,0,0.4); }
.ms-might i.on { background: linear-gradient(180deg, var(--gold-hi), var(--gold));
  box-shadow: 0 0 8px rgba(var(--gilt-lit-rgb),0.55); animation: msPip 0.32s var(--e-back) both; animation-delay: calc(var(--i) * 0.045s); }
@keyframes msPip { from { transform: scaleX(0); opacity: 0; } to { transform: none; opacity: 1; } }
.ms-might em { margin-left: auto; font: 700 10px var(--font-ui); letter-spacing: 0.8px; text-transform: uppercase;
  color: var(--muted); font-style: normal; white-space: nowrap; }

/* ---- the five bars ----
   Icon · label · bar · figure, in that order, five times, in the same order for every
   troop in the game. The bar is drawn against the strongest thing in the GAME rather
   than the strongest in this kingdom, so a length means the same number on all seven
   banners — see `STAT_PEAK` for why the scale is bent rather than linear. */
.ms-stats { flex: none; display: flex; flex-direction: column; gap: clamp(4px, 0.75vh, 7px); }
.ms-bar { display: grid; grid-template-columns: 17px 60px minmax(0, 1fr) 62px; align-items: center; gap: 8px; }
.ms-bar > i { display: inline-flex; font-size: 14px; color: var(--gilt-body); opacity: 0.9; }
.ms-bar > s { text-decoration: none; font: 700 10px var(--font-ui); letter-spacing: 1px; text-transform: uppercase; color: var(--muted); }
.ms-bar > u { position: relative; text-decoration: none; height: clamp(9px, 1.25vh, 13px); border-radius: var(--r-sm);
  background: rgba(0,0,0,0.5); box-shadow: inset 0 0 0 1px rgba(0,0,0,0.5), inset 0 1px 2px rgba(0,0,0,0.6); overflow: hidden; }
.ms-bar > u b { position: absolute; inset: 0 auto 0 0; width: var(--w); border-radius: inherit;
  background: linear-gradient(90deg, color-mix(in srgb, var(--fac) 72%, #000), var(--facl, var(--gold-lt)));
  box-shadow: 0 0 9px color-mix(in srgb, var(--facl, var(--fac)) 45%, transparent), inset 0 1px 0 rgba(255,255,255,0.28);
  animation: msFill 0.5s var(--e-out) both; }
@keyframes msFill { from { width: 0; } }
.ms-bar > em { font: 800 clamp(13px, 1.95vh, 17px) var(--font-display); font-style: normal; text-align: right;
  color: #f0e3c2; font-variant-numeric: var(--nums); white-space: nowrap; }
.ms-bar > em small { font-size: 0.72em; color: var(--muted); margin-left: 1px; }

/* ---- what it can DO ----
   Every special power the troop has, derived from its own data (C.powersOf) — the
   same list the build rack reads in battle, so nothing here can promise something
   the field does not deliver. */
.ms-pow { flex: none; display: flex; flex-wrap: wrap; gap: 5px; }
.ms-pow span { display: inline-flex; align-items: center; gap: 5px; padding: 4px 10px; border-radius: var(--r-pill);
  font: 600 clamp(8.5px, 1.2vh, 10px) var(--font-ui); color: var(--ink);
  background: rgba(0,0,0,0.34); box-shadow: inset 0 0 0 1px rgba(var(--gold-rgb),0.22);
  animation: msTag 0.28s var(--e-back) both; animation-delay: calc(var(--i) * 0.03s); }
.ms-pow span .ic { color: var(--gilt-lit); }
@keyframes msTag { from { opacity: 0; transform: translateY(4px) scale(0.94); } to { opacity: 1; transform: none; } }
.ms-nopow span { color: var(--muted); font-style: italic; box-shadow: none; background: none; padding-left: 0; }

/* the counter web, in one line each way */
.ms-vs { flex: none; display: flex; flex-direction: column; gap: 4px; padding: 8px 11px; border-radius: var(--r-md);
  background: rgba(0,0,0,0.28); box-shadow: inset 0 0 0 1px rgba(255,255,255,0.05); }
.ms-vs span { display: flex; gap: 7px; font: 500 clamp(11px, 1.55vh, 13.5px) 'Spectral', serif; line-height: 1.32; }
.ms-vs i { flex: none; font-style: normal; font-size: 11px; }
.ms-strong { color: #cfe0a0; }
.ms-weak { color: #e0a08c; }
.ms-desc { flex: none; font: italic 500 clamp(11px, 1.55vh, 13.5px) 'Spectral', serif; color: #c9bda1; line-height: 1.38;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }

/* ---- can you actually field it ----
   The same test the in-match build rack answers to, said once and plainly: what it
   costs you to raise its spawner if it is yours, and what it costs to make it yours
   if it is not. */


/* ---- THE CHAMPION RAIL ----
   The kingdom's own icons, along the foot of the stage: eight medallions, always
   eight, always in the same place on every banner. This is the part that makes
   choosing one a THING YOU DO rather than a list you read — the roster above is for
   understanding the army, and this is for meeting the handful it is famous for. */

/* the green tick, at a quarter the roster chip's weight — this rail is eight small
   pictures and a full badge on each of them would be the loudest thing in the column */


/* THE STAGE NEEDS A COLUMN'S WIDTH OR IT IS NOT A STAGE. Under 1200px there is not
   one to give without taking it off the roster, which is the thing the screen is
   actually about — a 300px stage there leaves the army about 600px, and the army is
   what the screen is FOR. So the third column goes, and `_stageLive()` hands the job
   back to the hover tip, which is what did it before this existed. */
@media (max-width: 1200px) {
  .musterlayout { grid-template-columns: minmax(180px, 214px) minmax(0, 1fr); }
  .musterstage { display: none; }
}

@media (max-width: 780px) {
  .musterlayout { grid-template-columns: 1fr; }
  .musterrail { flex-direction: row; overflow-x: auto; }
  .mtile { flex: 0 0 190px; }
  .md-cols { grid-template-columns: 1fr; }
}
/* signature-unit star on a build card */
.build .bsig { position: absolute; top: 3px; right: 4px; font-size: 12px; color: var(--gilt-lit); text-shadow: 0 0 6px rgba(var(--gilt-lit-rgb),0.9); pointer-events: none; }
.tip .trole { font: 700 11.5px 'Cinzel', serif; color: var(--ready); margin: 2px 0 4px; }

/* `#kingdomchip` — the pinned top-left banner during a duel — is gone with the
   JS that drew it; see the note beside the removed `showKingdomChip` in main.js. */

/* The purse no longer shows its working. `.rpbreak` and its three lines are gone: a
   victory multiplier and a last-stand bonus itemised under the total is a receipt, and
   the end of a battle is not the moment for one. */

/* Honour reduced-motion. The DELAY has to go with the duration: the result screen
   staggers its blocks in with per-element delays and a `both` fill, so cutting only the
   duration left each block held invisible at its backwards fill until its delay ran out
   — the proclamation assembled itself out of nothing over a second and a half, which is
   the opposite of what the setting asks for. An `!important` here outranks the inline
   delay the reveal writes. */
@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.001ms !important; animation-delay: 0s !important; transition-duration: 0.05s !important; }
}

/* --- universal tactile button press: every control gives a satisfying squish --- */
button { -webkit-tap-highlight-color: transparent; }
.bigbtn, .menubtn, .modecard, .backbtn, .buybtn, #buildbtn, #leavebtn, #sfxtoggle, .build, .bpupg, .fortbtn, .bldstopbtn {
  transition: transform 0.09s var(--e-back), filter 0.12s, border-color 0.12s, box-shadow 0.14s;
}
.bigbtn:active, .menubtn:active, .modecard:active, .backbtn:active, .buybtn:active:not(:disabled), #leavebtn:active, #sfxtoggle:active, .bpupg:active, .fortbtn:active, .bldstopbtn:active { transform: translateY(1px) scale(0.97) !important; filter: brightness(0.94); }
.build:active { transform: translateY(-1px) scale(0.96) !important; }
/* gold buttons get a moving sheen on hover */
.bigbtn, .buybtn.go, .menubtn.primary { position: relative; overflow: hidden; }
.buybtn.go::after, .menubtn.primary::after { content: ''; position: absolute; top: 0; left: -60%; width: 50%; height: 100%; background: linear-gradient(100deg, transparent, rgba(255,255,255,0.45), transparent); transform: skewX(-18deg); transition: left 0.5s ease; }
.buybtn.go:hover::after, .menubtn.primary:hover::after { left: 130%; }

/* --- value-change BUMP: HUD numbers pop when they jump (gold/energy/score) --- */
/* the scoreboard figure still pops; the gold readout deliberately does not — see
   updateHud(). A number that grows every few seconds cannot be read at a glance. */

/* --- DENY: a short red shake when an action can't be afforded / is on cooldown --- */
@keyframes denyShake { 0%,100% { transform: translateX(0); } 20% { transform: translateX(-4px); } 40% { transform: translateX(4px); } 60% { transform: translateX(-3px); } 80% { transform: translateX(2px); } }
.deny { animation: denyShake 0.34s ease; }
.deny.build { box-shadow: 0 0 0 2px rgba(232,90,70,0.8), 0 0 16px rgba(232,90,70,0.5) !important; }

/* --- smoother screen + panel entrances (scale/slide up, not a hard pop) --- */
/* .profbody is a class, not an id — as `#profbody` this rule never matched, so the
   Commander screen was the one screen that popped in without the entrance. */
.menucenter, .modegrid, .resultbox, .profbody, #armgrid, #armsummary, .searchbox { animation: scrIn 0.4s var(--e-out) both; }
@keyframes scrIn { from { opacity: 0; transform: translateY(16px) scale(0.985); } to { opacity: 1; transform: none; } }
.modecard, .racecard, .armcard, .stat { animation: cardIn 0.42s var(--e-out) both; }
@keyframes cardIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
.modegrid .modecard:nth-child(2), .racerow .racecard:nth-child(2) { animation-delay: 0.05s; }
.modegrid .modecard:nth-child(3), .racerow .racecard:nth-child(3) { animation-delay: 0.1s; }
.racerow .racecard:nth-child(4) { animation-delay: 0.15s; }
.racerow .racecard:nth-child(5) { animation-delay: 0.2s; }
.racerow .racecard:nth-child(6) { animation-delay: 0.25s; }
.racerow .racecard:nth-child(7) { animation-delay: 0.3s; }
.racerow .racecard:nth-child(8) { animation-delay: 0.35s; }
.racerow .racecard:nth-child(9) { animation-delay: 0.4s; }
.racerow .racecard:nth-child(10) { animation-delay: 0.45s; }
/* A filled-forwards animation's end state outranks any declared transform, so
   cardIn's `to { transform: none }` was quietly eating the banners' hover lift.
   Its end state IS the resting state here — backwards fill loses nothing on the way
   in and hands :hover its transform back. */
.racecard { animation-fill-mode: backwards; }

/* in-match selection panel & tooltip ease in instead of snapping */
#selpanel:not(.hidden) { animation: panelIn 0.16s ease-out; }
@keyframes panelIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
#tip:not(.hidden) { animation: tipIn 0.12s ease-out; }
@keyframes tipIn { from { opacity: 0; transform: translateY(6px) scale(0.98); } to { opacity: 1; transform: none; } }

/* --- live scoreboard: smooth bar fills + a flash on your row when you score --- */
.sb-bar i { transition: width 0.5s var(--e-out), background 0.3s; }
.sb-row { transition: order 0.3s, background 0.3s, box-shadow 0.3s; }
@keyframes rowScore { 0% { box-shadow: 0 0 0 0 rgba(255,224,130,0); } 30% { box-shadow: inset 0 0 18px rgba(255,224,130,0.4), 0 0 12px rgba(255,224,130,0.5); } 100% { box-shadow: none; } }
.sb-row.scored { animation: rowScore 0.7s ease-out; }
.sb-row.lead .sb-rank { text-shadow: 0 0 10px rgba(255,224,130,0.8); }

/* --- herald messages float in a touch more gracefully (kept subtle) --- */
.herald-msg { will-change: transform, opacity; }

/* crisper focus for keyboard users without harming the look */
button:focus-visible, .build:focus-visible { outline: 2px solid var(--gold-lt); outline-offset: 2px; }

/* --- the HUD assembles with intent when a battle begins (one-time, on reveal) ---
   The resource register has no entrance of its own: it is part of the map frame now,
   and a child dropping DOWN inside a parent rising UP is two objects again. */
#hud:not(.hidden) #factions { animation: hudDrop 0.45s var(--e-out) 0.08s both; }
#hud:not(.hidden) #mapframe { animation: hudRise 0.45s var(--e-out) 0.16s both; }
/* #bottombtns keeps its translateX(-50%) centring, so fade it (no transform anim) */
#hud:not(.hidden) #bottombtns { animation: hudFade 0.45s ease-out 0.12s both; }
@keyframes hudDrop { from { opacity: 0; transform: translateY(-16px); } to { opacity: 1; transform: none; } }
@keyframes hudRise { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: none; } }

@keyframes hudFade { from { opacity: 0; } to { opacity: 1; } }

/* --- auth gate modal --- */
#auth-gate {
  position: fixed; inset: 0; z-index: 9999;
  background: radial-gradient(ellipse at 50% 30%, rgba(30,22,12,0.97) 0%, rgba(8,7,4,0.99) 100%);
  display: flex; align-items: center; justify-content: center;
  opacity: 0; pointer-events: none; transition: opacity 0.35s ease;
}
#auth-gate.ag-in  { opacity: 1; pointer-events: auto; }
#auth-gate.ag-out { opacity: 0; pointer-events: none; }
.ag-card {
  background: linear-gradient(160deg, #1a1509 0%, #0d0b06 100%);
  border: 1px solid rgba(var(--gold-rgb),0.28);
  border-radius: var(--r-2xl);
  padding: 38px 42px 32px;
  width: min(420px, 90vw);
  box-shadow: var(--sh-4), 0 0 0 1px rgba(var(--gold-rgb),0.08), inset 0 1px 0 rgba(var(--gold-rgb),0.12);
  display: flex; flex-direction: column; align-items: center; gap: 0;
  animation: agCardIn 0.4s var(--e-out) both;
}
@keyframes agCardIn { from { transform: translateY(22px) scale(0.97); opacity:0; } to { transform: none; opacity:1; } }
.ag-crest  { font-size: 36px; line-height: 1; margin-bottom: 6px; filter: drop-shadow(0 0 12px rgba(var(--gold-rgb),0.5)); }
.ag-title  { font-family: 'Cinzel Decorative', serif; font-size: 22px; font-weight: 700; color: var(--gold-lt); letter-spacing: 0.5px; text-shadow: 0 0 18px rgba(var(--gold-rgb),0.4); }
.ag-sub    { font-size: 11px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--muted); margin-bottom: 24px; margin-top: 2px; }
.ag-tabs   { display: flex; gap: 0; border-radius: var(--r-lg); overflow: hidden; border: 1px solid rgba(255,255,255,0.09); margin-bottom: 20px; width: 100%; }
.ag-tab    { flex: 1; padding: 9px 0; font-size: 12px; font-weight: 700; letter-spacing: 0.6px; text-transform: uppercase; background: rgba(255,255,255,0.04); color: var(--muted); border: none; cursor: pointer; transition: background 0.18s, color 0.18s; }
.ag-tab--active { background: rgba(var(--gold-rgb),0.16); color: var(--gold-lt); }
.ag-tab:hover:not(.ag-tab--active) { background: rgba(255,255,255,0.07); color: var(--ink); }
.ag-field  { width: 100%; margin-bottom: 14px; }
.ag-label  { display: block; font-size: 10.5px; font-weight: 700; letter-spacing: 0.8px; text-transform: uppercase; color: var(--muted); margin-bottom: 5px; }
.ag-input  { width: 100%; box-sizing: border-box; padding: 11px 14px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.13); border-radius: var(--r-lg); color: var(--ink); font-size: 14px; font-family: inherit; transition: border-color 0.18s, box-shadow 0.18s; }
.ag-input:focus { outline: none; border-color: rgba(var(--gold-rgb),0.55); box-shadow: 0 0 0 2px rgba(var(--gold-rgb),0.14); }
.ag-err    { width: 100%; font-size: 12px; color: #f87070; background: rgba(248,112,112,0.1); border: 1px solid rgba(248,112,112,0.25); border-radius: var(--r-md); padding: 8px 12px; box-sizing: border-box; margin-bottom: 8px; }
.ag-hidden { display: none !important; }
.ag-btn {
  width: 100%; margin-top: 6px; padding: 13px 0;
  background: linear-gradient(180deg, #c8a23a 0%, #a07822 100%);
  color: #0a0800; font-size: 13px; font-weight: 900; letter-spacing: 1px; text-transform: uppercase;
  border: none; border-radius: var(--r-lg); cursor: pointer;
  box-shadow: var(--sh-3), 0 0 10px rgba(var(--gold-rgb),0.25);
  transition: filter 0.15s, transform 0.1s;
}
.ag-btn:hover:not(:disabled) { filter: brightness(1.1); transform: translateY(-1px); }
.ag-btn:active:not(:disabled) { transform: translateY(1px); }
.ag-btn:disabled { opacity: 0.55; cursor: default; }
.ag-guest  { margin-top: 16px; font-size: 11.5px; color: var(--muted); text-align: center; }
.ag-guest a { color: rgba(var(--gold-rgb),0.7); text-decoration: none; }
.ag-guest a:hover { color: var(--gold-lt); }
.ag-guest-note { opacity: 0.6; }

/* The account corner — tucked under the level/gold chip, top right. It used to
   sit alone in the opposite corner, which read as a stray label; who you are
   signed in as belongs with the rest of your account's numbers, not across
   the screen from them.
   `#acctstack` carries the ONE fixed position for the whole corner; the badge
   and the ranked card inside it are normal flow, right-aligned by the stack,
   so they take stock of each other's height for free instead of each being
   measured off the other with a published CSS variable. */
#acctstack {
  /* HUNG OFF THE REAL CHIP ROW, NOT OFF A REMEMBERED NUMBER. The offset used to be
     `the bar's padding + 46px`, where 46 was the chip's height written down once —
     and the chips are built from clamp()s, so on a tall or scaled viewport they are
     comfortably taller than that and the stack printed its name straight across the
     purse. `--chiprow` is the row's measured bottom edge (see UI.refreshTop); the
     46px fallback is only what is used before the first measurement. */
  position: fixed;
  top: calc(var(--chiprow, calc(clamp(6px, 1.4vh, 16px) + 46px)) / var(--ui) + 6px);
  right: calc(20px + var(--sar) / var(--ui)); z-index: 120;
  zoom: var(--ui);
  display: flex; flex-direction: column; align-items: flex-end; gap: 8px;
  pointer-events: auto;
}
#acctbadge {
  display: flex; align-items: center; justify-content: flex-end; gap: 7px;
  font-size: 11px; color: var(--muted);
}
/* THE NAME IS THE DOOR. It is a real button now (it opens your record — see
   #acctname in main.js), so it has to shed every default a button carries and
   still say, on hover, that it can be pressed. */
#acctname {
  font: inherit; font-weight: 700; letter-spacing: 0.4px;
  color: rgba(var(--gold-rgb),0.75);
  background: none; border: 0; padding: 2px 3px; margin: 0;
  cursor: pointer; border-radius: var(--r-sm);
  transition: color 0.15s, text-shadow 0.15s;
}
#acctname:hover { color: var(--gold-lt); text-shadow: 0 0 12px rgba(var(--gold-rgb),0.45); }
#acctname:focus-visible { outline: 1px solid rgba(var(--gold-rgb),0.6); outline-offset: 1px; }
#acctlogout {
  font-size: 10px; padding: 2px 7px; background: rgba(255,255,255,0.05);
  border: 1px solid rgba(255,255,255,0.12); border-radius: var(--r-sm); color: var(--muted);
  cursor: pointer; transition: color 0.15s, border-color 0.15s;
}
#acctlogout:hover { color: var(--ink); border-color: rgba(255,255,255,0.3); }

/* --- army composition counter hints --- */
/* `.bctr` — the green COUNTER tab — is gone with the markup that carried it. It was
   struck across the card's foot, which is where the price plate is, so the one card
   the counter web was pointing at was the one card whose cost you could not read. What
   it was saying is said by the rim below, which stands on nothing. */
.build.counterhint { border-color: rgba(127,224,160,0.55) !important; animation: counterHint 2s ease-in-out infinite; }
@keyframes counterHint { 0%,100% { box-shadow: var(--sh-2), 0 0 0 1px rgba(127,224,160,0.35); } 50% { box-shadow: var(--sh-2), 0 0 10px 2px rgba(127,224,160,0.45); } }

/* ===================================================================== */
/* ========================  MYTHIC FINISH PASS  ======================= */
/* ===================================================================== */

body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    linear-gradient(180deg, rgba(255, 238, 190, 0.06), transparent 22%, rgba(12, 18, 24, 0.14) 100%),
    repeating-linear-gradient(90deg, rgba(255,255,255,0.014) 0 1px, transparent 1px 5px);
  mix-blend-mode: soft-light;
  opacity: 0.7;
}

#hud,
#screens,
#fxflash,
#acctstack,
#sfxtoggle {
  z-index: 20;
}

.screen {
  background:
    radial-gradient(ellipse at 50% 18%, rgba(255, 216, 122, 0.18), transparent 28%),
    radial-gradient(ellipse at 18% 82%, rgba(84, 211, 141, 0.11), transparent 32%),
    radial-gradient(ellipse at 84% 76%, rgba(127, 207, 255, 0.12), transparent 34%),
    linear-gradient(180deg, rgba(5, 7, 8, 0.5), rgba(7, 6, 4, 0.86));
}

/* The "living light" drift used to PAN by translating this whole inset:0 layer —
   but a translated inset:0 box is, by definition, no longer flush with the box it
   was inset to: on every outward half of the swing it poked past .screen's own
   edge, and since this is a DIRECT pseudo-child of .screen (the overflow-y:auto
   scroll container itself, not a descendant nested inside another positioned
   element), that excess counted as real scrollable content — an ~18s-cycle
   scrollbar flicker with a matching pixel-shift as the content column resized
   around it. Confirmed by seeking the animation's own timeline and reading
   scrollHeight directly, not by inspection. Panning background-position instead
   moves the GRADIENT inside a box that never changes shape, so there is nothing
   for the scroll container to ever measure as overflow. */
.screen::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    linear-gradient(115deg, transparent 0 22%, rgba(255, 225, 160, 0.07) 28%, transparent 36%),
    linear-gradient(245deg, transparent 0 58%, rgba(127, 207, 255, 0.055) 65%, transparent 74%);
  background-size: 130% 130%, 130% 130%;
  background-position: 35% 25%, 35% 25%;
  animation: menuLightDrift 18s ease-in-out infinite alternate;
}

@keyframes menuLightDrift {
  from { opacity: 0.55; background-position: 30% 20%, 30% 20%; }
  to { opacity: 1; background-position: 70% 60%, 70% 60%; }
}

.topbarmenu {
  position: sticky;
  top: 0;
  z-index: 2;
}

.chip {
  background:
    linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0)),
    linear-gradient(145deg, rgba(42, 34, 20, 0.72), rgba(11, 10, 8, 0.72));
  border-color: rgba(255, 225, 156, 0.34);
  backdrop-filter: blur(10px) saturate(1.15);
}

.gametitle {
  letter-spacing: 0;
}

/* The letters are FILLED by a clipped background, so the fill hides everything behind
   the glyph and only the offset part of a shadow survives — which makes a stack of
   1px-apart shadows an extrusion rather than a blur. Four steps down into progressively
   darker bronze, then the cast shadow and the warm bloom: the wordmark reads as struck
   metal sitting straight on the backdrop instead of gold-coloured text printed on a box. */
.gametitle span {
  letter-spacing: 5px;
  text-shadow:
    0 1px 0 #dba93a, 0 2px 0 #c0922c, 0 3px 0 #9c7420, 0 4px 0 #785816,
    0 6px 14px rgba(0, 0, 0, 0.78), 0 0 22px rgba(var(--gilt-lit-rgb), 0.5), 0 0 52px rgba(255, 168, 48, 0.34);
}

/* A banner wears ITS OWN kingdom's colour, read from the faction data the card was
   built from. This was four hand-written colours keyed to card POSITION, and the list
   had drifted out of step with the data: the Human Realm — whose banner is blue — was
   painted gold, and the Dwarven Clans — gold — were painted blue. Sourcing the colour
   from the same place the crest comes from means the two can never disagree again. */
.racerow .racecard { color: var(--facl, var(--gold-lt)); }

.bigbtn, .menubtn.primary {
  border-radius: var(--r-lg);
  background:
    linear-gradient(180deg, rgba(255,255,255,0.42), rgba(255,255,255,0) 24%),
    linear-gradient(180deg, var(--gold-hi), #e0b94a 52%, #9d6e1d);
  box-shadow: var(--sh-4), 0 0 34px rgba(var(--gold-rgb),0.44), inset 0 1px 0 rgba(255,255,255,0.58), inset 0 -4px 10px rgba(80,48,10,0.42);
}

.menubtn, .backbtn, .modecard, .armcard, .stat, .armbar, .searchbox, #selpanel, #tip {
  border-radius: var(--r-lg);
}

/* `.resultbox` is deliberately NOT in this list any more: the proclamation carries its
   own verdict-tinted field (warm for victory, cold iron for defeat), and the shared
   neutral wash here would repaint both of them the same brown. */
.modecard,
.armcard,
.stat,
.searchbox {
  background:
    linear-gradient(180deg, rgba(255,255,255,0.055), rgba(255,255,255,0)),
    linear-gradient(150deg, rgba(34, 29, 22, 0.8), rgba(8, 8, 7, 0.82));
  backdrop-filter: blur(8px) saturate(1.08);
}
.resultbox { backdrop-filter: blur(8px) saturate(1.08); }

#topbar {
  /* the shorthand would silently discard the safe-area terms set at the top
     declaration — restate them (see the notch note there) */
  padding: calc(12px + var(--sat) / var(--ui)) calc(18px + var(--sar) / var(--ui)) 12px calc(18px + var(--sal) / var(--ui));
}

#factions,
#mapframe,
#selpanel,
#tip,
.build {
  backdrop-filter: blur(10px) saturate(1.12);
}

/* the sigils keep their struck-coin discs — the register has no chrome of its own,
   so these are the only thing marking one stat off from the next */
.ric {
  width: 15px;
  height: 15px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: rgba(255, 228, 150, 0.12);
  box-shadow: inset 0 0 0 1px rgba(255, 228, 150, 0.16);
}

#factions {
  min-width: 360px;
  background:
    linear-gradient(180deg, rgba(255,245,210,0.06), rgba(255,255,255,0)),
    linear-gradient(160deg, rgba(24, 22, 18, 0.54), rgba(5, 6, 7, 0.5));
  border-color: rgba(255, 225, 156, 0.32);
}

.sb-hdr {
  font-size: 10px;
  letter-spacing: 1.4px;
  text-transform: uppercase;
}

.sb-row {
  position: relative;
  overflow: hidden;
  padding: 5px 6px;
}

.sb-row::before {
  content: '';
  position: absolute;
  inset: 0;
  opacity: 0;
  background: linear-gradient(90deg, rgba(255,245,200,0.12), transparent 70%);
  transition: opacity 0.2s;
}

.sb-row.me::before,
.sb-row.lead::before {
  opacity: 1;
}

.sb-row > * {
  position: relative;
}

.sb-bar {
  height: 14px;
  background: linear-gradient(180deg, rgba(0,0,0,0.78), rgba(25,19,10,0.78));
}

.sb-bar i {
  position: relative;
  overflow: hidden;
}

.sb-bar i::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.32), transparent);
  animation: scoreSheen 2.8s ease-in-out infinite;
}

@keyframes scoreSheen {
  from { transform: translateX(-120%); }
  55%, to { transform: translateX(140%); }
}

#leavebtn,
#sfxtoggle,
#buildbtn {
  border-radius: var(--r-lg);
}

#leavebtn {
  background:
    linear-gradient(180deg, rgba(255,210,200,0.08), rgba(255,255,255,0)),
    linear-gradient(160deg, rgba(74,24,24,0.78), rgba(22,11,11,0.72));
  backdrop-filter: blur(8px);
}

#buildbtn {
  /* (no offset here: it is a static flex child of #bottombtns — the cluster
     positions it, and a `bottom` on it was a no-op that read like a promise) */
  background:
    linear-gradient(180deg, rgba(255,255,255,0.44), rgba(255,255,255,0) 24%),
    linear-gradient(180deg, var(--gold-hi), #d9aa36 58%, #8d5f19);
  box-shadow: var(--sh-3), 0 0 30px rgba(var(--gold-rgb),0.5), inset 0 1px 0 rgba(255,255,255,0.56);
}

#buildbar {
  /* clears the Build/Upgrades cluster (see #bottombtns) and, on a phone, the
     home indicator under it — over --rack, because the rack is zoomed by it and this
     offset is in the rack's own layout space (see the zoom line above) */
  bottom: calc((86px + var(--sab) / var(--ui)) / var(--rack));
  padding: 9px 12px;
  border-radius: var(--r-lg);
  background:
    linear-gradient(180deg, rgba(255,236,170,0.08), rgba(255,255,255,0)),
    linear-gradient(180deg, rgba(8,7,5,0.04), rgba(8,6,4,0.58));
}

.build {
  width: 110px;
  border-radius: var(--r-lg);
  background:
    linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0)),
    linear-gradient(160deg, rgba(31,27,20,0.84), rgba(8,8,7,0.84));
}

.build::after {
  border-radius: var(--r-lg);
}

.build.armed {
  border-color: var(--gold-bright) !important;
  box-shadow: var(--sh-4), 0 0 0 2px rgba(255,239,190,0.36), 0 0 34px rgba(var(--gold-rgb),0.58) !important;
}

.bicon {
  border-radius: 50%;
  background: radial-gradient(circle at 50% 35%, rgba(255,255,255,0.12), rgba(0,0,0,0.26));
}

/* the cartouche under the candle: the same vellum, foxing and grain as the base
   rule, lifted a stop and let go a little translucent so the battlefield reads
   faintly through the page it is drawn on */
#mapframe {
  border-radius: var(--r-lg);
  background:
    linear-gradient(180deg, rgba(255,245,210,0.08), rgba(255,255,255,0)),
    radial-gradient(120% 80% at 12% 4%, rgba(196,150,74,0.15), rgba(0,0,0,0) 60%),
    radial-gradient(90% 70% at 92% 96%, rgba(150,104,46,0.14), rgba(0,0,0,0) 62%),
    repeating-linear-gradient(112deg, rgba(var(--lit-rgb),0.032) 0 1px, rgba(0,0,0,0) 1px 5px),
    linear-gradient(158deg, rgba(44, 35, 23, 0.82), rgba(16, 13, 9, 0.84) 62%, rgba(6, 8, 8, 0.84));
  border-color: rgba(235, 193, 98, 0.78);
  box-shadow: var(--sh-4), 0 0 22px rgba(127,207,255,0.12), inset 0 1px 0 rgba(255,238,190,0.14),
    inset 0 0 0 4px rgba(var(--gold-rgb),0.17), inset 0 0 0 5px rgba(0,0,0,0.34);
}

#minimap {
  filter: saturate(1.16) contrast(1.08);
}

.herald-msg {
  border-radius: var(--r-lg);
  background:
    linear-gradient(180deg, rgba(255,245,210,0.08), rgba(255,255,255,0)),
    linear-gradient(180deg, rgba(39, 31, 20, 0.92), rgba(8, 8, 7, 0.9));
  backdrop-filter: blur(10px) saturate(1.18);
}

/* Under ~1200px there is no longer room for treasury · scorecard · retreat abreast
   without the outer two crowding the middle, so the scorecard takes a rank of its
   own, full width and still centred. The herald toasts drop below it. */
@media (max-width: 1199px) {
  #topbar {
    grid-template-columns: 1fr auto;
    row-gap: 8px;
  }

  /* Written through #topbar to match the specificity of the base placement above.
     Plain `#factions` is one ID against that rule's two and would lose here, and the
     card would stay pinned in a middle column this breakpoint no longer has. */
  #topbar > #leavebtn { grid-row: 1; grid-column: 2; }

  #topbar > #factions,
  #topbar > #factions.scorecard {
    grid-row: 2; grid-column: 1 / -1;
    width: min(100%, 640px);
    justify-self: center;
  }

  #herald {
    top: 186px;
  }
}

/* phone widths, and landscape phones where the height is the scarce axis: the map
   frame shrinks (--mmw), and the register above it shrinks with it rather than
   propping the frame open at a width the map no longer has */
@media (max-width: 700px), (max-height: 520px) {
  #topbar { padding: calc(10px + var(--sat) / var(--ui)) calc(10px + var(--sar) / var(--ui)) 10px calc(10px + var(--sal) / var(--ui)); gap: 8px; }
  #resources { padding-bottom: 4px; margin-bottom: 4px; }
  #resources b { font-size: 14px; }
  #resources em { font-size: 8.5px; letter-spacing: 0.3px; }
  .ric { width: 14px; height: 14px; font-size: 11px; }
}

/* Landscape phones: 390px of height has to carry a topbar, the scorecard, the rack
   and the minimap. Everything gives up size — nothing gives up its place.
   The map shrinks by SIZING THE CANVAS, not by transforming the frame: #mapframe
   runs hudRise, whose `to { transform: none }` + `both` fill wins over any transform
   set here (the same trap the scorecard's entrance fell into). Sizing also shrinks
   the frame's real footprint, so it stops colliding rather than merely looking
   smaller — and the minimap maps clicks off its bounding rect, so aim stays true. */
@media (max-height: 520px) {
  /* the purse is sized off the VIEWPORT, and a landscape phone is wide and short —
     so 3.1vw was still handing a 40px numeral to a 144px cartouche and leaving it
     no air at all. The register scales with the frame it is set in. */
  .ws-gold { font-size: clamp(23px, 2.3vw, 29px); }
  .ws-inc i { font-size: 11px; } .ws-inc em { font-size: 7px; letter-spacing: 1px; }
  #buildbar { max-height: calc(34vh / var(--ui)); }
  #herald { top: 168px; }   /* just under the stacked scorecard */
  /* narrow enough that its rank ends before the map frame's column begins */
  #factions.scorecard { width: min(100%, 480px); }
}

@media (max-width: 620px) {
  .gametitle span {
    letter-spacing: 2px;
  }

  /* Ten across a 390px screen is 36px a banner, which is a crest and no name — so
     the one rank the desktop keeps is the one thing a narrow screen cannot have.
     Five and five: two even ranks, no short row, and every banner still wide
     enough to read. The desktop's break-out is undone here too; at this width the
     column IS the screen. */
  .racerow {
    grid-template-columns: repeat(5, minmax(0, 1fr));
    gap: 6px;
    width: auto; margin-left: 0; transform: none;
  }

  /* Seven columns at 620px is a 70-odd px tile with an icon and a word crushed
     into it. Four re-columns it the same way the kingdom row above just did:
     two even ranks instead of one squeezed one. */
  .menurow { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }

  .racecard {
    --notch: 10px;
    width: auto;
    padding: 8px 3px calc(var(--notch) + 5px);
  }

  .raceblurb { display: none; }
  .racename { font-size: 10.5px; letter-spacing: 0; line-height: 1.15; }
  .racecrest { width: clamp(28px, 8vw, 40px); }
  /* the host survives the phone — it is the one thing on the card a crest and a name
     cannot say — but it drops to two troops so three never squeeze into 80px */
  .rc-host { min-height: 0; margin-top: 3px; }
  .rc-host canvas { width: clamp(20px, 7vw, 30px); }
  .rc-host canvas:nth-child(2) { display: none; }
  .rc-rod { left: -3px; right: -3px; }
  .rc-rod::before { left: -5px; }
  .rc-rod::after { right: -5px; }

  .menurow,
  .joinrow,
  .searchbtns {
    flex-wrap: wrap;
  }

  #buildbar {
    max-width: min(calc(100vw / var(--ui)), 620px);
    max-height: calc(38vh / var(--ui));
  }

  .build {
    width: 96px;
  }
}

/* Narrow windows: shrink the map so the Build cluster below it and the rack beside
   it keep their clearance. Size the map, don't transform the frame — #mapframe runs
   hudRise, whose `to { transform: none }` + `both` fill silently undid the scale
   this rule used to apply. */
@media (max-width: 820px) {
  .ws-gold { font-size: clamp(25px, 3.1vw, 32px); }
}

/* ===================================================================== */
/* ======================  THE KINGDOM DOSSIER  ======================== */
/* One panel, two homes: it fills the logo space on the menu while you    */
/* hover a banner, and docks under the in-match tug meter while you hover */
/* a faction row. Everything inside animates in so it reads as a briefing */
/* being laid out in front of you, not a tooltip blinking on.             */
/* ===================================================================== */

/* The kingdom dossier no longer overlays the menu logo — a kingdom is read on the
   muster screen, which has the room for it. What remains of the .kd-* system below
   dresses the in-match briefing (.facdossier), which owns its own frame. */

/* The standings used to centre themselves with translateX(-50%), which meant their
   entrance had to carry that -50% in every keyframe or the `both` fill would land
   the card half a panel off. The topbar grid centres the card now, so the transform
   is not just unnecessary — leaving it in shifted the card 320px left of its own
   column. A plain drop is all this needs. */
@keyframes hudDropMid {
  from { opacity: 0; transform: translateY(-16px); }
  to   { opacity: 1; transform: none; }
}
#hud:not(.hidden) #factions { animation: hudDropMid 0.45s var(--e-out) 0.08s both; }

/* ---- the in-match home: adhered under the tug meter ----
   NB: #factions.scorecard is `position: relative`, so it is its own containing block
   and this hangs off it. Do NOT give it `position: absolute` to centre it — the
   topbar grid does the centring, and taking the card out of flow collapses the
   column that keeps the treasury banner and the retreat button clear of it. */
.facdossier {
  --fac: var(--gold);
  position: absolute; top: calc(100% + 9px); right: 0;
  width: min(470px, 92vw); max-height: min(64vh, 600px);
  display: flex; flex-direction: column; gap: 6px;
  padding: 11px 13px 12px; text-align: left; z-index: 30;
  border-radius: var(--r-xl); overflow: hidden auto; pointer-events: none;
  background:
    linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0)),
    linear-gradient(165deg, rgba(42,35,23,0.97), rgba(12,11,8,0.98));
  border: 1px solid var(--edge); border-top: 3px solid var(--fac);
  box-shadow: var(--sh-4), 0 0 30px color-mix(in srgb, var(--fac) 20%, transparent);
  backdrop-filter: blur(8px) saturate(1.1);
}
.facdossier > * { position: relative; z-index: 1; }
.facdossier.in { animation: kdInR 0.3s var(--e-out) both; }

/* ---- ONE COMMANDER'S MATCH ------------------------------------------------
   Hovering a row on the alliance board used to open the KINGDOM's dossier — what
   the Jade Dynasty is like — which is a thing you read once at the muster while
   choosing, and which answers nothing at all with eight of them on a field. This
   is that commander's actual match instead: wall, army, works, kills, purse. The
   kingdom keeps one line of subtitle, because whose army it is still matters; it
   just is not the headline. Narrower than the kingdom panel it replaces — it is a
   readout, not a briefing. */
.cmdcard { width: min(330px, 88vw); gap: 9px; padding: 11px 12px 12px; }
.cmdcard.is-out { filter: grayscale(0.55); opacity: 0.86; }
.cc-head { display: flex; align-items: center; gap: 10px; padding-bottom: 8px;
  border-bottom: 1px solid rgba(var(--gold-rgb),0.2); }
.cc-crest { flex: none; filter: drop-shadow(0 2px 5px rgba(0,0,0,0.7)); }
.cc-id { min-width: 0; flex: 1; display: flex; flex-direction: column; }
.cc-name { font: 800 15px var(--font-display); color: #f4e6c0; letter-spacing: 0.3px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cc-sub { font: 600 10px var(--font-ui); letter-spacing: 0.8px; text-transform: uppercase;
  color: var(--facl, #c8b68a); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* which side, said the same way the board says it */
.cc-side { flex: none; font: 800 8.5px var(--font-display); letter-spacing: 1.2px;
  padding: 2px 6px; border-radius: var(--r-xs); white-space: nowrap; }
.cc-side.is-me { color: #1a140c; background: linear-gradient(180deg, var(--gold-lt), var(--gold)); }
.cc-side.is-ally { color: #8ec6f2; box-shadow: inset 0 0 0 1px rgba(110,168,216,0.45); }
.cc-side.is-foe { color: #ff7a68; box-shadow: inset 0 0 0 1px rgba(216,96,80,0.45); }
/* two columns of label → figure. Every figure is tabular, so nothing jumps as it
   counts up while you are reading it. */
.cc-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 5px 10px; }
.cc-stat { display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
  padding: 3px 6px; border-radius: var(--r-sm); background: rgba(0,0,0,0.26); min-width: 0; }
.cc-stat s { text-decoration: none; font: 700 8.5px var(--font-ui); letter-spacing: 0.8px;
  text-transform: uppercase; color: var(--muted); flex: none; }
.cc-stat b { font: 800 12px var(--font-display); color: #f0e3c2;
  font-variant-numeric: var(--nums); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* the keep is the win condition, so it is the one stat that shouts when it is going */
.cc-keep { grid-column: 1 / -1; background: rgba(0,0,0,0.36); }
.cc-keep b { font-size: 15px; color: var(--facl, #f0e3c2); }
.cc-keep.is-low b { color: #ff9c86; animation: sbPulse 1.1s ease-in-out infinite; }
.cc-keep.is-dead b { color: #b0685c; }
.cc-gold b, .cc-earned b, .cc-bounty b { color: var(--gold-lt); }
.cc-kills b { color: #f0c0b4; }
@keyframes kdInR {
  from { opacity: 0; transform: translateY(-10px) scale(0.985); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

/* ---- living backdrop: a slow faction-coloured wash + a one-shot light sweep ---- */
.kd-glow, .kd-sheen { position: absolute; inset: 0; z-index: 0 !important; pointer-events: none; border-radius: inherit; overflow: hidden; }
.kd-glow::after {
  content: ''; position: absolute; inset: -45%;
  background: conic-gradient(from 0turn,
    transparent, color-mix(in srgb, var(--fac) 42%, transparent) 12%,
    transparent 34%, rgba(255,226,148,0.16) 52%,
    transparent 74%, color-mix(in srgb, var(--fac) 30%, transparent) 88%, transparent);
  /* Static. This wash used to SPIN (kdSpin, 16s linear infinite) — a 42px-blurred
     conic gradient repainting every frame for a backdrop nobody watches, which is
     real GPU cost on mobile. The faction-coloured pooling is the point; the
     rotation was not. The one-shot kd-sheen sweep below still says "alive". */
  filter: blur(42px); opacity: 0.5;
}
.kd-sheen::after {
  content: ''; position: absolute; top: -20%; bottom: -20%; width: 42%; left: -60%;
  background: linear-gradient(100deg, transparent, rgba(255,246,220,0.13), transparent);
  transform: skewX(-18deg); animation: kdSheen 1.05s ease-out 0.08s both;
}
@keyframes kdSheen { to { left: 130%; } }

/* ---- shared entrance beats ---- */
@keyframes kdRise { from { opacity: 0; transform: translateY(7px); } to { opacity: 1; transform: none; } }
@keyframes kdSlide { from { opacity: 0; transform: translateX(-9px); } to { opacity: 1; transform: none; } }
@keyframes kdPop { from { opacity: 0; transform: translateY(6px) scale(0.86); } to { opacity: 1; transform: none; } }
@keyframes kdGrow { from { width: 0; } to { width: var(--mag); } }
@keyframes kdWide { from { width: 0; } to { width: var(--w); } }
@keyframes kdBob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }
@keyframes kdFloat { 0%, 100% { transform: translateY(0) rotate(-1.2deg); } 50% { transform: translateY(-5px) rotate(1.2deg); } }

/* ---- header: crest, name, counts ---- */
.kd-head { display: flex; align-items: center; gap: 16px; flex: none;
  padding-bottom: 12px; border-bottom: 1px solid rgba(255,255,255,0.09); }
.kd-crest { flex: none; filter: drop-shadow(0 3px 8px rgba(0,0,0,0.7)) drop-shadow(0 0 12px color-mix(in srgb, var(--fac) 45%, transparent));
  animation: kdFloat 4.4s ease-in-out infinite; }
.kd-id { min-width: 0; flex: 1; }
.kd-name { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  font: 900 30px var(--font-logo); color: #f9efd6; letter-spacing: 0.4px; line-height: 1.1;
  animation: kdSlide 0.34s ease-out 0.05s both; }
.kd-blurb { font: 500 15px 'Spectral', serif; font-style: italic; color: #cdc1a4; margin-top: 2px;
  animation: kdSlide 0.34s ease-out 0.1s both; }
.kd-style { font: 700 12px 'Spectral', serif; letter-spacing: 1.1px; text-transform: uppercase;
  color: var(--fac); margin-top: 4px; animation: kdSlide 0.34s ease-out 0.15s both; }
.kd-counts { margin-left: auto; display: flex; gap: 9px; flex: none; }
.kd-counts span { display: flex; flex-direction: column; align-items: center; padding: 7px 13px; border-radius: var(--r-lg);
  background: rgba(0,0,0,0.34); border: 1px solid rgba(var(--gold-rgb),0.2);
  font: 600 11px 'Spectral', serif; color: var(--muted); letter-spacing: 0.5px; white-space: nowrap;
  animation: kdPop 0.36s var(--e-out) both; animation-delay: calc(var(--i) * 0.07s + 0.14s); }
.kd-counts b { font: 800 21px var(--font-display); color: var(--gold-lt); line-height: 1.15; }

/* ---- the one-line doctrine ---- */
.kd-doctrine { flex: none; padding: 9px 14px; border-radius: var(--r-lg);
  background: linear-gradient(90deg, color-mix(in srgb, var(--fac) 18%, transparent), rgba(0,0,0,0.16));
  border-left: 4px solid var(--fac); animation: kdSlide 0.36s ease-out 0.18s both; }
.kd-doctrine b { font: 800 16.5px var(--font-display); color: var(--gilt-lit); letter-spacing: 0.8px; }
.kd-doctrine span { display: block; margin-top: 3px; font: 500 14.5px 'Spectral', serif; color: #d9cfb7; line-height: 1.5; }

/* ---- two columns of substance: traits+powers · composition+champions ----
   Bigger type needs room to breathe, so this reads as two roomy panels rather
   than four cramped ones — traits/powers stack on the left, composition and
   champions on the right. */
.kd-body { flex: none; display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.kd-col { padding: 14px 17px 16px; border-radius: var(--r-xl); min-width: 0;
  background: rgba(0,0,0,0.28); border: 1px solid rgba(var(--gold-rgb),0.18);
  display: flex; flex-direction: column; gap: 14px;
  animation: kdRise 0.4s ease-out both; }
.kd-col:nth-child(1) { animation-delay: 0.2s; }
.kd-col:nth-child(2) { animation-delay: 0.27s; }
.kd-col h4, .kd-h2 { margin: 0; font: 800 13px var(--font-display); letter-spacing: 1.2px;
  text-transform: uppercase; color: var(--fac); }
.kd-h2 { margin: 0 0 7px; }
.kd-h2 em { font-style: normal; font-weight: 600; font-size: 11px; color: #9c8f74; letter-spacing: 0.3px; text-transform: none; }

/* trait meters: signed bars growing out of a 100% baseline */
.kd-traits { display: flex; flex-direction: column; gap: 7px; }
.kd-trait { display: grid; grid-template-columns: 20px minmax(0, 1fr) 150px 52px; align-items: center; gap: 9px;
  animation: kdRise 0.36s ease-out both; animation-delay: calc(var(--i) * 0.06s + 0.26s); }
.kd-trait > i { font-style: normal; font-size: 14px; opacity: 0.85; }
.kd-trait > span { font: 600 14px 'Spectral', serif; color: #cabd9e; }
.kd-tbar { position: relative; height: 12px; border-radius: var(--r-md); background: rgba(0,0,0,0.5);
  box-shadow: inset 0 1px 2px rgba(0,0,0,0.6); }
.kd-tbar::before { content: ''; position: absolute; left: 50%; top: -1px; bottom: -1px; width: 1px;
  background: rgba(255,236,190,0.3); }
.kd-tbar b { position: absolute; top: 1px; bottom: 1px; width: 0; border-radius: var(--r-sm);
  animation: kdGrow 0.6s var(--e-out) both; animation-delay: calc(var(--i) * 0.06s + 0.34s); }
.kd-trait.up .kd-tbar b { left: 50%; background: linear-gradient(90deg, var(--ready-dk), var(--ready-lt)); box-shadow: 0 0 8px rgba(120,230,160,0.5); }
.kd-trait.dn .kd-tbar b { right: 50%; background: linear-gradient(270deg, #c9604a, #f0ab93); box-shadow: 0 0 8px rgba(230,130,100,0.45); }
.kd-trait > em { font: 800 14px var(--font-display); font-style: normal; text-align: right; font-variant-numeric: var(--nums); }
.kd-trait.up > em { color: var(--ready); }
.kd-trait.dn > em { color: #e8a08c; }
.kd-trait.ev > em { color: #8c8471; }

.kd-good, .kd-bad { font: 500 14px 'Spectral', serif; line-height: 1.5;
  animation: kdSlide 0.36s ease-out both; animation-delay: calc(var(--i) * 0.07s + 0.5s); }
.kd-good { color: #cfe0a0; }
.kd-bad { color: #e0a08c; }

/* kingdom powers */
.kd-powers { display: flex; flex-direction: column; gap: 6px; }
.kd-powers div { font: 600 14px 'Spectral', serif; line-height: 1.5; color: var(--ink);
  padding: 5px 9px; border-radius: var(--r-md); background: rgba(255,255,255,0.03);
  animation: kdSlide 0.38s ease-out both; animation-delay: calc(var(--i) * 0.08s + 0.32s); }

/* army composition: the SHAPE of the roster in one bar */
.kd-comp { display: flex; height: 16px; border-radius: var(--r-lg); overflow: hidden; background: rgba(0,0,0,0.5);
  box-shadow: inset 0 1px 2px rgba(0,0,0,0.6); }
.kd-comp span { flex: 0 0 auto; width: 0; background: var(--c); box-shadow: inset 0 1px 0 rgba(255,255,255,0.22);
  animation: kdWide 0.62s var(--e-out) both; animation-delay: calc(var(--i) * 0.045s + 0.38s); }
.kd-complg { display: flex; flex-wrap: wrap; gap: 5px 11px; }
.kd-complg span { display: inline-flex; align-items: center; gap: 5px; font: 600 12px 'Spectral', serif; color: #b6a988;
  animation: kdRise 0.34s ease-out both; animation-delay: calc(var(--i) * 0.04s + 0.46s); }
.kd-complg i { width: 9px; height: 9px; border-radius: var(--r-xs); background: var(--c); }
.kd-complg b { color: #efe0bd; font-weight: 800; }

/* champions: the troops nobody else musters, bobbing on the spot */
.kd-champs { display: flex; flex-wrap: wrap; gap: 8px; }
.kd-champ { position: relative; display: flex; flex-direction: column; align-items: center; width: 70px;
  padding: 6px 3px 5px; border-radius: var(--r-lg); background: rgba(0,0,0,0.3);
  border: 1px solid color-mix(in srgb, var(--c) 45%, transparent);
  animation: kdPop 0.4s var(--e-out) both; animation-delay: calc(var(--i) * 0.06s + 0.44s); }
.kd-champ canvas { animation: kdBob 2.8s ease-in-out infinite; animation-delay: calc(var(--i) * 0.24s); }
.kd-champ i { font: 600 10.5px 'Spectral', serif; font-style: normal; color: #e2d4ae; line-height: 1.15;
  margin-top: 2px; text-align: center; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kd-champ::after { content: '\2605'; position: absolute; top: -5px; right: -3px; font-size: 11px; color: var(--gilt-lit);
  text-shadow: 0 0 6px rgba(var(--gilt-lit-rgb),0.95); }

/* ---- the roster in full ---- */
.kd-rosterh { flex: none; }
.kd-roster { flex: 1 1 auto; min-height: 0; display: flex; flex-wrap: wrap;
  align-content: flex-start; gap: 7px; padding-right: 4px; }
.kd-troop { position: relative; display: flex; flex-direction: column; align-items: center;
  flex: 0 0 auto; width: 62px;
  padding: 4px 2px 3px; border-radius: var(--r-lg); background: rgba(0,0,0,0.26);
  border: 1px solid transparent; border-top: 3px solid var(--c, transparent);
  animation: kdPop 0.3s ease-out both; animation-delay: calc(var(--i) * 0.012s + 0.42s);
  transition: transform 0.12s, border-color 0.12s, background 0.12s; }
.kd-troop:hover { transform: translateY(-2px); border-color: var(--fac); background: rgba(255,255,255,0.06); }
.kd-troop i { font: 600 10px 'Spectral', serif; font-style: normal; color: #b3a689; line-height: 1.15;
  margin-top: 2px; text-align: center; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kd-troop.sig { border-color: rgba(var(--gilt-lit-rgb),0.5); background: rgba(var(--gilt-lit-rgb),0.09); }
.kd-troop.sig::after { content: '\2605'; position: absolute; top: -4px; right: -2px; font-size: 10px; color: var(--gilt-lit);
  text-shadow: 0 0 5px rgba(var(--gilt-lit-rgb),0.9); }
.kd-troop.sig i { color: var(--gold-lt); }
.kd-troop.start::before { content: ''; position: absolute; top: 3px; left: 3px; width: 7px; height: 7px;
  border-radius: 50%; background: var(--ready); box-shadow: 0 0 5px rgba(143,230,164,0.9); }

.kd-works { flex: none; display: flex; flex-wrap: wrap; gap: 7px; }
.kd-works span { display: flex; flex-direction: column; padding: 6px 12px; border-radius: var(--r-lg);
  background: rgba(30,38,50,0.72); border: 1px solid rgba(120,150,190,0.32);
  font: 600 13px 'Spectral', serif; color: #dce6f2;
  animation: kdPop 0.36s ease-out both; animation-delay: calc(var(--i) * 0.05s + 0.55s); }
.kd-works b { font: 800 10.5px var(--font-display); letter-spacing: 0.7px; text-transform: uppercase; }

/* ---- compact (in-match) variant ----
   Still two columns but sized down: this sits over a live battlefield, and the
   panel is pointer-events:none, so nothing below the fold is reachable — the full
   roster is dropped here rather than relied on to scroll. */
.kd-compact .kd-body { gap: 9px; }
.kd-compact .kd-col { padding: 8px 10px 9px; gap: 9px; }
.kd-compact .kd-head { gap: 10px; padding-bottom: 7px; }
.kd-compact .kd-crest { width: 50px; height: 59px; }
.kd-compact .kd-name { font-size: 19px; }
.kd-compact .kd-blurb { font-size: 12px; }
.kd-compact .kd-style { font-size: 10px; }
.kd-compact .kd-counts { gap: 5px; }
.kd-compact .kd-counts span { padding: 4px 8px; font-size: 9px; }
.kd-compact .kd-counts b { font-size: 15px; }
.kd-compact .kd-doctrine { padding: 6px 9px; }
.kd-compact .kd-doctrine b { font-size: 13px; }
.kd-compact .kd-doctrine span { font-size: 12px; }
.kd-compact .kd-col h4, .kd-compact .kd-h2 { font-size: 11px; }
.kd-compact .kd-trait { grid-template-columns: 15px minmax(0, 1fr) 78px 38px; gap: 5px; }
.kd-compact .kd-trait > i, .kd-compact .kd-trait > span, .kd-compact .kd-trait > em { font-size: 11px; }
.kd-compact .kd-tbar { height: 9px; }
.kd-compact .kd-powers div { padding: 3px 6px; font-size: 11.5px; }
.kd-compact .kd-good, .kd-compact .kd-bad { font-size: 11.5px; }
.kd-compact .kd-comp { height: 13px; }
.kd-compact .kd-complg span { font-size: 10px; }
.kd-compact .kd-champ { width: 54px; }
.kd-compact .kd-champ canvas { width: 38px; height: 38px; }
.kd-compact .kd-champ i { font-size: 8.5px; }

@media (max-width: 900px) {
  .kd-body { grid-template-columns: 1fr; }
}
@media (max-width: 560px) {
  .kd-counts { display: none; }
}

/* ===================================================================== */
/* ============  THE MENU CHOOSER: which kingdom to march as  =========== */
/* ===================================================================== */
/* Hovering lifts the banner off the wall and catches the light down its cloth; the
   glow is the kingdom's own colour, so seven cards never light up the same gold.
   Depth comes from a drop-shadow rather than a box-shadow — the card has no box any
   more, only a shape, and a box-shadow would draw the rectangle back around it. */
.racecard:hover, .racecard:focus-visible {
  transform: translateY(-5px);
  filter: drop-shadow(0 17px 25px rgba(0,0,0,0.62)) drop-shadow(0 0 20px color-mix(in srgb, var(--fac) 36%, transparent));
}
.racecard:focus-visible { outline: none; }
.racecard:hover .rc-cloth, .racecard:focus-visible .rc-cloth {
  background: linear-gradient(180deg, #fffbe8, color-mix(in srgb, var(--facl) 70%, var(--gold-bright)) 15%,
    var(--gold-lt) 42%, var(--gold) 70%, var(--gold-dk));
}
.racecard:active { transform: translateY(-2px); }
.racecard.on .rc-cloth {
  background: linear-gradient(180deg, #fff6dc, var(--facl) 18%, var(--fac) 66%, var(--facd, var(--gilt-seat)));
}
.racecard.on {
  filter: drop-shadow(0 14px 24px rgba(0,0,0,0.55)) drop-shadow(0 0 20px color-mix(in srgb, var(--fac) 62%, transparent));
}
.racetick { display: none; font: 800 9.5px var(--font-display); letter-spacing: 1px; text-transform: uppercase;
  color: #06150b; background: linear-gradient(180deg, var(--ready-lt), var(--ready-dk)); padding: 2.5px 9px; border-radius: var(--r-3xl); margin-top: 3px;
  box-shadow: var(--sh-0), 0 0 12px rgba(var(--ready-rgb),0.7); }
.racecard.on .racetick { display: block; animation: kdPop 0.3s var(--e-out) both; }

/* ===================================================================== */
/* ==========  ARMORY: WHICH KINGDOMS CAN FIELD THIS THING  ============ */
/* Gone, all of it — `.araces`, `.aracehd`, `.aexcl`, `.arace`, `.arace.aall`.    */
/* The question had no answer worth printing: the shelf belongs to one kingdom    */
/* and so does every troop, so the chip could only repeat the banner above it.    */
/* ===================================================================== */

/* ===================================================================== */
/* ==============  TROOP TOOLTIP (muster dossier)  ==================== */
/* ===================================================================== */
/* The muster is where a kingdom is chosen, so it is where a troop has to be
   readable. One panel, one hover: portrait and identity up top, the numbers in a
   tabular block, powers as chips, the counter pair, the flavour line, and a footer
   that says plainly whether you can field it yet. Pointer-events off so it can never
   sit between the cursor and the chip that summoned it. */
#unittip {
  position: fixed; z-index: 400; pointer-events: none; width: 320px; padding: 0;
  overflow: hidden; border-radius: var(--r-xl);
  background: linear-gradient(168deg, rgba(46,37,23,0.99), rgba(16,12,8,0.995));
  border: 1px solid var(--edge-strong);
  box-shadow: var(--sh-4), inset 0 1px 0 rgba(var(--lit-rgb),0.14);
  backdrop-filter: blur(10px) saturate(1.12);
  animation: tipIn 0.11s ease-out;
}
#unittip.hidden { display: none; }

/* ── THE TROOP SHEET ───────────────────────────────────────────────────────
   The same panel, docked instead of floated. On a phone the champion's stage is
   not on the screen, so a tap on a troop had nowhere to put its answer and put
   it nowhere — this is where that answer goes now.
   `pointer-events: auto` matters: the tooltip above deliberately ignores the
   cursor (it follows one, and must never be under it), but a sheet is a thing
   you touch — it has a close button and it scrolls. */
#unittip.sheet {
  pointer-events: auto; width: auto;
  left: 0; right: 0; bottom: 0; top: auto;
  max-height: min(76vh, 560px); overflow-y: auto; overscroll-behavior: contain;
  border-radius: var(--r-2xl) var(--r-2xl) 0 0;
  border-width: 2px 0 0; border-top-color: var(--gold);
  padding-bottom: max(10px, var(--sab));    /* clear of the home indicator */
  box-shadow: 0 -18px 44px rgba(0,0,0,0.6);
  /* THE RESTING STATE IS THE DEFAULT, AND THE SLIDE IS ONLY DECORATION. Written
     the obvious way — parked at `translateY(101%)` and moved to `none` by a class
     — the sheet's correct position was the END of a transition, so anything that
     stopped frames being produced (a backgrounded tab, a device under load) left
     it built, correct and sitting entirely below the fold, with a scrim over the
     screen and no way back. As a keyframe FROM off-screen, a slide that never
     runs leaves the sheet exactly where it belongs. */
  animation: sheetUp 0.26s var(--e-out);
}
@keyframes sheetUp { from { transform: translateY(101%); } to { transform: none; } }
#unittip .ut-close {
  position: absolute; top: 8px; right: 10px; z-index: 2;
  width: 32px; height: 32px; border-radius: 50%; cursor: pointer;
  font: 700 14px var(--font-ui); color: var(--gold-lt);
  background: rgba(0,0,0,0.5); border: 1px solid var(--edge);
}
#unittip:not(.sheet) .ut-close { display: none; }
/* Tap anywhere off the sheet to put it away — the gesture everyone already has. */
.unitscrim {
  position: fixed; inset: 0; z-index: 399; background: rgba(6,7,10,0.5);
  backdrop-filter: blur(2px);
  animation: scrimIn 0.26s ease;            /* opaque by default — see the sheet */
}
@keyframes scrimIn { from { opacity: 0; } to { opacity: 1; } }
.unitscrim.hidden { display: none; }
@media (prefers-reduced-motion: reduce) {
  #unittip.sheet, .unitscrim { animation: none; }
}

#unittip .ut-head {
  display: flex; gap: 11px; align-items: center; padding: 11px 13px 10px;
  border-bottom: 1px solid rgba(var(--gold-rgb),0.22);
  background: linear-gradient(150deg, color-mix(in srgb, var(--cat) 20%, transparent), transparent 72%);
}
#unittip .ut-art { flex: none; width: 60px; height: 60px; image-rendering: auto;
  filter: drop-shadow(0 3px 6px rgba(0,0,0,0.55)); }
#unittip .ut-id { min-width: 0; }
#unittip .ut-name { font: 800 18px/1.15 var(--font-display); color: var(--gold-bright); letter-spacing: 0.3px; }
#unittip .ut-star { color: var(--gilt-lit); font-size: 14px; }
#unittip .ut-role { font: 600 11.5px var(--font-ui); color: #cfe0ea; margin-top: 2px; }
#unittip .ut-cat { font: 600 11px var(--font-ui); color: var(--muted); margin-top: 3px; }
/* `.ut-only` is gone with the "${kingdom} only" line it coloured — see showUnitTip. */

/* the numbers, in a grid so every value shares a column and a baseline */
#unittip .ut-stats {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px 8px; padding: 10px 13px 0;
  font-variant-numeric: var(--nums);
}
#unittip .ut-stats span { display: flex; flex-direction: column; gap: 1px; }
#unittip .ut-stats i { font: 600 9px var(--font-ui); font-style: normal; letter-spacing: 0.6px;
  text-transform: uppercase; color: #8d8b7e; }
#unittip .ut-stats b { font: 700 13.5px var(--font-ui); color: #e9f1f8; }
#unittip .ut-stats em { font-style: normal; font-size: 10.5px; color: #9fb4c2; }

#unittip .ut-pow { display: flex; flex-wrap: wrap; gap: 4px 5px; padding: 9px 13px 0; }
#unittip .ut-pow span { font: 600 10.5px var(--font-ui); color: #eaf0f6;
  background: rgba(255,255,255,0.06); border: 1px solid rgba(var(--gold-rgb),0.24);
  padding: 2px 6px; border-radius: var(--r-sm); }

#unittip .ut-vs { display: flex; flex-direction: column; gap: 2px; padding: 9px 13px 0;
  font: 600 11.5px var(--font-ui); }
#unittip .ut-strong { color: var(--ready); }
#unittip .ut-weak { color: #f2ab94; }

#unittip .ut-desc { font: 400 12.5px/1.45 var(--font-flavor); color: #dccfb4; padding: 9px 13px 0; }

#unittip .ut-foot { margin-top: 10px; padding: 8px 13px 9px; font: 700 11px var(--font-ui);
  border-top: 1px solid rgba(var(--gold-rgb),0.3); }
#unittip .ut-foot.own { color: var(--ready); background: rgba(90,200,130,0.10); }
#unittip .ut-foot.locked { color: var(--gold-lt); background: rgba(var(--gold-rgb),0.09); }

/* the chip itself gets a clearer hover now that it owns a real tooltip */
.md-troop { cursor: help; }

/* ===================================================================== */
/* ============  RACK TRIM · CLOSE · STEADY NUMBERS  ================== */
/* ===================================================================== */

/* Tighter muster tiles. The role line under each name is gone (it truncated to
   nonsense at this width and the hover tip carries it in full), so the card can lose
   the row it occupied and a little of the padding around it. Everything else — the
   portrait, the name, the PLACE/RAISE footer — keeps its proportions. */
.build { width: 96px; padding: 7px 7px 8px; }
.bicon { width: 42px; height: 42px; }
.bn { font-size: 11px; }
.bupg { margin-top: 4px; padding: 3px 2px; }

/* the rack's ✕ — a zero-height sticky rail so it costs no layout and cannot scroll
   out of reach on a long roster */
.brackclose {
  order: -1; width: 100%; height: 0; margin: 0; padding: 0;
  position: sticky; top: 0; z-index: 7;
  display: flex; justify-content: flex-end; pointer-events: none;
}
.brackclose button {
  pointer-events: auto; transform: translateY(-13px);
  width: 22px; height: 22px; padding: 0; line-height: 1;
  display: flex; align-items: center; justify-content: center;
  font: 700 12px var(--font-ui); color: var(--gold-lt); cursor: pointer;
  background: linear-gradient(180deg, rgba(52,40,22,0.96), rgba(24,17,9,0.96));
  border: 1px solid var(--edge-strong); border-radius: var(--r-md);
  box-shadow: var(--sh-1), inset 0 1px 0 rgba(var(--lit-rgb),0.14);
  transition: color 0.14s, border-color 0.14s, background 0.14s, transform 0.14s;
}
.brackclose button:hover { color: var(--gold-bright); border-color: var(--gold-lt);
  background: linear-gradient(180deg, rgba(78,58,30,0.98), rgba(34,24,12,0.98));
  transform: translateY(-13px) scale(1.08); }

/* THE EASTERN KEEP'S BAR. `.sc-r { align-items: flex-end }` stopped this stretching,
   and every child of .sc-bar is absolutely positioned — so its content width was 0 and
   that side's health bar rendered as nothing at all while the other filled its side.
   The win condition, invisible for one of the two players. align-self beats the
   parent's align-items for this one item, on both sides. */
.sc-bar { align-self: stretch; width: 100%; }

/* STEADY NUMBERS. Each stat now sits in a fixed grid column, so a growing figure can
   no longer shove its neighbours — but it can still re-centre itself inside its own
   cell as it gains a digit. A floor under each holds the sigil and the number still
   while the value runs. The three columns are deliberately UNEQUAL: "165/165" is
   twice the figure "138" is, and splitting 230px of map width evenly would have sized
   every stat to the widest one. */
#resources b { text-align: center; }
.res-gold b { min-width: 5ch; }
.res-inc  b { min-width: 3ch; }
.res-pop  b { min-width: 6ch; }

/* ===================================================================== */
/* ==============  ARMORY CARD: SAY WHAT THE TOOLTIP SAYS  ============ */
/* ===================================================================== */
/* The kingdom banner moved up beside the category, where the card already says what
   a thing IS — so the "★ Only" caption that used to head its own row is gone. It was
   printed on all 182 cards and said nothing the banner beside it did not. */
.atags { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-top: 4px; }
.atags .acatpill { margin-top: 0; }

/* THE CARD IS FOR COMPARING, SO EVERY LINE ON IT IS SIZED TO BE READ. With the flavour
   paragraph gone (see the note in ui.js where it used to be appended) the tile has back
   the three lines it was spending on prose, and the type that was shrunk to fit around
   that prose — a 9px power chip, a 9.5px counter line clipped with an ellipsis — grows
   into the space. Nothing is truncated any more: a counter that ends in "…" is the half
   of the matchup you needed. Wider tiles too, so the longer strings wrap in twos rather
   than in ones. */
.armcards { grid-template-columns: repeat(auto-fill, minmax(238px, 1fr)); gap: 13px; }
.armcard { align-items: stretch; gap: 7px; padding: 14px 13px 12px; justify-content: flex-start; }

/* ---- A CARD IS A CARD, NOT A WINDOW ------------------------------------
   These sat at 0.80/0.82 alpha over a blurred backdrop, which is the shared
   glass the menus are built from — and the menus can afford it because they are
   text over an out-of-focus map. An armory card is a PORTRAIT: a 60px troop
   drawn in its kingdom's colours, over a live battle showing through the plate
   behind it. Every silhouette in the shelf had a moving field behind it, the
   card colours shifted with whatever the backdrop was doing, and two cards side
   by side never quite matched.
   They are opaque now, and the blur goes with the transparency — `backdrop-filter`
   on a grid of thirty cards is thirty separate backdrop reads a frame, for a
   blur that is now behind an opaque surface and cannot be seen at all.
   Specificity beats the shared glass rule below rather than relying on order. */
.armcards .armcard {
  background:
    linear-gradient(180deg, rgba(255,255,255,0.055), rgba(255,255,255,0) 38%),
    linear-gradient(158deg, #241d16 0%, #17130e 58%, #0c0a07 100%);
  backdrop-filter: none;
  box-shadow: var(--sh-2), inset 0 1px 0 rgba(var(--lit-rgb),0.07);
}
.armcards .armcard.armemp {
  background:
    linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0) 38%),
    linear-gradient(158deg, #1e2632 0%, #141a24 58%, #0b0e14 100%);
}
/* the portrait gets a seat of its own, so a troop is never read against the
   card's corner gradient */
.armcards .armicon {
  border-radius: var(--r-md);
  background: radial-gradient(circle at 50% 34%, #33291b, #0d0a06 78%);
  box-shadow: inset 0 0 0 1px rgba(var(--gold-rgb),0.24), inset 0 2px 6px rgba(0,0,0,0.6);
}
.armicon { width: 60px; height: 60px; }
.aname { font-size: 16px; line-height: 1.14; letter-spacing: 0.2px; }
.acatpill { font-size: 10.5px; padding: 1.5px 8px; }
.arole { font-size: 12px; line-height: 1.32; color: var(--ready-lt); }
.astats { gap: 5px 9px; padding: 7px 0 2px; }
.astats span { font-size: 13px; }
.astats i { font-size: 12px; opacity: 0.75; }
.afield { font-size: 12.5px; padding: 4px 8px; }
.afield i { font-size: 11.5px; }
.afield em { font-size: 10px; color: #b3a68a; }
.apow { display: flex; flex-wrap: wrap; gap: 4px 5px; padding: 2px 0 3px; }
.apow span {
  font: 600 11px var(--font-ui); color: #eef4fa; letter-spacing: 0.1px;
  background: rgba(255,255,255,0.075); border: 1px solid rgba(var(--gold-rgb),0.28);
  border-radius: var(--r-sm); padding: 2px 7px; white-space: nowrap;
}
.amatch { font-size: 11.5px; line-height: 1.35; gap: 2px; }
.astrong, .aweak { white-space: normal; overflow: visible; text-overflow: clip; }
/* THE PRICE, AT PRICE SIZE. With the shortfall bar out of the row the cost is the only
   thing in the footer, so it gets the whole width and a numeral you can read down a
   column of cards without leaning in. Locked is GREY, not merely dimmer gold: a
   desaturated slate against the affordable card's green reads as "not yet" from across
   the grid, which is the one distinction this row exists to draw. */
.armcard .buybtn { width: 100%; font: 800 17px var(--font-display); letter-spacing: 0.4px;
  padding: 9px 12px; font-variant-numeric: var(--nums); }
.armcard .buybtn.go { font-size: 15px; }        /* it carries "✨ Unlock ·" as well as the number */
.armcard .buybtn.dim, .armcard .buybtn:disabled {
  color: #9a9384; background: linear-gradient(180deg, #3c3830, #2a2721);
  border-color: #46423a; text-shadow: none; }
/* the buy footer stays pinned to the bottom however tall the body grows, so a grid of
   cards keeps one price line across the row instead of a ragged staircase */
.armcard .afoot { margin-top: auto; padding-top: 8px; }

/* ===================================================================== */
/* ==========  BUYING SOMETHING FEELS LIKE BUYING SOMETHING  =========== */
/* ===================================================================== */
/* Raising a spawner mid-battle has a sound, a shake when it is refused and a coin
   floating off the card. Unlocking a troop — the same economic act, and the one
   this whole screen exists to stage — had none of it: the shelf silently redrew.
   Three things say it now: the sound (see the buy handler), the purse flinching,
   and the card itself. */

/* THE SHELF STOPS RELOADING ON EVERY PURCHASE. `.armcard` carries an entrance, and
   a buy rebuilds all of them, so unlocking one troop replayed thirty-odd entrances
   — a wave of movement across everything that had NOT changed, which is the exact
   opposite of pointing at the thing that had. Held back for that one rebuild.

   `:not(.justbought)` matters: the id in this selector outranks the plain class on
   the flash below, so without the exception the quiet rule silences the one card it
   exists to make visible. */
#armgrid.armquiet .armcard:not(.justbought) { animation: none; }
#armgrid.armquiet .achip:not(.justbought) { animation: none; }

/* ===================================================================== */
/* ==========  THE SHELF AS A ROSTER — ONE ARMY, ONE SCREEN  =========== */
/* ===================================================================== */
/* Thirty-four tall cards in a 480px box ran 6,500px long: nineteen screens of
   scrolling to see one kingdom, so nobody ever saw an ARMY — only the four cards
   the scrollbar happened to be sitting over. The muster answers the same question
   by showing all of it at once, portraits grouped by role, and it is the right
   answer here for the same reason: you shop for an army, not for a unit.
   Same chip as `.md-troop`, with the one thing this screen adds — a price. */
/* THE ROLES SIT SIDE BY SIDE, not stacked. Chips alone got one kingdom from 6,500px
   down to 1,900 — but thirteen roles stacked in a column is thirteen HEADERS, most
   of them introducing two or three chips, and the headings were then the thing doing
   the scrolling. The muster lays its roles out as grid cells for exactly this reason
   (see `.md-roster`), and the same shape finishes the job here: four columns, every
   role a cell, one army on one screen. */
/* COLUMNS, NOT A GRID. A grid aligns rows, so every cell in a row is as tall as the
   tallest one in it — and with roles this uneven (Structures holds six, Siege holds
   two) a two-chip block was being stretched to 231px to keep rank with a six-chip
   one. Three quarters of that is nothing. Multi-column packing lets each role take
   only the height it needs and flows the next one up underneath, which is the whole
   difference between fitting on one screen and not. */
#armgrid { overflow-y: auto; scrollbar-gutter: stable;
  column-count: 4; column-gap: 14px; }
.armsec { margin-bottom: 10px; break-inside: avoid; -webkit-column-break-inside: avoid; }
/* the flat orderings (cheapest, dearest, A–Z) have no roles to pack — one running
   list of chips, across the whole width */
#armgrid > .armcards { column-span: all; }
/* FLEX WITH A FIXED CHIP, not `grid` with `auto-fill`. Inside a multi-column parent
   a grid cannot resolve an auto-fill track count against the column's width — it
   measured the whole shelf instead, laid ONE 250px track, and every chip overflowed
   its own role by forty pixels. A fixed chip and a wrap needs no such resolution. */
.armcards { display: flex; flex-wrap: wrap; gap: 6px; align-items: flex-start; }
/* the heading shrinks to a cell's width: the rule and the blurb were sized for a
   line running the full shelf, and a quarter of one is not that line */
#armgrid .armsechd { gap: 6px; margin-bottom: 6px; }
#armgrid .armsechd h3 { font-size: 12px; letter-spacing: 1px; }
#armgrid .armsecrole { display: none; }        /* no room for prose in a quarter-width cell */
#armgrid .armsecnum { font-size: 11px; }
#armgrid .armsecbar { width: 34px; height: 4px; }
/* ---- A SOLID PLATE BEHIND EVERY TROOP ---------------------------------
   The chip was `rgba(0,0,0,0.3)` over the armory's own translucent screen,
   which is a live battlefield: trees, keeps, banners and moving weather ran
   straight through the shelf, behind the art. That is the worst possible ground
   for the one thing this screen exists to show you — a 78px troop portrait, cut
   out, in its kingdom's colours. A silhouette needs a field behind it, every
   chip needs the SAME field as the chip beside it, and neither survives a
   backdrop that is different under every card and different again a second
   later.
   Opaque, and lit from the top the way every other struck plate in the game is,
   with the category's colour kept where it already was — on the rim. */
.achip {
  position: relative; display: flex; flex-direction: column; align-items: center; gap: 2px;
  width: 100px; padding: 7px 5px 6px; border-radius: var(--r-lg); cursor: pointer; text-align: center;
  background:
    linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0) 42%),
    linear-gradient(168deg, #26200f 0%, #191509 56%, #0e0b05 100%);
  border: 1px solid color-mix(in srgb, var(--cat) 30%, transparent);
  box-shadow: var(--sh-1), inset 0 1px 0 rgba(var(--lit-rgb),0.08);
  font-family: inherit; color: inherit;
  transition: transform 0.11s var(--e-back), border-color 0.14s, box-shadow 0.14s, filter 0.14s;
}
.achip:hover { transform: translateY(-3px); filter: brightness(1.16);
  border-color: var(--cat); box-shadow: var(--sh-3), 0 0 14px color-mix(in srgb, var(--cat) 34%, transparent); z-index: 2; }
/* THE ART IS THE POINT. The first pass drew it at 44px and then multiplied by the
   per-unit scale, so a levyman came out at THIRTY-SEVEN pixels — half what the card
   it replaced showed, on a screen whose whole job is looking at your army. The base
   is 60 now and the scale only ever grows it. */
.achip canvas { width: calc(78px * var(--us, 1)); height: calc(78px * var(--us, 1)); flex: none; }
.achip i { flex: none; font: 600 11px 'Spectral', serif; font-style: normal; color: #ded2b4;
  line-height: 1.14; width: 100%; overflow-wrap: break-word; hyphens: auto;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.achip i.long { font-size: 9.5px; letter-spacing: -0.2px; }
/* THE PRICE IS A BUTTON. A bare number told nobody it could be pressed — the card
   this replaced ended in a gilt "✨ Unlock · ⛁200" slab, and losing that lost the
   only thing on the chip saying an unlock was even possible. Same green, same
   sparkle, the shape of the button it came from. */
.achip s { text-decoration: none; margin-top: 2px; width: 100%; display: block;
  padding: 3px 4px; border-radius: var(--r-md); font: 800 10px var(--font-ui);
  font-variant-numeric: var(--nums); letter-spacing: 0.2px; }
.achip .ac-cost b { display: block; font: 800 8.5px var(--font-display); letter-spacing: 0.7px;
  text-transform: uppercase; opacity: 0.92; }
.achip.buy .ac-cost { color: #07140b; background: linear-gradient(180deg, var(--ready), var(--ready-dk));
  border: 1px solid #2f8f54; box-shadow: 0 1px 0 rgba(255,255,255,0.35) inset; }
.achip .ac-cost.locked { color: #9a9384; background: linear-gradient(180deg, #3c3830, #2a2721);
  border: 1px solid #46423a; }
.achip .ac-own { color: var(--ready); background: rgba(127,211,154,0.1); border: 1px solid rgba(127,211,154,0.3); }
/* affordable now: the one state worth lighting up */
.achip.buy { border-color: rgba(var(--ready-rgb),0.5);
  background:
    linear-gradient(180deg, rgba(var(--ready-rgb),0.13), rgba(255,255,255,0) 46%),
    linear-gradient(168deg, #1c2716 0%, #141c0f 56%, #0b0f08 100%); }
.achip.buy:hover { border-color: var(--ready); box-shadow: var(--sh-3), 0 0 16px rgba(var(--ready-rgb),0.4); }
.achip.buy:hover .ac-cost { filter: brightness(1.08); }
.achip:active .ac-cost { transform: translateY(1px); }
/* OWNED IS NOT DISABLED. A blanket `opacity: 0.75` re-opened the plate it sits
   on — the whole point of making it solid — and dimmed the art as if the troop
   you had already bought were the least interesting thing on the shelf. The tick
   in the corner says owned; the portrait stays at full strength. */
.achip.owned { cursor: default; }
.achip.owned i { color: #bfb59a; }
.achip.owned::before { content: '✓'; position: absolute; top: -6px; left: -5px; z-index: 2;
  width: 17px; height: 17px; border-radius: 50%; font: 800 11px var(--font-display); line-height: 17px;
  color: #06150b; background: linear-gradient(180deg, var(--ready-lt), var(--ready-dk));
  border: 1.5px solid rgba(6,21,11,0.72); box-shadow: var(--sh-0); }
.achip.sig::after { content: '★'; position: absolute; top: -5px; right: -3px; z-index: 2;
  font-size: 11px; color: var(--gilt-lit); text-shadow: 0 0 6px rgba(var(--gilt-lit-rgb),0.95); }
.achip.sig { border-color: rgba(var(--gilt-lit-rgb),0.45); }
/* a structure reads as stone rather than cloth, the way the rack separates them */
.achip.emp { background:
  linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0) 42%),
  linear-gradient(168deg, #1e2632 0%, #141a24 56%, #0a0d13 100%); }
/* refused, not dead — the in-battle rack's own answer to "you cannot afford that" */
.achip.deny { animation: achipDeny 0.36s ease-out; }
@keyframes achipDeny {
  0%,100% { transform: translateX(0); }
  20% { transform: translateX(-4px); } 45% { transform: translateX(4px); } 70% { transform: translateX(-2px); }
}
.achip.justbought { animation: armBought 0.72s var(--e-out) both; z-index: 2; }

/* ---- the chrome gives its height to the army ----
   Two hundred pixels of header, progress bar and filter rail sat above a shelf with
   470 to draw thirty-odd portraits in. Every one of those rows still says what it
   said; they just stop being sized as though they were the subject of the screen.
   The army is the subject. */
/* AUTO, not 26px — and the 26 comes back anyway. This is the rule that actually
   decides the bar's margins (an id beats the class rule that caps its width), so the
   centring has to be stated here or the cap just pins a 1848px bar to the left edge
   of an ultrawide. With `max-width: min(1848px, 100% - 52px)` above, a window under
   the cap leaves exactly 52px for the auto margins to split — the same 26 a side this
   said outright — and a window over it centres the bar on the shelf. */
#scr-armory .armbar { margin: 0 auto 6px; padding: 6px 14px; }
#scr-armory .armprogbar { height: 7px; }
#scr-armory .armready, #scr-armory .armdim { font-size: 12.5px; }
#scr-armory .armtools { margin: 0 26px 7px; gap: 5px; }
#scr-armory .armsearch { padding: 5px 11px; }
#scr-armory .armsearch input { font-size: 12.5px; }
#scr-armory .armwho { padding-bottom: 2px; }

/* …and the card that changed announces itself instead. It flares in the gold the
   price was printed in, then settles into the green it now wears as owned — the
   two states it just moved between, in that order. */
.armcard.justbought { animation: armBought 0.72s var(--e-out) both; z-index: 2; }
@keyframes armBought {
  0%   { transform: scale(1); box-shadow: 0 0 0 0 rgba(var(--gilt-lit-rgb),0); }
  22%  { transform: scale(1.045); box-shadow: 0 0 0 2px rgba(var(--gilt-lit-rgb),0.95), 0 0 26px rgba(var(--gilt-lit-rgb),0.6); }
  60%  { transform: scale(1); box-shadow: 0 0 0 2px rgba(var(--ready-rgb),0.7), 0 0 20px rgba(var(--ready-rgb),0.4); }
  100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(var(--ready-rgb),0); }
}
/* the green tick it just earned lands with it rather than simply being there */
.armcard.justbought .armiconwrap::before { animation: armTick 0.6s var(--e-back-lg) 0.12s both; }
@keyframes armTick { from { opacity: 0; transform: scale(0.2) rotate(-25deg); } to { opacity: 1; transform: none; } }

/* THE PURSE FLINCHES. It changed silently before — an odd way for the one number
   the whole decision was weighed against to behave at the moment it is spent. */
#hdrgold.spent { animation: goldSpent 0.5s ease-out both; }
@keyframes goldSpent {
  0%   { transform: scale(1); color: var(--gilt-lit); }
  30%  { transform: scale(0.9); color: #ffb570; }
  100% { transform: scale(1); }
}

/* ===================================================================== */
/* ======  THE SHELF, SPEAKING THE DOSSIER'S LANGUAGE  ================= */
/* ===================================================================== */
/* The muster and the Armory ask about the same 182 things and were drawing them as if
   they came from two different games: the dossier labelled a role in the role's colour
   under a rule of that colour and marked a troop with a green tick and a gold star; the
   shelf labelled the same role with a grey dot and a grey underline, greyed out anything
   you could not afford, and marked nothing at all. Same subject, same player, one
   vocabulary. The CARD keeps everything it carries — stats, powers, counters, price —
   because the shelf is where things are compared and bought; only the way it SPEAKS
   changes. */

/* ---- the section heading: the dossier's, carrying the shop's numbers ---- */
/* No dot and no bottom border. In the dossier the coloured rule IS the divider and the
   name IS the colour key, which is one mark doing the work the shelf was spending three
   on. `.armsecdot` is gone with the dot it drew. */
.armsechd { gap: 9px; padding-bottom: 0; border-bottom: none; margin-bottom: 11px; }
.armsechd h3 { flex: none;
  font: 800 15px var(--font-display); letter-spacing: 1.3px; text-transform: uppercase;
  color: color-mix(in srgb, var(--cat) 55%, #f2e7cd); text-shadow: 0 1px 2px rgba(0,0,0,0.8); }
/* an <s> purely as a rule, exactly as `.md-cath s` — it carries the category's colour
   out across the block and gives the trailing numbers something to sit against */
.armsecrule { flex: 1 1 auto; min-width: 10px; height: 2px; border-radius: var(--r-xs); text-decoration: none;
  background: linear-gradient(90deg, color-mix(in srgb, var(--cat) 62%, transparent), transparent); }
/* the blurb stops competing for the row: it no longer grows, so the rule does */
.armsecrole { flex: 0 1 auto; max-width: 42%; font-size: 11px; color: rgba(190,180,160,0.5); }
.armsecnum { color: color-mix(in srgb, var(--cat) 40%, #cfc3a6); }

/* ---- the portrait, and the two marks the dossier puts on it ---- */
.armcards { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); }
/* A NOTICEABLY BIGGER SOLDIER. 60px was a thumbnail beside three lines of type; the
   dossier gives the artwork the chip and reads as an army because of it. The backing
   store went to 96 with it, so the extra size is real detail rather than a stretched
   64px bitmap. */
.armiconwrap { position: relative; flex: none; line-height: 0; }
.armicon { width: 76px; height: 76px; }
/* NOT DIMMED, and NOT GREYED. Fading everything you cannot afford yet buried the
   artwork on two thirds of the shelf and made a kingdom look impoverished — the same
   reasoning that stopped the dossier greying unowned troops. The distinction is already
   drawn, twice: no tick on the portrait, and a grey price where an affordable card has
   a green one. */
.armcard.level { opacity: 1; }
.armcard.level .armicon, .armcard.gold .armicon { filter: drop-shadow(0 3px 4px rgba(0,0,0,0.5)); }
/* OWNED — the dossier's green tick, on the artwork's top-left corner. It hangs off the
   portrait rather than the card because the card is three times the portrait's width,
   and a mark that far from the thing it marks stops belonging to it. */
.armcard.owned .armiconwrap::before { content: '✓'; position: absolute; top: -6px; left: -6px; z-index: 2;
  width: 19px; height: 19px; border-radius: 50%; font: 800 12px var(--font-display); line-height: 19px; text-align: center;
  color: #06150b; background: linear-gradient(180deg, var(--ready-lt), var(--ready-dk));
  border: 1.5px solid rgba(6,21,11,0.72); box-shadow: var(--sh-0), 0 0 10px rgba(var(--ready-rgb),0.75); }
/* SIGNATURE — the dossier's gold star, opposite corner so it can never collide with the
   tick, at the size the dossier draws it. */
.armcard.sig .armiconwrap::after { content: '★'; position: absolute; top: -5px; right: -4px; z-index: 2;
  font-size: 12px; color: var(--gilt-lit); text-shadow: 0 0 6px rgba(var(--gilt-lit-rgb),0.95); }
/* …and the faint gold the dossier washes a signature chip with. `:not(.ready)` because
   a card you can buy THIS SECOND already owns its outline: "affordable now" is the one
   thing on this screen you can act on, and it outranks "this kingdom is known for it". */
.armcard.sig:not(.ready) { border-color: rgba(var(--gilt-lit-rgb),0.5); }
.armcard.sig:not(.ready)::before { box-shadow: 0 0 9px color-mix(in srgb, var(--cat) 55%, transparent), 0 0 14px rgba(var(--gilt-lit-rgb),0.25); }
.armcard.sig .aname { color: var(--gold-lt); }

/* ---------------- WAR-BANDS ------------------------------------------------
   The overlay is the Mint's, structurally: same layer, same fade, same
   backdrop dismiss (see `.mintlay`). It is a narrower box because it is a
   roster and two buttons rather than a shelf of five tiers. */
.plobbylay {
  position: fixed; inset: 0; z-index: 60; display: flex; align-items: center; justify-content: center;
  padding: 20px; background: rgba(6,7,10,0.72); backdrop-filter: blur(5px);
  opacity: 0; transition: opacity 0.18s ease;
}
.plobbylay.in { opacity: 1; }
.plobbylay.out { opacity: 0; }
/* A COLUMN, NOT A SCROLLER. The box used to scroll as one piece; it now holds
   two things with different jobs — a panel you read top to bottom, and a chat
   whose newest line must be the one on screen. So the panel takes the slack and
   scrolls, and the war-council is pinned to the bottom of the box at its own
   size with its own log scroll inside it. */
.plobbybox {
  position: relative; width: min(560px, 96vw); max-height: 92vh; overflow: hidden;
  display: flex; flex-direction: column; border-radius: var(--r-2xl); text-align: center;
  background: linear-gradient(170deg, rgba(44,36,23,0.98), rgba(13,11,8,0.99));
  border: 1px solid var(--edge); border-top: 3px solid var(--gold);
  box-shadow: var(--sh-4), 0 0 40px rgba(var(--gold-rgb),0.14);
  transform: translateY(10px) scale(0.99); transition: transform 0.22s var(--e-out);
}
.pbody { padding: 22px 24px 18px; overflow: auto; flex: 0 1 auto; }
.plobbylay.in .plobbybox { transform: none; }
.pb-t { font: 800 24px var(--font-display); color: var(--gold-lt); letter-spacing: 1px; }
.pb-s { font: 500 12.5px var(--font-ui); color: #bcae90; margin-top: 4px; line-height: 1.45; }
/* THE CODE IS THE POINT OF THE PANEL, so it is the biggest thing on it and set
   in a face where every glyph is unmistakable — it gets read aloud. The
   alphabet already excludes O/0 and I/1; monospace handles the rest. */
.pb-code-big {
  margin: 12px auto 6px; padding: 8px 18px; display: inline-block; border-radius: var(--r-xl);
  font: 800 34px ui-monospace, "SFMono-Regular", Consolas, monospace; letter-spacing: 8px; text-indent: 8px;
  color: var(--gold-bright); background: rgba(0,0,0,0.34); border: 1px solid var(--edge);
  text-shadow: 0 0 18px rgba(243,216,137,0.35);
}
.pb-linkrow { display: flex; gap: 8px; justify-content: center; margin-top: 10px; }
.pb-linkrow .menubtn { padding: 8px 14px; font-size: 12.5px; }
.pb-or { margin: 14px 0 8px; font: 600 11px var(--font-ui); color: var(--muted); letter-spacing: 1.2px; text-transform: uppercase; }
.pb-joinrow { display: flex; gap: 8px; justify-content: center; }
.pb-code {
  flex: 1; max-width: 220px; padding: 10px 12px; border-radius: var(--r-lg); text-align: center;
  font: 800 20px ui-monospace, Consolas, monospace; letter-spacing: 5px; text-indent: 5px;
  background: rgba(0,0,0,0.4); border: 1px solid var(--edge); color: var(--gold-bright);
}
.pb-code:focus { outline: none; border-color: var(--gold-lt); box-shadow: 0 0 12px rgba(var(--gold-rgb),0.3); }
.pb-moderow { display: flex; align-items: center; gap: 10px; justify-content: center; margin: 16px 0 10px; }
.pb-ml { font: 700 11px var(--font-display); color: var(--muted); letter-spacing: 1px; text-transform: uppercase; }
.pb-mode, .pb-modefix {
  padding: 7px 10px; border-radius: var(--r-lg); font: 600 13px var(--font-ui); color: var(--ink);
  background: rgba(0,0,0,0.35); border: 1px solid var(--edge);
}
.pb-mode:focus { outline: none; border-color: var(--gold-lt); }
/* ---- the banner you march under, chosen in the band panel ----
   The muster's rail, shrunk to fit under a roster: the same cloth, the same
   crest, the same tick. It is the same choice, so it looks like the same thing
   in both places (see `.mtile`). Seven of them scroll sideways rather than
   wrapping onto a second row — a rail you skim is one line by definition. */
.pb-kdom { margin: 6px 0 12px; }
.pb-kl {
  font: 700 11px var(--font-display); color: var(--muted);
  letter-spacing: 1px; text-transform: uppercase; text-align: left; margin-bottom: 6px;
}
.pb-krail {
  display: flex; gap: 6px; overflow-x: auto; overflow-y: hidden;
  padding-bottom: 4px; scrollbar-width: thin;
}
.pb-k {
  position: relative; flex: 1 0 62px; min-width: 62px; cursor: pointer;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 7px 4px 6px; border-radius: var(--r-lg);
  background: linear-gradient(180deg, color-mix(in srgb, var(--fac) 22%, rgba(0,0,0,0.45)), rgba(0,0,0,0.42));
  border: 1px solid color-mix(in srgb, var(--fac) 42%, transparent);
  transition: border-color 0.14s ease, transform 0.14s var(--e-out), box-shadow 0.14s ease;
}
.pb-k:hover { border-color: var(--facl); transform: translateY(-1px); }
.pb-k.on {
  border-color: var(--facl);
  background: linear-gradient(180deg, color-mix(in srgb, var(--fac) 46%, rgba(0,0,0,0.3)), rgba(0,0,0,0.38));
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--facl) 45%, transparent), 0 0 14px color-mix(in srgb, var(--fac) 40%, transparent);
}
.pb-kc { width: 30px; height: 36px; image-rendering: pixelated; }
.pb-kn {
  font: 800 9.5px var(--font-display); line-height: 1.15; letter-spacing: 0.3px;
  color: #cdbf9e; text-align: center;
}
.pb-k.on .pb-kn { color: #fff; }
/* The tick sits OVER the crest rather than beside the name: seven tiles this
   narrow have no width to spend on a column that is empty on six of them. */
.pb-ktick {
  position: absolute; top: 3px; right: 4px; font-size: 10px; font-weight: 900;
  color: var(--facl); opacity: 0; transition: opacity 0.14s ease;
}
.pb-k.on .pb-ktick { opacity: 1; }

/* ══════════════════════════════════════════════════════════════════════
   THE COMMANDER'S MARK — one identity, drawn the same way everywhere
   ══════════════════════════════════════════════════════════════════════
   Arms, name, house and rung, in that reading order, on the war-band roster,
   the muster strip, the search screen and the results banner. Every part is
   optional and the mark collapses cleanly around whatever is missing: a
   commander with no house and no rung is a name, which is exactly what they
   were before any of this existed. */
.cmark { display: flex; align-items: center; gap: 7px; min-width: 0; }
.cm-arms, .cm-noarms {
  flex: none; width: 26px; height: 31px;
  filter: drop-shadow(0 1px 2px rgba(0,0,0,0.6));
}
/* A commander with no house still holds the space one would take, so a roster
   of four does not go ragged the moment one of them leaves their guild. */
.cm-noarms {
  border-radius: 3px 3px 46% 46% / 3px 3px 30% 30%;
  background: rgba(var(--shade-rgb), 0.42);
  border: 1px solid var(--edge-quiet);
  box-sizing: border-box;
}
.cm-tx { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; min-width: 0; }
.cm-nm {
  font: 700 14px var(--font-ui); color: inherit;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 22ch;
}
.cm-crown { color: var(--gold-lt); font-size: 15px; }
.cm-sub { display: flex; align-items: center; gap: 5px; min-width: 0; }
.cm-tag {
  font: 800 9.5px var(--font-display); letter-spacing: 1px; text-transform: uppercase;
  color: var(--gilt-body); background: rgba(var(--shade-rgb), 0.46);
  border: 1px solid var(--edge-quiet); border-radius: var(--r-pill);
  padding: 0 5px; white-space: nowrap;
}
/* The rung, in its division's own metal — the same six colours the ladder
   screen, the home plate and the results block are painted in, so a pip on a
   roster row is recognisably the thing you climb. */
.cm-rk {
  font: 800 9.5px var(--font-display); letter-spacing: 0.8px; text-transform: uppercase;
  color: var(--rcl, var(--gold-lt)); white-space: nowrap;
  border: 1px solid color-mix(in srgb, var(--rc, #a2703c) 62%, transparent);
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--rc, #a2703c) 26%, transparent),
    color-mix(in srgb, var(--rc, #a2703c) 9%, transparent));
  border-radius: var(--r-pill); padding: 0 6px;
}
/* Never placed. Said plainly rather than left blank — in the commander opposite
   it is a real answer, and one worth knowing before the first blow. */
.cm-rk.unplaced { color: var(--muted); border-color: var(--edge-quiet); background: rgba(var(--shade-rgb), 0.4); }

/* ---- the small mark: a strip, not a roster ----
   One line, arms shrunk to a glance, no stacked sub-line. Used where the mark
   is riding inside a sentence — "Riding with X, Y" — and a two-row block would
   break the line it is part of. */
.cmark.sm { display: inline-flex; gap: 4px; vertical-align: middle; }
.cmark.sm .cm-arms, .cmark.sm .cm-noarms { width: 15px; height: 18px; }
.cmark.sm .cm-tx { flex-direction: row; align-items: center; gap: 4px; }
.cmark.sm .cm-nm { font: inherit; color: inherit; max-width: 14ch; }
.cmark.lg .cm-arms, .cmark.lg .cm-noarms { width: 34px; height: 41px; }
.cmark.lg .cm-nm { font-size: 16px; }

.pb-roster { display: flex; flex-direction: column; gap: 6px; margin: 6px 0 10px; }
.pb-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 8px 10px; border-radius: var(--r-lg);
  background: linear-gradient(180deg, rgba(255,255,255,0.045), rgba(0,0,0,0.26));
  border: 1px solid rgba(var(--gold-rgb),0.22);
}
.pb-row.me { border-color: var(--edge); background: linear-gradient(180deg, rgba(var(--gold-rgb),0.13), rgba(0,0,0,0.28)); }
/* The roster row IS a commander's mark (see `.cmark`) — name, house and rung
   are drawn by that, and this only says how large it sits on this screen. */
.pb-who { color: var(--ink); }
/* Which kingdom each commander is bringing, in that kingdom's own ink — the
   same colour their keep will be flying in an hour. Rides in the mark's sub
   line beside the house and the rung. */
.pb-mk {
  font: 800 9.5px var(--font-display); letter-spacing: 0.5px; text-transform: uppercase;
  color: var(--facl); white-space: nowrap;
}
.pb-teams { display: flex; gap: 5px; }
/* Green is the alliance you belong to and red is the one across the field —
   the same two colours the battlefield itself uses (see ALLY_GREEN/FOE_RED). */
.pb-team {
  padding: 5px 9px; border-radius: var(--r-md); cursor: pointer; white-space: nowrap;
  font: 700 11px var(--font-ui); color: var(--muted);
  background: rgba(0,0,0,0.32); border: 1px solid rgba(var(--gold-rgb),0.2);
  transition: border-color 0.12s ease, color 0.12s ease, background 0.12s ease;
}
.pb-team:hover:not(:disabled) { border-color: var(--gold-lt); color: var(--ink); }
.pb-team:disabled { opacity: 0.4; cursor: default; }
.pb-team.t0.on { color: #07120b; background: linear-gradient(180deg, #8ce88a, #3fbf63); border-color: #1c6b33; }
.pb-team.t1.on { color: #1a0707; background: linear-gradient(180deg, #ff9d92, #e0564a); border-color: #7a2822; }
.pb-team.on { opacity: 1; }
.pb-warn {
  margin: 8px 0; padding: 8px 12px; border-radius: var(--r-lg); font: 600 12px var(--font-ui); line-height: 1.45;
  color: #ffcfc7; background: rgba(224,86,74,0.12); border: 1px solid rgba(224,86,74,0.34);
}
.pb-foot { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }
.pb-wait { padding: 12px; border-radius: var(--r-lg); font: 600 13px var(--font-ui); color: var(--muted);
  background: rgba(0,0,0,0.28); border: 1px dashed rgba(var(--gold-rgb),0.3); }
.pb-wait b { color: var(--gold-lt); }
.pb-note { margin-top: 12px; font: 500 11px var(--font-ui); color: var(--muted); line-height: 1.5; }

/* ---- THE WAR-COUNCIL ------------------------------------------------------
   The band's own chat, sat at the foot of the panel: a header, a log that
   scrolls on its own, and one line to type into. It is a compartment rather
   than more panel — darker ground and a rule across the top — because it is
   the one part of this overlay that is not about arranging the line-up. */
.pchat {
  flex: 0 0 auto; display: flex; flex-direction: column; text-align: left;
  border-top: 1px solid rgba(var(--gold-rgb),0.28);
  background: linear-gradient(180deg, rgba(0,0,0,0.36), rgba(0,0,0,0.5));
  padding: 9px 14px 12px;
}
.pchat.hidden { display: none; }
.pc-hdr {
  display: flex; align-items: center; gap: 6px; margin-bottom: 6px;
  font: 700 11px var(--font-display); letter-spacing: 1px; text-transform: uppercase; color: var(--gold-lt);
}
.pc-hint { margin-left: auto; font: 500 10px var(--font-ui); letter-spacing: 0; text-transform: none; color: var(--muted); }
/* A FIXED HEIGHT, not a growing one. A log that sizes to its contents moves the
   input — the one control here — down the screen a line at a time as people
   talk, and on a tall band the panel above it would be squeezed instead. */
.pc-log {
  height: 132px; overflow-y: auto; overflow-x: hidden; scrollbar-width: thin;
  display: flex; flex-direction: column; gap: 3px;
  padding: 6px 8px; border-radius: var(--r-lg);
  background: rgba(0,0,0,0.34); border: 1px solid rgba(var(--gold-rgb),0.16);
}
.pc-l { font: 500 12.5px var(--font-ui); color: var(--ink); line-height: 1.45; word-wrap: break-word; overflow-wrap: anywhere; }
.pc-n { font-weight: 800; color: var(--gold-lt); margin-right: 6px; }
.pc-n::after { content: ':'; opacity: 0.55; }
.pc-l.me .pc-t { color: #f0e6cd; }
/* The band's own arrivals and partings, told apart from what people SAY by
   being quieter and italic — a log where the two read alike is one where
   "Chas has left the band" looks like something Chas said. */
.pc-l.sys { font: 500 11.5px var(--font-ui); color: var(--muted); font-style: italic; }
.pc-row { display: flex; gap: 6px; margin-top: 7px; }
.pc-in {
  flex: 1; min-width: 0; padding: 8px 10px; border-radius: var(--r-lg);
  font: 500 13px var(--font-ui); color: var(--ink);
  background: rgba(0,0,0,0.42); border: 1px solid var(--edge);
}
.pc-in::placeholder { color: #7d7159; }
.pc-in:focus { outline: none; border-color: var(--gold-lt); box-shadow: 0 0 10px rgba(var(--gold-rgb),0.22); }
.pc-send {
  padding: 8px 14px; border-radius: var(--r-lg); cursor: pointer; white-space: nowrap;
  font: 800 12px var(--font-display); letter-spacing: 0.6px; color: var(--gold-lt);
  background: linear-gradient(180deg, rgba(var(--gold-rgb),0.2), rgba(0,0,0,0.32));
  border: 1px solid rgba(var(--gold-rgb),0.4);
  transition: border-color 0.12s ease, color 0.12s ease;
}
.pc-send:hover { border-color: var(--gold-lt); color: #fff; }
/* On a short screen the PANEL is the thing that has to fit. A log holding its
   full height there spends a third of the box on talk and pushes MARCH TOGETHER
   — the one button the whole overlay exists to reach — below the fold. */
@media (max-height: 700px) {
  .pchat { padding: 7px 14px 9px; }
  .pc-log { height: 84px; }
}

/* The band, named on the muster and again on the search screen. Allies read
   green and opponents red, so "who am I actually fighting" is answered by
   colour before it is read. */
.musterparty, .searchband {
  text-align: center; font: 600 12px var(--font-ui); color: var(--muted);
  padding: 0 20px 8px; display: flex; gap: 7px; justify-content: center; align-items: center; flex-wrap: wrap;
}
.searchband { padding: 0 0 4px; }
/* The muster's band strip is the way back into the panel — and so into the
   war-council — from the screen its members are actually sitting on. */
.musterparty { cursor: pointer; }
.mp-open {
  display: inline-flex; align-items: center; gap: 4px; padding: 3px 9px; border-radius: var(--r-md);
  font: 700 10.5px var(--font-display); letter-spacing: 0.6px; text-transform: uppercase; color: var(--gold-lt);
  background: rgba(0,0,0,0.34); border: 1px solid rgba(var(--gold-rgb),0.32);
  transition: border-color 0.12s ease, color 0.12s ease;
}
.musterparty:hover .mp-open { border-color: var(--gold-lt); color: #fff; }
.mp-n { font-weight: 800; font-family: var(--font-display); letter-spacing: 0.4px; }
.mp-n.ally { color: #8ce88a; }
.mp-n.foe { color: #ff9d92; }
.mp-h { color: var(--muted); font-weight: 500; }

/* A toast that lives on the BODY, not inside `#hud` — the herald is hidden on
   the menu, which is exactly where a war-band does all its talking. */
#uinotice { position: fixed; left: 50%; top: 18px; transform: translateX(-50%); z-index: 90;
  display: flex; flex-direction: column; gap: 6px; align-items: center; pointer-events: none; }
.uinote {
  padding: 9px 16px; border-radius: var(--r-lg); max-width: min(520px, 92vw);
  font: 600 13px var(--font-ui); color: var(--ink); text-align: center;
  background: linear-gradient(180deg, rgba(46,38,24,0.97), rgba(16,13,9,0.98));
  border: 1px solid var(--edge); box-shadow: var(--sh-4);
  opacity: 0; transform: translateY(-8px); transition: opacity 0.2s ease, transform 0.2s ease;
}
.uinote.in { opacity: 1; transform: none; }
.uinote.out { opacity: 0; transform: translateY(-8px); }
.uinote.bad { color: #ffcfc7; border-color: rgba(224,86,74,0.5); }
.uinote.good { color: #d8ffe2; border-color: rgba(var(--ready-rgb),0.45); }

/* ═══════════════════ THE RACK, TIGHTENED ═══════════════════════════════════
   A card was 133px tall and spent it like this: 42px of unit art, 17px of name,
   11px of a COUNTER line that is blank most of the time, and 32px — a full
   QUARTER of the card — on an action plate reading "PLACE" over a price.

   "PLACE" is on every card in the rack. It is what the rack IS. A word that
   appears identically on all twelve tiles distinguishes none of them and cannot
   be what a quarter of the tile is for, while the one thing that actually tells
   the cards apart at a glance — the troop itself, drawn — was the smallest it
   has ever been. With twelve cards the rack wrapped to two ranks and took a
   third of the screen height off the battlefield.

   So: the plate lies down into one row (a hammer and a number, side by side),
   the counter cue comes out of the flow entirely, and every pixel that buys
   back goes to the art.  */

/* THE PLATE LIES DOWN. Column -> row halves it, and the row is a PRICE: one
   gold coin and one number, on one line, always.
   ---- TWO MARKS FOR ONE FACT ----------------------------------------------
   It used to carry a hammer glyph AND a coin AND the figure, three objects
   across about seventy usable pixels — and the hammer said "this builds", which
   is true of every tile in the rack and therefore distinguishes nothing. With
   three objects competing the number was the first thing to lose room, so a
   four-digit price wrapped onto a second line and the plate grew taller than
   the tile it sat in.
   The hammer is gone. What is left is the coin and the price, which is the only
   thing this plate was ever for, and there is room for the longest figure in
   the game on one line with the coin beside it. */
.bupg { flex-direction: row; flex-wrap: nowrap; align-items: center; justify-content: center; gap: 5px; padding: 4px 5px; }
.bupgw { display: none; }
.bupgc {
  font-size: 12.5px; letter-spacing: 0.2px;
  display: inline-flex; align-items: center; gap: 5px;
  white-space: nowrap;                     /* a price is one line, at any width */
  min-width: 0;
}
/* the coin: struck gold, drawn rather than spelt — it was a ⛁ glyph in the
   number, and it rendered as whatever the font happened to have for it */
.bupgc::before {
  content: ''; flex: none; display: inline-block; width: 11px; height: 11px;
  border-radius: 50%;
  background:
    radial-gradient(circle at 36% 28%, #fffdf2 6%, var(--gold-hi) 34%, var(--gold) 66%, var(--gold-dk) 100%);
  box-shadow: 0 0 6px rgba(var(--gilt-lit-rgb), 0.75), inset 0 -1px 1px rgba(0,0,0,0.45),
    inset 0 1px 1px rgba(255,255,255,0.5);
}
/* …and at the tightest rack widths the figure gives up letter-spacing before it
   ever gives up its line */
@media (max-width: 1180px) {
  .bupgc { font-size: 11.5px; letter-spacing: 0; gap: 4px; }
  .bupgc::before { width: 10px; height: 10px; }
}


/* THE TROOP GETS THE ROOM. Every pixel the two changes above bought goes here,
   because the drawn figure is the only thing on a card that tells you what it
   is without reading — and it was a third of the tile behind two lines of
   furniture. Half again as large, and the medallion it sits in with it. */
.bicon { width: 56px; height: 56px; margin: 2px auto 1px; }

/* …and the card comes in, HARD. "A rack of twelve stays on one rank" was the old
   aim and it was aiming at the wrong thing: a full roster is twenty-one, not
   twelve, so keeping one rank meant a 94px card times twenty-one — a slab 1400px
   across, SEVENTY PER CENT of a 1990px screen, lying over the battlefield it is
   supposed to be a tool for reading. One rank is not worth that. A rack is a
   palette; it should sit in a corner of the eye, not across the middle of it.

   68 now, not 76. The floor quoted above was "wider than the 64px portrait it
   carries" — and that portrait came down to 46 when the card's HEIGHT was cut, so
   the floor moved with it and the extra width was left over from a picture that is
   no longer there. A full roster is two ranks of eleven inside about 810px. */
.build { width: 68px; padding: 4px 4px 5px; }
.bn { font-size: 10px; }

/* ===== AND THE CARD COMES DOWN AS WELL AS IN =====
   The width above was fought over and settled; the HEIGHT never was, and height is
   what a rack costs a battlefield — a bar across the bottom of the screen is as
   tall as its tallest card, whatever its width. At 121px, two ranks of a full
   roster was 250px of ground.
   The portrait keeps its place as the biggest thing on the tile (it is still the
   only part that says what a troop is without reading) and gives up ten pixels;
   the rest comes from padding and from a price plate that was two ranks tall to
   say a hammer and a number. About a fifth off, and nothing is dropped.
   NOTE FOR ANYONE EDITING THE RACK: `.build`, `.bicon`, `.bn` and `.bupg` are each
   set in FIVE places in this file and the last one wins. This block is the last
   one. Editing the tidy rule up at line ~286 changes nothing at all. */
.bicon { width: 46px; height: 46px; margin: 1px auto 0; }
.bupg { margin-top: 2px; padding: 2px 4px; border-radius: var(--r-sm); }
/* `.build .bupgc` (two classes) is set further up the file and outranks a bare
   `.bupgc` however late it comes — specificity, not order. Matched here, or a
   five-figure price overflows the plate by 5px on a 68px card. */
.build .bupgw { font-size: 8.5px; }
.build .bupgc { font-size: 10px; }
.build .bupg { gap: 3px; padding: 2px 3px; }

/* …and the narrow-window step keeps stepping DOWN, or trimming the base above
   would have quietly made the cramped rack the wider one. */
@media (max-width: 1100px) {
  .build { width: 62px; }
  .bicon { width: 42px; height: 42px; }
}

/* ═══════════════════ THE WAR STATE ═════════════════════════════════════════
   The corner used to be a 230px map with a 17px strip of numbers crushed above
   it — three figures a commander checks constantly, given a tenth of the space
   of a picture they mostly did not look at. With the map out, the numbers ARE
   the panel and they are set like it.

   The purse is the hero, because gold is the only resource in this game and
   every decision is priced in it. Income rides under it as a rate rather than a
   second headline. The army is a BAR as well as a ratio — "112/165" is a
   calculation and a filled bar is a glance, and the one thing you need off it
   is how close to the ceiling you are. */
#warstate {
  width: var(--mmw); box-sizing: border-box;
  display: flex; flex-direction: column; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-3) var(--sp-2);
}

/* ---- THE BUILDER'S WORK LIST ----------------------------------------------
   Above the cartouche, because a queue reads DOWNWARD into the thing emptying
   it: row one is what the hammer is on, and the purse that paid for all of it is
   the panel underneath.

   THE WHOLE DESIGN BRIEF IS "DO NOT LOOK AT ME". This sits in the corner a
   commander's eye already rests in during a fight, and it is answering a question
   asked between fights — "what did I actually ask for?". So: no box of its own
   beyond a single hairline, type one step below the smallest thing near it, rows
   at 19px, and the entire panel display:none the instant the queue is empty. It
   costs nothing when there is nothing to say, which is most of a match.

   Rows do not scroll and do not wrap. Five is what fits without the corner
   growing into the field, and a sixth order says "+2 more" rather than pushing
   the purse up the screen — the deep tail of a queue is not what you are asking
   about when you glance here. */
#bldqueue {
  width: var(--mmw); box-sizing: border-box;
  padding: var(--sp-2) var(--sp-3) 0;
  animation: bqIn 0.22s var(--e-out) both;
}
#bldqueue.hidden { display: none; }
@keyframes bqIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
.bq-hd {
  display: flex; align-items: baseline; justify-content: space-between;
  font: 700 9px var(--font-ui); letter-spacing: 0.14em; text-transform: uppercase;
  color: rgba(var(--gold-rgb), 0.62);
  padding-bottom: 3px; margin-bottom: 3px;
  border-bottom: 1px solid rgba(var(--gold-rgb), 0.16);
}
.bq-hd b { font: 800 10px var(--font-display); font-variant-numeric: var(--nums); color: var(--gold-lt); letter-spacing: 0; }
#bq-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.bq-i {
  position: relative; overflow: hidden;
  display: flex; align-items: center; gap: 5px;
  height: 19px; padding: 0 4px 0 3px;
  border-radius: var(--r-xs);
  border-left: 2px solid rgba(var(--gold-rgb), 0.14);
  background: rgba(0, 0, 0, 0.18);
  cursor: pointer;
  transition: background 0.12s ease, border-color 0.12s ease;
}
.bq-i:hover { background: rgba(var(--gold-rgb), 0.13); border-left-color: rgba(var(--gold-rgb), 0.5); }
.bq-i canvas { width: 15px; height: 15px; flex: none; pointer-events: none;
  filter: drop-shadow(0 1px 1px rgba(0,0,0,0.65)); }
/* the NAME takes whatever is left and gives up gracefully — a Dracolich Roost and
   a Hut have to sit in the same 19px row without either one moving the other */
.bq-t {
  flex: 1 1 auto; min-width: 0;
  font: 600 10.5px var(--font-ui); line-height: 1;
  color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* WHAT IS BEING DONE TO IT — the one word that distinguishes two rows that are
   otherwise the same building twice. Raising and levelling are told apart by
   SHAPE rather than by hue: a level rides in a struck chip, a raise is bare
   type. The two signal colours in this interface mean "you can afford it" and
   "this is being taken from you", and neither of those is what an upgrade is. */
.bq-k {
  flex: none; font: 800 9px var(--font-display); font-style: normal; line-height: 1;
  font-variant-numeric: var(--nums); letter-spacing: 0.02em;
  color: var(--gilt-body);
}
.bq-i.upg .bq-k {
  padding: 2px 4px; border-radius: var(--r-xs);
  border: 1px solid rgba(var(--gold-rgb), 0.3);
  color: var(--gold-lt);
}
/* THE ROW THE HAMMER IS ON. Gilt rule down its left edge and its name in gold —
   the same "this is the live one" language the rest of the corner uses. */
.bq-i.live { border-left-color: var(--gold); background: rgba(var(--gold-rgb), 0.10); }
.bq-i.live .bq-t { color: var(--gold-lt); }
.bq-i.live .bq-k { color: var(--gilt-lit); }
/* …and its progress, as a hairline along the BOTTOM of the row rather than as a
   bar of its own. A queue of five with five bars in it is a chart; one row with a
   filling underline is a status. */
.bq-p {
  position: absolute; left: 0; bottom: 0; height: 2px; width: 0;
  background: linear-gradient(90deg, rgba(var(--gold-rgb),0.5), var(--gold-lt));
  transition: width 0.16s linear;
}
/* the tail nobody is asking about */
.bq-more {
  font: 700 9px var(--font-ui); letter-spacing: 0.06em;
  color: var(--muted);
  padding: 1px 0 0 5px;
}
html.touch .bq-i { height: 24px; }
html.touch .bq-t { font-size: 11.5px; }

/* ---- YOUR HOLDINGS, AS A ROW OF MARKS ------------------------------------
   Under the purse and the army, in the corner that already answers for your own
   side of the battle. It is a strip of PORTRAITS and not a list: the question it
   answers is "which of mine is which, and where", which is a picture question —
   the Upgrades panel is where the same buildings carry their numbers.

   It wraps rather than scrolls. A base can run to a dozen works and a strip that
   scrolls hides exactly the ones a commander has stopped thinking about, which
   are the ones this exists to surface. Two rows of six is still one glance. */
#bldstrip {
  width: var(--mmw); box-sizing: border-box;
  display: flex; flex-wrap: wrap; gap: 4px;
  padding: 0 var(--sp-3) var(--sp-3);
}
#bldstrip.hidden { display: none; }
.bs-i {
  position: relative; flex: none; width: 30px; height: 30px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer; border-radius: 6px;
  border: 1px solid rgba(var(--gold-rgb),0.26);
  background: linear-gradient(180deg, rgba(58,48,32,0.5), rgba(22,18,12,0.6));
  transition: border-color 0.12s ease, background 0.12s ease, transform 0.1s ease;
}
.bs-i canvas { width: 26px; height: 26px; pointer-events: none;
  filter: drop-shadow(0 1px 2px rgba(0,0,0,0.7)); }
/* the mark answers on the FIELD, so the tile only has to say it heard you */
.bs-i:hover { border-color: rgba(var(--gold-rgb),0.85);
  background: linear-gradient(180deg, rgba(86,70,42,0.72), rgba(34,27,17,0.7)); }
.bs-i:active { transform: scale(0.94); }
/* a work still going up is a promise, not a holding — dimmed until it stands */
.bs-i.raising { opacity: 0.5; }
.bs-i.raising canvas { filter: grayscale(0.5) drop-shadow(0 1px 2px rgba(0,0,0,0.7)); }
/* the level rides the corner, small enough to be a mark rather than a figure */
.bs-lv {
  position: absolute; right: -2px; bottom: -3px;
  font: 800 9px var(--font-ui); font-style: normal; line-height: 1;
  padding: 1px 2.5px; border-radius: 4px;
  color: #1a140c; background: linear-gradient(180deg, #f0d27e, #caa24e);
  box-shadow: 0 1px 2px rgba(0,0,0,0.6);
}
/* A THUMB IS NOT A CURSOR. 30px is a comfortable mouse target and half a touch
   one, and the strip lives in the corner a thumb already rests on. */
html.touch .bs-i { width: 38px; height: 38px; }
html.touch .bs-i canvas { width: 32px; height: 32px; }
/* the purse is a RANK over a figure (see THE LABEL RANK below), not a row — the
   rank carries the label and the rate, and the figure gets the whole width under it */
.ws-purse { display: block; }
/* THE NUMBER. Set at a size that can be read off the corner of the eye without
   turning to it, in the gold it counts. */
.ws-gold {
  font: 900 34px var(--font-logo); line-height: 0.95; letter-spacing: 0.5px;
  font-variant-numeric: var(--nums);
  color: var(--gold-lt);
  text-shadow: 0 2px 6px rgba(var(--shade-rgb),0.9), 0 0 18px rgba(var(--gold-rgb),0.35);
}
.ws-inc { display: flex; flex-direction: column; align-items: flex-end; line-height: 1.05; }
.ws-inc i {
  font: 800 15px var(--font-display); font-style: normal; font-variant-numeric: var(--nums);
  color: var(--ready); text-shadow: 0 0 10px rgba(var(--ready-rgb),0.45);
}
/* THE SIGN BELONGS TO THE SEAL, NOT TO THE FIGURE. It used to be an ::before on
   the number itself, which stopped painting the moment the number was restruck
   in clipped gilt: `background-clip: text` paints through the element's own text,
   and an opacity on the pseudo gives it a compositing layer of its own that the
   clip never reaches — so the glyph was drawn in the transparent fill and the
   rate silently read "50 per sec" instead of "+50 per sec". On the seal it is
   ordinary painted type and cannot be clipped away. */
.ws-inc::before {
  content: '+'; font: 800 11px var(--font-logo); line-height: 1;
  color: var(--ready-lt); opacity: 0.85; margin-right: -1px;
  text-shadow: 0 1px 2px rgba(var(--shade-rgb),0.8);
}
.ws-inc em {
  font: 600 8.5px var(--font-ui); font-style: normal; letter-spacing: 1.6px;
  text-transform: uppercase; color: var(--muted);
}
.ws-army { display: flex; flex-direction: column; gap: 3px; }
.ws-armhd { display: flex; align-items: baseline; justify-content: space-between; }
.ws-armhd span {
  font: 700 8.5px var(--font-display); letter-spacing: 2.6px; text-transform: uppercase; color: var(--gold-lt);
  text-shadow: 0 1px 2px rgba(var(--shade-rgb),0.9);
}
.ws-armhd b { font: 800 12.5px var(--font-display); font-variant-numeric: var(--nums); color: var(--ink); }
.ws-armhd u { text-decoration: none; color: var(--muted); }
/* THE CEILING, DRAWN — AS A SCALE BAR. It fills toward the cap and turns as it
   gets there, so "I am nearly capped" is a colour rather than a subtraction.
   And it is GRADUATED, because the one thing every map in the world carries in
   its corner is a scale bar with ticks on it: quarters engraved across the
   track, so the fill can be read as a fraction at a glance instead of only as
   "some of the way along". The ticks sit ON TOP of the fill — they are cut into
   the glass, not printed on the ground under it. */
.ws-armbar {
  position: relative; height: 11px; border-radius: var(--r-xs); overflow: hidden;
  background: linear-gradient(180deg, #0a0906, #14110b);
  box-shadow: var(--seat), inset 0 1px 2px rgba(var(--shade-rgb),0.7);
}
.ws-armbar i {
  display: block; height: 100%; width: 0%;
  background: linear-gradient(180deg, var(--gold-hi) 0 1px, var(--gold) 1px, var(--gilt-deep));
  box-shadow: 0 0 8px rgba(var(--gold-rgb),0.5);
  transition: width var(--t-3) var(--e-out), background var(--t-3) var(--e-out);
}
.ws-armbar::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background-image:
    repeating-linear-gradient(90deg,
      rgba(0,0,0,0.55) 0 1px, rgba(var(--lit-rgb),0.16) 1px 2px, rgba(0,0,0,0) 2px 25%);
  /* the first tick is the bar's own left rail, so start the run one quarter in */
  background-position: 25% 0; background-size: 100% 100%;
  box-shadow: inset 0 0 0 1px rgba(var(--shade-rgb),0.5), inset 0 1px 0 rgba(var(--lit-rgb),0.07);
  border-radius: inherit;
}
/* ---- THE WAR'S OWN MULTIPLIERS ----------------------------------------
   Built on .ws-armhd's split — a small-caps label on the left, the figure hard
   right — because it is the same KIND of line as "Army 12/40" and reading two
   different layouts in one cartouche is reading two panels. It differs in one
   way only: no bar under it. A bar means "of a ceiling", and these have no
   ceiling worth drawing (siege stops at 3, blades at 2.2, and neither number
   means anything as a fraction of its cap).
   The rule above is what separates it from the army block — the register break
   the purse and the muster get from their own labels, which these two rows share
   between them rather than each carrying one. */
.ws-war { display: flex; flex-direction: column; gap: 2px; padding-top: var(--sp-2); border-top: 1px solid rgba(var(--gold-rgb),0.16); }
.ws-warrow { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-2); }
.ws-warrow span {
  font: 700 8.5px var(--font-display); letter-spacing: 2.6px; text-transform: uppercase; color: var(--gold-lt);
  text-shadow: 0 1px 2px rgba(var(--shade-rgb),0.9);
}
.ws-warrow b { font: 800 12.5px var(--font-display); font-variant-numeric: var(--nums); color: var(--ink); }
/* past doubling the walls come down faster than they can be mended, and the
   figure says so in the colour everything else in this HUD says peril in */
.ws-war.hot .ws-warrow b { color: var(--peril); }
#warstate.near .ws-armbar i { background: linear-gradient(90deg, var(--gold), var(--peril)); }
#warstate.full .ws-armbar i { background: linear-gradient(90deg, var(--peril), #ff9c82); }
#warstate.full .ws-armhd b { color: var(--peril); }

/* the frame no longer holds a 230px square, so it stops reserving one */
:root { --mmw: 208px; }
@media (max-width: 1100px) { :root { --mmw: 190px; } }
@media (max-width: 820px) { :root { --mmw: 166px; } }
@media (max-height: 520px) { :root { --mmw: 144px; } }

/* THE RACK TAKES THE ROOM BACK. Its width was capped at calc(100vw - 544px)
   expressly to reserve the minimap's 248px-plus-margin on BOTH sides, so a
   centred rack could never grow under it. The map is gone and the readout in
   its place is 208px, so the reserve comes down with it — which on a 1440px
   screen is about 70px more rack, and about one more tile per rank before it
   wraps into the battlefield. */
@media (min-width: 621px) {
  #buildbar { max-width: min(calc(98vw / var(--ui)), calc(100vw / var(--ui) - (var(--mmw) + 46px) * 2), 930px); }
}

/* ══════════ THE CONDENSED ALLIANCE BOARD, IN THE DUEL'S HAND ═══════════════
   A 1v1 gets a hanging banner: the kingdom named in gilt display type, one fat
   pill of a wall bar tinted with that kingdom's own colour and glowing with it,
   and a short row of figures set large enough to read while something is on
   fire. Folded, a 4v4 got the same FACTS in a different voice entirely — a
   small caption, a thin 6px rule and figures at footnote size. Same game, same
   corner of the screen, same question being answered, two different designs.

   This is the duel's language applied to the folded board. Nothing moves and no
   markup changes: the line's name takes the display face, the wall bar becomes
   the duel's pill with the side's own tint running through it, and the tally
   under it is set in the duel's figures. Your line and theirs keep the green and
   the red they already fly, because on an eight-handed field that is the fastest
   sort an eye can make and the duel has no equivalent to borrow. */
#factions.alliance.folded .sb-sidenm {
  font: 800 15px var(--font-display); letter-spacing: 0.4px; color: #f4e6c0;
  text-shadow: 0 2px 5px rgba(var(--shade-rgb), 0.9);
}
#factions.alliance.folded .sb-side.us .sb-sidenm { color: var(--ready-lt); }
#factions.alliance.folded .sb-side:not(.us) .sb-sidenm { color: #ffb3a4; }

/* the bar the duel hangs its whole card on */
#factions.alliance.folded .sb-wallbar {
  position: relative; height: 22px; border-radius: var(--r-xl); overflow: hidden;
  margin-top: 5px;
  background: rgba(var(--shade-rgb), 0.58);
  border: 1px solid rgba(var(--shade-rgb), 0.65);
  box-shadow: inset 0 2px 6px rgba(var(--shade-rgb), 0.7);
}
#factions.alliance.folded .sb-wallbar i {
  border-radius: 0;
  background: linear-gradient(180deg, var(--ready-lt), var(--ready-dk));
  box-shadow: 0 0 14px rgba(var(--ready-rgb), 0.5);
  transition: width 0.32s var(--e-out);
}
#factions.alliance.folded .sb-side:not(.us) .sb-wallbar i {
  background: linear-gradient(180deg, #ff9a8a, #c4372c);
  box-shadow: 0 0 14px rgba(var(--peril-rgb), 0.5);
}
/* THE PERCENTAGE, ON THE BAR. The duel prints it in dark ink down the middle of
   the fill, which is the one place it cannot be missed and cannot collide with
   anything. It lives in the header here and the markup is not being rewritten
   for a restyle, so it is lifted onto the bar's line instead — same size, same
   weight, same read. */
#factions.alliance.folded .sb-sum {
  display: flex; align-items: baseline; gap: 12px;
  font: 600 12px var(--font-ui); color: #b6a889;
}
#factions.alliance.folded .sb-sum b,
#factions.alliance.folded .sb-sum s {
  font: 800 15px var(--font-display); text-decoration: none;
  color: #f2e7cd; font-variant-numeric: var(--nums);
}
#factions.alliance.folded .sb-sum s { color: var(--gold-lt); }
#factions.alliance.folded .sb-sidettl {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
}
/* and the field behind each half takes its side's tint, the way the duel's does */
#factions.alliance.folded .sb-side { position: relative; z-index: 0; }
#factions.alliance.folded .sb-side::before {
  content: ''; position: absolute; inset: -8px -12px -10px; z-index: -1; pointer-events: none;
  border-radius: var(--r-lg);
  background: linear-gradient(180deg, rgba(var(--ready-rgb), 0.10), transparent 72%);
}
#factions.alliance.folded .sb-side:not(.us)::before {
  background: linear-gradient(180deg, rgba(var(--peril-rgb), 0.12), transparent 72%);
}

/* ══════════ THE PURSE, ON THE RACK'S OWN TOP LINE ═════════════════════════
   The number that answers "can I raise any of these", set where the prices are.
   It USED to hang off the rack's top-left corner from the outside — and stopped
   being visible at all the day the rack took a max-height and an overflow, because
   a plate positioned above the top edge of a scroll box is outside the scroll box,
   and gets clipped. It now rides the rack's sticky top row, opposite the ✕ and in
   the same struck-plate furniture, so it holds its place while a long roster
   scrolls underneath it. Sized to the ✕ and lifted by the same 13px, so the two sit
   on one line in the rack's top padding without costing the tiles a pixel. */
#rackgold {
  align-self: flex-start; margin-right: 7px; transform: translateY(-13px);
  display: flex; align-items: center; gap: 6px;
  height: 22px; padding: 0 9px; pointer-events: none;
  border: 1px solid var(--edge-strong); border-radius: var(--r-md);
  background: linear-gradient(180deg, rgba(52,40,22,0.96), rgba(24,17,9,0.96));
  box-shadow: var(--sh-1), inset 0 1px 0 rgba(var(--lit-rgb), 0.14);
}
#rackgold i {
  width: 11px; height: 11px; border-radius: 50%; flex: none;
  background: radial-gradient(circle at 38% 32%, var(--gold-hi), var(--gold) 60%, var(--gold-dk));
  box-shadow: 0 0 7px rgba(var(--gold-rgb), 0.6), inset 0 -1px 2px rgba(var(--shade-rgb), 0.45);
}
/* A CELL WIDE ENOUGH FOR THE LARGEST PURSE, for the same reason the upgrade panel's
   figure has one (see `.uw-gold b`): this is rewritten on every frame of a match and
   the display face has no tabular figures, so a figure left to size itself would
   change the plate's width — and therefore the position of everything sharing its
   line — several times a second. The cell holds `888,888`: six figures of Cinzel
   plus the 3px the letter-spacing adds across them, which `ch` does not count. */
#rackgold b {
  font: 900 14px var(--font-logo); line-height: 1;
  min-width: calc(6ch + 3px); text-align: right;
  font-variant-numeric: var(--nums); letter-spacing: 0.4px;
  color: var(--gilt-body);
  text-shadow: 0 2px 5px rgba(var(--shade-rgb), 0.9);
  transition: color var(--t-2) var(--e-out), text-shadow var(--t-2) var(--e-out);
}
/* CAN I DO ANYTHING AT ALL. The purse lights the moment the cheapest tile on the
   rack is within reach and goes quiet again when it is not — so the one number
   answers the question without a single card being read. Colour AND weight of
   glow, never colour alone. */
#rackgold.can b { color: var(--gold-lt); text-shadow: 0 2px 5px rgba(var(--shade-rgb), 0.9), 0 0 16px rgba(var(--gold-rgb), 0.5); }
#rackgold.can i { box-shadow: 0 0 12px rgba(var(--gold-rgb), 0.85), inset 0 -1px 2px rgba(var(--shade-rgb), 0.45); }
/* the rack positions its own furniture (the top row, and the purse on it) */
#buildbar { position: absolute; }

/* ══════════ THE WAR STATE, STRUCK PROPERLY ════════════════════════════════
   The purse is the number a commander looks at more than any other thing in
   this game, so it is struck rather than merely set: a gilt gradient poured
   through the figures, a hairline of ink under them so they hold against any
   ground the panel floats over, and a soft bloom behind. Tabular, so a counter
   that ticks does not wobble as it counts. */
#warstate { gap: var(--sp-3); padding: var(--sp-4) var(--sp-4) var(--sp-3); }
/* ---- THE LABEL RANK ----------------------------------------------------
   Every register in the cartouche is a RANK — a small-caps name on the left, its
   annotation on the right, an engraved rule under it — and then the figure. It
   is the shape a map legend uses for every row it carries, and it has one
   property the old arrangement did not: nothing in the rank can be pushed out of
   place by the size of the figure below it. The purse used to set a 40px numeral
   and a 17px rate on a shared baseline, so at 24,910 the number grew into the
   rate and folded "per sec" onto two lines. */
.ws-rank {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3);
  padding-bottom: 3px; margin-bottom: 2px;
}
/* THE RULE UNDER A RANK IS THE MAP'S OWN LINE. The battlefield inks every
   territory border as a gilt dash with a gap of nine (see _realmBorders), so the
   cartouche rules its registers off with the same stroke rather than a plain
   hairline. It is the cheapest possible way for the panel and the ground under
   it to look like they were drawn by one hand. */

/* THE ROSE. A chart's corner carries a compass; so does this one. It is a
   watermark at a whisper — the panel is a readout first — but it is the mark
   that says at a glance which of the game's two visual languages this furniture
   belongs to. */
#warstate { position: relative; }
#warstate > * { position: relative; z-index: 1; }
/* It rides half off the corner, the way a chart's rose sits half into its own
   border, so most of what crosses the figures is the quiet outer arm rather than
   the hub — and at 6% it is texture, not an object competing with the ratio. */
#warstate::before {
  content: ''; position: absolute; right: -22px; bottom: -20px; z-index: 0;
  width: 96px; height: 96px; pointer-events: none; opacity: 0.06;
  background: var(--gold);
  clip-path: polygon(50% 0%, 56.5% 43.5%, 100% 50%, 56.5% 56.5%, 50% 100%,
    43.5% 56.5%, 0% 50%, 43.5% 43.5%);
}
#warstate::after {
  content: ''; position: absolute; right: -6px; bottom: -4px; z-index: 0;
  width: 64px; height: 64px; pointer-events: none; opacity: 0.06;
  border: 1px solid var(--gold); border-radius: 50%;
  box-shadow: inset 0 0 0 5px rgba(0,0,0,0), inset 0 0 0 6px var(--gold);
}
.ws-lbl {
  font: 700 8.5px var(--font-display); font-style: normal; letter-spacing: 2.6px;
  text-transform: uppercase; color: var(--gold-lt); white-space: nowrap;
  text-shadow: 0 1px 2px rgba(var(--shade-rgb),0.9);
}
.ws-purse { display: block; }
/* IT HAS TO READ AS GOLD, NOT AS BRONZE IN SHADOW — AND A CLIPPED RAMP COULD
   NOT PROMISE THAT.
   The figure was painted by pouring a gradient through the glyphs with
   `background-clip: text`. The trouble is WHERE that gradient lands: the paint
   box of a text-clipped background is the element's own box, and for this
   element that box is font-metric dependent — an inline `<b>` at
   `line-height: 0.9`, whose ink overflows the box it is measured by. The ink
   therefore sat at the BOTTOM of the ramp, and the bottom of the ramp was
   `--gold` — the token whose whole job is "gilt that is not being looked at".
   Sampled off the rendered panel, not one pixel of the figure got past R=196,
   while the little GOLD caption beside it — ordinary painted type — sat at a
   full 243. The biggest number on the screen was the dimmest gold in the HUD.
   (This had been "fixed" once already by tuning the stops, which is the tell:
   the stops were never the problem, the geometry was, and tuning colour to
   correct a geometry fault only moves the failure somewhere else.)
   So it is painted, not poured. Solid lit gilt, and the struck-metal read comes
   from what it is struck INTO — a hot edge under the face and the seat below —
   which is how every other gilt figure in this interface is drawn anyway. */
.ws-gold {
  font: 900 clamp(30px, 3.1vw, 40px) var(--font-logo);
  line-height: 0.9; letter-spacing: 0.5px;
  color: var(--gold-hi);
  text-shadow:
    0 1px 0 rgba(122, 94, 32, 0.9),                      /* the metal turning under its own face */
    0 2px 5px rgba(var(--shade-rgb), 0.92),              /* the seat it is struck into */
    0 0 20px rgba(var(--gilt-lit-rgb), 0.42),            /* the bloom off it */
    0 0 34px rgba(var(--gold-rgb), 0.26);
}
/* INCOME IS A RATE, AND IT READS AS ONE — and it rides in the purse's own rank,
   so it is annotation on the register rather than a second headline competing
   with the figure. One line, never two: it is struck on a seal that cannot wrap.
   Same gilt family so it belongs to the purse it fills, but in the ready-green
   that means "this is working for you", with the + carried in the mark rather
   than typed into the number. */
.ws-inc {
  display: inline-flex; flex-direction: row; align-items: baseline; gap: 3px;
  white-space: nowrap; padding: 1px 6px 2px; border-radius: var(--r-pill);
  background: linear-gradient(180deg, rgba(50,96,62,0.5), rgba(16,38,24,0.55));
  border: 1px solid rgba(var(--ready-rgb), 0.34);
  box-shadow: var(--sh-0), inset 0 1px 0 rgba(190,255,210,0.14);
}
.ws-inc i {
  font: 800 13px var(--font-logo); font-style: normal; line-height: 1;
  background: linear-gradient(180deg, var(--ready-lt), var(--ready));
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
  filter: drop-shadow(0 1px 0 rgba(var(--shade-rgb), 0.8)) drop-shadow(0 0 8px rgba(var(--ready-rgb), 0.45));
}
.ws-inc em {
  margin: 0; font: 700 7.5px var(--font-display); font-style: normal;
  letter-spacing: 1.3px; text-transform: uppercase; color: rgba(180, 232, 196, 0.9);
}
/* the same rank shape for the muster, so the two registers rhyme */
.ws-army { gap: 4px; }
.ws-armhd { padding-bottom: 3px; }
.ws-armhd b { font-size: 13.5px; }

/* ══════════ THE OMEN GLASS: EVERY RULE, ON THE FACE ═══════════════════════
   The muster's scoreboard, shrunk to live in the corner for the rest of the
   match. Same tokens, same source, same up/down colouring — so the briefing a
   commander read at the count-in and the glass they check at minute nine are
   visibly the same object saying the same thing, and neither can drift from the
   rules the server is running. */
/* THE RULES, AS A LEGEND — A GRID, NOT A WRAPPING ROW.
   These were inline-flex tokens with a min-width in a flex-wrap row, which gives
   a different shape for every hand it is dealt: three rules wrapped 2-then-1 and
   read as a mistake, and four rules simply overflowed the cartouche and hung out
   over the battlefield. Two fixed columns cost nothing and are the same shape
   every match — and the two rows a four-rule omen needs are exactly two rows.
   Each token is a two-line stack — the mark and the figure over the stat it
   moves — because the label gets the whole cell that way. Set on one line, a
   96px column had to ellipsise REACH into "REAC…", and a rule you cannot read
   the name of is not a rule you can play around. */

/* WHO THE RULE LANDS ON, when it does not land on everyone. Set quieter and
   smaller than the stat, because it qualifies the stat rather than competing
   with it — but present, because without it a scoped pair reads as a
   contradiction. A scoped token gives up the tight bottom padding to make room. */

/* DIRECTION IS THE FASTEST SORT AN EYE CAN DO, and it is carried in the border,
   the figure AND a mark — never hue alone. The mark is what a colour-blind
   commander reads, and it is what makes a column of these scannable without
   reading a single number. */

/* the clock and the quiet-sky note are STATEMENTS, not rules — they take the
   full width of the legend rather than pretending to be a fourth modifier */


/* ══════════ CAN I RAISE THIS ═══════════════════════════════════════════════
   Every tile in the rack looked identical whether it was within reach or not,
   so the only way to find out was to click and be refused. Twelve prices against
   one purse is arithmetic a commander should never be doing mid-fight.

   Signalled three ways, never by hue alone: the price plate LIGHTS when you can
   raise it (a struck green seal, the same --ready that means affordable
   everywhere else in this game), the whole tile goes quiet when you cannot, and
   the unaffordable price is set in the muted ink rather than the gilt. A
   colour-blind commander reads the brightness and the plate's fill; everyone
   reads the green. */

/* AFFORDABLE — the seal is struck and lit */
.build.ready .bupg, .bupg.go {
  background: linear-gradient(180deg, #2f7046, #14361f);
  border-color: var(--ready-dk);
  box-shadow: inset 0 1px 0 rgba(190,255,210,0.28), inset 0 -3px 8px rgba(var(--shade-rgb),0.45),
    0 0 12px rgba(var(--ready-rgb), 0.34);
}
/* the figure is gilt too — it is money, not a status light */
.build.ready .bupgc { color: var(--gold-hi); text-shadow: 0 1px 2px rgba(var(--shade-rgb),0.95); }
/* THE COIN IS ALWAYS GOLD. It used to be restruck in the ready-green whenever
   the plate under it went green — so the one mark on the tile that means "this
   is a price" changed metal depending on your purse, and on a rack where most
   tiles are affordable the money on screen was green. Gold is what gold costs,
   in every state; the PLATE is what carries whether you can pay for it. */
.build.ready .bupgc::before {
  box-shadow: 0 0 7px rgba(var(--gilt-lit-rgb), 0.9), inset 0 -1px 1px rgba(0,0,0,0.45),
    inset 0 1px 1px rgba(255,255,255,0.55), 0 0 0 1px rgba(20,40,26,0.45);
}

/* OUT OF REACH — the tile steps back rather than being switched off. It is still
   legible, still hoverable and still answers a tooltip, because "what does this
   cost and what does it do" is exactly the question being asked about a thing
   you cannot yet afford. It simply stops looking armed. */
/* ---- IN REACH: A TILE YOU WANT TO PRESS -------------------------------
   The rack had one look and it was the out-of-reach one — see the `isidle`
   note in main.js — so a troop you could buy this second was drawn in the
   same washed grey as one you were still saving for, and nothing in the row
   invited a click. An affordable tile is a FINISHED object now: the portrait
   at full colour with a touch of lift, the card wearing its own category
   colour rather than a hint of it, and a warm gilt rim so the row reads as a
   row of things that are yours to spend. */
.build.ready { filter: none; opacity: 1; }
.build.ready .bicon {
  filter: saturate(1.14) contrast(1.06) brightness(1.06) drop-shadow(0 2px 3px rgba(var(--shade-rgb),0.55));
  box-shadow: inset 0 3px 7px rgba(0,0,0,0.55), inset 0 0 0 2px var(--gold-lt),
    0 0 14px var(--cat, rgba(var(--gold-rgb),0.55));
}
.build.ready::after { opacity: 0.32; }               /* the category wash, properly lit */
.build.ready .bn { color: var(--gold-hi); text-shadow: 0 1px 2px rgba(var(--shade-rgb),0.9); }
.build.ready:hover::after { opacity: 0.46; }
.build.ready:hover .bicon { filter: saturate(1.2) contrast(1.08) brightness(1.12); }

/* OUT OF REACH — the tile steps back rather than being switched off. */
.build:not(.ready) { opacity: 0.62; filter: saturate(0.55); }
.build:not(.ready) .bicon { filter: grayscale(0.5) brightness(0.82); }
.build:not(.ready) .bupg {
  background: linear-gradient(180deg, #241c12, #130d06);
  border-color: rgba(var(--gold-rgb), 0.22);
  box-shadow: inset 0 -2px 6px rgba(var(--shade-rgb), 0.5);
}
.build:not(.ready) .bupgc { color: var(--gilt-body); }
.build:not(.ready) .bupgc::before { background: radial-gradient(circle at 38% 32%, #6a5c3c, #3a3122); box-shadow: none; }
.build:not(.ready) .bupgw { opacity: 0.45; }
/* hover still lifts it — you are allowed to look at what you are saving for */
.build:not(.ready):hover { opacity: 0.85; filter: saturate(0.8); }

/* ══════════ NO LETTERBOX ═══════════════════════════════════════════════════
   The muster used to close two black bars over the top and bottom of the field
   to make the briefing read as a cutscene. It reads as a black slab: the bars
   are opaque, they sit over the live battlefield the camera is pushing across —
   the whole point of the sequence — and on a short window the top bar alone ate
   a fifth of the view. The acts already carry the ceremony (the wash, the rule,
   the sky arriving, the count-in); they do not need the screen masked to be
   taken seriously.

   The bars are kept in the DOM and simply not drawn, because the CHARGE beat
   lifts them out of the stage and puts them back to fly them open — removing
   the elements would need that sequence rewritten for no gain. */
#battlecountdown .cbar { display: none !important; }

/* ═════════════════ THE GAME IN A HAND ══════════════════════════════════════
   Everything a touch screen needs and a desktop never notices. The canvas
   declares touch-action:none because without it the browser owns every drag as
   a candidate scroll/zoom gesture, cancels the pointer stream mid-move, and no
   pan or pinch of ours can ever complete — this single line is the difference
   between "the input layer works on a phone" and "taps sometimes register". */
#game { touch-action: none; }
/* ---- THE FILMIC POP, ON THE COMPOSITOR AND ONLY WHEN IT IS WANTED ---------
   This was Renderer._pop: the canvas composited onto ITSELF under `overlay`,
   every frame at every tier, which is the shape that takes a canvas off the GPU
   in Safari. A handset reported 1.3ms of drawing inside a 27.8ms frame — about
   111 megapixels a second, two orders off a GPU. Removing it took that phone
   from 37fps to 56.

   It is applied here instead, by the compositor, where it costs nothing. And
   only under `.flat`, which the renderer sets while the mood passes are off:
   with the grade, atmosphere, weather and vignette running, the curve moves the
   finished picture by half a percent and layering it on top would push contrast
   22% and saturation 31% past anything this game has looked like. With them off
   — the cheapest tiers — it is worth 12-15% of contrast, which is the flatness
   it was written to answer.

   The fit: overlay(x,x) at alpha 0.36 sends 0.25 to 0.205, leaves 0.5 alone and
   sends 0.75 to 0.795 — an S-curve of slope ~1.36 through the midpoint, plus the
   saturation lift overlay carries. */
html.flat #game { filter: contrast(1.22) saturate(1.12); }
html { -webkit-tap-highlight-color: transparent; }

/* (a finger is a blunter instrument than a cursor — but the corner chips now take
   that from `--chip` at the top of the rail, size and stacking together, so the
   three hand-tuned offsets that used to live here are gone with the drift) */

/* ---- the turn request -----------------------------------------------------
   A battlefield is wide. On a small screen held upright the topbar, the rack
   and the readout stack over what little ground is left — so instead of
   shipping a broken portrait layout, the page says what it needs. Landscape
   never sees this element; neither does any desktop, however narrow its
   window: it takes a coarse pointer AND portrait AND a small screen. */
#rotatehint {
  display: none;
  position: fixed; inset: 0; z-index: 300;
  flex-direction: column; align-items: center; justify-content: center; gap: 10px;
  text-align: center; padding: 30px;
  background: radial-gradient(120% 90% at 50% 30%, #241c10, #0c0a06 78%);
  color: var(--gold-lt); font-family: var(--font-display);
}
#rotatehint b { font-size: 22px; letter-spacing: 0.4px; }
#rotatehint span { font-size: 14px; color: var(--muted); font-family: var(--font-ui); }
#rotatehint .rh-phone {
  font-size: 54px; line-height: 1; color: var(--gold-lt);
  animation: rhTurn 2.2s var(--e-out) infinite;
}
@keyframes rhTurn {
  0%, 20% { transform: rotate(0); }
  55%, 100% { transform: rotate(90deg); }
}
@media (pointer: coarse) and (orientation: portrait) and (max-width: 620px) {
  #rotatehint { display: flex; }
}
@media (prefers-reduced-motion: reduce) { #rotatehint .rh-phone { animation: none; } }

/* ═══════════════════════════════════════════════════════════════════════════
                                  THE PHONE
   ═══════════════════════════════════════════════════════════════════════════

   NOT THE DESKTOP LAYOUT, SMALLER. That is what was here before and it is the
   thing that could never be made good: --ui shrank a 1600×900 HUD by a fixed
   factor onto a 667×375 screen, so the scorecard took a third of the picture,
   the cartouche took half the height, four build tiles stood in for a roster of
   twenty, and every control landed somewhere between "small" and "missable".
   A scale cannot fix that, because the two things a phone needs pull opposite
   ways: the FURNITURE has to get out of the way (a hand-held screen has a
   sixth of a desktop's area and the battlefield is the game), while the
   CONTROLS have to get bigger (a fingertip is 9mm; a cursor is one pixel).

   So a phone gets --ui: 1 — one CSS px is one px on the glass, which is the
   only way to promise a 44px target is actually 44px — and its own layout:

     · ONE TOP BAND, 40-46px, carrying everything that is standing furniture:
       the purse and muster on the left, the scorecard in the middle, the
       utility chips on the right. Nothing else ever floats over the ground.
     · THE WHOLE MIDDLE IS THE BATTLEFIELD. No panel is allowed into it that
       is not a transient answer to something the commander just pressed.
     · THE BOTTOM IS THUMBS. Build and Upgrades sit centre-bottom at a real
       touch size, the rack opens as a strip directly above them, and the two
       corners take one chip each — where a thumb already rests in landscape.

   Three width buckets (.ph-sm / .ph-md / .ph-lg) and a height flag
   (.ph-short), set by device.js. They exist because 667×375 and 932×430 are
   not the same room: the narrow one has to give up a name, a stat, a rank
   label that the wide one can keep, and saying which in a class beats a dozen
   width queries that each answer for one rule.
   ═══════════════════════════════════════════════════════════════════════════ */

html.phone {
  /* the one number the whole phone layout is measured from: the height of the
     standing top band. Everything that has to clear it says so in these terms
     rather than repeating a magic number. */
  --band: 44px;
  --tap: 44px;              /* the smallest thing a finger may be asked to hit */
  --pad-x: 10px;            /* the gutter from the screen's own edge */
  --edge-l: calc(var(--pad-x) + var(--sal));
  --edge-r: calc(var(--pad-x) + var(--sar));
  --edge-t: calc(6px + var(--sat));
  --edge-b: calc(8px + var(--sab));
}
html.phone.ph-short { --band: 38px; --tap: 42px; }

/* A LIVE HUD IS NOT A MENU. Several chips (sound, score) stand on both, and
   where they belong is different on each: on the menu they sit in a quiet
   corner, in a battle they join the top band. `.inmatch` is set on <html> for
   exactly the span of a match (see enterMatch / endMatchToMenu). */

/* ---------------------------------------------------------------- the band */
/* The topbar stops being a three-column grid that centres one card and becomes
   the band itself: left rail, scorecard, right rail, all on one row, all
   vertically centred, so the three never disagree about where the top of the
   screen is. */
html.phone #topbar {
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  padding: var(--edge-t) var(--edge-r) 0 var(--edge-l);
  gap: 6px;
  height: calc(var(--band) + var(--sat));
}

/* ---- the scorecard: a strip, not a hanging banner ----
   The banner's whole construction — the rod, the finials, the notched hem, the
   sheen — is 22px of ornament above and below a 24px bar, which is a fine trade
   on a 900px-tall screen and a terrible one on a 375px one. On a phone the card
   keeps the thing it is FOR (two keeps draining toward a clock, the win
   condition, unmistakable) and gives up the furniture around it. */
/* Written through `#topbar >` to outrank the ≤1199px block, which drops the card
   onto a rank of its own at `width: min(100%, 640px)` — the right answer for a
   narrow desktop window and the wrong one here, where a full-width card lands
   underneath the purse chip and the utility rail. It is taken out of the grid
   flow and centred on the screen instead, so the three occupants of the band
   cannot push each other about at any width.
   Centred by `left/right: 0 + margin-inline: auto` rather than by a translate:
   the card's entrance (hudDropMid) is a `both`-filled animation ending at
   `transform: none`, which silently wins over any transform set here. */
html.phone #topbar > #factions,
html.phone #topbar > #factions.scorecard {
  position: absolute; top: var(--edge-t); left: 0; right: 0; margin: 0 auto;
  grid-row: 1; grid-column: 1 / -1;
  /* THE WIDTH RESERVES THE OTHER TWO OCCUPANTS OF THE BAND. The purse chip on
     the left and the four utility chips on the right are both fixed to the
     screen's edges, so a card sized off the viewport alone slides under them the
     moment the viewport is narrow — which on the smallest phone is always. It is
     sized off what is LEFT: the screen less the two clusters, capped so it never
     grows past what a two-keep readout needs. */
  width: clamp(180px, calc(100vw - 396px), 330px);
  min-width: 0; max-width: none; filter: none; justify-self: center;
}
html.phone #factions.scorecard::before,      /* the rod */
html.phone #factions.scorecard::after,       /* its left finial */
html.phone .sc-rodcap,                       /* its right finial */
html.phone .sc-card::after {                 /* the travelling sheen */
  display: none;
}
html.phone .sc-card {
  --notch: 0px;
  gap: 7px; padding: 4px 9px 5px;
  clip-path: none; border-radius: var(--r-md);
  border: 1px solid rgba(var(--gold-rgb), 0.34);
  background: linear-gradient(170deg, rgba(44,36,23,0.9), rgba(15,12,8,0.93));
}
html.phone .sc-side { gap: 2px; }
html.phone .sc-side::before { display: none; }   /* the kingdom's colour pool */
html.phone .sc-id { gap: 5px; }
html.phone .sc-crest { width: 15px; height: 17px; }
html.phone .sc-name { font-size: 10px; letter-spacing: 0.2px; max-width: 96px; }
html.phone .sc-bar { height: 13px; border-radius: var(--r-md); }
html.phone .sc-bar s { font-size: 9.5px; letter-spacing: 0.2px; }
html.phone .sc-clock { font-size: 15px; letter-spacing: 0.5px; }
html.phone .sc-vs { display: none; }             /* "RAZE THE KEEP" is the briefing's line */
html.phone .sc-mid { padding: 0 2px; }
/* The three tallies — army, spawners, kills — are a second line under a bar
   that is already the answer to "am I winning". The purse chip carries the
   army count on the left of the same band, so on a narrow phone they go. */
html.phone.ph-sm .sc-stats, html.phone.ph-short .sc-stats { display: none; }
html.phone .sc-stats { gap: 9px; font-size: 10px; }
html.phone .sc-stats b { font-size: 11.5px; margin-left: 3px; }
html.phone.ph-sm .sc-name { max-width: 84px; font-size: 9.5px; }

/* ---- the alliance board (4v4), folded by default on a phone ----
   Eight rows is a screen's worth of list on a machine whose whole screen is
   375px tall. The strip — two lines, their keeps and their wall — is what the
   board folds to on every machine, and on a phone that is the resting state. */
/* An alliance war puts EIGHT keeps on the board, in two columns either side of
   the clock — about 200px of panel on a desktop and, unchanged, more than half
   of a 375px phone. It is wider than the duel's card because it has twice as
   much to say, and everything inside it is cut to the two figures that decide an
   alliance war: how many keeps a line still has, and how much wall is left
   across them. The eight rows are still one press away, and they scroll. */
html.phone #topbar > #factions.alliance { width: clamp(230px, calc(100vw - 392px), 440px); }
html.phone #factions.alliance::before,       /* the rod */
html.phone #factions.alliance::after,        /* its left finial */
html.phone #factions.alliance .sb-rodcap {   /* its right one */
  display: none;
}
html.phone .sb-card {
  --notch: 0px; clip-path: none; border-radius: var(--r-md);
  grid-template-columns: 1fr 54px 1fr;
  padding: 4px 8px 5px; backdrop-filter: none;
  border: 1px solid rgba(var(--gold-rgb), 0.34);
}
html.phone #factions.alliance .sb-side { padding: 0 6px; gap: 2px; }
html.phone #factions.alliance .sb-side + .sb-side { padding-left: 7px; }
html.phone .sb-sidettl { font-size: 8.5px; letter-spacing: 0.5px; gap: 4px; }
html.phone #factions.alliance.folded .sb-sidettl { font-size: 9.5px; }
html.phone .sb-sidenm { font-size: 8.5px; }
html.phone .sb-sum { gap: 3px; }
html.phone .sb-sum b { font-size: 11px; min-width: 24px; }
html.phone .sb-sum s { font-size: 10px; width: 24px; }
html.phone #factions.alliance.folded .sb-sum b { font-size: 13px; }
html.phone #factions.alliance.folded .sb-sum s { font-size: 11px; }
/* kills is the tally of the fighting, not of the win condition — the first thing
   to go when the line has to fit in half the width */
html.phone.ph-sm #factions.alliance .sb-sum b.sb-kills { display: none; }
html.phone .sb-wallbar { height: 5px; }
html.phone .sb-mid { width: 54px; padding: 0 2px; }
html.phone .sb-clock { font-size: 14px; }
html.phone .sb-phase { font-size: 7px; height: 9px; line-height: 9px; }
/* unfolded: four rows a column, scrolling, at a height that still leaves a
   battlefield under it */
html.phone .sb-rows { max-height: calc(var(--vh) * 26); overflow-y: auto; gap: 2px; }
html.phone #factions.alliance .sb-row { padding: 1px 2px; gap: 4px; }
html.phone .sb-name { font-size: 9.5px; }
html.phone #factions.alliance .fcrestwrap { width: 18px; height: 18px; }
html.phone #factions.alliance .fcrest { width: 15px; height: 17px; }
html.phone #factions.alliance .sb-bar { height: 7px; }
html.phone #factions.alliance .sb-barrow .sb-score { width: 24px; font-size: 9px; }
html.phone.ph-sm #factions.alliance .sb-k { display: none; }
html.phone .sb-tog { width: 30px; height: 18px; }

/* ---- the left of the band: the purse and the muster ----
   The cartouche was a 208×190 vellum plate pinned to the bottom-right corner —
   a fifth of the screen, permanently, for two numbers and a condition. Here it
   is a chip on the band: the purse with its rate, the army with its cap, and
   the omen as a mark you can press. Same three registers, one row. */
/* NO BACKDROP BLUR ON A PHONE. Three of these panels run a 10px backdrop blur,
   which on a hand-held GPU is a full-screen read-back per panel per frame while
   the battlefield underneath is the thing that has to hold 60fps — and it costs
   something worse than time: a `backdrop-filter` element becomes the containing
   block for `position: fixed` descendants, so the emote button, which is fixed
   to the bottom-right corner of the SCREEN, was being positioned against the
   180px purse chip it happens to live inside and ended up off the top edge. */
html.phone #factions, html.phone #mapframe, html.phone #selpanel,
html.phone #tip, html.phone .build, html.phone .sc-card {
  backdrop-filter: none;
}
/* …and the entrance animations go with them, for the second half of the same
   reason. `hudRise`/`hudDrop` are `both`-filled and end on `transform: none`,
   which Chrome keeps as an IDENTITY MATRIX rather than as no transform at all —
   and an element with a transform is also a containing block for fixed
   descendants. So the purse chip went on capturing the emote button after the
   blur was gone. A phone HUD that slides in from three directions on a 375px
   screen is motion for its own sake anyway; it simply arrives. */
html.phone #hud:not(.hidden) #mapframe,
html.phone #hud:not(.hidden) #factions,
html.phone #hud:not(.hidden) #bottombtns,
html.phone #hud:not(.hidden) #buildbar { animation: hudFade 0.3s var(--e-out) both; }

html.phone #mapframe {
  position: absolute; z-index: 8;
  top: var(--edge-t); left: var(--edge-l); right: auto; bottom: auto;
  display: flex; align-items: center; gap: 7px;
  padding: 3px 8px; border-width: 1px; border-radius: var(--r-md);
  height: var(--band); max-width: 192px; overflow: hidden;
  box-shadow: var(--sh-2), inset 0 0 0 1px rgba(0,0,0,0.5);
}
html.phone #mapframe::before { display: none; }   /* the four corner lozenges */
html.phone #warstate {
  width: auto; flex-direction: row; align-items: center; gap: 9px;
  padding: 0; background: none;
}
html.phone #warstate::before, html.phone #warstate::after { display: none; }  /* the ghost marks */
html.phone .ws-purse { display: flex; align-items: baseline; gap: 5px; }
html.phone .ws-rank { display: contents; }        /* the label rank collapses to a row */
html.phone .ws-lbl { display: none; }             /* the word GOLD — the coin's colour says it */
html.phone .ws-gold { font-size: 19px; line-height: 1; letter-spacing: 0; text-shadow: 0 1px 0 rgba(122,94,32,0.9), 0 2px 4px rgba(var(--shade-rgb),0.9); }
html.phone .ws-inc { padding: 0 5px 1px; }
html.phone .ws-inc i { font-size: 10.5px; }
html.phone .ws-inc em { display: none; }          /* "PER SEC" — a rate beside a purse is a rate */
html.phone .ws-army { flex-direction: column; gap: 2px; min-width: 56px; justify-content: center; }
html.phone .ws-armhd { padding-bottom: 0; gap: 5px; border: 0; }
html.phone .ws-armhd span { font-size: 7px; letter-spacing: 1.1px; }
html.phone .ws-armhd b { font-size: 11px; }
/* 5px of bar with the desktop's engraved tick marks over it is a black smudge —
   the ticks are 1px lines spaced a quarter of the width apart, drawn for an 11px
   rail. The rail keeps its fill and gives up the engraving. */
html.phone .ws-armbar { height: 5px; }
html.phone .ws-armbar::after { background-image: none; }
html.phone.ph-sm .ws-army { min-width: 50px; }

/* ---- the omen, as a mark you press ----
   On a desktop it is a named strip with its rules laid out as a legend and the
   prose one hover away. A phone has neither the width for the strip nor a
   hover to open it with, so it is the sky's own mark in a roundel — pressed,
   it drops the full legend below the band, which is where a phone puts a thing
   it just asked for. */

/* pressed: the legend drops out of the band, anchored to the screen's left rail
   rather than to a 26px roundel, so a four-rule omen has somewhere to be */


/* ---- the right of the band: the utility chips ----
   Sound, score, the controls reference and Surrender were four objects in
   three different corners, two of them stacked up the left edge exactly where
   a landscape grip puts a thumb. They are one row on the band, out of both
   thumb zones, at a size a finger can actually hit. */
/* ONE CORNER, EVERY SCREEN. The two switches used to stand at the bottom-left of
   the viewport on menu and battlefield alike — the one corner a landscape grip
   puts a thumb in, and on the muster and the Armory they sat directly on top of
   the roster and the shelf. They are top-right now, wherever you are, so there
   is one place to look for them and neither hand is resting on them. The
   headers below reserve the width. */
html.phone #sfxtoggle, html.phone #musictoggle, html.phone #keybtn {
  position: fixed; bottom: auto; top: var(--edge-t); left: auto; z-index: 46;
  width: 36px; border-radius: var(--r-md); font-size: 16px;
}
html.phone #sfxtoggle, html.phone #musictoggle { height: 36px; }
html.phone.inmatch #sfxtoggle, html.phone.inmatch #musictoggle,
html.phone.inmatch #keybtn { height: var(--band); }
html.phone #sfxtoggle   { right: var(--edge-r); }
html.phone #musictoggle { right: calc(var(--edge-r) + 40px); }
/* …and in a battle the reference joins them, with Surrender past it */
html.phone.inmatch #keybtn      { right: var(--edge-r); }
html.phone.inmatch #sfxtoggle   { right: calc(var(--edge-r) + 40px); }
html.phone.inmatch #musictoggle { right: calc(var(--edge-r) + 80px); }
html.phone.inmatch #leavebtn {
  position: fixed; top: var(--edge-t); right: calc(var(--edge-r) + 120px); z-index: 46;
  height: var(--band); min-width: 36px; padding: 0 9px;
  display: flex; align-items: center; justify-content: center;
  font-size: 11px; letter-spacing: 0.3px; border-radius: var(--r-md);
}
/* SURRENDER IS THE FLAG ALONE UNTIL IT IS ARMED. It already takes two presses
   on every machine (see resetSurrender in main.js) — what changes here is that
   on a phone it is the one control on the band that cannot be undone, sitting a
   thumb-width from three that are harmless, so it does not spend its resting
   state advertising itself. The banner mark alone; press it once and it becomes
   the full red "Confirm surrender?" it has always been, which is both the
   confirmation and the only time it is big enough to hit by accident.
   It lapses back on its own after four seconds. */
html.phone.inmatch #leavebtn { font-size: 0; padding: 0 9px; }
html.phone.inmatch #leavebtn .ic { font-size: 16px; }
html.phone.inmatch #leavebtn.arm {
  font-size: 10.5px; padding: 0 11px; letter-spacing: 0.2px;
  animation: leaveArm 1.1s ease-in-out infinite;
}
@keyframes leaveArm { 0%, 100% { box-shadow: 0 0 0 1px rgba(255,143,125,0.35); } 50% { box-shadow: 0 0 0 1px rgba(255,143,125,0.5), 0 0 14px 2px rgba(var(--peril-rgb), 0.55); } }


/* ------------------------------------------------------- the bottom, thumbs */
html.phone #bottombtns { bottom: var(--edge-b); gap: 8px; }
html.phone #buildbtn {
  min-height: var(--tap); padding: 0 24px; font-size: 15px; letter-spacing: 1px;
  border-radius: var(--r-xl);
}
html.phone #upgradebtn {
  min-height: var(--tap); padding: 0 16px; font-size: 13px; letter-spacing: 0.6px;
  border-radius: var(--r-xl);
}
html.phone.ph-sm #buildbtn { padding: 0 18px; font-size: 14px; }
html.phone.ph-sm #upgradebtn { padding: 0 13px; font-size: 12px; }
/* a press has to be FELT when there is no cursor to show it was received */
html.phone #buildbtn:active, html.phone #upgradebtn:active { transform: scale(0.96); filter: brightness(1.15); }
html.phone #buildbtn:hover, html.phone #upgradebtn:hover { transform: none; }

/* ---- the emote wheel: bottom-right chip, sheet above it ----
   It is a child of the map frame, which on a phone is up on the band — but
   speaking to your opponent is a bottom-corner action, so it goes fixed. */
html.phone #emotebtn {
  position: fixed; top: auto; right: var(--edge-r); bottom: var(--edge-b);
  width: var(--tap); height: var(--tap); z-index: 46;
}
/* …and march-all rides directly above it rather than beside it. The bottom rail
   is thumb territory and it is already the width of the emote chip plus the
   sheet's own edge; stacking keeps both on the same thumb arc instead of pushing
   one of them into the middle of the screen. A full tap target, like everything
   else a finger is asked to hit. */
html.phone #marchallbtn {
  position: fixed; top: auto; right: var(--edge-r);
  bottom: calc(var(--edge-b) + var(--tap) + 8px);
  width: var(--tap); height: var(--tap); z-index: 46;
}
html.phone #marchallbtn .ic { width: 21px; height: 21px; }
/* SEVEN ACROSS, so all twenty fit in three ranks with nothing below the fold.
   Five across put the last four emotes in a fourth rank inside a scroller, on a
   sheet a commander opens for two seconds mid-battle — a scroll nobody will
   find, hiding a fifth of the vocabulary. */
html.phone #emotewheel {
  position: fixed; right: var(--edge-r); left: var(--edge-l); width: auto;
  bottom: calc(var(--edge-b) + var(--tap) + 8px);
  grid-template-columns: repeat(7, 1fr);
  max-height: calc(var(--vh) * 100 - var(--band) - var(--tap) - 30px);
  overflow-y: auto; gap: 5px; padding: 8px;
}
html.phone.ph-lg #emotewheel { grid-template-columns: repeat(10, 1fr); }
html.phone .emtile { min-height: 46px; padding: 5px 2px; }
html.phone .emtile i { font-size: 8.5px; }

/* ---------------------------------------------------------------- the rack */
/* ONE ROW THAT SCROLLS SIDEWAYS, not a wrapped block that scrolls down. A
   wrapped rack on a phone is a 46vh slab over the battlefield you are choosing
   where to build on — and the roster is an ordered list (cheapest first), so a
   sideways strip is both the smaller object and the truer shape. Snap points,
   because a strip you have to land precisely on is a strip you fight. */
/* THE DESKTOP TRIM IS NOT TAKEN ON A PHONE. The rack there is already cut to the
   hand — the tiles are the fingertip targets everything else on the screen is sized
   around — so the quarter the desktop takes off would come off the only thing you
   can hit. It rides the same 10% the desktop rack just took, from its own baseline
   rather than from the trimmed one. */
html.phone { --rack: 1.1; }
html.phone #buildbar {
  flex-wrap: nowrap; align-content: center; justify-content: flex-start;
  /* as wide as the roster it holds, up to the screen — a strip that is always
     full width is a slab of empty plate beside five cards on a short roster */
  left: var(--edge-l); right: var(--edge-r); margin-inline: auto;
  width: fit-content;
  max-width: calc(100vw - var(--edge-l) - var(--edge-r));
  /* Divided by --rack for the same reason the desktop rule divides its own offset:
     the bar is zoomed by --rack, and a zoom scales the offset with it, so an
     undivided number walks up the screen every time the rack is resized. It was
     undivided because --rack was exactly 1 here, which made cancelling a no-op and
     hid the omission. The thumb-reach anchor — the edge, a tap target and 8px — is
     the same distance off the bottom at any rack scale. */
  bottom: calc((var(--edge-b) + var(--tap) + 8px) / var(--rack));
  max-height: none; height: auto;
  overflow-x: auto; overflow-y: hidden;
  padding: 6px 6px 7px; gap: 6px;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  background: linear-gradient(180deg, rgba(8,6,4,0.55), rgba(8,6,4,0.78));
  border: 1px solid rgba(var(--gold-rgb), 0.28); border-radius: var(--r-xl);
  box-shadow: var(--sh-3);
}
html.phone #buildbar::-webkit-scrollbar { display: none; }
/* THE ✕ IS A COLUMN OF THE STRIP, NOT A LINE ABOVE IT. On the desktop rack it is
   a `width: 100%`, zero-height sticky row that takes a line of its own between
   the wrapped ranks — which in a single NOWRAP row is not a line, it is 100% of
   the strip's width shoved in front of the first tile, and the whole roster went
   with it to the right-hand half of the screen. Here it is a real 30px column
   pinned to the strip's right edge, so it stays under the thumb however far the
   roster is scrolled. */
html.phone .brackclose {
  order: 99; position: sticky; right: 0; top: auto; z-index: 8;
  flex: none; width: 30px; height: auto; align-self: stretch;
  align-items: center; justify-content: center; pointer-events: auto;
  background: linear-gradient(90deg, rgba(8,6,4,0), rgba(10,8,5,0.9) 34%);
}
html.phone .brackclose button {
  transform: none; width: 26px; height: 26px; font-size: 13px;
}
html.phone .brackclose button:hover { transform: none; }
html.phone .build {
  width: 78px; padding: 5px 4px 5px; gap: 1px; scroll-snap-align: center;
  border-radius: var(--r-md);
}
html.phone .bicon { width: 38px; height: 38px; }
html.phone .bn { font-size: 10px; }
html.phone .bk { display: none; }                 /* there is no keyboard to press it on */
html.phone .bdots i { width: 5px; height: 5px; }
html.phone .bcd { margin-top: 0; }
html.phone .bcdbar { height: 4px; }
html.phone .bcdnum { font-size: 9px; min-width: 24px; }
html.phone .bupg { margin-top: 3px; padding: 3px 2px; }
html.phone .bupgw { font-size: 8.5px; }
html.phone .bupgc { font-size: 11px; }
html.phone .build:hover { transform: none; }
html.phone .build:active { transform: scale(0.95); }
html.phone.ph-short .build { width: 72px; }
html.phone.ph-short .bicon { width: 34px; height: 34px; }
/* the purse pinned to the rack: on a phone it is already on the band two
   inches away, and the strip has no room to spare */
html.phone #rackgold { display: none !important; }

/* ---- the Upgrades panel: a sheet ---- */
html.phone .uw-card {
  /* the same "as wide as what is in it" rule as the desktop card, against the
     phone's own edges — a keep and nothing else should not be a full-width sheet
     over a 390px-tall battlefield either */
  width: fit-content;
  min-width: min(230px, calc(100vw - var(--edge-l) - var(--edge-r)));
  max-width: calc(100vw - var(--edge-l) - var(--edge-r));
  max-height: calc(var(--vh) * 100 - var(--band) - var(--tap) - 34px);
  border-radius: var(--r-xl);
}
html.phone #upgradewin { left: 50%; right: auto; bottom: calc(100% + 8px); }
html.phone .uw-head { padding: 9px 12px 8px; gap: 6px 8px; }

html.phone .uw-strap { display: none; }
html.phone .uw-list { padding: 8px 10px 10px; gap: 6px; }
html.phone .uw-row { gap: 9px; padding: 7px 9px; }
html.phone .uw-spr { min-height: 52px; max-height: 62px; }
html.phone .uw-name { font-size: 14px; }
html.phone .uw-gains { gap: 4px 5px; margin-top: 4px; }
html.phone .uw-g { font-size: 11px; padding: 2px 7px; }
/* A LITTLE WIDER, AND THE PRICE MAY SHRINK. The row inherited the desk's 186px
   while the phone gave the price button a 96px floor — and a floor is a floor, so
   "🏰 Ramparts ⛁ 550" at 13px took the ~140px it wanted, the AUTO stud took its 40
   beside it, and the pair overran the row by a few pixels: the standing-order
   toggle was clipped in half against the card's edge. The row carries the width
   the two controls actually need (the card is fit-content now, so it simply grows
   with them), and the price gives ground first if a longer name still outruns it. */
html.phone .uw-row { width: 208px; }
html.phone .uw-up { min-width: 0; min-height: var(--tap); padding: 7px 10px; font-size: 13px; }
html.phone .uw-x { width: var(--tap); height: var(--tap); font-size: 19px; }
html.phone .uw-pips i { width: 9px; height: 9px; }

/* ------------------------------------------------- transient panels & cues */
/* Everything below is something the game says back. On a phone they all obey
   the same two rules: stay out of the band, and never be wider than the screen. */
/* the reference sheet: two groups abreast and nothing below the fold */
html.phone .km-card { padding: 12px 14px 14px; max-height: calc(var(--vh) * 100 - 16px); }
html.phone .km-title { font-size: 16px; }
html.phone .km-head { margin-bottom: 8px; }
html.phone .km-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 14px; }
html.phone .km-grp-ttl { font-size: 10.5px; margin-bottom: 3px; }
html.phone .km-row { font-size: 11px; padding: 2px 0; }
html.phone .km-row kbd { font-size: 10px; padding: 1px 5px; }
html.phone .km-gest .km-row { grid-template-columns: minmax(0, 7.6em) minmax(0, 1fr); gap: 3px 7px; }
/* the readout is defined once next to .km-card; a phone only tightens it */
html.phone .km-diag { margin-top: 9px; padding-top: 7px; font-size: 9.5px; line-height: 1.5; }

html.phone #tip {
  width: min(240px, calc(100vw - 24px)); font-size: 11px;
}
html.phone #tip .tart { width: 46px; height: 46px; }
html.phone #tip .tname { font-size: 15px; }
html.phone #tip .tgrid { grid-template-columns: repeat(3, 1fr); }
html.phone #tip .tcell b { font-size: 12px; }
/* THE SPAWNER WIDGET. It rests at 82% opacity and comes to full on hover — which
   on a touch screen is a panel that is permanently half-transparent, because the
   hover never arrives. You just pressed the building; it is the thing you are
   looking at. Full opacity, and a size that fits over a phone-sized spawner. */
html.phone #selpanel { font-size: 11px; }
html.phone #selpanel.atworld { opacity: 1; max-width: min(206px, calc(100vw - 32px)); min-width: 150px; padding: 5px 7px 6px; }
html.phone .bp-hdr { gap: 5px; margin-bottom: 3px; }
html.phone .bp-nm { font-size: 10.5px; }
html.phone .bp-cd { height: 8px; margin-bottom: 4px; }
html.phone .bpupg { padding: 6px 7px; font-size: 10px; }
/* THE ACTION ROW WRAPS. It is a nowrap flex row carrying, while a spawner is
   going up, four things at once: the portrait, "Raising… 85%", a two-line Cancel
   button and the AUTO switch. On a desktop widget there is room; at 206px they
   sat on top of one another and the cancel button covered the progress it was
   offering to cancel. */
/* Scoped to the widget: the AUTO switch also stands in the Upgrades sheet, in a
   row that is 600px wide and wants it exactly as it is. */
html.phone #selpanel .bp-act { flex-wrap: wrap; gap: 5px; }
html.phone #selpanel .bp-wait { flex: 1 1 auto; min-width: 0; font-size: 10px; }
html.phone #selpanel .bldcancel { flex: none; }
html.phone #selpanel .autobox { flex: 1 0 100%; width: auto; flex-direction: row; gap: 5px; padding: 5px 8px; }
html.phone .bpupg span { font-size: 8.5px; }
html.phone .selhdr { font-size: 11px; margin-bottom: 4px; }
html.phone .selchip canvas { width: 26px; height: 26px; }
html.phone .nl-card { padding: 20px 22px; max-width: calc(100vw - 40px); }
html.phone #netlost .nl-card { font-size: 13px; }

/* the count-in ceremony: it is the first thing a new commander sees and it was
   built for a 900px-tall stage. Nothing is cut — the acts, the omen, the
   CHARGE all still play — they simply play at a size that fits the room. */
/* The count-in sits at `8.5vh + 14px`, which on a 390px-tall screen is 47px —
   exactly the band the scorecard now stands on, so the muster counted down
   straight through the two keep bars. It clears the band by name instead of by
   a fraction of a height nobody can predict. */
html.phone #battlecountdown .cdmini { top: calc(var(--band) + var(--sat) + 16px); }
html.phone #battlecountdown .cdlabel { font-size: 10px; letter-spacing: 2.4px; }
html.phone #battlecountdown .cdnum { font-size: 30px; }
html.phone #battlecountdown .cdmini.final .cdnum { font-size: 52px; }
html.phone #battlecountdown .cdgo b { font-size: clamp(30px, 9vw, 58px); }
/* ---------------------------------------------------------- the menu shell */
/* A LADDER THAT FITS, RATHER THAN ONE THAT IS SHRUNK TO FIT. UI.fitScreen()
   zooms a screen's children until the whole thing is inside the frame; on a
   375px-tall phone that meant 0.55 — the floor — and a menu set in 7px type
   that still did not fit. The screens below give up the things a phone has no
   room for (a hero title twice the height of the buttons under it, a 190px
   portrait, a full-width dossier) so the fit has nothing left to do. */
html.phone .screen { overflow: hidden; }
/* The header is 26px of title over a 44px back button on a screen 375px tall.
   One row, one height, and the title gives up its letter-spacing before it gives
   up its size. */
html.phone .scrhdr { padding: var(--edge-t) calc(var(--edge-r) + 82px) 4px var(--edge-l); gap: 8px; }
html.phone .scrtitle { font-size: 17px; letter-spacing: 1px; }
html.phone .backbtn { min-height: 36px; padding: 0 13px; font-size: 12px; display: flex; align-items: center; }
html.phone .hdrright { gap: 6px; }
html.phone .chip { padding: 6px 11px; font-size: 12px; gap: 7px; }
html.phone .buygold { margin-right: 6px; padding: 7px 11px; font-size: 11.5px; }

/* ---- the main menu ----
   The lockup is the whole opening move on a desktop and it is sized to be: a
   210px crest over a 140px wordmark. On a 375px-tall phone that is the fold,
   twice over — so the crest and the flourish stand down (the 480px-height query
   already does this; a phone makes it unconditional) and the wordmark takes the
   size a title still needs to feel like one. What is left is exactly what the
   screen is for: the name, the seven kingdoms, and the two battles. */
html.phone .topbarmenu { padding: var(--edge-t) calc(var(--edge-r) + 82px) 2px var(--edge-l); }
html.phone .menucenter { padding: 2px 12px; max-width: 96vw; }
html.phone .menucenter::before { display: none; }        /* the aura behind the lockup */
html.phone .logocrest, html.phone .flourish, html.phone .lspark { display: none; }
html.phone .gametitle span:first-child { font-size: clamp(22px, 5.4vw, 40px); }
html.phone .gametitle span:last-child  { font-size: clamp(26px, 6.6vw, 48px); }
html.phone .gametitle { letter-spacing: 3px; }
/* The rank hangs from a rod drawn 5px ABOVE each banner (`.rc-rod`), and with the
   kingdoms header suppressed on a short phone that rod came to rest one pixel under
   the wordmark's box — close enough that the rail reads as struck through the last
   line of the title rather than as the thing the banners hang from. Five pixels of
   air is all it needs, and a 375px screen has them: the nav row still lands clear
   of the bottom edge. */
html.phone .racerow { gap: 5px; margin: 9px 0 6px; flex-wrap: nowrap; }
/* Seven banners on one rank, always. They wrapped 6+1 at 667px — one kingdom
   alone on a second line, which reads as a mistake rather than as a rank — and a
   phone has no vertical to spend on a second line anyway. They narrow instead,
   with a floor under the crest so a banner never stops being legible. */
html.phone .racecard { width: clamp(58px, 12vw, 92px); padding: 5px 4px calc(var(--notch) + 3px); }
html.phone .racecard .racecrest { width: clamp(26px, 5.4vw, 40px); height: auto; }
html.phone .racename { font-size: 8.5px; letter-spacing: 0.2px; line-height: 1.15; }
html.phone .rc-host { display: none; }
html.phone .raceblurb { display: none; }
html.phone .bigbtn.modebtn {
  width: clamp(230px, 52vw, 340px); min-height: var(--tap);
  padding: 8px 18px; font-size: 14px; letter-spacing: 1px;
}
html.phone .modewrap + .modewrap { margin-top: 6px; }
html.phone .rankcheck { margin-right: 6px; }
html.phone .rankcheck-word { display: none; }
html.phone .rankcheck-tick { width: 26px; height: 26px; font-size: 15px; }
html.phone .bigbtn::after { display: none; }             /* the travelling shine */
html.phone .menurow { gap: 6px; margin-top: 8px; }
html.phone .menubtn { min-height: 44px; padding: 5px 3px; font-size: 9.5px; gap: 3px; }
html.phone .menubtn .mb-ic { width: 15px; height: 15px; }

/* ---- the muster ----
   Three columns — rail, army, stage — on a screen that has room for one. The
   stage is already dropped under 1200px and the rail already turns horizontal
   under 780px; a phone takes the second of those at any width (a 932px phone is
   still a phone) and makes the rail a proper swipe strip rather than a row of
   full-height banners with their heads cut off. */
/* ── THE MUSTER IN A HAND ──────────────────────────────────────────────────
   The desktop muster is three columns that must not scroll: a rail, an army
   fitted to the room left over, and a stage for one troop. A phone can hold
   exactly one of those at a time, and the port to it had kept the SHAPE while
   losing what the shape was for — the army was squeezed into a 506px box it
   still overflowed, at a chip size (40px) under half a fingertip, inside a page
   that did not itself scroll. So: the page scrolls, the roster is laid out for
   thumbs rather than fitted, the march order stays under one, and the troop
   panel that the missing stage used to hold comes back as a sheet.
   See `onPhone()` in ui.js for the half of this that is behaviour. */
html.phone .musterwrap { gap: 6px; padding: 0 var(--edge-l) var(--edge-b); }
html.phone .mustermode { font-size: 11px; padding: 0 8px 4px; }
/* `auto auto`, NOT `auto minmax(0, 1fr)`. A 1fr row pins the dossier to the
   screen's leftover height and hands its overflow to an inner scroller — right
   on a desktop, where the panel must not scroll, and the direct cause of the
   trapped, clipped roster here. Both rows size to their content and `.musterwrap`
   above does the scrolling, which is what a phone page is expected to do. */
html.phone .musterlayout { grid-template-columns: 1fr; grid-template-rows: auto auto; gap: 7px; }
html.phone .musterstage { display: none; }
/* THE WHOLE CHAIN HAS TO STOP GROWING TO THE SCREEN, or none of the above means
   anything. Every link of it — layout, dossier, roster — carries `flex: 1 1 auto`
   so that on a desktop it fills a panel of a fixed height and the roster is fitted
   into whatever is left. Inside a SCROLLING parent that same rule reads "be
   exactly as tall as the viewport": the boxes stayed one screen high, the troops
   spilled out of them, and the march order — laid out after a roster whose box had
   already ended — was drawn near the top of the page with the army over it.
   `0 0 auto` is "be as tall as your contents"; the wrap does the scrolling. */
html.phone .musterlayout,
html.phone .musterdetail,
html.phone .md-roster { flex: 0 0 auto; min-height: 0; }
/* THE BANNERS DO NOT SCROLL AWAY. The dossier is taller than the screen by design
   and the rail sits at the top of it, so reading the army meant scrolling the one
   thing that says WHICH army off the screen — and the choice then had a page of
   troops between it and the thumb. The march order was already pinned to the foot
   of the scroller for exactly this reason; the rank it is marching under belongs
   at the head of it on the same terms. Between them the two questions this screen
   asks — who am I, and go — are always both on screen.
   It needs a ground of its own: without one the roster reads straight through the
   cloth as it passes under. Faded out at the bottom edge rather than cut with a
   line, so troops slide under the rail instead of vanishing at a border. */
html.phone .musterrail {
  flex-direction: row; gap: 6px; padding: 2px 0 8px;
  overflow-x: auto; overflow-y: hidden;
  scroll-snap-type: x proximity; scrollbar-width: none;
  position: sticky; top: 0; z-index: 6;
  background: linear-gradient(180deg, rgba(9,10,7,0.97) 74%, rgba(9,10,7,0));
  /* DELIBERATELY NOT `scroll-behavior: smooth`. It reads like free polish and it
     is not: it applies to PROGRAMMATIC scrolling too, so the assignment in
     `_pickMuster` that brings the chosen banner into view stops being an
     assignment and becomes an animation — one that a device with no frames to
     spare never finishes, leaving the tick on a tile that is still off-screen.
     A rail that always arrives beats a rail that usually glides. */
}
html.phone .musterrail::-webkit-scrollbar { display: none; }
/* A rail you choose a kingdom from is a row of touch targets before it is
   anything else. These came out 42px tall — under the floor a fingertip can be
   aimed at — and were raised to 46; 54 is the size they should have been. The
   page scrolls now, so the eight pixels are no longer taken from the army. */
html.phone .musterrail .mtile {
  min-height: 54px; scroll-snap-align: center;
  padding: 6px calc(var(--notch) + 4px) 6px 11px; gap: 8px;
}
html.phone .mtile:hover { transform: none; }
html.phone .mt-crest { width: 26px; }
html.phone .mt-name { font-size: 11.5px; }
html.phone .mt-tick { width: 18px; height: 18px; line-height: 18px; font-size: 10px; }
html.phone .musterdetail { padding: 7px 9px 8px; border-radius: var(--r-lg); border-top-width: 2px; }
/* THE HEADER IS A FIXED 112–162px BLOCK carrying four columns: the kingdom's
   identity, its doctrine, its trait bars and its powers. On a 375px screen that
   is a third of the page before a single troop is shown — and the roster IS what
   you came to compare. Two columns and half the height here: who they are, and
   how they win. The bars and the powers are printed in full in the Armory and on
   every troop's own card; the roster is not shown anywhere else. */
html.phone .md-head {
  height: auto; max-height: 84px;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
  gap: 8px; padding-bottom: 6px; margin-bottom: 6px;
}
html.phone .md-idcol { gap: 7px; align-items: flex-start; }
html.phone .md-id h2 { font-size: 14px; }
html.phone .md-style, html.phone .md-blurb { font-size: 9.5px; }
html.phone .md-crest { width: 30px; }
html.phone .md-brief { font-size: 10px; padding: 4px 7px; gap: 3px; }
html.phone .md-edge em { font-size: 9.5px; }
html.phone .md-h { font-size: 10px; }

/* ---- the army, laid out for thumbs ----
   `overflow: visible` and `height: auto` are the point: the roster is no longer a
   box with a scrollbar inside a page, it is simply part of the page. The desktop
   fit does not run here at all (see `_fitRoster`), so `--chip` is STATED rather
   than measured — 60px puts a troop's portrait at ~46px and its whole chip at
   ~80px wide, which is a target a thumb hits without aiming. The measured value
   on this screen was 40px. */
/* ONE ROLE PER ROW WAS THIRTEEN ROWS OF MOSTLY NOTHING. A kingdom fields twenty
   troops across thirteen roles, so a column of full-width role blocks is thirteen
   headings each carrying one or two chips and about seven chips' worth of empty
   width beside them — 1245px of roster, a page running to four and a half screens,
   for an army that occupies a fifth of the pixels it is spread over. "Hard to see
   all the units" is not the chip size; it is that they were dealt one to a line.
   So the blocks flow instead. `auto-fill` rather than a stated count because a
   phone held sideways is anywhere from 667 to 932 across and the right number of
   columns is different at each end. Same twenty troops at the same size, in
   ~400–500px instead of 1245.
   THE FLOOR IS A PAIR OF CHIPS, WRITTEN AS ONE. Squeezing in more columns is the
   obvious next move and it is the wrong one: a track too narrow for two chips
   side by side makes every role wrap to one chip per line, and the roster comes
   out TALLER at five columns than it was at four. So the floor is derived rather
   than typed — a chip is `--chip * 1.34` wide, two of them plus the 8px between
   is `--chip * 2.68 + 8`, and the extra 6 is for the subpixel the track loses to
   rounding. A stated number would have to be re-derived by hand every time the
   chip size moves, and it does move: `.ph-short` takes it to 54 six rules down.
   Checked by sweeping the roster's width a pixel at a time across every phone
   size and asserting no role block ever breaks its pair. */
html.phone .md-roster {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(calc(var(--chip, 60px) * 2.68 + 14px), 1fr));
  align-content: start; align-items: start; gap: 9px;
  --chip: 60px; height: auto; min-height: 0; overflow: visible;
  /* the desktop grid reserves a 4px gutter for the scrollbar it might grow; nothing
     scrolls in here, so that gutter is 4px taken off the last column for nothing */
  padding-right: 0; padding-bottom: 2px;
}
html.phone .md-cat { gap: 4px; min-width: 0; }
html.phone .md-catrow { gap: 7px 8px; }
/* The six roles a kingdom does not field, said once instead of six times. The
   desktop grid gives every role a FIXED cell so the Elves' cavalry sits where the
   Dwarves' absence of it does and the two read across; the phone grid FLOWS, so
   there is no cell for an empty role to hold and nothing to compare it against —
   six headings saying "fields none" would just be six holes in the scroll. */
html.phone .md-nofield {
  grid-column: 1 / -1;            /* a summary of the whole roster, not one cell of it */
  margin-top: 2px; padding: 7px 9px; border-radius: var(--r-md);
  font: 500 10.5px var(--font-ui); color: #8b8069; line-height: 1.5;
  background: rgba(0,0,0,0.26); border: 1px dashed rgba(var(--gold-rgb),0.2);
}
html.phone .md-nofield b { font-family: var(--font-display); color: #a2957a; letter-spacing: 0.4px; text-transform: uppercase; font-size: 9.5px; }

/* THE MARCH ORDER NEVER SCROLLS AWAY. The dossier is now taller than the screen
   by design, and the one button the whole page exists to reach was at the bottom
   of it — twenty troops' worth of scrolling from wherever you happened to be
   reading. Stuck to the foot of the scroller instead, in the thumb's own corner,
   with the panel's own ground behind it so the roster does not show through. */
html.phone .md-march {
  position: sticky; bottom: 0; z-index: 4;
  min-height: var(--tap); font-size: 14px; letter-spacing: 1.2px;
  margin-top: 8px; scroll-margin-bottom: 8px;
  box-shadow: 0 -10px 16px -6px rgba(11,9,6,0.92), var(--sh-3);
  /* THE GROUND HAS TO BE AS WIDE AS THE THING IT IS HIDING. The desk keeps this a
     centred button on purpose (see `.md-march` — a full-width bar is the loudest
     object on a screen you are there to read), and the phone inherited its 260px
     min-width along with the stickiness. A 260px pill stuck to the foot of a 667px
     scroller does not cover the scroller: the roster went on passing either side of
     it and directly behind it, so the Ravager sat half-visible against the button's
     edge and read as a layout fault rather than as a card scrolling under a rail.
     At phone width a full-bleed commit bar is not the desk's problem — it is 647px,
     not 1600 — and it is the one shape that actually seals the foot of the page. */
  width: 100%; min-width: 0; max-width: none;
}
/* …and the roster gets the bar's height back as clearance, so the last rank can be
   scrolled clear of it rather than ending underneath it. */
html.phone .md-roster { padding-bottom: calc(var(--tap) + 6px); }
/* …which only holds if nothing between it and the scroller clips or scrolls. The
   dossier's `overflow: hidden` (there for the seal flourish) would make it a
   scroll container of its own and pin the button to the bottom of the ARMY
   instead of the bottom of the screen. The rounded corners still clip the seal. */
html.phone .musterdetail { overflow: visible; }

html.phone.ph-short .md-head { max-height: 66px; }
html.phone.ph-short .md-blurb { display: none; }
html.phone.ph-short .md-roster { --chip: 54px; }

/* ---- the armory ----
   Same skeleton as the muster, same treatment: the rail lies down, the shelf
   takes the screen. The tools row (search, four state chips, a sort select) is
   two rows of controls above a grid on a 375px screen — it wraps to one scrolling
   rail instead, which is what a phone does with a filter bar. */
html.phone .armwrap { padding: 0 var(--edge-l) var(--edge-b); }
/* The global progress strip spends a whole band of the screen saying 35/140 — a
   number the rail already gives per kingdom, on the kingdom you are looking at.
   It stays where there is room and stands down where there is not. */
html.phone .armbar { margin: 0 var(--edge-l) 5px; padding: 5px 11px; gap: 8px; }
html.phone .armprog { gap: 8px; }
html.phone .armprogbar { height: 6px; max-width: none; }
html.phone .armcount { font-size: 11.5px; }
html.phone .armready, html.phone .armdim { font-size: 10.5px; }
html.phone.ph-short .armbar { display: none; }
html.phone .armlayout { grid-template-columns: 1fr; grid-template-rows: auto minmax(0, 1fr); gap: 7px; }
html.phone .armrail {
  flex-direction: row; gap: 6px; padding: 2px 0 4px;
  overflow-x: auto; overflow-y: hidden; scrollbar-width: none;
}
html.phone .armrail::-webkit-scrollbar { display: none; }
html.phone .armrail .mtile { min-height: 58px; padding: 5px calc(var(--notch) + 4px) 5px 10px; gap: 6px; }
html.phone .armrail .mt-crest { width: 24px; }
html.phone .armrail .mt-name { font-size: 10.5px; }
html.phone .at-bar { height: 4px; margin: 3px 0 2px; }
html.phone .at-num { font-size: 8.5px; }
html.phone .at-num b { font-size: 10.5px; }
/* ---- the tools stop taking the shelf ----
   Measured on an 852×393 handset: a 53px header, a 63px rail and a HUNDRED AND
   ONE pixel tools block left the shelf 146px — and a card is 120px tall. One row
   of troops, visible through a slot, under more than half a screen of furniture
   for searching and sorting the thing you cannot see. That is the whole of "the
   armory is impossible to view": not a broken layout, a starved one.

   The tools were a column of two rows because that is what they are on a desktop,
   where there is height to spend. Here they lie down into ONE row — whose shelf,
   the sort, the search, the four state chips, in that order — and scroll
   sideways, which is what a phone does with a filter bar and what the states row
   was already doing on its own. A little over fifty pixels comes back to the
   army, which is the difference between one row of cards and two. */
html.phone .armtools {
  margin: 0 0 5px; gap: 6px;
  flex-direction: row; flex-wrap: nowrap; align-items: center;
  overflow-x: auto; overflow-y: hidden; scrollbar-width: none;
}
html.phone .armtools::-webkit-scrollbar { display: none; }
html.phone .armtools > .armrow { flex: 0 0 auto; flex-wrap: nowrap; }
/* the search may still take the slack the chips do not want */
html.phone .armsearch { flex: 1 1 auto; }
/* whose shelf this is and how it is ordered, folded into the tools rail rather
   than standing as a headline of its own above it */
html.phone .armwho { gap: 8px; padding-bottom: 0; }
/* …and on the shortest screens the name goes too. The rail is directly above it
   with that kingdom's banner lit and ticked — printing the name again underneath
   is a second answer to a question already answered, and here it costs eighty
   pixels of width that the search and the chips both want. */
html.phone.ph-short .armwhotxt { display: none; }
html.phone .armwhotxt b { font-size: 13px; }
html.phone .armwhotxt em { font-size: 9.5px; }
html.phone .armsort { font-size: 9px; letter-spacing: 1px; }
html.phone.ph-short .armresult { display: none; }
html.phone .armrow { gap: 6px; flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
html.phone .armrow::-webkit-scrollbar { display: none; }
html.phone .armsearch { min-width: 118px; max-width: 260px; padding: 3px 9px; }
/* 16px or iOS zooms the page on focus — non-negotiable, and it is what sets this
   row's height, so the padding above absorbs it instead of adding to it. */
html.phone .armsearch input { font-size: 16px; line-height: 1.2; }
html.phone .armchip { padding: 8px 11px; font-size: 12px; flex: none; }
html.phone .armsort { flex: none; }
html.phone .armsort select { font-size: 12px; padding: 7px 8px; }
/* ═══════════════════════════════════════════════════════════════════════════
   EVERY KINGDOM ON THE RAIL AT ONCE
   ═══════════════════════════════════════════════════════════════════════════
   Seven kingdoms at a fixed 132px (muster) and 152px (armory) came to 960px and
   1100px of tiles in a rail about 840px wide, so one or two of them were always
   past the edge. A horizontal scroller is the right answer to a list; it is the
   WRONG answer to a CHOICE. You cannot weigh seven things against each other
   through a window showing five and a half of them, and the two that are off the
   edge are not merely inconvenient — a player who never scrolls does not know
   they exist. This is the screen whose entire job is "pick who you are", and it
   was the one screen hiding some of the options.

   So the tiles divide the rail instead of queueing along it: `flex: 1 1 0` gives
   seven equal columns that add up to exactly the width available, whatever the
   phone. The overflow stays `auto` and a floor is kept on the tile — an eighth
   kingdom would go on fitting, a tenth would start scrolling again rather than
   squeezing every banner down to a stripe. Degrading to the old behaviour beats
   degrading to an unreadable one.

   The tile turns on its side to pay for it. At ~113px (and ~86px on the smallest
   phone) a crest BESIDE a name leaves the name about forty pixels, which is not
   a kingdom name, it is the first syllable of one. Stacked, the name gets the
   tile's whole width. The banner's own furniture is untouched — cloth, sheen and
   rod are all absolutely positioned, so the rod still stands down the left edge
   and the notch still cuts the tail — only the crest and the text restack. */
html.phone .musterrail .mtile,
html.phone .armrail .mtile {
  --notch: 9px;                    /* the tail costs width a narrow tile has not got */
  flex: 1 1 0; width: auto; min-width: 72px;
  flex-direction: column; justify-content: center; text-align: center;
}
html.phone .musterrail .mt-txt,
html.phone .armrail .mt-txt { width: 100%; min-width: 0; }
html.phone .musterrail .mt-name,
html.phone .armrail .mt-name {
  /* two lines at most, and it ends in an ellipsis rather than in a clipped
     letter — "Warden Beas" reads as a rendering fault, "Warden Be…" reads as a
     name that did not fit */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; text-overflow: ellipsis; line-height: 1.15; width: 100%;
}
/* the chosen tick moves off the tail it used to sit in front of */
html.phone .musterrail .mt-tick,
html.phone .armrail .mt-tick { position: absolute; top: 2px; right: 3px; z-index: 3; }
/* The armory tile carries a progress bar and its numbers as well. The bar is the
   glanceable half and survives at any width; the figures under it are the half
   that stops being readable first, so on the shortest screens they go and the
   bar speaks for them. */
html.phone .armrail .at-bar { margin: 3px 0 2px; }
html.phone.ph-short .armrail .at-num { display: none; }

/* …and the same for the menu's wall of banners, which was 686px of card in a
   674px row — close enough to look intentional and wrong in the same way. */
html.phone .racerow { gap: 5px; width: auto; margin-left: 0; transform: none; }
html.phone .racerow .racecard { width: auto; min-width: 0; }

/* ---- PORTRAIT WRAPS INSTEAD OF SQUEEZING ----
   Seven across a 393px screen is 53px a banner, and 53px is a crest with no room
   for the name beside or beneath it — which fails the same test the scrolling
   rail failed, just in the other direction: you can see all seven and still not
   tell which is which. Portrait has the one thing landscape does not, though,
   which is HEIGHT. So the rail becomes two ranks, four and three, and every
   banner keeps a width its name can live in.
   `flex-grow: 0`, so the short second rank keeps the same tile width as the
   first and centres under it, rather than three tiles stretching to fill a row
   built for four and reading as a different, larger kind of thing. */
html.phone.portrait .musterrail,
html.phone.portrait .armrail {
  flex-wrap: wrap; justify-content: center;
  overflow-x: hidden; scroll-snap-type: none;
}
html.phone.portrait .musterrail .mtile,
html.phone.portrait .armrail .mtile {
  flex: 0 1 calc(25% - 5px); min-width: 0;
}
/* the banners are a grid rather than a rail, so portrait re-columns them instead
   of re-flowing them: five and five, the same two even ranks the narrow break takes */
html.phone.portrait .racerow { grid-template-columns: repeat(5, minmax(0, 1fr)); overflow-x: hidden; }

/* ═══════════════════════════════════════════════════════════════════════════
   THE SHELF FLOWS DOWN, NOT ACROSS
   ═══════════════════════════════════════════════════════════════════════════
   THIS is what "the armory can't even be viewed" was. Not the styling, which was
   all here — the FLOW. `#armgrid` is a four-column multi-column box, and that is
   the right call on a desktop: roles hold wildly different numbers of troops
   (Structures six, Siege two), and column packing lets each take the height it
   needs instead of every row stretching to its tallest member.

   Multi-column fills to the container's HEIGHT and then grows sideways. Give it
   a desktop's 700px and it makes four columns. Give it what a 393px-tall handset
   has left after the header, the rail and the tools — 175px — and each column
   holds about one section, so twelve sections became a strip 5830 PIXELS WIDE.
   Nine of the twelve were off the right-hand edge, and the shelf scrolled
   horizontally with nothing on screen to suggest it: the gesture everyone tries
   first, a vertical swipe, did nothing at all, because there was nothing below.
   A player could open the Armory, see three categories, and correctly conclude
   it was broken.

   So a phone gets one column and the ordinary vertical scroll everything else on
   this device already uses. The packing that multi-column was bought for does not
   apply here anyway — at this width a role is one wrapping row of chips, and rows
   already take only the height they need. */
/* `columns: auto`, NOT `column-count: 1`. They are not the same thing and the
   difference is the whole bug. A multi-column box with a definite height whose
   content does not fit spawns OVERFLOW COLUMNS past its column-count — that is
   the formatting context working as specified, not a count being ignored — so
   asking for one column produced one column of the requested kind and eleven
   more beside it, and the strip got wider rather than narrower. A box is a
   multicol container whenever column-count or column-width is anything but auto,
   so both have to go back to auto to leave the context and get ordinary block
   flow, which is the only thing here that scrolls the way a phone expects. */
/* ── …BUT DOWN IN A COLUMN PER ROLE IS A TENTH OF A SCREEN PER ROLE ─────────
   One block-flow column is what took the shelf out of multicol, and it had to.
   What it left behind is a density problem: every role — Levy, Ranged, Elite —
   costs a full-width heading and a full-width rank of chips, and most roles hold
   one or two troops. Measured on a 844×390 handset: 185px of the screen is header,
   kingdom rail and filters, the shelf itself is 198px, and twenty troops came to
   TEN SCREENS of scrolling with two chips in view at a time. A shop you meet two
   items at a time is a shop nobody reaches the bottom of.

   So the ROLES sit side by side, in a grid. Not multicol again — grid is exactly
   the formatting context that cannot do what multicol did here: it has no concept
   of an overflow column, so a section that does not fit wraps to the next ROW and
   the shelf goes on scrolling downwards, which is the whole property the single
   column was bought for. `align-*: start` keeps a two-troop role from stretching
   to the height of a six-troop one beside it — the packing multicol was chosen for,
   without the sideways growth it came with.

   176px is the track, measured rather than picked: it is one 98px chip plus its
   heading, so a one-troop role — which most of them are — costs a column and not a
   shelf. Wider tracks pack two chips per role and looked tidier on the big
   handsets while leaving the small ones on two columns; this is the width that
   improves every size at once. Measured across the roster: 844×390 goes from ten
   screens with two chips in view to under six with six, and 667×375 from eleven
   screens with two to five with five. Below ~190px of shelf it resolves to a
   single column on its own, which is the old behaviour exactly. */
html.phone #armgrid {
  columns: auto; column-gap: normal;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(176px, 1fr));
  gap: 2px 12px;
  align-content: start; align-items: start;
  overflow-x: hidden; overflow-y: auto;
  scrollbar-gutter: auto;
  -webkit-overflow-scrolling: touch;
}
/* the flat orderings (cheapest, dearest, A–Z) have no roles to pack — one running
   list of chips across the whole shelf. `column-span: all` said that in multicol;
   this is the same sentence in grid. */
html.phone #armgrid > .armcards { grid-column: 1 / -1; }
/* …and with a full-width column the headings are no longer squeezed into a
   quarter of the shelf, so the rule and the role blurb can come back — they are
   what makes a long scroll navigable rather than a wall of portraits. Not on the
   shortest screens, where every line costs a card. */
html.phone #armgrid .armsecrole { display: inline; }
html.phone.ph-short #armgrid .armsecrole { display: none; }
html.phone #armgrid .armsechd { gap: 6px; margin-bottom: 5px; }
html.phone .armsec { margin-bottom: 8px; }

/* ---- the rail pays for the shelf on a landscape phone ----
   Standing the tile up cost the rail 19px of the 48px the tools gave back, which
   is most of the point spent on the way to making it. In portrait there is height
   to spare and the tile can breathe. In landscape — `.ph-short`, which is how
   this game is actually held — the name folds to one line and the crest comes
   down, because the crest IS the identification at this size and the word beside
   it is confirmation. */
html.phone.ph-short .musterrail .mtile,
html.phone.ph-short .armrail .mtile { padding-top: 4px; padding-bottom: 4px; gap: 3px; }
html.phone.ph-short .musterrail .mt-crest,
html.phone.ph-short .armrail .mt-crest { width: 20px; }
/* TWO LINES, SMALLER — not one line, larger. Every kingdom here is two words and
   the distinguishing one is not always the first ("Jade Dynasty" and "Warden
   Beasts" are told apart by their second word, "Orc Horde" and "Elven Court" by
   their first). Clamping to a single line at 87px turns half the rail into
   "Warden Be…" and "Undead Domi…", which is a choice made from crests alone.
   Dropping the type instead fits both words for about eight pixels of rail — the
   cheapest legibility on this screen, and this is the screen whose whole job is
   telling seven near-identical banners apart. */
html.phone.ph-short .musterrail .mt-name,
html.phone.ph-short .armrail .mt-name { font-size: 9px; line-height: 1.1; letter-spacing: 0; }
html.phone.ph-short .armrail .at-bar { margin: 2px 0 0; height: 3px; }

html.phone .armgrid { grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 7px; padding: 4px 0 12px; }
html.phone .armcard { padding: 7px 5px; gap: 2px; border-radius: var(--r-lg); }
html.phone .armicon { width: 46px; height: 46px; }
html.phone .aname { font-size: 11.5px; }
html.phone .acat { font-size: 9.5px; }
html.phone .afoot { min-height: 24px; }
html.phone .buybtn { padding: 7px 12px; font-size: 12px; }
html.phone .armsechd { font-size: 11px; }

/* ---- the commander ---- */
html.phone .profbody { padding: 4px var(--edge-l) var(--edge-b); overflow-y: auto; }
html.phone .purse { margin: 6px auto 10px; padding: 9px 16px; max-width: 300px; border-width: 1px; }
html.phone .purseamt { font-size: 26px; }
html.phone .purselabel { font-size: 9px; letter-spacing: 2px; }
html.phone .statgrid { grid-template-columns: repeat(3, 1fr); gap: 7px; max-width: none; }
html.phone .stat { padding: 8px 6px; border-radius: var(--r-md); }
html.phone .stat b { font-size: 16px; }

/* ---- matchmaking, and the proclamation at the end ---- */
html.phone .searchbox { padding: 16px 20px; width: min(420px, calc(100vw - 32px)); }
html.phone .spinner { width: 40px; height: 40px; border-width: 3px; margin: 0 auto 12px; }
html.phone .searchtitle { font-size: 19px; letter-spacing: 3px; }
html.phone .searchmode { font-size: 13px; margin: 7px 0; }
html.phone .searchstatus { font-size: 12px; margin-bottom: 14px; min-height: 0; }
html.phone .searchbtns { gap: 8px; }
html.phone .searchbtns .menubtn { min-height: var(--tap); padding: 0 18px; font-size: 13px; }
/* THE PROCLAMATION FITS, INCLUDING THE BUTTON. It is ~500px of ceremony —
   crest, verdict, rule, flavour, three medallions, the purse and Continue — and
   on a 375px screen the last two of those were below the fold, so the end of a
   battle was a screen you could not leave without discovering it scrolled.
   Everything gives up size in the order it can afford to: the light-shaft and
   the motes (decoration) go first, then the flavour line, then the career
   footnote; the verdict, the tally, the purse and the way out all stay. */
html.phone .resultbox {
  padding: 10px 18px 12px; width: min(560px, calc(100vw - 20px));
  max-height: calc(var(--vh) * 100 - 12px); overflow-y: auto;
}
html.phone .resrays, html.phone .resmotes { display: none; }
html.phone .rescrest { width: 38px; }
html.phone .reskd { font-size: 9px; letter-spacing: 1.6px; }
html.phone .resverdict { font-size: clamp(28px, 7vw, 46px); letter-spacing: 4px; }
html.phone .reshead { margin: 5px auto 6px; }
html.phone .reshead b { font-size: 10px; letter-spacing: 1.4px; }
html.phone .resblurb { font-size: 11.5px; line-height: 1.3; }
html.phone .resmode { margin-top: 3px; font-size: 9px; }
html.phone .resspoils { gap: 7px; margin: 9px 0 8px; }
html.phone .rstat { padding: 6px 6px 5px; }
html.phone .rstat i { font-size: 13px; }
html.phone .rstat b { font-size: 19px; }
html.phone .rstat span { font-size: 8px; letter-spacing: 0.6px; }
html.phone .respurse { padding: 7px 14px 6px; min-width: 150px; }
html.phone .rpttl { font-size: 9px; }
html.phone .rpnum { font-size: 26px; }
html.phone .rescareer { margin-top: 7px; font-size: 9px; }
html.phone .levelup { margin: 8px 0 2px; padding: 7px 9px; }
html.phone .nutroop { width: 40px; }
html.phone .nutroop canvas { width: 28px; height: 28px; }
html.phone .resultbox .bigbtn { margin-top: 10px; min-height: var(--tap); font-size: 14px; padding: 8px 18px; }
html.phone.ph-short .resblurb, html.phone.ph-short .rescareer { display: none; }

/* ---- the mint, and the sign-in gate ---- */
html.phone .mintbox { padding: 12px 14px 10px; max-height: calc(var(--vh) * 100 - 24px); }
html.phone .mintgrid { grid-template-columns: repeat(3, 1fr); gap: 6px; }
html.phone .mint-t { font-size: 17px; }
html.phone .mint-s, html.phone .mint-joke { font-size: 10.5px; }
html.phone .mint-joke { margin: 6px auto 8px; padding: 5px 9px; }
html.phone .minttier { padding: 10px 5px 7px; }
html.phone .mt-emo { font-size: 21px; margin-top: 3px; }
html.phone .mt-amt { font-size: 14px; }
html.phone .mt-note { display: none; }
html.phone .mintfoot { margin-top: 7px; font-size: 9.5px; }
html.phone .ag-card { padding: 16px 18px; max-height: calc(var(--vh) * 100 - 24px); overflow-y: auto; }
html.phone .ag-crest { font-size: 26px; }
html.phone .ag-title { font-size: 20px; }
html.phone .ag-sub { font-size: 11px; }
html.phone .ag-input { min-height: 42px; font-size: 16px; }
html.phone .ag-btn { min-height: var(--tap); }

/* THE ACCOUNT CORNER HAS NO BUSINESS ON A BATTLEFIELD. It is `position: fixed`
   under the menu's chip row and nothing ever hides it, so in a match on a phone
   it hangs in the middle of the right-hand side of the field — "PCO  Sign out"
   printed over the ground, a thumb's width from the utility chips it is not part
   of. Whose account this is answers a question you ask on the menu, so that is
   where it stays. */
html.phone.inmatch #acctstack { display: none !important; }
html.phone #acctstack { top: calc(var(--edge-t) + 40px); right: var(--edge-r); font-size: 10px; }
/* …and the ranked card does not fit a phone at all — a fixed 240px sidebar on
   a ~390px screen would sit over the kingdom row it is drawn on top of. The
   standing itself is still one tap away, on The Ladder screen; this is only
   the always-on corner glance, which a phone has no spare corner for. */
html.phone #rankbadge { display: none !important; }

/* ---------------------------------------------------------------- the rest */
/* NO HOVER STATES ON A MACHINE WITH NO POINTER. Every :hover in this sheet was
   written for a cursor that arrives, rests and leaves. A touch screen fires
   them on TAP and then leaves them stuck on the last thing pressed — so a build
   tile a commander bought from ten seconds ago sits there lifted and glowing as
   if it were still under the finger. The presses below replace them: they end
   when the finger does. */
html.touch .build:hover, html.touch .modecard:hover, html.touch .armcard:hover,
html.touch .mt-tile:hover, html.touch .rc-card:hover, html.touch .menubtn:hover,
html.touch .navbtn:hover, html.touch .uw-row.can:hover {
  transform: none;
}
html.touch .menubtn:active, html.touch .navbtn:active, html.touch .modecard:active,
html.touch .armcard:active, html.touch .mt-tile:active { transform: scale(0.975); }

/* Finger-sized targets on every coarse screen, phone or tablet. The chips on
   the utility rail already grew (see the coarse block above); these are the
   controls a menu is made of. */
html.touch .menubtn, html.touch .navbtn, html.touch .backbtn,
html.touch .armfilters button, html.touch .mt-march {
  min-height: 44px;
}
html.touch input, html.touch select { min-height: 42px; font-size: 16px; }

/* A TABLET KEEPS THE DESKTOP LAYOUT and gains the targets. It has the room —
   a 1024×768 iPad is a wider stage than most laptops — so nothing is taken
   away; the controls simply stop being cursor-sized. */
html.tablet #buildbtn, html.tablet #upgradebtn { min-height: 46px; }
html.tablet .build { width: 118px; }
html.tablet .uw-up { min-height: 46px; }
html.tablet .uw-x, html.tablet .km-x { width: 42px; height: 42px; }

/* ═══════════════════════════════════════════════════════════════════════════
                    THE METAGAME — LADDER · HOUSE · BOARD
   ═══════════════════════════════════════════════════════════════════════════

   Three things that live between matches, and one rule shared by all of them:
   NOTHING HERE INVENTS A MATERIAL. The plate, the gilt, the two signal colours
   and the four heights are the tokens at the top of this file, and every panel
   below is built from them. The only new colour anywhere in this section is a
   RANK DIVISION's — and even that is passed in as `--rc/--rcl/--rcd` per
   element, the way a kingdom's banner colour already is, so a division tints
   the plate rather than replacing it.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ───────────────────────── the rank crest ─────────────────────────────────
   The one object that says what you are. It appears at three sizes and it is
   the SAME object at all three — a shield, a mark, a name and a sub-line —
   because a rank a player has to re-learn on each screen is a rank they will
   not read on any of them. */
.rcrest { display: inline-flex; flex-direction: column; align-items: center; gap: 2px; flex: 0 0 auto; }
.rcrest svg { width: 62px; height: 73px; filter: drop-shadow(0 2px 6px rgba(var(--shade-rgb), 0.5)); }
.rcrest.sm svg { width: 40px; height: 47px; }
.rcrest.big svg { width: 96px; height: 113px; }
.rc-nm { font: 800 11px var(--font-display); letter-spacing: 1.2px; text-transform: uppercase;
  color: var(--rcl); text-shadow: 0 1px 2px rgba(var(--shade-rgb), 0.7); }
.rc-sb { font: 600 9.5px var(--font-ui); color: var(--muted); font-variant-numeric: var(--nums); }
.rcrest.sm .rc-nm { font-size: 9px; letter-spacing: 0.8px; }
.rcrest.sm .rc-sb { font-size: 8.5px; }
.rcrest.big .rc-nm { font-size: 15px; }
.rcrest.big .rc-sb { font-size: 12px; }
/* Warden is the one rank with no rung under it, so it is the one crest that
   moves: a slow specular travel across the shield. RULE from the tokens at the
   top of this file — at most one element per screen wears the spark, and on a
   screen that has a Warden crest on it, this is that element. */
/* An unearned crest is drawn as unearned: the shield stays, drained of its
   division's colour, so the SHAPE of what is waiting is still legible. */
.rcrest.unplaced svg { filter: grayscale(0.85) brightness(0.62); }
.rcrest.unplaced .rc-nm { color: var(--muted); }
/* A SEASON IS EARNED BY DIVISION, NOT BY TIER — the roll pays on the highest
   division a commander reached and the tier they were sitting in when it closed
   is not part of it. So the crest that reports one says the division and stops:
   a "Tier 1" caption there is a number the reward does not use. */
.rcrest.divonly .rc-sb { display: none; }
.rcrest.divonly svg text { display: none; }
.rcrest.apex svg { filter: drop-shadow(0 2px 12px rgba(134, 89, 184, 0.6)); animation: apexshine 3.4s ease-in-out infinite; }
@keyframes apexshine { 0%, 100% { filter: drop-shadow(0 2px 8px rgba(201, 86, 155, 0.4)); } 50% { filter: drop-shadow(0 2px 16px rgba(255, 154, 210, 0.75)); } }

/* ───────────────────────── the pip track ─────────────────────────────────
   Six lamps. The whole visible economy of the ladder is here: a win lights one,
   a loss puts one out, and a commander can read what the next match is worth
   without being told. Shape as well as hue — a lit pip is LARGER and seated in
   its own glow — so the track survives grayscale, same rule as `.build.ready`. */
.piprow { display: inline-flex; align-items: center; gap: 5px; margin: 4px 0 2px; }
.piprow .pip { width: 10px; height: 10px; border-radius: var(--r-pill); flex: 0 0 auto;
  background: rgba(var(--shade-rgb), 0.5); border: 1px solid var(--gilt-seat); box-shadow: var(--seat); transition: all var(--t-2) var(--e-out); }
.piprow .pip.on { width: 12px; height: 12px; background: linear-gradient(180deg, var(--rcl), var(--rc));
  border-color: var(--gold-hi); box-shadow: 0 0 8px var(--rc), var(--sh-0); }
.piprow.apex, .piprow.ghost { font: 600 10.5px var(--font-ui); color: var(--muted); font-style: italic; }

/* ───────────────────────── the account corner's ranked card ───────────────
   Standing only now — the switch that used to share this panel lives beside
   each battle button instead (see .rankcheck and the note at `setRanked` in
   ui.js). A narrow column under the account name, not a wide plate of its
   own between the kingdoms and the buttons. */
#rankbadge {
  width: 240px; background: var(--plate); border: 1px solid var(--edge); border-radius: var(--r-xl);
  box-shadow: var(--sh-3), var(--bevel), var(--seat); overflow: hidden;
  transition: border-color var(--t-2) var(--e-out), box-shadow var(--t-2) var(--e-out);
}
/* ARMED. The tick beside a battle button is on, so the next press is a ladder
   match — and this card says so too, since it is the thing showing what that
   match would cost or win. */
#rankbadge.armed { border-color: var(--edge-strong); box-shadow: var(--sh-3), var(--bevel), var(--seat), 0 0 22px rgba(var(--gold-rgb), 0.18); }
.lp-hd { display: flex; align-items: center; gap: var(--sp-3); padding: 7px 12px;
  background: linear-gradient(180deg, rgba(var(--lit-rgb), 0.07), transparent);
  border-bottom: 1px solid var(--edge-quiet); }
.lp-ttl { font: 800 11px var(--font-display); letter-spacing: 1.2px; text-transform: uppercase; color: var(--gold-lt); display: inline-flex; align-items: center; gap: 6px; }
.lp-ttl .ic { width: 14px; height: 14px; }
.lp-season { font: 600 10px var(--font-ui); color: var(--muted); margin-left: auto; }
.lp-left { display: block; font: 700 10px var(--font-ui); color: var(--gilt-body); font-variant-numeric: var(--nums);
  padding: 4px 12px; border-bottom: 1px solid var(--edge-quiet); }
/* ONE COLUMN. The plate this card was cut from set its two queues side by
   side because it had 620px to spend; a 240px sidebar does not, so Duel and
   Team Battle stack instead. */
.lp-cards { display: grid; grid-template-columns: 1fr; gap: 1px; background: var(--edge-quiet); }
.lp-card { display: flex; align-items: center; gap: var(--sp-3); padding: 9px 11px; cursor: pointer;
  background: rgba(var(--shade-rgb), 0.22); border: 0; text-align: left; color: inherit;
  transition: background var(--t-2) var(--e-out); }
.lp-card:hover { background: rgba(var(--gold-rgb), 0.09); }
.lp-card.unplaced { opacity: 0.72; }
.lp-txt { display: flex; flex-direction: column; min-width: 0; }
.lp-q { font: 700 9.5px var(--font-display); letter-spacing: 1px; text-transform: uppercase; color: var(--gilt-body); }
.lp-rk { font: 800 15px var(--font-display); color: var(--ink); line-height: 1.15; }
.lp-rec { font: 600 10.5px var(--font-ui); color: var(--muted); font-variant-numeric: var(--nums); }
.lp-rec b { color: var(--ready-lt); }
/* ---- WHAT IS RIDING ON THE NEXT ONE ----
   Two short clauses, win first, in the two inks the ladder already uses for
   gain and loss — and a third, quieter ink for a loss that costs nothing,
   because "safe" is a good thing and printing it in the red of a demotion
   would read as a warning. Only drawn with the tick-box on. */
.lp-stakes { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; margin-top: 3px; }
.lp-stakes b {
  font: 800 9.5px var(--font-display); letter-spacing: 0.7px; text-transform: uppercase;
  white-space: nowrap;
}
.lp-stakes .st-w { color: var(--ready-lt); }
.lp-stakes .st-l { color: #ff9d92; }
.lp-stakes .st-safe { color: var(--gilt-body); }
.lp-stakes em { font: italic 500 10px var(--font-ui); color: var(--muted); flex-basis: 100%; }

/* The battle buttons wear the queue they are about to open. A button that reads
   the same either way is a button that gets pressed by accident. */
.bigbtn.modebtn.ranked { box-shadow: var(--sh-2), var(--bevel), 0 0 20px rgba(var(--gold-rgb), 0.28); }
/* A mode with no ladder behind it must not wear the mark even with the box
   ticked — it would be promising pips it cannot pay. */
.bigbtn.modebtn.noladder { opacity: 0.86; }

/* ───────────────────────── the ladder screen ─────────────────────────────── */
.rankbody { padding: 0 clamp(12px, 3vw, 30px) 22px; overflow: auto;
  width: 100%; max-width: var(--screenw); margin-inline: auto; }
.rk-tabs { display: flex; gap: var(--gutter); justify-content: center; margin-bottom: var(--sp-5); }
.rk-tab { padding: 7px 16px; cursor: pointer; font: 700 12.5px var(--font-display); letter-spacing: 0.6px;
  color: var(--gilt-body); background: var(--plate); border: 1px solid var(--edge-quiet);
  border-radius: var(--r-pill); transition: all var(--t-2) var(--e-out); }
.rk-tab:hover { border-color: var(--edge); color: var(--gold-lt); }
.rk-tab.on { color: var(--gilt-ink); background: linear-gradient(180deg, var(--gold-lt), var(--gold) 60%, var(--gold-dk)); border-color: var(--gilt-deep); }

.rk-hero { display: flex; align-items: center; gap: clamp(14px, 3vw, 28px);
  padding: clamp(12px, 2.4vh, 22px) clamp(14px, 3vw, 26px); margin-bottom: var(--sp-5);
  background: var(--plate); border: 1px solid var(--edge); border-top: 3px solid var(--gold);
  border-radius: var(--r-xl); box-shadow: var(--sh-3), var(--bevel); }
.rk-hx { min-width: 0; }
.rk-full { font: 900 clamp(24px, 4vw, 38px) var(--font-display); color: var(--gold-lt); letter-spacing: 1px; line-height: 1.1; }
.rk-sub { font: 500 12.5px var(--font-ui); color: var(--muted); margin: 2px 0 4px; }
.rk-sub b { color: var(--ink); font-variant-numeric: var(--nums); }
.rk-rec { display: flex; flex-wrap: wrap; gap: var(--sp-4); margin-top: 6px; font: 500 12px var(--font-ui); color: var(--muted); }
.rk-rec b { color: var(--ink); font-variant-numeric: var(--nums); }
.rk-rec .hot b { color: var(--ready-lt); }

/* THE CLIMB, DRAWN AS A CLIMB. Twenty-one rungs on one rail — see the note in
   ui.js. Six unrelated boxes would hide the only thing this screen is for. */
.rk-rail { display: grid; grid-template-columns: repeat(6, 1fr); gap: 1px; margin-bottom: var(--sp-5);
  background: var(--edge-quiet); border: 1px solid var(--edge); border-radius: var(--r-lg); overflow: hidden; }
.rk-div { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 9px 4px;
  background: linear-gradient(180deg, rgba(var(--shade-rgb), 0.2), rgba(var(--shade-rgb), 0.42)); }
.rk-div.here { background: linear-gradient(180deg, color-mix(in srgb, var(--rc) 26%, transparent), rgba(var(--shade-rgb), 0.42));
  box-shadow: inset 0 0 0 1px var(--rc); }
.rk-dn { font: 800 10px var(--font-display); letter-spacing: 0.9px; text-transform: uppercase; color: var(--rcl); }
.rk-rungs { display: flex; gap: 3px; }
.rk-rung { width: 9px; height: 9px; border-radius: 2px; background: rgba(var(--shade-rgb), 0.55); border: 1px solid var(--gilt-seat); }
.rk-rung.past { background: var(--rcd); border-color: var(--rc); }
.rk-rung.on { width: 11px; height: 11px; background: var(--rcl); border-color: var(--gold-hi); box-shadow: 0 0 8px var(--rc); }
.rk-best { font: 700 9.5px var(--font-ui); color: var(--gilt-body); font-variant-numeric: var(--nums); min-height: 12px; }

.rk-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: var(--sp-4); }
.rk-card { padding: var(--sp-4) var(--sp-5); background: var(--plate); border: 1px solid var(--edge-quiet);
  border-radius: var(--r-lg); box-shadow: var(--sh-2), var(--bevel); }
.rk-card h3 { font: 800 13px var(--font-display); letter-spacing: 1px; text-transform: uppercase;
  color: var(--gold-lt); margin: 0 0 var(--sp-3); }
.rk-rules { margin: 0; padding-left: 16px; display: flex; flex-direction: column; gap: 5px; }
.rk-rules li { font: 400 12px var(--font-ui); color: var(--muted); line-height: 1.45; }
.rk-rules b { color: var(--ink); }
.rk-rw { display: flex; flex-direction: column; gap: 3px; }
.rk-rwrow { display: flex; align-items: center; gap: var(--sp-3); padding: 4px 8px; border-radius: var(--r-sm);
  border-left: 3px solid var(--rc); background: rgba(var(--shade-rgb), 0.28); opacity: 0.55; }
.rk-rwrow.got { opacity: 1; background: color-mix(in srgb, var(--rc) 14%, rgba(var(--shade-rgb), 0.28)); }
.rk-rwn { font: 700 12px var(--font-display); color: var(--rcl); }
.rk-rwg { margin-left: auto; font: 700 12px var(--font-ui); color: var(--gold-lt); font-variant-numeric: var(--nums); }
.rk-note { font: 400 11px var(--font-ui); font-style: italic; color: var(--muted); margin-top: var(--sp-3); line-height: 1.4; }
.rk-boardlist { display: flex; flex-direction: column; gap: 2px; max-height: 260px; overflow: auto; }
.rk-brow { display: flex; align-items: center; gap: var(--sp-3); padding: 3px 7px; border-radius: var(--r-xs); font: 500 12px var(--font-ui); }
.rk-brow.me { background: rgba(var(--gold-rgb), 0.14); box-shadow: inset 0 0 0 1px var(--edge-quiet); }
.rk-bn { font: 800 11px var(--font-display); color: var(--gold-lt); min-width: 34px; font-variant-numeric: var(--nums); }
.rk-bw { color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rk-bs { margin-left: auto; color: var(--muted); font-variant-numeric: var(--nums); }
.rk-empty { font: 400 12px var(--font-ui); font-style: italic; color: var(--muted); line-height: 1.5; }

/* ───────────────────────── the search screen's queue ──────────────────────── */
.searchmode .srk { color: var(--gold-lt); letter-spacing: 1.6px; }
.rankedsearch .searchtitle { color: var(--gold-lt); text-shadow: 0 0 18px rgba(var(--gold-rgb), 0.45); }
.searchmode em { font-style: italic; color: var(--muted); }
/* A board square is dealt, not queued — there is nobody to wait for and nothing
   to vote on, so the controls that exist for a lobby are not shown. */
.searchbtns.chal .menubtn.primary { display: none; }

/* ───────────────────────── the results screen ─────────────────────────────── */
/* Both ride the result screen's own staged reveal — the delay is written per
   element by showResults, so these join the sequence rather than arriving on a
   second, competing beat of their own. */
.reschal, .resrank { width: min(420px, 90%); margin: var(--sp-4) auto 0; padding: var(--sp-4) var(--sp-5);
  background: rgba(var(--shade-rgb), 0.34); border: 1px solid var(--edge-quiet); border-radius: var(--r-lg);
  animation: resRise 0.5s var(--e-out) both; }
.rcttl, .rrttl { font: 800 11px var(--font-display); letter-spacing: 1.6px; text-transform: uppercase;
  color: var(--gilt-body); display: flex; align-items: center; justify-content: center; gap: 6px; margin-bottom: 5px; }
.rcttl .ic, .rrttl .ic { width: 14px; height: 14px; }
.rcsq { font: 500 13px var(--font-ui); color: var(--ink); }
.rcsq b { color: var(--gold-lt); }
.rcgold { font: 900 26px var(--font-display); color: var(--gold-lt); font-variant-numeric: var(--nums); margin: 3px 0; }
.rcbonus { font: 600 12px var(--font-ui); color: var(--ready-lt); }
.rcprog { font: 500 11px var(--font-ui); color: var(--muted); margin-top: 3px; font-variant-numeric: var(--nums); }
.resrank.pending .rrbody { font: 400 12px var(--font-ui); font-style: italic; color: var(--muted); }
.resrank.up { border-color: var(--edge-strong); box-shadow: 0 0 26px rgba(var(--gold-rgb), 0.22); }
.resrank.up .rrttl { color: var(--gold-bright); }
.resrank.down .rrttl { color: var(--peril); }
.rrline { display: flex; align-items: center; gap: var(--sp-4); justify-content: center; }
.rrtx { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }
.rrtx b { font: 800 16px var(--font-display); color: var(--ink); }
.rrtx em { font: 400 11.5px var(--font-ui); font-style: italic; color: var(--muted); }
.rrtx em.hot { color: var(--ready-lt); }
/* A pip earned off a Diamond is not the same pip as one earned off a Bronze. */
.rrfoe { display: flex; align-items: center; justify-content: center; gap: 6px; margin-top: 7px;
  font: 600 11px var(--font-ui); color: var(--muted); }

/* ---- THE PIP DELTA ----
   Where the track below says what you ARE, this says what just happened — the
   one number a commander is looking for when this block lands. */
.rrdelta { font: 900 11px var(--font-display); letter-spacing: 1px; padding: 1px 7px;
  border-radius: var(--r-pill); border: 1px solid currentColor; font-variant-numeric: var(--nums); }
.rrdelta.gain { color: var(--ready-lt); }
.rrdelta.lose { color: #ff9d92; }

/* ---- THE MOMENT ----
   Rising a rung is the thing a ladder is FOR and it used to be one more line of
   text. Rays, a struck banner, and a horn — the same ceremony the victory screen
   gives a won battle, because a promotion is rarer than that. A demotion gets
   the identical weight in cold iron rather than a smaller, sadder version. */
.rrrise { position: relative; margin-top: 9px; padding: 7px 0 5px; text-align: center; overflow: hidden;
  border-top: 1px solid var(--edge-quiet); }
.rrrise b { position: relative; z-index: 1; display: inline-block;
  font: 900 13px var(--font-display); letter-spacing: 2.4px;
  color: var(--gold-bright); text-shadow: 0 0 14px rgba(var(--gold-rgb), 0.55), 0 2px 0 rgba(0,0,0,0.5); }
.resrank.down .rrrise b { color: #ffb3aa; text-shadow: 0 0 12px rgba(214, 74, 60, 0.45), 0 2px 0 rgba(0,0,0,0.5); }
.rr-rays { position: absolute; inset: -60% -20%; z-index: 0; opacity: 0; pointer-events: none;
  background: conic-gradient(from 0deg,
    rgba(var(--gold-rgb),0.30) 0deg 7deg, transparent 7deg 30deg,
    rgba(var(--gold-rgb),0.30) 30deg 37deg, transparent 37deg 60deg,
    rgba(var(--gold-rgb),0.30) 60deg 67deg, transparent 67deg 90deg,
    rgba(var(--gold-rgb),0.30) 90deg 97deg, transparent 97deg 120deg,
    rgba(var(--gold-rgb),0.30) 120deg 127deg, transparent 127deg 150deg,
    rgba(var(--gold-rgb),0.30) 150deg 157deg, transparent 157deg 180deg,
    rgba(var(--gold-rgb),0.30) 180deg 187deg, transparent 187deg 210deg,
    rgba(var(--gold-rgb),0.30) 210deg 217deg, transparent 217deg 240deg,
    rgba(var(--gold-rgb),0.30) 240deg 247deg, transparent 247deg 270deg,
    rgba(var(--gold-rgb),0.30) 270deg 277deg, transparent 277deg 300deg,
    rgba(var(--gold-rgb),0.30) 300deg 307deg, transparent 307deg 330deg,
    rgba(var(--gold-rgb),0.30) 330deg 337deg, transparent 337deg 360deg);
  -webkit-mask-image: radial-gradient(closest-side, #000 20%, transparent 78%);
          mask-image: radial-gradient(closest-side, #000 20%, transparent 78%);
}
.resrank.down .rr-rays { background-image: conic-gradient(from 0deg,
  rgba(214,74,60,0.26) 0deg 7deg, transparent 7deg 45deg,
  rgba(214,74,60,0.26) 45deg 52deg, transparent 52deg 90deg,
  rgba(214,74,60,0.26) 90deg 97deg, transparent 97deg 135deg,
  rgba(214,74,60,0.26) 135deg 142deg, transparent 142deg 180deg,
  rgba(214,74,60,0.26) 180deg 187deg, transparent 187deg 225deg,
  rgba(214,74,60,0.26) 225deg 232deg, transparent 232deg 270deg,
  rgba(214,74,60,0.26) 270deg 277deg, transparent 277deg 315deg,
  rgba(214,74,60,0.26) 315deg 322deg, transparent 322deg 360deg); }
.rrrise.go b { animation: rrStrike 0.62s var(--e-out) both; }
.rrrise.go .rr-rays { animation: rrRays 2.6s linear both; }
@keyframes rrStrike {
  0%   { opacity: 0; transform: scale(1.5); letter-spacing: 9px; filter: blur(4px); }
  60%  { opacity: 1; filter: blur(0); }
  100% { opacity: 1; transform: scale(1); letter-spacing: 2.4px; }
}
@keyframes rrRays {
  0%   { opacity: 0; transform: rotate(0deg); }
  18%  { opacity: 1; }
  100% { opacity: 0; transform: rotate(52deg); }
}
/* Asked to sit still, it sits still — the words still land, they just do not fly. */
@media (prefers-reduced-motion: reduce) {
  .rrrise.go b { animation: none; }
  .rrrise.go .rr-rays { animation: none; opacity: 0.5; }
}

/* ═══════════════════════════ THE GUILD HALL ═══════════════════════════════ */
.guildbody { padding: 0 clamp(12px, 3vw, 30px) 20px; overflow: auto;
  width: 100%; max-width: var(--screenw); margin-inline: auto; }
.gh-empty { padding: 40px; text-align: center; font: 400 14px var(--font-ui); font-style: italic; color: var(--muted); }

.gh-found { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--sp-5); align-items: start; }
.gh-fx { padding: var(--sp-5) var(--sp-6); background: var(--plate); border: 1px solid var(--edge);
  border-top: 3px solid var(--gold); border-radius: var(--r-xl); box-shadow: var(--sh-3), var(--bevel); }
.gh-join { border-top-color: var(--gilt-deep); }
.gh-ft { font: 800 18px var(--font-display); color: var(--gold-lt); letter-spacing: 0.8px;
  display: flex; align-items: center; gap: 8px; }
.gh-ft .ic { width: 20px; height: 20px; }
.gh-fs { font: 400 12.5px var(--font-ui); color: var(--muted); line-height: 1.5; margin: 6px 0 var(--sp-4); }
.gh-fs b { color: var(--ink); }
.gh-lb { display: block; font: 700 10.5px var(--font-display); letter-spacing: 1.2px; text-transform: uppercase;
  color: var(--gilt-body); margin-bottom: var(--sp-3); }
.gh-lb em { font-style: italic; text-transform: none; letter-spacing: 0; color: var(--muted); }
.gh-lb input, .gh-joinrow input {
  display: block; width: 100%; margin-top: 3px; padding: 8px 10px; font: 500 14px var(--font-ui);
  color: var(--ink); background: rgba(var(--shade-rgb), 0.45); border: 1px solid var(--gilt-deep);
  border-radius: var(--r-sm); box-shadow: var(--seat); }
.gh-lb input:focus, .gh-joinrow input:focus { outline: none; border-color: var(--gold-lt); box-shadow: 0 0 10px rgba(var(--gold-rgb), 0.22); }
.gh-tag { text-transform: uppercase; letter-spacing: 3px; font-family: var(--font-display) !important; font-weight: 800 !important; }
.gh-armsrow { display: flex; align-items: center; gap: var(--sp-4); margin: var(--sp-4) 0; }
.gh-armsprev, .gh-arms { flex: 0 0 auto; filter: drop-shadow(0 3px 10px rgba(var(--shade-rgb), 0.55)); }
.gh-joinrow { display: flex; gap: var(--gutter); align-items: stretch; }
.gh-joinrow input { text-transform: uppercase; letter-spacing: 4px; text-align: center;
  font: 800 18px var(--font-display); margin-top: 0; }

/* ---- THE NOTICEBOARD OUTSIDE THE HALLS ----
   Everything above it requires already knowing somebody. This is the way in
   for a commander who does not — see the GuildDir note in the Worker. */
.gh-boardhd { display: flex; align-items: center; gap: var(--sp-3); margin: var(--sp-5) 0 var(--sp-3);
  padding-top: var(--sp-4); border-top: 1px solid var(--edge-quiet); }
.gh-boardhd > span { display: flex; align-items: center; gap: 6px; white-space: nowrap;
  font: 800 10.5px var(--font-display); letter-spacing: 1.2px; text-transform: uppercase; color: var(--gilt-body); }
.gh-boardhd .ic { width: 15px; height: 15px; }
.gh-bq { flex: 1; min-width: 0; padding: 6px 9px; font: 500 12.5px var(--font-ui);
  color: var(--ink); background: rgba(var(--shade-rgb), 0.45); border: 1px solid var(--gilt-deep);
  border-radius: var(--r-sm); box-shadow: var(--seat); }
.gh-bq:focus { outline: none; border-color: var(--gold-lt); box-shadow: 0 0 10px rgba(var(--gold-rgb), 0.22); }
.gh-board { display: flex; flex-direction: column; gap: 5px; max-height: 42vh; overflow-y: auto; }
.gh-bnone { padding: 14px 4px; text-align: center; font: italic 400 12px var(--font-ui); color: var(--muted); }
.gh-hcard { display: flex; align-items: center; gap: var(--sp-3); width: 100%; cursor: pointer; text-align: left;
  padding: 7px 9px; border-radius: var(--r-md);
  background: rgba(var(--shade-rgb), 0.34); border: 1px solid var(--edge-quiet);
  transition: border-color var(--t-1) var(--e-out), background var(--t-1) var(--e-out), transform var(--t-1) var(--e-out); }
.gh-hcard:hover { border-color: var(--gold-lt); background: rgba(var(--gold-rgb), 0.09); transform: translateX(2px); }
.hb-arms { flex: none; width: 30px; height: 36px; }
.hb-tx { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
.hb-nm { font: 700 13.5px var(--font-ui); color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hb-nm b { font-family: var(--font-display); font-size: 11px; letter-spacing: 1px; color: var(--gilt-body); }
.hb-mo { font: italic 400 11px var(--font-ui); color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hb-st { display: flex; gap: 9px; font: 600 10px var(--font-ui); color: var(--muted); font-variant-numeric: var(--nums); }
/* WHO IS ACTUALLY THERE is the first thing on the row, because "where could I
   get a game tonight" is what somebody opening this board is asking. */
.hb-on { color: var(--gilt-body); }
.hb-on.lit { color: var(--ready-lt); }
.hb-go { flex: none; font: 800 10px var(--font-display); letter-spacing: 1px; text-transform: uppercase;
  color: var(--gold-lt); opacity: 0; transition: opacity var(--t-1) var(--e-out); }
.gh-hcard:hover .hb-go { opacity: 1; }

/* ---- TWO BOARDS ----
   "Where could I get a game tonight" and "who is winning" are different
   questions, and a directory that answers only the first leaves every house's
   tally unread. */
.gh-btabs { display: flex; gap: 3px; margin-top: var(--sp-5); }
.gh-btab { flex: 1; cursor: pointer; padding: 6px 8px; border-radius: var(--r-md) var(--r-md) 0 0;
  font: 700 10.5px var(--font-display); letter-spacing: 0.9px; text-transform: uppercase;
  color: var(--muted); background: rgba(var(--shade-rgb), 0.34);
  border: 1px solid var(--edge-quiet); border-bottom: none;
  transition: color var(--t-1) var(--e-out), background var(--t-1) var(--e-out); }
.gh-btab:hover { color: var(--gilt-body); }
.gh-btab.on { color: var(--gold-lt); background: rgba(var(--gold-rgb), 0.12); border-color: var(--edge); }
.gh-boardhd { margin-top: 0; padding-top: var(--sp-3); }
/* THE PLACE, in the ordinal position the server counted — never one the client
   worked out from a page it could not see. */
.hb-pl { flex: none; min-width: 22px; text-align: center;
  font: 900 14px var(--font-display); color: var(--gilt-body); font-variant-numeric: var(--nums); }
.gh-hcard:nth-child(1) .hb-pl { color: var(--gold-bright); }
.gh-hcard:nth-child(2) .hb-pl, .gh-hcard:nth-child(3) .hb-pl { color: var(--gold-lt); }
.hb-w { color: var(--ready-lt); }
.gh-hcard.me { border-color: var(--gold-lt); background: rgba(var(--gold-rgb), 0.13); }
.gh-hcard[disabled] { cursor: default; }
.gh-hcard[disabled]:hover { transform: none; border-color: var(--edge-quiet); background: rgba(var(--shade-rgb), 0.34); }
.gh-hcard.me[disabled]:hover { border-color: var(--gold-lt); background: rgba(var(--gold-rgb), 0.13); }

/* ---- the hall's own copy of that board ---- */
.ghouse { margin-top: var(--sp-4); padding: var(--sp-4); border-radius: var(--r-lg);
  background: rgba(var(--shade-rgb), 0.3); border: 1px solid var(--edge-quiet); }
.gs-hdr { display: flex; align-items: center; gap: 6px; margin-bottom: var(--sp-3); }
.gs-hdr .ic { width: 15px; height: 15px; color: var(--gilt-body); }
.gs-ht { font: 800 10.5px var(--font-display); letter-spacing: 1.3px; text-transform: uppercase; color: var(--gilt-body); }
.gs-mine { margin-left: auto; font: 600 11px var(--font-ui); color: var(--muted); font-variant-numeric: var(--nums); }
.gs-mine b { font: 900 14px var(--font-display); color: var(--gold-lt); }
.gs-list { display: flex; flex-direction: column; gap: 3px; }
.gs-none { font: italic 400 11.5px var(--font-ui); color: var(--muted); padding: 6px 2px; }
.gs-gap { text-align: center; font: 700 13px var(--font-display); color: var(--edge); line-height: 0.9; }
.ghouse .gh-hcard { padding: 5px 8px; }
.ghouse .hb-arms { width: 22px; height: 26px; }
.ghouse .hb-nm { font-size: 12px; }

/* ---- ON THE BOARD, OR NOT ---- */
.gh-list { display: flex; align-items: center; gap: 7px; width: 100%; margin-top: 4px; padding: 6px 8px;
  cursor: pointer; border-radius: var(--r-md); border: 1px solid var(--edge-quiet);
  background: rgba(var(--shade-rgb), 0.3); transition: border-color var(--t-2) var(--e-out); }
.gh-list:hover { border-color: var(--edge); }
.gh-list input { position: absolute; opacity: 0; width: 0; height: 0; }
.gl-tick { width: 17px; height: 17px; flex: 0 0 auto; border-radius: var(--r-sm);
  border: 1px solid var(--gilt-deep); background: rgba(var(--shade-rgb), 0.5); box-shadow: var(--seat);
  display: grid; place-items: center; font: 900 11px var(--font-display); color: transparent;
  transition: all var(--t-2) var(--e-out); }
.gh-list.on .gl-tick { background: linear-gradient(180deg, var(--gold-lt), var(--gold) 60%, var(--gold-dk));
  border-color: var(--gilt-deep); color: var(--gilt-ink); box-shadow: 0 0 10px rgba(var(--gold-rgb), 0.45); }
.gl-tx { display: flex; flex-direction: column; text-align: left; min-width: 0;
  font: 700 10.5px var(--font-display); letter-spacing: 0.6px; color: var(--gilt-body); }
.gl-tx em { font: italic 400 10px var(--font-ui); letter-spacing: 0; color: var(--muted); }

/* ---- the hall itself ---- */
.gh-hall { display: grid; grid-template-columns: minmax(180px, 230px) 1fr; gap: var(--sp-5); align-items: start; }
.gh-crestcol { display: flex; flex-direction: column; align-items: center; gap: 5px; padding: var(--sp-5) var(--sp-4);
  background: var(--plate); border: 1px solid var(--edge); border-top: 3px solid var(--gold);
  border-radius: var(--r-xl); box-shadow: var(--sh-3), var(--bevel); }
.gh-nm { font: 900 19px var(--font-display); color: var(--gold-lt); text-align: center; line-height: 1.15; margin-top: 6px; }
.gh-tg { font: 800 12px var(--font-display); letter-spacing: 3px; color: var(--gilt-body); }
.gh-mt { font: 400 12px var(--font-flavor); font-style: italic; color: var(--muted); text-align: center; line-height: 1.4; }
.gh-ct { font: 600 11px var(--font-ui); color: var(--muted); font-variant-numeric: var(--nums); margin-bottom: 4px; }
.gh-ct .gh-on { color: var(--ready-lt); }
.gh-crestcol .menubtn { width: 100%; font-size: 12px; padding: 6px 8px; }
.gh-main { display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0; }

/* ---- the guild war-band noticeboard ---- */
.gh-band { padding: var(--sp-4) var(--sp-5); background: var(--plate); border: 1px solid var(--edge-quiet);
  border-radius: var(--r-lg); box-shadow: var(--sh-2), var(--bevel); }
/* A band is the one thing in a house that EXPIRES, which is the one thing worth
   pulling somebody out of a menu for — so it is the one state here that lights. */
.gh-band.live { border-color: var(--edge-strong); box-shadow: var(--sh-2), var(--bevel), 0 0 22px rgba(var(--gold-rgb), 0.2); }
.gb-t { font: 800 13px var(--font-display); letter-spacing: 1px; text-transform: uppercase; color: var(--gold-lt);
  display: flex; align-items: center; gap: 7px; }
.gb-t .ic { width: 16px; height: 16px; }
.gb-s, .gb-s2 { font: 400 12px var(--font-ui); color: var(--muted); line-height: 1.5; margin: 4px 0 var(--sp-3); }
.gb-s b { color: var(--ink); }
.gb-s .gb-rk { color: var(--gold-lt); letter-spacing: 1.4px; }
.gb-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--gutter); }
.gb-row .bigbtn { flex: 1 1 180px; margin: 0; padding: 9px 14px; font-size: 14px; }
.gb-mode { padding: 7px 10px; font: 600 12.5px var(--font-ui); color: var(--ink);
  background: rgba(var(--shade-rgb), 0.45); border: 1px solid var(--gilt-deep); border-radius: var(--r-sm); }
.gb-rank { display: inline-flex; align-items: center; gap: 5px; font: 700 12px var(--font-display);
  letter-spacing: 0.6px; color: var(--gilt-body); cursor: pointer; }
.gb-in { font: 400 12px var(--font-ui); font-style: italic; color: var(--muted); }

/* ---- the roster ---- */
.gh-roster { display: flex; flex-direction: column; gap: 1px; background: var(--edge-quiet);
  border: 1px solid var(--edge-quiet); border-radius: var(--r-lg); overflow: hidden; max-height: 40vh; overflow-y: auto; }
.gr-row { display: flex; align-items: center; gap: var(--sp-3); padding: 5px 10px; background: rgba(var(--shade-rgb), 0.3); }
.gr-row.me { background: rgba(var(--gold-rgb), 0.11); }
.gr-dot { width: 7px; height: 7px; border-radius: var(--r-pill); flex: 0 0 auto;
  background: var(--gilt-deep); box-shadow: var(--seat); }
.gr-row.on .gr-dot { background: var(--ready); box-shadow: 0 0 7px rgba(var(--ready-rgb), 0.8); }
.gr-nm { font: 600 13px var(--font-ui); color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gr-row:not(.on) .gr-nm { color: var(--muted); }
.gr-rk { font: 700 9.5px var(--font-display); letter-spacing: 1px; text-transform: uppercase;
  color: var(--gilt-body); border: 1px solid var(--edge-quiet); border-radius: var(--r-pill); padding: 0 6px; }
.gr-rk.r1 { color: var(--gold-lt); border-color: var(--edge); }
.gr-rk.r2 { color: var(--gilt-ink); background: linear-gradient(180deg, var(--gold-lt), var(--gold-dk)); border-color: var(--gilt-deep); }
.gr-act { margin-left: auto; display: flex; gap: 4px; }
.gr-b { padding: 2px 7px; cursor: pointer; font: 700 10px var(--font-display); letter-spacing: 0.5px;
  color: var(--gilt-body); background: rgba(var(--shade-rgb), 0.4); border: 1px solid var(--edge-quiet);
  border-radius: var(--r-xs); transition: all var(--t-1) var(--e-out); }
.gr-b:hover { color: var(--gold-lt); border-color: var(--edge); }
.gr-b.bad:hover { color: var(--peril); border-color: var(--peril); }

/* ---- the hall's talk ---- */
.gchat { display: flex; flex-direction: column; padding: var(--sp-3) var(--sp-4) var(--sp-4);
  background: var(--plate); border: 1px solid var(--edge-quiet); border-radius: var(--r-lg); box-shadow: var(--bevel); }
.gc-hdr { display: flex; align-items: center; gap: 6px; padding-bottom: 5px; border-bottom: 1px solid var(--edge-quiet); }
.gc-hdr .ic { width: 15px; height: 15px; color: var(--gilt-body); }
.gc-ht { font: 800 11px var(--font-display); letter-spacing: 1.3px; text-transform: uppercase; color: var(--gold-lt); }
.gc-hint { margin-left: auto; font: 400 10.5px var(--font-ui); font-style: italic; color: var(--muted); }
.gc-log { height: clamp(120px, 22vh, 240px); overflow-y: auto; padding: 6px 2px; display: flex; flex-direction: column; gap: 3px; }
.gc-line { font: 400 12.5px var(--font-ui); color: var(--ink); line-height: 1.4; text-align: left; }
.gc-line b { color: var(--gold-lt); margin-right: 6px; font-weight: 700; }
.gc-line.sys { color: var(--muted); font-size: 11.5px; }
.gc-line.sys i { font-style: italic; }
.gc-row { display: flex; gap: 6px; margin-top: 6px; }
.gc-in { flex: 1; padding: 7px 9px; font: 500 13px var(--font-ui); color: var(--ink);
  background: rgba(var(--shade-rgb), 0.45); border: 1px solid var(--gilt-deep); border-radius: var(--r-sm); box-shadow: var(--seat); }
.gc-in::placeholder { color: #7d7159; }
.gc-in:focus { outline: none; border-color: var(--gold-lt); box-shadow: 0 0 10px rgba(var(--gold-rgb), 0.22); }
.gc-send { padding: 7px 14px; cursor: pointer; font: 700 12px var(--font-display); letter-spacing: 0.6px;
  color: var(--gold-lt); background: var(--plate); border: 1px solid var(--edge); border-radius: var(--r-sm); }
.gc-send:hover { border-color: var(--gold-lt); color: var(--gold-bright); }

/* ═══════════════════════════ THE SIGIL FORGE ══════════════════════════════ */
/* ══════════════════════════════════════════════════════════════════════
   A SEASON CLOSING — the ladder's one calendar moment
   ══════════════════════════════════════════════════════════════════════
   Four weeks of climbing settled, paid and wiped back four tiers. It used to
   be a toast, which is what this game says when a unit is unaffordable. */
.seasonlay { position: fixed; inset: 0; z-index: 72; display: flex; align-items: center; justify-content: center;
  padding: 20px; background: rgba(6, 7, 10, 0.82); backdrop-filter: blur(6px);
  opacity: 0; transition: opacity 0.24s ease; }
.seasonlay.in { opacity: 1; }
.seasonlay.out { opacity: 0; }
.seasonbox { position: relative; overflow: hidden; width: min(500px, 94vw); max-height: 92vh; overflow-y: auto;
  padding: 22px 24px 18px; border-radius: var(--r-2xl); text-align: center;
  background: linear-gradient(170deg, rgba(46, 37, 22, 0.99), rgba(13, 11, 8, 0.99));
  border: 1px solid var(--edge); border-top: 3px solid var(--gold);
  box-shadow: var(--sh-4), 0 0 50px rgba(var(--gold-rgb), 0.2);
  transform: translateY(14px) scale(0.985); transition: transform 0.28s var(--e-out); }
.seasonlay.in .seasonbox { transform: none; }
.ss-rays { position: absolute; left: 50%; top: -40%; width: 150%; height: 150%; transform: translateX(-50%);
  pointer-events: none; opacity: 0;
  background: conic-gradient(from 0deg,
    rgba(var(--gold-rgb),0.22) 0deg 6deg, transparent 6deg 40deg,
    rgba(var(--gold-rgb),0.22) 40deg 46deg, transparent 46deg 80deg,
    rgba(var(--gold-rgb),0.22) 80deg 86deg, transparent 86deg 120deg,
    rgba(var(--gold-rgb),0.22) 120deg 126deg, transparent 126deg 160deg,
    rgba(var(--gold-rgb),0.22) 160deg 166deg, transparent 166deg 200deg,
    rgba(var(--gold-rgb),0.22) 200deg 206deg, transparent 206deg 240deg,
    rgba(var(--gold-rgb),0.22) 240deg 246deg, transparent 246deg 280deg,
    rgba(var(--gold-rgb),0.22) 280deg 286deg, transparent 286deg 320deg,
    rgba(var(--gold-rgb),0.22) 320deg 326deg, transparent 326deg 360deg);
  -webkit-mask-image: radial-gradient(closest-side, #000 8%, transparent 62%);
          mask-image: radial-gradient(closest-side, #000 8%, transparent 62%); }
.seasonlay.in .ss-rays { animation: ssRays 9s linear infinite, ssFade 1.1s ease both; }
@keyframes ssRays { to { transform: translateX(-50%) rotate(360deg); } }
@keyframes ssFade { to { opacity: 1; } }
.ss-hd { position: relative; z-index: 1; }
.ss-k { color: var(--gold-bright); }
.ss-k .ic { width: 34px; height: 34px; filter: drop-shadow(0 0 14px rgba(var(--gold-rgb), 0.6)); }
.ss-t { font: 900 23px var(--font-display); color: var(--gold-lt); letter-spacing: 1.4px; margin-top: 2px; }
.ss-s { font: 400 12px var(--font-ui); color: var(--muted); line-height: 1.5; margin: 6px auto 0; max-width: 40ch; }
.ss-rows { position: relative; z-index: 1; display: flex; flex-direction: column; gap: 6px; margin: var(--sp-5) 0; }
.ss-row { display: flex; align-items: center; gap: var(--sp-4); text-align: left; padding: 9px 11px;
  border-radius: var(--r-lg); background: rgba(var(--shade-rgb), 0.42);
  border: 1px solid color-mix(in srgb, var(--rc, #a2703c) 42%, transparent); }
.ss-rx { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.ss-q { font: 800 9.5px var(--font-display); letter-spacing: 1.2px; text-transform: uppercase; color: var(--gilt-body); }
.ss-fin { font: 600 13px var(--font-ui); color: var(--ink); }
.ss-fin b { color: var(--rcl, var(--gold-lt)); font-family: var(--font-display); }
.ss-now { font: italic 400 11.5px var(--font-ui); color: var(--muted); }
.ss-now b { font-style: normal; color: var(--gilt-body); }
.ss-g { flex: none; font: 900 17px var(--font-display); color: var(--gold-lt); font-variant-numeric: var(--nums); }
.ss-purse { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; gap: 1px;
  padding: 10px 0 4px; border-top: 1px solid var(--edge-quiet); }
.ss-purse span { font: 800 9.5px var(--font-display); letter-spacing: 1.6px; text-transform: uppercase; color: var(--gilt-body); }
.ss-purse b { font: 900 34px var(--font-display); color: var(--gold-bright); font-variant-numeric: var(--nums);
  text-shadow: 0 0 22px rgba(var(--gold-rgb), 0.45); }
.ss-none { position: relative; z-index: 1; font: italic 400 12px var(--font-ui); color: var(--muted);
  padding: 10px 0 4px; border-top: 1px solid var(--edge-quiet); }
.seasonbox .bigbtn { position: relative; z-index: 1; margin: var(--sp-4) 0 0; }
@media (prefers-reduced-motion: reduce) {
  .seasonlay.in .ss-rays { animation: ssFade 0.5s ease both; }
}

/* ══════════════════════════════════════════════════════════════════════
   ASK — the game's own way of asking a question
   ══════════════════════════════════════════════════════════════════════
   Replaces prompt() and confirm(), which arrive in the browser's chrome, in
   the browser's font, at the top of the window — and on a phone often do not
   arrive at all. Above the forge (z 62) because it is asked FROM panels. */
.asklay { position: fixed; inset: 0; z-index: 70; display: flex; align-items: center; justify-content: center;
  padding: 20px; background: rgba(6, 7, 10, 0.72); backdrop-filter: blur(4px);
  opacity: 0; transition: opacity 0.16s ease; }
.asklay.in { opacity: 1; }
.asklay.out { opacity: 0; }
.askbox { width: min(430px, 94vw); padding: 20px 22px 16px; border-radius: var(--r-2xl); text-align: center;
  background: linear-gradient(170deg, rgba(44, 36, 23, 0.98), rgba(13, 11, 8, 0.99));
  border: 1px solid var(--edge); border-top: 3px solid var(--gold);
  box-shadow: var(--sh-4), 0 0 40px rgba(var(--gold-rgb), 0.14);
  transform: translateY(10px) scale(0.99); transition: transform 0.2s var(--e-out); }
.asklay.in .askbox { transform: none; }
/* A question that cannot be taken back is asked in the colour of the thing it
   is about to do, so the weight of it is legible before the words are read. */
.asklay.peril .askbox { border-top-color: var(--peril); box-shadow: var(--sh-4), 0 0 40px rgba(214, 74, 60, 0.16); }
.ask-t { font: 800 19px var(--font-display); color: var(--gold-lt); letter-spacing: 0.6px; }
.asklay.peril .ask-t { color: #ffb3aa; }
.ask-s { font: 400 12.5px var(--font-ui); color: var(--muted); line-height: 1.5; margin-top: 6px; }
.ask-in { width: 100%; margin-top: var(--sp-4); padding: 9px 11px; border-radius: var(--r-md);
  font: 500 14px var(--font-ui); color: var(--ink); text-align: center;
  background: rgba(0,0,0,0.42); border: 1px solid var(--edge-quiet); }
.ask-in:focus { outline: none; border-color: var(--gold-lt); box-shadow: 0 0 0 2px rgba(var(--gold-rgb), 0.18); }
.ask-btns { display: flex; gap: var(--sp-3); justify-content: center; margin-top: var(--sp-5); }
.ask-btns .menubtn { margin: 0; padding: 8px 18px; }
.ask-btns .ask-yes.bad { color: #ffb3aa; border-color: rgba(214, 74, 60, 0.5); }
.ask-btns .ask-yes.bad:hover { border-color: #ffb3aa; background: rgba(214, 74, 60, 0.16); }

.forgelay { position: fixed; inset: 0; z-index: 62; display: flex; align-items: center; justify-content: center;
  padding: 20px; background: rgba(6, 7, 10, 0.74); backdrop-filter: blur(5px);
  opacity: 0; transition: opacity 0.18s ease; }
.forgelay.in { opacity: 1; }
.forgelay.out { opacity: 0; }
.forgebox { position: relative; width: min(760px, 96vw); max-height: 92vh; overflow: auto;
  padding: 20px 22px 16px; border-radius: var(--r-2xl);
  background: linear-gradient(170deg, rgba(44, 36, 23, 0.98), rgba(13, 11, 8, 0.99));
  border: 1px solid var(--edge); border-top: 3px solid var(--gold);
  box-shadow: var(--sh-4), 0 0 40px rgba(var(--gold-rgb), 0.14);
  transform: translateY(10px) scale(0.99); transition: transform 0.22s var(--e-out); }
.forgelay.in .forgebox { transform: none; }
.fg-hd { text-align: center; margin-bottom: var(--sp-5); }
.fg-t { font: 800 22px var(--font-display); color: var(--gold-lt); letter-spacing: 1px; }
.fg-s { font: 400 12.5px var(--font-ui); font-style: italic; color: var(--muted); }
.fg-body { display: grid; grid-template-columns: minmax(170px, 210px) 1fr; gap: var(--sp-5); align-items: start; }
.fg-prev { display: flex; flex-direction: column; align-items: center; gap: var(--sp-3); }
.fg-big { filter: drop-shadow(0 6px 18px rgba(var(--shade-rgb), 0.6)); }
.fg-sizes { display: flex; align-items: flex-end; gap: var(--sp-3); }
.fg-sl { font: 400 10px var(--font-ui); font-style: italic; color: var(--muted); max-width: 90px; line-height: 1.3; }
/* THE RULE OF TINCTURE, said out loud and not enforced — see the note in ui.js.
   It is advice in the game's own voice, so it wears the muted hand rather than
   the peril colour: nothing here is being taken from anybody. */
.fg-warn { font: 400 11px var(--font-ui); color: var(--ready-lt); text-align: center; line-height: 1.4; min-height: 30px; }
.fg-warn.on { color: var(--gilt-body); }
.fg-picks { display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0; }
.fg-grp { transition: opacity var(--t-2) var(--e-out); }
/* A choice that cannot matter yet — a second tincture on a plain field, a
   charge tincture with no charge — is dimmed rather than removed, so the panel
   does not re-lay itself every time a division is picked. */
.fg-grp.off { opacity: 0.35; }
.fg-gl { font: 800 10px var(--font-display); letter-spacing: 1.4px; text-transform: uppercase;
  color: var(--gilt-body); margin-bottom: 5px; }
.fg-divs, .fg-chs { display: flex; flex-wrap: wrap; gap: 5px; }
.fg-div, .fg-ch { padding: 2px; cursor: pointer; line-height: 0; background: rgba(var(--shade-rgb), 0.4);
  border: 1px solid var(--edge-quiet); border-radius: var(--r-xs); transition: all var(--t-1) var(--e-out); }
.fg-div:hover, .fg-ch:hover { border-color: var(--edge); transform: translateY(-1px); }
.fg-div.on, .fg-ch.on { border-color: var(--gold-lt); box-shadow: 0 0 10px rgba(var(--gold-rgb), 0.4); }
.fg-sws { display: flex; flex-wrap: wrap; gap: 5px; }
.fg-sw { width: 30px; height: 30px; padding: 3px; cursor: pointer; background: rgba(var(--shade-rgb), 0.4);
  border: 1px solid var(--edge-quiet); border-radius: var(--r-xs); transition: all var(--t-1) var(--e-out); }
.fg-sw span { display: block; width: 100%; height: 100%; border-radius: 2px; background: var(--tc);
  box-shadow: var(--seat), inset 0 1px 0 rgba(255, 255, 255, 0.25); }
.fg-sw:hover { border-color: var(--edge); transform: translateY(-1px); }
.fg-sw.on { border-color: var(--gold-lt); box-shadow: 0 0 10px rgba(var(--gold-rgb), 0.45); }
.fg-foot { display: flex; gap: var(--gutter); justify-content: center; align-items: center; margin-top: var(--sp-5); }
.fg-foot .bigbtn { margin: 0; }

/* ═══════════════════════════ THE CHALLENGE GRID ═══════════════════════════ */
.chalbody { padding: 0 clamp(10px, 2.4vw, 26px) 16px; overflow: auto;
  width: 100%; max-width: min(1780px, 98vw); margin-inline: auto; }
.cg-intro { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-5); margin-bottom: var(--sp-4);
  padding: var(--sp-3) var(--sp-5); background: linear-gradient(160deg, rgba(36,30,22,0.99), rgba(14,11,8,1)); border: 1px solid var(--edge-quiet);
  border-radius: var(--r-lg); box-shadow: var(--bevel); }
.cg-ix { flex: 1 1 320px; font: 400 12.5px var(--font-ui); color: var(--muted); line-height: 1.5; }
.cg-ix b { color: var(--ink); }
/* THE ACCOUNT BADGE IS FIXED TO THE TOP-RIGHT AND THIS PLATE IS NOT. On a wide
   window the plate stretched under it and the two printed over each other —
   "0 / 49 squares" with a sign-out button sitting on top of the words. The
   badge's own measured row bottom is published as `--chiprow` (see
   UI.refreshTop), so the progress block simply keeps out of that lane rather
   than guessing at a number that changes with the viewport. */
.cg-prog { flex: 0 0 auto; min-width: 210px; margin-right: clamp(0px, calc(100vw - 1500px), 150px); }
.cg-pbar { height: 8px; border-radius: var(--r-pill); background: rgba(var(--shade-rgb), 0.55);
  border: 1px solid var(--gilt-seat); box-shadow: var(--seat); overflow: hidden; }
.cg-pbar i { display: block; height: 100%; background: linear-gradient(90deg, var(--gold-dk), var(--gold-lt));
  box-shadow: 0 0 10px rgba(var(--gold-rgb), 0.5); transition: width var(--t-3) var(--e-out); }
/* A METER WITH NO LABEL IS A LINE. At zero the bar is invisible, so the words
   under it were carrying the whole readout on their own — and they read as a
   fragment rather than as progress through a board of forty-nine. */
.cg-plab { display: block; font: 800 8.5px var(--font-display); letter-spacing: 1.4px;
  text-transform: uppercase; color: var(--gilt-body); margin-bottom: 3px; }
.cg-pn { font: 600 12px var(--font-ui); color: var(--muted); margin-top: 3px; font-variant-numeric: var(--nums); }
.cg-pn b { color: var(--gold-lt); font-size: 14px; }
.cg-pn span { color: var(--gilt-body); }

/* THE PICK BAR IS NEVER BELOW THE FOLD. The board is the tall thing on this
   screen and the bar under it is the only control on it, so the board takes
   whatever room is left and scrolls inside that — rather than growing until the
   button a commander came here to press is off the bottom of a short window. */
.chalbody { display: flex; flex-direction: column; min-height: 0; }
.cg-wrap { flex: 1 1 auto; min-height: 0; overflow: auto; }
.cg-pickbar { flex: none; }
/* ROWS ARE THE BANNER YOU CARRY, COLUMNS THE ONE YOU FACE. The corner cell says
   so in two words rather than a legend somewhere else on the page. */
/* THE BOARD IS SQUARE-ISH, NOT LETTERBOXED. Stretched to a 1900px window each
   cell became 245x52 with one small number adrift in the middle of it, which is
   most of why the screen read as a placeholder: forty-nine wide empty plates.
   Capped and centred, a cell is about as tall as it is wide, which is the shape
   that can hold two crests and still be scanned as a grid. */
/* BIGGER, AND WITH A PRIZE COLUMN ON THE END. The board was capped at 1180px on
   the reasoning that a stretched cell is a wasted cell — right about the stretch
   and wrong about the cap, because at ten kingdoms a 1180px board is a 66px cell
   with two crests and three pips crammed into it. It takes the room it has now,
   up to a cap that keeps a cell roughly square, and the last column is what the
   row pays: that kingdom's twelfth keep. */
.cg-grid { display: grid;
  grid-template-columns: minmax(128px, 168px) repeat(var(--n), minmax(78px, 1fr)) minmax(78px, 104px);
  gap: 4px; min-width: 560px; max-width: min(1720px, 96vw); margin: 0 auto; }
/* the prize itself: the keep, and how close the row is to it */
.cg-prize {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
  padding: 4px 3px; border-radius: var(--r-sm);
  border: 1px dashed color-mix(in srgb, var(--fac) 45%, var(--edge-quiet));
  background: linear-gradient(180deg, rgba(var(--shade-rgb), 0.45), rgba(0,0,0,0.30));
}
/* UNWON IS NOT HIDDEN, IT IS UNLIT. A prize you cannot see is not a prize; a
   greyed one you can see the shape of is the thing worth going and getting. */
.cg-prize .cg-pz { width: 100%; height: auto; max-width: 52px; filter: grayscale(1) brightness(0.5); opacity: 0.62; }
.cg-prize.won { border-style: solid; border-color: var(--gold-dk);
  background: linear-gradient(180deg, rgba(58,47,29,0.75), rgba(18,14,9,0.75));
  box-shadow: 0 0 14px rgba(var(--gold-rgb), 0.22); }
.cg-prize.won .cg-pz { filter: none; opacity: 1; }
.cg-pzn { font: 800 8.5px var(--font-display); letter-spacing: 0.6px; color: var(--gilt-body);
  text-align: center; line-height: 1.15; overflow-wrap: anywhere; }
.cg-prize.won .cg-pzn { color: var(--gold-lt); }
.cg-pzhd { align-items: flex-end; }
.cg-corner { display: flex; flex-direction: column; justify-content: space-between; align-items: flex-start;
  padding: 4px 6px; font: 800 9.5px var(--font-display); letter-spacing: 1.2px; text-transform: uppercase; color: var(--gilt-body); }
.cg-corner span:last-child { align-self: flex-end; }
.cg-colhd, .cg-rowhd { display: flex; align-items: center; gap: 4px; padding: 4px 5px;
  background: linear-gradient(180deg, color-mix(in srgb, var(--fac) 30%, #16130e), #100e0a);
  border: 1px solid var(--edge-quiet); border-radius: var(--r-sm); }
.cg-colhd { flex-direction: column; text-align: center; }
.cg-colhd span, .cg-rowhd span { font: 700 9.5px var(--font-display); letter-spacing: 0.4px; color: var(--facl);
  line-height: 1.1; }
/* A KINGDOM'S NAME IS NEVER CUT. "Undead Domi…" is not a kingdom — it is a
   rendering fault the reader has to decode, on the one screen whose whole job
   is naming the seven of them. The name wraps to a second line instead, and the
   header column is wide enough that it rarely has to. */
.cg-rowhd span { font-size: 11px; white-space: normal; overflow-wrap: anywhere; }
.cg-crest { flex: 0 0 auto; }

/* ---- HOW CLOSE THIS ROW OR COLUMN IS ----
   Each is worth a second purse and both were invisible until the moment they
   landed — so nobody was ever three squares from a bonus they knew about, which
   is the only state in which a bonus does any work. A hairline bar and a count,
   at a weight that stays behind the kingdom's name rather than competing with
   it. The exact figure and what it pays are on the header's own tooltip. */
.cg-hp { flex: none; height: 3px; border-radius: var(--r-pill); overflow: hidden;
  background: rgba(0,0,0,0.45); box-shadow: inset 0 0 0 1px rgba(255,255,255,0.05); }
/* A ROW HEADER IS NARROW AND THE NAME IS THE POINT OF IT. Laid out as crest +
   a stacked text column rather than one flat row, so the bar and the count sit
   UNDER the kingdom rather than eating the width it needs — the first cut of
   this clipped every name to "Or…" to make room for three pixels of bar. */
.cg-rowhd { display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto;
  column-gap: 5px; row-gap: 2px; align-items: center; }
.cg-rowhd .cg-crest { grid-row: 1 / span 2; }
.cg-rowhd > span:not(.cg-hp):not(.cg-hn) { grid-column: 2; align-self: end; }
.cg-rowhd .cg-hp { grid-column: 2; width: auto; align-self: start; margin-right: 26px; }
.cg-rowhd .cg-hn { grid-column: 2; grid-row: 2; justify-self: end; align-self: start; }
.cg-colhd .cg-hp { width: 78%; }
.cg-hp i { display: block; height: 100%; background: var(--facl);
  box-shadow: 0 0 5px color-mix(in srgb, var(--facl) 70%, transparent);
  transition: width var(--t-2) var(--e-out); }
.cg-hn { flex: none; font: 700 8.5px var(--font-ui) !important; color: var(--muted) !important;
  letter-spacing: 0 !important; font-variant-numeric: var(--nums); }

/* Cleared: the whole header takes the gold this game reserves for something
   finished, and the count becomes the star the cells use for the same thing. */
.cg-rowhd.full, .cg-colhd.full { border-color: rgba(var(--gold-rgb), 0.55);
  box-shadow: 0 0 10px rgba(var(--gold-rgb), 0.2); }
.cg-rowhd.full .cg-hn, .cg-colhd.full .cg-hn { color: var(--gold-bright) !important; font-size: 11px !important; }
.cg-rowhd.full .cg-hp, .cg-colhd.full .cg-hp { display: none; }

/* EVERY CELL IS A PLATE, not a tint over the field. The board is drawn over the
   menu's live battlefield, and at the alpha the two banner colours wanted the
   terrain read straight through forty-nine squares of it — so the plate is laid
   opaque first and the two kingdoms tint THAT. The gradient still runs from the
   banner you carry to the banner you face, which is the one thing a cell has to
   say before you read the number on it. */
/* The pip row owns the bottom 9px of every cell, so nothing centred in the box
   can land on top of it — a price sitting under three pips was unreadable. */
.cg-cell { position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 1px; min-height: 58px; cursor: pointer;
  padding: 5px 4px 10px;
  background: linear-gradient(150deg, color-mix(in srgb, var(--fac) 20%, #14120d),
                                       color-mix(in srgb, var(--foe) 20%, #0d0c09));
  border: 1px solid var(--edge-quiet); border-radius: var(--r-sm);
  transition: transform var(--t-1) var(--e-out), border-color var(--t-1) var(--e-out), box-shadow var(--t-1) var(--e-out); }
.cg-cell:hover { transform: translateY(-2px); border-color: var(--edge); box-shadow: var(--sh-2); z-index: 1; }
.cg-cell.sel { border-color: var(--gold-lt); box-shadow: 0 0 16px rgba(var(--gold-rgb), 0.5); z-index: 2; }
/* THE DIAGONAL — a kingdom against itself. Marked, because it is the one square
   in every row that is a different KIND of match, and it pays a bonus for it. */
.cg-cell.mirror::before { content: ''; position: absolute; inset: 3px; border-radius: 3px;
  border: 1px dashed rgba(var(--gold-rgb), 0.32); pointer-events: none; }
.cg-cell.won { background: linear-gradient(150deg, color-mix(in srgb, var(--ready) 24%, #101410), #0b0e0b);
  border-color: rgba(var(--ready-rgb), 0.5); }
/* ---- THE MATCHUP, WHICH IS THE WHOLE CONTENT OF A SQUARE ----
   Your crest, the blades, theirs. The one thing about this cell that is true of
   no other cell on the board — where the price it used to print was true of
   forty-two of them at once. */
.cg-duel { display: flex; align-items: center; justify-content: center; gap: 3px; }
.cg-cc { flex: none; width: 19px; height: 23px; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.55)); }
.cg-x { font-style: normal; font-size: 9px; line-height: 1; color: rgba(255,255,255,0.34); flex: none; }
.cg-cell:hover .cg-x, .cg-cell.sel .cg-x { color: var(--gold-lt); }
.cg-state { font: 700 9.5px var(--font-display); letter-spacing: 0.9px; text-transform: uppercase;
  line-height: 1; font-variant-numeric: var(--nums); }
/* "Take" is an invitation and reads as one only while it is being considered —
   at rest it stays quiet so the board reads as banners rather than as buttons. */
.cg-take { color: var(--gold-lt); opacity: 0; font-variant-numeric: var(--nums);
  transition: opacity var(--t-1) var(--e-out); }
.cg-cell:hover .cg-take, .cg-cell.sel .cg-take, .cg-cell.next .cg-take { opacity: 1; }
/* The state line always holds its own height, so revealing the purse never
   makes a cell jump — a board that reflows under the cursor is a board that is
   hard to aim at. */
.cg-state { min-height: 11px; }
.cg-got { color: var(--ready-lt); }
.cg-star { font-size: 13px; color: var(--gold-bright); text-shadow: 0 0 9px rgba(var(--gold-rgb), 0.7); }
.cg-price { font: 700 11px var(--font-ui); color: var(--gilt-body); font-variant-numeric: var(--nums); }
.cg-tick { font: 900 17px var(--font-display); color: var(--ready-lt); line-height: 1;
  text-shadow: 0 0 10px rgba(var(--ready-rgb), 0.6); }
.cg-paid { font: 600 9.5px var(--font-ui); color: var(--muted); font-variant-numeric: var(--nums); }
/* ---- THE THREE FOES, AS THREE PIPS ----
   A tick said "done" and nothing else, so a square taken once looked exactly
   like a square taken three times — and two thirds of the board's fights were
   invisible. Three pips along the bottom edge say what is left, at a size that
   reads as texture across forty-nine cells and as a count on the one you are
   looking at. */
.cg-pips { position: absolute; left: 0; right: 0; bottom: 3px; display: flex; justify-content: center; gap: 3px;
  pointer-events: none; }
.cg-pip { width: 12px; height: 3px; border-radius: var(--r-pill); flex: none;
  background: rgba(255,255,255,0.13); box-shadow: inset 0 0 0 1px rgba(0,0,0,0.5); }
.cg-pip.on { background: var(--ready-lt); box-shadow: 0 0 5px rgba(var(--ready-rgb), 0.75); }
/* Every foe on the square beaten: the tick becomes a star and the cell takes
   the gold the rest of the game reserves for something finished. */
.cg-cell.full { border-color: rgba(var(--gold-rgb), 0.62);
  background: linear-gradient(150deg, color-mix(in srgb, var(--gold) 26%, #141008), #0d0a05); }
.cg-cell.full .cg-tick { color: var(--gold-bright); text-shadow: 0 0 11px rgba(var(--gold-rgb), 0.75); }
/* ---- WHERE WAS I ----
   Forty-nine buttons with nothing distinguishing them is a wall, and "where was
   I" is the question a returning commander always has. The next unfought square
   breathes — quietly, and only while nothing is selected. */
.cg-cell.next { border-color: rgba(var(--gold-rgb), 0.55); animation: cgNext 2.4s ease-in-out infinite; }
@keyframes cgNext {
  0%, 100% { box-shadow: 0 0 0 rgba(var(--gold-rgb), 0); }
  50%      { box-shadow: 0 0 13px rgba(var(--gold-rgb), 0.42); }
}
.cg-cell:focus-visible { outline: 2px solid var(--gold-lt); outline-offset: 1px; z-index: 3; }
@media (prefers-reduced-motion: reduce) { .cg-cell.next { animation: none; box-shadow: 0 0 10px rgba(var(--gold-rgb), 0.35); } }

.cg-pickbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: var(--sp-5);
  margin-top: var(--sp-4); padding: var(--sp-3) var(--sp-5); min-height: 54px;
  background: linear-gradient(160deg, rgba(36,30,22,0.99), rgba(14,11,8,1)); border: 1px solid var(--edge-quiet);
  border-radius: var(--r-lg); box-shadow: var(--bevel); }
.cg-pickbar.on { border-color: var(--edge); box-shadow: var(--sh-2), var(--bevel); }
.cg-hint { font: 400 12.5px var(--font-ui); font-style: italic; color: var(--muted); }
.cg-pk { display: flex; align-items: baseline; gap: var(--sp-3); }
.cg-pkm, .cg-pkf { font: 800 17px var(--font-display); color: var(--gold-lt); }
.cg-pkv { font: 400 12px var(--font-ui); font-style: italic; color: var(--muted); }
.cg-pkr { display: flex; align-items: center; gap: var(--sp-4); }
.cg-pkg { font: 600 13px var(--font-ui); color: var(--muted); }
.cg-pkg b { font: 800 17px var(--font-display); color: var(--gold-lt); font-variant-numeric: var(--nums); }
.cg-pkg em { font-style: italic; font-size: 11px; color: var(--gilt-body); }
.cg-pkdone { font: 400 12.5px var(--font-ui); font-style: italic; color: var(--muted); max-width: 300px; }
/* ---- WHICH FOE ----
   Three buttons, each carrying its own trade: the difficulty in the name, the
   purse under it. The board printed one price and dealt one fight; this is the
   choice that was already in the rules with nowhere to make it. */
.cg-tiers { display: flex; gap: 5px; }
.cg-tier { display: flex; flex-direction: column; align-items: center; gap: 1px; cursor: pointer;
  padding: 5px 11px; min-width: 74px; border-radius: var(--r-md);
  background: rgba(var(--shade-rgb), 0.44); border: 1px solid var(--edge-quiet);
  transition: border-color var(--t-1) var(--e-out), background var(--t-1) var(--e-out), transform var(--t-1) var(--e-out); }
.cg-tier:hover { border-color: var(--edge); transform: translateY(-1px); }
.cg-tier.on { border-color: var(--gold-lt); background: rgba(var(--gold-rgb), 0.14);
  box-shadow: 0 0 12px rgba(var(--gold-rgb), 0.3); }
.ct-n { font: 900 13px var(--font-display); color: var(--gilt-body); letter-spacing: 1px; line-height: 1; }
.cg-tier.on .ct-n { color: var(--gold-bright); }
.ct-nm { font: 700 9.5px var(--font-display); letter-spacing: 1.1px; text-transform: uppercase; color: var(--muted); }
.cg-tier.on .ct-nm { color: var(--ink); }
.ct-g { font: 600 10px var(--font-ui); color: var(--gilt-body); font-variant-numeric: var(--nums); }
/* A foe already beaten is not hidden — a commander may want the practice — but
   it stops advertising a purse it will not pay. */
.cg-tier.had .ct-g { color: var(--ready-lt); }
.cg-tier.had .ct-n, .cg-tier.had .ct-nm { opacity: 0.62; }
.cg-blurb { font: italic 400 12px var(--font-ui); color: var(--muted); flex-basis: 100%; text-align: center;
  margin-top: -2px; }
.rct { color: var(--gold-lt); }
.cg-pickbar .bigbtn { margin: 0; padding: 9px 20px; font-size: 15px; }

/* ── NARROW SCREENS ────────────────────────────────────────────────────────
   Every panel above is a two-column layout over a scrolling body, and on a
   phone in landscape there is no second column to have. They collapse to one
   rather than shrinking, because a 90px-wide roster is not a roster. */
@media (max-width: 860px) {
  .gh-hall { grid-template-columns: 1fr; }
  .gh-crestcol { flex-direction: row; flex-wrap: wrap; justify-content: center; }
  .gh-crestcol .menubtn { width: auto; }
  .fg-body { grid-template-columns: 1fr; }
  .rk-rail { grid-template-columns: repeat(3, 1fr); }
  .rk-hero { flex-direction: column; text-align: center; }
  .rk-rec { justify-content: center; }
}

/* ── A SHORT DESKTOP WINDOW ─────────────────────────────────────────────────
   #rankbadge is `position: fixed`, not part of the menu screen's own layout, so
   UI.fitScreen never sees it and never shrinks it — a short BROWSER window (not
   a phone; those hide this card outright, see html.phone above) is the one case
   left where it could run past the bottom of the viewport. It compacts instead:
   the record line goes (it is one press away, on the ladder screen), and the
   rows tighten. Everything that decides anything stays. */
@media (max-height: 620px) {
  .lp-hd { padding: 4px 9px; }
  .lp-ttl { font-size: 10.5px; }
  .lp-season, .lp-left { font-size: 10px; }
  .lp-card { padding: 5px 8px; gap: 7px; }
  .lp-rec { display: none; }
  .lp-stakes em { display: none; }
  .lp-rk { font-size: 13px; }
  .rcrest.sm svg { width: 30px; height: 35px; }
  .piprow .pip { width: 8px; height: 8px; }
  .piprow .pip.on { width: 9px; height: 9px; }
}

/* ═══════════════════════════ THE SEVEN MARCHES ═══════════════════════════
   The board is a grid you may enter anywhere; a march is a LINE you walk. Two
   views on one screen because they are the same journey at two scales — the
   realm, which is ten marches, and one march, which is ten fields on a road
   with a briefing at the end of the finger. */
.campbody { padding: 0 clamp(10px, 2.4vw, 26px) 16px; overflow: auto;
  display: flex; flex-direction: column; min-height: 0;
  width: 100%; max-width: min(1740px, 98vw); margin-inline: auto; }
.cp-intro { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-5); margin-bottom: var(--sp-4);
  padding: var(--sp-3) var(--sp-5); background: linear-gradient(160deg, rgba(36,30,22,0.99), rgba(14,11,8,1));
  border: 1px solid var(--edge-quiet); border-radius: var(--r-lg); box-shadow: var(--bevel); }
.cp-ix { flex: 1 1 320px; font: 400 12.5px var(--font-ui); color: var(--muted); line-height: 1.5; }
.cp-ix b { color: var(--ink); }
/* Same lane-keeping as the board's: the account badge is fixed top-right and
   this plate is not, so it stops short of the measured chip row. */
.cp-prog { flex: 0 0 auto; min-width: 230px; margin-right: clamp(0px, calc(100vw - 1500px), 150px); }
.cp-plab { display: block; font: 800 8.5px var(--font-display); letter-spacing: 1.4px;
  text-transform: uppercase; color: var(--gilt-body); margin-bottom: 3px; }
.cp-pbar { height: 8px; border-radius: var(--r-pill); background: rgba(var(--shade-rgb), 0.55);
  border: 1px solid var(--gilt-seat); box-shadow: var(--seat); overflow: hidden; }
.cp-pbar i { display: block; height: 100%; background: linear-gradient(90deg, var(--gold-dk), var(--gold-lt));
  box-shadow: 0 0 10px rgba(var(--gold-rgb), 0.5); transition: width var(--t-3) var(--e-out); }
.cp-pn { font: 600 12px var(--font-ui); color: var(--muted); margin-top: 3px; font-variant-numeric: var(--nums); }
.cp-pn b { color: var(--gold-lt); font-size: 14px; }
.cp-pn span { color: var(--gilt-body); }

/* ---- THE REALM: ten marches ----
   Cards rather than a list, because each one is a place with a name, a rival, a
   ten-battle shape and a prize — six things, which is a card. Auto-fill so the
   seven reflow from one column on a phone to three or four on a desk without a
   breakpoint per shape. */
/* BIGGER CARDS. At 300px a march card held a ten-word title, a rival line and ten
   pips with nothing left over — and it now carries two rewards rather than one, so
   it needs the room to show them. Wider minimum, wider column. */
.cp-marches { display: grid; gap: var(--gutter); align-content: start;
  grid-template-columns: repeat(auto-fill, minmax(392px, 1fr));
  max-width: min(1680px, 96vw); margin: 0 auto; width: 100%; }
/* the second reward: this kingdom's twelfth Builder, unlit until the march is walked */
.cp-mskin .cp-msk { flex: 0 0 auto; width: 34px; height: 34px;
  filter: grayscale(1) brightness(0.55); opacity: 0.66; }
.cp-mskin.won .cp-msk { filter: none; opacity: 1; }
.cp-mskin b { color: var(--gold-lt); }
.cp-march { display: flex; flex-direction: column; gap: 6px; text-align: left; cursor: pointer;
  padding: var(--sp-4) var(--sp-4) var(--sp-3); border-radius: var(--r-lg);
  background: linear-gradient(165deg, color-mix(in srgb, var(--fac) 26%, #16130e), #0d0b08);
  border: 1px solid var(--edge-quiet); box-shadow: var(--sh-2), var(--bevel);
  transition: transform var(--t-1) var(--e-out), border-color var(--t-1) var(--e-out), box-shadow var(--t-1) var(--e-out); }
.cp-march:hover { transform: translateY(-2px); border-color: var(--edge); box-shadow: var(--sh-3), var(--bevel); }
.cp-march:focus-visible { outline: 2px solid var(--gold-lt); outline-offset: 2px; }
/* WHERE YOU LEFT OFF, on a screen with seven possible answers. One card wears
   the cue and it is the only spark on the page. */
.cp-march.cue { border-color: var(--edge-strong); box-shadow: 0 0 24px rgba(var(--gold-rgb), 0.22), var(--bevel); }
.cp-march.done { border-color: rgba(var(--ready-rgb), 0.42); }
.cp-mtop { display: flex; align-items: center; gap: var(--sp-3); }
.cp-mcrest { flex: 0 0 auto; }
.cp-mnames { flex: 1 1 auto; min-width: 0; }
/* A MARCH'S NAME IS NEVER CUT. It is the one thing on the card that names the
   thing, and "The Emberwastes Ris…" is a rendering fault the reader decodes. */
.cp-mname { display: block; font: 800 14.5px var(--font-display); color: var(--gold-lt);
  line-height: 1.2; white-space: normal; overflow-wrap: anywhere; }
.cp-mwho { display: block; font: 500 10.5px var(--font-ui); color: var(--facl); opacity: 0.9; margin-top: 2px; }
.cp-mseal { flex: 0 0 auto; font: 900 18px var(--font-display); color: var(--ready-lt);
  text-shadow: 0 0 10px rgba(var(--ready-rgb), 0.5); }
.cp-mtell { font: 400 11.5px var(--font-ui); color: var(--muted); line-height: 1.45; min-height: 2.9em; }
/* Ten pips, one per battle: the SHAPE of the march, which a percentage cannot
   say. A rival battle is ringed, because those three are what it turns on. */
.cp-mdots { display: flex; gap: 3px; }
.cp-dot { flex: 1 1 0; height: 6px; border-radius: var(--r-pill); background: rgba(var(--shade-rgb), 0.5);
  border: 1px solid var(--gilt-seat); }
.cp-dot.riv { border-color: rgba(var(--peril-rgb), 0.75); }
.cp-dot.on { background: linear-gradient(90deg, var(--gold-dk), var(--gold-lt)); border-color: var(--gilt-lit); }
.cp-dot.at { background: rgba(var(--gilt-lit-rgb), 0.35); border-color: var(--gilt-lit);
  animation: cpPulse 1.9s ease-in-out infinite; }
@keyframes cpPulse { 0%, 100% { box-shadow: 0 0 0 rgba(var(--gold-rgb), 0); } 50% { box-shadow: 0 0 9px rgba(var(--gold-rgb), 0.7); } }
.cp-mfoot { display: flex; align-items: center; gap: var(--sp-3); }
.cp-mbar { flex: 1 1 auto; height: 3px; border-radius: var(--r-pill); overflow: hidden;
  background: rgba(var(--shade-rgb), 0.55); border: 1px solid var(--gilt-seat); }
.cp-mbar i { display: block; height: 100%; background: var(--gold); }
.cp-mn { flex: none; font: 700 10.5px var(--font-ui); color: var(--gilt-body); font-variant-numeric: var(--nums); }
.cp-mmed { flex: none; font: 700 10.5px var(--font-ui); color: var(--gold-lt); font-variant-numeric: var(--nums); }
/* THE PRIZE, ON THE CARD. A charge nobody who has not walked this march can
   wear is the reason to walk it, and it belongs where the decision is made. */
.cp-mprize { display: flex; align-items: center; gap: 7px; padding: 5px 7px; border-radius: var(--r-sm);
  background: rgba(var(--shade-rgb), 0.34); border: 1px solid var(--edge-quiet); }
.cp-mprize span { font: 500 10.5px var(--font-ui); color: var(--muted); line-height: 1.35; }
.cp-mprize.won { border-color: rgba(var(--ready-rgb), 0.4); }
.cp-mprize.won span { color: var(--ready-lt); }
.cp-mprize canvas { flex: 0 0 auto; opacity: 0.55; }
.cp-mprize.won canvas { opacity: 1; }
.cp-mgo { font: 800 10.5px var(--font-display); letter-spacing: 0.8px; text-transform: uppercase;
  color: var(--gilt-lit); text-align: right; }

/* ---- ONE MARCH: the road and the briefing ----
   The briefing is the control on this screen, so it is never below the fold:
   the road takes what is left and scrolls inside it. */
.campbody.inmarch { gap: var(--gutter); }
.cp-mhead { display: flex; align-items: center; gap: var(--sp-4); flex: none;
  padding: var(--sp-3) var(--sp-4); border-radius: var(--r-lg);
  background: linear-gradient(160deg, color-mix(in srgb, var(--fac) 24%, #16130e), #0d0b08);
  border: 1px solid var(--edge-quiet); box-shadow: var(--bevel); }
.cp-hcrest { flex: 0 0 auto; }
.cp-hnames { flex: 1 1 auto; min-width: 0; }
.cp-hnames b { display: block; font: 800 16px var(--font-display); color: var(--gold-lt); line-height: 1.2; }
.cp-hnames em { display: block; font: 400 11.5px var(--font-ui); font-style: italic; color: var(--muted); margin-top: 2px; }
.cp-hprog { flex: 0 0 auto; min-width: 130px; text-align: right; }
.cp-hprog span { display: block; font: 700 11px var(--font-ui); color: var(--gilt-body);
  font-variant-numeric: var(--nums); margin-bottom: 3px; }

/* THE ROAD. Ten nodes on one line, and a locked one is DRAWN rather than
   hidden: the point of a campaign is that you can see where it is going, and a
   road that only shows the next step is a corridor. Scrolls sideways on a
   phone, where ten nodes will not fit however small they are made. */
.cp-road { flex: none; display: flex; gap: 5px; overflow-x: auto; overflow-y: hidden;
  padding: 2px 2px 8px; scrollbar-width: thin; }
.cp-node { flex: 1 1 0; min-width: 92px; display: flex; flex-direction: column; align-items: center; gap: 3px;
  cursor: pointer; padding: 8px 5px 7px; border-radius: var(--r-md);
  background: linear-gradient(180deg, rgba(30,25,18,0.95), rgba(12,10,7,0.98));
  border: 1px solid var(--edge-quiet); box-shadow: var(--seat);
  transition: transform var(--t-1) var(--e-out), border-color var(--t-1) var(--e-out); }
.cp-node:hover { transform: translateY(-2px); border-color: var(--edge); }
.cp-node:focus-visible { outline: 2px solid var(--gold-lt); outline-offset: 2px; }
.cp-node.sel { border-color: var(--gold-lt); box-shadow: 0 0 16px rgba(var(--gold-rgb), 0.35); }
.cp-node.won { background: linear-gradient(180deg, color-mix(in srgb, var(--foe) 18%, #17140e), #0c0a07);
  border-color: rgba(var(--ready-rgb), 0.34); }
.cp-node.at .cp-nn { animation: cpPulse 1.9s ease-in-out infinite; }
.cp-node.shut { opacity: 0.5; cursor: default; }
.cp-node.shut:hover { transform: none; border-color: var(--edge-quiet); }
.cp-node.riv { border-top: 2px solid rgba(var(--peril-rgb), 0.7); }
.cp-node.crown { border-color: rgba(var(--gold-rgb), 0.5); }
.cp-nn { display: flex; align-items: center; justify-content: center; width: 22px; height: 22px;
  border-radius: 50%; font: 900 11px var(--font-display); color: var(--gilt-ink);
  background: linear-gradient(180deg, var(--gilt-lit), var(--gold-dk)); box-shadow: var(--sh-0); }
.cp-node.shut .cp-nn { background: rgba(var(--shade-rgb), 0.5); color: var(--gilt-body);
  border: 1px solid var(--gilt-seat); }
/* A JOB AND A PLACE ARE NAMES, so they wrap rather than truncate — ten nodes of
   "The Rever…" would be ten rendering faults on the screen whose subject is
   that each of these battles is a different thing. */
.cp-njob { font: 800 10px var(--font-display); color: var(--gold-lt); text-align: center;
  line-height: 1.15; white-space: normal; overflow-wrap: anywhere; }
.cp-nplace { font: 400 9px var(--font-ui); color: var(--muted); text-align: center;
  line-height: 1.2; white-space: normal; overflow-wrap: anywhere; }
.cp-nmeds { display: flex; gap: 3px; margin-top: 1px; }
.cp-med { width: 5px; height: 5px; border-radius: 50%; background: rgba(var(--shade-rgb), 0.55);
  border: 1px solid var(--gilt-seat); }
.cp-med.on { background: var(--gold-lt); border-color: var(--gilt-lit); box-shadow: 0 0 5px rgba(var(--gold-rgb), 0.8); }
.cp-nlock { position: absolute; }
.cp-node { position: relative; }
.cp-nlock { top: 4px; right: 5px; font-size: 9px; opacity: 0.6; }

/* THE BRIEFING. Everything the commander is agreeing to, before they agree to
   it: who, where, what ends it, what is bent, what it pays. A campaign battle
   that opens with a surprise rule is one that gets lost once and then resented. */
.cp-brief { flex: 1 1 auto; min-height: 0; overflow: auto; display: flex; flex-direction: column; gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5); border-radius: var(--r-lg);
  background: linear-gradient(160deg, rgba(36,30,22,0.99), rgba(14,11,8,1));
  border: 1px solid var(--edge); box-shadow: var(--sh-2), var(--bevel); }
.cp-brief.shut { border-color: var(--edge-quiet); }
.cp-bhd { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 10px; }
.cp-bn { font: 800 9px var(--font-display); letter-spacing: 1.3px; text-transform: uppercase; color: var(--gilt-body); }
.cp-bjob { font: 900 19px var(--font-display); color: var(--gold-lt); line-height: 1.15; }
.cp-bplace { font: 400 12px var(--font-ui); font-style: italic; color: var(--muted); }
.cp-btell { font: 400 13px var(--font-ui); color: var(--ink); line-height: 1.5; }
.cp-bcards { display: grid; gap: var(--gutter); grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); }
.cp-bc { padding: 7px 9px; border-radius: var(--r-sm); background: rgba(var(--shade-rgb), 0.34);
  border: 1px solid var(--edge-quiet); }
.cp-bcl { display: block; font: 800 8.5px var(--font-display); letter-spacing: 1.2px;
  text-transform: uppercase; color: var(--gilt-body); margin-bottom: 3px; }
.cp-bcv { display: flex; align-items: center; gap: 6px; }
.cp-bcv b { font: 800 14px var(--font-display); color: var(--gold-lt); line-height: 1.2;
  white-space: normal; overflow-wrap: anywhere; }
.cp-bc em { display: block; font: 400 10.5px var(--font-ui); font-style: normal; color: var(--muted);
  line-height: 1.4; margin-top: 3px; }
.cp-bc.pay b { font-size: 17px; font-variant-numeric: var(--nums); }
/* THE BENT RULES, EACH WITH THE SENTENCE THAT EXPLAINS IT. "Walled against you"
   is a warning; "their keep was fortified long before you came" is a plan. */
.cp-bmods { display: grid; gap: 5px; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); }
.cp-bmod { display: block; padding: 6px 9px; border-radius: var(--r-sm);
  background: rgba(var(--peril-rgb), 0.1); border-left: 3px solid rgba(var(--peril-rgb), 0.6); }
.cp-bmods.plain .cp-bmod { background: rgba(var(--shade-rgb), 0.3); border-left-color: var(--gilt-seat); }
.cp-bmod b { display: block; font: 800 11.5px var(--font-display); color: var(--ink); }
.cp-bmod em { display: block; font: 400 10.5px var(--font-ui); font-style: normal; color: var(--muted);
  line-height: 1.4; margin-top: 1px; }
/* THE THREE HONOURS, and which are still out there. One already held is drawn
   held: coming back for the one that got away is why a finished march still has
   evenings in it. */
.cp-bmeds { display: grid; gap: 5px; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }
.cp-bmed { display: flex; align-items: flex-start; gap: 7px; padding: 5px 8px; border-radius: var(--r-sm);
  background: rgba(var(--shade-rgb), 0.3); border: 1px solid var(--edge-quiet); }
.cp-bmed i { flex: 0 0 auto; width: 9px; height: 9px; margin-top: 3px; border-radius: 50%;
  background: rgba(var(--shade-rgb), 0.6); border: 1px solid var(--gilt-seat); }
.cp-bmed.on { border-color: rgba(var(--gold-rgb), 0.5); }
.cp-bmed.on i { background: var(--gold-lt); box-shadow: 0 0 7px rgba(var(--gold-rgb), 0.8); }
.cp-bmed b { font: 800 11px var(--font-display); color: var(--gilt-body); }
.cp-bmed.on b { color: var(--gold-lt); }
.cp-bmed em { display: block; font: 400 10px var(--font-ui); font-style: normal; color: var(--muted); line-height: 1.35; }
.cp-bmed div, .cp-bmed span { min-width: 0; }
.cp-bgo { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-4); margin-top: auto; padding-top: var(--sp-2); }
.cp-bgo .bigbtn { margin: 0; }
.cp-bshut { font: 500 12px var(--font-ui); font-style: italic; color: var(--muted); }
.cp-bprize { display: flex; align-items: center; gap: 8px; flex: 1 1 220px;
  font: 500 11px var(--font-ui); color: var(--gilt-body); line-height: 1.4; }
.cp-bprize b { color: var(--gold-lt); }

/* ---- A CHARGE YOU HAVE NOT MARCHED FOR, in the Sigil Forge ----
   Drawn locked rather than hidden: a device you cannot wear yet, with the march
   that earns it on its tooltip, is a reason to walk that march. */
.fg-ch { position: relative; }
.fg-ch.shut { opacity: 0.42; cursor: not-allowed; }
.fg-ch.shut canvas { filter: grayscale(1); }
.fg-lk { position: absolute; right: 1px; bottom: 0; font-size: 9px; font-style: normal; opacity: 0.9; }
.fg-wn { position: absolute; right: 1px; top: 0; font-size: 9px; font-style: normal; color: var(--gold-lt); }

/* ---- WHAT THE MARCH MADE OF IT, on the results screen ---- */
.rescamp { width: min(420px, 90%); margin: var(--sp-4) auto 0; padding: var(--sp-4) var(--sp-5);
  background: rgba(var(--shade-rgb), 0.34); border: 1px solid var(--edge-quiet); border-radius: var(--r-lg);
  animation: resRise 0.5s var(--e-out) both; }
.rescamp.won { border-color: var(--edge); }
.rcmeds { display: flex; justify-content: center; gap: 6px; margin: 6px 0 2px; flex-wrap: wrap; }
.rcmed { display: flex; align-items: center; gap: 4px; padding: 3px 8px; border-radius: var(--r-pill);
  background: rgba(var(--shade-rgb), 0.4); border: 1px solid var(--gilt-seat); }
.rcmed i { font-style: normal; font-size: 10px; color: var(--gilt-deep); }
.rcmed b { font: 700 10px var(--font-display); color: var(--gilt-body); }
.rcmed.on { border-color: rgba(var(--gold-rgb), 0.55); }
.rcmed.on i { color: var(--gold-lt); }
.rcmed.on b { color: var(--gold-lt); }
/* A medal taken THIS battle, as opposed to one already held. The screen has one
   spark to spend and this is where it goes when there is one to spend it on. */
.rcmed.new { box-shadow: 0 0 14px rgba(var(--gold-rgb), 0.5); animation: cpPulse 1.6s ease-in-out 3; }
.rccharge { display: flex; align-items: center; justify-content: center; gap: 8px; margin-top: 6px;
  padding: 6px 9px; border-radius: var(--r-sm); background: rgba(var(--gold-rgb), 0.1);
  border: 1px solid rgba(var(--gold-rgb), 0.4); }
.rccharge span { font: 500 11px var(--font-ui); color: var(--ink); line-height: 1.4; text-align: left; }
.rccharge b { color: var(--gold-lt); }

/* ---- WHAT ENDS THIS BATTLE, in the HUD ----
   Absent on every ordinary match, which is nearly all of them. It sits with the
   other standing registers rather than over the field: it is a thing you consult,
   not a thing that should be in the way of the fighting. */
#objstate { pointer-events: none; margin-bottom: 6px; padding: 7px 10px; border-radius: var(--r-md);
  background: linear-gradient(160deg, rgba(36,30,22,0.94), rgba(14,11,8,0.96));
  border: 1px solid var(--edge); box-shadow: var(--sh-2), var(--bevel); }
#objstate.theirs { border-color: rgba(var(--peril-rgb), 0.45); }
.ob-hd { display: flex; align-items: center; gap: 6px; }
.ob-hd i { font-style: normal; font-size: 13px; color: var(--gold-lt); }
#objstate.theirs .ob-hd i { color: var(--peril); }
.ob-hd b { font: 800 11.5px var(--font-display); letter-spacing: 0.4px; color: var(--gold-lt);
  line-height: 1.2; white-space: normal; overflow-wrap: anywhere; }
#objstate.theirs .ob-hd b { color: var(--peril); }
.ob-tell { font: 400 10px var(--font-ui); color: var(--muted); line-height: 1.35; margin: 2px 0 4px; }
.ob-bar { height: 5px; border-radius: var(--r-pill); overflow: hidden;
  background: rgba(var(--shade-rgb), 0.6); border: 1px solid var(--gilt-seat); }
.ob-bar i { display: block; height: 100%; background: linear-gradient(90deg, var(--gold-dk), var(--gold-lt));
  transition: width var(--t-2) linear; }
#objstate.near .ob-bar i { background: linear-gradient(90deg, var(--gold), var(--gold-bright));
  box-shadow: 0 0 9px rgba(var(--gold-rgb), 0.8); }
/* THE ONE CONDITION WHERE THE CLOCK RUNNING OUT IS A DEFEAT. By the last quarter
   of a blitz it is the whole of what this panel is for. */
#objstate.hot { border-color: rgba(var(--peril-rgb), 0.7); animation: cpPulse 1.4s ease-in-out infinite; }
#objstate.hot .ob-bar i { background: linear-gradient(90deg, #8a3a30, var(--peril)); }
.ob-num { font: 700 11px var(--font-ui); color: var(--ink); font-variant-numeric: var(--nums); margin-top: 3px; }

/* A phone has no room for a ten-node road AND a full briefing, so the road
   shrinks to what a finger can still hit and the briefing keeps the space. */
@media (max-width: 700px) {
  .cp-node { min-width: 78px; }
  .cp-nplace { display: none; }
  .cp-marches { grid-template-columns: 1fr; }
  .cp-mtell { min-height: 0; }
}
/* Two of the six bent rules are GIFTS — drawing them in the same alarm colour as
   "no walls of your own" tells a commander to brace for something that is help. */
.cp-bmod.boon { background: rgba(var(--ready-rgb), 0.1); border-left-color: rgba(var(--ready-rgb), 0.6); }
/* A raze beat's bar is its deadline, so it fills AGAINST the commander — the one
   register in this panel where a full bar is the bad news. */
#objstate.against .ob-bar i { background: linear-gradient(90deg, var(--gilt-deep), var(--gold-dk)); box-shadow: none; }
#objstate.against.hot .ob-bar i { background: linear-gradient(90deg, #8a3a30, var(--peril)); }

/* ═══════════════════════════ THE DAY'S ORDERS ═══════════════════════════
   Three cards on the home screen. Not behind a button — the whole job of a
   daily is to be a reason to open the game, and a reason you have to go looking
   for is a reason nobody finds twice. */
.dayquests { width: min(980px, 100%); margin: var(--sp-4) auto 0; padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-lg); background: linear-gradient(160deg, rgba(36,30,22,0.94), rgba(14,11,8,0.97));
  border: 1px solid var(--edge-quiet); box-shadow: var(--bevel); }
.dq-hd { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3); margin-bottom: var(--sp-3); }
.dq-ttl { display: flex; align-items: center; gap: 6px; font: 800 11px var(--font-display);
  letter-spacing: 1.5px; text-transform: uppercase; color: var(--gilt-body); }
.dq-ttl .ic { width: 14px; height: 14px; }
.dq-n { font: 800 12px var(--font-ui); color: var(--gilt-body); font-variant-numeric: var(--nums); }
.dq-n.all { color: var(--ready-lt); }
/* THE WEEK AS DAYS, NOT AS A STREAK. Seven pips with the fifth marked, so what
   is being asked for is visible: five out of any seven, and a missed evening
   costs nothing. */
.dq-wk { display: flex; align-items: center; gap: 3px; margin-left: auto; }
.dq-wk i { width: 7px; height: 7px; border-radius: 50%; background: rgba(var(--shade-rgb), 0.55);
  border: 1px solid var(--gilt-seat); }
.dq-wk i.on { background: var(--gold-lt); border-color: var(--gilt-lit); box-shadow: 0 0 5px rgba(var(--gold-rgb), 0.7); }
.dq-wk i.mark { outline: 1px solid rgba(var(--gold-rgb), 0.5); outline-offset: 1px; }
.dq-wk b { font: 700 10px var(--font-ui); color: var(--gilt-body); margin-left: 4px; font-variant-numeric: var(--nums); }
.dq-when { font: 400 10.5px var(--font-ui); font-style: italic; color: var(--muted); }
.dq-cards { display: grid; gap: var(--gutter); grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); }
.dq-card { display: flex; flex-direction: column; gap: 4px; padding: 8px 10px; border-radius: var(--r-md);
  background: rgba(var(--shade-rgb), 0.34); border: 1px solid var(--edge-quiet); }
.dq-card.done { border-color: rgba(var(--ready-rgb), 0.45); background: rgba(var(--ready-rgb), 0.07); }
.dq-top { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.dq-w { font: 800 8.5px var(--font-display); letter-spacing: 1.2px; text-transform: uppercase; color: var(--gilt-body); }
.dq-gold { font: 700 11px var(--font-ui); color: var(--gold-lt); font-variant-numeric: var(--nums); }
/* A QUEST'S NAME AND ITS ASK ARE BOTH SENTENCES — they wrap rather than
   truncate. "Work your keep up to Bast…" is a rendering fault, not a quest. */
.dq-t { font: 800 13px var(--font-display); color: var(--gold-lt); line-height: 1.2;
  white-space: normal; overflow-wrap: anywhere; }
.dq-card.done .dq-t { color: var(--ready-lt); }
.dq-d { font: 400 11px var(--font-ui); color: var(--muted); line-height: 1.4; }
.dq-bar { height: 4px; border-radius: var(--r-pill); overflow: hidden; margin-top: 2px;
  background: rgba(var(--shade-rgb), 0.6); border: 1px solid var(--gilt-seat); }
.dq-bar i { display: block; height: 100%; background: linear-gradient(90deg, var(--gold-dk), var(--gold-lt));
  transition: width var(--t-3) var(--e-out); }
.dq-card.done .dq-bar i { background: linear-gradient(90deg, var(--ready-dk), var(--ready-lt)); }
.dq-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.dq-at { font: 700 10.5px var(--font-ui); color: var(--gilt-body); font-variant-numeric: var(--nums); }
.dq-card.done .dq-at { color: var(--ready-lt); }
.dq-veto { cursor: pointer; font: 600 10px var(--font-ui); color: var(--muted);
  background: none; border: 1px solid var(--edge-quiet); border-radius: var(--r-pill); padding: 2px 8px;
  transition: color var(--t-1) var(--e-out), border-color var(--t-1) var(--e-out); }
.dq-veto:hover { color: var(--ink); border-color: var(--edge); }
.dq-note { font: 400 10.5px var(--font-ui); font-style: italic; color: var(--muted); margin-top: var(--sp-3); }

/* ...and what they paid, on the results screen */
.resday { width: min(420px, 90%); margin: var(--sp-4) auto 0; padding: var(--sp-4) var(--sp-5);
  background: rgba(var(--shade-rgb), 0.34); border: 1px solid var(--edge-quiet); border-radius: var(--r-lg);
  animation: resRise 0.5s var(--e-out) both; }
.rd-q { display: grid; grid-template-columns: 14px 1fr auto; gap: 3px 7px; align-items: center; margin-top: 5px; }
.rd-n { font: 900 11px var(--font-display); color: var(--ready-lt); }
.rd-t { font: 600 12px var(--font-ui); color: var(--ink); text-align: left;
  white-space: normal; overflow-wrap: anywhere; }
.rd-a { font: 700 11px var(--font-ui); color: var(--gilt-body); font-variant-numeric: var(--nums); }
.rd-q.done .rd-a { color: var(--gold-lt); }
.rd-b { grid-column: 2 / -1; height: 3px; border-radius: var(--r-pill); overflow: hidden;
  background: rgba(var(--shade-rgb), 0.6); }
.rd-b i { display: block; height: 100%; background: var(--gold); }
.rd-q.done .rd-b i { background: var(--ready); }

@media (max-width: 700px) {
  .dq-cards { grid-template-columns: 1fr; }
  .dq-when { display: none; }
}

/* ═══════════════════════════ THE WARDROBE ═══════════════════════════
   Fourteen turns-out for the Builder and fourteen for the keep, beside the
   kingdom they dress. Every tile is a live preview drawn by the same function
   the field uses.

   THE LEFT GUTTER IS FOR THE SOUND TOGGLES. They are fixed to the bottom-left
   corner at 13px, forty across, and at four skins a set this strip was one row
   tall and finished well above them. At fourteen it wraps to two and the second
   row's first tile ran underneath both — a tile you can see and cannot click.
   Padding rather than a z-index, because winning that fight the other way puts
   the wardrobe over the mute button instead. */
.musterward { width: 100%; margin-top: var(--sp-3); padding: var(--sp-3) var(--sp-4) var(--sp-3) 62px;
  border-radius: var(--r-lg); background: linear-gradient(160deg, rgba(36,30,22,0.94), rgba(14,11,8,0.97));
  border: 1px solid var(--edge-quiet); box-shadow: var(--bevel); }
.mw-hd { display: flex; align-items: baseline; gap: var(--sp-3); margin-bottom: var(--sp-2); }
.mw-t { display: flex; align-items: center; gap: 6px; font: 800 10.5px var(--font-display);
  letter-spacing: 1.4px; text-transform: uppercase; color: var(--gilt-body); }
.mw-t .ic { width: 13px; height: 13px; }
.mw-w { font: 700 11px var(--font-ui); color: var(--gold-lt); }
.mw-n { margin-left: auto; font: 600 10.5px var(--font-ui); color: var(--muted); font-variant-numeric: var(--nums); }
/* BOTH SETS ON ONE LINE WHEN THERE IS ROOM. Stacked, the two rows each take the
   full width of the muster screen, which stretches four tiles to ~340px apiece
   around a 74px sprite — a shop made mostly of empty tile. Side by side, a tile
   is about as wide as the thing it is selling. */
.mw-sets { display: grid; grid-template-columns: 1fr; gap: 2px; }
@media (min-width: 1100px) { .mw-sets { grid-template-columns: 1fr 1fr; gap: var(--sp-4); } }
/* FOURTEEN, NOT FOUR. A flex row with `flex: 1 1 0` per tile divided the width
   evenly, which was right for four and gives fourteen tiles about 24px each — an
   unreadable ribbon. A wrapping grid with a floor instead: as many across as fit
   at a usable size, and the rest on the next line. The label goes above the row
   rather than beside it, because a 78px gutter beside three wrapped rows is a
   78px column of nothing. */
.mw-set { display: flex; flex-direction: column; gap: 3px; margin-top: 6px; min-width: 0; }
.mw-sl { font: 700 10px var(--font-display); letter-spacing: 0.6px;
  text-transform: uppercase; color: var(--gilt-body); text-align: left; }
.mw-row { display: grid; grid-template-columns: repeat(auto-fill, minmax(68px, 1fr)); gap: 6px; min-width: 0; }
.mw-sk { min-width: 0; display: flex; flex-direction: column; align-items: center; gap: 2px;
  cursor: pointer; padding: 5px 4px 4px; border-radius: var(--r-md);
  background: rgba(var(--shade-rgb), 0.34); border: 1px solid var(--edge-quiet);
  transition: transform var(--t-1) var(--e-out), border-color var(--t-1) var(--e-out); }
.mw-sk:hover { transform: translateY(-2px); border-color: var(--edge); }
.mw-sk:focus-visible { outline: 2px solid var(--gold-lt); outline-offset: 2px; }
.mw-sk.on { border-color: var(--gold-lt); box-shadow: 0 0 12px rgba(var(--gold-rgb), 0.3); }
/* NOT OWNED IS STILL SHOWN IN FULL. This rule used to dim the preview to 0.42 and
   pull two thirds of the colour out of it, which read fine as "locked" and was
   otherwise a disaster: at 74px the four Builders became four identical grey
   blobs, and a shop whose whole job is showing you what a skin looks like was
   showing nothing. The lock is carried by the chrome instead — a quiet frame, a
   recessed tile, a price where "Worn" would be — and the art is left alone. */
.mw-sk.shut { background: rgba(var(--shade-rgb), 0.52); border-color: var(--edge-quiet); }
.mw-sk.shut .mw-pv { opacity: 0.88; }
/* The canvas is cut at a fixed size and DISPLAYED at the tile's — so a wide
   column shows a big preview and a phone shows a small one, without cutting
   ninety-eight sprites twice. */
.mw-pv { display: block; image-rendering: auto; width: 100%; height: auto; max-width: 92px; }
/* A SKIN'S NAME IS NEVER CUT. Four tiles across a rail is tight, so the name
   wraps to a second line rather than truncating — "The Siege-Scar…" is a
   rendering fault, and these names are most of what is being sold. */
.mw-nm { font: 700 9.5px var(--font-display); color: var(--gold-lt); text-align: center;
  line-height: 1.15; white-space: normal; overflow-wrap: anywhere; }
.mw-sk.shut .mw-nm { color: var(--gilt-body); }
.mw-st { font: 600 9px var(--font-ui); color: var(--muted); font-variant-numeric: var(--nums); }
.mw-sk.on .mw-st { color: var(--ready-lt); }
.mw-sk.shut .mw-st { color: var(--gold-lt); }   /* a price, not a footnote */
.mw-tell { min-height: 1.2em; margin-top: 5px; font: 400 10.5px var(--font-ui);
  font-style: italic; color: var(--muted); text-align: center; }

@media (max-width: 820px) {
  .mw-set { flex-direction: column; align-items: stretch; gap: 3px; }
  .mw-sl { flex: none; text-align: left; }
  .mw-nm { font-size: 9px; }
}

/* ── THE ROLL OF DEEDS ──────────────────────────────────────────────────────
   Eighty-seven entries in eight families, browsed for something to go and do.
   Every card shows its requirement and its progress whether or not it is taken:
   a locked deed with a hidden requirement teaches nobody anything, which is the
   one rule this screen is built around. */
/* A SCRIM UNDER THE WHOLE ROLL. The menu terrain lives behind every screen, and
   at 11px a requirement line loses to a river. The other screens are mostly
   large tiles that carry their own ground; this one is eighty-seven small
   paragraphs, so it needs its own. */
.rdbody { width: 100%; max-width: var(--screenw); margin: 0 auto; padding: var(--sp-3) var(--sp-4) var(--sp-6);
  border-radius: var(--r-lg); background: linear-gradient(180deg, rgba(10,9,7,0.72), rgba(10,9,7,0.82)); }
.rd-hd { display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-4); margin-bottom: var(--sp-4); border-radius: var(--r-lg);
  background: linear-gradient(160deg, rgba(36,30,22,0.94), rgba(14,11,8,0.97));
  border: 1px solid var(--edge-quiet); box-shadow: var(--bevel); }
.rd-tally { display: flex; align-items: baseline; gap: 6px; }
.rd-tally b { font: 800 26px var(--font-display); color: var(--gold-lt); font-variant-numeric: var(--nums); }
.rd-tally span { font: 600 11px var(--font-ui); color: var(--muted); }
.rd-bar { flex: 1 1 180px; min-width: 120px; height: 8px; border-radius: 4px; overflow: hidden;
  background: rgba(var(--shade-rgb), 0.6); border: 1px solid var(--edge-quiet); }
.rd-bar i { display: block; height: 100%; border-radius: 4px;
  background: linear-gradient(90deg, var(--gilt-body), var(--gold-lt)); transition: width var(--t-2) var(--e-out); }
.rd-worn { display: flex; align-items: center; gap: 6px; margin-left: auto; }
.rd-worn label { font: 700 10px var(--font-display); letter-spacing: 1px; text-transform: uppercase; color: var(--gilt-body); }
.rd-sel { font: 600 11.5px var(--font-ui); color: var(--gold-lt); padding: 5px 8px; border-radius: var(--r-sm);
  background: rgba(var(--shade-rgb), 0.7); border: 1px solid var(--edge); max-width: 240px; }
.rd-notitle { font: 400 11px var(--font-ui); font-style: italic; color: var(--muted); }

.rd-fam { margin-bottom: var(--sp-5); }
.rd-fh { display: flex; align-items: baseline; gap: var(--sp-3); flex-wrap: wrap; margin-bottom: var(--sp-2);
  padding-bottom: 5px; border-bottom: 1px solid var(--edge-quiet); }
.rd-fh b { font: 800 14px var(--font-display); color: var(--gold-lt); letter-spacing: 0.3px; }
.rd-ft { font: 400 11.5px var(--font-ui); font-style: italic; color: var(--muted); }
.rd-fn { margin-left: auto; font: 700 11px var(--font-ui); color: var(--gilt-body); font-variant-numeric: var(--nums); }
.rd-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(248px, 1fr)); gap: var(--sp-2); }

.rd-d { display: flex; flex-direction: column; gap: 4px; padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-md); background: rgba(var(--shade-rgb), 0.66);
  border: 1px solid var(--edge-quiet); min-width: 0; }
/* A TAKEN DEED IS LIT, NOT GREYED. The list is a record of what you have done as
   much as a list of what is left, so the done ones are the reward being shown
   back to you — dimming them would be filing your own trophies in a drawer. */
.rd-d.got { border-color: var(--gold-lt); background: linear-gradient(160deg, rgba(64,50,20,0.5), rgba(24,18,8,0.6));
  box-shadow: 0 0 10px rgba(var(--gold-rgb), 0.16) inset; }
.rd-dn { display: flex; align-items: baseline; gap: 6px; }
.rd-dn b { font: 700 12.5px var(--font-display); color: var(--gold-lt); line-height: 1.15;
  white-space: normal; overflow-wrap: anywhere; }
.rd-tick { margin-left: auto; color: var(--ready-lt); font: 700 12px var(--font-ui); text-decoration: none; }
.rd-dg { font: 400 11px var(--font-ui); color: var(--body); line-height: 1.3;
  white-space: normal; overflow-wrap: anywhere; }
.rd-dt { font: 400 10.5px var(--font-ui); font-style: italic; color: var(--muted); line-height: 1.3; }
.rd-dp { display: flex; align-items: center; gap: 6px; }
.rd-pb { flex: 1 1 auto; height: 5px; border-radius: 3px; overflow: hidden;
  background: rgba(var(--shade-rgb), 0.75); border: 1px solid var(--edge-quiet); }
.rd-pb i { display: block; height: 100%; background: var(--gilt-body); border-radius: 3px; }
.rd-pn { font: 600 9.5px var(--font-ui); color: var(--muted); font-variant-numeric: var(--nums); white-space: nowrap; }
.rd-pays { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 1px; }
.rd-p { font: 600 9px var(--font-ui); font-style: normal; padding: 1px 5px; border-radius: 3px;
  background: rgba(var(--shade-rgb), 0.6); color: var(--muted); border: 1px solid var(--edge-quiet); }
.rd-p.ti { color: var(--gold-lt); border-color: rgba(var(--gold-rgb), 0.42); }
.rd-p.sk { color: var(--ready-lt); }
.rd-p.gd::before { content: '⛁ '; }
/* A deed that cannot be reached until the Armory has been shopped at says so
   quietly, so nobody spends their first week hunting a Titan they cannot buy. */
.rd-d.late .rd-dg::after { content: ' · late-game'; color: var(--muted); font-style: italic; opacity: 0.85; }

/* ── THE MOMENT ─────────────────────────────────────────────────────────────
   Matched to the promotion's ceremony (see .rrrise) rather than a toast: this
   system's whole reward is a NAME, and a name announced in a corner is a name
   nobody wears. Several deeds can land on one battle, so they queue. */
.deedrise { position: fixed; left: 50%; top: 24%; transform: translate(-50%, -8px);
  z-index: 90; pointer-events: none; text-align: center;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  opacity: 0; transition: opacity var(--t-2) var(--e-out), transform var(--t-2) var(--e-out); }
.deedrise.go { opacity: 1; transform: translate(-50%, 0); }
.dr-rays { position: absolute; left: 50%; top: 50%; width: 420px; height: 420px; margin: -210px 0 0 -210px;
  background: radial-gradient(circle, rgba(var(--gold-rgb), 0.30) 0%, rgba(var(--gold-rgb), 0) 62%);
  animation: drpulse 2.6s var(--e-out); }
@keyframes drpulse { 0% { transform: scale(0.5); opacity: 0; } 22% { opacity: 1; } 100% { transform: scale(1.1); opacity: 0; } }
.dr-kind { font: 700 9.5px var(--font-display); letter-spacing: 2px; text-transform: uppercase; color: var(--gilt-body); }
.dr-nm { font: 800 30px var(--font-display); color: var(--gold-lt); letter-spacing: 0.5px;
  text-shadow: 0 2px 14px rgba(var(--gold-rgb), 0.5), 0 1px 0 #000; }
.dr-tell { font: 400 12px var(--font-ui); font-style: italic; color: var(--body); max-width: 420px; }
.dr-pays { display: flex; gap: 6px; margin-top: 4px; flex-wrap: wrap; justify-content: center; }
.dr-pays i { font: 600 10px var(--font-ui); font-style: normal; padding: 2px 7px; border-radius: 3px;
  background: rgba(0,0,0,0.5); border: 1px solid rgba(var(--gold-rgb), 0.4); color: var(--gold-lt); }
@media (prefers-reduced-motion: reduce) {
  .deedrise, .dr-rays { transition: none; animation: none; }
}

/* The opening roll, and the body `ask` now accepts. */
.ask-h { margin: var(--sp-2) 0; text-align: left; max-height: 46vh; overflow-y: auto; }
.rd-rollp, .rd-rollq { font: 400 12px var(--font-ui); color: var(--body); line-height: 1.45; margin: 0 0 var(--sp-2); }
.rd-rollq { color: var(--muted); font-style: italic; }
.rd-rollf { margin-bottom: 5px; }
.rd-rollf b { display: block; font: 700 10px var(--font-display); letter-spacing: 1px;
  text-transform: uppercase; color: var(--gilt-body); margin-bottom: 2px; }
.rd-rollf span { display: inline-block; font: 600 10.5px var(--font-ui); color: var(--gold-lt);
  padding: 1px 6px; margin: 0 3px 3px 0; border-radius: 3px;
  background: rgba(var(--shade-rgb), 0.55); border: 1px solid rgba(var(--gold-rgb), 0.3); }

/* ── THE EARNED NAME, WHEREVER A COMMANDER IS NAMED ─────────────────────────
   One line in the identity mark, under the given name and above the house. */
.cm-ti { display: block; font: 600 9.5px var(--font-ui); font-style: italic; color: var(--gold-lt);
  line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 22ch; }
.cmark.sm .cm-ti { font-size: 8.5px; max-width: 16ch; }
/* ...and on the seat plate of a live battle, which is the surface it is for. */
.sc-ti { font: 600 9px var(--font-ui); font-style: italic; color: var(--gold-lt);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 15ch; }

@media (max-width: 820px) {
  .rd-grid { grid-template-columns: 1fr 1fr; }
  .rd-worn { margin-left: 0; width: 100%; }
  .rd-sel { flex: 1 1 auto; max-width: none; }
  .dr-nm { font-size: 22px; }
  .dr-tell { font-size: 11px; padding: 0 var(--sp-4); }
}
@media (max-width: 520px) { .rd-grid { grid-template-columns: 1fr; } }
/* The earned skin sits with the four on sale and is marked, not priced. */
.mw-sk.won { border-color: rgba(var(--gold-rgb), 0.45); }
.mw-sk.won.shut .mw-st { color: var(--gilt-body); font-style: italic; }
.mw-sk.won .mw-st { white-space: normal; overflow-wrap: anywhere; line-height: 1.15; }

/* ── FIREWORKS ON THE MAP FRAME ─────────────────────────────────────────────
   The third control on the frame's top rail, beside the emote sheet and March
   All and directly above the purse it is bought with. It wears the colours of
   the shell it will fire, so the selection is legible without opening anything,
   and it draws its own cooldown rather than silently ignoring a second press. */
/* A THIRD STUD ON THE RAIL, to March All's left. Everything but the colour is
   inherited from the other two deliberately — they are the same KIND of thing,
   and a control that differs in size or finish implies a difference in rank that
   is not there. The first cut styled none of this and the button came out
   fourteen pixels wide, which is not a target anybody can hit mid-battle. */
#fwbtn { position: absolute; top: calc(var(--stud) * -0.73);
  right: calc(9px + var(--stud) * 2 + var(--studgap) * 2); z-index: 3;
  width: var(--stud); height: var(--stud); border-radius: 50%; cursor: pointer; line-height: 1;
  display: grid; place-items: center; overflow: hidden;
  background: linear-gradient(180deg, var(--gold-hi), var(--gold)); border: 1.5px solid var(--gold-dk);
  box-shadow: var(--sh-2), inset 0 1px 0 rgba(255,255,255,0.55);
  transition: transform 0.12s, filter 0.12s;
  --fw1: #ffd76a; --fw2: #ffb03a; }
#fwbtn:hover { filter: brightness(1.08); transform: scale(1.08); }
#fwbtn .ic { width: calc(var(--stud) * 0.57); height: calc(var(--stud) * 0.57); display: block; }
/* The mark wears the shell's colours so the selection is legible without opening
   anything — but on a gilt stud that needs a dark rim under it to read at all. */
#fwbtn .ic { color: var(--fw1); filter: drop-shadow(0 0 1px rgba(0,0,0,0.85)) drop-shadow(0 0 4px var(--fw2)); }
#fwbtn.open { border-color: var(--gold-lt); }
/* THE SWEEP. A bar that drains left to right over exactly the server's six
   seconds. It is a courtesy, not the rule — the room enforces the same window
   whatever this element is doing — but a control that looks dead for six
   seconds is a control people jab at. */
#fwbtn .fw-cd { position: absolute; left: 0; bottom: 0; height: 2px; width: 0;
  background: linear-gradient(90deg, var(--fw1), var(--fw2)); pointer-events: none; }
@keyframes fwcd { from { width: 100%; opacity: 0.95; } to { width: 0; opacity: 0.3; } }
@media (prefers-reduced-motion: reduce) { #fwbtn .fw-cd { animation-duration: 0.01s !important; } }

/* The rack of what you own, opened by the button's caret half or a right-click.
   Laid out like the emote sheet because it answers the same question — "which
   one" — and a commander should not have to learn two racks. */
#fwrack { position: absolute; z-index: 40; bottom: calc(100% + 8px); right: 0; display: flex; flex-wrap: wrap; gap: 4px;
  max-width: 268px; padding: var(--sp-2); border-radius: var(--r-md);
  background: linear-gradient(160deg, rgba(30,25,18,0.97), rgba(12,10,7,0.98));
  border: 1px solid var(--edge); box-shadow: var(--bevel), 0 8px 26px rgba(0,0,0,0.5); }
.fwtile { display: flex; flex-direction: column; align-items: center; gap: 3px; width: 58px;
  padding: 5px 3px; border-radius: var(--r-sm); cursor: pointer;
  background: rgba(var(--shade-rgb), 0.55); border: 1px solid var(--edge-quiet); }
.fwtile:hover { border-color: var(--edge); transform: translateY(-1px); }
.fwtile.on { border-color: var(--gold-lt); box-shadow: 0 0 9px rgba(var(--gold-rgb), 0.3); }
.fwtile i { font: 600 8.5px var(--font-ui); font-style: normal; color: var(--gilt-body);
  text-align: center; line-height: 1.1; white-space: normal; overflow-wrap: anywhere; }
.fwsw { display: flex; gap: 2px; height: 15px; align-items: center; }
.fwsw s { display: block; width: 9px; height: 9px; border-radius: 50%; text-decoration: none;
  box-shadow: 0 0 6px currentColor; }
.fwsw s.plus { width: auto; height: auto; background: none; box-shadow: none;
  font: 700 15px var(--font-display); color: var(--gilt-body); }

/* ── THE QUARTERMASTER ──────────────────────────────────────────────────────
   One counter for everything the game sells that is not an army. Three tabs
   because three categories is a rail, not a menu, and because the counts on
   them are the answer to "is there anything left to buy". */
.qmbody { width: 100%; max-width: var(--screenw); margin: 0 auto; padding: var(--sp-3) var(--sp-4) var(--sp-6);
  border-radius: var(--r-lg); background: linear-gradient(180deg, rgba(10,9,7,0.72), rgba(10,9,7,0.84)); }
.qm-tabs { display: flex; gap: var(--sp-2); margin-bottom: var(--sp-3); flex-wrap: wrap; }
.qm-tab { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; cursor: pointer;
  padding: 6px var(--sp-4); border-radius: var(--r-md);
  background: rgba(var(--shade-rgb), 0.5); border: 1px solid var(--edge-quiet); }
.qm-tab b { font: 800 12.5px var(--font-display); color: var(--gilt-body); letter-spacing: 0.3px; }
.qm-tab i { font: 600 9.5px var(--font-ui); font-style: normal; color: var(--muted); font-variant-numeric: var(--nums); }
.qm-tab.on { border-color: var(--gold-lt); background: linear-gradient(160deg, rgba(64,50,20,0.55), rgba(24,18,8,0.6)); }
.qm-tab.on b { color: var(--gold-lt); }

/* Fireworks: every tile runs the real break on a loop, so what is being sold is
   the thing itself rather than three coloured dots standing in for it. */
.qm-body.fw { display: grid; grid-template-columns: repeat(auto-fill, minmax(172px, 1fr)); gap: var(--sp-2); }
.qmfw { display: flex; flex-direction: column; align-items: center; gap: 2px; cursor: pointer;
  padding: var(--sp-2) var(--sp-2) 6px; border-radius: var(--r-md); min-width: 0;
  background: radial-gradient(120% 80% at 50% 100%, rgba(18,22,32,0.95), rgba(8,10,14,0.98));
  border: 1px solid var(--edge-quiet); transition: transform var(--t-1) var(--e-out), border-color var(--t-1) var(--e-out); }
.qmfw:hover { transform: translateY(-2px); border-color: var(--edge); }
.qmfw.on { border-color: var(--gold-lt); box-shadow: 0 0 12px rgba(var(--gold-rgb), 0.28); }
.qmfw-pv { display: block; border-radius: var(--r-sm); }
.qmfw b { font: 700 11.5px var(--font-display); color: var(--gold-lt); text-align: center;
  line-height: 1.15; white-space: normal; overflow-wrap: anywhere; }
.qmfw-t { font: 400 9.5px var(--font-ui); font-style: italic; color: var(--muted); text-align: center;
  line-height: 1.25; min-height: 2.4em; }
.qmfw-b { font: 600 10px var(--font-ui); color: var(--gilt-body); font-variant-numeric: var(--nums); }
.qmfw.on .qmfw-b { color: var(--ready-lt); }
.qmfw:not(.own) .qmfw-b { color: var(--gold-lt); }

/* Skins: all seven kingdoms, grouped, with previews from the real drawer. */
.qm-king { margin-bottom: var(--sp-3); }
.qm-kh { display: flex; align-items: baseline; gap: var(--sp-2); margin-bottom: 4px;
  padding-bottom: 3px; border-bottom: 1px solid var(--edge-quiet); }
.qm-kh b { font: 800 12.5px var(--font-display); color: var(--gold-lt); }
/* SEVEN ACROSS, so a set of fourteen is exactly two full rows and the layout
   reads as decided rather than as having run out. Falls back to auto-fill below
   the width where seven tiles stop being legible, which is the only place a
   ragged last row is better than a cramped even one. */
.qm-row { display: grid; grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); gap: 6px; max-width: 1180px; }
@media (min-width: 760px) { .qm-row { grid-template-columns: repeat(7, minmax(0, 1fr)); } }
.qmsk { display: flex; flex-direction: column; align-items: center; gap: 2px; cursor: pointer; min-width: 0;
  padding: 5px 4px; border-radius: var(--r-md);
  background: rgba(var(--shade-rgb), 0.55); border: 1px solid var(--edge-quiet);
  transition: transform var(--t-1) var(--e-out), border-color var(--t-1) var(--e-out); }
.qmsk:hover { transform: translateY(-2px); border-color: var(--edge); }
.qmsk.on { border-color: var(--gold-lt); box-shadow: 0 0 12px rgba(var(--gold-rgb), 0.3); }
.qmsk.won { border-color: rgba(var(--gold-rgb), 0.45); }
/* Unowned is shown IN FULL, for the reason the muster strip learned the hard
   way: a shop whose job is showing you what a skin looks like must show it. */
.qmsk.shut { background: rgba(var(--shade-rgb), 0.7); }
.qmsk.shut .qmsk-pv { opacity: 0.9; }
.qmsk-pv { display: block; width: 100%; height: auto; max-width: 118px; }
.qmsk b { font: 700 9.5px var(--font-display); color: var(--gold-lt); text-align: center;
  line-height: 1.15; white-space: normal; overflow-wrap: anywhere; }
.qmsk span { font: 600 9px var(--font-ui); color: var(--muted); text-align: center;
  font-variant-numeric: var(--nums); line-height: 1.15; white-space: normal; overflow-wrap: anywhere; }
.qmsk.on span { color: var(--ready-lt); }
.qmsk.shut:not(.won) span { color: var(--gold-lt); }

@media (max-width: 640px) {
  .qm-body.fw { grid-template-columns: 1fr 1fr; }
  .qm-row { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
