/* ==========================================================================
 * src/styles/combat-screens.css — THE WAR TABLE and THE FIGHT (b362)
 *
 * Phase 1 of docs/design/combat-screen-rework.md. Loads LAST, after
 * combat-hud.css, and everything in it is scoped to `#panel-combat` or to one
 * of the `wt-` / `fs-` namespaces this rework introduced.
 *
 * WHY THERE ARE `!important`s IN HERE, AND ONLY WHERE THERE ARE
 * -------------------------------------------------------------
 * `audit-overrides.css` declares the three-column combat grid with `!important`
 * and no media query (l.488), and combat-hud.css §7 already had to undo it once
 * for a 420px landscape phone. The two-screen split replaces that layout
 * outright, so the same undoing is needed at every width. Each `!important`
 * below is cancelling a specific weighted rule in an older sheet, never winning
 * an argument with an unweighted one. The legacy cards are HIDDEN, not deleted:
 * `#monster-list`, `#loadout-panel` and `#combat-area` are still live render
 * targets for the engine, so their hosts have to stay in the document.
 *
 * NO HARDCODED COLOURS. Every value is a theme token, so cozy-light and
 * hearthlight both get a designed screen from one sheet. The one exception is
 * the stage backdrop's black scrim, which is a LIGHTING layer over a photograph
 * rather than a surface colour — it is authored as `rgba(0,0,0,·)` for the same
 * reason a vignette is, and it is the layer COMBAT-UI-17's painted biomes drop
 * into (see §THE STAGE below).
 * ========================================================================== */

/* ── 1 · THE PANEL BECOMES TWO VIEWS ──────────────────────────────────────*/
#panel-combat.active {
  display: flex !important;
  flex-direction: column !important;
  grid-template-columns: none !important;
  grid-template-rows: none !important;
  grid-template-areas: none !important;
  gap: 0 !important;
  padding: 10px !important;
  overflow: hidden !important;
  min-height: 0;
}

/* The retired surfaces. Every one of these is REPLACED by a new element:
 *   .combat-picker      → the War Table grid          (still owns #monster-list)
 *   .combat-loadout     → the quick-swap strip        (still owns #loadout-panel)
 *   .combat-style-block → the on-stage style selector
 *   #hr-botd-card / #hr-weekly-card → destination cards
 *   #cmb-mob-tabs       → the split itself; the Fight has no sub-tabs, which is
 *                         what retires the b230 / b334 blank-screen trap class */
#panel-combat.active .combat-picker,
#panel-combat.active .combat-loadout,
#panel-combat.active > .combat-style-block,
#panel-combat.active #hr-botd-card,
#panel-combat.active #hr-weekly-card,
#panel-combat.active #cmb-mob-tabs {
  display: none !important;
}

#panel-combat .cbt-views { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
#panel-combat .wt-view,
#panel-combat .fs-view { display: none; flex: 1 1 auto; min-height: 0; }
#panel-combat[data-combat-view="table"] .wt-view { display: flex; flex-direction: column; gap: 10px; }
#panel-combat[data-combat-view="fight"] .fs-view { display: flex; flex-direction: column; gap: 8px; }

/* ══════════════════════════════════════════════════════════════════════════
 * 2 · THE WAR TABLE
 * ════════════════════════════════════════════════════════════════════════*/

/* COMBAT-UI-02 — the return ribbon. The hub's most prominent row when a fight
 * is live, because that is what makes browsing anxiety-free: the fight is
 * running, it is right there, and it is one tap back. */
#panel-combat .wt-ribbon {
  display: flex; align-items: center; gap: 12px;
  padding: 8px 12px;
  background: linear-gradient(180deg, var(--gold-bg), transparent);
  border: 1px solid var(--line-strong);
  border-radius: var(--r, 8px);
  flex: 0 0 auto;
}
#panel-combat .wt-ribbon[hidden] { display: none; }
#panel-combat .wtr-kicker {
  font-family: var(--f-label, var(--f-ui)); font-size: var(--t-micro, 11px);
  font-weight: 800; letter-spacing: .1em; text-transform: uppercase; color: var(--gold-2);
}
#panel-combat .wtr-art { width: 34px; height: 34px; display: flex; align-items: center; justify-content: center; }
#panel-combat .wtr-art img { width: 100%; height: 100%; object-fit: contain; }
#panel-combat .wtr-name {
  font-family: var(--f-display); font-weight: 800; font-size: var(--t-body, 15px);
  color: var(--ink); white-space: nowrap;
}
#panel-combat .wtr-hp {
  position: relative; flex: 1 1 120px; max-width: 220px; height: 18px;
  background: var(--field-sunk, rgba(0,0,0,.3)); border: 1px solid var(--line-soft);
  border-radius: 9px; overflow: hidden; display: flex; align-items: center;
}
/* The ribbon's two bars are the reason the ribbon exists — "am I winning?" at a
 * glance from the hub — so the fill takes the real accent rather than its 18%
 * wash, which measured as invisible against the sunken track. */
#panel-combat .wtr-hp i { position: absolute; inset: 0 auto 0 0; background: var(--red); opacity: .55; }
#panel-combat .wtr-hp.you i { background: var(--green); opacity: .55; }
#panel-combat .wtr-hp b {
  position: relative; width: 100%; text-align: center;
  font-variant-numeric: tabular-nums; font-size: var(--t-micro, 11px);
  font-weight: 800; color: var(--ink);
}
#panel-combat .wtr-kills { font-size: var(--t-micro, 11px); color: var(--ink-2); white-space: nowrap; }
#panel-combat .wtr-go { margin-left: auto; white-space: nowrap; }

/* COMBAT-UI-05 — the destination row. Cards are big because a destination is
 * supposed to be, and none of them ever disappears: an empty slot in a menu is
 * what teaches the rotation. */
#panel-combat .wt-dest-rail { flex: 0 0 auto; overflow-x: auto; overscroll-behavior-x: contain; }
/* The rail is a keyboard-reachable region (it carries tabindex=0 + a role in
 * combat-screens.js) because a sideways scroller a mouse can drag and a finger
 * can swipe is otherwise unreachable with arrow keys. A focus ring on a scroll
 * container is the whole point of giving it focus, so it is drawn, not removed. */
#panel-combat .wt-dest-rail:focus-visible,
#panel-combat .wt-classes:focus-visible {
  outline: 2px solid var(--gold); outline-offset: 2px; border-radius: var(--r, 3px);
}
/* THE RAIL FITS ITS SIX, OR IT WRAPS — IT NEVER HANGS OFF THE PANEL (b548).
 * `min-width: min-content` made the row as wide as the widest card's intrinsic
 * content, so at 1440x900 the six cards resolved to 217px each, the row to
 * 1340px inside a 1240px rail, and the World Event card's right edge landed at
 * 1530 against a panel that ends at 1440 — name, meta and its "Events" button
 * all sliced by `.panel`'s overflow, with the scroll that was supposed to
 * rescue it offering no affordance a player would ever find. A destination the
 * player cannot see is not a destination. The row now shrinks to the card
 * floor first and WRAPS second, which is the honest failure mode: a second row
 * of cards you can read beats a first row you cannot reach. */
#panel-combat .wt-dests { display: flex; flex-wrap: wrap; gap: 8px; min-width: 0; }
#panel-combat .wt-dest {
  flex: 1 1 190px; min-width: 0; max-width: 320px;
  display: flex; flex-direction: column; gap: 6px;
  padding: 8px 10px;
  background: var(--surf-sunken, rgba(0,0,0,.18));
  border: 1px solid var(--line-soft);
  border-radius: var(--r, 8px);
}
#panel-combat .wt-dest header { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
#panel-combat .wtd-kick {
  font-family: var(--f-label, var(--f-ui)); font-size: var(--t-micro, 11px);
  font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: var(--gold-2);
  min-width: 0;
}
#panel-combat .wtd-count {
  font-variant-numeric: tabular-nums; font-weight: 800; font-size: var(--t-micro, 11px);
  color: var(--ink); background: var(--gold-bg); border-radius: 999px; padding: 0 6px;
}
#panel-combat .wtd-timer { margin-left: auto; font-size: var(--t-micro, 11px); color: var(--ink-3); white-space: nowrap; }
#panel-combat .wtd-body { display: flex; gap: 8px; align-items: center; min-height: 46px; }
#panel-combat .wtd-img { width: 46px; height: 46px; object-fit: contain; flex: 0 0 auto; display: flex; align-items: center; justify-content: center; }
#panel-combat .wtd-img.is-emoji { font-size: 26px; }
/* b366: the destination glyph is a baked atlas icon, not an emoji. */
#panel-combat .wtd-img.is-glyph { color: var(--gold-2); }
#panel-combat .wtd-img.is-glyph svg { width: 30px; height: 30px; }
#panel-combat .wtr-kicker .hr-glyph { vertical-align: -2px; margin-right: 4px; }
#panel-combat .wtd-main { display: flex; flex-direction: column; min-width: 0; }
/* NAMES WRAP, THEY ARE NEVER TRIMMED. The catalogue's longest destination is
 * "Elderscale, the Great Wyrm" (26 chars) and the roster carries "Magma
 * Elemental" and "The Hollow Regent"; no card width that fits six across a
 * 1240px rail will ever hold those on one line, so the old
 * nowrap+ellipsis rendered "Magma Elemen..." as the permanent state of the
 * Boss of the Day. A destination's name is the one word the card exists to
 * say. Two short lines, full name, no tooltip required. */
#panel-combat .wtd-main b {
  font-family: var(--f-display); font-size: var(--t-small, 13px); color: var(--ink);
  white-space: normal; overflow-wrap: anywhere; line-height: 1.15;
}
#panel-combat .wtd-main span { font-size: var(--t-micro, 11px); color: var(--ink-2); }
#panel-combat .wt-dest footer { display: flex; }
#panel-combat .wt-dest footer .btn { width: 100%; justify-content: center; }

/* The filter bar — tier chips plus the class filter, one row. */
#panel-combat .wt-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; flex: 0 0 auto; }
#panel-combat .wt-chips { display: flex; gap: 4px; flex-wrap: wrap; }
#panel-combat .wt-classes { opacity: .95; }
#panel-combat .wt-chips .chip { padding: 3px 10px; font-size: var(--t-micro, 11px); }

/* COMBAT-UI-03 — the grid. Sized FROM THE ROSTER: the largest tier is 21
 * monsters, and at ~1600px of content a 160px track runs 9 across, so a whole
 * tier is three rows with no scroll. A menu that shows three items on a 1900px
 * screen is a failure, so the card bends to the roster and not the reverse. */
#panel-combat .wt-grid {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  /* `min-content` rows and a `min-height` on the card, because a grid row that
     is allowed to be shorter than its item makes the item's flex children
     SHRINK — measured in the b362 gate at 922x423: the row resolved to 48px, the
     128px portrait shrank to 9px, and every card was art-only with its name,
     weakness and kill count squeezed to nothing. A browsing card whose contents
     are negotiable is not a browsing card. */
  grid-auto-rows: min-content;
  gap: 8px;
  align-content: start;
  padding-right: 2px;
}
#panel-combat .wt-card {
  position: relative;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 8px 6px 6px;
  background: var(--surf-sunken, rgba(0,0,0,.18));
  border: 1px solid var(--line-soft);
  border-radius: var(--r, 8px);
  cursor: pointer; text-align: center; overflow: hidden;
  min-height: 186px;
  transition: border-color .12s, transform .12s;
}
#panel-combat .wt-card:hover:not(:disabled),
#panel-combat .wt-card:focus-visible { border-color: var(--gold-2); transform: translateY(-2px); }
#panel-combat .wt-card.is-live { border-color: var(--gold-2); box-shadow: 0 0 0 1px var(--gold-2) inset; }
#panel-combat .wt-card.is-locked { opacity: .5; cursor: default; }
#panel-combat .wtc-art { width: 100%; flex: 0 0 108px; display: flex; align-items: center; justify-content: center; }
#panel-combat .wtc-img { max-width: 100%; max-height: 108px; object-fit: contain; }
/* `.is-emoji` is retired: monsterArt() no longer has an emoji branch (the last
   one was `m.icon || '👾'` at portrait size). The unwired-monster fallback is a
   medallion or a red-ringed skull, both `.is-glyph`. Rule kept, sized for a
   glyph, so an old cached render does not lose its box.
   The svg is emitted at 100% now, so EVERY mount must state a box — an unsized
   `.is-glyph` would inherit the frame and paint a 340px skull. */
#panel-combat .wtc-img.is-emoji,
#panel-combat .wtc-img.is-glyph { display: flex; align-items: center; justify-content: center; line-height: 1; width: 84px; height: 84px; }
#panel-combat .wtc-img.is-glyph svg { width: 100%; height: 100%; }
#panel-combat .wtr-img.is-glyph { display: flex; align-items: center; justify-content: center; width: 38px; height: 38px; }
#panel-combat .wtd-img.is-glyph.wtd-img { width: 46px; height: 46px; }
/* THE FOE PLATE, and why this is keyed on `.hr-glyph` rather than on the
   renderer's own class. That plate is filled by TWO code paths — the legacy
   arena markup in legacy.js and `setHtmlOnce(...monsterArt(id,'fs-foe-img'))`
   in combat-screens.js — and only one of them carries `.fs-foe-img`. Sizing the
   ATOM (the glyph span every path emits) covers both, which a per-renderer
   selector demonstrably did not: I shipped one, photographed it, and the mark
   was still 19% of a 340px frame because the other path had drawn it.
   `!important` is required and is not laziness: HR.icon writes `--gsz` INLINE,
   and an inline custom property beats a stylesheet declaration without it. */
.arena-portrait .fs-foe-img.is-glyph,
#arena-foe-portrait .fs-foe-img.is-glyph {
  width: 100%; height: 100%;
  display: flex; align-items: center; justify-content: center;
}
.arena-portrait .hr-glyph,
#arena-foe-portrait .hr-glyph,
.arena-portrait .fs-foe-img.is-glyph > svg {
  width: 56% !important; height: 56% !important;
  filter: drop-shadow(0 3px 5px rgba(0,0,0,.7));
}
.arena-portrait .hr-glyph svg { width: 100%; height: 100%; }
#panel-combat .wtc-name {
  font-family: var(--f-display); font-size: var(--t-small, 13px); font-weight: 700;
  color: var(--ink); line-height: 1.15;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
/* The two numbers a player compares between foes, in a FIXED position on every
 * card so the grid can be scanned straight down instead of read row by row. */
#panel-combat .wtc-stats {
  display: flex; align-items: baseline; gap: 6px;
  font-size: var(--t-micro, 11px); font-variant-numeric: tabular-nums;
}
#panel-combat .wtc-stats em { font-style: normal; color: var(--ink-3); text-transform: capitalize; }
#panel-combat .wtc-stats b { color: var(--ink-2); }
#panel-combat .wtc-kills {
  font-family: var(--f-label, var(--f-ui)); font-size: var(--t-micro, 11px);
  font-weight: 800; letter-spacing: .04em; color: var(--ink-2);
}
#panel-combat .wtc-kills.is-new { color: var(--gold-2); }
#panel-combat .wtc-kills.is-live { color: var(--gold-2); }
#panel-combat .wtc-kills.is-locked { color: var(--ink-3); }
/* The drop summary is a HOVER reveal on desktop and simply absent on touch —
 * the same two lines live in the preview state, which is where a phone reads
 * them. It is absolutely positioned so revealing it cannot reflow the grid. */
/* BUG 5: the overlay now carries the FULL drop table (not a 2-drop teaser), so
 * it is a scrollable column that fills the card on hover/focus — the same
 * "what do I win" answer the fight-setup screen gives, one step earlier. Still
 * absolutely positioned so revealing it never reflows the grid. */
#panel-combat .wtc-drops {
  position: absolute; left: 0; right: 0; bottom: 0; top: 0;
  display: flex; flex-direction: column; gap: 1px;
  padding: 4px 6px; overflow-y: auto;
  background: var(--bg-3);
  font-size: var(--t-micro, 11px); color: var(--ink-2);
  opacity: 0; transform: translateY(6px); transition: transform .12s, opacity .12s;
  pointer-events: none;
}
#panel-combat .wt-card:hover:not(:disabled) .wtc-drops,
#panel-combat .wt-card:focus-visible .wtc-drops { opacity: 1; transform: translateY(0); pointer-events: auto; }
#panel-combat .wtc-drops-head {
  font-family: var(--f-label, var(--f-ui)); font-weight: 800; letter-spacing: .04em;
  color: var(--ink-3); text-transform: uppercase; font-size: var(--t-micro, 11px);
  padding-bottom: 2px; margin-bottom: 2px; border-bottom: 1px solid var(--line-soft);
  position: sticky; top: 0; background: var(--bg-3);
}
#panel-combat .wtc-drop-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 6px;
  font-variant-numeric: tabular-nums;
}
#panel-combat .wtc-drop-row em { font-style: normal; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#panel-combat .wtc-drop-row b { color: var(--ink-2); flex: 0 0 auto; }
#panel-combat .wtc-drop-row.is-coin b { color: var(--gold-2); }
#panel-combat .wtc-drop-row.is-empty em { color: var(--ink-3); font-style: italic; }
/* Same rarity ladder as the loot modal's rows (.fsm-drop.r-*): the pct band is
 * the tell that a drop is worth farming for. Tokens only. */
#panel-combat .wtc-drop-row.r-common b { color: var(--ink-2); }
#panel-combat .wtc-drop-row.r-uncommon b { color: var(--green); }
#panel-combat .wtc-drop-row.r-rare b { color: var(--gold-2); }
#panel-combat .wtc-drop-row.r-vrare b { color: var(--gem); }
#panel-combat .wtc-drop-row.r-legendary b { color: var(--red); }

/* COZY-LIGHT SURFACES. `--surf-sunken` resolves to `rgba(0,0,0,.3)` in every
 * theme, which is a recessed well under hearthlight's warm dark and a muddy
 * smear under cozy-light's parchment — with `--ink` (dark cocoa) printed on top
 * of it. Measured in the b362 gate: the War Table cards read as dark-on-dark.
 * The light theme takes its own elevated surface token instead, and the gilt
 * kickers step down to the deeper gold, which is the one that survives on
 * parchment. Tokens only; no theme is given a literal. */




/* ══════════════════════════════════════════════════════════════════════════
 * 3 · THE FIGHT
 * ════════════════════════════════════════════════════════════════════════*/
#panel-combat .fs-top {
  display: flex; align-items: center; gap: 10px; flex: 0 0 auto;
  padding: 4px 2px;
}
#panel-combat .fs-back {
  background: none; border: 1px solid var(--line-soft); border-radius: var(--r, 8px);
  color: var(--ink-2); padding: 4px 10px; cursor: pointer;
  font-family: var(--f-label, var(--f-ui)); font-size: var(--t-micro, 11px);
  font-weight: 800; letter-spacing: .06em; text-transform: uppercase;
}
#panel-combat .fs-back:hover { color: var(--gold-2); border-color: var(--gold-2); }
#panel-combat .fs-title { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
#panel-combat .fs-title b {
  font-family: var(--f-display); font-size: var(--t-lead, 17px); font-weight: 800;
  letter-spacing: .04em; text-transform: uppercase; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
#panel-combat .fs-title span { font-size: var(--t-micro, 11px); color: var(--ink-3); white-space: nowrap; }

/* ══════════════════════════════════════════════════════════════════════════
 * 3b · THE MANAGEMENT RAIL — COMBAT-UI-12, rebuilt (b366)
 *
 * Tyler, on b365: "I think you forgot some buttons? Loot? Eat food? How is
 * someone supposed to manage their loadouts/armor/weapon?" — and, on the
 * benchmark: "refer to the combat screen from Melvor. I think we need to
 * incorporate more."
 *
 * b362's quick-swap strip read the brief too literally: it SHOWED six slots and
 * could change none of them, and Melvor keeps the equipment panel, the food
 * slot and the loot container permanently on the fight screen. The rail is the
 * answer, and it is paid for out of space the stage was wasting — the b362
 * layout left a wide empty band either side of the two plates, which is the
 * "dead space is a defect" rule failing in the other direction.
 *
 * It sits BESIDE the stage rather than above it because the fight's own rows
 * are the thing that must not move; a rail can scroll internally on a short
 * screen without costing the stage a pixel of height.
 * ════════════════════════════════════════════════════════════════════════*/
/* THE RAIL IS ON THE LEFT, AND THAT IS A MEASURED CHOICE, NOT A STYLE ONE.
 * It is where Melvor puts it, but the reason it moved after the first capture
 * is ours: the notification stack and the Chat button both live at the RIGHT
 * edge, so a right-hand rail spends its life under a toast — the b366 desktop
 * capture had "Achievement unlocked", three "Equipped …" toasts and the daily
 * reward sitting directly on top of the weapon row and the drop table. A
 * persistent panel cannot share a column with transient overlays. */
#panel-combat .fs-body {
  flex: 1 1 auto; min-height: 0;
  display: grid; grid-template-columns: 232px minmax(0, 1fr); gap: 10px;
}
#panel-combat .fs-manage {
  min-height: 0; min-width: 0;
  display: flex; flex-direction: column; gap: 8px;
  overflow-y: auto; overscroll-behavior: contain;
  padding: 8px;
  background: var(--surf-sunken, rgba(0,0,0,.18));
  border: 1px solid var(--line-soft); border-radius: var(--r, 8px);
}
/* NEVER SHRINK. Measured at 922x423: with the default `flex-shrink: 1` the rail
 * squeezed each block below its content height, and because the equipment grid's
 * cells carry an `aspect-ratio` they did not shrink with it — so PROVISIONS
 * printed straight across the boots and WHAT IT DROPS across the food line. A
 * rail that runs out of room must SCROLL, not overlap itself. */
#panel-combat .fsm-block { display: flex; flex-direction: column; gap: 5px; flex: 0 0 auto; }
#panel-combat .fsm-block-grow { flex: 1 1 auto; min-height: 74px; }
#panel-combat .fsm-head {
  display: flex; align-items: baseline; gap: 6px;
  font-family: var(--f-label, var(--f-ui)); font-size: var(--t-micro, 11px);
  font-weight: 800; letter-spacing: .09em; text-transform: uppercase; color: var(--gold-2);
}
#panel-combat .fsm-head b {
  margin-left: auto; color: var(--ink-2); letter-spacing: 0; text-transform: none;
  font-family: var(--f-ui); font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* THE EQUIPMENT PANEL. Twelve slots in the canonical three-wide PAPER-DOLL —
 * helmet at the apex, weapon and offhand flanking the body, the ring pair
 * flanking the boots — each wearing the item's own painted art; the empty state
 * is legacy's gilt line glyph, never the emoji in EQUIP_SLOT_META. Click opens
 * a picker filtered to that slot.
 *
 * THE SHAPE IS NOT AUTHORED IN THIS SHEET. Every cell comes from
 * src/render/doll-layout.js and is applied inline by renderDoll(), the same
 * table Character > Equipment and the Inventory Equip column read, so a slot
 * cannot move between screens. The row count is inline too — this rail omits
 * belt and earrings, so it draws five rows where the Character doll draws six.
 * The cell is capped at 72px (the `--td-cell` the other mounts use) and the
 * grid is centred, so a wider rail gives a centred doll, never an inflated one.
 *
 * The historical note below is why the column count is THREE and must stay
 * three; it is now doubly true, because the columns are anatomy. */
/* THREE ACROSS, NOT FOUR (b371) — and the reason is the LABEL, measured.
 * `--t-micro` resolves to 14.5px (b227's legibility floor, guarded), so the
 * empty-slot caption cannot be shrunk to fit. At four columns in a 232px rail
 * each tile is 51px with 45px of usable line, and exactly two of the twelve
 * captions are wider than that: "Offhand" and "Necklace" came back as
 * "OFFHAN / D" and "NECKLA / CE" — the same half-a-word failure b139 ruled
 * against and b368 fixed from the other side. Three columns give a 68px tile
 * and a 62px line, which both fit whole.
 *
 * Renaming the two offenders was the cheaper fix and it is the wrong one: the
 * captions come from `EQUIP_SLOT_META`, which the Character paper-doll also
 * reads, so a local rename would make one screen call a slot Amulet and the
 * other Necklace. Widening the tile costs 128px of rail height (the DROPS
 * block, which grows, gives it up) and buys a second thing worth having — the
 * painted item art renders at 68px instead of 51, which is the difference
 * between reading an icon and recognising a smear. The 168px phone rail keeps
 * four across: the captions are hidden there anyway. */
#panel-combat .fsm-doll {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 72px));
  justify-content: center; gap: 4px;
}
#panel-combat .fsm-slot {
  position: relative; aspect-ratio: 1 / 1;
  display: flex; align-items: center; justify-content: center;
  padding: 3px;
  background: var(--field-sunk, rgba(0,0,0,.3));
  border: 1px solid var(--line-soft); border-radius: 6px;
  cursor: pointer; overflow: hidden;
  transition: border-color .12s;
}
#panel-combat .fsm-slot:hover, #panel-combat .fsm-slot:focus-visible { border-color: var(--gold-2); }
#panel-combat .fsm-slot.is-empty { border-style: dashed; }
#panel-combat .fsm-slot-art { display: flex; width: 100%; height: 100%; }
#panel-combat .fsm-slot-art img { width: 100%; height: 100%; object-fit: contain; }
#panel-combat .fsm-slot-gly { display: block; width: 46%; height: 46%; color: var(--gold-2); opacity: .55; }
#panel-combat .fsm-slot-gly svg { width: 100%; height: 100%; }
/* The slot NAME rides the bottom edge of the empty state only: a filled slot is
 * identified by the thing in it, and printing a label over real art is how the
 * b217 doll ended up illegible. */
#panel-combat .fsm-slot em {
  position: absolute; left: 0; right: 0; bottom: 1px;
  font-style: normal; font-family: var(--f-label, var(--f-ui));
  font-size: var(--t-micro, 11px); line-height: 1.05; letter-spacing: 0;
  color: var(--ink-3); text-align: center;
  /* WRAPS, NOT ELLIPSISES. At a 52px slot "Offhand" and "Necklace" came back
   * as "OFFHA…" and "NECKL…" — and b139 already ruled that a truncated slot
   * label reads as a random string rather than a slot name. Two short lines
   * are legible; half a word is not.
   *
   * b368: `word-break: break-word` is stronger than that argument needs and it
   * broke words that FIT — the desktop capture reads "WEAPO / N" and "OFFHAN /
   * D", which is the same "half a word" failure the rule above exists to
   * prevent, arriving from the other side. `overflow-wrap` only breaks a word
   * that cannot fit a line on its own, so Weapon and Offhand stay whole and
   * Necklace still wraps rather than clipping. */
  overflow: hidden; white-space: normal; word-break: normal; overflow-wrap: break-word;
}
#panel-combat .fsm-slot:not(.is-empty) em { display: none; }

/* THE FOOD SLOT. Not a second Eat button — the button is the HUD's, in the
 * action bar. This is what you will eat, what it heals and how many you hold,
 * visible BEFORE the fight, which is the only time that answer can still change
 * your mind. */
#panel-combat .fsm-food { display: flex; align-items: center; gap: 8px; }
#panel-combat .fsm-food-slot {
  position: relative; width: 44px; height: 44px; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center; padding: 3px;
  background: var(--field-sunk, rgba(0,0,0,.3));
  border: 1px solid var(--line-soft); border-radius: 6px;
}
#panel-combat .fsm-food-slot.is-empty { border-style: dashed; }
#panel-combat .fsm-food-slot img { width: 100%; height: 100%; object-fit: contain; }
#panel-combat .fsm-food-slot i {
  position: absolute; right: 1px; bottom: 0;
  font-style: normal; font-size: var(--t-micro, 11px); font-weight: 800;
  color: var(--ink); text-shadow: 0 1px 2px rgba(0,0,0,.65);
  font-variant-numeric: tabular-nums;
}
#panel-combat .fsm-food-txt { display: flex; flex-direction: column; min-width: 0; }
#panel-combat .fsm-food-txt b {
  font-family: var(--f-display); font-size: var(--t-small, 13px); color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#panel-combat .fsm-food-txt span { font-size: var(--t-micro, 11px); color: var(--ink-2); }

/* THE DROPS CONTAINER — the foe's table before the fight, the ledger during it.
 * The rarity band is the same ladder the Loot modal uses, carried as a colour on
 * the CHANCE rather than on the whole row: the row is a name, the rarity is a
 * property of the number beside it. */
#panel-combat .fsm-drops {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  display: flex; flex-direction: column; gap: 2px;
}
#panel-combat .fsm-drop {
  display: flex; align-items: center; gap: 6px;
  font-size: var(--t-micro, 11px); color: var(--ink-2);
}
#panel-combat .fsm-drop-art {
  width: 20px; height: 20px; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center; color: var(--gold-2);
}
#panel-combat .fsm-drop-art svg { width: 16px; height: 16px; }
#panel-combat .fsm-drop-art img { width: 100%; height: 100%; object-fit: contain; }
#panel-combat .fsm-drop span { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#panel-combat .fsm-drop b { color: var(--ink); font-variant-numeric: tabular-nums; white-space: nowrap; }
#panel-combat .fsm-drop.r-common b { color: var(--ink-2); }
#panel-combat .fsm-drop.r-uncommon b { color: var(--green); }
#panel-combat .fsm-drop.r-rare b,
#panel-combat .fsm-drop.is-coin b { color: var(--gold-2); }
#panel-combat .fsm-drop.r-vrare b { color: var(--gem); }
#panel-combat .fsm-drop.r-legendary b { color: var(--red); }
#panel-combat .fsm-empty,
#panel-combat .fsm-foot { font-size: var(--t-micro, 11px); color: var(--ink-3); }
#panel-combat .fsm-foot { margin-top: 4px; border-top: 1px solid var(--line-soft); padding-top: 3px; }
/* The picker's rows carry the item's art beside its name. */
.hr-room-scrim .fsm-pick-art { display: inline-flex; width: 22px; height: 22px; vertical-align: -6px; margin-right: 6px; }
.hr-room-scrim .fsm-pick-art img { width: 100%; height: 100%; object-fit: contain; }




/* THE STAGE. The arena card is now the page: no monster column, no loadout
 * column. `.fs-scrim` is a real element rather than another background layer on
 * the card, because COMBAT-UI-17's painted biomes drop in behind it — the image
 * goes on `.combat-arena`, the tier tint and the reduced scrim go here, and
 * neither has to touch the other's rules to land. Ships at today's dark stage. */
#panel-combat[data-combat-view="fight"] .combat-arena {
  grid-area: auto !important;
  flex: 1 1 auto; min-height: 0;
  display: flex; flex-direction: column;
  overflow: hidden !important;
  position: relative;
}
#panel-combat .fs-stage-host { flex: 1 1 auto; min-height: 0; display: flex; }

/* COMBAT-UI-17 — PAINTED PER-CLASS BACKDROPS.
 * background-collapsed here (was three separate sheets independently
 * painting dungeon.jpg onto this same element, stacking to ~90% black — see
 * the removal notes left in art-direction.css / audit-overrides.css /
 * theme-cozy.css). This is now the ONLY sheet that sets a background-image
 * on .combat-arena, and it loads last (index.html) so it wins the cascade.
 * `renderFight()` in combat-screens.js sets `data-foe-class`/`data-foe-tier`
 * on #panel-combat beside the existing `data-fightState` line. An unmatched
 * or missing class falls back cleanly to dungeon.jpg (the base rule below). */
body[data-theme] #panel-combat .combat-arena {
  background-image: url('../../assets/icons-bundle/backgrounds/dungeon.jpg') !important;
  background-position: center top !important;
  background-size: cover !important;
  background-repeat: no-repeat !important;
}
body[data-theme] #panel-combat[data-foe-class="vermin"] .combat-arena {
  background-image: url('../../assets/icons-bundle/backgrounds/bg_combat_vermin.jpg') !important;
}
body[data-theme] #panel-combat[data-foe-class="mammal"] .combat-arena {
  background-image: url('../../assets/icons-bundle/backgrounds/bg_combat_mammal.jpg') !important;
}
body[data-theme] #panel-combat[data-foe-class="plant"] .combat-arena {
  background-image: url('../../assets/icons-bundle/backgrounds/bg_combat_plant.jpg') !important;
}
body[data-theme] #panel-combat[data-foe-class="humanoid"] .combat-arena {
  background-image: url('../../assets/icons-bundle/backgrounds/bg_combat_humanoid.jpg') !important;
}
body[data-theme] #panel-combat[data-foe-class="human"] .combat-arena {
  background-image: url('../../assets/icons-bundle/backgrounds/bg_combat_human.jpg') !important;
}
body[data-theme] #panel-combat[data-foe-class="undead"] .combat-arena {
  background-image: url('../../assets/icons-bundle/backgrounds/bg_combat_undead.jpg') !important;
}
/* demon: NO PLATE — bg_combat_demon.png was rejected at QC (visible artist
 * signature mark, bottom-left corner). Falls back to dungeon.jpg above.
 * Regenerate and add a rule here when a clean plate lands. */
body[data-theme] #panel-combat[data-foe-class="dragon"] .combat-arena {
  background-image: url('../../assets/icons-bundle/backgrounds/bg_combat_dragon.jpg') !important;
}
body[data-theme] #panel-combat[data-foe-class="elemental"] .combat-arena {
  background-image: url('../../assets/icons-bundle/backgrounds/bg_combat_elemental.jpg') !important;
}
body[data-theme] #panel-combat[data-foe-class="construct"] .combat-arena {
  background-image: url('../../assets/icons-bundle/backgrounds/bg_combat_construct.jpg') !important;
}
body[data-theme] #panel-combat[data-foe-class="extradimensional"] .combat-arena {
  background-image: url('../../assets/icons-bundle/backgrounds/bg_combat_extradimensional.jpg') !important;
}

#panel-combat .fs-scrim {
  position: absolute; inset: 0; pointer-events: none;
  /* COMBAT-UI-17: lightened from .12/.55 to recover the painted plates above —
   * ~12% at the centre where the fighters stand, ~35% at the edges. */
  background: radial-gradient(120% 80% at 50% 40%, rgba(0,0,0,.12), rgba(0,0,0,.35) 92%);
  z-index: 0;
}
/* Tier tint — a token on .fs-scrim, mixed at low alpha, so six more files
 * don't have to exist for the same read. T1 warm/green -> T6 cold/violet. */
#panel-combat[data-foe-tier="1"] .fs-scrim { --tier-tint: rgba(154, 183, 106, .10); }
#panel-combat[data-foe-tier="2"] .fs-scrim { --tier-tint: rgba(201, 162, 74, .10); }
#panel-combat[data-foe-tier="3"] .fs-scrim { --tier-tint: rgba(224, 140, 74, .10); }
#panel-combat[data-foe-tier="4"] .fs-scrim { --tier-tint: rgba(200, 90, 90, .10); }
#panel-combat[data-foe-tier="5"] .fs-scrim { --tier-tint: rgba(110, 120, 200, .10); }
#panel-combat[data-foe-tier="6"] .fs-scrim { --tier-tint: rgba(150, 90, 200, .10); }
#panel-combat[data-foe-tier] .fs-scrim {
  background:
    radial-gradient(120% 80% at 50% 40%, var(--tier-tint, transparent), transparent 70%),
    radial-gradient(120% 80% at 50% 40%, rgba(0,0,0,.12), rgba(0,0,0,.35) 92%);
}
/* THE LIGHT THEME GETS A LIGHT STAGE, AND THIS IS NOT A DETAIL.
 * The arena card carries a dark dungeon photograph under a heavy scrim in every
 * theme (audit-overrides.css l.554, unthemed). That was survivable while the
 * arena was one third of the screen; now the stage IS the screen, and in
 * cozy-light every label on it is `--ink` — dark cocoa on a black photo, which
 * measured as illegible in the b362 visual gate (name, tiles and swing labels
 * all vanished). Two ways out: force a light ink onto the stage, or stop making
 * the stage dark in a light theme. The second is the honest one — cozy-light is
 * a parchment theme and a black rectangle is not cozy — and it keeps every
 * label on the theme's own token, which is what makes the type legible by
 * construction rather than by a second palette that can drift.
 * When the painted biomes land (COMBAT-UI-17) this is the one rule that decides
 * whether they appear here, and the scrim layer above is where their tint goes. */



#panel-combat[data-combat-view="fight"] .combat-arena .card-head { display: none !important; }

/* `flex: 1 0 auto` — GROW, NEVER SHRINK. The no-shrink half is b227's rule and
 * is asserted by its test: the log gives up height, never the champion. The
 * grow half is this rework's: the stage is the page now, so it takes the slack
 * instead of leaving a band of painted nothing between the action bar and the
 * log. Dead space is a defect. */
/* COMBAT-UI-09 — THE FIVE-ROW GRAMMAR IS A REAL GRID, NOT TWO COLUMNS THAT
 * HAPPEN TO MATCH. Two independent flex columns drift the moment the plates
 * differ in size (the foe's is 2x the player's by design), and a row that does
 * not line up across the VS cannot be compared across it — which is the entire
 * job of a symmetric frame. So `.arena-side` becomes `display: contents`: the
 * wrappers keep their classes (legacy.js finds the portraits through
 * `.arena-side.foe .arena-portrait` for the damage numbers, the pet perch and
 * the death stamp) but lose their boxes, and every row on both sides shares one
 * track. Name sits beside name, bar beside bar, tile beside tile, always. */
/* THE ONE `display: !important` THAT IS NOT UNDOING A STYLESHEET.
 * `refreshArenaVs()` in legacy.js hides the stage with an INLINE style whenever
 * no fight is running — correct when the arena shared a screen with the picker
 * (a stale foe lingering beside the monster list is a lie), and wrong now that
 * the preview state IS the stage with the fight not started. An `!important`
 * declaration outranks an inline style, so this is the whole fix, and the
 * alternative — teaching the engine's 200ms tick about a view attribute — would
 * put screen state inside the combat loop, which is the coupling this rework
 * exists to remove. */
#panel-combat[data-combat-view="fight"] .arena-vs.fs-stage { display: grid !important; }
#panel-combat .arena-vs.fs-stage {
  /* z-index 2, ABOVE the log row's 1 (b513). The stage carries every control
   * on this screen and the log carries none, so wherever the two boxes touch
   * the stage must be the one that gets the click. On a landscape phone the
   * stage's last rows overlap the log by ~13px, and at z-index 1 the log — a
   * later sibling — won the paint order and swallowed Eat and Stop: the
   * reachability guard reported "a click at the centre of #arena-act-player
   * lands on #combat-area". The controls were visible and dead. */
  position: relative; z-index: 2;
  /* GROW, NEVER SHRINK — but never exceed the card either. `flex: 1 0 auto`
   * takes the slack the log does not want (b227's rule, asserted by its test:
   * the log gives up height, never the champion). `max-height: 100%` is the
   * other half, added in b513: a no-shrink flex item whose content is taller
   * than its container simply overflows, and because `.combat-arena` is the
   * clipping element the rows that fell out the bottom were not merely off the
   * card — the metrics strip and the session tally were UNPAINTED, and the Eat
   * button sat one bad state away from joining them. Clamping the item makes
   * the grid height DEFINITE, which is what lets row 1 above resolve as `1fr`
   * and absorb the difference instead of the player's controls doing it. */
  flex: 1 0 auto; max-height: 100%;
  align-content: center;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  /* TEN ROWS, ALL NAMED BELOW. The template used to stop at eight and let the
   * readout strips land on IMPLICIT rows, which is how b487's session tally
   * came to be drawn on top of the metrics strip: `.fs-session` also carries
   * `.fs-metrics`, so it inherited `grid-row: 9` and the two lines printed
   * character-over-character. An implicit row is an unnamed row, and an unnamed
   * row is one class attribute away from being somebody else's row. */
  /* ROW 1 IS THE ONLY ELASTIC ROW, AND IT IS ELASTIC BY CONSTRUCTION (b513).
   * Every other row on this stage is fixed-height type or chrome; the plates
   * are the only thing that can pay for the fold. Until b513 that payment was
   * arithmetic in a comment — `min(42vh, 340px, calc(100vh - 540px))`, where
   * 540 was a hand-measurement of the nine rows below plus the shell. Any row
   * that grew by a pixel (a longer provision name, a knocked-out activity
   * line, a second tally clause) spent a pixel the constant did not know
   * about, and the Eat button walked out of the card 6–8px at a time. Twice.
   * `minmax(0, <cap>)` asks the browser the question the constant was guessing
   * at. The track's GROWTH LIMIT is the art direction (42vh, never past 340px,
   * so a 4K monitor cannot render a 900px slime); its BASE is zero, so when the
   * card is shorter than the cap plus the nine rows below, the plate — and only
   * the plate — gives the difference back. Whatever those rows measure TODAY,
   * in THIS state, row 1 takes the remainder and no more. `1fr` would have been
   * the obvious spelling and is wrong: a `fr` track collapses to min-content
   * whenever the grid's own height is indefinite, which is exactly the case on
   * a tall screen, and the foe measured 96px. See `max-height: 100%` below. */
  grid-template-rows: minmax(0, min(42vh, 340px)) auto auto auto auto auto auto auto auto auto;
  align-items: end;
  justify-items: center;
  gap: 4px 16px;
  padding: 12px 18px 8px;
  margin: 0;
}
#panel-combat .arena-vs.fs-stage > .arena-side { display: contents; }
#panel-combat .arena-vs.fs-stage .arena-side.player > * { grid-column: 1; }
#panel-combat .arena-vs.fs-stage .arena-side.foe > * { grid-column: 3; }
#panel-combat .arena-vs.fs-stage .arena-portrait { grid-row: 1; }
#panel-combat .arena-vs.fs-stage .arena-name { grid-row: 2; }
#panel-combat .arena-vs.fs-stage .fs-lv { grid-row: 3; }
#panel-combat .arena-vs.fs-stage .arena-hp-bar { grid-row: 4; }
#panel-combat .arena-vs.fs-stage .fs-swing { grid-row: 5; }
#panel-combat .arena-vs.fs-stage .fs-tiles { grid-row: 6; }
#panel-combat .arena-vs.fs-stage .fs-style,
#panel-combat .arena-vs.fs-stage .fs-weak { grid-row: 7; }
#panel-combat .arena-vs.fs-stage .arena-vs-divider {
  grid-column: 2; grid-row: 1 / 4; align-self: center;
}
#panel-combat .fs-actionbar { grid-column: 1 / -1; grid-row: 8; width: 100%; }
/* Row 9 = THIS FIGHT (the metrics strip). Row 10 = THIS HUNT (the session
 * tally). The `.fs-session` selector must come after and is more specific than
 * the shared `.fs-metrics` one, because the tally element carries BOTH classes
 * on purpose — it borrows the strip's type idiom and overrides its row. */
#panel-combat .fs-metrics { grid-column: 1 / -1; grid-row: 9; width: 100%; }
#panel-combat .fs-metrics.fs-session { grid-row: 10; }

/* COMBAT-UI-08 / 09 — the hero plates and the symmetric five-row grammar. Same
 * rows on both sides, in the same order, so the eye compares across instead of
 * hunting: name · level · HP+numerics · swing · stats. The foe is ~2x the art
 * weight because it is the subject of the screen. */
/* Both plates STRETCH into row 1 and are capped, rather than being sized: the
 * row hands them whatever is left after the controls are housed, and the cap is
 * the art direction (the foe is ~2x the player's weight because it is the
 * subject of the screen). `aspect-ratio` keeps them square while the height
 * moves, so a shorter viewport shrinks the picture and never the buttons. */
#panel-combat .arena-vs.fs-stage .arena-portrait {
  /* `height: 100%` and NOT `align-self: stretch`: an aspect-ratio box is not
   * auto-sized in either axis, so the stretch alignment is ignored and the
   * plate rendered at its 96px intrinsic size (measured). A percentage resolves
   * against the grid area, which is definite because the stage is clamped. */
  height: 100%; width: auto; aspect-ratio: 1 / 1; align-self: end;
}
#panel-combat .arena-vs.fs-stage .arena-side.player .arena-portrait { max-height: 170px; }
#panel-combat .arena-vs.fs-stage .arena-side.foe .arena-portrait {
  max-height: min(42vh, 340px);
}
#panel-combat .arena-vs.fs-stage .arena-portrait img { image-rendering: auto; }
#panel-combat .fs-lv {
  font-family: var(--f-label, var(--f-ui)); font-size: var(--t-micro, 11px);
  font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-3);
}

/* COMBAT-UI-10 — the numerals live INSIDE the bar. One element instead of two,
 * on both sides, tabular so the digits do not dance while you watch them. */
#panel-combat .arena-vs.fs-stage .arena-hp-bar {
  position: relative; width: min(100%, 300px); height: 20px; border-radius: 10px;
  display: flex; align-items: center;
}
#panel-combat .arena-vs.fs-stage .arena-hp-bar i { position: absolute; inset: 0 auto 0 0; }
#panel-combat .arena-vs.fs-stage .arena-hp-text {
  position: relative; z-index: 1; width: 100%; text-align: center;
  font-family: var(--f-ui); font-variant-numeric: tabular-nums;
  font-size: var(--t-micro, 11px); font-weight: 800;
  color: var(--ink); text-shadow: 0 1px 2px rgba(0,0,0,.55);
}

/* The swing bar, driven by the engine's own tick stamp and labelled with the
 * weapon — which is what makes weapon speed a visible stat for the first time. */
#panel-combat .fs-swing {
  position: relative; width: min(100%, 300px); height: 13px;
  background: var(--field-sunk, rgba(0,0,0,.3));
  border: 1px solid var(--line-soft); border-radius: 7px;
  overflow: hidden; display: flex; align-items: center;
}
/* The phase fill is the one thing on this bar a player tracks out of the
 * corner of their eye, so it takes the accent itself rather than its 14%
 * background wash — measured in the gate: the wash was invisible on the dark
 * stage, which makes a swing bar decoration.
 *
 * b366 — IT IS ANIMATED, NOT REPAINTED. Tyler: "the bar for attack swing not
 * being smooth is giving me a headache, but I love the addition." The fill's
 * width used to be written from the module's 200ms poll, so a 2.4s swing
 * advanced in twelve visible steps. It now runs ONE linear `scaleX` animation
 * whose duration is the swing itself (set inline from `combatTickMs()`, so
 * there is still exactly one clock), and JS only restarts it — on a weapon
 * swap, on start/stop, or on a real drift. `transform` also keeps the whole
 * thing on the compositor: no layout, no paint, per frame. */
@keyframes hr-fs-swing { from { transform: scaleX(0); } to { transform: scaleX(1); } }
#panel-combat .fs-swing i {
  position: absolute; inset: 0 auto 0 0; width: 100%;
  /* b368: .7 -> .9. Photographed beside a saturated HP bar, gold at 70% over the
   * sunk field resolved to a dull olive barely a value-step off its own trough —
   * a moving element that reads as static is worse than no element, because the
   * player stops looking at it. It is still quieter than HP, which is correct;
   * it is no longer invisible, which is the whole point of a swing clock. */
  background: var(--gold); opacity: .9;
  transform-origin: left center; transform: scaleX(0);
  animation: hr-fs-swing 2.4s linear infinite;
  animation-play-state: paused;
  will-change: transform;
}
#panel-combat .fs-swing[data-swing="run"] i { animation-play-state: running; }
/* REDUCED MOTION KEEPS THE STEPPED RENDER, and that is the honest reading of
 * the setting rather than a concession: a player who has asked for no
 * continuous movement is asking for exactly the thing b366 removed for
 * everyone else. The module writes `--fs-phase` in that mode and nothing
 * animates. */
#panel-combat .fs-swing[data-swing="step"] i {
  animation: none;
  transform: scaleX(var(--fs-phase, 0));
}
@media (prefers-reduced-motion: reduce) {
  #panel-combat .fs-swing i { animation: none; transform: scaleX(var(--fs-phase, 0)); }
}
#panel-combat .fs-swing span {
  position: relative; width: 100%; text-align: center;
  font-size: var(--t-micro, 11px); color: var(--ink-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding: 0 6px;
  /* The fill sweeps UNDER this label every swing, so the label has to be legible
   * on the sunk field and on gold, twice per cycle. Same shadow the HP numerics
   * already use for the same reason. */
  text-shadow: 0 1px 2px rgba(0,0,0,.6);
}

/* COMBAT-UI-11 — three tiles a side, always on. The deep rows (evasion,
 * resistances, per-style accuracy, the away projection) stay one click away in
 * the Stats modal; these three are the ones that change a decision. */
#panel-combat .fs-tiles { display: flex; gap: 4px; width: min(100%, 300px); }
#panel-combat .fs-tile {
  flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; align-items: center; gap: 0;
  padding: 3px 4px;
  background: var(--surf-sunken, rgba(0,0,0,.22));
  border: 1px solid var(--line-soft); border-radius: 5px;
}
#panel-combat .fs-tile em {
  font-style: normal; font-family: var(--f-label, var(--f-ui));
  font-size: var(--t-micro, 11px); letter-spacing: .07em;
  text-transform: uppercase; color: var(--ink-3);
}
#panel-combat .fs-tile b {
  font-family: var(--f-display); font-size: var(--t-small, 13px);
  font-variant-numeric: tabular-nums; color: var(--ink);
}
#panel-combat .fs-weak { font-size: var(--t-micro, 11px); color: var(--ink-2); }

/* COMBAT-UI-14 — the style selector, beside the fighter it re-tunes. The BLOCK
 * is legacy's one-and-only `.combat-style-block`, moved here by the module
 * rather than re-created (see adoptStyleBlock) — so this restyles a node it
 * does not own, and takes only what the stage has room for. Its title and its
 * four-fact `.csb-meta` line are what ran 270px tall on a narrow window; the
 * same facts are on the buttons' own tooltips and in the Stats modal. */
/* AND IT STAYS IN ITS OWN COLUMN. Four nowrap style buttons measure wider than
 * the player's half of the stage at 1440px as well as at 922px, and a grid
 * column does not clip: the row ran across the VS divider and printed over the
 * foe's "Weak to 1H Sword" line in the b366 desktop capture. A screen whose
 * whole grammar is symmetric comparison cannot have one fighter's controls
 * standing in the other fighter's column. */
#panel-combat .fs-style {
  display: flex; align-items: center; gap: 4px; flex-wrap: wrap; justify-content: center;
  /* The same `min(100%, 300px)` track every other row on this side uses — the
   * HP bar, the swing bar and the tile strip all take it, and the style row
   * being the one element that sized itself to its content is exactly why it
   * escaped. Four buttons wrap to two rows inside the column instead. */
  width: min(100%, 300px); max-width: 100%; min-width: 0;
}
#panel-combat .fs-style .combat-style-block { max-width: 100%; min-width: 0; }
#panel-combat .fs-style .csb-btn { flex: 0 1 auto; min-width: 0; }
/* THE ONE-FRAME ESCAPE, AND IT IS A REAL BUG THE SCREENSHOTS CAUGHT.
 * `adoptStyleBlock()` MOVES legacy's single `.combat-style-block` into the
 * stage on each paint, but legacy re-creates it in its own home on some combat
 * events — and in the ~200ms before the next adopt it renders OUTSIDE this
 * sheet's `.fs-style` scope, unconstrained, ~800px wide, straight across the VS
 * divider and over the foe's weakness line. Every measurement said 300px
 * because measuring happens between frames; two captures said otherwise,
 * which is the whole argument for reading the screenshot.
 * So: while the Fight view is up, that block is visible ONLY where this sheet
 * can see it. Specificity is equal with the rule below, so ORDER decides —
 * this one must stay above it. */
#panel-combat[data-combat-view="fight"] .combat-style-block { display: none !important; }
#panel-combat .fs-style .combat-style-block {
  display: flex !important; align-items: center; gap: 4px; flex-wrap: wrap;
  background: none !important; border: 0 !important; box-shadow: none !important;
  padding: 0 !important; margin: 0 !important; grid-area: auto !important;
}
#panel-combat .fs-style .combat-style-block h4,
#panel-combat .fs-style .combat-style-block .csb-meta { display: none !important; }
/* THE MEASUREMENT THAT SETTLED IT. The container is 300px in every state; the
 * BUTTONS were 522→1319 mid-fight and 522→813 (wrapped, correct) in preview.
 * So the block does not escape — its children do, and only once the live labels
 * grow ("Accurate ATTACK · 2.35s"). Something upstream is winning `flex-wrap`
 * in the live state, so wrap is asserted here and each button is given a real
 * basis: two per row, shrinkable, never wider than the column. */
#panel-combat .fs-style .combat-style-block .combat-style-buttons {
  display: flex !important; gap: 4px; flex-wrap: wrap !important; justify-content: center;
  width: 100%; max-width: 100%; grid-auto-flow: row !important;
}
#panel-combat .fs-style .combat-style-block .csb-btn {
  flex: 1 1 44%; max-width: 100%; min-width: 0;
}
/* THE SUB-LABEL THAT MOVED THE PRIMARY BUTTON (b512). The four style buttons
 * are the LAST row of the player column and the action bar carrying Eat is the
 * row directly under them, so anything that makes one of these buttons a line
 * taller pushes Eat down by that line — and the arena card has ~11px of
 * headroom at 900px, i.e. less than one line.
 * The `<small>` is a free-wrapping run of TWO facts ("Atk/Str/Def" + the swing
 * time) at the 14.5px `--t-micro` FLOOR, inside a 130px content box. Whether
 * that run takes two lines or three is decided by a few pixels of text
 * measurement, so the SAME markup rendered the style row 110px tall on one
 * boot and 127px on the next; the 17px it gained on the second is exactly the
 * 6px by which the Eat button left the card. A layout whose primary control
 * moves when a secondary label happens to break differently is not a layout,
 * it is a coincidence that has been holding.
 * THE FIX IS TO MAKE THE LINE COUNT STRUCTURAL, and it is two changes that
 * only work together. (1) The button becomes a COLUMN — name on its own line,
 * sub-label beneath at the full width of the button — because the sheet that
 * governs a live fight (audit-overrides.css, `body.in-combat`) lays the two
 * side by side, which leaves the sub-label a ~66px slot and prints
 * "Atk/Str/Def" straight out through the button's own gold border
 * (photographed at 1440x900). (2) The swing time goes, so what remains in
 * that full-width slot is one short string that cannot wrap.
 * The button is then EXACTLY two text lines tall in every state, at every
 * width, for every weapon: the style row measures 99px at 1920, 1440, 1024,
 * 922 and 700 alike, and no string a data row adds later can move the Eat
 * button again.
 * TWO ROUTES I REJECTED. (1) Shrinking the sub-label: `--t-micro` IS 14.5px,
 * the legibility floor the b227 type guards enforce — there is nothing below
 * it to reach for. (2) `white-space: nowrap` + ellipsis: deterministic, but
 * measured at 700–880px it cut "Atk/Str/Def · 2.4…" off the one button whose
 * entire job is to say which XP route you are choosing — the b348 hole
 * (Xarn's report) reopened from the other side.
 * Specificity note: `body.in-combat #panel-combat.active .combat-style-block
 * .csb-btn small` in audit-overrides.css is (1,4,2), so the plain stage
 * selector alone would lose to it during a live fight. The paired selector is
 * there to outrank exactly that rule, not for effect. */
#panel-combat .fs-style .combat-style-block .csb-btn,
body.in-combat #panel-combat .fs-style .combat-style-block .csb-btn {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0; text-align: center;
}
#panel-combat .fs-style .combat-style-block .csb-btn small,
body.in-combat #panel-combat .fs-style .combat-style-block .csb-btn small {
  display: block; width: 100%; margin-top: 0; line-height: 1.15;
}
/* The speed is the one fact that goes, and only here, because it is printed
 * TWICE within 60px of these buttons already: on `#fs-player-swing` two rows
 * up ("Unarmed · 2.40s" — a label b368 guards against ever being hidden) and
 * on each button's own tooltip. b366 made this cut and b368 reversed it, but
 * only because the mobile sheet had hidden the swing bar's label at the same
 * time and the number was then printed NOWHERE. It is printed here. */
#panel-combat .fs-style .combat-style-block .csb-btn .csb-swing {
  display: none;
}
#panel-combat .fs-style .csb-btn {
  padding: 3px 9px; font-size: var(--t-micro, 11px); font-weight: 700;
  background: var(--field-sunk, rgba(0,0,0,.3));
  border: 1px solid var(--line-soft); border-radius: 4px;
  color: var(--ink-2); cursor: pointer;
}
#panel-combat .fs-style .csb-btn.active { background: var(--gold-bg); border-color: var(--gold-2); color: var(--gold-2); }

/* COMBAT-UI-13 — the action bar. Every control carries live data, and only Eat
 * is bright: that is the existing button ladder and this rework does not touch
 * it. Stop is the honest verb for a server-authoritative pointer — there is no
 * pause state, because a pause state would lie. */
#panel-combat .fs-actionbar {
  display: flex; align-items: center; gap: 10px;
  padding: 6px 0 0; border-top: 1px solid var(--line-soft);
}
#panel-combat .fs-act-primary { display: flex; align-items: center; gap: 8px; }
#panel-combat .fs-act-ref { display: flex; align-items: center; gap: 6px; margin-left: auto; }
#panel-combat .fs-actionbar .arena-act { margin-top: 0; max-width: none; width: auto; }
#panel-combat .fs-history em { font-style: normal; color: var(--gold-2); font-weight: 800; }
/* The two states of the primary slot. In preview the fight has not started, so
 * Eat is meaningless and Fight is the only move; live, the reverse. */
#panel-combat[data-fight-state="preview"] #arena-act-player,
#panel-combat[data-fight-state="preview"] .fs-stop,
#panel-combat[data-fight-state="live"] .fs-fight { display: none !important; }

/* ══════════════════════════════════════════════════════════════════════════
 * 3.9 · THE REACHABILITY NET — AT EVERY HEIGHT, NOT ONLY ON A PHONE (b371)
 *
 * These two rules used to live inside `@media (max-height: 560px)` below, and
 * the reasoning attached to them there was right about everything except its
 * own scope. `.combat-arena` carries `overflow: hidden` so a bleeding backdrop
 * cannot escape the card; a clipping element reports its OWN short height, so
 * the scroll container above it (`.fs-view`) sees nothing to scroll, and the
 * rows the stage pushes past the fold are not clipped-but-scrollable — they
 * are UNREACHABLE, with no gesture that can get to them.
 *
 * That is not a landscape-phone property. It is a property of "the stage's
 * content is taller than the arena", and MEASURED AT 1366x768 — the most
 * common laptop resolution there is — the stage is 699px inside a 610px arena
 * and FIGHT lands at y=776..813 on a 768px screen: the primary call to action
 * of the primary screen, entirely below the fold, with nothing that scrolls.
 * At 1280x800 it is 11px tall and not hit-testable. The b366 comment called
 * this class of defect a P0 and then fenced the cure behind a query no desktop
 * matches.
 *
 * So the net is unconditional. It is still a NET and not the layout — the
 * portrait budget below is tuned so the action bar lands above the fold and
 * the net is never used — but a safety net that only exists on the viewport
 * where the accident did not happen is not a safety net.
 * ════════════════════════════════════════════════════════════════════════*/
#panel-combat[data-combat-view="fight"] .fs-view {
  overflow-y: auto; overscroll-behavior: contain;
}
#panel-combat[data-combat-view="fight"] .combat-arena {
  overflow: visible !important;
}

/* THE FOE PORTRAIT PAYS FOR THE FOLD, BECAUSE IT IS THE ONLY ELASTIC ROW —
 * and since b513 it pays by MEASUREMENT rather than by arithmetic.
 *
 * What used to be here:
 *     width:  min(42vh, 340px, calc(100vh - 540px));
 *     height: min(42vh, 340px, calc(100vh - 540px));
 * with a comment deriving 540 from "376px of fixed rows + ~148px of shell +
 * a 16px cushion". The derivation was correct on the day it was written and
 * wrong on every day since, because the nine rows below the plate are TYPE,
 * and type measures differently in different states: a knocked-out line on the
 * activity bar, a longer provision name in the tiles row, a style label that
 * wraps, a second clause on the session tally. Measured on this tree at
 * 1440x900 the stage was 723px inside a 690px card — the metrics strip and the
 * session tally were already below the card's edge in the DEFAULT state, and
 * the suite's b227 guard went red or green depending on which tests ran before
 * it and left 8 more pixels of type behind them.
 *
 * The cure is not a better constant. It is `minmax(0, 1fr)` on row 1 plus
 * `max-height: 100%` on the stage (both above): the plate is now whatever the
 * card has left after the controls are housed, in whatever state the screen is
 * in, at whatever height the viewport is — with the 42vh / 340px art-direction
 * cap still in force from the rule above so a 4K monitor never renders a 900px
 * slime. Nothing on this stage needs re-tuning when a row's copy changes.
 *
 * The §3.9 net above stays, because it guards the OTHER direction: below about
 * 640px of card the fixed rows alone outmeasure the space and something must be
 * scrollable. It is a net and, on every supported height, it is unused. */

/* The metrics strip. One sentence, always on — and it refuses to quote a rate
 * it has not measured (see the module's Ledger). */
#panel-combat .fs-metrics {
  font-size: var(--t-micro, 11px); color: var(--ink-2);
  /* 1.25, not the inherited 1.42. This is a data ledger, not prose: the same
   * tight register `.fs-lv` and the tile labels use one row up. It is also 5px
   * of the fold, paid out of leading rather than out of anything readable. */
  padding: 4px 0 0; line-height: 1.25; letter-spacing: .01em;
  /* BOTH STRIPS SIT ON A PHOTOGRAPH, and §3.10 below hands their quiet clauses
   * back to `--ink-3`. Sampled off the render, that value measures ~5:1 on the
   * dark barn boards and ~4:1 where the floor catches the light — under the
   * 4.5:1 line on exactly the patches a painted biome will vary most. The fix
   * is NOT to brighten the role (that would flatten the ladder the strip was
   * just given) but to darken the ground under the glyphs, which is the idiom
   * `.arena-hp-text` already uses six rules up for the same reason. */
  text-shadow: 0 1px 2px rgba(0, 0, 0, .6);
}
#panel-combat .fs-metrics b { color: var(--ink); font-variant-numeric: tabular-nums; }
#panel-combat .fs-metrics s { text-decoration: none; color: var(--ink-3); margin: 0 4px; }

/* ══════════════════════════════════════════════════════════════════════════
 * 3.10 · THE INK LADDER ON THIS SCREEN IS RESTORED BY FORCE, AND HERE IS WHY.
 *
 * Measured on the live fight screen at b491: `.fs-metrics`, `.fs-metrics b`,
 * `.fs-metrics s`, `.fs-lv`, `.fs-tile em`, `.fs-tile b`, `.fs-weak`,
 * `.arena-name` and `.fsm-head` ALL compute to the SAME colour — rgb(236,225,204),
 * i.e. `--ink`. Every secondary and hint role authored on this screen is dead.
 * The killer is one blanket in theme-cozy.css:
 *
 *   body[data-theme="hearthlight"] #panel-combat *:not(…) { color: var(--ink) !important }
 *
 * It is (1,2,1) AND important, so `#panel-combat .fs-metrics` (1,1,0, plain)
 * never had a chance. That is the real reason the b487 handoff read
 * "`.fs-sess-best`/`.fs-sess-foes` render identical to body text" — they are
 * not missing a rule, they were being overpainted, and so was everything
 * around them. Two strips cannot be told apart when the screen has one value.
 *
 * The blanket is a shared surface and widening its exclusion list would repaint
 * a dozen screens, so this reclaims exactly the two readout strips and nothing
 * else: `#panel-combat .arena-vs .fs-metrics …` is (1,3,0)+, which outranks it,
 * and the `!important` is required only because the blanket's declaration is.
 * Do not copy this pattern outward — the correct fix for the SCREEN is to take
 * combat off that list the way #panel-profile left it (see the note beside the
 * blanket); that is a separate pass with its own visual gate, filed in
 * DISCOVERIES.
 * ════════════════════════════════════════════════════════════════════════*/
#panel-combat .arena-vs .fs-metrics { color: var(--ink-2) !important; }
#panel-combat .arena-vs .fs-metrics b { color: var(--ink) !important; }
#panel-combat .arena-vs .fs-metrics s { color: var(--ink-3) !important; }

/* ══════════════════════════════════════════════════════════════════════════
 * 3.11 · THE SESSION TALLY STRIP — one line, its own row, its own register
 *
 * Two readouts live at the foot of this stage and they answer different
 * questions: row 9 is THIS FIGHT, row 10 is THIS HUNT. Before b492 they were
 * the same row and the same colour, so they were the same thing twice, printed
 * on top of each other.
 *
 * THE HEIGHT IS BOUNDED BY CONSTRUCTION, NOT BY A MEASUREMENT. A single
 * `nowrap` flex row cannot become two lines at any width there will ever be, so
 * it cannot grow into the action bar the way it did at 922x423 (measured: 34px,
 * two wrapped lines, printing through the metrics strip). Nothing here depends
 * on a breakpoint being tuned correctly — the row is one line because it has
 * one line to give.
 *
 * WIDTH IS SPENT IN PRIORITY ORDER, WHICH IS WHY THIS IS FLEX AND NOT A NOWRAP
 * BLOCK. A block with `text-overflow: ellipsis` clips at the box edge, and the
 * box edge lands wherever it lands — at 960px that cut the string mid-number
 * ("net 51,37…"), i.e. it damaged a FIGURE to save a foe roll. As flex items
 * the clauses can be ranked: the rubric and the rates never shrink, the
 * best-settle shrinks reluctantly, and the foe roll — reference, not rate —
 * absorbs almost all of it and ellipsises inside its own box. The numbers the
 * strip exists for are the last thing to go, never the first.
 * ════════════════════════════════════════════════════════════════════════*/
#panel-combat .fs-metrics.fs-session {
  display: flex; flex-wrap: nowrap; align-items: baseline;
  min-width: 0; overflow: hidden; white-space: nowrap;
  padding-top: 2px;
}
#panel-combat .arena-vs .fs-session > span { flex: 0 0 auto; }
/* 1 against 400 is not a ratio, it is an ORDER. Flex shares a deficit in
 * proportion to shrink-factor x basis, so at 400:1 the foe roll absorbs 99.9%
 * of it and the best-settle figure only starts to give once the roll has been
 * eaten to nothing. Measured with an even-handed 20:1 at 1366x768 the two
 * shared a 49px deficit and `best` lost 2px — enough to hang an ellipsis off
 * an intact number, which is the worst of both outcomes: a figure that looks
 * truncated and isn't. */
#panel-combat .arena-vs .fs-session > .fs-sess-best {
  flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis;
}
#panel-combat .arena-vs .fs-session > .fs-sess-foes {
  flex: 0 400 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis;
}
/* The rubric. Same small-caps label idiom as `.fs-lv` and `.fss-lbl`, which is
 * what says "a new readout starts here" without spending a rule, a box or a
 * second hairline on it — this screen already has one hairline above the action
 * bar and a second one 20px below it would be noise, not structure. */
#panel-combat .arena-vs .fs-session .fs-sess-head {
  font-family: var(--f-label, var(--f-ui)); font-weight: 800;
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--ink-3) !important;
  margin-right: .5em;
}
/* Separators are drawn, not written, so a clause can be dropped at a narrow
 * height without orphaning the middot that used to precede it. Leading (never
 * trailing) position is what makes that safe: the rubric is always first and
 * never hidden, so there is always something to the left of a separator. */
#panel-combat .arena-vs .fs-session > span + span::before {
  content: '\00b7'; color: var(--ink-3); margin: 0 .45em;
}
#panel-combat .arena-vs .fs-session > .fs-sess-head + span::before { content: none; }
#panel-combat .arena-vs .fs-session .fs-sess-stat { color: var(--ink-2) !important; }
#panel-combat .arena-vs .fs-session .fs-sess-stat b { color: var(--ink) !important; }
/* The ONE accent on the strip, and it is spent on the one number that is a
 * moment rather than a rate: the best single settle of this hunt. If `net`
 * were gilt too the gold would mean "a gold figure" instead of "your best",
 * which is the same as marking nothing (b371, the sapphire lesson). */
#panel-combat .arena-vs .fs-session .fs-sess-best { color: var(--ink-2) !important; }
#panel-combat .arena-vs .fs-session .fs-sess-best b { color: var(--gold-2) !important; }
/* The quietest clause, and the first the ellipsis eats. */
#panel-combat .arena-vs .fs-session .fs-sess-foes { color: var(--ink-3) !important; }
#panel-combat .arena-vs .fs-session .fs-sess-idle { color: var(--ink-3) !important; }

/* The log row: the combat log takes the slack, the loot rail rides its right
 * edge. Both live below the stage and neither can push the stage off screen. */
/* The log is watched, not acted on, so it gets a fixed slice rather than every
 * pixel the stage did not want — a 3-line log stretched over half a 1000px
 * screen is the same dead space the placeholder void used to be. */
/* `0 1`, not `0 0` (b513). The basis is unchanged — the log is watched, not
 * acted on, so it gets a fixed slice rather than every pixel the stage did not
 * want. But it must YIELD that slice rather than take it out of the stage: with
 * `flex-shrink: 0` the log held its 180px inside a 690px card and the stage was
 * allotted the 500 that were left, which shrank the foe plate to 96px on a
 * 1440x900 screen. The log is the one row here that can go below the fold
 * without costing the player an action (`.fs-view` scrolls — §3.9). */
#panel-combat .fs-logrow {
  /* The floor is what stops "yields" becoming "disappears". With `min-height: 0`
   * the log measured 0px at 1440x900 and there was no gesture that could bring
   * it back; three lines is the least that is worth watching. */
  flex: 0 1 clamp(110px, 20vh, 210px); min-height: 96px;
  display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px;
  padding: 0 14px 10px; position: relative; z-index: 1;
}
#panel-combat[data-combat-view="fight"] #combat-area {
  min-height: 0; overflow-y: auto; display: flex; flex-direction: column;
}
/* Everything renderCombat draws that the stage has already said better. The
 * combat LOG is the one thing here a player actually watches. */
#panel-combat[data-combat-view="fight"] #combat-area > .arena,
#panel-combat[data-combat-view="fight"] #combat-area > .empty,
#panel-combat[data-combat-view="fight"] #combat-area > .cbt-food,
#panel-combat[data-combat-view="fight"] #combat-area > .bounty-panel { display: none !important; }
#panel-combat[data-combat-view="fight"] #combat-area > .combat-log { flex: 1 1 auto; min-height: 60px; max-height: none; }

/* In PREVIEW there is no log and no loot, because there is no fight yet. An
 * empty 150px box under a still stage is the placeholder void in a new costume,
 * so the row does not exist until the fight does — and the stage, which grows,
 * takes the room. */
#panel-combat[data-fight-state="preview"] .fs-logrow { display: none; }
/* The log-row loot rail is gone — the management rail's DROPS block carries the
 * same ledger, in a container that also exists before the fight. */

/* ══════════════════════════════════════════════════════════════════════════
 * 4 · SHORT AND NARROW SCREENS — COMBAT-UI-07 and COMBAT-UI-16
 *
 * The target is paione's 922x423 landscape phone, which is WIDER than the
 * project's 900px mobile breakpoint and therefore correctly gets the
 * scaled-desktop layout (CLAUDE.md § Mobile rules, b310). So the query that
 * matters here is HEIGHT: no desktop is 560px tall, and every landscape phone
 * is. Nothing actionable is hidden at this size — the stage, both bars, the
 * tiles, the action bar and the metrics strip are all above the fold; only the
 * log, which is watched rather than acted on, goes below it.
 * ════════════════════════════════════════════════════════════════════════*/
@media (max-height: 560px) {
  #panel-combat.active { padding: 6px !important; }
  #panel-combat .wt-view { gap: 5px; }
  /* §2.4 — DESTINATIONS BECOME A CHIP RAIL. At 423px a 150px destination card
   * would eat a third of the screen to say the same six words, so each one
   * drops to a single scannable chip: kicker, art, name, verb. The meta line
   * and the countdown go — they are on the destination's own screen, and the
   * chip's job here is only "this exists and it is one tap away". The rail
   * scrolls sideways, which is how the sixth one stays reachable. */
  /* THE LANDSCAPE RAIL STAYS A SIDEWAYS SCROLLER (b548 re-asserts it). The base
     rule above wraps, which is right on a desktop and wrong on a 393px-tall
     phone: measured here, two rows of chips cost 229px of rail against the
     scroller's 112px, and squeezing all six onto one 123px-wide row broke "The
     Hollow Regent" over four lines for MORE height than it saved. The chip's
     floor is the 40px thumb target on its button plus a 46px glyph, so a second
     row is not affordable on this screen. */
  #panel-combat .wt-dests { flex-wrap: nowrap; min-width: min-content; }
  /* ── THE SCROLL HAS AN AFFORDANCE NOW (b550) ─────────────────────────────
   * b548 kept the sideways scroll on evidence (two rows of chips cost 229px of
   * a 393px screen against the scroller's 112px) and then filed the honest
   * remainder: six destinations in a sideways scroller with NO hint that it
   * scrolls. The eight P1 `clipped-by-parent` findings on this rail were never
   * a clipping bug — Clan Raid and World Event are reachable — they were a
   * DISCOVERABILITY bug, and clipped text with no cue is indistinguishable from
   * broken text.
   *
   * Three cues, all on the container, none costing a pixel of the 393px:
   *   · an EDGE FADE that says "the row continues this way". It is directional
   *     and state-driven (`data-scroll` is set from the scroll position in
   *     combat-screens.js): fade on the right at the start, both in the middle,
   *     only the left at the end. A permanently-faded right edge on a rail that
   *     is already scrolled to its end is a lie, and a lie is worse than no cue.
   *   · SCROLL SNAP, so a swipe lands on a chip edge rather than halfway
   *     through a name — which is what made the peek read as damage.
   *   · the PEEK itself, preserved: `scroll-padding-inline` keeps the snapped
   *     chip clear of the fade, and the end padding stops the last chip from
   *     resting underneath it.
   * `mask-image` is used rather than a background gradient because the cards
   * are children: a container background paints BEHIND them and would be
   * invisible under exactly the chip it needs to fade. Only the ALPHA of a mask
   * stop matters, so the opaque stops are `currentColor` — no palette decision
   * is being made here and none should be spelled as a colour literal. */
  #panel-combat .wt-dest-rail {
    scroll-snap-type: x proximity;
    scroll-padding-inline: 22px 26px;
    padding-right: 26px;
  }
  #panel-combat .wt-dest { scroll-snap-align: start; }
  /* The class-filter rail one row below is the same component with the same
     missing cue, so it gets the same treatment rather than a fix aimed at the
     one selector the gate happened to name. */
  #panel-combat .wt-classes { scroll-snap-type: x proximity; scroll-padding-inline: 18px 22px; }
  #panel-combat .wt-classes .chip { scroll-snap-align: start; }
  #panel-combat [data-scroll="start"] {
    -webkit-mask-image: linear-gradient(90deg, currentColor 0, currentColor calc(100% - 30px), transparent 100%);
    mask-image: linear-gradient(90deg, currentColor 0, currentColor calc(100% - 30px), transparent 100%);
  }
  #panel-combat [data-scroll="mid"] {
    -webkit-mask-image: linear-gradient(90deg, transparent 0, currentColor 26px, currentColor calc(100% - 30px), transparent 100%);
    mask-image: linear-gradient(90deg, transparent 0, currentColor 26px, currentColor calc(100% - 30px), transparent 100%);
  }
  #panel-combat [data-scroll="end"] {
    -webkit-mask-image: linear-gradient(90deg, transparent 0, currentColor 26px, currentColor 100%);
    mask-image: linear-gradient(90deg, transparent 0, currentColor 26px, currentColor 100%);
  }
  #panel-combat .wt-dest {
    flex: 0 0 auto; min-width: 132px; max-width: 180px; padding: 2px 6px; gap: 0;
  }
  #panel-combat .wt-dest .wtd-body { min-height: 0; gap: 5px; }
  #panel-combat .wtd-timer, #panel-combat .wtd-main span { display: none; }
  #panel-combat .wtd-img { width: 20px; height: 20px; }
  /* 16px, not 14: b227's floor is 14.5px and its guard is a blanket scan of the
     sheets, so shrinking a glyph here trips it. The chip's height is bought from
     leading and button chrome below instead — a destination that renders as an
     emoji rather than a portrait is exactly the one that can least afford to
     lose legibility. */
  #panel-combat .wtd-img.is-emoji { font-size: 16px; }
  #panel-combat .wtd-img.is-glyph svg { width: 18px; height: 18px; }
  #panel-combat .wt-dest footer .btn { padding: 0 6px; min-height: 0; }
  /* THE RAIL PAYS FOR THE SECOND ROW OF THE TIER.
   * §2.4 budgets the destination rail at 66px and the card grid at two full
   * rows with a third peeking. Measured at 922x423 the rail came in at 96px and
   * the grid at 175 — 1.7 rows, so the tier read as a single strip of clipped
   * names and the scroll affordance the peek exists to create was gone. These
   * four lines are the 30px, taken from leading and button chrome rather than
   * from anything a player reads: the chip still carries kicker, art, name and
   * verb, which is everything §2.4 asks of it. */
  #panel-combat .wtd-kick { line-height: 1.15; }
  #panel-combat .wtd-main b { font-size: var(--t-micro, 11px); line-height: 1.15; }
  #panel-combat .wt-dest footer .btn { min-height: 18px; line-height: 18px; font-size: var(--t-micro, 11px); }
  #panel-combat .wt-bar { padding: 0; }
  /* One filter row, scrolling sideways rather than wrapping to three. */
  #panel-combat .wt-bar { flex-wrap: nowrap; gap: 8px; overflow: hidden; }
  #panel-combat .wt-chips { flex-wrap: nowrap; }
  #panel-combat .wt-classes { overflow-x: auto; overscroll-behavior-x: contain; }
  /* NOT SHRUNK ON PURPOSE. These chips resolve to 40px from
     `.panel .chip { min-height: 40px !important }` in theme-cozy.css — b285's
     deliberate thumb-target floor for in-panel filters on a landscape phone.
     The second card row is worth 16px; a filter you cannot reliably hit with a
     thumb is not. The row below pays for the grid instead. */
  #panel-combat .wt-chips .chip { padding: 1px 8px; white-space: nowrap; }
  #panel-combat .wt-ribbon { padding: 3px 8px; gap: 8px; }
  #panel-combat .wtr-art { width: 24px; height: 24px; }
  /* 104px cards, 8 across, two full rows visible with the third peeking so the
     scroll is discoverable. */
  /* §2.4 — 104px cards, 8 across, TWO FULL ROWS with the third peeking so the
   * scroll is discoverable. The name clamps to one line here: two lines of
   * display type is 18px, and 18px is the difference between seeing the second
   * row of the tier and seeing half of it. */
  #panel-combat .wt-grid { grid-template-columns: repeat(auto-fill, minmax(102px, 1fr)); gap: 4px; }
  /* THE CARD IS SIZED FROM THE ROW BUDGET, NOT THE OTHER WAY ROUND. After the
     rail trim the grid measures 188px at 922x423. At 100px the card gave 1.8
     rows — a clipped second row and no peek, which is the "cramped list" this
     rework exists to answer wearing a new costume. At 86 (+4 gap = 90) two full
     rows land in 180px and the third peeks by 8px, which is exactly §2.4: two
     rows readable, the scroll discoverable. */
  #panel-combat .wt-card { padding: 3px 4px; gap: 0; min-height: 86px; }
  #panel-combat .wtc-art { flex: 0 0 40px; }
  #panel-combat .wtc-img { max-height: 40px; }
  /* The two text rows inherit the 16px body size this breakpoint sets for
     legibility, which resolved them to 20px LINES — 14px of pure leading across
     the card, which is the difference between two rows of the tier and one and a
     half. The glyphs keep their size; only the leading is spent. */
  #panel-combat .wtc-stats,
  #panel-combat .wtc-kills { line-height: 1.1; }
  #panel-combat .wtc-img.is-glyph svg { width: 36px; height: 36px; }
  #panel-combat .wtc-name { -webkit-line-clamp: 1; }
  #panel-combat .wtc-drops { display: none; }
  /* THE WEAKNESS+HP LINE MUST BE ONE LINE. §2.4's whole point is that the two
     numbers a player compares between foes sit in a FIXED position so the grid
     scans straight down; at 102px wide "2H Hammer 100 HP" wrapped to two lines,
     which both broke that alignment and pushed the card ~16px over its budget,
     costing the second row. Shrinking the gap and letting the label ellipsise
     keeps both numbers visible and the row count honest. */
  #panel-combat .wtc-stats { flex-wrap: nowrap; gap: 3px; min-width: 0; }
  #panel-combat .wtc-stats em {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
  }
  #panel-combat .wtc-stats b { white-space: nowrap; flex: 0 0 auto; }

  #panel-combat .fs-top { padding: 0; gap: 6px; }
  #panel-combat .fs-back { padding: 2px 8px; }
  /* THE RAIL ON A 423px SCREEN. It narrows to 168px and its three blocks
   * scroll as one column — the equipment panel, the food slot and the drops
   * are all reachable without a gesture the layout has not offered, which is
   * the rule the b362 action-bar clip broke. The rail is NOT hidden here:
   * Tyler's complaint is that these controls do not exist on the fight screen,
   * and a phone is where "manage my loadout without leaving" matters most. */
  #panel-combat .fs-body { grid-template-columns: 168px minmax(0, 1fr); gap: 6px; }
  #panel-combat .fs-manage { padding: 5px; gap: 5px; }
  /* STILL THREE ACROSS, and the four-across override that lived here is gone.
   * It was a density trade that only made sense while this was a TABLE: a
   * paper-doll's columns are anatomy (left hand / body / right hand), so
   * reflowing it to four columns on a short screen does not compress the same
   * picture, it draws a different one — and the whole point of the rebuild is
   * that a slot sits in the same place on every screen. What DOES scale is the
   * cell: 44px x 5 rows is 220px against the 168px rail's 236px of usable
   * width, and 44px is the tap floor, where the old four-across grid was
   * shipping 36.75px targets. */
  #panel-combat .fsm-doll {
    grid-template-columns: repeat(3, minmax(0, 44px));
    justify-content: center; gap: 3px;
  }
  #panel-combat .fsm-food-slot { width: 34px; height: 34px; }
  #panel-combat .fsm-drop-art { width: 16px; height: 16px; }
  #panel-combat .fsm-head, #panel-combat .fsm-drop,
  #panel-combat .fsm-food-txt span, #panel-combat .fsm-empty { line-height: 1.15; }
  /* THE TOTALS TAKE THEIR OWN LINE, AND THEY WRAP. Measured by the 820x360
   * landscape guard: "+12 atk · +34 str · +45 def" is ~130px of nowrap type
   * sharing a 150px row with the word LOADOUT, so it spilled 43px past the
   * right edge of the screen. A number a player cannot see is not denser than
   * no number — it is a clipped screen. */
  #panel-combat .fsm-head { flex-wrap: wrap; }
  #panel-combat .fsm-head b { margin-left: 0; width: 100%; white-space: normal; }
  #panel-combat .fs-title b { font-size: var(--t-small, 13px); }
  /* THE 423px BUDGET. ~104px goes to the shell (topbar, activity bar, the
   * screen's own header row), leaving ~319px. The stage takes ~233 of it, the
   * action bar 42 and the metrics strip 16 — so everything ACTIONABLE is above
   * the fold and only the log, which is watched rather than acted on, is not.
   * The stage stops growing here: on a short screen the slack belongs to the
   * controls, not to the painting. */
  /* THE ACTION BAR IS NOT ALLOWED TO BE CLIPPED — the two rules that make that
   * true (`.fs-view` scrolls, `.combat-arena` stops clipping) MOVED OUT of this
   * query in b371 and now apply at every height. They were authored here for
   * the 423px screen, where the stage's content measured 425px inside a 277px
   * arena; the same arithmetic holds at 1366x768, which is where the b370 audit
   * found FIGHT eight pixels below the fold with nothing to scroll. See §3.9.
   * Do not re-scope them to a breakpoint: an unreachable control is the one
   * defect class this screen has now shipped twice. */
  #panel-combat .arena-vs.fs-stage { flex: 0 0 auto; padding: 2px 10px; gap: 1px 10px; }
  /* THE PHONE KEEPS FIXED PLATES. The desktop stage makes row 1 elastic
   * because it has room to spend; at 423px it does not, and an elastic row with
   * nothing left over renders a 0px track with the plate hanging out of the top
   * of the card (photographed while proving b513). So the phone states the two
   * sizes outright and pays for them out of the style row instead — see the
   * one-row-of-four rule at the foot of this query, which returned 81px. */
  #panel-combat .arena-vs.fs-stage .arena-side.player .arena-portrait { width: 52px; height: 52px; }
  /* Trimmed from 21vh/88 to 18vh/72: the four rows below the plate (name, level,
   * HP, swing) plus the tiles, the style row, the action bar and the metrics
   * strip are all fixed-height, so the plate is the ONLY row that can pay for
   * the overflow measured above. The foe is still the largest thing on screen. */
  #panel-combat .arena-vs.fs-stage .arena-side.foe .arena-portrait {
    width: min(18vh, 72px); height: min(18vh, 72px);
  }
  #panel-combat[data-combat-view="fight"] .fs-view { gap: 2px; }
  #panel-combat .arena-vs.fs-stage .arena-name { font-size: var(--t-micro, 11px); line-height: 1.1; }
  #panel-combat .fs-lv { line-height: 1.05; }
  #panel-combat .arena-vs.fs-stage .arena-hp-bar { height: 12px; }
  /* b368 — THE LABEL STAYS. b366 hid it here ("the weapon is already named on
   * the quick-swap strip two rows up") and, eleven rules further down, hid
   * `.csb-swing` on the style buttons for the mirror-image reason ("the swing
   * time is also printed on the player's own swing bar two rows up"). Each rule
   * deleted the duplicate the other one was relying on, so on a landscape phone
   * the swing time was printed NOWHERE — which is precisely what Tyler reported
   * ("you also removed the attack swing timer"). Two rules that each defer to
   * the other are not a density decision, they are a hole.
   *
   * The bar keeps desktop's 13px here rather than 10, because a 10px trough
   * cannot hold 11px type. That is 3px of the 423px budget, and it is paid out
   * of the action bar's own top padding rather than out of the foe plate: the
   * plate is the screen's subject, and 2px of gutter above a row of buttons is
   * the cheapest 423px in this sheet. Measured after: action bar bottom 414 of
   * 423 (b366 shipped 416). */
  #panel-combat .fs-swing { height: 13px; }
  #panel-combat .fs-tile { padding: 0 4px; }
  #panel-combat .fs-style .csb-btn { padding: 0 6px; }
  #panel-combat .fs-style .csb-btn small { font-size: var(--t-micro, 11px); }
  #panel-combat .fs-actionbar .btn { padding: 2px 8px; min-height: 0; }
  #panel-combat .fs-metrics { padding-top: 1px; line-height: 1.15; }
  /* The tally keeps its 2px of separation from the strip above it even here —
   * the two rows answer different questions and the gap is what says so. It is
   * the cheapest 2px on this screen and the row is one line either way. */
  #panel-combat .fs-metrics.fs-session { padding-top: 2px; line-height: 1.15; }
  #panel-combat .arena-vs.fs-stage .arena-name { font-size: var(--t-small, 13px); }
  #panel-combat .arena-vs.fs-stage .arena-hp-bar { height: 15px; }
  #panel-combat .fs-actionbar { padding-top: 2px; gap: 6px; }
  #panel-combat .arena-vs.fs-stage .arena-eat { min-height: 34px; padding: 3px 8px; }
  /* The XP route stays (b348 — Xarn's report); the swing time is the one that
   * goes, because it is also printed on the player's own swing bar two rows up. */
  #panel-combat .fs-style .csb-btn .csb-swing { display: none; }
  /* The six numbers survive; only their boxes do not. */
  #panel-combat .fs-tiles { gap: 0; }
  #panel-combat .fs-tile { background: none; border: 0; flex-direction: row; gap: 3px; padding: 0 4px; }
  #panel-combat .fs-tile + .fs-tile { border-left: 1px solid var(--line-soft); }
  #panel-combat .fs-style .csb-btn { padding: 2px 6px; }
  /* THE EMPTY-SLOT LABEL GOES, AND ONLY HERE. A 168px rail gives each slot 38px;
   * at this breakpoint's 16px body type "Necklace" wrapped to "NECK / LACE" and
   * "Offhand" to "OFFH / AND", which is the truncation defect wearing two lines.
   * The gilt line glyph is the slot's identity — a helmet, a ring, a cape are
   * legible shapes — and it grows to fill the slot it now has to itself. The
   * `title` still names it. */
  #panel-combat .fsm-slot em { display: none; }
  #panel-combat .fsm-slot-gly { width: 68%; height: 68%; opacity: .7; }
  /* THE STYLE ROW MUST STAY IN ITS OWN COLUMN. Four nowrap buttons measured
   * wider than the player's half of the stage at 922px, so the row ran straight
   * across the VS and printed over the foe's "Weak to 1H Sword" line. A grid
   * whose whole job is symmetric comparison cannot have one side's controls
   * standing in the other side's column. */
  /* TWO ROWS OF TWO, AND THE PLATE PAYS FOR THE SECOND ROW. Four-across
   * (`flex: 1 1 0`) fits a 69px cell in PREVIEW and wraps the live labels
   * ("Aggressive STRENGTH · 2.35s") to four lines once the fight starts — the
   * action bar measured 389px in preview and 506px live, i.e. Eat and Stop off
   * a 423px screen the moment they matter. So the buttons keep the 44% basis
   * that behaves the same in both states, and the 8px it costs comes off the
   * foe plate, which is the only elastic row on this stage (b362's own note).
   * Measured live: 372px, above the fold, in both states. */
  #panel-combat .arena-vs.fs-stage .arena-side.foe .arena-portrait {
    width: min(16vh, 64px); height: min(16vh, 64px);
  }
  /* Two rows of buttons cost their leading twice. The glyph sizes are
   * untouched (b227's 14.5px floor is a TYPE rule, not a spacing one); this is
   * pure leading and chrome, and it buys ~20px back for the action bar. */
  #panel-combat .fs-style .combat-style-block .csb-btn { line-height: 1.1; padding: 1px 6px; }
  #panel-combat .fs-style .combat-style-block .csb-btn small { line-height: 1.05; }
  #panel-combat .fs-style { max-width: 100%; min-width: 0; }
  #panel-combat .fs-style .combat-style-block { max-width: 100%; min-width: 0; }
  #panel-combat .fs-style .csb-btn { flex: 0 1 auto; min-width: 0; }
  /* ONE ROW OF FOUR, AND THE COLUMN GETS ITS FULL WIDTH (b513).
   * MEASURED at 922x423 before this rule: the style row was 144px of a 227px
   * stage — 63% of a landscape phone's arena spent on four buttons — because
   * `width: min(100%, 300px)` gave each 44%-basis cell 132px, b512 stacked the
   * name over its route, and the route then wrapped again inside 132px. Three
   * lines per button, two rows of buttons, six lines. The plates were pushed
   * out of the top of the card and the tally strip out of the bottom.
   * The column is ~330px wide here, so four cells of ~80px hold "Aggressive"
   * over "STRENGTH" in exactly the two lines b512 designed, and the row costs
   * ~40px instead of 144. The 300px track the other rows share is a DESKTOP
   * rhythm; on a phone the row's own legibility is worth more than matching a
   * bar above it that is itself only 300px because the desktop stage is wide. */
  /* THE PHONE DOES NOT GET THE ELASTIC ROW, AND IT DOES NOT GET `center`.
   * `minmax(0, cap)` on row 1 is a DESKTOP mechanism: it needs positive free
   * space to grow into, and at 423px there is none, so the track resolved to
   * 0px and the fixed 64px plate hung out of the TOP of the arena card, over
   * the screen's own header (photographed). Here the rows state their own
   * heights and the stage packs from the top, so whatever does not fit spills
   * DOWNWARD — into the log's space, where §3.9's scroll can still reach it —
   * instead of over the chrome above. */
  #panel-combat .arena-vs.fs-stage {
    align-content: start;
    grid-template-rows: auto auto auto auto auto auto auto auto auto auto;
  }
  #panel-combat .fs-style { width: 100%; }
  /* BOTH containers, because legacy renders the four buttons directly in
   * `.combat-style-block` on some paints and inside `.combat-style-buttons` on
   * others (the b366 one-frame adopt). Measured bimodal 48px/92px until the
   * outer box was told too — a row height that depends on which paint you
   * caught is the same defect b512 fixed on the desktop row. */
  #panel-combat .fs-style,
  #panel-combat .fs-style .combat-style-block,
  #panel-combat .fs-style .combat-style-block .combat-style-buttons { flex-wrap: nowrap !important; }
  #panel-combat .fs-style .combat-style-block .csb-btn { flex: 1 1 0; min-width: 0; }
  /* TWO LINES, ALWAYS. The name and the route are each one word-run; letting
   * either wrap is what made this row measure 63px on one boot and 107px on the
   * next ("Atk/Str/D ef", photographed, broken mid-word through the button's
   * own outline). `nowrap` + a 1px tighter tracking makes the row's height a
   * constant, which is the same property b512 asserted for the desktop row. */
  #panel-combat .fs-style .combat-style-block .csb-btn,
  #panel-combat .fs-style .combat-style-block .csb-btn small {
    white-space: nowrap; letter-spacing: 0;
  }
  #panel-combat .fs-style .combat-style-block .csb-btn { padding: 1px 4px; }
  /* The log goes below the fold, and the rail goes with it. */
  #panel-combat .fs-logrow { padding: 0 8px 6px; min-height: 90px; }
}

/* ══════════════════════════════════════════════════════════════════════════
 * THE TALLY SHEDS ITS TAIL WHEN THE STRIP IS TOO NARROW TO HOLD IT HONESTLY,
 * AND THIS IS A WIDTH RULE ON PURPOSE.
 *
 * The obvious home for it was the `max-height: 560px` block above — that is
 * where every other landscape-phone compaction lives, and 922x423 is the phone
 * we care about. It would have been wrong twice. Dropping a clause saves ZERO
 * height (the row is a nowrap flex line either way), so a height query buys
 * nothing it is asked for; and it would have fired on Tyler's 1568x558 desktop
 * window, where the strip is 1190px wide and the whole string fits with 190px
 * to spare. The thing that actually goes wrong is horizontal: the five rate
 * clauses cannot shrink, so below ~660px of strip they stop ellipsising and
 * start being CUT, which damages a figure to save a foe roll.
 *
 * 1100px of viewport is where the strip crosses that line — and it catches
 * both narrow cases the height query could not tell apart: the 922x423 phone
 * (strip 630px, icon rail) and a half-width 1024-wide desktop window (strip
 * ~580px, full sidebar). Above it the flex order above handles the squeeze.
 * The full string is never lost: the away/welcome-back card reads the identical
 * shape from the identical receipts (src/features/session-tally.js).
 * ════════════════════════════════════════════════════════════════════════*/
@media (max-width: 1100px) {
  #panel-combat .arena-vs .fs-session .fs-sess-foes,
  #panel-combat .arena-vs .fs-session .fs-sess-best { display: none; }
}

/* Narrow — the destination rail scrolls sideways instead of squeezing, and the
 * stage stacks its two fighters rather than shrinking them past reading size. */
@media (max-width: 900px) {
  #panel-combat .wt-dest { flex: 0 0 190px; }
  #panel-combat .fs-top { flex-wrap: wrap; }
  #panel-combat .fs-body { grid-template-columns: 150px minmax(0, 1fr); }
  /* Three columns here too — see the note at max-height:560. A paper-doll's
   * columns mean something, so this rail narrows the CELL (42px in a 150px
   * rail) rather than reflowing the figure into a different arrangement. */
  #panel-combat .fsm-doll {
    grid-template-columns: repeat(3, minmax(0, 42px));
    justify-content: center;
  }
  /* Same 150px rail, same clipped totals — see the note at max-height:560. */
  #panel-combat .fsm-head { flex-wrap: wrap; }
  #panel-combat .fsm-head b { margin-left: 0; width: 100%; white-space: normal; }
}
/* Below 640px the two fighters stack, so the rail cannot also hold a column —
 * it becomes the LAST row of a single column and scrolls with the screen. */
@media (max-width: 640px) {
  #panel-combat .arena-vs.fs-stage {
    grid-template-columns: 1fr;
    grid-template-areas: "foe" "vs" "player" "bar" "metrics";
  }
  #panel-combat .arena-vs.fs-stage .arena-vs-divider { font-size: 16px; }
  #panel-combat .fs-body { grid-template-columns: minmax(0, 1fr); grid-auto-rows: min-content; }
  #panel-combat .fs-manage { overflow: visible; }
  #panel-combat[data-fight-state="preview"] .fs-logrow { display: none; }
}
