/* styles/kit.css — THE COMPONENT KIT, in the game.
   ───────────────────────────────────────────────────────────────────────────
   Every player-facing surface is composed from the pieces in this file and
   nothing else. The vocabulary is the kit artifact's (§03 sub-components,
   §04 assemblies, §05 bodies, §07 menus, §08 tooltips, §10 cards), under the
   same names, so a rule read on the kit page is the rule that runs here.

   THE SHELL'S OWN CLASSES ARE KIT ASSEMBLIES UNDER THE REPO'S NAMES. The kit
   page called them `.as-head`, `.as-foot`, `.as-iconbtn`, `.as-btnrow`; the
   game had already named the same assemblies `.modal-head`, `.modal-foot`,
   `.modal-iconbtn`/`.modal-close`, `.modal-btnrow`, and thirty tools read
   those names off the rendered page. So this file styles BOTH spellings with
   one rule each — the repo name is the hook, the kit name is the recipe — and
   nothing in styles/ui.css may draw shell chrome any more.

   SCALE. The kit page was authored at a 15px root; this game's root is 10px
   (html { font-size: 62.5% }). Every kit rem is ×1.5 here, once, at the
   declaration — never at a call site. Radii are px in both, unchanged.

   LAWS KEPT: no vw/vh (the body is zoomed by --ui-zoom); the tap floor is
   --tap-floor and the icon box is --iconbtn-size (base.css); rem for type
   and the boxes that hold it, px for geometry that must not scale with type;
   one home per fact.
   ─────────────────────────────────────────────────────────────────────────── */

:root {
  /* the kit's own two page tokens the game lacked */
  --ink-dim: #b9ac91;
  --rule: #2a2419;
  --r-pill: 999px;
}

/* ═══ TEXT ATOMS ════════════════════════════════════════════════════════════ */
.as-eyebrow { display: block; font-size: 1rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--muted); }
.as-title-l { --track: 0.05em; margin: 0; font: 700 2.55rem/1.1 var(--font-display); letter-spacing: var(--track); text-indent: var(--track); color: var(--parchment); }
.as-title-m { margin: 0; font: 600 2.25rem/1.1 var(--font-display); letter-spacing: 0.03em; color: var(--parchment); }
.as-title-s { margin: 0; font: 600 1.8rem/1.15 var(--font-display); letter-spacing: 0.045em; color: var(--gold); }
.as-subtitle { margin: 0; color: var(--muted); font-size: 1.32rem; line-height: 1.4; }
.as-hairline { border: 0; border-top: 1px solid var(--line); margin: 0; }
.as-status { color: var(--muted); font-size: 1.2rem; text-align: right; }
.as-flavor { color: var(--muted); font-style: italic; font-size: 1.28rem; }
.as-prose { margin: 0; color: var(--parchment); font-size: 1.4rem; line-height: 1.5; }
.as-prose + .as-prose { color: var(--muted); font-size: 1.3rem; }
/* Ornament — the only decorative atom, spent on the title screen and on a
   decision. It marks "you are being asked something". */
.as-ornament { display: flex; align-items: center; gap: 1.05rem; color: var(--gold); }
.as-ornament::before, .as-ornament::after { content: ''; flex: 1; height: 1px; background: linear-gradient(90deg, transparent, var(--line-soft)); }
.as-ornament::after { background: linear-gradient(270deg, transparent, var(--line-soft)); }
.as-ornament span { font-size: 0.9rem; line-height: 1; }

/* ═══ CONTAINMENT — a label can never leave its control ═════════════════════ */
/* THE RECIPE, on the shell's own text-bearing controls, as one rule: the
   defect was found five times on five controls and is fixed once. */
.modal-btnrow > button, .modal-tab, .modal-head-status, .modal-foot-note > span, .modal-head-id h2 {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.as-btn, .as-tab, .modal-tab, .as-railitem, .as-seg button, .as-iconbtn,
.as-pill, .as-tag, .as-chip .cv, .as-chip .ck,
.as-row > .r-label, .as-option .on, .cb-name, .cd-name, .as-status,
.as-statrow .sr-name, .as-detailcard .dc-name, .modal-head-status,
.as-labelstack .ls-label, .as-labelstack .ls-hint,
.as-meter .m-label, .as-meter .m-value, .as-statpair .sp-k, .as-statpair .sp-v {
  min-width: 0; overflow: hidden; text-overflow: ellipsis;
}
.as-btnrow, .as-head, .as-head-id, .as-head-actions, .as-row, .as-option,
.as-statrow, .as-statstrip, .as-kitline, .as-tabs, .as-pane-head, .as-labelstack,
.as-meter, .as-meter > .m-plate, .as-meters, .as-cluster, .as-band-row,
.as-detailbody, .as-detailbody .lines { min-width: 0; }
.as-btnrow > *, .as-row > *, .as-option > *, .as-statrow > *, .as-pane-head > * { min-width: 0; }

/* Text in a flex heading needs its own shrinking box: ellipsis cannot paint
   on an anonymous flex text item. Badges and controls keep their own space. */
.as-label-text { display: block; flex: 0 1 auto; min-width: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.as-option .on, .as-face .on { min-width: 0; max-width: 100%; }
.as-option .on > :not(.as-label-text), .as-face .on > :not(.as-label-text) { flex: 0 0 auto; max-width: 100%; }
.as-option, .as-face, .as-detailcard, .as-optionrow, .as-options, .as-statpair, .as-kitline .ki { min-width: 0; max-width: 100%; }
.as-option .ob, .as-face .ob, .as-option .ob > *, .as-face .ob > *, .as-detailcard > * { min-width: 0; max-width: 100%; }
.as-option .od, .as-option .om, .as-face .od, .as-face .om,
.as-detailcard, .as-prose, .as-subtitle, .as-flavor, .as-labelstack .ls-hint,
.as-statrow .sr-hint, .as-blocker { overflow-wrap: anywhere; }
.as-detailcard .dc-name { white-space: normal; }
.as-row > .r-trail, .as-option > .r-trail { min-width: 0; max-width: 50%; flex-wrap: wrap; }
.as-row > .r-trail > *, .as-option > .r-trail > * { min-width: 0; max-width: 100%; }
button, .as-btn { max-width: 100%; overflow: hidden; text-overflow: ellipsis; }

/* ═══ CONTROL ATOMS ═════════════════════════════════════════════════════════ */
/* Button — three weights, no fourth. The base `button` element IS the kit's
   secondary button, so a screen that writes <button> gets the kit for free
   and nothing can draw a fourth weight by omitting a class. */
button, .as-btn {
  min-height: var(--tap-floor);
  padding: 0.75rem 1.65rem;
  text-align: center; white-space: nowrap;
  background: var(--panel);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-3);
  color: var(--parchment);
  font: 500 1.38rem/1 var(--font-body);
  letter-spacing: 0;
  text-transform: none;
  cursor: pointer;
  transition: background 120ms, border-color 120ms;
}
button:hover, .as-btn:hover, button:focus-visible, .as-btn:focus-visible { background: var(--panel); border-color: var(--gold); }
button:disabled, .as-btn:disabled, .as-btn[aria-disabled="true"] { border-color: var(--line); color: var(--muted); background: var(--panel); cursor: default; }
button.subtle { border-color: var(--line-soft); color: var(--parchment); font-size: 1.38rem; padding: 0.75rem 1.65rem; }
button.primary, .as-btn.primary { background-color: color-mix(in srgb, var(--gold) 20%, var(--panel)); border-color: var(--gold); color: var(--parchment); font-weight: 600; }
button.primary:hover, .as-btn.primary:hover { background-color: color-mix(in srgb, var(--gold) 30%, var(--panel)); }
button.primary:disabled, .as-btn.primary:disabled { background-color: var(--panel); border-color: var(--line); color: var(--muted); }
button.danger, .as-btn.danger, button.subtle.danger { border-color: var(--danger); color: var(--danger); background: var(--panel); }
button.danger:hover, .as-btn.danger:hover { background: rgba(212, 98, 46, 0.08); }
/* A loss is the way forward on a danger door: primary weight, danger colour. */
/* Opaque on purpose: the same tint over the panel, mixed rather than layered,
   so the face has one colour a contrast check (and a screenshot) can read.
   background-color, not background: the hold-progress image lives on the same
   element and a colour rule must never reset it. */
button.primary.danger, .as-btn.primary.danger { background-color: color-mix(in srgb, var(--danger) 14%, var(--panel)); border-color: var(--danger); color: var(--parchment); }
button.primary.danger:hover, .as-btn.primary.danger:hover { background-color: color-mix(in srgb, var(--danger) 24%, var(--panel)); }

/* IconButton — ONE tap-safe box for every header action, close included. A
   LENGTH, not a floor: --iconbtn-size (base.css), the same on every component. */
.as-iconbtn, .modal-iconbtn, .modal-close {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--iconbtn-size); height: var(--iconbtn-size); flex: 0 0 auto;
  min-width: var(--iconbtn-size); min-height: var(--iconbtn-size);
  padding: 0;
  background: var(--panel);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-2);
  color: var(--parchment);
  font: 400 1.6rem/1 var(--font-body);
  letter-spacing: 0;
  cursor: pointer;
}
.as-iconbtn:hover, .modal-iconbtn:hover, .modal-close:hover,
.as-iconbtn:focus-visible, .modal-iconbtn:focus-visible, .modal-close:focus-visible { border-color: var(--gold); color: var(--gold); background: var(--panel); }

/* Close keeps the full accessible hit target above, while its visible square
   is 75% of that target. Every modal inherits the same smaller corner mark. */
.modal-close { position: relative; background: transparent; border-color: transparent; }
.modal-close > .modal-close-face {
  display: inline-flex; align-items: center; justify-content: center;
  width: 75%; height: 75%; pointer-events: none;
  background: var(--panel); border: 1px solid var(--line-soft); border-radius: var(--r-2);
}
.modal-close:hover, .modal-close:focus-visible { background: transparent; border-color: transparent; }
.modal-close:hover > .modal-close-face, .modal-close:focus-visible > .modal-close-face { border-color: var(--gold); }

/* ButtonRow — a grid of EQUAL tracks; the row is min(box, n × step). */
.as-btnrow, .modal-btnrow {
  display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr);
  align-items: stretch; gap: 8px; min-width: 0;
  --n: 1; --step: 15rem;
  width: min(100%, calc(var(--n) * var(--step) + (var(--n) - 1) * 8px));
}
.as-btnrow:has(> :nth-child(2):last-child), .modal-btnrow:has(> :nth-child(2):last-child) { --n: 2; }
.as-btnrow:has(> :nth-child(3):last-child), .modal-btnrow:has(> :nth-child(3):last-child) { --n: 3; }
.as-btnrow:has(> :nth-child(4):last-child), .modal-btnrow:has(> :nth-child(4):last-child) { --n: 4; }
.as-btnrow:has(> :nth-child(5):last-child), .modal-btnrow:has(> :nth-child(5):last-child) { --n: 5; }
.as-btnrow[data-size="short"],  .modal-btnrow[data-size='short']  { --step: 10rem; }
.as-btnrow[data-size="medium"], .modal-btnrow[data-size='medium'] { --step: 15rem; }
.as-btnrow[data-size="long"],   .modal-btnrow[data-size='long']   { --step: 21rem; }
.as-btnrow[data-size="fill"],   .modal-btnrow[data-size='fill']   { width: 100%; }
.as-btnrow > button, .modal-btnrow > button {
  container-type: inline-size;
  padding-inline: clamp(0.5rem, 8%, 1.4rem);
  font-size: clamp(1.1rem, 9cqi, 1.38rem);
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.as-btnrow > button[data-size="square"], .modal-btnrow > button[data-size='square'] {
  aspect-ratio: 1; width: auto; height: auto; justify-self: start;
  min-width: var(--tap-floor); min-height: var(--tap-floor); padding-inline: 0;
}
.as-btnrow.end, .modal-btnrow.end { justify-content: end; margin-left: auto; }
.as-btnrow.center, .modal-btnrow.center { margin-inline: auto; }
/* A button that carries an identity (a flask's art + name) keeps the art
   inside its own box: the line grows to the art, never the reverse. */
.as-btnrow > button:has(.flask-identity), .modal-btnrow > button:has(.flask-identity) { display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem; line-height: 1.3; }

/* Tab — switches SUBJECT. Underline, no box. */
.as-tabs, .modal-tabs { display: flex; gap: 0.2rem; min-width: 0; }
.as-tab, .modal-tab {
  min-height: var(--tap-floor);
  padding: 0.6rem 1rem;
  background: transparent;
  border: 0; border-bottom: 2px solid transparent;
  border-radius: var(--r-2) var(--r-2) 0 0;
  color: var(--ink-dim);
  font: 500 1.38rem/1 var(--font-body);
  white-space: nowrap;
  cursor: pointer;
}
.as-tab[aria-selected="true"], .modal-tab[aria-selected="true"], .modal-tab.on, .as-tab.on { color: var(--gold); border-bottom-color: var(--gold); background: transparent; }
.as-tab:hover, .modal-tab:hover, .as-tab:focus-visible, .modal-tab:focus-visible { color: var(--parchment); background: transparent; border-color: transparent; border-bottom-color: var(--line-soft); box-shadow: none; }
.as-tab[aria-selected="true"]:hover, .modal-tab[aria-selected="true"]:hover, .modal-tab.on:hover { border-bottom-color: var(--gold); }

/* RailItem — switches SECTION. Bordered, full width of its rail, gold when
   current. Always in a vertical column. */
.as-railitem {
  display: block; width: 100%;
  min-height: var(--tap-floor);
  padding: 0.75rem 0.9rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-2);
  color: var(--ink-dim);
  font: 500 1.35rem/1.2 var(--font-body);
  text-align: left; white-space: nowrap;
  cursor: pointer;
}
.as-railitem:hover { border-color: var(--line-soft); color: var(--parchment); }
.as-railitem[aria-current="true"], .as-railitem[aria-selected="true"], .as-railitem.on { border-color: var(--gold); color: var(--gold); background: rgba(var(--accent-rgb), 0.08); }

/* Segmented — 2–3 mutually exclusive values, one visible at a glance. */
.as-seg { display: inline-flex; border: 1px solid var(--line-soft); border-radius: var(--r-2); overflow: hidden; }
.as-seg button { min-height: calc(var(--tap-floor) - 2px); padding: 0.6rem 1.2rem; background: transparent; border: 0; border-radius: 0; color: var(--ink-dim); font: 500 1.23rem/1 var(--font-body); cursor: pointer; }
.as-seg button + button { border-left: 1px solid var(--line-soft); }
.as-seg button:hover, .as-seg button:focus-visible { background: rgba(var(--accent-rgb), 0.06); color: var(--parchment); border-color: var(--line-soft); box-shadow: none; }
.as-seg button[aria-pressed="true"], .as-seg button.on { background: rgba(var(--accent-rgb), 0.14); color: var(--gold); }
.as-seg button:disabled { color: var(--muted); background: transparent; }

/* Toggle — a binary setting. The knob is the state; the pill is the track. */
.as-toggle {
  position: relative; display: inline-block; flex: 0 0 auto;
  width: 5.2rem; height: 2.8rem; min-height: 0; padding: 0;
  background: var(--panel); border: 1px solid var(--line-soft); border-radius: var(--r-pill);
  cursor: pointer;
}
.as-toggle .knob { position: absolute; top: 0.3rem; left: 0.3rem; width: 2rem; height: 2rem; border-radius: 50%; background: var(--muted); transition: left 140ms, background 140ms; }
.as-toggle.on, .as-toggle[aria-checked="true"] { border-color: var(--gold); background: rgba(var(--accent-rgb), 0.14); }
.as-toggle.on .knob, .as-toggle[aria-checked="true"] .knob { left: calc(100% - 2.3rem); background: var(--gold); }
.as-toggle:disabled { opacity: 0.55; cursor: default; }
/* The drawn pill is 2.8rem × 5.2rem; the finger gets the tap floor around it
   on both axes. `overflow: visible` because `button { overflow: hidden }`
   (CONTAINMENT, above) clipped this pseudo to the pill: measured 17–33 px of
   hit area, the pill's own height, before 2026-09-14. */
.as-toggle { overflow: visible; }
.as-toggle::after { content: ''; position: absolute; left: 50%; top: 50%; width: max(100%, var(--tap-floor)); height: max(100%, var(--tap-floor)); transform: translate(-50%, -50%); }

/* StatePill — binary state, scannable down a column. */
.as-pill {
  display: inline-block; padding: 0.15rem 0.6rem;
  border: 1px solid var(--line-soft); border-radius: var(--r-1);
  font: 500 1.02rem/1.5 var(--font-body); letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--muted); white-space: nowrap;
}
.as-pill[data-on="true"] { border-color: var(--gold); color: var(--gold); }
.as-pill.round { border-radius: var(--r-pill); font-size: 0.93rem; letter-spacing: 0.1em; }

/* Tag — micro-label in its parent's tint. */
.as-tag { display: inline-block; padding: 0.08rem 0.5rem; border: 1px solid currentColor; border-radius: var(--r-1); font: 500 0.96rem/1.5 var(--font-body); color: var(--muted); white-space: nowrap; }
.as-tag.more { border-style: dashed; color: var(--muted); }

/* Keycap */
.as-keycap {
  display: inline-grid; place-items: center;
  min-width: 2rem; padding: 0.08rem 0.4rem;
  background: var(--bg); border: 1px solid var(--line-soft); border-radius: var(--r-1);
  font: 500 1.05rem/1.5 var(--font-body); color: var(--ink-dim);
}

/* Glyph — an icon column wide enough that labels align with or without one.
   `min-width` and not a fixed basis: a keyboard or a die is a WIDE emoji (⌨ is
   20 px against a 17.5 px column, measured) and a fixed basis clipped it, which
   is the one thing §02 says a mark may never do. Narrow glyphs still align. */
.as-glyph { flex: 0 0 auto; min-width: 1.75rem; color: var(--muted); font-size: 1.42rem; line-height: 1; text-align: center; }

/* LabelStack — what a control does, and one line on how. */
.as-labelstack { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.as-labelstack .ls-label { font-size: 1.41rem; color: var(--parchment); white-space: nowrap; }
.as-labelstack .ls-hint { color: var(--muted); font-size: 1.2rem; white-space: normal; overflow: visible; text-overflow: clip; }

/* ArtWell — a square that holds a thing's art at a size worth looking at. */
.as-artwell {
  flex: 0 0 auto; display: grid; place-items: center;
  width: 8.4rem; height: 8.4rem;
  background: radial-gradient(circle at 50% 40%, #3a1a18, #17110d);
  border: 1px solid var(--line-soft); border-radius: var(--r-3);
  font-size: 3.6rem; line-height: 1;
}
.as-artwell.sm { width: 5.1rem; height: 5.1rem; font-size: 2.25rem; }
.as-artwell.cool { background: radial-gradient(circle at 50% 40%, #2b3a44, #12100c); }
.as-artwell img { width: 100%; height: 100%; object-fit: contain; }

/* DetailCard — an inset, gold-edged card that shows exactly what is at stake. */
.as-detailcard { display: flex; flex-direction: column; gap: 0.2rem; padding: 1.28rem 1.5rem; background: var(--panel); border: 1px solid var(--gold); border-radius: var(--r-3); text-align: left; }
.as-detailcard .dc-eyebrow { font-size: 1.02rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); }
.as-detailcard .dc-name { margin: 0.1rem 0 0.2rem; font: 600 1.72rem/1.1 var(--font-display); color: var(--gold); }
.as-detailcard .dc-line { font-size: 1.32rem; font-weight: 600; color: var(--parchment); }
.as-detailcard .dc-meta { font-size: 1.05rem; letter-spacing: 0.08em; color: var(--muted); }
.as-detailcard.muted { border-color: var(--line); }
.as-detailcard.muted .dc-name { color: var(--muted); }

/* ═══ THE TWO LIST ATOMS ════════════════════════════════════════════════════ */
/* Row — Glyph + label + StatusText + trail. Menus, popovers, action menus. */
.as-row {
  display: flex; align-items: center; gap: 1.05rem;
  width: 100%; min-height: var(--tap-floor);
  padding: 0.7rem 1.35rem;
  background: transparent; border: 0; border-radius: 0;
  color: var(--parchment);
  font: 500 1.47rem/1.3 var(--font-body);
  text-align: left; white-space: nowrap;
  cursor: pointer;
}
.as-row:hover, .as-row:focus-visible { background: var(--panel); border-color: transparent; box-shadow: none; }
.as-row > .r-label { flex: 0 1 auto; }
.as-row > .as-status, .as-row > .r-trail { margin-left: auto; }
.as-row > .as-status + .r-trail { margin-left: 0; }
.as-row > .as-status { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.as-row .r-trail { flex: 0 0 auto; display: flex; align-items: center; gap: 0.75rem; }
.as-row[data-tone="current"], .as-row[data-tone="current"] .as-glyph, .as-row.on, .as-row.on .as-glyph { color: var(--gold); }
.as-row[data-tone="loss"], .as-row[data-tone="loss"] .as-glyph, .as-row.danger, .as-row.danger .as-glyph { color: var(--danger); }
.as-row[aria-disabled="true"], .as-row:disabled { color: var(--muted); cursor: default; background: transparent; }
.as-row[aria-disabled="true"]:hover, .as-row:disabled:hover { background: transparent; }
/* The settings variant: same grammar, hairline between, no hover fill — a
   settings row is read, not chosen. It may wrap its control under the label
   when the pane is narrow, and it never clips a hint. */
.as-row.setting { padding: var(--modal-inset, 1.4rem); border-bottom: 1px solid var(--rule); cursor: default; white-space: normal; flex-wrap: wrap; row-gap: 0.6rem; }
.as-row.setting:hover { background: transparent; }
.as-row.setting:last-child { border-bottom: 0; }
.as-row.setting > .as-labelstack { flex: 1 1 18rem; }
.as-row.setting > .r-trail { margin-left: auto; }
.as-row.setting > .r-trail input[type="text"] { width: 100%; }

/* OptionCard — choosing between things, not settings. */
.as-options { display: flex; flex-direction: column; gap: 1.05rem; width: 100%; }
.as-option {
  position: relative;
  display: flex; gap: 1.5rem; align-items: center;
  width: 100%;
  padding: 1.28rem 3.3rem 1.28rem 1.5rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-3);
  color: inherit; text-align: left; white-space: normal;
  font: inherit;
  cursor: pointer;
}
.as-option:hover { border-color: var(--line-soft); background: var(--panel); }
.as-option::after { content: '›'; position: absolute; top: 50%; right: 1.28rem; transform: translateY(-50%); color: var(--muted); font-size: 1.72rem; line-height: 1; }
.as-option:hover::after, .as-option[aria-checked="true"]::after, .as-option[aria-pressed="true"]::after { color: var(--gold); }
/* Selection changes colour, never footprint: 1px border in both states, halo by shadow. */
.as-option[aria-checked="true"], .as-option[aria-pressed="true"], .as-option.is-selected {
  border-color: var(--gold);
  box-shadow: 0 0 0 1px rgba(var(--accent-rgb), 0.35), 0 0 18px rgba(var(--accent-rgb), 0.18);
}
.as-option .og { flex: 0 0 3rem; font-size: 2.25rem; line-height: 1; color: var(--gold); text-align: center; }
.as-option .relic-art { width: 3rem; height: 3rem; }
.tray-icon > .relic-art { width: 100%; height: 100%; }
.as-option .ob { min-width: 0; flex: 1 1 auto; display: flex; flex-direction: column; gap: 0.2rem; }
.as-option .on { display: flex; align-items: center; gap: 0.75rem; font: 600 1.65rem/1.2 var(--font-display); letter-spacing: 0.03em; color: var(--gold); white-space: nowrap; }
.as-option .od { margin: 0; color: var(--ink-dim); font-size: 1.35rem; line-height: 1.4; }
.as-option .om { margin: 0; color: var(--muted); font-size: 1.05rem; letter-spacing: 0.08em; }
.as-option > .r-trail { flex: 0 0 auto; margin-left: auto; display: flex; align-items: center; gap: 0.75rem; }
.as-option.noarrow > .r-trail { margin-right: 0; }
.as-option[aria-disabled="true"], .as-option:disabled { opacity: 0.55; cursor: default; }
.as-option[aria-disabled="true"] .on, .as-option:disabled .on { color: var(--muted); }
.as-option.noarrow { padding-right: 1.5rem; }
/* `.class-pick` (§ SCREENS) re-declares the padding at 1.5rem all round, and
   an OptionCard that is also a class-pick kept its chevron: the last word of a
   two-line description ran under the › (measured on the Reaver and Herald rows,
   2026-09-11 — text edge 17 px from the border, chevron 12.8 px in). The arrow
   side keeps the OptionCard's own clearance. */
.as-option.class-pick:not(.noarrow) { padding-right: 3.3rem; }
.as-option.noarrow::after { content: none; }
/* A row of an option and a trailing IconButton (delete) share one height. */
.as-optionrow { display: flex; align-items: stretch; gap: 0.75rem; }
.as-optionrow > .as-option { flex: 1 1 auto; }
.as-optionrow > .as-iconbtn { align-self: center; }

/* ═══ NUMBER ATOMS ══════════════════════════════════════════════════════════ */
.as-statrow { display: flex; align-items: flex-start; gap: 1.5rem; padding: 1.05rem 1.35rem; border: 1px solid var(--rule); border-radius: var(--r-2); background: var(--bg-raise); }
.as-statrow + .as-statrow { margin-top: 0.6rem; }
.as-statrow .sr-id { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.as-statrow .sr-name { font: 600 1.47rem/1.2 var(--font-body); color: var(--parchment); white-space: nowrap; }
.as-statrow .sr-vals { margin-left: auto; display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 0.2rem 1.2rem; text-align: right; }
.as-statrow.drill { cursor: pointer; }
.as-statrow.drill:hover { border-color: var(--line-soft); }
.as-statrow.drill::after { content: '›'; align-self: center; color: var(--muted); font-size: 1.65rem; line-height: 1; }
.as-statpair { display: inline-flex; align-items: baseline; gap: 0.45rem; white-space: nowrap; }
.as-statpair .sp-k { font-size: 0.93rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); }
.as-statpair .sp-v { font: 600 1.5rem/1 var(--font-body); font-variant-numeric: tabular-nums; color: var(--parchment); }
.as-delta { display: inline-flex; align-items: baseline; gap: 0.45rem; white-space: nowrap; font-variant-numeric: tabular-nums; }
.as-delta .d-from { color: var(--muted); font-weight: 600; }
.as-delta .d-arrow { color: var(--muted); }
.as-delta .d-to { font-weight: 700; }
.as-delta[data-dir="up"] .d-to { color: var(--green); }
.as-delta[data-dir="down"] .d-to { color: var(--danger); }
.as-delta[data-dir="flat"] .d-to { color: var(--parchment); }
.as-blocker { padding: 1.05rem 1.35rem; border: 1px solid var(--danger); border-radius: var(--r-2); background: rgba(212, 98, 46, 0.08); color: var(--danger); font: 600 1.42rem/1.3 var(--font-body); }
.as-blocker.pinned { margin-top: auto; }
.as-blocker.in-foot { flex: 0 1 auto; min-width: 0; padding: 0.6rem 1.05rem; font-size: 1.32rem; }
.as-blocker.in-foot::before { content: '⚠ '; }

/* StatChip / KitLine — an option card's FACTS, on one baseline, one gap. */
.as-statstrip, .as-kitline { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.75rem 1.35rem; margin: 0.6rem 0 0; }
.as-kitline { font-size: 1.28rem; color: var(--ink-dim); }
.as-chip { display: inline-flex; align-items: baseline; gap: 0.45rem; white-space: nowrap; cursor: help; }
.as-chip .ck { border-bottom: 1px dotted var(--line-soft); font-size: 0.93rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); }
.as-chip .cv { font: 600 1.42rem/1 var(--font-body); font-variant-numeric: tabular-nums; color: var(--parchment); }
.as-chip:hover .ck { border-bottom-color: var(--gold); color: var(--ink-dim); }
.as-kitline .ki { display: inline-flex; align-items: baseline; gap: 0.5rem; cursor: help; }
.as-kitline .ki .kg { flex: 0 0 auto; color: var(--gold); }
.as-kitline .ki .kn { border-bottom: 1px dotted var(--line-soft); }
.as-kitline .ki:hover .kn { border-bottom-color: var(--gold); }

/* ═══ ASSEMBLIES — the shell ════════════════════════════════════════════════ */
.modal-veil, .as-veil {
  position: fixed; inset: 0; z-index: 500;
  display: flex; align-items: center; justify-content: center;
  background: rgba(0, 0, 0, 0.62);
  animation: door-veil-in var(--door-ms) ease-out;
}
/* A DOOR IS PLACED, NOT POPPED: the veil fades up and the door fades in over
   it over --door-ms (one number; the tooltip's open reads it too, its rung
   step is tooltipHelp.stepMs). The reduced-motion kill in base.css collapses
   both. NO SCALE, measured: a 98% settle shrinks every rect inside the door
   for its first frames, and the instruments read the tap floor the tick a
   door opens — startup-gate read Load Game's Close at 43.9 of 44 px (CI run
   1454) — so the door's box is final from frame one. OPEN ONLY, on purpose:
   a door's removal is synchronous by contract — thirteen tools and the
   confirmation contract read `.modal-veil` absence on the tick of the close —
   so the way out stays a cut rather than a veil that lingers over the board. */
@keyframes door-veil-in { from { opacity: 0; } }
@keyframes door-in { from { opacity: 0; } }
.modal-veil > .modal, .modal-veil > .as-modal, .as-veil > .as-modal { animation: door-in var(--door-ms) ease-out; }
/* Under reduced motion the door is simply there — no name, not a 0.01 ms
   run whose first frame is still at 0 (measured: frame one read opacity 0
   under the kill alone). */
.reduced-motion .modal-veil, .reduced-motion .as-veil, .reduced-motion .modal-veil > .modal, .reduced-motion .modal-veil > .as-modal, .reduced-motion .as-veil > .as-modal,
.reduced-motion #tooltip[data-open="true"], .reduced-motion #tooltip-2[data-open="true"] { animation: none; }
@media (prefers-reduced-motion: reduce) {
  .modal-veil, .as-veil, .modal-veil > .modal, .modal-veil > .as-modal, .as-veil > .as-modal,
  #tooltip[data-open="true"], #tooltip-2[data-open="true"] { animation: none; }
}
/* THE INSET IS THE DOOR'S: one number, all four sides, every bar. */
.as-modal, .modal {
  --modal-inset: 1.4rem;
  container-type: inline-size;
  display: flex; flex-direction: column;
  width: 100%; max-height: 90%;
  padding: 0;
  background: var(--bg-raise);
  border: 1px solid var(--gold);
  border-radius: var(--r-4);
  overflow: hidden;
  box-shadow: 0 24px 72px rgba(0, 0, 0, 0.69), inset 0 1px 0 rgba(var(--accent-rgb), 0.09);
}
/* FOUR WIDTHS, a body picks the smallest that holds it — and the rung it lands
   on is also PUBLISHED as `--modal-rung-width`, because a door that computes
   its own width from the rung (the Settings window, which takes the smaller of
   the rung and the player's own window width) must not re-type the number.
   `data-size` is the rung in force: the body's own, moved by the Modals width
   choice. A rule that belongs to ONE DOOR rather than to a rung keys off
   `data-authored-size`, which is what that door asked for and never moves. */
.modal[data-size='sm'], .as-modal[data-size="sm"] { --modal-rung-width: 43rem; width: var(--modal-rung-width); max-width: 100%; }
.modal[data-size='md'], .as-modal[data-size="md"] { --modal-rung-width: 57rem; width: var(--modal-rung-width); max-width: 100%; }
.modal[data-size='lg'], .as-modal[data-size="lg"] { --modal-rung-width: 76rem; width: var(--modal-rung-width); max-width: 100%; height: 86%; }
.modal[data-size='xl'], .as-modal[data-size="xl"] { --modal-rung-width: 96rem; width: var(--modal-rung-width); max-width: 100%; height: 88%; }
/* A FIXED BOX AT THE RAILED RUNGS (Constantine, 2026-09-03: "modals should be
   fixed size"): the height is the veil's share, so switching from a four-row
   pane to a nine-row pane changes what scrolls, never the door. sm and md
   follow their body — a question and a detail card have floors, not a share. */

/* ModalHead = (Eyebrow + Title·S | Tab×n) + IconButton×n. Identity left,
   actions right, hairline under; the actions row sits at the TOP. */
.modal-head, .as-head {
  display: flex; align-items: center; justify-content: space-between; gap: 1.2rem;
  flex: 0 0 auto;
  padding: var(--modal-inset);
  border-bottom: 1px solid var(--line);
}
.modal-head-id, .as-head-id { display: flex; flex-direction: column; gap: 0.2rem; min-width: 0; }
.modal-head-id h2, .as-head-id h2, .as-head-id .as-title-s { margin: 0; font: 600 1.8rem/1.15 var(--font-display); letter-spacing: 0.045em; color: var(--gold); white-space: nowrap; }
.modal-eyebrow { font-size: 1rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--muted); }
.modal-head-actions, .as-head-actions { display: flex; align-items: flex-start; align-self: flex-start; gap: 0.75rem; flex: 0 0 auto; }
.modal-head-status { color: var(--muted); font-size: 1.2rem; white-space: nowrap; align-self: center; }

/* ModalFoot = FootNote + ButtonRow. */
.modal-foot, .as-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 1.2rem;
  flex: 0 0 auto;
  padding: var(--modal-inset);
  border-top: 1px solid var(--line);
}
.as-foot-note, .modal-foot-note { flex: 1 12 27rem; min-width: 0; container-type: inline-size; color: var(--muted); font-size: 1.23rem; }
.as-foot-note > span, .modal-foot-note > span { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@container (max-width: 9rem) { .modal-foot-note > span { visibility: hidden; } .as-foot-note > span { visibility: hidden; } }
.modal-foot-actions, .as-foot-actions { margin-left: auto; flex: 0 1 auto; justify-content: end; }
.modal-foot-actions button { min-height: var(--tap-floor); }

/* Body — the surface's own, and what flexes. */
.modal-body, .as-body { flex: 1 1 auto; min-height: 0; overflow: auto; }

/* NavRail + Pane — body A. Vertical at every width the modal can be. */
.as-railed { display: grid; grid-template-columns: 17.25rem minmax(0, 1fr); min-height: 22.5rem; height: 100%; }
.as-rail { display: flex; flex-direction: column; gap: 0.6rem; padding: var(--modal-inset); border-right: 1px solid var(--line); overflow: auto; }
.as-pane { display: flex; flex-direction: column; padding: var(--modal-inset); min-width: 0; overflow: auto; }
.as-pane > .as-hairline { margin: 1.4rem 0; }
.as-pane > .as-subtitle { margin-top: 0.5rem; }
.as-pane > .as-title-m { margin-top: 0.3rem; }
.as-pane-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1.5rem; }
@container (max-width: 52rem) {
  .as-railed { grid-template-columns: 11.25rem minmax(0, 1fr); }
  .as-rail { padding: 0.9rem; gap: 0.45rem; }
  .as-railitem { padding: 0.75rem 0.8rem; font-size: 1.2rem; }
}

/* Decision — body C. The one place the game centres text. */
.as-decide { padding: 2.4rem 2.1rem 1.8rem; text-align: center; display: flex; flex-direction: column; gap: 0; }
.as-decide > .as-ornament { margin: 1.28rem 0 1.65rem; }
.as-decide > .as-title-l { text-align: center; }
.as-decide > .prompt { margin: 1.5rem 0 0; color: var(--muted); font-size: 1.32rem; }
.as-decide > .as-options { margin-top: 0.3rem; text-align: left; }
.as-decide > .as-detailcard { text-align: left; }
.as-decide > .as-prose { text-align: center; }

/* Detail — body B. The height is the art's, not the prose's. */
.as-detailbody { display: flex; gap: 1.95rem; padding: 2.1rem; align-items: flex-start; min-height: calc(8.4rem + 4.2rem); }
.as-detailbody .lines { flex: 1 1 27rem; min-width: 0; display: flex; flex-direction: column; gap: 0.75rem; }

/* Workspace — body D. Two panes and a separator. */
.as-splitbody { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); min-height: 22.5rem; }
.as-splitbody > * + * { border-left: 1px solid var(--line); }
@container (max-width: 51rem) { .as-splitbody { grid-template-columns: 1fr; } .as-splitbody > * + * { border-left: 0; border-top: 1px solid var(--line); } }

/* ═══ MENUS — three widths, one Row atom ════════════════════════════════════ */
.as-pop { --place-gap: 6px; width: 31.5rem; max-width: 100%; background: var(--bg-raise); border: 1px solid var(--line-soft); border-radius: var(--r-3); box-shadow: 0 10px 28px rgba(0, 0, 0, 0.6); overflow: hidden auto; }
.as-pop-cap { padding: 0.9rem 1.35rem; border-bottom: 1px solid var(--line); }
.as-group { padding: 0.45rem 0; border-bottom: 1px solid var(--line); }
.as-group:last-child { border-bottom: 0; }
.as-actions { width: 23.25rem; }
.as-actions .am-name { padding: 0.9rem 1.35rem 0.5rem; font: 600 1.65rem/1 var(--font-display); letter-spacing: 0.04em; color: var(--gold); }
.as-actions .am-detail { padding: 0 1.35rem 0.9rem; border-bottom: 1px solid var(--line); color: var(--muted); font-size: 1.28rem; line-height: 1.45; }

/* TitleMenu — the one menu with no panel around it. */
.offline-play-modal .offline-actions { display: flex; flex-wrap: wrap; gap: 0.8rem; margin-block: 1rem; }
.offline-play-modal .offline-direct { display: inline-block; margin-block: 0.4rem 0.8rem; font-weight: 600; }
.offline-play-modal #offline-branch { display: block; width: 100%; min-height: 44px; padding: 0.6rem; margin-block: 0.5rem 1rem; font: inherit; color: var(--parchment); background: var(--panel); border: 1px solid var(--line); border-radius: 6px; color-scheme: dark; }
.offline-play-modal #offline-branch:disabled { opacity: 0.65; }
.offline-play-modal #offline-progress { display: block; width: 100%; height: 1rem; accent-color: var(--gold, #c9a84c); }
.offline-play-modal li { margin-block: 0.6rem; }
.offline-play-modal .modal-body { overflow-wrap: anywhere; }
.offline-play-modal .modal-body { padding: 1.4rem; }
.offline-play-modal ul { padding-inline-start: 1.5rem; list-style: disc; }
.offline-play-modal h3 { margin-block: 1.5rem 0.7rem; }
.as-titlemenu { width: 100%; max-width: 40rem; margin-inline: auto; text-align: center; container-type: inline-size; }
/* The name fills its line: an indent would overflow and disable centering.
   Shift half the trailing tracking instead, keeping the visible text centered. */
/* THE translateX COMPENSATION IS CORRECT. IT WAS REMOVED ONCE, IN ERROR, AND
   THIS PARAGRAPH IS THE RECORD SO IT IS NOT REMOVED AGAIN.

   `letter-spacing` adds its track AFTER the final glyph too, so a centred line
   has its INK sitting half a track LEFT of the box centre. The translateX puts
   it back. #910 deleted it, on a measurement that took a Range over the text
   as if that were the ink — but a Range INCLUDES the trailing advance, so it
   reported the uncompensated layout as perfectly centred. The number was real;
   it was measuring the wrong box, and it read 0px for a wordmark that was
   visibly off.

   Re-measured from PAINTED PIXELS — a screenshot scanned for the leftmost and
   rightmost lit column of the wordmark, which has no box model to be wrong
   about — as the ink's offset from the viewport centre, at three widths:

                          390      1200     2550
     with the transform   -1.5px   -1.5px   -2.5px
     without it           -3.0px   -3.5px   -5.5px

   The compensation roughly halves the error at every width. It is not a second
   correction for a solved problem; the browser does NOT discount the trailing
   advance when centring, and #910's claim that it does was wrong.

   startup-gate now subtracts the trailing advance before judging centring
   (A11.RESPONSIVE-SHAPE, and A11.CENTERING-DETECTOR is its self-test), so the
   gate and this rule finally agree. Before that fix the gate measured the raw
   advance box, which made the MISSING compensation look green — two errors
   cancelling — and that false green is what invited #910.

   `--ink-bias` IS THE SECOND, SEPARATE HALF OF THE SAME CORRECTION. Half a
   track centres the glyphs' ADVANCES; ink is narrower than advances by each
   end glyph's side bearing, and those do not cancel. In Cinzel 700 the
   leading A's left bearing is 0.008em and the trailing E's right bearing is
   0.045em (both read from a 1000px raster of each glyph), so the ink sits
   (0.045 - 0.008) / 2 = 0.0185em LEFT of its advances' centre. That is about
   1px at 1x, which is why it hid inside the tolerance — but `body { zoom }`
   is 1.7 at 2550x1305, and at Text XL (57.6px) it grew to 1.8px of the
   -2.67px startup-gate read there against its 2.5px ink budget. It is an em
   quantity, so it scales with the type and the zoom and needs no per-shape
   tuning. Inter (the readable-ui face) reads 0.0136em, within 0.3px at this
   size, so one token serves both. It is specific to the A…E lockup: a
   different name needs its own end-glyph bearings measured. */
.as-titlemenu .tm-name { --track: 0.08em; --ink-bias: 0.0185em; margin: 0; transform: translateX(calc(var(--track) / 2 + var(--ink-bias))); font: 700 5.4rem/1.25 var(--font-display); font-size: min(12cqi, 5.4rem); letter-spacing: var(--track); text-indent: 0; color: var(--parchment); white-space: nowrap; text-shadow: 0 2px 24px rgba(var(--accent-rgb), 0.28); }
.as-titlemenu .tm-sub { --track: 0.28em; margin: 0.75rem 0 0; font-size: 1.05rem; letter-spacing: var(--track); text-indent: var(--track); text-transform: uppercase; color: var(--muted); white-space: nowrap; }
.as-titlemenu ul { list-style: none; margin: 1.8rem 0; padding: 0; }
.as-titlemenu li + li { border-top: 1px solid var(--rule); }
.as-titlemenu .tm-entry {
  display: block; width: 100%; min-height: var(--tap-floor);
  padding: 1.05rem 0;
  background: transparent; border: 0; border-radius: 0;
  color: var(--parchment);
  font: 600 1.88rem/1 var(--font-display);
  --track: 0.1em;
  letter-spacing: var(--track); text-indent: var(--track); text-transform: uppercase;
  cursor: pointer;
}
.as-titlemenu .tm-entry:hover, .as-titlemenu .tm-entry:focus-visible { color: var(--gold); background: transparent; box-shadow: none; }
.as-titlemenu .tm-entry:disabled { color: var(--muted); background: transparent; }
.as-titlemenu .tm-reason { margin: -0.4rem 0 0.6rem; padding: 0; border: 0; color: var(--muted); text-align: center; }
.as-titlemenu > .as-ornament { margin-top: 1.35rem; margin-inline: auto; }
.as-titlemenu .tm-foot { --track: 0.2em; margin-top: 1.5rem; color: var(--muted); font-size: 1.05rem; letter-spacing: var(--track); text-indent: var(--track); text-transform: uppercase; }

/* ═══ TOOLTIP — Title + meta + effect + hairline + glossary + flavour ══════ */
.as-tip { width: 36rem; max-width: 100%; padding: 1.1rem 1.35rem; background: var(--panel); border: 1px solid var(--gold); border-radius: var(--r-2); box-shadow: 0 8px 22px rgba(0, 0, 0, 0.55); font-size: 1.35rem; line-height: 1.45; color: var(--parchment); }
.as-tip .ti-title { margin-bottom: 0.45rem; font: 600 1.58rem/1.2 var(--font-display); letter-spacing: 0.03em; color: var(--gold); }
.as-tip .ti-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 0.6rem 0.9rem; margin-bottom: 0.75rem; }
.as-tip .ti-meta .as-tag { letter-spacing: 0.14em; text-transform: uppercase; font-size: 0.9rem; }
.as-tip .ti-cost { font-size: 1.2rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.as-tip .ti-cost b { color: var(--parchment); font-weight: 600; }
.as-tip .ti-detail { margin-top: 0.9rem; padding-top: 0.9rem; border-top: 1px solid var(--line); color: var(--muted); font-size: 1.28rem; }
.as-tip .ti-detail b { color: var(--parchment); }
.as-tip .ti-list { margin: 0.75rem 0 0; padding-left: 1.65rem; color: var(--muted); font-size: 1.28rem; }
.as-tip .as-flavor { display: block; margin-top: 0.75rem; }

/* ═══ PAGE-LEVEL ═══════════════════════════════════════════════════════════ */
/* A phone: the inset stays one number, the note goes, every row fills. */
@media (max-width: 640px) {
  .as-modal, .modal { --modal-inset: 1rem; }
  .modal-foot, .as-foot { align-items: stretch; }
  .modal-foot-note, .as-foot-note { display: none; }
  .modal-foot-actions, .as-foot-actions { width: 100%; }
  .modal-btnrow[data-size], .as-btnrow[data-size] { width: 100%; }
  .modal-btnrow > button, .as-btnrow > button { padding-inline: clamp(0.4rem, 6%, 8px); }
  .as-railed { grid-template-columns: 10rem minmax(0, 1fr); }
}
@media (prefers-reduced-motion: reduce) { .as-toggle .knob { transition: none; } }

/* ═══ INPUTS INSIDE A ROW — the kit's field, one look ══════════════════════ */
.as-row.setting input[type="text"], .as-row.setting input[type="number"] {
  min-height: var(--tap-floor); padding: 0.6rem 1rem;
  background: var(--bg); border: 1px solid var(--line-soft); border-radius: var(--r-2);
  color: var(--parchment); font: 500 1.38rem/1 var(--font-body); font-variant-numeric: tabular-nums;
}
.as-row.setting input[type="number"] { width: 9rem; text-align: right; }
.as-row.setting input[type="text"] { width: min(100%, 30rem); }
.as-row.setting input:focus-visible { border-color: var(--gold); }
.as-row.setting input[type="range"] { width: 18rem; accent-color: var(--gold); cursor: pointer; }
input[type="range"] { min-height: var(--tap-floor); }
.as-row.setting > .as-status.range-val { flex: 0 0 auto; margin-left: auto; min-width: 3.6rem; color: var(--gold); font-variant-numeric: tabular-nums; }
.as-row.setting > .as-status.range-val + .r-trail { margin-left: 0; }
.as-row.setting .set-applied-slot:empty { display: none; }
.as-row.setting .set-applied-slot { color: var(--ink-dim); font-size: 1.2rem; margin-top: 0.2rem; }
/* A row that does nothing while a switch is off (settings.js refreshGates):
   the label dims, the control is disabled, and the gate line says which switch
   opens it. The line keeps full ink — it is the answer to "why is this grey". */
.as-row.setting.set-row-gated .ls-label { color: var(--ink-dim); }
.as-row.setting.set-row-gated .r-trail { opacity: 0.55; }
.as-row.setting .set-gate-note { color: var(--ink); font-style: italic; }
.as-row.setting .ls-hint code { color: var(--ink-dim); }
.set-section-head { display: flex; flex-direction: column; gap: 0.3rem; }
.set-section-head + .as-hairline { margin: 1.4rem 0; }
.as-pane > .as-pane-head { margin-bottom: 1.2rem; }

/* ═══ HOUSEKEEPING ═════════════════════════════════════════════════════════ */
/* `hidden` wins over any display a kit class sets — a detail card the caller
   hid must not draw its border. */
.as-detailcard[hidden], .as-option[hidden], .as-row[hidden], .modal-foot-note[hidden], .as-pane [hidden], .modal-head [hidden], .as-iconbtn[hidden], .modal-close[hidden], .as-btnrow[hidden], .as-band[hidden], .as-pop[hidden], .as-slot[hidden] { display: none !important; }
/* A hold-armed IconButton keeps its glyph; the hold hint is the tooltip's job. */
.as-iconbtn .hold-hint, .modal-iconbtn .hold-hint, .modal-close .hold-hint { display: none; }
/* THE SCROLL OWNERS INSIDE A DOOR wear the house scrollbar, not Chromium's. */
.modal-body, .as-body, .as-pane, .as-rail, .as-pop, .modal .set-panel {
  scrollbar-width: thin; scrollbar-color: var(--line-soft) transparent;
}
.modal-body::-webkit-scrollbar, .as-pane::-webkit-scrollbar, .as-rail::-webkit-scrollbar, .as-pop::-webkit-scrollbar { width: 8px; height: 8px; }
.modal-body::-webkit-scrollbar-thumb, .as-pane::-webkit-scrollbar-thumb, .as-rail::-webkit-scrollbar-thumb, .as-pop::-webkit-scrollbar-thumb { background: var(--line-soft); border-radius: 4px; }
.modal-body::-webkit-scrollbar-track, .as-pane::-webkit-scrollbar-track, .as-rail::-webkit-scrollbar-track, .as-pop::-webkit-scrollbar-track { background: transparent; }

/* ═══ THE LIVE TOOLTIP — one panel per level, four rungs by height ═════════ */
/* `--place-gap` stays the ONE home fx.js placeGap() reads (tools/placement.mjs
   P2); px because a hairline gap is chrome, not read text. */
#tooltip, #tooltip-2 {
  --place-gap: 14px;
  position: fixed; z-index: 1000; left: 0; top: 0;
  width: var(--tip-w, 30rem); max-width: calc(100% - 16px);
  max-height: var(--tip-h, 19.5rem); overflow: hidden;
  padding: 1.1rem 1.35rem;
  background: var(--panel); border: 1px solid var(--gold); border-radius: var(--r-2);
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.55);
  font-size: 1.35rem; line-height: 1.45; color: var(--parchment);
  display: none; opacity: 0; visibility: hidden;
  transition: opacity 140ms ease-out;
  pointer-events: none;
}
#tooltip-2 { z-index: 1001; }
/* WT0.ARROW — on the edge facing the trigger, pointing at its centre after
   any flip or shift (tooltip.js pointArrow, TooltipPlacementModel). It lies
   outside the panel, so it is `fixed`: the panel's overflow clip does not
   reach a fixed descendant, and both share the zoomed body's local px. */
#tooltip::after, #tooltip-2::after {
  content: none; position: fixed; pointer-events: none;
  left: var(--tip-arrow-left, 0px); top: var(--tip-arrow-top, 0px);
  width: var(--tip-arrow-w, 0px); height: var(--tip-arrow-h, 0px);
  background: var(--gold);
}
#tooltip[data-arrow="bottom"]::after, #tooltip-2[data-arrow="bottom"]::after { content: ''; clip-path: polygon(0 0, 100% 0, 50% 100%); }
#tooltip[data-arrow="top"]::after, #tooltip-2[data-arrow="top"]::after { content: ''; clip-path: polygon(50% 0, 100% 100%, 0 100%); }
#tooltip[data-arrow="left"]::after, #tooltip-2[data-arrow="left"]::after { content: ''; clip-path: polygon(100% 0, 100% 100%, 0 50%); }
#tooltip[data-arrow="right"]::after, #tooltip-2[data-arrow="right"]::after { content: ''; clip-path: polygon(0 0, 100% 50%, 0 100%); }
/* THE RUNGS DIFFER BY HEIGHT: small a value and a line; medium prose and a
   short list; large an effect and a glossary; expanded is large extended
   down. A tooltip never scrolls and never clips: it steps up, and past
   `expanded` the answer is a modal. */
#tooltip[data-size="small"],    #tooltip-2[data-size="small"]    { --tip-w: 25.5rem; --tip-h: 12rem; }
#tooltip[data-size="medium"],   #tooltip-2[data-size="medium"]   { --tip-w: 31.5rem; --tip-h: 19.5rem; }
#tooltip[data-size="large"],    #tooltip-2[data-size="large"]    { --tip-w: 37.5rem; --tip-h: 27rem; }
#tooltip[data-size="expanded"], #tooltip-2[data-size="expanded"] { --tip-w: 37.5rem; --tip-h: 42rem; }
#tooltip[data-overflow="true"], #tooltip-2[data-overflow="true"] { overflow: auto; scrollbar-width: thin; scrollbar-color: var(--line-soft) transparent; }
/* THE TOOLTIP OPENS THE WAY A DOOR DOES — the declared opacity transition
   never ran on open (display none → block is a fresh render), so the open is a
   keyframe; the rung's step-up is a clip reveal driven from tooltip.js. */
@keyframes tip-in { from { opacity: 0; } }
#tooltip[data-open="true"], #tooltip-2[data-open="true"] { opacity: 1; visibility: visible; pointer-events: auto; animation: tip-in var(--door-ms) ease-out; }
#tooltip[data-stuck="true"] { pointer-events: none; }
/* ON A TOUCH SCREEN A TOOLTIP THAT NOTHING INSIDE CAN ANSWER IS SEE-THROUGH.
   There is no hover to keep, so an open tooltip only ever needs the finger
   when it scrolls or carries a term that opens a nested reading. Every other
   one used to swallow the tap that landed on it — measured at 390x844 on
   character creation: the seed field's "Edit seed." sat over the STARTING
   EQUIP row and the row could not be opened at all. The tap now reaches the
   control beneath, and the document's own pointerdown closes the tooltip. */
@media (pointer: coarse) {
  #tooltip[data-open="true"]:not([data-overflow="true"]):not(:has(.tooltip-keyword[data-tip], .inspection-tag[data-tip], [data-tip][role="button"], a, button)),
  #tooltip-2[data-open="true"]:not([data-overflow="true"]):not(:has(.tooltip-keyword[data-tip], .inspection-tag[data-tip], [data-tip][role="button"], a, button)) { pointer-events: none; }
}
#tooltip.is-fading { opacity: 0; transition: opacity var(--tooltip-fade-duration) ease-out; }
.reduced-motion #tooltip, .reduced-motion #tooltip-2 { transition: none !important; }
@media (prefers-reduced-motion: reduce) { #tooltip, #tooltip-2 { transition: none !important; } }
/* The content grammar: title, meta (type + cost), effect, tags, then under
   the one divider the glossary and the flavour. `.tt-*` are the same parts
   under the names the surfaces already write. */
#tooltip .ti-title, #tooltip-2 .ti-title, #tooltip .tt-title, #tooltip-2 .tt-title { margin-bottom: 0.45rem; font: 600 1.58rem/1.2 var(--font-display); letter-spacing: 0.03em; color: var(--gold); }
#tooltip .ti-meta, #tooltip-2 .ti-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 0.6rem 0.9rem; margin-bottom: 0.75rem; }
#tooltip .ti-meta .as-tag { letter-spacing: 0.14em; text-transform: uppercase; font-size: 0.9rem; }
#tooltip .ti-cost, #tooltip-2 .ti-cost { font-size: 1.2rem; color: var(--muted); font-variant-numeric: tabular-nums; }
#tooltip .ti-cost b { color: var(--parchment); font-weight: 600; }
#tooltip .ti-tags { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.8rem; }
#tooltip .ti-detail, #tooltip-2 .ti-detail, #tooltip .tt-kw, #tooltip-2 .tt-kw { margin-top: 0.9rem; padding-top: 0.9rem; border-top: 1px solid var(--line); color: var(--muted); font-size: 1.28rem; }
#tooltip .ti-detail b, #tooltip .tt-kw b, #tooltip-2 .tt-kw b { color: var(--parchment); }
#tooltip .ti-list { margin: 0.75rem 0 0; padding-left: 1.65rem; color: var(--muted); font-size: 1.28rem; }
#tooltip .ti-flavor, #tooltip .as-flavor { display: block; margin-top: 0.75rem; color: var(--muted); font-style: italic; font-size: 1.28rem; }
#tooltip .mk-val, #tooltip .val { color: var(--parchment); font-weight: 600; }
/* A term inside a tooltip that has its own entry says so with the dotted mark. */
#tooltip [data-tip], #tooltip [title] { border-bottom: 1px dotted var(--line-soft); cursor: help; }
#tooltip [data-tip]:hover, #tooltip [title]:hover { border-bottom-color: var(--gold); }
/* A target being read says so, quietly. */
[data-tip-open="true"] { outline: 1px solid rgba(var(--accent-rgb), 0.45); outline-offset: 2px; }
/* Variants a surface may still pin (widths in the surface's own terms). */
#tooltip[data-tooltip-variant='combatant-folded'] { --tip-w: 28.5rem; padding: 0.9rem 1.2rem; }
#tooltip[data-tooltip-variant='combatant-expanded'] { --tip-w: 36rem; }
#tooltip[data-tooltip-variant='combatant-context'] { --place-gap: 6px; --tip-w: 31.5rem; padding: 0.5rem 0.9rem; font-size: 1.5rem; line-height: 1.15; overflow-wrap: anywhere; }
#tooltip[data-tooltip-variant='combatant-context'] .tt-title { margin-bottom: 0.15rem; font-size: 1.5rem; line-height: 1.1; }
#tooltip[data-tooltip-variant='combatant-context'] .tt-combatant-line b { color: var(--gold); }
#tooltip .tt-combatant-line { color: var(--muted); font-size: 0.9em; }
#tooltip .tt-combatant-focus { display: grid; gap: 0.25rem; margin-top: 0.65rem; padding-top: 0.65rem; border-top: 1px solid var(--line-soft); }
#tooltip .tt-combatant-focus b { color: var(--gold); }
#tooltip .tt-combatant-focus span, #tooltip .tt-combatant-hint { color: var(--muted); }
#tooltip .tt-combatant-hint { margin-top: 0.65rem; font-size: 0.82em; }
#tooltip[data-tooltip-variant='equipment-comparison'] { --tip-w: 42rem; overflow: auto; }
#tooltip .equip-candidate-comparison { margin-left: 0; }

/* ═══ CONTROLS — bindings are Keycaps, rebinds are short buttons ═══════════ */
.rebind-ctl { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; justify-content: flex-end; }
.rebind-ctl .as-keycap { min-width: 4.4rem; min-height: var(--tap-floor); padding-inline: 0.8rem; font-size: 1.2rem; color: var(--gold); }
.rebind-ctl .rebind-btn { padding-inline: 1.2rem; }
.rebind-btn.listening { border-color: var(--ember); color: var(--ember); animation: pulse-gold 0.9s ease-in-out infinite alternate; }
.pad-live { color: var(--green); }
.pad-off { color: var(--muted); }
.controls-pane { padding: 0; }
.overlay-body > .controls-pane { padding: var(--modal-inset); }

/* ═══ THE QUICK MENU HANGS OFF ITS BUTTON ══════════════════════════════════ */
/* The popover is PLACED (quicknav.js → fx.placeAnchored: under the ☰,
   right-aligned) — so its veil is a plain scrim, not a centring flex box,
   and the panel is fixed-position so left/top mean what the placer wrote. */
.modal-veil.qn-veil { display: block; padding: 0; background: rgba(0, 0, 0, 0.35); }
.qn-panel { position: fixed; left: 0; top: 0; }

/* ═══ THE CARD — one fixed box, fixed landmarks, one shared row budget ═════ */
/* `.card` is the hook every screen and tool reads; `.as-card` is the recipe.
   THE BOX IS FIXED — `height`, not a floor — and its total is spelled out from
   its landmarks so a landmark that changes moves the box with it. Below the
   band there is ONE budget of rows that tags and text share: one tag row
   leaves three lines of text, two leave two. Both full is the only case that
   truncates; selecting the face reveals its Information button for full text. */
:root {
  --card-w: 14rem;
  --card-name-h: 2.6rem;      /* FIXED. two lines' worth of room, one line of type */
  --card-art-h: 6rem;         /* FIXED. */
  --card-band-h: 1.7rem;      /* FIXED. */
  --card-row: 1.5rem;         /* one row of the shared budget */
  --card-body-rows: 4;
  --card-pad: 0.8rem;
  --card-gap: 0.4rem;
}
.card, .as-card {
  position: relative;
  box-sizing: border-box;
  width: var(--card-w);
  height: calc(var(--card-name-h) + var(--card-art-h) + var(--card-band-h) + var(--card-body-rows) * var(--card-row) + 3 * var(--card-gap) + 2 * var(--card-pad) + 1.2rem);
  padding: calc(var(--card-pad) + 1.2rem) var(--card-pad) var(--card-pad);
  display: flex; flex-direction: column; align-items: stretch; gap: var(--card-gap);
  background: var(--bg-raise);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-3);
  color: var(--parchment);
  cursor: pointer; flex-shrink: 0;
  transition: transform 140ms, box-shadow 140ms, border-color 140ms;
}
.card.rarity-uncommon { border-color: var(--frost); }
.card.rarity-rare { border-color: var(--gold); }
.card.rarity-special { border-color: #6a3a7a; }
/* SELECTION CHANGES COLOUR, NEVER FOOTPRINT — the same signal as an OptionCard. */
.card.selected, .card[data-selected="true"], .as-card[data-selected="true"] { border-color: var(--gold); filter: var(--selection-glow, drop-shadow(0 0 calc(.35 * max(16px / var(--ui-zoom, 1), 1rem)) var(--gold))); }
/* THE CHOSEN CARD WEARS ONE GREEN RING, AND NOTHING STANDS UNDER IT.
   Victory wrote this signal first (`.reward-selected`): the card you picked is
   ringed in green, on its own edge. Every other card picker now borrows the
   SAME ring — creation's kit, the stables' mounts — because a player who
   learns the signal at the spoils door should not have to learn a second one
   in the Armoury, and because a word printed under the art ("Selected") reads
   as a button you can press when it is really a state you are in.
   GOLD STAYS HOVER, FOCUS AND INSPECTION; GREEN IS THE CHOICE THAT STANDS.
   `.hand .card.selected` in combat is deliberately NOT this: an armed card is
   a momentary aim you can still cancel, not a commitment, and it keeps the
   gold lift combat.css gives it.
   Two arms because the two card families are drawn differently — a `.card`
   carries its own border, while an `.equipment-poker-card` is only the aspect
   box and the frame it draws is `.epc-frame`, whose inset bevel has to
   survive the ring (restated here, not dropped). */
:is(.card, .as-card).is-chosen,
.reward-door .reward-row .card.reward-selected {
  border-color: var(--green);
  box-shadow: 0 0 0 2px var(--green), 0 10px 30px color-mix(in srgb, var(--green) 28%, transparent);
}
.equipment-poker-card.is-chosen > .epc-frame {
  border-color: var(--green);
  box-shadow:
    0 0 0 2px var(--green),
    0 0 20px color-mix(in srgb, var(--green) 30%, transparent),
    0 9px 26px #0006,
    inset 0 0 0 4px #17120e,
    inset 0 0 0 5px color-mix(in srgb, var(--green) 45%, #695334);
}
.card.unaffordable { filter: grayscale(0.7) brightness(0.7); }
.card[data-small="true"] { transform: scale(0.92); }
/* CostMedallion — top-left, outside the frame, the only element allowed to break the edge. */
.card .card-costs { display: contents; }
.card .cost, .as-cost {
  position: absolute; top: -1rem; left: -1rem;
  display: grid; place-items: center;
  width: 3rem; height: 3rem;
  background: var(--bg); border: 2px solid var(--gold); border-radius: 50%;
  font: 600 1.4rem/1 var(--font-body); font-variant-numeric: tabular-nums; color: var(--parchment);
}
.card .mana-cost, .card .stamina-cost {
  position: absolute; right: -0.7rem; min-width: 3.2rem; height: 2.4rem; padding: 0 0.6rem;
  display: flex; align-items: center; justify-content: center; gap: 0.25rem;
  border-radius: var(--r-pill); font: 600 1rem/1 var(--font-body); font-variant-numeric: tabular-nums; z-index: 3;
}
.card .mana-cost { top: -0.7rem; background: #111b35; border: 2px solid #4f82d8; color: #cfe1ff; }
.card .stamina-cost { top: 1.9rem; background: #142016; border: 2px solid #5f925d; color: var(--parchment); }
/* Approved WC0/WC1 cost rail: below the name, inside the art. Costs have
   contrast outlines, never boxes; the remaining exposed fan edge shows all
   three resource types. Legacy classes remain available to screen callers. */
.card .card-cost-rail {
  position: absolute;
  top: calc(var(--card-pad) + 1.2rem + var(--card-name-h) + var(--card-gap) + 0.25rem);
  left: calc(var(--card-pad) + 0.25rem);
  display: flex; flex-direction: column; gap: 0.2rem;
  z-index: 3; pointer-events: none;
}
.card .card-cost-rail > div {
  position: static; display: flex; justify-content: flex-start; gap: 0.2rem;
  width: auto; min-width: 0; height: auto; padding: 0;
  border: 0; border-radius: 0; background: none;
  font: 600 max(12px, 1.1rem)/1.2 var(--font-body);
  paint-order: stroke fill; -webkit-text-stroke: 2px var(--bg);
  text-shadow: 0 1px 2px var(--bg);
}
.card .card-cost-rail > .cost { color: var(--gold); }
.card .card-cost-rail > .stamina-cost { color: var(--green); }
.card .card-cost-rail > .mana-cost { color: var(--frost); }
/* THE NAME IS ONE LINE IN A FIXED BOX. A long name steps its type down. */
.card .cname, .as-card .cd-name {
  --track: 0.02em;
  flex: 0 0 var(--card-name-h); height: var(--card-name-h);
  display: block; align-content: center; text-overflow: ellipsis;
  font: 600 1.2rem/1.15 var(--font-display); letter-spacing: var(--track); text-indent: var(--track);
  text-align: center; white-space: nowrap; overflow: hidden; min-width: 0;
}
.card[data-name="long"] .cname { font-size: 1.05rem; }
.card[data-name="verylong"] .cname { font-size: 0.92rem; }
.card.upgraded .cname { color: var(--green); }
/* ArtWell at a fixed landmark. */
.card .art {
  flex: 0 0 var(--card-art-h); height: var(--card-art-h); width: 100%;
  display: grid; place-items: center;
  background: #0f0c09; border: 1px solid var(--line); border-radius: var(--r-2);
  font-size: 2.9rem; line-height: 1; color: var(--parchment);
}
/* TypeBand — the type's own tint, full card width, fixed landmark. */
.card .ctype, .as-typeband {
  --track: 0.22em;
  flex: 0 0 var(--card-band-h); height: var(--card-band-h);
  display: grid; place-items: center;
  border-radius: var(--r-1);
  font: 500 0.95rem/1 var(--font-body); letter-spacing: var(--track); text-indent: var(--track); text-transform: uppercase;
  color: var(--card-type-color, var(--gold));
  background: color-mix(in srgb, var(--card-type-color, var(--gold)) 15%, #2a241c);
  white-space: nowrap; overflow: hidden; min-width: 0;
}
/* THE SHARED BUDGET. */
.card .cd-body {
  flex: 0 0 calc(var(--card-body-rows) * var(--card-row));
  height: calc(var(--card-body-rows) * var(--card-row));
  display: flex; flex-direction: column; gap: 0.3rem; overflow: hidden; min-width: 0;
}
/* THE TWO-ROW BUDGET IS MEASURED, NOT GUESSED IN CALC. This rule used to clamp
   the strip with `max-height: calc(2 * (1.5rem + 0.3rem))`, which is a guess at
   what a chip row costs — and the guess was low: a shipped `.ctag` measures
   17px against the 15px assumed, so even a correctly fitted two-row strip was
   clipped, and an over-long one lost whole rows with no ellipsis and no `+N`.
   fitCardFace() counts the rows on the real face and hides every chip past the
   second behind a `+N` chip, so the strip's natural height IS the budget.
   `overflow: hidden` stays only as the belt for the frame before the first fit. */
.card .ctags { flex: 0 0 auto; display: flex; flex-wrap: wrap; gap: 0.3rem; justify-content: center; align-content: flex-start; line-height: 1; overflow: hidden; }
.card .ctag {
  font-size: 0.85rem; letter-spacing: 0.04em; white-space: nowrap; line-height: 1.3;
  color: var(--tag-color, var(--muted));
  border: 1px solid color-mix(in srgb, var(--tag-color, var(--muted)) 45%, transparent);
  background: color-mix(in srgb, var(--tag-color, var(--muted)) 12%, transparent);
  border-radius: var(--r-1); padding: 0.1rem 0.4rem;
}
.card .ctag[hidden] { display: none; }
.card .ctext {
  flex: 1 1 auto; min-height: 0;
  display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden;
  line-height: var(--card-row); font-size: 1.1rem; text-align: center; color: var(--ink-dim);
}
.ctext b { color: var(--parchment); }
.card[data-tag-rows="0"] .ctext { -webkit-line-clamp: 4; }
.card[data-tag-rows="1"] .ctext { -webkit-line-clamp: 3; }
.card[data-tag-rows="2"] .ctext { -webkit-line-clamp: 2; }

/* WC0: one intrinsic face in every host. The four tracks own the full
   height; metadata never becomes a gameplay action.

   THE SHAPE AND THE TRACKS ARE READ, NOT RESTATED. This rule used to write
   `aspect-ratio: 5 / 8` and `1fr 4fr 4fr 1fr` out longhand, which was the same
   pair of facts content/config/ui/components/card.json already declared — so
   editing the JSON moved the hand's layout arithmetic (HandLayout, via
   wireframeUi) and left the card's own face exactly as it was. Both halves now
   read one home; src/ui/models/CardSizeModel.js projects it onto :root and
   carries the note on which shape won. There is deliberately NO fallback here:
   an unprojected card is shapeless and loud, never quietly the wrong shape. */
.card[data-wireframe="WC1"] {
  aspect-ratio: var(--card-ratio);
  height: auto;
  padding: 0;
  display: grid;
  grid-template-rows: var(--card-bands);
  gap: 0;
  overflow: visible;
}
.card[data-wireframe="WC1"] > .cname,
.card[data-wireframe="WC1"] > .art,
.card[data-wireframe="WC1"] > .cd-body {
  height: auto; min-height: 0; min-width: 0; margin: 0;
  box-sizing: border-box;
}
.card[data-wireframe="WC1"] > .cname { padding-inline: 0.3rem; }
.card[data-wireframe="WC1"] > .art {
  position: relative; border-radius: 0; border-inline: 0;
  overflow: hidden;
}
.card[data-wireframe="WC1"] .ctags {
  position: absolute; inset: auto 0.25rem 0.25rem;
}
.card[data-wireframe="WC1"] > .cd-body { padding: 0.4rem; }
.card[data-wireframe="WC1"] .ctext { -webkit-line-clamp: 5; }
/* The rail hangs just under the name band, so it reads that band's share
   rather than repeating "10%" — a third copy of the same authored number. */
.card[data-wireframe="WC1"] > .card-cost-rail { top: calc(var(--card-band-head) + 0.25rem); left: 0.35rem; }
.card[data-wireframe="WC1"] > .card-metadata {
  display: flex; align-items: center; justify-content: space-between;
  min-height: 0; overflow: hidden; padding-inline: 0.4rem;
  border-top: 1px solid var(--line-soft); color: var(--muted);
  font-size: 0.9rem; text-transform: capitalize;
}
/* WCI3: the owned count holds the end of the band even when rarity is absent. */
.card[data-wireframe="WC1"] > .card-metadata > [data-meta-slot="end"] { margin-inline-start: auto; }
.card[data-wireframe="WC1"] > .card-metadata > [data-meta-kind="owned"] { text-transform: none; white-space: nowrap; }
/* WCI2: contained artwork keeps its intrinsic ratio; the anchor only says
   where the spare room goes (around it, or above a figure on its baseline). */
[data-identity-part="artwork"][data-artwork-anchor] :is(img, video) { object-fit: contain; }
[data-identity-part="artwork"][data-artwork-anchor="center"] :is(img, video) { object-position: center; }
[data-identity-part="artwork"][data-artwork-anchor="bottom"] :is(img, video) { object-position: center bottom; }
/* ═══ COMBATANT — one shape, three statures ════════════════════════════════ */
/* Stature is DERIVED from the encounter pool (normal → normal, elite → large,
   boss → huge). Only the sprite well scales; nameplate, bars and meters stay
   where a player learned them. The player is always normal. */
.combatant { --stature: 1; }
.combatant[data-stature="large"] { --stature: 1.15; }
.combatant[data-stature="huge"] { --stature: 1.3; }
.combatant .sprite > img { transform-origin: bottom center; scale: var(--stature); }
.combatant .meters { width: calc(13rem * var(--stature)); }
/* The marks inside card text — the same in the fan and in the tooltip. */
.ctext .kw { color: var(--gold); }
.ctext .st-bleed { color: var(--bleed); font-weight: 600; }
.ctext .val { font-weight: 700; }
.ctext .val.up { color: var(--green); }
.ctext .val.down { color: var(--ember); }

/* The nameplate is the way into a combatant's full read on touch (there is no
   `I` on a phone). It reads as a control without becoming a button-shaped box
   over the battlefield: the hit area is the tap floor, the affordance is the
   cursor and the focus ring. */
.combatant .nm-inspect { cursor: help; min-height: var(--tap-floor); display: flex; flex-direction: column; justify-content: center; }
.combatant .nm-inspect:hover .ls-label, .combatant .nm-inspect:focus-visible .ls-label { color: var(--gold); }
.combatant .nm-inspect:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-ring-offset); border-radius: var(--r-1); }
.as-tip[data-tooltip-variant="combatant-context"] > .as-btn {
  display: flex; width: 100%; min-height: 0; padding: 3px 8px;
  margin-top: 6px; font-size: 0.85em; line-height: 1.3;
}
/* A card's pending choice (Warrior's Vow's stance, ui/components/cardChoiceModal.js). */
.card-choice .card-choice-body { padding: var(--modal-inset); display: flex; flex-direction: column; gap: 1rem; }
.card-choice .card-choice-row { display: flex; flex-direction: column; gap: 0.35rem; }
.card-choice .card-choice-row .as-prose { margin: 0; }
/* The full read, in a door: the same sections the edge tray renders. */
.combatant-door .combatant-inspector-body { padding: var(--modal-inset); display: flex; flex-direction: column; gap: 1.4rem; min-height: 0; overflow: hidden; }
/* W1w: sprite/name/HP preview left, details right; only the details scroll. */
.combatant-door .combatant-inspector-layout {
  display: grid; grid-template-columns: minmax(0, calc(var(--inspector-preview-fraction, .38) * 100%)) minmax(0, 1fr);
  gap: 1rem; flex: 1 1 auto; min-height: 0; align-items: start;
}
.combatant-door .combatant-inspector-details { display: flex; flex-direction: column; gap: 1.4rem; min-height: 0; max-height: 100%; overflow-y: auto; overflow-x: hidden; }
.combatant-door .combatant-inspector-preview { display: grid; gap: .5rem; min-width: 0; align-content: start; }
.combatant-door .combatant-inspector-art { display: grid; place-items: end center; aspect-ratio: 3 / 4; overflow: hidden; }
.combatant-door .combatant-inspector-art > * { max-width: 100%; max-height: 100%; }

/* ═══ QUICK SETTINGS — the utility rail, under the band on the right ══════ */
/* Fullscreen and music are the two controls that belong to the PAGE, not to
   whatever is on it. They ride a rail of their own: a vertical stack pinned to
   the right edge, immediately BELOW the band, over the surface's content.
   WHY NOT THE TOP-RIGHT CORNER, which is where they were for a day. Owner,
   2026-09-04: "move full screen and music … to anchored to top right corner
   and not block any text"; owner, 2026-09-05, with a screenshot of build
   0.4.0.1454: "I miss the old Hudson and music and full screen placement".
   The corner is inside the band, so keeping text out from under the pair meant
   every surface reserving a horizontal slice of its own top row — and the
   measurement is what settles it: at 360x800 the map's header reserved
   243 of 434 CSS px, 56% of the band, for a pair 106px wide, and the identity
   ellipsed to "FORSAKEN ·…" while the meters collapsed to 50px. Below the band
   the pair overlays content that was already yielding to it: `.field` in
   styles/combat.css pads its right edge for exactly this rail, and the map's
   route strip calls its own right margin "the HUD's utility gutter". Those two
   gutters were written for this placement and were never removed, so for a day
   the game reserved space in three places for two buttons.
   The band is the containing block (`.as-band { position: relative }`), so
   `top: 100%` follows the band's own height on every surface and at every text
   size without a number to keep in step. `--hud-quick-edge-gap` is authored
   (balance.ui.hudQuickSettings.edgeGapPx, planted on <html> by main.js) and
   divided by the zoom, so the gap is the same on glass at every UI size. */
/* THE RAIL'S FOOTPRINT, as one token the surfaces under it reserve. It is ONE
   IconButton wide, not two, because the rail is a COLUMN — the same value this
   token replaced when it was inlined in styles/combat.css. */
:root { --quick-corner: calc(var(--iconbtn-size) + (2 * var(--hud-quick-edge-gap, 4px) / var(--ui-zoom, 1))); }
.hud-quick-settings {
  position: absolute; z-index: 80;
  top: calc(100% + (var(--hud-quick-edge-gap, 4px) / var(--ui-zoom, 1)));
  right: calc(env(safe-area-inset-right, 0px) + (var(--hud-quick-edge-gap, 4px) / var(--ui-zoom, 1)));
  display: flex; flex-direction: column; align-items: flex-end; gap: 0.6rem;
  margin: 0; padding: 0;
}
/* The notice speaks to the LEFT of the rail rather than pushing it off the
   edge: the rail is a column now, so there is no room under it. */
.hud-quick-settings > .hud-quick-notice {
  position: absolute; top: 0; right: calc(var(--iconbtn-size) + 0.4rem);
  max-width: 22rem; text-align: right; white-space: normal;
}
.hud-quick-settings > .hud-quick-notice[hidden] { display: none; }
/* NOTHING RESERVES A SLICE OF ITS OWN TOP ROW ANY MORE. The rail sits under the
   band, so the run header, the co-op banner and the co-op board top each get
   their full width back — that is the point of moving it, and re-adding a
   `padding-inline-end: var(--quick-corner)` to any of them would give the dead
   space back. What DOES reserve for the rail is what sits beneath it:
   `.field` (styles/combat.css) and the map's route strip, both already written
   for this placement. */
/* A narrow map header cannot hold its whole line AND the corner, and `nowrap`
   made its actions overflow INTO the corner rather than yield. It wraps: the
   way out drops to its own line under the pair (kit law — a narrow shape is a
   different composition), which is what keeps the corner clear of text. */
.topbar.map-header { flex-wrap: wrap; row-gap: 0.4rem; }
/* THE ROOMS WEAR THE MAP'S BAND (components/runHud.js): the merchant, the
   Shrine and an event mount the same header above their `.screen`, so a
   player between fights always sees the purse, the pools and the act. The
   screen below it keeps its own composition; only the top edge changes —
   a page under a band starts at the band, not at a centred column's top
   padding. */
.topbar.room-header { flex-wrap: wrap; row-gap: 0.4rem; }
.screen.room-screen { justify-content: flex-start; justify-content: safe flex-start; }
/* The belt (relics left, flasks right) hangs 0.4rem BELOW the band, absolutely,
   over whatever follows — on the map that is parchment, in a room it was the
   title ("Shrine of Ember" under a flask at 390 wide). The room's top padding
   is the belt's own height plus the band's gap, so the page starts under it. */
.screen.room-screen { padding-top: calc(var(--iconbtn-size) + 1.6rem); }

/* ═══ SCREENS — a page wears the same atoms a door does ═══════════════════ */
/* A screen's banner is Title·L over an Eyebrow-weight subtitle; the same
   two atoms the kit's TitleMenu and Pane use, centred because a screen's
   banner is the one centred text a page has. */
.screen > h2, .screen > .as-title-l { --track: 0.06em; margin: 0; font: 700 2.55rem/1.1 var(--font-display); letter-spacing: var(--track); text-indent: var(--track); color: var(--gold); text-align: center; }
.screen > .subtitle, .screen > .as-eyebrow { margin: 0; color: var(--muted); font-size: 1.1rem; letter-spacing: 0.22em; text-transform: uppercase; text-align: center; }
/* OptionCard under the names the screens already write: `.class-pick` is the
   card, `.glyph` its Glyph, `.cp-body` its body, `h3` its Title·S, `p` its
   prose, `.chip` a StatePill, `.locked` disabled. A row of them is `.class-row`. */
.class-row { display: flex; flex-wrap: wrap; gap: 1.05rem; justify-content: center; width: 100%; max-width: 80rem; }
.class-pick {
  position: relative; display: flex; gap: 1.5rem; align-items: center;
  flex: 1 1 26rem; max-width: 40rem; min-width: 0;
  padding: 1.28rem 1.5rem;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-3);
  color: inherit; text-align: left; cursor: pointer;
  transition: border-color 150ms, box-shadow 150ms;
}
.class-pick:hover { border-color: var(--line-soft); }
.class-pick.selected, .class-pick[aria-checked="true"], .class-pick[aria-pressed="true"] { border-color: var(--gold); box-shadow: 0 0 0 1px rgba(var(--accent-rgb), 0.35), 0 0 18px rgba(var(--accent-rgb), 0.18); }
.class-pick.locked { opacity: 0.55; cursor: default; }
/* `quiet`: still a choice, but not the one to reach for — Rest at full
   health and mana still ends the visit, so it stays pressable and reads muted. */
.class-pick.quiet { opacity: 0.7; }
.class-pick.quiet > .cp-body > h3 { color: var(--muted); }
.class-pick.locked:hover { border-color: var(--line); }
.class-pick > .glyph { flex: 0 0 3rem; font-size: 2.25rem; line-height: 1; color: var(--gold); text-align: center; }
.class-pick > .cp-body, .class-pick > .ob { min-width: 0; flex: 1 1 auto; display: flex; flex-direction: column; gap: 0.2rem; }
.class-pick h3 { margin: 0; display: flex; align-items: center; gap: 0.75rem; font: 600 1.65rem/1.2 var(--font-display); letter-spacing: 0.03em; color: var(--gold); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.class-pick p { margin: 0; color: var(--ink-dim); font-size: 1.35rem; line-height: 1.4; }
.class-pick .chip { display: inline-block; align-self: flex-start; margin-top: 0.3rem; padding: 0.15rem 0.6rem; border: 1px solid var(--line-soft); border-radius: var(--r-1); font: 500 1.02rem/1.5 var(--font-body); letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); white-space: nowrap; }
.class-pick .chip.reward-new { color: var(--gold); border-color: var(--gold); }
.class-pick > button.reward-skip { flex: 0 0 auto; align-self: center; }
/* The reward menu is a column of OptionCards, full pane width. */
.reward-menu { flex-direction: column; align-items: stretch; max-width: 60rem; }
.reward-menu > .class-pick, .shrine-option-list > .class-pick { max-width: none; flex: 0 0 auto; }
.reward-row { display: flex; flex-wrap: wrap; gap: 2.2rem; justify-content: center; }
.reward-row .card:hover { transform: translateY(-8px) scale(1.06); box-shadow: 0 10px 30px rgba(201, 162, 39, 0.25); }
.reward-kind[data-state='taken'] { animation: reward-took 420ms ease-out; }

/* ═══ CARD SHELF — a row of resting cards, four across, whole ══════════════ */
/* WHAT THIS REPLACES. A shelf of cards you are browsing past was laid out five
   different ways: the merchant's armaments in `min(280px, 100%)` tiles (the
   FOCUS width, around a 152px GLANCE card), its relics and flasks in
   `flex: 1 1 26rem` OptionCards, a mount's deck list and the pile viewer in
   fixed 140px and 220px boxes. None of them said how many cards a row holds,
   so the answer was whatever the container happened to be — at 1328x744 the
   merchant's armament shelf held ONE, with the rest behind a scroll.

   THE RULE, ONCE. `maxColumns` cards across; each track as wide as the row
   allows, never below `--card-shelf-min` and never above a card's resting
   width; below that floor the row holds fewer rather than shaving them. The
   three numbers are authored at `content/config/ui/components/card.json ->
   sizing.shelf` and projected by CardSizeModel.js, whose `cardShelfColumnsAt`
   is this rule's arithmetic, asked from Node by tests/card-shelf.test.mjs.

   The floor is written `min(--card-shelf-min, --card-w-glance)` for the same
   reason the model clamps it: a card tuned (or a phone variant dropped) below
   the floor would otherwise be stretched to a floor wider than itself. */
.card-shelf {
  --card-shelf-track: max(
    min(var(--card-shelf-min, 120px), var(--card-w-glance, 152px)),
    calc((100% - (var(--card-shelf-cols, 4) - 1) * var(--card-shelf-gap, 12px)) / var(--card-shelf-cols, 4))
  );
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: center;
  gap: var(--card-shelf-gap, 12px);
  width: 100%;
  max-width: none;
}
/* NO GROW. A wrapping flex row lets the last row's stragglers spread into the
   slack beside them — measured on the merchant's five-card shelf at 1328x744:
   four cards at 145px and the fifth, alone on its row, at 155. One shelf, one
   card, two sizes. The track already fills the row, because
   components/cardShelf.js writes `--card-shelf-cols` from the count the shelf
   actually holds at its measured width, so there is nothing left for growth to
   do but break the agreement between the rows. */
.card-shelf > * {
  flex: 0 1 var(--card-shelf-track);
  min-width: 0;
  max-width: var(--card-w-glance, 152px);
  margin: 0;
}
/* A line of prose on a shelf is not a card: an empty shelf's "nothing for sale
   on this visit" would otherwise be squeezed into one 152px column. */
.card-shelf > :is(.as-status, p, h3, .shop-offer-avail) { flex-basis: 100%; max-width: none; }
/* NOR IS AN OFFER WITH NO CARD IN IT. The merchant's SELL shelf is mixed: an
   armament he will buy is a card, a relic or flask is an OptionCard with a
   name, a description and a price and no face at all. Capping THAT at a card's
   width ellipsised the name and stood the description in a 152px strip, so a
   tile that holds no card keeps the kit's own measure (`.class-pick`, kit
   above) and takes a row of its own. `.shop-collectible-offer` is the tile
   that DOES hold a face and is deliberately not exempt. */
.card-shelf > .shop-text-offer { flex: 1 1 26rem; max-width: 40rem; }
/* THE PLAYING CARD IS A FIXED BOX and `flex-shrink: 0`, so on a shelf it would
   have overflowed its track rather than fitting in it. `--card-w` is the box's
   one width input and the height is computed from its landmarks, not from the
   width — so a narrower card is the same card with its text clamped by the
   budget that already clamps it (`.ctext` line-clamp); Information opens the
   full text. */
.card-shelf .card { --card-w: 100%; width: 100%; flex-shrink: 1; }
/* `[data-small]` scales the card by transform: on a shelf the TRACK is what
   makes it small, and a transform on top of that only opens a gap. Hover and
   focus keep their lift. */
.card-shelf .card[data-small="true"]:not(:hover, :focus-visible, :focus-within) { transform: none; }
/* The pick row's lift, kept: a shop shelf used to be a `.reward-row` and wore
   it, and the signal should not depend on which class a shelf happens to have. */
.card-shelf .card:hover { transform: translateY(-8px) scale(1.06); box-shadow: 0 10px 30px rgba(201, 162, 39, 0.25); }

/* ═══ EVENT — W1u, a choice body on the page ════════════════════════════════ */
/* The head names the event and its {Status}; the narrative (ArtWell + prose)
   stands beside the responses, stacked on narrow hosts; the foot holds
   Continue, allowed once a response is taken. It stands on the page rather
   than over one, so it wears the door's box without the veil. The frame is
   the CHOICE BODY block below. */
.event-narrative > .as-artwell { margin: 0 auto 1.2rem; }
.event-narrative > .as-prose { margin: 0; }
.event-door .as-options { text-align: left; }
.event-door .as-option.ev-choice { min-height: var(--tap-floor); }
.event-door .as-option.ev-choice[disabled] { opacity: 0.55; cursor: default; }
.event-door .as-option .on { white-space: normal; }
/* The label's own ellipsis rule outranked the line above, so a choice's
   consequence — "gain 90 cinders; take 6 damage" — was cut to "gain 90
   cinders; t…" on a phone. A choice wraps; it never hides.
   MEASURED, NOT REASONED — the first fix here was `.event-door .as-option .on
   .as-label-text` at (0,4,0) and it lost: the ellipsis rule is `.as-option .on
   > span:not(.as-pill):not(.as-tag)` (§ OptionCard below), and `:not()` counts
   its argument, so that rule is (0,4,1). The label still read `nowrap` at
   390x844 on 2026-09-11 with the "fix" in the file. `.ev-choices` lifts this
   to (0,5,0), which is the smallest step that actually wins. */
.event-door .ev-choices .as-option .on .as-label-text { white-space: normal; overflow: visible; text-overflow: clip; }

/* ═══ CHOICE BODY — the W1 frame for W1s Rest and W1u Event ═══════════════ */
/* Shares arrive as custom properties from ChoiceBodyModel (wireframeUi
   .choiceBody), divided by --ui-zoom so each is a share of the visible
   viewport. The frame takes what the run HUD leaves, capped at its nominal
   height; only the body scrolls. Fallbacks are first-paint values only. */
.screen.choice-screen { justify-content: flex-start; gap: 0; padding-inline: var(--choice-outer-x, 2.4rem); padding-bottom: var(--choice-outer-y, 2.4rem); }
.modal.choice-door { flex: 1 1 auto; min-height: 0; width: var(--choice-frame-width, 100%); max-width: 100%; max-height: var(--choice-frame-height, 100%); }
.choice-door > .modal-head { flex-wrap: wrap; row-gap: 0.4rem; box-sizing: border-box; min-height: var(--choice-head-min, auto); padding-inline: var(--choice-inset-x, var(--modal-inset)); }
.choice-door > .modal-head .modal-head-id h2 { white-space: normal; }
.choice-door > .modal-head > .modal-head-actions { flex: 0 1 auto; min-width: 0; align-self: center; }
.choice-door > .modal-body { padding: var(--choice-inset-top, var(--modal-inset)) var(--choice-inset-x, var(--modal-inset)); }
.choice-door > .modal-foot { box-sizing: border-box; min-height: var(--choice-foot-min, auto); padding-inline: var(--choice-inset-x, var(--modal-inset)); }
.choice-door > .modal-foot > .modal-foot-actions { flex: 1 1 auto; margin-left: 0; }
/* The two body slots: side by side on wide hosts, stacked on narrow ones. */
.choice-body-columns { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); column-gap: var(--choice-column-gap, 1.5rem); row-gap: var(--choice-row-gap, 1rem); align-items: start; }
:root[data-layout='narrow'] .choice-body-columns { grid-template-columns: minmax(0, 1fr); }
.choice-body-columns > * { min-width: 0; }
.choice-body .class-row.shrine-option-list {
  width: 100%; max-width: none; margin-inline: 0;
  /* Six collapsed actions are the Shrine's initial menu, not overflow
     content. Keep their rhythm compact enough to show the whole menu first;
     the body still scrolls when a fold opens or the viewport is genuinely
     short. */
  gap: 0.8rem;
}
/* W1s availability column: what arrival restored, then each choice's state. */
.choice-status { display: grid; gap: 0.6rem; align-content: start; }
.screen .choice-status .rest-refill { margin: 0; max-width: none; text-align: left; }
.choice-status-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.4rem; }
.choice-status-row { display: flex; justify-content: space-between; gap: 1rem; min-width: 0; }
.choice-status-row[data-state='available'] .choice-status-state { color: var(--gold); }
.choice-status-row:is([data-state='unavailable'], [data-state='used']) .choice-status-state { color: var(--muted); }

/* ═══ ARMOURY — an xl door on the shell ════════════════════════════════════ */
/* The head is the kit's (tab strip = the views, IconButton close in the same
   corner as every door). Under the hairline the body is the surface's own
   two-pane machine, edge to edge; every pane is kit pieces — OptionCards for
   the positions and the inventory, DetailCards for the character's numbers,
   Folding Trays along the bottom — and the layout facts (pane ratios, the
   tray caps) arrive as custom properties from armouryUi.json. `.armoury-*`
   and `.equip-*` stay on the parts because the tools read them. */
.armoury > .modal-body.armoury-shell-body { display: flex; flex-direction: column; overflow: hidden; padding: 0; }
.armoury-content { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.armoury-trays { display: contents; }
.armoury-trays > [data-tray-edge="bottom"]:first-child { margin-top: auto; }
/* A context tray arrives at its content's height, capped so the subject keeps
   the room (Constantine, 2026-08-23: the tray must not claim half the door). */
.armoury .as-tray { --tray-compact: 35%; }
.equip-resource-change > .as-flavor { display: block; }
.as-blocker.armoury-notice { flex: 0 0 auto; border-radius: 0; border-width: 0 0 1px; padding-inline: var(--modal-inset); }
/* THE SUBJECT GROWS FROM NOTHING (basis 0), so an open tray keeps its own
   content height and the subject takes what is left — never the other way
   round — and it always keeps a floor of the door. */
.armoury-subject { flex: 1 1 0; display: flex; min-width: 0; min-height: 34%; overflow: hidden; }
.armoury .as-tray[data-collapsed="0"] { flex: 0 1 auto; }
/* The Armaments tray stands INSIDE a pane and fills it. */
.armoury-body .as-tray.armoury-equipment { --tray-compact: none; flex: 1 1 auto; min-height: 0; border-top: 0; max-height: none; }
.armoury-body .as-tray.armoury-equipment[data-collapsed="1"] { flex: 0 0 auto; }
.armoury-body .as-tray.armoury-equipment > .as-pane { flex: 1 1 auto; }
.armoury-body .as-tray.armoury-equipment > .tray-header > .as-row { padding-inline: 0.6rem; }
.armoury-body .as-tray.armoury-equipment > .tray-header { padding-right: 0; }
/* THE BODY IS A GRID OF PANES; the ratios are the authored ones. */
.armoury-body {
  flex: 1 1 auto; min-height: 0; min-width: 0; overflow: hidden;
  display: grid; gap: var(--armoury-gap, 1.6rem); padding: var(--modal-inset);
  grid-template-columns: minmax(0, var(--armoury-character-ratio, 0.4fr)) minmax(0, var(--armoury-equipment-ratio, 0.6fr));
}
.armoury[data-pane="both"] .armoury-body { grid-template-columns: minmax(0, var(--armoury-character-ratio, 0.4fr)) 0.7rem minmax(0, var(--armoury-equipment-ratio, 0.6fr)); gap: 0; }
.armoury[data-pane="both"] .armoury-left { grid-column: 1; padding-right: 0.7rem; }
.armoury[data-pane="both"] .armoury-right { grid-column: 3; padding-left: 0.7rem; }
.armoury[data-pane="both"] .armoury-hybrid-splitter { display: block; grid-column: 2; grid-row: 1; }
.armoury[data-pane="character"] .armoury-body, .armoury[data-pane="inventory"] .armoury-body { grid-template-columns: minmax(0, 1fr); }
.armoury[data-pane="character"] .armoury-right, .armoury[data-pane="inventory"] .armoury-right { display: none; }
.armoury-left, .armoury-right { display: flex; flex-direction: column; gap: 1.2rem; min-width: 0; min-height: 0; overflow: hidden auto; }
/* Inventory view: armaments beside the inventory, a splitter between. */
.armoury[data-pane="inventory"] .armoury-content { display: grid; grid-template-columns: minmax(0, var(--armoury-armaments-pane-width, 0.6fr)) 0.7rem minmax(0, var(--armoury-inventory-pane-width, 0.4fr)); gap: 0; }
.armoury[data-pane="inventory"] .armoury-body { grid-column: 1; grid-row: 1; }
.armoury[data-pane="inventory"] .armoury-inventory { grid-column: 3; grid-row: 1; min-height: 0; overflow-y: auto; border-top: 0; }
.armoury[data-pane="inventory"] .armoury-pane-splitter { display: block; grid-column: 2; grid-row: 1; }
.armoury[data-pane="inventory"] .armoury-inventory.as-tray[data-collapsed="0"] { align-self: stretch; height: auto; max-height: none; }
.armoury[data-pane="inventory"] .armoury-inventory.as-tray[data-collapsed="0"] > .as-pane { flex: 1 1 auto; }
/* A PHONE STACKS the two panes and turns the splitter on its side. */
.armoury[data-responsive="phone"] .armoury-body { gap: 0.7rem; padding: 0.7rem; }
.armoury[data-pane="inventory"][data-responsive="phone"] .armoury-content { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, var(--armoury-armaments-pane-width, 0.6fr)) 0.55rem minmax(0, var(--armoury-inventory-pane-width, 0.4fr)); }
.armoury[data-pane="inventory"][data-responsive="phone"] .armoury-body { grid-column: 1; grid-row: 1; }
.armoury[data-pane="inventory"][data-responsive="phone"] .armoury-pane-splitter { grid-column: 1; grid-row: 2; }
.armoury[data-pane="inventory"][data-responsive="phone"] .armoury-inventory { grid-column: 1; grid-row: 3; border-top: 1px solid var(--line); }
.armoury[data-pane="inventory"][data-responsive="phone"] .armoury-content:has(> .armoury-inventory[data-collapsed="1"]) { grid-template-rows: minmax(0, 1fr) auto; }
.armoury[data-pane="inventory"][data-responsive="phone"] .armoury-content:has(> .armoury-inventory[data-collapsed="1"]) > .armoury-pane-splitter { display: none; }
.armoury[data-pane="inventory"][data-responsive="phone"] .armoury-content:has(> .armoury-inventory[data-collapsed="1"]) > .armoury-inventory { grid-row: 2; }
/* THE CHARACTER PANE: LabelStack summary, the figure in an ArtWell, the
   numbers in fold-open DetailCards. In the Character view the numbers stand
   beside the figure; elsewhere they stack under it. */
.armoury-character { display: grid; grid-template-rows: auto minmax(8rem, var(--armoury-sprite-ratio, 0.38fr)) minmax(0, var(--armoury-stats-ratio, 0.62fr)); gap: 0.9rem; min-width: 0; min-height: 100%; }
.armoury-character > .character-summary { align-self: start; }
.armoury[data-pane="character"] .armoury-character { grid-template-columns: minmax(0, var(--armoury-summary-pane-ratio, 0.4fr)) minmax(0, var(--armoury-stats-pane-ratio, 0.6fr)); grid-template-rows: auto minmax(0, 1fr); gap: 1rem 1.4rem; }
.armoury[data-pane="character"] .character-summary { grid-column: 1; grid-row: 1; }
.armoury[data-pane="character"] .armoury-sprite-pane { grid-column: 1; grid-row: 2; }
.armoury[data-pane="character"] .armoury-character-stats { grid-column: 2; grid-row: 1 / span 2; align-self: stretch; }
.character-summary { padding-bottom: 0.6rem; border-bottom: 1px solid var(--line); }
.character-summary > .as-title-s { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.character-summary > .as-subtitle { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.armoury[data-responsive="phone"] .character-summary > .as-subtitle { display: none; }
.armoury-sprite-pane.as-artwell { width: 100%; height: 100%; min-height: 0; max-height: 100%; }
.armoury-sprite-pane .armoury-figure { position: relative; width: auto; height: min(82%, 22rem); aspect-ratio: 150 / 190; max-width: 78%; max-height: 82%; }
.armoury-figure img, .armoury-figure > * { width: 100%; height: 100%; object-fit: contain; }
.armoury[data-responsive="phone"] .armoury-sprite-pane .armoury-figure { height: min(74%, 14rem); max-width: 68%; max-height: 74%; }
.armoury-character-stats { display: flex; flex-direction: column; gap: 0.6rem; min-width: 0; min-height: 0; overflow: auto; }
/* An attribute face and a power face are label + value, one per line. */
.armoury-character-stats .disc-faces > .disc-face .on { font: 500 1.41rem/1.3 var(--font-body); letter-spacing: 0; color: var(--parchment); }
.armoury-character-stats .disc-faces > .disc-face .od {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 1.2rem;
}
.armoury-character-stats .disc-faces > .disc-face > .r-trail,
.armoury-character-stats .disc-faces > .disc-face > .r-trail > .as-status {
  flex: 0 0 auto; min-width: 2ch; white-space: nowrap;
}
/* THE POSITIONS: one OptionCard per position; the occupied one opens its
   DetailCard under itself. */
.equip-slots.as-options { gap: 0.75rem; }
.armoury-equipment-slot-component, .armoury-equipment-slot-component > .es-sets { display: contents; }
.armoury-position-card { min-width: 0; width: 100%; }
details.armoury-position-card > summary { list-style: none; }
details.armoury-position-card > summary::-webkit-details-marker { display: none; }
details.armoury-position-card[open] > summary { border-radius: var(--r-3) var(--r-3) 0 0; }
details.armoury-position-card > .as-detailcard { border-radius: 0 0 var(--r-3) var(--r-3); border-top: 0; }
.armoury-position-card .r-trail { flex-direction: column; align-items: flex-end; gap: 0.5rem; }
.armoury-position-card .r-trail > .as-btn.armoury-position-action { padding-inline: 1rem; font-size: 1.2rem; }
.armoury-position-grid-groups { display: grid; gap: 1.2rem; }
.armoury-position-grid-group { display: grid; gap: 0.6rem; min-width: 0; }
.armoury-position-grid-tiles > .as-options.grid { --opt-min: 9rem; gap: 0.6rem; }
.armoury-position-grid-card { flex-direction: column; text-align: center; padding: 0.9rem 0.6rem; gap: 0.5rem; }
.armoury-position-grid-card.is-empty { grid-column: 1 / -1; width: 100%; }
.armoury-position-grid-card > .ob { align-items: center; }
.armoury-position-grid-card .on { font-size: 1.3rem; }
.armoury-position-grid-detail { margin-top: 0.4rem; }
/* THE INVENTORY: one shared list of OptionCard faces; the open one reveals
   art beside its facts. */
.armoury-inventory .ep-list > .disc-faces { gap: var(--armoury-tray-content-gap, 0.35rem); }
.ep-list { min-width: 0; }
.inventory-face .inventory-equipped { color: var(--equip-equipped-tag-color, var(--gold)); border-color: var(--equip-equipped-tag-color, var(--gold)); }
.inventory-face[draggable="true"] { cursor: grab; }
.inventory-face.dragging { cursor: grabbing; opacity: 0.55; }
.inventory-face > .hold-hint { margin: 0; white-space: nowrap; }
.inventory-detail { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 1.5rem; align-items: start; min-width: 0; }
.inventory-detail.beat-hold { position: relative; }
.inventory-detail.beat-hold > .hold-hint { position: absolute; top: 0.35rem; right: 0.45rem; margin: 0; white-space: nowrap; }
.inventory-information { display: flex; flex-direction: column; gap: 0.6rem; min-width: 0; }
.inventory-information > .as-kitline, .inventory-information > .as-statstrip { margin: 0; }
.inventory-information > .tags { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.inventory-card-action-label { pointer-events: none; align-self: flex-start; }
.ep-equip { align-self: flex-start; }
.ep-equip.locked { opacity: 0.55; cursor: not-allowed; }
.armoury[data-responsive="phone"] .inventory-detail { grid-template-columns: minmax(0, 1fr); }
.armoury[data-responsive="phone"] .inventory-detail > .inventory-model { justify-self: center; }
/* THE COMPARISON — a fold of StatRows and deltas inside the reveal. */
.equip-candidate-comparison { min-width: 0; overflow-wrap: anywhere; }
.equip-candidate-comparison > summary { display: flex; align-items: center; gap: 0.75rem; min-height: var(--tap-floor); cursor: pointer; list-style: none; }
.equip-candidate-comparison > summary::-webkit-details-marker { display: none; }
.equip-candidate-comparison > summary > .as-eyebrow { color: var(--gold); }
.equip-card-changes, .equip-receipt-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.equip-card-changes > li, .equip-receipt-list > li { min-width: 0; }
.equip-added-effect.none, .equip-resource-change.none { color: var(--muted); }
.equip-resource-change > .as-kitline, .equip-receipt-list .as-kitline { margin: 0; }
.equipment-requirements, .player-poise-receipt, .player-load-receipt, .equip-role-receipts { display: flex; flex-direction: column; gap: 0.4rem; min-width: 0; overflow-wrap: anywhere; }
/* A receipt's key is a formula, not a word: it wraps, and the row stacks its values under the name when narrow. */
.armoury-equipment-receipts .as-statpair, .equip-candidate-comparison .as-statpair { white-space: normal; }
.armoury-equipment-receipts .as-statrow, .equip-candidate-comparison .as-statrow { flex-wrap: wrap; }
.armoury-equipment-receipts .as-statrow .sr-vals, .equip-candidate-comparison .as-statrow .sr-vals { flex: 1 1 12rem; }
.equipment-requirements .met, .equip-card-changes .met { color: var(--green); }
.equipment-requirements .unmet, .equip-card-changes .unmet { color: var(--danger); }
/* THE CARDS TRAY: one OptionCard row per equipment card, fold-open. */
.armoury-card-list.as-options { gap: 0.45rem; }
.armoury-card-list[data-card-view="grid"] { display: grid; grid-template-columns: repeat(var(--armoury-card-grid-columns, 4), minmax(0, 1fr)); }
.armoury-card-row { min-width: 0; }
details.armoury-card-row > summary { list-style: none; }
details.armoury-card-row > summary::-webkit-details-marker { display: none; }
details.armoury-card-row[open] > summary { border-radius: var(--r-3) var(--r-3) 0 0; }
details.armoury-card-row > .as-detailcard { border-radius: 0 0 var(--r-3) var(--r-3); border-top: 0; }
.armoury-card-row-detail { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 1.5rem; align-items: start; }
.armoury[data-responsive="phone"] .armoury-card-row-detail { grid-template-columns: minmax(0, 1fr); }
.armoury-card-row .r-trail { flex-wrap: wrap; justify-content: flex-end; }
/* THE STATS TRAY (Inventory view): DetailCards of StatPairs in a grid. */
.armoury-stats-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap: 0.6rem; }
.armoury-stats-summary > .armoury-equipment-receipts { grid-column: 1 / -1; }
.armoury-equipment-receipts { display: flex; flex-direction: column; gap: 0.6rem; min-width: 0; }
/* The kit picker chip (creation's kit picker, pieceChip) is an OptionCard;
   the rarity rides the border. */
.as-option.rarity-uncommon { border-left: 3px solid var(--frost); }
.as-option.rarity-rare { border-left: 3px solid var(--gold); }
.equip-chip.as-option { min-height: var(--tap-floor); }
.equip-chip .ec-tags { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.2rem; }
.equip-chip .ec-equipped { color: var(--equip-equipped-tag-color, var(--gold)); border-color: var(--equip-equipped-tag-color, var(--gold)); }

/* ═══ SHRINE — equal-height option headers, with expanding fold content ═══ */
.shrine-option-list, .shrine-option-grid {
  --shrine-card-h: max(6rem, min(var(--shrine-folded-card-height, 10vh), var(--shrine-folded-card-max-height, 8.4rem)));
}
.shrine-option-list { width: min(100%, 44rem); margin-inline: auto; flex-direction: column; align-items: stretch; gap: 1.05rem; }
.shrine-option-list > .class-pick { width: 100%; }
:is(.shrine-option-list, .shrine-option-grid) > .class-pick { box-sizing: border-box; height: var(--shrine-card-h); }
:is(.shrine-option-list, .shrine-option-grid) > .shrine-fold[open] { height: auto; }
:is(.shrine-option-list, .shrine-option-grid) > .class-pick p { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.shrine-option-grid { align-items: stretch; }
.shrine-fold { display: block; padding: 0; overflow: hidden; text-align: left; }
.shrine-fold > summary { display: flex; gap: 1.5rem; align-items: center; height: calc(var(--shrine-card-h) - 2px); box-sizing: border-box; padding: 1.28rem 1.5rem; cursor: pointer; list-style: none; }
.shrine-fold > summary::-webkit-details-marker { display: none; }
.shrine-fold > summary > .glyph { flex: 0 0 3rem; margin: 0; font-size: 2.25rem; line-height: 1; color: var(--gold); text-align: center; }
.shrine-fold > summary > .ob { min-width: 0; flex: 1 1 auto; display: flex; flex-direction: column; gap: 0.2rem; }
.shrine-fold .on { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 600 1.65rem/1.2 var(--font-display); letter-spacing: 0.03em; color: var(--gold); }
.shrine-fold .om { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); font-size: 1.05rem; letter-spacing: 0.08em; }
.shrine-fold-caret { flex: 0 0 auto; color: var(--muted); font-size: 1.72rem; line-height: 1; transition: transform 150ms; }
.shrine-fold:hover .shrine-fold-caret, .shrine-fold[open] .shrine-fold-caret { color: var(--gold); }
.shrine-fold[open] .shrine-fold-caret { transform: rotate(90deg); }
.shrine-fold[open] > summary { border-bottom: 1px solid var(--line-soft); }
.shrine-fold-content { display: grid; gap: 1.05rem; padding: 1.28rem 1.5rem 1.5rem; }
.shrine-fold-detail { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1.2rem; align-items: center; }

/* ═══ REWARDS — a decision door over the battlefield ═══════════════════════ */
/* Victory is a modal, not a screen: the md door stands on the veil over the
   field it was won on (or the map, or a bare backdrop after a reload). Rows
   are OptionCards; the foot carries the hold copy and Continue on the ladder. */
.reward-backdrop { min-height: 100%; }
.reward-door > .modal-body { display: flex; flex-direction: column; align-items: center; gap: 1.2rem; padding: var(--modal-inset); }
.reward-door[data-victory-compact='true'] > .modal-head { justify-content: center; }
.reward-door[data-victory-compact='true'] > .modal-head .modal-head-id { align-items: center; text-align: center; }
.reward-door[data-victory-compact='true'] > .modal-head .modal-head-actions { display: none; }
.reward-door .reward-compact-xp { width: 100%; min-width: 0; display: grid; gap: .65rem; }
.reward-door .reward-compact-summary { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: baseline; column-gap: .6rem; min-width: 0; font-variant-numeric: tabular-nums; }
.reward-door .reward-compact-heading { white-space: nowrap; color: var(--parchment); font-weight: 700; }
.reward-door .reward-compact-arithmetic { display: flex; align-items: baseline; justify-content: flex-start; gap: .15rem; min-width: 0; }
.reward-door .reward-compact-formula { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); }
.reward-door .reward-compact-more { flex: none; border: 0; padding: 0 .15rem; background: transparent; color: var(--gold); font: inherit; cursor: help; }
.reward-door .reward-compact-more[hidden] { display: none; }
.reward-door .reward-compact-value { justify-self: end; white-space: nowrap; color: var(--gold); }
.reward-door .reward-compact-list { box-sizing: border-box; height: min(calc(var(--receipt-rows, 4) * 2.1rem + .7rem), 32vh); margin: 0; padding: .35rem .55rem; list-style: none; overflow-y: auto; overflow-x: hidden; scrollbar-width: thin; border: 1px solid var(--line); border-radius: var(--r-4); background: color-mix(in srgb, var(--panel) 72%, var(--bg)); }
.reward-door .reward-compact-row { min-height: 2.1rem; display: flex; align-items: center; justify-content: space-between; gap: .7rem; min-width: 0; }
.reward-door .reward-compact-row-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.reward-door .reward-compact-row-value { flex: none; white-space: nowrap; color: var(--gold); font-variant-numeric: tabular-nums; }
.reward-door .reward-compact-continue:disabled { opacity: .58; cursor: default; }
.reward-door .reward-compact-continue.is-ready { background: #2b7047; border-color: #71c990; color: #fff; }
.reward-door .reward-compact-continue.is-ready:is(:hover, :focus-visible) { background: #368a59; border-color: #a0e6b6; }
.reward-door .reward-menu { max-width: none; width: 100%; }
/* W1t: reward choices beside their claim status; stacked on narrow hosts. */
.reward-door .reward-claim-layout { width: 100%; display: grid;grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 2vw; align-items: start; }
:root[data-layout='narrow'] .reward-door .reward-claim-layout { grid-template-columns: minmax(0, 1fr); gap: 2vh; }
.reward-door .reward-claim-status { display: grid; gap: .6rem; min-width: 0; }
.reward-door .reward-claim-list { list-style: none; margin: 0; padding: 0; display: grid; gap: .4rem; }
.reward-door .reward-claim-row { display: flex; justify-content: space-between; gap: 1rem; min-width: 0; }
.reward-door .reward-claim-row[data-state='taken'] .reward-claim-state { color: var(--gold); }
.reward-door .reward-claim-row:is([data-state='blocked'], [data-state='skipped']) .reward-claim-state { color: var(--muted); }
.reward-door .reward-claim-required { display: grid; gap: .2rem; margin: 0; }
/* PROGRESSION, above the claim status (Constantine 2026-09-20): what the fight
   moved, in the same right-hand column as what is left to take. One row is
   name · level · meter · the level it fills toward, with the fight's gain on a
   second line; narrow hosts drop the meter to its own line rather than
   squeezing four columns into a phone. */
.reward-door .reward-side { display: grid; gap: 1.2rem; min-width: 0; align-content: start; }
.reward-door .reward-progress { display: grid; gap: .5rem; min-width: 0; grid-template-columns: minmax(0, auto) auto minmax(3.5rem, 1fr) auto; }
.reward-door .reward-progress > * { grid-column: 1 / -1; }
.reward-door .reward-progress-list { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; }
/* `row-gap: 0` and a margin on the gain line, not a gap: the gain row is
   declared for every row but filled only where the fight paid, and a gap
   would make a row with no gain a hair taller than one with. */
.reward-door .reward-progress-row {
  display: grid; min-width: 0; gap: 0 .6rem; align-items: center;
  grid-template-columns: minmax(0, auto) auto minmax(3.5rem, 1fr) auto;
  grid-template-areas: 'name level bar next' 'gain gain gain gain';
}
/* THE BARS MUST LINE UP, and a per-row grid cannot promise that: each row sized
   its own name column, so "Reaver mastery" pushed its meter right of "Blade"'s
   and the panel read as four unrelated measurements. Subgrid gives the list one
   set of columns and the rows borrow it; the per-row rule above stays as the
   fallback for a host without it. */
@supports (grid-template-columns: subgrid) {
  /* Both lists borrow the PANEL's columns, so the character level and the
     skill tracks below it are one ledger read down a single set of bars. */
  .reward-door .reward-progress-list { grid-template-columns: subgrid; }
  .reward-door .reward-progress-row { grid-column: 1 / -1; grid-template-columns: subgrid; }
}
/* The track labels are authored lowercase where they name a weight class
   ("heavy armour"); a line that starts a row is capitalised on sight, not by
   re-writing the content table every screen reads. */
.reward-door .rp-name::first-letter { text-transform: uppercase; }
.reward-door .rp-name { grid-area: name; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--parchment); }
.reward-door .rp-level { grid-area: level; white-space: nowrap; color: var(--gold); font-variant-numeric: tabular-nums; }
.reward-door .rp-next { grid-area: next; white-space: nowrap; color: var(--muted); font-variant-numeric: tabular-nums; }
.reward-door .rp-gain { grid-area: gain; margin-top: .15rem; color: var(--muted); font-size: .92em; font-variant-numeric: tabular-nums; }
.reward-door .rp-bar { grid-area: bar; min-width: 0; }
.reward-door .reward-level-ready { --level-ready-blue: #75bfff; }
.reward-door .rp-bar-ready { --meter-tone: var(--level-ready-blue); }
.reward-door .rp-bar-ready .m-track { background: color-mix(in srgb, var(--level-ready-blue) 18%, var(--bg)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--level-ready-blue) 48%, var(--line)); }
.reward-door .reward-level-up { grid-area: next; box-sizing: border-box; height: 1.6rem; min-height: 0; padding: 0 .5rem; white-space: nowrap; border-color: var(--level-ready-blue); background: color-mix(in srgb, var(--level-ready-blue) 28%, var(--panel)); color: #e9f6ff; font-size: .75rem; line-height: 1; font-weight: 700; }
.reward-door .reward-level-up:is(:hover, :focus-visible) { background: color-mix(in srgb, var(--level-ready-blue) 43%, var(--panel)); border-color: #a7d9ff; }
.reward-door .rp-layered-bar { position: relative; height: 1.6rem; overflow: hidden; border-radius: 999px; background: #2b2821; box-shadow: inset 0 0 0 1px var(--line); }
.reward-door .rp-layered-bar .rp-under, .reward-door .rp-layered-bar .rp-over { position: absolute; inset: 0 auto 0 0; border-radius: inherit; }
.reward-door .rp-layered-bar .rp-under { background: #62c686; z-index: 1; }
.reward-door .rp-layered-bar .rp-over { background: #d7b64b; z-index: 2; }
.reward-door .rp-layered-bar .rp-bar-text { position: absolute; inset: 0 .55rem 0 auto; z-index: 3; display: flex; align-items: center; color: #fff; font: 700 .75rem/1 var(--font-body); white-space: nowrap; text-shadow: 0 1px 2px #111; }
.reward-door .rp-layered-bar.rp-bar-ready .rp-under, .reward-door .rp-layered-bar.rp-bar-ready .rp-over { background: #6ab8f5; }
.reward-door .reward-progress-row[data-kind='character'] { grid-template-columns: auto minmax(5rem, 1fr) auto; grid-template-areas: 'level bar next'; gap: .6rem; white-space: nowrap; }
.reward-door .reward-progress-row[data-kind='character'] .rp-level { color: var(--parchment); }
.reward-door .reward-progress-row[data-kind='character'] .rp-next { position: relative; z-index: 1; transition: transform .35s ease; }
.reward-door .reward-progress-row[data-kind='character'].reward-level-ready .rp-next { transform: translateX(-2rem); }
.reward-door .reward-progress-row[data-kind='character'] .reward-level-up { position: relative; z-index: 2; grid-area: next; }
.reward-door .reward-stat-allocation { display: grid; gap: .5rem; width: 100%; border-top: 1px solid var(--line); padding-top: .8rem; }
.reward-door .reward-stat-allocation h3 { margin: 0; font-size: 1rem; }
.reward-door .reward-stat-row { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; gap: .6rem; }
.reward-door .reward-stat-add { min-width: 2.5rem; }
/* What the per-fight level cap threw away (SPEC §15.2): its own line under the row. */
.reward-door .rp-discarded { grid-column: 1 / -1; margin-top: .1rem; color: var(--ember, var(--gold)); font-size: .88em; font-variant-numeric: tabular-nums; }
.reward-door .reward-progress-more { margin: 0; color: var(--muted); font-size: .92em; }
:root[data-layout='narrow'] .reward-door .reward-progress { grid-template-columns: minmax(0, 1fr) auto; }
:root[data-layout='narrow'] .reward-door .reward-progress-row {
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: 'name level' 'bar bar' 'gain next';
}
:root[data-layout='narrow'] .reward-door .reward-progress-row.reward-level-ready { grid-template-areas: 'name level' 'bar next' 'gain gain'; }
:root[data-layout='narrow'] .reward-door .reward-progress-row[data-kind='character'] { grid-template-columns: auto minmax(3rem, 1fr) auto; grid-template-areas: 'level bar next'; font-size: .85rem; }
:root[data-layout='narrow'] .reward-door .rp-next { justify-self: end; }
.reward-door .reward-row { justify-content: center; flex-wrap: wrap; padding: 0.6rem 0; max-width: 100%; }
.reward-door .reward-row .reward-node { white-space: normal; }
.reward-door .reward-kind[data-state='taken'] .chip { color: var(--gold); border-color: var(--gold); }
/* The ring itself is the shared one above; the door adds only the lift. */
.reward-door .reward-row .card.reward-selected { transform: translateY(-8px) scale(1.06); }
.reward-door .reward-confirm:not(:disabled) {
  background: color-mix(in srgb, var(--green) 22%, var(--panel));
  border-color: var(--green);
  color: var(--parchment);
}
.reward-door .reward-confirm:not(:disabled):hover,
.reward-door .reward-confirm:not(:disabled):focus-visible {
  background: color-mix(in srgb, var(--green) 34%, var(--panel));
  border-color: var(--green);
}

/* ═══ PAGE DOOR — a door that stands on the page, edge to edge ═══════════ */
/* A PAGE DOOR wears the door's box without the veil (as the event does), in
   two rungs. The DEFAULT hugs its content and stands on the page ground — a
   result, a question, a notice. `full` takes the whole screen for a place you
   work in (creation, the Custom Climb): the phone gets every pixel, a wide
   screen a capped column with the ground either side. The rules for the
   default rung are in the PAGE DOORS section below; `full` overrides them. */
.screen.as-page { justify-content: flex-start; gap: 0; padding: 0; overflow: hidden; }
.as-page > .modal.as-pagedoor.full, .screen > .modal.as-pagedoor.full {
  width: 100%; max-width: 124rem; height: 100%; max-height: 100%; flex: 1 1 auto;
  border-radius: 0; border-width: 0 1px;
}
.as-pagedoor.full > .modal-body {
  display: flex; flex-direction: column; align-items: stretch; gap: 1.6rem;
  padding: var(--modal-inset);
  scrollbar-width: thin; scrollbar-color: var(--line) transparent;
}
.as-pagedoor.full > .modal-foot { background: var(--panel); }
/* A pane that sits inside another inset (a fold's panel) carries none of its own. */
.as-pane.flush { padding: 0; overflow: visible; }
/* A pane's items never shrink into each other: the pane scrolls instead. */
.as-pane > * { flex-shrink: 0; }
.as-pane.flush > .as-hairline { margin: 1rem 0; }

/* ═══ SPLIT — two panes and a draggable handle ══════════════════════════════ */
/* Body D's two panes with a handle between them: `--split-share` is the left
   pane's share, the handle is a real button on the tap floor (keys move it
   too), and under a narrow box the split stacks and the handle goes. */
.as-split {
  --split-share: 30%;
  display: grid; grid-template-columns: minmax(18rem, var(--split-share)) auto minmax(0, 1fr);
  gap: 1.05rem; align-items: stretch; width: 100%; min-width: 0;
}
.as-split > .as-split-pane { min-width: 0; display: flex; flex-direction: column; border: 1px solid var(--line); border-radius: var(--r-3); background: var(--panel); }
/* A pane inside a Split on the page is not a scroll owner — the page body is. */
.as-split > .as-split-pane > .as-pane { flex: 1 1 auto; overflow: visible; }
.as-split-handle {
  position: relative; justify-self: center; align-self: stretch;
  width: var(--tap-floor); min-width: var(--tap-floor); min-height: 0; padding: 0;
  border: 0; border-radius: 0; background: transparent; cursor: col-resize;
}
.as-split-handle::before { content: ''; position: absolute; left: 50%; top: 0; bottom: 0; width: 1px; background: var(--line-soft); }
.as-split-handle::after {
  content: '‹ ›'; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  padding: 0.4rem 0.3rem; color: var(--muted); background: var(--panel);
  font: 500 1.2rem/1 var(--font-body); letter-spacing: 0.1em; white-space: nowrap;
}
.as-split-handle:hover, .as-split-handle:focus-visible { background: transparent; border: 0; box-shadow: none; }
.as-split-handle:hover::after, .as-split-handle:focus-visible::after { color: var(--gold); }
@container (max-width: 64rem) {
  .as-split { grid-template-columns: minmax(0, 1fr); }
  .as-split-handle { display: none; }
  /* THE DECISION COMES FIRST ON A PHONE. Stacked, the class step led with the
     preview pane — figure, resources, relic — and the list of classes, the
     one thing the step asks for, began below the fold at 390x844 (measured
     2026-09-11). Beside each other the order is preview | chooser and reads
     fine; stacked, the chooser goes first and the preview follows it. */
  .cc-class-split > .cc-class-preview-host { order: 2; }
}

/* ═══ OPTIONS — list or grid, one atom ══════════════════════════════════════ */
/* The same OptionCards, in a column (the default) or a grid: a `data-view` a
   Segmented control writes. A card in either never keeps the class-pick
   alias's row width — the container decides the footprint. */
.as-options > .as-option.class-pick { max-width: none; flex: 0 0 auto; }
.as-options[data-view="grid"] { display: grid; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); gap: 0.9rem; }
.as-options[data-view="grid"] > * { min-width: 0; width: 100%; }
.as-options[data-view="grid"] > .as-option { flex-direction: column; align-items: stretch; text-align: center; padding-right: 1.5rem; }
.as-options[data-view="grid"] > .as-option::after { content: none; }
.as-options[data-view="grid"] > .as-option .on { justify-content: center; }
.as-options > .as-option .on > .as-pill { margin-left: 0.3rem; }

/* SWATCH lives once, below — B's square-pick block was folded into it at the
   merge (one home per fact). `.tint` is a no-op class kept as a hook. */

/* ═══ FIGURE WELL — an ArtWell tall enough for a standing sprite ═══════════ */
.as-artwell.figure { width: 16rem; height: 20rem; font-size: 6.4rem; border-width: 2px; transition: border-color 200ms, box-shadow 300ms; }
/* The figure well explicitly keeps the painting as authored. This also guards
   portrait-like figure surfaces if combat-facing rules are introduced later. */
.as-artwell.figure .class-sprite > .facing { transform: none; }

/* ═══ STEPPER — a Segmented of − / value / + ════════════════════════════════ */
.as-seg.stepper button { min-width: var(--tap-floor); min-height: calc(var(--tap-floor) - 2px); padding-inline: 0.9rem; font-size: 1.6rem; }
.as-seg.stepper .seg-value {
  display: inline-grid; place-items: center; min-width: 3.6rem; padding-inline: 0.6rem;
  border-left: 1px solid var(--line-soft); border-right: 1px solid var(--line-soft);
  font: 600 1.5rem/1 var(--font-body); font-variant-numeric: tabular-nums; color: var(--parchment);
}
.as-seg button[aria-disabled="true"] { color: var(--muted); cursor: default; background: transparent; }
.as-seg button[aria-disabled="true"]:hover { background: transparent; color: var(--muted); }

/* ═══ A FOLD INSIDE A ROW ═══════════════════════════════════════════════════ */
/* A disclosure host in a settings row dissolves: its face and its reveal
   become the row's own wrap items, so the reveal drops under the whole row
   (trail included) with the row's own gap — the D26 mechanism, unchanged. */
.as-row.setting > .as-row-fold, .as-row.setting > .as-row-fold > .disc-faces,
.as-row.setting > .as-row-fold > .disc-fold-row { display: contents; }
.as-row.setting > .as-row-fold .disc-face { flex: 1 1 16rem; min-width: 0; }
.as-row-fold > .disc-faces > .disc-face { flex: 1 1 100%; }
.as-row-fold > .disc-fold-row > .disc-face { width: 100%; }
/* An open reveal drops UNDER the whole row: the face and the trail keep their line. */
.as-row.setting > .as-row-fold .disc-reveal { order: 2; }
.as-row.setting:has(> .as-row-fold) { align-items: flex-start; }

/* Allocation uses the same connected primary-stat fold, with the point
   controls inside its header. The details element keeps its own layout box. */
.as-row.setting.se-row { display: block; padding: 0; border: 0; }
.as-row.setting.se-row > .as-row-fold,
.as-row.setting.se-row > .as-row-fold > .disc-fold-row { display: block; width: 100%; }
.as-row.setting.se-row .disc-face { width: 100%; }
.as-row.setting.se-row .disc-face > .as-row { flex-wrap: nowrap; }
.as-row.setting.se-row .se-controls { flex: 0 0 auto; margin-left: auto; }
.as-row.setting.se-row .disc-summary { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.disc-face > .as-row { flex-wrap: nowrap; }
.disc-face > .as-row { padding: 0; min-height: 0; width: 100%; gap: 0.75rem; }
.disc-face > .as-row > .as-labelstack { flex: 1 1 auto; }
.disc-face > .as-row .ls-label { color: var(--gold); font: 600 1.5rem/1.2 var(--font-display); letter-spacing: 0.03em; }
.disc-face > .as-row .ls-hint { font-family: var(--font-body); letter-spacing: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.disc-face > .as-row > .as-status { font: 600 1.6rem/1 var(--font-body); font-variant-numeric: tabular-nums; color: var(--parchment); flex: 0 0 auto; }
.disc-face > .as-row.face-lite .ls-label { color: var(--parchment); font: 500 1.3rem/1.2 var(--font-display); letter-spacing: 0.06em; text-transform: uppercase; }
.disc-face > .as-row.face-lite > .as-status { color: var(--muted); font: 500 1.28rem/1 var(--font-body); }

/* ═══ FIELD NOTE — the sentence under a field that refuses ═════════════════ */
.as-fieldnote { flex: 1 1 100%; margin: 0; padding-left: 0.8rem; border-left: 2px solid var(--danger); color: var(--parchment); font-size: 1.2rem; line-height: 1.4; }
.as-row.setting input[aria-invalid="true"] { border-color: var(--danger); }
/* The same note under a forward control that refuses (refusal.js reasonNote):
   the reason a Next / Continue / Confirm is off, as text, never only a tooltip.
   Its own floor keeps it readable wherever a screen shrinks its type. */
.as-reasonnote { display: block; flex: 1 1 100%; width: 100%; box-sizing: border-box; margin: 0.4rem 0 0; font-size: max(11px, 1.2rem); text-align: start; }
.as-reasonnote[hidden] { display: none; }
/* A hidden row takes its reason line with it (the line is the row's next sibling). */
[hidden] + .as-reasonnote, [style*="display: none"] + .as-reasonnote { display: none; }
/* In a foot (a flex row), the shown line takes its own row under the buttons,
   on the primary's side. */
.modal-foot:has(> .as-reasonnote:not([hidden])), .as-foot:has(> .as-reasonnote:not([hidden])) { flex-wrap: wrap; }
.modal-foot > .as-reasonnote, .as-foot > .as-reasonnote { order: 2; margin: 0; padding: 0 0.8rem 0 0; border-left: 0; border-right: 2px solid var(--danger); text-align: end; }

/* ═══ STACK — a column of kit pieces with one gap ══════════════════════════ */
.as-stack { display: flex; flex-direction: column; gap: 1.2rem; min-width: 0; width: 100%; }
.as-stack.tight { gap: 0.6rem; }
.as-pane > .as-statstrip { margin-top: 0.9rem; }
.as-pane > .as-btnrow.end { margin-top: 1.2rem; }
.as-pane-head { flex-wrap: wrap; row-gap: 0.6rem; }
.as-pane-head > .set-section-head { min-width: 0; }
.as-pane-head > .r-trail > .as-row.setting { width: auto; border-bottom: 0; padding-block: 0; min-height: 0; }
.as-pane > .as-artwell, .as-stack > .as-artwell { align-self: center; }
.as-pane-head > .r-trail { flex: 0 0 auto; display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; justify-content: flex-end; }
/* ═══ OPTION GRID / OPTION FLOW — OptionCards in columns, or wrapping ══════ */
/* `.as-options` is the column. `grid` is as many columns as fit (`--opt-min`
   the floor); `flow` is a wrapping row of self-sized cards where `wide` takes
   its line; `tight` closes the gap for a dense list. */
.as-options.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--opt-min, 22rem)), 1fr)); gap: 1.05rem; }
.as-options.grid > .as-option { width: auto; }
.as-options.flow { flex-direction: row; flex-wrap: wrap; align-items: stretch; gap: 0.6rem; }
.as-options.flow > .as-option { width: auto; flex: 1 1 auto; }
.as-options.flow > .as-option.wide { flex: 1 1 100%; }
.as-options.tight { gap: 0.45rem; }
/* An option may carry an ArtWell where its glyph would be, or a compact inset. */
.as-option > .as-artwell { flex: 0 0 auto; }
.as-option.compact { padding: 0.75rem 1.2rem; gap: 1rem; }
.as-option.compact > .as-artwell.sm { width: 4rem; height: 4rem; font-size: 1.8rem; }
.as-option.compact .on { font-size: 1.45rem; }
.as-option.more { border-style: dashed; }
.as-option.more .on { color: var(--muted); }
.as-option.drop-ready { border-color: var(--green); box-shadow: inset 0 0 0 2px var(--green); }
.as-option .on > .as-pill, .as-option .on > .as-tag { flex: 0 0 auto; }
/* The name is a flex item inside `.on`, so the ellipsis lives on it. */
.as-option .on > span:not(.as-pill):not(.as-tag), .as-option .on > strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.as-option .r-trail > .as-status { text-align: right; }
.as-option .r-trail > .as-btn { min-height: var(--tap-floor); }
.as-option .ob > .as-kitline, .as-option .ob > .as-statstrip { margin-top: 0.3rem; }
/* Prose in a card breaks a word before it breaks the card. */
.as-option .od, .as-option .om, .as-face .od, .as-face .om { overflow-wrap: anywhere; }
.as-option .ob > .tags { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.2rem; }
/* ═══ FACE HOST — an option whose whole content is one face element ════════ */
/* The host stays bare so its one child owns the padding and can paint the
   whole card (a hold fill, a drag ghost). Both supported children — Face and
   Row — use the same balanced inset; a hosted Row must not dissolve all the
   way to the border. */
.as-option.hosts-face { padding: 0; }
.as-face { display: flex; align-items: center; gap: 1.5rem; width: 100%; min-width: 0; min-height: var(--tap-floor); padding: 1.28rem 1.5rem; border-radius: inherit; text-align: left; }
.as-option.hosts-face > .as-row { width: 100%; min-height: var(--tap-floor); padding: 1.28rem 1.5rem; }
.as-option.compact.hosts-face > .as-face { padding: 0.75rem 1.2rem; gap: 1rem; }
.as-option.compact.hosts-face > .as-row { min-height: 5rem; padding: 0.75rem 1.2rem; gap: 1rem; }
/* Primary stats keep this compact face in creation, allocation, and Armoury.
   Two text lines are reserved even when a short summary happens to fit on one,
   so a narrow INT or CON summary cannot make one card taller than its peers. */
.as-option.cc-primary-stat.hosts-face > .as-row { min-height: 7rem; padding: 0.75rem 1.2rem; gap: 1rem; }
.as-face > .ob { min-width: 0; flex: 1 1 auto; display: flex; flex-direction: column; gap: 0.2rem; }
.as-face > .ob { flex: 1 1 8rem; }
.as-face > .r-trail { flex: 0 1 auto; margin-left: auto; display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: 0.5rem; max-width: 50%; }
.as-face > .r-trail > .as-status { flex: 0 0 auto; }
/* ═══ STAT ROW — body E's row, with its builder's parts ════════════════════ */
.as-statrow .sr-hint { color: var(--muted); font-size: 1.02rem; letter-spacing: 0.08em; text-transform: uppercase; }
.as-statrow .sr-vals > .as-statpair, .as-statrow .sr-vals > .as-delta { white-space: nowrap; }
.as-statrow .sr-vals .as-delta { font-size: 1.32rem; }
.as-statrow.flat { border: 0; border-radius: 0; background: transparent; padding: 0.6rem 0; border-top: 1px solid var(--rule); }
.as-statrow.flat + .as-statrow.flat { margin-top: 0; }
.as-statrow.flat:first-child { border-top: 0; }
.as-statrow .sr-name > .as-pill, .as-statrow .sr-name > .as-tag { margin-left: 0.5rem; vertical-align: middle; }
.as-statrow.muted { opacity: 0.55; }
/* ═══ DETAIL CARD — what may stand inside one ═════════════════════════════ */
.as-detailcard > .as-prose { margin-top: 0.3rem; }
.as-detailcard > .as-kitline, .as-detailcard > .as-statstrip { margin-top: 0.4rem; }
.as-detailcard > .as-flavor { margin-top: 0.4rem; }
.as-detailcard > .as-hairline { margin: 0.6rem 0; }
.as-detailcard > .as-eyebrow { margin-top: 0.6rem; }
.as-detailcard > .as-statrow.flat:first-of-type { border-top: 0; }
.as-detailcard .dc-lines { margin: 0.3rem 0 0; padding-left: 1.65rem; color: var(--muted); font-size: 1.28rem; line-height: 1.45; }
.as-detailcard .dc-lines b { color: var(--parchment); }
.as-detailcard > .tags { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.4rem; }
.as-detailcard.wide { width: 100%; }
/* A FOLD: a DetailCard whose head is a Row that opens it. */
details.as-detailcard { padding: 0; gap: 0; }
details.as-detailcard > summary { list-style: none; cursor: pointer; border-radius: inherit; }
details.as-detailcard > summary::-webkit-details-marker { display: none; }
details.as-detailcard > summary > .as-row { padding-inline: 1.5rem; }
details.as-detailcard > summary > .as-row > .as-title-s { font-size: 1.35rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 0 0 auto; max-width: 100%; }
details.as-detailcard > summary > .as-row > .as-status { flex: 1 1 0; min-width: 2rem; }
details.as-detailcard > :not(summary) { padding: 0 1.5rem 1.28rem; }
details.as-detailcard[open] > summary { border-bottom: 1px solid var(--line); margin-bottom: 0.9rem; }
.as-glyph.caret { display: inline-block; transition: transform 140ms; }
details[open] > summary .as-glyph.caret { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) { .as-glyph.caret { transition: none; } }
/* Armoury information folds grow with their labels and summaries. */
.armoury-character-stats > details.character-info-card { flex: 0 0 auto; width: 100%; min-width: 0; }
details.character-info-card > summary > .character-info-head {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  height: auto; min-height: var(--tap-floor); gap: 0.3rem 0.75rem;
  padding-block: 0.7rem; white-space: normal;
}
details.character-info-card > summary > .character-info-head > .character-info-label {
  grid-column: 2; min-width: 0; max-width: 100%; font-size: max(1.35rem, calc(15px / var(--ui-zoom, 1)));
  white-space: normal; overflow-wrap: anywhere; overflow: visible;
}
details.character-info-card > summary > .character-info-head > .as-status {
  grid-column: 2; min-width: 0; margin-left: 0; text-align: left;
  font-size: max(1.2rem, calc(15px / var(--ui-zoom, 1))); line-height: 1.45;
  white-space: normal; overflow-wrap: anywhere; overflow: visible;
}
details.character-info-card > summary:focus-visible { outline: var(--focus-ring); outline-offset: -2px; }
/* THE CLIMB. The level bar closes the character summary; the skill track bars
   stack inside the Skill progression fold. Both are the kit's stacked Meter,
   so nothing here draws a bar — it only places them. */
.character-summary > .character-level-meter { margin-top: 0.55rem; }
.character-level-meter > .m-plate, .character-skill-meter > .m-plate { flex-wrap: wrap; row-gap: 0.15rem; }
.character-level-meter .character-level-points { flex: 0 0 auto; margin-left: 0.6rem; color: var(--gold); }
.character-level-meter[data-capped="true"] .m-value { color: var(--gold); }
/* A point waiting is a thing to go and do, so the bar says so in the accent. */
.character-level-meter.has-points .m-label { color: var(--gold); }
.character-skills > .as-meters.character-skill-meters { display: flex; flex-direction: column; gap: 0.7rem; }
.character-skill-meter.own-class .m-label { color: var(--parchment); }
.character-skill-meter .character-skill-drafts { flex: 0 0 auto; margin-left: 0.6rem; }
/* ═══ STEPPER — a −/count/+ unit of two tap-floor buttons ═════════════════ */
.as-stepper { display: inline-flex; align-items: center; gap: 0.75rem; flex: 0 0 auto; }
.as-stepper > .as-btn { width: var(--tap-floor); min-width: var(--tap-floor); padding: 0; font-size: 1.8rem; line-height: 1; }
.as-stepper > .as-btn[aria-disabled="true"] { color: var(--muted); border-color: var(--line); cursor: default; }
.as-stepper > .as-btn[aria-disabled="true"]:hover { border-color: var(--line); }
.as-stepper > .st-value { min-width: 2ch; text-align: center; font: 600 1.6rem/1 var(--font-body); font-variant-numeric: tabular-nums; color: var(--parchment); }
/* ═══ SPLITTER — a draggable separator between two panes ══════════════════ */
.as-splitter { display: none; position: relative; min-width: 0.7rem; min-height: 0.7rem; cursor: col-resize; touch-action: none; }
.as-splitter[aria-orientation="horizontal"] { cursor: row-resize; }
.as-splitter::before { content: ''; position: absolute; inset: 0 0.28rem; background: var(--line-soft); border-radius: var(--r-pill); }
.as-splitter[aria-orientation="horizontal"]::before { inset: 0.22rem 0; }
.as-splitter:hover::before, .as-splitter:focus-visible::before, .as-splitter.dragging::before { background: var(--gold); box-shadow: 0 0 0.5rem rgba(var(--accent-rgb), 0.55); }
/* ═══ FOLDING TRAY — a docked region that folds to its header ═════════════ */
/* The header is a Row (caret Glyph + Title·S + StatusText count) beside an
   IconButton; the body is a Pane. Folded, only the header stands; unfolded,
   the tray HUGS ITS CONTENT up to a compact cap (`--tray-compact`) until a
   hand resizes it, after which its height is the remembered share of its
   host — a percentage of the panel, never of the glass (no vh below body).
   `.folding-tray` / `.tray-*` / `.region-fold` / `.rf-*` / `[data-fold]` /
   `[data-collapsed]` / `[data-sized]` / `[data-tray-edge]` / `[data-role]`
   are the hooks the tools read; they draw nothing of their own. */
.as-tray { --tray-compact: none; position: relative; display: flex; flex-direction: column; min-width: 0; min-height: 0; border-top: 1px solid var(--line); background: var(--bg-raise); }
.as-tray > .tray-header { display: flex; align-items: center; gap: 0.75rem; min-width: 0; flex: 0 0 auto; padding-right: var(--modal-inset, 1.4rem); }
.as-tray > .tray-header > .as-row { flex: 1 1 auto; min-width: 0; padding-inline: var(--modal-inset, 1.4rem); }
.as-tray > .tray-header > .as-row > .as-glyph { color: var(--gold); }
.as-tray > .tray-header > .as-row > .as-title-s { flex: 0 0 auto; max-width: 60%; font-size: 1.35rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.as-tray > .tray-header > .as-row > .as-status { flex: 1 1 auto; }
.as-tray > .tray-header > .as-iconbtn { flex: 0 0 auto; }
.as-tray > .as-pane { flex: 0 1 auto; min-height: 0; overflow: auto; padding-top: 0.9rem; }
.as-tray > .as-pane[hidden] { display: none !important; }
.as-tray[data-collapsed="1"] { flex: 0 0 auto; }
.as-tray[data-collapsed="1"] > :not(.tray-header) { display: none; }
.as-tray[data-collapsed="0"] > .tray-header { border-bottom: 1px solid var(--line); }
.as-tray[data-tray-edge="top"][data-collapsed="0"][data-sized="0"], .as-tray[data-tray-edge="bottom"][data-collapsed="0"][data-sized="0"] { max-height: var(--tray-compact); }
.as-tray[data-tray-edge="top"][data-collapsed="0"], .as-tray[data-tray-edge="bottom"][data-collapsed="0"] { min-height: var(--tap-floor); }
.as-tray[data-sized="1"] > .as-pane { flex: 1 1 auto; }
/* Side trays: a folded rail the height of its host, the title turned. */
.as-tray[data-tray-edge="left"], .as-tray[data-tray-edge="right"] { border-top: 0; }
.as-tray[data-tray-edge="left"][data-collapsed="1"], .as-tray[data-tray-edge="right"][data-collapsed="1"] { align-self: stretch; flex: 0 0 var(--tap-floor); width: var(--tap-floor); }
.as-tray[data-tray-edge="left"][data-collapsed="1"] > .tray-header, .as-tray[data-tray-edge="right"][data-collapsed="1"] > .tray-header { flex-direction: column; height: 100%; padding: 0; }
.as-tray[data-tray-edge="left"][data-collapsed="1"] > .tray-header > .as-row, .as-tray[data-tray-edge="right"][data-collapsed="1"] > .tray-header > .as-row { flex-direction: column; justify-content: flex-start; width: var(--tap-floor); height: 100%; padding: 0.6rem 0; }
.as-tray[data-tray-edge="left"][data-collapsed="1"] .tray-title, .as-tray[data-tray-edge="right"][data-collapsed="1"] .tray-title,
.as-tray[data-tray-edge="left"][data-collapsed="1"] .tray-count, .as-tray[data-tray-edge="right"][data-collapsed="1"] .tray-count { writing-mode: vertical-rl; text-orientation: mixed; margin: 0; text-align: left; flex: 0 0 auto; }
.as-tray[data-tray-edge="left"] { margin-block: 0.6rem; margin-left: 0.6rem; }
.as-tray[data-tray-edge="right"] { margin-block: 0.6rem; margin-right: 0.6rem; }
.as-tray[data-tray-edge="left"][data-collapsed="0"], .as-tray[data-tray-edge="right"][data-collapsed="0"] { align-self: stretch; width: calc(100% - 0.6rem); height: auto; }
/* The resize grip: a tap-floor surface astride the tray's free edge. */
.as-tray > .tray-resize-handle { position: absolute; z-index: 2; touch-action: none; user-select: none; outline: 0; }
.as-tray > .tray-resize-handle::after { content: ''; position: absolute; border-radius: var(--r-pill); background: color-mix(in srgb, var(--gold) 58%, transparent); transition: background-color 120ms ease, transform 120ms ease; }
.as-tray > .tray-resize-handle[data-ui-variant="top"], .as-tray > .tray-resize-handle[data-ui-variant="bottom"] { left: 50%; width: 9.6rem; height: var(--tap-floor); transform: translateX(-50%); cursor: ns-resize; }
.as-tray > .tray-resize-handle[data-ui-variant="top"] { bottom: 0; transform: translate(-50%, 50%); }
.as-tray > .tray-resize-handle[data-ui-variant="bottom"] { top: 0; transform: translate(-50%, -50%); }
.as-tray > .tray-resize-handle[data-ui-variant="top"]::after, .as-tray > .tray-resize-handle[data-ui-variant="bottom"]::after { width: 4.8rem; height: 2px; left: 50%; top: 50%; transform: translate(-50%, -50%); }
.as-tray > .tray-resize-handle[data-ui-variant="left"], .as-tray > .tray-resize-handle[data-ui-variant="right"] { top: 50%; width: var(--tap-floor); height: 9.6rem; transform: translateY(-50%); cursor: ew-resize; }
.as-tray > .tray-resize-handle[data-ui-variant="left"] { right: 0; }
.as-tray > .tray-resize-handle[data-ui-variant="right"] { left: 0; }
.as-tray > .tray-resize-handle[data-ui-variant="left"]::after, .as-tray > .tray-resize-handle[data-ui-variant="right"]::after { width: 2px; height: 4.8rem; left: 50%; top: 50%; transform: translate(-50%, -50%); }
.as-tray > .tray-resize-handle:is(:hover, :focus-visible)::after, .as-tray > .tray-resize-handle[data-dragging="1"]::after { background: var(--gold); }
/* ═══ DISCLOSURE — faces are OptionCards, the reveal is a DetailCard ═══════ */
/* components/disclosure.js draws every fold in the tree: a row of faces
   (`.as-options.flow`, or a column) and ONE reveal, a full-width DetailCard
   moved under the tapped face's line. The `.disc-*` names are the hooks the
   tools read; the shapes are the kit's. */
.disc-faces > .disc-reveal { flex: 1 1 100%; width: 100%; grid-column: 1 / -1; }
.disc-faces > .disc-face .disc-value { white-space: nowrap; }
.disc-faces > .disc-face .disc-summary { white-space: normal; }
.disc-faces > .disc-face[data-reveal="open"] { border-color: var(--gold); }
/* A self-contained fold keeps its face and reveal as siblings inside one
   semantic card, matching the Armoury's card-row grammar. */
details.disc-fold-row { min-width: 0; }
details.disc-fold-row > summary { list-style: none; }
details.disc-fold-row > summary::-webkit-details-marker { display: none; }
details.disc-fold-row[open] > summary { border-radius: var(--r-3) var(--r-3) 0 0; border-color: var(--gold); }
details.disc-fold-row > .disc-reveal { border-radius: 0 0 var(--r-3) var(--r-3); border-top: 0; }

/* Creation uses the same attached OptionCard / DetailCard fold as the Armoury.
   Physical text floors preserve readability when the game canvas scales down. */
.customize .cz-disc { display: flex; flex-direction: column; gap: 8px; }
.customize .disc-fold-row > summary { min-height: calc(44px / var(--ui-zoom, 1)); }
.customize .disc-fold-row > summary::after { content: '›'; position: static; flex: 0 0 auto; margin-left: 8px; }
.customize .disc-fold-row[open] > summary::after { transform: rotate(90deg); }
.customize :is(.disc-name, .on, .dc-name, .ls-label, .as-title-s, input, button) {
  font-size: max(1.4rem, calc(16px / var(--ui-zoom, 1))); line-height: 1.4;
}
.customize :is(.od, .ls-hint, .as-status, .disc-value, .as-flavor, .as-prose, .dc-meta, .dc-lines, .equip-role-receipts, .cc-equip-group) {
  font-size: max(1.2rem, calc(14px / var(--ui-zoom, 1))); line-height: 1.5;
}
.customize .cc-equip-group :is(small, span, p, b, i) { font-size: inherit; }
.customize .cc-equip-group .as-statrow { flex-wrap: wrap; }
.customize .cc-equip-group :is(.sr-name, .as-statpair) { white-space: normal; }
.customize .cc-equip-group .sr-vals { flex: 1 1 100%; text-align: left; justify-content: flex-start; }
.customize .cc-equip-group .sp-k { letter-spacing: normal; text-transform: none; }
.rating-calculations { display:grid; gap:10px; margin-top:8px; }
.rating-calculation { display:grid; gap:5px; padding:9px 0 0; border-top:1px solid color-mix(in srgb,var(--gold,#c9a227) 32%,transparent); min-width:0; }
.rating-calculation-head { display:flex; align-items:baseline; justify-content:space-between; gap:12px; color:var(--gold,#c9a227); letter-spacing:.12em; }
.rating-calculation-head strong { color:var(--ink,#f6eed8); font-size:1.08em; letter-spacing:normal; }
.rating-attributes,.rating-formula { color:var(--ink,#f6eed8); line-height:1.5; overflow-wrap:anywhere; }
.rating-line-label { display:block; color:var(--muted,#b9aa8e); font-size:.78em; letter-spacing:.12em; text-transform:uppercase; }
.rating-separator { color:var(--muted,#b9aa8e); }
.rating-term,.rating-source { white-space:normal; }
.customize .as-pane-head { flex-wrap: wrap; }
.customize .as-pane-head > .r-trail { display: flex; flex: 1 1 100%; flex-wrap: wrap; gap: 8px; justify-content: flex-start; }
.customize .disc-face :is(.ob, .r-trail) { min-width: 0; }
.customize .disc-face :is(.disc-name, .on, .ls-label) { white-space: normal; overflow: visible; text-overflow: clip; }
.customize .disc-face .r-trail { flex-shrink: 1; overflow-wrap: anywhere; }
.customize .cc-primary-stat .disc-summary { white-space: normal; }
.customize .cc-preview-side > .cz-portrait { position: relative; overflow: hidden; width: 100%; height: auto; min-height: 24rem; border: 0; }
@media (max-width: 900px) {
  .customize .cc-character-grid { grid-template-columns: minmax(0, 1fr); }
  .customize .cc-preview-side { min-height: 0; }
  .customize .cc-preview-side > .cz-portrait { flex: none; height: 24rem; }
}
/* ASSIGN POINTS IS NARROWER THAN THE COLUMN THE RULE ABOVE WAS WRITTEN FOR.
   `white-space: normal` lets a summary read in full where there is room, which
   is right in Character Creation. Inside the Assign Points overlay there is not
   room: Intelligence's hint — the longest of the five — wraps to a second line,
   which takes its inner row from 29.41px to 43.44px and its card from the 44px
   tap floor the other four sit exactly on to 45.44px. Five cards, four at 44
   and one at 45.44, is no longer the one compact family this surface promises,
   and tools/uniform-stat-foldouts.mjs has been red on dev for it since #647.
   Measured in headless Chromium at 390x844.

   The compact face row already ellipsises by default (`.disc-face > .as-row
   .ls-hint`, above); this restores that default on the one surface too narrow
   to honour the wrap, which is what `.armoury-character-stats` does for the
   same content. Scoped to the overlay on purpose — Character Creation is wide
   enough to wrap without overflowing, and keeps the full sentence. */
.cc-stat-overlay .disc-faces > .disc-face .disc-summary {
  min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* A FULL-WIDTH FACE IS A LINE OF ITS OWN, AND THE `.disc-faces >` IS LOAD-BEARING.
   Without it this reads `.disc-face.disc-pick` — two classes — and loses on
   specificity to `.as-options.flow > .as-option { flex: 1 1 auto }` three rules
   up, which is three. Measured at d51b46a, headless Chromium 1200x730: all four
   creation sections (CLASS / CHARACTER / STARTING EQUIP / SEED) computed
   `flex: 1 1 auto` and shared ONE line at top=77, so the one reveal — a
   `flex: 1 1 100%` row of the same wrap — could only ever land after the whole
   row. That is Constantine's "instead it just shows up at hte bottom", and it
   is the SAME defect MR-287 fixed in disclosure.js, re-opened by a stylesheet:
   the placement code was right and had nothing to place between. Scoped to the
   child combinator the count is three either way and the later rule — this one
   — wins, on flow hosts and column hosts alike. */
.disc-faces > .disc-face.disc-pick, .disc-faces > .disc-face.disc-armament, .disc-faces > .disc-face.disc-relic, .disc-faces > .disc-face.disc-item, .disc-faces > .disc-face.disc-power, .disc-faces > .disc-face.disc-stat, .disc-faces > .disc-face.disc-attribute, .disc-faces > .disc-face.disc-section { flex: 1 1 100%; }
.disc-reveal > .dc-name { font-size: 1.5rem; }
.disc-reveal .disc-receipt { display: block; margin-top: 0.4rem; }
/* ═══ SMITH — W1i upgrade, W1j extract, W1k install ════════════════════════ */
/* Three W1 workspaces with no categories (components/w1Workspace.js frames
   them). The item list is the left column — the kit rail, one row per item;
   on compact hosts one selector above the pane — and the pane is the selected
   item: a DetailCard whose facts scroll and whose Stone cost stays pinned at
   its foot, then the consequence line. `.smith-*` are the hooks
   tools/armament-smithing-ui.mjs reads. */
.smith-candidate-region > .as-rail { gap: 0.5rem; }
.as-railitem.smith-candidate-card {
  position: relative; display: flex; align-items: center; gap: 0.8rem;
  padding: 0.45rem 3.6rem 0.45rem 0.6rem; white-space: normal;
}
.smith-candidate-card > .as-artwell.sm { flex: 0 0 auto; width: 3.4rem; height: 3.4rem; font-size: 1.6rem; overflow: hidden; }
.smith-candidate-card > .as-artwell > img, .smith-selected-head > .as-artwell > img { width: 100%; height: 100%; object-fit: contain; }
.smith-row-text { display: flex; flex-direction: column; gap: 0.15rem; flex: 1 1 auto; min-width: 0; }
.smith-row-text > .smith-weapon-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--parchment); font-weight: 600; }
.smith-row-text > .as-status { text-align: left; }
.smith-candidate-card > .smith-weapon-count { flex: 0 0 auto; }
.smith-candidate-card.card-inspection-target > .card-info-button { top: 50%; right: 8px; left: auto; transform: translateY(-50%); }
.smith-item-type-row, .smith-weapon-tags { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.smith-item-type-row > .as-tag { color: var(--gold); }
.smith-preview-region.as-pane { gap: 0.6rem; min-height: 0; overflow: hidden; background: var(--panel); }
.smith-pane-status { flex: 0 0 auto; text-align: left; }
.smith-preview-host { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.smith-consequence { flex: 0 0 auto; margin: 0; }
.mount-service-outcome { display: flex; gap: 1.2rem; align-items: flex-start; margin: 0.4rem 0 0.8rem; }
/* The pane's card scrolls its facts; its padding-bottom is the cost strip's,
   so the pinned strip sits flush on the card's foot. */
.smith-preview-card { flex: 1 1 auto; min-height: 0; width: 100%; gap: 0; overflow: auto; overscroll-behavior: contain; padding-bottom: 0; }
/* The summary: the selected item — its art beside its name, tier, kind and
   tags — one column, the rows flush so the stats row follows. */
.smith-summary-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0.5rem; padding-bottom: 0.6rem; }
.smith-summary-cell { min-width: 0; }
.smith-selected-head { display: grid; grid-template-columns: auto minmax(0, 1fr); column-gap: 1rem; row-gap: 0.2rem; align-items: start; }
.smith-selected-head > .as-artwell { grid-row: 1 / span 5; width: 6rem; height: 6rem; overflow: hidden; }
.smith-selected-head > :not(.as-artwell) { grid-column: 2; min-width: 0; }
/* W1i/W1j/W1k "Cost / available stones": the last slot, pinned under the facts. */
.smith-cost-strip {
  position: sticky; bottom: 0; z-index: 1; display: flex; flex-direction: column; gap: 0.5rem;
  margin-top: 0.9rem; padding: 0.8rem 0 1.28rem; border-top: 1px solid var(--line); background: var(--panel);
}
.smith-cost-strip > .as-blocker.pinned { margin: 0; }
.smith-selected-head > .sr-name { font: 600 1.47rem/1.2 var(--font-body); color: var(--parchment); }
.smith-preview-economy { display: flex; align-items: baseline; }
.smith-economy-values { display: inline-flex; align-items: baseline; flex-wrap: wrap; gap: 0 0.45rem; font: 600 1.47rem/1.2 var(--font-body); color: var(--parchment); }
.smith-economy-values > b { white-space: nowrap; font-weight: 600; }
.smith-cost-pair > .sp-k { font-size: 1rem; letter-spacing: 0.18em; }
.smith-cost-pair > .sp-k > i, .smith-cost-pair > .sp-v > i { font-style: normal; color: var(--muted); padding-inline: 0.1em; }
.smith-preview-economy.affordable .smith-cost-available, .confirmation-cost.affordable .smith-cost-available { color: var(--green); }
.smith-preview-economy.unaffordable .smith-cost-available, .confirmation-cost.unaffordable .smith-cost-available { color: var(--danger, var(--ember)); }
.smith-preview-card .smith-fold-values { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 0.3rem 1rem; }
.smith-preview-card .smith-data-heading { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.smith-preview-card .sr-vals { flex-direction: column; align-items: flex-end; gap: 0.3rem; }
/* A fact row that runs out of room stacks its values under its name. */
.smith-preview-card > .as-statrow.flat { flex-wrap: wrap; }
.smith-preview-card > .as-statrow.flat > .sr-id { flex: 1 1 12rem; }
.smith-preview-card > .as-statrow.flat > .sr-vals { flex: 1 1 14rem; }
.smith-stat-costs > i { font-style: normal; }
.smith-requirement small { display: block; color: var(--muted); font-size: 1.05rem; text-align: right; }
.smith-preview-card > .as-blocker.pinned { margin-top: 1.2rem; }
.confirmation-change-list { display: flex; flex-direction: column; }
.smith-upgrade-folds { display: flex; flex-direction: column; gap: 0.6rem; margin-top: 0.6rem; }
.smith-upgrade-fold { border: 1px solid var(--line); border-radius: var(--r-2); background: var(--bg-raise); }
.smith-upgrade-fold > summary { list-style: none; cursor: pointer; }
.smith-upgrade-fold > summary::-webkit-details-marker { display: none; }
.smith-upgrade-fold > summary > .as-statrow { display: flex; border: 0; background: transparent; padding: 0.9rem 1.2rem; min-height: var(--tap-floor); }
.smith-upgrade-fold > summary > .as-statrow > .sr-vals { flex-direction: row; align-items: center; }
.smith-fold-detail { padding: 0.9rem 1.2rem; border-top: 1px solid var(--line); }
.smith-upgrade-fold.is-unused { opacity: 0.46; }
.smith-fold-detail { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 1.2rem; }
.smith-fold-detail.no-card { grid-template-columns: minmax(0, 1fr); }
/* The CostMedallion is the one element allowed to break a card's edge (kit
   §03), so the sprite box leaves it room instead of clipping it off. */
.smith-card-sprite { min-width: 0; padding: 1rem 0 0 1rem; }
.smith-card-sprite .card { transform: none !important; }
.smith-card-facts { display: flex; flex-direction: column; gap: 0.35rem; min-width: 0; }
.smith-preview-empty { margin: auto; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 0.6rem; color: var(--muted); }
.smith-confirm[data-smith-action-state="blocked"] { cursor: help; }
.smith-modal-footer .as-blocker.in-foot { margin-right: auto; }
/* ═══ SHRINE FOLDS — the flask stepper and the level preview ═══════════════ */
.shrine-fold-content > .as-subtitle { margin: 0; }
.flask-increment { display: flex; flex-direction: column; gap: 0; }
.flask-increment > .as-row.setting { padding-block: 0.6rem; }
.flask-increment > .as-row.setting > .as-labelstack { flex: 1 1 12rem; }
.flask-increment-id .flask-identity { display: inline-flex; align-items: center; gap: 0.6rem; }
.flask-increment > .as-status.flask-increment-total { display: block; margin-top: 0.6rem; text-align: center; }
.level-xp-preview { margin: 0; }
.level-xp-preview > .level-points-waiting { color: var(--ember); font-weight: 600; }
.level-points-result[hidden] { display: none; }

/* ═══ FOLDED TITLE — the gate is the TitleMenu before its entries ══════════ */
/* A separate surface from the title menu: the menu is not mounted behind it,
   so hidden controls cannot leak into Tab order or the accessibility tree.
   Copy and particle positions arrive through StartupGateModels.js; this block
   owns layout only — the lockup is the kit's TitleMenu, the stamp sits in the
   grid's last row, and the ash is the gate's ambience (the title's embers). */
.startup-gate {
  position: relative; isolation: isolate;
  display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) auto; justify-items: center; align-items: center;
  gap: 1.2rem; padding: 2.4rem; min-height: 100%; overflow: hidden; cursor: pointer;
  background:
    linear-gradient(180deg, rgba(11, 9, 6, 0.34), rgba(9, 7, 5, 0.96)),
    url('../assets/bg/title-city-tower.webp') center / cover;
  transition: opacity 180ms ease-out;
}
.startup-gate::after { content: ''; position: absolute; inset: 0; pointer-events: none; background: radial-gradient(ellipse at 50% 20%, transparent 0, rgba(0, 0, 0, 0.12) 42%, rgba(0, 0, 0, 0.66) 100%); }
.startup-gate:focus { outline: none; }
.startup-gate:focus-visible .startup-prompt { color: var(--gold); text-shadow: 0 0 1rem rgba(var(--accent-rgb), 0.48); }
.startup-gate.is-revealing { opacity: 0; pointer-events: none; }
.reduced-motion .startup-gate { transition-duration: 140ms; transition-timing-function: linear; }
.startup-gate > .as-titlemenu { position: relative; z-index: 1; align-self: center; animation: startupMarkIn 520ms ease-out both; }
.startup-gate > .as-titlemenu .tm-name {
  font-size: min(12cqi, 5.4rem);
  text-shadow: 0 0 3.2rem rgba(var(--accent-rgb), 0.38);
}
.startup-gate > .as-titlemenu .tm-sub { margin-top: 0.75rem; }
.startup-gate > .as-titlemenu > .as-ornament { margin-top: 1.5rem; }
.startup-gate > .as-titlemenu .tm-foot { margin-top: 3rem; color: var(--parchment); opacity: 0.82; white-space: normal; animation: startupPromptPulse 1.8s ease-in-out infinite alternate; }
.startup-gate > .build-stamp { position: relative; z-index: 1; align-self: end; margin-bottom: max(0.4rem, env(safe-area-inset-bottom)); color: var(--parchment); text-align: center; }
@keyframes startupMarkIn { from { opacity: 0; transform: translateY(0.65rem); } to { opacity: 1; transform: none; } }
@keyframes startupPromptPulse { from { opacity: 0.5; } to { opacity: 0.95; } }
.reduced-motion .startup-gate > .as-titlemenu, .reduced-motion .startup-gate .tm-foot { animation: none; }
/* The built-in art's status line (docs/EXTERNAL-ASSETS-PLAN.md step 5): its
   own component beside the startup gate in #app, not one of the gate's parts
   (SPEC §7.1), laid over the top edge of the gate, clear of its centred lockup
   and its build stamp, and taking no input. Only in a build that pins packs. It stays through the reveal while a
   late load settles; it pulses while loading, plain text with Reduced motion. */
#app > .boot-art-status {
  position: fixed; z-index: 2; left: 50%; translate: -50% 0;
  top: max(1rem, env(safe-area-inset-top));
  width: min(36rem, calc(100% - 2rem)); margin: 0; min-height: 1.4em;
  color: var(--parchment); text-align: center; font-size: 0.95rem; letter-spacing: 0.06em; opacity: 0.86;
  pointer-events: none;
}
#app > .boot-art-status[data-state="loading"] { animation: bootArtPulse 1.6s ease-in-out infinite alternate; }
#app > .boot-art-status[data-state="failed"] { color: var(--gold); opacity: 1; }
@keyframes bootArtPulse { from { opacity: 0.55; } to { opacity: 0.9; } }
.reduced-motion #app > .boot-art-status { animation: none; }
@media (prefers-reduced-motion: reduce) { #app > .boot-art-status { animation: none; } }
/* The title's notice when the art could not be loaded (step 5): a small panel
   at the top of the title, never a dialog; Retry is a plain control. */
.title-screen > .art-load-notice {
  position: absolute; z-index: 3; top: max(0.8rem, env(safe-area-inset-top)); left: 50%; translate: -50% 0;
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 0.6rem 1rem;
  width: min(36rem, calc(100% - 2 * (44px + 1.2rem))); padding: 0.7rem 1rem;
  border: 1px solid var(--line-soft); border-radius: var(--r-3);
  background: color-mix(in srgb, var(--panel) 90%, transparent); color: var(--parchment);
}
.title-screen > .art-load-notice > .art-load-notice-text { margin: 0; flex: 1 1 16rem; text-align: center; }
.title-screen > .art-load-notice > .art-load-notice-retry { min-height: 44px; min-width: 44px; }
.startup-ash-field { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
.startup-ash {
  position: absolute; left: var(--ash-left); bottom: -1rem;
  width: var(--ash-size); height: var(--ash-size); border-radius: var(--r-round);
  background: var(--ember); box-shadow: 0 0 0.55rem rgba(201, 80, 46, 0.55); opacity: 0;
  animation: startupAshRise var(--ash-duration) linear var(--ash-delay) infinite;
}
@keyframes startupAshRise {
  0% { transform: translate3d(0, 0, 0); opacity: 0; }
  14% { opacity: 0.62; }
  72% { opacity: 0.28; }
  100% { transform: translate3d(1.4rem, -78rem, 0); opacity: 0; }
}
@media (max-width: 640px) {
  .startup-gate { padding: 1.2rem 0.8rem; }
  .startup-gate > .as-titlemenu .tm-foot { margin-top: 2.4rem; }
}

/* METER lives once — see the HUD's METER section: a plate beside a well.
   C's overlay-text meter was folded into it at the merge (one home per fact);
   the co-op party strip reads as `inset`, which puts the plate in the track. */
/* ═══ PARTY STRIP — who is here, and how they are ══════════════════════════ */
/* One Member per seat: Glyph + inline Meter. State rides on `data-state`
   (me · away · dead); the kit says it with the border and the ink. */
.as-party { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem 0.9rem; min-width: 0; }
.as-member { display: inline-flex; align-items: center; gap: 0.45rem; padding: 0.25rem 0.6rem; background: var(--panel); border: 1px solid var(--line-soft); border-radius: var(--r-2); }
.as-member > .as-glyph { flex-basis: auto; font-size: 1.5rem; }
/* A member's Meter has a FLOOR: its inset value ("61/72") must read whole, so
   the track never squeezes below it and the strip wraps to a second line
   instead. Measured on the co-op map header at 1200x730, where two members'
   values overlapped by 16px. */
.as-member > .as-meter { flex: 0 0 auto; width: 8.4rem; min-width: 8.4rem; }
.as-member[data-state="me"] { border-color: var(--gold); }
.as-member[data-state="away"] { opacity: 0.55; }
.as-member[data-state="dead"] { opacity: 0.4; filter: grayscale(1); }
/* A StatStrip centred under a figure (a seat's line on the board). */
.as-statstrip.centered { justify-content: center; align-items: center; gap: 0.5rem 0.75rem; width: 13rem; max-width: 100%; margin-inline: auto; text-align: center; }
.as-statstrip.centered > span:first-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 1.15rem; }
/* A DetailCard that stands in a flow at its own width (an arming prompt). */
.as-detailcard.floating { width: fit-content; max-width: 100%; margin: 0.4rem auto; }
.as-detailcard.floating > .modal-btnrow { margin-top: 0.6rem; }
/* A scene door over the board: the shared card row and choices inside a decide body. */
.coop-scene > .as-party { flex: 0 0 auto; justify-content: center; }
.as-pagedoor .as-decide > .reward-row { justify-content: center; flex-wrap: wrap; margin: 1.2rem 0; max-width: 100%; }
.as-pagedoor .as-decide > .as-subtitle { margin-top: 0.6rem; }
.as-pagedoor .as-decide > .as-options { margin-top: 1.2rem; }
.as-pagedoor .as-decide > .as-flavor { display: block; margin-top: 0.6rem; }
.as-pagedoor .as-decide > .as-blocker { margin: 0.6rem 0; }
/* The dock's pending pip. */
.as-dock .as-tab > .as-glyph { flex-basis: auto; }
.as-dock .as-tab > .as-pill.round { padding-inline: 0.35rem; }

/* ═══ SWATCH — a colour you can pick ═══════════════════════════════════════ */
/* A round button whose face IS the value (`--swatch`); the chosen one wears
   a parchment ring. A row of them is `.as-swatches`. Tap floor on both axes. */
.as-swatches { display: flex; flex-wrap: wrap; gap: 0.6rem; }
.as-swatch {
  position: relative; display: inline-grid; place-items: center; flex: 0 0 auto;
  width: var(--tap-floor); height: var(--tap-floor); min-height: 0; padding: 0;
  background: transparent; border: 1px solid transparent; border-radius: var(--r-pill);
  cursor: pointer;
}
.as-swatch::before { content: ''; width: 2.8rem; height: 2.8rem; border-radius: 50%; background: var(--swatch, var(--gold)); box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35); }
.as-swatch:hover, .as-swatch:focus-visible { border-color: var(--line-soft); background: transparent; }
.as-swatch[aria-pressed="true"], .as-swatch.on { border-color: var(--parchment); box-shadow: 0 0 10px rgba(255, 255, 255, 0.25); }

/* ═══ BANNER — a transient display-face announcement ═══════════════════════ */
/* ENEMY TURN / TURN 3: it stands over the board for a second and goes. Not a
   modal (nothing to answer), not a status line (it is the moment itself). */
.as-banner {
  position: fixed; left: 50%; top: 38%; transform: translate(-50%, -50%);
  z-index: 640; pointer-events: none;
  margin: 0; color: var(--gold);
  font: 700 4rem/1 var(--font-display); letter-spacing: 0.3em; text-indent: 0.3em; text-transform: uppercase; white-space: nowrap;
  text-shadow: 0 0 24px rgba(var(--accent-rgb), 0.5), 0 2px 8px #000;
  animation: as-banner 1100ms ease-out forwards;
}
.as-banner.small { font-size: 2.2rem; top: 30%; animation-duration: 900ms; }
@keyframes as-banner {
  0% { opacity: 0; transform: translate(-50%, -50%) scale(0.92); }
  18% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  78% { opacity: 1; }
  100% { opacity: 0; }
}
.reduced-motion .as-banner { animation-duration: 500ms; }

/* ═══ DOCK — a floating strip of Tabs ══════════════════════════════════════ */
/* Seats on a couch: one Tab per subject, a Keycap for the switch, a StatePill
   for "has something to do". It hangs off the bottom edge over the board. */
.as-dock {
  position: fixed; left: 50%; bottom: 4.6rem; transform: translateX(-50%);
  z-index: 620; max-width: calc(100% - 2rem);
  display: flex; align-items: center; gap: 0.75rem;
  padding: 0.3rem 0.6rem;
  background: var(--bg-raise); border: 1px solid var(--line-soft); border-radius: var(--r-3);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.6);
}
.as-dock > .as-tabs { flex: 1 1 auto; min-width: 0; overflow-x: auto; }
.as-dock .as-tab { display: inline-flex; align-items: center; gap: 0.5rem; }

/* ═══ FOLD — a Row that opens ══════════════════════════════════════════════ */
/* A <details> whose summary is the kit's Row: label left, StatusText right,
   caret in the Glyph column. What it opens is prose under a hairline. */
.as-fold { border-bottom: 1px solid var(--rule); }
.as-fold:last-child { border-bottom: 0; }
.as-fold > summary { list-style: none; }
.as-fold > summary::-webkit-details-marker { display: none; }
.as-fold > summary > .as-glyph { transition: transform 150ms; }
.as-fold[open] > summary > .as-glyph { transform: rotate(90deg); color: var(--gold); }
.as-fold > .fold-body { display: flex; flex-direction: column; gap: 0.6rem; padding: 0 1.35rem 1rem calc(1.35rem + 1.75rem + 1.05rem); }
.as-fold > .fold-body code { font-family: monospace; font-size: 1.2rem; color: var(--ink-dim); word-break: break-word; }
@media (prefers-reduced-motion: reduce) { .as-fold > summary > .as-glyph { transition: none; } }

/* ═══ LOG — a monospace scroll box ═════════════════════════════════════════ */
.as-log {
  margin: 0; flex: 1 1 auto; min-height: 18rem; max-height: 100%; overflow: auto;
  padding: 1rem 1.2rem; background: var(--bg); border: 1px solid var(--line); border-radius: var(--r-2);
  color: var(--ink-dim); font: 400 1.15rem/1.45 monospace; white-space: pre-wrap; word-break: break-word;
  scrollbar-width: thin; scrollbar-color: var(--line-soft) transparent;
}

/* ═══ PAGE DOORS — a door standing on the page, and a pane on the page ═════ */
/* A screen that asks one thing (the run's end, the crisis notice) wears the
   door's box without the veil, like the event. A screen that lists things
   (history, the compendium) is a Pane bounded to a reading width. */
.screen > .modal.as-pagedoor { flex: 0 1 auto; max-height: 100%; }
.screen > .modal.as-pagedoor[data-size="lg"], .screen > .modal.as-pagedoor[data-size="xl"] { height: auto; max-height: 100%; }
.screen > .modal.as-pagedoor > .modal-body { min-height: 0; }
.as-pagedoor .as-decide > .as-detailcard + .as-detailcard { margin-top: 0.9rem; }
.as-pagedoor .as-decide > .as-btnrow, .as-pagedoor .as-decide > .modal-btnrow { margin: 1.5rem auto 0; }
.as-pagedoor .as-decide > .modal-btnrow + .modal-btnrow { margin-top: 0.8rem; }
.as-pagedoor .as-decide > .as-kitline, .as-pagedoor .as-decide > .as-statstrip { justify-content: center; margin-top: 1.2rem; }
.as-pagedoor .as-decide > .as-status { display: block; text-align: center; margin-top: 0.9rem; }
.as-pagedoor .as-decide > .as-status:empty { display: none; }
.as-pagedoor .as-decide > .as-fold { text-align: left; margin-top: 1.2rem; border-top: 1px solid var(--rule); border-bottom: 0; }
.as-pagedoor .as-decide > .set-section-head { align-items: center; margin-top: 1.8rem; }
.as-pagedoor .as-decide > .as-prose + .as-detailcard, .as-pagedoor .as-decide > .as-prose + .as-prose { margin-top: 1rem; }
.as-title-l[data-tone="loss"] { color: var(--danger); }
/* A DetailCard that carries its own actions and its own support fold. */
.as-detailcard > .modal-btnrow, .as-detailcard > .as-btnrow { margin-top: 0.8rem; }
.as-detailcard > .as-fold { border: 0; margin-top: 0.2rem; }
.as-detailcard > .as-fold > summary { padding-inline: 0; }
.as-detailcard > .as-fold > .fold-body { padding-inline: 0; }
.as-detailcard > .as-prose { font-size: 1.32rem; }
/* A RailItem with a count beside its label. */
.as-railitem.with-status { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; }
/* The count is the half a narrow rail must not eat: it holds its size and the
   NAME yields — first by wrapping onto its own line, then by ellipsis. Measured
   at 390x844, where "WEAPONS 0/9" clipped its count against the rail's end. */
.as-railitem.with-status { flex-wrap: wrap; row-gap: 0.15rem; white-space: normal; }
.as-railitem.with-status > .as-status { flex: 0 0 auto; font-size: 1.05rem; }
.as-railitem.with-status > :not(.as-status) { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A railed screen scrolls its grid, never its pane. */
.screen > .as-railed > .as-pane { overflow: hidden; }
.as-pane > .as-options.grid { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 2px; scrollbar-width: thin; scrollbar-color: var(--line-soft) transparent; }
.screen > .as-pane { --modal-inset: 1.4rem; width: min(100%, 76rem); flex: 0 1 auto; max-height: 100%; }
.screen > .as-railed { --modal-inset: 1.4rem; container-type: inline-size; width: min(100%, 96rem); flex: 1 1 auto; min-height: 0; max-height: 100%; background: var(--bg-raise); border: 1px solid var(--line); border-radius: var(--r-4); overflow: hidden; }
@media (max-width: 640px) { .screen > .as-railed, .screen > .as-pane { --modal-inset: 1rem; } }
/* A KitLine that lists rather than explains: no help underline, no help cursor. */
.as-kitline.plain .ki { cursor: default; }
.as-kitline.plain .ki .kn { border-bottom: 0; }
.screen > .as-railed > .as-pane { height: 100%; }
/* Rows that read a fact rather than choose one: the table's replacement. */
.as-pane .as-row.setting > .r-label { flex: 0 1 auto; color: var(--muted); }
.as-pane .as-row.setting > .as-status { color: var(--parchment); text-align: right; }
/* A pane's foot: the ButtonRow under a hairline. */
.as-pane > .as-btnrow, .as-pane > .modal-btnrow { margin-top: 1.4rem; flex: 0 0 auto; }
.as-pane > .as-options { flex: 0 0 auto; }
/* An OptionCard whose Glyph is an ArtWell (a piece's own icon). */
.as-option > .as-artwell { flex: 0 0 auto; }
.as-option.is-ghost > .as-artwell img { filter: brightness(0) invert(1); opacity: 0.72; }
.as-option.is-ghost { border-style: dashed; background: transparent; }
.as-option.is-ghost .on { color: var(--muted); font-style: italic; }
.as-option[data-rarity="rare"]:not(.is-selected) { border-left: 3px solid var(--gold); }
.as-option[data-rarity="uncommon"]:not(.is-selected) { border-left: 3px solid var(--frost); }
/* A grid of OptionCards: the compendium's constant grid. */
.as-options.grid { align-content: start; }
.as-options.grid > .as-option { width: auto; }
/* The kit's danger tint (the kit page's rgba(212, 98, 46)); nothing in
   base.css named it, so every `.danger` weight read an unset variable. */
:root { --danger: #d4622e; }
/* THE FOCUS RING, ONE TOKEN (Constantine's review, 2026-09-11). Every control
   — kit atom, repo hook class, the pad's cursor — wears this ring on
   :focus-visible and .gp-focus, from base.css's one house rule. Fourteen
   per-component rings (2px, 3px, gold, #f2d87e, #fff0bd, box-shadow halos,
   inset lines) used to say the same thing in different inks; a component
   may still move its OFFSET (inside a card, on a tab) but never its ink. */
:root { --focus-ring: 2px solid var(--gold); --focus-ring-offset: 2px; --door-ms: 150ms; }

/* ═══ METER — a resource read at a glance ══════════════════════════════════ */
/* Label · value · track. The TRACK's length is data (the surface writes its
   width inline: max ÷ reference, so a bigger pool is a longer bar), the fill is
   cur ÷ max. `.skinny` is the poise weight: no label, no value, a hairline
   track. `.inset` puts the value inside the track for the under-model strip,
   where there is no room beside it. Tones name the resource, never a screen. */
.as-meter { --meter-h: 10px; --meter-tone: var(--gold); position: relative; display: flex; align-items: center; gap: 0.6rem; min-width: 0; width: 100%; container-type: inline-size; }
/* THE LADDER, MEASURED, NOT GUESSED (§02): a meter too narrow for its name
   drops the NAME and keeps the glyph and the value, because the value is the
   fact and the glyph is the identity. Only then does anything truncate. */
/* 9rem, AND rem RATHER THAN em, both measured. This rung was written `8em` and
   never fired once: font-relative units in a container query resolve against the
   container's OWN font-size, and these meters inherit 0.92rem = 9.2px, so 8em
   was 73.6 px — under every column the game actually draws (the arcane receipt's
   container is 130 px at 1200x730 and 80 px at 390x844, in the LOCAL px a
   container query compares, which the battlefield's scale then renders as 88 and
   68). The name stayed and clipped instead: "ARCANE …" at the desk, "A…" on the
   phone. 9rem = 90 px falls between those two columns, which is the cut this
   ladder is about — wide enough for HP/MP/SP wherever they are drawn (120 px at
   320 px of viewport), too narrow for a word. */
@container (max-width: 9rem) { .as-meter > .m-plate > .m-label { display: none; } }
.as-meter > .m-plate { flex: 0 0 auto; display: inline-flex; align-items: baseline; gap: 0.5rem; min-width: 0; white-space: nowrap; }
.as-meter .m-label { flex: 0 0 auto; white-space: nowrap; font: 600 1.05rem/1 var(--font-body); letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); }
.as-meter .m-value { flex: 0 0 auto; white-space: nowrap; font: 600 1.15rem/1 var(--font-body); font-variant-numeric: tabular-nums; color: var(--parchment); }
.as-meter > .m-well { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; }
/* `stack` — the plate ABOVE the track, for a meter in a narrow column (under a
   combatant, in a list). THE NAME WRAPS RATHER THAN TRUNCATING, and the receipt
   grows downward to hold it. Measured on the arcane receipt at ?shot=combat,
   whose name is fifteen characters: "Arcane Exposure" wants 131 px and its
   column is 92 px at 1200x730 and 68 px at 390x844, so with the label on one
   nowrap line it read "ARCANE …" at the desk and "A…" on the phone — a label
   that has stopped being a word. The 8em rung above drops a name that does not
   fit a WIDE meter; in a stacked one there is a whole line to give it, and a
   two-line name costs the sprite nothing. The value never wraps and never
   truncates: it is the fact. */
.as-meter.stack { flex-direction: column; align-items: stretch; gap: 0.15rem; }
.as-meter.stack > .m-plate { display: flex; align-items: baseline; justify-content: space-between; gap: 0.6rem; }
.as-meter.stack .m-label { flex: 0 1 auto; min-width: 0; overflow: visible; text-overflow: clip; white-space: normal; overflow-wrap: normal; line-height: 1.2; }
/* The rung above is also what stops this wrap from becoming a letter-per-line
   column: at 390x844 the plate leaves the name about 20 px, and `anywhere`
   spelled ARCANE EXPOSURE downwards. `overflow-wrap: normal` keeps the break at
   the space, and under 9rem there is no name to break — the glyph says which
   school it is. */
.as-meter.stack .m-value { flex: 0 0 auto; }
.as-meter .m-track { position: relative; flex: 0 0 auto; box-sizing: border-box; width: 100%; max-width: 100%; height: var(--meter-h); min-width: 0; padding: 0; border: 0; background: var(--bg); box-shadow: inset 0 0 0 1px var(--line); border-radius: var(--r-pill); overflow: hidden; }
.as-meter .m-fill { position: absolute; inset: 0 auto 0 0; background: var(--meter-tone); border-radius: inherit; }
.as-meter[data-tone="hp"] { --meter-tone: var(--res-hp, var(--blood)); }
.as-meter[data-tone="mp"], .as-meter[data-tone="mana"] { --meter-tone: #4f82d8; }
.as-meter[data-tone="sp"], .as-meter[data-tone="stamina"] { --meter-tone: #5f925d; }
.as-meter[data-tone="poise"] { --meter-tone: var(--gold); }
.as-meter[data-tone="arcane"] { --meter-tone: #8a6fd6; }
.as-meter[data-tone="danger"] { --meter-tone: var(--danger); }
/* The two climbs: character XP in gold, a skill track in the cooler steel
   so a glance separates "the level" from "a track". */
.as-meter[data-tone="xp"] { --meter-tone: var(--gold); }
.as-meter[data-tone="skill"] { --meter-tone: #7fa8c9; }
.as-meter.inset { --meter-h: 13px; }
.as-meter.inset .m-label { display: none; }
.as-meter.inset > .m-plate { position: absolute; inset: 0; display: grid; place-items: center; pointer-events: none; z-index: 1; }
.as-meter.inset .m-value { font-size: 0.95rem; letter-spacing: 0.04em; text-shadow: 0 1px 2px #000; }
.as-meter.skinny { --meter-h: 4px; }
.as-meter.skinny > .m-plate { display: none; }
.as-meter.pulse .m-fill { animation: pulse-gold 600ms infinite alternate; }
.reduced-motion .as-meter.pulse .m-fill { animation: none; }
/* A dashed rim says "this length is the floor, not the data". */
.as-meter .m-track[data-floored="1"] { box-shadow: none; border: 1px dashed var(--line-soft); }
/* A meter that is locked reads dimmed; a striped fill says a resistance holds. */
.as-meter.locked { opacity: 0.6; }
.as-meter.resisted .m-fill { background: repeating-linear-gradient(45deg, var(--meter-tone) 0 4px, transparent 4px 8px); }
/* A stack of meters, one under the other, tight. */
.as-meters { display: flex; flex-direction: column; gap: 0.3rem; width: 100%; min-width: 0; }
.as-meters.tight { gap: 2px; }
/* Main Vitals use the shared screen-pixel token. Dividing by the page zoom
   makes the authored 3px separation land as 3px on glass at every UI scale;
   model strips retain the kit's tighter local stack. */
.resbars[data-surface="main"] { gap: calc(var(--hud-resource-row-gap-px) / var(--ui-zoom, 1)); }

/* ═══ SLOT — a square control that holds one thing: art, a count, a key ═══ */
/* Flasks, relics, quick items. The box is the IconButton's (a control) or `sm`
   (a chip that is only read). The count is a round StatePill on the corner,
   the key a Keycap in the other. */
.as-slot { position: relative; display: inline-grid; place-items: center; flex: 0 0 auto; box-sizing: border-box; width: var(--iconbtn-size); height: var(--iconbtn-size); min-width: var(--iconbtn-size); min-height: var(--iconbtn-size); padding: 0; background: var(--panel); border: 1px solid var(--line-soft); border-radius: var(--r-2); color: var(--parchment); cursor: pointer; overflow: visible; }
.as-slot:hover, .as-slot:focus-visible { border-color: var(--gold); background: var(--panel); }
.as-slot.is-selected { border-color: var(--parchment); box-shadow: 0 0 0 1px var(--parchment); }
.as-slot[aria-disabled="true"] > .sl-art { opacity: 0.5; filter: grayscale(0.6); }
.as-slot.sm { width: 3rem; height: 3rem; min-width: 3rem; min-height: 3rem; }
.as-slot.static { cursor: default; }
.as-slot.static:hover { border-color: var(--line-soft); }
.as-slot > .sl-art { display: grid; place-items: center; width: 72%; height: 72%; font-size: 1.8rem; line-height: 1; }
.as-slot.sm > .sl-art { font-size: 1.4rem; }
.as-slot > .sl-art img { width: 100%; height: 100%; object-fit: contain; display: block; }
/* Whatever art is handed to the well fills it, wrappers and all. */
.as-slot > .sl-art > *, .as-slot > .sl-art > * > * { display: grid; place-items: center; width: 100%; height: 100%; max-width: 100%; max-height: 100%; min-width: 0; margin: 0; }
.as-slot > .as-pill.round { position: absolute; top: 1px; right: 1px; padding: 0 0.4rem; background: var(--bg); border-color: var(--gold); color: var(--parchment); font-size: 1rem; line-height: 1.5; letter-spacing: 0; }
.as-slot > .as-keycap { position: absolute; right: 1px; bottom: 1px; min-width: 1.5rem; padding: 0 0.3rem; font-size: 0.9rem; line-height: 1.4; }
.as-slot[aria-disabled="true"] > .as-pill.round { border-color: var(--line-soft); color: var(--muted); }
/* A gamepad's glyph is round; the keycap follows the device. */
body.pad-mode .as-slot > .as-keycap { border-radius: var(--r-pill); }
/* While a thumb is driving there is no key to press: every keycap on a
   control leaves — a Slot's, a Button's, the one floating on a card. */
@media (pointer: coarse) { .as-slot > .as-keycap, .as-btn > .as-keycap, .as-keycap.float { display: none; } }

/* ═══ BAND — a page-level chrome strip (the HUD, an action row, a zoom bar) ═ */
/* `.topbar` is the hook co-op's bar still writes; the kit name is the recipe. */
/* `position: relative` ON BOTH, because the utility rail hangs off the bottom
   of whichever one carries it (`top: 100%`, § QUICK SETTINGS). `.as-band`
   already had it for its grip; `.topbar` did not, and the co-op board's two
   headers (`screens/coop.js`) are bare `.topbar` with no `as-band` — so the
   rail would have escaped to the nearest positioned ancestor and hung a full
   screen height below the fold. hud-quick-compact's 18 surfaces are solo
   map/combat/title, so no gate would have said so. */
.as-band, .topbar { position: relative; display: flex; align-items: center; gap: 1.2rem; flex: 0 0 auto; min-width: 0; padding: 0.6rem 1.6rem; background: var(--bg-raise); border-bottom: 1px solid var(--line); }
.as-band.foot { border-bottom: 0; border-top: 1px solid var(--line); }
.as-band.stack { flex-direction: column; align-items: stretch; gap: 0.5rem; }
.as-band.quiet { background: transparent; border: 0; }
.as-band-row { display: flex; align-items: center; gap: 1.2rem; min-width: 0; width: 100%; }
/* THE METERS START LEVEL WITH THE CONTROLS BESIDE THEM. The resource row's
   height is set by its right-hand cluster — two rows of IconButtons, 112 CSS px
   at 360x800 — while the three meters are 41, so `align-items: center` left
   them straddling the gap between the ⚒ ☰ row and the flasks below, aligned to
   neither. Cross-axis only: the row's height is the cluster's either way, so
   this moves no other row and the map and combat HUDs stay the same height. */
.hud-resource-row { align-items: flex-end; }
.hud-resource-row > .hud-vitals-panel { align-self: flex-end; }
.as-band-row > .grow { flex: 1 1 auto; min-width: 0; }
.as-band-row.split { justify-content: space-between; }
.as-band-row.end { justify-content: flex-end; }
.as-band-row.center { justify-content: center; }
/* Three tracks: identity, a centred receipt, the trail — the run header's shape. */
.as-band-row.thirds { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; }
.as-band-row.thirds > :last-child { justify-self: end; }

/* ═══ BAND FOLD — ONE FACT LINE AND ONE METER LINE ════════════════════
   A HOST that gives the shared band less room than its two full-width rows
   need FOLDS it, and the fold is one rule set here, not one per host. Two ask
   for it and they asked differently, which is the defect this block closes:
   the map's W4b header (§ MAP HEADER, `data-hud-layout="map-compact"`) carried
   these rules alone, and when W4c's dialogue band took the same fold
   (§ DIALOGUE, `data-hud-compact='true'`, 2026-09-17) it took the GRID and
   left the yielding behind — so at 430 wide the three fact tracks and the
   stacked meters overflowed their half-width columns and printed on top of
   each other. A fold that is copied is a fold that is copied wrong.

   WHAT YIELDS, in the text law's order. The row keeps its three tracks at
   every width, because the centred receipt is centred at every width and that
   is the whole point of the middle track. What gives is DETAIL: a chip may
   shrink, and the class name and the act's seat name ellipsize inside their
   own track rather than running over Cinders. Keys and numbers never do.
   The meters stop stacking and sit side by side on one line. */
.as-band-fold, .shared-hud[data-hud-layout="map-compact"],
.dialogue-screen[data-hud-compact='true'] .shared-hud { --band-fold-gap: calc(8px / var(--ui-zoom, 1)); }
:is(.as-band-fold, .shared-hud[data-hud-layout="map-compact"],
    .dialogue-screen[data-hud-compact='true'] .shared-hud) .hud-info-row > .as-statstrip {
  min-width: 0; margin: 0; flex-wrap: nowrap;
}
/* The trail FILLS its track and packs right, rather than the kit's
   `justify-self: end` (above), which sizes it to its max-content and lets it
   run left over Cinders on a phone instead of yielding. */
:is(.as-band-fold, .shared-hud[data-hud-layout="map-compact"],
    .dialogue-screen[data-hud-compact='true'] .shared-hud) .hud-info-row .hud-run-meta {
  justify-self: stretch; justify-content: flex-end; gap: 0 var(--band-fold-gap);
}
:is(.as-band-fold, .shared-hud[data-hud-layout="map-compact"],
    .dialogue-screen[data-hud-compact='true'] .shared-hud) .hud-info-row .as-chip {
  display: flex; flex: 0 0 auto; min-width: 0; white-space: nowrap;
}
:is(.as-band-fold, .shared-hud[data-hud-layout="map-compact"],
    .dialogue-screen[data-hud-compact='true'] .shared-hud) .hud-info-row :is(.hud-class, .hud-act) { flex: 0 1 auto; }
:is(.as-band-fold, .shared-hud[data-hud-layout="map-compact"],
    .dialogue-screen[data-hud-compact='true'] .shared-hud) .hud-info-row .as-chip > .ck { flex: 0 0 auto; }
:is(.as-band-fold, .shared-hud[data-hud-layout="map-compact"],
    .dialogue-screen[data-hud-compact='true'] .shared-hud) .hud-info-row :is(.hud-class, .hud-act) > .cv {
  display: block; flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* One meter line: HP, MP and SP side by side instead of stacked. */
:is(.as-band-fold, .shared-hud[data-hud-layout="map-compact"],
    .dialogue-screen[data-hud-compact='true'] .shared-hud) .resbars {
  display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr);
  column-gap: calc(10px / var(--ui-zoom, 1)); row-gap: 0;
}
:is(.as-band-fold, .shared-hud[data-hud-layout="map-compact"],
    .dialogue-screen[data-hud-compact='true'] .shared-hud) .resbars > .resline {
  min-width: 0; gap: calc(6px / var(--ui-zoom, 1));
}
:is(.as-band-fold, .shared-hud[data-hud-layout="map-compact"],
    .dialogue-screen[data-hud-compact='true'] .shared-hud) .resbars .resunit { gap: calc(5px / var(--ui-zoom, 1)); }
/* ═══ BAND ON A PHONE — THE SAME BAND IN EVERY VIEW ══════════════════
   THE OWNER'S RULE (2026-09-18): "just vertically stacked vitality block,
   cinders, armament and menu button. should be uniform in every view. act and
   floor should show up in wide screen but on mobile, that's how it should be."

   So on a phone the band carries FOUR THINGS and nothing else: the three
   meters STACKED, the Cinders receipt, Armoury, Menu. The class name, the act
   and the floor are WIDE-SCREEN FACTS — they are not truncated on a phone,
   they are not there. A phone reads the same band on the map, in a room, in a
   fight and in a conversation, which is what "uniform in every view" means and
   what neither the fold nor the map header gave it before: the fold's one
   meter line and its ellipsized act chip are a WIDE answer to a narrow band,
   and they stop at this gate.

   THE GATE IS THE PUBLISHED ONE. `data-layout="narrow"` is written on <html>
   by main.js's applyUiScale from balance.ui.uiScale.narrowMax (520 visual px),
   by the same call that chose the zoom. This block re-measures nothing. */
:root[data-layout='narrow'] .shared-hud .hud-info-row > :is(.hud-identity, .hud-run-meta) { display: none; }
/* The receipt keeps the middle track of § BAND thirds, so it stays centred
   with its neighbours gone rather than sliding to an edge. */
:root[data-layout='narrow'] .shared-hud .hud-info-row > .hud-center { grid-column: 2; justify-self: center; }
/* STACKED, in every host. This outranks § BAND FOLD's one meter line on
   purpose: the fold is what a band does when it is too short for its rows, and
   a phone answers that by dropping the wide facts instead. */
:root[data-layout='narrow'] .shared-hud .resbars {
  display: flex; flex-direction: column; align-items: stretch; row-gap: calc(4px / var(--ui-zoom, 1));
}
:root[data-layout='narrow'] .shared-hud .resbars > .resline { min-width: 0; }
/* "JUST" IS FOUR THINGS, so the map's route receipt goes with the act chip it
   restates — the same act name, one line lower. A phone's band is the band,
   not the band plus this host's extra. The map still carries the route on
   every wider screen, where it sits beside the facts rather than under them. */
:root[data-layout='narrow'] .shared-hud .act-route-strip { display: none; }
/* The band takes the height its four things need. W4b's 10 vh and W4c's band
   are WIDE-SCREEN heights, and a height that holds one folded line cannot hold
   three meters — honoured as a floor here, never as a ceiling. */
:root[data-layout='narrow'] .shared-hud[data-hud-layout="map-compact"] {
  height: auto; min-height: calc(var(--map-header-height-px, 80) * 1px / var(--ui-zoom, 1));
}

/* On a phone the three tracks stay three — the centred receipt is centred at
   every width, which is the whole point of the middle track — and what yields
   is DETAIL: a trail keeps the first part of each compound fact and drops the
   rest (a build keeps its number, drops its source). */
@media (max-width: 640px) {
  .as-band-row.thirds { gap: 0.5rem; }
  /* TWO SELECTORS, NOT ONE, and the phone photograph is why. A trail carries two
     shapes of compound fact: the build stamp (number · source) and a progress
     Chip (key, then value / total). `> * > :nth-child(n+2)` hit both — right for
     the stamp, wrong for the chip, where the second child IS the value: at
     390x844 the header read "ACT" and "FLOOR" with no numbers at all, which is a
     label announcing nothing. So the head of each fact stays and only its tail
     goes: the stamp keeps BUILD and drops src, the chip keeps "1" and drops
     "/ 12". */
  .as-statstrip.trail > .build-stamp > :nth-child(n+2) { display: none; }
  /* `> *` and not `:nth-child(n+2)`: the value itself is a TEXT NODE inside
     `.cv` and the "/ total" is the only ELEMENT in there, so counting elements
     from the second would keep the tail it is here to drop. */
  .as-statstrip.trail > .as-chip > .cv > * { display: none; }
  /* THREE TRACKS AT EVERY WIDTH, again. From 2026-08 to 2026-09-05 the phone
     shape was two tracks with the trail dropped to its own row, because the
     corner had taken the trail's track and the over-wide trail spilled onto
     the centred receipt. Both of the things that made that necessary are gone:
     the trail is empty (the build stamp left the band) and so is the identity
     cluster (name, class and floor left it the same day). With nothing in the
     outer tracks, a two-track grid only did harm — its `auto` middle track sat
     at the right edge, so the Cinders receipt read right-aligned at 390 and
     320 wide (hudparity `cinders-centre`, twelve findings; Constantine:
     "cinder should be centered"). The wide rule above now holds here too. */
  /* ONE LINE FOR THE IDENTITY'S EYEBROW, so the run HUD is the SAME HEIGHT on
     every screen that draws it. The context line beneath it is already nowrap;
     the eyebrow was not, so it wrapped wherever its track was tightest — and
     the map's track is tighter than combat's, which made the map's HUD 19px
     taller than the same HUD one screen earlier (hud-quick-compact:
     "Map and Combat draw different HUDs"). A HUD that changes height as you
     walk through a door moves every control under it. It truncates instead,
     which is the step the text law names after the box has stopped yielding. */
  .hud-identity > .as-eyebrow { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* PADDING YIELDS BEFORE THE ROW WRAPS, which is the first step of the text
     law and the second half of the same equal-height problem. `.hud-top` is
     content-sized, so the trail's track is as wide as the widest row beside it
     — 159px on the map, which carries no flask belt, against 247px in combat.
     At the kit's 1.35rem column gap the map's three facts wanted 168px, so the
     build stamp dropped to a second line and the map's HUD grew a row. The
     gap gives up 8.5px instead; all three facts keep their whole text and both
     screens draw the same HUD. */
  .as-statstrip.trail { gap: 0.75rem 0.5rem; }
}
/* A cluster is a group of IconButtons or Slots that share one gap. */
.as-cluster { display: flex; align-items: center; gap: 0.6rem; flex: 0 0 auto; min-width: 0; }
.as-cluster.wrap { flex-wrap: wrap; }
.as-cluster.stack { flex-direction: column; align-items: stretch; }
.as-cluster.end { margin-left: auto; }
/* QUICK ACCESS now carries only Armoury and Menu — Crimson and Azure moved
   into the potion belt — so the two remaining controls take the full
   meter-stack height instead of staying a compact 2 x 2 square. The max()
   keeps the guarantee that a face never falls below the authored tap floor,
   and the transparent ::after still carries that floor around each face.
   hud-quick-compact.mjs, which measures this edge against the SP row, is
   ALREADY red on dev (15 findings) before this change; it reads 12 with this
   change and 12 with the old 1.8rem face, so the size is not what breaks it. */
.shared-hud {
  --hud-quick-tile-size: max(2.8rem, var(--tap-floor));
  --hud-quick-tile-gap: 0.45rem;
}
.shared-hud .hud-control-grid,
.shared-hud .hud-control-grid > .as-cluster { gap: var(--hud-quick-tile-gap); }
.shared-hud .hud-control-grid :is(.as-iconbtn, .as-slot) {
  position: relative;
  width: var(--hud-quick-tile-size); height: var(--hud-quick-tile-size);
  min-width: var(--hud-quick-tile-size); min-height: var(--hud-quick-tile-size);
}
.shared-hud .hud-control-grid :is(.as-iconbtn, .as-slot)::after {
  content: '';
  position: absolute;
  left: 50%; top: 50%;
  width: var(--tap-floor); height: var(--tap-floor);
  transform: translate(-50%, -50%);
}

/* Resources and relics share the left grid column. The rail participates in
   normal flow, so expanding its icons grows the background with the content. */
.shared-hud > .hud-top {
  display: grid; width: 100%; min-width: 0; height: auto;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "info info" "meters actions" "rail actions" "route route";
  align-items: start; gap: 0.5rem 1.2rem;
}
.shared-hud .hud-resource-row { display: contents; }
.shared-hud .hud-info-row { grid-area: info; }
.shared-hud .hud-vitals-panel { grid-area: meters; min-width: 0; align-self: start; }
.shared-hud .hud-control-grid { grid-area: actions; align-self: start; }
.shared-hud .act-route-strip { grid-area: route; }
.shared-hud .hud-bottom {
  position: static; grid-area: rail; min-width: 0; width: 100%;
  align-self: start; pointer-events: none;
}
.shared-hud .hud-bottom:empty { display: none; }
.shared-hud .hud-bottom > :not(.icon-tray) { pointer-events: auto; }
.as-band { position: relative; }
/* A detail selected by a tap, waiting for the second tap that explains it
   (components/tooltip.js). Screens with their own selected look (the combat
   overhead controls) restate it; this is the one everything else wears. */
.tooltip-selected:not(.overhead-control) { outline: 2px solid var(--gold); outline-offset: 2px; border-radius: var(--r-1, 4px); }

/* ═══ CARD GRID — faces in a scrolling body, wrapped, one gap ══════════════ */
.as-cardgrid { display: flex; flex-wrap: wrap; gap: 1.4rem; align-content: flex-start; }

/* ═══ HOLD — the second beat on a control ══════════════════════════════════ */
/* `.beat-hold` is written by the machinery (components/holdconfirm.js) on any
   control the second-beat table arms; `--hold` is 0..1 per frame. The fill is a
   high-contrast light/dark stripe instead of an accent wash: accent-filled
   primary buttons already look lit, so another gold layer disappears on them.
   Alternating luminance stays legible on both primary and plain controls. It
   is a BACKGROUND (paints under a bare text node), never eased (an abort must
   read instantly), and it survives reduced-motion because it is state, not motion.
   `.hold-hint` is the word beside the label; it leaves an icon-sized control
   and leaves the control a focus cursor is on (a pad confirms in one press). */
.beat-hold, button.beat-hold, .as-btn.beat-hold,
button.beat-hold:hover, .as-btn.beat-hold:hover,
button.beat-hold:focus, .as-btn.beat-hold:focus,
button.beat-hold:active, .as-btn.beat-hold:active {
  background-image: repeating-linear-gradient(135deg,
    rgba(255, 255, 255, 0.52) 0 8px,
    rgba(0, 0, 0, 0.58) 8px 16px);
  background-repeat: no-repeat;
  background-size: calc(var(--hold, 0) * 100%) 100%;
  transition-property: border-color, color, transform;
}
.beat-hold[data-hold-feedback="delegated"], .beat-hold[data-hold-feedback="delegated"]:hover,
.beat-hold[data-hold-feedback="delegated"]:focus, .beat-hold[data-hold-feedback="delegated"]:active { background-image: none; }
.beat-hold[data-hold="holding"] { border-color: var(--gold); }
.hold-hint { margin-left: 0.6em; font-size: 0.75em; letter-spacing: 0.14em; color: var(--gold); opacity: 0.75; }
/* IN A ROW THE WORD FOLLOWS THE LABEL, NOT THE TRAIL. The trail takes the row's
   free space with `margin-left: auto`, so a hint appended after it (which is
   where the machinery appends it — it dresses the control, not the row) landed
   past the keycap and was clipped by the panel's padding box. Ordering it ahead
   of the trail puts it where it reads, beside the verb it qualifies. */
.as-row > .hold-hint { order: 0; flex: 0 0 auto; }
.as-row > .as-status, .as-row > .r-trail { order: 1; }
.gp-focus > .hold-hint, .as-slot .hold-hint, .flask-slot .hold-hint { display: none; }
/* The armed control stays identified while its question stands. */
.beat-armed { outline: 2px solid var(--gold); outline-offset: 2px; }
/* The confirm form (the Smith, the brazier) is a DetailCard with a ButtonRow. */
.beat-confirm { flex-basis: 100%; display: flex; flex-direction: column; align-items: center; gap: 0.6rem; margin: 6px 0 2px; padding: 1.05rem 1.35rem; max-width: 34rem; background: var(--panel); border: 1px solid var(--gold); border-radius: var(--r-2); }
.beat-confirm .beat-q { margin: 0; color: var(--parchment); font-size: 1.32rem; text-align: center; }
.beat-confirm .beat-detail { color: var(--muted); font-size: 1.2rem; text-align: center; }
.beat-confirm .beat-actions { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
.beat-confirm .beat-actions button { min-height: var(--tap-floor); min-width: var(--tap-floor); }

/* ═══ SPOTLIGHT — a coaching bubble over a live board ══════════════════════ */
/* The veil takes no input: the board beneath keeps answering. Only the bubble
   (a Popover with a Title·S, prose and a ButtonRow) is pressable. */
.as-spot-veil { position: fixed; inset: 0; z-index: 800; pointer-events: none; }
.as-spot { position: absolute; border: 2px solid var(--gold); border-radius: var(--r-3); box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.25), 0 0 0 9999px rgba(0, 0, 0, 0.55); pointer-events: none; transition: left 200ms, top 200ms, width 200ms, height 200ms; }
.as-spot-veil > .as-pop { position: absolute; pointer-events: auto; padding: 1.35rem 1.5rem; display: flex; flex-direction: column; gap: 0.75rem; }
.as-spot-veil > .as-pop > .as-btnrow { margin-top: 0.6rem; margin-left: auto; }
.reduced-motion .as-spot { transition: none; }

/* ═══ PILL TONES · STATPAIR SIZES · KEYCAP FLOAT · BUTTONROW WIDE ══════════ */
/* An intent, a block count, a stance: a StatePill in the fact's own tint. */
.as-pill[data-tone="danger"] { border-color: var(--danger); color: var(--danger); }
.as-pill[data-tone="frost"] { border-color: var(--frost); color: var(--frost); }
.as-pill[data-tone="gold"] { border-color: var(--gold); color: var(--gold); }
.as-pill[data-tone="green"] { border-color: var(--green); color: var(--green); }
.as-pill[data-tone="violet"] { border-color: #a07ad6; color: #a07ad6; }
.as-pill[data-tone="ember"] { border-color: var(--ember); color: var(--ember); }
.as-pill.lg { padding: 0.35rem 0.9rem; font: 700 1.35rem/1.3 var(--font-body); letter-spacing: 0.06em; background: var(--bg-raise); }
.as-pill.lg .as-glyph { display: inline; flex: none; font-size: 1.2rem; color: inherit; margin-right: 0.4rem; }
.as-pill.dashed { border-style: dashed; }
.as-pill.solid { background: var(--frost); color: #0d0b08; border-color: var(--frost); font-weight: 800; }
/* A StatPair that stands alone reads large; `.stack` puts the value over the key. */
.as-statpair.lg .sp-v { font-size: 2rem; }
.as-statpair.stack { flex-direction: column; align-items: center; gap: 0.15rem; }
.as-statpair.stack .sp-k { font-size: 0.85rem; letter-spacing: 0.1em; }
.as-statpair.stack .sp-v { font-size: 1.5rem; line-height: 1.1; }
/* A keycap that floats on its subject's top edge (a card's positional key). */
.as-keycap.float { position: absolute; top: -1rem; left: 50%; transform: translateX(-50%); z-index: 3; min-width: 2rem; font-weight: 700; font-size: 1.05rem; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.55); pointer-events: none; }
body.pad-mode .as-keycap.float { display: none; }
/* A pill pinned to its subject's foot (a card's unavailability reason). */
.as-pill.foot { position: absolute; left: 0.3rem; right: 0.3rem; bottom: 0.3rem; z-index: 3; text-align: center; white-space: normal; background: var(--bg); text-transform: none; letter-spacing: 0; font-size: 0.85rem; line-height: 1.2; }
/* A keycap inside a button sits before its verb with one gap. */
.as-btn > .as-keycap:first-child { margin-right: 0.5rem; }
/* A ButtonRow member that deserves twice the width (the primary). */
.as-btnrow > .wide, .modal-btnrow > .wide { grid-column: span 2; }
.as-btnrow > button.tall, .modal-btnrow > button.tall { display: inline-flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.2rem; white-space: normal; line-height: 1.1; }
.as-btnrow > button.tall > .as-keycap { font-size: 0.9rem; }
/* `.cell` — a StatPair that STANDS IN A ROW OF CONTROLS and therefore has to
   read as one: the panel ground, the line, the row's radius, the tap floor's
   height. It is still read and not pressed (no hover, no pointer). Measured:
   on bare ground a receipt in the combat action row painted 3% of its box, and
   an instrument that asks whether a control is drawn could not see it. */
.as-statpair.cell {
  box-sizing: border-box; justify-content: center; gap: 0.2rem;
  min-width: var(--tap-floor); min-height: var(--tap-floor); padding: 0.4rem 0.6rem;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-3);
  cursor: default;
}
/* A StatPair standing in a ButtonRow fills its track like a button would, so
   the row's edges are the row's and it is as tall as the button beside it. */
.as-btnrow > .as-statpair { justify-self: stretch; align-self: stretch; }
.as-btnrow > button > .as-statpair { pointer-events: none; }
/* THE COMBATANT'S OWN ATOMS — the nameplate is a LabelStack, the intent, the
   block count and the stance are StatePills, the effects are Pips and every
   bar is a Meter. Only what the SPRITE surface asks of them is here: a
   nameplate reads centred under the figure, and a pill under a sprite is
   tighter than one in a list. */
.combatant .as-labelstack { align-items: center; text-align: center; }
.combatant .as-labelstack .ls-label { font-size: 1.2rem; }
.combatant .as-pill.lg { font-size: 1.2rem; padding: 0.2rem 0.7rem; }
.combatant .as-meter { --meter-h: 12px; }
.combatant .as-meter.skinny { --meter-h: 6px; }
.combatant .as-meter.procbar { --meter-h: 8px; }
.combatant .as-meter.procbar > .m-plate { position: absolute; right: 2px; top: 50%; transform: translateY(-50%); z-index: 1; pointer-events: none; }
.combatant .as-meter.procbar .m-value { font-size: 0.8rem; }
.combatant .block-badge { position: absolute; left: -1.4rem; bottom: 3.4rem; z-index: 2; }
/* THE INSPECTOR'S CONTENT is Panes' parts inside a Folding Tray: an Eyebrow
   over a hairline, then Rows. The tray owns its box (combat.css places the
   host at the field's edge); these say only how tight the reading is. */
.combatant-inspector-summary { display: grid; gap: 0.55rem; }
.combatant-inspector-resources { gap: 0.45rem; }
.combatant-inspector-section { margin-top: 0.7rem; }
.combatant-abilities > summary, .combatant-ability > summary { cursor: pointer; padding-block: 0.4rem; }
.combatant-abilities > summary { display: list-item; list-style-position: inside; }
.combatant-ability { border-bottom: 1px solid var(--line); padding-block: 0.25rem; }
.combatant-ability > summary { color: var(--gold); font-weight: 600; }
.combatant-ability > .as-prose { margin: 0.2rem 0 0.65rem; }
.combatant-abilities summary:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-ring-offset); }
.combatant-inspector-section > .as-hairline { margin: 0.35rem 0 0.2rem; }
.combatant-inspector-row { padding-inline: 0; white-space: normal; align-items: flex-start; }
.combatant-inspector-row > .r-label { flex: 0 1 auto; }
.combatant-inspector-row > .as-status { text-align: right; }
/* A pip: a round badge with a glyph and a count, for statuses in a row. */
.as-pips { display: flex; flex-wrap: wrap; gap: 0.4rem; justify-content: center; min-width: 0; }
.as-pip { position: relative; display: inline-grid; place-items: center; width: 2.2rem; height: 2.2rem; border-radius: 50%; background: var(--panel); border: 1.5px solid var(--pip-tone, var(--muted)); font-size: 1.1rem; line-height: 1; color: var(--parchment); cursor: default; }
.as-pip > .as-pill.round { position: absolute; right: -0.5rem; bottom: -0.5rem; padding: 0 0.3rem; background: var(--bg); color: var(--parchment); border-color: var(--line); font-size: 0.85rem; letter-spacing: 0; line-height: 1.4; }
.as-pip.ring { color: #fff; text-shadow: 0 0 2px #000; }

/* ═══ ICON TRAY — the one row of round icons (components/iconTray.js) ═══════
   The combatant card's status effects, the relic rail and the Potions minis
   are one component (owner, 2026-09-14), the status row its reference. A Pips
   row that never wraps or scrolls: iconTray.js writes --icon-tray-size and
   --icon-tray-gap from planIconTray, and what does not fit is the final +N
   tile. The fallbacks are that plan's numbers before its first fit. Only the
   icons take a press; the row itself passes it through. */
.as-pips.icon-tray {
  flex-wrap: nowrap; justify-content: flex-start; align-items: center;
  gap: var(--icon-tray-gap, calc(3px / var(--ui-zoom, 1)));
  min-width: 0; min-height: var(--icon-tray-size, calc(25px / var(--ui-zoom, 1)));
  pointer-events: none;
}
.icon-tray > * { pointer-events: auto; }
.icon-tray .as-pip {
  flex: none; box-sizing: border-box;
  width: var(--icon-tray-size, calc(25px / var(--ui-zoom, 1))); height: var(--icon-tray-size, calc(25px / var(--ui-zoom, 1)));
  min-width: 0; min-height: 0;
  font-size: calc(var(--icon-tray-size, calc(25px / var(--ui-zoom, 1))) * 0.62);
  cursor: pointer;
}
.icon-tray .as-pip:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
.icon-tray .as-pip[aria-disabled="true"] > :not(.as-pill) { opacity: 0.5; filter: grayscale(0.6); }
/* A painted face (a flask) sits in the round well the way a glyph does. */
.icon-tray .as-pip > .flask-identity { display: grid; place-items: center; width: 78%; height: 78%; }
.icon-tray .as-pip > .flask-identity :is(.flask-art, .flask-art-image) { display: grid; place-items: center; width: 100%; height: 100%; object-fit: contain; }
/* The +N tile: a round tile that opens the host's full list (WCF2), or a
   disclosure whose popover holds the rest. */
.icon-tray > .status-overflow-more {
  flex: none; box-sizing: border-box; padding: 0; min-width: 0; min-height: 0;
  width: var(--icon-tray-size, calc(25px / var(--ui-zoom, 1))); height: var(--icon-tray-size, calc(25px / var(--ui-zoom, 1)));
  display: grid; place-items: center; border-radius: 50%; line-height: 1; color: var(--gold);
}
.status-overflow { position: relative; font-size: calc(11px / var(--ui-zoom, 1)); }
.status-overflow summary { cursor: pointer; list-style: none; padding: 2px; color: var(--gold); }
.status-overflow-list { position: absolute; top: 100%; right: 0; display: flex; flex-wrap: wrap; gap: 8px; padding: 10px; width: calc(170px / var(--ui-zoom, 1)); background: #17130f; border: 1px solid var(--gold); z-index: 100; }
/* Legend rows and other read-only Rows: no hover, no cursor. */
.as-row.static, .as-row.static:hover { cursor: default; background: transparent; }

/* ═══ MAP — the act's chrome around the board ══════════════════════════════ */
/* The board's own geometry (nodes, edges, fog, the camera) stays in
   styles/map.css: it is drawn in the SVG's user space and is not chrome. What
   is here is the chrome around it, and all of it is kit pieces.

   THE ROUTE STRIP is a quiet Band: the act's name as a Title·S over a
   StatStrip whose two ends are Eyebrows with a rail between them. It is a
   receipt — no pointer, nothing to tap — and it leaves the HUD's right gutter
   free so the utility rail never lands on it. */
/* ═══ MAP HEADER — W4b's 10 vh band (owner: "10 vh for w4b") ══════════════
   The map draws the SAME run HUD in a host context, `data-hud-layout=
   "map-compact"`, with its route strip laid out inside it. Combat and the
   rooms never set the attribute, so nothing below reaches them.
   HEIGHT comes from ui/models/MapHeaderLayout.js through components/
   mapHeader.js as unitless physical px: 10% of the visible height, never less
   than one tap row plus its insets. Divided by the page zoom here, so it lands
   on glass as computed.
   COMPOSITION is a grid on `.hud-top`; the resource row dissolves into it so
   the meters and the Armoury / Menu pair are cells of the same grid.
     narrow  facts   | actions        wide  route | facts  | actions
             meters  | actions              route | meters | actions
             route   | actions
   Text keeps a physical floor (10 px labels) rather than scaling with the band;
   what yields first is detail — the act chip's seat name ellipsizes (the route
   line carries the act's name), then the narrow route line drops. */
.shared-hud[data-hud-layout="map-compact"] {
  --mh-inset: calc(var(--map-header-inset-px, 4) * 1px / var(--ui-zoom, 1));
  --mh-gap: calc(var(--map-header-gap-px, 2) * 1px / var(--ui-zoom, 1));
  box-sizing: border-box; justify-content: center; gap: 0;
  height: auto; min-height: calc(var(--map-header-height-px, 80) * 1px / var(--ui-zoom, 1));
  padding: var(--mh-inset) 2.5%;
}
.shared-hud[data-hud-layout="map-compact"] > .hud-top {
  display: grid; height: auto; min-height: 0; min-width: 0;
  align-content: center; align-items: center;
  column-gap: calc(12px / var(--ui-zoom, 1)); row-gap: var(--mh-gap);
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "info actions" "meters actions" "rail actions" "route actions";
}
.shared-hud[data-hud-layout="map-compact"][data-map-header-layout="wide"] > .hud-top {
  grid-template-columns: fit-content(32%) minmax(0, 1fr) auto;
  grid-template-areas: "route info actions" "route meters actions" "route rail actions";
}
.shared-hud[data-hud-layout="map-compact"][data-map-header-route="hidden"] > .hud-top {
  grid-template-areas: "info actions" "meters actions" "rail actions";
}
.shared-hud[data-hud-layout="map-compact"][data-map-header-route="hidden"] .act-route-strip { display: none; }
.shared-hud[data-hud-layout="map-compact"] .hud-resource-row { display: contents; }
.shared-hud[data-hud-layout="map-compact"] .hud-info-row { grid-area: info; min-width: 0; gap: calc(8px / var(--ui-zoom, 1)); }
.shared-hud[data-hud-layout="map-compact"] .hud-vitals-panel { grid-area: meters; min-width: 0; align-self: center; }
.shared-hud[data-hud-layout="map-compact"] .hud-control-grid { grid-area: actions; align-self: center; }
/* The fold's yield rules are § BAND FOLD, shared with W4c's folded band. */
/* THE ROUTE STRIP is a receipt — no pointer, nothing to tap: the act's name as
   a Title·S beside (narrow) or over (wide) a StatStrip whose two ends are
   Eyebrows with a rail between them. */
.act-route-strip {
  box-sizing: border-box; pointer-events: none; user-select: none; cursor: default;
}
.shared-hud[data-hud-layout="map-compact"] .act-route-strip {
  grid-area: route; min-width: 0; width: auto; margin: 0; padding: 0;
  display: grid; grid-template-columns: minmax(0, auto) minmax(4rem, 1fr); align-items: center;
  column-gap: calc(8px / var(--ui-zoom, 1)); row-gap: calc(3px / var(--ui-zoom, 1));
  border: 0; background: none;
}
.shared-hud[data-hud-layout="map-compact"][data-map-header-layout="wide"] .act-route-strip {
  grid-template-columns: minmax(0, 1fr); align-self: center;
  padding-inline-end: calc(12px / var(--ui-zoom, 1)); border-right: 1px solid var(--line);
}
.act-route-strip > .as-title-s {
  margin: 0; min-width: 0; font-size: max(1.1rem, calc(11px / var(--ui-zoom, 1))); line-height: 1.2;
  letter-spacing: 0.12em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.act-route-strip .map-orientation-progress {
  display: grid; grid-template-columns: auto minmax(1.5rem, 1fr) auto;
  align-items: center; gap: calc(6px / var(--ui-zoom, 1)); margin: 0; width: 100%; min-width: 0;
}
.act-route-strip .map-orientation-progress .as-eyebrow { font-size: max(0.9rem, calc(10px / var(--ui-zoom, 1))); line-height: 1.2; letter-spacing: 0.1em; }
/* Narrow: the title already stands at the rail's start, so ENTRANCE yields. */
.shared-hud[data-hud-layout="map-compact"][data-map-header-layout="narrow"] .act-route-strip [data-role="start"] { display: none; }
.shared-hud[data-hud-layout="map-compact"][data-map-header-layout="narrow"] .act-route-strip .map-orientation-progress { grid-template-columns: minmax(1.5rem, 1fr) auto; }
.act-route-strip [data-role="boss"] { color: var(--ember); }
.act-route-strip .map-orientation-rail { position: relative; height: 1px; background: var(--map-structure); }
.act-route-strip .map-orientation-rail::after {
  content: ''; position: absolute; right: 0; top: 50%; width: 6px; height: 6px;
  border-top: 1px solid var(--map-structure); border-right: 1px solid var(--map-structure);
  transform: translateY(-50%) rotate(45deg);
}

/* Map context follows the shared HUD instead of being compressed into it. */
.mapscreen > .act-route-strip {
  box-sizing: border-box; flex-direction: column; align-items: center; gap: 3px;
  width: 80%; margin-inline: auto; padding: 12px 12px 6px;
  border-bottom: 1px solid var(--line); background: var(--bg-raise);
  pointer-events: none; user-select: none; cursor: default;
}
.mapscreen > .act-route-strip > .as-title-s { font-size: 1.1rem; letter-spacing: 0.2em; }
@media (max-width: 700px) {
  .mapscreen > .act-route-strip {
    width: min(80%, calc(100% - var(--tap-floor) - calc(1.75rem / var(--ui-zoom, 1))));
    margin-left: auto; margin-right: calc(var(--tap-floor) + calc(14px / var(--ui-zoom, 1)));
  }
}
/* THE ZOOM BAR is a Band on the foot of the map with four IconButtons in it,
   BESIDE the board and never over it (styles/map.css says why). The bar has no
   reserved height: it is a flex item, so the band is its buttons' own height
   and the scrollport gets exactly the rest. */
.mapscreen > .map-zoom { gap: 0.6rem; padding: 0.6rem 1.4rem; background: var(--panel); }
/* The map's notes are StatusText, centred, and cost no height when silent. */
.mapscreen .map-tapnote, .mapscreen .map-clipnote {
  margin: 0; padding: 0.2rem 1rem; text-align: center; color: var(--gold); font-size: 1.1rem; line-height: 1.4;
}
.mapscreen .map-tapnote[hidden], .mapscreen .map-clipnote[hidden] { display: none; }
/* The legend hangs off the ? in the zoom bar: the kit Popover, placed by the
   surface that owns the corner it opens into. */
.map-legend-pop { position: absolute; right: 1.2rem; bottom: calc(100% + 0.4rem); z-index: 20; width: 24rem; }
.map-legend-pop .as-glyph { color: var(--gold); }
/* THE CONTEXT STRIP is a ButtonRow of kit Buttons with Keycaps (hints.js). Two
   rules that are not its shape and are kept because they are DECISIONS:
   · it is centred at the bottom of its own screen, in the flow, so the band it
     takes is its own height and nothing has to reserve one (map.css says why);
   · it does not appear on a narrow layout — a strip naming five keys to a
     device with no keys costs a row of a screen that has none to spare
     (EldenSpire#29 slice 0). `body.hide-hints` stays the player's own switch. */
.hint-bar { margin: 0 auto 0.6rem; max-width: 96%; justify-content: center; }
.hint-bar .as-keycap { color: var(--gold); }
body.hide-hints .hint-bar, :root[data-layout='narrow'] .hint-bar { display: none; }
/* The map header is the same Band combat wears; only the act's own tones and
   the player's two display switches are the map's. */
.map-header .hud-act .cv, .map-header .hud-floor .cv { color: var(--parchment); }
body.map-compact .map-header .hud-identity .as-eyebrow { display: none; }
body.hide-header-relics .map-header .hud-relics { display: none; }

/* ═══ THE RUN'S OTHER DOORS AND GATES — what the atoms need from them ══════ */
/* THE PILE VIEWER is body A with no rail: the shell's head names the pile and
   carries the count, the body is a CardGrid of the same faces the fan draws.
   Only the door's own insets are here. */
.pile-body { padding: var(--modal-inset); }
/* Body B inside the flask's inspect door takes the SAME inset as every other
   bar of every door — one token, four sides (tools/modal-shell-contract). */
.flask-inspect-body { padding: var(--modal-inset); }
.pile-body .as-cardgrid { justify-content: center; }
.pile-empty { padding: 2rem; }
/* W1h: rail | pane. The pane holds the pile beside the selected card's
   reading; compact hosts put the kit's [Pile ▾] selector above the pane and
   the reading under the pile. Only the two columns scroll. */
.spent-pile-modal .pile-body { display: flex; min-height: 0; }
.spent-pile-modal .pile-body > .as-railed { flex: 1 1 auto; min-height: 0; width: 100%; }
.spent-pile-modal .pile-pane { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 2vw; min-height: 0; padding-inline-start: 2.5vw; }
.spent-pile-modal :is(.pile-collection, .pile-detail) { min-height: 0; overflow: auto; }
.spent-pile-modal .pile-detail { padding: var(--modal-inset); border-left: 1px solid var(--line); font-size: calc(15px / var(--ui-zoom, 1)); line-height: 1.5; color: var(--ink-dim); }
.spent-pile-modal .pile-detail .tt-title { font-size: calc(18px / var(--ui-zoom, 1)); font-weight: 600; color: var(--parchment); margin-bottom: .4em; }
.spent-pile-modal .pile-detail .ti-meta { display: flex; gap: .6em; align-items: center; margin-bottom: .6em; }
:root[data-layout='narrow'] .spent-pile-modal .pile-pane { grid-template-columns: minmax(0, 1fr); padding-inline-start: 0; }
:root[data-layout='narrow'] .spent-pile-modal .pile-detail { border-left: 0; border-top: 1px solid var(--line); }

/* THE BOSS SPLASH is Eyebrow + Title·L + Ornament on a near-black veil. The
   veil and the beats are FX (the animation lives in styles/combat.css); what is
   here is the type's own scale, which is bigger than any other Title·L in the
   game because this one is a name being announced. */
.boss-intro { position: fixed; inset: 0; z-index: 650; display: flex; align-items: center; justify-content: center; cursor: pointer; }
.boss-intro .bi-stack { display: flex; flex-direction: column; align-items: center; gap: 1.4rem; text-align: center; max-width: 80%; }
.boss-intro .bi-tag { letter-spacing: 0.5em; }
.boss-intro .bi-name { font-size: 5.4rem; letter-spacing: 0.12em; line-height: 1.15; color: var(--gold); text-shadow: 0 0 34px rgba(var(--accent-rgb), 0.55), 0 2px 10px #000; }
.boss-intro .bi-rule { width: 34rem; max-width: 60%; }
/* A NAME THAT WRAPS ON A PHONE IS STILL A NAME, but at 5.4rem "The Fell Warden"
   took two lines across a 390 px screen and its second line landed on the boss's
   own nameplate underneath (photographed at 390x844, ?shot=boss). The splash is
   an announcement, so the type yields before the box does. */
@media (max-width: 640px) {
  .boss-intro .bi-name { font-size: 3.6rem; }
  .boss-intro .bi-stack { max-width: 88%; }
}

/* THE SHORT-SCREEN GATE is the Decision body with no controls in it. TYPE
   DIVIDED BY --ui-zoom is the one carve-out and it is deliberate: this is the
   message ABOUT the app having zoomed itself to its floor, so scaling it by
   that zoom shrinks the sentence exactly when it matters most (the same idiom
   as --tap-floor in base.css). Law 4 is intact — the unit is still rem, so
   Text size still scales these glyphs; what is cancelled is UI size. */
.upright-veil { z-index: 900; background: rgba(8, 6, 4, 0.985); padding: 24px; overflow: hidden; }
.upright-card { max-width: 46rem; gap: 0.6rem; }
.upright-card > .as-title-l { font-size: calc(2.4rem / var(--ui-zoom, 1)); text-align: center; }
.upright-card > .upright-say { font-size: calc(1.6rem / var(--ui-zoom, 1)); }
.upright-card > .upright-safe { color: var(--muted); font-size: calc(1.4rem / var(--ui-zoom, 1)); }
.upright-card > .upright-hint { margin-top: 1.4rem; font-size: calc(1.25rem / var(--ui-zoom, 1)); opacity: 0.78; }
.upright-glyph { display: grid; place-items: center; height: 76px; margin-bottom: 18px; }
.upright-phone { display: block; width: 34px; height: 56px; border: 2px solid var(--gold); border-radius: var(--r-2); transform-origin: 50% 50%; animation: upright-turn 2.6s ease-in-out infinite; }
.upright-veil[data-advice='resize'] .upright-phone { width: 56px; height: 40px; animation: none; }
/* Below the compact composition's floor the glyph goes entirely: it is
   aria-hidden decoration whose message the heading already carries in words,
   and at the one height where it competes with a sentence, the sentence wins. */
:root[data-short='true'] .upright-veil { padding: 12px 24px; }
:root[data-short='true'] .upright-glyph { display: none; }
:root[data-short='true'] .upright-card { gap: 0.4rem; }
@keyframes upright-turn { 0%, 18% { transform: rotate(-90deg); } 55%, 100% { transform: rotate(0deg); } }
.reduced-motion .upright-phone { animation: none; }
@media (prefers-reduced-motion: reduce) { .upright-phone { animation: none; } }

/* THE FIRST-RUN CALLOUT is the Spotlight: the lit box and a Popover beside it.
   Both are placed in the veil's own local space by components/tutorial.js. */
.tut-bubble { width: 30rem; max-width: calc(100% - 2.4rem); }
.tut-bubble .tut-title { color: var(--gold); }
.tut-bubble .tut-text { font-size: 1.25rem; }

/* ═══ FLASK — one identity, one menu, one door ═════════════════════════════ */
/* THE IDENTITY is the data's, not a screen's: the art (a rendered bottle or its
   glyph fallback) and, where a surface asks for it, the name. Every surface
   that draws a flask — the HUD's Slots, the shop's OptionCards, the Armoury's
   rows, the inspect door's ArtWell — mounts this same fragment
   (components/flask.js flaskIdentityHtml), so it has one home here. */
.flask-identity { display: inline-flex; align-items: center; gap: 0.35em; min-width: 0; color: var(--flask-tint); }
.flask-art { flex: 0 0 auto; display: inline-grid; place-items: center; width: 1.8em; height: 1.8em; }
.flask-art-image { display: block; width: 100%; height: 100%; object-fit: contain; }
.flask-art-glyph {
  display: inline-grid; place-items: center; width: 1.35em; height: 1.35em;
  border: 1px solid currentColor; border-radius: 45% 45% 55% 55%;
  background: color-mix(in srgb, var(--flask-tint) 22%, #111);
}
.flask-name { color: var(--parchment); }
/* THE ACTION MENU is the kit Popover, hung under the slot the player tapped
   (flask.js places it; `--place-gap` is the hairline under the control, per
   placed element by construction). Narrower than the quick menu because it
   holds three verbs, and it scrolls inside itself because its opening sentence
   is content a designer may make long. */
.as-pop.flask-action-menu {
  --place-gap: 6px;
  position: fixed; z-index: 450;
  width: auto; min-width: 20rem; max-width: 28rem;
  overflow: hidden auto;
}
.flask-action-menu .as-pop-cap { display: flex; flex-direction: column; gap: 0.3rem; }
.flask-action-menu .flask-action-detail { font-size: 1.15rem; }
/* An unavailable row has to LOOK unavailable, and these rows are
   `aria-disabled` rather than `disabled` — they stay focusable so a pad can
   land on them and read the reason, which is why `.as-row:disabled` alone was
   never enough. */
.flask-action[aria-disabled="true"] { color: var(--muted); cursor: default; }
.flask-action[aria-disabled="true"]:hover { background: transparent; }
.flask-action[aria-disabled="true"] .as-keycap { opacity: 0.5; }
.flask-action.gp-focus { background: var(--panel); color: var(--gold); }
/* THE WORD DOES NOT FIT AFTER A KEYCAP. A menu row already ends in the binding
   it answers to, and three letters of HOLD past that keycap is noise in a 20rem
   column — the same call the flask SLOT makes. The fill still runs (it is the
   background rule above) and the row's tooltip still says "Hold to drink", so
   nothing about the gesture is hidden, only the label for it. */
.flask-action .hold-hint { display: none; }
/* THE INSPECT DOOR is body B: the art's ArtWell sets the height, so a one-line
   flask and a two-line one are the same door (measured on the Crimson Flask /
   Round Shield pair, where they were not). */
.flask-inspect-art { width: 12rem; height: 12rem; font-size: 5.3rem; }
.flask-inspect-art .flask-identity, .flask-inspect-art .flask-art { width: 100%; height: 100%; }
.flask-inspect-lines .as-prose + .as-prose { color: var(--muted); font-size: 1.2rem; }
@media (max-width: 640px) {
  .flask-inspect-body { flex-direction: column; align-items: center; text-align: center; }
  .flask-inspect-lines { align-items: center; }
}

/* ═══ A SEGMENTED CONTROL MAY WRAP RATHER THAN LOSE ITS TAIL ═══════════════ */
/* Measured at 390x844 in the run's Settings pane (☰ → Settings → Game): the Map
   zoom row's seven options are 342 px of content inside a 311 px pane, so the
   last option — 200 — fell outside the pane, and `.as-seg`'s own
   `overflow: hidden` cut it in half. A choice the player cannot see is a choice
   they do not have, and the pane's `overflow-x: auto` is not the answer either:
   nothing on the row says there is more of it to the right.
   So on a narrow shape the group wraps. It grows downward, where the pane
   already scrolls on purpose, and every option keeps its own tap box. The
   separator between two buttons is dropped when it wraps because a border-left
   on the first button of a new line is a hairline hanging off nothing; the
   group's own border still says where the control begins and ends. */
@media (max-width: 700px) {
  /* The trail has to be ALLOWED to be narrower than its contents first: it is
     `flex: 0 0 auto` on a nowrap row, so the group was handed its max-content
     295 px inside a 311 px pane and never wrapped at all (measured; wrapping
     `.as-seg` alone changed nothing). */
  .as-row > .r-trail, .as-option > .r-trail { flex: 0 1 auto; min-width: 0; flex-wrap: wrap; justify-content: flex-end; }
  .as-seg { flex-wrap: wrap; }
  .as-seg button + button { border-left: 0; }
  .as-seg button { border: 1px solid var(--line-soft); margin: -1px 0 0 -1px; }
}

/* Move-set articles describe intent; they do not open or select an action. */
article.enemy-move-card.as-option { cursor: default; }
article.enemy-move-card.as-option:not(.is-selected):hover { border-color: var(--line); }

/* Painted full-body previews stay inside the armory art well. */
.armoury-sprite-pane:has(.painted-armoury) { position: relative; }
.armoury-sprite-pane .armoury-figure.painted-armoury { position: absolute; inset: 9%; width: 82%; height: 82%; max-width: none; max-height: none; aspect-ratio: auto; }
/* Potion disclosures use the same joined OptionCard/DetailCard as Armoury. */
.combat-potion-menu .potion-fold { margin-bottom: 6px; }
.combat-potion-menu .potion-fold > summary { cursor: pointer; }
.combat-potion-menu .potion-fold .flask-identity { flex: 0 0 44px; }
.combat-potion-menu .potion-fold .flask-art { width: 40px; height: 40px; }
.combat-potion-menu .potion-fold .potion-use { flex-shrink: 0; min-height: var(--tap-floor); }
.combat-potion-menu .potion-fold[open] > summary { border-color: var(--gold); }
.combat-potion-menu .potion-fold[open] > summary::after { transform: rotate(90deg); }
.combat-potion-menu .potion-fold > .as-detailcard .potion-use { margin-top: 8px; align-self: flex-end; }

/* Inspection copy stays readable even when the surrounding game scales down. */
.flask-inspect-lines .as-prose,
.combat-potion-menu .potion-fold > .as-detailcard .dc-line {
  font-size: max(1.6rem, calc(18px / var(--ui-zoom, 1))); line-height: 1.5;
}
.flask-inspect-lines .as-prose + .as-prose,
.combat-potion-menu .potion-fold > .as-detailcard .as-flavor,
.combat-potion-menu .potion-fold > .as-detailcard .dc-meta {
  font-size: max(1.4rem, calc(16px / var(--ui-zoom, 1))); line-height: 1.45; color: var(--parchment);
}
.flask-inspect-modal .modal-head-id h2,
.combat-potion-menu .potion-fold > .as-detailcard .dc-name {
  font-size: max(1.8rem, calc(20px / var(--ui-zoom, 1))); white-space: normal;
}
.flask-inspect-done { font-size: max(1.4rem, calc(16px / var(--ui-zoom, 1))); }

/* Preview wells keep a card silhouette; figure width derives from both available axes. */
.customize .cc-preview-side { min-height: 0; align-items: center; justify-content: flex-start; }
.customize .cc-preview-side > .cz-portrait {
  flex: none; width: min(100%, 30rem); height: auto; min-height: 0;
  aspect-ratio: 3 / 4; border: 1px solid var(--line); border-radius: var(--r-3);
}
.customize .cc-preview-fold { width: 100%; }
.armoury-sprite-pane.as-artwell { container-type: size; overflow: hidden; }
.armoury-sprite-pane .armoury-figure,
.armoury[data-responsive="phone"] .armoury-sprite-pane .armoury-figure {
  width: min(78cqw, calc(82cqh * 150 / 190), 22rem);
  height: auto; aspect-ratio: 150 / 190; max-width: none; max-height: none;
}

/* Pile readers use physical-size floors, independent of the scaled game canvas. */
.pile-body .as-cardgrid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, calc(150px / var(--ui-zoom, 1))), 1fr));
  gap: calc(18px / var(--ui-zoom, 1)); padding: calc(8px / var(--ui-zoom, 1)); justify-items: center;
}
.pile-body .card {
  --card-w: calc(220px / var(--ui-zoom, 1));
  width: 100%; max-width: var(--card-w); height: auto; aspect-ratio: var(--card-ratio);
  --card-name-h: calc(32px / var(--ui-zoom, 1)); --card-art-h: calc(52px / var(--ui-zoom, 1));
  --card-band-h: calc(20px / var(--ui-zoom, 1)); --card-gap: calc(5px / var(--ui-zoom, 1)); --card-pad: calc(8px / var(--ui-zoom, 1));
  margin: 0; transform: none;
}
.pile-body .card :is(.cname, .ctext) { font-size: calc(15px / var(--ui-zoom, 1)); line-height: 1.35; }
.pile-body .card .art { overflow: hidden; contain: layout paint; }
.pile-body .card .art img { width: 100%; height: 100%; object-fit: contain; }
.pile-body .card .ctag { font-size: calc(10px / var(--ui-zoom, 1)); }
.pile-body .card .cost { font-size: calc(14px / var(--ui-zoom, 1)); width: calc(24px / var(--ui-zoom, 1)); height: calc(24px / var(--ui-zoom, 1)); }
.modal:has(.pile-body) .modal-tab { font-size: calc(15px / var(--ui-zoom, 1)); min-height: var(--tap-floor); }

/* Read-only detail rows: let names claim 10–30%, then wrap the explanation. */
:is(.modal, .as-tip) div.as-row:not(.character-info-head):has(> .r-label):has(> .as-status),
.combatant-inspector-row {
  align-items: flex-start; white-space: normal; gap: 0.75rem;
  font-size: max(1.47rem, calc(15px / var(--ui-zoom, 1))); line-height: 1.45;
}
:is(.modal, .as-tip) div.as-row:not(.character-info-head):has(> .as-status) > .r-label,
.combatant-inspector-row:has(> .as-status) > .r-label {
  flex: 0 0 auto; width: max-content; min-width: 10%; max-width: 30%;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
:is(.modal, .as-tip) div.as-row:not(.character-info-head):has(> .r-label) > .as-status,
.combatant-inspector-row > .as-status {
  flex: 1 1 0; min-width: 0; margin-left: 0; font-size: inherit; line-height: inherit;
  white-space: normal; overflow-wrap: anywhere; overflow: visible; text-overflow: clip; text-align: left;
}
:is(.modal, .as-tip) .as-statrow { align-items: flex-start; font-size: max(1.4rem, calc(15px / var(--ui-zoom, 1))); }
:is(.modal, .as-tip) .as-statrow > .sr-id { flex: 0 0 auto; width: max-content; min-width: 10%; max-width: 30%; }
:is(.modal, .as-tip) .as-statrow .sr-name { font-size: inherit; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
:is(.modal, .as-tip) .as-statrow > .sr-vals { flex: 1 1 0; min-width: 0; margin-left: 0; justify-content: flex-start; text-align: left; }
:is(.modal, .as-tip) .as-statrow .sr-vals :is(.as-statpair, .sp-k, .sp-v) { white-space: normal; overflow-wrap: anywhere; font-size: inherit; }
.combatant-inspector-body .as-eyebrow { font-size: max(1rem, calc(12px / var(--ui-zoom, 1))); }

:is(.modal, .as-tip) .as-statrow .sr-hint { font-size: inherit; line-height: 1.45; overflow-wrap: anywhere; }

/* Readable HUD ink without scaling the surrounding layout or hit targets. */
.shared-hud .hud-info-row .ck { font-size: max(0.93rem, calc(10px / var(--ui-zoom, 1))); line-height: 1.302rem; letter-spacing: 0; }
.shared-hud .hud-info-row .cv { font-size: max(1.42rem, calc(13px / var(--ui-zoom, 1))); line-height: 1; }
.shared-hud .resplate .m-label { font-size: max(1.05rem, calc(11px / var(--ui-zoom, 1))); }
.shared-hud .resplate .m-value { font-size: max(1.15rem, calc(11px / var(--ui-zoom, 1))); }
.shared-hud .hud-actions .as-iconbtn { font-size: max(1.8rem, calc(18px / var(--ui-zoom, 1))); }
#tooltip[data-tooltip-variant='combatant-context'] { font-size: max(1.5rem, calc(15px / var(--ui-zoom, 1))); line-height: 1.1; }
#tooltip[data-tooltip-variant='combatant-context'] .tt-title { font-size: inherit; line-height: 1.1; }
#tooltip[data-tooltip-variant='combatant-context'] .tt-combatant-line { font-size: inherit; }
#tooltip[data-tooltip-variant='combatant-context'] .ti-detail { font-size: max(1.28rem, calc(12px / var(--ui-zoom, 1))); line-height: 1.2; margin-top: 0.3rem; padding-top: 0.3rem; }
#tooltip[data-tooltip-variant='combatant-context'] > .as-btn { font-size: max(1.2rem, calc(13px / var(--ui-zoom, 1))); line-height: 1.15; }
.combatant .intent.as-pill.lg { font-size: max(1.2rem, calc(14px / var(--ui-zoom, 1))); line-height: 1; padding-block: 0.2rem; letter-spacing: 0; }
.combatant .intent.as-pill.lg .as-glyph { font-size: inherit; }

/* Painted equipment: keep the silhouette legible in both collection and detail. */
.inventory-face .inventory-item-art,
.equip-chip > .as-artwell,
.es-cell > .as-artwell {
  background: radial-gradient(ellipse at 50% 35%, #373025, #18130f 76%);
  border-color: var(--line-soft);
  box-shadow: inset 0 0 20px #0005;
  overflow: hidden;
}
.inventory-item-art img, .equip-chip > .as-artwell img, .es-cell > .as-artwell img { padding: .35rem; }
.inventory-face { gap: 1rem; }
.inventory-face > .r-trail { flex-wrap: wrap; justify-content: flex-end; gap: .4rem; }
.inventory-detail {
  padding: 1.4rem;
  border: 1px solid var(--line-soft);
  border-radius: var(--r-3);
  background: linear-gradient(140deg, #241c16, #17120f);
}
.inventory-detail > .inventory-model {
  width: clamp(8.4rem, 16vw, 14rem); height: auto; aspect-ratio: 1;
  padding: .7rem; background: radial-gradient(ellipse at 50% 35%, #403527, #18130f 76%);
}
.inventory-information { min-width: 0; }
.inventory-information > .inventory-kind { margin-block: .45rem 1rem; }
.inventory-tags { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: 1rem; }
@media (max-width: 600px) {
  .inventory-face { gap: .65rem; flex-wrap: wrap; }
  .inventory-face > .inventory-item-art { width: 3.6rem; height: 3.6rem; }
  .inventory-face > .r-trail { width: 100%; justify-content: flex-start; }
  .inventory-detail { grid-template-columns: minmax(0, 1fr); padding: 1rem; }
  .inventory-detail > .inventory-model { width: min(100%, 13rem); justify-self: center; }
}
/* Let item names keep their space when ownership badges wrap. */
.inventory-face.as-face { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; }
.inventory-face.as-face > .inventory-item-art { grid-column: 1; grid-row: 1 / span 2; }
.inventory-face.as-face > .ob { grid-column: 2; grid-row: 1; }
.inventory-face.as-face > .r-trail { grid-column: 2; grid-row: 2; justify-content: flex-start; width: auto; margin-left: 0; }
.inventory-face .inventory-name { white-space: normal; overflow-wrap: anywhere; }
.inventory-face .on { white-space: normal; overflow: visible; text-overflow: clip; }
/* Compact inventory panes must yield before their text loses its padding. */
.armoury .as-statrow { flex-wrap: wrap; gap: 0.4rem 1rem; }
.armoury .as-statrow .sr-id { flex: 1 1 10rem; width: auto; max-width: 100%; }
.armoury .as-statrow .sr-vals { flex: 1 1 8rem; min-width: 0; max-width: 100%; }
.armoury .as-statpair { flex-wrap: wrap; }
.armoury .as-statpair .sp-k, .armoury .as-statpair .sp-v { max-width: 100%; }
.armoury-position-grid-card > .ob { width: 100%; }
.armoury-stats-summary { grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr)); }

/* Armoury: one scrolling page, with natural-height equipment and disclosures. */
.armoury > .modal-body.armoury-shell-body { overflow-y: auto; overflow-x: hidden; padding: max(1.2rem, calc(12px / var(--ui-zoom, 1))); gap: 1.4rem; }
.armoury .armoury-content, .armoury .armoury-body, .armoury .armoury-left, .armoury .armoury-right, .armoury .armoury-trays { min-height: 0; height: auto; max-height: none; overflow: visible; }
.armoury .armoury-content { display: flex; flex: 0 0 auto; flex-direction: column; gap: 1.4rem; }
.armoury .armoury-body { padding: 0; flex: none; gap: 1.6rem; }
.armoury[data-pane='both'] .armoury-body { grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr); gap: 1.6rem; }
.armoury[data-pane='both'] .armoury-left, .armoury[data-pane='both'] .armoury-right { grid-column: auto; padding: 0; }
.armoury[data-pane='inventory'] .armoury-content { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: start; gap: 1.6rem; }
.armoury[data-pane='inventory'] .armoury-inventory { grid-column: 2; grid-row: 1; overflow: visible; }
.armoury .armoury-trays { display: grid; gap: 1.4rem; flex: none; }
.armoury .armoury-section { min-width: 0; margin: 0; border: 1px solid var(--line); border-radius: var(--r-2); background: var(--panel); overflow: hidden; }
.armoury .armoury-section-head { display: flex; align-items: center; gap: 0.8rem; padding: 1rem; min-height: calc(44px / var(--ui-zoom, 1)); box-sizing: border-box; color: var(--gold); }
.armoury .armoury-section-head > :first-child { margin: 0; flex: 1; font-size: max(1.5rem, calc(15px / var(--ui-zoom, 1))); }
.armoury summary.armoury-section-head { cursor: pointer; list-style: none; }
.armoury summary.armoury-section-head::-webkit-details-marker { display: none; }
.armoury summary.armoury-section-head::after { content: '›'; font-size: 1.8rem; }
.armoury details[open] > summary.armoury-section-head::after { transform: rotate(90deg); }
.armoury details[open] > summary.armoury-section-head { border-bottom: 1px solid var(--line-soft); }
.armoury .armoury-section-content, .armoury .equip-slots { padding: 1rem; }
.armoury .armoury-help { padding: 0 1rem; margin: 0 0 1rem; color: var(--muted); }
.armoury .armoury-section :is(.as-btn, .as-status, .as-prose), .armoury .inventory-information { font-size: max(1.2rem, calc(14px / var(--ui-zoom, 1))); }
.armoury .armoury-position-card.is-locked { padding: 0.8rem; opacity: 0.65; }
.armoury .armoury-character { display: flex; flex-direction: column; height: auto; gap: 1rem; }
.armoury .armoury-sprite-pane.as-artwell { flex: none; height: calc(190px / var(--ui-zoom, 1)); min-height: 0; }
.armoury .armoury-character :is(.armoury-fold, details) > summary { min-height: calc(48px / var(--ui-zoom, 1)); height: auto; }
.armoury .armoury-character summary :is(.as-status, .fold-summary) { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.armoury .armoury-character summary { overflow: hidden; }
.armoury .armoury-position-summary .on { font-size: max(1.5rem, calc(15px / var(--ui-zoom, 1))); }
.armoury .armoury-position-summary :is(.od, .om) { font-size: max(1.1rem, calc(12px / var(--ui-zoom, 1))); }
.armoury[data-responsive='phone'] .armoury-content, .armoury[data-responsive='phone'] .armoury-body { display: flex; flex-direction: column; gap: 1.2rem; }
.armoury[data-responsive='phone'] .armoury-left, .armoury[data-responsive='phone'] .armoury-right { width: 100%; flex: none; }
.armoury[data-responsive='phone'][data-pane='character'] .armoury-right, .armoury[data-responsive='phone'][data-pane='inventory'] .armoury-right { display: none; }
.armoury[data-responsive='phone'] .armoury-section-head .as-btn { padding-inline: 0.6rem; }
.armoury details.character-info-card > summary > .character-info-head { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0.3rem 0.8rem; height: auto; min-height: calc(48px / var(--ui-zoom, 1)); padding: 0.7rem; }
.armoury details.character-info-card > summary > .character-info-head > .character-info-label { width: auto; max-width: none; font-size: max(1.4rem, calc(15px / var(--ui-zoom, 1))); white-space: normal; }
.armoury details.character-info-card > summary > .character-info-head > .as-status { grid-column: 2; width: 100%; max-width: none; margin: 0; white-space: normal; overflow: visible; overflow-wrap: anywhere; text-align: left; font-size: max(1.2rem, calc(15px / var(--ui-zoom, 1))); }
.armoury details.character-info-card > summary { height: auto; padding: 0; }
.armoury .armoury-position-card.is-locked { display: flex; flex-direction: row; align-items: center; gap: 1rem; }
.armoury .armoury-position-card.is-locked > .glyph { flex: 0 0 2rem; margin: 0; font-size: 1.6rem; }
.armoury .armoury-position-card.is-locked .on { font-size: max(1.2rem, calc(13px / var(--ui-zoom, 1))); }
.armoury .armoury-selection-context { display: flex; flex-wrap: wrap; align-items: center; gap: 0.7rem; padding: 1rem; border-bottom: 1px solid var(--line); }
.armoury .armoury-section, .armoury .armoury-left, .armoury .armoury-right, .armoury .armoury-character-stats, .armoury .character-info-card { box-sizing: border-box; }
.armoury[data-pane='character'][data-responsive='desktop'] .armoury-character { display: grid; grid-template-columns: minmax(0, .65fr) minmax(0, 1.35fr); align-items: start; }
.armoury[data-pane='character'][data-responsive='desktop'] .character-summary { grid-column: 1 / -1; }
.armoury[data-pane='character'][data-responsive='desktop'] .armoury-sprite-pane.as-artwell { height: calc(300px / var(--ui-zoom, 1)); }
.armoury .armoury-character-stats { overflow: visible; }
.armoury[data-responsive='phone'] .armoury-body { width: 100%; max-width: 100%; padding: 0; }
.armoury[data-pane='character'][data-responsive='desktop'] .armoury-character-stats { grid-column: 2; grid-row: 2; }
.armoury .armoury-position-values strong { white-space: normal; overflow: visible; text-overflow: clip; }
.armoury summary.armoury-section-head::after { content: '▸'; }
.armoury details[open] > summary.armoury-section-head::after { content: '▾'; transform: none; }
.armoury .armoury-sprite-pane.as-artwell { display: flex; align-items: center; justify-content: center; }

/* Dedicated Armoury pages keep deck faces and inventory out of folding trays. */
.armoury[data-pane='inventory'] .armoury-content { display: flex; }
.armoury .armoury-inventory, .armoury .armoury-strip { width: 100%; min-width: 0; padding: 0; border: 0; overflow: visible; }
.armoury .armoury-shell-body > *, .armoury .armoury-trays { flex: 0 0 auto; }
.armoury-card-gallery { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, calc(190px / var(--ui-zoom, 1))), calc(230px / var(--ui-zoom, 1)))); justify-content: center; align-items: start; gap: calc(20px / var(--ui-zoom, 1)); padding: calc(14px / var(--ui-zoom, 1)) 0; }
.armoury-card-gallery > .card { --card-w: 100%; width: 100%; height: auto; aspect-ratio: var(--card-ratio); min-height: 0; max-height: none; transform: none; margin: 0; box-sizing: border-box; --card-art-h: 5rem; --card-name-h: 3.5rem; }
.armoury-card-gallery .cname { font-size: max(1.6rem, calc(16px / var(--ui-zoom, 1))); line-height: 1.2; white-space: normal; }
.armoury-card-gallery .ctext { font-size: max(1.5rem, calc(15px / var(--ui-zoom, 1))); line-height: 1.35; }
.armoury-card-gallery .cost { font-size: max(1.5rem, calc(15px / var(--ui-zoom, 1))); }
.armoury-card-gallery .ctype, .armoury-card-gallery .ctag { font-size: max(.9rem, calc(10px / var(--ui-zoom, 1))); }
/* CONTAINMENT, AND WHERE IT STOPS.
   `contain: layout paint` on the art wells below is free: they already declare
   `overflow: hidden`, so paint containment clips nothing that was not already
   clipped, and their only child is a statically positioned <img>, so layout
   containment steals no containing block from anyone. What it buys is that a
   sprite swapping frames inside one card cannot dirty the layout or the paint
   of the gallery around it — which on a phone scrolling forty cards is the
   difference between one invalidated box and forty.

   It stops at the well, and the stopping point is the load-bearing part.
   ui/cardEffectLayers.js mounts its planes absolutely, at percentage offsets
   under `translate(-50%,-50%)`, DELIBERATELY extending past the face they
   decorate — the overhang is the effect. Today they mount on `.card-flight`
   (combat.js), a fixed-position wrapper on <body> that holds a card ghost, so
   the wells below never host one and this containment is provably inert.
   That is a fact about the current call site, not a property of the selector:
   lifting `contain` to `.card` — or mounting a layer inside `.art` — would
   clip every hit spark to a border, and would look harmless in review because
   nothing else in this file mentions the layers. Hence this paragraph. */
.armoury-card-gallery .art { min-height: calc(55px / var(--ui-zoom, 1)); overflow: hidden; contain: layout paint; }
/* W1e / W1n — THE ARMOURY ON THE W1 SHELL. One title and the close in the
   head; the views are the kit's W1 category navigation (a rail beside the
   pane on wide hosts, one [Category ▾] selector above it on compact ones);
   the footer holds Back and the selected item's action. Only the pane, or
   the pane's two columns, scrolls. */
.armoury > .modal-body.armoury-shell-body { display: flex; flex-direction: column; overflow: hidden; padding: 0; gap: 0; }
.armoury .armoury-shell-body > .as-railed.armoury-railed { flex: 1 1 auto; min-height: 0; height: auto; }
.armoury .armoury-railed > .armoury-pane { display: flex; flex-direction: column; gap: 1.4rem; min-height: 0; overflow-y: auto; overflow-x: hidden; }
/* The active view fills the pane. The inventory page's `align-items: start`
   (written for its old two-column grid) shrank Equipment to its content. */
.armoury .armoury-railed .armoury-content > .armoury-body { align-self: stretch; }
/* The active pane's split: the item collection beside (wide) or above (phone)
   the selected item's detail. Tracks come from wireframeUi.armoury through
   --armoury-collection-track / --armoury-detail-track (equipment.js draw). */
.armoury[data-view='hybrid'] .armoury-railed > .armoury-pane { overflow: hidden; }
.armoury[data-view='hybrid'] .armoury-content,
.armoury[data-view='hybrid'] .armoury-inventory { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; overflow: hidden; }
.armoury .armoury-inventory > .ep-list.armoury-split {
  flex: 1 1 auto; min-height: 0; display: grid; column-gap: 1.2rem;
  grid-template-columns: minmax(0, var(--armoury-collection-track, 1fr)) minmax(0, var(--armoury-detail-track, 1fr));
  grid-template-rows: auto minmax(0, 1fr);
}
.armoury .ep-list.armoury-split > .armoury-selection-context { grid-column: 1 / -1; grid-row: 1; }
.armoury .ep-list.armoury-split > .armoury-item-collection { grid-column: 1; grid-row: 2; }
.armoury .ep-list.armoury-split > .armoury-item-detail { grid-column: 2; grid-row: 2; border-left: 1px solid var(--line); padding-left: 1.2rem; }
.armoury .ep-list.armoury-split > :is(.armoury-item-collection, .armoury-item-detail) { min-width: 0; min-height: 0; overflow-y: auto; overflow-x: hidden; }
.armoury .ep-list.armoury-split.is-empty > .armoury-item-collection { grid-column: 1 / -1; }
.armoury[data-split='rows'] .armoury-inventory > .ep-list.armoury-split {
  grid-template-columns: minmax(0, 1fr); row-gap: 0.8rem;
  grid-template-rows: auto minmax(0, var(--armoury-collection-track, 1fr)) minmax(0, var(--armoury-detail-track, 1fr));
}
.armoury[data-split='rows'] .ep-list.armoury-split > .armoury-item-detail { grid-column: 1; grid-row: 3; border-left: 0; padding-left: 0; border-top: 1px solid var(--line); padding-top: 0.8rem; }
/* Before a selection the phone detail row holds one line of instruction; a
   half-height track for it squeezed the item list to a sliver. It sizes to
   its content until an item is chosen. */
.armoury[data-split='rows'] .armoury-inventory > .ep-list.armoury-split:not([data-detail-open='true']) {
  grid-template-rows: auto minmax(0, 1fr) auto;
}
.armoury .armoury-item-detail > .disc-reveal { margin: 0; }
/* The shared equipment inspection is laid out for a 450px face (220px art
   beside its details) and stacks only under the phone media rule below. The
   Armoury's detail columns can be narrower than that on any viewport, so they
   stack it the same way, measured on the column itself (a container query). */
.armoury :is(.armoury-item-detail, .armoury-position-grid-detail) { container: armoury-detail / inline-size; }
/* Scoped under .armoury so these outrank the base inspection rules that come
   later in this file (same specificity would lose on source order). */
@container armoury-detail (max-width: 480px) {
  .armoury .card-inspection-layout { grid-template-columns: minmax(0, 1fr); gap: 16px; padding: 16px; }
  .armoury .card-inspection-art { width: 100%; max-width: 100%; margin-inline: auto; }
  .armoury .card-inspection-layout > .card-inspection-art { flex-basis: 100%; }
  .armoury .card-inspection-art > .card { width: 160px; height: auto; aspect-ratio: var(--card-ratio); margin-inline: auto; }
  .armoury .card-inspection-art > .equipment-poker-card { width: 100%; height: auto; margin-inline: auto; }
  .armoury .card-inspection-art > .inspection-tags { justify-content: center; }
}
.armoury .armoury-selection-facts { display: flex; flex-direction: column; gap: 0.4rem; margin-block: 0.6rem; }
.armoury .armoury-selection-facts > .as-status { white-space: normal; overflow: visible; }
/* W1e Equipment in its grid presentation: the slot tiles beside their detail. */
.armoury .armoury-position-grid-groups {
  display: grid; gap: 1.2rem; align-items: start;
  grid-template-columns: minmax(0, var(--armoury-collection-track, 1fr)) minmax(0, var(--armoury-detail-track, 1fr));
}
.armoury .armoury-position-grid-groups > .armoury-position-grid-detail { position: sticky; top: 0; }
.armoury[data-split='rows'] .armoury-position-grid-groups { grid-template-columns: minmax(0, 1fr); }
.armoury[data-split='rows'] .armoury-position-grid-groups > .armoury-position-grid-detail { position: static; }
/* Approved 350 × 490 poker canvas. Every measurement scales together. */
/* HOW BIG A CARD IS, IN ONE PLACE. `--epc-level-w` is written by
   renderEquipmentCard from content/config/ui/components/card.json
   (sizing.levels, via src/ui/models/CardSizeModel.js) — one of glance, focus
   or inspect. NINE SURFACES USED TO EACH WRITE THEIR OWN `max-width` HERE and
   the card came out whatever that number happened to be: 152 / 180 / 220 /
   280 / uncapped, a 2.05x spread on one renderer, with INSPECT SMALLER THAN
   FOCUS. Do not add a per-surface `max-width` back; declare the level at the
   render call, or a variant of one in the JSON. */
.equipment-poker-card { display:block; position:relative; width:100%; max-width:var(--epc-level-w, var(--card-w-focus, 280px)); aspect-ratio:5/7; color:#eee3ca; text-align:left; margin-inline:auto; }
.equipment-poker-card .epc-frame { box-sizing:border-box; display:grid; grid-template-rows:25px minmax(0,1fr) 25px 39px 19px 82px 36px 13px; position:absolute; inset:0 auto auto 0; width:var(--epc-frame-w,350px); height:var(--epc-frame-h,490px); padding:var(--epc-pad,18px); gap:var(--epc-gap,5px); border:1px solid var(--accent); border-radius:13px; transform-origin:top left; transform:scale(var(--card-scale,1)); box-shadow:0 9px 26px #0006,inset 0 0 0 4px #17120e,inset 0 0 0 5px #695334; background:linear-gradient(125deg,#302319,#19150f 45%,#251d15); font:13px/1.35 Arial,sans-serif; }
.epc-frame > * { min-width:0; min-height:0; box-sizing:border-box; }
.epc-name { display:flex; justify-content:space-between; gap:8px; align-items:center; font:600 20px/1.12 Georgia,serif; color:#f0dfaf; }
.epc-name > span { font-size:13px; color:var(--accent); }
.epc-art { display:grid; place-items:center; border:1px solid #665238; border-radius:5px; background:radial-gradient(ellipse at 50% 40%,#403322,#18140f 75%); }
.epc-art img { box-sizing:border-box; height:100%; width:100%; min-height:0; object-fit:contain; padding:6px; filter:drop-shadow(0 3px 5px #0006); }
.epc-type { font:600 11px/1.2 Arial,sans-serif; text-align:center; text-transform:uppercase; letter-spacing:.12em; padding:5px 2px; color:var(--accent); background:color-mix(in srgb,var(--accent) 13%,#1c1813); border-block:1px solid #665238; }
.epc-facts { display:flex; justify-content:space-around; gap:5px; padding:5px 0; border-bottom:1px solid #5e4c32; }
.epc-fact { font:10px/1.1 Arial,sans-serif; text-transform:uppercase; text-align:center; color:#b1a38c; }
.epc-fact strong { font:600 16px Georgia,serif; display:block; color:#eee3ca; margin-bottom:2px; }
.epc-tags { display:flex; gap:5px; flex-wrap:wrap; align-items:center; }
.epc-tag { font:10px/1.1 Arial,sans-serif; text-transform:uppercase; letter-spacing:.05em; color:#d1ba82; border:1px solid #615033; border-radius:3px; padding:3px 5px; }
.epc-effects { padding:3px 0; font:13px/1.35 Arial,sans-serif; color:#e5dccb; }
.epc-heading { display:block; font:10px Arial,sans-serif; text-transform:uppercase; letter-spacing:.12em; color:#bca779; margin:0 0 5px; }
.epc-bonus { display:block; }
.epc-bonus + .epc-bonus { margin-top:3px; }
.epc-flavor { font:italic 12px/1.3 Georgia,serif; color:#baa98c; padding-top:6px; border-top:1px solid #5e4c32; }
.epc-footer { display:flex; justify-content:space-between; gap:8px; font:10px/1.1 Arial,sans-serif; color:#ac9c7f; text-transform:uppercase; letter-spacing:.04em; }
/* WC2: metadata only — rarity at the start, the owned count at the end. */
.epc-footer > [data-meta-kind="rarity"] { color:var(--accent); }
.epc-footer > [data-meta-slot="end"] { margin-inline-start:auto; }
.epc-footer > [data-meta-kind="owned"] { text-transform:none; }
/* WC2a body row one: slot, then requirements, in the one type band. */
.epc-type { display:flex; justify-content:center; align-items:center; gap:.5em; }
.epc-type > .epc-requirement { text-transform:none; letter-spacing:.02em; font-weight:400; color:#d1ba82; }
.epc-type > .epc-requirement::before { content:'·'; margin-inline-end:.5em; color:var(--accent); }
.epc-type > span { min-width:0; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
.epc-type > span:first-child { flex:0 1 auto; }
.epc-type > .epc-requirement { flex:0 1 auto; }
.equipment-poker-card [tabindex]:focus-visible { outline:var(--focus-ring); outline-offset:var(--focus-ring-offset); }
.equipment-poker-inspection { min-width:0; width:100%; }
.equipment-poker-explanations { margin-top:12px; font:14px/1.5 Arial,sans-serif; color:#eee3ca; text-align:left; overflow-wrap:anywhere; }
.equipment-poker-explanations summary { cursor:pointer; padding:12px; border:1px solid #74603c; border-radius:5px; background:#221b14; }
.equipment-poker-explanations dt { font-weight:bold; color:#e4ca80; margin-top:12px; }
.equipment-poker-explanations dd { margin:4px 0 12px; }
.inventory-face.as-face.poker-inventory-face { display:flex; flex-direction:column; align-items:stretch; width:100%; min-width:0; }
.inventory-face.poker-inventory-face > .r-trail { justify-content:flex-start; margin-top:10px; }
.inventory-detail.poker-inventory-detail { display:flex; flex-direction:column; align-items:stretch; min-width:0; }
.equip-chip.poker-equipment-choice { display:block; width:100%; padding:6px; }
/* Cards share the existing disclosure navigation and reveal placement. */
.armoury .disc-faces:has(.poker-inventory-face) { display:flex; flex-flow:row wrap; align-items:flex-start; }
.armoury .disc-reveal:has(.poker-inventory-detail) { flex-basis:100%; min-width:0; }

/* The merchant shares the same equipment canvas; prices stay outside the card. */
/* THE FACE IS THE TILE. The offer used to be a 280px OptionCard — panel,
   border and 8px of padding — wrapped around a card rendered at the 152px
   resting width, so more than half of every tile was empty box and two tiles
   did not fit the offers column. The shelf sizes the tile now, and the
   OptionCard's chrome appears only when it means something: under the pointer,
   under focus, or on the offer the pane is describing. It is the same
   `.as-option` element with the same hit target, inspection and outline. */
.shop-armament-offer { min-width:0; }
.shop-armament-offer > .as-option { display:block; width:100%; padding:0; background:none; border-color:transparent; }
.shop-armament-offer > .as-option:is(:hover, :focus-visible, :focus-within) { background:var(--panel); border-color:var(--line); }
.shop-offer.shop-armament-offer.is-selected > .as-option { background:var(--panel); border-color:var(--line); }
.shop-armament-offer > .as-status { display:block; margin-top:8px; white-space:normal; }

/* Playing cards share the equipment motif without changing the hand's geometry. */
.card.playing-poker-card {
  --playing-frame:#b39760;
  background:linear-gradient(125deg,#302319,#19150f 45%,#251d15);
  border-color:var(--playing-frame);
  box-shadow:0 6px 18px #0006,inset 0 0 0 3px #17120e,inset 0 0 0 4px #695334;
  color:#eee3ca;
}
.playing-poker-card.rarity-uncommon { --playing-frame:#87b5d0; }
.playing-poker-card.rarity-rare { --playing-frame:#e0bd68; }
.playing-poker-card.rarity-special { --playing-frame:#b78ec9; }
.card.playing-poker-card .cname { color:#f0dfaf; font-family:Georgia,serif; text-align:left; padding-inline:3px; }
.card.playing-poker-card.upgraded .cname { color:var(--green); }
.card.playing-poker-card .art {
  background:radial-gradient(ellipse at 50% 40%,#403322,#18140f 75%);
  border-color:#665238; box-shadow:inset 0 0 8px #0005;
}
.card.playing-poker-card .ctype {
  color:color-mix(in srgb,var(--card-type-color,#d0ac5d) 40%,#f0dfaf);
  background:color-mix(in srgb,var(--card-type-color,#d0ac5d) 13%,#1c1813);
  border-block:1px solid #665238; letter-spacing:.12em;
}
.card.playing-poker-card .ctags { justify-content:flex-start; }
.card.playing-poker-card .ctag { border-radius:3px; background:#221b14; }
.card.playing-poker-card .ctext { text-align:left; color:#e5dccb; }
.card.playing-poker-card .cost { background:#211a12; border-color:#d0ac5d; color:#f0dfaf; box-shadow:0 2px 5px #0008,inset 0 0 0 2px #17120e; }
/* A CHOSEN CARD IS NOT MERELY A SELECTED ONE, and this gold ring stands down
   for it (`:not(.is-chosen)`) rather than out-specifying the green one by
   accident of where it sits in the file. Focus keeps the gold ring on a chosen
   card too — where you are and what you picked are two different facts. */
.card.playing-poker-card:is(.selected,[data-selected='true']):not(.is-chosen),
.card.playing-poker-card:focus-visible {
  border-color:#f2d87e; box-shadow:0 0 0 2px #d0ac5d,0 0 18px #d0ac5d66,inset 0 0 0 3px #17120e,inset 0 0 0 4px #b39760;
}
/* The chosen ring on a poker card, restated at the weight the gold one carries
   so the two cannot trade places: the green edge, the card's own inset bevel. */
.card.playing-poker-card.is-chosen {
  border-color:var(--green);
  box-shadow:0 0 0 2px var(--green),0 0 18px color-mix(in srgb, var(--green) 40%, transparent),inset 0 0 0 3px #17120e,inset 0 0 0 4px color-mix(in srgb, var(--green) 45%, #b39760);
}

/* One fixed grid track size prevents the last row from stretching its cards. */
/* THE PICKER IS STYLED BY ITS STATE, NOT BY WHAT IS INSIDE IT (2026-09-18).
   `.cc-card-selectors` used to be matched here by `:has(.poker-equipment-choice)`
   — a content sniff. `:has()` takes the specificity of its most specific
   argument, so that selector (0,3,0) and the section's own (0,4,0) both
   outranked `.as-options[data-view="grid"]` (0,2,0) — which, before this
   change, was the only rule in the kit that `data-view` drove at all (the
   picker's own two branches below are the rules this change adds).
   The screen wrote the player's choice onto the container
   (customize.js) and the stylesheet then ignored it, so List and Grid rendered
   the same grid — and there was no `[data-view="list"]` rule anywhere, so even
   at equal specificity List had nothing to apply. The toggle had one authored
   state, not two.
   The picker arm therefore reads the container's own `data-view` now. The
   armoury and shop shelves have no toggle and keep the sniff; consolidating
   those onto one mechanism belongs with the card-size work, not here. */
/* THE MERCHANT'S SHELVES LEFT THIS RULE, 2026-09-20. `#shop-armaments`,
   `#shop-relics` and `#shop-flasks` were `auto-fill` tracks of up to 280px —
   the FOCUS width, for cards drawn at the 152px resting width — so the offers
   column fitted two tracks at best and one below 360px, with the rest behind a
   scroll. They are `.card-shelf` now, which is the one place that answers how
   many resting cards a row holds. The armoury's faces and the creation
   picker's grid keep the rule: their items are not resting cards. */
.armoury .disc-faces:has(.poker-inventory-face),
.cc-card-selectors[data-view="grid"] {
  display:grid; grid-template-columns:repeat(auto-fill,minmax(0,280px));
  justify-content:center; align-items:start; gap:16px;
}
.armoury .disc-faces > .disc-face.disc-item:has(.poker-inventory-face) { width:100%; max-width:none; min-width:0; }
.armoury .disc-reveal:has(.poker-inventory-detail) { grid-column:1 / -1; }
.epc-art-glyph { font-size:90px; filter:drop-shadow(0 5px 10px #0008); }
.collectible-poker-card .epc-frame { grid-template-rows:45px minmax(0,1fr) 25px 39px 19px 100px 36px 13px; }
/* THE SAME BARGAIN THE ARMAMENT OFFER MAKES, and for a second reason here:
   `box-sizing` is border-box repo-wide, so the tile's 8px of padding and its
   border came OUT of the shelf's track — a relic's face was drawn at 136px on
   a 152px track while an armament's filled it, and the primitive's promise
   that a resting card is drawn at its resting width was false on two of the
   merchant's five shelves. Measured at 1328x744 before this: armaments 152,
   relics and flasks 136. So the face is the tile here too, and the OptionCard's
   panel appears under the pointer, under focus, or on the offer being
   described. The body keeps its own gap from the face rather than borrowing
   the tile's padding for it. */
.shop-collectible-offer { display:flex; flex-direction:column; width:100%; min-width:0; padding:0; background:none; border-color:transparent; }
.shop-collectible-offer:is(:hover, :focus-visible, :focus-within) { background:var(--panel); border-color:var(--line); }
.shop-offer.shop-collectible-offer.is-selected { background:var(--panel); border-color:var(--line); }
/* `:root` for the weight, not for the root. A narrow shape re-declares
   `.class-pick`'s padding and centring at (0,3,0) — styles/ui.css, for a
   COLUMN of text OptionCards, which is not what a tile on a card shelf is —
   and the phone kept the 16px inset that had just been taken off the desktop:
   measured 106px faces on a 137px track. One class more, so a shelf answers
   for its own tiles at every width. */
:root .card-shelf > .shop-collectible-offer { padding:0; align-items:stretch; }
.shop-collectible-offer > .equipment-poker-card { width:100%; }
.shop-collectible-offer > .cp-body { width:100%; margin-top:8px; }
.beat-hold[data-hold='holding'] .equipment-poker-card::after {
  content:''; position:absolute; inset:0; z-index:5; pointer-events:none; border-radius:12px;
  background:linear-gradient(90deg,#e8bc4b55,#f6d46f88);
  clip-path:inset(0 calc((1 - var(--hold,0)) * 100%) 0 0);
  box-shadow:inset 0 0 0 3px #f6d46f;
}
@media(max-width:360px) {
  .armoury .disc-faces:has(.poker-inventory-face), .cc-card-selectors[data-view="grid"] { grid-template-columns:minmax(0,280px); }
}

/* ═══ W1d / W1v — THE MERCHANT AS A WORKSPACE (screens/shop.js) ═══════════
   One frame under the run band: W0 head, a category rail beside the pane
   (above it on compact hosts), the W1v pane of offers and the selected
   offer's detail, and the W0 foot. The page never scrolls; only the offers
   and the detail do. ShopWorkspaceModel.js owns every number and shop.js
   writes them as custom properties and data attributes. */
.screen.shop-workspace { overflow: hidden; gap: 0; padding-inline: 2.5%; padding-bottom: 1rem; }
.shop-frame {
  --modal-inset: 1.4rem;
  display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0;
  width: 100%;
  background: var(--bg-raise); border: 1px solid var(--line); border-radius: var(--r-4); overflow: hidden;
}
.shop-frame > .modal-head { padding-block: 0.6rem; }
/* The rail is its own scrollport: on a short host its items scroll inside it
   and never run under the foot (measured at 844x390, where they did). */
.shop-frame > .shop-railed { flex: 1 1 auto; min-height: 0; height: auto; grid-template-columns: var(--shop-rail-width, 17.25rem) minmax(0, 1fr); grid-template-rows: minmax(0, 1fr); }
.shop-frame > .shop-foot-host > .modal-foot { padding-block: 0.6rem; }
:root .shop-workspace[data-shop-rail='side'] .shop-railed > .shop-rail { flex-direction: column; overflow-x: hidden; overflow-y: auto; }
/* Compact: the kit's [Category ▾] selector above the pane (kit.css W1
   CATEGORY NAVIGATION); data-shop-rail mirrors its decision. */
:root .shop-workspace[data-shop-rail='top'] .shop-railed { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }
.shop-rail > .as-railitem.with-status > .as-status { color: inherit; opacity: 0.8; }
.shop-pane { min-height: 0; overflow: hidden; padding-block: 0.8rem; }
.as-pane > .shop-pane-head { flex: 0 0 auto; margin-bottom: 0.6rem; }
.as-pane > .shop-body { flex: 1 1 auto; min-height: 0; display: grid; gap: var(--shop-gap, 1rem); }
.shop-workspace[data-shop-pane='columns'] .shop-body { grid-template-columns: minmax(0, var(--shop-offers-fr, 1fr)) minmax(0, var(--shop-detail-fr, 1fr)); }
.shop-workspace[data-shop-pane='stacked'] .shop-body { grid-template-rows: minmax(0, 1fr) auto; }
.shop-workspace[data-shop-pane='stacked'] .shop-detail { max-height: var(--shop-detail-max, 50%); }
.shop-body > .shop-detail[hidden] { display: none; }
/* The two scrollports. `scrollbar-color` inherits from `.screen`, so the
   product thumb reads the same here as on every other page. */
.shop-offers, .shop-detail { min-height: 0; overflow: auto; overscroll-behavior: contain; scrollbar-width: thin; }
.shop-offers > .shop-shelf { max-width: none; width: 100%; padding: 2px; }
.shop-offers > .shop-services { display: flex; flex-direction: column; gap: 1rem; }
.shop-offers > .shop-services .class-row, .shop-offers > .shop-services > #shop-remove { max-width: none; }
/* A narrow `.class-row` stacks as a column, where the kit's `flex: 1 1 26rem`
   on `.class-pick` becomes a 260 px HEIGHT: the remove, smith and sell tiles
   each stood a third of a phone tall around one line of text (measured at
   390x844). In the offers a tile keeps its content height. */
:root[data-layout='narrow'] .shop-offers .class-row > .class-pick { flex: 0 0 auto; }
.shop-offer.is-selected > .card, .shop-offer.is-selected > .shop-inspect-card, .shop-offer.is-selected > div > .card { outline: 2px solid var(--gold); outline-offset: 3px; }
.shop-offer.class-pick.is-selected { border-color: var(--gold); box-shadow: 0 0 0 1px rgba(var(--accent-rgb), 0.35), 0 0 18px rgba(var(--accent-rgb), 0.18); }
.shop-offer-avail { display: block; white-space: normal; color: var(--muted); }
.shop-detail { display: flex; flex-direction: column; gap: 0.5rem; padding: 0.9rem 1rem; border: 1px solid var(--line); border-radius: var(--r-2); background: var(--panel); }
.shop-detail > .shop-detail-name { margin: 0; }
.shop-detail > .shop-detail-desc { margin: 0; color: var(--ink-dim); font-size: 1.3rem; line-height: 1.4; }
.shop-detail > .as-status { white-space: normal; text-align: left; align-self: flex-start; }
.shop-detail > .shop-detail-price { color: var(--gold); }
.shop-detail > .shop-detail-avail.is-available { color: var(--parchment); }
/* Shared inspection: 270px artwork + 180px information within the 450px face. */
.equipment-poker-card .epc-frame,
.collectible-poker-card .epc-frame { padding:var(--epc-pad,19px); gap:var(--epc-gap,3px); }
.epc-frame > .epc-name { position:absolute; z-index:1; top:26px; left:27px; right:27px; min-height:29px; height:auto; display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; background:#211a12dd; padding:3px 6px; }
.epc-name > span { display:none; }
.epc-art img { padding:35px 8px 8px; }
.epc-frame > :is(.epc-type,.epc-flavor), .epc-footer > span { overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
/* A BONUS IS THE MECHANIC, SO IT WRAPS BEFORE IT TRUNCATES. `nowrap` + ellipsis
   cut real numbers mid-word — "Class power: +1 Poten…" — which is worse than a
   second line. It still ends in an ellipsis once it passes the authored line
   budget, so a pathological string cannot push the footer off the card. */
.epc-bonus { display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:var(--epc-bonus-lines,2); overflow:hidden; white-space:normal; }

/* TYPE SCALES WITH THE CARD, AND CLAMPS RATHER THAN CLIPS. Each size is a
   clamp(min, ideal, max) emitted from balance.ui.equipmentCard.text; the ideal
   is in `cqw` against the frame's own inline size, so a face dropped into a
   narrow column shrinks its own type instead of cutting the line off. */
.epc-name { font-size:var(--epc-text-name, 20px); }
.epc-type { font-size:var(--epc-text-type, 11px); }
.epc-fact { font-size:var(--epc-text-fact, 10px); }
.epc-fact strong { font-size:var(--epc-text-factValue, 16px); }
.epc-tag { font-size:var(--epc-text-tag, 10px); }
.epc-heading { font-size:var(--epc-text-heading, 10px); }
.epc-bonus { font-size:var(--epc-text-bonus, 13px); line-height:1.3; }
.epc-flavor { font-size:var(--epc-text-flavor, 12px); }
.epc-footer { font-size:var(--epc-text-footer, 10px); }

.epc-frame > .epc-tags { flex-wrap:nowrap; overflow:hidden; }
.epc-frame > .epc-effects { overflow:hidden; }
.epc-heading { margin-bottom:2px; }
.epc-flavor { padding-top:3px; }
/* The art column is sized by the inspect level, not by a number kept here —
   an art track narrower than the card makes the card smaller than the one you
   tapped to open it, which is the defect this whole change closes. */
/* OUTSIDE THE MODAL THE LAYOUT WRAPS ON ITS OWN BOX (#1164). The two-column
   grid used to hold here for every host, and only two things stacked it: the
   `max-width:600px` collapse below and the `[data-card-door='stacked']` rules,
   which are scoped to `.card-inspection-modal` because that is the only host
   `cardInspectionLayout()` measures. So a bare host (reward, inventory,
   service modal) or an Armoury pane kept two columns from 601px up —
   measured on weapon-cards-preview at 601/650/703: details 162/211/264px.
   A flex row that wraps needs no breakpoint and no viewport reading: the art
   asks for the inspect width, the details ask for the readable minimum, and
   when both do not fit in THIS layout's content box the details drop to a line
   of their own. The details are therefore at least the readable minimum beside
   the card, or the full line under it — never the seam between.
   384px IS card.json's `sizing.doorReadableMinPx`; CSS cannot read it, so
   tests/card-door-stack.test.mjs holds this literal to the config. The modal
   keeps its grid and its measured `data-card-door` decision (rules below). */
.card-inspection-layout { display:flex; flex-wrap:wrap; align-items:flex-start; gap:24px; min-width:0; width:100%; padding:16px; box-sizing:border-box; }
.card-inspection-layout > .card-inspection-art { flex:0 1 var(--card-w-inspect,320px); }
.card-inspection-layout > .card-inspection-details { flex:1 1 384px; }
/* A wrapped art line is centred, not left on a 320px box at the line start;
   the details' end margin takes any slack on a two-column line, so a wide host
   still reads card-then-text from the start edge. Not the modal: its grid has
   its own tracks and a stacked door centres its art itself (rules below). */
.card-inspection-layout:not(.card-inspection-modal .card-inspection-layout) { justify-content:center; }
.card-inspection-layout:not(.card-inspection-modal .card-inspection-layout) > .card-inspection-details { margin-inline-end:auto; }
.card-inspection-art { min-width:0; width:100%; }
.card-inspection-art > .equipment-poker-card { width:100%; }
.card-inspection-details { min-width:0; max-width:65ch; font:16px/1.55 Arial,sans-serif; overflow-wrap:anywhere; }
.card-inspection-details dl { margin:0; }
.card-inspection-details dt:first-child { margin-top:0; }
.card-inspection-modal .modal-body { overflow:auto; overscroll-behavior:contain; }
.equipment-poker-explanations { margin-top:0; }
/* One drawn face for both information controls; the size is a variable so the
   WCB1: every host's control is the one shared circle (--inspect-size, a
   44 physical px target, with a 16 px label) unless the host declares its own
   --card-info-size, which nothing does today — the hook stays for a host that
   must pin the circle to a size the zoom does not choose. */
.card-inspection-target > .card-info-button,
.combatant-leading > .combatant-info { --inspect-box:var(--card-info-size, var(--inspect-size, calc(2.75 * max(16px / var(--ui-zoom, 1), 1rem)))); box-sizing:border-box; width:var(--inspect-box); height:var(--inspect-box); min-width:var(--inspect-box); min-height:var(--inspect-box); max-width:var(--inspect-box); max-height:var(--inspect-box); padding:0; margin:0; display:grid; place-items:center; border:1px solid #d0ac5d; border-radius:50%; background:#211a12; color:#ffe3a0; font:bold var(--inspect-label, calc(16px / var(--ui-zoom, 1)))/1 Georgia,serif; cursor:pointer; touch-action:manipulation; }
.card-inspection-target > .card-info-button { position:absolute; z-index:8; top:calc(-1 * (var(--inspect-box) + var(--inspect-gap, calc(10px / var(--ui-zoom, 1))))); right:auto; bottom:auto; left:50%; transform:translateX(-50%); visibility:hidden; pointer-events:none; }
.card-inspection-target:is(.inspection-selected, .selected, [aria-selected='true']) > .card-info-button { visibility:visible; pointer-events:auto; }
/* WCF3: the selected card and its inspect control share one glow. The hand
   moves that control into an overlay, so the portal wears the same filter. */
.inspection-selected, .hand-info-portal { filter:var(--selection-glow, drop-shadow(0 0 calc(.35 * max(16px / var(--ui-zoom, 1), 1rem)) var(--gold))); }
.card-info-button:focus-visible { outline:var(--focus-ring); outline-offset:var(--focus-ring-offset); }
.card-info-button::before { content:''; position:absolute; inset:100% -8px -12px; }
.card-inspection-modal .card-inspection-layout { padding:24px; gap:28px; grid-template-columns:minmax(0,var(--card-w-inspect,320px)) minmax(0,1fr); }
.card-inspection-modal .card-inspection-art > .card { width:100%; height:auto; aspect-ratio:var(--card-ratio); box-sizing:border-box; }
.playing-card-details { display:flex; flex-direction:column; gap:16px; padding:4px 0; }
.playing-card-details .tt-title { display:none; }
.playing-card-details .ti-meta { display:flex; flex-wrap:wrap; align-items:center; gap:10px; padding-bottom:16px; border-bottom:1px solid var(--line); }
.playing-card-details .ctext { font-size:18px; line-height:1.55; }
.playing-card-details .ti-flavor { color:var(--muted); line-height:1.5; }
.card-inspection-modal button.card-inspection-play:disabled { opacity:1; cursor:not-allowed; }
.cc-equip-group[data-equipment-section] { display:grid; grid-template-columns:minmax(220px, .85fr) minmax(0,1.15fr); gap:24px; align-items:start; }
.cc-equip-group > .cc-card-selectors[data-view="grid"] { grid-template-columns:repeat(auto-fit,minmax(180px,220px)); gap:16px; }
.cc-equipment-details { padding:16px; border:1px solid #74603c; border-radius:8px; min-width:0; }
@media (max-width:700px) { .cc-equip-group[data-equipment-section] { grid-template-columns:minmax(0,1fr); } }

.card-inspection-art > .card { width:220px; height:308px; min-width:0; max-width:100%; margin:0 auto; transform:none; }
.card-inspection-art > .card .ctext { overflow:hidden; text-overflow:ellipsis; }

.inspection-tags { display:flex; flex-wrap:wrap; gap:6px; margin-top:12px; }
.inspection-tag { display:inline-block; max-width:100%; padding:4px 8px; border:1px solid var(--line); border-radius:5px; color:var(--gold); font:inherit; font-size:13px; line-height:1.4; white-space:normal; overflow-wrap:anywhere; cursor:help; }
.tooltip-keyword { color:var(--gold); border-bottom:1px dotted currentColor; cursor:help; }
.tooltip-keyword:focus-visible, .inspection-tag:focus-visible { outline:var(--focus-ring); outline-offset:3px; }
.inspection-facts { display:flex; flex-wrap:wrap; gap:8px; }
.inspection-facts .inspection-tag { font-size:15px; color:var(--parchment); }
.inspection-effect { margin:0 0 14px; font-size:18px; line-height:1.55; }
.inspection-facts + .inspection-effect { margin-top:18px; }
.inspection-requirement { color:var(--gold); line-height:1.5; }
.equipment-poker-explanations { min-width:0; }
.equipment-poker-explanations .inspection-effect:last-of-type { margin-bottom:0; }
.as-tip .ctext { white-space:normal; max-height:none; overflow:visible; }
.as-tip .inspection-tags { gap:5px; }
@media(max-width:600px) {
  .card-inspection-modal .card-inspection-layout, .card-inspection-layout { grid-template-columns:minmax(0,1fr); gap:16px; padding:16px; }
  .playing-card-details .ctext { font-size:16px; }
  .card-inspection-art { width:100%; max-width:100%; margin-inline:auto; }
  .card-inspection-layout > .card-inspection-art { flex-basis:100%; }
  .card-inspection-art > .card { width:160px; height:auto; aspect-ratio:var(--card-ratio); margin-inline:auto; }
  .card-inspection-art > .equipment-poker-card { width:100%; height:auto; margin-inline:auto; }
  .card-inspection-art > .inspection-tags { justify-content:center; }
  .inspection-effect { font-size:16px; }
  .inspection-tag { min-height:32px; display:inline-flex; align-items:center; }
}
.equipment-choose { width:100%; margin-top:10px; min-height:var(--tap-floor); }
/* CHOSEN IS THE RING ON THE CARD, NOT A LABEL UNDER IT. The button that made
   the choice goes quiet once the choice is made — hidden, never removed, so
   the row keeps the height it had and the cards beneath it do not walk up the
   page when you pick one. The state a sighted player reads off the ring is
   carried for everyone else by the chip's `.equip-chosen-note`. */
.poker-equipment-choice.on > .equipment-choose { visibility:hidden; pointer-events:none; }
/* The gold `.poker-equipment-choice.on` outline that used to sit here is
   GONE ON PURPOSE (#997): it was this picker's own invention, and the chosen
   state is now the one green ring the whole game shares, drawn on the card's
   own face by `.is-chosen` above. `.on` stays on the CHIP because the rule
   just above still needs it to quiet the button. */

/* Creation keeps card nodes in place so focus can lift and settle smoothly.
   ONE PLACEMENT, AND THE ROOM IT NEEDS (Constantine's review, 2026-09-12).
   The information badge hangs above a selected card on every surface, this one
   included; a corner exception here read as a second rule for the same
   control. What made the exception necessary was room, not placement — with
   nothing reserved the badge straddled the STARTING ARMOUR header's border
   (measured 1280x800, 2026-09-11) — so these selectors reserve it: the 64 px
   row gap every other card scrollport keeps, and a head that is DERIVED from
   the badge it has to clear, floored at the 80 px this rule used to type.
   THE HEAD IS THE BADGE'S OWN RISE, READ FROM THE BADGE'S OWN TOKENS.
   Nothing declares --card-info-size any more, so the control here is the shared
   circle, and main.js writes its size and hang onto the document root from
   wireframeUi.inspect (--inspect-size, --inspect-gap). --cc-choice-rise reads
   those same two properties through the same var chain the button's own `top`
   uses, so a config change moves the head with the badge instead of past it: at
   sizeRem 4 / gapPx 20 the badge grows to 67.2 px and the old head, which had
   the 2.75 and the 10 typed into it, clipped 17.3 px (measured 2026-09-14).
   The third term is the focus lift — translateY(-6px) plus the 5 % of the
   card's height that scale(1.05) about center bottom grows upward, 20.2 px of
   layout at UI size S — which is what the 26 covers, with slack.
   A head derived from --tap-floor was tried and refused, and the max() floor is
   why: that token divides by a zoom that runs ABOVE 1 on a desktop (1.48 at
   1920), and a head that shrinks with it clipped 8 px off the first row there
   while the badge had not moved. This expression only ever GROWS the head, and
   it resolves to exactly 80 px at every zoom of 1 or more, so the Auto sizes
   keep the geometry measured above.
   THE ROW GAP RIDES THE SAME TOKEN, and keeps the 64 every other card
   scrollport keeps wherever the head is at its floor. A second row's badge
   overlaps the row above by design — it is drawn over that card at z-index 8,
   measured 2.9 px into it at 1280 with the default tokens — so the gap is not
   full clearance and is not what keeps a badge whole. Deriving it only stops a
   bigger badge from deepening that overlap: the gap grows by exactly what the
   head grows, never less than 64.
   The floor alone was not enough: at UI size S (--ui-zoom 0.85) with NORMAL
   motion the badge's top sat 0.8 px past the scrollport's padding box at every
   desk width measured — 1280, 1366, 1440 and 1920 (2026-09-14; ui-sweep's
   fixture asks for reduced motion, which drops the lift, so R5 could not see
   it, and the row now runs a second pass that can). Derived, the head is
   89.53 px there and the same cells clear by 7.3 px; 390 at Auto (zoom 0.9)
   takes 86 px and clears by 18.1.
   Measured whole and clear of the section face at 1280, 1366, 1440, 1920 and
   390, at UI sizes Auto, S, M and XL, under normal motion.
*/
/* The derived head belongs to the SCROLLPORT, not to one of its two layouts:
   the badge hangs above a selected card in either view, and in either view the
   first row is the one that would push it onto the section face. So the two
   custom properties and the group placement sit on the container itself and
   both `data-view` branches read them. */
.cc-equip-group > .cc-card-selectors { --cc-choice-rise:calc(var(--inspect-size, calc(2.75 * max(16px / var(--ui-zoom, 1), 1rem))) + var(--inspect-gap, calc(10px / var(--ui-zoom, 1))) + 26px); --cc-choice-head:max(80px, var(--cc-choice-rise)); grid-row:1 / span 2; }
.cc-equip-group > .cc-card-selectors[data-view="grid"] { padding:var(--cc-choice-head) 6px 14px; gap:calc(64px + var(--cc-choice-head) - 80px) 12px; grid-template-columns:repeat(auto-fill,minmax(132px,1fr)); align-content:start; }
.cc-card-selectors[data-view="grid"] .poker-equipment-choice { position:relative; min-width:0; width:100%; justify-self:center; padding:4px; }
/* FOUR CHOICES IN THE GLASS, NOT TWO (2026-09-12). Constantine: "cards are
   too big. I should easily be able to see 4 cards on this screen." Measured
   before this rule, the Main Hand choices column showed exactly two cards at
   every desktop size (1366x768: a 497x505 scrollport holding 228x371 cards;
   1440x900: 548x600 holding 252x412; 1920x1080: 703x710 holding 326x529) —
   the column was pinned to two tracks, so a wide pane bought bigger cards
   rather than more of them. The track count above is therefore unpinned and
   the card is browsing-sized at 152px; the reading is the detail pane's job
   and the art is the inspect modal's.
   THE HEAD AND ROW GAP ABOVE ARE NOT PART OF THAT SAVING. This branch also
   removed them, on the premise that the information badge sat inside the
   card's corner and the head was reserving nothing. That premise no longer
   holds: the badge hangs ABOVE a selected card on every surface, and the head
   is what keeps the first row's badge off the section face. So the derived
   head and gap stand exactly as they are, and only the track count and the
   card cap come across.
   Choose appears below the selected card, in its own reserved action slot. */
.cc-card-selectors[data-view="grid"] .equipment-choose { position:static; width:100%; margin-top:10px; }
.cc-card-selectors .equipment-poker-card { position:relative; z-index:1; transform-origin:center bottom; transition:transform 180ms ease-out, box-shadow 180ms ease-out; }
.cc-card-selectors .choice-focused .equipment-poker-card { transform:translateY(-6px) scale(1.05); z-index:3; box-shadow:0 12px 26px #0008,0 0 18px #d0ac5d55; border-radius:12px; }
.cc-card-selectors .equipment-choose { white-space:normal; overflow-wrap:anywhere; line-height:1.2; padding:6px 4px; font-size:14px; }
/* Retain the action slot so browsing does not move the next row under a pointer. */
.cc-card-selectors[data-view="grid"] .poker-equipment-choice:not(.choice-focused) .equipment-choose { visibility:hidden; pointer-events:none; }

/* ═══ THE SAME CHOICES AS A LIST ══════════════════════════════════════════
   Grid, above, is a browsing wall: the card is shrunk to 152px, the Choose
   button follows its foot in a reserved action slot, and both it
   and the information badge are hidden until a card is focused — everything
   bought back is spent on fitting more cards in the glass (#996).
   List makes the opposite trade and is the honest counterpart the toggle
   never had. One card per row at a readable 108px beside a text column, so
   the name and the Choose button are legible WITHOUT focusing first, and the
   action is a plain block in the flow rather than an overlay: a row has the
   height to hold it, so hiding it would buy nothing. Row count, not card
   count, is what a list is for.
   The card keeps its lift on focus — that is the picker's focus language and
   it reads the same in either view. */
.cc-card-selectors[data-view="list"] { display:flex; flex-direction:column; gap:10px; padding:var(--cc-choice-head,80px) 6px 14px; align-content:start; }
.cc-card-selectors[data-view="list"] .poker-equipment-choice {
  position:relative; display:grid; grid-template-columns:var(--card-w-glance-compact, 108px) minmax(0,1fr); gap:12px;
  align-items:center; width:100%; min-width:0; max-width:none; padding:8px;
  border:1px solid var(--line-soft, #5e4c32); border-radius:10px;
}
.cc-card-selectors[data-view="list"] .poker-equipment-choice.on { border-color:var(--gold, #c9a227); }
/* The list view sets its cards beside their own text, so it reads a narrower
   width — `sizing.levels.glance.variants.compact` in card.json, not a number
   invented here. The TRACK above reads the same token, so the column and the
   card it holds cannot disagree; a literal there would cap whatever the card
   asked for, which is how the shop's oversized frame happened (#1167).
   THE VIEW DECIDES THIS WIDTH, NOT THE LEVEL, and keying it on `[data-level]`
   was wrong: `levelForView('list')` returns `focus` (src/model/cardFields.js),
   so EVERY card in this view is `focus`, selected or not — measured in the
   real picker, not inferred. A `[data-level="glance"]` key therefore matched
   nothing here and the rule was dead on its only surface.
   That is also why writing it unconditionally does not bring back the defect
   it was keyed to avoid: nothing in this view promotes on selection, so there
   is no card here that this cap would hold at a resting size. The level still
   chooses the FIELD SET the card shows — which is the whole point of a list
   row, name and effects legible beside the card — while the view chooses how
   wide it is drawn. */
.cc-card-selectors[data-view="list"] .equipment-poker-card { width:100%; transform-origin:center left; max-width:var(--card-w-glance-compact, 108px); }
.cc-card-selectors[data-view="list"] .equipment-choose {
  position:static; width:100%; margin-top:0; min-height:var(--tap-floor); font-size:14px;
}

/* WHAT AN UNFOCUSED CHIP WITHHOLDS IS AUTHORED NOW, NOT HAND-ROLLED HERE.
   A `:not(.choice-focused) .card-info-button { visibility:hidden }` rule stood
   here: one screen's private flag, in CSS, for the one field anybody had got
   round to hiding. That is the mechanism the presentation levels replace —
   `content/config/ui/components/card.json` says what each of glance / focus /
   inspect shows, `src/model/cardFields.js` answers it, and the face does not
   render what it is not saying. A second copy of that decision in a selector
   is exactly the drift Law 1 is about, so the rule is gone rather than kept
   "just for the button". */
/* The catalogue's three-level specimen: FLOW ONLY. Each face is sized by the
   picker rules above, because how big a card is has one home and this is not
   it; all this adds is that the three stand side by side under their names. */
.cc-card-levels { display:flex; flex-wrap:wrap; align-items:flex-start; }
.cc-card-levels > .cc-card-level { margin:0; }
.cc-card-levels > .cc-card-level > figcaption { text-align:center; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); }
.cc-equipment-details { max-width:none; }
.cc-equipment-details h3 { margin:0 0 8px; }
.cc-choice-context, .cc-package-context { color:var(--muted); font-size:14px; line-height:1.4; margin:0 0 12px; }
/* THE SUBSTEP'S WAY ON OUTRANKS THE STAGE'S SKIP.
   The stage row above (`.cz-stage .modal-btnrow.end`) is pinned to the
   scrollport floor at `z-index:2`, and this button is right-aligned in the same
   band. MEASURED at 1366x768 with Main Hand open: scrolled so `Continue to Off
   Hand` reaches the floor, the two overlap by 46.9px vertically and 203.7px
   horizontally, and `elementFromPoint` at this button's own centre returned
   `BUTTON.as-btn cz-next` — the player pressing the section's Continue was
   reaching `Continue to seed`, SKIPPING the armament they had come to choose.
   Below 700px the sticky rule further down already answers this with
   `z-index:3`; above it this button is a static grid item, so it lost the paint
   order to a later, positioned sibling. A grid item's containing block is its
   own one-row grid area, so it cannot be made sticky here (the reason the
   sticky rule is mobile-only) — but it does not need travel, only rank.
   `position:relative` with no offsets moves nothing and changes no layout; it
   only lets the z-index apply. */
.cc-equipment-continue { grid-column:2; grid-row:2; justify-self:end; align-self:end; min-height:44px; max-width:100%; white-space:normal; position:relative; z-index:3; }
/* The ready lift scales the button about its centre, and at the grid's right
   edge that is a 2px horizontal overflow (measured: section scrollWidth 1100
   against clientWidth 1098 at 1024 wide). Growing from the right edge keeps
   the lift inside the section. */
.cc-equipment-continue, .cz-next, .cc-primary-continue { transform-origin:100% 50%; }
/* Ready, the label is bold, and bold "Continue to equipment" ran 2px past the
   long button's box at 1440 wide (measured: scrollWidth 210 in a 208 box).
   Less side padding than the row's clamp gives it, so the words stay whole. */
.cz-next { padding-inline:clamp(0.5rem, 5%, 1rem); }
/* AND THE ROW GIVES THE WORDS THE ROOM, because the padding alone could not.
   Creation's physical text floor (`.customize :is(..., button)`, 16px over the
   zoom) outranks the ButtonRow's fit-to-box type clamp, so the label does not
   shrink into a `long` row: "Continue to character" wanted 227px of that row's
   208px box at 1280x720 and ellipsed to "Continue to chara...". The step's way
   on is one button alone on its row, so the row is sized to the longest of
   those labels instead of the shared `long` step. */
.as-btnrow:has(> .cz-next), .modal-btnrow:has(> .cz-next) { --step: 26rem; }
/* THE WAY ON NEVER SCROLLS OUT OF REACH (Constantine, 2026-09-12: *"the
   continue button always in view and not requiring too much scrolling"*).
   MEASURED: with the right-hand section open at 390x844 the creation scroll is
   2129px against a 784px viewport, and the section's Continue sits at the end
   of it. A player who had chosen scrolled back through everything to leave.
   STICKY RATHER THAN MOVED TO THE FOOT, and the reason is the hooks: `#cz-start`
   and `.cz-next[data-next]` are read by startup-gate, character-creation-check
   and the contact sheet, and the foot is `.cz-actions`. Relocating the buttons
   changes what every one of those instruments walks. Sticky changes where the
   same button PAINTS and nothing about where it lives, so the fix costs no
   instrument a re-teach.
   The row carries its own ground: a transparent sticky control with the stage's
   content sliding under it is unreadable at the moment it matters. */
.cz-stage {
  /* How far above the scrollport's floor a SECOND sticky row has to rest, so it
     clears the first. Authored once because CSS cannot ask how tall a sibling
     is — but DERIVED, not typed, because every part of that height is a setting
     the player can move:
        var(--tap-floor)  the button inside the row, which is
                          `calc(var(--tap-target) / var(--ui-zoom, 1))` — it
                          grows with Minimum tap size and with Auto's zoom
        12px              the row's own `padding-block: 10px 2px`, below
        10px              the scrollport's `padding-bottom`, which is why the
                          stage row's painted bottom sits above the floor
                          rather than on it
     THIS WAS A CONSTANT AND THE CONSTANT WAS WRONG. 64px measured clean at the
     default 44px target, and collided again as soon as the target moved: at
     `--tap-target: 60px` the rows overlapped by 8px at zoom 1 and by 27.8px at
     zoom 0.62. A number that only holds at one setting is a number that has
     not been derived. */
  --cz-continue-row: calc(var(--tap-floor) + 22px);
}
.cz-stage .modal-btnrow.end,
.cz-stage .cc-primary-continue-row {
  position:sticky; bottom:0; z-index:2;
  background:linear-gradient(to top, var(--panel) 62%, transparent);
  padding-block:10px 2px; margin-block-start:8px;
}
/* TWO WAYS ON CAN BE STICKY AT ONCE, AND THEY MUST NOT SHARE THE BAND.
   The character stage holds both: the nested `Continue` that closes Primary
   Stats and opens the Keepsake, and the stage's own `Continue to equipment`.
   MEASURED at 390x844 with Primary Stats open, both rested at exactly
   top 693 / bottom 748 — the same 55px band, the same z-index — so the stage
   row, later in the DOM, painted over the nested one. What a player saw at the
   bottom of the glass was the DISABLED `Continue to equipment`, with the
   actionable control hidden underneath it.
   Both are needed: with the nested row made static it falls to 828, which is
   71px below a band that ends at 757, so it cannot simply stop being sticky.
   So they STACK — the nested row rests one row higher, and paints above, which
   also keeps it legible while the two pass each other on a scroll.
   The selector carries `.modal-btnrow` because the nested row wears `end` too,
   and would otherwise lose the specificity contest with the rule above. */
.cz-stage .modal-btnrow.cc-primary-continue-row {
  bottom:var(--cz-continue-row); z-index:3;
}
/* A fold's summary is a control: it takes the tap floor and the kit's ring.
   THE FLOOR IS THE TOKEN, NOT THE NUMBER. `--tap-floor` is
   `calc(var(--tap-target) / var(--ui-zoom, 1))` precisely because `body` carries
   `zoom: var(--ui-zoom)`: a literal 44px below body renders at 44 x zoom, so at
   Auto's 0.62 floor this control would have been a ~27px physical target while
   every button beside it stayed 44. The division cancels the zoom; the number
   does not. The comment said "takes the tap floor" while the code took a
   constant that happens to equal it at zoom 1. */
.cc-starting-fold > .cc-starting-summary {
  display:flex; flex-wrap:wrap; align-items:baseline; gap:4px 10px;
  min-height:var(--tap-floor); padding:10px 4px; cursor:pointer; list-style:none;
}
.cc-starting-fold > .cc-starting-summary::-webkit-details-marker { display:none; }
.cc-starting-fold > .cc-starting-summary::after {
  content:'\203A'; margin-inline-start:auto; color:var(--muted);
  transition:transform 150ms ease-out;
}
.cc-starting-fold[open] > .cc-starting-summary::after { transform:rotate(90deg); }
.cc-starting-fold > .cc-starting-summary b { color:var(--gold); }
.cc-starting-fold > .cc-starting-summary .cc-package-context { margin:0; }
.reduced-motion .cc-starting-fold > .cc-starting-summary::after { transition:none; }
@media (prefers-reduced-motion: reduce) {
  .cc-starting-fold > .cc-starting-summary::after { transition:none; }
}
/* INSIDE THE FOLD THE CARDS ARE A REFERENCE, AND ARE DRAWN AS ONE. The
   renderer's `small` flag was the wrong tool and is not used here: its only
   effect is `transform: scale(0.92)`, which `.cc-starting-card > .card` above
   correctly cancels — a transformed card still occupies its untransformed box,
   so it saves no scroll at all. A smaller BOX does. Three to a row instead of
   two turns six faces from three rows into two. */
.cc-starting-fold .cc-starting-card-grid { grid-template-columns:repeat(3,minmax(0,1fr)); gap:9px 7px; }
.cc-starting-fold .cc-starting-card { padding-top:34px; }
.cc-starting-fold .cc-starting-card > .card { max-width:104px; }
.cc-starting-fold .cc-starting-card .card-info-button { top:-36px; }
.cc-starting-package { margin-top:20px; border-top:1px solid var(--line); padding-top:16px; }
.cc-starting-package h4 { margin:0 0 8px; color:var(--gold); font-size:18px; }
.cc-starting-card-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:11px 9px; }
.cc-starting-card { min-width:0; display:flex; flex-direction:column; align-items:center; gap:6px; padding-top:42px; }
.cc-card-quantity { font-size:13px; color:var(--gold); align-self:start; }
.cc-starting-card > .card { width:100%; max-width:150px; height:auto; aspect-ratio:var(--card-ratio); min-width:0; margin:0; transform:none; }
.equipment-poker-card[data-item='empty-hand'] .epc-art-glyph { font-size:92px; line-height:1; color:var(--gold); }
.cc-starting-card .card-info-button { top:-44px; }
/* CARD LORE — the identity line in inspection and the lore modal behind it
   (components/loreLine.js). The player picks the face, size, spacing and slant
   under Advanced → Text & lore; models/LoreTypeModel.js writes the choice on
   <html> as data-lore-* words and the rules below turn words into type. The
   faces are bundled (assets/fonts/, CREDITS.md) under their own "AS Lore"
   names, so none of them changes the game's headings or body text. The lore
   window sizes in rem against the 62.5% root (1rem = 10px at Text M); the
   inspection line is 1.6rem (16px at Text M, so Text size grows it) with the
   card-inspection pane's zoom floor, the same in playing-card and equipment
   inspection. */
/* FONT LICENCES — carried here so the notices travel inside every build,
   single-file and mobile alike (the same text is asset-data/fonts/OFL.txt).

Card lore faces bundled in assets/fonts/ (Latin subset WOFF2, from the Fontsource
packages of the Google Fonts releases). Each is licensed under the SIL Open Font
License, Version 1.1, reproduced below; also available with a FAQ at https://openfontlicense.org

IM Fell English: Copyright (c) 2010, Igino Marini (mail@iginomarini.com)
EB Garamond: Copyright 2017 The EB Garamond Project Authors (https://github.com/octaviopardo/EBGaramond12)
Cormorant Garamond: Copyright 2015 The Cormorant Project Authors (github.com/CatharsisFonts/Cormorant)
Crimson Pro: Copyright 2018 The Crimson Pro Project Authors (https://github.com/Fonthausen/CrimsonPro)
Spectral: Copyright 2017 The Spectral Project Authors (https://github.com/productiontype/Spectral)
Libre Baskerville: Copyright 2012 The Libre Baskerville Project Authors (https://github.com/impallari/Libre-Baskerville)
Cinzel: Copyright 2020 The Cinzel Project Authors (https://github.com/NDISCOVER/Cinzel)
Inter: Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter)

SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------

PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.

The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.

DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.

"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).

"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).

"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.

"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.

PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:

1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.

2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.

3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.

5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.

TERMINATION
This license becomes null and void if any of the above conditions are
not met.

DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
*/
@font-face { font-family:'AS Lore Fell'; font-style:normal; font-weight:400; font-display:swap; src:url('../assets/fonts/im-fell-english-400-normal.woff2') format('woff2'); }
@font-face { font-family:'AS Lore Fell'; font-style:italic; font-weight:400; font-display:swap; src:url('../assets/fonts/im-fell-english-400-italic.woff2') format('woff2'); }
@font-face { font-family:'AS Lore Garamond'; font-style:normal; font-weight:400; font-display:swap; src:url('../assets/fonts/eb-garamond-400-normal.woff2') format('woff2'); }
@font-face { font-family:'AS Lore Garamond'; font-style:italic; font-weight:400; font-display:swap; src:url('../assets/fonts/eb-garamond-400-italic.woff2') format('woff2'); }
@font-face { font-family:'AS Lore Cormorant'; font-style:normal; font-weight:400; font-display:swap; src:url('../assets/fonts/cormorant-garamond-500-normal.woff2') format('woff2'); }
@font-face { font-family:'AS Lore Cormorant'; font-style:italic; font-weight:400; font-display:swap; src:url('../assets/fonts/cormorant-garamond-500-italic.woff2') format('woff2'); }
@font-face { font-family:'AS Lore Crimson'; font-style:normal; font-weight:400; font-display:swap; src:url('../assets/fonts/crimson-pro-400-normal.woff2') format('woff2'); }
@font-face { font-family:'AS Lore Crimson'; font-style:italic; font-weight:400; font-display:swap; src:url('../assets/fonts/crimson-pro-400-italic.woff2') format('woff2'); }
@font-face { font-family:'AS Lore Spectral'; font-style:normal; font-weight:400; font-display:swap; src:url('../assets/fonts/spectral-400-normal.woff2') format('woff2'); }
@font-face { font-family:'AS Lore Spectral'; font-style:italic; font-weight:400; font-display:swap; src:url('../assets/fonts/spectral-400-italic.woff2') format('woff2'); }
@font-face { font-family:'AS Lore Baskerville'; font-style:normal; font-weight:400; font-display:swap; src:url('../assets/fonts/libre-baskerville-400-normal.woff2') format('woff2'); }
@font-face { font-family:'AS Lore Baskerville'; font-style:italic; font-weight:400; font-display:swap; src:url('../assets/fonts/libre-baskerville-400-italic.woff2') format('woff2'); }
@font-face { font-family:'AS Lore Cinzel'; font-style:normal; font-weight:400; font-display:swap; src:url('../assets/fonts/cinzel-400-normal.woff2') format('woff2'); }
@font-face { font-family:'AS Lore Inter'; font-style:normal; font-weight:400; font-display:swap; src:url('../assets/fonts/inter-400-normal.woff2') format('woff2'); }
@font-face { font-family:'AS Lore Inter'; font-style:italic; font-weight:400; font-display:swap; src:url('../assets/fonts/inter-400-italic.woff2') format('woff2'); }
:root { --lore-font:'AS Lore Fell', Georgia, serif; --lore-size:1.7rem; --lore-scale:1; --lore-leading:1.6; --lore-tracking:0em; --lore-slant:italic; --lore-identity-slant:italic; }
:root[data-lore-face="fell"] { --lore-font:'AS Lore Fell', Georgia, serif; }
:root[data-lore-face="garamond"] { --lore-font:'AS Lore Garamond', Georgia, serif; }
:root[data-lore-face="cormorant"] { --lore-font:'AS Lore Cormorant', Georgia, serif; }
:root[data-lore-face="crimson"] { --lore-font:'AS Lore Crimson', Georgia, serif; }
:root[data-lore-face="spectral"] { --lore-font:'AS Lore Spectral', Georgia, serif; }
:root[data-lore-face="baskerville"] { --lore-font:'AS Lore Baskerville', Georgia, serif; }
:root[data-lore-face="cinzel"] { --lore-font:'AS Lore Cinzel', Georgia, serif; }
:root[data-lore-face="inter"] { --lore-font:'AS Lore Inter', 'Segoe UI', system-ui, sans-serif; }
:root[data-lore-face="georgia"] { --lore-font:Georgia, 'Times New Roman', serif; }
:root[data-lore-size="S"] { --lore-size:1.5rem; --lore-scale:0.9; }
:root[data-lore-size="M"] { --lore-size:1.7rem; --lore-scale:1; }
:root[data-lore-size="L"] { --lore-size:1.9rem; --lore-scale:1.1; }
:root[data-lore-size="XL"] { --lore-size:2.2rem; --lore-scale:1.25; }
:root[data-lore-leading="tight"] { --lore-leading:1.4; }
:root[data-lore-leading="normal"] { --lore-leading:1.6; }
:root[data-lore-leading="loose"] { --lore-leading:1.85; }
:root[data-lore-tracking="tight"] { --lore-tracking:-0.01em; }
:root[data-lore-tracking="normal"] { --lore-tracking:0em; }
:root[data-lore-tracking="wide"] { --lore-tracking:0.04em; }
:root[data-lore-slant="upright"] { --lore-slant:normal; }
:root[data-lore-identity-slant="upright"] { --lore-identity-slant:normal; }
/* Cinzel ships no italic; an upright face beats a browser-faked slant. */
:root[data-lore-face="cinzel"] { --lore-slant:normal; --lore-identity-slant:normal; }
.inspection-lore-line { display:flex; align-items:baseline; gap:12px; width:100%; box-sizing:border-box; margin:12px 0 0; padding:10px 12px; border:1px solid var(--line); border-radius:8px; background:transparent; color:var(--parchment); text-align:left; font:inherit; }
button.inspection-lore-line { cursor:pointer; }
button.inspection-lore-line:hover { border-color:var(--gold); }
button.inspection-lore-line:focus-visible { outline:2px solid var(--gold); outline-offset:2px; }
.inspection-lore-line .lore-identity { flex:1; min-width:0; font-family:var(--lore-font); font-size:calc(var(--lore-scale) * max(1.6rem, calc(16px / var(--ui-zoom, 1)))); font-style:var(--lore-identity-slant); letter-spacing:var(--lore-tracking); line-height:1.35; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* ONE LINE, like creation flavor (SPEC): a long identity ends in an ellipsis;
   the whole of it opens in the lore modal from this same line. */
/* A line with nothing behind it (an equipment blurb) has no modal to open,
   so it wraps in full rather than hide words a touch player cannot reach. */
p.inspection-lore-line .lore-identity { white-space:normal; overflow:visible; text-overflow:clip; }
.inspection-lore-line .lore-read { flex:none; font-size:calc(0.75 * max(1.6rem, calc(16px / var(--ui-zoom, 1)))); font-weight:600; letter-spacing:0.1em; text-transform:uppercase; color:var(--gold); }
.lore-modal-text { display:grid; gap:14px; max-width:62ch; padding:1.6rem 2rem 2.2rem; font-family:var(--lore-font); font-size:var(--lore-size); line-height:var(--lore-leading); letter-spacing:var(--lore-tracking); font-style:var(--lore-slant); }
.lore-modal-text p { margin:0; }
.lore-modal-text .lore-identity { font-size:calc(var(--lore-size) + 0.2rem); font-style:var(--lore-identity-slant); }
.lore-modal-text .lore-closing { margin-top:4px; padding-top:12px; border-top:1px solid var(--line); color:var(--parchment); opacity:0.82; }
.equipment-poker-card .epc-frame > .epc-flavor { display:block; box-sizing:border-box; min-width:0; min-height:0; max-height:100%; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-size:calc(var(--epc-flavor-row,14px) - 2px); line-height:1; padding-top:0; }
/* THE EQUIPMENT SUMMARY: one row of inert cards — the character, then what
   is worn and held in the order the sections ask for it — with the receipts
   folded beneath. The character card keeps the poker cards' proportion so the
   row reads as one hand. */
.cc-summary-cards { display:grid; grid-template-columns:repeat(auto-fit, minmax(150px, 180px)); gap:16px 14px; justify-content:start; align-items:start; }
.cc-summary-slot { position:relative; min-width:0; display:flex; flex-direction:column; gap:6px; }
.cc-summary-slot > .as-eyebrow { margin:0; }
.cc-summary-slot > .equipment-poker-card { width:100%; margin:0; }
.cc-summary-slot > .card-inspection-target {
  position:relative; z-index:1;
  transition:margin-top var(--card-info-fade,120ms) ease;
}
.cc-summary-slot > .card-inspection-target.inspection-selected {
  z-index:2;
  margin-top:calc(var(--inspect-size, calc(2.75 * max(16px / var(--ui-zoom, 1), 1rem))) + var(--inspect-gap, calc(10px / var(--ui-zoom, 1))));
}
@media (prefers-reduced-motion:reduce) {
  .cc-summary-slot > .card-inspection-target { transition:none; }
}
.cc-summary-slot.unmet > .equipment-poker-card { outline:2px solid var(--danger); outline-offset:2px; border-radius:10px; }
.cc-summary-unmet { margin:0; color:var(--danger); font-size:13px; line-height:1.35; }
.cc-summary-empty { display:grid; place-items:center; aspect-ratio:var(--card-ratio); width:100%; max-width:var(--card-w-glance, 152px); border:1px dashed var(--line); border-radius:10px; color:var(--muted); }
.cc-summary-character { position:relative; display:flex; flex-direction:column; gap:8px; width:100%; max-width:var(--card-w-glance, 152px); aspect-ratio:var(--card-ratio); overflow:visible; box-sizing:border-box; padding:12px; border:1px solid var(--gold); border-radius:10px; background:color-mix(in srgb, var(--gold) 6%, var(--panel)); cursor:help; }
.cc-summary-eyebrow { color:var(--muted); font-size:11px; letter-spacing:0.1em; text-transform:uppercase; }
.cc-summary-name { margin:0; color:var(--gold); font:700 18px/1.15 var(--font-display); overflow-wrap:anywhere; }
.cc-summary-character > .as-hairline { margin:0; }
.cc-summary-stats { display:grid; grid-template-columns:repeat(2, minmax(0,1fr)); gap:4px 10px; }
.cc-summary-stats > .as-statpair { justify-content:space-between; font-size:13px; }
.cc-summary-character > .cc-summary-stats:last-child { margin-top:auto; }
.cc-summary-more { grid-column:1 / -1; color:var(--muted); font:700 16px/0.7 var(--font-body); letter-spacing:0.15em; }
.card-inspection-art > .cc-summary-character {
  width:100%; max-width:var(--card-w-inspect,320px); min-height:0; aspect-ratio:var(--card-ratio); overflow:hidden; margin:0 auto;
  cursor:default;
}
.cc-summary-character[data-summary-level='inspect'] .cc-summary-stats { grid-template-columns:minmax(0,1fr); gap:4px; }
.cc-summary-character[data-summary-level='inspect'] .cc-summary-stats > .as-statpair { font-size:14px; }
.cc-summary-character[data-summary-level='inspect'] .cc-summary-stats .sp-k {
  overflow:visible; text-overflow:clip; white-space:normal;
}
.card-inspection-modal:has(.cc-summary-character) .modal-body { overflow-y:auto; overflow-x:hidden; }
.card-inspection-modal:has(.cc-summary-character) .card-inspection-layout { height:auto; align-content:start; }
.card-inspection-modal:has(.cc-summary-character) .card-inspection-details { min-height:auto; overflow:visible; }
.cc-summary-inspection-details h3 { margin:0 0 12px; color:var(--gold); font:700 20px/1.2 var(--font-display); }
.cc-summary-inspection-details dt { margin-top:12px; color:var(--parchment); font-weight:700; }
.cc-summary-inspection-details dd { margin:3px 0 0; color:var(--muted); }
.cc-summary-calculations { margin-top:4px; }
/* THE CLASS STEP FITS THE GLASS ON DESKTOP (2026-09-12).
   Constantine: "continue button is cut off". The step's one way on — its
   "Continue to character" — stood below the door's foot at EVERY desktop size
   measured, because the split grows to its content inside a scrolling body and
   the Continue row is the last thing in that column:

     1280x720   glass 477px, step 598px, Continue at y=725, foot at y=621
     1366x768   glass 515px, step 628px, Continue at y=762, foot at y=666
     1512x860   glass 584px, step 694px, Continue at y=842, foot at y=752
     1920x1080  glass 737px, step 859px, Continue at y=1044, foot at y=946

   Same cure as THE OPEN EQUIPMENT SECTION below, one step later in the same
   flow: the split is capped to what the door body leaves under the step's own
   face, and its two panes scroll INSIDE that cap — the class list and the
   preview each own their overflow — so the Continue never leaves the glass.
   The subtraction is the same one, for the same reason: the door's head and
   foot, the step face and the panel's breathing room, over the zoom (the
   chrome measured 23.2-24.5rem across the four sizes above, so 25rem is the
   conservative floor for all of them). The `max()` keeps a short window from
   collapsing the split to nothing — under it the body scrolls as it did.
   Below the split's own stacking width the phone layout stands: chooser first,
   preview after it, the page scrolling as one. */
@container (width > 64rem) {
  .cc-class-split { max-height: max(30rem, calc(100dvh / var(--ui-zoom, 1) - 25rem)); grid-template-rows: minmax(0, 1fr); }
  .cc-class-split > .as-split-pane { min-height: 0; }
  .cc-class-split > .as-split-pane > .as-pane { min-height: 0; overflow: hidden; }
  .cc-class-split > .cc-class-preview-host > .as-pane { overflow-y: auto; scrollbar-gutter: stable; }
  .cc-class-split .cc-choice-collection { flex: 1 1 auto; min-height: 0; overflow-y: auto; scrollbar-gutter: stable; }
}
/* THE OPEN EQUIPMENT SECTION FITS THE GLASS ON DESKTOP (2026-09-11).
   Constantine: "the screen is slightly too big, i should be able to see
   everything when it opens on desktop." Measured at 1366x768 before this
   rule: the Main Hand section stood 1423px tall — the card-package preview in
   the detail pane — with its Continue at y=1707 on a 768px glass. The section
   is capped to what the door body leaves under the section's own face, and
   its two columns scroll INSIDE that cap: the choices column and the detail
   pane each own their overflow, so the Continue never leaves the glass. The
   subtraction is the door's head and foot, the section face and the panel's
   breathing room, divided by the zoom for the reason .modal's ceiling is.
   The choices scrollport's head (above) is what keeps the above-card inspect
   control off the section face on the first row. Phones keep the stacked,
   page-scrolling layout below. */
@media(min-width:701px) {
  .cc-equip-group[data-equipment-section] { max-height:calc(100dvh / var(--ui-zoom, 1) - 25rem); grid-template-rows:minmax(0,1fr) auto; }
  .cc-equip-group[data-equipment-section] > .cc-card-selectors { overflow-y:auto; min-height:0; max-height:100%; scrollbar-gutter:stable; }
  .cc-equip-group[data-equipment-section] > .cc-equipment-details { overflow-y:auto; min-height:0; max-height:100%; scrollbar-gutter:stable; }
}
@media(max-width:700px) {
  .cc-equip-group > .cc-card-selectors { grid-row:auto; }
  .cc-equipment-continue { grid-column:1; grid-row:auto; width:100%; }
  /* THE SECTION'S OWN WAY ON, MEASURED: with the Main Hand section open at
     390x844 the `Continue to Off Hand` button sat 1815px down a 784px
     scrollport — 1058px below the fold, at the end of the armament picker it
     exists to close. The stage-level sticky rule above never reached it: that
     rule matches `.modal-btnrow.end` and `.cc-primary-continue-row`, and this
     is a bare `.cc-equipment-continue` inside `.cc-equip-group`.
     THE GROUP BECOMES A COLUMN FLEX CONTAINER HERE, and that is load-bearing
     rather than cosmetic: a grid item's containing block is its own one-row
     grid area, so `position:sticky` inside it has nowhere to travel and does
     nothing. A flex item's containing block is the container's content box —
     the full height of the open section — which is the travel sticky needs.
     At this width the group already rendered as one stacked column, so the
     switch changes the layout in no other way. */
  .cc-equip-group[data-equipment-section] { display:flex; flex-direction:column; padding-bottom:4px; }
  .cc-equipment-continue { position:sticky; bottom:4px; z-index:3; }
  .cc-card-selectors .choice-focused .equipment-poker-card { transform:translateY(-4px) scale(1.03); }
  .cc-starting-card-grid { gap:9px 6px; }
  .cc-starting-card > .card { max-width:150px; }
}
@media(prefers-reduced-motion:reduce) {
  .cc-card-selectors .equipment-poker-card { transition:none; }
  .cc-card-selectors .choice-focused .equipment-poker-card { transform:none; }
}
.reduced-motion .cc-card-selectors .equipment-poker-card { transition:none; }
.reduced-motion .cc-card-selectors .choice-focused .equipment-poker-card { transform:none; }

.shop-inspect-card { position:relative; }
/* Face rows come from balance.ui.equipmentCard via the card's own custom
   property: regions declare a floor and a priority, and src/model/equipmentCard.js
   solves them. The literal is the fallback for a card rendered before its tokens
   land, not a second opinion about the layout. */
.equipment-poker-card .epc-frame, .collectible-poker-card .epc-frame {
  grid-template-rows:var(--epc-rows, 231.6px 22px 40px 18px 88.4px 18px 16px);
  container-type:inline-size;
}
.epc-type { font-size:14px; padding:2px; letter-spacing:.04em; }
.epc-facts { padding:2px 0; }
.epc-fact { font-size:14px; }
.epc-fact strong { font-size:22px; }
.epc-tag { font-size:14px; padding:1px 4px; letter-spacing:0; }
.epc-effects { font-size:14px; line-height:1.2; padding:1px 0; }
.epc-heading { font-size:12px; letter-spacing:.03em; }
.epc-flavor { font-size:14px; line-height:1.1; padding-top:0; }
.epc-footer { font-size:13px; letter-spacing:0; }
/* The inspection door's own height, so it keeps it at whatever rung the
   Modals width choice puts the door on (it asks for lg). */
.card-inspection-modal.modal[data-authored-size='lg'] { height:auto; }
.equipment-poker-card, .card.playing-poker-card, .shop-inspect-card { -webkit-user-select:none; user-select:none; }
.card-inspection-details { font-size:max(16px, calc(16px / var(--ui-zoom, 1))); }
.epc-effects { position:relative; }
.epc-bonus + .epc-bonus { display:none; }
.epc-effects:has(.epc-bonus + .epc-bonus) .epc-bonus { padding-right:16px; }
.epc-effects:has(.epc-bonus + .epc-bonus)::after { content:'…'; position:absolute; right:0; bottom:0; }
.epc-frame > .epc-tags { display:block; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
.epc-tag { display:inline-block; margin-right:3px; }
/* WC2a1–WC2c3 possession sub-variants. PossessionVariantModel decides which
   rows a region carries and how many the face shows (wireframeUi.possession);
   rows past that budget carry `hidden` and stay listed in the inspection. */
.equipment-poker-card .epc-frame [hidden] { display:none !important; }
.equipment-poker-card .epc-facts[data-layout='lines'] { display:flex; flex-direction:column; justify-content:center; gap:1px; padding:2px 0; }
.epc-facts[data-layout='lines'] > .epc-line { display:block; min-width:0; font:13px/1.2 Arial,sans-serif; color:#e5dccb; text-align:left; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* WC2c2: the registered resource tint marks the row; the text keeps its contrast. */
.epc-facts[data-layout='lines'] > .epc-line[style*='--line-tint'] { border-inline-start:3px solid var(--line-tint); padding-inline-start:5px; }
.equipment-poker-card[data-possession] .epc-bonus + .epc-bonus:not([hidden]) { display:-webkit-box; margin-top:0; }
.equipment-poker-card[data-possession] .epc-effects[data-entries='many'] .epc-bonus { -webkit-line-clamp:1; }
.equipment-poker-card[data-possession] .epc-effects:not([data-more]) .epc-bonus { padding-right:0; }
.equipment-poker-card[data-possession] .epc-effects:has(.epc-bonus + .epc-bonus):not([data-more])::after { content:none; }
/* WC2a1/WC2a2 detail one (Damage / Defense values): value over label inside the
   solved 40 px row. The 22 px value's normal line box, its margin and the row
   padding measured 44 px, spilling into the tag row. */
.equipment-poker-card[data-possession] .epc-facts:not([data-layout='lines']) { padding-block:0; align-items:center; }
.equipment-poker-card[data-possession] .epc-fact strong { line-height:1; margin-bottom:0; }
/* Detail surfaces disclose the complete label, even when compact faces truncate. */
.modal-head-id h2, .as-head-id h2, .as-tip .tt-title {
  font-size: clamp(calc(16px / var(--ui-zoom, 1)), 2rem, calc(22px / var(--ui-zoom, 1))); white-space: normal; overflow: visible;
  text-overflow: clip; overflow-wrap: anywhere; line-height: 1.3; max-height: none;
}
.as-ornament::before, .as-ornament::after { flex: 1 1 0; min-width: 0; }
.title-screen .as-titlemenu { margin-inline: auto; text-align: center; }
/* W3b: with a save, the menu and that save's preview share the body; the
   wordmark above stays centred. W3a (no save) keeps the lone centred menu. */
.title-screen .as-titlemenu:has(> .title-home) { max-width: 60rem; }
/* The wordmark sizes from its container (12cqi); widening the block for the
   preview must not grow it. min(12cqi, 4.8rem) is exactly its size in the
   40rem no-save block, whatever the host width. */
.title-screen .as-titlemenu:has(> .title-home) .tm-name { font-size: min(12cqi, 4.8rem); }
.title-screen .title-home { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 2vw; align-items: center; margin: 1.8rem 0; }
.title-screen .title-home > .title-menu { margin: 0; }
:root[data-layout='narrow'] .title-screen .title-home { grid-template-columns: minmax(0, 1fr); gap: 2vh; }
.title-screen .title-save-preview { display: grid; gap: .35rem; min-width: 0; padding: 1rem 1.2rem; text-align: left; border: 1px solid var(--line-soft); border-radius: var(--r-3); background: color-mix(in srgb, var(--panel) 82%, transparent); }
.title-screen .title-save-preview > p { margin: 0; }
.title-screen .title-save-name { font: 600 1.6rem/1.2 var(--font-display); color: var(--parchment); }
.title-screen .title-save-facts, .title-screen .title-save-identity { color: var(--muted); overflow-wrap: anywhere; }
.title-screen .title-menu .tm-entry.slot-continue.is-highlighted:not(:disabled) { color: var(--gold); font-weight: 700; }
.title-screen .as-ornament { width: 100%; justify-content: center; margin-inline: auto; }
.set-range { touch-action: pan-x; min-height: var(--tap-floor); }
.cz-stage .equipment-poker-explanations { font-size: max(1.4rem, calc(14px / var(--ui-zoom, 1))); line-height: 1.5; }
.cz-stage .equipment-poker-explanations :is(dt, dd) { white-space: normal; overflow-wrap: anywhere; }

/* Equipment disclosure preserves the shared above-card placement. */
.equipment-poker-card > .card-info-button { width:var(--inspect-box); height:var(--inspect-box); min-width:var(--inspect-box); min-height:var(--inspect-box); max-width:var(--inspect-box); max-height:var(--inspect-box); top:calc(-1 * (var(--inspect-box) + var(--inspect-gap, calc(10px / var(--ui-zoom, 1))))); }
@media (pointer:coarse) {
  .equipment-poker-card > .card-info-button { opacity:0; pointer-events:none; transition:opacity var(--card-info-fade,120ms) ease; }
  .equipment-poker-card.inspection-info-visible > .card-info-button, .equipment-poker-card:has(:focus-visible) > .card-info-button, .equipment-poker-card:focus-visible > .card-info-button { opacity:1; pointer-events:auto; }
}
@media (prefers-reduced-motion:reduce) { .equipment-poker-card > .card-info-button { transition:none; } }

.equipment-poker-card:focus-visible > .card-info-button, .equipment-poker-card:has(:focus-visible) > .card-info-button { visibility:visible; pointer-events:auto; opacity:1; }

/* Reserve room for the above-card control in Inventory scroll content. */
.armoury .disc-faces:has(.poker-inventory-face) { padding-top:64px; }

.armoury .disc-face:has(.poker-inventory-face) { overflow:visible; }

/* Selected content keeps Information after a host redraw, including radio choices. */
.card-inspection-target:is(.reward-selected, [aria-checked='true']) > .card-info-button {
  visibility: visible; pointer-events: auto;
}
.equipment-poker-card:is(.selected, [aria-selected='true'], [aria-checked='true']) > .card-info-button {
  visibility: visible; opacity: 1; pointer-events: auto;
}
/* Leave space inside scrollports for the above-card control, including wrapped rows. */
:is(.reward-row, .mount-card-list, .armoury-card-gallery, .pile-body .grid):has(.card-inspection-target) {
  padding-top: 64px; row-gap: 64px;
}
/* Reward-card status stack, top to bottom: Information, NEW, card.
   These three variables are the only placement knobs a theme or future card
   size needs. The empty Information lane is reserved so revealing it never
   pushes the row or the card under the player's pointer. */
.reward-door .reward-row {
  --reward-status-gap: 6px;
  --reward-new-badge-height: calc(1.08rem + 6px);
  --reward-status-stack-height: calc(var(--inspect-size, calc(2.75 * max(16px / var(--ui-zoom, 1), 1rem))) + var(--reward-new-badge-height) + var(--reward-status-gap) + var(--reward-status-gap));
}
.reward-door .reward-row:has(.card[data-new='1']) {
  padding-top: calc(var(--reward-status-stack-height) + var(--reward-status-gap));
  row-gap: calc(var(--reward-status-stack-height) + var(--reward-status-gap));
}
.reward-door .reward-row .card[data-new='1'] > .card-info-button {
  top: calc(-1 * var(--reward-status-stack-height));
}
.mount-row.card-inspection-target { position: relative; padding-right: 54px; }
.mount-row.card-inspection-target > .card-info-button {
  top: 50%; right: 8px; left: auto; transform: translateY(-50%);
}
/* Focusing a selected card must retain its inspection gutter in scrollports. */
.card-inspection-target { scroll-margin-top: 64px; }

/* Inspection shows the complete face; compact pickers retain their text budget. */
.card-inspection-art > .card { min-height:308px; }
.card-inspection-modal .card-inspection-art > .card { height:auto; aspect-ratio:auto; }
.card-inspection-art > .card .cd-body { flex:0 0 auto; height:auto; overflow:visible; }
.card-inspection-art > .card .ctext { display:block; flex:0 0 auto; overflow:visible; -webkit-line-clamp:unset; }
@media (max-width:600px) {
  .card-inspection-modal .card-inspection-layout { grid-template-columns:minmax(0,1fr); }
  .card-inspection-modal .card-inspection-art { width:100%; max-width:var(--card-w-inspect, 320px); }
}
/* Green means the primary action is ready; native/ARIA disabled states remain muted.
   A DANGER PRIMARY IS NOT GREEN. Green is "go" (Constantine, 2026-09-04); a
   loss that is the way forward — Overwrite, Delete — keeps the danger tint
   from `button.primary.danger` above. This rule's four :not()s outrank that
   two-class rule, so without the exclusion every ready danger door painted
   its red button green (measured on the title's Overwrite and Delete doors,
   2026-09-11: background rgb(37, 92, 57) on both). */
/* The positive (ready) role, one home; the End Turn exception below reuses it. */
:root { --control-positive-fill:#255c39; --control-positive-border:#6fb580; --control-positive-text:#f0fff3; }
button.primary:not(.danger):not(:disabled):not([aria-disabled='true']):not([data-confirm-ready='false']),
.as-btn.primary:not(.danger):not(:disabled):not([aria-disabled='true']):not([data-confirm-ready='false']) {
  background-color:var(--control-positive-fill); border-color:var(--control-positive-border); color:var(--control-positive-text);
}
button.primary:not(.danger):not(:disabled):not([aria-disabled='true']):not([data-confirm-ready='false']):hover,
.as-btn.primary:not(.danger):not(:disabled):not([aria-disabled='true']):not([data-confirm-ready='false']):hover {
  background-color:var(--control-positive-fill);
}
/* COLOR-INTERACTION-CONTRACT §3 (models/ControlAppearance.js): exits stay
   brown/gold until highlighted by pointer, keyboard or pad, then turn red. */
.as-btn[data-control-role='exit']:not(:disabled):not([aria-disabled='true']):is(:hover, :focus-visible, .gp-focus) {
  background-color:color-mix(in srgb, var(--danger) 14%, var(--panel)); border-color:var(--danger); color:var(--parchment);
}
.modal-close[data-control-role='exit']:is(:hover, :focus-visible, .gp-focus) > .modal-close-face { border-color:var(--danger); color:var(--danger); }
/* Registered exception combatEndTurn: a legal early end is neutral at rest and
   green while highlighted; spent actions already mark it ready above. */
.as-btn[data-control-exception='combatEndTurn']:not(:disabled):not([aria-disabled='true']):is(:hover, :focus-visible, .gp-focus) {
  background-color:var(--control-positive-fill); border-color:var(--control-positive-border); color:var(--control-positive-text);
}
button.primary:disabled, button.primary[aria-disabled='true'],
.as-btn.primary:disabled, .as-btn.primary[aria-disabled='true'] {
  background-color:var(--panel); border-color:var(--line); color:var(--muted);
}
/* Modal height is independent of its standard width. Keep chrome pinned and
   let the body scroll at its natural text/art size. */
/* The veil's vertical padding and the door's height ceiling are ONE pair of
   numbers: the ceiling used to subtract a literal 24px that matched neither
   the 8px minimum nor a notched phone's insets, so a content-tall door could
   paint under the home indicator. Insets are divided by the zoom for the
   reason combat.css's --safe-*-local are: an env() length inside the zoomed
   body renders at env x zoom, which under-covers the notch at every zoom
   below 1. */
.modal-veil:not(dialog), .as-veil {
  --veil-pad-top: max(8px, calc(env(safe-area-inset-top, 0px) / var(--ui-zoom, 1)));
  --veil-pad-bottom: max(8px, calc(env(safe-area-inset-bottom, 0px) / var(--ui-zoom, 1)));
  padding: var(--veil-pad-top) max(8px, calc(env(safe-area-inset-right, 0px) / var(--ui-zoom, 1))) var(--veil-pad-bottom) max(8px, calc(env(safe-area-inset-left, 0px) / var(--ui-zoom, 1)));
}
.modal, .as-modal { max-height: calc(100dvh / var(--ui-zoom, 1) - var(--veil-pad-top, 8px) - var(--veil-pad-bottom, 8px)); min-height: 0; }
.modal[data-height='long'], .as-modal[data-height='long'] { height: calc(100dvh / var(--ui-zoom, 1) - var(--veil-pad-top, 8px) - var(--veil-pad-bottom, 8px)); }
/* A short viewport (the decider's own `data-short`, written by main.js): the
   railed doors give up their fixed share and follow their body, and the
   veil's ceiling above still caps them. */
:root[data-short='true'] .modal[data-size='lg'], :root[data-short='true'] .modal[data-size='xl'],
:root[data-short='true'] .as-modal[data-size='lg'], :root[data-short='true'] .as-modal[data-size='xl'] { height: auto; }
.modal-body, .as-body, .as-pane { overscroll-behavior: contain; scrollbar-gutter: stable; }
.as-pane > * { flex-shrink: 0; }
.modal-head-id h2, .as-head-id h2, .as-head-id .as-title-s { white-space: normal; overflow-wrap: anywhere; }
/* Ready actions rise slightly without moving surrounding layout. */
button.primary, .as-btn.primary, button.primary.beat-hold, .as-btn.primary.beat-hold { transition:background-color 480ms ease, border-color 480ms ease, color 480ms ease, scale 480ms ease, translate 480ms ease, box-shadow 480ms ease; }
button.primary:not(:disabled):not([aria-disabled='true']):not([data-confirm-ready='false']),
.as-btn.primary:not(:disabled):not([aria-disabled='true']):not([data-confirm-ready='false']) {
  scale:1.015; translate:0 -2px; box-shadow:0 4px 10px #0005;
}
/* The lift's shadow used to replace a box-shadow ring here; the ring is an
   OUTLINE now (--focus-ring, base.css), so lift and ring never compete. */
.modal-foot:has(> .modal-foot-actions > .primary:not([hidden]):not(:disabled):not([aria-disabled='true']):not([data-confirm-ready='false'])) {
  flex-direction:column; align-items:stretch;
}
.modal-foot:has(> .modal-foot-actions > .primary:not([hidden]):not(:disabled):not([aria-disabled='true']):not([data-confirm-ready='false'])) > .modal-foot-note { display:none; }
/* THE WAY ON IS ON THE RIGHT; THE WAY BACK IS ON THE LEFT. Constantine,
   2026-09-11: "progression buttons should be on the right, and back buttons
   should be on the left". `modalFooter()` already builds the row in that
   order — every secondary, then the one primary — and `.modal-btnrow` lays it
   out as equal grid tracks, which is the owner's earlier rule from
   2026-09-03: "make primary and secondary buttons at the bottom uniform in
   size" (tools/modal-shell-contract.mjs asserts those equal tracks). So a
   ready primary needs NOTHING here but the full width of the foot to divide.

   IT USED TO TAKE MORE THAN THAT, AND THAT IS WHAT PUT IT ON THE LEFT. The
   block replaced the ladder's tracks with `grid-template-columns:repeat(2,
   minmax(0,1fr))` and gave the primary `grid-column:1 / -1; order:1`,
   intending a full-width row of its own. Grid does not place it that way: the
   ordered item takes the explicit two-track span, the auto-placed secondary
   is pushed into an IMPLICIT third track, and the line reads primary-then-back
   from the left. Measured at 1280x800 before this change — creation's foot
   computed `grid-template-columns: 383.47px 383.47px 383.47px`, Begin at x=10
   spanning 842px and Back at x=854; the title's slot picker did the same with
   Continue at x=424 and Back at x=717.

   A FLEX ROW IS NOT THE FIX EITHER, measured rather than assumed: the ladder's
   buttons carry `min-width:0` with an ellipsis, so a primary on `flex:1 1 auto`
   shrank Back to a 32px box with the word gone — on both surfaces, and
   `flex:0 0 auto` on the secondaries did not bring it back. The tracks stay;
   only the overrides go. The emphasis this block was written for is the
   primary's colour, lift and focus ring above, none of which moved. */
.modal-foot-actions:has(> .primary:not([hidden]):not(:disabled):not([aria-disabled='true']):not([data-confirm-ready='false'])) {
  width:100%; margin-left:0;
}
.modal-foot-actions > .primary:not([hidden]):not(:disabled):not([aria-disabled='true']):not([data-confirm-ready='false']) {
  max-width:none; min-width:0;
}
/* A lone exit (pile, flask, combatant door) lost `.primary` with its role;
   it still spans the foot it is the only way out of. */
.modal-foot-actions:has(> [data-control-role='exit']:only-child) { width:100%; margin-left:0; }
.modal-foot-actions > [data-control-role='exit']:only-child { max-width:none; min-width:0; }
/* THE CREATION FOOT KEEPS BACK LEFT AND BEGIN RIGHT once Begin is ready. The
   stacked ready-primary foot above is for a door whose primary is the one
   thing left to do; here Back is a real way out, and Constantine (2026-09-11):
   "continue and done etc confirmation buttons should be on the right, and
   back buttons on the left". Scoped to this foot; every other door keeps the
   stack. */
.modal-foot.cz-actions:has(> .modal-foot-actions > .primary:not([hidden]):not(:disabled):not([aria-disabled='true']):not([data-confirm-ready='false'])) {
  flex-direction:row; align-items:center;
}
.modal-foot.cz-actions:has(> .modal-foot-actions > .primary:not([hidden]):not(:disabled):not([aria-disabled='true']):not([data-confirm-ready='false'])) > .modal-foot-note { display:block; }
.modal-foot.cz-actions > .modal-foot-actions:has(> .primary:not([hidden]):not(:disabled):not([aria-disabled='true']):not([data-confirm-ready='false'])) {
  display:grid; grid-auto-flow:column; grid-auto-columns:minmax(0,1fr); grid-template-columns:none;
  width:min(100%, calc(var(--n) * var(--step) + (var(--n) - 1) * 8px)); margin-left:auto;
}
.modal-foot.cz-actions > .modal-foot-actions > .primary:not([hidden]):not(:disabled):not([aria-disabled='true']):not([data-confirm-ready='false']) {
  grid-column:auto; order:0; width:auto;
}
@media (prefers-reduced-motion:reduce) { button.primary, .as-btn.primary, button.primary.beat-hold, .as-btn.primary.beat-hold { transition:none; } }
body.reduced-motion button.primary, body.reduced-motion .as-btn.primary { transition:none; }
/* Pending confirmations keep their explanatory text even on narrow screens. */
.modal-foot:not(:has(> .modal-foot-actions > .primary:not([hidden]):not(:disabled):not([aria-disabled='true']):not([data-confirm-ready='false']))) { flex-wrap:wrap; }
.modal-foot:not(:has(> .modal-foot-actions > .primary:not([hidden]):not(:disabled):not([aria-disabled='true']):not([data-confirm-ready='false']))) > .modal-foot-note { display:block; flex:1 1 100%; }
.modal-foot > .modal-foot-note > span { visibility:visible; white-space:normal; overflow:visible; }
/* Redrawn ready controls fade in too; hold progress itself remains uneased. */
@starting-style {
  button.primary:not(:disabled):not([aria-disabled='true']):not([data-confirm-ready='false']),
  .as-btn.primary:not(:disabled):not([aria-disabled='true']):not([data-confirm-ready='false']) {
    background-color:color-mix(in srgb, var(--gold) 20%, var(--panel));
    border-color:var(--gold); scale:1; translate:0 0; box-shadow:none;
  }
}

/* Local entrance preview: one activation lights the city and crossfades
   into the hall before releasing the real title menu. */
.startup-gate { background: #100e0b; }
.startup-gate .tower-scene,
.tower-scene .tower-exterior,
.tower-scene .tower-city-unlit,
.tower-scene .tower-city-lit,
.tower-scene .tower-hall {
  position: absolute; inset: 0; pointer-events: none;
  background-position: center; background-size: cover;
}
.startup-gate .tower-scene { z-index: 0; overflow: hidden; }
.tower-city-unlit { background-image: url('../assets/bg/river-citadel-unlit.webp'); }
.tower-city-lit { background-image: url('../assets/bg/river-citadel-lit.webp'); opacity: 0; }
.tower-hall { opacity: 0; background: #080705; }
.tower-interior-city, .tower-door-frame {
  position: absolute; inset: 0; pointer-events: none;
  background-position: center; background-size: cover;
}
.tower-interior-city {
  background-image: url('../assets/bg/tower-city-background-unlit.webp');
  filter: brightness(.28) saturate(.82);
}
.tower-door-frame {
  background-image: linear-gradient(180deg, rgba(8,7,5,.28), rgba(8,7,5,.48)), url('../assets/bg/tower-entrance-hall.webp');
  -webkit-mask: url('../assets/bg/tower-door-mask.svg') center / cover no-repeat;
  mask: url('../assets/bg/tower-door-mask.svg') center / cover no-repeat;
}
.startup-gate::after {
  background: linear-gradient(180deg, rgba(8,7,5,.28), rgba(8,7,5,.48)),
    radial-gradient(ellipse at center, rgba(7,6,4,.25), rgba(7,6,4,.5));
}
.startup-gate .startup-ash-field { opacity: 0; }
.startup-gate.is-revealing { opacity: 1; }
.startup-gate.is-revealing > .as-titlemenu,
.startup-gate.is-revealing > .build-stamp { animation: towerLockupAway 380ms ease both; }
.startup-gate.is-revealing .tower-city-lit { animation: towerLightsWake var(--tower-light-ms) ease both; }
.startup-gate.is-revealing .tower-exterior { animation: towerExteriorFade var(--tower-fade-ms) ease var(--tower-fade-delay) both; }
.startup-gate.is-revealing .tower-hall { animation: towerHallFade var(--tower-fade-ms) ease var(--tower-fade-delay) both; }
@keyframes towerLockupAway { to { opacity: 0; } }
@keyframes towerLightsWake { from { opacity: 0; } to { opacity: 1; } }
@keyframes towerExteriorFade {
  0% { opacity: 1; }
  100% { opacity: 0; }
}
@keyframes towerHallFade {
  0% { opacity: 0; }
  100% { opacity: 1; }
}
.title-screen { background: #080705; }
.title-screen::before { background: none; }
.title-screen > .tower-hall { position: absolute; inset: 0; z-index: 0; opacity: 1; pointer-events: none; }
.title-screen .tower-door-frame { animation: towerDoorBreathe 5500ms ease-in-out infinite alternate; }
@keyframes towerDoorBreathe { from { opacity: 1; } to { opacity: .92; } }
.reduced-motion .title-screen .tower-door-frame,
:root[data-ambient='off'] .title-screen .tower-door-frame { animation: none; }
.title-screen .as-titlemenu { text-shadow: 0 2px 12px #090705; }
.startup-wordmark, .title-screen .tm-name { position: relative; isolation: isolate; }
.startup-wordmark::before, .title-screen .tm-name::before {
  content: ''; position: absolute; inset: -.14em -.25em; z-index: -1;
  pointer-events: none; border-radius: .18em;
  background: linear-gradient(90deg, transparent, rgba(7,6,4,.68) 15%, rgba(7,6,4,.68) 85%, transparent);
  -webkit-mask-image: linear-gradient(transparent, #000 22%, #000 78%, transparent);
  mask-image: linear-gradient(transparent, #000 22%, #000 78%, transparent);
  animation: titleBackdropIn 850ms ease both;
}
@keyframes titleBackdropIn { from { opacity: 0; } to { opacity: 1; } }
.reduced-motion .startup-wordmark::before,
.reduced-motion .title-screen .tm-name::before { animation: none; }
.tower-preview-replay {
  margin-top: .8rem; padding: .5rem 1rem; min-height: 3.2rem;
  color: var(--gold); border: 1px solid rgba(190,160,95,.3);
  background: rgba(8,7,5,.65); font-size: 1.1rem; letter-spacing: .08em;
  cursor: pointer; border-radius: 2px;
}
.tower-preview-replay:hover, .tower-preview-replay:focus-visible { border-color: var(--gold); color: var(--parchment); }
@media (orientation: portrait) {
  .tower-door-frame {
    background-image: linear-gradient(180deg, rgba(8,7,5,.28), rgba(8,7,5,.48)), url('../assets/bg/tower-entrance-hall-phone.webp');
    -webkit-mask-image: url('../assets/bg/tower-door-mask-phone.svg');
    mask-image: url('../assets/bg/tower-door-mask-phone.svg');
  }
}
.startup-gate.tower-calm .tower-exterior,
.startup-gate.tower-calm .tower-city-lit,
.startup-gate.tower-calm .tower-hall,
.startup-gate.tower-calm > .as-titlemenu,
.startup-gate.tower-calm > .build-stamp { animation: none; }
.startup-gate.tower-calm .tower-exterior { opacity: 0; }
.startup-gate.tower-calm .tower-hall { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .title-screen .tower-door-frame { animation: none; }
  .startup-gate > .as-titlemenu, .startup-gate .tm-foot { animation: none; }
  .startup-wordmark::before, .title-screen .tm-name::before { animation: none; }
}
@media (max-width: 360px) and (orientation: portrait) {
  .title-screen { padding-top: 8.2rem; padding-bottom: 1.2rem; }
}
/* Approved wireframe composition. Domain controls retain their handlers. */
/* THE W4 PARENT. W4a combat and W4c dialogue are one frame of four bands, top
   to bottom: the HUD, the scene, the context and the footer. Their sizes come
   from allocateSceneBands (models/CombatLayout.js) through each screen's
   adapter (components/combatLayout.js, components/dialogueStage.js) as
   --w4-band-*; the scene is the flexible row. A screen names only its
   first-paint fallbacks (the co-op screen mounts the same adapter since
   2026-10-01). Combat's hand area spans the context and footer
   bands and divides them itself. */
:root :is(.combat[data-layout='formation'], .w4-frame) {
  display: grid; flex: none; height: calc(100dvh / var(--ui-zoom, 1)); min-height: 0; overflow: clip;
  grid-template-rows: auto minmax(0, 1fr)
    var(--w4-band-context, var(--w4-context-fallback, 35%)) var(--w4-band-footer, var(--w4-footer-fallback, 15%));
}
:root .w4-frame > .topbar { grid-row: 1; min-height: var(--w4-band-hud, 0px); height: auto; box-sizing: border-box; overflow: visible; }
:root .combat[data-layout='formation'] {
  --w4-context-fallback: max(calc(208px / var(--ui-zoom, 1)), 30%);
  --w4-footer-fallback: max(calc(56px / var(--ui-zoom, 1)), 5%);
}
:root .combat[data-layout='formation'] > .hand-area {
  grid-row: 3 / span 2;
  grid-template-rows: minmax(0, 1fr) var(--w4-band-footer, max(calc(56px / var(--ui-zoom, 1)), 14.2857%));
}
:root .combat[data-layout='formation'] .hand[data-wireframe-hand='true'] {
  position: relative; display: block; padding: 0; overflow-x: auto; overflow-y: hidden;
  scrollbar-width: thin;
}
:root .combat[data-layout='formation'] .hand[data-wireframe-hand='true'] .card {
  position: absolute; left: var(--hand-card-x); top: var(--hand-card-y);
  flex: none; margin: 0; padding: 0; zoom: 1;
  width: var(--hand-card-width); min-width: var(--hand-card-width); max-width: var(--hand-card-width);
  height: var(--hand-card-height); min-height: var(--hand-card-height); max-height: var(--hand-card-height);
  aspect-ratio: var(--card-ratio); transform-origin: center center;
  transform: rotate(var(--hand-card-angle)) !important;
  z-index: auto !important;
}
:root .combat[data-layout='formation'] .hand[data-wireframe-hand='true'] .card:is(.selected,.inspection-selected) {
  transform: translateY(calc(-1 * var(--hand-selection-lift))) !important;
  z-index: 50 !important;
}
:root .combat[data-layout='formation'] .hand[data-wireframe-hand='true'] .card > .art {
  width: 100%; height: auto; margin: 0;
}
:root .combat[data-layout='formation'] .hand[data-wireframe-hand='true'] .card .cname {
  font-size: max(calc(12px / var(--ui-zoom, 1)), .9rem); min-height: 0;
}
:root .combat[data-layout='formation'] .hand[data-wireframe-hand='true'] .card .ctext {
  font-size: max(calc(12px / var(--ui-zoom, 1)), .8rem); line-height: 1.25;
}
:root .combat[data-layout='formation'] .hand .card > .card-cost-rail {
  top: calc(10% + .25rem); left: .35rem; right: auto; bottom: auto;
  display: flex; flex-direction: column; gap: .2rem; width: auto;
}
:root .combat[data-layout='formation'] .hand .card > .card-cost-rail > :is(.cost,.mana-cost,.stamina-cost) {
  position: static; border: 0; background: none; width: auto; height: auto; min-width: 0;
  padding: 0; font-size: max(calc(12px / var(--ui-zoom, 1)), .9rem); line-height: 1.2;
}
:root .combat[data-layout='formation'] .combat-action-row {
  display: grid; grid-template-columns: auto minmax(0, 1fr) minmax(0, 2fr) minmax(0, 1fr) auto;
  gap: .25rem; max-width: calc(640px / var(--ui-zoom, 1)); justify-self: center; padding: .15rem;
  --combat-control-size: calc(48px / var(--ui-zoom, 1)) !important;
}
:root .combat[data-layout='formation'] .combat-action-row > * {
  min-width: 0; width: 100%; max-width: none; margin: 0; align-self: center;
}
:root .combat[data-layout='formation'] .combat-action-row :is(.energy-orb,.combat-potions,.end-turn) {
  height: var(--combat-control-size); min-height: var(--combat-control-size); max-height: var(--combat-control-size);
}
:root .combat[data-layout='formation'] .combat-action-row .energy-orb,
:root .combat[data-layout='formation'] .combat-action-row .combat-potions {
  width: var(--combat-control-size); min-width: var(--combat-control-size); aspect-ratio: 1;
}
:root .combat[data-layout='formation'] .combat-action-row .pile {
  height: calc(44px / var(--ui-zoom, 1)); min-height: calc(44px / var(--ui-zoom, 1));
}
/* WGC6: one centered, tightly packed five-track group sized by
   packCombatFooter(). Applies once the layout adapter has measured the row.
   Solo and co-op both (`:is(.coop,:not(.coop))` keeps the specificity the old
   solo-only `:not(.coop)` had); the co-op board mounts the same footer and
   the same adapter (owner, 2026-10-01). */
:root .combat[data-layout='formation']:is(.coop,:not(.coop)) .combat-action-row[data-footer-geometry='supported']{
  --combat-control-size: var(--footer-circle) !important;
  grid-template-columns: var(--footer-circle) var(--footer-pile-width) var(--footer-end-width) var(--footer-pile-width) var(--footer-circle);
  justify-content: center; align-items: center; gap: var(--footer-gap);
  width: 100%; max-width: none; height: 100%; padding: 0; box-sizing: border-box;
}
:root .combat[data-layout='formation']:is(.coop,:not(.coop)) .combat-action-row[data-footer-geometry='supported']> * {
  grid-column: span 1; grid-row: 1;
}
:root .combat[data-layout='formation']:is(.coop,:not(.coop)) .combat-action-row[data-footer-geometry='supported']> .pile {
  width: var(--footer-pile-width); height: var(--footer-pile-height); min-height: var(--footer-pile-height);
}
/* W4a short landscape: allocateCombatBands() chose rails. The footer row is
   gone; the same WGC6 grid overlays the hand band and wraps the hand:
   (Actions)[Draw] | hand | [End Turn] over [Discard](Potions). */
:root .combat[data-layout='formation'][data-combat-arrangement='rails']:is(.coop,:not(.coop)) > .hand-area {
  grid-template-columns: var(--wireframe-rail-width) minmax(0, 1fr) var(--wireframe-rail-width);
  grid-template-rows: minmax(0, 1fr); grid-template-areas: 'lead hand trail';
  column-gap: var(--wireframe-rail-gap);
}
:root .combat[data-layout='formation'][data-combat-arrangement='rails']:is(.coop,:not(.coop)) .combat-action-row[data-footer-geometry='supported'] {
  grid-area: 1 / 1 / 2 / 4; z-index: 2; pointer-events: none;
  grid-template-columns: var(--footer-circle) var(--footer-pile-width) minmax(0, 1fr) var(--footer-pile-width) var(--footer-circle);
  grid-template-rows: minmax(0, 1fr) var(--footer-circle) var(--footer-circle);
  row-gap: var(--footer-gap); align-content: stretch;
}
:root .combat[data-layout='formation'][data-combat-arrangement='rails']:is(.coop,:not(.coop)) .combat-action-row[data-footer-geometry='supported'] > * {
  grid-row: 3; pointer-events: auto;
}
:root .combat[data-layout='formation'][data-combat-arrangement='rails']:is(.coop,:not(.coop)) .combat-action-row[data-footer-geometry='supported'] > .energy-orb { grid-column: 1; }
:root .combat[data-layout='formation'][data-combat-arrangement='rails']:is(.coop,:not(.coop)) .combat-action-row[data-footer-geometry='supported'] > .pile.draw { grid-column: 2; }
:root .combat[data-layout='formation'][data-combat-arrangement='rails']:is(.coop,:not(.coop)) .combat-action-row[data-footer-geometry='supported'] > .end-turn { grid-column: 4 / 6; grid-row: 2; }
:root .combat[data-layout='formation'][data-combat-arrangement='rails']:is(.coop,:not(.coop)) .combat-action-row[data-footer-geometry='supported'] > .pile.spent { grid-column: 4; }
:root .combat[data-layout='formation'][data-combat-arrangement='rails']:is(.coop,:not(.coop)) .combat-action-row[data-footer-geometry='supported'] > .combat-potions { grid-column: 5; }
/* Notched hosts held sideways: the device's safe-area insets (combat.css's
   --safe-*-local, zero on every other host) keep the rails and the hand they
   wrap, the formation, and the HUD's two ends off the notch. The layout
   adapter plans the rails for the width left between the insets. */
:root .combat[data-layout='formation']:is(.coop,:not(.coop)) > .hand-area {
  padding-inline: var(--safe-left-local, 0px) var(--safe-right-local, 0px);
}
:root .combat[data-layout='formation']:not(.coop) > .field {
  margin-inline: var(--safe-left-local, 0px) var(--safe-right-local, 0px);
}
:root .combat[data-layout='formation']:not(.coop) > .topbar {
  padding-inline: max(1.6rem, var(--safe-left-local, 0px)) max(1.6rem, var(--safe-right-local, 0px));
}
@media (max-height: 500px) {
  :root .combat[data-layout='formation']:not(.coop) > .topbar {
    padding-inline: max(calc(8px / var(--ui-zoom, 1)), var(--safe-left-local, 0px)) max(calc(8px / var(--ui-zoom, 1)), var(--safe-right-local, 0px));
  }
}
:root .combat[data-layout='formation'] .combatant .meters .bar .label {
  font-size: calc(12px / var(--ui-zoom, 1));
}
:root .combat[data-layout='formation'] .combatant .meters .bar.hp {
  height: max(.85rem, calc(14px / var(--ui-zoom, 1)));
}
/* WCM0: row heights from combatantMeterGeometry (the combat layout adapter).
   HP carries the compact current/maximum at the value-text size; a secondary
   or buildup row is too short for readable numbers, so its exact values live
   in its tooltip and the inspector. Stance is a strip the size of HP. */
:root .combat[data-layout='formation']:not(.coop) .combatant .meters { gap: var(--combatant-meter-gap, calc(3px / var(--ui-zoom, 1))); }
:root .combat[data-layout='formation']:not(.coop) .combatant .meters .bar { height: var(--combatant-secondary-h, calc(10px / var(--ui-zoom, 1))); }
:root .combat[data-layout='formation']:not(.coop) .combatant .meters .as-meter[data-res='hp'] .bar { height: var(--combatant-hp-h, max(.85rem, calc(14px / var(--ui-zoom, 1)))); }
:root .combat[data-layout='formation']:not(.coop) .combatant .meters .as-meter[data-res='hp'] .m-value { font-size: var(--combatant-value-text, calc(12px / var(--ui-zoom, 1))); line-height: 1; }
:root .combat[data-layout='formation']:not(.coop) .combatant .meters [data-meter-row='resource'] .m-plate { display: none; }
:root .combat[data-layout='formation']:not(.coop) .combatant .stance-chip[data-meter-row='stance'] {
  display: flex; align-items: center; justify-content: center; gap: .25rem; box-sizing: border-box;
  width: 100%; height: var(--combatant-stance-h, max(.85rem, calc(14px / var(--ui-zoom, 1)))); min-height: 0;
  padding: 0 .3rem; border-radius: 3px; font-size: var(--combatant-value-text, calc(12px / var(--ui-zoom, 1))); line-height: 1;
}
/* Rows the configured list keeps for the selected combatant only. */
:root .combat[data-layout='formation']:not(.coop) .combatant:not(.context-selected) [data-selected-only='true'] { display: none; }
/* THE ART TRACK IS THE CARD'S SIZE, NOT A SHARE OF THE DOOR. A `34fr` split
   made the inspected card a percentage of whatever modal it landed in — 292 px
   on a desktop and 118 px on a phone, where the phone's is SMALLER than the
   glance card you tapped to open it. The track is the authored inspect width;
   the details take what is left. */
.card-inspection-modal .card-inspection-layout {
  display: grid; grid-template-columns: minmax(0, var(--card-w-inspect, 320px)) minmax(0, 1fr);
  min-height: 0; height: 100%; gap: 2%; padding: 1rem;
}
.card-inspection-modal .card-inspection-art { min-width: 0; max-width: var(--card-w-inspect, 320px); width: 100%; }
.card-inspection-modal .card-inspection-art > .card[data-wireframe='WC1'] {
  width: 100%; height: auto; min-height: 0; aspect-ratio: var(--card-ratio);
}
.card-inspection-modal .modal-body { overflow: hidden; min-height: 0; }
.card-inspection-modal .card-inspection-details { min-height: 0; overflow-y: auto; overflow-x: hidden; }
/* THE READING DOOR STACKS WHEN TWO COLUMNS NO LONGER FIT.
   Two separate faults put this here, and both are worth naming because they
   are the same shape as each other:
   1. The two-column rule pinned the art track to the authored inspect width
      and sat AFTER the `max-width: 600px` collapses further up this file —
      same specificity, later in source — so it won at every width. On a 390px
      phone the card took 320 and the details took a sliver.
   2. The first fix for that repeated the mistake one level down: its mobile
      overflow rules sat BEFORE `.modal-body { overflow: hidden }` and
      `.card-inspection-details { overflow-y: auto }` just above, so the
      details stayed an inner scrollport and the body stayed clipped. The
      promised single scroll was not what shipped. Copilot caught it on #1162.
   Hence its position here: after everything it has to beat.

   THE THRESHOLD IS NOT A NUMBER IN THIS FILE. A media query cannot read a
   custom property, so an earlier draft wrote `calc(20rem + 24rem)` and called
   it derived — it was a second copy of the inspect width that would not have
   moved when card.json did. `cardInspectionLayout()` in
   src/ui/components/cardInspection.js measures against the authored width plus
   the authored readable measure and sets `data-card-door`, so the one home
   stays the config. It sits with the door rather than in `src/main.js`, which
   four of the five pages that host this layout never load. */
:root[data-card-door='stacked'] .card-inspection-modal .card-inspection-layout {
  grid-template-columns: minmax(0, 1fr);
  grid-auto-rows: max-content;
  align-content: start;
  height: auto;
  gap: 1rem;
}
:root[data-card-door='stacked'] .card-inspection-modal .card-inspection-art {
  max-width: 100%; margin-inline: auto;
}
:root[data-card-door='stacked'] .card-inspection-modal .card-inspection-art > .equipment-poker-card,
:root[data-card-door='stacked'] .card-inspection-modal .card-inspection-art > .card {
  width: 100%; max-width: var(--card-w-inspect, 320px); margin-inline: auto;
}
/* The column scrolls as ONE: the details stop owning a scrollport of their own
   and the body takes it, which is what "read down the page" means. */
:root[data-card-door='stacked'] .card-inspection-modal .card-inspection-details {
  overflow-y: visible; min-height: 0;
}
:root[data-card-door='stacked'] .card-inspection-modal .modal-body {
  overflow-y: auto; overflow-x: hidden;
}
:root .combat[data-layout='formation'] .combatant .combatant-info { visibility: hidden; pointer-events: none; }
:root .combat[data-layout='formation'] .combatant[data-inspect-ready='true'] .combatant-info { visibility: visible; pointer-events: auto; }
/* Solo combat reads the configured selected-only list (WCM0, data-selected-only);
   co-op keeps its name-only rule. */
:root .combat.coop[data-layout='formation'] .combatant:not(.context-selected) .nm { display: none; }
/* WCO2: the guard badge sits outside the sprite on its role's side, at its
   role's height (overlay.defenseAnchorByRole via the combat layout adapter);
   the fallbacks are the same accepted geometry. Never mirrored. */
:root .combat[data-layout='formation'] .combatant .sprite > .block-badge {
  right: var(--defense-right-player, auto); left: var(--defense-left-player, calc(100% + .5rem));
  top: var(--defense-top-player, 12%); bottom: auto; transform: translateY(-50%);
}
:root .combat[data-layout='formation'] .combatant.enemy .sprite > .block-badge {
  left: var(--defense-left-enemy, auto); right: var(--defense-right-enemy, calc(100% + .5rem)); top: var(--defense-top-enemy, 88%);
}
/* The badge sits inside the zoomed sprite host (battlefieldStage). It undoes
   that zoom and anchors to the visible art's box, published by the stage:
   --s is 1 for the right edge and 0 for the left; --f the height fraction. */
:root .combat[data-layout='formation']:not(.coop) .combatant .sprite > .block-badge {
  --s: var(--defense-right-player, 1); --f: var(--defense-fraction-player, .12);
  /* Out of the host's flex row: a relative badge was offset from its place
     after the art, which is why it floated far from the sprite. */
  position: absolute;
  zoom: calc(1 / var(--sprite-zoom, 1));
  left: calc(var(--art-left, 0px) + (var(--art-right, 100%) - var(--art-left, 0px)) * var(--s) + var(--defense-gap, .5rem) * (2 * var(--s) - 1));
  right: auto; bottom: auto;
  top: calc(var(--art-top, 0px) + var(--art-height, 100%) * var(--f));
  transform: translate(calc((var(--s) - 1) * 100%), -50%);
  display: flex; align-items: center; justify-content: center; box-sizing: border-box;
  min-width: var(--defense-min, 3.5rem); min-height: var(--defense-min, 3.5rem);
  font-size: max(1.2rem, var(--overlay-value-font, calc(13.2px / var(--ui-zoom, 1)))); line-height: 1;
}
:root .combat[data-layout='formation']:not(.coop) .combatant.enemy .sprite > .block-badge {
  --s: var(--defense-right-enemy, 0); --f: var(--defense-fraction-enemy, .88);
}
/* WCO1: the intent keeps its minimum height and value font. */
:root .combat[data-layout='formation']:not(.coop) .combatant-leading .intent {
  min-height: max(calc(48px / var(--ui-zoom, 1)), var(--intent-min, 2.8rem));
}
:root .combat[data-layout='formation'] .combatant .sprite::before {
  content: ''; position: absolute; left: 50%; bottom: 0;
  width: 70%; height: 8%; border-radius: 50%; transform: translate(-50%,50%);
  background: #0008; pointer-events: none; z-index: -1;
}
.modal-foot-actions[data-action-count] {
  display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0,1fr);
  width: 100%; min-width: 0; gap: var(--button-gap);
}
.modal-foot-actions[data-action-count] > button { width: 100%; min-width: 0; max-width: none; }
.modal-foot-actions[data-action-count='0'] { display: none; }
/* The compact receipt has one small way forward at its lower-right edge. It
   keeps the shared footer assembly, but does not stretch that single action
   across the entire modal as a full reward decision does. */
.reward-door[data-victory-compact='true'] > .reward-foot { flex-direction: row; align-items: center; }
.reward-door[data-victory-compact='true'] > .reward-foot > .modal-foot-actions[data-action-count] {
  display: flex; width: 100%; margin-left: 0; justify-content: flex-end;
}
.reward-door[data-victory-compact='true'] > .reward-foot > .modal-foot-actions > .reward-compact-continue {
  flex: 0 0 10rem; width: 10rem; min-width: 0; max-width: none;
}
/* WCB0 BUTTON SIZES. Every value comes from wireframeUi.buttons through
   ButtonSizeModel.buttonSizeTokens(), written onto :root by the kit; this
   block only combines them. A reference rem is at least 16 physical px (the
   hand and footer plans' unit), so heights neither shrink with the zoom nor
   with the 10px root. Widths belong to the group that owns the action region:
   the footer above divides it into equal shares after the shared gap (a sole
   action fills it); a choice row gives every sibling one preset of the row,
   capped by the equal share, and wraps labels rather than sizing to them. */
:root {
  --button-reference-rem: max(calc(16px / var(--ui-zoom, 1)), 1rem);
  --button-gap: calc(var(--button-gap-rem, 0) * var(--button-reference-rem));
  --button-minimum: calc(var(--button-minimum-rem, 0) * var(--button-reference-rem));
}
button[data-button-size$='-standard'] { --button-height-multiplier: var(--button-height-standard); }
button[data-button-size$='-tall'] { --button-height-multiplier: var(--button-height-tall); }
button[data-button-size$='-double'] { --button-height-multiplier: var(--button-height-double); }
button[data-button-size] {
  min-height: max(var(--tap-floor), calc(var(--button-standard-rem, 0) * var(--button-height-multiplier, 1) * var(--button-reference-rem)));
}
.as-choicerow {
  display: flex; flex-wrap: nowrap; justify-content: center; align-items: stretch;
  gap: var(--button-gap); width: 100%; min-width: 0; box-sizing: border-box;
}
.as-choicerow > button[data-button-size^='third-'] { --button-preset: var(--button-third); }
.as-choicerow > button[data-button-size^='half-'] { --button-preset: var(--button-half); }
.as-choicerow > button[data-button-size^='full-'] { --button-preset: var(--button-full); }
.as-choicerow > button {
  flex: 0 0 auto; box-sizing: border-box; min-width: 0; max-width: 100%;
  width: min(var(--button-preset), calc((100% - (var(--button-count) - 1) * var(--button-gap)) / var(--button-count)));
  white-space: normal; overflow-wrap: anywhere; text-overflow: clip; line-height: 1.2;
}
.as-choicerow[data-button-count='1'] > button { min-width: min(var(--button-minimum), 100%); }
.as-pagedoor .as-decide > .as-choicerow { margin-top: 1.5rem; }
.modal-close { aspect-ratio: 1; flex: none; padding: 0; }
.hand-overlay { position: relative; }
.hand-overlay > .hand-info-portal {
  position: absolute; z-index: 500; transform: translateX(-50%);
  --inspect-box: var(--inspect-size, calc(2.75 * max(16px / var(--ui-zoom, 1), 1rem)));
  width: var(--inspect-box); height: var(--inspect-box);
  min-width: var(--inspect-box); min-height: var(--inspect-box);
  border: 1px solid var(--gold); border-radius: 50%; padding: 0;
  display: grid; place-items: center; background: var(--bg); color: var(--gold);
  visibility: visible; pointer-events: auto; font-size: var(--inspect-label, calc(16px / var(--ui-zoom, 1)));
}
:root .combat[data-layout='formation'] .hand[data-wireframe-hand='true'] .card,
:root .combat[data-layout='formation'] .hand[data-wireframe-hand='true'] .card > * { pointer-events: none; }
:root .combat[data-layout='formation'] .hand[data-wireframe-hand='true'] .card > .hand-hit-lane {
  pointer-events: auto; position: absolute; left: 0; top: 0;
  width: var(--hand-hit-width); height: var(--hand-card-height); z-index: 20;
  transform-origin: calc(var(--hand-card-width) / 2) calc(var(--hand-card-height) / 2);
  transform: rotate(calc(-1 * var(--hand-card-angle))) translateY(calc(-1 * var(--hand-card-arc)));
}
:root .combat[data-layout='formation'] .hand[data-wireframe-hand='true'] .card:is(.selected,.inspection-selected) > .hand-hit-lane {
  transform: translateY(calc(var(--hand-selection-lift) - var(--hand-card-arc)));
}

/* ═══ W1 WORKSPACE — W1f Compendium, W1g Profile ═══════════════════════════ */
/* The frame is the wireframe's share of the visible viewport; the shares are
   wireframeUi.workspace, written as custom properties by
   components/w1Workspace.js. Head and foot size to their controls, the body
   takes the rest, and only the pane's two slots scroll: never the page. */
:root .modal.w1-workspace[data-size] {
  width: min(calc(100vw / var(--ui-zoom, 1) * var(--w1-frame-w)), 100%);
  height: min(calc(var(--visible-height, 100dvh) / var(--ui-zoom, 1) * var(--w1-frame-h)), 100%);
  max-width: 100%;
}
.screen.compendium { padding: 0; overflow: hidden; }
.w1-workspace > .modal-body { display: flex; min-height: 0; padding: 0; overflow: hidden; }
.w1-workspace > .modal-body > .as-railed {
  flex: 1 1 auto; width: 100%; height: auto; min-height: 0;
  grid-template-columns: clamp(var(--w1-rail-min), calc(100vw / var(--ui-zoom, 1) * var(--w1-rail)), var(--w1-rail-max)) minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
}
.w1-workspace .as-railitem { min-height: var(--tap-floor); }
.w1-workspace .as-railed > .as-pane { min-height: 0; overflow: hidden; }
.w1-workspace .as-pane > .w1-split { flex: 1 1 auto; min-height: 0; }
.w1-split {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); grid-template-rows: minmax(0, 1fr);
  column-gap: calc(100vw / var(--ui-zoom, 1) * var(--w1-col-gap));
}
.w1-split > * { min-width: 0; min-height: 0; overflow: auto; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: var(--line-soft) transparent; }
/* W1f: the entry list scrolls its grid; the detail is the selected entry. */
.w1-split > .cp-list { display: flex; flex-direction: column; overflow: hidden; }
.cp-list > .cp-grid { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 2px; }
.cp-detail { display: flex; flex-direction: column; gap: 0.9rem; }
.cp-detail > .as-artwell { align-self: center; flex: 0 0 auto; }
.cp-detail:not([data-state='held']) > .as-artwell img { filter: brightness(0) invert(1); opacity: 0.72; }
.cp-detail:not([data-state='held']) > .as-detailcard { border-style: dashed; }
.cp-detail-card > .cp-mod { margin-top: 0.5rem; }
/* W1g: identity beside the records. */
.prof-identity, .prof-records { display: flex; flex-direction: column; gap: 0.9rem; }
.prof-identity > .as-title-m { margin: 0; }
.prof-records > .as-status:empty { display: none; }
/* Compact: the category navigation is the kit's [Category ▾] selector (see
   W1 CATEGORY NAVIGATION below); the pane's two slots stack. */
:root[data-layout='narrow'] .w1-split {
  grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
  column-gap: 0; row-gap: calc(var(--visible-height, 100dvh) / var(--ui-zoom, 1) * var(--w1-row-gap));
}

/* ═══ W1 CATEGORY NAVIGATION — kit/categoryNav.js ═══════════════════════════ */
/* ONE component for every categorized W1 surface (Settings, Shop, Armoury,
   Compendium, Profile, the pile viewer). models/CategoryNavModel.js decides
   rail or selector from the host's own box and the budget in
   wireframeUi.categoryNav, and the kit writes the answer as data-cat-nav on
   the .as-railed host; this sheet measures nothing.
   rail      the NavRail beside the pane (the .as-railed defaults above).
   selector  rule 11's compact form: one [Category ▾] control above the pane
             that opens the SAME rail as a list under it (data-cat-open).
             Never a horizontal strip, never an accordion. */
.as-catnav-toggle { display: none; }
.as-railed[data-cat-nav] { position: relative; }
:root .as-railed.as-railed[data-cat-nav='selector'] { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }
:root .as-railed[data-cat-nav='selector'] > .as-catnav-toggle {
  display: flex; align-items: center; justify-content: space-between; gap: 0.6rem;
  grid-column: 1; grid-row: 1; width: auto; min-width: 0; min-height: var(--tap-floor);
  margin: 0.6rem var(--modal-inset);
  color: var(--gold); border-color: var(--gold); background: rgba(var(--accent-rgb), 0.08);
}
.as-catnav-face { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.as-catnav-caret { flex: 0 0 auto; transition: transform 0.15s ease; }
.as-railed[data-cat-open='true'] > .as-catnav-toggle > .as-catnav-caret { transform: rotate(180deg); }
:root .as-railed[data-cat-nav='selector'] > :not(.as-rail):not(.as-catnav-toggle) { grid-column: 1; grid-row: 2; }
:root .as-railed[data-cat-nav='selector'] > .as-rail { display: none; }
/* The open list drops over the pane's area, as tall as that area and no
   taller, and scrolls inside itself; the page never scrolls. */
:root .as-railed[data-cat-nav='selector'][data-cat-open='true'] > .as-rail {
  display: flex; flex-direction: column; flex-wrap: nowrap; position: absolute; z-index: 5;
  grid-column: 1; grid-row: 2; top: 0; bottom: auto; left: var(--modal-inset); right: var(--modal-inset);
  max-height: 100%; overflow-x: hidden; overflow-y: auto; padding: 0.6rem; gap: 0.45rem;
  background: var(--bg-raise); border: 1px solid var(--line); border-radius: var(--r-2); box-shadow: 0 12px 32px rgba(0, 0, 0, 0.6);
}
:root .as-railed[data-cat-nav='selector'][data-cat-open='true'] > .as-rail > * { flex: 0 0 auto; width: 100%; min-height: var(--tap-floor); }
/* While the list is open it is the only thing to choose: the pane under it
   leaves the cursor's reach (input.js skips hidden controls) until it closes. */
:root .as-railed[data-cat-nav='selector'][data-cat-open='true'] > :not(.as-rail):not(.as-catnav-toggle) { visibility: hidden; }
@media (prefers-reduced-motion: reduce) { .as-catnav-caret { transition: none; } }

/* ═══ DIALOGUE — W4c, the quest dialogue screen (WGQ0–WGQ8) ════════════════ */
/* A child of the W4 parent above (owner, 2026-09-15). Layers, bottom to top
   (DialogueModel.DIALOGUE_LAYER_Z, written as --dialogue-z-*): the skybox
   (WGS6) and the floor (WGS7), one environment plate fitted to the scene
   window between the HUD and the context and running on behind every band;
   the two portraits (WGQ2/WGQ3), the speaker over the listener; the opaque
   context band (WGQ4); the HUD and footer bands. Each portrait is the whole
   figure zoomed so its top third meets the context band's top edge (the
   reveal line); that band, the footer and the frame edge cut the rest, with
   no mask. components/dialogueStage.js writes the bands, the frame units
   (--w4-vw/--w4-vh, local px), the reveal and floor lines and each figure's
   placement; lengths come from DialogueModel (wireframeUi.dialogue). The
   fallbacks here are first-paint values only. */
:root .w4-frame.dialogue-screen { position: relative; width: 100%; background: #17130f; isolation: isolate; }
/* The plate, the HUD and the footer are there at t=0: no screen-in animation.
   Only the portraits and the context enter (DialogueModel.dialogueEntrance). */
#app > .dialogue-screen { animation: none; }
.dialogue-screen > .topbar { position: relative; z-index: var(--dialogue-z-hud, 6); background: rgba(13, 11, 8, 0.92); }
/* THE BAND ALWAYS DRAWS, AND HOLDS ITS OWN ROWS (owner, 2026-09-15). The
   shared HUD stacks an identity row above a resource row, which is two rows in
   a band that is 10% of the host: 103px of rows in 94px of band at 390x844,
   and the same again on a short landscape screen. Folded, the two rows KEEP
   THEIR OWN LINE and are drawn smaller (below), and below the narrow gate the
   band is four things and fits without either (§ BAND ON A PHONE). They sat
   side by side on one line until 2026-09-18, which is what drew CLASS over
   ACT. The relic and potion belt follows vitality in the same grid.
   Its height contributes to the band, including when relics expand. */
/* dev's own answer to the same overlap (25f513d, "owner, a real phone"):
   both rows KEEP THEIR OWN LINE here and are drawn smaller. It stands — it
   is what a SHORT WIDE host needs, where the facts are still drawn. § BAND
   ON A PHONE takes over below the narrow gate, where they are not. */
.dialogue-screen[data-hud-compact='true'] > .topbar {
  /* Both rows KEEP THEIR OWN LINE and are drawn smaller, rather than being
     folded side by side: at 411px wide two rows in one line put "CLASS Reaver"
     on top of "CINDERS 0" and the act name through "FLOOR 0" (owner, a real
     phone, 2026-09-18). A row that cannot be read is worse than a row that is
     small, and the band still has to hold both. */
  padding-block: var(--dialogue-hud-pad, 0.25rem);
  font-size: var(--dialogue-hud-scale, 0.85em);
}
.dialogue-screen[data-hud-compact='true'] > .topbar > .hud-top {
  gap: var(--dialogue-hud-row-gap, 0.15rem);
}
.dialogue-screen[data-hud-compact='true'] > .topbar > .hud-top > :is(.hud-info-row, .hud-resource-row) {
  min-width: 0;
  margin: 0;
  column-gap: var(--dialogue-hud-col-gap, 0.6rem);
}
/* The fact row's own gap is the fold's (§ BAND FOLD), so the facts yield the
   same way here as they do in the map's header. */
.dialogue-screen[data-hud-compact='true'] > .topbar .hud-info-row { gap: var(--band-fold-gap); }
/* The identity truncates rather than wrapping, the way the map's compact
   header already does: a HUD that changes height moves every control below it. */
.dialogue-screen[data-hud-compact='true'] > .topbar .hud-identity > .as-eyebrow,
.dialogue-screen[data-hud-compact='true'] > .topbar .hud-identity > .as-context {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dialogue-plate-layer { position: absolute; inset: 0; pointer-events: none; }
.dialogue-plate-layer > .environment-backdrop { top: 0; height: 100%; }
.dialogue-skybox { z-index: var(--dialogue-z-skybox, 2); clip-path: inset(0 0 calc(100% - var(--dialogue-floor-line, 18%)) 0); }
.dialogue-floor { z-index: var(--dialogue-z-floor, 3); clip-path: inset(var(--dialogue-floor-line, 18%) 0 0 0); }
.dialogue-scene { grid-row: 2; min-height: 0; pointer-events: none; }
.dialogue-portraits { position: absolute; inset: 0; z-index: var(--dialogue-z-portraits, 4); pointer-events: none; isolation: isolate; }
.dialogue-portrait { position: absolute; inset: 0; margin: 0; z-index: var(--dialogue-own-z, 0); transition: opacity 160ms ease, filter 160ms ease; }
.dialogue-portrait[data-layer='playerPortrait'] { --dialogue-own-z: var(--dialogue-z-player-portrait, 4); }
.dialogue-portrait[data-layer='npcPortrait'] { --dialogue-own-z: var(--dialogue-z-npc-portrait, 4); }
/* Inside the portrait layer the speaker is drawn over the listener
   (sceneLayouts W4c portraits.speakerAbove); the listener dims below. */
.dialogue-portrait[data-speaking='true'] { z-index: calc(var(--dialogue-own-z, 0) + var(--dialogue-speaker-lift, 1)); }
/* The listener is dimmed to the floor its config sets (positioning.portraits
   .listener), never past it: both speakers stay readable at every size. */
.dialogue-portrait[data-speaking='false'] {
  opacity: var(--dialogue-listener-opacity, 0.62);
  filter: saturate(var(--dialogue-listener-saturation, 0.55)) brightness(var(--dialogue-listener-brightness, 0.8));
}
.dialogue-portrait-art { position: absolute; inset: 0; }
/* WCF3: the speaker wears the shared selection glow. It sits on the untransformed
   art layer so the zoom does not thicken it. */
.dialogue-portrait[data-speaking='true'] > .dialogue-portrait-art {
  filter: var(--selection-glow, drop-shadow(0 0 calc(0.35 * max(16px / var(--ui-zoom, 1), 1rem)) var(--gold)));
}
/* The shared sprite functions stamp combat's fixed sizes inline; the stage
   adapter places the figure by transform from the layer's origin. */
.dialogue-portrait-art > .dialogue-figure { position: absolute !important; left: 0; top: 0; margin: 0; transform-origin: 0 0; flex: none; }
.dialogue-portrait[data-figure-fit='pending'] > .dialogue-portrait-art { visibility: hidden; }
/* Painted player figures face right as authored (combat does the same). */
.dialogue-portrait .class-sprite > .facing { transform: none; }
/* The speaker's art faces the player unless the layout says otherwise (mirrorNpc). */
.dialogue-screen[data-mirror-npc='false'] .dialogue-portrait[data-layer='npcPortrait'] .facing { transform: none !important; }
.dialogue-portrait-slot {
  position: absolute; box-sizing: border-box; display: flex; flex-direction: column; justify-content: flex-end;
  top: calc(var(--dialogue-scene-top, var(--w4-band-hud, 10%)) + var(--dialogue-slot-top, 2%));
  height: calc(var(--dialogue-reveal-line, 50%) - var(--dialogue-scene-top, var(--w4-band-hud, 10%)) - var(--dialogue-slot-top, 2%));
  width: var(--dialogue-portrait-w, 20%);
}
.dialogue-portrait[data-side='left'] > .dialogue-portrait-slot { left: var(--dialogue-inset-x, 2.5%); }
.dialogue-portrait[data-side='right'] > .dialogue-portrait-slot { right: var(--dialogue-inset-x, 2.5%); }
.dialogue-screen[data-dialogue-compact='true'] .dialogue-portrait-slot { width: var(--dialogue-portrait-w-compact, 30%); }
.dialogue-portrait-name {
  margin: 0; padding: 0.2em 0.5em; text-align: center; color: #f1e6cf; text-shadow: 0 1px 2px #000;
  font-size: max(1.3rem, calc(14px / var(--ui-zoom, 1))); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  background: linear-gradient(to top, rgba(13, 11, 8, 0.82), rgba(13, 11, 8, 0));
}
/* The name plate: a speaker with no art still stands in its slot, on the
   reveal line; the plate carries the name. */
.dialogue-portrait[data-portrait='plate'] .dialogue-portrait-name { display: none; }
.dialogue-plate { display: grid; place-items: center; box-sizing: border-box; width: 100%; min-height: calc(3 * var(--dialogue-ref-rem, 1.6rem)); padding: 0.8rem; border: 1px solid var(--line-soft); border-radius: var(--r-1); background: var(--panel); color: var(--gold); font: 600 max(1.6rem, calc(16px / var(--ui-zoom, 1)))/1.25 var(--font-display); text-align: center; }
/* L5: the context band is opaque; its top edge is the reveal line. */
.dialogue-context {
  grid-row: 3; position: relative; z-index: var(--dialogue-z-context, 5); min-height: 0; box-sizing: border-box;
  display: flex; justify-content: center; padding: var(--dialogue-context-inset-y, 2%) var(--dialogue-context-inset-x, 2.5%);
  background: #15120e; border-top: 1px solid var(--line);
}
/* The context band's panel: the quest title on one line, the narrative beat,
   then (last beat only) the responses as one grid. behavior.maxVisibleResponses
   of them show without scrolling: dialogueStage uses the first of the scene
   config's responseLayouts that holds them (a column or two below the text,
   or a grid beside it on short hosts); only responses past that scroll this
   panel. Every size is the scene config's, in reference rems; the fallbacks
   here are first-paint values only. */
.dialogue-region {
  flex: 0 1 var(--dialogue-context-w, 95%); max-width: 100%; min-width: 0; min-height: 0; overflow-y: auto; overscroll-behavior: contain; box-sizing: border-box;
  display: grid; grid-template-columns: minmax(0, 1fr); grid-template-areas: 'title' 'text' 'responses';
  align-content: start; gap: var(--dialogue-context-gap, 0.4rem);
  padding: var(--dialogue-context-pad, 0.6rem);
  background: var(--panel); border: 1px solid var(--line-soft); border-radius: var(--r-1);
}
.dialogue-region[data-response-placement='beside'] {
  grid-template-columns: minmax(0, calc(var(--dialogue-beside-text, 0.5) * 100%)) minmax(0, 1fr);
  grid-template-areas: 'title responses' 'text responses';
  grid-template-rows: auto 1fr;
}
.dialogue-title {
  grid-area: title; margin: 0; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  font: 600 var(--dialogue-title-size, 1.5rem) / var(--dialogue-title-line, 1.2) var(--font-display); color: var(--gold);
}
.dialogue-caption-text { grid-area: text; margin: 0; font-size: var(--dialogue-text-size, 1.4rem); line-height: var(--dialogue-caption-line, 1.35); }
/* Room for the caption's lines while speaking, so beats never move the
   controls; on the answering beat that room belongs to the responses. */
.dialogue-region:not([data-phase='respond']) .dialogue-caption-text { min-height: var(--dialogue-caption-min, 4rem); }
.dialogue-region[data-side='player'] .dialogue-caption-text { font-style: italic; }
.dialogue-responses {
  grid-area: responses; min-width: 0; display: grid; align-content: start;
  grid-template-columns: repeat(var(--dialogue-response-columns, 1), minmax(0, 1fr));
  gap: var(--dialogue-response-gap, 0.4rem);
}
/* THE ANSWERS TAKE THE BAND'S SPARE HEIGHT rather than leaving a strip of it
   empty under the last one (owner, a phone, 2026-09-18) — but only when the
   band can hold them grown, which the stage measures before asking for it.
   They stop at sizing.responses.maxHeightRem, so four answers never become
   slabs on a desktop. */
.dialogue-region[data-response-stretch='true'] > .dialogue-responses {
  align-content: stretch;
  grid-auto-rows: var(--dialogue-response-row-h, minmax(var(--dialogue-response-min-h, 2.75rem), var(--dialogue-response-max-h, 5.5rem)));
}
.dialogue-responses:empty { display: none; }
.dialogue-region[data-response-placement='beside'] > .dialogue-responses { align-self: center; }
.dialogue-responses > .dialogue-response {
  width: 100%; height: auto; min-height: var(--dialogue-response-min-h, 44px); margin: 0; box-sizing: border-box;
  display: flex; flex-direction: column; align-items: flex-start; justify-content: center;
  padding: var(--dialogue-response-pad-block, 0.3rem) var(--dialogue-response-pad-inline, 0.6rem);
  font-size: var(--dialogue-response-size, 1.3rem); line-height: var(--dialogue-response-line, 1.2);
  white-space: normal; text-align: left;
}
.dialogue-response-label {
  display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden; max-width: 100%;
  -webkit-line-clamp: var(--dialogue-response-lines, 2); line-clamp: var(--dialogue-response-lines, 2);
}
.dialogue-response-note { display: block; font-size: 0.85em; color: var(--muted); }
/* L6: the footer band (WGQ5), the three-action variant: equal thirds after the
   shared gaps (WCB0 footer shares), 6vh tall, never under the touch target. */
:root .w4-frame > .dialogue-foot {
  grid-row: 4; position: relative; z-index: var(--dialogue-z-footer, 6); min-height: 0; box-sizing: border-box;
  /* One full-width track: the kit buttons are inline-size containers, so an
     auto track would size to nothing and collapse the equal shares. */
  display: grid; grid-template-columns: minmax(0, 1fr); align-content: center; gap: 0;
  padding: 0 var(--dialogue-foot-inset-x, 2.5%);
  background: #15120e; border-top: 1px solid var(--line);
}
.dialogue-foot > .modal-foot-actions { width: 100%; margin-left: 0; gap: var(--dialogue-foot-gap, 1.5%); }
.dialogue-foot > .modal-foot-actions > button {
  height: var(--dialogue-action-h, calc(44px / var(--ui-zoom, 1)));
  min-height: var(--dialogue-action-h, calc(44px / var(--ui-zoom, 1)));
}
/* A layer switched off (sceneLayouts layering.layers[].enabled) draws nothing:
   the bands' own display rules must not outrank `hidden`. */
:root .w4-frame > :is(.dialogue-context, .dialogue-foot, .dialogue-plate-layer, .dialogue-portraits, .topbar)[hidden],
:root .w4-frame .dialogue-portrait[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) { .dialogue-portrait { transition: none; } }

/* W1c — CHARACTER CREATION IS A W1 WORKSPACE (FRONTEND-WIREFRAMES "W1c").
   The head carries the title, the small portrait and the exit; the body is
   the kit's railed layout — the categories in a rail beside the pane, or as
   one [Category ▾] selector above it on a compact host (categoryNav) — and
   only the active category is in the pane. The pane is the one scroll
   region; the page and the body never scroll. The portrait's size and the
   attribute grid's column count are the model's (uiConfig.screens.creation),
   written as custom properties by the screen; nothing here is a number. */
.customize:not(.component-catalog) .modal-body.cz-scroll { display: flex; flex-direction: column; overflow: hidden; min-height: 0; padding: 0; }
.customize .cz-railed { flex: 1 1 auto; min-height: 0; height: 100%; width: 100%; }
.customize .cz-pane { overflow: auto; min-height: 0; height: 100%; }
.customize .cz-pane > .cz-stage { height: auto; }
.customize .modal-head .figure.cz-portrait {
  flex: 0 0 auto; width: var(--creation-portrait) !important; height: var(--creation-portrait) !important;
  min-height: 0 !important; min-width: 0 !important; max-width: none; margin-inline-start: var(--creation-portrait-gap); border-radius: 50%; overflow: hidden; align-self: center;
}
.customize .modal-head .figure.cz-portrait > * { width: 100%; height: 100%; object-fit: cover; }
.customize .cz-tab { display: flex; flex-direction: column; align-items: flex-start; gap: var(--creation-rail-gap); white-space: normal; }
.customize .cz-tab-value { font-size: calc(1em * var(--creation-rail-value-scale)); color: var(--muted); }
.customize .cc-primary-stats { display: grid; grid-template-columns: repeat(var(--creation-attribute-columns), minmax(0, 1fr)); gap: var(--creation-attribute-gap); }
.customize .cc-review { padding: 0 var(--creation-review-pad) var(--creation-review-pad); }

/* W1c, the choices in view: no head over a choice collection, only the small
   view switch at the right; descriptions clip to the config's lines; a pane
   too short for the class list folds them to one line, then drops the
   preview column so the cards take the width (customize.js fitStage). */
.customize .cz-head-tools { display: flex; align-items: center; gap: var(--creation-head-tools-gap); margin-inline-start: auto; }
.customize .cz-head-tools > [hidden] { display: none; }
.customize .cc-view-switch { min-width: var(--tap-floor); min-height: var(--tap-floor); padding-inline: var(--creation-view-switch-pad); font-size: var(--creation-view-switch-glyph); line-height: 1; }
.customize .cz-class .od { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: var(--creation-choice-lines); overflow: hidden; }
.customize .cz-railed[data-choice-fit="compact"] .cz-class .od { -webkit-line-clamp: 1; }
.customize .cz-railed[data-choice-fit="compact"] .cz-class { padding-block: var(--creation-choice-compact-pad); }
.customize .cz-railed[data-choice-fit="compact"] .cc-choice-collection { gap: var(--creation-choice-compact-gap); }
.customize .cz-railed[data-choice-fit="bare"] .cz-class .od { display: none; }
.customize .cz-railed[data-choice-fit="bare"] .cz-class { padding-block: var(--creation-choice-bare-pad); }
.customize .cz-railed[data-choice-fit="bare"] .cc-choice-collection { gap: var(--creation-choice-bare-gap); }
.customize .cz-railed[data-preview-off="true"] .cc-class-split > .cc-class-preview-host,
.customize .cz-railed[data-preview-off="true"] .cc-class-split > .cc-class-divider { display: none; }
.customize .cz-railed[data-preview-off="true"] .cc-class-split { grid-template-columns: minmax(0, 1fr); }

/* Intrinsic run-HUD sizing wins over host percentage bands and short-landscape
   chrome. All content stays inside its background; menu never shares a row
   with the relic icons. */
:root .shared-hud > .hud-top { height: auto; }
:root .shared-hud .resbars { display: flex; flex-direction: column; align-items: stretch; }
:root[data-layout='narrow'] .shared-hud > .hud-top {
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "info info" "meters actions" "rail actions";
}
:root[data-layout='narrow'] .shared-hud .hud-control-grid { align-self: start; }
/* W1c, the chosen card unfolds (owner, 2026-09-19): with
   behavior.classPreview "unfold" the preview column is gone and the picked
   class card opens to --creation-unfold-height, the portrait taking
   --creation-unfold-portrait of its width and the summary the rest, nothing
   past the card's box. The card is a flex row; the unfold wraps under it. */
.customize .cz-railed[data-class-preview="unfold"] .cc-class-split > .cc-class-preview-host,
.customize .cz-railed[data-class-preview="unfold"] .cc-class-split > .cc-class-divider { display: none; }
.customize .cz-railed[data-class-preview="unfold"] .cc-class-split { grid-template-columns: minmax(0, 1fr); }
.customize .cz-class.unfolded { display: grid; grid-template-columns: auto minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); align-items: center; height: max(var(--creation-unfold-height), var(--creation-unfold-min)); overflow: hidden; }
.customize .cz-class.unfolded > .cc-class-unfold { grid-column: 1 / -1; min-height: 0; height: 100%; align-self: stretch; display: grid; grid-template-columns: var(--creation-unfold-portrait) minmax(0, var(--creation-unfold-summary)); gap: var(--creation-head-tools-gap); overflow: hidden; }
.customize .cc-unfold-portrait { display: flex; align-items: center; justify-content: center; height: 100%; min-height: 0; overflow: hidden; }
.customize .cc-unfold-portrait > .figure.cc-unfold-art { display: block; height: 100% !important; width: auto !important; aspect-ratio: 1 / 1; max-width: 100%; max-height: 100%; min-height: 0; min-width: 0; }
.customize .cc-unfold-portrait > .figure.cc-unfold-art > * { width: 100%; height: 100%; object-fit: contain; }
.customize .cc-unfold-summary { display: flex; flex-direction: column; justify-content: center; gap: var(--creation-head-tools-gap); min-width: 0; min-height: 0; overflow: hidden; text-align: start; font-weight: normal; }
.customize .cc-unfold-relic { display: inline-flex; align-items: center; gap: var(--creation-rail-gap); color: var(--ink-dim); font-size: calc(1em * var(--creation-rail-value-scale)); }
.customize .cz-class.unfolded::after { display: none; }

/* Creation is read top-to-bottom: compact stats, resources, then Continue.
   Only the workspace pane scrolls when enlarged text needs more room. */
.customize #cz-primary-stats { gap:4px; }
.customize .cc-name-row { display:grid; grid-template-columns:auto minmax(0,1fr); align-items:center; gap:1rem; padding-block:.5rem; }
.customize .cc-name-row > .r-trail { max-width:100%; width:100%; }
.customize .cc-name-row .cz-name { width:100%; max-width:100%; }
.customize #cz-primary-stats .cc-primary-stat.hosts-face > .as-row,
.cc-stat-modal .cc-primary-stat.hosts-face > .as-row { min-height:var(--tap-floor); padding:.35rem .8rem; gap:.6rem; }
.customize #cz-primary-stats .cc-primary-stat { min-height:var(--tap-floor); }
.customize #cz-primary-stats .cc-primary-stat > .as-row { font-size:max(1rem, calc(14px / var(--ui-zoom, 1))); }
.customize #cz-primary-stats :is(.ls-label, .as-status) { font-size:inherit; }
.customize #cz-primary-stats .ls-hint { font-size:max(.9em, calc(12px / var(--ui-zoom, 1))); line-height:1.25; }
.customize #cz-primary-group { gap:.6rem; }
.customize .cc-mode-select { width:100%; min-height:var(--tap-floor); padding:.4rem .8rem; font:inherit; font-size:max(1rem, calc(14px / var(--ui-zoom, 1))); color:var(--ink); background:var(--panel); border:1px solid var(--line); border-radius:var(--r-1); color-scheme:dark; }
.customize .cz-stage .modal-btnrow.cc-primary-continue-row { position:static; margin-top:.5rem; padding:0; background:none; }
.customize #cz-derived .cc-derived { display:flex; flex-wrap:wrap; gap:.35rem .75rem; }
.customize #cz-derived .as-chip { padding:.15rem 0; font-size:max(1rem, calc(14px / var(--ui-zoom, 1))); }
.customize #cz-derived .as-chip .cv { font-size:inherit; }
.customize #cz-derived .as-chip .ck { font-size:max(.8em, calc(11px / var(--ui-zoom, 1))); letter-spacing:.03em; }
.customize .cc-equipment-details:empty { display:none; }
.customize .cc-equip-group:has(> .cc-equipment-details:empty) { grid-template-columns:minmax(0,1fr); }
.customize .cc-equip-group > .cc-card-selectors { --cc-choice-head:14px; grid-row:auto; overflow:visible; max-height:none; }
.customize .cc-equip-group > .cc-card-selectors[data-view="grid"] { padding-top:var(--cc-choice-head); gap:24px 12px; }
.customize .cc-equip-group[data-equipment-section] { max-height:none; grid-template-rows:auto auto; gap:12px; }
.customize .cc-equip-group > .cc-equipment-details { max-height:none; overflow:visible; }
.customize .cc-equipment-continue { position:static; grid-column:1 / -1; grid-row:auto; }
.customize .cc-card-selectors .equipment-choose { font-size:max(14px, calc(14px / var(--ui-zoom, 1))); }
.customize .cc-card-selectors[data-view="list"] .equipment-choose { grid-column:1 / -1; }
.customize .cc-card-selectors .poker-equipment-choice:not(.choice-focused) .equipment-choose { visibility:hidden; pointer-events:none; }
.customize .card-info-button.creation-info-popover:popover-open {
  position:fixed; inset:auto; margin:0; transform:none; visibility:visible; opacity:1;
  pointer-events:auto; z-index:2147483647; overflow:visible;
  box-shadow:0 0 16px #d0ac5d80;
}

/* Compact equipment selection: one header, one active section, one way forward. */
.customize .cc-equipment-header { display:flex; align-items:center; gap:12px; padding:10px 6px; border-bottom:1px solid #74603c; }
.customize .cc-equipment-header b { color:var(--gold,#c9a227); }
.customize .cc-equipment-header select { flex:1; min-width:0; background:transparent; color:inherit; border:0; font:inherit; }
.customize #cz-equipment-count { white-space:nowrap; font-size:12px; }
.customize [data-creation-stage="equipment"] .as-catnav-toggle { display:none; }
.customize #cz-equipment-fold .disc-fold-row:not([open]) { display:none; }
.customize #cz-equipment-fold .disc-fold-row > summary { display:none; }
.customize #cz-equipment-fold .disc-fold-row { border:0; padding:0; }
.customize .cc-equip-group[data-equipment-section] { display:flex; flex-direction:column; gap:8px; padding:0; }
.customize .cc-equip-group > .cc-card-selectors[data-view="grid"] { display:grid; grid-template-columns:repeat(auto-fit,minmax(132px,180px)); justify-content:center; padding:58px 6px 4px; gap:16px 12px; }
.customize .cc-equip-group > .cc-card-selectors[data-view="list"] { padding-top:58px; }
.customize .cc-card-selectors .equipment-choose { display:none; }
.customize .cc-equipment-details { border:0; padding:4px 8px; }
.customize .cc-item-heading { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.customize .cc-item-heading h3 { margin:0 auto 0 0; font-size:1.25rem; }
.customize .cc-item-heading .inspection-facts { display:flex; gap:6px; margin:0; }
.customize .cc-compact-details { display:flex; flex-direction:column; gap:8px; }
.customize .cc-compact-details .inspection-effect,.customize .cc-compact-details .inspection-requirement { margin:0; font-size:14px; }
.customize .cc-compact-details .inspection-lore-line { margin:0; padding:6px 8px; }
.customize .cc-single-tags { display:flex; flex-wrap:nowrap; gap:5px; min-width:0; width:100%; }
.customize .cc-single-tags > .inspection-tag { flex:none; white-space:nowrap; font-size:12px; padding:5px 7px; }
.cc-single-tags > [hidden] { display:none!important; }
.cc-tags-popup { position:fixed; inset:auto; margin:0; width:min(290px,calc(100vw - 24px)); padding:12px; background:#302719; border:1px solid #b79a4e; border-radius:8px; color:#e4d5af; box-shadow:0 8px 28px #000b; }
.cc-tags-popup .inspection-tag { display:inline-block; margin:0 5px 5px 0; cursor:pointer; }
.cc-tag-explanation { margin:6px 0 0; font-size:14px; line-height:1.4; }
.customize .cc-equipment-continue { margin:2px 8px 8px; position:static; }
.customize .cc-equipment-continue:not([aria-disabled="true"]),.customize .cz-next-stage:not([aria-disabled="true"]) { background:#245a3a; border-color:#76aa78; color:white; }
.customize #cz-equipment-receipts,.customize .cc-move-note { display:none; }
.customize .cc-card-selectors .epc-facts { display:flex; gap:16px; justify-content:center; }
.customize .cc-card-selectors .epc-fact { display:flex; flex-direction:row; gap:5px; align-items:center; }
.customize .cc-card-selectors .epc-heading { display:none; }

.customize:has(#cz-equipment-panel) .as-catnav-toggle { display:none!important; }
.customize .cc-equip-group[data-equipment-section] { max-width:480px; width:100%; margin:0 auto; }
.customize .cc-equip-group > .cc-card-selectors[data-view="grid"] { grid-template-columns:repeat(2,minmax(0,1fr)); width:100%; }
.customize .cc-equipment-continue { align-self:stretch; width:auto; }
.customize .cc-equipment-details .cc-item-heading h3 { margin:0 auto 0 0; }
.customize .cc-compact-details .inspection-facts { flex-wrap:nowrap; }
.customize .cc-compact-details .inspection-facts .inspection-tag { padding:4px 6px; font-size:12px; white-space:nowrap; }

.customize .cc-compact-details { width:100%; max-width:none; }
.customize .cc-item-heading h3 { font-size:max(20px,calc(20px / var(--ui-zoom,1))); }
.customize .cc-single-tags > .inspection-tag { font-size:max(12px,calc(12px / var(--ui-zoom,1))); }
.customize .cc-item-heading .inspection-facts { font-size:max(12px,calc(12px / var(--ui-zoom,1))); }

.customize .cc-equip-group > .cc-card-selectors[data-view="grid"][data-many="true"] { grid-template-columns:none; grid-auto-flow:column; grid-auto-columns:44%; justify-content:start; overflow-x:auto; overflow-y:hidden; padding-bottom:14px; }

.customize .cc-equip-group > .cc-equipment-details { width:100%; max-width:100%; box-sizing:border-box; align-self:stretch; }
.customize .cc-single-tags { box-sizing:border-box; overflow:clip; }

.customize .cc-starting-package > h4 { display:none; }
.customize .cc-starting-package { margin-top:8px; }
.customize .cc-starting-summary { padding:8px; }

.customize .cc-equipment-header { font-size:max(14px,calc(14px / var(--ui-zoom,1))); }
.customize #cz-equipment-count { font-size:max(12px,calc(12px / var(--ui-zoom,1))); }
.customize .cc-equip-group[data-equipment-section] { max-width:max(440px,calc(440px / var(--ui-zoom,1))); }
@media (max-width:700px) {
  .customize:has(#cz-equipment-panel) .cz-railed { display:block; }
  .customize:has(#cz-equipment-panel) .as-rail { display:none; }
  .customize:has(#cz-equipment-panel) .cz-pane { width:100%; height:100%; }
}
.customize .cc-card-selectors .epc-footer { font-size:13px; line-height:1; }
.customize .cc-compact-details .inspection-facts .inspection-tag { font-size:max(12px,calc(12px / var(--ui-zoom,1))); }

/* Picker faces use the shared glance sizing, including the selected card. */
.customize .cc-card-selectors .equipment-poker-card { width:100%; max-width:var(--card-w-glance,152px); }
.customize .cc-card-selectors .choice-focused .equipment-poker-card { transform:translateY(-4px); }
.customize .cc-equip-group > .cc-card-selectors[data-view="grid"] { grid-template-columns:repeat(auto-fit,minmax(0,var(--card-w-glance,152px))); }
.customize .cc-equip-group > .cc-card-selectors[data-view="grid"][data-many="true"] { grid-template-columns:none; grid-auto-columns:var(--card-w-glance,152px); }
.customize .cc-card-selectors .epc-fact { white-space:nowrap; }

/* Equipment uses the available page height; actions own a reserved bottom row. */
.customize .cz-pane:has(> #cz-equipment-panel) { overflow:hidden; }
.customize .cz-pane > #cz-equipment-panel { height:100%; min-height:0; display:grid; grid-template-rows:auto minmax(0,1fr); padding-bottom:0; }
.customize #cz-equipment-fold { min-height:0; height:100%; }
.customize #cz-equipment-fold > div { height:100%; }
.customize #cz-equipment-fold > div:not(:has(> details[open])) { display:none; }
.customize #cz-equipment-fold .disc-fold-row[open] { height:100%; min-height:0; margin:0; }
.customize #cz-equipment-fold .disc-reveal { height:100%; min-height:0; padding:0; margin:0; box-sizing:border-box; }
.customize .cc-equip-group[data-equipment-section] { display:grid; grid-template-columns:minmax(0,1.25fr) minmax(0,1fr); grid-template-rows:minmax(0,1fr) auto; gap:12px 20px; width:100%; max-width:none; height:100%; min-height:0; padding:0 12px; box-sizing:border-box; }
.customize .cc-equip-group > .cc-card-selectors[data-view="grid"],
.customize .cc-equip-group > .cc-card-selectors[data-view="grid"][data-many="true"] { grid-column:1; grid-row:1 / -1; grid-auto-flow:row; grid-auto-columns:auto; grid-template-columns:repeat(auto-fit,minmax(0,var(--card-w-glance,152px))); align-content:start; justify-content:center; overflow:auto; min-height:0; padding:58px 6px 12px; scroll-padding-inline:6px; }
.customize .cc-equip-group > .cc-card-selectors[data-view="list"] { grid-column:1; grid-row:1 / -1; min-height:0; overflow:auto; }
.customize .cc-equip-group > .cc-equipment-details { grid-column:2; grid-row:1; margin:0; padding:20px 0 0; min-height:0; overflow:auto; align-self:stretch; }
.customize .cc-equipment-continue { grid-column:2; grid-row:2; align-self:end; margin:0 0 12px; width:100%; }
.customize .cc-item-heading { gap:6px; }
.customize .cc-compact-details { gap:8px; }
.customize .cc-starting-summary { display:flex; flex-wrap:wrap; gap:4px 8px; }
.customize .cc-starting-summary .cc-package-context { font-size:12px; margin:0; }
@media(max-width:700px) {
 .customize .cc-equip-group[data-equipment-section] { grid-template-columns:minmax(0,1fr); grid-template-rows:auto minmax(0,1fr) auto; gap:8px; padding:0 10px; }
 .customize .cc-equip-group > .cc-card-selectors[data-view="grid"],
 .customize .cc-equip-group > .cc-card-selectors[data-view="grid"][data-many="true"] { grid-column:1; grid-row:1; grid-auto-flow:column; grid-template-columns:none; grid-auto-columns:calc((100% - 24px) / 3); justify-content:start; gap:12px; padding:54px 0 10px; overflow-x:auto; overflow-y:hidden; scroll-snap-type:x mandatory; }
 .customize .cc-card-selectors[data-view="grid"] .poker-equipment-choice { padding:0; scroll-snap-align:start; }
 .customize .cc-card-selectors .equipment-poker-card { width:100%; max-width:var(--card-w-glance,152px); }
 .customize .cc-equip-group > .cc-card-selectors[data-view="list"] { grid-column:1; grid-row:1; max-height:32vh; }
 .customize .cc-equip-group > .cc-equipment-details { grid-column:1; grid-row:2; padding:2px 0; }
 .customize .cc-equipment-continue { grid-column:1; grid-row:3; margin:0 0 10px; }
}

.customize #cz-equipment-fold .disc-fold-row[open] { position:relative; }
.customize #cz-equipment-fold .disc-reveal { position:absolute; inset:0; height:auto; display:block; }
.customize .cc-equip-group > .cc-card-selectors[data-view="grid"] { row-gap:54px; }
@media(max-width:700px) {
 .customize .cc-equip-group > .cc-card-selectors[data-view="grid"],
 .customize .cc-equip-group > .cc-card-selectors[data-view="grid"][data-many="true"] { padding-top:70px; row-gap:12px; }
}

/* Creation header: stable menu and a single, plain close control. */
.customize .cz-menu-button { min-width: var(--tap-floor); min-height: var(--tap-floor); font: 24px/1 Arial, sans-serif; padding: 0; }
/* `--place-gap` is the ONE home for the hairline under the ☰ button: fx.js
   placeGap() reads this declaration, customize.js writes nothing, and
   tools/placement.mjs reads the same line the code used. */
.customize .cz-head-tools.cz-header-menu { --place-gap: 8px; display: none; position: fixed; inset: auto; margin: 0; padding: 12px; width: 220px; background: var(--panel); color: var(--text); border: 1px solid var(--line-soft); border-radius: var(--r-2); box-shadow: 0 8px 24px #0008; }
/* A window too short for four categories scrolls the menu instead of pinning
   Review off the bottom edge (customize.js writes the max-height it was bound to). */
.customize .cz-head-tools.cz-header-menu:popover-open { display: grid; gap: 12px; overflow: hidden auto; }
.customize .cz-menu-navigation { display: grid; gap: 6px; }
.customize .modal-head .modal-close { border: 1px solid var(--line-soft); }
.customize .modal-head .modal-close > .modal-close-face { width: 100%; height: 100%; border: 0; background: transparent; font: 28px/1 Arial, sans-serif; }

/* Keep all three creation header controls on the same square footprint. */
.customize .modal-head :is(.figure.cz-portrait, .cz-menu-button, .modal-close) {
  box-sizing: border-box;
  flex: 0 0 var(--tap-floor);
  width: var(--tap-floor) !important;
  height: var(--tap-floor) !important;
  min-width: var(--tap-floor) !important;
  min-height: var(--tap-floor) !important;
  max-width: var(--tap-floor);
  max-height: var(--tap-floor);
  padding: 0;
  align-self: center;
}
